diff --git a/bun.lock b/bun.lock index f61606a..3d3c497 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,8 @@ "devDependencies": { "@biomejs/biome": "^2.3.10", "@types/bun": "latest", + "typedoc": "^0.27.0", + "typedoc-plugin-markdown": "^4.4.0", "typescript": "^5.7.0", "zod": "^4.2.1", }, @@ -37,16 +39,56 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.10", "", { "os": "win32", "cpu": "x64" }, "sha512-pHEFgq7dUEsKnqG9mx9bXihxGI49X+ar+UBrEIj3Wqj3UCZp1rNgV+OoyjFgcXsjCWpuEAF4VJdkZr3TrWdCbQ=="], + "@gerrit0/mini-shiki": ["@gerrit0/mini-shiki@1.27.2", "", { "dependencies": { "@shikijs/engine-oniguruma": "^1.27.2", "@shikijs/types": "^1.27.2", "@shikijs/vscode-textmate": "^10.0.1" } }, "sha512-GeWyHz8ao2gBiUW4OJnQDxXQnFgZQwwQk05t/CVVgNBN7/rK8XZ7xY6YhLVv9tH3VppWWmr9DCl3MwemB/i+Og=="], + + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1" } }, "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA=="], + + "@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="], + + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="], + + "lunr": ["lunr@2.3.9", "", {}, "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow=="], + + "markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="], + + "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="], + + "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], + + "typedoc": ["typedoc@0.27.9", "", { "dependencies": { "@gerrit0/mini-shiki": "^1.24.0", "lunr": "^2.3.9", "markdown-it": "^14.1.0", "minimatch": "^9.0.5", "yaml": "^2.6.1" }, "peerDependencies": { "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x" }, "bin": { "typedoc": "bin/typedoc" } }, "sha512-/z585740YHURLl9DN2jCWe6OW7zKYm6VoQ93H0sxZ1cwHQEQrUn5BJrEnkWhfzUdyO+BLGjnKUZ9iz9hKloFDw=="], + + "typedoc-plugin-markdown": ["typedoc-plugin-markdown@4.9.0", "", { "peerDependencies": { "typedoc": "0.28.x" } }, "sha512-9Uu4WR9L7ZBgAl60N/h+jqmPxxvnC9nQAlnnO/OujtG2ubjnKTVUFY1XDhcMY+pCqlX3N2HsQM2QTYZIU9tJuw=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + "zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], } } diff --git a/docs/api-reference/classes/droid.mdx b/docs/api-reference/classes/droid.mdx new file mode 100644 index 0000000..d567925 --- /dev/null +++ b/docs/api-reference/classes/droid.mdx @@ -0,0 +1,205 @@ +--- +title: Droid +description: Main entry point for the Droid SDK +--- + +# Droid + +The `Droid` class is the main entry point for the SDK. It provides methods for executing prompts and managing conversation threads. + +## Import + +```typescript +import { Droid, MODELS } from '@activade/droid-sdk'; +``` + +## Constructor + +```typescript +new Droid(config?: DroidConfig) +``` + +Creates a new Droid instance with the specified configuration. + +### Parameters + + + Configuration options for the Droid instance. + + + + Working directory for CLI operations. + + + AI model to use for generation. + + + Level of autonomous decision-making: `'default'`, `'low'`, `'medium'`, or `'high'`. + + + Reasoning intensity: `'off'`, `'none'`, `'low'`, `'medium'`, or `'high'`. + + + Path to the Droid CLI binary. + + + Maximum execution time in milliseconds. + + + + +### Example + +```typescript +const droid = new Droid({ + model: MODELS.CLAUDE_SONNET, + autonomyLevel: 'high', + reasoningEffort: 'medium', + cwd: '/path/to/project', + timeout: 300000 +}); +``` + +## Properties + +### config + +```typescript +get config(): Readonly +``` + +Returns the current configuration as a readonly object. + +```typescript +const droid = new Droid({ model: MODELS.CLAUDE_SONNET }); +console.log(droid.config.model); // 'claude-sonnet-4-5-20250929' +``` + +## Methods + +### startThread() + +```typescript +startThread(options?: ThreadOptions): Thread +``` + +Creates a new conversation thread for multi-turn interactions. + + + Thread-specific configuration that overrides instance defaults. + + +**Returns:** A new `Thread` instance. + +```typescript +const thread = droid.startThread({ + autonomyLevel: 'high', + enabledTools: ['file_read', 'file_write'] +}); + +await thread.run('Create a REST API'); +await thread.run('Add authentication'); + +console.log('Session ID:', thread.id); +``` + +### resumeThread() + +```typescript +resumeThread(sessionId: string, options?: ThreadOptions): Thread +``` + +Resumes a previously created conversation thread. + + + The unique session identifier from a previous thread. + + + Thread-specific configuration overrides. + + +**Returns:** A `Thread` instance connected to the existing session. + +```typescript +const thread = droid.resumeThread('session_abc123...'); +await thread.run('What did we work on last time?'); +``` + +### exec() + +```typescript +exec(prompt: string, options?: ExecOptions): Promise +``` + +Executes a single prompt without maintaining conversation state. + + + The natural language prompt to execute. + + + Execution options including model, output schema, etc. + + +**Returns:** A promise resolving to the execution result. + +**Throws:** +- `ExecutionError` - If the CLI execution fails +- `TimeoutError` - If the operation exceeds the configured timeout +- `CliNotFoundError` - If the Droid CLI is not installed + +```typescript +const result = await droid.exec('Generate a UUID'); +console.log(result.finalResponse); +``` + +### listTools() + +```typescript +listTools(model?: string): Promise +``` + +Lists all available tools for the configured or specified model. + + + Model ID to query tools for (defaults to instance model). + + +**Returns:** A promise resolving to an array of tool names. + +```typescript +const tools = await droid.listTools(); +console.log('Available tools:', tools); +// ['file_read', 'file_write', 'shell_exec', ...] +``` + +## Full Example + +```typescript +import { Droid, MODELS } from '@activade/droid-sdk'; +import { z } from 'zod'; + +const droid = new Droid({ + model: MODELS.CLAUDE_SONNET, + autonomyLevel: 'high', + cwd: './my-project' +}); + +// One-shot execution +const result = await droid.exec('Create a hello world function'); +console.log(result.finalResponse); + +// Multi-turn conversation +const thread = droid.startThread(); +await thread.run('Create a React component'); +await thread.run('Add unit tests for it'); + +// Structured output +const schema = z.object({ + name: z.string(), + version: z.string() +}); + +const pkgResult = await droid.exec('Get package info as JSON'); +const pkg = pkgResult.parse(schema); +console.log(pkg.name, pkg.version); +``` diff --git a/docs/api-reference/classes/thread.mdx b/docs/api-reference/classes/thread.mdx new file mode 100644 index 0000000..cfcd096 --- /dev/null +++ b/docs/api-reference/classes/thread.mdx @@ -0,0 +1,226 @@ +--- +title: Thread +description: Multi-turn conversation management +--- + +# Thread + +The `Thread` class manages multi-turn conversations with the Droid AI. It maintains context across prompts and supports both synchronous and streaming execution. + +## Import + +```typescript +import { Droid } from '@activade/droid-sdk'; + +// Threads are created via Droid methods +const droid = new Droid(); +const thread = droid.startThread(); +``` + +## Properties + +### id + +```typescript +get id(): string | undefined +``` + +The unique session identifier for this thread. Returns `undefined` until the first prompt is executed. + +```typescript +const thread = droid.startThread(); +console.log(thread.id); // undefined + +await thread.run('Hello'); +console.log(thread.id); // 'session_abc123...' +``` + +### cwd + +```typescript +get cwd(): string +``` + +The working directory for this thread's CLI operations. + +```typescript +const thread = droid.startThread({ cwd: '/projects/my-app' }); +console.log(thread.cwd); // '/projects/my-app' +``` + +## Methods + +### run() + +```typescript +run(prompt: string, options?: RunOptions): Promise +``` + +Executes a prompt and waits for the complete response. + + + The natural language prompt to execute. + + + Run-specific configuration. + + + + JSON Schema for structured output. + + + Path to a file containing the prompt. + + + File attachments to include with the prompt. + + + Override the model for this run. + + + Override the autonomy level for this run. + + + + +**Returns:** A promise resolving to the execution result. + +```typescript +const result = await thread.run('Create a function to sort an array'); +console.log(result.finalResponse); + +// Access tool calls +for (const call of result.toolCalls) { + console.log(`Used tool: ${call.toolName}`); +} +``` + +### runStreamed() + +```typescript +runStreamed(prompt: string, options?: RunOptions): Promise +``` + +Executes a prompt with real-time streaming of events. + + + The natural language prompt to execute. + + + Run-specific configuration. + + +**Returns:** A `StreamedTurn` containing an async event iterator and result promise. + +```typescript +const { events, result } = await thread.runStreamed('Build a REST API'); + +for await (const event of events) { + switch (event.type) { + case 'message': + console.log(`[${event.role}] ${event.text}`); + break; + case 'tool_call': + console.log(`Calling: ${event.toolName}`); + break; + case 'tool_result': + console.log(`Result: ${event.isError ? 'ERROR' : 'OK'}`); + break; + case 'completion': + console.log(`Completed in ${event.durationMs}ms`); + break; + } +} + +const finalResult = await result; +``` + +## StreamedTurn + +The return type of `runStreamed()`: + +```typescript +interface StreamedTurn { + events: AsyncIterable; + result: Promise; +} +``` + +### Event Types + +| Type | Description | Properties | +|------|-------------|------------| +| `system` | System init | `cwd`, `session_id`, `tools`, `model` | +| `message` | Message | `role`, `text`, `id`, `timestamp` | +| `tool_call` | Tool call | `toolName`, `parameters`, `id` | +| `tool_result` | Tool result | `toolName`, `value`, `isError` | +| `completion` | Complete | `finalText`, `durationMs`, `numTurns` | +| `turn.failed` | Failed | `error.message`, `error.code` | + +### Type Guards + +Use type guards for type-safe event handling: + +```typescript +import { + isToolCallEvent, + isToolResultEvent, + isMessageEvent +} from '@activade/droid-sdk'; + +for await (const event of events) { + if (isToolCallEvent(event)) { + // TypeScript knows event is ToolCallEvent + console.log(event.toolName, event.parameters); + } +} +``` + +## File Attachments + +Attach files to provide context: + +```typescript +const result = await thread.run('Describe this image', { + attachments: [ + { path: './screenshot.png', type: 'image' } + ] +}); + +const result = await thread.run('Review these files', { + attachments: [ + { path: './src/main.ts', type: 'text', description: 'Main entry' }, + { path: './data.json', type: 'data' } + ] +}); +``` + +## Full Example + +```typescript +import { Droid, MODELS, isToolCallEvent } from '@activade/droid-sdk'; + +const droid = new Droid({ model: MODELS.CLAUDE_SONNET }); +const thread = droid.startThread(); + +// First turn +await thread.run('Create a TypeScript function to validate emails'); + +// Follow-up with context +await thread.run('Add support for custom domain restrictions'); + +// Streaming execution +const { events, result } = await thread.runStreamed('Write tests'); + +for await (const event of events) { + if (isToolCallEvent(event)) { + console.log(`Tool: ${event.toolName}`); + } +} + +const finalResult = await result; +console.log('Tests created:', finalResult.finalResponse); + +// Save session for later +console.log('Session ID:', thread.id); +``` diff --git a/docs/api-reference/classes/turn-result.mdx b/docs/api-reference/classes/turn-result.mdx new file mode 100644 index 0000000..f4c8d66 --- /dev/null +++ b/docs/api-reference/classes/turn-result.mdx @@ -0,0 +1,267 @@ +--- +title: TurnResult +description: Execution result wrapper with parsing and accessors +--- + +# TurnResult + +The `TurnResult` class encapsulates all data from a single AI interaction, including the final response, tool calls, messages, and execution metadata. + +## Import + +```typescript +import { TurnResult } from '@activade/droid-sdk'; + +// TurnResult is returned from Thread.run() and Droid.exec() +const result = await thread.run('Generate code'); +``` + +## Properties + +### finalResponse + +```typescript +readonly finalResponse: string +``` + +The final text response from the AI. For structured output, this contains JSON that can be parsed using `parse()` or `tryParse()`. + +### items + +```typescript +readonly items: AnyTurnItem[] +``` + +All items from this execution turn (messages, tool calls, tool results) in chronological order. + +### sessionId + +```typescript +readonly sessionId: string +``` + +The unique session identifier. Use with `Droid.resumeThread()` to continue the conversation. + +### durationMs + +```typescript +readonly durationMs: number +``` + +Total execution time in milliseconds. + +### numTurns + +```typescript +readonly numTurns: number +``` + +Number of conversation turns in this execution. + +### isError + +```typescript +readonly isError: boolean +``` + +Whether the execution resulted in an error. + +## Methods + +### parse() + +```typescript +parse(schema: { parse: (data: unknown) => T }): T +``` + +Parses the final response as JSON and validates against a schema. + + + A schema with a `parse` method (e.g., Zod schema). + + +**Returns:** The parsed and validated data. + +**Throws:** +- `ParseError` - If the response is not valid JSON +- Schema error - If validation fails + +```typescript +import { z } from 'zod'; + +const UserSchema = z.object({ + id: z.number(), + name: z.string(), + email: z.string().email() +}); + +const result = await thread.run('Generate a user object as JSON'); +const user = result.parse(UserSchema); +// TypeScript knows: user.id is number, user.name is string, etc. +``` + +### tryParse() + +```typescript +tryParse(schema: { safeParse: (data: unknown) => { success: boolean; data?: T } }): T | null +``` + +Attempts to parse the final response, returning `null` on failure. + +```typescript +const config = result.tryParse(ConfigSchema); + +if (config) { + console.log('Config:', config); +} else { + console.log('No valid config in response'); +} +``` + +### toolCalls + +```typescript +get toolCalls(): ToolCallItem[] +``` + +All tool calls made during this execution. + +```typescript +for (const call of result.toolCalls) { + console.log(`Tool: ${call.toolName}`); + console.log(`Params: ${JSON.stringify(call.parameters)}`); +} +``` + +### toolResults + +```typescript +get toolResults(): ToolResultItem[] +``` + +All tool results from this execution. + +```typescript +for (const toolResult of result.toolResults) { + if (toolResult.isError) { + console.error(`${toolResult.toolName} failed: ${toolResult.value}`); + } else { + console.log(`${toolResult.toolName} succeeded`); + } +} +``` + +### messages + +```typescript +get messages(): MessageItem[] +``` + +All messages (user and assistant) from this execution. + +### assistantMessages + +```typescript +get assistantMessages(): MessageItem[] +``` + +Assistant messages only, excluding user prompts. + +```typescript +for (const msg of result.assistantMessages) { + console.log(`[${new Date(msg.timestamp).toISOString()}] ${msg.text}`); +} +``` + +### toJSON() + +```typescript +toJSON(): TurnResultData +``` + +Converts the result to a plain JSON-serializable object. + +```typescript +// Save to file +fs.writeFileSync('result.json', JSON.stringify(result.toJSON(), null, 2)); +``` + +## Item Types + +### MessageItem + +```typescript +interface MessageItem { + type: 'message'; + role: 'user' | 'assistant'; + id: string; + text: string; + timestamp: number; +} +``` + +### ToolCallItem + +```typescript +interface ToolCallItem { + type: 'tool_call'; + id: string; + messageId: string; + toolId: string; + toolName: string; + parameters: Record; + timestamp: number; +} +``` + +### ToolResultItem + +```typescript +interface ToolResultItem { + type: 'tool_result'; + id: string; + messageId: string; + toolId: string; + toolName: string; + isError: boolean; + value: string; + timestamp: number; +} +``` + +## Full Example + +```typescript +import { Droid } from '@activade/droid-sdk'; +import { z } from 'zod'; + +const droid = new Droid(); +const thread = droid.startThread(); + +const result = await thread.run('Create a config file and return the config as JSON'); + +// Access metadata +console.log(`Session: ${result.sessionId}`); +console.log(`Duration: ${result.durationMs}ms`); +console.log(`Turns: ${result.numTurns}`); +console.log(`Error: ${result.isError}`); + +// Access tool activity +console.log(`Tool calls: ${result.toolCalls.length}`); +for (const call of result.toolCalls) { + const resultItem = result.toolResults.find(r => r.toolId === call.toolId); + console.log(` ${call.toolName}: ${resultItem?.isError ? 'FAILED' : 'OK'}`); +} + +// Parse structured output +const ConfigSchema = z.object({ + port: z.number(), + host: z.string(), + debug: z.boolean().optional() +}); + +const config = result.tryParse(ConfigSchema); +if (config) { + console.log(`Server: ${config.host}:${config.port}`); +} +``` diff --git a/docs/api-reference/cli/installer.mdx b/docs/api-reference/cli/installer.mdx new file mode 100644 index 0000000..83bee90 --- /dev/null +++ b/docs/api-reference/cli/installer.mdx @@ -0,0 +1,141 @@ +--- +title: CLI Installer +description: Functions for installing and managing the Droid CLI +--- + +# CLI Installer + +Functions for detecting and installing the Droid CLI binary. + +## Import + +```typescript +import { + isDroidCliInstalled, + getDroidCliPath, + ensureDroidCli +} from '@activade/droid-sdk/cli'; +``` + +## isDroidCliInstalled() + +Check if the CLI is installed: + +```typescript +async function isDroidCliInstalled(): Promise +``` + +### Example + +```typescript +const installed = await isDroidCliInstalled(); +if (!installed) { + console.log('CLI not found, installing...'); + await ensureDroidCli(); +} +``` + +## getDroidCliPath() + +Get the path to an installed CLI: + +```typescript +async function getDroidCliPath(): Promise +``` + +### Example + +```typescript +const path = await getDroidCliPath(); +if (path) { + console.log('CLI found at:', path); +} else { + console.log('CLI not installed'); +} +``` + +## ensureDroidCli() + +Install the CLI if not present: + +```typescript +async function ensureDroidCli(options?: InstallOptions): Promise +``` + +### Parameters + +```typescript +interface InstallOptions { + /** Progress callback */ + onProgress?: (progress: InstallProgress) => void; + + /** Force reinstall even if already installed */ + force?: boolean; +} + +interface InstallProgress { + /** Current phase of installation */ + phase: 'checking' | 'downloading' | 'installing' | 'complete'; + + /** Human-readable progress message */ + message: string; + + /** Progress percentage (0-100), if available */ + percent?: number; +} +``` + +### Example + +```typescript +const cliPath = await ensureDroidCli({ + onProgress: (progress) => { + console.log(`[${progress.phase}] ${progress.message}`); + if (progress.percent !== undefined) { + console.log(`Progress: ${progress.percent}%`); + } + } +}); + +console.log('CLI ready at:', cliPath); +``` + +### Force Reinstall + +```typescript +await ensureDroidCli({ force: true }); +``` + +## Full Example + +```typescript +import { Droid } from '@activade/droid-sdk'; +import { ensureDroidCli, isDroidCliInstalled } from '@activade/droid-sdk/cli'; + +async function setup() { + // Check and install if needed + if (!(await isDroidCliInstalled())) { + console.log('Installing Droid CLI...'); + await ensureDroidCli({ + onProgress: (p) => console.log(` ${p.message}`) + }); + } + + // Now safe to create Droid instance + const droid = new Droid(); + return droid; +} +``` + +## CI/CD Usage + +```typescript +// In CI, install quietly +await ensureDroidCli({ + onProgress: (p) => { + if (p.phase === 'complete') { + console.log('CLI installed successfully'); + } + } +}); +``` diff --git a/docs/api-reference/cli/process.mdx b/docs/api-reference/cli/process.mdx new file mode 100644 index 0000000..30c70c5 --- /dev/null +++ b/docs/api-reference/cli/process.mdx @@ -0,0 +1,188 @@ +--- +title: CLI Process +description: Low-level CLI process management functions +--- + +# CLI Process + +Low-level functions for spawning and managing CLI processes. These are primarily used internally but are exported for advanced use cases. + +## Import + +```typescript +import { + spawnDroid, + spawnDroidStreaming, + execDroidJson, + listDroidTools, + findDroidPath +} from '@activade/droid-sdk/cli'; +``` + +## findDroidPath() + +Locate the CLI binary: + +```typescript +async function findDroidPath(customPath?: string): Promise +``` + +### Example + +```typescript +try { + const path = await findDroidPath(); + console.log('Found CLI at:', path); +} catch (error) { + if (error instanceof CliNotFoundError) { + console.log('Searched:', error.searchedPaths); + } +} +``` + +## spawnDroid() + +Spawn a CLI process and wait for completion: + +```typescript +async function spawnDroid(options: SpawnOptions): Promise +``` + +### SpawnOptions + +```typescript +interface SpawnOptions { + prompt: string; + cwd?: string; + droidPath?: string; + model?: string; + outputFormat?: OutputFormat; + timeout?: number; + autonomyLevel?: AutonomyLevel; + reasoningEffort?: ReasoningEffort; + outputSchema?: JsonSchema; + sessionId?: string; + enabledTools?: string[]; + promptFile?: string; + attachments?: FileAttachment[]; +} +``` + +### DroidProcessResult + +```typescript +interface DroidProcessResult { + stdout: string; + stderr: string; + exitCode: number | null; +} +``` + +### Example + +```typescript +const result = await spawnDroid({ + prompt: 'Hello', + cwd: '/my/project', + outputFormat: 'json' +}); + +console.log('Output:', result.stdout); +console.log('Exit code:', result.exitCode); +``` + +## spawnDroidStreaming() + +Spawn a CLI process with streaming output: + +```typescript +async function spawnDroidStreaming( + options: SpawnOptions +): Promise +``` + +### StreamingDroidProcess + +```typescript +interface StreamingDroidProcess { + stdout: ReadableStream; + stderr: ReadableStream; + exitCode: Promise; +} +``` + +### Example + +```typescript +const process = await spawnDroidStreaming({ + prompt: 'Build feature', + outputFormat: 'stream-json' +}); + +const reader = process.stdout.getReader(); +while (true) { + const { done, value } = await reader.read(); + if (done) break; + console.log('Chunk:', new TextDecoder().decode(value)); +} + +const exitCode = await process.exitCode; +``` + +## execDroidJson() + +Execute and parse JSON response: + +```typescript +async function execDroidJson(options: SpawnOptions): Promise +``` + +### Example + +```typescript +const result = await execDroidJson({ + prompt: 'Generate data', + cwd: '/project' +}); + +console.log('Session:', result.session_id); +console.log('Response:', result.result); +``` + +## listDroidTools() + +List available tools: + +```typescript +async function listDroidTools( + droidPath?: string, + model?: string +): Promise +``` + +### Example + +```typescript +const tools = await listDroidTools(); +console.log('Available tools:', tools); +// ['Read', 'Write', 'Bash', 'Glob', 'Grep', ...] +``` + +## Advanced Usage + +```typescript +import { + spawnDroidStreaming, + parseJsonLines +} from '@activade/droid-sdk/cli'; + +// Custom streaming with event parsing +const process = await spawnDroidStreaming({ + prompt: 'Complex task', + outputFormat: 'stream-json' +}); + +for await (const event of parseJsonLines(process.stdout)) { + console.log('Event:', event); +} +``` diff --git a/docs/api-reference/errors/cli-not-found-error.mdx b/docs/api-reference/errors/cli-not-found-error.mdx new file mode 100644 index 0000000..cd0f412 --- /dev/null +++ b/docs/api-reference/errors/cli-not-found-error.mdx @@ -0,0 +1,74 @@ +--- +title: CliNotFoundError +description: Thrown when the Droid CLI is not installed +--- + +# CliNotFoundError + +Thrown when the Droid CLI binary cannot be found on the system. + +## Import + +```typescript +import { CliNotFoundError, ensureDroidCli } from '@activade/droid-sdk'; +``` + +## Properties + +### searchedPaths + +```typescript +readonly searchedPaths: string[] +``` + +List of paths that were searched for the CLI. + +## Usage + +```typescript +try { + await droid.exec('Hello'); +} catch (error) { + if (error instanceof CliNotFoundError) { + console.log('CLI not found in:', error.searchedPaths); + + // Auto-install the CLI + await ensureDroidCli(); + + // Retry the operation + await droid.exec('Hello'); + } +} +``` + +## Common Causes + +1. **CLI not installed** - Run the installation script +2. **PATH not configured** - Add CLI location to PATH +3. **Custom path not set** - Configure `droidPath` in Droid options + +## Solutions + +### Install the CLI + +```bash +curl -fsSL https://app.factory.ai/cli | sh +``` + +### Use Auto-installer + +```typescript +import { ensureDroidCli } from '@activade/droid-sdk/cli'; + +await ensureDroidCli({ + onProgress: (p) => console.log(`[${p.phase}] ${p.message}`) +}); +``` + +### Specify Custom Path + +```typescript +const droid = new Droid({ + droidPath: '/custom/path/to/droid' +}); +``` diff --git a/docs/api-reference/errors/droid-error.mdx b/docs/api-reference/errors/droid-error.mdx new file mode 100644 index 0000000..283e768 --- /dev/null +++ b/docs/api-reference/errors/droid-error.mdx @@ -0,0 +1,76 @@ +--- +title: DroidError +description: Base error class for all SDK errors +--- + +# DroidError + +The base class for all Droid SDK errors. Extend this for custom error handling. + +## Import + +```typescript +import { DroidError } from '@activade/droid-sdk'; +``` + +## Usage + +```typescript +try { + await droid.exec('Do something'); +} catch (error) { + if (error instanceof DroidError) { + // Handle any SDK error + console.error('SDK error:', error.message); + } +} +``` + +## Properties + +### message + +```typescript +readonly message: string +``` + +Human-readable error description. + +### name + +```typescript +readonly name: string +``` + +Error class name (`'DroidError'` or subclass name). + +## Error Hierarchy + +``` +DroidError +├── CliNotFoundError +├── ExecutionError +├── ParseError +├── TimeoutError +└── StreamError +``` + +## Catching All SDK Errors + +```typescript +import { DroidError } from '@activade/droid-sdk'; + +async function safeDroidOperation() { + try { + return await droid.exec('Generate code'); + } catch (error) { + if (error instanceof DroidError) { + // Log and handle SDK errors + console.error(`Droid SDK error: ${error.name} - ${error.message}`); + return null; + } + // Re-throw unknown errors + throw error; + } +} +``` diff --git a/docs/api-reference/errors/execution-error.mdx b/docs/api-reference/errors/execution-error.mdx new file mode 100644 index 0000000..b6622e6 --- /dev/null +++ b/docs/api-reference/errors/execution-error.mdx @@ -0,0 +1,85 @@ +--- +title: ExecutionError +description: Thrown when CLI execution fails +--- + +# ExecutionError + +Thrown when the Droid CLI process exits with an error. + +## Import + +```typescript +import { ExecutionError } from '@activade/droid-sdk'; +``` + +## Properties + +### exitCode + +```typescript +readonly exitCode: number | null +``` + +The CLI process exit code, or `null` if unavailable. + +### stderr + +```typescript +readonly stderr: string +``` + +Standard error output from the CLI. + +### stdout + +```typescript +readonly stdout: string +``` + +Standard output from the CLI (may contain partial results). + +## Usage + +```typescript +try { + await droid.exec('Risky operation'); +} catch (error) { + if (error instanceof ExecutionError) { + console.error('Execution failed:'); + console.error('Exit code:', error.exitCode); + console.error('Error output:', error.stderr); + console.error('Standard output:', error.stdout); + } +} +``` + +## Common Causes + +1. **Invalid prompt** - Malformed or empty prompt +2. **Permission denied** - Insufficient permissions for file operations +3. **Tool failure** - A tool returned an error +4. **API error** - Backend API returned an error + +## Debugging + +```typescript +try { + await droid.exec(prompt); +} catch (error) { + if (error instanceof ExecutionError) { + // Log full error details + console.error({ + message: error.message, + exitCode: error.exitCode, + stderr: error.stderr, + stdout: error.stdout + }); + + // Check for specific error patterns + if (error.stderr.includes('permission denied')) { + console.log('Try running with higher autonomy level'); + } + } +} +``` diff --git a/docs/api-reference/errors/parse-error.mdx b/docs/api-reference/errors/parse-error.mdx new file mode 100644 index 0000000..2ee3677 --- /dev/null +++ b/docs/api-reference/errors/parse-error.mdx @@ -0,0 +1,94 @@ +--- +title: ParseError +description: Thrown when JSON parsing fails +--- + +# ParseError + +Thrown when attempting to parse a non-JSON response. + +## Import + +```typescript +import { ParseError } from '@activade/droid-sdk'; +``` + +## Properties + +### rawText + +```typescript +readonly rawText: string +``` + +The original text that failed to parse. + +## Usage + +```typescript +import { z } from 'zod'; + +const Schema = z.object({ name: z.string() }); + +try { + const data = result.parse(Schema); +} catch (error) { + if (error instanceof ParseError) { + console.error('Not valid JSON'); + console.error('Raw response:', error.rawText); + } +} +``` + +## Distinguishing Parse Errors + +```typescript +import { ParseError } from '@activade/droid-sdk'; +import { z } from 'zod'; + +try { + const data = result.parse(MySchema); +} catch (error) { + if (error instanceof ParseError) { + // Response was not valid JSON + console.error('JSON syntax error:', error.message); + console.error('Response was:', error.rawText); + } else if (error instanceof z.ZodError) { + // JSON was valid but didn't match schema + console.error('Validation errors:', error.issues); + } +} +``` + +## Using tryParse Instead + +For optional JSON parsing, use `tryParse()` to avoid exceptions: + +```typescript +const data = result.tryParse(MySchema); + +if (data) { + console.log('Parsed:', data); +} else { + console.log('Response was not valid JSON'); + console.log('Raw response:', result.finalResponse); +} +``` + +## Prompting for Better JSON + +If you frequently get ParseError, improve your prompts: + +```typescript +// Better prompt for JSON output +const result = await droid.exec(` + Analyze the file and return ONLY a JSON object with this structure: + { + "lines": number, + "functions": number, + "complexity": "low" | "medium" | "high" + } + + Return ONLY the JSON, no other text. +`); +``` diff --git a/docs/api-reference/errors/stream-error.mdx b/docs/api-reference/errors/stream-error.mdx new file mode 100644 index 0000000..bcaeaed --- /dev/null +++ b/docs/api-reference/errors/stream-error.mdx @@ -0,0 +1,96 @@ +--- +title: StreamError +description: Thrown when stream processing fails +--- + +# StreamError + +Thrown when reading or processing a streaming response fails. + +## Import + +```typescript +import { StreamError } from '@activade/droid-sdk'; +``` + +## Properties + +### cause + +```typescript +readonly cause?: Error +``` + +The underlying error that caused the stream failure, if available. + +## Usage + +```typescript +try { + const { events, result } = await thread.runStreamed('Build feature'); + + for await (const event of events) { + // Process events + } + + await result; +} catch (error) { + if (error instanceof StreamError) { + console.error('Stream failed:', error.message); + if (error.cause) { + console.error('Caused by:', error.cause); + } + } +} +``` + +## Common Causes + +1. **Connection interrupted** - Network issues during streaming +2. **Process terminated** - CLI process was killed +3. **Invalid stream data** - Malformed JSON in stream +4. **Resource exhaustion** - Out of memory or file descriptors + +## Handling Stream Errors + +```typescript +async function resilientStream(thread: Thread, prompt: string) { + const maxRetries = 3; + + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + const { events, result } = await thread.runStreamed(prompt); + + for await (const event of events) { + if (event.type === 'turn.failed') { + throw new Error(event.error.message); + } + // Process event + } + + return await result; + } catch (error) { + if (error instanceof StreamError && attempt < maxRetries - 1) { + console.log(`Stream error, retrying (${attempt + 1}/${maxRetries})`); + await new Promise(r => setTimeout(r, 1000 * (attempt + 1))); + continue; + } + throw error; + } + } +} +``` + +## Inline Error Events + +Note that some errors come as events rather than thrown exceptions: + +```typescript +for await (const event of events) { + if (event.type === 'turn.failed') { + // Error during execution (not a StreamError) + console.error('Turn failed:', event.error); + break; + } +} +``` diff --git a/docs/api-reference/errors/timeout-error.mdx b/docs/api-reference/errors/timeout-error.mdx new file mode 100644 index 0000000..96a1763 --- /dev/null +++ b/docs/api-reference/errors/timeout-error.mdx @@ -0,0 +1,90 @@ +--- +title: TimeoutError +description: Thrown when operations exceed the timeout +--- + +# TimeoutError + +Thrown when an operation takes longer than the configured timeout. + +## Import + +```typescript +import { TimeoutError } from '@activade/droid-sdk'; +``` + +## Properties + +### timeoutMs + +```typescript +readonly timeoutMs: number +``` + +The timeout value in milliseconds that was exceeded. + +## Usage + +```typescript +try { + await droid.exec('Complex long-running task'); +} catch (error) { + if (error instanceof TimeoutError) { + console.error(`Timed out after ${error.timeoutMs}ms`); + } +} +``` + +## Preventing Timeouts + +### Increase Timeout + +```typescript +const droid = new Droid({ + timeout: 600000 // 10 minutes +}); +``` + +### Per-execution Timeout + +```typescript +// Use a longer timeout for specific operations +const result = await droid.exec('Build entire project', { + timeout: 900000 // 15 minutes +}); +``` + +## Handling Timeouts Gracefully + +```typescript +async function executeWithFallback(prompt: string) { + const droid = new Droid({ timeout: 60000 }); + + try { + return await droid.exec(prompt); + } catch (error) { + if (error instanceof TimeoutError) { + console.log('Operation timed out, trying with streaming...'); + + // Fall back to streaming for progress visibility + const thread = droid.startThread(); + const { events, result } = await thread.runStreamed(prompt); + + for await (const event of events) { + console.log('Progress:', event.type); + } + + return await result; + } + throw error; + } +} +``` + +## Default Timeout + +The default timeout is 600000ms (10 minutes). For complex operations, consider: + +1. Breaking into smaller tasks +2. Using streaming for progress visibility +3. Increasing the timeout value diff --git a/docs/api-reference/models/model-info.mdx b/docs/api-reference/models/model-info.mdx new file mode 100644 index 0000000..b5dd14e --- /dev/null +++ b/docs/api-reference/models/model-info.mdx @@ -0,0 +1,144 @@ +--- +title: Model Info +description: Model metadata and utilities +--- + +# Model Info + +Utilities for working with model metadata. + +## Import + +```typescript +import { + getModelInfo, + isValidModel, + MODEL_INFO +} from '@activade/droid-sdk'; +``` + +## MODEL_INFO + +Registry of model metadata: + +```typescript +const MODEL_INFO: Record = { + 'claude-opus-4-5-20251101': { + id: 'claude-opus-4-5-20251101', + name: 'Claude Opus 4.5', + provider: 'anthropic', + contextWindow: 200000, + maxOutput: 16384 + }, + 'claude-sonnet-4-5-20250929': { + id: 'claude-sonnet-4-5-20250929', + name: 'Claude Sonnet 4.5', + provider: 'anthropic', + contextWindow: 200000, + maxOutput: 16384 + }, + // ... more models +}; +``` + +## ModelInfo Interface + +```typescript +interface ModelInfo { + /** Model identifier */ + id: ModelId; + + /** Human-readable model name */ + name: string; + + /** Model provider */ + provider: 'anthropic' | 'openai' | 'google' | 'open-source'; + + /** Context window size in tokens */ + contextWindow: number; + + /** Maximum output tokens */ + maxOutput: number; +} +``` + +## getModelInfo() + +Get metadata for a model: + +```typescript +function getModelInfo(modelId: string): ModelInfo | undefined +``` + +### Example + +```typescript +const info = getModelInfo('claude-sonnet-4-5-20250929'); + +if (info) { + console.log(`${info.name} by ${info.provider}`); + console.log(`Context: ${info.contextWindow} tokens`); + console.log(`Max output: ${info.maxOutput} tokens`); +} +``` + +## isValidModel() + +Check if a model ID is valid: + +```typescript +function isValidModel(modelId: string): modelId is ModelId +``` + +### Example + +```typescript +const modelId = 'claude-sonnet-4-5-20250929'; + +if (isValidModel(modelId)) { + // TypeScript knows modelId is ModelId + const droid = new Droid({ model: modelId }); +} +``` + +## Practical Examples + +### Model Comparison + +```typescript +import { MODEL_INFO, MODELS } from '@activade/droid-sdk'; + +// Compare context windows +const sonnet = MODEL_INFO[MODELS.CLAUDE_SONNET]; +const haiku = MODEL_INFO[MODELS.CLAUDE_HAIKU]; + +console.log(`Sonnet context: ${sonnet.contextWindow}`); +console.log(`Haiku context: ${haiku.contextWindow}`); +``` + +### Selecting by Provider + +```typescript +import { MODEL_INFO, ModelId } from '@activade/droid-sdk'; + +function getAnthropicModels(): ModelId[] { + return Object.values(MODEL_INFO) + .filter(m => m.provider === 'anthropic') + .map(m => m.id); +} +``` + +### Validation + +```typescript +function createDroid(modelId: string) { + if (!isValidModel(modelId)) { + throw new Error(`Unknown model: ${modelId}`); + } + + const info = getModelInfo(modelId)!; + console.log(`Using ${info.name}`); + + return new Droid({ model: modelId }); +} +``` diff --git a/docs/api-reference/models/overview.mdx b/docs/api-reference/models/overview.mdx new file mode 100644 index 0000000..f30b4cb --- /dev/null +++ b/docs/api-reference/models/overview.mdx @@ -0,0 +1,90 @@ +--- +title: Models Overview +description: Available AI models and configuration +--- + +# Models + +The SDK supports multiple AI models from different providers. + +## Available Models + +```typescript +import { MODELS } from '@activade/droid-sdk'; +``` + +### Anthropic Claude + +| Constant | Model ID | Description | +|----------|----------|-------------| +| `MODELS.CLAUDE_OPUS` | `claude-opus-4-5-20251101` | Most capable, best for complex tasks | +| `MODELS.CLAUDE_SONNET` | `claude-sonnet-4-5-20250929` | Balanced performance and speed | +| `MODELS.CLAUDE_HAIKU` | `claude-haiku-4-5-20251001` | Fastest, best for simple tasks | + +### OpenAI GPT + +| Constant | Model ID | Description | +|----------|----------|-------------| +| `MODELS.GPT_5_1` | `gpt-5.1` | GPT-5.1 base model | +| `MODELS.GPT_5_1_CODEX` | `gpt-5.1-codex` | Optimized for code | +| `MODELS.GPT_5_2` | `gpt-5.2` | Latest GPT-5 variant | + +### Google Gemini + +| Constant | Model ID | Description | +|----------|----------|-------------| +| `MODELS.GEMINI_3_PRO` | `gemini-3-pro-preview` | Gemini 3 Pro preview | +| `MODELS.GEMINI_3_FLASH` | `gemini-3-flash-preview` | Fast Gemini variant | + +### Open Source + +| Constant | Model ID | Description | +|----------|----------|-------------| +| `MODELS.DROID_CORE` | `glm-4.6` | Open source GLM model | + +## Usage + +```typescript +import { Droid, MODELS } from '@activade/droid-sdk'; + +// Using a constant +const droid = new Droid({ model: MODELS.CLAUDE_SONNET }); + +// Using a string +const droid = new Droid({ model: 'claude-sonnet-4-5-20250929' }); + +// Override per-thread +const thread = droid.startThread({ model: MODELS.CLAUDE_OPUS }); + +// Override per-run +const result = await thread.run('Task', { model: MODELS.CLAUDE_HAIKU }); +``` + +## Model Selection + +Choose based on your needs: + +| Use Case | Recommended Model | +|----------|------------------| +| Complex analysis | `CLAUDE_OPUS` | +| General development | `CLAUDE_SONNET` | +| Quick tasks | `CLAUDE_HAIKU` | +| Code generation | `GPT_5_1_CODEX` | +| Fast iteration | `GEMINI_3_FLASH` | + +## ModelId Type + +```typescript +type ModelId = + | 'claude-opus-4-5-20251101' + | 'claude-sonnet-4-5-20250929' + | 'claude-haiku-4-5-20251001' + | 'gpt-5.1' + | 'gpt-5.1-codex' + | 'gpt-5.2' + | 'gemini-3-pro-preview' + | 'gemini-3-flash-preview' + | 'glm-4.6'; +``` + +The `model` configuration accepts either a `ModelId` or any string for custom/new models. diff --git a/docs/api-reference/overview.mdx b/docs/api-reference/overview.mdx new file mode 100644 index 0000000..50c3b82 --- /dev/null +++ b/docs/api-reference/overview.mdx @@ -0,0 +1,126 @@ +--- +title: API Overview +description: Complete reference for the Droid SDK API +--- + +# API Reference + +This section provides comprehensive documentation for all classes, types, and functions in the Droid SDK. + +## Core Classes + + + + Main entry point for the SDK + + + Multi-turn conversation management + + + Execution result wrapper + + + +## Quick Reference + +### Creating a Droid Instance + +```typescript +import { Droid, MODELS } from '@activade/droid-sdk'; + +const droid = new Droid({ + model: MODELS.CLAUDE_SONNET, + autonomyLevel: 'high', + reasoningEffort: 'medium', + cwd: '/path/to/project', + timeout: 600000 +}); +``` + +### Configuration Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `model` | `ModelId \| string` | - | AI model to use | +| `autonomyLevel` | `'default' \| 'low' \| 'medium' \| 'high'` | `'default'` | Autonomous decision level | +| `reasoningEffort` | `'off' \| 'none' \| 'low' \| 'medium' \| 'high'` | - | Reasoning intensity | +| `cwd` | `string` | `process.cwd()` | Working directory | +| `timeout` | `number` | `600000` | Timeout in milliseconds | +| `droidPath` | `string` | `'droid'` | Path to CLI binary | + +### Available Models + +```typescript +import { MODELS } from '@activade/droid-sdk'; + +// Anthropic +MODELS.CLAUDE_OPUS // claude-opus-4-5-20251101 +MODELS.CLAUDE_SONNET // claude-sonnet-4-5-20250929 +MODELS.CLAUDE_HAIKU // claude-haiku-4-5-20251001 + +// OpenAI +MODELS.GPT_5_1 // gpt-5.1 +MODELS.GPT_5_1_CODEX // gpt-5.1-codex +MODELS.GPT_5_2 // gpt-5.2 + +// Google +MODELS.GEMINI_3_PRO // gemini-3-pro-preview +MODELS.GEMINI_3_FLASH // gemini-3-flash-preview + +// Open Source +MODELS.DROID_CORE // glm-4.6 +``` + +### Error Classes + +| Error | Description | +|-------|-------------| +| `DroidError` | Base class for all SDK errors | +| `CliNotFoundError` | CLI binary not found | +| `ExecutionError` | CLI execution failed | +| `ParseError` | JSON parsing failed | +| `TimeoutError` | Operation timed out | +| `StreamError` | Stream reading failed | + +### Event Types + +| Event Type | Description | +|------------|-------------| +| `system` | System initialization | +| `message` | User or assistant message | +| `tool_call` | Tool invocation | +| `tool_result` | Tool result | +| `completion` | Turn completed | +| `turn.failed` | Turn failed | + +## Import Patterns + +```typescript +// Main SDK +import { Droid, Thread, TurnResult, MODELS } from '@activade/droid-sdk'; + +// Error classes +import { + DroidError, + CliNotFoundError, + ExecutionError, + ParseError, + TimeoutError, + StreamError +} from '@activade/droid-sdk'; + +// Event types and guards +import { + isToolCallEvent, + isToolResultEvent, + isMessageEvent, + type StreamEvent, + type ToolCallEvent +} from '@activade/droid-sdk'; + +// CLI utilities (separate entry point) +import { ensureDroidCli, isDroidCliInstalled } from '@activade/droid-sdk/cli'; + +// Model utilities +import { getModelInfo, isValidModel, MODEL_INFO } from '@activade/droid-sdk'; +``` diff --git a/docs/api-reference/types/config.mdx b/docs/api-reference/types/config.mdx new file mode 100644 index 0000000..01ed417 --- /dev/null +++ b/docs/api-reference/types/config.mdx @@ -0,0 +1,86 @@ +--- +title: Configuration Types +description: SDK configuration interfaces and defaults +--- + +# Configuration Types + +Types for configuring Droid instances. + +## DroidConfig + +Main configuration interface for the Droid class. + +```typescript +interface DroidConfig { + /** Working directory for CLI operations */ + cwd?: string; + + /** AI model to use */ + model?: ModelId | string; + + /** Level of autonomous decision-making */ + autonomyLevel?: AutonomyLevel; + + /** Reasoning intensity for the AI */ + reasoningEffort?: ReasoningEffort; + + /** Path to the Droid CLI binary */ + droidPath?: string; + + /** Maximum execution time in milliseconds */ + timeout?: number; +} +``` + +## Usage + +```typescript +import { Droid, MODELS } from '@activade/droid-sdk'; + +const config: DroidConfig = { + model: MODELS.CLAUDE_SONNET, + autonomyLevel: 'high', + reasoningEffort: 'medium', + cwd: '/path/to/project', + timeout: 300000, + droidPath: '/custom/bin/droid' +}; + +const droid = new Droid(config); +``` + +## Defaults + +| Property | Default Value | +|----------|--------------| +| `cwd` | `process.cwd()` | +| `model` | (none) | +| `autonomyLevel` | `'default'` | +| `reasoningEffort` | (none) | +| `droidPath` | `'droid'` | +| `timeout` | `600000` (10 min) | + +## Constants + +```typescript +/** Default execution timeout in milliseconds (10 minutes) */ +const DEFAULT_TIMEOUT = 600000; + +/** Default CLI binary name */ +const DEFAULT_DROID_PATH = 'droid'; +``` + +## Config Immutability + +The config is frozen after construction: + +```typescript +const droid = new Droid({ model: MODELS.CLAUDE_SONNET }); + +// Read config +console.log(droid.config.model); // 'claude-sonnet-4-5-20250929' + +// Config is readonly +droid.config.model = 'other'; // TypeScript error +``` diff --git a/docs/api-reference/types/events.mdx b/docs/api-reference/types/events.mdx new file mode 100644 index 0000000..51f057b --- /dev/null +++ b/docs/api-reference/types/events.mdx @@ -0,0 +1,179 @@ +--- +title: Event Types +description: Streaming event types and type guards +--- + +# Event Types + +Types for streaming events emitted during execution. + +## StreamEvent + +Union type of all possible events: + +```typescript +type StreamEvent = + | SystemInitEvent + | MessageEvent + | ToolCallEvent + | ToolResultEvent + | TurnCompletedEvent + | TurnFailedEvent; +``` + +## SystemInitEvent + +Emitted at the start of execution: + +```typescript +interface SystemInitEvent { + type: 'system'; + cwd: string; + session_id: string; + tools: string[]; + model: string; +} +``` + +## MessageEvent + +User or assistant messages: + +```typescript +interface MessageEvent { + type: 'message'; + role: 'user' | 'assistant'; + id: string; + text: string; + timestamp: number; +} +``` + +## ToolCallEvent + +Tool invocations: + +```typescript +interface ToolCallEvent { + type: 'tool_call'; + id: string; + messageId: string; + toolId: string; + toolName: string; + parameters: Record; + timestamp: number; +} +``` + +## ToolResultEvent + +Tool execution results: + +```typescript +interface ToolResultEvent { + type: 'tool_result'; + id: string; + messageId: string; + toolId: string; + toolName: string; + isError: boolean; + value: string; + timestamp: number; +} +``` + +## TurnCompletedEvent + +Successful completion: + +```typescript +interface TurnCompletedEvent { + type: 'completion'; + finalText: string; + durationMs: number; + numTurns: number; +} +``` + +## TurnFailedEvent + +Execution failure: + +```typescript +interface TurnFailedEvent { + type: 'turn.failed'; + error: { + message: string; + code?: string; + }; +} +``` + +## Type Guards + +Import type guards for type-safe event handling: + +```typescript +import { + isSystemInitEvent, + isMessageEvent, + isToolCallEvent, + isToolResultEvent, + isTurnCompletedEvent, + isTurnFailedEvent +} from '@activade/droid-sdk'; + +for await (const event of events) { + if (isToolCallEvent(event)) { + // TypeScript knows: event is ToolCallEvent + console.log(event.toolName, event.parameters); + } + + if (isMessageEvent(event)) { + // TypeScript knows: event is MessageEvent + console.log(`[${event.role}] ${event.text}`); + } +} +``` + +## Type Guard Functions + +```typescript +function isSystemInitEvent(e: StreamEvent): e is SystemInitEvent; +function isMessageEvent(e: StreamEvent): e is MessageEvent; +function isToolCallEvent(e: StreamEvent): e is ToolCallEvent; +function isToolResultEvent(e: StreamEvent): e is ToolResultEvent; +function isTurnCompletedEvent(e: StreamEvent): e is TurnCompletedEvent; +function isTurnFailedEvent(e: StreamEvent): e is TurnFailedEvent; +``` + +## Complete Example + +```typescript +import { + isMessageEvent, + isToolCallEvent, + isToolResultEvent, + isTurnFailedEvent +} from '@activade/droid-sdk'; + +const { events, result } = await thread.runStreamed('Build feature'); + +for await (const event of events) { + if (isMessageEvent(event) && event.role === 'assistant') { + console.log('AI:', event.text); + } + + if (isToolCallEvent(event)) { + console.log(`Running: ${event.toolName}`); + } + + if (isToolResultEvent(event)) { + console.log(` ${event.isError ? '✗' : '✓'} ${event.toolName}`); + } + + if (isTurnFailedEvent(event)) { + console.error('Failed:', event.error.message); + } +} +``` diff --git a/docs/api-reference/types/options.mdx b/docs/api-reference/types/options.mdx new file mode 100644 index 0000000..f9853d3 --- /dev/null +++ b/docs/api-reference/types/options.mdx @@ -0,0 +1,150 @@ +--- +title: Option Types +description: Execution and thread option types +--- + +# Option Types + +Types for configuring thread and execution options. + +## AutonomyLevel + +Controls how independently the AI operates: + +```typescript +type AutonomyLevel = 'default' | 'low' | 'medium' | 'high'; +``` + +| Level | Behavior | +|-------|----------| +| `default` | Standard confirmation prompts | +| `low` | Conservative, confirms most operations | +| `medium` | Balanced autonomy | +| `high` | Maximum autonomy, minimal confirmations | + +## ReasoningEffort + +Controls reasoning intensity: + +```typescript +type ReasoningEffort = 'off' | 'none' | 'low' | 'medium' | 'high'; +``` + +## OutputFormat + +Output format for CLI operations: + +```typescript +type OutputFormat = 'text' | 'json' | 'stream-json'; +``` + +## ThreadOptions + +Options for creating threads: + +```typescript +interface ThreadOptions { + /** Override working directory */ + cwd?: string; + + /** Override autonomy level */ + autonomyLevel?: AutonomyLevel; + + /** Restrict available tools */ + enabledTools?: string[]; + + /** Override model */ + model?: string; +} +``` + +## RunOptions + +Options for run/runStreamed: + +```typescript +interface RunOptions { + /** JSON Schema for structured output */ + outputSchema?: JsonSchema; + + /** Path to a prompt file */ + promptFile?: string; + + /** File attachments */ + attachments?: FileAttachment[]; + + /** Override model for this run */ + model?: string; + + /** Override autonomy level */ + autonomyLevel?: AutonomyLevel; +} +``` + +## ExecOptions + +Options for one-shot execution: + +```typescript +interface ExecOptions extends RunOptions { + /** Override working directory */ + cwd?: string; + + /** Override timeout */ + timeout?: number; +} +``` + +## FileAttachment + +File attachment for prompts: + +```typescript +interface FileAttachment { + /** Path to the file */ + path: string; + + /** Type of file */ + type: 'image' | 'text' | 'data'; + + /** Optional description */ + description?: string; +} +``` + +## JsonSchema + +JSON Schema for structured output: + +```typescript +interface JsonSchema { + type?: string; + properties?: Record; + items?: JsonSchema; + required?: string[]; + enum?: unknown[]; + description?: string; + [key: string]: unknown; +} +``` + +## Usage Example + +```typescript +const thread = droid.startThread({ + autonomyLevel: 'high', + enabledTools: ['Read', 'Write'] +}); + +const result = await thread.run('Analyze code', { + attachments: [ + { path: './src/main.ts', type: 'text' } + ], + outputSchema: { + type: 'object', + properties: { + issues: { type: 'array' } + } + } +}); +``` diff --git a/docs/api-reference/types/turn-items.mdx b/docs/api-reference/types/turn-items.mdx new file mode 100644 index 0000000..0e76c71 --- /dev/null +++ b/docs/api-reference/types/turn-items.mdx @@ -0,0 +1,161 @@ +--- +title: Turn Item Types +description: Types for turn results and items +--- + +# Turn Item Types + +Types representing items within a turn result. + +## AnyTurnItem + +Union of all item types: + +```typescript +type AnyTurnItem = MessageItem | ToolCallItem | ToolResultItem; +``` + +## MessageItem + +Represents a message in the conversation: + +```typescript +interface MessageItem { + type: 'message'; + role: 'user' | 'assistant'; + id: string; + text: string; + timestamp: number; +} +``` + +## ToolCallItem + +Represents a tool invocation: + +```typescript +interface ToolCallItem { + type: 'tool_call'; + id: string; + messageId: string; + toolId: string; + toolName: string; + parameters: Record; + timestamp: number; +} +``` + +## ToolResultItem + +Represents a tool execution result: + +```typescript +interface ToolResultItem { + type: 'tool_result'; + id: string; + messageId: string; + toolId: string; + toolName: string; + isError: boolean; + value: string; + timestamp: number; +} +``` + +## TurnResultData + +Serializable turn result data: + +```typescript +interface TurnResultData { + sessionId: string; + finalResponse: string; + items: AnyTurnItem[]; + durationMs: number; + numTurns: number; + isError: boolean; +} +``` + +## JsonResult + +Response from JSON-mode execution: + +```typescript +interface JsonResult { + session_id: string; + is_error: boolean; + duration_ms: number; + num_turns: number; + result: string; +} +``` + +## Usage Examples + +### Accessing Items + +```typescript +const result = await thread.run('Create a file'); + +// All items in order +for (const item of result.items) { + console.log(item.type, item.timestamp); +} + +// Filter by type +const messages = result.messages; +const toolCalls = result.toolCalls; +const toolResults = result.toolResults; +``` + +### Correlating Tool Calls and Results + +```typescript +for (const call of result.toolCalls) { + const callResult = result.toolResults.find( + r => r.toolId === call.toolId + ); + + console.log(`${call.toolName}:`, callResult?.isError ? 'FAILED' : 'OK'); +} +``` + +### Serializing Results + +```typescript +const result = await thread.run('Build feature'); + +// Convert to plain object +const data = result.toJSON(); + +// Save to file +fs.writeFileSync('result.json', JSON.stringify(data, null, 2)); + +// Structure matches TurnResultData +console.log(data.sessionId); +console.log(data.items.length); +``` + +### Type Narrowing + +```typescript +for (const item of result.items) { + switch (item.type) { + case 'message': + // item is MessageItem + console.log(`[${item.role}] ${item.text}`); + break; + + case 'tool_call': + // item is ToolCallItem + console.log(`Call: ${item.toolName}`); + break; + + case 'tool_result': + // item is ToolResultItem + console.log(`Result: ${item.isError ? 'Error' : 'Success'}`); + break; + } +} +``` diff --git a/docs/concepts/droid.mdx b/docs/concepts/droid.mdx new file mode 100644 index 0000000..a5e71c5 --- /dev/null +++ b/docs/concepts/droid.mdx @@ -0,0 +1,86 @@ +--- +title: The Droid Class +description: Understanding the main SDK entry point +--- + +# The Droid Class + +The `Droid` class is your gateway to the Factory Droid CLI. It manages configuration, creates conversation threads, and executes AI-powered tasks. + +## Core Responsibilities + +The Droid class handles: + +- **Configuration Management** - Model selection, timeouts, autonomy levels +- **Thread Creation** - Starting new or resuming existing conversations +- **One-shot Execution** - Running single prompts without conversation state +- **Tool Discovery** - Listing available AI tools + +## Creating an Instance + +```typescript +import { Droid, MODELS } from '@activade/droid-sdk'; + +const droid = new Droid({ + model: MODELS.CLAUDE_SONNET, + autonomyLevel: 'high', + cwd: './my-project', + timeout: 300000 +}); +``` + +## Configuration Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `model` | `ModelId \| string` | - | AI model to use | +| `autonomyLevel` | `AutonomyLevel` | `'default'` | Level of autonomous decision-making | +| `reasoningEffort` | `ReasoningEffort` | - | Reasoning intensity | +| `cwd` | `string` | `process.cwd()` | Working directory for CLI operations | +| `timeout` | `number` | `600000` | Maximum execution time in ms | +| `droidPath` | `string` | `'droid'` | Path to CLI binary | + +## Autonomy Levels + +The autonomy level controls how independently the AI makes decisions: + +- **`default`** - Standard behavior, asks for confirmation on risky operations +- **`low`** - More conservative, confirms most file operations +- **`medium`** - Balanced autonomy for routine tasks +- **`high`** - Maximum autonomy, executes most operations without confirmation + +```typescript +// High autonomy for automated workflows +const automatedDroid = new Droid({ autonomyLevel: 'high' }); + +// Low autonomy for careful review +const reviewDroid = new Droid({ autonomyLevel: 'low' }); +``` + +## Execution Patterns + +### One-shot Execution + +Use `exec()` for stateless, single-prompt operations: + +```typescript +const result = await droid.exec('Generate a random UUID'); +console.log(result.finalResponse); +``` + +### Threaded Conversations + +Use threads for multi-turn conversations with context: + +```typescript +const thread = droid.startThread(); +await thread.run('Create a REST API'); +await thread.run('Add authentication'); // Has context from previous turn +``` + +## Best Practices + +1. **Reuse Droid instances** - Create once, use for multiple operations +2. **Match autonomy to use case** - Higher for automation, lower for interactive +3. **Set appropriate timeouts** - Longer for complex tasks +4. **Use structured output** - For programmatic response handling diff --git a/docs/concepts/streaming.mdx b/docs/concepts/streaming.mdx new file mode 100644 index 0000000..4a60123 --- /dev/null +++ b/docs/concepts/streaming.mdx @@ -0,0 +1,210 @@ +--- +title: Streaming Events +description: Real-time event handling during AI execution +--- + +# Streaming Events + +Streaming provides real-time visibility into AI execution, allowing you to display progress, handle tool calls, and respond to events as they occur. + +## Basic Streaming + +```typescript +const { events, result } = await thread.runStreamed('Create a web server'); + +for await (const event of events) { + console.log(event.type, event); +} + +const finalResult = await result; +``` + +## Event Types + +### System Event + +Emitted at the start with session metadata: + +```typescript +{ + type: 'system', + cwd: '/path/to/project', + session_id: 'session_abc123', + tools: ['Read', 'Write', 'Bash', ...], + model: 'claude-sonnet-4-5-20250929' +} +``` + +### Message Event + +User or assistant text messages: + +```typescript +{ + type: 'message', + role: 'assistant', + id: 'msg_123', + text: 'I will create the server...', + timestamp: 1704067200000 +} +``` + +### Tool Call Event + +When the AI invokes a tool: + +```typescript +{ + type: 'tool_call', + id: 'tc_456', + messageId: 'msg_123', + toolId: 'tool_789', + toolName: 'Write', + parameters: { + path: 'server.ts', + content: '...' + }, + timestamp: 1704067201000 +} +``` + +### Tool Result Event + +Tool execution results: + +```typescript +{ + type: 'tool_result', + id: 'tr_012', + messageId: 'msg_123', + toolId: 'tool_789', + toolName: 'Write', + isError: false, + value: 'File written successfully', + timestamp: 1704067202000 +} +``` + +### Completion Event + +Execution completed successfully: + +```typescript +{ + type: 'completion', + finalText: 'Server created at server.ts', + durationMs: 5432, + numTurns: 1 +} +``` + +### Turn Failed Event + +Execution failed: + +```typescript +{ + type: 'turn.failed', + error: { + message: 'Command timed out', + code: 'TIMEOUT' + } +} +``` + +## Type Guards + +Use type guards for type-safe event handling: + +```typescript +import { + isMessageEvent, + isToolCallEvent, + isToolResultEvent, + isTurnCompletedEvent, + isTurnFailedEvent +} from '@activade/droid-sdk'; + +for await (const event of events) { + if (isMessageEvent(event)) { + // TypeScript knows: event.role, event.text + console.log(`[${event.role}] ${event.text}`); + } + + if (isToolCallEvent(event)) { + // TypeScript knows: event.toolName, event.parameters + console.log(`Calling ${event.toolName}`); + } + + if (isToolResultEvent(event)) { + // TypeScript knows: event.isError, event.value + if (event.isError) { + console.error(`Tool failed: ${event.value}`); + } + } +} +``` + +## Building a Progress UI + +```typescript +const { events, result } = await thread.runStreamed(prompt); + +for await (const event of events) { + switch (event.type) { + case 'message': + if (event.role === 'assistant') { + updateUI({ message: event.text }); + } + break; + + case 'tool_call': + updateUI({ + status: 'working', + tool: event.toolName, + action: `Running ${event.toolName}...` + }); + break; + + case 'tool_result': + updateUI({ + status: event.isError ? 'error' : 'success', + result: event.value + }); + break; + + case 'completion': + updateUI({ + status: 'complete', + duration: `${event.durationMs}ms` + }); + break; + + case 'turn.failed': + updateUI({ + status: 'failed', + error: event.error.message + }); + break; + } +} +``` + +## StreamedTurn Structure + +```typescript +interface StreamedTurn { + events: AsyncIterable; + result: Promise; +} +``` + +- **events** - Async iterator of events as they occur +- **result** - Promise that resolves to the final TurnResult + +## Best Practices + +1. **Always await result** - Even if you only need events +2. **Handle all event types** - Especially `turn.failed` +3. **Use type guards** - For type-safe event handling +4. **Stream for long tasks** - Better UX than waiting diff --git a/docs/concepts/structured-output.mdx b/docs/concepts/structured-output.mdx new file mode 100644 index 0000000..a5090e9 --- /dev/null +++ b/docs/concepts/structured-output.mdx @@ -0,0 +1,181 @@ +--- +title: Structured Output +description: Parse AI responses into typed data structures +--- + +# Structured Output + +The SDK supports parsing AI responses into strongly-typed data structures using Zod schemas. + +## Basic Usage + +```typescript +import { Droid } from '@activade/droid-sdk'; +import { z } from 'zod'; + +const droid = new Droid(); + +const UserSchema = z.object({ + id: z.number(), + name: z.string(), + email: z.string().email() +}); + +const result = await droid.exec('Generate a user object as JSON'); +const user = result.parse(UserSchema); + +// TypeScript knows the type: +console.log(user.id); // number +console.log(user.name); // string +console.log(user.email); // string +``` + +## Parse Methods + +### parse() - Strict Parsing + +Throws on invalid data: + +```typescript +const ConfigSchema = z.object({ + port: z.number(), + host: z.string() +}); + +try { + const config = result.parse(ConfigSchema); + console.log(`Server: ${config.host}:${config.port}`); +} catch (error) { + if (error instanceof ParseError) { + console.error('Invalid JSON:', error.message); + } + // Schema validation errors are thrown as-is +} +``` + +### tryParse() - Safe Parsing + +Returns `null` on failure: + +```typescript +const config = result.tryParse(ConfigSchema); + +if (config) { + console.log('Config:', config); +} else { + console.log('Response was not valid config JSON'); +} +``` + +## Complex Schemas + +### Nested Objects + +```typescript +const ProjectSchema = z.object({ + name: z.string(), + version: z.string(), + dependencies: z.record(z.string()), + scripts: z.object({ + build: z.string().optional(), + test: z.string().optional() + }) +}); + +const result = await droid.exec('Analyze package.json and return as JSON'); +const project = result.parse(ProjectSchema); +``` + +### Arrays + +```typescript +const TodoSchema = z.object({ + id: z.number(), + title: z.string(), + completed: z.boolean() +}); + +const TodoListSchema = z.array(TodoSchema); + +const result = await droid.exec('Generate 5 todo items as JSON array'); +const todos = result.parse(TodoListSchema); +``` + +### Unions and Optionals + +```typescript +const ResponseSchema = z.object({ + status: z.enum(['success', 'error']), + data: z.union([ + z.object({ users: z.array(z.string()) }), + z.object({ error: z.string() }) + ]).optional() +}); +``` + +## JSON Schema Output + +For direct schema specification in prompts: + +```typescript +const result = await thread.run('Generate user data', { + outputSchema: { + type: 'object', + properties: { + id: { type: 'number' }, + name: { type: 'string' } + }, + required: ['id', 'name'] + } +}); +``` + +## Error Handling + +```typescript +import { ParseError } from '@activade/droid-sdk'; +import { z } from 'zod'; + +try { + const data = result.parse(MySchema); +} catch (error) { + if (error instanceof ParseError) { + // Response was not valid JSON + console.error('JSON parse failed:', error.message); + console.error('Raw response:', error.rawText); + } else if (error instanceof z.ZodError) { + // JSON was valid but didn't match schema + console.error('Validation failed:', error.issues); + } +} +``` + +## Prompting for JSON + +For best results, be explicit about JSON format: + +```typescript +// Good - explicit JSON request +const result = await droid.exec( + 'Analyze the codebase and return a JSON object with: ' + + '{ files: number, lines: number, languages: string[] }' +); + +// Better - with example +const result = await droid.exec(` + Analyze the codebase and return JSON like: + { + "files": 42, + "lines": 1234, + "languages": ["TypeScript", "JavaScript"] + } +`); +``` + +## Best Practices + +1. **Use tryParse for optional data** - When JSON might not be present +2. **Use parse for required data** - When you need guaranteed structure +3. **Define precise schemas** - Be specific about types and constraints +4. **Handle both error types** - ParseError and ZodError +5. **Prompt clearly** - Ask explicitly for JSON with structure hints diff --git a/docs/concepts/threads.mdx b/docs/concepts/threads.mdx new file mode 100644 index 0000000..1f266ef --- /dev/null +++ b/docs/concepts/threads.mdx @@ -0,0 +1,123 @@ +--- +title: Conversation Threads +description: Managing multi-turn AI conversations +--- + +# Conversation Threads + +Threads maintain context across multiple prompts, enabling natural multi-turn conversations with the AI. + +## Creating Threads + +Threads are created from a Droid instance: + +```typescript +import { Droid } from '@activade/droid-sdk'; + +const droid = new Droid(); +const thread = droid.startThread(); +``` + +## Thread Lifecycle + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Create │────▶│ Execute │────▶│ Resume │ +│ (new) │ │ (run/ │ │ (later) │ +│ │ │ stream) │ │ │ +└─────────────┘ └─────────────┘ └─────────────┘ + │ │ │ + │ ▼ │ + │ ┌─────────────┐ │ + │ │ Session │◀────────────┘ + └──────────▶│ ID │ + └─────────────┘ +``` + +## Multi-turn Conversations + +Each prompt in a thread builds on previous context: + +```typescript +const thread = droid.startThread(); + +// Turn 1: Create initial code +await thread.run('Create a UserService class with CRUD methods'); + +// Turn 2: AI remembers the UserService +await thread.run('Add validation to the create method'); + +// Turn 3: AI has full context +await thread.run('Write unit tests for the validation'); +``` + +## Session Persistence + +Threads can be resumed across process restarts: + +```typescript +// First session +const thread = droid.startThread(); +await thread.run('Start building a payment system'); +const sessionId = thread.id; +// Save sessionId to database/file + +// Later session +const resumedThread = droid.resumeThread(sessionId); +await resumedThread.run('Continue with refund handling'); +``` + +## Thread Options + +Override Droid configuration per-thread: + +```typescript +const thread = droid.startThread({ + autonomyLevel: 'high', // Override autonomy + enabledTools: ['Read', 'Write', 'Bash'], // Limit tools + cwd: '/specific/project' // Different working directory +}); +``` + +## Execution Methods + +### Synchronous Execution + +Wait for the complete response: + +```typescript +const result = await thread.run('Create a config file'); +console.log(result.finalResponse); +``` + +### Streaming Execution + +Process events in real-time: + +```typescript +const { events, result } = await thread.runStreamed('Build a complex feature'); + +for await (const event of events) { + if (event.type === 'tool_call') { + console.log(`Using: ${event.toolName}`); + } +} + +const finalResult = await result; +``` + +## Thread vs Exec + +| Feature | Thread | Exec | +|---------|--------|------| +| Context | Maintained | None | +| Session ID | Yes | Per-call | +| Resume | Yes | No | +| Use case | Multi-turn | One-shot | + +## Best Practices + +1. **Use threads for related tasks** - Keep context across related operations +2. **Save session IDs** - Enable resumption for long-running projects +3. **Use exec for isolated tasks** - When context isn't needed +4. **Stream for long operations** - Better UX with progress updates diff --git a/docs/guides/ci-cd.mdx b/docs/guides/ci-cd.mdx new file mode 100644 index 0000000..2ce515c --- /dev/null +++ b/docs/guides/ci-cd.mdx @@ -0,0 +1,223 @@ +--- +title: CI/CD Integration +description: Using the SDK in automated pipelines +--- + +# CI/CD Integration + +The Droid SDK is designed for integration into CI/CD pipelines, enabling AI-powered automation in your development workflow. + +## Setup + +### Installing in CI + +```yaml +# GitHub Actions +- name: Install Droid CLI + run: curl -fsSL https://app.factory.ai/cli | sh + +- name: Install dependencies + run: npm install +``` + +Or use the SDK's auto-installer: + +```typescript +import { ensureDroidCli } from '@activade/droid-sdk/cli'; + +// Automatically install if not present +await ensureDroidCli({ + onProgress: (p) => console.log(`[${p.phase}] ${p.message}`) +}); +``` + +### Environment Configuration + +```yaml +env: + # Set any required API keys + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} +``` + +## Common Use Cases + +### Automated Code Review + +```typescript +import { Droid, MODELS } from '@activade/droid-sdk'; +import { execSync } from 'child_process'; + +async function reviewPR() { + const droid = new Droid({ + model: MODELS.CLAUDE_SONNET, + autonomyLevel: 'low' // Read-only operations + }); + + // Get changed files + const diff = execSync('git diff origin/main').toString(); + + const result = await droid.exec(` + Review this code diff for: + - Bugs and potential issues + - Security vulnerabilities + - Code style violations + + Diff: + ${diff} + `); + + return result.finalResponse; +} +``` + +### Automated Documentation + +```typescript +async function generateDocs() { + const droid = new Droid({ autonomyLevel: 'high' }); + const thread = droid.startThread(); + + await thread.run('Analyze the src/ directory and update README.md'); + await thread.run('Generate API documentation for public exports'); + + // Commit changes + execSync('git add README.md docs/'); + execSync('git commit -m "docs: auto-update documentation"'); +} +``` + +### Test Generation + +```typescript +async function generateTests(changedFiles: string[]) { + const droid = new Droid({ autonomyLevel: 'high' }); + + for (const file of changedFiles) { + if (!file.endsWith('.test.ts')) { + await droid.exec(`Generate unit tests for ${file}`); + } + } +} +``` + +## GitHub Actions Example + +```yaml +name: AI Code Review + +on: + pull_request: + branches: [main] + +jobs: + review: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Droid CLI + run: curl -fsSL https://app.factory.ai/cli | sh + + - name: Install dependencies + run: npm install + + - name: Run AI Review + run: npx tsx scripts/ai-review.ts + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + + - name: Comment on PR + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const review = fs.readFileSync('review.md', 'utf8'); + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: review + }); +``` + +## Best Practices + +### 1. Use Appropriate Autonomy + +```typescript +// CI environments should typically use lower autonomy +const droid = new Droid({ + autonomyLevel: 'low', // Safer for automated runs + timeout: 300000 // 5 minute timeout +}); +``` + +### 2. Handle Errors Gracefully + +```typescript +async function ciTask() { + try { + const result = await droid.exec('Analyze codebase'); + return { success: true, output: result.finalResponse }; + } catch (error) { + console.error('AI task failed:', error); + // Don't fail the whole pipeline for AI errors + return { success: false, error: error.message }; + } +} +``` + +### 3. Set Timeouts + +```typescript +const droid = new Droid({ + timeout: 180000 // 3 minutes - reasonable for CI +}); +``` + +### 4. Cache CLI Installation + +```yaml +- name: Cache Droid CLI + uses: actions/cache@v4 + with: + path: ~/.local/bin/droid + key: droid-cli-${{ runner.os }} +``` + +### 5. Use Structured Output + +```typescript +import { z } from 'zod'; + +const ReviewSchema = z.object({ + issues: z.array(z.object({ + severity: z.enum(['error', 'warning', 'info']), + file: z.string(), + line: z.number().optional(), + message: z.string() + })), + summary: z.string() +}); + +const result = await droid.exec('Review code and return JSON'); +const review = result.tryParse(ReviewSchema); + +if (review && review.issues.some(i => i.severity === 'error')) { + process.exit(1); // Fail pipeline on errors +} +``` + +## Security Considerations + +1. **Store API keys as secrets** - Never commit credentials +2. **Use read-only mode** - Lower autonomy prevents unwanted changes +3. **Validate outputs** - Don't blindly trust AI-generated code +4. **Limit scope** - Restrict working directory and available tools +5. **Review before merge** - AI suggestions should be reviewed diff --git a/docs/guides/error-handling.mdx b/docs/guides/error-handling.mdx new file mode 100644 index 0000000..c753577 --- /dev/null +++ b/docs/guides/error-handling.mdx @@ -0,0 +1,245 @@ +--- +title: Error Handling +description: Gracefully handling SDK errors +--- + +# Error Handling + +The Droid SDK provides specific error classes for different failure scenarios, enabling precise error handling in your applications. + +## Error Hierarchy + +``` +DroidError (base) +├── CliNotFoundError +├── ExecutionError +├── ParseError +├── TimeoutError +└── StreamError +``` + +## Error Types + +### CliNotFoundError + +Thrown when the Droid CLI is not installed: + +```typescript +import { CliNotFoundError, ensureDroidCli } from '@activade/droid-sdk'; + +try { + const result = await droid.exec('Hello'); +} catch (error) { + if (error instanceof CliNotFoundError) { + console.log('CLI not found, installing...'); + await ensureDroidCli(); + } +} +``` + +### ExecutionError + +Thrown when CLI execution fails: + +```typescript +import { ExecutionError } from '@activade/droid-sdk'; + +try { + const result = await droid.exec('Perform risky operation'); +} catch (error) { + if (error instanceof ExecutionError) { + console.error('Execution failed:', error.message); + console.error('Exit code:', error.exitCode); + console.error('Stderr:', error.stderr); + } +} +``` + +### ParseError + +Thrown when JSON parsing fails: + +```typescript +import { ParseError } from '@activade/droid-sdk'; + +try { + const data = result.parse(MySchema); +} catch (error) { + if (error instanceof ParseError) { + console.error('Not valid JSON:', error.message); + console.error('Raw text:', error.rawText); + } +} +``` + +### TimeoutError + +Thrown when operations exceed the timeout: + +```typescript +import { TimeoutError } from '@activade/droid-sdk'; + +try { + const result = await droid.exec('Complex operation'); +} catch (error) { + if (error instanceof TimeoutError) { + console.error(`Operation timed out after ${error.timeoutMs}ms`); + } +} +``` + +### StreamError + +Thrown when stream processing fails: + +```typescript +import { StreamError } from '@activade/droid-sdk'; + +try { + const { events } = await thread.runStreamed('Build feature'); + for await (const event of events) { + // Process events + } +} catch (error) { + if (error instanceof StreamError) { + console.error('Stream error:', error.message); + } +} +``` + +## Comprehensive Error Handling + +```typescript +import { + Droid, + DroidError, + CliNotFoundError, + ExecutionError, + TimeoutError, + ParseError, + StreamError, + ensureDroidCli +} from '@activade/droid-sdk'; + +async function safeExecute(prompt: string) { + const droid = new Droid({ timeout: 60000 }); + + try { + return await droid.exec(prompt); + } catch (error) { + if (error instanceof CliNotFoundError) { + console.log('Installing CLI...'); + await ensureDroidCli(); + // Retry + return await droid.exec(prompt); + } + + if (error instanceof TimeoutError) { + console.error('Operation timed out. Try with higher timeout.'); + throw error; + } + + if (error instanceof ExecutionError) { + console.error('Execution failed:', error.message); + throw error; + } + + if (error instanceof DroidError) { + // Catch-all for other SDK errors + console.error('SDK error:', error.message); + throw error; + } + + // Unknown error + throw error; + } +} +``` + +## Retry Patterns + +### Simple Retry + +```typescript +async function withRetry( + fn: () => Promise, + maxRetries = 3 +): Promise { + let lastError: Error | undefined; + + for (let i = 0; i < maxRetries; i++) { + try { + return await fn(); + } catch (error) { + lastError = error as Error; + + // Don't retry certain errors + if (error instanceof CliNotFoundError) throw error; + if (error instanceof ParseError) throw error; + + // Wait before retry + await new Promise(r => setTimeout(r, 1000 * (i + 1))); + } + } + + throw lastError; +} + +// Usage +const result = await withRetry(() => droid.exec('Generate code')); +``` + +### Exponential Backoff + +```typescript +async function withExponentialBackoff( + fn: () => Promise, + options = { maxRetries: 3, baseDelay: 1000 } +): Promise { + for (let i = 0; i < options.maxRetries; i++) { + try { + return await fn(); + } catch (error) { + if (i === options.maxRetries - 1) throw error; + + const delay = options.baseDelay * Math.pow(2, i); + await new Promise(r => setTimeout(r, delay)); + } + } + throw new Error('Unreachable'); +} +``` + +## Streaming Error Handling + +```typescript +async function safeStream(thread: Thread, prompt: string) { + try { + const { events, result } = await thread.runStreamed(prompt); + + for await (const event of events) { + if (event.type === 'turn.failed') { + console.error('Turn failed:', event.error.message); + // Handle inline error + break; + } + // Process other events + } + + return await result; + } catch (error) { + if (error instanceof StreamError) { + console.error('Stream interrupted:', error.message); + } + throw error; + } +} +``` + +## Best Practices + +1. **Catch specific errors** - Handle each error type appropriately +2. **Use the error hierarchy** - Catch `DroidError` as fallback +3. **Implement retries** - For transient failures +4. **Log error details** - Use error properties for debugging +5. **Handle stream errors inline** - Check for `turn.failed` events diff --git a/docs/guides/file-attachments.mdx b/docs/guides/file-attachments.mdx new file mode 100644 index 0000000..9bf0a4b --- /dev/null +++ b/docs/guides/file-attachments.mdx @@ -0,0 +1,178 @@ +--- +title: File Attachments +description: Providing context through file attachments +--- + +# File Attachments + +File attachments allow you to provide additional context to the AI by including files with your prompts. + +## Attachment Types + +The SDK supports several attachment types: + +| Type | Description | Use Cases | +|------|-------------|-----------| +| `image` | Image files | Screenshots, diagrams, UI mockups | +| `text` | Text files | Source code, configs, logs | +| `data` | Data files | JSON, CSV, structured data | + +## Basic Usage + +```typescript +const result = await thread.run('Describe this image', { + attachments: [ + { path: './screenshot.png', type: 'image' } + ] +}); +``` + +## Image Attachments + +Attach images for visual analysis: + +```typescript +// Single image +const result = await thread.run('What does this UI show?', { + attachments: [ + { path: './mockup.png', type: 'image' } + ] +}); + +// Multiple images +const result = await thread.run('Compare these two designs', { + attachments: [ + { path: './design-v1.png', type: 'image' }, + { path: './design-v2.png', type: 'image' } + ] +}); +``` + +## Text Attachments + +Attach text files for code review or analysis: + +```typescript +// Code review +const result = await thread.run('Review this code for security issues', { + attachments: [ + { path: './src/auth.ts', type: 'text', description: 'Authentication module' } + ] +}); + +// Multiple files with context +const result = await thread.run('How do these modules interact?', { + attachments: [ + { path: './src/user.ts', type: 'text', description: 'User model' }, + { path: './src/auth.ts', type: 'text', description: 'Auth service' }, + { path: './src/api.ts', type: 'text', description: 'API routes' } + ] +}); +``` + +## Data Attachments + +Attach structured data files: + +```typescript +// JSON data +const result = await thread.run('Analyze this API response', { + attachments: [ + { path: './response.json', type: 'data' } + ] +}); + +// Configuration analysis +const result = await thread.run('Review our build configuration', { + attachments: [ + { path: './tsconfig.json', type: 'data' }, + { path: './package.json', type: 'data' } + ] +}); +``` + +## File Attachment Interface + +```typescript +interface FileAttachment { + /** Path to the file */ + path: string; + + /** Type of file content */ + type: 'image' | 'text' | 'data'; + + /** Optional description for context */ + description?: string; +} +``` + +## Practical Examples + +### Screenshot Analysis + +```typescript +async function analyzeScreenshot(screenshotPath: string) { + const droid = new Droid(); + const thread = droid.startThread(); + + const result = await thread.run( + 'Analyze this screenshot and identify any UI issues or improvements', + { + attachments: [ + { path: screenshotPath, type: 'image' } + ] + } + ); + + return result.finalResponse; +} +``` + +### Code Review Workflow + +```typescript +async function reviewChanges(files: string[]) { + const droid = new Droid(); + const thread = droid.startThread(); + + const attachments = files.map(file => ({ + path: file, + type: 'text' as const, + description: `File: ${file}` + })); + + const result = await thread.run( + 'Review these files for bugs, security issues, and code quality', + { attachments } + ); + + return result.finalResponse; +} +``` + +### Error Log Analysis + +```typescript +async function analyzeErrors(logPath: string) { + const droid = new Droid(); + + const result = await droid.exec( + 'Analyze this log file and summarize the errors', + { + attachments: [ + { path: logPath, type: 'text', description: 'Application error log' } + ] + } + ); + + return result.finalResponse; +} +``` + +## Best Practices + +1. **Use descriptions** - Help the AI understand file purpose +2. **Limit file size** - Large files may be truncated +3. **Choose correct type** - Affects how content is processed +4. **Group related files** - Attach files that work together +5. **Be specific in prompts** - Reference attachments in your prompt diff --git a/docs/guides/multi-turn.mdx b/docs/guides/multi-turn.mdx new file mode 100644 index 0000000..66004ab --- /dev/null +++ b/docs/guides/multi-turn.mdx @@ -0,0 +1,173 @@ +--- +title: Multi-turn Conversations +description: Building complex interactions across multiple prompts +--- + +# Multi-turn Conversations + +Multi-turn conversations allow you to build complex features incrementally, with the AI maintaining context from previous interactions. + +## Basic Multi-turn Flow + +```typescript +import { Droid, MODELS } from '@activade/droid-sdk'; + +const droid = new Droid({ model: MODELS.CLAUDE_SONNET }); +const thread = droid.startThread(); + +// Turn 1: Set up the project +await thread.run('Create a new Express.js REST API project structure'); + +// Turn 2: Add specific functionality (AI remembers the project) +await thread.run('Add a /users endpoint with CRUD operations'); + +// Turn 3: Continue building (AI has full context) +await thread.run('Add JWT authentication middleware'); + +// Turn 4: Finish up +await thread.run('Add unit tests for the authentication'); +``` + +## Saving and Resuming Sessions + +Sessions persist beyond process restarts: + +```typescript +// Initial session +async function startProject() { + const droid = new Droid(); + const thread = droid.startThread(); + + await thread.run('Initialize a React TypeScript project'); + await thread.run('Create a basic component structure'); + + // Save the session ID + return thread.id; +} + +// Resume later +async function continueProject(sessionId: string) { + const droid = new Droid(); + const thread = droid.resumeThread(sessionId); + + // AI remembers the React project + await thread.run('Add routing with React Router'); + await thread.run('Create a navigation component'); +} +``` + +## Building Features Incrementally + +### Step-by-step Feature Development + +```typescript +const thread = droid.startThread(); + +// Step 1: Design +const design = await thread.run( + 'Design a user authentication system. List the components needed.' +); +console.log('Design:', design.finalResponse); + +// Step 2: Data models +await thread.run('Create the User and Session data models'); + +// Step 3: Core logic +await thread.run('Implement the authentication service'); + +// Step 4: API layer +await thread.run('Create login and logout API endpoints'); + +// Step 5: Middleware +await thread.run('Add authentication middleware'); + +// Step 6: Tests +await thread.run('Write tests for the authentication flow'); +``` + +### Iterative Refinement + +```typescript +const thread = droid.startThread(); + +// Initial implementation +await thread.run('Create a function to validate email addresses'); + +// Refine based on requirements +await thread.run('Add support for custom TLDs'); + +// Handle edge cases +await thread.run('Handle emails with + signs and dots'); + +// Optimize +await thread.run('Optimize for performance with large lists'); +``` + +## Managing Long Sessions + +### Checking Session State + +```typescript +const thread = droid.startThread(); +await thread.run('Start working on feature X'); + +// Thread ID is available after first run +console.log('Session:', thread.id); + +// Check what's been done +const status = await thread.run('Summarize what we have done so far'); +console.log(status.finalResponse); +``` + +### Session Storage Patterns + +```typescript +// Store in a database +interface ProjectSession { + projectId: string; + sessionId: string; + createdAt: Date; + lastActivity: Date; +} + +async function saveSession(projectId: string, sessionId: string) { + await db.sessions.upsert({ + projectId, + sessionId, + lastActivity: new Date() + }); +} + +async function getSession(projectId: string): Promise { + const session = await db.sessions.findOne({ projectId }); + return session?.sessionId ?? null; +} +``` + +## Parallel Thread Execution + +Run multiple threads concurrently: + +```typescript +const droid = new Droid(); + +// Create multiple threads +const featureThread = droid.startThread(); +const testThread = droid.startThread(); +const docsThread = droid.startThread(); + +// Run in parallel +const [feature, tests, docs] = await Promise.all([ + featureThread.run('Implement user profile feature'), + testThread.run('Write test suite for API endpoints'), + docsThread.run('Generate API documentation') +]); +``` + +## Best Practices + +1. **Be specific** - Clear prompts lead to better context retention +2. **Reference previous work** - "Now add tests for the function we just created" +3. **Save session IDs** - For long-running projects +4. **Use summaries** - Ask for summaries to verify context +5. **Start fresh when needed** - New threads for unrelated work diff --git a/docs/installation.mdx b/docs/installation.mdx new file mode 100644 index 0000000..cbd93f3 --- /dev/null +++ b/docs/installation.mdx @@ -0,0 +1,165 @@ +--- +title: Installation +description: Install the Droid SDK and CLI in your project +--- + +# Installation + +## SDK Installation + +Install the Droid SDK using your preferred package manager: + + + + ```bash + npm install @activade/droid-sdk + ``` + + + ```bash + bun add @activade/droid-sdk + ``` + + + ```bash + yarn add @activade/droid-sdk + ``` + + + ```bash + pnpm add @activade/droid-sdk + ``` + + + +## CLI Installation + +The Droid SDK requires the Factory Droid CLI to be installed. You have two options: + +### Option 1: Manual Installation + +Install the CLI globally: + +```bash +curl -fsSL https://app.factory.ai/cli | sh +``` + +### Option 2: Automatic Installation + +Use the SDK's built-in installer: + +```typescript +import { ensureDroidCli } from '@activade/droid-sdk/cli'; + +// Install CLI if not already present +const cliPath = await ensureDroidCli({ + onProgress: (progress) => { + console.log(`[${progress.phase}] ${progress.message}`); + } +}); + +console.log('CLI installed at:', cliPath); +``` + +## Optional Dependencies + +### Zod (Recommended) + +For structured output validation, install Zod: + +```bash +npm install zod +``` + +Then use it with the SDK: + +```typescript +import { z } from 'zod'; + +const schema = z.object({ + name: z.string(), + version: z.string() +}); + +const result = await droid.exec('Get package info as JSON'); +const data = result.parse(schema); +``` + +## TypeScript Configuration + +The SDK is written in TypeScript and includes type definitions. Ensure your `tsconfig.json` includes: + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true + } +} +``` + +## Verification + +Verify your installation: + +```typescript +import { Droid, MODELS } from '@activade/droid-sdk'; + +const droid = new Droid({ model: MODELS.CLAUDE_SONNET }); + +// List available tools +const tools = await droid.listTools(); +console.log('Available tools:', tools); + +// Test execution +const result = await droid.exec('Say hello'); +console.log(result.finalResponse); +``` + +## Environment Variables + +The SDK respects these environment variables: + +| Variable | Description | +|----------|-------------| +| `PATH` | System PATH for finding the CLI | +| `HOME` | Home directory for CLI installation | + +## Troubleshooting + +### CLI Not Found + +If you get a `CliNotFoundError`: + +```typescript +import { CliNotFoundError, ensureDroidCli } from '@activade/droid-sdk'; + +try { + await droid.exec('Hello'); +} catch (error) { + if (error instanceof CliNotFoundError) { + console.log('Installing CLI...'); + await ensureDroidCli(); + } +} +``` + +### Permission Issues + +On Unix systems, ensure the CLI is executable: + +```bash +chmod +x ~/.local/bin/droid +``` + +### Timeout Errors + +Increase the timeout for long operations: + +```typescript +const droid = new Droid({ + timeout: 600000 // 10 minutes +}); +``` diff --git a/docs/introduction.mdx b/docs/introduction.mdx new file mode 100644 index 0000000..ffb36ba --- /dev/null +++ b/docs/introduction.mdx @@ -0,0 +1,82 @@ +--- +title: Introduction +description: TypeScript SDK for Factory Droid CLI - AI-powered code generation and automation +--- + +# Droid SDK + +The Droid SDK provides a programmatic TypeScript/JavaScript interface to the Factory Droid CLI, enabling AI-powered code generation and task automation in your applications. + +## What is Droid? + +Droid is Factory's AI development agent that can: + +- Generate and modify code based on natural language prompts +- Execute multi-turn conversations with context retention +- Use tools like file operations, shell commands, and web requests +- Support multiple AI models (Claude, GPT, Gemini, and more) + +## Why Use the SDK? + +The SDK wraps the Droid CLI to enable: + + + + Integrate AI code generation into CI/CD pipelines and automation workflows + + + Maintain context across multiple prompts with persistent sessions + + + Observe tool calls and intermediate results as they happen + + + Parse AI responses with Zod schemas for type-safe data + + + +## Quick Example + +```typescript +import { Droid, MODELS } from '@activade/droid-sdk'; + +const droid = new Droid({ + model: MODELS.CLAUDE_SONNET, + autonomyLevel: 'high' +}); + +// One-shot execution +const result = await droid.exec('Create a hello world function'); +console.log(result.finalResponse); + +// Multi-turn conversation +const thread = droid.startThread(); +await thread.run('Create a React component'); +await thread.run('Add unit tests for it'); +``` + +## Features + +- **Thread-based conversations**: Maintain context across multiple prompts +- **Session persistence**: Resume conversations across process restarts +- **Real-time streaming**: Observe tool calls as they happen +- **Structured output**: Validate responses with Zod schemas +- **Automatic CLI installation**: Install the CLI programmatically if needed +- **Multiple model support**: Claude, GPT, Gemini, and more + +## Next Steps + + + + Get up and running in minutes + + + Detailed installation instructions + + + Complete API documentation + + + Learn through practical examples + + diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx new file mode 100644 index 0000000..7ab0fbc --- /dev/null +++ b/docs/quickstart.mdx @@ -0,0 +1,154 @@ +--- +title: Quick Start +description: Get started with the Droid SDK in under 5 minutes +--- + +# Quick Start + +This guide will get you up and running with the Droid SDK in just a few minutes. + +## Prerequisites + +- Node.js 18+ or Bun 1.0+ +- Factory Droid CLI installed (or let the SDK install it for you) + +## Installation + + + + ```bash + npm install @activade/droid-sdk + ``` + + + ```bash + bun add @activade/droid-sdk + ``` + + + ```bash + yarn add @activade/droid-sdk + ``` + + + ```bash + pnpm add @activade/droid-sdk + ``` + + + +## Basic Usage + +### 1. Create a Droid Instance + +```typescript +import { Droid, MODELS } from '@activade/droid-sdk'; + +const droid = new Droid({ + model: MODELS.CLAUDE_SONNET, + cwd: process.cwd() +}); +``` + +### 2. Execute a Prompt + +```typescript +// One-shot execution +const result = await droid.exec('Create a TypeScript function that validates email addresses'); + +console.log(result.finalResponse); +console.log(`Completed in ${result.durationMs}ms`); +``` + +### 3. Multi-turn Conversation + +```typescript +// Start a conversation thread +const thread = droid.startThread(); + +// First prompt - establish context +await thread.run('Create a REST API for a todo list'); + +// Follow-up - the AI remembers the previous context +await thread.run('Add authentication using JWT'); + +// Another follow-up +await thread.run('Write tests for the authentication'); + +// Save session for later +console.log('Session ID:', thread.id); +``` + +### 4. Resume a Conversation + +```typescript +// Resume a previous conversation +const thread = droid.resumeThread('session_abc123...'); + +await thread.run('What did we work on last time?'); +``` + +## Streaming Example + +For real-time progress updates: + +```typescript +const thread = droid.startThread(); +const { events, result } = await thread.runStreamed('Build a React component'); + +// Process events as they arrive +for await (const event of events) { + switch (event.type) { + case 'tool_call': + console.log(`Calling: ${event.toolName}`); + break; + case 'tool_result': + console.log(`Result: ${event.isError ? 'ERROR' : 'OK'}`); + break; + case 'message': + console.log(`[${event.role}] ${event.text.slice(0, 100)}...`); + break; + } +} + +// Get final result +const finalResult = await result; +console.log('Done:', finalResult.finalResponse); +``` + +## Structured Output + +Parse AI responses with Zod schemas: + +```typescript +import { z } from 'zod'; + +const TaskSchema = z.object({ + title: z.string(), + priority: z.enum(['low', 'medium', 'high']), + completed: z.boolean() +}); + +const result = await thread.run('Create a task object as JSON'); +const task = result.parse(TaskSchema); + +console.log(task.title); // TypeScript knows this is a string +console.log(task.priority); // TypeScript knows this is 'low' | 'medium' | 'high' +``` + +## Next Steps + + + + Learn about Droid, Threads, and TurnResults + + + Master real-time event handling + + + Handle errors gracefully + + + Complete API documentation + + diff --git a/mint.json b/mint.json new file mode 100644 index 0000000..79fb54d --- /dev/null +++ b/mint.json @@ -0,0 +1,125 @@ +{ + "$schema": "https://mintlify.com/schema.json", + "name": "Droid SDK", + "logo": { + "dark": "/logo/dark.svg", + "light": "/logo/light.svg" + }, + "favicon": "/favicon.svg", + "colors": { + "primary": "#6366F1", + "light": "#818CF8", + "dark": "#4F46E5", + "anchors": { + "from": "#6366F1", + "to": "#8B5CF6" + } + }, + "topbarLinks": [ + { + "name": "GitHub", + "url": "https://github.com/activadee/droid-sdk" + } + ], + "topbarCtaButton": { + "name": "Get Started", + "url": "/quickstart" + }, + "tabs": [ + { + "name": "API Reference", + "url": "api-reference" + } + ], + "anchors": [ + { + "name": "GitHub", + "icon": "github", + "url": "https://github.com/activadee/droid-sdk" + }, + { + "name": "NPM", + "icon": "npm", + "url": "https://www.npmjs.com/package/@activade/droid-sdk" + } + ], + "navigation": [ + { + "group": "Getting Started", + "pages": [ + "introduction", + "quickstart", + "installation" + ] + }, + { + "group": "Core Concepts", + "pages": [ + "concepts/droid", + "concepts/threads", + "concepts/streaming", + "concepts/structured-output" + ] + }, + { + "group": "Guides", + "pages": [ + "guides/multi-turn", + "guides/file-attachments", + "guides/error-handling", + "guides/ci-cd" + ] + }, + { + "group": "API Reference", + "pages": [ + "api-reference/overview", + { + "group": "Classes", + "pages": [ + "api-reference/classes/droid", + "api-reference/classes/thread", + "api-reference/classes/turn-result" + ] + }, + { + "group": "Errors", + "pages": [ + "api-reference/errors/droid-error", + "api-reference/errors/cli-not-found-error", + "api-reference/errors/execution-error", + "api-reference/errors/parse-error", + "api-reference/errors/timeout-error", + "api-reference/errors/stream-error" + ] + }, + { + "group": "Types", + "pages": [ + "api-reference/types/config", + "api-reference/types/options", + "api-reference/types/events", + "api-reference/types/turn-items" + ] + }, + { + "group": "CLI", + "pages": [ + "api-reference/cli/installer", + "api-reference/cli/process" + ] + }, + { + "group": "Models", + "pages": [ + "api-reference/models/overview", + "api-reference/models/model-info" + ] + } + ] + } + ], + "footerSocials": { + "github": "https://github.com/activadee/droid-sdk" + } +} diff --git a/package.json b/package.json index 02fb32d..3348d21 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "lint:fix": "bunx biome check --write .", "format": "bunx biome format --write .", "clean": "rm -rf dist", - "prepublishOnly": "bun run clean && bun run lint && bun run test && bun run build" + "prepublishOnly": "bun run clean && bun run lint && bun run test && bun run build", + "docs:dev": "bunx mintlify dev --port 3333" }, "keywords": [ "factory", diff --git a/src/cli/installer.ts b/src/cli/installer.ts index 91291a9..6594d9b 100644 --- a/src/cli/installer.ts +++ b/src/cli/installer.ts @@ -5,26 +5,88 @@ import { DroidError } from '../errors'; import { findDroidPath } from './process'; import { streamToString, waitForExit } from './utils'; +/** + * Options for installing the Droid CLI. + * + * @category CLI + */ export interface InstallOptions { + /** + * Directory to install the CLI binary. + * @default ~/.droid-sdk/bin + */ installDir?: string; + + /** + * Force reinstallation even if CLI is already present. + * @default false + */ force?: boolean; + + /** + * Specific version to install. + * If not specified, installs the latest version. + */ version?: string; + + /** + * Callback for installation progress updates. + */ onProgress?: (progress: InstallProgress) => void; } +/** + * Installation progress update. + * + * @category CLI + */ export interface InstallProgress { + /** + * Current phase of the installation process. + */ phase: 'checking' | 'downloading' | 'installing' | 'verifying' | 'complete'; + + /** + * Percentage complete (0-100), if available. + */ percent?: number; + + /** + * Human-readable status message. + */ message?: string; } const INSTALL_SCRIPT_URL = 'https://app.factory.ai/cli'; +/** + * Gets the default installation directory. + * + * @returns Path to the default install directory + * + * @internal + */ function getDefaultInstallDir(): string { const home = process.env.HOME ?? process.env.USERPROFILE ?? ''; return join(home, '.droid-sdk', 'bin'); } +/** + * Checks if the Droid CLI is installed and accessible. + * + * @returns True if the CLI is found, false otherwise + * + * @example + * ```typescript + * if (await isDroidCliInstalled()) { + * console.log('Droid CLI is ready'); + * } else { + * console.log('Please install the Droid CLI'); + * } + * ``` + * + * @category CLI + */ export async function isDroidCliInstalled(): Promise { try { await findDroidPath(); @@ -34,6 +96,21 @@ export async function isDroidCliInstalled(): Promise { } } +/** + * Gets the path to the Droid CLI if installed. + * + * @returns Path to the CLI binary, or null if not installed + * + * @example + * ```typescript + * const path = await getDroidCliPath(); + * if (path) { + * console.log('CLI located at:', path); + * } + * ``` + * + * @category CLI + */ export async function getDroidCliPath(): Promise { try { return await findDroidPath(); @@ -42,6 +119,44 @@ export async function getDroidCliPath(): Promise { } } +/** + * Ensures the Droid CLI is installed, installing it if necessary. + * + * This is the recommended way to guarantee CLI availability before + * using the SDK. It handles: + * - Checking for existing installations + * - Downloading and installing if needed + * - Platform-specific installation (Unix/Windows) + * - Progress reporting + * + * @param options - Installation options + * @returns Path to the installed CLI binary + * + * @throws {DroidError} If installation fails + * + * @example + * ```typescript + * import { ensureDroidCli } from '@activade/droid-sdk/cli'; + * + * // Simple usage + * const cliPath = await ensureDroidCli(); + * + * // With progress reporting + * const cliPath = await ensureDroidCli({ + * onProgress: (progress) => { + * console.log(`[${progress.phase}] ${progress.message}`); + * if (progress.percent) { + * console.log(`Progress: ${progress.percent}%`); + * } + * } + * }); + * + * // Force reinstall + * const cliPath = await ensureDroidCli({ force: true }); + * ``` + * + * @category CLI + */ export async function ensureDroidCli(options: InstallOptions = {}): Promise { const { force = false, onProgress } = options; @@ -69,6 +184,15 @@ export async function ensureDroidCli(options: InstallOptions = {}): Promise { const { onProgress } = options; @@ -115,6 +239,15 @@ async function installUnix(installDir: string, options: InstallOptions): Promise return droidPath; } +/** + * Installs the Droid CLI on Windows. + * + * @param _installDir - Directory to install the binary (may be ignored by Windows installer) + * @param options - Installation options + * @returns Path to the installed binary + * + * @internal + */ async function installWindows(_installDir: string, options: InstallOptions): Promise { const { onProgress } = options; diff --git a/src/cli/process.ts b/src/cli/process.ts index c4158f1..b3ff41a 100644 --- a/src/cli/process.ts +++ b/src/cli/process.ts @@ -13,32 +13,88 @@ import type { import { parseJsonLines } from './stream-parser'; import { nodeStreamToWebStream, streamToString, waitForExit } from './utils'; +/** + * Options for spawning a Droid CLI process. + * + * Used internally to configure CLI invocations for execution. + * + * @category CLI + */ export interface SpawnOptions { + /** The prompt to execute */ prompt?: string; + /** Path to a file containing the prompt */ promptFile?: string; + /** Session ID to resume a conversation */ sessionId?: string; + /** Working directory for the CLI process */ cwd?: string; + /** Path to the Droid CLI binary */ droidPath?: string; + /** Timeout in milliseconds */ timeout?: number; + /** Output format for CLI response */ outputFormat?: OutputFormat; + /** Thread-level options */ threadOptions?: ThreadOptions; + /** Run-level options */ runOptions?: RunOptions; + /** File attachments to include */ attachments?: FileAttachment[]; } +/** + * Result from a synchronous Droid CLI execution. + * + * @category CLI + */ export interface DroidProcessResult { + /** Standard output from the process */ stdout: string; + /** Standard error from the process */ stderr: string; + /** Process exit code */ exitCode: number; } +/** + * A streaming Droid CLI process. + * + * Provides access to real-time events as well as process control methods. + * + * @category CLI + */ export interface StreamingDroidProcess { + /** Async iterable of stream events */ events: AsyncIterable; + /** The underlying Node.js child process */ process: ChildProcess; + /** Wait for the process to exit and return the exit code */ waitForExit: () => Promise; + /** Kill the process */ kill: () => void; } +/** + * Builds a prompt string with file attachments. + * + * Prepends file references using the `@path` syntax before the main prompt. + * + * @param prompt - The main prompt text + * @param attachments - File attachments to include + * @returns Combined prompt string, or undefined if both are empty + * + * @example + * ```typescript + * const prompt = buildPromptWithAttachments( + * 'Analyze this code', + * [{ path: './src/main.ts', type: 'text' }] + * ); + * // Returns: "@./src/main.ts\n\nAnalyze this code" + * ``` + * + * @category CLI + */ export function buildPromptWithAttachments( prompt: string | undefined, attachments: FileAttachment[] | undefined, @@ -68,6 +124,14 @@ export function buildPromptWithAttachments( return `${attachmentBlock}\n\n${prompt}`; } +/** + * Builds CLI arguments from spawn options. + * + * @param options - The spawn options to convert + * @returns Array of CLI arguments + * + * @internal + */ function buildArgs(options: SpawnOptions): string[] { const args: string[] = ['exec']; @@ -133,6 +197,30 @@ function buildArgs(options: SpawnOptions): string[] { return args; } +/** + * Finds the Droid CLI binary path. + * + * Searches in the following order: + * 1. Preferred path (if provided) + * 2. System PATH directories + * 3. Common installation locations (~/.local/bin, ~/.droid-sdk/bin, etc.) + * + * @param preferredPath - Optional preferred path to check first + * @returns The path to the Droid CLI binary + * + * @throws {CliNotFoundError} If the CLI cannot be found in any location + * + * @example + * ```typescript + * // Find CLI in default locations + * const path = await findDroidPath(); + * + * // Check specific path first + * const path = await findDroidPath('/custom/path/droid'); + * ``` + * + * @category CLI + */ export async function findDroidPath(preferredPath?: string): Promise { const searchPaths: string[] = []; @@ -170,6 +258,35 @@ export async function findDroidPath(preferredPath?: string): Promise { throw new CliNotFoundError(searchPaths); } +/** + * Spawns a Droid CLI process and waits for completion. + * + * Executes the CLI synchronously and returns the complete output. + * For real-time streaming, use {@link spawnDroidStreaming} instead. + * + * @param options - Spawn configuration options + * @returns Process result with stdout, stderr, and exit code + * + * @throws {CliNotFoundError} If the CLI cannot be found + * @throws {TimeoutError} If the operation exceeds the timeout + * + * @example + * ```typescript + * const result = await spawnDroid({ + * prompt: 'Generate hello world', + * outputFormat: 'json', + * timeout: 60000 + * }); + * + * if (result.exitCode === 0) { + * console.log(result.stdout); + * } else { + * console.error(result.stderr); + * } + * ``` + * + * @category CLI + */ export async function spawnDroid(options: SpawnOptions): Promise { const droidPath = await findDroidPath(options.droidPath); const args = buildArgs(options); @@ -207,6 +324,34 @@ export async function spawnDroid(options: SpawnOptions): Promise { const droidPath = await findDroidPath(options.droidPath); const args = buildArgs({ ...options, outputFormat: 'stream-json' }); @@ -227,6 +372,32 @@ export async function spawnDroidStreaming(options: SpawnOptions): Promise { const result = await spawnDroid({ ...options, outputFormat: 'json' }); @@ -249,6 +420,26 @@ export async function execDroidJson(options: SpawnOptions): Promise } } +/** + * Lists available tools for a given model. + * + * Queries the CLI for the list of tools available to the AI during execution. + * + * @param droidPath - Optional path to the CLI binary + * @param model - Optional model to query tools for + * @returns Array of tool names, or empty array on failure + * + * @example + * ```typescript + * const tools = await listDroidTools(); + * console.log('Available tools:', tools); + * // ['file_read', 'file_write', 'shell_exec', ...] + * + * const gptTools = await listDroidTools(undefined, 'gpt-5.1'); + * ``` + * + * @category CLI + */ export async function listDroidTools(droidPath?: string, model?: string): Promise { const args = ['exec', '--list-tools', '-o', 'json']; if (model) { diff --git a/src/cli/stream-parser.ts b/src/cli/stream-parser.ts index 9dbef28..f2a9bf7 100644 --- a/src/cli/stream-parser.ts +++ b/src/cli/stream-parser.ts @@ -1,6 +1,28 @@ import { ParseError, StreamError } from '../errors'; import type { StreamEvent } from '../types'; +/** + * Parses a stream of newline-delimited JSON into stream events. + * + * This async generator reads from a byte stream, buffers partial lines, + * and yields parsed {@link StreamEvent} objects as they become available. + * + * @param stream - A readable stream of bytes (from CLI stdout) + * @yields Parsed stream events in order + * + * @throws {StreamError} If reading from the stream fails + * @throws {ParseError} If a line cannot be parsed as JSON + * + * @example + * ```typescript + * const stream = process.stdout; // Assume Web ReadableStream + * for await (const event of parseJsonLines(stream)) { + * console.log(event.type, event); + * } + * ``` + * + * @category CLI + */ export async function* parseJsonLines( stream: ReadableStream, ): AsyncGenerator { @@ -40,6 +62,16 @@ export async function* parseJsonLines( } } +/** + * Parses a single JSON line into a stream event. + * + * @param line - A single line of JSON text + * @returns Parsed stream event + * + * @throws {ParseError} If the line is not valid JSON + * + * @internal + */ function parseJsonLine(line: string): StreamEvent { try { return JSON.parse(line) as StreamEvent; @@ -52,12 +84,42 @@ function parseJsonLine(line: string): StreamEvent { } } +/** + * Collects summary information from an array of stream events. + * + * Extracts the session ID, final response, timing, and error status + * from a collection of events. Useful for aggregating streaming results. + * + * @param events - Array of stream events to process + * @returns Collected summary with session info, final text, and status + * + * @example + * ```typescript + * const events: StreamEvent[] = []; + * for await (const event of stream) { + * events.push(event); + * } + * + * const summary = collectStreamEvents(events); + * console.log(`Session: ${summary.sessionId}`); + * console.log(`Duration: ${summary.durationMs}ms`); + * console.log(`Result: ${summary.finalText}`); + * ``` + * + * @category CLI + */ export function collectStreamEvents(events: StreamEvent[]): { + /** Session ID from the events */ sessionId: string | undefined; + /** Final response text */ finalText: string | undefined; + /** Total execution duration in milliseconds */ durationMs: number; + /** Number of conversation turns */ numTurns: number; + /** Whether an error occurred */ isError: boolean; + /** Error message if isError is true */ errorMessage?: string; } { let sessionId: string | undefined; diff --git a/src/cli/utils.ts b/src/cli/utils.ts index 7e45efe..3f27df4 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -2,7 +2,22 @@ import type { ChildProcess } from 'node:child_process'; import type { Readable } from 'node:stream'; /** - * Convert a Node.js Readable stream to a string. + * Converts a Node.js Readable stream to a string. + * + * Collects all chunks from the stream and concatenates them into + * a UTF-8 string. + * + * @param stream - A Node.js Readable stream, or null + * @returns Promise resolving to the complete stream content as a string + * + * @example + * ```typescript + * const proc = spawn('echo', ['Hello']); + * const output = await streamToString(proc.stdout); + * console.log(output); // 'Hello\n' + * ``` + * + * @category CLI */ export function streamToString(stream: Readable | null): Promise { if (!stream) return Promise.resolve(''); @@ -15,8 +30,25 @@ export function streamToString(stream: Readable | null): Promise { } /** - * Wait for a child process to exit and return the exit code. - * Handles the race condition where process may exit before listener is registered. + * Waits for a child process to exit and returns the exit code. + * + * Handles the race condition where a process may exit before the + * listener is registered by checking exitCode after attaching the + * close handler. + * + * @param proc - The child process to wait for + * @returns Promise resolving to the exit code (0 if null) + * + * @example + * ```typescript + * const proc = spawn('npm', ['test']); + * const exitCode = await waitForExit(proc); + * if (exitCode !== 0) { + * console.error('Tests failed'); + * } + * ``` + * + * @category CLI */ export function waitForExit(proc: ChildProcess): Promise { return new Promise((resolve) => { @@ -39,7 +71,28 @@ export function waitForExit(proc: ChildProcess): Promise { } /** - * Convert a Node.js Readable stream to a Web ReadableStream. + * Converts a Node.js Readable stream to a Web ReadableStream. + * + * Creates a WHATWG-compatible ReadableStream that wraps a Node.js + * Readable stream, enabling use with web APIs. + * + * @param nodeStream - A Node.js Readable stream + * @returns A Web ReadableStream of Uint8Array chunks + * + * @example + * ```typescript + * const proc = spawn('cat', ['file.txt']); + * const webStream = nodeStreamToWebStream(proc.stdout!); + * + * const reader = webStream.getReader(); + * while (true) { + * const { done, value } = await reader.read(); + * if (done) break; + * console.log(new TextDecoder().decode(value)); + * } + * ``` + * + * @category CLI */ export function nodeStreamToWebStream(nodeStream: Readable): ReadableStream { return new ReadableStream({ diff --git a/src/droid.ts b/src/droid.ts index 09c1733..384e93f 100644 --- a/src/droid.ts +++ b/src/droid.ts @@ -4,9 +4,68 @@ import { buildTurnResultFromJson, type TurnResult } from './turn'; import type { DroidConfig, ExecOptions, ThreadOptions } from './types'; import { DEFAULT_DROID_PATH, DEFAULT_TIMEOUT } from './types'; +/** + * Main entry point for the Droid SDK. + * + * The `Droid` class provides a high-level interface to interact with the Factory Droid CLI, + * enabling AI-powered code generation and task automation. It supports both stateless + * one-shot execution and stateful multi-turn conversations through threads. + * + * @example + * ```typescript + * import { Droid, MODELS } from '@activade/droid-sdk'; + * + * // Create a new Droid instance with configuration + * const droid = new Droid({ + * model: MODELS.CLAUDE_SONNET, + * autonomyLevel: 'high', + * cwd: './my-project' + * }); + * + * // One-shot execution + * const result = await droid.exec('Create a hello world function'); + * console.log(result.finalResponse); + * + * // Multi-turn conversation + * const thread = droid.startThread(); + * await thread.run('Create a React component'); + * await thread.run('Add unit tests for it'); + * ``` + * + * @see {@link Thread} for multi-turn conversation management + * @see {@link TurnResult} for response handling + * + * @category Core + */ export class Droid { private readonly _config: DroidConfig; + /** + * Creates a new Droid instance with the specified configuration. + * + * @param config - Configuration options for the Droid instance + * @param config.cwd - Working directory for CLI operations (defaults to process.cwd()) + * @param config.model - AI model to use for generation (e.g., 'claude-sonnet-4-5-20250929') + * @param config.autonomyLevel - Level of autonomous decision-making ('low', 'medium', 'high') + * @param config.reasoningEffort - Reasoning intensity for complex tasks ('off', 'low', 'medium', 'high') + * @param config.droidPath - Custom path to the Droid CLI binary + * @param config.timeout - Maximum execution time in milliseconds (default: 600000) + * + * @example + * ```typescript + * // Basic usage with defaults + * const droid = new Droid(); + * + * // With full configuration + * const droid = new Droid({ + * model: 'claude-opus-4-5-20251101', + * autonomyLevel: 'high', + * reasoningEffort: 'medium', + * cwd: '/path/to/project', + * timeout: 300000 + * }); + * ``` + */ constructor(config: DroidConfig = {}) { this._config = { cwd: config.cwd ?? process.cwd(), @@ -18,10 +77,53 @@ export class Droid { }; } + /** + * Returns the current configuration as a readonly object. + * + * @returns The current Droid configuration + * + * @example + * ```typescript + * const droid = new Droid({ model: 'claude-sonnet-4-5-20250929' }); + * console.log(droid.config.model); // 'claude-sonnet-4-5-20250929' + * ``` + */ get config(): Readonly { return this._config; } + /** + * Creates a new conversation thread for multi-turn interactions. + * + * Threads maintain context across multiple prompts, allowing for iterative + * development and refinement of AI responses. Each thread has a unique + * session ID that persists the conversation state. + * + * @param options - Optional thread-specific configuration that overrides instance defaults + * @returns A new Thread instance ready for interaction + * + * @example + * ```typescript + * const droid = new Droid({ model: MODELS.CLAUDE_SONNET }); + * + * // Start a new thread + * const thread = droid.startThread({ + * autonomyLevel: 'high', + * enabledTools: ['file_read', 'file_write'] + * }); + * + * // Use the thread for multi-turn conversation + * await thread.run('Create a REST API'); + * await thread.run('Add authentication'); + * await thread.run('Write tests'); + * + * // Access the session ID for later resumption + * console.log('Session ID:', thread.id); + * ``` + * + * @see {@link Thread.run} for executing prompts + * @see {@link resumeThread} for continuing a previous conversation + */ startThread(options: ThreadOptions = {}): Thread { const mergedOptions: ThreadOptions = { model: this._config.model, @@ -33,6 +135,36 @@ export class Droid { return new Thread(this._config, mergedOptions); } + /** + * Resumes a previously created conversation thread using its session ID. + * + * This allows continuing a conversation across process restarts or + * different execution contexts. The session state is persisted by the + * Droid CLI and can be resumed at any time. + * + * @param sessionId - The unique session identifier from a previous thread + * @param options - Optional thread configuration overrides + * @returns A Thread instance connected to the existing session + * + * @example + * ```typescript + * // Save the session ID from a previous thread + * const previousSessionId = 'session_abc123...'; + * + * // Resume the conversation later + * const droid = new Droid(); + * const thread = droid.resumeThread(previousSessionId); + * + * // Continue where you left off + * const result = await thread.run('What did we work on last time?'); + * console.log(result.finalResponse); + * ``` + * + * @throws {ExecutionError} If the session ID is invalid or expired + * + * @see {@link startThread} for creating new threads + * @see {@link Thread.id} for accessing session IDs + */ resumeThread(sessionId: string, options: ThreadOptions = {}): Thread { const mergedOptions: ThreadOptions = { model: this._config.model, @@ -44,6 +176,45 @@ export class Droid { return new Thread(this._config, mergedOptions, sessionId); } + /** + * Executes a single prompt without maintaining conversation state. + * + * This is ideal for one-off tasks that don't require context from + * previous interactions. For multi-turn conversations, use + * {@link startThread} instead. + * + * @param prompt - The natural language prompt to execute + * @param options - Execution options including model, output schema, etc. + * @returns A promise resolving to the execution result + * + * @example + * ```typescript + * const droid = new Droid({ model: MODELS.CLAUDE_SONNET }); + * + * // Simple execution + * const result = await droid.exec('Generate a UUID'); + * console.log(result.finalResponse); + * + * // With structured output using Zod schema + * import { z } from 'zod'; + * + * const schema = z.object({ + * name: z.string(), + * version: z.string() + * }); + * + * const result = await droid.exec('Get package info as JSON', { + * outputSchema: { type: 'object', properties: { name: {}, version: {} } } + * }); + * const data = result.parse(schema); + * ``` + * + * @throws {ExecutionError} If the CLI execution fails + * @throws {TimeoutError} If the operation exceeds the configured timeout + * @throws {CliNotFoundError} If the Droid CLI is not installed + * + * @see {@link TurnResult} for parsing and accessing response data + */ async exec(prompt: string, options: ExecOptions = {}): Promise { const mergedOptions: ExecOptions = { model: this._config.model, @@ -65,6 +236,28 @@ export class Droid { return buildTurnResultFromJson(jsonResult); } + /** + * Lists all available tools for the configured or specified model. + * + * Tools represent capabilities that the AI can use during execution, + * such as file operations, shell commands, or web requests. + * + * @param model - Optional model ID to query tools for (defaults to instance model) + * @returns A promise resolving to an array of tool names + * + * @example + * ```typescript + * const droid = new Droid({ model: MODELS.CLAUDE_SONNET }); + * + * // List tools for the configured model + * const tools = await droid.listTools(); + * console.log('Available tools:', tools); + * // ['file_read', 'file_write', 'shell_exec', ...] + * + * // List tools for a specific model + * const gptTools = await droid.listTools('gpt-5.1'); + * ``` + */ async listTools(model?: string): Promise { return listDroidTools(this._config.droidPath, model ?? this._config.model); } diff --git a/src/errors.ts b/src/errors.ts index f65a9b1..8cdf280 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,4 +1,36 @@ +/** + * Base error class for all Droid SDK errors. + * + * All SDK-specific errors extend this class, making it easy to catch + * and handle Droid-related errors separately from other exceptions. + * + * @example + * ```typescript + * import { Droid, DroidError, CliNotFoundError } from '@activade/droid-sdk'; + * + * try { + * const droid = new Droid(); + * await droid.exec('Generate code'); + * } catch (error) { + * if (error instanceof CliNotFoundError) { + * console.log('Please install the Droid CLI first'); + * } else if (error instanceof DroidError) { + * console.log('Droid error:', error.message); + * } else { + * throw error; // Re-throw non-Droid errors + * } + * } + * ``` + * + * @category Errors + */ export class DroidError extends Error { + /** + * Creates a new DroidError instance. + * + * @param message - Human-readable error description + * @param options - Optional error options including cause + */ constructor(message: string, options?: { cause?: Error }) { super(message, options); this.name = 'DroidError'; @@ -6,9 +38,43 @@ export class DroidError extends Error { } } +/** + * Thrown when the Droid CLI binary cannot be found. + * + * This error indicates that the Droid CLI is not installed or not + * in the system PATH. The {@link searchedPaths} property contains + * all locations that were checked. + * + * @example + * ```typescript + * import { Droid, CliNotFoundError } from '@activade/droid-sdk'; + * import { ensureDroidCli } from '@activade/droid-sdk/cli'; + * + * try { + * const droid = new Droid(); + * await droid.exec('Hello'); + * } catch (error) { + * if (error instanceof CliNotFoundError) { + * console.log('Searched paths:', error.searchedPaths); + * // Auto-install the CLI + * await ensureDroidCli(); + * } + * } + * ``` + * + * @category Errors + */ export class CliNotFoundError extends DroidError { + /** + * List of filesystem paths that were searched for the CLI binary. + */ readonly searchedPaths: string[]; + /** + * Creates a new CliNotFoundError instance. + * + * @param searchedPaths - Array of paths that were checked for the CLI + */ constructor(searchedPaths: string[]) { super( `Droid CLI not found. Searched: ${searchedPaths.join(', ')}. Install with: curl -fsSL https://app.factory.ai/cli | sh`, @@ -18,11 +84,55 @@ export class CliNotFoundError extends DroidError { } } +/** + * Thrown when the Droid CLI process fails to execute. + * + * This error provides details about the failure including the exit code, + * stderr output, and optionally the session ID if one was established. + * + * @example + * ```typescript + * import { Droid, ExecutionError } from '@activade/droid-sdk'; + * + * try { + * await droid.exec('Invalid command'); + * } catch (error) { + * if (error instanceof ExecutionError) { + * console.log('Exit code:', error.exitCode); + * console.log('Stderr:', error.stderr); + * if (error.sessionId) { + * console.log('Session:', error.sessionId); + * } + * } + * } + * ``` + * + * @category Errors + */ export class ExecutionError extends DroidError { + /** + * The process exit code (non-zero indicates failure). + */ readonly exitCode: number; + + /** + * Standard error output from the CLI process. + */ readonly stderr: string; + + /** + * The session ID if one was established before the error occurred. + */ readonly sessionId?: string; + /** + * Creates a new ExecutionError instance. + * + * @param message - Human-readable error description + * @param exitCode - The process exit code + * @param stderr - Standard error output + * @param sessionId - Optional session ID from the execution + */ constructor(message: string, exitCode: number, stderr: string, sessionId?: string) { super(message); this.name = 'ExecutionError'; @@ -32,9 +142,45 @@ export class ExecutionError extends DroidError { } } +/** + * Thrown when parsing JSON or structured output fails. + * + * This error includes the raw content that failed to parse, useful + * for debugging malformed responses. + * + * @example + * ```typescript + * import { Droid, ParseError } from '@activade/droid-sdk'; + * import { z } from 'zod'; + * + * const schema = z.object({ value: z.number() }); + * + * try { + * const result = await droid.exec('Generate JSON'); + * result.parse(schema); + * } catch (error) { + * if (error instanceof ParseError) { + * console.log('Failed to parse:', error.raw.slice(0, 100)); + * console.log('Cause:', error.cause); + * } + * } + * ``` + * + * @category Errors + */ export class ParseError extends DroidError { + /** + * The raw content that failed to parse. + */ readonly raw: string; + /** + * Creates a new ParseError instance. + * + * @param message - Human-readable error description + * @param raw - The content that failed to parse + * @param cause - Optional underlying error that caused the parse failure + */ constructor(message: string, raw: string, cause?: Error) { super(message, { cause }); this.name = 'ParseError'; @@ -42,9 +188,39 @@ export class ParseError extends DroidError { } } +/** + * Thrown when an operation exceeds the configured timeout. + * + * The {@link timeoutMs} property indicates the timeout value that was exceeded. + * + * @example + * ```typescript + * import { Droid, TimeoutError } from '@activade/droid-sdk'; + * + * const droid = new Droid({ timeout: 30000 }); // 30 seconds + * + * try { + * await droid.exec('Long running task'); + * } catch (error) { + * if (error instanceof TimeoutError) { + * console.log(`Operation timed out after ${error.timeoutMs}ms`); + * } + * } + * ``` + * + * @category Errors + */ export class TimeoutError extends DroidError { + /** + * The timeout duration in milliseconds that was exceeded. + */ readonly timeoutMs: number; + /** + * Creates a new TimeoutError instance. + * + * @param timeoutMs - The timeout value in milliseconds + */ constructor(timeoutMs: number) { super(`Operation timed out after ${timeoutMs}ms`); this.name = 'TimeoutError'; @@ -52,7 +228,38 @@ export class TimeoutError extends DroidError { } } +/** + * Thrown when reading from a stream fails. + * + * This typically occurs during streaming execution when the event + * stream is interrupted or corrupted. + * + * @example + * ```typescript + * import { StreamError } from '@activade/droid-sdk'; + * + * try { + * const { events } = await thread.runStreamed('Generate code'); + * for await (const event of events) { + * console.log(event); + * } + * } catch (error) { + * if (error instanceof StreamError) { + * console.log('Stream failed:', error.message); + * console.log('Cause:', error.cause); + * } + * } + * ``` + * + * @category Errors + */ export class StreamError extends DroidError { + /** + * Creates a new StreamError instance. + * + * @param message - Human-readable error description + * @param cause - Optional underlying error that caused the stream failure + */ constructor(message: string, cause?: Error) { super(message, { cause }); this.name = 'StreamError'; diff --git a/src/events.ts b/src/events.ts index d6e663e..0655383 100644 --- a/src/events.ts +++ b/src/events.ts @@ -1,11 +1,47 @@ import type { TurnResult } from './turn'; import type { StreamEvent } from './types'; +/** + * A streaming turn result with real-time events. + * + * Returned by {@link Thread.runStreamed} to provide access to events + * as they occur, along with a promise for the final result. + * + * @example + * ```typescript + * const { events, result } = await thread.runStreamed('Generate code'); + * + * // Process events as they arrive + * for await (const event of events) { + * console.log(event.type); + * } + * + * // Get the final result after all events + * const finalResult = await result; + * console.log(finalResult.finalResponse); + * ``` + * + * @category Events + */ export interface StreamedTurn { + /** + * Async iterable of stream events. + * + * Yields events in real-time as they are emitted by the CLI. + * Must be fully consumed before accessing the result promise. + */ events: AsyncIterable; + + /** + * Promise that resolves to the final turn result. + * + * Resolves after all events have been emitted and the CLI + * process has completed. + */ result: Promise; } +// Re-export event types for convenient access export type { MessageEvent, StreamEvent, @@ -16,6 +52,7 @@ export type { TurnFailedEvent, } from './types'; +// Re-export type guards for event handling export { isMessageEvent, isSystemInitEvent, diff --git a/src/models.ts b/src/models.ts index 7741b81..535f555 100644 --- a/src/models.ts +++ b/src/models.ts @@ -1,13 +1,55 @@ export { MODELS, type ModelId } from './types/options'; +/** + * Metadata about a supported AI model. + * + * Contains information about the model's capabilities, provider, + * and default settings. + * + * @example + * ```typescript + * const info = getModelInfo('claude-sonnet-4-5-20250929'); + * if (info) { + * console.log(`${info.name} by ${info.provider}`); + * console.log(`Supports reasoning: ${info.supportsReasoning}`); + * } + * ``` + * + * @category Models + */ export interface ModelInfo { + /** Unique model identifier */ id: string; + /** Human-readable model name */ name: string; + /** Model provider */ provider: 'anthropic' | 'openai' | 'google' | 'opensource'; + /** Whether the model supports reasoning/thinking mode */ supportsReasoning: boolean; + /** Default reasoning effort level for this model */ defaultReasoningEffort?: string; } +/** + * Registry of all supported models with their metadata. + * + * Use {@link getModelInfo} to look up a specific model, or + * iterate this object to list all available models. + * + * @example + * ```typescript + * // List all models + * for (const [id, info] of Object.entries(MODEL_INFO)) { + * console.log(`${info.name} (${id})`); + * } + * + * // Get models by provider + * const claudeModels = Object.values(MODEL_INFO) + * .filter(m => m.provider === 'anthropic'); + * ``` + * + * @category Models + */ export const MODEL_INFO: Record = { 'claude-opus-4-5-20251101': { id: 'claude-opus-4-5-20251101', @@ -80,10 +122,47 @@ export const MODEL_INFO: Record = { }, }; +/** + * Retrieves metadata for a specific model. + * + * @param modelId - The model identifier to look up + * @returns Model metadata, or undefined if not found + * + * @example + * ```typescript + * const info = getModelInfo(MODELS.CLAUDE_SONNET); + * if (info) { + * console.log(`Using ${info.name}`); + * if (info.supportsReasoning) { + * console.log(`Default reasoning: ${info.defaultReasoningEffort}`); + * } + * } + * ``` + * + * @category Models + */ export function getModelInfo(modelId: string): ModelInfo | undefined { return MODEL_INFO[modelId]; } +/** + * Checks if a model identifier is valid. + * + * A model is valid if it exists in {@link MODEL_INFO} or is a custom + * model (prefixed with "custom:"). + * + * @param modelId - The model identifier to validate + * @returns True if the model is valid + * + * @example + * ```typescript + * isValidModel('claude-sonnet-4-5-20250929'); // true + * isValidModel('custom:my-fine-tuned-model'); // true + * isValidModel('invalid-model'); // false + * ``` + * + * @category Models + */ export function isValidModel(modelId: string): boolean { return modelId in MODEL_INFO || modelId.startsWith('custom:'); } diff --git a/src/schemas/zod-adapter.ts b/src/schemas/zod-adapter.ts index a6c6fb2..227d0c8 100644 --- a/src/schemas/zod-adapter.ts +++ b/src/schemas/zod-adapter.ts @@ -1,5 +1,13 @@ import type { JsonSchema } from '../types'; +/** + * Internal type representing a Zod-like schema structure. + * + * This type allows the SDK to work with Zod schemas without requiring + * Zod as a direct dependency. + * + * @internal + */ type ZodTypeLike = { _def?: { typeName?: string; @@ -13,6 +21,46 @@ type ZodTypeLike = { shape?: Record; }; +/** + * Converts a Zod schema to a JSON Schema object. + * + * This enables the SDK to accept Zod schemas for structured output + * validation and convert them to JSON Schema format for the CLI. + * + * Supported Zod types: + * - Primitives: string, number, boolean, null, undefined + * - Literals and enums + * - Arrays and objects + * - Optional, nullable, and default + * - Union, intersection, record, tuple + * + * @param schema - A Zod schema or Zod-like object + * @returns Equivalent JSON Schema object + * + * @example + * ```typescript + * import { z } from 'zod'; + * + * const userSchema = z.object({ + * name: z.string().min(1), + * email: z.string().email(), + * age: z.number().optional() + * }); + * + * const jsonSchema = zodToJsonSchema(userSchema); + * // { + * // type: 'object', + * // properties: { + * // name: { type: 'string', minLength: 1 }, + * // email: { type: 'string', format: 'email' }, + * // age: { type: 'number' } + * // }, + * // required: ['name', 'email'] + * // } + * ``` + * + * @category Schema + */ export function zodToJsonSchema(schema: ZodTypeLike): JsonSchema { const def = schema._def; if (!def) { @@ -94,6 +142,14 @@ export function zodToJsonSchema(schema: ZodTypeLike): JsonSchema { } } +/** + * Builds a JSON Schema for a Zod string type. + * + * @param def - The Zod definition object + * @returns JSON Schema for a string type + * + * @internal + */ function buildStringSchema(def: ZodTypeLike['_def']): JsonSchema { const schema: JsonSchema = { type: 'string' }; @@ -127,6 +183,14 @@ function buildStringSchema(def: ZodTypeLike['_def']): JsonSchema { return schema; } +/** + * Builds a JSON Schema for a Zod number type. + * + * @param def - The Zod definition object + * @returns JSON Schema for a number type + * + * @internal + */ function buildNumberSchema(def: ZodTypeLike['_def']): JsonSchema { const schema: JsonSchema = { type: 'number' }; @@ -150,6 +214,15 @@ function buildNumberSchema(def: ZodTypeLike['_def']): JsonSchema { return schema; } +/** + * Builds a JSON Schema for a Zod object type. + * + * @param schema - The Zod schema object + * @param def - The Zod definition object + * @returns JSON Schema for an object type + * + * @internal + */ function buildObjectSchema(schema: ZodTypeLike, def: ZodTypeLike['_def']): JsonSchema { const shape = def?.shape?.() ?? schema.shape ?? {}; const properties: Record = {}; @@ -174,6 +247,25 @@ function buildObjectSchema(schema: ZodTypeLike, def: ZodTypeLike['_def']): JsonS }; } +/** + * Type guard to check if a value is a Zod schema. + * + * @param value - Value to check + * @returns True if the value appears to be a Zod schema + * + * @example + * ```typescript + * import { z } from 'zod'; + * + * const schema = z.object({ name: z.string() }); + * + * if (isZodSchema(schema)) { + * const jsonSchema = zodToJsonSchema(schema); + * } + * ``` + * + * @category Schema + */ export function isZodSchema(value: unknown): value is ZodTypeLike { return ( typeof value === 'object' && diff --git a/src/thread.ts b/src/thread.ts index 678ee17..2a63f4e 100644 --- a/src/thread.ts +++ b/src/thread.ts @@ -4,12 +4,77 @@ import type { StreamedTurn } from './events'; import { buildTurnResultFromEvents, buildTurnResultFromJson, type TurnResult } from './turn'; import type { DroidConfig, RunOptions, StreamEvent, ThreadOptions } from './types'; +/** + * Represents a conversation thread with the Droid AI. + * + * A Thread maintains conversational context across multiple interactions, + * allowing for iterative development and refinement of AI responses. Each + * thread is identified by a unique session ID that persists the conversation + * state across process restarts. + * + * Threads support both synchronous execution via {@link run} and real-time + * streaming via {@link runStreamed} for observing tool calls and intermediate + * results as they happen. + * + * @example + * ```typescript + * import { Droid, MODELS } from '@activade/droid-sdk'; + * + * const droid = new Droid({ model: MODELS.CLAUDE_SONNET }); + * const thread = droid.startThread(); + * + * // First interaction - establish context + * const result1 = await thread.run('Create a TypeScript function to validate emails'); + * + * // Follow-up - the AI remembers the previous context + * const result2 = await thread.run('Add support for custom domain restrictions'); + * + * // Access session ID for later resumption + * console.log('Session ID:', thread.id); + * ``` + * + * @example + * ```typescript + * // Streaming example - observe tool calls in real-time + * const { events, result } = await thread.runStreamed('Build a REST API'); + * + * for await (const event of events) { + * if (event.type === 'tool_call') { + * console.log(`Calling tool: ${event.toolName}`); + * } else if (event.type === 'tool_result') { + * console.log(`Tool result: ${event.value.slice(0, 100)}...`); + * } + * } + * + * const finalResult = await result; + * console.log('Final response:', finalResult.finalResponse); + * ``` + * + * @see {@link Droid.startThread} for creating new threads + * @see {@link Droid.resumeThread} for resuming existing threads + * @see {@link TurnResult} for handling execution results + * + * @category Core + */ export class Thread { private _sessionId: string | undefined; private readonly _cwd: string; private readonly _config: DroidConfig; private readonly _threadOptions: ThreadOptions; + /** + * Creates a new Thread instance. + * + * @param config - The Droid configuration inherited from the parent Droid instance + * @param threadOptions - Thread-specific options that override config defaults + * @param sessionId - Optional session ID to resume an existing conversation + * + * @remarks + * This constructor is typically not called directly. Use {@link Droid.startThread} + * or {@link Droid.resumeThread} instead for proper initialization. + * + * @internal + */ constructor(config: DroidConfig, threadOptions: ThreadOptions = {}, sessionId?: string) { this._config = config; this._threadOptions = threadOptions; @@ -17,14 +82,104 @@ export class Thread { this._cwd = threadOptions.cwd ?? config.cwd ?? process.cwd(); } + /** + * The unique session identifier for this thread. + * + * The session ID is assigned after the first prompt is executed and + * remains constant for the lifetime of the thread. Use this ID with + * {@link Droid.resumeThread} to continue the conversation later. + * + * @returns The session ID, or undefined if no prompts have been executed yet + * + * @example + * ```typescript + * const thread = droid.startThread(); + * console.log(thread.id); // undefined + * + * await thread.run('Hello'); + * console.log(thread.id); // 'session_abc123...' + * + * // Save for later resumption + * localStorage.setItem('sessionId', thread.id); + * ``` + */ get id(): string | undefined { return this._sessionId; } + /** + * The working directory for this thread's CLI operations. + * + * All file operations and commands executed by the AI will be relative + * to this directory. + * + * @returns The absolute path to the working directory + * + * @example + * ```typescript + * const thread = droid.startThread({ cwd: '/projects/my-app' }); + * console.log(thread.cwd); // '/projects/my-app' + * ``` + */ get cwd(): string { return this._cwd; } + /** + * Executes a prompt with real-time streaming of events. + * + * This method provides access to intermediate events (tool calls, tool results, + * messages) as they occur, enabling real-time UI updates and progress monitoring. + * The final result is available via the returned promise. + * + * @param prompt - The natural language prompt to execute + * @param options - Optional run-specific configuration + * @returns A StreamedTurn containing an async event iterator and result promise + * + * @example + * ```typescript + * // Basic streaming with event handling + * const { events, result } = await thread.runStreamed('Create a React component'); + * + * for await (const event of events) { + * switch (event.type) { + * case 'message': + * console.log(`[${event.role}] ${event.text}`); + * break; + * case 'tool_call': + * console.log(`Calling: ${event.toolName}(${JSON.stringify(event.parameters)})`); + * break; + * case 'tool_result': + * console.log(`Result: ${event.isError ? 'ERROR' : 'OK'}`); + * break; + * case 'completion': + * console.log(`Completed in ${event.durationMs}ms`); + * break; + * } + * } + * + * const finalResult = await result; + * ``` + * + * @example + * ```typescript + * // Use with type guards for type-safe event handling + * import { isToolCallEvent, isToolResultEvent } from '@activade/droid-sdk'; + * + * for await (const event of events) { + * if (isToolCallEvent(event)) { + * // TypeScript knows event is ToolCallEvent + * console.log(event.toolName, event.parameters); + * } + * } + * ``` + * + * @throws {ExecutionError} If the Droid process exits with a non-zero code + * @throws {StreamError} If there's an error reading the event stream + * + * @see {@link run} for simpler synchronous execution + * @see {@link StreamedTurn} for the return type structure + */ async runStreamed(prompt: string, options: RunOptions = {}): Promise { const spawnOptions = this.buildSpawnOptions(prompt, options); const streamingProcess = await spawnDroidStreaming(spawnOptions); @@ -77,6 +232,61 @@ export class Thread { }; } + /** + * Executes a prompt and waits for the complete response. + * + * This method waits for the AI to finish processing before returning, + * providing the complete result including all tool calls and the final + * response. For real-time updates, use {@link runStreamed} instead. + * + * @param prompt - The natural language prompt to execute + * @param options - Optional run-specific configuration + * @returns A promise resolving to the execution result + * + * @example + * ```typescript + * // Simple execution + * const result = await thread.run('Create a function to sort an array'); + * console.log(result.finalResponse); + * + * // Access tool calls made during execution + * for (const call of result.toolCalls) { + * console.log(`Used tool: ${call.toolName}`); + * } + * ``` + * + * @example + * ```typescript + * // With file attachments + * const result = await thread.run('Describe this image', { + * attachments: [ + * { path: './screenshot.png', type: 'image' } + * ] + * }); + * ``` + * + * @example + * ```typescript + * // With structured output validation + * import { z } from 'zod'; + * + * const TaskSchema = z.object({ + * title: z.string(), + * priority: z.enum(['low', 'medium', 'high']), + * completed: z.boolean() + * }); + * + * const result = await thread.run('Create a task object as JSON'); + * const task = result.parse(TaskSchema); + * console.log(task.title, task.priority); + * ``` + * + * @throws {ExecutionError} If the CLI execution fails + * @throws {TimeoutError} If the operation exceeds the configured timeout + * + * @see {@link runStreamed} for real-time event streaming + * @see {@link TurnResult} for the return type and parsing methods + */ async run(prompt: string, options: RunOptions = {}): Promise { const spawnOptions = this.buildSpawnOptions(prompt, options); @@ -89,6 +299,15 @@ export class Thread { return buildTurnResultFromJson(jsonResult); } + /** + * Builds the spawn options for CLI execution. + * + * @param prompt - The prompt to execute + * @param options - Run-specific options + * @returns Configured spawn options for the CLI process + * + * @internal + */ private buildSpawnOptions(prompt: string, options: RunOptions): SpawnOptions { const mergedOptions: ThreadOptions = { ...this._threadOptions, diff --git a/src/turn.ts b/src/turn.ts index c9a3992..6d6d469 100644 --- a/src/turn.ts +++ b/src/turn.ts @@ -8,14 +8,99 @@ import type { TurnResultData, } from './types'; +/** + * Represents the result of a Droid execution turn. + * + * TurnResult encapsulates all data from a single AI interaction, including + * the final response, tool calls made, messages exchanged, and execution + * metadata. It provides convenient accessors for filtering results and + * methods for parsing structured output. + * + * @example + * ```typescript + * const result = await thread.run('Generate a config object'); + * + * // Access the final text response + * console.log(result.finalResponse); + * + * // Check execution metadata + * console.log(`Completed in ${result.durationMs}ms over ${result.numTurns} turns`); + * + * // Access tool calls and results + * for (const call of result.toolCalls) { + * console.log(`Called: ${call.toolName}`); + * } + * + * // Parse structured JSON output + * import { z } from 'zod'; + * const Config = z.object({ port: z.number(), host: z.string() }); + * const config = result.parse(Config); + * ``` + * + * @see {@link Thread.run} for creating TurnResult instances + * @see {@link Thread.runStreamed} for streaming execution + * + * @category Core + */ export class TurnResult { + /** + * The final text response from the AI. + * + * This contains the complete response after all tool calls have been + * processed. For structured output, this will be JSON that can be + * parsed using {@link parse} or {@link tryParse}. + */ readonly finalResponse: string; + + /** + * All items from this execution turn. + * + * Items include messages (user and assistant), tool calls, and tool + * results in chronological order. Use the typed accessors like + * {@link toolCalls}, {@link toolResults}, and {@link messages} for + * filtered access. + */ readonly items: AnyTurnItem[]; + + /** + * The unique session identifier for this conversation. + * + * This ID can be used with {@link Droid.resumeThread} to continue + * the conversation later. + */ readonly sessionId: string; + + /** + * Total execution time in milliseconds. + * + * Measures the time from prompt submission to final response, + * including all tool calls. + */ readonly durationMs: number; + + /** + * Number of conversation turns in this execution. + * + * A turn typically represents one prompt-response cycle, though + * complex operations may involve multiple internal turns. + */ readonly numTurns: number; + + /** + * Indicates whether the execution resulted in an error. + * + * When true, {@link finalResponse} may contain error details + * rather than the expected output. + */ readonly isError: boolean; + /** + * Creates a new TurnResult instance. + * + * @param data - The raw turn result data from the CLI + * + * @internal + */ constructor(data: TurnResultData) { this.finalResponse = data.finalResponse; this.items = data.items; @@ -25,6 +110,36 @@ export class TurnResult { this.isError = data.isError; } + /** + * Parses the final response as JSON and validates against a schema. + * + * This method is designed to work with Zod schemas but supports any + * object with a `parse` method that throws on invalid input. + * + * @typeParam T - The expected output type + * @param schema - A schema with a parse method (e.g., Zod schema) + * @returns The parsed and validated data + * + * @example + * ```typescript + * import { z } from 'zod'; + * + * const UserSchema = z.object({ + * id: z.number(), + * name: z.string(), + * email: z.string().email() + * }); + * + * const result = await thread.run('Generate a user object as JSON'); + * const user = result.parse(UserSchema); + * // TypeScript knows: user.id is number, user.name is string, etc. + * ``` + * + * @throws {ParseError} If the response is not valid JSON + * @throws {Error} If the schema validation fails (error from schema.parse) + * + * @see {@link tryParse} for non-throwing validation + */ parse(schema: { parse: (data: unknown) => T }): T { try { const data = JSON.parse(this.finalResponse); @@ -37,6 +152,38 @@ export class TurnResult { } } + /** + * Attempts to parse the final response, returning null on failure. + * + * This is a safe alternative to {@link parse} that returns null instead + * of throwing when parsing or validation fails. Ideal for optional + * structured output. + * + * @typeParam T - The expected output type + * @param schema - A schema with a safeParse method (e.g., Zod schema) + * @returns The parsed data, or null if parsing/validation fails + * + * @example + * ```typescript + * import { z } from 'zod'; + * + * const ConfigSchema = z.object({ + * debug: z.boolean(), + * logLevel: z.enum(['info', 'warn', 'error']) + * }); + * + * const result = await thread.run('Generate config if needed'); + * const config = result.tryParse(ConfigSchema); + * + * if (config) { + * console.log('Config:', config); + * } else { + * console.log('No valid config in response'); + * } + * ``` + * + * @see {@link parse} for throwing validation + */ tryParse(schema: { safeParse: (data: unknown) => { success: boolean; data?: T } }): T | null { try { const data = JSON.parse(this.finalResponse); @@ -47,22 +194,103 @@ export class TurnResult { } } + /** + * All tool calls made during this execution. + * + * Tool calls represent AI requests to use external capabilities + * like file operations, shell commands, or web requests. + * + * @returns Array of tool call items in chronological order + * + * @example + * ```typescript + * const result = await thread.run('Create and test a function'); + * + * for (const call of result.toolCalls) { + * console.log(`Tool: ${call.toolName}`); + * console.log(`Params: ${JSON.stringify(call.parameters)}`); + * } + * ``` + */ get toolCalls(): ToolCallItem[] { return this.items.filter((item): item is ToolCallItem => item.type === 'tool_call'); } + /** + * All tool results from this execution. + * + * Tool results contain the output (or error) from each tool call. + * Use {@link ToolResultItem.isError} to check for failures. + * + * @returns Array of tool result items in chronological order + * + * @example + * ```typescript + * const result = await thread.run('Read and process files'); + * + * for (const toolResult of result.toolResults) { + * if (toolResult.isError) { + * console.error(`${toolResult.toolName} failed: ${toolResult.value}`); + * } else { + * console.log(`${toolResult.toolName} succeeded`); + * } + * } + * ``` + */ get toolResults(): ToolResultItem[] { return this.items.filter((item): item is ToolResultItem => item.type === 'tool_result'); } + /** + * All messages from this execution. + * + * Messages include both user prompts and assistant responses. + * Use {@link assistantMessages} for AI responses only. + * + * @returns Array of message items in chronological order + */ get messages(): MessageItem[] { return this.items.filter((item): item is MessageItem => item.type === 'message'); } + /** + * Assistant messages only from this execution. + * + * Filters to only AI responses, excluding user prompts. + * + * @returns Array of assistant message items in chronological order + * + * @example + * ```typescript + * const result = await thread.run('Explain your reasoning'); + * + * for (const msg of result.assistantMessages) { + * console.log(`[${new Date(msg.timestamp).toISOString()}] ${msg.text}`); + * } + * ``` + */ get assistantMessages(): MessageItem[] { return this.messages.filter((m) => m.role === 'assistant'); } + /** + * Converts the result to a plain JSON-serializable object. + * + * Useful for logging, caching, or transmitting results. + * + * @returns A plain object representation of the turn result + * + * @example + * ```typescript + * const result = await thread.run('Generate code'); + * + * // Save to file + * fs.writeFileSync('result.json', JSON.stringify(result.toJSON(), null, 2)); + * + * // Or use with JSON.stringify directly + * console.log(JSON.stringify(result)); + * ``` + */ toJSON(): TurnResultData { return { finalResponse: this.finalResponse, @@ -75,6 +303,17 @@ export class TurnResult { } } +/** + * Builds a TurnResult from an array of stream events. + * + * Processes events from a streaming execution to extract the final + * response, session ID, and all interaction items. + * + * @param events - Array of stream events from execution + * @returns A constructed TurnResult instance + * + * @internal + */ export function buildTurnResultFromEvents(events: StreamEvent[]): TurnResult { const items: AnyTurnItem[] = []; let sessionId = ''; @@ -147,6 +386,17 @@ export function buildTurnResultFromEvents(events: StreamEvent[]): TurnResult { }); } +/** + * Builds a TurnResult from a JSON CLI response. + * + * Converts the raw JSON output format from the Droid CLI into a + * TurnResult instance. + * + * @param json - The JSON response from the CLI + * @returns A constructed TurnResult instance + * + * @internal + */ export function buildTurnResultFromJson(json: { result: string; session_id: string; diff --git a/src/types/config.ts b/src/types/config.ts index 8a8be53..32fe66f 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -1,13 +1,124 @@ import type { AutonomyLevel, ModelId, ReasoningEffort } from './options'; +/** + * Configuration options for the Droid SDK. + * + * This interface defines the global settings that apply to all operations + * performed by a Droid instance. Individual operations can override these + * settings using their respective options. + * + * @example + * ```typescript + * import { Droid, MODELS } from '@activade/droid-sdk'; + * + * const config: DroidConfig = { + * model: MODELS.CLAUDE_SONNET, + * autonomyLevel: 'high', + * reasoningEffort: 'medium', + * cwd: '/path/to/project', + * timeout: 300000 // 5 minutes + * }; + * + * const droid = new Droid(config); + * ``` + * + * @see {@link Droid} for usage context + * + * @category Configuration + */ export interface DroidConfig { + /** + * Working directory for CLI operations. + * + * All file paths and commands will be relative to this directory. + * Defaults to `process.cwd()` if not specified. + * + * @default process.cwd() + */ cwd?: string; + + /** + * The AI model to use for generation. + * + * Can be a predefined model ID from {@link MODELS} or a custom model string. + * Different models have different capabilities, speeds, and costs. + * + * @example + * ```typescript + * // Using predefined model constant + * { model: MODELS.CLAUDE_SONNET } + * + * // Using model ID string + * { model: 'claude-opus-4-5-20251101' } + * + * // Custom model + * { model: 'custom:my-fine-tuned-model' } + * ``` + */ model?: ModelId | string; + + /** + * Level of autonomous decision-making. + * + * Controls how independently the AI operates: + * - `'default'`: Use model's default behavior + * - `'low'`: Require confirmation for most actions + * - `'medium'`: Allow some autonomous actions + * - `'high'`: Maximize autonomous operation + * + * @default 'default' + */ autonomyLevel?: AutonomyLevel; + + /** + * Reasoning intensity for complex tasks. + * + * Controls the depth of reasoning the AI uses: + * - `'off'` or `'none'`: Minimal reasoning + * - `'low'`: Light reasoning + * - `'medium'`: Balanced reasoning + * - `'high'`: Deep, thorough reasoning + * + * Higher values may improve quality but increase latency and cost. + */ reasoningEffort?: ReasoningEffort; + + /** + * Path to the Droid CLI binary. + * + * Allows specifying a custom CLI location. If not provided, + * the SDK will search standard locations and the system PATH. + * + * @default 'droid' + */ droidPath?: string; + + /** + * Maximum execution time in milliseconds. + * + * Operations that exceed this timeout will be terminated and + * throw a {@link TimeoutError}. + * + * @default 600000 (10 minutes) + */ timeout?: number; } +/** + * Default timeout for Droid operations in milliseconds. + * + * Set to 10 minutes (600,000ms) to accommodate complex, long-running tasks. + * + * @category Configuration + */ export const DEFAULT_TIMEOUT = 600_000; + +/** + * Default path/command to invoke the Droid CLI. + * + * Uses 'droid' which relies on the binary being in the system PATH. + * Can be overridden via {@link DroidConfig.droidPath}. + * + * @category Configuration + */ export const DEFAULT_DROID_PATH = 'droid'; diff --git a/src/types/events.ts b/src/types/events.ts index 7e134d7..5565cef 100644 --- a/src/types/events.ts +++ b/src/types/events.ts @@ -1,63 +1,182 @@ +/** + * System initialization event. + * + * Emitted at the start of a streaming execution to indicate the + * session configuration and available tools. + * + * @category Events + */ export interface SystemInitEvent { + /** Event type identifier */ type: 'system'; + /** Event subtype */ subtype: 'init'; + /** Working directory for this session */ cwd: string; + /** Unique session identifier */ session_id: string; + /** List of available tool names */ tools: string[]; + /** AI model being used */ model: string; } +/** + * Message event for user or assistant messages. + * + * Emitted when a message is sent or received during execution. + * Check the `role` property to distinguish between user prompts + * and AI responses. + * + * @category Events + */ export interface MessageEvent { + /** Event type identifier */ type: 'message'; + /** Message author role */ role: 'user' | 'assistant'; + /** Unique message identifier */ id: string; + /** Message text content */ text: string; + /** Unix timestamp in milliseconds */ timestamp: number; + /** Session identifier */ session_id: string; } +/** + * Tool call event. + * + * Emitted when the AI invokes a tool during execution. + * Contains the tool name and parameters being passed. + * + * @example + * ```typescript + * if (isToolCallEvent(event)) { + * console.log(`Calling ${event.toolName} with:`, event.parameters); + * } + * ``` + * + * @category Events + */ export interface ToolCallEvent { + /** Event type identifier */ type: 'tool_call'; + /** Unique identifier for this tool call */ id: string; + /** ID of the message containing this tool call */ messageId: string; + /** Internal tool identifier */ toolId: string; + /** Human-readable tool name */ toolName: string; + /** Parameters passed to the tool */ parameters: Record; + /** Unix timestamp in milliseconds */ timestamp: number; + /** Session identifier */ session_id: string; } +/** + * Tool result event. + * + * Emitted after a tool call completes with the result or error. + * + * @example + * ```typescript + * if (isToolResultEvent(event)) { + * if (event.isError) { + * console.error(`${event.toolName} failed:`, event.value); + * } else { + * console.log(`${event.toolName} succeeded:`, event.value.slice(0, 100)); + * } + * } + * ``` + * + * @category Events + */ export interface ToolResultEvent { + /** Event type identifier */ type: 'tool_result'; + /** Unique identifier for this result */ id: string; + /** ID of the message containing the original tool call */ messageId: string; + /** Internal tool identifier */ toolId: string; + /** Human-readable tool name */ toolName: string; + /** Whether the tool execution resulted in an error */ isError: boolean; + /** Tool output value or error message */ value: string; + /** Unix timestamp in milliseconds */ timestamp: number; + /** Session identifier */ session_id: string; } +/** + * Turn completed event. + * + * Emitted when execution completes successfully with the final response + * and execution statistics. + * + * @category Events + */ export interface TurnCompletedEvent { + /** Event type identifier */ type: 'completion'; + /** Final text response from the AI */ finalText: string; + /** Number of conversation turns */ numTurns: number; + /** Total execution duration in milliseconds */ durationMs: number; + /** Session identifier */ session_id: string; + /** Unix timestamp in milliseconds */ timestamp: number; } +/** + * Turn failed event. + * + * Emitted when execution fails with error details. + * + * @category Events + */ export interface TurnFailedEvent { + /** Event type identifier */ type: 'turn.failed'; + /** Error information */ error: { + /** Human-readable error message */ message: string; + /** Error code if available */ code?: string; }; + /** Session identifier */ session_id: string; + /** Unix timestamp in milliseconds */ timestamp: number; } +/** + * Union of all stream event types. + * + * Use the type guard functions to narrow the type: + * - {@link isSystemInitEvent} + * - {@link isMessageEvent} + * - {@link isToolCallEvent} + * - {@link isToolResultEvent} + * - {@link isTurnCompletedEvent} + * - {@link isTurnFailedEvent} + * + * @category Events + */ export type StreamEvent = | SystemInitEvent | MessageEvent @@ -66,26 +185,121 @@ export type StreamEvent = | TurnCompletedEvent | TurnFailedEvent; +/** + * Type guard for system initialization events. + * + * @param event - The event to check + * @returns True if the event is a SystemInitEvent + * + * @example + * ```typescript + * for await (const event of events) { + * if (isSystemInitEvent(event)) { + * console.log('Session started:', event.session_id); + * console.log('Available tools:', event.tools); + * } + * } + * ``` + * + * @category Events + */ export function isSystemInitEvent(event: StreamEvent): event is SystemInitEvent { return event.type === 'system' && (event as SystemInitEvent).subtype === 'init'; } +/** + * Type guard for message events. + * + * @param event - The event to check + * @returns True if the event is a MessageEvent + * + * @example + * ```typescript + * if (isMessageEvent(event)) { + * console.log(`[${event.role}] ${event.text}`); + * } + * ``` + * + * @category Events + */ export function isMessageEvent(event: StreamEvent): event is MessageEvent { return event.type === 'message'; } +/** + * Type guard for tool call events. + * + * @param event - The event to check + * @returns True if the event is a ToolCallEvent + * + * @example + * ```typescript + * if (isToolCallEvent(event)) { + * console.log(`Calling ${event.toolName}...`); + * } + * ``` + * + * @category Events + */ export function isToolCallEvent(event: StreamEvent): event is ToolCallEvent { return event.type === 'tool_call'; } +/** + * Type guard for tool result events. + * + * @param event - The event to check + * @returns True if the event is a ToolResultEvent + * + * @example + * ```typescript + * if (isToolResultEvent(event)) { + * if (event.isError) { + * console.error('Tool error:', event.value); + * } + * } + * ``` + * + * @category Events + */ export function isToolResultEvent(event: StreamEvent): event is ToolResultEvent { return event.type === 'tool_result'; } +/** + * Type guard for turn completed events. + * + * @param event - The event to check + * @returns True if the event is a TurnCompletedEvent + * + * @example + * ```typescript + * if (isTurnCompletedEvent(event)) { + * console.log(`Completed in ${event.durationMs}ms`); + * } + * ``` + * + * @category Events + */ export function isTurnCompletedEvent(event: StreamEvent): event is TurnCompletedEvent { return event.type === 'completion'; } +/** + * Type guard for turn failed events. + * + * @param event - The event to check + * @returns True if the event is a TurnFailedEvent + * + * @example + * ```typescript + * if (isTurnFailedEvent(event)) { + * console.error('Execution failed:', event.error.message); + * } + * ``` + * + * @category Events + */ export function isTurnFailedEvent(event: StreamEvent): event is TurnFailedEvent { return event.type === 'turn.failed'; } diff --git a/src/types/options.ts b/src/types/options.ts index b04e60f..f6bfc50 100644 --- a/src/types/options.ts +++ b/src/types/options.ts @@ -1,21 +1,62 @@ +/** + * Autonomy level for AI decision-making. + * + * Controls how independently the AI operates during execution: + * - `'default'`: Use the model's default behavior + * - `'low'`: Require confirmation for most actions + * - `'medium'`: Allow some autonomous actions + * - `'high'`: Maximize autonomous operation + * + * @category Configuration + */ export type AutonomyLevel = 'default' | 'low' | 'medium' | 'high'; +/** + * Reasoning intensity level. + * + * Controls the depth of reasoning the AI uses during execution: + * - `'off'` or `'none'`: Minimal reasoning (fastest) + * - `'low'`: Light reasoning + * - `'medium'`: Balanced reasoning + * - `'high'`: Deep, thorough reasoning (highest quality) + * + * Higher values may improve quality but increase latency and cost. + * + * @category Configuration + */ export type ReasoningEffort = 'off' | 'none' | 'low' | 'medium' | 'high'; +/** + * Output format for CLI responses. + * + * - `'text'`: Plain text output (default) + * - `'json'`: Single JSON object response + * - `'stream-json'`: Newline-delimited JSON events + * - `'stream-jsonrpc'`: JSON-RPC formatted events + * + * @category Configuration + */ export type OutputFormat = 'text' | 'json' | 'stream-json' | 'stream-jsonrpc'; /** * Supported file attachment types. - * - 'image': Image files (png, jpg, gif, webp, etc.) for vision-capable models - * - 'text': Text files, code, markdown, etc. - * - 'data': JSON, CSV, or other data files - * - 'other': Any other file type + * + * Used with {@link FileAttachment} to indicate the type of attached file: + * - `'image'`: Image files (png, jpg, gif, webp, etc.) for vision-capable models + * - `'text'`: Text files, code, markdown, etc. + * - `'data'`: JSON, CSV, or other data files + * - `'other'`: Any other file type + * + * @category Types */ export type FileAttachmentType = 'image' | 'text' | 'data' | 'other'; /** * A file attachment to include with the prompt. - * The file will be referenced using the @path syntax that the droid CLI understands. + * + * Files are referenced using the `@path` syntax that the Droid CLI understands. + * Attachments allow you to provide context files, images for vision models, + * or data files for processing. * * @example * ```typescript @@ -23,72 +64,292 @@ export type FileAttachmentType = 'image' | 'text' | 'data' | 'other'; * const result = await thread.run('Describe this screenshot', { * attachments: [{ path: './screenshot.png', type: 'image' }] * }); + * ``` * - * // Attach multiple files + * @example + * ```typescript + * // Attach multiple files with descriptions * const result = await thread.run('Review these files', { * attachments: [ - * { path: './src/main.ts', type: 'text' }, - * { path: './data.json', type: 'data' } + * { path: './src/main.ts', type: 'text', description: 'Main entry point' }, + * { path: './data.json', type: 'data', description: 'Sample data' } * ] * }); * ``` + * + * @category Types */ export interface FileAttachment { - /** Path to the file (relative to cwd or absolute) */ + /** + * Path to the file (relative to cwd or absolute). + */ path: string; - /** Type of the file for context */ + + /** + * Type of the file for context. + * Helps the AI understand how to process the attachment. + */ type: FileAttachmentType; - /** Optional description to include with the file reference */ + + /** + * Optional description to include with the file reference. + * Provides additional context about the file's purpose. + */ description?: string; } +/** + * Predefined model identifiers for supported AI models. + * + * Use these constants to specify the model in configuration: + * + * @example + * ```typescript + * import { Droid, MODELS } from '@activade/droid-sdk'; + * + * // Claude models + * const droid = new Droid({ model: MODELS.CLAUDE_OPUS }); + * const droid = new Droid({ model: MODELS.CLAUDE_SONNET }); + * const droid = new Droid({ model: MODELS.CLAUDE_HAIKU }); + * + * // OpenAI models + * const droid = new Droid({ model: MODELS.GPT_5_1_CODEX }); + * + * // Google models + * const droid = new Droid({ model: MODELS.GEMINI_3_PRO }); + * ``` + * + * @category Configuration + */ export const MODELS = { + /** Claude Opus 4.5 - Most capable Claude model */ CLAUDE_OPUS: 'claude-opus-4-5-20251101', + /** Claude Sonnet 4.5 - Balanced performance and speed */ CLAUDE_SONNET: 'claude-sonnet-4-5-20250929', + /** Claude Haiku 4.5 - Fast and efficient */ CLAUDE_HAIKU: 'claude-haiku-4-5-20251001', + /** GPT-5.1 - OpenAI's base GPT-5.1 model */ GPT_5_1: 'gpt-5.1', + /** GPT-5.1 Codex - Optimized for code */ GPT_5_1_CODEX: 'gpt-5.1-codex', + /** GPT-5.1 Codex Max - Extended context and capabilities */ GPT_5_1_CODEX_MAX: 'gpt-5.1-codex-max', + /** GPT-5.2 - Latest GPT model */ GPT_5_2: 'gpt-5.2', + /** Gemini 3 Pro - Google's most capable model */ GEMINI_3_PRO: 'gemini-3-pro-preview', + /** Gemini 3 Flash - Fast Gemini variant */ GEMINI_3_FLASH: 'gemini-3-flash-preview', + /** Droid Core - Factory's open-source GLM-based model */ DROID_CORE: 'glm-4.6', } as const; +/** + * Union type of all predefined model IDs. + * + * @category Types + */ export type ModelId = (typeof MODELS)[keyof typeof MODELS]; +/** + * Thread-level execution options. + * + * These options configure a thread's behavior for all prompts executed + * within it. They can be specified when creating a thread via + * {@link Droid.startThread} or {@link Droid.resumeThread}. + * + * @example + * ```typescript + * const thread = droid.startThread({ + * model: MODELS.CLAUDE_SONNET, + * autonomyLevel: 'high', + * cwd: '/path/to/project', + * enabledTools: ['file_read', 'file_write', 'shell_exec'], + * disabledTools: ['web_search'] + * }); + * ``` + * + * @category Configuration + */ export interface ThreadOptions { + /** + * Working directory for this thread. + * Overrides the Droid instance's cwd setting. + */ cwd?: string; + + /** + * AI model to use for this thread. + * Overrides the Droid instance's model setting. + */ model?: ModelId | string; + + /** + * Autonomy level for this thread. + * @see {@link AutonomyLevel} + */ autonomyLevel?: AutonomyLevel; + + /** + * Reasoning effort for this thread. + * @see {@link ReasoningEffort} + */ reasoningEffort?: ReasoningEffort; + + /** + * Whether to use specification mode. + * When true, generates a spec before implementing. + */ useSpec?: boolean; + + /** + * Model to use for specification generation. + * Only applies when `useSpec` is true. + */ specModel?: string; + + /** + * Reasoning effort for specification generation. + * Only applies when `useSpec` is true. + */ specReasoningEffort?: ReasoningEffort; + + /** + * Whitelist of tools the AI is allowed to use. + * If specified, only these tools will be available. + * + * @example + * ```typescript + * { enabledTools: ['file_read', 'file_write'] } + * ``` + */ enabledTools?: string[]; + + /** + * Blacklist of tools the AI is not allowed to use. + * These tools will be excluded even if in enabledTools. + * + * @example + * ```typescript + * { disabledTools: ['shell_exec', 'web_request'] } + * ``` + */ disabledTools?: string[]; + + /** + * Skip permission checks (unsafe). + * + * @warning This bypasses safety checks and should only be used + * in trusted environments where you fully control the prompts. + */ skipPermissionsUnsafe?: boolean; } +/** + * JSON Schema definition for structured output. + * + * Used with {@link RunOptions.outputSchema} to define the expected + * structure of AI responses. Follows the JSON Schema specification. + * + * @example + * ```typescript + * const schema: JsonSchema = { + * type: 'object', + * properties: { + * name: { type: 'string' }, + * age: { type: 'number' }, + * email: { type: 'string', format: 'email' } + * }, + * required: ['name', 'email'] + * }; + * ``` + * + * @category Types + */ export interface JsonSchema { + /** The type of the value */ type?: string; + /** Object properties (for type: 'object') */ properties?: Record; + /** Required property names */ required?: string[]; + /** Array item schema (for type: 'array') */ items?: JsonSchema; + /** Allowed values (for enums) */ enum?: unknown[]; + /** Whether additional properties are allowed */ additionalProperties?: boolean | JsonSchema; + /** Additional schema properties */ [key: string]: unknown; } +/** + * Run-level execution options. + * + * These options configure a single prompt execution within a thread. + * They extend {@link ThreadOptions} and add prompt-specific settings. + * + * @example + * ```typescript + * const result = await thread.run('Generate a user object', { + * outputSchema: { + * type: 'object', + * properties: { + * name: { type: 'string' }, + * email: { type: 'string' } + * } + * }, + * attachments: [ + * { path: './template.json', type: 'data' } + * ] + * }); + * ``` + * + * @category Configuration + */ export interface RunOptions extends Partial { + /** + * JSON Schema for structured output. + * When provided, the AI will format its response to match this schema. + */ outputSchema?: JsonSchema; + + /** + * Path to a file containing the prompt. + * Alternative to passing the prompt as a string. + */ promptFile?: string; + + /** + * File attachments to include with the prompt. + * @see {@link FileAttachment} + */ attachments?: FileAttachment[]; } +/** + * Options for one-shot execution via {@link Droid.exec}. + * + * Extends {@link RunOptions} with the ability to specify a session ID + * for continuing a previous conversation. + * + * @example + * ```typescript + * // One-shot with session continuation + * const result = await droid.exec('Continue our previous work', { + * sessionId: 'session_abc123...' + * }); + * ``` + * + * @category Configuration + */ export interface ExecOptions extends RunOptions { + /** + * Session ID to continue a previous conversation. + * If provided, the execution will have access to previous context. + */ sessionId?: string; } diff --git a/src/types/turn.ts b/src/types/turn.ts index 94409f5..3b36117 100644 --- a/src/types/turn.ts +++ b/src/types/turn.ts @@ -1,53 +1,186 @@ +/** + * Base interface for all turn items. + * + * Turn items represent discrete events that occurred during an + * execution turn, such as messages, tool calls, and tool results. + * + * @category Types + */ export interface TurnItem { + /** Discriminator for the item type */ type: 'message' | 'tool_call' | 'tool_result'; } +/** + * A message item from an execution turn. + * + * Represents a message exchanged during the conversation, either + * from the user (prompt) or assistant (AI response). + * + * @example + * ```typescript + * for (const msg of result.messages) { + * console.log(`[${msg.role}] ${msg.text}`); + * } + * ``` + * + * @category Types + */ export interface MessageItem extends TurnItem { + /** Item type discriminator */ type: 'message'; + /** Message author: 'user' for prompts, 'assistant' for AI responses */ role: 'user' | 'assistant'; + /** Unique message identifier */ id: string; + /** Message text content */ text: string; + /** Unix timestamp in milliseconds when the message was created */ timestamp: number; } +/** + * A tool call item from an execution turn. + * + * Represents an AI request to execute a tool with specific parameters. + * Each tool call will have a corresponding {@link ToolResultItem}. + * + * @example + * ```typescript + * for (const call of result.toolCalls) { + * console.log(`Tool: ${call.toolName}`); + * console.log(`Params: ${JSON.stringify(call.parameters, null, 2)}`); + * } + * ``` + * + * @category Types + */ export interface ToolCallItem extends TurnItem { + /** Item type discriminator */ type: 'tool_call'; + /** Unique identifier for this tool call */ id: string; + /** ID of the message that contains this tool call */ messageId: string; + /** Internal tool identifier */ toolId: string; + /** Human-readable tool name (e.g., 'file_read', 'shell_exec') */ toolName: string; + /** Parameters passed to the tool */ parameters: Record; + /** Unix timestamp in milliseconds */ timestamp: number; } +/** + * A tool result item from an execution turn. + * + * Represents the output from a tool execution, which may be a + * successful result or an error. + * + * @example + * ```typescript + * for (const result of result.toolResults) { + * if (result.isError) { + * console.error(`${result.toolName} failed: ${result.value}`); + * } else { + * console.log(`${result.toolName}: ${result.value.slice(0, 100)}...`); + * } + * } + * ``` + * + * @category Types + */ export interface ToolResultItem extends TurnItem { + /** Item type discriminator */ type: 'tool_result'; + /** Unique identifier for this result */ id: string; + /** ID of the message containing the original tool call */ messageId: string; + /** Internal tool identifier */ toolId: string; + /** Human-readable tool name */ toolName: string; + /** Whether the tool execution resulted in an error */ isError: boolean; + /** Tool output value or error message */ value: string; + /** Unix timestamp in milliseconds */ timestamp: number; } +/** + * Union type of all turn item types. + * + * Use the `type` property to narrow the type: + * + * @example + * ```typescript + * for (const item of result.items) { + * switch (item.type) { + * case 'message': + * console.log(`Message: ${item.text}`); + * break; + * case 'tool_call': + * console.log(`Tool call: ${item.toolName}`); + * break; + * case 'tool_result': + * console.log(`Tool result: ${item.value}`); + * break; + * } + * } + * ``` + * + * @category Types + */ export type AnyTurnItem = MessageItem | ToolCallItem | ToolResultItem; +/** + * Data structure for TurnResult construction. + * + * Contains all the data needed to construct a {@link TurnResult} instance. + * This is typically created internally from CLI output. + * + * @category Types + */ export interface TurnResultData { + /** The final text response from the AI */ finalResponse: string; + /** All items from this execution turn */ items: AnyTurnItem[]; + /** Unique session identifier */ sessionId: string; + /** Total execution duration in milliseconds */ durationMs: number; + /** Number of conversation turns */ numTurns: number; + /** Whether the execution resulted in an error */ isError: boolean; } +/** + * JSON response structure from the Droid CLI. + * + * This represents the raw JSON output format when using the `-o json` + * output format with the CLI. + * + * @internal + * @category Types + */ export interface JsonResult { + /** Response type */ type: 'result'; + /** Result subtype indicating success or error */ subtype: 'success' | 'error'; + /** Whether the execution resulted in an error */ is_error: boolean; + /** Execution duration in milliseconds */ duration_ms: number; + /** Number of conversation turns */ num_turns: number; + /** Final response text */ result: string; + /** Session identifier */ session_id: string; }