// @generated — DO NOT EDIT. Source: packages/ai/providers/claude-agent-sdk.ts /** * Claude Agent SDK provider — the first concrete AIProvider implementation. * * Uses @anthropic-ai/claude-agent-sdk to create sessions that can: * - Start fresh with Plannotator context as the system prompt * - Fork from a parent Claude Code session (preserving full history) * - Resume a previous Plannotator inline chat session * - Stream text deltas back to the UI in real time * * Sessions are read-only by default (tools limited to Read, Glob, Grep) * to keep inline chat safe and cost-bounded. */ import { buildSystemPrompt, buildForkPreamble, buildEffectivePrompt } from "../context.ts"; import { BaseSession } from "../base-session.ts"; import type { AIProvider, AIProviderCapabilities, AISession, AIMessage, CreateSessionOptions, ClaudeAgentSDKConfig, } from "../types.ts"; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const PROVIDER_NAME = "claude-agent-sdk"; /** Default read-only tools for inline chat. */ const DEFAULT_ALLOWED_TOOLS = ["Read", "Glob", "Grep", "WebSearch"]; const DEFAULT_MAX_TURNS = 99; const DEFAULT_MODEL = "claude-sonnet-4-6"; // --------------------------------------------------------------------------- // SDK query options — typed to catch typos at compile time // --------------------------------------------------------------------------- interface ClaudeSDKQueryOptions { model: string; maxTurns: number; allowedTools: string[]; cwd: string; abortController: AbortController; includePartialMessages: boolean; persistSession: boolean; maxBudgetUsd?: number; systemPrompt?: string | { type: "preset"; preset: string; append?: string }; resume?: string; forkSession?: boolean; permissionMode?: ClaudeAgentSDKConfig['permissionMode']; allowDangerouslySkipPermissions?: boolean; pathToClaudeCodeExecutable?: string; settingSources?: string[]; } // --------------------------------------------------------------------------- // Provider // --------------------------------------------------------------------------- export class ClaudeAgentSDKProvider implements AIProvider { readonly name = PROVIDER_NAME; readonly capabilities: AIProviderCapabilities = { fork: true, resume: true, streaming: true, tools: true, }; readonly models = [ { id: 'claude-sonnet-4-6', label: 'Sonnet 4.6', default: true }, { id: 'claude-sonnet-4-6[1m]', label: 'Sonnet 4.6 (1M)' }, { id: 'claude-opus-4-7', label: 'Opus 4.7' }, { id: 'claude-opus-4-7[1m]', label: 'Opus 4.7 (1M)' }, { id: 'claude-opus-4-6', label: 'Opus 4.6' }, { id: 'claude-opus-4-6[1m]', label: 'Opus 4.6 (1M)' }, { id: 'claude-haiku-4-5', label: 'Haiku 4.5' }, ] as const; private config: ClaudeAgentSDKConfig; constructor(config: ClaudeAgentSDKConfig) { this.config = config; } async createSession(options: CreateSessionOptions): Promise { return new ClaudeAgentSDKSession({ ...this.baseConfig(options), systemPrompt: buildSystemPrompt(options.context), cwd: options.cwd ?? this.config.cwd ?? process.cwd(), parentSessionId: null, forkFromSession: null, }); } async forkSession(options: CreateSessionOptions): Promise { const parent = options.context.parent; if (!parent) { throw new Error( "Cannot fork: no parent session provided in context. " + "Use createSession() for standalone sessions." ); } return new ClaudeAgentSDKSession({ ...this.baseConfig(options), systemPrompt: null, forkPreamble: buildForkPreamble(options.context), cwd: parent.cwd, parentSessionId: parent.sessionId, forkFromSession: parent.sessionId, }); } async resumeSession(sessionId: string): Promise { return new ClaudeAgentSDKSession({ ...this.baseConfig(), systemPrompt: null, cwd: this.config.cwd ?? process.cwd(), parentSessionId: null, forkFromSession: null, resumeSessionId: sessionId, }); } dispose(): void { // No persistent resources to clean up } private baseConfig(options?: CreateSessionOptions) { return { model: options?.model ?? this.config.model ?? DEFAULT_MODEL, maxTurns: options?.maxTurns ?? DEFAULT_MAX_TURNS, maxBudgetUsd: options?.maxBudgetUsd, allowedTools: this.config.allowedTools ?? DEFAULT_ALLOWED_TOOLS, permissionMode: this.config.permissionMode ?? "default", claudeExecutablePath: this.config.claudeExecutablePath, settingSources: this.config.settingSources ?? ['user', 'project'], }; } } // --------------------------------------------------------------------------- // SDK import cache — resolve once, reuse across all queries // --------------------------------------------------------------------------- // biome-ignore lint/suspicious/noExplicitAny: SDK types resolved at runtime via dynamic import let sdkQueryFn: ((...args: any[]) => any) | null = null; async function getSDKQuery() { if (!sdkQueryFn) { const sdk = await import("@anthropic-ai/claude-agent-sdk"); sdkQueryFn = sdk.query; } return sdkQueryFn!; } // --------------------------------------------------------------------------- // Session // --------------------------------------------------------------------------- interface SessionConfig { systemPrompt: string | null; forkPreamble?: string; model: string; maxTurns: number; maxBudgetUsd?: number; allowedTools: string[]; permissionMode: ClaudeAgentSDKConfig['permissionMode']; cwd: string; parentSessionId: string | null; forkFromSession: string | null; resumeSessionId?: string; claudeExecutablePath?: string; settingSources?: string[]; } class ClaudeAgentSDKSession extends BaseSession { private config: SessionConfig; /** Active Query object — needed to send control responses (permission decisions) */ private _activeQuery: { streamInput: (iter: AsyncIterable) => Promise } | null = null; constructor(config: SessionConfig) { super({ parentSessionId: config.parentSessionId, initialId: config.resumeSessionId, }); this.config = config; } async *query(prompt: string): AsyncIterable { const started = this.startQuery(); if (!started) { yield BaseSession.BUSY_ERROR; return; } const { gen } = started; try { const queryFn = await getSDKQuery(); const queryPrompt = buildEffectivePrompt( prompt, this.config.forkPreamble ?? null, this._firstQuerySent, ); const options = this.buildQueryOptions(); const stream = queryFn({ prompt: queryPrompt, options }) as AsyncIterable> & { streamInput: (iter: AsyncIterable) => Promise }; this._activeQuery = stream; this._firstQuerySent = true; for await (const message of stream) { const mapped = mapSDKMessage(message); // Capture the real session ID from the init message if ( !this._resolvedId && "session_id" in message && typeof message.session_id === "string" && message.session_id ) { this.resolveId(message.session_id); } for (const msg of mapped) { yield msg; } } } catch (err) { yield { type: "error", error: err instanceof Error ? err.message : String(err), code: "provider_error", }; } finally { this.endQuery(gen); this._activeQuery = null; } } abort(): void { this._activeQuery = null; super.abort(); } respondToPermission(requestId: string, allow: boolean, message?: string): void { if (!this._activeQuery || !this._activeQuery.streamInput) return; const response = allow ? { type: 'control_response', response: { subtype: 'success', request_id: requestId, response: { behavior: 'allow' } } } : { type: 'control_response', response: { subtype: 'success', request_id: requestId, response: { behavior: 'deny', message: message ?? 'User denied this action' } } }; this._activeQuery.streamInput( (async function* () { yield response; })() ).catch(() => {}); } // ------------------------------------------------------------------------- // Internal // ------------------------------------------------------------------------- private buildQueryOptions(): ClaudeSDKQueryOptions { const opts: ClaudeSDKQueryOptions = { model: this.config.model, maxTurns: this.config.maxTurns, allowedTools: this.config.allowedTools, cwd: this.config.cwd, abortController: this._currentAbort!, includePartialMessages: true, persistSession: true, ...(this.config.claudeExecutablePath && { pathToClaudeCodeExecutable: this.config.claudeExecutablePath, }), ...(this.config.settingSources && { settingSources: this.config.settingSources, }), }; if (this.config.maxBudgetUsd) { opts.maxBudgetUsd = this.config.maxBudgetUsd; } // After the first query resolves a real session ID, all subsequent // queries must resume that session to continue the conversation. if (this._resolvedId) { opts.resume = this._resolvedId; return this.applyPermissionMode(opts); } // First query: use Claude Code's built-in prompt with our context appended if (this.config.systemPrompt) { opts.systemPrompt = { type: "preset", preset: "claude_code", append: this.config.systemPrompt, }; } if (this.config.forkFromSession) { opts.resume = this.config.forkFromSession; opts.forkSession = true; } if (this.config.resumeSessionId) { opts.resume = this.config.resumeSessionId; } return this.applyPermissionMode(opts); } private applyPermissionMode(opts: ClaudeSDKQueryOptions): ClaudeSDKQueryOptions { if (this.config.permissionMode === "bypassPermissions") { opts.permissionMode = "bypassPermissions"; opts.allowDangerouslySkipPermissions = true; } else if (this.config.permissionMode === "plan") { opts.permissionMode = "plan"; } return opts; } } // --------------------------------------------------------------------------- // Message mapping // --------------------------------------------------------------------------- /** * Map an SDK message to one or more AIMessages. * * An SDK assistant message can contain both text and tool_use content blocks * in a single response. We emit each block as a separate AIMessage so no * content is dropped. */ function mapSDKMessage(msg: Record): AIMessage[] { const type = msg.type as string; switch (type) { case "assistant": { const message = msg.message as Record | undefined; if (!message) return [{ type: "unknown", raw: msg }]; const content = message.content as Array>; if (!content) return [{ type: "unknown", raw: msg }]; const messages: AIMessage[] = []; const textParts: string[] = []; for (const block of content) { if (block.type === "text" && typeof block.text === "string") { textParts.push(block.text); } else if (block.type === "tool_use") { // Flush accumulated text before the tool_use block if (textParts.length > 0) { messages.push({ type: "text", text: textParts.join("") }); textParts.length = 0; } messages.push({ type: "tool_use", toolName: block.name as string, toolInput: block.input as Record, toolUseId: block.id as string, }); } } // Flush any remaining text after the last block if (textParts.length > 0) { messages.push({ type: "text", text: textParts.join("") }); } return messages.length > 0 ? messages : [{ type: "unknown", raw: msg }]; } case "stream_event": { const event = msg.event as Record | undefined; if (!event) return [{ type: "unknown", raw: msg }]; const eventType = event.type as string; if (eventType === "content_block_delta") { const delta = event.delta as Record; if (delta?.type === "text_delta" && typeof delta.text === "string") { return [{ type: "text_delta", delta: delta.text }]; } } return [{ type: "unknown", raw: msg }]; } case "user": { // SDK wraps tool results in SDKUserMessage (type: "user") if (msg.tool_use_result != null) { return [{ type: "tool_result", result: typeof msg.tool_use_result === "string" ? msg.tool_use_result : JSON.stringify(msg.tool_use_result), }]; } return [{ type: "unknown", raw: msg }]; } case "control_request": { const request = msg.request as Record | undefined; if (request?.subtype === "can_use_tool") { return [{ type: "permission_request", requestId: msg.request_id as string, toolName: request.tool_name as string, toolInput: (request.input as Record) ?? {}, title: request.title as string | undefined, displayName: request.display_name as string | undefined, description: request.description as string | undefined, toolUseId: request.tool_use_id as string, }]; } return [{ type: "unknown", raw: msg }]; } case "result": { const sessionId = (msg.session_id as string) ?? ""; const subtype = msg.subtype as string; return [{ type: "result", sessionId, success: subtype === "success", result: (msg.result as string) ?? undefined, costUsd: msg.total_cost_usd as number | undefined, turns: msg.num_turns as number | undefined, }]; } default: return [{ type: "unknown", raw: msg }]; } } // --------------------------------------------------------------------------- // Factory registration // --------------------------------------------------------------------------- import { registerProviderFactory } from "../provider.ts"; registerProviderFactory( PROVIDER_NAME, async (config) => new ClaudeAgentSDKProvider(config as ClaudeAgentSDKConfig) );