295 lines
9.7 KiB
Markdown
295 lines
9.7 KiB
Markdown
# 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*
|