docs: map existing codebase
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
# Coding Conventions
|
||||
|
||||
**Analysis Date:** 2026-02-09
|
||||
|
||||
## Naming Patterns
|
||||
|
||||
**Files:**
|
||||
- Source files: camelCase with `.ts` extension — `clientFactory.ts`, `hybridSearch.ts`, `vectorStore.ts`
|
||||
- React/Ink components: PascalCase with `.tsx` extension (in `src/frontends/tui/components/`)
|
||||
- Test files: co-located with source, suffixed `.test.ts` — `agent.test.ts` beside `agent.ts`
|
||||
- Type-only files: `types.ts` in each module directory
|
||||
- Index barrel files: `index.ts` in each module directory
|
||||
|
||||
**Classes:**
|
||||
- PascalCase: `NativeAgent`, `ModelRouter`, `ToolRegistry`, `ChannelRegistry`, `SessionStore`
|
||||
- Implementation classes use `implements` with interface: `class AnthropicClient implements ModelClient`
|
||||
|
||||
**Interfaces/Types:**
|
||||
- PascalCase: `ChatRequest`, `ChatResponse`, `ToolResult`, `InboundMessage`
|
||||
- Config interfaces suffixed with `Config`: `NativeAgentConfig`, `OrchestratorConfig`, `AnthropicClientConfig`
|
||||
- Union types use `type` keyword: `type ConversationMessage = Message | ToolMessage`
|
||||
- Zod-inferred types: `type Config = z.infer<typeof configSchema>` in `src/config/schema.ts`
|
||||
|
||||
**Functions:**
|
||||
- camelCase: `loadConfig()`, `expandEnvVars()`, `createClientFromConfig()`
|
||||
- Factory functions prefixed with `create`: `createMemoryReadTool()`, `createWebSearchTool()`, `createBrowserTools()`
|
||||
- Boolean checks prefixed with `is`/`has`/`should`: `isSupportedImage()`, `hasImages()`, `shouldCompact()`
|
||||
|
||||
**Variables/Fields:**
|
||||
- camelCase for public: `modelClient`, `systemPrompt`, `toolRegistry`
|
||||
- Underscore prefix for private fields: `_config`, `_dirtyNamespaces`, `_totalUsage`, `_callCount`
|
||||
- Constants: UPPER_SNAKE_CASE: `FETCH_TIMEOUT_MS`, `MAX_RESULTS`, `DEFAULT_RETRY_CONFIG`, `MODEL_COSTS_PER_MILLION`
|
||||
|
||||
**Tool Names:**
|
||||
- Dot-separated namespacing: `shell.exec`, `file.read`, `file.write`, `memory.read`, `web.fetch`
|
||||
- MCP tools use colon separator: `mcp:filesystem:read_file`
|
||||
- Tool groups prefixed with `group:`: `group:fs`, `group:runtime`, `group:web`, `group:memory`
|
||||
|
||||
## Code Style
|
||||
|
||||
**Formatting:**
|
||||
- No Prettier or formatter config detected — formatting is enforced by convention
|
||||
- 2-space indentation throughout
|
||||
- Single quotes for strings
|
||||
- Trailing commas in multiline structures
|
||||
- Semicolons always used
|
||||
|
||||
**Linting:**
|
||||
- ESLint v9+ configured (via `eslint` devDependency), no config file found — likely uses flat config defaults
|
||||
- Run with: `pnpm lint` (runs `eslint src/`)
|
||||
|
||||
**TypeScript Strictness:**
|
||||
- `strict: true` in `tsconfig.json`
|
||||
- Target: ES2022, Module: NodeNext, ModuleResolution: NodeNext
|
||||
- JSX: react-jsx (for Ink TUI components)
|
||||
- All declarations, declaration maps, and source maps enabled
|
||||
|
||||
## Import Organization
|
||||
|
||||
**Order:**
|
||||
1. Node.js stdlib: `import { readFileSync } from 'fs';`
|
||||
2. Third-party packages: `import Anthropic from '@anthropic-ai/sdk';`
|
||||
3. Local imports: `import { configSchema } from './schema.js';`
|
||||
|
||||
**Path Style:**
|
||||
- Always use `.js` extensions for local imports (NodeNext resolution):
|
||||
```typescript
|
||||
import { NativeAgent } from './agent.js';
|
||||
import type { Config } from '../config/schema.js';
|
||||
```
|
||||
|
||||
**Type-only Imports:**
|
||||
- Use `import type` for type-only imports:
|
||||
```typescript
|
||||
import type { ModelClient, ChatResponse } from '../../models/types.js';
|
||||
import type { Tool, ToolResult } from '../../tools/types.js';
|
||||
```
|
||||
|
||||
**Barrel Files:**
|
||||
- Every module has an `index.ts` that re-exports public API
|
||||
- Use explicit named exports, not `export *`:
|
||||
```typescript
|
||||
// src/tools/index.ts
|
||||
export type { Tool, ToolCall, ToolResult } from './types.js';
|
||||
export { ToolRegistry } from './registry.js';
|
||||
export { ToolExecutor } from './executor.js';
|
||||
```
|
||||
- Types re-exported with `export type`:
|
||||
```typescript
|
||||
export type { AnthropicToolDef, OpenAIToolDef } from './registry.js';
|
||||
```
|
||||
|
||||
## Tool Patterns
|
||||
|
||||
Flynn tools follow three distinct patterns. Use the appropriate one:
|
||||
|
||||
**Static Tool (no dependencies):**
|
||||
```typescript
|
||||
// src/tools/builtin/shell.ts
|
||||
import type { Tool, ToolResult } from '../types.js';
|
||||
|
||||
interface ShellExecArgs {
|
||||
command: string;
|
||||
cwd?: string;
|
||||
}
|
||||
|
||||
export const shellExecTool: Tool = {
|
||||
name: 'shell.exec',
|
||||
description: 'Execute a shell command...',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
command: { type: 'string', description: 'The shell command to execute' },
|
||||
},
|
||||
required: ['command'],
|
||||
},
|
||||
execute: async (rawArgs: unknown): Promise<ToolResult> => {
|
||||
const args = rawArgs as ShellExecArgs;
|
||||
// implementation
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**Factory Tool (single tool needing dependency injection):**
|
||||
```typescript
|
||||
// src/tools/builtin/memory-read.ts
|
||||
export function createMemoryReadTool(store: MemoryStore): Tool {
|
||||
return {
|
||||
name: 'memory.read',
|
||||
description: '...',
|
||||
inputSchema: { ... },
|
||||
execute: async (rawArgs: unknown): Promise<ToolResult> => {
|
||||
const args = rawArgs as MemoryReadArgs;
|
||||
// uses `store` from closure
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Multi-Factory (related tool set):**
|
||||
```typescript
|
||||
// src/tools/builtin/index.ts
|
||||
export function createMemoryTools(store: MemoryStore, hybridSearch?: HybridSearch): Tool[] {
|
||||
return [
|
||||
createMemoryReadTool(store),
|
||||
createMemoryWriteTool(store),
|
||||
createMemorySearchTool(store, hybridSearch),
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
**Registration chain:**
|
||||
1. Tool file (e.g., `src/tools/builtin/shell.ts`)
|
||||
2. `src/tools/builtin/index.ts` (barrel exports + `allBuiltinTools` array)
|
||||
3. `src/tools/index.ts` (barrel re-exports)
|
||||
4. Registered in `src/daemon/index.ts` via `ToolRegistry.register()`
|
||||
|
||||
## Error Handling
|
||||
|
||||
**Pattern 1: Return ToolResult with error field (tools):**
|
||||
```typescript
|
||||
try {
|
||||
const content = store.read(args.namespace);
|
||||
return { success: true, output: content };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 2: Throw with descriptive message (config/setup):**
|
||||
```typescript
|
||||
if (envValue === undefined) {
|
||||
throw new Error(`Environment variable ${envVar} is not set`);
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 3: Throw for duplicate registration (registries):**
|
||||
```typescript
|
||||
if (this.tools.has(tool.name)) {
|
||||
throw new Error(`Tool '${tool.name}' is already registered`);
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 4: Yield error events in streams:**
|
||||
```typescript
|
||||
try {
|
||||
for await (const event of stream) {
|
||||
yield { type: 'content', content: event.delta.text };
|
||||
}
|
||||
} catch (error) {
|
||||
yield {
|
||||
type: 'error',
|
||||
error: error instanceof Error ? error : new Error(String(error)),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 5: Fire-and-forget with error logging (channels):**
|
||||
```typescript
|
||||
this.messageHandler(msg, reply).catch((err: unknown) => {
|
||||
console.error(`Error handling message from '${msg.channel}':`, err);
|
||||
});
|
||||
```
|
||||
|
||||
**Pattern 6: Promise.allSettled for non-critical failures:**
|
||||
```typescript
|
||||
const results = await Promise.allSettled(adapters.map((a) => a.connect()));
|
||||
for (const [i, result] of results.entries()) {
|
||||
if (result.status === 'rejected') {
|
||||
console.error(`Failed to start channel '${adapters[i].name}':`, result.reason);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**instanceof check pattern:** Always use `error instanceof Error ? error.message : String(error)` for unknown errors.
|
||||
|
||||
## Logging
|
||||
|
||||
**Framework:** `console` (no logging library)
|
||||
|
||||
**Patterns:**
|
||||
- `console.log()` for informational messages: startup, config loaded
|
||||
- `console.warn()` for non-fatal issues: missing handler, unknown channel
|
||||
- `console.error()` for failures: failed connections, adapter errors
|
||||
- No structured logging — messages are plain strings with context
|
||||
|
||||
## Comments
|
||||
|
||||
**JSDoc-style comments on interfaces:**
|
||||
```typescript
|
||||
/** Media attachment received from or sent to a channel. */
|
||||
export interface Attachment {
|
||||
/** MIME type (e.g. "image/jpeg", "audio/ogg", "application/pdf"). */
|
||||
mimeType: string;
|
||||
/** Base64-encoded data (preferred for model APIs). */
|
||||
data?: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Section dividers in larger files:**
|
||||
```typescript
|
||||
// ── Public types ──────────────────────────────────────────────────────
|
||||
// ── Helpers ─────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
**Inline comments for non-obvious logic:**
|
||||
```typescript
|
||||
// Policy check (defense in depth — tools should also be filtered at listing time)
|
||||
// Fire and forget — errors are logged, not propagated
|
||||
```
|
||||
|
||||
**When to comment:**
|
||||
- Always add JSDoc on exported interfaces and their fields
|
||||
- Use section dividers in files > 100 lines
|
||||
- Add inline comments for non-obvious behavior, workarounds, or design decisions
|
||||
- No comments on self-explanatory code
|
||||
|
||||
## Function Design
|
||||
|
||||
**Size:** Functions generally stay under 50 lines. Complex logic is extracted into private methods (e.g., `toolLoop()`, `singleTurn()` in `NativeAgent`).
|
||||
|
||||
**Parameters:**
|
||||
- Config objects for constructors with >2 params: `NativeAgentConfig`, `OrchestratorConfig`
|
||||
- Raw args typed as `unknown`, then cast internally: `const args = rawArgs as ShellExecArgs`
|
||||
- Optional params use `?` with defaults via `??`: `config.maxTokens ?? 4096`
|
||||
|
||||
**Return Values:**
|
||||
- Async functions return `Promise<T>`
|
||||
- Tools return `Promise<ToolResult>` with `{ success, output, error? }`
|
||||
- Streams use `AsyncIterable<ChatStreamEvent>`
|
||||
- Model clients always return `ChatResponse` with required `content`, `stopReason`, `usage`
|
||||
|
||||
## Module Design
|
||||
|
||||
**Exports:** Named exports only — no default exports anywhere in the codebase.
|
||||
|
||||
**Barrel Files:** Every module directory has `index.ts`. Import from barrel in consuming code:
|
||||
```typescript
|
||||
import { ToolRegistry, ToolExecutor, ToolPolicy } from '../../tools/index.js';
|
||||
```
|
||||
|
||||
**Config validation:** Use Zod schemas in `src/config/schema.ts`. All config types are inferred from schemas:
|
||||
```typescript
|
||||
export const configSchema = z.object({ ... });
|
||||
export type Config = z.infer<typeof configSchema>;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Convention analysis: 2026-02-09*
|
||||
Reference in New Issue
Block a user