docs: map existing codebase

This commit is contained in:
William Valentin
2026-02-09 19:31:05 -08:00
parent 85b1401440
commit d2d64f3361
7 changed files with 2042 additions and 0 deletions
+294
View File
@@ -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*