/** * 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, 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,} from "@t3tools/contracts";import { applyClaudePromptEffortPrefix, getModelSelectionBooleanOptionValue, getModelSelectionStringOptionValue, getProviderOptionDescriptors, resolvePromptInjectedEffort,} 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, normalizeClaudeCliEffort, resolveClaudeApiModelId, resolveClaudeEffort,} 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 ClaudeSdkEffort = NonNullable<ClaudeQueryOptions["effort"]>; 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 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<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.make(value);} function maxClaudeContextWindowFromModelUsage( modelUsage: Record<string, ModelUsage> | 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<string, unknown>; 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<string, unknown>): 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<string, unknown> => 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, 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)}`; }Lines 1–500 of 3223. 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, type ModelUsage, } from "@anthropic-ai/claude-agent-sdk"; import { parseCliArgs } from "@t3tools/shared/cliArgs"; 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, } from "@t3tools/contracts"; import { applyClaudePromptEffortPrefix, getModelSelectionBooleanOptionValue, getModelSelectionStringOptionValue, getProviderOptionDescriptors, resolvePromptInjectedEffort, } 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, normalizeClaudeCliEffort, resolveClaudeApiModelId, resolveClaudeEffort, } 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 ClaudeSdkEffort = NonNullable<ClaudeQueryOptions["effort"]>; 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 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<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.make(value); } function maxClaudeContextWindowFromModelUsage( modelUsage: Record<string, ModelUsage> | 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<string, unknown>; 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<string, unknown>): 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<string, unknown> => 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, 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)}`; }