@tanstack/ai-claude-code 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"translate.js","sources":["../../../src/stream/translate.ts"],"sourcesContent":["import { EventType, buildBaseUsage } from '@tanstack/ai'\nimport type { StreamChunk, TokenUsage } from '@tanstack/ai'\nimport type {\n AgentSdkMessage,\n SdkAssistantMessage,\n SdkPartialAssistantMessage,\n SdkResultMessage,\n SdkToolResultContent,\n SdkUsage,\n SdkUserMessage,\n} from './sdk-types'\n\n/** Name of the CUSTOM event carrying the Claude Code session id. */\nexport const SESSION_ID_EVENT = 'claude-code.session-id'\n\n/** Server name used for bridged TanStack tools (model sees `mcp__tanstack__<name>`). */\nexport const BRIDGED_MCP_SERVER_NAME = 'tanstack'\n\nconst BRIDGED_MCP_PREFIX = `mcp__${BRIDGED_MCP_SERVER_NAME}__`\n\n/** Claude Code-specific usage details attached to RUN_FINISHED usage. */\nexport type ClaudeCodeProviderUsageDetails = {\n /** Total cost of the harness run in USD, as reported by Claude Code. */\n totalCostUsd?: number\n}\n\nexport interface TranslateContext {\n model: string\n runId: string\n threadId: string\n parentRunId?: string\n genId: () => string\n /** Called as soon as the harness reports its session id. */\n onSessionId?: (sessionId: string) => void\n /** Called for each raw SDK message, for logging. */\n onSdkMessage?: (message: AgentSdkMessage) => void\n}\n\n/**\n * Strip the bridged MCP server prefix so tool-call events match the TanStack\n * tool names the application registered. Built-in harness tools (Bash, Read,\n * Edit, ...) and foreign MCP tools pass through verbatim.\n */\nexport function stripMcpPrefix(name: string): string {\n return name.startsWith(BRIDGED_MCP_PREFIX)\n ? name.slice(BRIDGED_MCP_PREFIX.length)\n : name\n}\n\nfunction stringifyToolResultContent(\n content: SdkToolResultContent | undefined,\n): string {\n if (content === undefined) return ''\n if (typeof content === 'string') return content\n return content\n .map((block) => (typeof block.text === 'string' ? block.text : ''))\n .join('')\n}\n\nfunction buildUsage(\n usage: SdkUsage | undefined,\n totalCostUsd: number | undefined,\n): TokenUsage<ClaudeCodeProviderUsageDetails> | undefined {\n if (!usage) return undefined\n const promptTokens = usage.input_tokens ?? 0\n const completionTokens = usage.output_tokens ?? 0\n const result = buildBaseUsage<ClaudeCodeProviderUsageDetails>({\n promptTokens,\n completionTokens,\n totalTokens: promptTokens + completionTokens,\n })\n const cacheWrite = usage.cache_creation_input_tokens\n const cacheRead = usage.cache_read_input_tokens\n const promptTokensDetails = {\n ...(cacheWrite ? { cacheWriteTokens: cacheWrite } : {}),\n ...(cacheRead ? { cachedTokens: cacheRead } : {}),\n }\n if (Object.keys(promptTokensDetails).length > 0) {\n result.promptTokensDetails = promptTokensDetails\n }\n if (totalCostUsd !== undefined) {\n result.providerUsageDetails = { totalCostUsd }\n }\n return result\n}\n\n/**\n * Translate a Claude Code Agent SDK message stream into AG-UI StreamChunk\n * events.\n *\n * The harness runs its own agent loop and executes its own tools, so the\n * translation always ends with `finishReason: 'stop'` (or `'length'` /\n * RUN_ERROR) — never `'tool_calls'`. Harness tool activity is emitted as\n * already-resolved TOOL_CALL_START/ARGS/END + TOOL_CALL_RESULT sequences so\n * UIs can render it, while the TanStack engine never tries to execute them.\n *\n * Invariant: every TOOL_CALL_START is eventually paired with a\n * TOOL_CALL_RESULT (synthesized as `{\"status\":\"interrupted\"}` when the run\n * ends or aborts before the harness reported one) so the engine's\n * pending-tool-call scan on the next request never force-executes them.\n */\nexport async function* translateSdkStream(\n sdkMessages: AsyncIterable<AgentSdkMessage>,\n ctx: TranslateContext,\n): AsyncIterable<StreamChunk> {\n const { model, runId, threadId, genId } = ctx\n const now = () => Date.now()\n\n let runStarted = false\n /** Tool calls started but with no result yet. */\n const unresolvedToolCalls = new Set<string>()\n /** Anthropic message ids whose text/thinking already streamed via partials. */\n const streamedMessageIds = new Set<string>()\n\n // Partial-stream state\n let partialMessageId: string | null = null\n let partialBlockType: string | null = null\n let partialTextMessageId: string | null = null\n let partialTextContent = ''\n let partialTextStarted = false\n let partialReasoningId: string | null = null\n\n function* startRun(): Generator<StreamChunk> {\n if (runStarted) return\n runStarted = true\n yield {\n type: EventType.RUN_STARTED,\n runId,\n threadId,\n model,\n timestamp: now(),\n ...(ctx.parentRunId !== undefined && { parentRunId: ctx.parentRunId }),\n }\n }\n\n function* synthesizeUnresolvedResults(): Generator<StreamChunk> {\n for (const toolCallId of unresolvedToolCalls) {\n yield {\n type: EventType.TOOL_CALL_RESULT,\n toolCallId,\n messageId: genId(),\n model,\n timestamp: now(),\n content: JSON.stringify({ status: 'interrupted' }),\n }\n }\n unresolvedToolCalls.clear()\n }\n\n function* closePartialText(): Generator<StreamChunk> {\n if (partialTextStarted && partialTextMessageId) {\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId: partialTextMessageId,\n model,\n timestamp: now(),\n }\n }\n partialTextStarted = false\n partialTextMessageId = null\n partialTextContent = ''\n }\n\n function* closePartialReasoning(): Generator<StreamChunk> {\n if (partialReasoningId) {\n yield {\n type: EventType.REASONING_MESSAGE_END,\n messageId: partialReasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: partialReasoningId,\n model,\n timestamp: now(),\n }\n }\n partialReasoningId = null\n }\n\n function* emitToolUse(block: {\n id: string\n name: string\n input: unknown\n }): Generator<StreamChunk> {\n const toolCallName = stripMcpPrefix(block.name)\n const args = JSON.stringify(block.input ?? {})\n yield {\n type: EventType.TOOL_CALL_START,\n toolCallId: block.id,\n toolCallName,\n toolName: toolCallName,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: block.id,\n model,\n timestamp: now(),\n delta: args,\n args,\n }\n yield {\n type: EventType.TOOL_CALL_END,\n toolCallId: block.id,\n toolCallName,\n toolName: toolCallName,\n model,\n timestamp: now(),\n input: block.input ?? {},\n }\n unresolvedToolCalls.add(block.id)\n }\n\n function* handleAssistant(\n message: SdkAssistantMessage,\n ): Generator<StreamChunk> {\n const alreadyStreamed =\n message.message.id !== undefined &&\n streamedMessageIds.has(message.message.id)\n\n for (const block of message.message.content) {\n if (block.type === 'text') {\n if (alreadyStreamed) continue\n const messageId = message.message.id ?? genId()\n const text = (block as { text: string }).text\n yield {\n type: EventType.TEXT_MESSAGE_START,\n messageId,\n model,\n timestamp: now(),\n role: 'assistant',\n }\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId,\n model,\n timestamp: now(),\n delta: text,\n content: text,\n }\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId,\n model,\n timestamp: now(),\n }\n } else if (block.type === 'thinking') {\n if (alreadyStreamed) continue\n const reasoningId = genId()\n const thinking = (block as { thinking: string }).thinking\n yield {\n type: EventType.REASONING_START,\n messageId: reasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_START,\n messageId: reasoningId,\n role: 'reasoning' as const,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_CONTENT,\n messageId: reasoningId,\n delta: thinking,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_END,\n messageId: reasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: reasoningId,\n model,\n timestamp: now(),\n }\n } else if (block.type === 'tool_use') {\n yield* emitToolUse(\n block as { id: string; name: string; input: unknown },\n )\n }\n }\n }\n\n function* handleUser(message: SdkUserMessage): Generator<StreamChunk> {\n const content = message.message.content\n if (typeof content === 'string') return\n for (const block of content) {\n if (block.type !== 'tool_result') continue\n const toolResult = block as {\n tool_use_id: string\n content?: SdkToolResultContent\n is_error?: boolean\n }\n unresolvedToolCalls.delete(toolResult.tool_use_id)\n yield {\n type: EventType.TOOL_CALL_RESULT,\n toolCallId: toolResult.tool_use_id,\n messageId: genId(),\n model,\n timestamp: now(),\n content: stringifyToolResultContent(toolResult.content),\n ...(toolResult.is_error === true && { state: 'output-error' as const }),\n }\n }\n }\n\n function* handleResult(message: SdkResultMessage): Generator<StreamChunk> {\n yield* closePartialText()\n yield* closePartialReasoning()\n yield* synthesizeUnresolvedResults()\n\n const usage = buildUsage(message.usage, message.total_cost_usd)\n if (message.subtype === 'success') {\n yield {\n type: EventType.RUN_FINISHED,\n runId,\n threadId,\n model,\n timestamp: now(),\n finishReason: 'stop',\n ...(usage !== undefined && { usage }),\n }\n } else if (message.subtype === 'error_max_turns') {\n yield {\n type: EventType.RUN_FINISHED,\n runId,\n threadId,\n model,\n timestamp: now(),\n finishReason: 'length',\n ...(usage !== undefined && { usage }),\n }\n } else {\n const errorMessage =\n message.errors && message.errors.length > 0\n ? message.errors.join('; ')\n : `Claude Code run failed: ${message.subtype}`\n yield {\n type: EventType.RUN_ERROR,\n model,\n timestamp: now(),\n message: errorMessage,\n code: message.subtype,\n error: { message: errorMessage, code: message.subtype },\n }\n }\n }\n\n function* handleStreamEvent(\n message: SdkPartialAssistantMessage,\n ): Generator<StreamChunk> {\n const event = message.event\n if (event.type === 'message_start') {\n partialMessageId = event.message.id ?? genId()\n streamedMessageIds.add(partialMessageId)\n } else if (event.type === 'content_block_start') {\n partialBlockType = event.content_block.type\n if (partialBlockType === 'text') {\n partialTextMessageId = partialMessageId ?? genId()\n partialTextContent = ''\n if (!partialTextStarted) {\n partialTextStarted = true\n yield {\n type: EventType.TEXT_MESSAGE_START,\n messageId: partialTextMessageId,\n model,\n timestamp: now(),\n role: 'assistant',\n }\n }\n } else if (partialBlockType === 'thinking') {\n partialReasoningId = genId()\n yield {\n type: EventType.REASONING_START,\n messageId: partialReasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_START,\n messageId: partialReasoningId,\n role: 'reasoning' as const,\n model,\n timestamp: now(),\n }\n }\n } else if (event.type === 'content_block_delta') {\n if (\n event.delta.type === 'text_delta' &&\n partialTextStarted &&\n partialTextMessageId &&\n typeof event.delta.text === 'string'\n ) {\n partialTextContent += event.delta.text\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId: partialTextMessageId,\n model,\n timestamp: now(),\n delta: event.delta.text,\n content: partialTextContent,\n }\n } else if (\n event.delta.type === 'thinking_delta' &&\n partialReasoningId &&\n typeof event.delta.thinking === 'string'\n ) {\n yield {\n type: EventType.REASONING_MESSAGE_CONTENT,\n messageId: partialReasoningId,\n delta: event.delta.thinking,\n model,\n timestamp: now(),\n }\n }\n } else if (event.type === 'content_block_stop') {\n if (partialBlockType === 'text') {\n yield* closePartialText()\n } else if (partialBlockType === 'thinking') {\n yield* closePartialReasoning()\n }\n partialBlockType = null\n }\n }\n\n try {\n for await (const sdkMessage of sdkMessages) {\n ctx.onSdkMessage?.(sdkMessage)\n\n if (sdkMessage.type === 'system' && sdkMessage.subtype === 'init') {\n yield* startRun()\n ctx.onSessionId?.(sdkMessage.session_id)\n yield {\n type: EventType.CUSTOM,\n model,\n timestamp: now(),\n name: SESSION_ID_EVENT,\n value: {\n sessionId: sdkMessage.session_id,\n model: sdkMessage.model,\n tools: sdkMessage.tools,\n },\n }\n continue\n }\n\n // Anything before init still needs RUN_STARTED first.\n yield* startRun()\n\n if (sdkMessage.type === 'stream_event') {\n if (sdkMessage.parent_tool_use_id !== null) continue\n yield* handleStreamEvent(sdkMessage)\n } else if (sdkMessage.type === 'assistant') {\n if (sdkMessage.parent_tool_use_id !== null) continue\n yield* handleAssistant(sdkMessage)\n } else if (sdkMessage.type === 'user') {\n if (sdkMessage.parent_tool_use_id !== null) continue\n yield* handleUser(sdkMessage)\n } else if (sdkMessage.type === 'result') {\n yield* handleResult(sdkMessage)\n }\n // All other SDK message types (status, hooks, notifications, ...) are\n // harness-internal and intentionally ignored.\n }\n } catch (error) {\n // The run is dying (abort or SDK failure). Pair any started tool calls\n // with a synthetic result first so the next request's pending-tool-call\n // scan doesn't try to execute them, then let the adapter surface the\n // error as RUN_ERROR.\n yield* synthesizeUnresolvedResults()\n throw error\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"translate.js","names":[],"sources":["../../../src/stream/translate.ts"],"sourcesContent":["import { EventType, buildBaseUsage } from '@tanstack/ai'\nimport type { StreamChunk, TokenUsage } from '@tanstack/ai'\nimport type {\n AgentSdkMessage,\n SdkAssistantMessage,\n SdkPartialAssistantMessage,\n SdkResultMessage,\n SdkToolResultContent,\n SdkUsage,\n SdkUserMessage,\n} from './sdk-types'\n\n/** Name of the CUSTOM event carrying the Claude Code session id. */\nexport const SESSION_ID_EVENT = 'claude-code.session-id'\n\n/** Server name used for bridged TanStack tools (model sees `mcp__tanstack__<name>`). */\nexport const BRIDGED_MCP_SERVER_NAME = 'tanstack'\n\nconst BRIDGED_MCP_PREFIX = `mcp__${BRIDGED_MCP_SERVER_NAME}__`\n\n/** Claude Code-specific usage details attached to RUN_FINISHED usage. */\nexport type ClaudeCodeProviderUsageDetails = {\n /** Total cost of the harness run in USD, as reported by Claude Code. */\n totalCostUsd?: number\n}\n\nexport interface TranslateContext {\n model: string\n runId: string\n threadId: string\n parentRunId?: string\n genId: () => string\n /** Called as soon as the harness reports its session id. */\n onSessionId?: (sessionId: string) => void\n /** Called for each raw SDK message, for logging. */\n onSdkMessage?: (message: AgentSdkMessage) => void\n}\n\n/**\n * Strip the bridged MCP server prefix so tool-call events match the TanStack\n * tool names the application registered. Built-in harness tools (Bash, Read,\n * Edit, ...) and foreign MCP tools pass through verbatim.\n */\nexport function stripMcpPrefix(name: string): string {\n return name.startsWith(BRIDGED_MCP_PREFIX)\n ? name.slice(BRIDGED_MCP_PREFIX.length)\n : name\n}\n\nfunction stringifyToolResultContent(\n content: SdkToolResultContent | undefined,\n): string {\n if (content === undefined) return ''\n if (typeof content === 'string') return content\n return content\n .map((block) => (typeof block.text === 'string' ? block.text : ''))\n .join('')\n}\n\nfunction buildUsage(\n usage: SdkUsage | undefined,\n totalCostUsd: number | undefined,\n): TokenUsage<ClaudeCodeProviderUsageDetails> | undefined {\n if (!usage) return undefined\n const promptTokens = usage.input_tokens ?? 0\n const completionTokens = usage.output_tokens ?? 0\n const result = buildBaseUsage<ClaudeCodeProviderUsageDetails>({\n promptTokens,\n completionTokens,\n totalTokens: promptTokens + completionTokens,\n })\n const cacheWrite = usage.cache_creation_input_tokens\n const cacheRead = usage.cache_read_input_tokens\n const promptTokensDetails = {\n ...(cacheWrite ? { cacheWriteTokens: cacheWrite } : {}),\n ...(cacheRead ? { cachedTokens: cacheRead } : {}),\n }\n if (Object.keys(promptTokensDetails).length > 0) {\n result.promptTokensDetails = promptTokensDetails\n }\n if (totalCostUsd !== undefined) {\n result.providerUsageDetails = { totalCostUsd }\n }\n return result\n}\n\n/**\n * Translate a Claude Code Agent SDK message stream into AG-UI StreamChunk\n * events.\n *\n * The harness runs its own agent loop and executes its own tools, so the\n * translation always ends with `finishReason: 'stop'` (or `'length'` /\n * RUN_ERROR) — never `'tool_calls'`. Harness tool activity is emitted as\n * already-resolved TOOL_CALL_START/ARGS/END + TOOL_CALL_RESULT sequences so\n * UIs can render it, while the TanStack engine never tries to execute them.\n *\n * Invariant: every TOOL_CALL_START is eventually paired with a\n * TOOL_CALL_RESULT (synthesized as `{\"status\":\"interrupted\"}` when the run\n * ends or aborts before the harness reported one) so the engine's\n * pending-tool-call scan on the next request never force-executes them.\n */\nexport async function* translateSdkStream(\n sdkMessages: AsyncIterable<AgentSdkMessage>,\n ctx: TranslateContext,\n): AsyncIterable<StreamChunk> {\n const { model, runId, threadId, genId } = ctx\n const now = () => Date.now()\n\n let runStarted = false\n /** Tool calls started but with no result yet. */\n const unresolvedToolCalls = new Set<string>()\n /** Anthropic message ids whose text/thinking already streamed via partials. */\n const streamedMessageIds = new Set<string>()\n\n // Partial-stream state\n let partialMessageId: string | null = null\n let partialBlockType: string | null = null\n let partialTextMessageId: string | null = null\n let partialTextContent = ''\n let partialTextStarted = false\n let partialReasoningId: string | null = null\n\n function* startRun(): Generator<StreamChunk> {\n if (runStarted) return\n runStarted = true\n yield {\n type: EventType.RUN_STARTED,\n runId,\n threadId,\n model,\n timestamp: now(),\n ...(ctx.parentRunId !== undefined && { parentRunId: ctx.parentRunId }),\n }\n }\n\n function* synthesizeUnresolvedResults(): Generator<StreamChunk> {\n for (const toolCallId of unresolvedToolCalls) {\n yield {\n type: EventType.TOOL_CALL_RESULT,\n toolCallId,\n messageId: genId(),\n model,\n timestamp: now(),\n content: JSON.stringify({ status: 'interrupted' }),\n }\n }\n unresolvedToolCalls.clear()\n }\n\n function* closePartialText(): Generator<StreamChunk> {\n if (partialTextStarted && partialTextMessageId) {\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId: partialTextMessageId,\n model,\n timestamp: now(),\n }\n }\n partialTextStarted = false\n partialTextMessageId = null\n partialTextContent = ''\n }\n\n function* closePartialReasoning(): Generator<StreamChunk> {\n if (partialReasoningId) {\n yield {\n type: EventType.REASONING_MESSAGE_END,\n messageId: partialReasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: partialReasoningId,\n model,\n timestamp: now(),\n }\n }\n partialReasoningId = null\n }\n\n function* emitToolUse(block: {\n id: string\n name: string\n input: unknown\n }): Generator<StreamChunk> {\n const toolCallName = stripMcpPrefix(block.name)\n const args = JSON.stringify(block.input ?? {})\n yield {\n type: EventType.TOOL_CALL_START,\n toolCallId: block.id,\n toolCallName,\n toolName: toolCallName,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: block.id,\n model,\n timestamp: now(),\n delta: args,\n args,\n }\n yield {\n type: EventType.TOOL_CALL_END,\n toolCallId: block.id,\n toolCallName,\n toolName: toolCallName,\n model,\n timestamp: now(),\n input: block.input ?? {},\n }\n unresolvedToolCalls.add(block.id)\n }\n\n function* handleAssistant(\n message: SdkAssistantMessage,\n ): Generator<StreamChunk> {\n const alreadyStreamed =\n message.message.id !== undefined &&\n streamedMessageIds.has(message.message.id)\n\n for (const block of message.message.content) {\n if (block.type === 'text') {\n if (alreadyStreamed) continue\n const messageId = message.message.id ?? genId()\n const text = (block as { text: string }).text\n yield {\n type: EventType.TEXT_MESSAGE_START,\n messageId,\n model,\n timestamp: now(),\n role: 'assistant',\n }\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId,\n model,\n timestamp: now(),\n delta: text,\n content: text,\n }\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId,\n model,\n timestamp: now(),\n }\n } else if (block.type === 'thinking') {\n if (alreadyStreamed) continue\n const reasoningId = genId()\n const thinking = (block as { thinking: string }).thinking\n yield {\n type: EventType.REASONING_START,\n messageId: reasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_START,\n messageId: reasoningId,\n role: 'reasoning' as const,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_CONTENT,\n messageId: reasoningId,\n delta: thinking,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_END,\n messageId: reasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: reasoningId,\n model,\n timestamp: now(),\n }\n } else if (block.type === 'tool_use') {\n yield* emitToolUse(\n block as { id: string; name: string; input: unknown },\n )\n }\n }\n }\n\n function* handleUser(message: SdkUserMessage): Generator<StreamChunk> {\n const content = message.message.content\n if (typeof content === 'string') return\n for (const block of content) {\n if (block.type !== 'tool_result') continue\n const toolResult = block as {\n tool_use_id: string\n content?: SdkToolResultContent\n is_error?: boolean\n }\n unresolvedToolCalls.delete(toolResult.tool_use_id)\n yield {\n type: EventType.TOOL_CALL_RESULT,\n toolCallId: toolResult.tool_use_id,\n messageId: genId(),\n model,\n timestamp: now(),\n content: stringifyToolResultContent(toolResult.content),\n ...(toolResult.is_error === true && { state: 'output-error' as const }),\n }\n }\n }\n\n function* handleResult(message: SdkResultMessage): Generator<StreamChunk> {\n yield* closePartialText()\n yield* closePartialReasoning()\n yield* synthesizeUnresolvedResults()\n\n const usage = buildUsage(message.usage, message.total_cost_usd)\n if (message.subtype === 'success') {\n yield {\n type: EventType.RUN_FINISHED,\n runId,\n threadId,\n model,\n timestamp: now(),\n finishReason: 'stop',\n ...(usage !== undefined && { usage }),\n }\n } else if (message.subtype === 'error_max_turns') {\n yield {\n type: EventType.RUN_FINISHED,\n runId,\n threadId,\n model,\n timestamp: now(),\n finishReason: 'length',\n ...(usage !== undefined && { usage }),\n }\n } else {\n const errorMessage =\n message.errors && message.errors.length > 0\n ? message.errors.join('; ')\n : `Claude Code run failed: ${message.subtype}`\n yield {\n type: EventType.RUN_ERROR,\n model,\n timestamp: now(),\n message: errorMessage,\n code: message.subtype,\n error: { message: errorMessage, code: message.subtype },\n }\n }\n }\n\n function* handleStreamEvent(\n message: SdkPartialAssistantMessage,\n ): Generator<StreamChunk> {\n const event = message.event\n if (event.type === 'message_start') {\n partialMessageId = event.message.id ?? genId()\n streamedMessageIds.add(partialMessageId)\n } else if (event.type === 'content_block_start') {\n partialBlockType = event.content_block.type\n if (partialBlockType === 'text') {\n partialTextMessageId = partialMessageId ?? genId()\n partialTextContent = ''\n if (!partialTextStarted) {\n partialTextStarted = true\n yield {\n type: EventType.TEXT_MESSAGE_START,\n messageId: partialTextMessageId,\n model,\n timestamp: now(),\n role: 'assistant',\n }\n }\n } else if (partialBlockType === 'thinking') {\n partialReasoningId = genId()\n yield {\n type: EventType.REASONING_START,\n messageId: partialReasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_START,\n messageId: partialReasoningId,\n role: 'reasoning' as const,\n model,\n timestamp: now(),\n }\n }\n } else if (event.type === 'content_block_delta') {\n if (\n event.delta.type === 'text_delta' &&\n partialTextStarted &&\n partialTextMessageId &&\n typeof event.delta.text === 'string'\n ) {\n partialTextContent += event.delta.text\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId: partialTextMessageId,\n model,\n timestamp: now(),\n delta: event.delta.text,\n content: partialTextContent,\n }\n } else if (\n event.delta.type === 'thinking_delta' &&\n partialReasoningId &&\n typeof event.delta.thinking === 'string'\n ) {\n yield {\n type: EventType.REASONING_MESSAGE_CONTENT,\n messageId: partialReasoningId,\n delta: event.delta.thinking,\n model,\n timestamp: now(),\n }\n }\n } else if (event.type === 'content_block_stop') {\n if (partialBlockType === 'text') {\n yield* closePartialText()\n } else if (partialBlockType === 'thinking') {\n yield* closePartialReasoning()\n }\n partialBlockType = null\n }\n }\n\n try {\n for await (const sdkMessage of sdkMessages) {\n ctx.onSdkMessage?.(sdkMessage)\n\n if (sdkMessage.type === 'system' && sdkMessage.subtype === 'init') {\n yield* startRun()\n ctx.onSessionId?.(sdkMessage.session_id)\n yield {\n type: EventType.CUSTOM,\n model,\n timestamp: now(),\n name: SESSION_ID_EVENT,\n value: {\n sessionId: sdkMessage.session_id,\n model: sdkMessage.model,\n tools: sdkMessage.tools,\n },\n }\n continue\n }\n\n // Anything before init still needs RUN_STARTED first.\n yield* startRun()\n\n if (sdkMessage.type === 'stream_event') {\n if (sdkMessage.parent_tool_use_id !== null) continue\n yield* handleStreamEvent(sdkMessage)\n } else if (sdkMessage.type === 'assistant') {\n if (sdkMessage.parent_tool_use_id !== null) continue\n yield* handleAssistant(sdkMessage)\n } else if (sdkMessage.type === 'user') {\n if (sdkMessage.parent_tool_use_id !== null) continue\n yield* handleUser(sdkMessage)\n } else if (sdkMessage.type === 'result') {\n yield* handleResult(sdkMessage)\n }\n // All other SDK message types (status, hooks, notifications, ...) are\n // harness-internal and intentionally ignored.\n }\n } catch (error) {\n // The run is dying (abort or SDK failure). Pair any started tool calls\n // with a synthetic result first so the next request's pending-tool-call\n // scan doesn't try to execute them, then let the adapter surface the\n // error as RUN_ERROR.\n yield* synthesizeUnresolvedResults()\n throw error\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-claude-code",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Claude Code harness adapter for TanStack AI — run Claude Code as a chat backend with local tool execution and stateful sessions.",
5
5
  "author": "",
6
6
  "license": "MIT",
@@ -37,21 +37,21 @@
37
37
  "src"
38
38
  ],
39
39
  "peerDependencies": {
40
- "@tanstack/ai": "^0.41.0",
41
- "@tanstack/ai-sandbox": "^0.2.3"
40
+ "@tanstack/ai": "^0.43.0",
41
+ "@tanstack/ai-sandbox": "^0.3.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@vitest/coverage-v8": "4.0.14",
45
- "@tanstack/ai": "0.41.0",
46
- "@tanstack/ai-sandbox": "0.2.3",
47
- "@tanstack/ai-sandbox-local-process": "0.2.0"
45
+ "@tanstack/ai": "0.43.0",
46
+ "@tanstack/ai-sandbox": "0.3.0",
47
+ "@tanstack/ai-sandbox-local-process": "0.2.1"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "vite build",
51
51
  "clean": "premove ./build ./dist",
52
- "lint:fix": "eslint ./src --fix",
52
+ "lint:fix": "oxlint src --type-aware --fix",
53
53
  "test:build": "publint --strict",
54
- "test:eslint": "eslint ./src",
54
+ "test:oxlint": "oxlint src --type-aware",
55
55
  "test:lib": "vitest",
56
56
  "test:lib:dev": "pnpm test:lib --watch",
57
57
  "test:types": "tsc"
@@ -3,16 +3,23 @@ import { toRunErrorRawEvent } from '@tanstack/ai/adapter-internals'
3
3
  import { BaseTextAdapter } from '@tanstack/ai/adapters'
4
4
  import {
5
5
  SandboxCapability,
6
+ alignedIfAttaching,
6
7
  approvalId,
7
8
  buildApprovalRequestedEvent,
8
9
  createBridgeEventChannel,
10
+ createRunScopedIdGen,
11
+ encodeRunId,
9
12
  getSandbox,
13
+ getSandboxDurability,
10
14
  getSandboxPolicy,
11
15
  getToolBridgeProvisioner,
12
16
  getWorkspaceProjection,
17
+ journalOptionsFor,
13
18
  mergeChunkStreams,
14
19
  nodeHttpBridgeProvisioner,
15
20
  resolveApproval,
21
+ resolveDurableRunId,
22
+ resolveDurableThreadId,
16
23
  spawnNdjson,
17
24
  } from '@tanstack/ai-sandbox'
18
25
  import { buildPrompt } from '../messages/prompt'
@@ -301,8 +308,37 @@ export class ClaudeCodeTextAdapter<
301
308
  const sandbox = this.sandboxFrom(options)
302
309
  cleanupSandbox = sandbox
303
310
  const cwd = this.workdir(options)
304
- const runId = options.runId ?? this.generateId()
305
- const threadId = options.threadId ?? this.generateId()
311
+ // Durability comes from `withSandbox(sandbox, { runs, durability })`, read
312
+ // back off the capability bus. Absent it, everything below resolves to
313
+ // exactly today's behavior (no journal option, no alignment, and a
314
+ // generated `runId` when the caller didn't supply one).
315
+ const durability = options.capabilities
316
+ ? getSandboxDurability(options.capabilities, { optional: true })
317
+ : undefined
318
+ // The journaled path below derives its journal path and its
319
+ // message-id generator from `runId`. A resuming host must recompute
320
+ // the same `runId` to find the same journal file and reproduce the
321
+ // same translated ids — that's only possible when the caller supplies
322
+ // `runId` explicitly. `resolveDurableRunId` enforces that loudly (via
323
+ // `DurableRunIdRequiredError`) whenever durability is wired, and falls
324
+ // back to a fresh `this.generateId()` otherwise, preserving today's
325
+ // behavior for non-durable runs.
326
+ const runId = resolveDurableRunId(options.runId, {
327
+ durable: durability !== undefined,
328
+ adapter: 'claude-code',
329
+ fallback: () => this.generateId(),
330
+ })
331
+ // `threadId` is stamped on every chunk `translateSdkStream` emits, so an
332
+ // ATTACHING run that mints a fresh one replays a stream the stored log
333
+ // cannot match at index 0. `resolveDurableThreadId` refuses that up front
334
+ // instead of letting alignment discover it mid-stream; a durable FRESH run
335
+ // and a non-durable run both keep the generated fallback untouched.
336
+ const threadId = resolveDurableThreadId(options.threadId, {
337
+ durable: durability !== undefined,
338
+ attaching: durability?.attach === true,
339
+ adapter: 'claude-code',
340
+ fallback: () => this.generateId(),
341
+ })
306
342
  // Surfaces custom events from bridged tools (e.g. code mode console logs)
307
343
  // on this run's live output stream.
308
344
  channel = createBridgeEventChannel({ model: this.model, threadId, runId })
@@ -358,6 +394,16 @@ export class ClaudeCodeTextAdapter<
358
394
  options.messages,
359
395
  options.modelOptions?.sessionId,
360
396
  )
397
+ // Both files below name themselves after `runId`, and durability makes
398
+ // `runId` CALLER-chosen. Raw, a `/` in it would silently turn each basename
399
+ // into a nested path (writing outside the intended directory, or failing on
400
+ // one that does not exist), `..` would climb out of that directory, and a
401
+ // long id would fail the spawn with `ENAMETOOLONG`. `encodeRunId` collapses
402
+ // any id to one bounded, injective path segment — the same encoder
403
+ // `journalPaths` uses, so these files and the journal agree on how a given
404
+ // id spells. Computed once so the two filenames cannot drift apart.
405
+ const runIdSegment = encodeRunId(runId)
406
+
361
407
  // The bridge MCP config carries the per-run bearer token. Write it to a
362
408
  // file and pass claude the PATH, so the token never appears in argv (where
363
409
  // any process in the sandbox could read it via `ps` / `/proc/<pid>/cmdline`).
@@ -369,7 +415,7 @@ export class ClaudeCodeTextAdapter<
369
415
  // `/workspace` — e.g. local-process on Windows, where git-bash resolves
370
416
  // `/workspace` to `C:\Program Files\Git\workspace` and the file is "not
371
417
  // found". The bare filename resolves correctly on every provider.
372
- const mcpConfigFile = `.tanstack-mcp-bridge-${runId}.json`
418
+ const mcpConfigFile = `.tanstack-mcp-bridge-${runIdSegment}.json`
373
419
  const mcpConfigPath = `${cwd}/${mcpConfigFile}`
374
420
  await sandbox.fs.write(mcpConfigPath, bridgeToMcpConfig(bridge))
375
421
  tempFiles.push(mcpConfigPath)
@@ -392,7 +438,7 @@ export class ClaudeCodeTextAdapter<
392
438
  let runCommand = command
393
439
  let stdinInput: string | undefined = prompt
394
440
  if (sandbox.capabilities.writableStdin === false) {
395
- const promptPath = `/tmp/tanstack-claude-prompt-${runId}`
441
+ const promptPath = `/tmp/tanstack-claude-prompt-${runIdSegment}`
396
442
  await sandbox.fs.write(promptPath, prompt)
397
443
  tempFiles.push(promptPath)
398
444
  runCommand = `${command} < ${q(promptPath)}`
@@ -404,6 +450,7 @@ export class ClaudeCodeTextAdapter<
404
450
  { provider: 'claude-code', model: this.model },
405
451
  )
406
452
 
453
+ const journalOptions = journalOptionsFor(durability, runId)
407
454
  const rawEvents = spawnNdjson(sandbox, runCommand, {
408
455
  cwd,
409
456
  ...(stdinInput !== undefined ? { input: stdinInput } : {}),
@@ -423,27 +470,54 @@ export class ClaudeCodeTextAdapter<
423
470
  logger.provider(`provider=claude-code non-json line: ${line}`, {
424
471
  chunk: line,
425
472
  }),
473
+ // Journal + attach both come from the sandbox durability capability, so
474
+ // the attach route configures takeover by passing `attach: true` to
475
+ // `withSandbox` — `chat()` stays free of sandbox vocabulary. Omitted
476
+ // entirely (not passed as `undefined`) when the run isn't durable, so
477
+ // `spawnNdjson` takes its original, unjournaled path byte-for-byte.
478
+ ...(journalOptions === undefined ? {} : { journal: journalOptions }),
426
479
  })
427
480
 
428
481
  async function* asMessages(): AsyncIterable<AgentSdkMessage> {
429
482
  for await (const event of rawEvents) yield event as AgentSdkMessage
430
483
  }
431
484
 
432
- yield* mergeChunkStreams(
433
- translateSdkStream(asMessages(), {
434
- model: this.model,
435
- runId,
436
- threadId,
437
- ...(options.parentRunId !== undefined && {
438
- parentRunId: options.parentRunId,
439
- }),
440
- genId: () => this.generateId(),
441
- onSdkMessage: (message) =>
442
- logger.provider(`provider=claude-code type=${message.type}`, {
443
- chunk: message,
485
+ // `mergeChunkStreams` splices `channel.stream` (host-tool-bridge CUSTOM
486
+ // events from LIVE tool execution) into the deterministic translator
487
+ // output. Those events do not occur on a replay, so a takeover's replay is
488
+ // NOT chunk-for-chunk identical to what the log holds. `alignedIfAttaching`
489
+ // handles it: alignment skips stored out-of-band CUSTOM entries within a
490
+ // bounded window (see `align.ts`), so a bridged-tool run can be taken over
491
+ // without a spurious `JournalReplayDivergedError` — while a genuine
492
+ // determinism regression still throws. The wrap goes OUTSIDE the merge:
493
+ // the stored log holds the previous host's merged output, so aligning the
494
+ // pre-merge translated stream alone would compare against a log it never
495
+ // produced. On a non-attaching run this is a pure passthrough (see
496
+ // `alignedIfAttaching`'s `attach` guard).
497
+ yield* alignedIfAttaching(
498
+ mergeChunkStreams(
499
+ translateSdkStream(asMessages(), {
500
+ model: this.model,
501
+ runId,
502
+ threadId,
503
+ ...(options.parentRunId !== undefined && {
504
+ parentRunId: options.parentRunId,
444
505
  }),
445
- }),
446
- channel.stream,
506
+ // Deterministic on the journaled path: two translations of the same
507
+ // journal prefix from a fresh generator seeded with the same
508
+ // `runId` mint the same message ids (unlike `this.generateId()`,
509
+ // which mixes in `Date.now()` / `Math.random()`). See
510
+ // `createRunScopedIdGen` in `@tanstack/ai-sandbox`.
511
+ genId: createRunScopedIdGen(runId),
512
+ onSdkMessage: (message) =>
513
+ logger.provider(`provider=claude-code type=${message.type}`, {
514
+ chunk: message,
515
+ }),
516
+ }),
517
+ channel.stream,
518
+ ),
519
+ durability,
520
+ logger,
447
521
  )
448
522
 
449
523
  // Surface the working-tree diff so UIs can render what the agent changed.
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;"}