/** * 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,} from "@anthropic-ai/claude-agent-sdk";import { ApprovalRequestId, type CanonicalItemType, type CanonicalRequestType, EventId, type ProviderApprovalDecision, ProviderItemId, type ProviderRuntimeEvent, type ProviderRuntimeTurnStatus, type ProviderSendTurnInput, type ProviderSession, type ThreadTokenUsageSnapshot, type ProviderUserInputAnswers, type RuntimeContentStreamKind, RuntimeItemId, RuntimeRequestId, RuntimeTaskId, ThreadId, TurnId, type UserInputQuestion, ClaudeCodeEffort,} from "@t3tools/contracts";import { applyClaudePromptEffortPrefix, resolveApiModelId, resolveEffort, trimOrNull,} from "@t3tools/shared/model";import { Cause, DateTime, Deferred, Effect, Exit, FileSystem, Fiber, Layer, Queue, Random, Ref, Stream,} from "effect"; import { resolveAttachmentPath } from "../../attachmentStore.ts";import { ServerConfig } from "../../config.ts";import { ServerSettingsService } from "../../serverSettings.ts";import { getClaudeModelCapabilities } from "./ClaudeProvider.ts";import { ProviderAdapterProcessError, ProviderAdapterRequestError, ProviderAdapterSessionClosedError, ProviderAdapterSessionNotFoundError, ProviderAdapterValidationError, type ProviderAdapterError,} from "../Errors.ts";import { ClaudeAdapter, type ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts";import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; const PROVIDER = "claudeAgent" as const;type ClaudeTextStreamKind = Extract<RuntimeContentStreamKind, "assistant_text" | "reasoning_text">;type ClaudeToolResultStreamKind = Extract< RuntimeContentStreamKind, "command_output" | "file_change_output">; 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<unknown>; readonly assistantTextBlocks: Map<number, AssistantTextBlockState>; readonly assistantTextBlockOrder: Array<AssistantTextBlockState>; readonly capturedProposedPlanKeys: Set<string>; 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<PermissionUpdate>; readonly decision: Deferred.Deferred<ProviderApprovalDecision>;} interface PendingUserInput { readonly questions: ReadonlyArray<UserInputQuestion>; readonly answers: Deferred.Deferred<ProviderUserInputAnswers>;} interface ToolInFlight { readonly itemId: string; readonly itemType: CanonicalItemType; readonly toolName: string; readonly title: string; readonly detail?: string; readonly input: Record<string, unknown>; readonly partialInputJson: string; readonly lastEmittedInputFingerprint?: string;} interface ClaudeSessionContext { session: ProviderSession; readonly promptQueue: Queue.Queue<PromptQueueItem>; readonly query: ClaudeQueryRuntime; streamFiber: Fiber.Fiber<void, Error> | undefined; readonly startedAt: string; readonly basePermissionMode: PermissionMode | undefined; currentApiModelId: string | undefined; resumeSessionId: string | undefined; readonly pendingApprovals: Map<ApprovalRequestId, PendingApproval>; readonly pendingUserInputs: Map<ApprovalRequestId, PendingUserInput>; readonly turns: Array<{ id: TurnId; items: Array<unknown>; }>; readonly inFlightTools: Map<number, ToolInFlight>; turnState: ClaudeTurnState | undefined; lastKnownContextWindow: number | undefined; lastKnownTokenUsage: ThreadTokenUsageSnapshot | undefined; lastAssistantUuid: string | undefined; lastThreadStartedId: string | undefined; stopped: boolean;} interface ClaudeQueryRuntime extends AsyncIterable<SDKMessage> { readonly interrupt: () => Promise<void>; readonly setModel: (model?: string) => Promise<void>; readonly setPermissionMode: (mode: PermissionMode) => Promise<void>; readonly setMaxThinkingTokens: (maxThinkingTokens: number | null) => Promise<void>; readonly close: () => void;} export interface ClaudeAdapterLiveOptions { readonly createQuery?: (input: { readonly prompt: AsyncIterable<SDKUserMessage>; 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 toMessage(cause: unknown, fallback: string): string { if (cause instanceof Error && cause.message.length > 0) { return cause.message; } return fallback;} function toError(cause: unknown, fallback: string): Error { return cause instanceof Error ? cause : new Error(toMessage(cause, fallback));} function normalizeClaudeStreamMessages(cause: Cause.Cause<Error>): ReadonlyArray<string> { 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 getEffectiveClaudeCodeEffort( effort: ClaudeCodeEffort | null | undefined,): Exclude<ClaudeCodeEffort, "ultrathink"> | null { if (!effort) { return null; } return effort === "ultrathink" ? null : effort;} 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<Error>): boolean { return ( Cause.hasInterruptsOnly(cause) || normalizeClaudeStreamMessages(cause).some(isClaudeInterruptedMessage) );} function messageFromClaudeStreamCause(cause: Cause.Cause<Error>, fallback: string): string { return normalizeClaudeStreamMessages(cause)[0] ?? fallback;} function interruptionMessageFromClaudeCause(cause: Cause.Cause<Error>): 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.makeUnsafe(value);} function maxClaudeContextWindowFromModelUsage(modelUsage: unknown): number | undefined { if (!modelUsage || typeof modelUsage !== "object") { return undefined; } let maxContextWindow: number | undefined; for (const value of Object.values(modelUsage as Record<string, unknown>)) { if (!value || typeof value !== "object") { continue; } const contextWindow = (value as { contextWindow?: unknown }).contextWindow; if ( typeof contextWindow !== "number" || !Number.isFinite(contextWindow) || contextWindow <= 0 ) { continue; } maxContextWindow = Math.max(maxContextWindow ?? 0, contextWindow); } return maxContextWindow;} function normalizeClaudeTokenUsage( usage: unknown, contextWindow?: number,): ThreadTokenUsageSnapshot | undefined { if (!usage || typeof usage !== "object") { return undefined; } const record = usage as Record<string, unknown>; const directUsedTokens = typeof record.total_tokens === "number" && Number.isFinite(record.total_tokens) ? record.total_tokens : undefined; const inputTokens = (typeof record.input_tokens === "number" && Number.isFinite(record.input_tokens) ? record.input_tokens : 0) + (typeof record.cache_creation_input_tokens === "number" && Number.isFinite(record.cache_creation_input_tokens) ? record.cache_creation_input_tokens : 0) + (typeof record.cache_read_input_tokens === "number" && Number.isFinite(record.cache_read_input_tokens) ? record.cache_read_input_tokens : 0); const outputTokens = typeof record.output_tokens === "number" && Number.isFinite(record.output_tokens) ? record.output_tokens : 0; const derivedUsedTokens = inputTokens + outputTokens; const usedTokens = directUsedTokens ?? (derivedUsedTokens > 0 ? derivedUsedTokens : undefined); if (usedTokens === undefined || usedTokens <= 0) { return undefined; } return { usedTokens, lastUsedTokens: usedTokens, ...(inputTokens > 0 ? { inputTokens } : {}), ...(outputTokens > 0 ? { outputTokens } : {}), ...(typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0 ? { maxTokens: contextWindow } : {}), ...(typeof record.tool_uses === "number" && Number.isFinite(record.tool_uses) ? { toolUses: record.tool_uses } : {}), ...(typeof record.duration_ms === "number" && Number.isFinite(record.duration_ms) ? { durationMs: record.duration_ms } : {}), };} function asCanonicalTurnId(value: TurnId): TurnId { return value;} function asRuntimeRequestId(value: ApprovalRequestId): RuntimeRequestId { return RuntimeRequestId.makeUnsafe(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.makeUnsafe(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 summarizeToolRequest(toolName: string, input: Record<string, unknown>): 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)}`; } const serialized = JSON.stringify(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([Lines 1–500 of 3067. Load more
/** * 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, } from "@anthropic-ai/claude-agent-sdk"; import { ApprovalRequestId, type CanonicalItemType, type CanonicalRequestType, EventId, type ProviderApprovalDecision, ProviderItemId, type ProviderRuntimeEvent, type ProviderRuntimeTurnStatus, type ProviderSendTurnInput, type ProviderSession, type ThreadTokenUsageSnapshot, type ProviderUserInputAnswers, type RuntimeContentStreamKind, RuntimeItemId, RuntimeRequestId, RuntimeTaskId, ThreadId, TurnId, type UserInputQuestion, ClaudeCodeEffort, } from "@t3tools/contracts"; import { applyClaudePromptEffortPrefix, resolveApiModelId, resolveEffort, trimOrNull, } from "@t3tools/shared/model"; import { Cause, DateTime, Deferred, Effect, Exit, FileSystem, Fiber, Layer, Queue, Random, Ref, Stream, } from "effect"; import { resolveAttachmentPath } from "../../attachmentStore.ts"; import { ServerConfig } from "../../config.ts"; import { ServerSettingsService } from "../../serverSettings.ts"; import { getClaudeModelCapabilities } from "./ClaudeProvider.ts"; import { ProviderAdapterProcessError, ProviderAdapterRequestError, ProviderAdapterSessionClosedError, ProviderAdapterSessionNotFoundError, ProviderAdapterValidationError, type ProviderAdapterError, } from "../Errors.ts"; import { ClaudeAdapter, type ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts"; import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; const PROVIDER = "claudeAgent" as const; type ClaudeTextStreamKind = Extract<RuntimeContentStreamKind, "assistant_text" | "reasoning_text">; type ClaudeToolResultStreamKind = Extract< RuntimeContentStreamKind, "command_output" | "file_change_output" >; 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<unknown>; readonly assistantTextBlocks: Map<number, AssistantTextBlockState>; readonly assistantTextBlockOrder: Array<AssistantTextBlockState>; readonly capturedProposedPlanKeys: Set<string>; 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<PermissionUpdate>; readonly decision: Deferred.Deferred<ProviderApprovalDecision>; } interface PendingUserInput { readonly questions: ReadonlyArray<UserInputQuestion>; readonly answers: Deferred.Deferred<ProviderUserInputAnswers>; } interface ToolInFlight { readonly itemId: string; readonly itemType: CanonicalItemType; readonly toolName: string; readonly title: string; readonly detail?: string; readonly input: Record<string, unknown>; readonly partialInputJson: string; readonly lastEmittedInputFingerprint?: string; } interface ClaudeSessionContext { session: ProviderSession; readonly promptQueue: Queue.Queue<PromptQueueItem>; readonly query: ClaudeQueryRuntime; streamFiber: Fiber.Fiber<void, Error> | undefined; readonly startedAt: string; readonly basePermissionMode: PermissionMode | undefined; currentApiModelId: string | undefined; resumeSessionId: string | undefined; readonly pendingApprovals: Map<ApprovalRequestId, PendingApproval>; readonly pendingUserInputs: Map<ApprovalRequestId, PendingUserInput>; readonly turns: Array<{ id: TurnId; items: Array<unknown>; }>; readonly inFlightTools: Map<number, ToolInFlight>; turnState: ClaudeTurnState | undefined; lastKnownContextWindow: number | undefined; lastKnownTokenUsage: ThreadTokenUsageSnapshot | undefined; lastAssistantUuid: string | undefined; lastThreadStartedId: string | undefined; stopped: boolean; } interface ClaudeQueryRuntime extends AsyncIterable<SDKMessage> { readonly interrupt: () => Promise<void>; readonly setModel: (model?: string) => Promise<void>; readonly setPermissionMode: (mode: PermissionMode) => Promise<void>; readonly setMaxThinkingTokens: (maxThinkingTokens: number | null) => Promise<void>; readonly close: () => void; } export interface ClaudeAdapterLiveOptions { readonly createQuery?: (input: { readonly prompt: AsyncIterable<SDKUserMessage>; 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 toMessage(cause: unknown, fallback: string): string { if (cause instanceof Error && cause.message.length > 0) { return cause.message; } return fallback; } function toError(cause: unknown, fallback: string): Error { return cause instanceof Error ? cause : new Error(toMessage(cause, fallback)); } function normalizeClaudeStreamMessages(cause: Cause.Cause<Error>): ReadonlyArray<string> { 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 getEffectiveClaudeCodeEffort( effort: ClaudeCodeEffort | null | undefined, ): Exclude<ClaudeCodeEffort, "ultrathink"> | null { if (!effort) { return null; } return effort === "ultrathink" ? null : effort; } 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<Error>): boolean { return ( Cause.hasInterruptsOnly(cause) || normalizeClaudeStreamMessages(cause).some(isClaudeInterruptedMessage) ); } function messageFromClaudeStreamCause(cause: Cause.Cause<Error>, fallback: string): string { return normalizeClaudeStreamMessages(cause)[0] ?? fallback; } function interruptionMessageFromClaudeCause(cause: Cause.Cause<Error>): 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.makeUnsafe(value); } function maxClaudeContextWindowFromModelUsage(modelUsage: unknown): number | undefined { if (!modelUsage || typeof modelUsage !== "object") { return undefined; } let maxContextWindow: number | undefined; for (const value of Object.values(modelUsage as Record<string, unknown>)) { if (!value || typeof value !== "object") { continue; } const contextWindow = (value as { contextWindow?: unknown }).contextWindow; if ( typeof contextWindow !== "number" || !Number.isFinite(contextWindow) || contextWindow <= 0 ) { continue; } maxContextWindow = Math.max(maxContextWindow ?? 0, contextWindow); } return maxContextWindow; } function normalizeClaudeTokenUsage( usage: unknown, contextWindow?: number, ): ThreadTokenUsageSnapshot | undefined { if (!usage || typeof usage !== "object") { return undefined; } const record = usage as Record<string, unknown>; const directUsedTokens = typeof record.total_tokens === "number" && Number.isFinite(record.total_tokens) ? record.total_tokens : undefined; const inputTokens = (typeof record.input_tokens === "number" && Number.isFinite(record.input_tokens) ? record.input_tokens : 0) + (typeof record.cache_creation_input_tokens === "number" && Number.isFinite(record.cache_creation_input_tokens) ? record.cache_creation_input_tokens : 0) + (typeof record.cache_read_input_tokens === "number" && Number.isFinite(record.cache_read_input_tokens) ? record.cache_read_input_tokens : 0); const outputTokens = typeof record.output_tokens === "number" && Number.isFinite(record.output_tokens) ? record.output_tokens : 0; const derivedUsedTokens = inputTokens + outputTokens; const usedTokens = directUsedTokens ?? (derivedUsedTokens > 0 ? derivedUsedTokens : undefined); if (usedTokens === undefined || usedTokens <= 0) { return undefined; } return { usedTokens, lastUsedTokens: usedTokens, ...(inputTokens > 0 ? { inputTokens } : {}), ...(outputTokens > 0 ? { outputTokens } : {}), ...(typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0 ? { maxTokens: contextWindow } : {}), ...(typeof record.tool_uses === "number" && Number.isFinite(record.tool_uses) ? { toolUses: record.tool_uses } : {}), ...(typeof record.duration_ms === "number" && Number.isFinite(record.duration_ms) ? { durationMs: record.duration_ms } : {}), }; } function asCanonicalTurnId(value: TurnId): TurnId { return value; } function asRuntimeRequestId(value: ApprovalRequestId): RuntimeRequestId { return RuntimeRequestId.makeUnsafe(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.makeUnsafe(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 summarizeToolRequest(toolName: string, input: Record<string, unknown>): 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)}`; } const serialized = JSON.stringify(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([