/** * ClaudeAdapterLive - Scoped live implementation for the Claude Agent provider adapter. * * Wraps `@anthropic-ai/claude-agent-sdk` query sessions behind the generic * provider adapter contract and emits canonical runtime events. * * @module ClaudeAdapterLive */ import { type CanUseTool, query, type Options as ClaudeQueryOptions, type PermissionMode, type PermissionResult, type PermissionUpdate, type SDKMessage, type SDKResultMessage, type SettingSource, type SDKUserMessage, type ModelUsage, } from "@anthropic-ai/claude-agent-sdk"; import { parseCliArgs } from "@t3tools/shared/cliArgs"; import { ApprovalRequestId, type CanonicalItemType, type CanonicalRequestType, type ClaudeSettings, EventId, type ProviderApprovalDecision, ProviderDriverKind, ProviderInstanceId, ProviderItemId, type ProviderRuntimeEvent, type ProviderRuntimeTurnStatus, type ProviderSendTurnInput, type ProviderSession, type ThreadTokenUsageSnapshot, type ProviderUserInputAnswers, type RuntimeContentStreamKind, RuntimeItemId, RuntimeRequestId, RuntimeTaskId, ThreadId, TurnId, type UserInputQuestion, } from "@t3tools/contracts"; import { applyClaudePromptEffortPrefix, getModelSelectionBooleanOptionValue, getModelSelectionStringOptionValue, getProviderOptionDescriptors, resolvePromptInjectedEffort, } from "@t3tools/shared/model"; import * as Cause from "effect/Cause"; import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as FileSystem from "effect/FileSystem"; import * as Fiber from "effect/Fiber"; import * as Path from "effect/Path"; import * as Queue from "effect/Queue"; import * as Random from "effect/Random"; import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; import { resolveAttachmentPath } from "../../attachmentStore.ts"; import { ServerConfig } from "../../config.ts"; import { makeClaudeEnvironment } from "../Drivers/ClaudeHome.ts"; import { getClaudeModelCapabilities, normalizeClaudeCliEffort, resolveClaudeApiModelId, resolveClaudeEffort, } from "./ClaudeProvider.ts"; import { ProviderAdapterProcessError, ProviderAdapterRequestError, ProviderAdapterSessionClosedError, ProviderAdapterSessionNotFoundError, ProviderAdapterValidationError, type ProviderAdapterError, } from "../Errors.ts"; import { type ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts"; import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.UnknownFromJsonString); const decodeUnknownJsonStringExit = Schema.decodeUnknownExit(Schema.UnknownFromJsonString); const PROVIDER = ProviderDriverKind.make("claudeAgent"); type ClaudeTextStreamKind = Extract; type ClaudeToolResultStreamKind = Extract< RuntimeContentStreamKind, "command_output" | "file_change_output" >; type ClaudeSdkEffort = NonNullable; function encodeJsonStringForDiagnostics(input: unknown): string | undefined { const result = encodeUnknownJsonStringExit(input); return Exit.isSuccess(result) ? result.value : undefined; } type PromptQueueItem = | { readonly type: "message"; readonly message: SDKUserMessage; } | { readonly type: "terminate"; }; interface ClaudeResumeState { readonly threadId?: ThreadId; readonly resume?: string; readonly resumeSessionAt?: string; readonly turnCount?: number; } interface ClaudeTurnState { readonly turnId: TurnId; readonly startedAt: string; readonly items: Array; readonly assistantTextBlocks: Map; readonly assistantTextBlockOrder: Array; readonly capturedProposedPlanKeys: Set; nextSyntheticAssistantBlockIndex: number; } interface AssistantTextBlockState { readonly itemId: string; readonly blockIndex: number; emittedTextDelta: boolean; fallbackText: string; streamClosed: boolean; completionEmitted: boolean; } interface PendingApproval { readonly requestType: CanonicalRequestType; readonly detail?: string; readonly suggestions?: ReadonlyArray; readonly decision: Deferred.Deferred; } interface PendingUserInput { readonly questions: ReadonlyArray; readonly answers: Deferred.Deferred; } interface ToolInFlight { readonly itemId: string; readonly itemType: CanonicalItemType; readonly toolName: string; readonly title: string; readonly detail?: string; readonly input: Record; readonly partialInputJson: string; readonly lastEmittedInputFingerprint?: string; } interface ClaudeSessionContext { session: ProviderSession; readonly promptQueue: Queue.Queue; readonly query: ClaudeQueryRuntime; streamFiber: Fiber.Fiber | undefined; readonly startedAt: string; readonly basePermissionMode: PermissionMode | undefined; currentApiModelId: string | undefined; resumeSessionId: string | undefined; readonly pendingApprovals: Map; readonly pendingUserInputs: Map; readonly turns: Array<{ id: TurnId; items: Array; }>; readonly inFlightTools: Map; turnState: ClaudeTurnState | undefined; lastKnownContextWindow: number | undefined; lastKnownTokenUsage: ThreadTokenUsageSnapshot | undefined; lastAssistantUuid: string | undefined; lastThreadStartedId: string | undefined; stopped: boolean; } interface ClaudeQueryRuntime extends AsyncIterable { readonly interrupt: () => Promise; readonly setModel: (model?: string) => Promise; readonly setPermissionMode: (mode: PermissionMode) => Promise; readonly setMaxThinkingTokens: (maxThinkingTokens: number | null) => Promise; readonly close: () => void; } export interface ClaudeAdapterLiveOptions { readonly instanceId?: ProviderInstanceId; readonly environment?: NodeJS.ProcessEnv; readonly createQuery?: (input: { readonly prompt: AsyncIterable; readonly options: ClaudeQueryOptions; }) => ClaudeQueryRuntime; readonly nativeEventLogPath?: string; readonly nativeEventLogger?: EventNdjsonLogger; } function isUuid(value: string): boolean { return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value); } function isSyntheticClaudeThreadId(value: string): boolean { return value.startsWith("claude-thread-"); } function hasDurableClaudeSessionId(message: SDKMessage): boolean { if (message.type !== "system") { return true; } return ( message.subtype !== "hook_started" && message.subtype !== "hook_progress" && message.subtype !== "hook_response" ); } function toMessage(cause: unknown, fallback: string): string { if (cause instanceof Error && cause.message.length > 0) { return cause.message; } return fallback; } function toProcessError( cause: unknown, fallback: string, threadId: ThreadId, ): ProviderAdapterProcessError { return new ProviderAdapterProcessError({ provider: PROVIDER, threadId, detail: toMessage(cause, fallback), cause, }); } function normalizeClaudeStreamMessages( cause: Cause.Cause<{ readonly message: string }>, ): ReadonlyArray { const errors = Cause.prettyErrors(cause) .map((error) => error.message.trim()) .filter((message) => message.length > 0); if (errors.length > 0) { return errors; } const squashed = toMessage(Cause.squash(cause), "").trim(); return squashed.length > 0 ? [squashed] : []; } function getEffectiveClaudeAgentEffort(effort: string | null | undefined): ClaudeSdkEffort | null { const normalized = normalizeClaudeCliEffort(effort); return normalized ? (normalized as ClaudeSdkEffort) : null; } function isClaudeInterruptedMessage(message: string): boolean { const normalized = message.toLowerCase(); return ( normalized.includes("all fibers interrupted without error") || normalized.includes("request was aborted") || normalized.includes("interrupted by user") ); } function isClaudeInterruptedCause(cause: Cause.Cause<{ readonly message: string }>): boolean { return ( Cause.hasInterruptsOnly(cause) || normalizeClaudeStreamMessages(cause).some(isClaudeInterruptedMessage) ); } function messageFromClaudeStreamCause( cause: Cause.Cause<{ readonly message: string }>, fallback: string, ): string { return normalizeClaudeStreamMessages(cause)[0] ?? fallback; } function interruptionMessageFromClaudeCause( cause: Cause.Cause<{ readonly message: string }>, ): string { const message = messageFromClaudeStreamCause(cause, "Claude runtime interrupted."); return isClaudeInterruptedMessage(message) ? "Claude runtime interrupted." : message; } function resultErrorsText(result: SDKResultMessage): string { return "errors" in result && Array.isArray(result.errors) ? result.errors.join(" ").toLowerCase() : ""; } function isInterruptedResult(result: SDKResultMessage): boolean { const errors = resultErrorsText(result); if (errors.includes("interrupt")) { return true; } return ( result.subtype === "error_during_execution" && result.is_error === false && (errors.includes("request was aborted") || errors.includes("interrupted by user") || errors.includes("aborted")) ); } function asRuntimeItemId(value: string): RuntimeItemId { return RuntimeItemId.make(value); } function maxClaudeContextWindowFromModelUsage( modelUsage: Record | undefined, ): number | undefined { if (!modelUsage) return undefined; let maxContextWindow: number | undefined; for (const value of Object.values(modelUsage)) { const contextWindow = value.contextWindow; maxContextWindow = Math.max(maxContextWindow ?? 0, contextWindow); } return maxContextWindow; } function normalizeClaudeTokenUsage( value: unknown, contextWindow?: number, ): ThreadTokenUsageSnapshot | undefined { if (!value || typeof value !== "object") { return undefined; } const usage = value as Record; const inputTokens = (typeof usage.input_tokens === "number" && Number.isFinite(usage.input_tokens) ? usage.input_tokens : 0) + (typeof usage.cache_creation_input_tokens === "number" && Number.isFinite(usage.cache_creation_input_tokens) ? usage.cache_creation_input_tokens : 0) + (typeof usage.cache_read_input_tokens === "number" && Number.isFinite(usage.cache_read_input_tokens) ? usage.cache_read_input_tokens : 0); const outputTokens = typeof usage.output_tokens === "number" && Number.isFinite(usage.output_tokens) ? usage.output_tokens : 0; const derivedTotalProcessedTokens = inputTokens + outputTokens; const totalProcessedTokens = (typeof usage.total_tokens === "number" && Number.isFinite(usage.total_tokens) ? usage.total_tokens : undefined) ?? (derivedTotalProcessedTokens > 0 ? derivedTotalProcessedTokens : undefined); if (totalProcessedTokens === undefined || totalProcessedTokens <= 0) { return undefined; } const maxTokens = typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0 ? contextWindow : undefined; const usedTokens = maxTokens !== undefined ? Math.min(totalProcessedTokens, maxTokens) : totalProcessedTokens; return { usedTokens, lastUsedTokens: usedTokens, ...(totalProcessedTokens > usedTokens ? { totalProcessedTokens } : {}), ...(inputTokens > 0 ? { inputTokens } : {}), ...(outputTokens > 0 ? { outputTokens } : {}), ...(maxTokens !== undefined ? { maxTokens } : {}), ...(typeof usage.tool_uses === "number" && Number.isFinite(usage.tool_uses) ? { toolUses: usage.tool_uses } : {}), ...(typeof usage.duration_ms === "number" && Number.isFinite(usage.duration_ms) ? { durationMs: usage.duration_ms } : {}), }; } function asCanonicalTurnId(value: TurnId): TurnId { return value; } function asRuntimeRequestId(value: ApprovalRequestId): RuntimeRequestId { return RuntimeRequestId.make(value); } function readClaudeResumeState(resumeCursor: unknown): ClaudeResumeState | undefined { if (!resumeCursor || typeof resumeCursor !== "object") { return undefined; } const cursor = resumeCursor as { threadId?: unknown; resume?: unknown; sessionId?: unknown; resumeSessionAt?: unknown; turnCount?: unknown; }; const threadIdCandidate = typeof cursor.threadId === "string" ? cursor.threadId : undefined; const threadId = threadIdCandidate && !isSyntheticClaudeThreadId(threadIdCandidate) ? ThreadId.make(threadIdCandidate) : undefined; const resumeCandidate = typeof cursor.resume === "string" ? cursor.resume : typeof cursor.sessionId === "string" ? cursor.sessionId : undefined; const resume = resumeCandidate && isUuid(resumeCandidate) ? resumeCandidate : undefined; const resumeSessionAt = typeof cursor.resumeSessionAt === "string" ? cursor.resumeSessionAt : undefined; const turnCountValue = typeof cursor.turnCount === "number" ? cursor.turnCount : undefined; return { ...(threadId ? { threadId } : {}), ...(resume ? { resume } : {}), ...(resumeSessionAt ? { resumeSessionAt } : {}), ...(turnCountValue !== undefined && Number.isInteger(turnCountValue) && turnCountValue >= 0 ? { turnCount: turnCountValue } : {}), }; } function classifyToolItemType(toolName: string): CanonicalItemType { const normalized = toolName.toLowerCase(); if (normalized.includes("agent")) { return "collab_agent_tool_call"; } if ( normalized === "task" || normalized === "agent" || normalized.includes("subagent") || normalized.includes("sub-agent") ) { return "collab_agent_tool_call"; } if ( normalized.includes("bash") || normalized.includes("command") || normalized.includes("shell") || normalized.includes("terminal") ) { return "command_execution"; } if ( normalized.includes("edit") || normalized.includes("write") || normalized.includes("file") || normalized.includes("patch") || normalized.includes("replace") || normalized.includes("create") || normalized.includes("delete") ) { return "file_change"; } if (normalized.includes("mcp")) { return "mcp_tool_call"; } if (normalized.includes("websearch") || normalized.includes("web search")) { return "web_search"; } if (normalized.includes("image")) { return "image_view"; } return "dynamic_tool_call"; } function isReadOnlyToolName(toolName: string): boolean { const normalized = toolName.toLowerCase(); return ( normalized === "read" || normalized.includes("read file") || normalized.includes("view") || normalized.includes("grep") || normalized.includes("glob") || normalized.includes("search") ); } function classifyRequestType(toolName: string): CanonicalRequestType { if (isReadOnlyToolName(toolName)) { return "file_read_approval"; } const itemType = classifyToolItemType(toolName); return itemType === "command_execution" ? "command_execution_approval" : itemType === "file_change" ? "file_change_approval" : "dynamic_tool_call"; } function isTodoTool(toolName: string): boolean { return toolName.toLowerCase().includes("todowrite"); } type PlanStep = { step: string; status: "pending" | "inProgress" | "completed"; }; function extractPlanStepsFromTodoInput(input: Record): PlanStep[] | null { // TodoWrite format: { todos: [{ content, status, activeForm? }] } const todos = input.todos; if (!Array.isArray(todos) || todos.length === 0) { return null; } return todos .filter((t): t is Record => t !== null && typeof t === "object") .map((todo) => ({ step: typeof todo.content === "string" && todo.content.trim().length > 0 ? todo.content.trim() : "Task", status: todo.status === "completed" ? "completed" : todo.status === "in_progress" ? "inProgress" : "pending", })); } function summarizeToolRequest(toolName: string, input: Record): string { const commandValue = input.command ?? input.cmd; const command = typeof commandValue === "string" ? commandValue : undefined; if (command && command.trim().length > 0) { return `${toolName}: ${command.trim().slice(0, 400)}`; } // For agent/subagent tools, prefer human-readable description or prompt over raw JSON const itemType = classifyToolItemType(toolName); if (itemType === "collab_agent_tool_call") { const description = typeof input.description === "string" ? input.description.trim() : undefined; const prompt = typeof input.prompt === "string" ? input.prompt.trim() : undefined; const subagentType = typeof input.subagent_type === "string" ? input.subagent_type.trim() : undefined; const label = description || (prompt ? prompt.slice(0, 200) : undefined); if (label) { return subagentType ? `${subagentType}: ${label}` : label; } } const serialized = encodeJsonStringForDiagnostics(input) ?? "[unserializable input]"; if (serialized.length <= 400) { return `${toolName}: ${serialized}`; } return `${toolName}: ${serialized.slice(0, 397)}...`; } function titleForTool(itemType: CanonicalItemType): string { switch (itemType) { case "command_execution": return "Command run"; case "file_change": return "File change"; case "mcp_tool_call": return "MCP tool call"; case "collab_agent_tool_call": return "Subagent task"; case "web_search": return "Web search"; case "image_view": return "Image view"; case "dynamic_tool_call": return "Tool call"; default: return "Item"; } } const SUPPORTED_CLAUDE_IMAGE_MIME_TYPES = new Set([ "image/gif", "image/jpeg", "image/png", "image/webp", ]); const CLAUDE_SETTING_SOURCES = [ "user", "project", "local", ] as const satisfies ReadonlyArray; function buildPromptText( input: ProviderSendTurnInput, boundInstanceId: ProviderInstanceId, ): string { const rawEffort = input.modelSelection?.instanceId === boundInstanceId ? getModelSelectionStringOptionValue(input.modelSelection, "effort") : null; const claudeModel = input.modelSelection?.instanceId === boundInstanceId ? input.modelSelection.model : undefined; const caps = getClaudeModelCapabilities(claudeModel); const promptEffort = resolvePromptInjectedEffort(caps, rawEffort); return applyClaudePromptEffortPrefix(input.input?.trim() ?? "", promptEffort); } function buildUserMessage(input: { readonly sdkContent: Array>; }): SDKUserMessage { return { type: "user", session_id: "", parent_tool_use_id: null, message: { role: "user", content: input.sdkContent as unknown as SDKUserMessage["message"]["content"], }, } as SDKUserMessage; } function buildClaudeImageContentBlock(input: { readonly mimeType: string; readonly bytes: Uint8Array; }): Record { return { type: "image", source: { type: "base64", media_type: input.mimeType, data: Buffer.from(input.bytes).toString("base64"), }, }; } const buildUserMessageEffect = Effect.fn("buildUserMessageEffect")(function* ( input: ProviderSendTurnInput, dependencies: { readonly fileSystem: FileSystem.FileSystem; readonly attachmentsDir: string; readonly boundInstanceId: ProviderInstanceId; }, ) { const text = buildPromptText(input, dependencies.boundInstanceId); const sdkContent: Array> = []; if (text.length > 0) { sdkContent.push({ type: "text", text }); } for (const attachment of input.attachments ?? []) { if (attachment.type !== "image") { continue; } if (!SUPPORTED_CLAUDE_IMAGE_MIME_TYPES.has(attachment.mimeType)) { return yield* new ProviderAdapterRequestError({ provider: PROVIDER, method: "turn/start", detail: `Unsupported Claude image attachment type '${attachment.mimeType}'.`, }); } const attachmentPath = resolveAttachmentPath({ attachmentsDir: dependencies.attachmentsDir, attachment, }); if (!attachmentPath) { return yield* new ProviderAdapterRequestError({ provider: PROVIDER, method: "turn/start", detail: `Invalid attachment id '${attachment.id}'.`, }); } const bytes = yield* dependencies.fileSystem.readFile(attachmentPath).pipe( Effect.mapError( (cause) => new ProviderAdapterRequestError({ provider: PROVIDER, method: "turn/start", detail: toMessage(cause, "Failed to read attachment file."), cause, }), ), ); sdkContent.push( buildClaudeImageContentBlock({ mimeType: attachment.mimeType, bytes, }), ); } return buildUserMessage({ sdkContent }); }); function turnStatusFromResult(result: SDKResultMessage): ProviderRuntimeTurnStatus { if (result.subtype === "success") { return "completed"; } const errors = resultErrorsText(result); if (isInterruptedResult(result)) { return "interrupted"; } if (errors.includes("cancel")) { return "cancelled"; } return "failed"; } function streamKindFromDeltaType(deltaType: string): ClaudeTextStreamKind { return deltaType.includes("thinking") ? "reasoning_text" : "assistant_text"; } function nativeProviderRefs( _context: ClaudeSessionContext, options?: { readonly providerItemId?: string | undefined; }, ): NonNullable { if (options?.providerItemId) { return { providerItemId: ProviderItemId.make(options.providerItemId), }; } return {}; } function extractAssistantTextBlocks(message: SDKMessage): Array { if (message.type !== "assistant") { return []; } const content = (message.message as { content?: unknown } | undefined)?.content; if (!Array.isArray(content)) { return []; } const fragments: string[] = []; for (const block of content) { if (!block || typeof block !== "object") { continue; } const candidate = block as { type?: unknown; text?: unknown }; if ( candidate.type === "text" && typeof candidate.text === "string" && candidate.text.length > 0 ) { fragments.push(candidate.text); } } return fragments; } function extractContentBlockText(block: unknown): string { if (!block || typeof block !== "object") { return ""; } const candidate = block as { type?: unknown; text?: unknown }; return candidate.type === "text" && typeof candidate.text === "string" ? candidate.text : ""; } function extractTextContent(value: unknown): string { if (typeof value === "string") { return value; } if (Array.isArray(value)) { return value.map((entry) => extractTextContent(entry)).join(""); } if (!value || typeof value !== "object") { return ""; } const record = value as { text?: unknown; content?: unknown; }; if (typeof record.text === "string") { return record.text; } return extractTextContent(record.content); } function extractExitPlanModePlan(value: unknown): string | undefined { if (!value || typeof value !== "object") { return undefined; } const record = value as { plan?: unknown; }; return typeof record.plan === "string" && record.plan.trim().length > 0 ? record.plan.trim() : undefined; } function exitPlanCaptureKey(input: { readonly toolUseId?: string | undefined; readonly planMarkdown: string; }): string { return input.toolUseId && input.toolUseId.length > 0 ? `tool:${input.toolUseId}` : `plan:${input.planMarkdown}`; } function tryParseJsonRecord(value: string): Record | undefined { const result = decodeUnknownJsonStringExit(value); if (!Exit.isSuccess(result)) { return undefined; } const parsed = result.value; return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record) : undefined; } function toolInputFingerprint(input: Record): string | undefined { return encodeJsonStringForDiagnostics(input); } function toolResultStreamKind(itemType: CanonicalItemType): ClaudeToolResultStreamKind | undefined { switch (itemType) { case "command_execution": return "command_output"; case "file_change": return "file_change_output"; default: return undefined; } } function toolResultBlocksFromUserMessage(message: SDKMessage): Array<{ readonly toolUseId: string; readonly block: Record; readonly text: string; readonly isError: boolean; }> { if (message.type !== "user") { return []; } const content = (message.message as { content?: unknown } | undefined)?.content; if (!Array.isArray(content)) { return []; } const blocks: Array<{ readonly toolUseId: string; readonly block: Record; readonly text: string; readonly isError: boolean; }> = []; for (const entry of content) { if (!entry || typeof entry !== "object") { continue; } const block = entry as Record; if (block.type !== "tool_result") { continue; } const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : undefined; if (!toolUseId) { continue; } blocks.push({ toolUseId, block, text: extractTextContent(block.content), isError: block.is_error === true, }); } return blocks; } function toSessionError( threadId: ThreadId, cause: unknown, ): ProviderAdapterSessionNotFoundError | ProviderAdapterSessionClosedError | undefined { const normalized = toMessage(cause, "").toLowerCase(); if (normalized.includes("unknown session") || normalized.includes("not found")) { return new ProviderAdapterSessionNotFoundError({ provider: PROVIDER, threadId, cause, }); } if (normalized.includes("closed")) { return new ProviderAdapterSessionClosedError({ provider: PROVIDER, threadId, cause, }); } return undefined; } function toRequestError(threadId: ThreadId, method: string, cause: unknown): ProviderAdapterError { const sessionError = toSessionError(threadId, cause); if (sessionError) { return sessionError; } return new ProviderAdapterRequestError({ provider: PROVIDER, method, detail: toMessage(cause, `${method} failed`), cause, }); } function sdkMessageType(value: unknown): string | undefined { if (!value || typeof value !== "object") { return undefined; } const record = value as { type?: unknown }; return typeof record.type === "string" ? record.type : undefined; } function sdkMessageSubtype(value: unknown): string | undefined { if (!value || typeof value !== "object") { return undefined; } const record = value as { subtype?: unknown }; return typeof record.subtype === "string" ? record.subtype : undefined; } function sdkNativeMethod(message: SDKMessage): string { const subtype = sdkMessageSubtype(message); if (subtype) { return `claude/${message.type}/${subtype}`; } if (message.type === "stream_event") { const streamType = sdkMessageType(message.event); if (streamType) { const deltaType = streamType === "content_block_delta" ? sdkMessageType((message.event as { delta?: unknown }).delta) : undefined; if (deltaType) { return `claude/${message.type}/${streamType}/${deltaType}`; } return `claude/${message.type}/${streamType}`; } } return `claude/${message.type}`; } function sdkNativeItemId(message: SDKMessage): string | undefined { if (message.type === "assistant") { const maybeId = (message.message as { id?: unknown }).id; if (typeof maybeId === "string") { return maybeId; } return undefined; } if (message.type === "user") { return toolResultBlocksFromUserMessage(message)[0]?.toolUseId; } if (message.type === "stream_event") { const event = message.event as { type?: unknown; content_block?: { id?: unknown }; }; if (event.type === "content_block_start" && typeof event.content_block?.id === "string") { return event.content_block.id; } } return undefined; } export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( claudeSettings: ClaudeSettings, options?: ClaudeAdapterLiveOptions, ) { const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("claudeAgent"); const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const serverConfig = yield* ServerConfig; const claudeEnvironment = yield* makeClaudeEnvironment(claudeSettings, options?.environment).pipe( Effect.provideService(Path.Path, path), ); const nativeEventLogger = options?.nativeEventLogger ?? (options?.nativeEventLogPath !== undefined ? yield* makeEventNdjsonLogger(options.nativeEventLogPath, { stream: "native", }) : undefined); const createQuery = options?.createQuery ?? ((input: { readonly prompt: AsyncIterable; readonly options: ClaudeQueryOptions; }) => query({ prompt: input.prompt, options: input.options, }) as ClaudeQueryRuntime); const sessions = new Map(); const runtimeEventQueue = yield* Queue.unbounded(); const nowIso = Effect.map(DateTime.now, DateTime.formatIso); const nextEventId = Effect.map(Random.nextUUIDv4, (id) => EventId.make(id)); const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso }); const offerRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect => Queue.offer(runtimeEventQueue, event).pipe(Effect.asVoid); const logNativeSdkMessage = Effect.fn("logNativeSdkMessage")(function* ( context: ClaudeSessionContext, message: SDKMessage, ) { if (!nativeEventLogger) { return; } const observedAt = yield* nowIso; const itemId = sdkNativeItemId(message); yield* nativeEventLogger.write( { observedAt, event: { id: "uuid" in message && typeof message.uuid === "string" ? message.uuid : yield* Random.nextUUIDv4, kind: "notification", provider: PROVIDER, createdAt: observedAt, method: sdkNativeMethod(message), ...(typeof message.session_id === "string" ? { providerThreadId: message.session_id } : {}), ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId), } : {}), ...(itemId ? { itemId: ProviderItemId.make(itemId) } : {}), payload: message, }, }, context.session.threadId, ); }); const snapshotThread = Effect.fn("snapshotThread")(function* (context: ClaudeSessionContext) { const threadId = context.session.threadId; if (!threadId) { return yield* new ProviderAdapterValidationError({ provider: PROVIDER, operation: "readThread", issue: "Session thread id is not initialized yet.", }); } return { threadId, turns: context.turns.map((turn) => ({ id: turn.id, items: [...turn.items], })), }; }); const updateResumeCursor = Effect.fn("updateResumeCursor")(function* ( context: ClaudeSessionContext, ) { const threadId = context.session.threadId; if (!threadId) return; const resumeCursor = { threadId, ...(context.resumeSessionId ? { resume: context.resumeSessionId } : {}), ...(context.lastAssistantUuid ? { resumeSessionAt: context.lastAssistantUuid } : {}), turnCount: context.turns.length, }; context.session = { ...context.session, resumeCursor, updatedAt: yield* nowIso, }; }); const ensureAssistantTextBlock = Effect.fn("ensureAssistantTextBlock")(function* ( context: ClaudeSessionContext, blockIndex: number, options?: { readonly fallbackText?: string; readonly streamClosed?: boolean; }, ) { const turnState = context.turnState; if (!turnState) { return undefined; } const existing = turnState.assistantTextBlocks.get(blockIndex); if (existing && !existing.completionEmitted) { if (existing.fallbackText.length === 0 && options?.fallbackText) { existing.fallbackText = options.fallbackText; } if (options?.streamClosed) { existing.streamClosed = true; } return { blockIndex, block: existing }; } const block: AssistantTextBlockState = { itemId: yield* Random.nextUUIDv4, blockIndex, emittedTextDelta: false, fallbackText: options?.fallbackText ?? "", streamClosed: options?.streamClosed ?? false, completionEmitted: false, }; turnState.assistantTextBlocks.set(blockIndex, block); turnState.assistantTextBlockOrder.push(block); return { blockIndex, block }; }); const createSyntheticAssistantTextBlock = Effect.fn("createSyntheticAssistantTextBlock")( function* (context: ClaudeSessionContext, fallbackText: string) { const turnState = context.turnState; if (!turnState) { return undefined; } const blockIndex = turnState.nextSyntheticAssistantBlockIndex; turnState.nextSyntheticAssistantBlockIndex -= 1; return yield* ensureAssistantTextBlock(context, blockIndex, { fallbackText, streamClosed: true, }); }, ); const completeAssistantTextBlock = Effect.fn("completeAssistantTextBlock")(function* ( context: ClaudeSessionContext, block: AssistantTextBlockState, options?: { readonly force?: boolean; readonly rawMethod?: string; readonly rawPayload?: unknown; }, ) { const turnState = context.turnState; if (!turnState || block.completionEmitted) { return; } if (!options?.force && !block.streamClosed) { return; } if (!block.emittedTextDelta && block.fallbackText.length > 0) { const deltaStamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "content.delta", eventId: deltaStamp.eventId, provider: PROVIDER, createdAt: deltaStamp.createdAt, threadId: context.session.threadId, turnId: turnState.turnId, itemId: asRuntimeItemId(block.itemId), payload: { streamKind: "assistant_text", delta: block.fallbackText, }, providerRefs: nativeProviderRefs(context), ...(options?.rawMethod || options?.rawPayload ? { raw: { source: "claude.sdk.message" as const, ...(options.rawMethod ? { method: options.rawMethod } : {}), payload: options?.rawPayload, }, } : {}), }); } block.completionEmitted = true; if (turnState.assistantTextBlocks.get(block.blockIndex) === block) { turnState.assistantTextBlocks.delete(block.blockIndex); } const stamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "item.completed", eventId: stamp.eventId, provider: PROVIDER, createdAt: stamp.createdAt, itemId: asRuntimeItemId(block.itemId), threadId: context.session.threadId, turnId: turnState.turnId, payload: { itemType: "assistant_message", status: "completed", title: "Assistant message", ...(block.fallbackText.length > 0 ? { detail: block.fallbackText } : {}), }, providerRefs: nativeProviderRefs(context), ...(options?.rawMethod || options?.rawPayload ? { raw: { source: "claude.sdk.message" as const, ...(options.rawMethod ? { method: options.rawMethod } : {}), payload: options?.rawPayload, }, } : {}), }); }); const backfillAssistantTextBlocksFromSnapshot = Effect.fn( "backfillAssistantTextBlocksFromSnapshot", )(function* (context: ClaudeSessionContext, message: SDKMessage) { const turnState = context.turnState; if (!turnState) { return; } const snapshotTextBlocks = extractAssistantTextBlocks(message); if (snapshotTextBlocks.length === 0) { return; } const orderedBlocks = turnState.assistantTextBlockOrder.map((block) => ({ blockIndex: block.blockIndex, block, })); for (const [position, text] of snapshotTextBlocks.entries()) { const existingEntry = orderedBlocks[position]; const entry = existingEntry ?? (yield* createSyntheticAssistantTextBlock(context, text).pipe( Effect.map((created) => { if (!created) { return undefined; } orderedBlocks.push(created); return created; }), )); if (!entry) { continue; } if (entry.block.fallbackText.length === 0) { entry.block.fallbackText = text; } if (entry.block.streamClosed && !entry.block.completionEmitted) { yield* completeAssistantTextBlock(context, entry.block, { rawMethod: "claude/assistant", rawPayload: message, }); } } }); const ensureThreadId = Effect.fn("ensureThreadId")(function* ( context: ClaudeSessionContext, message: SDKMessage, ) { if (typeof message.session_id !== "string" || message.session_id.length === 0) { return; } if (!hasDurableClaudeSessionId(message)) { return; } const nextThreadId = message.session_id; context.resumeSessionId = message.session_id; yield* updateResumeCursor(context); if (context.lastThreadStartedId !== nextThreadId) { context.lastThreadStartedId = nextThreadId; const stamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "thread.started", eventId: stamp.eventId, provider: PROVIDER, createdAt: stamp.createdAt, threadId: context.session.threadId, payload: { providerThreadId: nextThreadId, }, providerRefs: {}, raw: { source: "claude.sdk.message", method: "claude/thread/started", payload: { session_id: message.session_id, }, }, }); } }); const emitRuntimeError = Effect.fn("emitRuntimeError")(function* ( context: ClaudeSessionContext, message: string, cause?: unknown, ) { if (cause !== undefined) { void cause; } const turnState = context.turnState; const stamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "runtime.error", eventId: stamp.eventId, provider: PROVIDER, createdAt: stamp.createdAt, threadId: context.session.threadId, ...(turnState ? { turnId: asCanonicalTurnId(turnState.turnId) } : {}), payload: { message, class: "provider_error", ...(cause !== undefined ? { detail: cause } : {}), }, providerRefs: nativeProviderRefs(context), }); }); const emitRuntimeWarning = Effect.fn("emitRuntimeWarning")(function* ( context: ClaudeSessionContext, message: string, detail?: unknown, ) { const turnState = context.turnState; const stamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "runtime.warning", eventId: stamp.eventId, provider: PROVIDER, createdAt: stamp.createdAt, threadId: context.session.threadId, ...(turnState ? { turnId: asCanonicalTurnId(turnState.turnId) } : {}), payload: { message, ...(detail !== undefined ? { detail } : {}), }, providerRefs: nativeProviderRefs(context), }); }); const emitProposedPlanCompleted = Effect.fn("emitProposedPlanCompleted")(function* ( context: ClaudeSessionContext, input: { readonly planMarkdown: string; readonly toolUseId?: string | undefined; readonly rawSource: "claude.sdk.message" | "claude.sdk.permission"; readonly rawMethod: string; readonly rawPayload: unknown; }, ) { const turnState = context.turnState; const planMarkdown = input.planMarkdown.trim(); if (!turnState || planMarkdown.length === 0) { return; } const captureKey = exitPlanCaptureKey({ toolUseId: input.toolUseId, planMarkdown, }); if (turnState.capturedProposedPlanKeys.has(captureKey)) { return; } turnState.capturedProposedPlanKeys.add(captureKey); const stamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "turn.proposed.completed", eventId: stamp.eventId, provider: PROVIDER, createdAt: stamp.createdAt, threadId: context.session.threadId, turnId: turnState.turnId, payload: { planMarkdown, }, providerRefs: nativeProviderRefs(context, { providerItemId: input.toolUseId, }), raw: { source: input.rawSource, method: input.rawMethod, payload: input.rawPayload, }, }); }); const completeTurn = Effect.fn("completeTurn")(function* ( context: ClaudeSessionContext, status: ProviderRuntimeTurnStatus, errorMessage?: string, result?: SDKResultMessage, ) { const resultContextWindow = maxClaudeContextWindowFromModelUsage(result?.modelUsage); if (resultContextWindow !== undefined) { context.lastKnownContextWindow = resultContextWindow; } // The SDK result.usage contains *accumulated* totals across all API calls // (input_tokens, cache_read_input_tokens, etc. summed over every request). // This does NOT represent the current context window size. // Instead, use the last known context-window-accurate usage from task_progress // events and treat the accumulated total as totalProcessedTokens. const accumulatedSnapshot = normalizeClaudeTokenUsage( result?.usage, resultContextWindow ?? context.lastKnownContextWindow, ); const accumulatedTotalProcessedTokens = accumulatedSnapshot?.totalProcessedTokens ?? accumulatedSnapshot?.usedTokens; const lastGoodUsage = context.lastKnownTokenUsage; const maxTokens = resultContextWindow ?? context.lastKnownContextWindow; const usageSnapshot: ThreadTokenUsageSnapshot | undefined = lastGoodUsage ? { ...lastGoodUsage, ...(typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0 ? { maxTokens } : {}), ...(typeof accumulatedTotalProcessedTokens === "number" && Number.isFinite(accumulatedTotalProcessedTokens) && accumulatedTotalProcessedTokens > lastGoodUsage.usedTokens ? { totalProcessedTokens: accumulatedTotalProcessedTokens, } : {}), } : accumulatedSnapshot; const turnState = context.turnState; if (!turnState) { if (usageSnapshot) { const usageStamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "thread.token-usage.updated", eventId: usageStamp.eventId, provider: PROVIDER, createdAt: usageStamp.createdAt, threadId: context.session.threadId, payload: { usage: usageSnapshot, }, providerRefs: {}, }); } const stamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "turn.completed", eventId: stamp.eventId, provider: PROVIDER, createdAt: stamp.createdAt, threadId: context.session.threadId, payload: { state: status, ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}), ...(result?.usage ? { usage: result.usage } : {}), ...(result?.modelUsage ? { modelUsage: result.modelUsage } : {}), ...(typeof result?.total_cost_usd === "number" ? { totalCostUsd: result.total_cost_usd } : {}), ...(errorMessage ? { errorMessage } : {}), }, providerRefs: {}, }); return; } for (const [index, tool] of context.inFlightTools.entries()) { const toolStamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "item.completed", eventId: toolStamp.eventId, provider: PROVIDER, createdAt: toolStamp.createdAt, threadId: context.session.threadId, turnId: turnState.turnId, itemId: asRuntimeItemId(tool.itemId), payload: { itemType: tool.itemType, status: status === "completed" ? "completed" : "failed", title: tool.title, ...(tool.detail ? { detail: tool.detail } : {}), data: { toolName: tool.toolName, input: tool.input, }, }, providerRefs: nativeProviderRefs(context, { providerItemId: tool.itemId, }), raw: { source: "claude.sdk.message", method: "claude/result", payload: result ?? { status }, }, }); context.inFlightTools.delete(index); } // Clear any remaining stale entries (e.g. from interrupted content blocks) context.inFlightTools.clear(); for (const block of turnState.assistantTextBlockOrder) { yield* completeAssistantTextBlock(context, block, { force: true, rawMethod: "claude/result", rawPayload: result ?? { status }, }); } context.turns.push({ id: turnState.turnId, items: [...turnState.items], }); if (usageSnapshot) { const usageStamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "thread.token-usage.updated", eventId: usageStamp.eventId, provider: PROVIDER, createdAt: usageStamp.createdAt, threadId: context.session.threadId, turnId: turnState.turnId, payload: { usage: usageSnapshot, }, providerRefs: nativeProviderRefs(context), }); } const stamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "turn.completed", eventId: stamp.eventId, provider: PROVIDER, createdAt: stamp.createdAt, threadId: context.session.threadId, turnId: turnState.turnId, payload: { state: status, ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}), ...(result?.usage ? { usage: result.usage } : {}), ...(result?.modelUsage ? { modelUsage: result.modelUsage } : {}), ...(typeof result?.total_cost_usd === "number" ? { totalCostUsd: result.total_cost_usd } : {}), ...(errorMessage ? { errorMessage } : {}), }, providerRefs: nativeProviderRefs(context), }); const updatedAt = yield* nowIso; context.turnState = undefined; context.session = { ...context.session, status: "ready", activeTurnId: undefined, updatedAt, ...(status === "failed" && errorMessage ? { lastError: errorMessage } : {}), }; yield* updateResumeCursor(context); }); const handleStreamEvent = Effect.fn("handleStreamEvent")(function* ( context: ClaudeSessionContext, message: SDKMessage, ) { if (message.type !== "stream_event") { return; } const { event } = message; if (event.type === "content_block_delta") { if ( (event.delta.type === "text_delta" || event.delta.type === "thinking_delta") && context.turnState ) { const deltaText = event.delta.type === "text_delta" ? event.delta.text : typeof event.delta.thinking === "string" ? event.delta.thinking : ""; if (deltaText.length === 0) { return; } const streamKind = streamKindFromDeltaType(event.delta.type); const assistantBlockEntry = event.delta.type === "text_delta" ? yield* ensureAssistantTextBlock(context, event.index) : context.turnState.assistantTextBlocks.get(event.index) ? { blockIndex: event.index, block: context.turnState.assistantTextBlocks.get( event.index, ) as AssistantTextBlockState, } : undefined; if (assistantBlockEntry?.block && event.delta.type === "text_delta") { assistantBlockEntry.block.emittedTextDelta = true; } const stamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "content.delta", eventId: stamp.eventId, provider: PROVIDER, createdAt: stamp.createdAt, threadId: context.session.threadId, turnId: context.turnState.turnId, ...(assistantBlockEntry?.block ? { itemId: asRuntimeItemId(assistantBlockEntry.block.itemId), } : {}), payload: { streamKind, delta: deltaText, }, providerRefs: nativeProviderRefs(context), raw: { source: "claude.sdk.message", method: "claude/stream_event/content_block_delta", payload: message, }, }); return; } if (event.delta.type === "input_json_delta") { const tool = context.inFlightTools.get(event.index); if (!tool || typeof event.delta.partial_json !== "string") { return; } const partialInputJson = tool.partialInputJson + event.delta.partial_json; const parsedInput = tryParseJsonRecord(partialInputJson); const detail = parsedInput ? summarizeToolRequest(tool.toolName, parsedInput) : tool.detail; let nextTool: ToolInFlight = { ...tool, partialInputJson, ...(parsedInput ? { input: parsedInput } : {}), ...(detail ? { detail } : {}), }; const nextFingerprint = parsedInput && Object.keys(parsedInput).length > 0 ? toolInputFingerprint(parsedInput) : undefined; context.inFlightTools.set(event.index, nextTool); if ( !parsedInput || !nextFingerprint || tool.lastEmittedInputFingerprint === nextFingerprint ) { return; } nextTool = { ...nextTool, lastEmittedInputFingerprint: nextFingerprint, }; context.inFlightTools.set(event.index, nextTool); const stamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "item.updated", eventId: stamp.eventId, provider: PROVIDER, createdAt: stamp.createdAt, threadId: context.session.threadId, ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId), } : {}), itemId: asRuntimeItemId(nextTool.itemId), payload: { itemType: nextTool.itemType, status: "inProgress", title: nextTool.title, ...(nextTool.detail ? { detail: nextTool.detail } : {}), data: { toolName: nextTool.toolName, input: nextTool.input, }, }, providerRefs: nativeProviderRefs(context, { providerItemId: nextTool.itemId, }), raw: { source: "claude.sdk.message", method: "claude/stream_event/content_block_delta/input_json_delta", payload: message, }, }); // Emit plan update when TodoWrite input is parsed if (parsedInput && isTodoTool(nextTool.toolName)) { const planSteps = extractPlanStepsFromTodoInput(parsedInput); if (planSteps && planSteps.length > 0) { const planStamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "turn.plan.updated", eventId: planStamp.eventId, provider: PROVIDER, createdAt: planStamp.createdAt, threadId: context.session.threadId, ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId), } : {}), payload: { plan: planSteps, }, providerRefs: nativeProviderRefs(context), }); } } } return; } if (event.type === "content_block_start") { const { index, content_block: block } = event; if (block.type === "text") { yield* ensureAssistantTextBlock(context, index, { fallbackText: extractContentBlockText(block), }); return; } if ( block.type !== "tool_use" && block.type !== "server_tool_use" && block.type !== "mcp_tool_use" ) { return; } const toolName = block.name; const itemType = classifyToolItemType(toolName); const toolInput = typeof block.input === "object" && block.input !== null ? (block.input as Record) : {}; const itemId = block.id; const detail = summarizeToolRequest(toolName, toolInput); const inputFingerprint = Object.keys(toolInput).length > 0 ? toolInputFingerprint(toolInput) : undefined; const tool: ToolInFlight = { itemId, itemType, toolName, title: titleForTool(itemType), detail, input: toolInput, partialInputJson: "", ...(inputFingerprint ? { lastEmittedInputFingerprint: inputFingerprint } : {}), }; context.inFlightTools.set(index, tool); const stamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "item.started", eventId: stamp.eventId, provider: PROVIDER, createdAt: stamp.createdAt, threadId: context.session.threadId, ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), itemId: asRuntimeItemId(tool.itemId), payload: { itemType: tool.itemType, status: "inProgress", title: tool.title, ...(tool.detail ? { detail: tool.detail } : {}), data: { toolName: tool.toolName, input: toolInput, }, }, providerRefs: nativeProviderRefs(context, { providerItemId: tool.itemId, }), raw: { source: "claude.sdk.message", method: "claude/stream_event/content_block_start", payload: message, }, }); return; } if (event.type === "content_block_stop") { const { index } = event; const assistantBlock = context.turnState?.assistantTextBlocks.get(index); if (assistantBlock) { assistantBlock.streamClosed = true; yield* completeAssistantTextBlock(context, assistantBlock, { rawMethod: "claude/stream_event/content_block_stop", rawPayload: message, }); return; } const tool = context.inFlightTools.get(index); if (!tool) { return; } } }); const handleUserMessage = Effect.fn("handleUserMessage")(function* ( context: ClaudeSessionContext, message: SDKMessage, ) { if (message.type !== "user") { return; } if (context.turnState) { context.turnState.items.push(message.message); } for (const toolResult of toolResultBlocksFromUserMessage(message)) { const toolEntry = Array.from(context.inFlightTools.entries()).find( ([, tool]) => tool.itemId === toolResult.toolUseId, ); if (!toolEntry) { continue; } const [index, tool] = toolEntry; const itemStatus = toolResult.isError ? "failed" : "completed"; const toolData = { toolName: tool.toolName, input: tool.input, result: toolResult.block, }; const updatedStamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "item.updated", eventId: updatedStamp.eventId, provider: PROVIDER, createdAt: updatedStamp.createdAt, threadId: context.session.threadId, ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), itemId: asRuntimeItemId(tool.itemId), payload: { itemType: tool.itemType, status: toolResult.isError ? "failed" : "inProgress", title: tool.title, ...(tool.detail ? { detail: tool.detail } : {}), data: toolData, }, providerRefs: nativeProviderRefs(context, { providerItemId: tool.itemId, }), raw: { source: "claude.sdk.message", method: "claude/user", payload: message, }, }); const streamKind = toolResultStreamKind(tool.itemType); if (streamKind && toolResult.text.length > 0 && context.turnState) { const deltaStamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "content.delta", eventId: deltaStamp.eventId, provider: PROVIDER, createdAt: deltaStamp.createdAt, threadId: context.session.threadId, turnId: context.turnState.turnId, itemId: asRuntimeItemId(tool.itemId), payload: { streamKind, delta: toolResult.text, }, providerRefs: nativeProviderRefs(context, { providerItemId: tool.itemId, }), raw: { source: "claude.sdk.message", method: "claude/user", payload: message, }, }); } const completedStamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "item.completed", eventId: completedStamp.eventId, provider: PROVIDER, createdAt: completedStamp.createdAt, threadId: context.session.threadId, ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), itemId: asRuntimeItemId(tool.itemId), payload: { itemType: tool.itemType, status: itemStatus, title: tool.title, ...(tool.detail ? { detail: tool.detail } : {}), data: toolData, }, providerRefs: nativeProviderRefs(context, { providerItemId: tool.itemId, }), raw: { source: "claude.sdk.message", method: "claude/user", payload: message, }, }); context.inFlightTools.delete(index); } }); const handleAssistantMessage = Effect.fn("handleAssistantMessage")(function* ( context: ClaudeSessionContext, message: SDKMessage, ) { if (message.type !== "assistant") { return; } // Auto-start a synthetic turn for assistant messages that arrive without // an active turn (e.g., background agent/subagent responses between user prompts). if (!context.turnState) { const turnId = TurnId.make(yield* Random.nextUUIDv4); const startedAt = yield* nowIso; context.turnState = { turnId, startedAt, items: [], assistantTextBlocks: new Map(), assistantTextBlockOrder: [], capturedProposedPlanKeys: new Set(), nextSyntheticAssistantBlockIndex: -1, }; context.session = { ...context.session, status: "running", activeTurnId: turnId, updatedAt: startedAt, }; const turnStartedStamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "turn.started", eventId: turnStartedStamp.eventId, provider: PROVIDER, createdAt: turnStartedStamp.createdAt, threadId: context.session.threadId, turnId, payload: {}, providerRefs: { ...nativeProviderRefs(context), providerTurnId: turnId, }, raw: { source: "claude.sdk.message", method: "claude/synthetic-turn-start", payload: {}, }, }); } const content = message.message?.content; if (Array.isArray(content)) { for (const block of content) { if (!block || typeof block !== "object") { continue; } const toolUse = block as { type?: unknown; id?: unknown; name?: unknown; input?: unknown; }; if (toolUse.type !== "tool_use" || toolUse.name !== "ExitPlanMode") { continue; } const planMarkdown = extractExitPlanModePlan(toolUse.input); if (!planMarkdown) { continue; } yield* emitProposedPlanCompleted(context, { planMarkdown, toolUseId: typeof toolUse.id === "string" ? toolUse.id : undefined, rawSource: "claude.sdk.message", rawMethod: "claude/assistant", rawPayload: message, }); } } if (context.turnState) { context.turnState.items.push(message.message); yield* backfillAssistantTextBlocksFromSnapshot(context, message); } context.lastAssistantUuid = message.uuid; yield* updateResumeCursor(context); }); const handleResultMessage = Effect.fn("handleResultMessage")(function* ( context: ClaudeSessionContext, message: SDKMessage, ) { if (message.type !== "result") { return; } const status = turnStatusFromResult(message); const errorMessage = message.subtype === "success" ? undefined : message.errors[0]; if (status === "failed") { yield* emitRuntimeError(context, errorMessage ?? "Claude turn failed."); } yield* completeTurn(context, status, errorMessage, message); }); const handleSystemMessage = Effect.fn("handleSystemMessage")(function* ( context: ClaudeSessionContext, message: SDKMessage, ) { if (message.type !== "system") { return; } const stamp = yield* makeEventStamp(); const base = { eventId: stamp.eventId, provider: PROVIDER, createdAt: stamp.createdAt, threadId: context.session.threadId, ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), providerRefs: nativeProviderRefs(context), raw: { source: "claude.sdk.message" as const, method: sdkNativeMethod(message), messageType: `${message.type}:${message.subtype}`, payload: message, }, }; switch (message.subtype) { case "init": yield* offerRuntimeEvent({ ...base, type: "session.configured", payload: { config: message as Record, }, }); return; case "status": yield* offerRuntimeEvent({ ...base, type: "session.state.changed", payload: { state: message.status === "compacting" ? "waiting" : "running", reason: `status:${message.status ?? "active"}`, detail: message, }, }); return; case "compact_boundary": yield* offerRuntimeEvent({ ...base, type: "thread.state.changed", payload: { state: "compacted", detail: message, }, }); return; case "hook_started": yield* offerRuntimeEvent({ ...base, type: "hook.started", payload: { hookId: message.hook_id, hookName: message.hook_name, hookEvent: message.hook_event, }, }); return; case "hook_progress": yield* offerRuntimeEvent({ ...base, type: "hook.progress", payload: { hookId: message.hook_id, output: message.output, stdout: message.stdout, stderr: message.stderr, }, }); return; case "hook_response": yield* offerRuntimeEvent({ ...base, type: "hook.completed", payload: { hookId: message.hook_id, outcome: message.outcome, output: message.output, stdout: message.stdout, stderr: message.stderr, ...(typeof message.exit_code === "number" ? { exitCode: message.exit_code } : {}), }, }); return; case "task_started": yield* offerRuntimeEvent({ ...base, type: "task.started", payload: { taskId: RuntimeTaskId.make(message.task_id), description: message.description, ...(message.task_type ? { taskType: message.task_type } : {}), }, }); return; case "task_progress": if (message.usage) { const normalizedUsage = normalizeClaudeTokenUsage( message.usage, context.lastKnownContextWindow, ); if (normalizedUsage) { context.lastKnownTokenUsage = normalizedUsage; const usageStamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ ...base, eventId: usageStamp.eventId, createdAt: usageStamp.createdAt, type: "thread.token-usage.updated", payload: { usage: normalizedUsage, }, }); } } yield* offerRuntimeEvent({ ...base, type: "task.progress", payload: { taskId: RuntimeTaskId.make(message.task_id), description: message.description, ...(message.summary ? { summary: message.summary } : {}), ...(message.usage ? { usage: message.usage } : {}), ...(message.last_tool_name ? { lastToolName: message.last_tool_name } : {}), }, }); return; case "task_notification": if (message.usage) { const normalizedUsage = normalizeClaudeTokenUsage( message.usage, context.lastKnownContextWindow, ); if (normalizedUsage) { context.lastKnownTokenUsage = normalizedUsage; const usageStamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ ...base, eventId: usageStamp.eventId, createdAt: usageStamp.createdAt, type: "thread.token-usage.updated", payload: { usage: normalizedUsage, }, }); } } yield* offerRuntimeEvent({ ...base, type: "task.completed", payload: { taskId: RuntimeTaskId.make(message.task_id), status: message.status, ...(message.summary ? { summary: message.summary } : {}), ...(message.usage ? { usage: message.usage } : {}), }, }); return; case "files_persisted": yield* offerRuntimeEvent({ ...base, type: "files.persisted", payload: { files: Array.isArray(message.files) ? message.files.map((file: { filename: string; file_id: string }) => ({ filename: file.filename, fileId: file.file_id, })) : [], ...(Array.isArray(message.failed) ? { failed: message.failed.map((entry: { filename: string; error: string }) => ({ filename: entry.filename, error: entry.error, })), } : {}), }, }); return; default: yield* emitRuntimeWarning( context, `Unhandled Claude system message subtype '${message.subtype}'.`, message, ); return; } }); const handleSdkTelemetryMessage = Effect.fn("handleSdkTelemetryMessage")(function* ( context: ClaudeSessionContext, message: SDKMessage, ) { const stamp = yield* makeEventStamp(); const base = { eventId: stamp.eventId, provider: PROVIDER, createdAt: stamp.createdAt, threadId: context.session.threadId, ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), providerRefs: nativeProviderRefs(context), raw: { source: "claude.sdk.message" as const, method: sdkNativeMethod(message), messageType: message.type, payload: message, }, }; if (message.type === "tool_progress") { yield* offerRuntimeEvent({ ...base, type: "tool.progress", payload: { toolUseId: message.tool_use_id, toolName: message.tool_name, elapsedSeconds: message.elapsed_time_seconds, ...(message.task_id ? { summary: `task:${message.task_id}` } : {}), }, }); return; } if (message.type === "tool_use_summary") { yield* offerRuntimeEvent({ ...base, type: "tool.summary", payload: { summary: message.summary, ...(message.preceding_tool_use_ids.length > 0 ? { precedingToolUseIds: message.preceding_tool_use_ids, } : {}), }, }); return; } if (message.type === "auth_status") { yield* offerRuntimeEvent({ ...base, type: "auth.status", payload: { isAuthenticating: message.isAuthenticating, output: message.output, ...(message.error ? { error: message.error } : {}), }, }); return; } if (message.type === "rate_limit_event") { yield* offerRuntimeEvent({ ...base, type: "account.rate-limits.updated", payload: { rateLimits: message, }, }); return; } }); const handleSdkMessage = Effect.fn("handleSdkMessage")(function* ( context: ClaudeSessionContext, message: SDKMessage, ) { yield* logNativeSdkMessage(context, message); yield* ensureThreadId(context, message); switch (message.type) { case "stream_event": yield* handleStreamEvent(context, message); return; case "user": yield* handleUserMessage(context, message); return; case "assistant": yield* handleAssistantMessage(context, message); return; case "result": yield* handleResultMessage(context, message); return; case "system": yield* handleSystemMessage(context, message); return; case "tool_progress": case "tool_use_summary": case "auth_status": case "rate_limit_event": yield* handleSdkTelemetryMessage(context, message); return; default: yield* emitRuntimeWarning( context, `Unhandled Claude SDK message type '${message.type}'.`, message, ); return; } }); const runSdkStream = ( context: ClaudeSessionContext, ): Effect.Effect => Stream.fromAsyncIterable(context.query, (cause) => toProcessError(cause, "Claude runtime stream failed.", context.session.threadId), ).pipe( Stream.takeWhile(() => !context.stopped), Stream.runForEach((message) => handleSdkMessage(context, message)), ); const handleStreamExit = Effect.fn("handleStreamExit")(function* ( context: ClaudeSessionContext, exit: Exit.Exit, ) { if (context.stopped) { return; } if (Exit.isFailure(exit)) { if (isClaudeInterruptedCause(exit.cause)) { if (context.turnState) { yield* completeTurn( context, "interrupted", interruptionMessageFromClaudeCause(exit.cause), ); } } else { const message = messageFromClaudeStreamCause(exit.cause, "Claude runtime stream failed."); yield* emitRuntimeError(context, message, Cause.pretty(exit.cause)); yield* completeTurn(context, "failed", message); } } else if (context.turnState) { yield* completeTurn(context, "interrupted", "Claude runtime stream ended."); } yield* stopSessionInternal(context, { emitExitEvent: true, }); }); const stopSessionInternal = Effect.fn("stopSessionInternal")(function* ( context: ClaudeSessionContext, options?: { readonly emitExitEvent?: boolean }, ) { if (context.stopped) return; context.stopped = true; for (const [requestId, pending] of context.pendingApprovals) { yield* Deferred.succeed(pending.decision, "cancel"); const stamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "request.resolved", eventId: stamp.eventId, provider: PROVIDER, createdAt: stamp.createdAt, threadId: context.session.threadId, ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), requestId: asRuntimeRequestId(requestId), payload: { requestType: pending.requestType, decision: "cancel", }, providerRefs: nativeProviderRefs(context), }); } context.pendingApprovals.clear(); if (context.turnState) { yield* completeTurn(context, "interrupted", "Session stopped."); } yield* Queue.shutdown(context.promptQueue); const streamFiber = context.streamFiber; context.streamFiber = undefined; if (streamFiber && streamFiber.pollUnsafe() === undefined) { yield* Fiber.interrupt(streamFiber); } yield* Effect.try({ try: () => context.query.close(), catch: (cause) => new ProviderAdapterProcessError({ provider: PROVIDER, threadId: context.session.threadId, detail: toMessage(cause, "Failed to close Claude runtime query."), cause, }), }).pipe( Effect.catch((cause) => emitRuntimeError(context, "Failed to close Claude runtime query.", cause), ), ); const updatedAt = yield* nowIso; context.session = { ...context.session, status: "closed", activeTurnId: undefined, updatedAt, }; if (options?.emitExitEvent !== false) { const stamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "session.exited", eventId: stamp.eventId, provider: PROVIDER, createdAt: stamp.createdAt, threadId: context.session.threadId, payload: { reason: "Session stopped", exitKind: "graceful", }, providerRefs: {}, }); } sessions.delete(context.session.threadId); }); const requireSession = ( threadId: ThreadId, ): Effect.Effect => { const context = sessions.get(threadId); if (!context) { return Effect.fail( new ProviderAdapterSessionNotFoundError({ provider: PROVIDER, threadId, }), ); } if (context.stopped || context.session.status === "closed") { return Effect.fail( new ProviderAdapterSessionClosedError({ provider: PROVIDER, threadId, }), ); } return Effect.succeed(context); }; const startSession: ClaudeAdapterShape["startSession"] = Effect.fn("startSession")( function* (input) { if (input.provider !== undefined && input.provider !== PROVIDER) { return yield* new ProviderAdapterValidationError({ provider: PROVIDER, operation: "startSession", issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`, }); } const existingContext = sessions.get(input.threadId); if (existingContext) { yield* Effect.logWarning("claude.session.replacing", { threadId: input.threadId, existingSessionStatus: existingContext.session.status, reason: "startSession called with existing active session", }); yield* stopSessionInternal(existingContext, { emitExitEvent: false, }).pipe( // Replacement cleanup is best-effort: never block the new session on // either typed failures or unexpected defects from tearing down the old one. Effect.catchCause((cause) => Effect.logWarning("claude.session.replace.stop-failed", { threadId: input.threadId, cause, }), ), ); } const startedAt = yield* nowIso; const resumeState = readClaudeResumeState(input.resumeCursor); const threadId = input.threadId; const existingResumeSessionId = resumeState?.resume; const newSessionId = existingResumeSessionId === undefined ? yield* Random.nextUUIDv4 : undefined; const sessionId = existingResumeSessionId ?? newSessionId; const runtimeContext = yield* Effect.context(); const runFork = Effect.runForkWith(runtimeContext); const runPromise = Effect.runPromiseWith(runtimeContext); const promptQueue = yield* Queue.unbounded(); const prompt = Stream.fromQueue(promptQueue).pipe( Stream.filter((item) => item.type === "message"), Stream.map((item) => item.message), Stream.catchCause((cause) => Cause.hasInterruptsOnly(cause) ? Stream.empty : Stream.failCause(cause), ), Stream.toAsyncIterable, ); const pendingApprovals = new Map(); const pendingUserInputs = new Map(); const inFlightTools = new Map(); const contextRef = yield* Ref.make(undefined); /** * Handle AskUserQuestion tool calls by emitting a `user-input.requested` * runtime event and waiting for the user to respond via `respondToUserInput`. */ const handleAskUserQuestion = Effect.fn("handleAskUserQuestion")(function* ( context: ClaudeSessionContext, toolInput: Record, callbackOptions: { readonly signal: AbortSignal; readonly toolUseID?: string; }, ) { const requestId = ApprovalRequestId.make(yield* Random.nextUUIDv4); // Parse questions from the SDK's AskUserQuestion input. // `id` MUST equal the full question text — Claude SDK >= 2.1.121 looks // up answers by question text in `mapToolResultToToolResultBlockParam`, // so the key the UI uses to keep its draft answer must match the SDK's // expected lookup key. See https://github.com/pingdotgg/t3code/issues/2388 const rawQuestions = Array.isArray(toolInput.questions) ? toolInput.questions : []; const questions: Array = rawQuestions.map( (q: Record, idx: number) => ({ id: typeof q.question === "string" && q.question.length > 0 ? q.question : `q-${idx}`, header: typeof q.header === "string" ? q.header : `Question ${idx + 1}`, question: typeof q.question === "string" ? q.question : "", options: Array.isArray(q.options) ? q.options.map((opt: Record) => ({ label: typeof opt.label === "string" ? opt.label : "", description: typeof opt.description === "string" ? opt.description : "", })) : [], multiSelect: typeof q.multiSelect === "boolean" ? q.multiSelect : false, }), ); const answersDeferred = yield* Deferred.make(); let aborted = false; const pendingInput: PendingUserInput = { questions, answers: answersDeferred, }; // Emit user-input.requested so the UI can present the questions. const requestedStamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "user-input.requested", eventId: requestedStamp.eventId, provider: PROVIDER, createdAt: requestedStamp.createdAt, threadId: context.session.threadId, ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId), } : {}), requestId: asRuntimeRequestId(requestId), payload: { questions }, providerRefs: nativeProviderRefs(context, { providerItemId: callbackOptions.toolUseID, }), raw: { source: "claude.sdk.permission", method: "canUseTool/AskUserQuestion", payload: { toolName: "AskUserQuestion", input: toolInput, }, }, }); pendingUserInputs.set(requestId, pendingInput); // Handle abort (e.g. turn interrupted while waiting for user input). const onAbort = () => { if (!pendingUserInputs.has(requestId)) { return; } aborted = true; pendingUserInputs.delete(requestId); runFork(Deferred.succeed(answersDeferred, {} as ProviderUserInputAnswers)); }; callbackOptions.signal.addEventListener("abort", onAbort, { once: true, }); // Block until the user provides answers. const answers = yield* Deferred.await(answersDeferred); pendingUserInputs.delete(requestId); // Emit user-input.resolved so the UI knows the interaction completed. const resolvedStamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "user-input.resolved", eventId: resolvedStamp.eventId, provider: PROVIDER, createdAt: resolvedStamp.createdAt, threadId: context.session.threadId, ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId), } : {}), requestId: asRuntimeRequestId(requestId), payload: { answers }, providerRefs: nativeProviderRefs(context, { providerItemId: callbackOptions.toolUseID, }), raw: { source: "claude.sdk.permission", method: "canUseTool/AskUserQuestion/resolved", payload: { answers }, }, }); if (aborted) { return { behavior: "deny", message: "User cancelled tool execution.", } satisfies PermissionResult; } // Return the answers to the SDK in the expected format: // { questions: [...], answers: { questionText: selectedLabel } } return { behavior: "allow", updatedInput: { questions: toolInput.questions, answers, }, } satisfies PermissionResult; }); const canUseToolEffect = Effect.fn("canUseTool")(function* ( toolName: Parameters[0], toolInput: Parameters[1], callbackOptions: Parameters[2], ) { const context = yield* Ref.get(contextRef); if (!context) { return { behavior: "deny", message: "Claude session context is unavailable.", } satisfies PermissionResult; } // Handle AskUserQuestion: surface clarifying questions to the // user via the user-input runtime event channel, regardless of // runtime mode (plan mode relies on this heavily). if (toolName === "AskUserQuestion") { return yield* handleAskUserQuestion(context, toolInput, callbackOptions); } if (toolName === "ExitPlanMode") { const planMarkdown = extractExitPlanModePlan(toolInput); if (planMarkdown) { yield* emitProposedPlanCompleted(context, { planMarkdown, toolUseId: callbackOptions.toolUseID, rawSource: "claude.sdk.permission", rawMethod: "canUseTool/ExitPlanMode", rawPayload: { toolName, input: toolInput, }, }); } return { behavior: "deny", message: "The client captured your proposed plan. Stop here and wait for the user's feedback or implementation request in a later turn.", } satisfies PermissionResult; } const runtimeMode = input.runtimeMode ?? "full-access"; if (runtimeMode === "full-access") { return { behavior: "allow", updatedInput: toolInput, } satisfies PermissionResult; } const requestId = ApprovalRequestId.make(yield* Random.nextUUIDv4); const requestType = classifyRequestType(toolName); const detail = summarizeToolRequest(toolName, toolInput); const decisionDeferred = yield* Deferred.make(); const pendingApproval: PendingApproval = { requestType, detail, decision: decisionDeferred, ...(callbackOptions.suggestions ? { suggestions: callbackOptions.suggestions } : {}), }; const requestedStamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "request.opened", eventId: requestedStamp.eventId, provider: PROVIDER, createdAt: requestedStamp.createdAt, threadId: context.session.threadId, ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), requestId: asRuntimeRequestId(requestId), payload: { requestType, detail, args: { toolName, input: toolInput, ...(callbackOptions.toolUseID ? { toolUseId: callbackOptions.toolUseID } : {}), }, }, providerRefs: nativeProviderRefs(context, { providerItemId: callbackOptions.toolUseID, }), raw: { source: "claude.sdk.permission", method: "canUseTool/request", payload: { toolName, input: toolInput, }, }, }); pendingApprovals.set(requestId, pendingApproval); const onAbort = () => { if (!pendingApprovals.has(requestId)) { return; } pendingApprovals.delete(requestId); runFork(Deferred.succeed(decisionDeferred, "cancel")); }; callbackOptions.signal.addEventListener("abort", onAbort, { once: true, }); const decision = yield* Deferred.await(decisionDeferred); pendingApprovals.delete(requestId); const resolvedStamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "request.resolved", eventId: resolvedStamp.eventId, provider: PROVIDER, createdAt: resolvedStamp.createdAt, threadId: context.session.threadId, ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), requestId: asRuntimeRequestId(requestId), payload: { requestType, decision, }, providerRefs: nativeProviderRefs(context, { providerItemId: callbackOptions.toolUseID, }), raw: { source: "claude.sdk.permission", method: "canUseTool/decision", payload: { decision, }, }, }); if (decision === "accept" || decision === "acceptForSession") { return { behavior: "allow", updatedInput: toolInput, ...(decision === "acceptForSession" && pendingApproval.suggestions ? { updatedPermissions: [...pendingApproval.suggestions], } : {}), } satisfies PermissionResult; } return { behavior: "deny", message: decision === "cancel" ? "User cancelled tool execution." : "User declined tool execution.", } satisfies PermissionResult; }); const canUseTool: CanUseTool = (toolName, toolInput, callbackOptions) => runPromise(canUseToolEffect(toolName, toolInput, callbackOptions)); const claudeBinaryPath = claudeSettings.binaryPath; const extraArgs = parseCliArgs(claudeSettings.launchArgs).flags; const modelSelection = input.modelSelection?.instanceId === boundInstanceId ? input.modelSelection : undefined; const caps = getClaudeModelCapabilities(modelSelection?.model); const descriptors = getProviderOptionDescriptors({ caps }); const apiModelId = modelSelection ? resolveClaudeApiModelId(modelSelection) : undefined; const rawEffort = getModelSelectionStringOptionValue(modelSelection, "effort"); const effort = resolveClaudeEffort(caps, rawEffort) ?? null; const fastModeSupported = descriptors.some( (descriptor) => descriptor.type === "boolean" && descriptor.id === "fastMode", ); const thinkingSupported = descriptors.some( (descriptor) => descriptor.type === "boolean" && descriptor.id === "thinking", ); const fastMode = getModelSelectionBooleanOptionValue(modelSelection, "fastMode") === true && fastModeSupported; const thinking = thinkingSupported ? getModelSelectionBooleanOptionValue(modelSelection, "thinking") : undefined; const effectiveEffort = getEffectiveClaudeAgentEffort(effort); const runtimeModeToPermission: Record = { "auto-accept-edits": "acceptEdits", "full-access": "bypassPermissions", }; const permissionMode = runtimeModeToPermission[input.runtimeMode]; const settings = { ...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}), ...(fastMode ? { fastMode: true } : {}), }; const queryOptions: ClaudeQueryOptions = { ...(input.cwd ? { cwd: input.cwd } : {}), ...(apiModelId ? { model: apiModelId } : {}), pathToClaudeCodeExecutable: claudeBinaryPath, systemPrompt: { type: "preset", preset: "claude_code" }, settingSources: [...CLAUDE_SETTING_SOURCES], // The SDK type lags the CLI here: Opus 4.7 accepts `xhigh` even though // the published `Options["effort"]` union currently stops at `max`. ...(effectiveEffort ? { effort: effectiveEffort as unknown as NonNullable, } : {}), ...(permissionMode ? { permissionMode } : {}), ...(permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {}), ...(Object.keys(settings).length > 0 ? { settings } : {}), ...(existingResumeSessionId ? { resume: existingResumeSessionId } : {}), ...(newSessionId ? { sessionId: newSessionId } : {}), includePartialMessages: true, canUseTool, env: claudeEnvironment, ...(input.cwd ? { additionalDirectories: [input.cwd] } : {}), ...(Object.keys(extraArgs).length > 0 ? { extraArgs } : {}), }; yield* Effect.annotateCurrentSpan({ "provider.kind": PROVIDER, "provider.thread_id": threadId, "provider.runtime_mode": input.runtimeMode, "claude.resume.source": existingResumeSessionId !== undefined ? "resume-session" : "generated-session", "claude.resume.thread_id": resumeState?.threadId ?? "", "claude.resume.session_id": existingResumeSessionId ?? "", "claude.resume.session_at": resumeState?.resumeSessionAt ?? "", "claude.resume.turn_count": resumeState?.turnCount ?? -1, "claude.query.cwd": input.cwd ?? "", "claude.query.model": apiModelId ?? "", "claude.query.effort": effectiveEffort ?? "", "claude.query.permission_mode": permissionMode ?? "", "claude.query.allow_dangerously_skip_permissions": permissionMode === "bypassPermissions", "claude.query.resume": existingResumeSessionId ?? "", "claude.query.session_id": newSessionId ?? "", "claude.query.include_partial_messages": true, "claude.query.additional_directories": input.cwd ? [input.cwd] : [], "claude.query.setting_sources": [...CLAUDE_SETTING_SOURCES], "claude.query.settings_json": encodeJsonStringForDiagnostics(settings) ?? "", "claude.query.extra_args_json": encodeJsonStringForDiagnostics(extraArgs) ?? "", "claude.query.path_to_executable": claudeBinaryPath, }); const queryRuntime = yield* Effect.try({ try: () => createQuery({ prompt, options: queryOptions, }), catch: (cause) => new ProviderAdapterProcessError({ provider: PROVIDER, threadId, detail: toMessage(cause, "Failed to start Claude runtime session."), cause, }), }); const session: ProviderSession = { threadId, provider: PROVIDER, providerInstanceId: boundInstanceId, status: "ready", runtimeMode: input.runtimeMode, ...(input.cwd ? { cwd: input.cwd } : {}), ...(modelSelection?.model ? { model: modelSelection.model } : {}), ...(threadId ? { threadId } : {}), resumeCursor: { ...(threadId ? { threadId } : {}), ...(sessionId ? { resume: sessionId } : {}), ...(resumeState?.resumeSessionAt ? { resumeSessionAt: resumeState.resumeSessionAt } : {}), turnCount: resumeState?.turnCount ?? 0, }, createdAt: startedAt, updatedAt: startedAt, }; const context: ClaudeSessionContext = { session, promptQueue, query: queryRuntime, streamFiber: undefined, startedAt, basePermissionMode: permissionMode, currentApiModelId: apiModelId, resumeSessionId: sessionId, pendingApprovals, pendingUserInputs, turns: [], inFlightTools, turnState: undefined, lastKnownContextWindow: undefined, lastKnownTokenUsage: undefined, lastAssistantUuid: resumeState?.resumeSessionAt, lastThreadStartedId: undefined, stopped: false, }; yield* Ref.set(contextRef, context); sessions.set(threadId, context); const sessionStartedStamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "session.started", eventId: sessionStartedStamp.eventId, provider: PROVIDER, createdAt: sessionStartedStamp.createdAt, threadId, payload: input.resumeCursor !== undefined ? { resume: input.resumeCursor } : {}, providerRefs: {}, }); const configuredStamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "session.configured", eventId: configuredStamp.eventId, provider: PROVIDER, createdAt: configuredStamp.createdAt, threadId, payload: { config: { ...(apiModelId ? { model: apiModelId } : {}), ...(input.cwd ? { cwd: input.cwd } : {}), ...(effectiveEffort ? { effort: effectiveEffort } : {}), ...(permissionMode ? { permissionMode } : {}), ...(fastMode ? { fastMode: true } : {}), }, }, providerRefs: {}, }); const readyStamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "session.state.changed", eventId: readyStamp.eventId, provider: PROVIDER, createdAt: readyStamp.createdAt, threadId, payload: { state: "ready", }, providerRefs: {}, }); let streamFiber: Fiber.Fiber; streamFiber = runFork( Effect.exit(runSdkStream(context)).pipe( Effect.flatMap((exit) => { if (context.stopped) { return Effect.void; } if (context.streamFiber === streamFiber) { context.streamFiber = undefined; } return handleStreamExit(context, exit); }), ), ); context.streamFiber = streamFiber; streamFiber.addObserver(() => { if (context.streamFiber === streamFiber) { context.streamFiber = undefined; } }); return { ...session, }; }, ); const sendTurn: ClaudeAdapterShape["sendTurn"] = Effect.fn("sendTurn")(function* (input) { const context = yield* requireSession(input.threadId); const modelSelection = input.modelSelection !== undefined && input.modelSelection.instanceId === boundInstanceId ? input.modelSelection : undefined; if (context.turnState) { // Auto-close a stale synthetic turn (from background agent responses // between user prompts) to prevent blocking the user's next turn. yield* completeTurn(context, "completed"); } if (modelSelection?.model) { const apiModelId = resolveClaudeApiModelId(modelSelection); if (context.currentApiModelId !== apiModelId) { yield* Effect.tryPromise({ try: () => context.query.setModel(apiModelId), catch: (cause) => toRequestError(input.threadId, "turn/setModel", cause), }); context.currentApiModelId = apiModelId; } context.session = { ...context.session, model: modelSelection.model, }; } // Apply interaction mode by switching the SDK's permission mode. // "plan" maps directly to the SDK's "plan" permission mode; // "default" restores the session's original permission mode. // When interactionMode is absent we leave the current mode unchanged. if (input.interactionMode === "plan") { yield* Effect.tryPromise({ try: () => context.query.setPermissionMode("plan"), catch: (cause) => toRequestError(input.threadId, "turn/setPermissionMode", cause), }); } else if (input.interactionMode === "default") { yield* Effect.tryPromise({ try: () => context.query.setPermissionMode(context.basePermissionMode ?? "default"), catch: (cause) => toRequestError(input.threadId, "turn/setPermissionMode", cause), }); } const turnId = TurnId.make(yield* Random.nextUUIDv4); const turnState: ClaudeTurnState = { turnId, startedAt: yield* nowIso, items: [], assistantTextBlocks: new Map(), assistantTextBlockOrder: [], capturedProposedPlanKeys: new Set(), nextSyntheticAssistantBlockIndex: -1, }; const updatedAt = yield* nowIso; context.turnState = turnState; context.session = { ...context.session, status: "running", activeTurnId: turnId, updatedAt, }; const turnStartedStamp = yield* makeEventStamp(); yield* offerRuntimeEvent({ type: "turn.started", eventId: turnStartedStamp.eventId, provider: PROVIDER, createdAt: turnStartedStamp.createdAt, threadId: context.session.threadId, turnId, payload: modelSelection?.model ? { model: modelSelection.model } : {}, providerRefs: {}, }); const message = yield* buildUserMessageEffect(input, { fileSystem, attachmentsDir: serverConfig.attachmentsDir, boundInstanceId, }); yield* Queue.offer(context.promptQueue, { type: "message", message, }).pipe(Effect.mapError((cause) => toRequestError(input.threadId, "turn/start", cause))); return { threadId: context.session.threadId, turnId, ...(context.session.resumeCursor !== undefined ? { resumeCursor: context.session.resumeCursor } : {}), }; }); const interruptTurn: ClaudeAdapterShape["interruptTurn"] = Effect.fn("interruptTurn")( function* (threadId, _turnId) { const context = yield* requireSession(threadId); yield* Effect.tryPromise({ try: () => context.query.interrupt(), catch: (cause) => toRequestError(threadId, "turn/interrupt", cause), }); }, ); const readThread: ClaudeAdapterShape["readThread"] = Effect.fn("readThread")( function* (threadId) { const context = yield* requireSession(threadId); return yield* snapshotThread(context); }, ); const rollbackThread: ClaudeAdapterShape["rollbackThread"] = Effect.fn("rollbackThread")( function* (threadId, numTurns) { const context = yield* requireSession(threadId); const nextLength = Math.max(0, context.turns.length - numTurns); context.turns.splice(nextLength); yield* updateResumeCursor(context); return yield* snapshotThread(context); }, ); const respondToRequest: ClaudeAdapterShape["respondToRequest"] = Effect.fn("respondToRequest")( function* (threadId, requestId, decision) { const context = yield* requireSession(threadId); const pending = context.pendingApprovals.get(requestId); if (!pending) { return yield* new ProviderAdapterRequestError({ provider: PROVIDER, method: "item/requestApproval/decision", detail: `Unknown pending approval request: ${requestId}`, }); } context.pendingApprovals.delete(requestId); yield* Deferred.succeed(pending.decision, decision); }, ); const respondToUserInput: ClaudeAdapterShape["respondToUserInput"] = Effect.fn( "respondToUserInput", )(function* (threadId, requestId, answers) { const context = yield* requireSession(threadId); const pending = context.pendingUserInputs.get(requestId); if (!pending) { return yield* new ProviderAdapterRequestError({ provider: PROVIDER, method: "item/tool/respondToUserInput", detail: `Unknown pending user-input request: ${requestId}`, }); } context.pendingUserInputs.delete(requestId); yield* Deferred.succeed(pending.answers, answers); }); const stopSession: ClaudeAdapterShape["stopSession"] = Effect.fn("stopSession")( function* (threadId) { const context = yield* requireSession(threadId); yield* stopSessionInternal(context, { emitExitEvent: true, }); }, ); const listSessions: ClaudeAdapterShape["listSessions"] = () => Effect.sync(() => Array.from(sessions.values(), ({ session }) => ({ ...session }))); const hasSession: ClaudeAdapterShape["hasSession"] = (threadId) => Effect.sync(() => { const context = sessions.get(threadId); return context !== undefined && !context.stopped; }); const stopAll: ClaudeAdapterShape["stopAll"] = () => Effect.forEach( sessions, ([, context]) => stopSessionInternal(context, { emitExitEvent: true, }), { discard: true }, ); yield* Effect.addFinalizer(() => Effect.forEach( sessions, ([, context]) => stopSessionInternal(context, { emitExitEvent: false, }), { discard: true }, ).pipe(Effect.tap(() => Queue.shutdown(runtimeEventQueue))), ); return { provider: PROVIDER, capabilities: { sessionModelSwitch: "in-session", }, startSession, sendTurn, interruptTurn, readThread, rollbackThread, respondToRequest, respondToUserInput, stopSession, listSessions, hasSession, stopAll, get streamEvents() { return Stream.fromQueue(runtimeEventQueue); }, } satisfies ClaudeAdapterShape; });