@tanstack/ai-opencode 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,6 +30,8 @@ export declare class OpencodeTextAdapter<TModel extends OpencodeModel> extends B
30
30
  private readonly adapterConfig;
31
31
  constructor(config: OpencodeTextConfig, model: TModel);
32
32
  private sandboxFrom;
33
+ supportsCombinedToolsAndSchema(): boolean;
34
+ combinedStructuredOutputSource(): 'event';
33
35
  private applySystemPrompts;
34
36
  chatStream(options: TextOptions<OpencodeTextProviderOptions>): AsyncIterable<StreamChunk>;
35
37
  structuredOutput(_options: StructuredOutputOptions<OpencodeTextProviderOptions>): Promise<StructuredOutputResult<unknown>>;
@@ -6,7 +6,7 @@ import { AsyncQueue } from "../stream/queue.js";
6
6
  import { translateOpencodeStream } from "../stream/translate.js";
7
7
  import { projectOpencodeWorkspace } from "./projection.js";
8
8
  import { EventType, normalizeSystemPrompts } from "@tanstack/ai";
9
- import { toRunErrorRawEvent } from "@tanstack/ai/adapter-internals";
9
+ import { appendOutputSchemaInstruction, parseJsonFromAssistantText, structuredOutputCompleteChunk, structuredOutputStartChunk, toRunErrorRawEvent } from "@tanstack/ai/adapter-internals";
10
10
  import { BaseTextAdapter } from "@tanstack/ai/adapters";
11
11
  import { DurableAttachNotSupportedError, SandboxCapability, buildApprovalRequestedEvent, createBridgeEventChannel, getSandbox, getSandboxDurability, getToolBridgeProvisioner, getWorkspaceProjection, mergeChunkStreams, nodeHttpBridgeProvisioner, resolveDurableRunId } from "@tanstack/ai-sandbox";
12
12
  //#region src/adapters/text.ts
@@ -34,6 +34,12 @@ var OpencodeTextAdapter = class extends BaseTextAdapter {
34
34
  if (!ctx) throw new Error("Adapter \"opencode\" requires a sandbox. Add withSandbox(defineSandbox({ ... })) to chat() middleware.");
35
35
  return getSandbox(ctx);
36
36
  }
37
+ supportsCombinedToolsAndSchema() {
38
+ return true;
39
+ }
40
+ combinedStructuredOutputSource() {
41
+ return "event";
42
+ }
37
43
  applySystemPrompts(options, prompt) {
38
44
  const systemPrompts = normalizeSystemPrompts(options.systemPrompts).map((systemPrompt) => systemPrompt.content).filter((content) => content.trim() !== "");
39
45
  if (systemPrompts.length === 0) return prompt;
@@ -137,15 +143,20 @@ var OpencodeTextAdapter = class extends BaseTextAdapter {
137
143
  kind: "session",
138
144
  sessionId: session.sessionId
139
145
  });
