@mono-agent/agent-runtime 0.6.1 → 0.8.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.
- package/README.md +36 -16
- package/package.json +14 -7
- package/src/agent/approval.js +52 -17
- package/src/agent/sandbox-seam.js +1 -0
- package/src/agent/tools/pi-bridge.js +15 -42
- package/src/agent/tools/shared/ripgrep.js +12 -8
- package/src/ai/file-change-stats.js +0 -21
- package/src/ai/index.js +8 -0
- package/src/ai/providers/claude-cli.js +109 -5
- package/src/ai/providers/claude-sandbox.js +71 -0
- package/src/ai/providers/claude-sdk-discovery-worker.js +53 -0
- package/src/ai/providers/claude-sdk-discovery.js +352 -0
- package/src/ai/providers/claude-sdk.js +315 -163
- package/src/ai/providers/codex-app.js +823 -78
- package/src/ai/providers/opencode-app.js +682 -96
- package/src/ai/providers/opencode-server.js +508 -0
- package/src/ai/providers/pi-native/stream-subscriber.js +9 -0
- package/src/ai/runtime/capabilities.js +12 -0
- package/src/ai/runtime/context-windows.js +8 -0
- package/src/ai/runtime/registry.js +8 -2
- package/src/ai/runtime/router.js +627 -29
- package/src/ai/streaming/codex-events.js +7 -15
- package/src/ai/types.js +29 -2
- package/src/index.js +6 -0
- package/src/runtime.js +17 -1
- package/types/agent/approval.d.ts +4 -7
- package/types/agent/sandbox-seam.d.ts +5 -0
- package/types/ai/backend.d.ts +16 -0
- package/types/ai/file-change-stats.d.ts +0 -24
- package/types/ai/index.d.ts +1 -0
- package/types/ai/providers/claude-cli.d.ts +116 -0
- package/types/ai/providers/claude-sandbox.d.ts +79 -0
- package/types/ai/providers/claude-sdk-discovery-worker.d.ts +1 -0
- package/types/ai/providers/claude-sdk-discovery.d.ts +97 -0
- package/types/ai/providers/claude-sdk.d.ts +81 -5
- package/types/ai/providers/codex-app.d.ts +11 -7
- package/types/ai/providers/opencode-app.d.ts +15 -16
- package/types/ai/providers/opencode-server.d.ts +20 -0
- package/types/ai/runtime/capabilities.d.ts +19 -0
- package/types/ai/runtime/context-windows.d.ts +1 -0
- package/types/ai/runtime/router.d.ts +24 -23
- package/types/ai/streaming/codex-events.d.ts +15 -6
- package/types/ai/types.d.ts +75 -2
- package/types/index.d.ts +1 -0
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { estimateCost } from "../cost.js";
|
|
3
3
|
import { buildCapabilitiesUsed } from "../runtime/capabilities-used.js";
|
|
4
|
+
import { createApprovalManager, RISK_TIERS } from "../../agent/approval.js";
|
|
5
|
+
import { resolveSandboxPolicy } from "../../agent/tools/shared/tool-context.js";
|
|
6
|
+
import { createIsolatedOpencode } from "./opencode-server.js";
|
|
4
7
|
import {
|
|
5
8
|
toolUseEvent,
|
|
6
9
|
toolResultEvent,
|
|
@@ -10,15 +13,15 @@ import {
|
|
|
10
13
|
} from "../streaming/opencode-events.js";
|
|
11
14
|
|
|
12
15
|
// OpenCode agent backend (sdk='opencode', execution_mode='cli'). Drives the local
|
|
13
|
-
// `opencode` server via @opencode-ai/sdk and resolves provider credentials from
|
|
16
|
+
// `opencode` server via @opencode-ai/sdk/v2 and resolves provider credentials from
|
|
14
17
|
// OpenCode's own auth.json (Copilot / ChatGPT / Zen / 75+ providers). Structurally
|
|
15
18
|
// modeled on codex-app.js, but over the SDK's HTTP + event-stream surface.
|
|
16
19
|
//
|
|
17
|
-
// Capability notes (verified against @opencode-ai/sdk
|
|
20
|
+
// Capability notes (verified against @opencode-ai/sdk/v2):
|
|
18
21
|
// - `session.prompt` blocks until the turn is done and returns the final message.
|
|
19
|
-
// - The
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
+
// - The v2 prompt accepts a `format` field, but this bridge does not yet enforce
|
|
23
|
+
// structured output; the system prompt asks for JSON and the host can recover
|
|
24
|
+
// it (same as codex-app).
|
|
22
25
|
// - No mid-turn steering primitive and no native-subagent injection in this SDK
|
|
23
26
|
// revision, so supports_live_input / supports_native_subagents are false.
|
|
24
27
|
const OPENCODE_APP_CAPABILITIES = {
|
|
@@ -26,9 +29,9 @@ const OPENCODE_APP_CAPABILITIES = {
|
|
|
26
29
|
runtime: "app-server",
|
|
27
30
|
streaming: true,
|
|
28
31
|
structured_output: false,
|
|
29
|
-
supports_session_resume:
|
|
32
|
+
supports_session_resume: false,
|
|
30
33
|
native_runtime_config: null,
|
|
31
|
-
supports_mcp:
|
|
34
|
+
supports_mcp: false,
|
|
32
35
|
supports_skills: false,
|
|
33
36
|
supports_builtin_tools: true,
|
|
34
37
|
supports_live_input: false,
|
|
@@ -39,6 +42,35 @@ const OPENCODE_APP_CAPABILITIES = {
|
|
|
39
42
|
// How long to keep draining the event stream after session.prompt resolves, in
|
|
40
43
|
// case the terminal session.idle event lands just after the HTTP response.
|
|
41
44
|
const POST_PROMPT_DRAIN_MS = 1500;
|
|
45
|
+
const OPENCODE_ERROR_MESSAGE_MAX_CHARS = 1000;
|
|
46
|
+
const OPENCODE_PERMISSION_TOOL_NAMES = {
|
|
47
|
+
read: "Read",
|
|
48
|
+
edit: "Edit",
|
|
49
|
+
glob: "Glob",
|
|
50
|
+
grep: "Grep",
|
|
51
|
+
list: "List",
|
|
52
|
+
bash: "Bash",
|
|
53
|
+
task: "Task",
|
|
54
|
+
external_directory: "ExternalDirectory",
|
|
55
|
+
todowrite: "TodoWrite",
|
|
56
|
+
question: "Question",
|
|
57
|
+
webfetch: "WebFetch",
|
|
58
|
+
websearch: "WebSearch",
|
|
59
|
+
lsp: "LSP",
|
|
60
|
+
doom_loop: "DoomLoop",
|
|
61
|
+
skill: "Skill",
|
|
62
|
+
repo_clone: "RepoClone",
|
|
63
|
+
repo_overview: "RepoOverview",
|
|
64
|
+
plan_enter: "PlanEnter",
|
|
65
|
+
plan_exit: "PlanExit",
|
|
66
|
+
};
|
|
67
|
+
const OPENCODE_UNSUPPORTED_PERMISSIONS = new Set([
|
|
68
|
+
"question",
|
|
69
|
+
"task",
|
|
70
|
+
"plan_enter",
|
|
71
|
+
"plan_exit",
|
|
72
|
+
]);
|
|
73
|
+
const OPENCODE_RUN_AGENT = "mono-agent-run";
|
|
42
74
|
|
|
43
75
|
function delay(ms) {
|
|
44
76
|
return new Promise((resolve) => {
|
|
@@ -59,14 +91,195 @@ function promptFromMessages(messages) {
|
|
|
59
91
|
: String(messages || "");
|
|
60
92
|
}
|
|
61
93
|
|
|
94
|
+
function normalizePermissionMode(permissionMode) {
|
|
95
|
+
return ["plan", "acceptEdits", "bypassPermissions"].includes(permissionMode)
|
|
96
|
+
? permissionMode
|
|
97
|
+
: "default";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function opencodePermissionConfig(permissionMode) {
|
|
101
|
+
const mode = normalizePermissionMode(permissionMode);
|
|
102
|
+
const unsupported = {
|
|
103
|
+
question: "deny",
|
|
104
|
+
task: "deny",
|
|
105
|
+
plan_enter: "deny",
|
|
106
|
+
plan_exit: "deny",
|
|
107
|
+
};
|
|
108
|
+
if (mode === "plan") {
|
|
109
|
+
return {
|
|
110
|
+
"*": "deny",
|
|
111
|
+
read: {
|
|
112
|
+
"*": "allow",
|
|
113
|
+
"*.env": "deny",
|
|
114
|
+
"*.env.*": "deny",
|
|
115
|
+
"*.env.example": "allow",
|
|
116
|
+
},
|
|
117
|
+
...unsupported,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (mode === "bypassPermissions") {
|
|
121
|
+
return { "*": "allow", ...unsupported };
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
"*": "ask",
|
|
125
|
+
...unsupported,
|
|
126
|
+
...(mode === "acceptEdits" ? { edit: "allow" } : {}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function opencodeRunAgent(permission) {
|
|
131
|
+
return {
|
|
132
|
+
name: `${OPENCODE_RUN_AGENT}-${randomUUID()}`,
|
|
133
|
+
config: {
|
|
134
|
+
description: "mono-agent isolated run",
|
|
135
|
+
mode: "primary",
|
|
136
|
+
permission,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function opencodePermissionToolName(permissionType) {
|
|
142
|
+
const raw = typeof permissionType === "string" && permissionType.length > 0
|
|
143
|
+
? permissionType
|
|
144
|
+
: "unknown_permission";
|
|
145
|
+
return OPENCODE_PERMISSION_TOOL_NAMES[raw] || raw;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function normalizeApprovalToolName(toolName) {
|
|
149
|
+
if (typeof toolName !== "string" || toolName.trim().length === 0) return null;
|
|
150
|
+
const normalized = toolName.trim();
|
|
151
|
+
const rawPermissionType = Object.keys(OPENCODE_PERMISSION_TOOL_NAMES)
|
|
152
|
+
.find((key) => key.toLowerCase() === normalized.toLowerCase());
|
|
153
|
+
if (rawPermissionType !== undefined) return OPENCODE_PERMISSION_TOOL_NAMES[rawPermissionType];
|
|
154
|
+
const canonical = Object.values(OPENCODE_PERMISSION_TOOL_NAMES)
|
|
155
|
+
.find((name) => name.toLowerCase() === normalized.toLowerCase());
|
|
156
|
+
return canonical || normalized;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function opencodeAlwaysAllowTools(value) {
|
|
160
|
+
if (!Array.isArray(value)) return [];
|
|
161
|
+
return [...new Set(value.map(normalizeApprovalToolName).filter(Boolean))];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function opencodeRiskTiersByTool(value) {
|
|
165
|
+
const source = value && typeof value === "object" ? value : {};
|
|
166
|
+
const normalized = { ...source };
|
|
167
|
+
for (const [permissionType, toolName] of Object.entries(OPENCODE_PERMISSION_TOOL_NAMES)) {
|
|
168
|
+
if (normalized[toolName] === undefined && RISK_TIERS.includes(source[permissionType])) {
|
|
169
|
+
normalized[toolName] = source[permissionType];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return normalized;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function opencodeRiskTier(options, permissionType, toolName) {
|
|
176
|
+
const configured = options.toolRiskTiers?.[toolName]
|
|
177
|
+
|| options.toolRiskTiers?.[permissionType]
|
|
178
|
+
|| options.approvalDefaultRiskTier;
|
|
179
|
+
return RISK_TIERS.includes(configured) ? configured : "medium";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function strictOpenCodeApprovalCallback(callback) {
|
|
183
|
+
return async (payload) => {
|
|
184
|
+
const response = await callback(payload);
|
|
185
|
+
if (response?.decision === "approve" || response?.decision === "always" || response?.decision === "deny") {
|
|
186
|
+
return response;
|
|
187
|
+
}
|
|
188
|
+
return { decision: "deny", reason: "invalid_host_response" };
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function emitForcedApproval(emit, perm, toolName, riskTier, decision, reason) {
|
|
193
|
+
emit({
|
|
194
|
+
type: decision === "deny" ? "tool_approval_denied" : "tool_approval_granted",
|
|
195
|
+
requestId: typeof perm.id === "string" && perm.id.trim().length > 0 ? perm.id : null,
|
|
196
|
+
toolName,
|
|
197
|
+
toolUseId: perm.tool?.callID || null,
|
|
198
|
+
decision,
|
|
199
|
+
reason,
|
|
200
|
+
riskTier,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function opencodePermissionDecision(perm, context) {
|
|
205
|
+
const {
|
|
206
|
+
options,
|
|
207
|
+
reference,
|
|
208
|
+
emit,
|
|
209
|
+
approvalManager,
|
|
210
|
+
alwaysAllowTools,
|
|
211
|
+
} = context;
|
|
212
|
+
const permissionMode = normalizePermissionMode(options.permissionMode);
|
|
213
|
+
const permissionType = typeof perm.permission === "string" ? perm.permission : "unknown_permission";
|
|
214
|
+
const toolName = opencodePermissionToolName(permissionType);
|
|
215
|
+
const riskTier = approvalManager?.riskTierFor(toolName)
|
|
216
|
+
|| opencodeRiskTier(options, permissionType, toolName);
|
|
217
|
+
|
|
218
|
+
if (permissionMode === "plan") {
|
|
219
|
+
emitForcedApproval(emit, perm, toolName, riskTier, "deny", "permission_mode_plan");
|
|
220
|
+
return "reject";
|
|
221
|
+
}
|
|
222
|
+
if (OPENCODE_UNSUPPORTED_PERMISSIONS.has(permissionType)) {
|
|
223
|
+
emitForcedApproval(emit, perm, toolName, riskTier, "deny", "unsupported_permission_type");
|
|
224
|
+
return "reject";
|
|
225
|
+
}
|
|
226
|
+
if (permissionMode === "bypassPermissions") {
|
|
227
|
+
emitForcedApproval(emit, perm, toolName, riskTier, "always", "permission_mode_bypass");
|
|
228
|
+
return "once";
|
|
229
|
+
}
|
|
230
|
+
if (permissionMode === "acceptEdits" && permissionType === "edit") {
|
|
231
|
+
emitForcedApproval(emit, perm, toolName, riskTier, "always", "permission_mode_accept_edits");
|
|
232
|
+
return "once";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (approvalManager === null) {
|
|
236
|
+
if (alwaysAllowTools.has(toolName)) {
|
|
237
|
+
emitForcedApproval(emit, perm, toolName, riskTier, "always", "session_allowed");
|
|
238
|
+
return "once";
|
|
239
|
+
}
|
|
240
|
+
emitForcedApproval(emit, perm, toolName, riskTier, "deny", "no_host_callback");
|
|
241
|
+
return "reject";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const verdict = await approvalManager.request({
|
|
245
|
+
requestId: perm.id,
|
|
246
|
+
toolName,
|
|
247
|
+
toolUseId: perm.tool?.callID || null,
|
|
248
|
+
input: {
|
|
249
|
+
patterns: Array.isArray(perm.patterns) ? perm.patterns : [],
|
|
250
|
+
metadata: perm.metadata || {},
|
|
251
|
+
},
|
|
252
|
+
model: reference,
|
|
253
|
+
});
|
|
254
|
+
if (verdict.decision === "deny") return "reject";
|
|
255
|
+
// OpenCode's `always` reply persists a project-wide approval. Mono-agent keeps
|
|
256
|
+
// `always` decisions inside this run's ApprovalManager and sends only `once`
|
|
257
|
+
// to the isolated provider server so no user project policy is mutated.
|
|
258
|
+
if (verdict.decision === "always" || approvalManager.isAlwaysAllowed(toolName)) return "once";
|
|
259
|
+
return "once";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function opencodePermissionReplyError(code, permissionId = null) {
|
|
263
|
+
return Object.assign(
|
|
264
|
+
new Error(code === "opencode_permission_invalid"
|
|
265
|
+
? "OpenCode emitted an invalid permission request."
|
|
266
|
+
: "OpenCode could not accept the permission decision; the turn was aborted."),
|
|
267
|
+
{
|
|
268
|
+
opencodeFailureKind: "tool_failure",
|
|
269
|
+
opencodeErrorCode: code,
|
|
270
|
+
opencodePermissionId: typeof permissionId === "string" && permissionId.length > 0
|
|
271
|
+
? permissionId
|
|
272
|
+
: null,
|
|
273
|
+
},
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
62
277
|
// hey-api RequestResult resolves to { data, error }. Surface errors as throws so
|
|
63
278
|
// the caller's try/catch maps them to a failure kind.
|
|
64
279
|
function unwrap(result) {
|
|
65
280
|
if (result && typeof result === "object" && ("data" in result || "error" in result)) {
|
|
66
281
|
if (result.error) {
|
|
67
|
-
const message =
|
|
68
|
-
? result.error
|
|
69
|
-
: (result.error?.message || JSON.stringify(result.error));
|
|
282
|
+
const message = safeOpenCodeErrorMessage(result.error, "OpenCode request failed.");
|
|
70
283
|
throw Object.assign(new Error(message), { opencodeError: result.error });
|
|
71
284
|
}
|
|
72
285
|
return result.data;
|
|
@@ -74,27 +287,25 @@ function unwrap(result) {
|
|
|
74
287
|
return result;
|
|
75
288
|
}
|
|
76
289
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
...(cfg.headers && typeof cfg.headers === "object" ? { headers: cfg.headers } : {}),
|
|
93
|
-
enabled: true,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
290
|
+
/**
|
|
291
|
+
* Extract only SDK-declared human-readable fields. In particular, never
|
|
292
|
+
* stringify API error objects: `responseBody`, response headers, and metadata
|
|
293
|
+
* can contain provider credentials or echoed request secrets.
|
|
294
|
+
*/
|
|
295
|
+
export function safeOpenCodeErrorMessage(error, fallback = "OpenCode request failed.") {
|
|
296
|
+
let candidate;
|
|
297
|
+
try {
|
|
298
|
+
candidate = typeof error?.message === "string" && error.message.trim().length > 0
|
|
299
|
+
? error.message
|
|
300
|
+
: typeof error?.data?.message === "string" && error.data.message.trim().length > 0
|
|
301
|
+
? error.data.message
|
|
302
|
+
: undefined;
|
|
303
|
+
} catch {
|
|
304
|
+
candidate = undefined;
|
|
96
305
|
}
|
|
97
|
-
|
|
306
|
+
const text = (candidate || fallback).trim();
|
|
307
|
+
if (text.length <= OPENCODE_ERROR_MESSAGE_MAX_CHARS) return text;
|
|
308
|
+
return `${text.slice(0, OPENCODE_ERROR_MESSAGE_MAX_CHARS - 1)}…`;
|
|
98
309
|
}
|
|
99
310
|
|
|
100
311
|
function usageFromInfo(info) {
|
|
@@ -108,6 +319,39 @@ function usageFromInfo(info) {
|
|
|
108
319
|
};
|
|
109
320
|
}
|
|
110
321
|
|
|
322
|
+
function aggregateAssistantInfos(infos) {
|
|
323
|
+
const entries = [...infos];
|
|
324
|
+
const totals = {
|
|
325
|
+
input_tokens: 0,
|
|
326
|
+
output_tokens: 0,
|
|
327
|
+
cache_read_tokens: 0,
|
|
328
|
+
cache_creation_tokens: 0,
|
|
329
|
+
};
|
|
330
|
+
const seenUsage = new Set();
|
|
331
|
+
let reportedCost = 0;
|
|
332
|
+
let hasReportedCost = false;
|
|
333
|
+
for (const info of entries) {
|
|
334
|
+
const next = usageFromInfo(info);
|
|
335
|
+
for (const key of Object.keys(totals)) {
|
|
336
|
+
if (Number.isFinite(next[key])) {
|
|
337
|
+
totals[key] += next[key];
|
|
338
|
+
seenUsage.add(key);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const cost = num(info?.cost);
|
|
342
|
+
if (cost !== null) {
|
|
343
|
+
reportedCost += cost;
|
|
344
|
+
hasReportedCost = true;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
usage: entries.length === 0
|
|
349
|
+
? null
|
|
350
|
+
: Object.fromEntries(Object.keys(totals).map((key) => [key, seenUsage.has(key) ? totals[key] : null])),
|
|
351
|
+
reportedCost: hasReportedCost ? reportedCost : null,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
111
355
|
function finalTextFromParts(parts) {
|
|
112
356
|
const text = (Array.isArray(parts) ? parts : [])
|
|
113
357
|
.filter((p) => p?.type === "text")
|
|
@@ -120,7 +364,8 @@ function finalTextFromParts(parts) {
|
|
|
120
364
|
export function mapErrorFailureKind(error) {
|
|
121
365
|
const name = error?.name || error?.data?.name || "";
|
|
122
366
|
if (name === "MessageAbortedError") return "cancelled";
|
|
123
|
-
if (name === "MessageOutputLengthError") return "usage_limit";
|
|
367
|
+
if (name === "MessageOutputLengthError" || name === "ContextOverflowError") return "usage_limit";
|
|
368
|
+
if (name === "ProviderAuthError") return "provider_auth";
|
|
124
369
|
return "provider_unavailable";
|
|
125
370
|
}
|
|
126
371
|
|
|
@@ -130,6 +375,53 @@ export function mapSpawnFailureKind(err) {
|
|
|
130
375
|
return "provider_unavailable";
|
|
131
376
|
}
|
|
132
377
|
|
|
378
|
+
function opencodeToolPolicyProblem(options) {
|
|
379
|
+
const allowedTools = Array.isArray(options.allowedTools) ? options.allowedTools : null;
|
|
380
|
+
const disallowedTools = Array.isArray(options.disallowedTools) ? options.disallowedTools : [];
|
|
381
|
+
const exactAllowAll = allowedTools === null
|
|
382
|
+
|| (allowedTools.length === 1 && allowedTools[0] === "*");
|
|
383
|
+
return exactAllowAll && disallowedTools.length === 0
|
|
384
|
+
? null
|
|
385
|
+
: "Direct OpenCode cannot enforce allowedTools/disallowedTools. Use exact allow-all ([\"*\"] with no disallowedTools) or a Pi runtime (including pi:opencode-go:*).";
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function opencodeCapabilityMismatchResult({
|
|
389
|
+
reference,
|
|
390
|
+
outputSchema,
|
|
391
|
+
start,
|
|
392
|
+
error,
|
|
393
|
+
code,
|
|
394
|
+
}) {
|
|
395
|
+
return {
|
|
396
|
+
text: null,
|
|
397
|
+
structuredResult: undefined,
|
|
398
|
+
structuredResultSource: null,
|
|
399
|
+
events: [],
|
|
400
|
+
usage: {},
|
|
401
|
+
durationMs: Date.now() - start,
|
|
402
|
+
numTurns: 0,
|
|
403
|
+
model: reference,
|
|
404
|
+
effort: null,
|
|
405
|
+
sdk: "opencode",
|
|
406
|
+
providerSessionId: null,
|
|
407
|
+
provider_session_id: null,
|
|
408
|
+
cancelled: false,
|
|
409
|
+
error,
|
|
410
|
+
failureKind: "skipped_capability_mismatch",
|
|
411
|
+
diagnostics: { opencode_error_code: code },
|
|
412
|
+
capabilitiesUsed: buildCapabilitiesUsed({
|
|
413
|
+
promptCacheActive: null,
|
|
414
|
+
thinkingEnabled: null,
|
|
415
|
+
structuredOutputEnforced: !!outputSchema,
|
|
416
|
+
subagentInvoked: null,
|
|
417
|
+
mcpServersUsed: [],
|
|
418
|
+
nativeSubagentsUsed: [],
|
|
419
|
+
toolCompactionApplied: false,
|
|
420
|
+
contextCompactionApplied: null,
|
|
421
|
+
}),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
133
425
|
async function generateOpencodeAppResponse(systemPrompt, options = {}) {
|
|
134
426
|
const start = Date.now();
|
|
135
427
|
const resolved = options.model?.sdk
|
|
@@ -138,6 +430,115 @@ async function generateOpencodeAppResponse(systemPrompt, options = {}) {
|
|
|
138
430
|
const providerID = resolved.provider;
|
|
139
431
|
const modelID = resolved.model;
|
|
140
432
|
const reference = resolved.reference || `opencode:${providerID}:${modelID}`;
|
|
433
|
+
const requestedSessionId = (typeof options.providerSessionId === "string" && options.providerSessionId)
|
|
434
|
+
|| (typeof options.sessionId === "string" && options.sessionId)
|
|
435
|
+
|| null;
|
|
436
|
+
|
|
437
|
+
if (requestedSessionId !== null) {
|
|
438
|
+
return opencodeCapabilityMismatchResult({
|
|
439
|
+
reference,
|
|
440
|
+
outputSchema: options.outputSchema,
|
|
441
|
+
start,
|
|
442
|
+
error: "Direct OpenCode session resume is disabled because each run uses isolated provider state. Start a fresh run or use pi:opencode-go:* for resumable sessions.",
|
|
443
|
+
code: "opencode_session_resume_unsupported",
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (
|
|
448
|
+
options.outputSchema !== undefined
|
|
449
|
+
&& options.outputSchema !== null
|
|
450
|
+
) {
|
|
451
|
+
return opencodeCapabilityMismatchResult({
|
|
452
|
+
reference,
|
|
453
|
+
outputSchema: options.outputSchema,
|
|
454
|
+
start,
|
|
455
|
+
error: "Direct OpenCode cannot enforce structured output for outputSchema. Remove outputSchema or use a runtime that advertises structured_output.",
|
|
456
|
+
code: "opencode_structured_output_unsupported",
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
if (Array.isArray(options.nativeSubagents?.teammates) && options.nativeSubagents.teammates.length > 0) {
|
|
460
|
+
return opencodeCapabilityMismatchResult({
|
|
461
|
+
reference,
|
|
462
|
+
outputSchema: options.outputSchema,
|
|
463
|
+
start,
|
|
464
|
+
error: "Direct OpenCode does not expose native subagents through this bridge. Remove nativeSubagents or use a capable runtime.",
|
|
465
|
+
code: "opencode_native_subagents_unsupported",
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
if (options.liveInput) {
|
|
469
|
+
return opencodeCapabilityMismatchResult({
|
|
470
|
+
reference,
|
|
471
|
+
outputSchema: options.outputSchema,
|
|
472
|
+
start,
|
|
473
|
+
error: "Direct OpenCode does not support live input through this bridge. Remove liveInput or use a capable runtime.",
|
|
474
|
+
code: "opencode_live_input_unsupported",
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
if (options.fastMode === true) {
|
|
478
|
+
return opencodeCapabilityMismatchResult({
|
|
479
|
+
reference,
|
|
480
|
+
outputSchema: options.outputSchema,
|
|
481
|
+
start,
|
|
482
|
+
error: "Direct OpenCode does not support fastMode through this bridge. Disable fastMode or use a capable runtime.",
|
|
483
|
+
code: "opencode_fast_mode_unsupported",
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
if (Array.isArray(options.skills) && options.skills.length > 0) {
|
|
487
|
+
return opencodeCapabilityMismatchResult({
|
|
488
|
+
reference,
|
|
489
|
+
outputSchema: options.outputSchema,
|
|
490
|
+
start,
|
|
491
|
+
error: "Direct OpenCode cannot expose runtime skill metadata because external skills are disabled. Use full prompt disclosure, remove skills, or use a Pi runtime.",
|
|
492
|
+
code: "opencode_skills_unsupported",
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (resolveSandboxPolicy(options.toolContext, options.sandboxPolicy) !== undefined) {
|
|
497
|
+
return opencodeCapabilityMismatchResult({
|
|
498
|
+
reference,
|
|
499
|
+
outputSchema: options.outputSchema,
|
|
500
|
+
start,
|
|
501
|
+
error: "Direct OpenCode cannot enforce mono-agent's native srt sandbox scopes. Remove the mono-agent sandbox policy or use a Pi runtime (including pi:opencode-go:*) for exact readableRoots, writableRoots, denyWrite, and network rules.",
|
|
502
|
+
code: "opencode_sandbox_policy_unsupported",
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
const toolPolicyProblem = opencodeToolPolicyProblem(options);
|
|
506
|
+
if (toolPolicyProblem !== null) {
|
|
507
|
+
return opencodeCapabilityMismatchResult({
|
|
508
|
+
reference,
|
|
509
|
+
outputSchema: options.outputSchema,
|
|
510
|
+
start,
|
|
511
|
+
error: toolPolicyProblem,
|
|
512
|
+
code: "opencode_tool_policy_unsupported",
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
if (Object.keys(options.mcpServers || {}).length > 0) {
|
|
516
|
+
return opencodeCapabilityMismatchResult({
|
|
517
|
+
reference,
|
|
518
|
+
outputSchema: options.outputSchema,
|
|
519
|
+
start,
|
|
520
|
+
error: "Direct OpenCode cannot safely inject MCP configuration because provider-owned shell tools inherit the server environment. Remove mcpServers or use a Pi runtime (including pi:opencode-go:*).",
|
|
521
|
+
code: "opencode_mcp_unsupported",
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
if (typeof options.effort === "string" && options.effort.trim().length > 0) {
|
|
525
|
+
return opencodeCapabilityMismatchResult({
|
|
526
|
+
reference,
|
|
527
|
+
outputSchema: options.outputSchema,
|
|
528
|
+
start,
|
|
529
|
+
error: "Direct OpenCode has no reasoning-effort input, so runtime.effort cannot be enforced. Remove the effort setting or use a runtime that supports it.",
|
|
530
|
+
code: "opencode_effort_unsupported",
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
if (Number.isFinite(Number(options.maxTurns)) && Number(options.maxTurns) > 0) {
|
|
534
|
+
return opencodeCapabilityMismatchResult({
|
|
535
|
+
reference,
|
|
536
|
+
outputSchema: options.outputSchema,
|
|
537
|
+
start,
|
|
538
|
+
error: "Direct OpenCode has no enforceable hard turn cap, so runtime.maxTurns cannot be enforced. Omit maxTurns, set it to 0, or use a runtime that supports a hard cap.",
|
|
539
|
+
code: "opencode_max_turns_unsupported",
|
|
540
|
+
});
|
|
541
|
+
}
|
|
141
542
|
|
|
142
543
|
const events = [];
|
|
143
544
|
const emit = (event) => {
|
|
@@ -146,26 +547,71 @@ async function generateOpencodeAppResponse(systemPrompt, options = {}) {
|
|
|
146
547
|
try { options.onEvent?.(event); } catch { /* listener errors must not abort the run */ }
|
|
147
548
|
};
|
|
148
549
|
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
550
|
+
const permission = opencodePermissionConfig(options.permissionMode);
|
|
551
|
+
const alwaysAllowToolNames = opencodeAlwaysAllowTools(options.approvalAlwaysAllowTools);
|
|
552
|
+
const alwaysAllowTools = new Set(alwaysAllowToolNames);
|
|
553
|
+
const approvalManager = typeof options.onToolApprovalRequest === "function"
|
|
554
|
+
? createApprovalManager({
|
|
555
|
+
onToolApprovalRequest: strictOpenCodeApprovalCallback(options.onToolApprovalRequest),
|
|
556
|
+
defaultRiskTier: options.approvalDefaultRiskTier,
|
|
557
|
+
timeoutMs: options.approvalTimeoutMs,
|
|
558
|
+
onEvent: emit,
|
|
559
|
+
riskTiersByTool: opencodeRiskTiersByTool(options.toolRiskTiers),
|
|
560
|
+
alwaysAllowTools: alwaysAllowToolNames,
|
|
561
|
+
// An OpenCode permission.asked event is an explicit ask. Even a low-risk
|
|
562
|
+
// tier must receive a strict host answer rather than silently widening it.
|
|
563
|
+
autoApproveLowRisk: false,
|
|
564
|
+
})
|
|
565
|
+
: null;
|
|
566
|
+
const runAgent = opencodeRunAgent(permission);
|
|
567
|
+
const directoryParams = typeof options.cwd === "string" && options.cwd.length > 0
|
|
568
|
+
? { directory: options.cwd }
|
|
569
|
+
: {};
|
|
570
|
+
let sessionId = requestedSessionId;
|
|
153
571
|
|
|
154
572
|
let client = null;
|
|
155
573
|
let server = null;
|
|
156
574
|
let usage = null;
|
|
157
575
|
let errorMessage = null;
|
|
158
576
|
let failureKind = null;
|
|
577
|
+
let permissionErrorCode = null;
|
|
578
|
+
let permissionErrorId = null;
|
|
159
579
|
let finalText = null;
|
|
160
580
|
const seenToolUse = new Set();
|
|
581
|
+
const seenToolResult = new Set();
|
|
582
|
+
// v2 sends text/reasoning content as `message.part.delta` events that only
|
|
583
|
+
// identify the part by ID. Remember the owning run + part type from the
|
|
584
|
+
// preceding `message.part.updated` event before accepting any delta.
|
|
585
|
+
const streamParts = new Map();
|
|
586
|
+
const textDeltaPartIds = new Set();
|
|
587
|
+
const reasoningDeltaPartIds = new Set();
|
|
588
|
+
const assistantInfos = new Map();
|
|
589
|
+
const recordAssistantInfo = (info, fallbackKey) => {
|
|
590
|
+
if (info?.role !== "assistant") return;
|
|
591
|
+
const key = typeof info.id === "string" && info.id.length > 0 ? info.id : fallbackKey;
|
|
592
|
+
assistantInfos.set(key, info);
|
|
593
|
+
usage = aggregateAssistantInfos(assistantInfos.values()).usage;
|
|
594
|
+
};
|
|
161
595
|
|
|
162
596
|
const abortHandler = () => {
|
|
163
|
-
try {
|
|
597
|
+
try {
|
|
598
|
+
const abortResult = sessionId
|
|
599
|
+
? client?.session?.abort?.({ sessionID: sessionId, ...directoryParams })
|
|
600
|
+
: undefined;
|
|
601
|
+
if (abortResult !== undefined) {
|
|
602
|
+
void Promise.resolve(abortResult).catch(() => undefined);
|
|
603
|
+
}
|
|
604
|
+
} catch { /* best effort */ }
|
|
164
605
|
};
|
|
165
606
|
|
|
166
607
|
try {
|
|
167
|
-
const opencode = await
|
|
168
|
-
|
|
608
|
+
const opencode = await createIsolatedOpencode(/** @type {any} */ ({
|
|
609
|
+
hostname: "127.0.0.1",
|
|
610
|
+
port: 0,
|
|
611
|
+
config: {
|
|
612
|
+
permission,
|
|
613
|
+
agent: { [runAgent.name]: runAgent.config },
|
|
614
|
+
},
|
|
169
615
|
...(options.abortSignal ? { signal: options.abortSignal } : {}),
|
|
170
616
|
}));
|
|
171
617
|
client = opencode.client;
|
|
@@ -173,38 +619,86 @@ async function generateOpencodeAppResponse(systemPrompt, options = {}) {
|
|
|
173
619
|
options.abortSignal?.addEventListener?.("abort", abortHandler, { once: true });
|
|
174
620
|
|
|
175
621
|
if (!sessionId) {
|
|
176
|
-
const created = unwrap(await client.session.create(
|
|
622
|
+
const created = unwrap(await client.session.create(directoryParams));
|
|
177
623
|
sessionId = created?.id;
|
|
178
624
|
if (!sessionId) throw new Error("opencode did not return a session id");
|
|
179
625
|
}
|
|
180
626
|
|
|
181
627
|
let pumpDone = false;
|
|
628
|
+
let rejectPermissionFailure;
|
|
629
|
+
const permissionFailure = new Promise((_, reject) => {
|
|
630
|
+
rejectPermissionFailure = reject;
|
|
631
|
+
});
|
|
632
|
+
// The prompt race below observes this rejection. Keep a second handler so a
|
|
633
|
+
// very fast prompt resolution cannot turn a later permission failure into
|
|
634
|
+
// an unhandled rejection while the event pump drains.
|
|
635
|
+
void permissionFailure.catch(() => undefined);
|
|
636
|
+
|
|
637
|
+
const failPermissionReply = (error) => {
|
|
638
|
+
if (permissionErrorCode !== null) return;
|
|
639
|
+
permissionErrorCode = error.opencodeErrorCode || "opencode_permission_reply_failed";
|
|
640
|
+
permissionErrorId = error.opencodePermissionId || null;
|
|
641
|
+
errorMessage = error.message;
|
|
642
|
+
failureKind = error.opencodeFailureKind || "tool_failure";
|
|
643
|
+
pumpDone = true;
|
|
644
|
+
rejectPermissionFailure(error);
|
|
645
|
+
try {
|
|
646
|
+
const abortResult = client.session.abort({
|
|
647
|
+
sessionID: sessionId,
|
|
648
|
+
...directoryParams,
|
|
649
|
+
});
|
|
650
|
+
void Promise.resolve(abortResult).catch(() => undefined);
|
|
651
|
+
} catch { /* the raced failure already terminates the run */ }
|
|
652
|
+
};
|
|
182
653
|
|
|
183
654
|
const respondToPermission = async (perm) => {
|
|
184
655
|
if (perm.sessionID && perm.sessionID !== sessionId) return;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
} catch {
|
|
199
|
-
decision = "reject";
|
|
200
|
-
}
|
|
656
|
+
if (typeof perm.id !== "string" || perm.id.trim().length === 0) {
|
|
657
|
+
const permissionType = typeof perm.permission === "string" ? perm.permission : "unknown_permission";
|
|
658
|
+
const toolName = opencodePermissionToolName(permissionType);
|
|
659
|
+
emitForcedApproval(
|
|
660
|
+
emit,
|
|
661
|
+
perm,
|
|
662
|
+
toolName,
|
|
663
|
+
approvalManager?.riskTierFor(toolName) || opencodeRiskTier(options, permissionType, toolName),
|
|
664
|
+
"deny",
|
|
665
|
+
"invalid_permission_id",
|
|
666
|
+
);
|
|
667
|
+
failPermissionReply(opencodePermissionReplyError("opencode_permission_invalid"));
|
|
668
|
+
return;
|
|
201
669
|
}
|
|
670
|
+
if (typeof perm.permission !== "string" || perm.permission.trim().length === 0) {
|
|
671
|
+
const toolName = opencodePermissionToolName(perm.permission);
|
|
672
|
+
emitForcedApproval(
|
|
673
|
+
emit,
|
|
674
|
+
perm,
|
|
675
|
+
toolName,
|
|
676
|
+
approvalManager?.riskTierFor(toolName) || opencodeRiskTier(options, perm.permission, toolName),
|
|
677
|
+
"deny",
|
|
678
|
+
"invalid_permission_type",
|
|
679
|
+
);
|
|
680
|
+
failPermissionReply(opencodePermissionReplyError("opencode_permission_invalid", perm.id));
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const decision = await opencodePermissionDecision(perm, {
|
|
684
|
+
options,
|
|
685
|
+
reference,
|
|
686
|
+
emit,
|
|
687
|
+
approvalManager,
|
|
688
|
+
alwaysAllowTools,
|
|
689
|
+
});
|
|
202
690
|
try {
|
|
203
|
-
await client.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
691
|
+
const processed = unwrap(await client.permission.reply({
|
|
692
|
+
requestID: perm.id,
|
|
693
|
+
reply: decision,
|
|
694
|
+
...directoryParams,
|
|
695
|
+
}));
|
|
696
|
+
if (processed !== true) {
|
|
697
|
+
throw opencodePermissionReplyError("opencode_permission_reply_failed", perm.id);
|
|
698
|
+
}
|
|
699
|
+
} catch {
|
|
700
|
+
failPermissionReply(opencodePermissionReplyError("opencode_permission_reply_failed", perm.id));
|
|
701
|
+
}
|
|
208
702
|
};
|
|
209
703
|
|
|
210
704
|
const handleEvent = async (event) => {
|
|
@@ -213,29 +707,62 @@ async function generateOpencodeAppResponse(systemPrompt, options = {}) {
|
|
|
213
707
|
switch (event.type) {
|
|
214
708
|
case "message.part.updated": {
|
|
215
709
|
const part = props.part;
|
|
216
|
-
if (
|
|
710
|
+
if (
|
|
711
|
+
!part
|
|
712
|
+
|| (props.sessionID && props.sessionID !== sessionId)
|
|
713
|
+
|| (part.sessionID && part.sessionID !== sessionId)
|
|
714
|
+
) return;
|
|
715
|
+
if (typeof part.id === "string" && part.id.length > 0) {
|
|
716
|
+
streamParts.set(part.id, {
|
|
717
|
+
type: part.type,
|
|
718
|
+
sessionID: part.sessionID || props.sessionID || sessionId,
|
|
719
|
+
...(part.type === "reasoning" && typeof part.text === "string"
|
|
720
|
+
? { latestText: part.text }
|
|
721
|
+
: {}),
|
|
722
|
+
});
|
|
723
|
+
}
|
|
217
724
|
if (part.type === "tool") {
|
|
218
725
|
if (!seenToolUse.has(part.callID)) {
|
|
219
726
|
seenToolUse.add(part.callID);
|
|
220
727
|
emit(toolUseEvent(part));
|
|
221
728
|
}
|
|
222
|
-
if (toolPartSettled(part)
|
|
223
|
-
|
|
224
|
-
|
|
729
|
+
if (toolPartSettled(part) && !seenToolResult.has(part.callID)) {
|
|
730
|
+
seenToolResult.add(part.callID);
|
|
731
|
+
emit(toolResultEvent(part));
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
case "message.part.delta": {
|
|
737
|
+
if (
|
|
738
|
+
props.sessionID !== sessionId
|
|
739
|
+
|| props.field !== "text"
|
|
740
|
+
|| typeof props.partID !== "string"
|
|
741
|
+
|| typeof props.delta !== "string"
|
|
742
|
+
|| props.delta.length === 0
|
|
743
|
+
) return;
|
|
744
|
+
const tracked = streamParts.get(props.partID);
|
|
745
|
+
if (!tracked || tracked.sessionID !== sessionId) return;
|
|
746
|
+
if (tracked.type === "text") {
|
|
747
|
+
textDeltaPartIds.add(props.partID);
|
|
748
|
+
emit(assistantTextEvent(props.delta));
|
|
749
|
+
} else if (tracked.type === "reasoning") {
|
|
750
|
+
reasoningDeltaPartIds.add(props.partID);
|
|
751
|
+
emit(thinkingEvent({ text: props.delta }));
|
|
225
752
|
}
|
|
226
753
|
return;
|
|
227
754
|
}
|
|
228
755
|
case "message.updated": {
|
|
229
756
|
const info = props.info;
|
|
230
|
-
|
|
757
|
+
recordAssistantInfo(info, "event-anonymous");
|
|
231
758
|
return;
|
|
232
759
|
}
|
|
233
|
-
case "permission.
|
|
760
|
+
case "permission.asked":
|
|
234
761
|
await respondToPermission(props);
|
|
235
762
|
return;
|
|
236
763
|
case "session.error":
|
|
237
764
|
if (props.sessionID && props.sessionID !== sessionId) return;
|
|
238
|
-
errorMessage = props.error
|
|
765
|
+
errorMessage = safeOpenCodeErrorMessage(props.error, "OpenCode session error.");
|
|
239
766
|
failureKind = mapErrorFailureKind(props.error);
|
|
240
767
|
pumpDone = true;
|
|
241
768
|
return;
|
|
@@ -247,7 +774,7 @@ async function generateOpencodeAppResponse(systemPrompt, options = {}) {
|
|
|
247
774
|
}
|
|
248
775
|
};
|
|
249
776
|
|
|
250
|
-
const subscription = await client.event.subscribe();
|
|
777
|
+
const subscription = await client.event.subscribe(directoryParams);
|
|
251
778
|
const pump = (async () => {
|
|
252
779
|
try {
|
|
253
780
|
for await (const event of subscription.stream) {
|
|
@@ -257,29 +784,60 @@ async function generateOpencodeAppResponse(systemPrompt, options = {}) {
|
|
|
257
784
|
} catch { /* stream closed; teardown handles the rest */ }
|
|
258
785
|
})();
|
|
259
786
|
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
},
|
|
267
|
-
})
|
|
787
|
+
const promptRequest = client.session.prompt({
|
|
788
|
+
sessionID: sessionId,
|
|
789
|
+
...directoryParams,
|
|
790
|
+
model: { providerID, modelID },
|
|
791
|
+
system: systemPrompt,
|
|
792
|
+
agent: runAgent.name,
|
|
793
|
+
parts: [{ type: "text", text: promptFromMessages(options.messages) }],
|
|
794
|
+
});
|
|
795
|
+
void Promise.resolve(promptRequest).catch(() => undefined);
|
|
796
|
+
const promptResult = unwrap(await Promise.race([promptRequest, permissionFailure]));
|
|
268
797
|
|
|
269
798
|
// Let the pump drain to the terminal session.idle (which lands around when
|
|
270
799
|
// prompt resolves); don't force-stop it or in-flight tool events are lost.
|
|
271
800
|
await Promise.race([pump, delay(POST_PROMPT_DRAIN_MS)]);
|
|
272
801
|
|
|
802
|
+
// Some providers send only completed reasoning-part updates, while others
|
|
803
|
+
// stream deltas and then repeat the completed text. Keep the live deltas;
|
|
804
|
+
// emit the latest completed text only for parts that never streamed.
|
|
805
|
+
for (const [partID, part] of streamParts) {
|
|
806
|
+
if (
|
|
807
|
+
part.type === "reasoning"
|
|
808
|
+
&& !reasoningDeltaPartIds.has(partID)
|
|
809
|
+
&& typeof part.latestText === "string"
|
|
810
|
+
&& part.latestText.length > 0
|
|
811
|
+
) {
|
|
812
|
+
emit(thinkingEvent({ text: part.latestText }));
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
273
816
|
const info = promptResult?.info || {};
|
|
274
|
-
|
|
817
|
+
recordAssistantInfo(info, "prompt-final");
|
|
275
818
|
if (info.error && !errorMessage) {
|
|
276
|
-
errorMessage = info.error
|
|
819
|
+
errorMessage = safeOpenCodeErrorMessage(info.error, "OpenCode turn failed.");
|
|
277
820
|
failureKind = mapErrorFailureKind(info.error);
|
|
278
821
|
}
|
|
279
|
-
|
|
280
|
-
|
|
822
|
+
const finalParts = Array.isArray(promptResult?.parts) ? promptResult.parts : [];
|
|
823
|
+
finalText = finalTextFromParts(finalParts);
|
|
824
|
+
// `session.prompt` returns complete parts after streaming. Deduplicate by
|
|
825
|
+
// part ID rather than globally: a multi-part response may stream one part
|
|
826
|
+
// while another arrives only in the final response.
|
|
827
|
+
for (const part of finalParts) {
|
|
828
|
+
if (
|
|
829
|
+
part?.type === "text"
|
|
830
|
+
&& typeof part.text === "string"
|
|
831
|
+
&& part.text.length > 0
|
|
832
|
+
&& !(typeof part.id === "string" && textDeltaPartIds.has(part.id))
|
|
833
|
+
) {
|
|
834
|
+
emit(assistantTextEvent(part.text));
|
|
835
|
+
}
|
|
836
|
+
}
|
|
281
837
|
|
|
282
|
-
const
|
|
838
|
+
const aggregate = aggregateAssistantInfos(assistantInfos.values());
|
|
839
|
+
usage = aggregate.usage;
|
|
840
|
+
const reportedCost = aggregate.reportedCost;
|
|
283
841
|
const costUsd = reportedCost !== null ? reportedCost : estimateCost({
|
|
284
842
|
resolveCustomPricing: options.resolveCustomPricing,
|
|
285
843
|
model: reference,
|
|
@@ -296,54 +854,82 @@ async function generateOpencodeAppResponse(systemPrompt, options = {}) {
|
|
|
296
854
|
events,
|
|
297
855
|
usage: { ...(usage || {}), cost_usd: costUsd },
|
|
298
856
|
durationMs: Date.now() - start,
|
|
299
|
-
numTurns: 1,
|
|
857
|
+
numTurns: Math.max(1, assistantInfos.size),
|
|
300
858
|
model: reference,
|
|
301
|
-
effort:
|
|
859
|
+
effort: null,
|
|
302
860
|
sdk: "opencode",
|
|
303
|
-
providerSessionId:
|
|
304
|
-
provider_session_id:
|
|
861
|
+
providerSessionId: null,
|
|
862
|
+
provider_session_id: null,
|
|
305
863
|
cancelled: !!options.abortSignal?.aborted,
|
|
306
864
|
error: errorMessage,
|
|
307
865
|
failureKind,
|
|
308
|
-
diagnostics:
|
|
866
|
+
diagnostics: permissionErrorCode === null
|
|
867
|
+
? {}
|
|
868
|
+
: {
|
|
869
|
+
opencode_error_code: permissionErrorCode,
|
|
870
|
+
...(permissionErrorId === null ? {} : { opencode_permission_id: permissionErrorId }),
|
|
871
|
+
},
|
|
309
872
|
capabilitiesUsed: buildCapabilitiesUsed({
|
|
310
873
|
promptCacheActive: (usage?.cache_read_tokens || 0) > 0 || (usage?.cache_creation_tokens || 0) > 0,
|
|
311
874
|
thinkingEnabled: null,
|
|
312
875
|
structuredOutputEnforced: false,
|
|
313
876
|
subagentInvoked: null,
|
|
314
|
-
mcpServersUsed:
|
|
877
|
+
mcpServersUsed: [],
|
|
315
878
|
nativeSubagentsUsed: [],
|
|
316
879
|
toolCompactionApplied: false,
|
|
317
880
|
contextCompactionApplied: null,
|
|
318
881
|
}),
|
|
319
882
|
};
|
|
320
883
|
} catch (err) {
|
|
884
|
+
const partialAggregate = aggregateAssistantInfos(assistantInfos.values());
|
|
885
|
+
const partialUsage = partialAggregate.usage ?? usage;
|
|
886
|
+
const partialCost = partialAggregate.reportedCost !== null
|
|
887
|
+
? partialAggregate.reportedCost
|
|
888
|
+
: partialUsage === null
|
|
889
|
+
? 0
|
|
890
|
+
: estimateCost({
|
|
891
|
+
resolveCustomPricing: options.resolveCustomPricing,
|
|
892
|
+
model: reference,
|
|
893
|
+
inputTokens: Math.max(0, (partialUsage.input_tokens || 0) - (partialUsage.cache_read_tokens || 0)),
|
|
894
|
+
outputTokens: partialUsage.output_tokens || 0,
|
|
895
|
+
cachedTokens: partialUsage.cache_read_tokens || 0,
|
|
896
|
+
cacheWriteTokens: partialUsage.cache_creation_tokens || 0,
|
|
897
|
+
});
|
|
321
898
|
return {
|
|
322
899
|
text: finalText,
|
|
323
900
|
structuredResult: undefined,
|
|
324
901
|
structuredResultSource: null,
|
|
325
902
|
events,
|
|
326
|
-
usage:
|
|
903
|
+
usage: partialUsage ? { ...partialUsage, cost_usd: partialCost } : null,
|
|
327
904
|
durationMs: Date.now() - start,
|
|
328
|
-
numTurns: events.length ? 1 : 0,
|
|
905
|
+
numTurns: assistantInfos.size > 0 ? assistantInfos.size : (events.length ? 1 : 0),
|
|
329
906
|
model: reference,
|
|
330
|
-
effort:
|
|
907
|
+
effort: null,
|
|
331
908
|
sdk: "opencode",
|
|
332
|
-
providerSessionId:
|
|
333
|
-
provider_session_id:
|
|
909
|
+
providerSessionId: null,
|
|
910
|
+
provider_session_id: null,
|
|
334
911
|
cancelled: !!options.abortSignal?.aborted,
|
|
335
912
|
error: err?.message || String(err),
|
|
336
913
|
failureKind: failureKind
|
|
914
|
+
|| err?.opencodeFailureKind
|
|
337
915
|
|| (err?.opencodeError ? mapErrorFailureKind(err.opencodeError) : mapSpawnFailureKind(err)),
|
|
338
|
-
diagnostics: {
|
|
916
|
+
diagnostics: {
|
|
917
|
+
...(events.length ? { had_partial_progress: true } : {}),
|
|
918
|
+
...((err?.opencodeErrorCode || permissionErrorCode) === null || (err?.opencodeErrorCode || permissionErrorCode) === undefined
|
|
919
|
+
? {}
|
|
920
|
+
: { opencode_error_code: err?.opencodeErrorCode || permissionErrorCode }),
|
|
921
|
+
...((err?.opencodePermissionId || permissionErrorId) === null || (err?.opencodePermissionId || permissionErrorId) === undefined
|
|
922
|
+
? {}
|
|
923
|
+
: { opencode_permission_id: err?.opencodePermissionId || permissionErrorId }),
|
|
924
|
+
},
|
|
339
925
|
capabilitiesUsed: buildCapabilitiesUsed({
|
|
340
926
|
structuredOutputEnforced: false,
|
|
341
|
-
mcpServersUsed:
|
|
927
|
+
mcpServersUsed: [],
|
|
342
928
|
}),
|
|
343
929
|
};
|
|
344
930
|
} finally {
|
|
345
931
|
options.abortSignal?.removeEventListener?.("abort", abortHandler);
|
|
346
|
-
try { server?.close?.(); } catch { /* best effort */ }
|
|
932
|
+
try { await server?.close?.(); } catch { /* best effort */ }
|
|
347
933
|
}
|
|
348
934
|
}
|
|
349
935
|
|