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