@tanstack/ai-opencode 0.3.3 → 0.3.4
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,6 +1,6 @@
|
|
|
1
1
|
import { BaseTextAdapter, StructuredOutputOptions, StructuredOutputResult } from '@tanstack/ai/adapters';
|
|
2
2
|
import { SandboxHandle } from '@tanstack/ai-sandbox';
|
|
3
|
-
import { DefaultMessageMetadataByModality, Modality,
|
|
3
|
+
import { DefaultMessageMetadataByModality, Modality, AdapterYieldChunk, TextOptions } from '@tanstack/ai';
|
|
4
4
|
import { OpencodePermissionMode, PermissionHandler } from '../process/permissions.js';
|
|
5
5
|
import { OpencodeModel } from '../model-meta.js';
|
|
6
6
|
import { OpencodeTextProviderOptions } from '../provider-options.js';
|
|
@@ -33,7 +33,7 @@ export declare class OpencodeTextAdapter<TModel extends OpencodeModel> extends B
|
|
|
33
33
|
supportsCombinedToolsAndSchema(): boolean;
|
|
34
34
|
combinedStructuredOutputSource(): 'event';
|
|
35
35
|
private applySystemPrompts;
|
|
36
|
-
chatStream(options: TextOptions<OpencodeTextProviderOptions>): AsyncIterable<
|
|
36
|
+
chatStream(options: TextOptions<OpencodeTextProviderOptions>): AsyncIterable<AdapterYieldChunk>;
|
|
37
37
|
structuredOutput(_options: StructuredOutputOptions<OpencodeTextProviderOptions>): Promise<StructuredOutputResult<unknown>>;
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text.js","names":[],"sources":["../../../src/adapters/text.ts"],"sourcesContent":["import { EventType, normalizeSystemPrompts } from '@tanstack/ai'\nimport {\n appendOutputSchemaInstruction,\n parseJsonFromAssistantText,\n structuredOutputCompleteChunk,\n structuredOutputStartChunk,\n toRunErrorRawEvent,\n} from '@tanstack/ai/adapter-internals'\nimport { BaseTextAdapter } from '@tanstack/ai/adapters'\nimport {\n DurableAttachNotSupportedError,\n SandboxCapability,\n buildApprovalRequestedEvent,\n createBridgeEventChannel,\n getSandbox,\n getSandboxDurability,\n getToolBridgeProvisioner,\n getWorkspaceProjection,\n mergeChunkStreams,\n nodeHttpBridgeProvisioner,\n resolveDurableRunId,\n} from '@tanstack/ai-sandbox'\nimport { buildPrompt } from '../messages/prompt'\nimport { startOpencodeSession } from '../process/server'\nimport { startOpencodeServerInSandbox } from '../process/sandbox-server'\nimport { resolveInteractivePermission } from '../process/permissions'\nimport { AsyncQueue } from '../stream/queue'\nimport { translateOpencodeStream } from '../stream/translate'\nimport { projectOpencodeWorkspace } from './projection'\nimport type { HostToolBridge, SandboxHandle } from '@tanstack/ai-sandbox'\nimport type {\n StructuredOutputOptions,\n StructuredOutputResult,\n} from '@tanstack/ai/adapters'\nimport type {\n DefaultMessageMetadataByModality,\n Modality,\n StreamChunk,\n TextOptions,\n} from '@tanstack/ai'\nimport type { OpencodeSessionHandle } from '../process/server'\nimport type {\n OpencodePermissionMode,\n PermissionHandler,\n} from '../process/permissions'\nimport type { OpencodeStreamEvent } from '../stream/sdk-types'\nimport type { OpencodeModel } from '../model-meta'\nimport type { OpencodeTextProviderOptions } from '../provider-options'\n\nconst DEFAULT_WORKDIR = '/workspace'\nconst DEFAULT_PORT = 4096\n\nexport interface OpencodeTextConfig {\n /** Working directory inside the sandbox. Defaults to `/workspace`. */\n directory?: string\n /**\n * Port the in-sandbox `opencode serve` listens on. Defaults to 4096. For the\n * Docker provider this port must also be published (`publishPorts: [4096]`)\n * so the host can reach it.\n */\n port?: number\n /** Hostname the in-sandbox server binds. Defaults to `0.0.0.0`. */\n hostname?: string\n /**\n * OpenCode permission mode driving the dynamic permission handler. Defaults\n * to `'default'`; set `'acceptEdits'` / `'bypassPermissions'` to let the\n * harness edit files and run commands autonomously inside the sandbox.\n */\n permissionMode?: OpencodePermissionMode\n /** Custom permission handler; replaces the adapter's default policy. */\n onPermissionRequest?: PermissionHandler\n}\n\n/** Split a `provider/model` id into its provider and model halves. */\nfunction splitModel(model: string): { providerID: string; modelID: string } {\n const slash = model.indexOf('/')\n if (slash <= 0 || slash === model.length - 1) {\n throw new Error(\n `OpenCode models must be addressed as \"provider/model\" (e.g. \"anthropic/claude-sonnet-4-5\"); received \"${model}\".`,\n )\n }\n return { providerID: model.slice(0, slash), modelID: model.slice(slash + 1) }\n}\n\nexport class OpencodeTextAdapter<\n TModel extends OpencodeModel,\n> extends BaseTextAdapter<\n TModel,\n OpencodeTextProviderOptions,\n ReadonlyArray<Modality> & readonly ['text'],\n DefaultMessageMetadataByModality,\n ReadonlyArray<string>,\n unknown,\n never\n> {\n readonly name = 'opencode' as const\n\n override readonly requires = [SandboxCapability] as const\n\n private readonly adapterConfig: OpencodeTextConfig\n\n constructor(config: OpencodeTextConfig, model: TModel) {\n super({}, model)\n this.adapterConfig = config\n }\n\n private sandboxFrom(\n options: TextOptions<OpencodeTextProviderOptions>,\n ): SandboxHandle {\n const ctx = options.capabilities\n if (!ctx) {\n throw new Error(\n 'Adapter \"opencode\" requires a sandbox. Add withSandbox(defineSandbox({ ... })) to chat() middleware.',\n )\n }\n return getSandbox(ctx)\n }\n\n supportsCombinedToolsAndSchema(): boolean {\n return true\n }\n\n combinedStructuredOutputSource(): 'event' {\n return 'event'\n }\n\n private applySystemPrompts(\n options: TextOptions<OpencodeTextProviderOptions>,\n prompt: string,\n ): string {\n const systemPrompts = normalizeSystemPrompts(options.systemPrompts)\n .map((systemPrompt) => systemPrompt.content)\n .filter((content) => content.trim() !== '')\n if (systemPrompts.length === 0) return prompt\n return `${systemPrompts.join('\\n\\n')}\\n\\n${prompt}`\n }\n\n async *chatStream(\n options: TextOptions<OpencodeTextProviderOptions>,\n ): AsyncIterable<StreamChunk> {\n const { logger } = options\n let server:\n | Awaited<ReturnType<typeof startOpencodeServerInSandbox>>\n | undefined\n let handle: OpencodeSessionHandle | undefined\n let bridge: HostToolBridge | undefined\n const externalSignal =\n options.abortController?.signal ?? options.request?.signal ?? undefined\n let onAbort: (() => void) | undefined\n // This adapter does not journal yet, so a generated id is still fine.\n // Routed through the helper anyway so journaling here inherits the\n // caller-supplied-runId requirement instead of re-deriving it.\n const runId = resolveDurableRunId(options.runId, {\n durable: false,\n adapter: 'opencode',\n fallback: () => this.generateId(),\n })\n const threadId = options.threadId ?? this.generateId()\n // Surfaces custom events from bridged tools (e.g. code mode console logs)\n // on this run's live output stream.\n const channel = createBridgeEventChannel({\n model: this.model,\n threadId,\n runId,\n })\n\n try {\n const sandbox = this.sandboxFrom(options)\n const directory =\n options.modelOptions?.directory ??\n this.adapterConfig.directory ??\n DEFAULT_WORKDIR\n\n // Durability wired onto a path that cannot deliver it. Two outcomes, split\n // by whether a first attempt has already run.\n //\n // ATTACH is fatal. `sandboxRunDriver`'s `drive()` sets `attach: true` only\n // when a previous host was already streaming this run, so continuing past\n // here reaches `startOpencodeServerInSandbox` + the session prompt and\n // re-runs the agent from scratch against the workspace that attempt\n // already mutated, appending its whole output to a log that still holds\n // the first attempt's. This adapter has no journal to tail, no\n // `awaitAttachableJournal` to refuse the attach up front, and no\n // `alignedIfAttaching` to suppress the already-delivered prefix — so there\n // is nothing between here and that corruption except this throw.\n //\n // A FRESH durable run only fails to be recoverable LATER, which an app may\n // knowingly accept (it can wire `withSandbox({ runs, durability })` once at\n // the middleware level and still route some runs through this adapter). So\n // that is a warn, not a throw: audible, not fatal. Once per run, not per\n // chunk — a per-chunk warning would be worse than none. Mirrors\n // `ai-grok-build`'s `chatStreamAcp`.\n const durability = options.capabilities\n ? getSandboxDurability(options.capabilities, { optional: true })\n : undefined\n if (durability !== undefined) {\n if (durability.attach) {\n throw new DurableAttachNotSupportedError(\n 'opencode',\n 'this adapter drives the harness over the opencode HTTP server ' +\n 'and does not journal',\n )\n }\n logger.warn(\n 'opencode: sandbox durability is wired but this adapter never ' +\n 'journals — this run will not be recoverable on reconnect. Use a ' +\n 'journaling harness adapter for runs that must survive a host ' +\n 'restart, or drop durability if these runs are not meant to.',\n { runId, adapter: 'opencode' },\n )\n }\n\n // Project workspace skills / MCP servers into the sandbox before starting\n // the opencode server so the workspace config is in place for the session.\n if (options.capabilities !== undefined) {\n const projection = getWorkspaceProjection(options.capabilities, {\n optional: true,\n })\n if (projection !== undefined) {\n await projectOpencodeWorkspace(sandbox, projection)\n }\n }\n\n const modelOptions = options.modelOptions\n const sessionId = modelOptions?.sessionId\n const { prompt: resumePrompt } = buildPrompt(options.messages, sessionId)\n const { providerID, modelID } = splitModel(this.model)\n\n // Bridge chat()-provided tools into the in-sandbox server over MCP\n // (configured via OPENCODE_CONFIG_CONTENT at server spawn).\n const bridgedToolNames = new Set(\n (options.tools ?? []).map((tool) => tool.name),\n )\n if (options.tools && options.tools.length > 0) {\n const provisioner =\n (options.capabilities\n ? getToolBridgeProvisioner(options.capabilities, { optional: true })\n : undefined) ?? nodeHttpBridgeProvisioner\n bridge = await provisioner.provision(options.tools, {\n provider: sandbox.provider,\n context: options.context,\n emitCustomEvent: channel.emitCustomEvent,\n ...(externalSignal ? { signal: externalSignal } : {}),\n })\n }\n\n // Approval-requested events for `ask`-policy actions with no client\n // decision yet, emitted after the stream so the client can approve + re-run.\n const approvalRequests: Array<StreamChunk> = []\n\n const queue = new AsyncQueue<OpencodeStreamEvent>()\n const mode =\n modelOptions?.permissionMode ??\n this.adapterConfig.permissionMode ??\n 'default'\n const permissionHandler: PermissionHandler =\n this.adapterConfig.onPermissionRequest ??\n ((request) => {\n const result = resolveInteractivePermission(\n request,\n mode,\n bridgedToolNames,\n options.approvals,\n )\n if (result.approvalId !== undefined) {\n approvalRequests.push(\n buildApprovalRequestedEvent({\n approvalId: result.approvalId,\n title: result.title ?? request.title,\n threadId,\n runId,\n detail: { provider: 'opencode' },\n }),\n )\n }\n return result.response\n })\n\n logger.request(\n `activity=chat provider=opencode model=${this.model} sandbox=${sandbox.provider} messages=${options.messages.length} resume=${sessionId ?? 'none'}`,\n { provider: 'opencode', model: this.model },\n )\n\n const serverEnv = bridge\n ? {\n OPENCODE_CONFIG_CONTENT: JSON.stringify({\n mcp: {\n [bridge.name]: {\n type: 'remote',\n url: bridge.url,\n enabled: true,\n headers: { Authorization: `Bearer ${bridge.token}` },\n },\n },\n }),\n }\n : undefined\n\n server = await startOpencodeServerInSandbox(sandbox, {\n port: this.adapterConfig.port ?? DEFAULT_PORT,\n ...(this.adapterConfig.hostname !== undefined && {\n hostname: this.adapterConfig.hostname,\n }),\n cwd: directory,\n ...(serverEnv ? { env: serverEnv } : {}),\n ...(externalSignal ? { signal: externalSignal } : {}),\n })\n\n handle = await startOpencodeSession({\n baseUrl: server.baseUrl,\n // Forward the channel's auth headers (e.g. Daytona's preview token) so\n // the host client can reach a token-gated preview proxy.\n ...(server.headers !== undefined && { headers: server.headers }),\n // NOTE: do NOT pass `directory` here. `directory` is the VIRTUAL sandbox\n // path (e.g. `/workspace`); the server is already spawned with that as its\n // cwd (the provider handle maps it to the real workdir — `/workspace`\n // inside Docker, a host temp dir for local-process). Forwarding the\n // virtual path to the host-side opencode HTTP API breaks local-process,\n // where `/workspace` doesn't exist → the API stalls until the request\n // times out. Omitting it makes opencode use the server's (correct) cwd.\n providerID,\n modelID,\n ...(sessionId !== undefined && { resumeSessionId: sessionId }),\n onEvent: (event) => queue.push({ kind: 'event', event }),\n onPermissionRequest: permissionHandler,\n onError: (error) => queue.fail(error),\n })\n const session = handle\n\n if (externalSignal !== undefined) {\n onAbort = () => void session.abort().catch(() => undefined)\n if (externalSignal.aborted) onAbort()\n else externalSignal.addEventListener('abort', onAbort, { once: true })\n }\n\n queue.push({ kind: 'session', sessionId: session.sessionId })\n\n let promptText = this.applySystemPrompts(\n options,\n session.resumed || sessionId === undefined\n ? resumePrompt\n : buildPrompt(options.messages, undefined).prompt,\n )\n if (options.outputSchema) {\n promptText = appendOutputSchemaInstruction(\n promptText,\n options.outputSchema,\n )\n }\n\n let lastAssistantText = ''\n session\n .prompt(promptText)\n .then(({ message, text }) => {\n lastAssistantText = text\n queue.push({ kind: 'done', message })\n queue.end()\n })\n .catch((error: unknown) => queue.fail(error))\n\n let heldFinished: StreamChunk | undefined\n let lastTextMessageId: string | undefined\n for await (const chunk of mergeChunkStreams(\n translateOpencodeStream(queue, {\n model: this.model,\n runId,\n threadId,\n ...(options.parentRunId !== undefined && {\n parentRunId: options.parentRunId,\n }),\n genId: () => this.generateId(),\n bridgedToolNames,\n onStreamEvent: (event) =>\n logger.provider(`provider=opencode kind=${event.kind}`, {\n chunk: event,\n }),\n }),\n channel.stream,\n )) {\n if (options.outputSchema && chunk.type === EventType.RUN_FINISHED) {\n heldFinished = chunk\n continue\n }\n if (\n chunk.type === EventType.TEXT_MESSAGE_START &&\n typeof chunk.messageId === 'string' &&\n chunk.messageId !== ''\n ) {\n lastTextMessageId = chunk.messageId\n }\n yield chunk\n }\n\n if (options.outputSchema) {\n try {\n const object = parseJsonFromAssistantText(lastAssistantText)\n const messageId = lastTextMessageId ?? this.generateId()\n yield structuredOutputStartChunk({\n messageId,\n model: this.model,\n threadId,\n runId,\n })\n yield structuredOutputCompleteChunk({\n messageId,\n model: this.model,\n threadId,\n runId,\n object,\n raw: lastAssistantText,\n })\n } catch (error: unknown) {\n const message =\n error instanceof Error\n ? error.message\n : 'Failed to parse structured output'\n yield {\n type: EventType.RUN_ERROR,\n model: this.model,\n timestamp: Date.now(),\n message,\n error: { message },\n }\n }\n }\n if (heldFinished) yield heldFinished\n\n // Surface pending approval requests (ask-policy actions awaiting a client\n // decision); the client approves and re-runs to continue.\n for (const event of approvalRequests) yield event\n } catch (error: unknown) {\n const err = error as Error & { code?: string }\n const rawEvent = toRunErrorRawEvent(error)\n logger.errors('opencode.chatStream fatal', {\n error,\n source: 'opencode.chatStream',\n })\n yield {\n type: EventType.RUN_ERROR,\n model: options.model,\n timestamp: Date.now(),\n message: err.message || 'Unknown error occurred',\n ...(err.code !== undefined && { code: err.code }),\n ...(rawEvent !== undefined && { rawEvent }),\n error: {\n message: err.message || 'Unknown error occurred',\n ...(err.code !== undefined && { code: err.code }),\n },\n }\n } finally {\n if (externalSignal !== undefined && onAbort !== undefined) {\n externalSignal.removeEventListener('abort', onAbort)\n }\n channel.close()\n await handle?.dispose()\n await server?.dispose()\n await bridge?.close()\n }\n }\n\n structuredOutput(\n _options: StructuredOutputOptions<OpencodeTextProviderOptions>,\n ): Promise<StructuredOutputResult<unknown>> {\n return Promise.reject(\n new Error(\n 'This harness honors outputSchema on chat() in the same turn. ' +\n 'Pass outputSchema to chat(), or use a model adapter for a one-shot extract.',\n ),\n )\n }\n}\n\n/**\n * Creates an OpenCode harness adapter that runs **inside a sandbox**.\n *\n * It declares `requires: [SandboxCapability]`, spawns `opencode serve` inside\n * the sandbox provided by `withSandbox(...)`, exposes its port, and connects\n * the `@opencode-ai/sdk` HTTP client to it. OpenCode owns the agent loop and\n * executes its native tools against the sandbox workspace. The sandbox image\n * must provide the `opencode` executable (Docker: also publish the server port\n * via `publishPorts`). chat()-provided tools aren't bridged yet.\n */\nexport function opencodeText<TModel extends OpencodeModel>(\n model: TModel,\n config: OpencodeTextConfig = {},\n): OpencodeTextAdapter<TModel> {\n return new OpencodeTextAdapter(config, model)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"text.js","names":[],"sources":["../../../src/adapters/text.ts"],"sourcesContent":["import { EventType, normalizeSystemPrompts } from '@tanstack/ai'\nimport {\n appendOutputSchemaInstruction,\n parseJsonFromAssistantText,\n structuredOutputCompleteChunk,\n structuredOutputStartChunk,\n toRunErrorRawEvent,\n} from '@tanstack/ai/adapter-internals'\nimport { BaseTextAdapter } from '@tanstack/ai/adapters'\nimport {\n DurableAttachNotSupportedError,\n SandboxCapability,\n buildApprovalRequestedEvent,\n createBridgeEventChannel,\n getSandbox,\n getSandboxDurability,\n getToolBridgeProvisioner,\n getWorkspaceProjection,\n mergeChunkStreams,\n nodeHttpBridgeProvisioner,\n resolveDurableRunId,\n} from '@tanstack/ai-sandbox'\nimport { buildPrompt } from '../messages/prompt'\nimport { startOpencodeSession } from '../process/server'\nimport { startOpencodeServerInSandbox } from '../process/sandbox-server'\nimport { resolveInteractivePermission } from '../process/permissions'\nimport { AsyncQueue } from '../stream/queue'\nimport { translateOpencodeStream } from '../stream/translate'\nimport { projectOpencodeWorkspace } from './projection'\nimport type { HostToolBridge, SandboxHandle } from '@tanstack/ai-sandbox'\nimport type {\n StructuredOutputOptions,\n StructuredOutputResult,\n} from '@tanstack/ai/adapters'\nimport type {\n DefaultMessageMetadataByModality,\n Modality,\n AdapterYieldChunk,\n TextOptions,\n} from '@tanstack/ai'\nimport type { OpencodeSessionHandle } from '../process/server'\nimport type {\n OpencodePermissionMode,\n PermissionHandler,\n} from '../process/permissions'\nimport type { OpencodeStreamEvent } from '../stream/sdk-types'\nimport type { OpencodeModel } from '../model-meta'\nimport type { OpencodeTextProviderOptions } from '../provider-options'\n\nconst DEFAULT_WORKDIR = '/workspace'\nconst DEFAULT_PORT = 4096\n\nexport interface OpencodeTextConfig {\n /** Working directory inside the sandbox. Defaults to `/workspace`. */\n directory?: string\n /**\n * Port the in-sandbox `opencode serve` listens on. Defaults to 4096. For the\n * Docker provider this port must also be published (`publishPorts: [4096]`)\n * so the host can reach it.\n */\n port?: number\n /** Hostname the in-sandbox server binds. Defaults to `0.0.0.0`. */\n hostname?: string\n /**\n * OpenCode permission mode driving the dynamic permission handler. Defaults\n * to `'default'`; set `'acceptEdits'` / `'bypassPermissions'` to let the\n * harness edit files and run commands autonomously inside the sandbox.\n */\n permissionMode?: OpencodePermissionMode\n /** Custom permission handler; replaces the adapter's default policy. */\n onPermissionRequest?: PermissionHandler\n}\n\n/** Split a `provider/model` id into its provider and model halves. */\nfunction splitModel(model: string): { providerID: string; modelID: string } {\n const slash = model.indexOf('/')\n if (slash <= 0 || slash === model.length - 1) {\n throw new Error(\n `OpenCode models must be addressed as \"provider/model\" (e.g. \"anthropic/claude-sonnet-4-5\"); received \"${model}\".`,\n )\n }\n return { providerID: model.slice(0, slash), modelID: model.slice(slash + 1) }\n}\n\nexport class OpencodeTextAdapter<\n TModel extends OpencodeModel,\n> extends BaseTextAdapter<\n TModel,\n OpencodeTextProviderOptions,\n ReadonlyArray<Modality> & readonly ['text'],\n DefaultMessageMetadataByModality,\n ReadonlyArray<string>,\n unknown,\n never\n> {\n readonly name = 'opencode' as const\n\n override readonly requires = [SandboxCapability] as const\n\n private readonly adapterConfig: OpencodeTextConfig\n\n constructor(config: OpencodeTextConfig, model: TModel) {\n super({}, model)\n this.adapterConfig = config\n }\n\n private sandboxFrom(\n options: TextOptions<OpencodeTextProviderOptions>,\n ): SandboxHandle {\n const ctx = options.capabilities\n if (!ctx) {\n throw new Error(\n 'Adapter \"opencode\" requires a sandbox. Add withSandbox(defineSandbox({ ... })) to chat() middleware.',\n )\n }\n return getSandbox(ctx)\n }\n\n supportsCombinedToolsAndSchema(): boolean {\n return true\n }\n\n combinedStructuredOutputSource(): 'event' {\n return 'event'\n }\n\n private applySystemPrompts(\n options: TextOptions<OpencodeTextProviderOptions>,\n prompt: string,\n ): string {\n const systemPrompts = normalizeSystemPrompts(options.systemPrompts)\n .map((systemPrompt) => systemPrompt.content)\n .filter((content) => content.trim() !== '')\n if (systemPrompts.length === 0) return prompt\n return `${systemPrompts.join('\\n\\n')}\\n\\n${prompt}`\n }\n\n async *chatStream(\n options: TextOptions<OpencodeTextProviderOptions>,\n ): AsyncIterable<AdapterYieldChunk> {\n const { logger } = options\n let server:\n | Awaited<ReturnType<typeof startOpencodeServerInSandbox>>\n | undefined\n let handle: OpencodeSessionHandle | undefined\n let bridge: HostToolBridge | undefined\n const externalSignal =\n options.abortController?.signal ?? options.request?.signal ?? undefined\n let onAbort: (() => void) | undefined\n // This adapter does not journal yet, so a generated id is still fine.\n // Routed through the helper anyway so journaling here inherits the\n // caller-supplied-runId requirement instead of re-deriving it.\n const runId = resolveDurableRunId(options.runId, {\n durable: false,\n adapter: 'opencode',\n fallback: () => this.generateId(),\n })\n const threadId = options.threadId ?? this.generateId()\n // Surfaces custom events from bridged tools (e.g. code mode console logs)\n // on this run's live output stream.\n const channel = createBridgeEventChannel({\n model: this.model,\n threadId,\n runId,\n })\n\n try {\n const sandbox = this.sandboxFrom(options)\n const directory =\n options.modelOptions?.directory ??\n this.adapterConfig.directory ??\n DEFAULT_WORKDIR\n\n // Durability wired onto a path that cannot deliver it. Two outcomes, split\n // by whether a first attempt has already run.\n //\n // ATTACH is fatal. `sandboxRunDriver`'s `drive()` sets `attach: true` only\n // when a previous host was already streaming this run, so continuing past\n // here reaches `startOpencodeServerInSandbox` + the session prompt and\n // re-runs the agent from scratch against the workspace that attempt\n // already mutated, appending its whole output to a log that still holds\n // the first attempt's. This adapter has no journal to tail, no\n // `awaitAttachableJournal` to refuse the attach up front, and no\n // `alignedIfAttaching` to suppress the already-delivered prefix — so there\n // is nothing between here and that corruption except this throw.\n //\n // A FRESH durable run only fails to be recoverable LATER, which an app may\n // knowingly accept (it can wire `withSandbox({ runs, durability })` once at\n // the middleware level and still route some runs through this adapter). So\n // that is a warn, not a throw: audible, not fatal. Once per run, not per\n // chunk — a per-chunk warning would be worse than none. Mirrors\n // `ai-grok-build`'s `chatStreamAcp`.\n const durability = options.capabilities\n ? getSandboxDurability(options.capabilities, { optional: true })\n : undefined\n if (durability !== undefined) {\n if (durability.attach) {\n throw new DurableAttachNotSupportedError(\n 'opencode',\n 'this adapter drives the harness over the opencode HTTP server ' +\n 'and does not journal',\n )\n }\n logger.warn(\n 'opencode: sandbox durability is wired but this adapter never ' +\n 'journals — this run will not be recoverable on reconnect. Use a ' +\n 'journaling harness adapter for runs that must survive a host ' +\n 'restart, or drop durability if these runs are not meant to.',\n { runId, adapter: 'opencode' },\n )\n }\n\n // Project workspace skills / MCP servers into the sandbox before starting\n // the opencode server so the workspace config is in place for the session.\n if (options.capabilities !== undefined) {\n const projection = getWorkspaceProjection(options.capabilities, {\n optional: true,\n })\n if (projection !== undefined) {\n await projectOpencodeWorkspace(sandbox, projection)\n }\n }\n\n const modelOptions = options.modelOptions\n const sessionId = modelOptions?.sessionId\n const { prompt: resumePrompt } = buildPrompt(options.messages, sessionId)\n const { providerID, modelID } = splitModel(this.model)\n\n // Bridge chat()-provided tools into the in-sandbox server over MCP\n // (configured via OPENCODE_CONFIG_CONTENT at server spawn).\n const bridgedToolNames = new Set(\n (options.tools ?? []).map((tool) => tool.name),\n )\n if (options.tools && options.tools.length > 0) {\n const provisioner =\n (options.capabilities\n ? getToolBridgeProvisioner(options.capabilities, { optional: true })\n : undefined) ?? nodeHttpBridgeProvisioner\n bridge = await provisioner.provision(options.tools, {\n provider: sandbox.provider,\n context: options.context,\n emitCustomEvent: channel.emitCustomEvent,\n ...(externalSignal ? { signal: externalSignal } : {}),\n })\n }\n\n // Approval-requested events for `ask`-policy actions with no client\n // decision yet, emitted after the stream so the client can approve + re-run.\n const approvalRequests: Array<AdapterYieldChunk> = []\n\n const queue = new AsyncQueue<OpencodeStreamEvent>()\n const mode =\n modelOptions?.permissionMode ??\n this.adapterConfig.permissionMode ??\n 'default'\n const permissionHandler: PermissionHandler =\n this.adapterConfig.onPermissionRequest ??\n ((request) => {\n const result = resolveInteractivePermission(\n request,\n mode,\n bridgedToolNames,\n options.approvals,\n )\n if (result.approvalId !== undefined) {\n approvalRequests.push(\n buildApprovalRequestedEvent({\n approvalId: result.approvalId,\n title: result.title ?? request.title,\n threadId,\n runId,\n detail: { provider: 'opencode' },\n }),\n )\n }\n return result.response\n })\n\n logger.request(\n `activity=chat provider=opencode model=${this.model} sandbox=${sandbox.provider} messages=${options.messages.length} resume=${sessionId ?? 'none'}`,\n { provider: 'opencode', model: this.model },\n )\n\n const serverEnv = bridge\n ? {\n OPENCODE_CONFIG_CONTENT: JSON.stringify({\n mcp: {\n [bridge.name]: {\n type: 'remote',\n url: bridge.url,\n enabled: true,\n headers: { Authorization: `Bearer ${bridge.token}` },\n },\n },\n }),\n }\n : undefined\n\n server = await startOpencodeServerInSandbox(sandbox, {\n port: this.adapterConfig.port ?? DEFAULT_PORT,\n ...(this.adapterConfig.hostname !== undefined && {\n hostname: this.adapterConfig.hostname,\n }),\n cwd: directory,\n ...(serverEnv ? { env: serverEnv } : {}),\n ...(externalSignal ? { signal: externalSignal } : {}),\n })\n\n handle = await startOpencodeSession({\n baseUrl: server.baseUrl,\n // Forward the channel's auth headers (e.g. Daytona's preview token) so\n // the host client can reach a token-gated preview proxy.\n ...(server.headers !== undefined && { headers: server.headers }),\n // NOTE: do NOT pass `directory` here. `directory` is the VIRTUAL sandbox\n // path (e.g. `/workspace`); the server is already spawned with that as its\n // cwd (the provider handle maps it to the real workdir — `/workspace`\n // inside Docker, a host temp dir for local-process). Forwarding the\n // virtual path to the host-side opencode HTTP API breaks local-process,\n // where `/workspace` doesn't exist → the API stalls until the request\n // times out. Omitting it makes opencode use the server's (correct) cwd.\n providerID,\n modelID,\n ...(sessionId !== undefined && { resumeSessionId: sessionId }),\n onEvent: (event) => queue.push({ kind: 'event', event }),\n onPermissionRequest: permissionHandler,\n onError: (error) => queue.fail(error),\n })\n const session = handle\n\n if (externalSignal !== undefined) {\n onAbort = () => void session.abort().catch(() => undefined)\n if (externalSignal.aborted) onAbort()\n else externalSignal.addEventListener('abort', onAbort, { once: true })\n }\n\n queue.push({ kind: 'session', sessionId: session.sessionId })\n\n let promptText = this.applySystemPrompts(\n options,\n session.resumed || sessionId === undefined\n ? resumePrompt\n : buildPrompt(options.messages, undefined).prompt,\n )\n if (options.outputSchema) {\n promptText = appendOutputSchemaInstruction(\n promptText,\n options.outputSchema,\n )\n }\n\n let lastAssistantText = ''\n session\n .prompt(promptText)\n .then(({ message, text }) => {\n lastAssistantText = text\n queue.push({ kind: 'done', message })\n queue.end()\n })\n .catch((error: unknown) => queue.fail(error))\n\n let heldFinished: AdapterYieldChunk | undefined\n let lastTextMessageId: string | undefined\n for await (const chunk of mergeChunkStreams(\n translateOpencodeStream(queue, {\n model: this.model,\n runId,\n threadId,\n ...(options.parentRunId !== undefined && {\n parentRunId: options.parentRunId,\n }),\n genId: () => this.generateId(),\n bridgedToolNames,\n onStreamEvent: (event) =>\n logger.provider(`provider=opencode kind=${event.kind}`, {\n chunk: event,\n }),\n }),\n channel.stream,\n )) {\n if (options.outputSchema && chunk.type === EventType.RUN_FINISHED) {\n heldFinished = chunk\n continue\n }\n if (\n chunk.type === EventType.TEXT_MESSAGE_START &&\n typeof chunk.messageId === 'string' &&\n chunk.messageId !== ''\n ) {\n lastTextMessageId = chunk.messageId\n }\n yield chunk\n }\n\n if (options.outputSchema) {\n try {\n const object = parseJsonFromAssistantText(lastAssistantText)\n const messageId = lastTextMessageId ?? this.generateId()\n yield structuredOutputStartChunk({\n messageId,\n model: this.model,\n threadId,\n runId,\n })\n yield structuredOutputCompleteChunk({\n messageId,\n model: this.model,\n threadId,\n runId,\n object,\n raw: lastAssistantText,\n })\n } catch (error: unknown) {\n const message =\n error instanceof Error\n ? error.message\n : 'Failed to parse structured output'\n yield {\n type: EventType.RUN_ERROR,\n model: this.model,\n timestamp: Date.now(),\n message,\n error: { message },\n }\n }\n }\n if (heldFinished) yield heldFinished\n\n // Surface pending approval requests (ask-policy actions awaiting a client\n // decision); the client approves and re-runs to continue.\n for (const event of approvalRequests) yield event\n } catch (error: unknown) {\n const err = error as Error & { code?: string }\n const rawEvent = toRunErrorRawEvent(error)\n logger.errors('opencode.chatStream fatal', {\n error,\n source: 'opencode.chatStream',\n })\n yield {\n type: EventType.RUN_ERROR,\n model: options.model,\n timestamp: Date.now(),\n message: err.message || 'Unknown error occurred',\n ...(err.code !== undefined && { code: err.code }),\n ...(rawEvent !== undefined && { rawEvent }),\n error: {\n message: err.message || 'Unknown error occurred',\n ...(err.code !== undefined && { code: err.code }),\n },\n }\n } finally {\n if (externalSignal !== undefined && onAbort !== undefined) {\n externalSignal.removeEventListener('abort', onAbort)\n }\n channel.close()\n await handle?.dispose()\n await server?.dispose()\n await bridge?.close()\n }\n }\n\n structuredOutput(\n _options: StructuredOutputOptions<OpencodeTextProviderOptions>,\n ): Promise<StructuredOutputResult<unknown>> {\n return Promise.reject(\n new Error(\n 'This harness honors outputSchema on chat() in the same turn. ' +\n 'Pass outputSchema to chat(), or use a model adapter for a one-shot extract.',\n ),\n )\n }\n}\n\n/**\n * Creates an OpenCode harness adapter that runs **inside a sandbox**.\n *\n * It declares `requires: [SandboxCapability]`, spawns `opencode serve` inside\n * the sandbox provided by `withSandbox(...)`, exposes its port, and connects\n * the `@opencode-ai/sdk` HTTP client to it. OpenCode owns the agent loop and\n * executes its native tools against the sandbox workspace. The sandbox image\n * must provide the `opencode` executable (Docker: also publish the server port\n * via `publishPorts`). chat()-provided tools aren't bridged yet.\n */\nexport function opencodeText<TModel extends OpencodeModel>(\n model: TModel,\n config: OpencodeTextConfig = {},\n): OpencodeTextAdapter<TModel> {\n return new OpencodeTextAdapter(config, model)\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AdapterYieldChunk } from '@tanstack/ai';
|
|
2
2
|
import { OpencodeStreamEvent } from './sdk-types.js';
|
|
3
3
|
/** Name of the CUSTOM event carrying the OpenCode session id. */
|
|
4
4
|
export declare const SESSION_ID_EVENT = "opencode.session-id";
|
|
@@ -48,4 +48,4 @@ export declare function resolveToolName(tool: string, bridgedToolNames: Readonly
|
|
|
48
48
|
* ends or aborts before the harness reported one) so the engine's
|
|
49
49
|
* pending-tool-call scan on the next request never force-executes them.
|
|
50
50
|
*/
|
|
51
|
-
export declare function translateOpencodeStream(events: AsyncIterable<OpencodeStreamEvent>, ctx: TranslateContext): AsyncIterable<
|
|
51
|
+
export declare function translateOpencodeStream(events: AsyncIterable<OpencodeStreamEvent>, ctx: TranslateContext): AsyncIterable<AdapterYieldChunk>;
|
|
@@ -1 +1 @@
|
|
|
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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"}
|
|
1
|
+
{"version":3,"file":"translate.js","names":[],"sources":["../../../src/stream/translate.ts"],"sourcesContent":["import { EventType, buildBaseUsage } from '@tanstack/ai'\nimport type { AdapterYieldChunk, 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<AdapterYieldChunk> {\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<AdapterYieldChunk> {\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<AdapterYieldChunk> {\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<AdapterYieldChunk> {\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<AdapterYieldChunk> {\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<AdapterYieldChunk> {\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<AdapterYieldChunk> {\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<AdapterYieldChunk> {\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<AdapterYieldChunk> {\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<AdapterYieldChunk> {\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(\n message: OpencodeAssistantMessage,\n ): Generator<AdapterYieldChunk> {\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.3.
|
|
3
|
+
"version": "0.3.4",
|
|
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,13 +38,13 @@
|
|
|
38
38
|
"@opencode-ai/sdk": "^1.17.4"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@tanstack/ai": "^0.
|
|
42
|
-
"@tanstack/ai-sandbox": "^0.
|
|
41
|
+
"@tanstack/ai": "^0.48.0",
|
|
42
|
+
"@tanstack/ai-sandbox": "^0.5.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@vitest/coverage-v8": "4.1.10",
|
|
46
|
-
"@tanstack/ai": "0.
|
|
47
|
-
"@tanstack/ai-sandbox": "0.
|
|
46
|
+
"@tanstack/ai": "0.48.0",
|
|
47
|
+
"@tanstack/ai-sandbox": "0.5.0"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "vite build",
|
package/src/adapters/text.ts
CHANGED
|
@@ -35,7 +35,7 @@ import type {
|
|
|
35
35
|
import type {
|
|
36
36
|
DefaultMessageMetadataByModality,
|
|
37
37
|
Modality,
|
|
38
|
-
|
|
38
|
+
AdapterYieldChunk,
|
|
39
39
|
TextOptions,
|
|
40
40
|
} from '@tanstack/ai'
|
|
41
41
|
import type { OpencodeSessionHandle } from '../process/server'
|
|
@@ -137,7 +137,7 @@ export class OpencodeTextAdapter<
|
|
|
137
137
|
|
|
138
138
|
async *chatStream(
|
|
139
139
|
options: TextOptions<OpencodeTextProviderOptions>,
|
|
140
|
-
): AsyncIterable<
|
|
140
|
+
): AsyncIterable<AdapterYieldChunk> {
|
|
141
141
|
const { logger } = options
|
|
142
142
|
let server:
|
|
143
143
|
| Awaited<ReturnType<typeof startOpencodeServerInSandbox>>
|
|
@@ -246,7 +246,7 @@ export class OpencodeTextAdapter<
|
|
|
246
246
|
|
|
247
247
|
// Approval-requested events for `ask`-policy actions with no client
|
|
248
248
|
// decision yet, emitted after the stream so the client can approve + re-run.
|
|
249
|
-
const approvalRequests: Array<
|
|
249
|
+
const approvalRequests: Array<AdapterYieldChunk> = []
|
|
250
250
|
|
|
251
251
|
const queue = new AsyncQueue<OpencodeStreamEvent>()
|
|
252
252
|
const mode =
|
|
@@ -358,7 +358,7 @@ export class OpencodeTextAdapter<
|
|
|
358
358
|
})
|
|
359
359
|
.catch((error: unknown) => queue.fail(error))
|
|
360
360
|
|
|
361
|
-
let heldFinished:
|
|
361
|
+
let heldFinished: AdapterYieldChunk | undefined
|
|
362
362
|
let lastTextMessageId: string | undefined
|
|
363
363
|
for await (const chunk of mergeChunkStreams(
|
|
364
364
|
translateOpencodeStream(queue, {
|
package/src/stream/translate.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventType, buildBaseUsage } from '@tanstack/ai'
|
|
2
|
-
import type {
|
|
2
|
+
import type { AdapterYieldChunk, TokenUsage } from '@tanstack/ai'
|
|
3
3
|
import type {
|
|
4
4
|
OpencodeAssistantMessage,
|
|
5
5
|
OpencodeEvent,
|
|
@@ -112,7 +112,7 @@ function messageError(
|
|
|
112
112
|
export async function* translateOpencodeStream(
|
|
113
113
|
events: AsyncIterable<OpencodeStreamEvent>,
|
|
114
114
|
ctx: TranslateContext,
|
|
115
|
-
): AsyncIterable<
|
|
115
|
+
): AsyncIterable<AdapterYieldChunk> {
|
|
116
116
|
const { model, runId, threadId, genId } = ctx
|
|
117
117
|
const now = () => Date.now()
|
|
118
118
|
|
|
@@ -129,7 +129,7 @@ export async function* translateOpencodeStream(
|
|
|
129
129
|
let openTextId: string | null = null
|
|
130
130
|
let openReasoningId: string | null = null
|
|
131
131
|
|
|
132
|
-
function* startRun(): Generator<
|
|
132
|
+
function* startRun(): Generator<AdapterYieldChunk> {
|
|
133
133
|
if (runStarted) return
|
|
134
134
|
runStarted = true
|
|
135
135
|
yield {
|
|
@@ -142,7 +142,7 @@ export async function* translateOpencodeStream(
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
function* closeText(): Generator<
|
|
145
|
+
function* closeText(): Generator<AdapterYieldChunk> {
|
|
146
146
|
if (openTextId !== null) {
|
|
147
147
|
yield {
|
|
148
148
|
type: EventType.TEXT_MESSAGE_END,
|
|
@@ -154,7 +154,7 @@ export async function* translateOpencodeStream(
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
function* closeReasoning(): Generator<
|
|
157
|
+
function* closeReasoning(): Generator<AdapterYieldChunk> {
|
|
158
158
|
if (openReasoningId !== null) {
|
|
159
159
|
yield {
|
|
160
160
|
type: EventType.REASONING_MESSAGE_END,
|
|
@@ -172,7 +172,7 @@ export async function* translateOpencodeStream(
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
function* synthesizeUnresolvedResults(): Generator<
|
|
175
|
+
function* synthesizeUnresolvedResults(): Generator<AdapterYieldChunk> {
|
|
176
176
|
for (const toolCallId of unresolvedToolCalls) {
|
|
177
177
|
yield {
|
|
178
178
|
type: EventType.TOOL_CALL_RESULT,
|
|
@@ -189,7 +189,7 @@ export async function* translateOpencodeStream(
|
|
|
189
189
|
function* handleTextPart(
|
|
190
190
|
part: Extract<OpencodePart, { type: 'text' }>,
|
|
191
191
|
delta: string | undefined,
|
|
192
|
-
): Generator<
|
|
192
|
+
): Generator<AdapterYieldChunk> {
|
|
193
193
|
yield* closeReasoning()
|
|
194
194
|
|
|
195
195
|
const prev = textAccumulators.get(part.id) ?? ''
|
|
@@ -228,7 +228,7 @@ export async function* translateOpencodeStream(
|
|
|
228
228
|
function* handleReasoningPart(
|
|
229
229
|
part: Extract<OpencodePart, { type: 'reasoning' }>,
|
|
230
230
|
delta: string | undefined,
|
|
231
|
-
): Generator<
|
|
231
|
+
): Generator<AdapterYieldChunk> {
|
|
232
232
|
yield* closeText()
|
|
233
233
|
|
|
234
234
|
const prev = textAccumulators.get(part.id) ?? ''
|
|
@@ -271,7 +271,7 @@ export async function* translateOpencodeStream(
|
|
|
271
271
|
|
|
272
272
|
function* openToolCall(
|
|
273
273
|
part: Extract<OpencodePart, { type: 'tool' }>,
|
|
274
|
-
): Generator<
|
|
274
|
+
): Generator<AdapterYieldChunk> {
|
|
275
275
|
if (openedToolCalls.has(part.callID)) return
|
|
276
276
|
openedToolCalls.add(part.callID)
|
|
277
277
|
const toolCallName = resolveToolName(part.tool, ctx.bridgedToolNames)
|
|
@@ -307,7 +307,7 @@ export async function* translateOpencodeStream(
|
|
|
307
307
|
|
|
308
308
|
function* handleToolPart(
|
|
309
309
|
part: Extract<OpencodePart, { type: 'tool' }>,
|
|
310
|
-
): Generator<
|
|
310
|
+
): Generator<AdapterYieldChunk> {
|
|
311
311
|
yield* closeText()
|
|
312
312
|
yield* closeReasoning()
|
|
313
313
|
yield* openToolCall(part)
|
|
@@ -330,7 +330,7 @@ export async function* translateOpencodeStream(
|
|
|
330
330
|
}
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
-
function* handleEvent(event: OpencodeEvent): Generator<
|
|
333
|
+
function* handleEvent(event: OpencodeEvent): Generator<AdapterYieldChunk> {
|
|
334
334
|
if (event.type === 'message.part.updated') {
|
|
335
335
|
const { part, delta } = event.properties
|
|
336
336
|
if (isTextPart(part)) {
|
|
@@ -355,7 +355,9 @@ export async function* translateOpencodeStream(
|
|
|
355
355
|
// terminal `done` event and are ignored.
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
-
function* finish(
|
|
358
|
+
function* finish(
|
|
359
|
+
message: OpencodeAssistantMessage,
|
|
360
|
+
): Generator<AdapterYieldChunk> {
|
|
359
361
|
yield* startRun()
|
|
360
362
|
yield* closeText()
|
|
361
363
|
yield* closeReasoning()
|