140
- const promptText = this.applySystemPrompts(options, session.resumed || sessionId === void 0 ? resumePrompt : buildPrompt(options.messages, void 0).prompt);
141
- session.prompt(promptText).then(({ message }) => {
146
+ let promptText = this.applySystemPrompts(options, session.resumed || sessionId === void 0 ? resumePrompt : buildPrompt(options.messages, void 0).prompt);
147
+ if (options.outputSchema) promptText = appendOutputSchemaInstruction(promptText, options.outputSchema);
148
+ let lastAssistantText = "";
149
+ session.prompt(promptText).then(({ message, text }) => {
150
+ lastAssistantText = text;
142
151
  queue.push({
143
152
  kind: "done",
144
153
  message
145
154
  });
146
155
  queue.end();
147
156
  }).catch((error) => queue.fail(error));
148
- yield* mergeChunkStreams(translateOpencodeStream(queue, {
157
+ let heldFinished;
158
+ let lastTextMessageId;
159
+ for await (const chunk of mergeChunkStreams(translateOpencodeStream(queue, {
149
160
  model: this.model,
150
161
  runId,
151
162
  threadId,
@@ -153,7 +164,42 @@ var OpencodeTextAdapter = class extends BaseTextAdapter {
153
164
  genId: () => this.generateId(),
154
165
  bridgedToolNames,
155
166
  onStreamEvent: (event) => logger.provider(`provider=opencode kind=${event.kind}`, { chunk: event })
156
- }), channel.stream);
167
+ }), channel.stream)) {
168
+ if (options.outputSchema && chunk.type === EventType.RUN_FINISHED) {
169
+ heldFinished = chunk;
170
+ continue;
171
+ }
172
+ if (chunk.type === EventType.TEXT_MESSAGE_START && typeof chunk.messageId === "string" && chunk.messageId !== "") lastTextMessageId = chunk.messageId;
173
+ yield chunk;
174
+ }
175
+ if (options.outputSchema) try {
176
+ const object = parseJsonFromAssistantText(lastAssistantText);
177
+ const messageId = lastTextMessageId ?? this.generateId();
178
+ yield structuredOutputStartChunk({
179
+ messageId,
180
+ model: this.model,
181
+ threadId,
182
+ runId
183
+ });
184
+ yield structuredOutputCompleteChunk({
185
+ messageId,
186
+ model: this.model,
187
+ threadId,
188
+ runId,
189
+ object,
190
+ raw: lastAssistantText
191
+ });
192
+ } catch (error) {
193
+ const message = error instanceof Error ? error.message : "Failed to parse structured output";
194
+ yield {
195
+ type: EventType.RUN_ERROR,
196
+ model: this.model,
197
+ timestamp: Date.now(),
198
+ message,
199
+ error: { message }
200
+ };
201
+ }
202
+ if (heldFinished) yield heldFinished;
157
203
  for (const event of approvalRequests) yield event;
158
204
  } catch (error) {
159
205
  const err = error;
@@ -183,7 +229,7 @@ var OpencodeTextAdapter = class extends BaseTextAdapter {
183
229
  }
184
230
  }
185
231
  structuredOutput(_options) {
186
- return Promise.reject(/* @__PURE__ */ new Error("Structured output is not yet supported by the in-sandbox OpenCode adapter. Use a model adapter for structured output, or omit outputSchema."));
232
+ return Promise.reject(/* @__PURE__ */ new Error("This harness honors outputSchema on chat() in the same turn. Pass outputSchema to chat(), or use a model adapter for a one-shot extract."));
187
233
  }
188
234
  };
189
235
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"text.js","names":[],"sources":["../../../src/adapters/text.ts"],"sourcesContent":["import { EventType, normalizeSystemPrompts } from '@tanstack/ai'\nimport { toRunErrorRawEvent } 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 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 const promptText = this.applySystemPrompts(\n options,\n session.resumed || sessionId === undefined\n ? resumePrompt\n : buildPrompt(options.messages, undefined).prompt,\n )\n\n session\n .prompt(promptText)\n .then(({ message }) => {\n queue.push({ kind: 'done', message })\n queue.end()\n })\n .catch((error: unknown) => queue.fail(error))\n\n yield* 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\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 'Structured output is not yet supported by the in-sandbox OpenCode adapter. ' +\n 'Use a model adapter for structured output, or omit outputSchema.',\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 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 +1 @@
1
- {"version":3,"file":"server.js","names":[],"sources":["../../../src/process/server.ts"],"sourcesContent":["import { createOpencode, createOpencodeClient } from '@opencode-ai/sdk'\nimport type { Config, Event, OpencodeClient, Part } from '@opencode-ai/sdk'\nimport type {\n OpencodeAssistantMessage,\n OpencodeEvent,\n} from '../stream/sdk-types'\nimport type {\n OpencodePermissionRequest,\n OpencodePermissionResponse,\n} from './permissions'\n\n/** A live OpenCode session backed by an `opencode serve` HTTP server. */\nexport interface OpencodeSessionHandle {\n sessionId: string\n /** Whether an existing session was actually resumed. */\n resumed: boolean\n /**\n * Run one prompt turn. Resolves with the final assistant message (finish\n * reason, token usage, error) and its concatenated text once the harness\n * goes idle. Streaming deltas arrive via `onEvent` while this is pending.\n */\n prompt: (\n text: string,\n ) => Promise<{ message: OpencodeAssistantMessage; text: string }>\n /** Ask the harness to abort the in-flight prompt turn. */\n abort: () => Promise<void>\n /** Tear down the event subscription and (if owned) the server. */\n dispose: () => Promise<void>\n}\n\nexport interface StartOpencodeSessionOptions {\n /** Connect to an already-running server instead of spawning one. */\n baseUrl?: string\n /**\n * Headers attached to every request to the opencode server — used to\n * authenticate a token-gated preview channel (e.g. Daytona). Without them a\n * gated preview proxy rejects the requests (404 \"Not found.\").\n */\n headers?: Record<string, string>\n /** Hostname for the spawned server. Defaults to the SDK default. */\n hostname?: string\n /** Port for the spawned server. Defaults to the SDK default. */\n port?: number\n /**\n * Directory the opencode HTTP API scopes the session to. Omit to use the\n * server's own launch cwd (the common case): the server is spawned with the\n * correct working dir per-provider, so passing a directory here is only needed\n * to override it. Passing a VIRTUAL sandbox path (e.g. `/workspace`) is wrong\n * for host-running providers (local-process), where that path doesn't exist —\n * the API then stalls on it. Leave undefined and rely on the server cwd.\n */\n directory?: string\n /** Provider id (the part before `/` in the model id). */\n providerID: string\n /** Model id (the part after `/` in the model id). */\n modelID: string\n /** Extra OpenCode config merged with the adapter's mcp/permission config. */\n config?: Config\n /** Baseline permission policy applied to the spawned server. */\n permission?: Config['permission']\n /** MCP servers (e.g. the TanStack tool bridge) for the session. */\n mcpServers?: Array<{ name: string; url: string }>\n /** Session id to resume; falls back to a fresh session when not found. */\n resumeSessionId?: string\n onEvent: (event: OpencodeEvent) => void\n onPermissionRequest: (\n request: OpencodePermissionRequest,\n ) => Promise<OpencodePermissionResponse> | OpencodePermissionResponse\n /** Called when the event subscription fails mid-turn. */\n onError?: (error: unknown) => void\n}\n\n/** Locate the session id an OpenCode event belongs to, when it carries one. */\nfunction sessionIdOf(event: Event): string | undefined {\n const props = event.properties as { sessionID?: string } | undefined\n if (props?.sessionID !== undefined) return props.sessionID\n if (event.type === 'message.part.updated') {\n return event.properties.part.sessionID\n }\n if (event.type === 'message.updated') {\n return event.properties.info.sessionID\n }\n if (event.type === 'permission.updated') {\n return event.properties.sessionID\n }\n return undefined\n}\n\nfunction buildConfig(options: StartOpencodeSessionOptions): Config {\n const mcp: NonNullable<Config['mcp']> = { ...options.config?.mcp }\n for (const server of options.mcpServers ?? []) {\n mcp[server.name] = { type: 'remote', url: server.url, enabled: true }\n }\n return {\n ...options.config,\n ...(Object.keys(mcp).length > 0 && { mcp }),\n ...(options.permission !== undefined && { permission: options.permission }),\n }\n}\n\n/**\n * Boot (or attach to) an OpenCode HTTP server, resolve a session, and wire its\n * event subscription + permission replies.\n *\n * This module is the only place that touches `@opencode-ai/sdk`; the rest of\n * the package works with the structural types in `sdk-types.ts`.\n *\n * Resume semantics: when `resumeSessionId` is set and the server still knows\n * the session (same machine, same data dir), it is reused. Otherwise a fresh\n * session is created and `resumed: false` tells the adapter to send the\n * flattened transcript.\n */\nexport async function startOpencodeSession(\n options: StartOpencodeSessionOptions,\n): Promise<OpencodeSessionHandle> {\n const { directory } = options\n // Spread into a `query` object only when set; omitting lets opencode use the\n // server's launch cwd (correct for every provider — see `directory` docs).\n const dirQuery = directory !== undefined ? { directory } : {}\n\n let client: OpencodeClient\n let ownedServer: { close: () => void } | undefined\n\n if (options.baseUrl !== undefined) {\n client = createOpencodeClient({\n baseUrl: options.baseUrl,\n ...(options.headers !== undefined && { headers: options.headers }),\n ...(directory !== undefined && { directory }),\n })\n } else {\n const config = buildConfig(options)\n const result = await createOpencode({\n ...(options.hostname !== undefined && { hostname: options.hostname }),\n ...(options.port !== undefined && { port: options.port }),\n ...(Object.keys(config).length > 0 && { config }),\n })\n client = result.client\n ownedServer = result.server\n }\n\n // Mutated from several closures (the subscription loop, dispose, teardown);\n // a holder object keeps reads typed as `boolean` rather than being\n // flow-narrowed to a literal across those boundaries.\n const lifecycle = { disposed: false }\n\n const teardown = async (): Promise<void> => {\n if (lifecycle.disposed) return\n lifecycle.disposed = true\n ownedServer?.close()\n await Promise.resolve()\n }\n\n try {\n // Resolve the session before subscribing so the event filter has an id.\n let sessionId: string | undefined\n let resumed = false\n if (options.resumeSessionId !== undefined) {\n const existing = await client.session.get({\n path: { id: options.resumeSessionId },\n query: dirQuery,\n })\n if (existing.data) {\n sessionId = options.resumeSessionId\n resumed = true\n }\n }\n if (sessionId === undefined) {\n const created = await client.session.create({\n query: dirQuery,\n body: {},\n throwOnError: true,\n })\n sessionId = created.data.id\n }\n const resolvedSessionId = sessionId\n\n const handlePermission = async (\n permission: Extract<Event, { type: 'permission.updated' }>['properties'],\n ): Promise<void> => {\n try {\n const response = await options.onPermissionRequest({\n id: permission.id,\n sessionID: permission.sessionID,\n type: permission.type,\n title: permission.title,\n ...(permission.callID !== undefined && { callID: permission.callID }),\n })\n await client.postSessionIdPermissionsPermissionId({\n path: { id: permission.sessionID, permissionID: permission.id },\n query: dirQuery,\n body: { response },\n throwOnError: true,\n })\n } catch (error) {\n if (!lifecycle.disposed) options.onError?.(error)\n }\n }\n\n const subscription = await client.event.subscribe()\n const stream = subscription.stream\n\n void (async () => {\n try {\n for await (const event of stream) {\n if (lifecycle.disposed) break\n const sid = sessionIdOf(event)\n if (sid !== undefined && sid !== resolvedSessionId) continue\n if (event.type === 'permission.updated') {\n void handlePermission(event.properties)\n continue\n }\n // The SDK event union is a structural superset of the subset the\n // translator consumes; unknown event types match no translator\n // branch and are ignored.\n options.onEvent(event as OpencodeEvent)\n }\n } catch (error) {\n if (!lifecycle.disposed) options.onError?.(error)\n }\n })()\n\n return {\n sessionId: resolvedSessionId,\n resumed,\n prompt: async (text: string) => {\n const result = await client.session.prompt({\n path: { id: resolvedSessionId },\n query: dirQuery,\n body: {\n model: { providerID: options.providerID, modelID: options.modelID },\n parts: [{ type: 'text', text }],\n },\n throwOnError: true,\n })\n const data = result.data\n const message = data.info as OpencodeAssistantMessage\n const responseText = data.parts\n .filter(\n (part): part is Extract<Part, { type: 'text' }> =>\n part.type === 'text',\n )\n .map((part) => part.text)\n .join('')\n return { message, text: responseText }\n },\n abort: async () => {\n try {\n await client.session.abort({\n path: { id: resolvedSessionId },\n query: dirQuery,\n })\n } catch {\n // Best-effort: the turn may already be finishing.\n }\n },\n dispose: async () => {\n lifecycle.disposed = true\n try {\n await stream.return(undefined)\n } catch {\n // Ignore: stream may already be closed.\n }\n ownedServer?.close()\n },\n }\n } catch (error) {\n await teardown()\n throw error\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"server.js","names":[],"sources":["../../../src/process/server.ts"],"sourcesContent":["import { createOpencode, createOpencodeClient } from '@opencode-ai/sdk'\nimport type { Config, Event, OpencodeClient, Part } from '@opencode-ai/sdk'\nimport type {\n OpencodeAssistantMessage,\n OpencodeEvent,\n} from '../stream/sdk-types'\nimport type {\n OpencodePermissionRequest,\n OpencodePermissionResponse,\n} from './permissions'\n\n/** A live OpenCode session backed by an `opencode serve` HTTP server. */\nexport interface OpencodeSessionHandle {\n sessionId: string\n /** Whether an existing session was actually resumed. */\n resumed: boolean\n /**\n * Run one prompt turn. Resolves with the final assistant message (finish\n * reason, token usage, error) and its concatenated text once the harness\n * goes idle. Streaming deltas arrive via `onEvent` while this is pending.\n */\n prompt: (\n text: string,\n ) => Promise<{ message: OpencodeAssistantMessage; text: string }>\n /** Ask the harness to abort the in-flight prompt turn. */\n abort: () => Promise<void>\n /** Tear down the event subscription and (if owned) the server. */\n dispose: () => Promise<void>\n}\n\nexport interface StartOpencodeSessionOptions {\n /** Connect to an already-running server instead of spawning one. */\n baseUrl?: string\n /**\n * Headers attached to every request to the opencode server — used to\n * authenticate a token-gated preview channel (e.g. Daytona). Without them a\n * gated preview proxy rejects the requests (404 \"Not found.\").\n */\n headers?: Record<string, string>\n /** Hostname for the spawned server. Defaults to the SDK default. */\n hostname?: string\n /** Port for the spawned server. Defaults to the SDK default. */\n port?: number\n /**\n * Directory the opencode HTTP API scopes the session to. Omit to use the\n * server's own launch cwd (the common case): the server is spawned with the\n * correct working dir per-provider, so passing a directory here is only needed\n * to override it. Passing a VIRTUAL sandbox path (e.g. `/workspace`) is wrong\n * for host-running providers (local-process), where that path doesn't exist —\n * the API then stalls on it. Leave undefined and rely on the server cwd.\n */\n directory?: string\n /** Provider id (the part before `/` in the model id). */\n providerID: string\n /** Model id (the part after `/` in the model id). */\n modelID: string\n /** Extra OpenCode config merged with the adapter's mcp/permission config. */\n config?: Config\n /** Baseline permission policy applied to the spawned server. */\n permission?: Config['permission']\n /** MCP servers (e.g. the TanStack tool bridge) for the session. */\n mcpServers?: Array<{ name: string; url: string }>\n /** Session id to resume; falls back to a fresh session when not found. */\n resumeSessionId?: string\n onEvent: (event: OpencodeEvent) => void\n onPermissionRequest: (\n request: OpencodePermissionRequest,\n ) => Promise<OpencodePermissionResponse> | OpencodePermissionResponse\n /** Called when the event subscription fails mid-turn. */\n onError?: (error: unknown) => void\n}\n\n/** Locate the session id an OpenCode event belongs to, when it carries one. */\nfunction sessionIdOf(event: Event): string | undefined {\n const props = event.properties as { sessionID?: string } | undefined\n if (props?.sessionID !== undefined) return props.sessionID\n if (event.type === 'message.part.updated') {\n return event.properties.part.sessionID\n }\n if (event.type === 'message.updated') {\n return event.properties.info.sessionID\n }\n if (event.type === 'permission.updated') {\n return event.properties.sessionID\n }\n return undefined\n}\n\nfunction buildConfig(options: StartOpencodeSessionOptions): Config {\n const mcp: NonNullable<Config['mcp']> = { ...options.config?.mcp }\n for (const server of options.mcpServers ?? []) {\n mcp[server.name] = { type: 'remote', url: server.url, enabled: true }\n }\n return {\n ...options.config,\n ...(Object.keys(mcp).length > 0 && { mcp }),\n ...(options.permission !== undefined && { permission: options.permission }),\n }\n}\n\n/**\n * Boot (or attach to) an OpenCode HTTP server, resolve a session, and wire its\n * event subscription + permission replies.\n *\n * This module is the only place that touches `@opencode-ai/sdk`; the rest of\n * the package works with the structural types in `sdk-types.ts`.\n *\n * Resume semantics: when `resumeSessionId` is set and the server still knows\n * the session (same machine, same data dir), it is reused. Otherwise a fresh\n * session is created and `resumed: false` tells the adapter to send the\n * flattened transcript.\n */\nexport async function startOpencodeSession(\n options: StartOpencodeSessionOptions,\n): Promise<OpencodeSessionHandle> {\n const { directory } = options\n // Spread into a `query` object only when set; omitting lets opencode use the\n // server's launch cwd (correct for every provider — see `directory` docs).\n const dirQuery = directory !== undefined ? { directory } : {}\n\n let client: OpencodeClient\n let ownedServer: { close: () => void } | undefined\n\n if (options.baseUrl !== undefined) {\n client = createOpencodeClient({\n baseUrl: options.baseUrl,\n ...(options.headers !== undefined && { headers: options.headers }),\n ...(directory !== undefined && { directory }),\n })\n } else {\n const config = buildConfig(options)\n const result = await createOpencode({\n ...(options.hostname !== undefined && { hostname: options.hostname }),\n ...(options.port !== undefined && { port: options.port }),\n ...(Object.keys(config).length > 0 && { config }),\n })\n client = result.client\n ownedServer = result.server\n }\n\n // Mutated from several closures (the subscription loop, dispose, teardown);\n // a holder object keeps reads typed as `boolean` rather than being\n // flow-narrowed to a literal across those boundaries.\n const lifecycle = { disposed: false }\n\n const teardown = async (): Promise<void> => {\n if (lifecycle.disposed) return\n lifecycle.disposed = true\n ownedServer?.close()\n await Promise.resolve()\n }\n\n try {\n // Resolve the session before subscribing so the event filter has an id.\n let sessionId: string | undefined\n let resumed = false\n if (options.resumeSessionId !== undefined) {\n const existing = await client.session.get({\n path: { id: options.resumeSessionId },\n query: dirQuery,\n })\n if (existing.data) {\n sessionId = options.resumeSessionId\n resumed = true\n }\n }\n if (sessionId === undefined) {\n const created = await client.session.create({\n query: dirQuery,\n body: {},\n throwOnError: true,\n })\n sessionId = created.data.id\n }\n const resolvedSessionId = sessionId\n\n const handlePermission = async (\n permission: Extract<Event, { type: 'permission.updated' }>['properties'],\n ): Promise<void> => {\n try {\n const response = await options.onPermissionRequest({\n id: permission.id,\n sessionID: permission.sessionID,\n type: permission.type,\n title: permission.title,\n ...(permission.callID !== undefined && { callID: permission.callID }),\n })\n await client.postSessionIdPermissionsPermissionId({\n path: { id: permission.sessionID, permissionID: permission.id },\n query: dirQuery,\n body: { response },\n throwOnError: true,\n })\n } catch (error) {\n if (!lifecycle.disposed) options.onError?.(error)\n }\n }\n\n const subscription = await client.event.subscribe()\n const stream = subscription.stream\n\n void (async () => {\n try {\n for await (const event of stream) {\n if (lifecycle.disposed) break\n const sid = sessionIdOf(event)\n if (sid !== undefined && sid !== resolvedSessionId) continue\n if (event.type === 'permission.updated') {\n void handlePermission(event.properties)\n continue\n }\n // The SDK event union is a structural superset of the subset the\n // translator consumes; unknown event types match no translator\n // branch and are ignored.\n options.onEvent(event as OpencodeEvent)\n }\n } catch (error) {\n if (!lifecycle.disposed) options.onError?.(error)\n }\n })()\n\n return {\n sessionId: resolvedSessionId,\n resumed,\n prompt: async (text: string) => {\n const result = await client.session.prompt({\n path: { id: resolvedSessionId },\n query: dirQuery,\n body: {\n model: { providerID: options.providerID, modelID: options.modelID },\n parts: [{ type: 'text', text }],\n },\n throwOnError: true,\n })\n const data = result.data\n const message = data.info as OpencodeAssistantMessage\n const responseText = data.parts\n .filter(\n (part): part is Extract<Part, { type: 'text' }> =>\n part.type === 'text',\n )\n .map((part) => part.text)\n .join('')\n return { message, text: responseText }\n },\n abort: async () => {\n try {\n await client.session.abort({\n path: { id: resolvedSessionId },\n query: dirQuery,\n })\n } catch {\n // Best-effort: the turn may already be finishing.\n }\n },\n dispose: async () => {\n lifecycle.disposed = true\n try {\n await stream.return(undefined)\n } catch {\n // Ignore: stream may already be closed.\n }\n ownedServer?.close()\n },\n }\n } catch (error) {\n await teardown()\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.4",
3
+ "version": "0.3.0",
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.43.0",
42
- "@tanstack/ai-sandbox": "^0.3.0"
41
+ "@tanstack/ai": "^0.45.0",
42
+ "@tanstack/ai-sandbox": "^0.3.3"
43
43
  },
44
44
  "devDependencies": {
45
- "@vitest/coverage-v8": "4.0.14",
46
- "@tanstack/ai": "0.43.0",
47
- "@tanstack/ai-sandbox": "0.3.0"
45
+ "@vitest/coverage-v8": "4.1.10",
46
+ "@tanstack/ai": "0.45.0",
47
+ "@tanstack/ai-sandbox": "0.3.3"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "vite build",
@@ -1,5 +1,11 @@
1
1
  import { EventType, normalizeSystemPrompts } from '@tanstack/ai'
2
- import { toRunErrorRawEvent } from '@tanstack/ai/adapter-internals'
2
+ import {
3
+ appendOutputSchemaInstruction,
4
+ parseJsonFromAssistantText,
5
+ structuredOutputCompleteChunk,
6
+ structuredOutputStartChunk,
7
+ toRunErrorRawEvent,
8
+ } from '@tanstack/ai/adapter-internals'
3
9
  import { BaseTextAdapter } from '@tanstack/ai/adapters'
4
10
  import {
5
11
  DurableAttachNotSupportedError,
@@ -110,6 +116,14 @@ export class OpencodeTextAdapter<
110
116
  return getSandbox(ctx)
111
117
  }
112
118
 
119
+ supportsCombinedToolsAndSchema(): boolean {
120
+ return true
121
+ }
122
+
123
+ combinedStructuredOutputSource(): 'event' {
124
+ return 'event'
125
+ }
126
+
113
127
  private applySystemPrompts(
114
128
  options: TextOptions<OpencodeTextProviderOptions>,
115
129
  prompt: string,
@@ -321,22 +335,32 @@ export class OpencodeTextAdapter<
321
335
 
322
336
  queue.push({ kind: 'session', sessionId: session.sessionId })
323
337
 
324
- const promptText = this.applySystemPrompts(
338
+ let promptText = this.applySystemPrompts(
325
339
  options,
326
340
  session.resumed || sessionId === undefined
327
341
  ? resumePrompt
328
342
  : buildPrompt(options.messages, undefined).prompt,
329
343
  )
344
+ if (options.outputSchema) {
345
+ promptText = appendOutputSchemaInstruction(
346
+ promptText,
347
+ options.outputSchema,
348
+ )
349
+ }
330
350
 
351
+ let lastAssistantText = ''
331
352
  session
332
353
  .prompt(promptText)
333
- .then(({ message }) => {
354
+ .then(({ message, text }) => {
355
+ lastAssistantText = text
334
356
  queue.push({ kind: 'done', message })
335
357
  queue.end()
336
358
  })
337
359
  .catch((error: unknown) => queue.fail(error))
338
360
 
339
- yield* mergeChunkStreams(
361
+ let heldFinished: StreamChunk | undefined
362
+ let lastTextMessageId: string | undefined
363
+ for await (const chunk of mergeChunkStreams(
340
364
  translateOpencodeStream(queue, {
341
365
  model: this.model,
342
366
  runId,
@@ -352,7 +376,54 @@ export class OpencodeTextAdapter<
352
376
  }),
353
377
  }),
354
378
  channel.stream,
355
- )
379
+ )) {
380
+ if (options.outputSchema && chunk.type === EventType.RUN_FINISHED) {
381
+ heldFinished = chunk
382
+ continue
383
+ }
384
+ if (
385
+ chunk.type === EventType.TEXT_MESSAGE_START &&
386
+ typeof chunk.messageId === 'string' &&
387
+ chunk.messageId !== ''
388
+ ) {
389
+ lastTextMessageId = chunk.messageId
390
+ }
391
+ yield chunk
392
+ }
393
+
394
+ if (options.outputSchema) {
395
+ try {
396
+ const object = parseJsonFromAssistantText(lastAssistantText)
397
+ const messageId = lastTextMessageId ?? this.generateId()
398
+ yield structuredOutputStartChunk({
399
+ messageId,
400
+ model: this.model,
401
+ threadId,
402
+ runId,
403
+ })
404
+ yield structuredOutputCompleteChunk({
405
+ messageId,
406
+ model: this.model,
407
+ threadId,
408
+ runId,
409
+ object,
410
+ raw: lastAssistantText,
411
+ })
412
+ } catch (error: unknown) {
413
+ const message =
414
+ error instanceof Error
415
+ ? error.message
416
+ : 'Failed to parse structured output'
417
+ yield {
418
+ type: EventType.RUN_ERROR,
419
+ model: this.model,
420
+ timestamp: Date.now(),
421
+ message,
422
+ error: { message },
423
+ }
424
+ }
425
+ }
426
+ if (heldFinished) yield heldFinished
356
427
 
357
428
  // Surface pending approval requests (ask-policy actions awaiting a client
358
429
  // decision); the client approves and re-runs to continue.
@@ -392,8 +463,8 @@ export class OpencodeTextAdapter<
392
463
  ): Promise<StructuredOutputResult<unknown>> {
393
464
  return Promise.reject(
394
465
  new Error(
395
- 'Structured output is not yet supported by the in-sandbox OpenCode adapter. ' +
396
- 'Use a model adapter for structured output, or omit outputSchema.',
466
+ 'This harness honors outputSchema on chat() in the same turn. ' +
467
+ 'Pass outputSchema to chat(), or use a model adapter for a one-shot extract.',
397
468
  ),
398
469
  )
399
470
  }