@tanstack/ai-opencode 0.2.3 → 0.2.5

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 OpencodeAssistantMessage,\n OpencodeEvent,\n OpencodePart,\n OpencodeStreamEvent,\n OpencodeTokens,\n} from './sdk-types'\n\n/** Name of the CUSTOM event carrying the OpenCode session id. */\nexport const SESSION_ID_EVENT = 'opencode.session-id'\n\n/** Name of the CUSTOM event carrying the harness's todo list updates. */\nexport const TODO_EVENT = 'opencode.todo'\n\n/** Server name used for bridged TanStack tools. */\nexport const BRIDGED_MCP_SERVER_NAME = 'tanstack'\n\nexport interface TranslateContext {\n model: string\n runId: string\n threadId: string\n parentRunId?: string\n genId: () => string\n /**\n * Names of bridged TanStack tools, used to surface the harness's MCP tool\n * calls under the names the application registered.\n */\n bridgedToolNames?: ReadonlySet<string>\n /** Called for each raw stream event, for logging. */\n onStreamEvent?: (event: OpencodeStreamEvent) => void\n}\n\n/**\n * Resolve the AG-UI tool-call name for an OpenCode tool part. OpenCode names\n * MCP tools `<server>_<tool>`, so bridged TanStack tools arrive as\n * `tanstack_<tool>` and are surfaced under the names the application\n * registered; everything else (built-in `read`, `edit`, `bash`, ... and\n * foreign MCP tools) uses the harness tool name verbatim.\n */\nexport function resolveToolName(\n tool: string,\n bridgedToolNames: ReadonlySet<string> | undefined,\n): string {\n if (!bridgedToolNames || bridgedToolNames.size === 0) return tool\n if (bridgedToolNames.has(tool)) return tool\n if (tool.startsWith('tanstack_') && bridgedToolNames.has(tool.slice(9))) {\n return tool.slice(9)\n }\n return tool\n}\n\nfunction buildUsage(\n tokens: OpencodeTokens | undefined,\n): TokenUsage | undefined {\n if (!tokens) return undefined\n const promptTokens = tokens.input ?? 0\n const completionTokens = tokens.output ?? 0\n const result = buildBaseUsage({\n promptTokens,\n completionTokens,\n totalTokens: promptTokens + completionTokens,\n })\n if (tokens.cache?.read) {\n result.promptTokensDetails = { cachedTokens: tokens.cache.read }\n }\n if (tokens.reasoning) {\n result.completionTokensDetails = { reasoningTokens: tokens.reasoning }\n }\n return result\n}\n\ntype TextPart = Extract<OpencodePart, { type: 'text' }>\ntype ReasoningPart = Extract<OpencodePart, { type: 'reasoning' }>\ntype ToolPart = Extract<OpencodePart, { type: 'tool' }>\n\nconst isTextPart = (part: OpencodePart): part is TextPart =>\n part.type === 'text'\nconst isReasoningPart = (part: OpencodePart): part is ReasoningPart =>\n part.type === 'reasoning'\nconst isToolPart = (part: OpencodePart): part is ToolPart =>\n part.type === 'tool'\n\nfunction messageError(\n message: OpencodeAssistantMessage,\n): { message: string } | undefined {\n if (!message.error) return undefined\n return { message: message.error.data?.message ?? message.error.name }\n}\n\n/**\n * Translate an OpenCode event stream into AG-UI StreamChunk 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 * OpenCode delivers true token-level deltas for both assistant text and\n * reasoning via `message.part.updated` events (a `delta` string when\n * incremental, otherwise the full part text, from which the delta is\n * derived). The final assistant message — finish reason, token usage, and any\n * fatal error — arrives as the terminal `done` event.\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* translateOpencodeStream(\n events: AsyncIterable<OpencodeStreamEvent>,\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, keyed by callID. */\n const unresolvedToolCalls = new Set<string>()\n /** Tool call ids that already emitted TOOL_CALL_START/ARGS/END. */\n const openedToolCalls = new Set<string>()\n /** Tool call ids that already emitted a TOOL_CALL_RESULT. */\n const resolvedToolCalls = new Set<string>()\n\n /** Accumulated text per text-part id, for delta derivation. */\n const textAccumulators = new Map<string, string>()\n let openTextId: string | null = null\n let openReasoningId: 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* closeText(): Generator<StreamChunk> {\n if (openTextId !== null) {\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId: openTextId,\n model,\n timestamp: now(),\n }\n openTextId = null\n }\n }\n\n function* closeReasoning(): Generator<StreamChunk> {\n if (openReasoningId !== null) {\n yield {\n type: EventType.REASONING_MESSAGE_END,\n messageId: openReasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: openReasoningId,\n model,\n timestamp: now(),\n }\n openReasoningId = null\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* handleTextPart(\n part: Extract<OpencodePart, { type: 'text' }>,\n delta: string | undefined,\n ): Generator<StreamChunk> {\n yield* closeReasoning()\n\n const prev = textAccumulators.get(part.id) ?? ''\n let deltaText: string\n if (typeof delta === 'string' && delta !== '') {\n deltaText = delta\n textAccumulators.set(part.id, prev + delta)\n } else {\n const full = part.text\n deltaText = full.startsWith(prev) ? full.slice(prev.length) : full\n textAccumulators.set(part.id, full)\n }\n if (deltaText === '') return\n\n if (openTextId !== part.id) {\n yield* closeText()\n openTextId = part.id\n yield {\n type: EventType.TEXT_MESSAGE_START,\n messageId: part.id,\n model,\n timestamp: now(),\n role: 'assistant',\n }\n }\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId: part.id,\n model,\n timestamp: now(),\n delta: deltaText,\n content: textAccumulators.get(part.id) ?? deltaText,\n }\n }\n\n function* handleReasoningPart(\n part: Extract<OpencodePart, { type: 'reasoning' }>,\n delta: string | undefined,\n ): Generator<StreamChunk> {\n yield* closeText()\n\n const prev = textAccumulators.get(part.id) ?? ''\n let deltaText: string\n if (typeof delta === 'string' && delta !== '') {\n deltaText = delta\n textAccumulators.set(part.id, prev + delta)\n } else {\n const full = part.text\n deltaText = full.startsWith(prev) ? full.slice(prev.length) : full\n textAccumulators.set(part.id, full)\n }\n if (deltaText === '') return\n\n if (openReasoningId !== part.id) {\n yield* closeReasoning()\n openReasoningId = part.id\n yield {\n type: EventType.REASONING_START,\n messageId: part.id,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_START,\n messageId: part.id,\n role: 'reasoning' as const,\n model,\n timestamp: now(),\n }\n }\n yield {\n type: EventType.REASONING_MESSAGE_CONTENT,\n messageId: part.id,\n delta: deltaText,\n model,\n timestamp: now(),\n }\n }\n\n function* openToolCall(\n part: Extract<OpencodePart, { type: 'tool' }>,\n ): Generator<StreamChunk> {\n if (openedToolCalls.has(part.callID)) return\n openedToolCalls.add(part.callID)\n const toolCallName = resolveToolName(part.tool, ctx.bridgedToolNames)\n const input = part.state.input ?? {}\n const args = JSON.stringify(input)\n yield {\n type: EventType.TOOL_CALL_START,\n toolCallId: part.callID,\n toolCallName,\n toolName: toolCallName,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: part.callID,\n model,\n timestamp: now(),\n delta: args,\n args,\n }\n yield {\n type: EventType.TOOL_CALL_END,\n toolCallId: part.callID,\n toolCallName,\n toolName: toolCallName,\n model,\n timestamp: now(),\n input,\n }\n unresolvedToolCalls.add(part.callID)\n }\n\n function* handleToolPart(\n part: Extract<OpencodePart, { type: 'tool' }>,\n ): Generator<StreamChunk> {\n yield* closeText()\n yield* closeReasoning()\n yield* openToolCall(part)\n\n const state = part.state\n if (state.status !== 'completed' && state.status !== 'error') return\n if (resolvedToolCalls.has(part.callID)) return\n resolvedToolCalls.add(part.callID)\n unresolvedToolCalls.delete(part.callID)\n\n const isError = state.status === 'error'\n yield {\n type: EventType.TOOL_CALL_RESULT,\n toolCallId: part.callID,\n messageId: genId(),\n model,\n timestamp: now(),\n content: isError ? state.error : state.output,\n ...(isError && { state: 'output-error' as const }),\n }\n }\n\n function* handleEvent(event: OpencodeEvent): Generator<StreamChunk> {\n if (event.type === 'message.part.updated') {\n const { part, delta } = event.properties\n if (isTextPart(part)) {\n yield* handleTextPart(part, delta)\n } else if (isReasoningPart(part)) {\n yield* handleReasoningPart(part, delta)\n } else if (isToolPart(part)) {\n yield* handleToolPart(part)\n }\n // Other part kinds (file, step-start/finish, snapshot, ...) carry no\n // state the chunk stream needs.\n } else if (event.type === 'todo.updated') {\n yield {\n type: EventType.CUSTOM,\n model,\n timestamp: now(),\n name: TODO_EVENT,\n value: { todos: event.properties.todos },\n }\n }\n // session.idle / session.status / message.updated are redundant with the\n // terminal `done` event and are ignored.\n }\n\n function* finish(message: OpencodeAssistantMessage): Generator<StreamChunk> {\n yield* startRun()\n yield* closeText()\n yield* closeReasoning()\n yield* synthesizeUnresolvedResults()\n\n const error = messageError(message)\n if (error) {\n yield {\n type: EventType.RUN_ERROR,\n model,\n timestamp: now(),\n message: error.message,\n error,\n }\n return\n }\n\n const usage = buildUsage(message.tokens)\n const finishReason = message.finish === 'length' ? 'length' : 'stop'\n yield {\n type: EventType.RUN_FINISHED,\n runId,\n threadId,\n model,\n timestamp: now(),\n finishReason,\n ...(usage !== undefined && { usage }),\n }\n }\n\n try {\n for await (const streamEvent of events) {\n ctx.onStreamEvent?.(streamEvent)\n\n if (streamEvent.kind === 'session') {\n yield* startRun()\n yield {\n type: EventType.CUSTOM,\n model,\n timestamp: now(),\n name: SESSION_ID_EVENT,\n value: { sessionId: streamEvent.sessionId },\n }\n } else if (streamEvent.kind === 'event') {\n yield* startRun()\n yield* handleEvent(streamEvent.event)\n } else {\n yield* finish(streamEvent.message)\n }\n }\n } catch (error) {\n // The run is dying (abort, server exit, or connection failure). Close any\n // open message and pair started tool calls with a synthetic result first\n // so the next request's pending-tool-call scan doesn't try to execute\n // them, then let the adapter surface the error as RUN_ERROR.\n yield* closeText()\n yield* closeReasoning()\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 OpencodeAssistantMessage,\n OpencodeEvent,\n OpencodePart,\n OpencodeStreamEvent,\n OpencodeTokens,\n} from './sdk-types'\n\n/** Name of the CUSTOM event carrying the OpenCode session id. */\nexport const SESSION_ID_EVENT = 'opencode.session-id'\n\n/** Name of the CUSTOM event carrying the harness's todo list updates. */\nexport const TODO_EVENT = 'opencode.todo'\n\n/** Server name used for bridged TanStack tools. */\nexport const BRIDGED_MCP_SERVER_NAME = 'tanstack'\n\nexport interface TranslateContext {\n model: string\n runId: string\n threadId: string\n parentRunId?: string\n genId: () => string\n /**\n * Names of bridged TanStack tools, used to surface the harness's MCP tool\n * calls under the names the application registered.\n */\n bridgedToolNames?: ReadonlySet<string>\n /** Called for each raw stream event, for logging. */\n onStreamEvent?: (event: OpencodeStreamEvent) => void\n}\n\n/**\n * Resolve the AG-UI tool-call name for an OpenCode tool part. OpenCode names\n * MCP tools `<server>_<tool>`, so bridged TanStack tools arrive as\n * `tanstack_<tool>` and are surfaced under the names the application\n * registered; everything else (built-in `read`, `edit`, `bash`, ... and\n * foreign MCP tools) uses the harness tool name verbatim.\n */\nexport function resolveToolName(\n tool: string,\n bridgedToolNames: ReadonlySet<string> | undefined,\n): string {\n if (!bridgedToolNames || bridgedToolNames.size === 0) return tool\n if (bridgedToolNames.has(tool)) return tool\n if (tool.startsWith('tanstack_') && bridgedToolNames.has(tool.slice(9))) {\n return tool.slice(9)\n }\n return tool\n}\n\nfunction buildUsage(\n tokens: OpencodeTokens | undefined,\n): TokenUsage | undefined {\n if (!tokens) return undefined\n const promptTokens = tokens.input ?? 0\n const completionTokens = tokens.output ?? 0\n const result = buildBaseUsage({\n promptTokens,\n completionTokens,\n totalTokens: promptTokens + completionTokens,\n })\n if (tokens.cache?.read) {\n result.promptTokensDetails = { cachedTokens: tokens.cache.read }\n }\n if (tokens.reasoning) {\n result.completionTokensDetails = { reasoningTokens: tokens.reasoning }\n }\n return result\n}\n\ntype TextPart = Extract<OpencodePart, { type: 'text' }>\ntype ReasoningPart = Extract<OpencodePart, { type: 'reasoning' }>\ntype ToolPart = Extract<OpencodePart, { type: 'tool' }>\n\nconst isTextPart = (part: OpencodePart): part is TextPart =>\n part.type === 'text'\nconst isReasoningPart = (part: OpencodePart): part is ReasoningPart =>\n part.type === 'reasoning'\nconst isToolPart = (part: OpencodePart): part is ToolPart =>\n part.type === 'tool'\n\nfunction messageError(\n message: OpencodeAssistantMessage,\n): { message: string } | undefined {\n if (!message.error) return undefined\n return { message: message.error.data?.message ?? message.error.name }\n}\n\n/**\n * Translate an OpenCode event stream into AG-UI StreamChunk 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 * OpenCode delivers true token-level deltas for both assistant text and\n * reasoning via `message.part.updated` events (a `delta` string when\n * incremental, otherwise the full part text, from which the delta is\n * derived). The final assistant message — finish reason, token usage, and any\n * fatal error — arrives as the terminal `done` event.\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* translateOpencodeStream(\n events: AsyncIterable<OpencodeStreamEvent>,\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, keyed by callID. */\n const unresolvedToolCalls = new Set<string>()\n /** Tool call ids that already emitted TOOL_CALL_START/ARGS/END. */\n const openedToolCalls = new Set<string>()\n /** Tool call ids that already emitted a TOOL_CALL_RESULT. */\n const resolvedToolCalls = new Set<string>()\n\n /** Accumulated text per text-part id, for delta derivation. */\n const textAccumulators = new Map<string, string>()\n let openTextId: string | null = null\n let openReasoningId: 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* closeText(): Generator<StreamChunk> {\n if (openTextId !== null) {\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId: openTextId,\n model,\n timestamp: now(),\n }\n openTextId = null\n }\n }\n\n function* closeReasoning(): Generator<StreamChunk> {\n if (openReasoningId !== null) {\n yield {\n type: EventType.REASONING_MESSAGE_END,\n messageId: openReasoningId,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: openReasoningId,\n model,\n timestamp: now(),\n }\n openReasoningId = null\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* handleTextPart(\n part: Extract<OpencodePart, { type: 'text' }>,\n delta: string | undefined,\n ): Generator<StreamChunk> {\n yield* closeReasoning()\n\n const prev = textAccumulators.get(part.id) ?? ''\n let deltaText: string\n if (typeof delta === 'string' && delta !== '') {\n deltaText = delta\n textAccumulators.set(part.id, prev + delta)\n } else {\n const full = part.text\n deltaText = full.startsWith(prev) ? full.slice(prev.length) : full\n textAccumulators.set(part.id, full)\n }\n if (deltaText === '') return\n\n if (openTextId !== part.id) {\n yield* closeText()\n openTextId = part.id\n yield {\n type: EventType.TEXT_MESSAGE_START,\n messageId: part.id,\n model,\n timestamp: now(),\n role: 'assistant',\n }\n }\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId: part.id,\n model,\n timestamp: now(),\n delta: deltaText,\n content: textAccumulators.get(part.id) ?? deltaText,\n }\n }\n\n function* handleReasoningPart(\n part: Extract<OpencodePart, { type: 'reasoning' }>,\n delta: string | undefined,\n ): Generator<StreamChunk> {\n yield* closeText()\n\n const prev = textAccumulators.get(part.id) ?? ''\n let deltaText: string\n if (typeof delta === 'string' && delta !== '') {\n deltaText = delta\n textAccumulators.set(part.id, prev + delta)\n } else {\n const full = part.text\n deltaText = full.startsWith(prev) ? full.slice(prev.length) : full\n textAccumulators.set(part.id, full)\n }\n if (deltaText === '') return\n\n if (openReasoningId !== part.id) {\n yield* closeReasoning()\n openReasoningId = part.id\n yield {\n type: EventType.REASONING_START,\n messageId: part.id,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_START,\n messageId: part.id,\n role: 'reasoning' as const,\n model,\n timestamp: now(),\n }\n }\n yield {\n type: EventType.REASONING_MESSAGE_CONTENT,\n messageId: part.id,\n delta: deltaText,\n model,\n timestamp: now(),\n }\n }\n\n function* openToolCall(\n part: Extract<OpencodePart, { type: 'tool' }>,\n ): Generator<StreamChunk> {\n if (openedToolCalls.has(part.callID)) return\n openedToolCalls.add(part.callID)\n const toolCallName = resolveToolName(part.tool, ctx.bridgedToolNames)\n const input = part.state.input ?? {}\n const args = JSON.stringify(input)\n yield {\n type: EventType.TOOL_CALL_START,\n toolCallId: part.callID,\n toolCallName,\n toolName: toolCallName,\n model,\n timestamp: now(),\n }\n yield {\n type: EventType.TOOL_CALL_ARGS,\n toolCallId: part.callID,\n model,\n timestamp: now(),\n delta: args,\n args,\n }\n yield {\n type: EventType.TOOL_CALL_END,\n toolCallId: part.callID,\n toolCallName,\n toolName: toolCallName,\n model,\n timestamp: now(),\n input,\n }\n unresolvedToolCalls.add(part.callID)\n }\n\n function* handleToolPart(\n part: Extract<OpencodePart, { type: 'tool' }>,\n ): Generator<StreamChunk> {\n yield* closeText()\n yield* closeReasoning()\n yield* openToolCall(part)\n\n const state = part.state\n if (state.status !== 'completed' && state.status !== 'error') return\n if (resolvedToolCalls.has(part.callID)) return\n resolvedToolCalls.add(part.callID)\n unresolvedToolCalls.delete(part.callID)\n\n const isError = state.status === 'error'\n yield {\n type: EventType.TOOL_CALL_RESULT,\n toolCallId: part.callID,\n messageId: genId(),\n model,\n timestamp: now(),\n content: isError ? state.error : state.output,\n ...(isError && { state: 'output-error' as const }),\n }\n }\n\n function* handleEvent(event: OpencodeEvent): Generator<StreamChunk> {\n if (event.type === 'message.part.updated') {\n const { part, delta } = event.properties\n if (isTextPart(part)) {\n yield* handleTextPart(part, delta)\n } else if (isReasoningPart(part)) {\n yield* handleReasoningPart(part, delta)\n } else if (isToolPart(part)) {\n yield* handleToolPart(part)\n }\n // Other part kinds (file, step-start/finish, snapshot, ...) carry no\n // state the chunk stream needs.\n } else if (event.type === 'todo.updated') {\n yield {\n type: EventType.CUSTOM,\n model,\n timestamp: now(),\n name: TODO_EVENT,\n value: { todos: event.properties.todos },\n }\n }\n // session.idle / session.status / message.updated are redundant with the\n // terminal `done` event and are ignored.\n }\n\n function* finish(message: OpencodeAssistantMessage): Generator<StreamChunk> {\n yield* startRun()\n yield* closeText()\n yield* closeReasoning()\n yield* synthesizeUnresolvedResults()\n\n const error = messageError(message)\n if (error) {\n yield {\n type: EventType.RUN_ERROR,\n model,\n timestamp: now(),\n message: error.message,\n error,\n }\n return\n }\n\n const usage = buildUsage(message.tokens)\n const finishReason = message.finish === 'length' ? 'length' : 'stop'\n yield {\n type: EventType.RUN_FINISHED,\n runId,\n threadId,\n model,\n timestamp: now(),\n finishReason,\n ...(usage !== undefined && { usage }),\n }\n }\n\n try {\n for await (const streamEvent of events) {\n ctx.onStreamEvent?.(streamEvent)\n\n if (streamEvent.kind === 'session') {\n yield* startRun()\n yield {\n type: EventType.CUSTOM,\n model,\n timestamp: now(),\n name: SESSION_ID_EVENT,\n value: { sessionId: streamEvent.sessionId },\n }\n } else if (streamEvent.kind === 'event') {\n yield* startRun()\n yield* handleEvent(streamEvent.event)\n } else {\n yield* finish(streamEvent.message)\n }\n }\n } catch (error) {\n // The run is dying (abort, server exit, or connection failure). Close any\n // open message and pair started tool calls with a synthetic result first\n // so the next request's pending-tool-call scan doesn't try to execute\n // them, then let the adapter surface the error as RUN_ERROR.\n yield* closeText()\n yield* closeReasoning()\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-opencode",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "OpenCode harness adapter for TanStack AI — run OpenCode as a chat backend with local tool execution and stateful sessions.",
5
5
  "author": "",
6
6
  "license": "MIT",
@@ -38,20 +38,20 @@
38
38
  "@opencode-ai/sdk": "^1.17.4"
39
39
  },
40
40
  "peerDependencies": {
41
- "@tanstack/ai": "^0.42.0",
42
- "@tanstack/ai-sandbox": "^0.2.4"
41
+ "@tanstack/ai": "^0.44.0",
42
+ "@tanstack/ai-sandbox": "^0.3.1"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@vitest/coverage-v8": "4.0.14",
46
- "@tanstack/ai": "0.42.0",
47
- "@tanstack/ai-sandbox": "0.2.4"
46
+ "@tanstack/ai": "0.44.0",
47
+ "@tanstack/ai-sandbox": "0.3.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"
@@ -2,14 +2,17 @@ import { EventType, normalizeSystemPrompts } from '@tanstack/ai'
2
2
  import { toRunErrorRawEvent } from '@tanstack/ai/adapter-internals'
3
3
  import { BaseTextAdapter } from '@tanstack/ai/adapters'
4
4
  import {
5
+ DurableAttachNotSupportedError,
5
6
  SandboxCapability,
6
7
  buildApprovalRequestedEvent,
7
8
  createBridgeEventChannel,
8
9
  getSandbox,
10
+ getSandboxDurability,
9
11
  getToolBridgeProvisioner,
10
12
  getWorkspaceProjection,
11
13
  mergeChunkStreams,
12
14
  nodeHttpBridgeProvisioner,
15
+ resolveDurableRunId,
13
16
  } from '@tanstack/ai-sandbox'
14
17
  import { buildPrompt } from '../messages/prompt'
15
18
  import { startOpencodeSession } from '../process/server'
@@ -130,7 +133,14 @@ export class OpencodeTextAdapter<
130
133
  const externalSignal =
131
134
  options.abortController?.signal ?? options.request?.signal ?? undefined
132
135
  let onAbort: (() => void) | undefined
133
- const runId = options.runId ?? this.generateId()
136
+ // This adapter does not journal yet, so a generated id is still fine.
137
+ // Routed through the helper anyway so journaling here inherits the
138
+ // caller-supplied-runId requirement instead of re-deriving it.
139
+ const runId = resolveDurableRunId(options.runId, {
140
+ durable: false,
141
+ adapter: 'opencode',
142
+ fallback: () => this.generateId(),
143
+ })
134
144
  const threadId = options.threadId ?? this.generateId()
135
145
  // Surfaces custom events from bridged tools (e.g. code mode console logs)
136
146
  // on this run's live output stream.
@@ -147,6 +157,45 @@ export class OpencodeTextAdapter<
147
157
  this.adapterConfig.directory ??
148
158
  DEFAULT_WORKDIR
149
159
 
160
+ // Durability wired onto a path that cannot deliver it. Two outcomes, split
161
+ // by whether a first attempt has already run.
162
+ //
163
+ // ATTACH is fatal. `sandboxRunDriver`'s `drive()` sets `attach: true` only
164
+ // when a previous host was already streaming this run, so continuing past
165
+ // here reaches `startOpencodeServerInSandbox` + the session prompt and
166
+ // re-runs the agent from scratch against the workspace that attempt
167
+ // already mutated, appending its whole output to a log that still holds
168
+ // the first attempt's. This adapter has no journal to tail, no
169
+ // `awaitAttachableJournal` to refuse the attach up front, and no
170
+ // `alignedIfAttaching` to suppress the already-delivered prefix — so there
171
+ // is nothing between here and that corruption except this throw.
172
+ //
173
+ // A FRESH durable run only fails to be recoverable LATER, which an app may
174
+ // knowingly accept (it can wire `withSandbox({ runs, durability })` once at
175
+ // the middleware level and still route some runs through this adapter). So
176
+ // that is a warn, not a throw: audible, not fatal. Once per run, not per
177
+ // chunk — a per-chunk warning would be worse than none. Mirrors
178
+ // `ai-grok-build`'s `chatStreamAcp`.
179
+ const durability = options.capabilities
180
+ ? getSandboxDurability(options.capabilities, { optional: true })
181
+ : undefined
182
+ if (durability !== undefined) {
183
+ if (durability.attach) {
184
+ throw new DurableAttachNotSupportedError(
185
+ 'opencode',
186
+ 'this adapter drives the harness over the opencode HTTP server ' +
187
+ 'and does not journal',
188
+ )
189
+ }
190
+ logger.warn(
191
+ 'opencode: sandbox durability is wired but this adapter never ' +
192
+ 'journals — this run will not be recoverable on reconnect. Use a ' +
193
+ 'journaling harness adapter for runs that must survive a host ' +
194
+ 'restart, or drop durability if these runs are not meant to.',
195
+ { runId, adapter: 'opencode' },
196
+ )
197
+ }
198
+
150
199
  // Project workspace skills / MCP servers into the sandbox before starting
151
200
  // the opencode server so the workspace config is in place for the session.
152
201
  if (options.capabilities !== undefined) {
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;"}