diff --git a/README.md b/README.md index d10738c..1d7c84c 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ try { if (msg.type === DroidMessageType.AssistantTextDelta) { process.stdout.write(msg.text); } - if (msg.type === DroidMessageType.TurnComplete) { + if (msg.type === DroidMessageType.Result) { console.log('\nDone!'); } } @@ -409,30 +409,35 @@ if (msg.type === DroidMessageType.AssistantTextDelta) { } ``` -| Type | Description | -| -------------------------- | --------------------------------------- | -| `assistant_text_delta` | Streaming text token from the assistant | -| `thinking_text_delta` | Streaming reasoning/thinking token | -| `tool_use` | Tool invocation by the assistant | -| `tool_result` | Result from a tool execution | -| `tool_progress` | Progress update during tool execution | -| `working_state_changed` | Agent working state transition | -| `token_usage_update` | Updated token usage counters | -| `create_message` | Full assistant message created | -| `turn_complete` | Sentinel: agent turn finished | -| `session_title_updated` | Session title changed | -| `mcp_status_changed` | MCP server status changed | -| `mission_state_changed` | Mission state changed | -| `mission_features_changed` | Mission features changed | -| `mission_progress_entry` | Mission progress log changed | -| `mission_heartbeat` | Mission heartbeat | -| `mission_worker_started` | Mission worker started | -| `mission_worker_completed` | Mission worker completed | -| `mcp_auth_required` | MCP authentication required | -| `mcp_auth_completed` | MCP authentication completed | -| `permission_resolved` | Tool permission request resolved | -| `settings_updated` | Session settings changed | -| `error` | Error event from the process | +| Type | Description | +| -------------------------- | ----------------------------------------------- | +| `assistant` | Complete assistant message | +| `user` | Complete user message | +| `assistant_text_delta` | Streaming text token from the assistant | +| `assistant_text_complete` | End of an assistant text block | +| `thinking_text_delta` | Streaming reasoning/thinking token | +| `thinking_text_complete` | End of a thinking block | +| `tool_call` | Tool invocation by the assistant | +| `tool_call_delta` | Streaming tool call input | +| `tool_result` | Result from a tool execution | +| `tool_progress` | Progress update during tool execution | +| `hook` | File hook execution event (started or finished) | +| `working_state_changed` | Agent working state transition | +| `token_usage_update` | Updated token usage counters | +| `result` | End-of-turn sentinel with aggregated metadata | +| `session_title_updated` | Session title changed | +| `settings_updated` | Session settings changed | +| `permission_resolved` | Tool permission request resolved | +| `mcp_status_changed` | MCP server status changed | +| `mcp_auth_required` | MCP authentication required | +| `mcp_auth_completed` | MCP authentication completed | +| `error` | Error event from the process | +| `mission_state_changed` | Mission state changed | +| `mission_features_changed` | Mission features changed | +| `mission_progress_entry` | Mission progress log changed | +| `mission_heartbeat` | Mission heartbeat | +| `mission_worker_started` | Mission worker started | +| `mission_worker_completed` | Mission worker completed | ### Options diff --git a/docs/examples/init-metadata.md b/docs/examples/init-metadata.md deleted file mode 100644 index e59a932..0000000 --- a/docs/examples/init-metadata.md +++ /dev/null @@ -1,47 +0,0 @@ -# Example: initialization metadata - -Use this when you need the raw initialization payload, session IDs, or model/settings metadata. - -## What this example shows - -- reading `session.initResult` from `createSession()` and `resumeSession()` - -## Key snippet: read metadata from sessions - -```ts -const session = await createSession({ cwd: process.cwd() }); -const resumed = await resumeSession(session.sessionId, { cwd: process.cwd() }); - -console.log(session.initResult.settings.modelId); -console.log(resumed.initResult.cwd); -``` - -## Full script - -```ts -import { createSession, resumeSession } from '@factory/droid-sdk'; - -async function main(): Promise { - const session = await createSession({ cwd: process.cwd() }); - let resumed: Awaited> | null = null; - - try { - resumed = await resumeSession(session.sessionId, { - cwd: process.cwd(), - }); - - console.log(session.sessionId); - console.log(session.initResult.settings.modelId); - console.log(resumed.sessionId); - console.log(resumed.initResult.cwd); - } finally { - await resumed?.close(); - await session.close(); - } -} - -main().catch((err: unknown) => { - console.error('Error:', err); - process.exit(1); -}); -``` diff --git a/docs/examples/list-sessions.md b/docs/examples/list-sessions.md deleted file mode 100644 index f365287..0000000 --- a/docs/examples/list-sessions.md +++ /dev/null @@ -1,64 +0,0 @@ -# Example: list saved sessions - -Use `listSessions()` when you need recent Droid session history without launching a subprocess. - -## What this example shows - -- listing recent sessions for the current project -- listing recent sessions across all projects -- formatting returned `SessionMetadata` - -## Key snippet: project-scoped listing - -```ts -const currentProject = await listSessions({ numSessions: 10 }); -``` - -## Key snippet: cross-project listing - -```ts -const allSessions = await listSessions({ - fetchOutsideCWD: true, - numSessions: 5, -}); -``` - -This helper reads local session files directly and returns `SessionMetadata[]`. - -## Full script - -```ts -import { listSessions } from '@factory/droid-sdk'; - -function formatDate(date: Date): string { - return date.toISOString().replace('T', ' ').slice(0, 19); -} - -async function main(): Promise { - const currentProject = await listSessions({ numSessions: 10 }); - - for (const session of currentProject) { - const title = session.sessionTitle ?? session.title ?? '(untitled)'; - console.log( - `[${session.id.slice(0, 8)}] ${formatDate(session.modifiedTime)} ${session.messageCount} msgs — ${title}` - ); - } - - const allSessions = await listSessions({ - fetchOutsideCWD: true, - numSessions: 5, - }); - - for (const session of allSessions) { - const title = session.sessionTitle ?? session.title ?? '(untitled)'; - console.log( - `[${session.id.slice(0, 8)}] ${formatDate(session.modifiedTime)} — ${title}` - ); - } -} - -main().catch((err: unknown) => { - console.error('Error:', err); - process.exit(1); -}); -``` diff --git a/docs/examples/multi-turn-session.md b/docs/examples/multi-turn-session.md deleted file mode 100644 index 769c150..0000000 --- a/docs/examples/multi-turn-session.md +++ /dev/null @@ -1,90 +0,0 @@ -# Example: multi-turn session - -Use `createSession()` when you want conversation state to persist across turns. - -## What this example shows - -- creating a persistent session -- streaming turns with `session.stream()` -- closing the session cleanly - -## Key snippet: create the session - -```ts -const session = await createSession({ cwd: process.cwd() }); -``` - -Create the session once and reuse it across prompts. - -## Key snippet: stream a turn - -```ts -import { DroidMessageType } from '@factory/droid-sdk'; - -for await (const msg of session.stream( - 'List the TypeScript files in this project' -)) { - if (msg.type === DroidMessageType.AssistantTextDelta) { - process.stdout.write(msg.text); - } -} -``` - -Use `session.stream()` when you want incremental output. - -## Key snippet: collect streamed text - -```ts -let text = ''; -for await (const msg of session.stream( - 'Summarize the project in one sentence' -)) { - if (msg.type === DroidMessageType.AssistantTextDelta) { - text += msg.text; - } -} -console.log(text); -``` - -Use `run()` for one-shot aggregated output; use `session.stream()` for persistent sessions. - -## Full script - -```ts -import { createSession, DroidMessageType } from '@factory/droid-sdk'; - -async function main(): Promise { - const session = await createSession({ cwd: process.cwd() }); - - console.log(`Session created: ${session.sessionId}\n`); - - try { - for await (const msg of session.stream( - 'List the TypeScript files in this project' - )) { - if (msg.type === DroidMessageType.AssistantTextDelta) { - process.stdout.write(msg.text); - } - } - - console.log('\n'); - - let summary = ''; - for await (const msg of session.stream( - 'Summarize the project in one sentence' - )) { - if (msg.type === DroidMessageType.AssistantTextDelta) { - summary += msg.text; - } - } - console.log(summary); - } finally { - await session.close(); - } -} - -main().catch((err: unknown) => { - console.error('Error:', err); - process.exit(1); -}); -``` diff --git a/docs/examples/permission-handler.md b/docs/examples/permission-handler.md deleted file mode 100644 index 42d053f..0000000 --- a/docs/examples/permission-handler.md +++ /dev/null @@ -1,112 +0,0 @@ -# Example: permission handler - -Use a `permissionHandler` when your app needs to inspect or approve tool calls. - -## What this example shows - -- receiving tool confirmation requests -- inspecting `params.toolUses` -- approving each request with `ToolConfirmationOutcome.ProceedOnce` - -## Key snippet: define the handler - -```ts -function permissionHandler( - allowedFilePath: string, - params: RequestPermissionRequestParams -): ToolConfirmationOutcome { - const onlyAllowedCreate = params.toolUses.every( - (item) => - item.details.type === ToolConfirmationType.Create && - item.details.filePath === allowedFilePath - ); - - return onlyAllowedCreate - ? ToolConfirmationOutcome.ProceedOnce - : ToolConfirmationOutcome.Cancel; -} -``` - -- `params.toolUses` describes the pending tool calls -- `ProceedOnce` approves the current request only - -## Key snippet: pass the handler to `createSession()` - -```ts -const session = await createSession({ - cwd: process.cwd(), - permissionHandler: (params) => permissionHandler(outputPath, params), -}); - -for await (const msg of session.stream( - `Create a file called ${outputPath} with the text 'Hello, World!'` -)) { - // Handle streamed messages. -} -``` - -## Full script - -```ts -import { - DroidMessageType, - createSession, - ToolConfirmationOutcome, - ToolConfirmationType, - type RequestPermissionRequestParams, -} from '@factory/droid-sdk'; -import { mkdtemp, rm } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; - -function permissionHandler( - allowedFilePath: string, - params: RequestPermissionRequestParams -): ToolConfirmationOutcome { - for (const item of params.toolUses) { - console.log(`\n[Permission] Tool: ${item.toolUse.name}`); - console.log(`Type: ${item.confirmationType}`); - } - - const onlyAllowedCreate = params.toolUses.every( - (item) => - item.details.type === ToolConfirmationType.Create && - item.details.filePath === allowedFilePath - ); - - return onlyAllowedCreate - ? ToolConfirmationOutcome.ProceedOnce - : ToolConfirmationOutcome.Cancel; -} - -async function main(): Promise { - const tempDir = await mkdtemp(join(tmpdir(), 'droid-sdk-permission-')); - const outputPath = join(tempDir, 'hello.txt'); - - try { - const session = await createSession({ - cwd: process.cwd(), - permissionHandler: (params) => permissionHandler(outputPath, params), - }); - - try { - for await (const msg of session.stream( - `Create a file called ${outputPath} with the text 'Hello, World!'` - )) { - if (msg.type === DroidMessageType.AssistantTextDelta) { - process.stdout.write(msg.text); - } - } - } finally { - await session.close(); - } - } finally { - await rm(tempDir, { recursive: true, force: true }); - } -} - -main().catch((err: unknown) => { - console.error('Error:', err); - process.exit(1); -}); -``` diff --git a/docs/examples/run.md b/docs/examples/run.md deleted file mode 100644 index 12a10bd..0000000 --- a/docs/examples/run.md +++ /dev/null @@ -1,52 +0,0 @@ -# Example: one-shot run - -Use this when you want a single prompt and only need the final aggregated -result. - -## What this example shows - -- sending a one-shot request with `run()` -- reading the final assistant text -- inspecting collected messages and token usage - -## Key snippet - -```ts -const result = await run('What files are in the current directory?', { - cwd: process.cwd(), -}); - -console.log(result.text); -``` - -`run()` creates a temporary session, sends the prompt, returns a -`DroidResult`, and closes the session automatically. - -## Full script - -```ts -import { run } from '@factory/droid-sdk'; - -async function main(): Promise { - const text = process.argv.slice(2).join(' ') || 'What is 2 + 2?'; - - const result = await run(text, { - cwd: process.cwd(), - }); - - console.log(result.text); - console.log(`Messages received: ${result.messages.length}`); - - if (result.tokenUsage) { - console.log( - `Tokens — input: ${result.tokenUsage.inputTokens}, ` + - `output: ${result.tokenUsage.outputTokens}` - ); - } -} - -main().catch((err: unknown) => { - console.error('Error:', err); - process.exit(1); -}); -``` diff --git a/docs/examples/session-stream.md b/docs/examples/session-stream.md deleted file mode 100644 index 55e79a4..0000000 --- a/docs/examples/session-stream.md +++ /dev/null @@ -1,78 +0,0 @@ -# Example: session streaming - -Use this when you want to stream output from a session turn as it arrives. - -## What this example shows - -- creating a session with `createSession()` -- streaming a turn with `session.stream()` -- streaming assistant text incrementally -- observing tool activity and turn completion - -## Key snippet: create the session and stream a turn - -```ts -const session = await createSession({ cwd: process.cwd() }); - -for await (const msg of session.stream( - 'List all TypeScript files in this project' -)) { - // Handle streamed messages. -} -``` - -- the stream prompt is the user message for this turn -- `cwd` is the directory Droid should operate in - -## Key snippet: consume streamed messages - -```ts -import { DroidMessageType } from '@factory/droid-sdk'; - -for await (const msg of session.stream(prompt)) { - if (msg.type === DroidMessageType.AssistantTextDelta) { - process.stdout.write(msg.text); - } -} -``` - -- `assistant_text_delta` gives you streaming text output -- you can also handle `tool_use`, `tool_result`, and `turn_complete` - -## Full script - -```ts -import { DroidMessageType, createSession } from '@factory/droid-sdk'; - -async function main(): Promise { - const prompt = process.argv[2] ?? 'What files are in the current directory?'; - - const session = await createSession({ cwd: process.cwd() }); - - try { - for await (const msg of session.stream(prompt)) { - switch (msg.type) { - case DroidMessageType.AssistantTextDelta: - process.stdout.write(msg.text); - break; - case DroidMessageType.ToolUse: - console.log(`\n[Tool] ${msg.toolName}`); - break; - case DroidMessageType.ToolResult: - console.log(`[Tool Result] ${msg.isError ? 'Error' : 'OK'}`); - break; - case DroidMessageType.TurnComplete: - console.log('\n\n--- Turn complete ---'); - break; - } - } - } finally { - await session.close(); - } -} - -main().catch((err: unknown) => { - console.error('Error:', err); - process.exit(1); -}); -``` diff --git a/docs/examples/spec-mode.md b/docs/examples/spec-mode.md deleted file mode 100644 index 9028c67..0000000 --- a/docs/examples/spec-mode.md +++ /dev/null @@ -1,107 +0,0 @@ -# Example: spec mode approval flow - -Use spec mode when you want Droid to propose a plan first and only implement after approval. - -## What this example shows - -- starting a session in `DroidInteractionMode.Spec` -- detecting an `ExitSpecMode` confirmation request -- choosing whether implementation stays in the same session or moves to a new one - -## Key snippet: start in spec mode - -```ts -const session = await createSession({ - cwd: process.cwd(), - interactionMode: DroidInteractionMode.Spec, - specModeReasoningEffort: ReasoningEffort.High, - permissionHandler(params) { - return ToolConfirmationOutcome.ProceedOnce; - }, -}); - -for await (const msg of session.stream(prompt)) { - // Handle streamed messages. -} -``` - -## Key snippet: detect the spec approval request - -```ts -const exitSpec = params.toolUses.find( - (t) => t.confirmationType === ToolConfirmationType.ExitSpecMode -); -``` - -Choose the outcome you want: - -- `ToolConfirmationOutcome.ProceedOnce` keeps implementation in the same session -- `ToolConfirmationOutcome.ProceedNewSessionHigh` hands implementation to a new session - -## Full script - -```ts -import { - DroidMessageType, - DroidInteractionMode, - createSession, - ReasoningEffort, - ToolConfirmationOutcome, - ToolConfirmationType, -} from '@factory/droid-sdk'; -import { mkdtemp, rm } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; - -async function main(): Promise { - const tempDir = await mkdtemp(join(tmpdir(), 'droid-sdk-spec-')); - const outputPath = join(tempDir, 'hello-from-droid.txt'); - const prompt = - `Plan how to create a small ${outputPath} file containing the text ` + - '"Hello from Droid". Keep the plan short and concrete.'; - - try { - const session = await createSession({ - cwd: process.cwd(), - interactionMode: DroidInteractionMode.Spec, - specModeReasoningEffort: ReasoningEffort.High, - permissionHandler(params) { - const exitSpec = params.toolUses.find( - (item) => item.details.type === ToolConfirmationType.ExitSpecMode - ); - - if (exitSpec) { - return ToolConfirmationOutcome.ProceedOnce; - } - - const onlyAllowedCreate = params.toolUses.every( - (item) => - item.details.type === ToolConfirmationType.Create && - item.details.filePath === outputPath - ); - - return onlyAllowedCreate - ? ToolConfirmationOutcome.ProceedOnce - : ToolConfirmationOutcome.Cancel; - }, - }); - - try { - for await (const msg of session.stream(prompt)) { - if (msg.type === DroidMessageType.AssistantTextDelta) { - process.stdout.write(msg.text); - } - } - } finally { - await session.close(); - } - } finally { - await rm(tempDir, { recursive: true, force: true }); - } -} - -main().catch((err: unknown) => { - console.error('Error:', err); - process.exit(1); -}); -``` diff --git a/docs/examples/tool-controls.md b/docs/examples/tool-controls.md deleted file mode 100644 index 2d2f946..0000000 --- a/docs/examples/tool-controls.md +++ /dev/null @@ -1,90 +0,0 @@ -# Example: tool controls - -Use this when you want to programmatically shape which built-in exec tools are available. - -## What this example shows - -- setting initial tool overrides -- inspecting the current tool catalog with `listTools()` -- updating tool overrides later with `updateSettings()` - -## Key snippet: set tool overrides at session creation - -```ts -const session = await createSession({ - cwd: process.cwd(), - enabledToolIds: ['Read', 'Glob', 'Grep'], - disabledToolIds: ['Execute'], -}); -``` - -## Key snippet: inspect tool state - -```ts -const result = await session.listTools(); -console.log(result.tools); -``` - -## Key snippet: update tool state later - -```ts -await session.updateSettings({ - disabledToolIds: ['Read', 'Execute'], -}); -``` - -## Full script - -```ts -import { createSession, type ExecToolInfo } from '@factory/droid-sdk'; - -function printToolState(label: string, tool: ExecToolInfo | undefined): void { - if (!tool) { - console.log(`${label}: not present in tool catalog`); - return; - } - - console.log( - `${label}: defaultAllowed=${tool.defaultAllowed}, currentlyAllowed=${tool.currentlyAllowed}` - ); -} - -async function main(): Promise { - const session = await createSession({ - cwd: process.cwd(), - enabledToolIds: ['Read', 'Glob', 'Grep'], - disabledToolIds: ['Execute'], - }); - - try { - const initial = await session.listTools(); - const initialRead = initial.tools.find((tool) => tool.llmId === 'Read'); - const initialExecute = initial.tools.find( - (tool) => tool.llmId === 'Execute' - ); - - printToolState('Read', initialRead); - printToolState('Execute', initialExecute); - - await session.updateSettings({ - disabledToolIds: ['Read', 'Execute'], - }); - - const updated = await session.listTools(); - const updatedRead = updated.tools.find((tool) => tool.llmId === 'Read'); - const updatedExecute = updated.tools.find( - (tool) => tool.llmId === 'Execute' - ); - - printToolState('Read', updatedRead); - printToolState('Execute', updatedExecute); - } finally { - await session.close(); - } -} - -main().catch((err: unknown) => { - console.error('Error:', err); - process.exit(1); -}); -``` diff --git a/docs/sdk-usage-guide.md b/docs/sdk-usage-guide.md new file mode 100644 index 0000000..98d926c --- /dev/null +++ b/docs/sdk-usage-guide.md @@ -0,0 +1,678 @@ +# SDK Usage Guide + +## Getting Started + +```bash +npm install @factory/droid-sdk +``` + +Requires Node.js 18+ and the `droid` CLI on your PATH. + +```ts +import { run } from '@factory/droid-sdk'; + +const result = await run('What files are in this directory?', { + cwd: process.cwd(), +}); +console.log(result.text); +``` + +--- + +## One-shot Run + +Send a prompt, get a result, done. The session is created and closed automatically. + +```ts +import { run } from '@factory/droid-sdk'; + +const result = await run('What is 2 + 2?', { cwd: process.cwd() }); +console.log(result.text); +``` + +## Structured Output + +Force the response to match a JSON schema. The validated object is available on `result.structuredOutput`. + +```ts +import { OutputFormatType, run } from '@factory/droid-sdk'; + +const result = await run('Pick a number between 1 and 42.', { + cwd: process.cwd(), + outputFormat: { + type: OutputFormatType.JsonSchema, + schema: { + type: 'object', + properties: { number: { type: 'number' } }, + required: ['number'], + }, + }, +}); + +console.log((result.structuredOutput as { number: number }).number); +``` + +## Multi-turn Session + +Create a session once, then call `stream()` multiple times. Context is preserved across turns. + +```ts +import { createSession } from '@factory/droid-sdk'; + +const session = await createSession({ cwd: process.cwd() }); + +for await (const msg of session.stream('Remember the word "mango".')) { + // consume first turn +} + +for await (const msg of session.stream('What word did I say?')) { + if (msg.type === 'assistant') console.log(msg.text); +} + +await session.close(); +``` + +## Resume Session + +Reconnect to a previously created session by its ID. + +```ts +import { resumeSession } from '@factory/droid-sdk'; + +const session = await resumeSession('existing-session-id'); + +for await (const msg of session.stream('Continue where we left off.')) { + if (msg.type === 'assistant') console.log(msg.text); +} + +await session.close(); +``` + +## Full Message Streaming + +`stream()` yields complete messages: assistant text, tool calls, tool results, hooks, errors, and the final result. + +```ts +import { createSession, DroidMessageType } from '@factory/droid-sdk'; + +const session = await createSession({ cwd: process.cwd() }); + +for await (const msg of session.stream( + 'List files in the current directory.' +)) { + switch (msg.type) { + case DroidMessageType.Assistant: + console.log(msg.text); + break; + case DroidMessageType.ToolCall: + console.log(`[Tool] ${msg.toolUse.name}`); + break; + case DroidMessageType.ToolResult: + console.log(`[Result] ${msg.isError ? 'Error' : 'OK'}`); + break; + case DroidMessageType.Result: + console.log(`Done in ${msg.durationMs}ms`); + break; + } +} + +await session.close(); +``` + +## Partial Message Streaming + +Enable `includePartialMessages` to get token-by-token deltas, thinking blocks, and tool progress as they arrive. + +```ts +import { createSession, DroidMessageType } from '@factory/droid-sdk'; + +const session = await createSession({ cwd: process.cwd() }); + +for await (const msg of session.stream('Explain recursion.', { + includePartialMessages: true, +})) { + if (msg.type === DroidMessageType.AssistantTextDelta) { + process.stdout.write(msg.text); + } +} + +await session.close(); +``` + +## Interrupt or Cancel Running Work + +Use `session.interrupt()` to stop the current turn server-side, or pass an `AbortSignal` to cancel from the client. + +```ts +import { createSession } from '@factory/droid-sdk'; + +// Interrupt after receiving some output +const session = await createSession({ cwd: process.cwd() }); +for await (const msg of session.stream('Write a long essay.')) { + if (msg.type === 'assistant') { + await session.interrupt(); + } +} +await session.close(); + +// Or cancel with AbortSignal +const session2 = await createSession({ cwd: process.cwd() }); +const controller = new AbortController(); +setTimeout(() => controller.abort(), 2000); + +try { + for await (const msg of session2.stream('Write a long essay.', { + abortSignal: controller.signal, + })) { + } +} catch { + console.log('Aborted'); +} +await session2.close(); +``` + +## SDK-backed MCP Tools + +Define custom tools that Droid can call during a session. Tools are served via a local MCP server that the SDK manages automatically. + +```ts +import { + createSession, + createSdkMcpServer, + tool, + ToolConfirmationOutcome, +} from '@factory/droid-sdk'; +import { z } from 'zod'; + +const server = createSdkMcpServer({ + name: 'my-tools', + tools: [ + tool( + 'lookup', + 'Look up a user by name', + { name: z.string() }, + ({ name }) => { + return `${name} is user #42.`; + } + ), + ], +}); + +const session = await createSession({ + cwd: process.cwd(), + mcpServers: [server], + permissionHandler: () => ToolConfirmationOutcome.ProceedOnce, +}); + +for await (const msg of session.stream('Look up Alice.')) { + if (msg.type === 'assistant') console.log(msg.text); +} + +await session.close(); +``` + +## Autonomy Levels + +Control what Droid can do without asking for permission. Set at session creation or change mid-session. + +```ts +import { createSession, AutonomyLevel } from '@factory/droid-sdk'; + +const session = await createSession({ + cwd: process.cwd(), + autonomyLevel: AutonomyLevel.High, // Off | Low | Medium | High +}); + +// Change mid-session +await session.updateSettings({ autonomyLevel: AutonomyLevel.Low }); +await session.close(); +``` + +## Enabled/Disabled Tools + +Restrict which tools Droid can use. Accepts tool IDs like `'Read'`, `'Execute'`, `'Grep'`. + +```ts +import { createSession } from '@factory/droid-sdk'; + +const session = await createSession({ + cwd: process.cwd(), + enabledToolIds: ['Read', 'Grep'], + disabledToolIds: ['Execute'], +}); + +// Change mid-session +await session.updateSettings({ disabledToolIds: ['Read', 'Execute'] }); +await session.close(); +``` + +## Permission Handler + +Programmatically approve or reject tool calls instead of prompting a human. Receives full tool details including file paths and commands. + +```ts +import { + run, + ToolConfirmationOutcome, + ToolConfirmationType, +} from '@factory/droid-sdk'; + +await run('Create hello.txt with "Hello, World!"', { + cwd: process.cwd(), + permissionHandler(params) { + const safe = params.toolUses.every( + (item) => item.details.type === ToolConfirmationType.Create + ); + return safe + ? ToolConfirmationOutcome.ProceedOnce + : ToolConfirmationOutcome.Cancel; + }, +}); +``` + +## Spec Mode + +Start Droid in read-only planning mode. It will research and produce a plan, then request to exit spec mode for implementation. + +```ts +import { + createSession, + DroidInteractionMode, + ToolConfirmationOutcome, + ToolConfirmationType, +} from '@factory/droid-sdk'; + +const session = await createSession({ + cwd: process.cwd(), + interactionMode: DroidInteractionMode.Spec, + permissionHandler(params) { + const exitsSpec = params.toolUses.some( + (t) => t.details.type === ToolConfirmationType.ExitSpecMode + ); + return exitsSpec + ? ToolConfirmationOutcome.ProceedOnce + : ToolConfirmationOutcome.Cancel; + }, +}); + +for await (const msg of session.stream('Plan a refactor of src/utils.ts')) { +} +await session.close(); +``` + +## Multimodal Input + +Send images or documents alongside your prompt. Images must be base64-encoded. + +```ts +import { readFileSync } from 'node:fs'; +import { createSession } from '@factory/droid-sdk'; + +const session = await createSession({ cwd: process.cwd() }); + +for await (const msg of session.stream('Describe this image.', { + images: [ + { + type: 'base64', + data: readFileSync('screenshot.png').toString('base64'), + mediaType: 'image/png', + }, + ], +})) { + if (msg.type === 'assistant') console.log(msg.text); +} + +await session.close(); +``` + +## Fork Session + +Create a copy of the current session with all context preserved. Useful for branching a conversation. + +```ts +import { createSession, resumeSession } from '@factory/droid-sdk'; + +const session = await createSession({ cwd: process.cwd() }); +for await (const msg of session.stream('Remember: the password is "banana".')) { +} + +const { newSessionId } = await session.forkSession(); +const fork = await resumeSession(newSessionId); + +for await (const msg of fork.stream('What is the password?')) { + if (msg.type === 'assistant') console.log(msg.text); +} + +await fork.close(); +await session.close(); +``` + +## Compact Session + +Summarize and remove old messages to free up context window space. + +```ts +import { createSession } from '@factory/droid-sdk'; + +const session = await createSession({ cwd: process.cwd() }); + +// ... after many turns ... +const result = await session.compactSession(); +console.log( + `New session: ${result.newSessionId}, removed: ${result.removedCount} messages` +); + +await session.close(); +``` + +## Rewind + +Undo to a specific message and optionally restore files to their state at that point. + +```ts +import { + DroidClient, + ProcessTransport, + AutonomyLevel, +} from '@factory/droid-sdk'; + +const transport = new ProcessTransport({ cwd: process.cwd() }); +await transport.connect(); +const client = new DroidClient({ transport }); + +await client.initializeSession({ + machineId: 'default', + cwd: process.cwd(), + autonomyLevel: AutonomyLevel.High, +}); + +const messageId = 'target-message-id'; +const info = await client.getRewindInfo({ messageId }); +console.log(`Files available to restore: ${info.availableFiles.length}`); + +const result = await client.executeRewind({ + messageId, + filesToRestore: info.availableFiles, + filesToDelete: [], + forkTitle: 'Rewind checkpoint', +}); +console.log(`Rewound into session: ${result.newSessionId}`); + +await client.close(); +``` + +## List Sessions + +Discover saved sessions on disk. Filters to the current project by default. + +```ts +import { listSessions } from '@factory/droid-sdk'; + +const sessions = await listSessions({ numSessions: 10 }); + +for (const s of sessions) { + console.log(`${s.id}: ${s.sessionTitle ?? '(untitled)'}`); +} +``` + +## Model and Reasoning Effort + +Choose which model to use and how much reasoning effort to apply. Configurable at creation or mid-session. + +```ts +import { createSession, ReasoningEffort } from '@factory/droid-sdk'; + +const session = await createSession({ + cwd: process.cwd(), + modelId: 'claude-sonnet-4-20250514', + reasoningEffort: ReasoningEffort.High, +}); + +// Change mid-session +await session.updateSettings({ + reasoningEffort: ReasoningEffort.Low, +}); + +await session.close(); +``` + +## Hook Execution Monitoring + +Observe file hooks (pre/post tool execution hooks) as they run during a session. + +```ts +import { createSession, DroidMessageType } from '@factory/droid-sdk'; + +const session = await createSession({ cwd: process.cwd() }); + +for await (const msg of session.stream('Create a new file.')) { + if (msg.type === DroidMessageType.Hook) { + if (msg.status === 'started') { + console.log(`[Hook] ${msg.command}`); + } else { + console.log(`[Hook ${msg.status}] exit=${msg.exitCode}`); + } + } +} + +await session.close(); +``` + +## Ask-User Handler + +Programmatically answer questions that Droid asks the user during execution. + +```ts +import { createSession } from '@factory/droid-sdk'; + +const session = await createSession({ + cwd: process.cwd(), + askUserHandler(params) { + return { + cancelled: false, + answers: params.questions.map((q) => ({ + index: q.index, + question: q.question, + answer: q.options[0] ?? 'yes', + })), + }; + }, +}); + +for await (const msg of session.stream('Help me set up this project.')) { + if (msg.type === 'assistant') console.log(msg.text); +} + +await session.close(); +``` + +## MCP Server Management + +Add, remove, toggle, and list MCP servers at runtime within an active session. + +```ts +import { createSession, McpServerType } from '@factory/droid-sdk'; + +const session = await createSession({ cwd: process.cwd() }); + +await session.addMcpServer({ + name: 'my-server', + type: McpServerType.Http, + url: 'https://mcp.example.com/mcp', +}); + +const { servers, summary } = await session.listMcpServers(); +console.log(`MCP status: ${summary.status}, servers: ${servers.length}`); + +await session.close(); +``` + +## Context Stats + +Query current context window usage to understand how much capacity remains. + +```ts +import { createSession } from '@factory/droid-sdk'; + +const session = await createSession({ cwd: process.cwd() }); +for await (const msg of session.stream('Hello')) { +} + +const stats = await session.getContextStats(); +console.log( + `Used: ${stats.used}, Remaining: ${stats.remaining}, Limit: ${stats.limit}` +); + +await session.close(); +``` + +## Token Usage Tracking + +Monitor token consumption in real-time via stream events, or read the final totals from the result. + +```ts +import { createSession, DroidMessageType } from '@factory/droid-sdk'; + +const session = await createSession({ cwd: process.cwd() }); + +for await (const msg of session.stream('Summarize this project.', { + includePartialMessages: true, +})) { + if (msg.type === DroidMessageType.TokenUsageUpdate) { + console.log(`Tokens — in: ${msg.inputTokens}, out: ${msg.outputTokens}`); + } + if (msg.type === DroidMessageType.Result && msg.tokenUsage) { + console.log( + `Final — in: ${msg.tokenUsage.inputTokens}, out: ${msg.tokenUsage.outputTokens}` + ); + } +} + +await session.close(); +``` + +## List Skills + +List all available skills in the current session. + +```ts +import { createSession } from '@factory/droid-sdk'; + +const session = await createSession({ cwd: process.cwd() }); +const { skills } = await session.listSkills(); + +for (const skill of skills) { + console.log(`${skill.name} (${skill.location}): ${skill.description ?? ''}`); +} + +await session.close(); +``` + +## Raw Notification Subscription + +Subscribe to raw protocol notifications for custom event handling beyond the stream API. + +```ts +import { createSession, SessionNotificationType } from '@factory/droid-sdk'; + +const session = await createSession({ cwd: process.cwd() }); + +const unsubscribe = session.onNotification( + (notification) => { + console.log('Notification:', notification); + }, + { type: SessionNotificationType.ERROR } +); + +// ... use the session ... + +unsubscribe(); +await session.close(); +``` + +## Error Handling + +The SDK throws typed errors with structured context. Catch specific error classes to handle different failure modes. + +```ts +import { + resumeSession, + ConnectionError, + TimeoutError, + SessionNotFoundError, + ProtocolError, +} from '@factory/droid-sdk'; + +try { + const session = await resumeSession('nonexistent-id'); +} catch (error) { + if (error instanceof SessionNotFoundError) { + console.log(`Session not found: ${error.sessionId}`); + } else if (error instanceof TimeoutError) { + console.log('Request timed out'); + } else if (error instanceof ConnectionError) { + console.log(`Connection failed: ${error.message}`); + } else if (error instanceof ProtocolError) { + console.log(`Protocol error ${error.code}: ${error.message}`); + } +} +``` + +## Low-level APIs + +Most users should use `run()` and `DroidSession`. For direct RPC access, the SDK also exports `ProcessTransport`, `ProtocolEngine`, and `DroidClient`. + +`DroidClient` exposes additional methods not on `DroidSession`: `killWorkerSession()`, `cancelMcpAuth()`, `clearMcpAuth()`, `submitMcpAuthCode()`, `listMcpRegistry()`, `toggleMcpTool()`, and `submitBugReport()`. + +The package also exports its full Zod schema surface from `src/schemas/index.ts` for runtime validation of protocol payloads. + +--- + +## Configuration Reference + +### `CreateSessionOptions` + +| Field | Type | Description | +| :------------------------ | :----------------------- | :---------------------------------------------------------- | +| `cwd` | `string` | Working directory for the session | +| `machineId` | `string` | Machine identifier for initialization | +| `modelId` | `string` | LLM model identifier | +| `autonomyLevel` | `AutonomyLevel` | `Off` \| `Low` \| `Medium` \| `High` | +| `interactionMode` | `DroidInteractionMode` | `Auto` \| `Spec` \| `AGI` | +| `reasoningEffort` | `ReasoningEffort` | `None` \| `Low` \| `Medium` \| `High` \| `Max` (and others) | +| `specModeModelId` | `string` | Override model for spec mode | +| `specModeReasoningEffort` | `ReasoningEffort` | Override reasoning effort for spec mode | +| `mcpServers` | `DroidMcpServerConfig[]` | Initial MCP server configurations | +| `enabledToolIds` | `string[]` | Tool allowlist | +| `disabledToolIds` | `string[]` | Tool denylist | +| `tags` | `SessionTag[]` | Session tags for categorization | +| `permissionHandler` | `PermissionHandler` | Tool confirmation callback | +| `askUserHandler` | `AskUserHandler` | Structured user-input callback | +| `execPath` | `string` | Path to `droid` executable (default: `"droid"`) | +| `execArgs` | `string[]` | Extra CLI arguments for the subprocess | +| `env` | `Record` | Environment variables for the subprocess | +| `transport` | `DroidClientTransport` | Custom transport (skips subprocess spawn) | +| `abortSignal` | `AbortSignal` | Cancellation signal | + +### `MessageOptions` + +Accepted by `session.stream()` and `run()`: + +| Field | Type | Description | +| :----------------------- | :-------------------- | :------------------------------------------- | +| `images` | `Base64ImageSource[]` | Base64-encoded image attachments | +| `files` | `DocumentSource[]` | Document/file attachments | +| `outputFormat` | `OutputFormat` | Structured output JSON schema request | +| `includePartialMessages` | `boolean` | Yield token-level deltas and progress events | +| `abortSignal` | `AbortSignal` | Cancellation signal for this turn | + +### Error Types + +| Error | Description | +| :--------------------- | :---------------------------------------- | +| `ConnectionError` | Failed to connect to the droid subprocess | +| `ProtocolError` | JSON-RPC or protocol-level failure | +| `SessionError` | Base session error | +| `SessionNotFoundError` | Requested session does not exist | +| `TimeoutError` | RPC timed out | +| `ProcessExitError` | Subprocess exited unexpectedly | diff --git a/docs/typescript-sdk-reference.md b/docs/typescript-sdk-reference.md deleted file mode 100644 index 4f5b755..0000000 --- a/docs/typescript-sdk-reference.md +++ /dev/null @@ -1,425 +0,0 @@ -# Droid SDK reference - TypeScript - -> Public API reference for `@factory/droid-sdk`, the TypeScript SDK for the Factory Droid CLI. - -## Documentation Index - -Use these pages together: - -- API reference: `docs/typescript-sdk-reference.md` -- Example walkthroughs: `docs/examples/` -- Source exports: [`src/index.ts`](../src/index.ts) -- Runnable repo examples: [`examples/`](../examples) - -## Installation - -```bash -npm install @factory/droid-sdk -``` - -## Requirements - -- Node.js `18+` -- The `droid` CLI installed and available on your `PATH` - -## What this SDK provides - -The SDK wraps `droid exec` as a subprocess and exposes two main prompt patterns: - -- `run()` for one-shot prompt/response flows that return an aggregated result -- `createSession()` / `resumeSession()` with `session.stream()` for streamed multi-turn sessions - -It also includes: - -- streaming message events -- structured output with JSON Schema -- permission and ask-user handlers -- SDK-backed in-process MCP tools -- MCP server management -- spec mode controls -- tool allow/deny controls -- session utilities such as rename, fork, compact, and rewind -- local session discovery with `listSessions()` -- low-level access via `ProcessTransport`, `ProtocolEngine`, and `DroidClient` - -## Example walkthroughs - -For copy-pasteable walkthroughs and complete scripts, see: - -- [One-shot run](./examples/run.md) -- [Session streaming](./examples/session-stream.md) -- [Multi-turn session](./examples/multi-turn-session.md) -- [Permission handler](./examples/permission-handler.md) -- [Initialization metadata](./examples/init-metadata.md) -- [Spec mode approval flow](./examples/spec-mode.md) -- [Tool controls](./examples/tool-controls.md) -- [List saved sessions](./examples/list-sessions.md) - -## Functions - -### `run()` - -Creates a session, sends one message, consumes the turn, closes the session, and returns an aggregated `DroidResult`. - -```ts -function run(prompt: string, options?: RunOptions): Promise; -``` - -`RunOptions` combines `CreateSessionOptions` and `MessageOptions`, so it accepts session setup fields such as `cwd`, `execPath`, `modelId`, `mcpServers`, handlers, and tool overrides, plus message fields such as `images`, `files`, `outputFormat`, and `abortSignal`. - -### `createSession()` - -Creates a persistent `DroidSession` for multi-turn conversations. - -```ts -function createSession(options?: CreateSessionOptions): Promise; -``` - -### `resumeSession()` - -Reconnects to an existing session by ID. The resumed session always runs in the -working directory persisted with the session; `ResumeSessionOptions` does not -accept `cwd`. To run in a different directory, create a new session or fork the -existing one. - -```ts -function resumeSession( - sessionId: string, - options?: ResumeSessionOptions -): Promise; -``` - -### `listSessions()` - -Discovers saved sessions directly from `~/.factory/sessions/` without spawning `droid`. - -```ts -function listSessions( - options?: ListSessionsOptions -): Promise; -``` - -#### `ListSessionsOptions` - -| Field | Type | Description | -| :---------------- | :-------- | :---------------------------------------------------------------- | -| `cwd` | `string` | Scope results to a project directory. Defaults to `process.cwd()` | -| `fetchOutsideCWD` | `boolean` | Return sessions across all projects | -| `numSessions` | `number` | Maximum number of sessions to return | -| `sessionsDir` | `string` | Override the session storage root | - -#### Returned `SessionMetadata` - -Common fields include: - -- `id` -- `title` -- `sessionTitle` -- `owner` -- `messageCount` -- `modifiedTime` -- `createdTime` -- `isFavorite` -- `cwd` -- `decompSessionType` -- `decompMissionId` - -Archived sessions are excluded automatically, and results are sorted by `modifiedTime` descending. - -### `createSdkMcpServer()` and `tool()` - -Creates an SDK-managed, in-process MCP server that can be passed in `mcpServers` at session creation. - -```ts -function createSdkMcpServer(options: SdkMcpServerOptions): SdkMcpServer; - -function tool( - name: string, - description: string, - handler: DroidTool['handler'] -): DroidTool; - -function tool>( - name: string, - description: string, - inputSchema: InputShape, - handler: ( - input: z.infer> - ) => DroidToolResult | Promise -): DroidTool; -``` - -`tool()` accepts either an untyped handler or a Zod object shape for typed input validation. Tool handlers can return plain text or a Model Context Protocol `CallToolResult`. - -## `DroidSession` - -Returned by `createSession()` and `resumeSession()`. - -### Core methods - -| Method | Description | -| :------------------------- | :------------------------------------------------- | -| `stream(prompt, options?)` | Yields `DroidMessage` events until `turn_complete` | -| `interrupt()` | Gracefully interrupts the current turn | -| `close()` | Closes the underlying connection | -| `updateSettings(params)` | Updates model/session settings | -| `enterSpecMode(params?)` | Switches the current session into spec mode | - -### Session utilities - -| Method | Description | -| :------------------------ | :----------------------------------------------------- | -| `forkSession()` | Creates a new server-side session from the current one | -| `renameSession(params)` | Renames the current session | -| `compactSession(params?)` | Requests server-side compaction | -| `getContextStats()` | Reads current context window utilization | -| `getRewindInfo(params)` | Fetches rewind metadata | -| `executeRewind(params)` | Executes a rewind | - -### MCP and discovery helpers - -| Method | Description | -| :---------------------------------- | :------------------------------------------------------- | -| `addMcpServer(params)` | Adds an MCP server | -| `removeMcpServer(params)` | Removes an MCP server | -| `toggleMcpServer(params)` | Enables or disables a server | -| `listMcpServers()` | Lists configured MCP servers | -| `listMcpTools()` | Lists tools exposed by connected MCP servers | -| `authenticateMcpServer(params)` | Starts MCP OAuth/authentication flow | -| `listSkills()` | Lists available skills | -| `listTools(params?)` | Lists the exec tool catalog and current allow/deny state | -| `onNotification(callback, filter?)` | Subscribes to raw session notifications | - -### Properties - -| Property | Type | Description | -| :----------- | :--------------------------------------------- | :-------------------------- | -| `sessionId` | `string` | Active session ID | -| `initResult` | `InitializeSessionResult \| LoadSessionResult` | Raw initialize/load payload | - -### Message attachments - -`session.stream(prompt, options?)` accepts `MessageOptions`: - -| Field | Type | Description | -| :------------- | :-------------------- | :----------------------------------------------- | -| `images` | `Base64ImageSource[]` | Inline image attachments | -| `files` | `DocumentSource[]` | Inline file/document attachments | -| `outputFormat` | `OutputFormat` | Structured output request | -| `abortSignal` | `AbortSignal` | External cancellation signal for the active turn | - -`run()` accepts the same message options. - -### Structured output - -Structured output is requested with `OutputFormatType.JsonSchema`: - -```ts -import { OutputFormatType, run } from '@factory/droid-sdk'; - -const result = await run('Pick a favorite number between 1 and 42.', { - cwd: '/my/project', - outputFormat: { - type: OutputFormatType.JsonSchema, - schema: { - type: 'object', - properties: { - favoriteNumber: { - type: 'number', - minimum: 1, - maximum: 42, - }, - }, - required: ['favoriteNumber'], - }, - }, -}); - -console.log(result.structuredOutput?.favoriteNumber); -``` - -Structured output is parsed into `DroidResult.structuredOutput` when the turn returns valid JSON matching the requested object shape. - -### `DroidResult` - -Returned by `run()`: - -| Field | Type | Description | -| :----------------- | :------------------------- | :----------------------------------------------- | -| `sessionId` | `string` | Session that produced the result | -| `text` | `string` | Concatenated assistant text | -| `messages` | `DroidMessage[]` | All stream messages from the turn | -| `tokenUsage` | `TokenUsageUpdate \| null` | Final token usage if available | -| `durationMs` | `number` | Wall-clock duration spent consuming the turn | -| `turnCount` | `number` | Number of completed turns observed in the stream | -| `error` | `ErrorEvent \| null` | First Droid error event, if any | -| `structuredOutput` | `JsonObject \| null` | Parsed structured JSON object, if requested | -| `success` | `boolean` | `true` when no Droid error event was emitted | - -## Stream message model - -All streamed events are part of the `DroidMessage` union and are discriminated by `type`. - -### Common message types - -| Type | Description | -| :---------------------- | :------------------------------------------ | -| `assistant_text_delta` | Assistant text token | -| `thinking_text_delta` | Thinking/reasoning token | -| `tool_use` | Tool invocation | -| `tool_result` | Tool result | -| `tool_progress` | Tool progress update | -| `working_state_changed` | Agent state transition | -| `token_usage_update` | Token counter update | -| `create_message` | Full assistant message | -| `permission_resolved` | Permission outcome | -| `settings_updated` | Session settings changed | -| `session_title_updated` | Session title changed | -| `mcp_status_changed` | MCP status changed | -| `mcp_auth_required` | MCP authentication required | -| `mcp_auth_completed` | MCP authentication completed | -| `error` | Error from the process | -| `turn_complete` | End-of-turn sentinel synthesized by the SDK | - -### Mission and AGI messages - -When running in `DroidInteractionMode.AGI`, the stream can also emit: - -- `mission_state_changed` -- `mission_features_changed` -- `mission_progress_entry` -- `mission_heartbeat` -- `mission_worker_started` -- `mission_worker_completed` - -## Configuration and enums - -High-value exported enums include: - -- `DroidInteractionMode` -- `AutonomyLevel` -- `ReasoningEffort` -- `OutputFormatType` -- `ToolConfirmationOutcome` -- `ToolConfirmationType` -- `SessionNotificationType` -- `DroidWorkingState` -- `McpServerType` -- `McpServerStatus` -- `McpAuthOutcome` -- `MissionState` -- `FeatureStatus` -- `SettingsLevel` -- `SkillLocation` - -### Common `CreateSessionOptions` - -`CreateSessionOptions` supports the main runtime controls: - -| Field | Description | -| :------------------------------ | :------------------------------------------------- | -| `cwd` | Working directory | -| `machineId` | Machine identifier passed during initialization | -| `modelId` | Default model | -| `autonomyLevel` | Command autonomy level | -| `interactionMode` | `Auto`, `Spec`, or `AGI` | -| `reasoningEffort` | LLM reasoning depth | -| `specModeModelId` | Spec-mode model override | -| `specModeReasoningEffort` | Spec-mode reasoning override | -| `mcpServers` | Initial MCP server configs | -| `enabledToolIds` | Explicit tool allowlist | -| `disabledToolIds` | Explicit tool denylist | -| `tags` | Session tags; the SDK also appends its own SDK tag | -| `permissionHandler` | Tool confirmation callback | -| `askUserHandler` | Structured user-input callback | -| `transport` | Custom transport override | -| `execPath` / `execArgs` / `env` | Subprocess configuration | -| `abortSignal` | External cancellation signal | - -## Permission and ask-user handling - -The high-level APIs support: - -- `permissionHandler` for tool approval decisions -- `askUserHandler` for structured clarification flows - -For spec mode, approval flows also support fresh-session outcomes such as `ToolConfirmationOutcome.ProceedNewSessionHigh`. - -## MCP support - -The SDK supports: - -- SDK-backed in-process MCP servers with `createSdkMcpServer()` and `tool()` -- configuring MCP servers at session startup -- adding/removing/toggling servers during a session -- listing MCP servers and tools -- starting authentication flows with `authenticateMcpServer()` -- receiving `mcp_auth_required` and `mcp_auth_completed` stream events - -Advanced auth controls such as cancelling or clearing MCP auth are available on the low-level `DroidClient`. - -## Low-level APIs - -Most users should use `run()` and `DroidSession`, but the package also exports lower layers. - -### `ProcessTransport` - -Spawns `droid exec --input-format stream-jsonrpc --output-format stream-jsonrpc` and implements the transport interface used by the client. - -### `ProtocolEngine` - -Handles JSON-RPC framing, request/response correlation, notifications, permission requests, and ask-user requests. - -### `DroidClient` - -Low-level typed JSON-RPC client. Use it when you need direct RPC access beyond `DroidSession`. - -Notable client-only capabilities include: - -- `killWorkerSession()` -- `cancelMcpAuth()` -- `clearMcpAuth()` -- `submitMcpAuthCode()` -- `listMcpRegistry()` -- `toggleMcpTool()` -- `submitBugReport()` - -## Runtime validation and schemas - -The package exports its Zod schema surface from `src/schemas/index.ts`, including: - -- request and response schemas -- notification schemas -- enum schemas and constants -- message/content schemas -- mission schemas -- MCP schemas - -This makes it possible to validate protocol payloads at runtime in advanced integrations. - -## Errors - -The SDK exports these custom errors: - -| Error | Description | -| :--------------------- | :---------------------------------------- | -| `ConnectionError` | Failed to connect to the Droid subprocess | -| `ProtocolError` | JSON-RPC or protocol-level failure | -| `SessionError` | Base session error | -| `SessionNotFoundError` | Requested session does not exist | -| `TimeoutError` | RPC timed out | -| `ProcessExitError` | The subprocess exited unexpectedly | - -## Feature coverage in this document - -This reference intentionally documents only features confirmed in the current SDK implementation and recent shipped commits, including: - -- one-shot `run()` -- structured output -- SDK-backed MCP tools -- spec mode controls -- tool controls -- initialization metadata -- session forking -- filesystem-based session discovery -- rename, compact, rewind, and MCP/session management APIs