# 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` 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 => { 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 => { 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` - Tools return `Promise` with `{ success, output, error? }` - Streams use `AsyncIterable` - 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; ``` --- *Convention analysis: 2026-02-09*