@tanstack/ai-opencode 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/adapters/projection.js +140 -67
- package/dist/esm/adapters/projection.js.map +1 -1
- package/dist/esm/adapters/text.js +194 -220
- package/dist/esm/adapters/text.js.map +1 -1
- package/dist/esm/index.js +6 -21
- package/dist/esm/messages/prompt.js +27 -33
- package/dist/esm/messages/prompt.js.map +1 -1
- package/dist/esm/model-meta.js +23 -12
- package/dist/esm/model-meta.js.map +1 -1
- package/dist/esm/process/permissions.js +56 -44
- package/dist/esm/process/permissions.js.map +1 -1
- package/dist/esm/process/sandbox-server.js +52 -68
- package/dist/esm/process/sandbox-server.js.map +1 -1
- package/dist/esm/process/server.js +162 -154
- package/dist/esm/process/server.js.map +1 -1
- package/dist/esm/stream/queue.js +60 -55
- package/dist/esm/stream/queue.js.map +1 -1
- package/dist/esm/stream/translate.js +311 -296
- package/dist/esm/stream/translate.js.map +1 -1
- package/package.json +7 -7
- package/src/adapters/text.ts +50 -1
- package/dist/esm/index.js.map +0 -1
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import { normalizeSystemPrompts, EventType } from "@tanstack/ai";
|
|
2
|
-
import { toRunErrorRawEvent } from "@tanstack/ai/adapter-internals";
|
|
3
|
-
import { BaseTextAdapter } from "@tanstack/ai/adapters";
|
|
4
|
-
import { SandboxCapability, getSandbox, createBridgeEventChannel, getWorkspaceProjection, getToolBridgeProvisioner, nodeHttpBridgeProvisioner, buildApprovalRequestedEvent, mergeChunkStreams } from "@tanstack/ai-sandbox";
|
|
5
1
|
import { buildPrompt } from "../messages/prompt.js";
|
|
6
2
|
import { startOpencodeSession } from "../process/server.js";
|
|
7
3
|
import { startOpencodeServerInSandbox } from "../process/sandbox-server.js";
|
|
@@ -9,223 +5,201 @@ import { resolveInteractivePermission } from "../process/permissions.js";
|
|
|
9
5
|
import { AsyncQueue } from "../stream/queue.js";
|
|
10
6
|
import { translateOpencodeStream } from "../stream/translate.js";
|
|
11
7
|
import { projectOpencodeWorkspace } from "./projection.js";
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
import { EventType, normalizeSystemPrompts } from "@tanstack/ai";
|
|
9
|
+
import { toRunErrorRawEvent } from "@tanstack/ai/adapter-internals";
|
|
10
|
+
import { BaseTextAdapter } from "@tanstack/ai/adapters";
|
|
11
|
+
import { DurableAttachNotSupportedError, SandboxCapability, buildApprovalRequestedEvent, createBridgeEventChannel, getSandbox, getSandboxDurability, getToolBridgeProvisioner, getWorkspaceProjection, mergeChunkStreams, nodeHttpBridgeProvisioner, resolveDurableRunId } from "@tanstack/ai-sandbox";
|
|
12
|
+
//#region src/adapters/text.ts
|
|
13
|
+
var DEFAULT_WORKDIR = "/workspace";
|
|
14
|
+
var DEFAULT_PORT = 4096;
|
|
15
|
+
/** Split a `provider/model` id into its provider and model halves. */
|
|
14
16
|
function splitModel(model) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return { providerID: model.slice(0, slash), modelID: model.slice(slash + 1) };
|
|
22
|
-
}
|
|
23
|
-
class OpencodeTextAdapter extends BaseTextAdapter {
|
|
24
|
-
name = "opencode";
|
|
25
|
-
requires = [SandboxCapability];
|
|
26
|
-
adapterConfig;
|
|
27
|
-
constructor(config, model) {
|
|
28
|
-
super({}, model);
|
|
29
|
-
this.adapterConfig = config;
|
|
30
|
-
}
|
|
31
|
-
sandboxFrom(options) {
|
|
32
|
-
const ctx = options.capabilities;
|
|
33
|
-
if (!ctx) {
|
|
34
|
-
throw new Error(
|
|
35
|
-
'Adapter "opencode" requires a sandbox. Add withSandbox(defineSandbox({ ... })) to chat() middleware.'
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
return getSandbox(ctx);
|
|
39
|
-
}
|
|
40
|
-
applySystemPrompts(options, prompt) {
|
|
41
|
-
const systemPrompts = normalizeSystemPrompts(options.systemPrompts).map((systemPrompt) => systemPrompt.content).filter((content) => content.trim() !== "");
|
|
42
|
-
if (systemPrompts.length === 0) return prompt;
|
|
43
|
-
return `${systemPrompts.join("\n\n")}
|
|
44
|
-
|
|
45
|
-
${prompt}`;
|
|
46
|
-
}
|
|
47
|
-
async *chatStream(options) {
|
|
48
|
-
const { logger } = options;
|
|
49
|
-
let server;
|
|
50
|
-
let handle;
|
|
51
|
-
let bridge;
|
|
52
|
-
const externalSignal = options.abortController?.signal ?? options.request?.signal ?? void 0;
|
|
53
|
-
let onAbort;
|
|
54
|
-
const runId = options.runId ?? this.generateId();
|
|
55
|
-
const threadId = options.threadId ?? this.generateId();
|
|
56
|
-
const channel = createBridgeEventChannel({
|
|
57
|
-
model: this.model,
|
|
58
|
-
threadId,
|
|
59
|
-
runId
|
|
60
|
-
});
|
|
61
|
-
try {
|
|
62
|
-
const sandbox = this.sandboxFrom(options);
|
|
63
|
-
const directory = options.modelOptions?.directory ?? this.adapterConfig.directory ?? DEFAULT_WORKDIR;
|
|
64
|
-
if (options.capabilities !== void 0) {
|
|
65
|
-
const projection = getWorkspaceProjection(options.capabilities, {
|
|
66
|
-
optional: true
|
|
67
|
-
});
|
|
68
|
-
if (projection !== void 0) {
|
|
69
|
-
await projectOpencodeWorkspace(sandbox, projection);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
const modelOptions = options.modelOptions;
|
|
73
|
-
const sessionId = modelOptions?.sessionId;
|
|
74
|
-
const { prompt: resumePrompt } = buildPrompt(options.messages, sessionId);
|
|
75
|
-
const { providerID, modelID } = splitModel(this.model);
|
|
76
|
-
const bridgedToolNames = new Set(
|
|
77
|
-
(options.tools ?? []).map((tool) => tool.name)
|
|
78
|
-
);
|
|
79
|
-
if (options.tools && options.tools.length > 0) {
|
|
80
|
-
const provisioner = (options.capabilities ? getToolBridgeProvisioner(options.capabilities, { optional: true }) : void 0) ?? nodeHttpBridgeProvisioner;
|
|
81
|
-
bridge = await provisioner.provision(options.tools, {
|
|
82
|
-
provider: sandbox.provider,
|
|
83
|
-
context: options.context,
|
|
84
|
-
emitCustomEvent: channel.emitCustomEvent,
|
|
85
|
-
...externalSignal ? { signal: externalSignal } : {}
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
const approvalRequests = [];
|
|
89
|
-
const queue = new AsyncQueue();
|
|
90
|
-
const mode = modelOptions?.permissionMode ?? this.adapterConfig.permissionMode ?? "default";
|
|
91
|
-
const permissionHandler = this.adapterConfig.onPermissionRequest ?? ((request) => {
|
|
92
|
-
const result = resolveInteractivePermission(
|
|
93
|
-
request,
|
|
94
|
-
mode,
|
|
95
|
-
bridgedToolNames,
|
|
96
|
-
options.approvals
|
|
97
|
-
);
|
|
98
|
-
if (result.approvalId !== void 0) {
|
|
99
|
-
approvalRequests.push(
|
|
100
|
-
buildApprovalRequestedEvent({
|
|
101
|
-
approvalId: result.approvalId,
|
|
102
|
-
title: result.title ?? request.title,
|
|
103
|
-
threadId,
|
|
104
|
-
runId,
|
|
105
|
-
detail: { provider: "opencode" }
|
|
106
|
-
})
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
return result.response;
|
|
110
|
-
});
|
|
111
|
-
logger.request(
|
|
112
|
-
`activity=chat provider=opencode model=${this.model} sandbox=${sandbox.provider} messages=${options.messages.length} resume=${sessionId ?? "none"}`,
|
|
113
|
-
{ provider: "opencode", model: this.model }
|
|
114
|
-
);
|
|
115
|
-
const serverEnv = bridge ? {
|
|
116
|
-
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
117
|
-
mcp: {
|
|
118
|
-
[bridge.name]: {
|
|
119
|
-
type: "remote",
|
|
120
|
-
url: bridge.url,
|
|
121
|
-
enabled: true,
|
|
122
|
-
headers: { Authorization: `Bearer ${bridge.token}` }
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
})
|
|
126
|
-
} : void 0;
|
|
127
|
-
server = await startOpencodeServerInSandbox(sandbox, {
|
|
128
|
-
port: this.adapterConfig.port ?? DEFAULT_PORT,
|
|
129
|
-
...this.adapterConfig.hostname !== void 0 && {
|
|
130
|
-
hostname: this.adapterConfig.hostname
|
|
131
|
-
},
|
|
132
|
-
cwd: directory,
|
|
133
|
-
...serverEnv ? { env: serverEnv } : {},
|
|
134
|
-
...externalSignal ? { signal: externalSignal } : {}
|
|
135
|
-
});
|
|
136
|
-
handle = await startOpencodeSession({
|
|
137
|
-
baseUrl: server.baseUrl,
|
|
138
|
-
// Forward the channel's auth headers (e.g. Daytona's preview token) so
|
|
139
|
-
// the host client can reach a token-gated preview proxy.
|
|
140
|
-
...server.headers !== void 0 && { headers: server.headers },
|
|
141
|
-
// NOTE: do NOT pass `directory` here. `directory` is the VIRTUAL sandbox
|
|
142
|
-
// path (e.g. `/workspace`); the server is already spawned with that as its
|
|
143
|
-
// cwd (the provider handle maps it to the real workdir — `/workspace`
|
|
144
|
-
// inside Docker, a host temp dir for local-process). Forwarding the
|
|
145
|
-
// virtual path to the host-side opencode HTTP API breaks local-process,
|
|
146
|
-
// where `/workspace` doesn't exist → the API stalls until the request
|
|
147
|
-
// times out. Omitting it makes opencode use the server's (correct) cwd.
|
|
148
|
-
providerID,
|
|
149
|
-
modelID,
|
|
150
|
-
...sessionId !== void 0 && { resumeSessionId: sessionId },
|
|
151
|
-
onEvent: (event) => queue.push({ kind: "event", event }),
|
|
152
|
-
onPermissionRequest: permissionHandler,
|
|
153
|
-
onError: (error) => queue.fail(error)
|
|
154
|
-
});
|
|
155
|
-
const session = handle;
|
|
156
|
-
if (externalSignal !== void 0) {
|
|
157
|
-
onAbort = () => void session.abort().catch(() => void 0);
|
|
158
|
-
if (externalSignal.aborted) onAbort();
|
|
159
|
-
else externalSignal.addEventListener("abort", onAbort, { once: true });
|
|
160
|
-
}
|
|
161
|
-
queue.push({ kind: "session", sessionId: session.sessionId });
|
|
162
|
-
const promptText = this.applySystemPrompts(
|
|
163
|
-
options,
|
|
164
|
-
session.resumed || sessionId === void 0 ? resumePrompt : buildPrompt(options.messages, void 0).prompt
|
|
165
|
-
);
|
|
166
|
-
session.prompt(promptText).then(({ message }) => {
|
|
167
|
-
queue.push({ kind: "done", message });
|
|
168
|
-
queue.end();
|
|
169
|
-
}).catch((error) => queue.fail(error));
|
|
170
|
-
yield* mergeChunkStreams(
|
|
171
|
-
translateOpencodeStream(queue, {
|
|
172
|
-
model: this.model,
|
|
173
|
-
runId,
|
|
174
|
-
threadId,
|
|
175
|
-
...options.parentRunId !== void 0 && {
|
|
176
|
-
parentRunId: options.parentRunId
|
|
177
|
-
},
|
|
178
|
-
genId: () => this.generateId(),
|
|
179
|
-
bridgedToolNames,
|
|
180
|
-
onStreamEvent: (event) => logger.provider(`provider=opencode kind=${event.kind}`, {
|
|
181
|
-
chunk: event
|
|
182
|
-
})
|
|
183
|
-
}),
|
|
184
|
-
channel.stream
|
|
185
|
-
);
|
|
186
|
-
for (const event of approvalRequests) yield event;
|
|
187
|
-
} catch (error) {
|
|
188
|
-
const err = error;
|
|
189
|
-
const rawEvent = toRunErrorRawEvent(error);
|
|
190
|
-
logger.errors("opencode.chatStream fatal", {
|
|
191
|
-
error,
|
|
192
|
-
source: "opencode.chatStream"
|
|
193
|
-
});
|
|
194
|
-
yield {
|
|
195
|
-
type: EventType.RUN_ERROR,
|
|
196
|
-
model: options.model,
|
|
197
|
-
timestamp: Date.now(),
|
|
198
|
-
message: err.message || "Unknown error occurred",
|
|
199
|
-
...err.code !== void 0 && { code: err.code },
|
|
200
|
-
...rawEvent !== void 0 && { rawEvent },
|
|
201
|
-
error: {
|
|
202
|
-
message: err.message || "Unknown error occurred",
|
|
203
|
-
...err.code !== void 0 && { code: err.code }
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
} finally {
|
|
207
|
-
if (externalSignal !== void 0 && onAbort !== void 0) {
|
|
208
|
-
externalSignal.removeEventListener("abort", onAbort);
|
|
209
|
-
}
|
|
210
|
-
channel.close();
|
|
211
|
-
await handle?.dispose();
|
|
212
|
-
await server?.dispose();
|
|
213
|
-
await bridge?.close();
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
structuredOutput(_options) {
|
|
217
|
-
return Promise.reject(
|
|
218
|
-
new Error(
|
|
219
|
-
"Structured output is not yet supported by the in-sandbox OpenCode adapter. Use a model adapter for structured output, or omit outputSchema."
|
|
220
|
-
)
|
|
221
|
-
);
|
|
222
|
-
}
|
|
17
|
+
const slash = model.indexOf("/");
|
|
18
|
+
if (slash <= 0 || slash === model.length - 1) throw new Error(`OpenCode models must be addressed as "provider/model" (e.g. "anthropic/claude-sonnet-4-5"); received "${model}".`);
|
|
19
|
+
return {
|
|
20
|
+
providerID: model.slice(0, slash),
|
|
21
|
+
modelID: model.slice(slash + 1)
|
|
22
|
+
};
|
|
223
23
|
}
|
|
24
|
+
var OpencodeTextAdapter = class extends BaseTextAdapter {
|
|
25
|
+
name = "opencode";
|
|
26
|
+
requires = [SandboxCapability];
|
|
27
|
+
adapterConfig;
|
|
28
|
+
constructor(config, model) {
|
|
29
|
+
super({}, model);
|
|
30
|
+
this.adapterConfig = config;
|
|
31
|
+
}
|
|
32
|
+
sandboxFrom(options) {
|
|
33
|
+
const ctx = options.capabilities;
|
|
34
|
+
if (!ctx) throw new Error("Adapter \"opencode\" requires a sandbox. Add withSandbox(defineSandbox({ ... })) to chat() middleware.");
|
|
35
|
+
return getSandbox(ctx);
|
|
36
|
+
}
|
|
37
|
+
applySystemPrompts(options, prompt) {
|
|
38
|
+
const systemPrompts = normalizeSystemPrompts(options.systemPrompts).map((systemPrompt) => systemPrompt.content).filter((content) => content.trim() !== "");
|
|
39
|
+
if (systemPrompts.length === 0) return prompt;
|
|
40
|
+
return `${systemPrompts.join("\n\n")}\n\n${prompt}`;
|
|
41
|
+
}
|
|
42
|
+
async *chatStream(options) {
|
|
43
|
+
const { logger } = options;
|
|
44
|
+
let server;
|
|
45
|
+
let handle;
|
|
46
|
+
let bridge;
|
|
47
|
+
const externalSignal = options.abortController?.signal ?? options.request?.signal ?? void 0;
|
|
48
|
+
let onAbort;
|
|
49
|
+
const runId = resolveDurableRunId(options.runId, {
|
|
50
|
+
durable: false,
|
|
51
|
+
adapter: "opencode",
|
|
52
|
+
fallback: () => this.generateId()
|
|
53
|
+
});
|
|
54
|
+
const threadId = options.threadId ?? this.generateId();
|
|
55
|
+
const channel = createBridgeEventChannel({
|
|
56
|
+
model: this.model,
|
|
57
|
+
threadId,
|
|
58
|
+
runId
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
const sandbox = this.sandboxFrom(options);
|
|
62
|
+
const directory = options.modelOptions?.directory ?? this.adapterConfig.directory ?? DEFAULT_WORKDIR;
|
|
63
|
+
const durability = options.capabilities ? getSandboxDurability(options.capabilities, { optional: true }) : void 0;
|
|
64
|
+
if (durability !== void 0) {
|
|
65
|
+
if (durability.attach) throw new DurableAttachNotSupportedError("opencode", "this adapter drives the harness over the opencode HTTP server and does not journal");
|
|
66
|
+
logger.warn("opencode: sandbox durability is wired but this adapter never journals — this run will not be recoverable on reconnect. Use a journaling harness adapter for runs that must survive a host restart, or drop durability if these runs are not meant to.", {
|
|
67
|
+
runId,
|
|
68
|
+
adapter: "opencode"
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (options.capabilities !== void 0) {
|
|
72
|
+
const projection = getWorkspaceProjection(options.capabilities, { optional: true });
|
|
73
|
+
if (projection !== void 0) await projectOpencodeWorkspace(sandbox, projection);
|
|
74
|
+
}
|
|
75
|
+
const modelOptions = options.modelOptions;
|
|
76
|
+
const sessionId = modelOptions?.sessionId;
|
|
77
|
+
const { prompt: resumePrompt } = buildPrompt(options.messages, sessionId);
|
|
78
|
+
const { providerID, modelID } = splitModel(this.model);
|
|
79
|
+
const bridgedToolNames = new Set((options.tools ?? []).map((tool) => tool.name));
|
|
80
|
+
if (options.tools && options.tools.length > 0) bridge = await ((options.capabilities ? getToolBridgeProvisioner(options.capabilities, { optional: true }) : void 0) ?? nodeHttpBridgeProvisioner).provision(options.tools, {
|
|
81
|
+
provider: sandbox.provider,
|
|
82
|
+
context: options.context,
|
|
83
|
+
emitCustomEvent: channel.emitCustomEvent,
|
|
84
|
+
...externalSignal ? { signal: externalSignal } : {}
|
|
85
|
+
});
|
|
86
|
+
const approvalRequests = [];
|
|
87
|
+
const queue = new AsyncQueue();
|
|
88
|
+
const mode = modelOptions?.permissionMode ?? this.adapterConfig.permissionMode ?? "default";
|
|
89
|
+
const permissionHandler = this.adapterConfig.onPermissionRequest ?? ((request) => {
|
|
90
|
+
const result = resolveInteractivePermission(request, mode, bridgedToolNames, options.approvals);
|
|
91
|
+
if (result.approvalId !== void 0) approvalRequests.push(buildApprovalRequestedEvent({
|
|
92
|
+
approvalId: result.approvalId,
|
|
93
|
+
title: result.title ?? request.title,
|
|
94
|
+
threadId,
|
|
95
|
+
runId,
|
|
96
|
+
detail: { provider: "opencode" }
|
|
97
|
+
}));
|
|
98
|
+
return result.response;
|
|
99
|
+
});
|
|
100
|
+
logger.request(`activity=chat provider=opencode model=${this.model} sandbox=${sandbox.provider} messages=${options.messages.length} resume=${sessionId ?? "none"}`, {
|
|
101
|
+
provider: "opencode",
|
|
102
|
+
model: this.model
|
|
103
|
+
});
|
|
104
|
+
const serverEnv = bridge ? { OPENCODE_CONFIG_CONTENT: JSON.stringify({ mcp: { [bridge.name]: {
|
|
105
|
+
type: "remote",
|
|
106
|
+
url: bridge.url,
|
|
107
|
+
enabled: true,
|
|
108
|
+
headers: { Authorization: `Bearer ${bridge.token}` }
|
|
109
|
+
} } }) } : void 0;
|
|
110
|
+
server = await startOpencodeServerInSandbox(sandbox, {
|
|
111
|
+
port: this.adapterConfig.port ?? DEFAULT_PORT,
|
|
112
|
+
...this.adapterConfig.hostname !== void 0 && { hostname: this.adapterConfig.hostname },
|
|
113
|
+
cwd: directory,
|
|
114
|
+
...serverEnv ? { env: serverEnv } : {},
|
|
115
|
+
...externalSignal ? { signal: externalSignal } : {}
|
|
116
|
+
});
|
|
117
|
+
handle = await startOpencodeSession({
|
|
118
|
+
baseUrl: server.baseUrl,
|
|
119
|
+
...server.headers !== void 0 && { headers: server.headers },
|
|
120
|
+
providerID,
|
|
121
|
+
modelID,
|
|
122
|
+
...sessionId !== void 0 && { resumeSessionId: sessionId },
|
|
123
|
+
onEvent: (event) => queue.push({
|
|
124
|
+
kind: "event",
|
|
125
|
+
event
|
|
126
|
+
}),
|
|
127
|
+
onPermissionRequest: permissionHandler,
|
|
128
|
+
onError: (error) => queue.fail(error)
|
|
129
|
+
});
|
|
130
|
+
const session = handle;
|
|
131
|
+
if (externalSignal !== void 0) {
|
|
132
|
+
onAbort = () => void session.abort().catch(() => void 0);
|
|
133
|
+
if (externalSignal.aborted) onAbort();
|
|
134
|
+
else externalSignal.addEventListener("abort", onAbort, { once: true });
|
|
135
|
+
}
|
|
136
|
+
queue.push({
|
|
137
|
+
kind: "session",
|
|
138
|
+
sessionId: session.sessionId
|
|
139
|
+
});
|
|
140
|
+
const promptText = this.applySystemPrompts(options, session.resumed || sessionId === void 0 ? resumePrompt : buildPrompt(options.messages, void 0).prompt);
|
|
141
|
+
session.prompt(promptText).then(({ message }) => {
|
|
142
|
+
queue.push({
|
|
143
|
+
kind: "done",
|
|
144
|
+
message
|
|
145
|
+
});
|
|
146
|
+
queue.end();
|
|
147
|
+
}).catch((error) => queue.fail(error));
|
|
148
|
+
yield* mergeChunkStreams(translateOpencodeStream(queue, {
|
|
149
|
+
model: this.model,
|
|
150
|
+
runId,
|
|
151
|
+
threadId,
|
|
152
|
+
...options.parentRunId !== void 0 && { parentRunId: options.parentRunId },
|
|
153
|
+
genId: () => this.generateId(),
|
|
154
|
+
bridgedToolNames,
|
|
155
|
+
onStreamEvent: (event) => logger.provider(`provider=opencode kind=${event.kind}`, { chunk: event })
|
|
156
|
+
}), channel.stream);
|
|
157
|
+
for (const event of approvalRequests) yield event;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const err = error;
|
|
160
|
+
const rawEvent = toRunErrorRawEvent(error);
|
|
161
|
+
logger.errors("opencode.chatStream fatal", {
|
|
162
|
+
error,
|
|
163
|
+
source: "opencode.chatStream"
|
|
164
|
+
});
|
|
165
|
+
yield {
|
|
166
|
+
type: EventType.RUN_ERROR,
|
|
167
|
+
model: options.model,
|
|
168
|
+
timestamp: Date.now(),
|
|
169
|
+
message: err.message || "Unknown error occurred",
|
|
170
|
+
...err.code !== void 0 && { code: err.code },
|
|
171
|
+
...rawEvent !== void 0 && { rawEvent },
|
|
172
|
+
error: {
|
|
173
|
+
message: err.message || "Unknown error occurred",
|
|
174
|
+
...err.code !== void 0 && { code: err.code }
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
} finally {
|
|
178
|
+
if (externalSignal !== void 0 && onAbort !== void 0) externalSignal.removeEventListener("abort", onAbort);
|
|
179
|
+
channel.close();
|
|
180
|
+
await handle?.dispose();
|
|
181
|
+
await server?.dispose();
|
|
182
|
+
await bridge?.close();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
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."));
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Creates an OpenCode harness adapter that runs **inside a sandbox**.
|
|
191
|
+
*
|
|
192
|
+
* It declares `requires: [SandboxCapability]`, spawns `opencode serve` inside
|
|
193
|
+
* the sandbox provided by `withSandbox(...)`, exposes its port, and connects
|
|
194
|
+
* the `@opencode-ai/sdk` HTTP client to it. OpenCode owns the agent loop and
|
|
195
|
+
* executes its native tools against the sandbox workspace. The sandbox image
|
|
196
|
+
* must provide the `opencode` executable (Docker: also publish the server port
|
|
197
|
+
* via `publishPorts`). chat()-provided tools aren't bridged yet.
|
|
198
|
+
*/
|
|
224
199
|
function opencodeText(model, config = {}) {
|
|
225
|
-
|
|
200
|
+
return new OpencodeTextAdapter(config, model);
|
|
226
201
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
//# sourceMappingURL=text.js.map
|
|
202
|
+
//#endregion
|
|
203
|
+
export { OpencodeTextAdapter, opencodeText };
|
|
204
|
+
|
|
205
|
+
//# sourceMappingURL=text.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text.js","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 SandboxCapability,\n buildApprovalRequestedEvent,\n createBridgeEventChannel,\n getSandbox,\n getToolBridgeProvisioner,\n getWorkspaceProjection,\n mergeChunkStreams,\n nodeHttpBridgeProvisioner,\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 const runId = options.runId ?? this.generateId()\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 // 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"],"names":[],"mappings":";;;;;;;;;;;AAwCA,MAAM,kBAAkB;AACxB,MAAM,eAAe;AAwBrB,SAAS,WAAW,OAAwD;AAC1E,QAAM,QAAQ,MAAM,QAAQ,GAAG;AAC/B,MAAI,SAAS,KAAK,UAAU,MAAM,SAAS,GAAG;AAC5C,UAAM,IAAI;AAAA,MACR,yGAAyG,KAAK;AAAA,IAAA;AAAA,EAElH;AACA,SAAO,EAAE,YAAY,MAAM,MAAM,GAAG,KAAK,GAAG,SAAS,MAAM,MAAM,QAAQ,CAAC,EAAA;AAC5E;AAEO,MAAM,4BAEH,gBAQR;AAAA,EACS,OAAO;AAAA,EAEE,WAAW,CAAC,iBAAiB;AAAA,EAE9B;AAAA,EAEjB,YAAY,QAA4B,OAAe;AACrD,UAAM,CAAA,GAAI,KAAK;AACf,SAAK,gBAAgB;AAAA,EACvB;AAAA,EAEQ,YACN,SACe;AACf,UAAM,MAAM,QAAQ;AACpB,QAAI,CAAC,KAAK;AACR,YAAM,IAAI;AAAA,QACR;AAAA,MAAA;AAAA,IAEJ;AACA,WAAO,WAAW,GAAG;AAAA,EACvB;AAAA,EAEQ,mBACN,SACA,QACQ;AACR,UAAM,gBAAgB,uBAAuB,QAAQ,aAAa,EAC/D,IAAI,CAAC,iBAAiB,aAAa,OAAO,EAC1C,OAAO,CAAC,YAAY,QAAQ,KAAA,MAAW,EAAE;AAC5C,QAAI,cAAc,WAAW,EAAG,QAAO;AACvC,WAAO,GAAG,cAAc,KAAK,MAAM,CAAC;AAAA;AAAA,EAAO,MAAM;AAAA,EACnD;AAAA,EAEA,OAAO,WACL,SAC4B;AAC5B,UAAM,EAAE,WAAW;AACnB,QAAI;AAGJ,QAAI;AACJ,QAAI;AACJ,UAAM,iBACJ,QAAQ,iBAAiB,UAAU,QAAQ,SAAS,UAAU;AAChE,QAAI;AACJ,UAAM,QAAQ,QAAQ,SAAS,KAAK,WAAA;AACpC,UAAM,WAAW,QAAQ,YAAY,KAAK,WAAA;AAG1C,UAAM,UAAU,yBAAyB;AAAA,MACvC,OAAO,KAAK;AAAA,MACZ;AAAA,MACA;AAAA,IAAA,CACD;AAED,QAAI;AACF,YAAM,UAAU,KAAK,YAAY,OAAO;AACxC,YAAM,YACJ,QAAQ,cAAc,aACtB,KAAK,cAAc,aACnB;AAIF,UAAI,QAAQ,iBAAiB,QAAW;AACtC,cAAM,aAAa,uBAAuB,QAAQ,cAAc;AAAA,UAC9D,UAAU;AAAA,QAAA,CACX;AACD,YAAI,eAAe,QAAW;AAC5B,gBAAM,yBAAyB,SAAS,UAAU;AAAA,QACpD;AAAA,MACF;AAEA,YAAM,eAAe,QAAQ;AAC7B,YAAM,YAAY,cAAc;AAChC,YAAM,EAAE,QAAQ,aAAA,IAAiB,YAAY,QAAQ,UAAU,SAAS;AACxE,YAAM,EAAE,YAAY,QAAA,IAAY,WAAW,KAAK,KAAK;AAIrD,YAAM,mBAAmB,IAAI;AAAA,SAC1B,QAAQ,SAAS,CAAA,GAAI,IAAI,CAAC,SAAS,KAAK,IAAI;AAAA,MAAA;AAE/C,UAAI,QAAQ,SAAS,QAAQ,MAAM,SAAS,GAAG;AAC7C,cAAM,eACH,QAAQ,eACL,yBAAyB,QAAQ,cAAc,EAAE,UAAU,KAAA,CAAM,IACjE,WAAc;AACpB,iBAAS,MAAM,YAAY,UAAU,QAAQ,OAAO;AAAA,UAClD,UAAU,QAAQ;AAAA,UAClB,SAAS,QAAQ;AAAA,UACjB,iBAAiB,QAAQ;AAAA,UACzB,GAAI,iBAAiB,EAAE,QAAQ,mBAAmB,CAAA;AAAA,QAAC,CACpD;AAAA,MACH;AAIA,YAAM,mBAAuC,CAAA;AAE7C,YAAM,QAAQ,IAAI,WAAA;AAClB,YAAM,OACJ,cAAc,kBACd,KAAK,cAAc,kBACnB;AACF,YAAM,oBACJ,KAAK,cAAc,wBAClB,CAAC,YAAY;AACZ,cAAM,SAAS;AAAA,UACb;AAAA,UACA;AAAA,UACA;AAAA,UACA,QAAQ;AAAA,QAAA;AAEV,YAAI,OAAO,eAAe,QAAW;AACnC,2BAAiB;AAAA,YACf,4BAA4B;AAAA,cAC1B,YAAY,OAAO;AAAA,cACnB,OAAO,OAAO,SAAS,QAAQ;AAAA,cAC/B;AAAA,cACA;AAAA,cACA,QAAQ,EAAE,UAAU,WAAA;AAAA,YAAW,CAChC;AAAA,UAAA;AAAA,QAEL;AACA,eAAO,OAAO;AAAA,MAChB;AAEF,aAAO;AAAA,QACL,yCAAyC,KAAK,KAAK,YAAY,QAAQ,QAAQ,aAAa,QAAQ,SAAS,MAAM,WAAW,aAAa,MAAM;AAAA,QACjJ,EAAE,UAAU,YAAY,OAAO,KAAK,MAAA;AAAA,MAAM;AAG5C,YAAM,YAAY,SACd;AAAA,QACE,yBAAyB,KAAK,UAAU;AAAA,UACtC,KAAK;AAAA,YACH,CAAC,OAAO,IAAI,GAAG;AAAA,cACb,MAAM;AAAA,cACN,KAAK,OAAO;AAAA,cACZ,SAAS;AAAA,cACT,SAAS,EAAE,eAAe,UAAU,OAAO,KAAK,GAAA;AAAA,YAAG;AAAA,UACrD;AAAA,QACF,CACD;AAAA,MAAA,IAEH;AAEJ,eAAS,MAAM,6BAA6B,SAAS;AAAA,QACnD,MAAM,KAAK,cAAc,QAAQ;AAAA,QACjC,GAAI,KAAK,cAAc,aAAa,UAAa;AAAA,UAC/C,UAAU,KAAK,cAAc;AAAA,QAAA;AAAA,QAE/B,KAAK;AAAA,QACL,GAAI,YAAY,EAAE,KAAK,UAAA,IAAc,CAAA;AAAA,QACrC,GAAI,iBAAiB,EAAE,QAAQ,mBAAmB,CAAA;AAAA,MAAC,CACpD;AAED,eAAS,MAAM,qBAAqB;AAAA,QAClC,SAAS,OAAO;AAAA;AAAA;AAAA,QAGhB,GAAI,OAAO,YAAY,UAAa,EAAE,SAAS,OAAO,QAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAQtD;AAAA,QACA;AAAA,QACA,GAAI,cAAc,UAAa,EAAE,iBAAiB,UAAA;AAAA,QAClD,SAAS,CAAC,UAAU,MAAM,KAAK,EAAE,MAAM,SAAS,OAAO;AAAA,QACvD,qBAAqB;AAAA,QACrB,SAAS,CAAC,UAAU,MAAM,KAAK,KAAK;AAAA,MAAA,CACrC;AACD,YAAM,UAAU;AAEhB,UAAI,mBAAmB,QAAW;AAChC,kBAAU,MAAM,KAAK,QAAQ,QAAQ,MAAM,MAAM,MAAS;AAC1D,YAAI,eAAe,QAAS,SAAA;AAAA,4BACR,iBAAiB,SAAS,SAAS,EAAE,MAAM,MAAM;AAAA,MACvE;AAEA,YAAM,KAAK,EAAE,MAAM,WAAW,WAAW,QAAQ,WAAW;AAE5D,YAAM,aAAa,KAAK;AAAA,QACtB;AAAA,QACA,QAAQ,WAAW,cAAc,SAC7B,eACA,YAAY,QAAQ,UAAU,MAAS,EAAE;AAAA,MAAA;AAG/C,cACG,OAAO,UAAU,EACjB,KAAK,CAAC,EAAE,cAAc;AACrB,cAAM,KAAK,EAAE,MAAM,QAAQ,SAAS;AACpC,cAAM,IAAA;AAAA,MACR,CAAC,EACA,MAAM,CAAC,UAAmB,MAAM,KAAK,KAAK,CAAC;AAE9C,aAAO;AAAA,QACL,wBAAwB,OAAO;AAAA,UAC7B,OAAO,KAAK;AAAA,UACZ;AAAA,UACA;AAAA,UACA,GAAI,QAAQ,gBAAgB,UAAa;AAAA,YACvC,aAAa,QAAQ;AAAA,UAAA;AAAA,UAEvB,OAAO,MAAM,KAAK,WAAA;AAAA,UAClB;AAAA,UACA,eAAe,CAAC,UACd,OAAO,SAAS,0BAA0B,MAAM,IAAI,IAAI;AAAA,YACtD,OAAO;AAAA,UAAA,CACR;AAAA,QAAA,CACJ;AAAA,QACD,QAAQ;AAAA,MAAA;AAKV,iBAAW,SAAS,iBAAkB,OAAM;AAAA,IAC9C,SAAS,OAAgB;AACvB,YAAM,MAAM;AACZ,YAAM,WAAW,mBAAmB,KAAK;AACzC,aAAO,OAAO,6BAA6B;AAAA,QACzC;AAAA,QACA,QAAQ;AAAA,MAAA,CACT;AACD,YAAM;AAAA,QACJ,MAAM,UAAU;AAAA,QAChB,OAAO,QAAQ;AAAA,QACf,WAAW,KAAK,IAAA;AAAA,QAChB,SAAS,IAAI,WAAW;AAAA,QACxB,GAAI,IAAI,SAAS,UAAa,EAAE,MAAM,IAAI,KAAA;AAAA,QAC1C,GAAI,aAAa,UAAa,EAAE,SAAA;AAAA,QAChC,OAAO;AAAA,UACL,SAAS,IAAI,WAAW;AAAA,UACxB,GAAI,IAAI,SAAS,UAAa,EAAE,MAAM,IAAI,KAAA;AAAA,QAAK;AAAA,MACjD;AAAA,IAEJ,UAAA;AACE,UAAI,mBAAmB,UAAa,YAAY,QAAW;AACzD,uBAAe,oBAAoB,SAAS,OAAO;AAAA,MACrD;AACA,cAAQ,MAAA;AACR,YAAM,QAAQ,QAAA;AACd,YAAM,QAAQ,QAAA;AACd,YAAM,QAAQ,MAAA;AAAA,IAChB;AAAA,EACF;AAAA,EAEA,iBACE,UAC0C;AAC1C,WAAO,QAAQ;AAAA,MACb,IAAI;AAAA,QACF;AAAA,MAAA;AAAA,IAEF;AAAA,EAEJ;AACF;AAYO,SAAS,aACd,OACA,SAA6B,IACA;AAC7B,SAAO,IAAI,oBAAoB,QAAQ,KAAK;AAC9C;"}
|
|
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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"}
|
package/dist/esm/index.js
CHANGED
|
@@ -1,23 +1,8 @@
|
|
|
1
|
-
import { OpencodeTextAdapter, opencodeText } from "./adapters/text.js";
|
|
2
|
-
import { OPENCODE_MODELS } from "./model-meta.js";
|
|
3
|
-
import { BRIDGED_MCP_SERVER_NAME, SESSION_ID_EVENT, TODO_EVENT, resolveToolName, translateOpencodeStream } from "./stream/translate.js";
|
|
4
|
-
import { matchBridgedToolName, resolvePermission } from "./process/permissions.js";
|
|
5
|
-
import { startOpencodeSession } from "./process/server.js";
|
|
6
1
|
import { buildPrompt } from "./messages/prompt.js";
|
|
2
|
+
import { startOpencodeSession } from "./process/server.js";
|
|
7
3
|
import { startOpencodeServerInSandbox } from "./process/sandbox-server.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
TODO_EVENT,
|
|
14
|
-
buildPrompt,
|
|
15
|
-
matchBridgedToolName,
|
|
16
|
-
opencodeText,
|
|
17
|
-
resolvePermission,
|
|
18
|
-
resolveToolName,
|
|
19
|
-
startOpencodeServerInSandbox,
|
|
20
|
-
startOpencodeSession,
|
|
21
|
-
translateOpencodeStream
|
|
22
|
-
};
|
|
23
|
-
//# sourceMappingURL=index.js.map
|
|
4
|
+
import { matchBridgedToolName, resolvePermission } from "./process/permissions.js";
|
|
5
|
+
import { BRIDGED_MCP_SERVER_NAME, SESSION_ID_EVENT, TODO_EVENT, resolveToolName, translateOpencodeStream } from "./stream/translate.js";
|
|
6
|
+
import { OpencodeTextAdapter, opencodeText } from "./adapters/text.js";
|
|
7
|
+
import { OPENCODE_MODELS } from "./model-meta.js";
|
|
8
|
+
export { BRIDGED_MCP_SERVER_NAME, OPENCODE_MODELS, OpencodeTextAdapter, SESSION_ID_EVENT, TODO_EVENT, buildPrompt, matchBridgedToolName, opencodeText, resolvePermission, resolveToolName, startOpencodeServerInSandbox, startOpencodeSession, translateOpencodeStream };
|
|
@@ -1,37 +1,31 @@
|
|
|
1
|
+
//#region src/messages/prompt.ts
|
|
1
2
|
function extractText(content) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
(part) => part.type === "text" && typeof part.content === "string" ? part.content : ""
|
|
6
|
-
).join("");
|
|
3
|
+
if (content === null) return "";
|
|
4
|
+
if (typeof content === "string") return content;
|
|
5
|
+
return content.map((part) => part.type === "text" && typeof part.content === "string" ? part.content : "").join("");
|
|
7
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Convert TanStack chat history into the OpenCode prompt + resume inputs.
|
|
9
|
+
*
|
|
10
|
+
* With a `sessionId`, the harness already holds the conversation context, so
|
|
11
|
+
* only the trailing user message is sent and the session is resumed. Without
|
|
12
|
+
* one, prior turns are flattened into a plain-text transcript preamble (tool
|
|
13
|
+
* messages and tool-call-only assistant turns are harness-internal noise and
|
|
14
|
+
* are skipped; prompts are text-only in v1).
|
|
15
|
+
*/
|
|
8
16
|
function buildPrompt(messages, sessionId) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const priorTurns = messages.slice(0, -1).filter(
|
|
20
|
-
(message) => (message.role === "user" || message.role === "assistant") && extractText(message.content).trim() !== ""
|
|
21
|
-
).map(
|
|
22
|
-
(message) => `${message.role === "user" ? "User" : "Assistant"}: ${extractText(message.content).trim()}`
|
|
23
|
-
);
|
|
24
|
-
if (priorTurns.length === 0) {
|
|
25
|
-
return { prompt: lastUserText };
|
|
26
|
-
}
|
|
27
|
-
return {
|
|
28
|
-
prompt: `Previous conversation:
|
|
29
|
-
${priorTurns.join("\n")}
|
|
30
|
-
|
|
31
|
-
${lastUserText}`
|
|
32
|
-
};
|
|
17
|
+
const lastMessage = messages.at(-1);
|
|
18
|
+
const lastUserText = lastMessage?.role === "user" ? extractText(lastMessage.content).trim() : "";
|
|
19
|
+
if (!lastUserText) throw new Error("OpenCode adapter requires a trailing user message with text content.");
|
|
20
|
+
if (sessionId !== void 0) return {
|
|
21
|
+
prompt: lastUserText,
|
|
22
|
+
resume: sessionId
|
|
23
|
+
};
|
|
24
|
+
const priorTurns = messages.slice(0, -1).filter((message) => (message.role === "user" || message.role === "assistant") && extractText(message.content).trim() !== "").map((message) => `${message.role === "user" ? "User" : "Assistant"}: ${extractText(message.content).trim()}`);
|
|
25
|
+
if (priorTurns.length === 0) return { prompt: lastUserText };
|
|
26
|
+
return { prompt: `Previous conversation:\n${priorTurns.join("\n")}\n\n${lastUserText}` };
|
|
33
27
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
//# sourceMappingURL=prompt.js.map
|
|
28
|
+
//#endregion
|
|
29
|
+
export { buildPrompt };
|
|
30
|
+
|
|
31
|
+
//# sourceMappingURL=prompt.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompt.js","sources":["../../../src/messages/prompt.ts"],"sourcesContent":["import type { ModelMessage } from '@tanstack/ai'\n\nexport interface BuiltPrompt {\n prompt: string\n /** OpenCode session id to resume, when the caller threaded one through. */\n resume?: string\n}\n\nfunction extractText(content: ModelMessage['content']): string {\n if (content === null) return ''\n if (typeof content === 'string') return content\n return content\n .map((part) =>\n part.type === 'text' && typeof part.content === 'string'\n ? part.content\n : '',\n )\n .join('')\n}\n\n/**\n * Convert TanStack chat history into the OpenCode prompt + resume inputs.\n *\n * With a `sessionId`, the harness already holds the conversation context, so\n * only the trailing user message is sent and the session is resumed. Without\n * one, prior turns are flattened into a plain-text transcript preamble (tool\n * messages and tool-call-only assistant turns are harness-internal noise and\n * are skipped; prompts are text-only in v1).\n */\nexport function buildPrompt(\n messages: Array<ModelMessage>,\n sessionId: string | undefined,\n): BuiltPrompt {\n const lastMessage = messages.at(-1)\n const lastUserText =\n lastMessage?.role === 'user' ? extractText(lastMessage.content).trim() : ''\n\n if (!lastUserText) {\n throw new Error(\n 'OpenCode adapter requires a trailing user message with text content.',\n )\n }\n\n if (sessionId !== undefined) {\n return { prompt: lastUserText, resume: sessionId }\n }\n\n const priorTurns = messages\n .slice(0, -1)\n .filter(\n (message) =>\n (message.role === 'user' || message.role === 'assistant') &&\n extractText(message.content).trim() !== '',\n )\n .map(\n (message) =>\n `${message.role === 'user' ? 'User' : 'Assistant'}: ${extractText(message.content).trim()}`,\n )\n\n if (priorTurns.length === 0) {\n return { prompt: lastUserText }\n }\n\n return {\n prompt: `Previous conversation:\\n${priorTurns.join('\\n')}\\n\\n${lastUserText}`,\n }\n}\n"],"
|
|
1
|
+
{"version":3,"file":"prompt.js","names":[],"sources":["../../../src/messages/prompt.ts"],"sourcesContent":["import type { ModelMessage } from '@tanstack/ai'\n\nexport interface BuiltPrompt {\n prompt: string\n /** OpenCode session id to resume, when the caller threaded one through. */\n resume?: string\n}\n\nfunction extractText(content: ModelMessage['content']): string {\n if (content === null) return ''\n if (typeof content === 'string') return content\n return content\n .map((part) =>\n part.type === 'text' && typeof part.content === 'string'\n ? part.content\n : '',\n )\n .join('')\n}\n\n/**\n * Convert TanStack chat history into the OpenCode prompt + resume inputs.\n *\n * With a `sessionId`, the harness already holds the conversation context, so\n * only the trailing user message is sent and the session is resumed. Without\n * one, prior turns are flattened into a plain-text transcript preamble (tool\n * messages and tool-call-only assistant turns are harness-internal noise and\n * are skipped; prompts are text-only in v1).\n */\nexport function buildPrompt(\n messages: Array<ModelMessage>,\n sessionId: string | undefined,\n): BuiltPrompt {\n const lastMessage = messages.at(-1)\n const lastUserText =\n lastMessage?.role === 'user' ? extractText(lastMessage.content).trim() : ''\n\n if (!lastUserText) {\n throw new Error(\n 'OpenCode adapter requires a trailing user message with text content.',\n )\n }\n\n if (sessionId !== undefined) {\n return { prompt: lastUserText, resume: sessionId }\n }\n\n const priorTurns = messages\n .slice(0, -1)\n .filter(\n (message) =>\n (message.role === 'user' || message.role === 'assistant') &&\n extractText(message.content).trim() !== '',\n )\n .map(\n (message) =>\n `${message.role === 'user' ? 'User' : 'Assistant'}: ${extractText(message.content).trim()}`,\n )\n\n if (priorTurns.length === 0) {\n return { prompt: lastUserText }\n }\n\n return {\n prompt: `Previous conversation:\\n${priorTurns.join('\\n')}\\n\\n${lastUserText}`,\n }\n}\n"],"mappings":";AAQA,SAAS,YAAY,SAA0C;CAC7D,IAAI,YAAY,MAAM,OAAO;CAC7B,IAAI,OAAO,YAAY,UAAU,OAAO;CACxC,OAAO,QACJ,KAAK,SACJ,KAAK,SAAS,UAAU,OAAO,KAAK,YAAY,WAC5C,KAAK,UACL,EACN,CAAC,CACA,KAAK,EAAE;AACZ;;;;;;;;;;AAWA,SAAgB,YACd,UACA,WACa;CACb,MAAM,cAAc,SAAS,GAAG,EAAE;CAClC,MAAM,eACJ,aAAa,SAAS,SAAS,YAAY,YAAY,OAAO,CAAC,CAAC,KAAK,IAAI;CAE3E,IAAI,CAAC,cACH,MAAM,IAAI,MACR,sEACF;CAGF,IAAI,cAAc,KAAA,GAChB,OAAO;EAAE,QAAQ;EAAc,QAAQ;CAAU;CAGnD,MAAM,aAAa,SAChB,MAAM,GAAG,EAAE,CAAC,CACZ,QACE,aACE,QAAQ,SAAS,UAAU,QAAQ,SAAS,gBAC7C,YAAY,QAAQ,OAAO,CAAC,CAAC,KAAK,MAAM,EAC5C,CAAC,CACA,KACE,YACC,GAAG,QAAQ,SAAS,SAAS,SAAS,YAAY,IAAI,YAAY,QAAQ,OAAO,CAAC,CAAC,KAAK,GAC5F;CAEF,IAAI,WAAW,WAAW,GACxB,OAAO,EAAE,QAAQ,aAAa;CAGhC,OAAO,EACL,QAAQ,2BAA2B,WAAW,KAAK,IAAI,EAAE,MAAM,eACjE;AACF"}
|