9.7 KiB
Coding Conventions
Analysis Date: 2026-02-09
Naming Patterns
Files:
- Source files: camelCase with
.tsextension —clientFactory.ts,hybridSearch.ts,vectorStore.ts - React/Ink components: PascalCase with
.tsxextension (insrc/frontends/tui/components/) - Test files: co-located with source, suffixed
.test.ts—agent.test.tsbesideagent.ts - Type-only files:
types.tsin each module directory - Index barrel files:
index.tsin each module directory
Classes:
- PascalCase:
NativeAgent,ModelRouter,ToolRegistry,ChannelRegistry,SessionStore - Implementation classes use
implementswith interface:class AnthropicClient implements ModelClient
Interfaces/Types:
- PascalCase:
ChatRequest,ChatResponse,ToolResult,InboundMessage - Config interfaces suffixed with
Config:NativeAgentConfig,OrchestratorConfig,AnthropicClientConfig - Union types use
typekeyword:type ConversationMessage = Message | ToolMessage - Zod-inferred types:
type Config = z.infer<typeof configSchema>insrc/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
eslintdevDependency), no config file found — likely uses flat config defaults - Run with:
pnpm lint(runseslint src/)
TypeScript Strictness:
strict: trueintsconfig.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:
- Node.js stdlib:
import { readFileSync } from 'fs'; - Third-party packages:
import Anthropic from '@anthropic-ai/sdk'; - Local imports:
import { configSchema } from './schema.js';
Path Style:
- Always use
.jsextensions for local imports (NodeNext resolution):import { NativeAgent } from './agent.js'; import type { Config } from '../config/schema.js';
Type-only Imports:
- Use
import typefor type-only imports:import type { ModelClient, ChatResponse } from '../../models/types.js'; import type { Tool, ToolResult } from '../../tools/types.js';
Barrel Files:
- Every module has an
index.tsthat re-exports public API - Use explicit named exports, not
export *:// 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:export type { AnthropicToolDef, OpenAIToolDef } from './registry.js';
Tool Patterns
Flynn tools follow three distinct patterns. Use the appropriate one:
Static Tool (no dependencies):
// 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):
// 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):
// src/tools/builtin/index.ts
export function createMemoryTools(store: MemoryStore, hybridSearch?: HybridSearch): Tool[] {
return [
createMemoryReadTool(store),
createMemoryWriteTool(store),
createMemorySearchTool(store, hybridSearch),
];
}
Registration chain:
- Tool file (e.g.,
src/tools/builtin/shell.ts) src/tools/builtin/index.ts(barrel exports +allBuiltinToolsarray)src/tools/index.ts(barrel re-exports)- Registered in
src/daemon/index.tsviaToolRegistry.register()
Error Handling
Pattern 1: Return ToolResult with error field (tools):
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):
if (envValue === undefined) {
throw new Error(`Environment variable ${envVar} is not set`);
}
Pattern 3: Throw for duplicate registration (registries):
if (this.tools.has(tool.name)) {
throw new Error(`Tool '${tool.name}' is already registered`);
}
Pattern 4: Yield error events in streams:
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):
this.messageHandler(msg, reply).catch((err: unknown) => {
console.error(`Error handling message from '${msg.channel}':`, err);
});
Pattern 6: Promise.allSettled for non-critical failures:
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 loadedconsole.warn()for non-fatal issues: missing handler, unknown channelconsole.error()for failures: failed connections, adapter errors- No structured logging — messages are plain strings with context
Comments
JSDoc-style comments on interfaces:
/** 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:
// ── Public types ──────────────────────────────────────────────────────
// ── Helpers ─────────────────────────────────────────────────────────
Inline comments for non-obvious logic:
// 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
ChatResponsewith requiredcontent,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:
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:
export const configSchema = z.object({ ... });
export type Config = z.infer<typeof configSchema>;
Convention analysis: 2026-02-09