@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
@@ -0,0 +1,376 @@
1
+ // @ts-check
2
+ // Compatibility boundary between mono-agent's Pi-native bridge and the
3
+ // lane-based AgentHarness API introduced in pi-agent-core 0.85.
4
+
5
+ import {
6
+ AgentHarness,
7
+ BACKGROUND_CONTEXT,
8
+ createBranchSummaryMessage,
9
+ createCompactionSummaryMessage,
10
+ getOrThrow,
11
+ } from "@earendil-works/pi-agent-core";
12
+ import { installPromptCacheDiagnostics, promptCacheRequest } from "./prompt-cache-diagnostics.js";
13
+
14
+ export const PI_CONTEXT = BACKGROUND_CONTEXT;
15
+
16
+ function operationError(record, fallback) {
17
+ const error = new Error(record?.error?.message || fallback);
18
+ if (record?.error?.code) /** @type {any} */ (error).code = record.error.code;
19
+ return error;
20
+ }
21
+
22
+ function isContextMessage(message) {
23
+ return message?.role !== "assistant"
24
+ || !["error", "aborted", "deferred"].includes(message.stopReason);
25
+ }
26
+
27
+ /**
28
+ * Pi 0.85 no longer exports its session-context projector. Reproduce the
29
+ * public entry contract here so transcript accounting uses the same latest-
30
+ * compaction and failed-assistant filtering rules as the harness.
31
+ * @param {any[]} pathEntries
32
+ * @param {{includeFailed?: boolean}} [options]
33
+ */
34
+ export function buildPiSessionContext(pathEntries, { includeFailed = false } = {}) {
35
+ let start = 0;
36
+ for (let index = pathEntries.length - 1; index >= 0; index -= 1) {
37
+ if (pathEntries[index]?.type === "compaction") {
38
+ start = index;
39
+ break;
40
+ }
41
+ }
42
+ const entries = start > 0 ? pathEntries.slice(start) : pathEntries;
43
+ const messages = [];
44
+ for (const entry of entries) {
45
+ if (entry?.type === "message") {
46
+ if (includeFailed || isContextMessage(entry.message)) messages.push(entry.message);
47
+ } else if (entry?.type === "compaction") {
48
+ messages.push(createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp));
49
+ messages.push(...(includeFailed
50
+ ? (entry.retainedTail || [])
51
+ : (entry.retainedTail || []).filter(isContextMessage)));
52
+ } else if (entry?.type === "branch_summary" && entry.summary) {
53
+ messages.push(createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp));
54
+ }
55
+ }
56
+ return messages;
57
+ }
58
+
59
+ /**
60
+ * Give the surrounding bridge the small session surface it used before Pi
61
+ * moved branch operations onto AgentLane. The raw session remains available to
62
+ * AgentHarness.create(), while all transcript reads and writes use the attached
63
+ * main lane once the harness exists.
64
+ * @param {any} rawSession
65
+ */
66
+ export function createPiSessionAdapter(rawSession) {
67
+ let lane = null;
68
+ let harness = null;
69
+ let closePromise = null;
70
+
71
+ const requireLane = () => {
72
+ if (!lane) throw new Error("Pi session lane is not attached");
73
+ return lane;
74
+ };
75
+
76
+ return {
77
+ rawSession,
78
+ get metadata() { return rawSession.metadata; },
79
+ attach(nextHarness, nextLane) {
80
+ harness = nextHarness;
81
+ lane = nextLane;
82
+ },
83
+ async buildContext() {
84
+ const entries = await requireLane().findEntries({ order: "oldestFirst" }, PI_CONTEXT);
85
+ // This bridge-facing transcript includes terminal error/abort messages so
86
+ // result classification can observe them. Pi filters those only when it
87
+ // constructs the next provider request.
88
+ return { messages: buildPiSessionContext(entries, { includeFailed: true }) };
89
+ },
90
+ getEntries() {
91
+ return requireLane().findEntries({ order: "oldestFirst" }, PI_CONTEXT);
92
+ },
93
+ getLeafId() {
94
+ return requireLane().getTipId(PI_CONTEXT);
95
+ },
96
+ appendMessage(message) {
97
+ return requireLane().appendMessage(message, PI_CONTEXT);
98
+ },
99
+ async moveTo(targetId) {
100
+ const result = getOrThrow(await requireLane().navigateTree(
101
+ targetId,
102
+ { summarize: false },
103
+ PI_CONTEXT,
104
+ ));
105
+ const record = result.navigation;
106
+ if (record.status === "failed") throw operationError(record, "Pi session navigation failed");
107
+ if (record.status !== "completed") throw operationError(record, "Pi session navigation was not completed");
108
+ return record.tipId;
109
+ },
110
+ getMetadata() {
111
+ return Promise.resolve(rawSession.metadata);
112
+ },
113
+ close() {
114
+ if (!closePromise) {
115
+ closePromise = (harness
116
+ ? harness.close(PI_CONTEXT)
117
+ : rawSession.close(PI_CONTEXT));
118
+ }
119
+ return closePromise;
120
+ },
121
+ };
122
+ }
123
+
124
+ function adaptTool(tool) {
125
+ return {
126
+ ...tool,
127
+ async execute(toolCallId, params, onUpdate, _toolContext, _invocation, context) {
128
+ return tool.execute(toolCallId, params, context.abortSignal, onUpdate);
129
+ },
130
+ };
131
+ }
132
+
133
+ const FORWARDED_EVENT_TYPES = [
134
+ "run_start",
135
+ "run_resume",
136
+ "run_suspend",
137
+ "operation_abort",
138
+ "run_end",
139
+ "fault",
140
+ "handler_error",
141
+ "turn_start",
142
+ "turn_end",
143
+ "retry_scheduled",
144
+ "retry_start",
145
+ "retry_end",
146
+ "message_start",
147
+ "message_update",
148
+ "message_end",
149
+ "tool_start",
150
+ "tool_update",
151
+ "tool_end",
152
+ "entry_added",
153
+ "queue_update",
154
+ "value_update",
155
+ "config_update",
156
+ "compaction_start",
157
+ "compaction_end",
158
+ "navigation_start",
159
+ "navigation_end",
160
+ "lane_created",
161
+ "usage",
162
+ ];
163
+
164
+ function legacyEvent(event) {
165
+ // appendMessage() emits runless lifecycle events for transcript seeding.
166
+ // They are persistence notifications, not output from the active provider
167
+ // run, and forwarding them would manufacture assistant boundaries/usage for
168
+ // prior history. Real run-owned message events always carry runId in Pi 0.85.
169
+ if ((event.type === "message_start" || event.type === "message_end") && !event.runId) {
170
+ return null;
171
+ }
172
+ if (event.type === "message_update") {
173
+ return { ...event, assistantMessageEvent: event.event };
174
+ }
175
+ if (event.type === "tool_start") return { ...event, type: "tool_execution_start" };
176
+ if (event.type === "tool_update") return { ...event, type: "tool_execution_update" };
177
+ if (event.type === "tool_end") return { ...event, type: "tool_execution_end" };
178
+ return event;
179
+ }
180
+
181
+ /**
182
+ * Attach Pi 0.85's harness to one session and expose the intentionally small
183
+ * surface consumed by mono-agent's turn runner.
184
+ * @param {any} session
185
+ * @param {any} options
186
+ */
187
+ export async function createPiHarnessAdapter(session, options) {
188
+ const originalTools = Array.isArray(options.tools) ? options.tools : [];
189
+ const adaptedTools = originalTools.map(adaptTool);
190
+ const activeToolNames = originalTools.map((tool) => tool.name);
191
+ // Pi 0.85 removed mixed per-tool scheduling from AgentHarness: its runner
192
+ // consults only this global setting. Preserve the safety contract by
193
+ // serializing the batch whenever any offered tool is stateful, mutating, or
194
+ // MCP-backed. Read-only-only tool sets can still overlap in safe-parallel.
195
+ const toolExecution = originalTools.some((tool) => tool?.executionMode === "sequential")
196
+ ? "sequential"
197
+ : "parallel";
198
+
199
+ /** @type {any} */
200
+ let created;
201
+ /** @type {any} */
202
+ let rawHarness;
203
+ /** @type {any} */
204
+ let lane;
205
+ let removePromptCacheDiagnostics = () => {};
206
+ try {
207
+ created = await AgentHarness.create({
208
+ ...options,
209
+ session: session.rawSession,
210
+ tools: adaptedTools,
211
+ activeToolNames,
212
+ toolExecution,
213
+ // mono-agent owns proactive/reactive compaction policy. The permanent hook
214
+ // below also declines Pi's overflow recovery so one bridge never runs two
215
+ // competing policies.
216
+ compaction: { enabled: false, reserveTokens: 16_384, keepRecentTokens: 20_000 },
217
+ }, PI_CONTEXT);
218
+ rawHarness = created.harness;
219
+ // Attach the harness before lane/configuration awaits so any later failure
220
+ // can close the partially constructed handle instead of wedging the repo.
221
+ session.attach(rawHarness, null);
222
+ lane = await rawHarness.lane("main", PI_CONTEXT);
223
+ session.attach(rawHarness, lane);
224
+
225
+ // A restored lane carries its prior configuration, so explicitly bind it to
226
+ // this run's model, effort, and available tools. Legacy v3 transcripts import
227
+ // with an empty active-tool list and are upgraded atomically by Pi on write.
228
+ await rawHarness.setTools(adaptedTools, PI_CONTEXT);
229
+ await lane.setModel({ provider: options.model.provider, modelId: options.model.id }, PI_CONTEXT);
230
+ await lane.setThinkingLevel(options.thinkingLevel ?? "off", PI_CONTEXT);
231
+ await lane.setActiveTools(activeToolNames, PI_CONTEXT);
232
+
233
+ rawHarness.hooks.on("before_compaction", (event) => (
234
+ event.reason === "manual" ? undefined : { decline: true }
235
+ ), { id: "mono-agent-compaction-owner" });
236
+ removePromptCacheDiagnostics = installPromptCacheDiagnostics(rawHarness, options);
237
+ } catch (error) {
238
+ try { await session.close(); } catch { /* preserve the construction error */ }
239
+ throw error;
240
+ }
241
+
242
+ let closed = false;
243
+ let currentModel = options.model;
244
+ let currentThinkingLevel = options.thinkingLevel ?? "off";
245
+ let currentActiveToolNames = [...activeToolNames];
246
+ const manuallyAppendedEntryIds = new Set();
247
+
248
+ const adapter = {
249
+ getPromptCacheRequest: () => promptCacheRequest(rawHarness),
250
+ models: options.models,
251
+ getModel: () => currentModel,
252
+ getThinkingLevel: () => currentThinkingLevel,
253
+ getActiveTools: () => originalTools.filter((tool) => currentActiveToolNames.includes(tool.name)),
254
+ async setActiveTools(names) {
255
+ await lane.setActiveTools(names, PI_CONTEXT);
256
+ currentActiveToolNames = [...names];
257
+ },
258
+ async setCompactionSettings(settings) {
259
+ await rawHarness.setCompactionSettings(settings, PI_CONTEXT);
260
+ },
261
+ async appendMessage(message) {
262
+ const entryId = await lane.appendMessage(message, PI_CONTEXT);
263
+ manuallyAppendedEntryIds.add(entryId);
264
+ return entryId;
265
+ },
266
+ // Mirror pi's own lane.prompt() (accept → drive) rather than calling it,
267
+ // so the operation id is known the moment Pi admits the run instead of
268
+ // only when it settles. The live-input epoch needs it up front: without
269
+ // it every steer consumed mid-run stays "pending" until the whole run
270
+ // ends and is only acknowledged in one batch at the end.
271
+ async prompt(text, promptOptions) {
272
+ const images = promptOptions?.images;
273
+ const admission = getOrThrow(await lane.accept({
274
+ kind: "prompt",
275
+ prompt: text,
276
+ ...(Array.isArray(images) && images.length > 0 ? { images } : {}),
277
+ }, PI_CONTEXT));
278
+ const { operationId } = admission;
279
+ if (typeof operationId !== "string" || operationId.length === 0) {
280
+ throw new Error("Pi run was admitted without an operation id");
281
+ }
282
+ promptOptions?.onOperationAdmitted?.(operationId);
283
+ const driven = getOrThrow(await lane.drive({ operationId, waitForRetry: true }, PI_CONTEXT));
284
+ if (driven.kind === "settled") return driven.outcome;
285
+ if (driven.kind === "waiting" && driven.reason === "deferred") {
286
+ return { operationId, status: "suspended", deferred: driven.deferred };
287
+ }
288
+ throw new Error(`Pi run ${operationId} returned an unwaited retry`);
289
+ },
290
+ // Pi's QueueResult carries `{ entryId }`; the live-input runner keys prompt
291
+ // epoch registration, `cancelQueued` and `message_end` correlation on the
292
+ // bare entry id, so unwrap it here (as appendMessage does) rather than hand
293
+ // the runner an object it would settle as "uncertain" without ever
294
+ // registering the steer.
295
+ async steer(message) {
296
+ const { entryId } = getOrThrow(await lane.steer(message, undefined, PI_CONTEXT));
297
+ if (typeof entryId !== "string" || entryId.length === 0) {
298
+ throw new Error("Pi steer settled without a queue entry id");
299
+ }
300
+ return entryId;
301
+ },
302
+ async cancelQueued(entryId) {
303
+ return getOrThrow(await lane.cancelQueued(entryId, PI_CONTEXT));
304
+ },
305
+ async abort() {
306
+ const result = await lane.abort(PI_CONTEXT);
307
+ // Aborting an already-idle lane is a benign race with prompt settlement.
308
+ if (!result.ok) {
309
+ const error = /** @type {{error: any}} */ (result).error;
310
+ if (error?._tag !== "NoActiveOperation") throw error;
311
+ }
312
+ },
313
+ waitForIdle() {
314
+ return lane.waitForIdle(PI_CONTEXT);
315
+ },
316
+ async compact() {
317
+ const value = getOrThrow(await lane.compact(undefined, PI_CONTEXT));
318
+ const record = value.compaction;
319
+ if (record.status === "failed") throw operationError(record, "Pi compaction failed");
320
+ if (record.status !== "completed") throw operationError(record, "Pi compaction cancelled");
321
+ const entry = record.tipId
322
+ ? await session.rawSession.getEntry(record.tipId, PI_CONTEXT)
323
+ : undefined;
324
+ if (!entry || entry.type !== "compaction") {
325
+ throw new Error("Pi compaction completed without a compaction entry");
326
+ }
327
+ return entry;
328
+ },
329
+ on(type, handler) {
330
+ if (type === "tool_result") {
331
+ return rawHarness.hooks.on("after_tool", (event) => handler(event));
332
+ }
333
+ if (type === "session_before_compact") {
334
+ return rawHarness.hooks.on("before_compaction", async (event, context) => {
335
+ const branchEntries = await lane.findEntries({ order: "oldestFirst" }, context);
336
+ const result = await handler({
337
+ ...event,
338
+ branchEntries,
339
+ signal: context.abortSignal,
340
+ context,
341
+ });
342
+ if (result?.cancel) return { decline: true };
343
+ return result?.compaction === undefined ? undefined : { compaction: result.compaction };
344
+ });
345
+ }
346
+ throw new Error(`Unsupported Pi harness hook: ${String(type)}`);
347
+ },
348
+ subscribe(listener) {
349
+ const removes = FORWARDED_EVENT_TYPES.map((type) => rawHarness.events.on(
350
+ /** @type {any} */ (type),
351
+ (event) => {
352
+ if (event.type === "message_end" && manuallyAppendedEntryIds.has(event.entryId)) return;
353
+ const converted = legacyEvent(event);
354
+ if (converted) listener(converted);
355
+ },
356
+ ));
357
+ return () => removes.forEach((remove) => remove());
358
+ },
359
+ async abortOpenOperations() {
360
+ if (!created.open.some((operation) => operation.lane === "main")) return;
361
+ // mono-agent tools predate Pi's invocation memo/checkpoint API, so replaying
362
+ // an interrupted durable operation could repeat an external side effect.
363
+ // Fail closed by settling it as aborted before admitting a new prompt.
364
+ getOrThrow(await lane.abort(PI_CONTEXT));
365
+ await lane.waitForIdle(PI_CONTEXT);
366
+ },
367
+ async close() {
368
+ if (closed) return;
369
+ closed = true;
370
+ removePromptCacheDiagnostics();
371
+ await session.close();
372
+ },
373
+ };
374
+
375
+ return adapter;
376
+ }
@@ -0,0 +1,103 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+
3
+ const activeRequests = new WeakMap();
4
+ export const promptCacheRequest = (harness) => activeRequests.get(harness);
5
+
6
+ const MAX_MESSAGE_FINGERPRINTS = 128;
7
+ const safeString = (value) => typeof value === "string" ? value.slice(0, 160) : undefined;
8
+ const serialized = (value) => JSON.stringify(value) ?? "null";
9
+ const fingerprint = (value) => createHash("sha256").update(serialized(value)).digest("hex").slice(0, 16);
10
+ const bytes = (value) => value == null ? 0 : Buffer.byteLength(typeof value === "string" ? value : serialized(value));
11
+ const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
12
+ const list = (value) => Array.isArray(value) ? value : [];
13
+
14
+ function normalizedPayload(event) {
15
+ const payload = record(event?.payload);
16
+ if (!payload) return { family: "unsupported", reason: "payload_not_object" };
17
+ const api = safeString(event?.model?.api) ?? "unknown";
18
+ if (api === "google-generative-ai" || api === "google-vertex") {
19
+ const config = record(payload.config) ?? {};
20
+ const tools = list(config.tools).flatMap((tool) => list(record(tool)?.functionDeclarations).length > 0
21
+ ? list(record(tool)?.functionDeclarations) : [tool]);
22
+ return { family: "google", system: config.systemInstruction ?? null, tools, messages: list(payload.contents), payload };
23
+ }
24
+ if (api === "pi-messages") {
25
+ const context = record(payload.context);
26
+ if (context && ("messages" in context || "systemPrompt" in context || "tools" in context)) {
27
+ return { family: "pi-messages", system: context.systemPrompt ?? null, tools: list(context.tools), messages: list(context.messages), payload };
28
+ }
29
+ }
30
+ if (api === "bedrock-converse-stream") {
31
+ return { family: "bedrock", system: payload.system ?? null, tools: list(record(payload.toolConfig)?.tools), messages: list(payload.messages), payload };
32
+ }
33
+ if (api === "anthropic-messages") {
34
+ return { family: "anthropic", system: payload.system ?? null, tools: list(payload.tools), messages: list(payload.messages), payload };
35
+ }
36
+ if (["openai-responses", "azure-openai-responses", "openai-codex-responses"].includes(api)) {
37
+ return { family: api === "openai-codex-responses" ? "openai-codex" : "openai-responses", system: payload.instructions ?? null, tools: list(payload.tools), messages: list(payload.input), payload };
38
+ }
39
+ return { family: "unsupported", reason: `unrecognized_api:${api}` };
40
+ }
41
+
42
+ function cacheMetadata(payload, family) {
43
+ const disabled = payload.prompt_cache_retention === "none" || record(payload.options)?.cacheRetention === "none";
44
+ const key = payload.prompt_cache_key ?? payload.cache_key ?? payload.cachedContent
45
+ ?? (family === "pi-messages" ? record(payload.options)?.sessionId : undefined);
46
+ let explicit = false;
47
+ const pending = [{ value: payload, depth: 0 }];
48
+ let visited = 0;
49
+ while (pending.length > 0 && visited < 10_000) {
50
+ const { value, depth } = pending.pop();
51
+ visited += 1;
52
+ if (depth > 12 || value === null || typeof value !== "object") continue;
53
+ for (const [name, child] of Object.entries(value)) {
54
+ if (["cache_control", "cachePoint", "cache_point", "prompt_cache_retention", "prompt_cache_options"].includes(name) && child != null) explicit = true;
55
+ if (typeof child === "object" && child !== null) pending.push({ value: child, depth: depth + 1 });
56
+ }
57
+ }
58
+ return {
59
+ cacheMode: disabled ? "disabled" : [key === undefined ? "" : "keyed", explicit ? "explicit" : ""].filter(Boolean).join("+") || "provider-default",
60
+ ...(disabled || key === undefined ? {} : { cacheKeyFingerprint: fingerprint(key) }),
61
+ };
62
+ }
63
+
64
+ /** Install a metadata-only, request-lifecycle-bounded Pi payload diagnostic. */
65
+ export function installPromptCacheDiagnostics(harness, options) {
66
+ if (options.promptCacheDiagnostics !== true || typeof options.onEvent !== "function") return () => {};
67
+ let ordinal = 0;
68
+ const remove = harness.hooks.on("before_payload", (event) => {
69
+ const correlation = { phase: "assistant", requestId: randomUUID() };
70
+ activeRequests.set(harness, correlation);
71
+ const normalized = normalizedPayload(event);
72
+ const model = event?.model ?? options.model ?? {};
73
+ const base = {
74
+ type: "prompt_cache_diagnostic",
75
+ ...correlation,
76
+ requestOrdinal: ++ordinal,
77
+ model: [safeString(model.provider), safeString(model.id)].filter(Boolean).join(":") || "unknown",
78
+ api: safeString(model.api) ?? "unknown",
79
+ payloadFamily: normalized.family,
80
+ };
81
+ if (normalized.family === "unsupported") {
82
+ options.onEvent({ ...base, supported: false, unsupportedReason: normalized.reason });
83
+ return;
84
+ }
85
+ const messages = normalized.messages;
86
+ const logicalInputInterpretation = normalized.payload.previous_response_id === undefined ? "full" : "delta";
87
+ const codexWireUnknown = normalized.family === "openai-codex" && logicalInputInterpretation === "full";
88
+ options.onEvent({
89
+ ...base,
90
+ supported: true,
91
+ systemBytes: bytes(normalized.system), systemFingerprint: fingerprint(normalized.system),
92
+ toolDefinitionCount: normalized.tools.length, toolDefinitionsFingerprint: fingerprint(normalized.tools),
93
+ messageCount: messages.length,
94
+ messageFingerprints: messages.slice(0, MAX_MESSAGE_FINGERPRINTS).map(fingerprint),
95
+ messageFingerprintsTruncated: messages.length > MAX_MESSAGE_FINGERPRINTS,
96
+ ...cacheMetadata(normalized.payload, normalized.family),
97
+ logicalInputInterpretation,
98
+ inputInterpretation: codexWireUnknown ? "unavailable" : logicalInputInterpretation,
99
+ inputInterpretationSource: codexWireUnknown ? "pre_transport_payload" : "provider_payload",
100
+ });
101
+ }, { id: "mono-agent-prompt-cache-diagnostics" });
102
+ return () => { activeRequests.delete(harness); remove(); };
103
+ }
@@ -0,0 +1,102 @@
1
+ // @ts-check
2
+
3
+ const OPENCODE_HOST = "opencode.ai";
4
+ const OPENCODE_PROVIDERS = new Set(["opencode", "opencode-go"]);
5
+ const REQUEST_METHODS = new Set([
6
+ "stream",
7
+ "complete",
8
+ "streamSimple",
9
+ "completeSimple",
10
+ "streamDeferred",
11
+ "fetchDeferred",
12
+ "cancelDeferred",
13
+ ]);
14
+
15
+ /** @typedef {import("@earendil-works/pi-ai").Models} Models */
16
+ /** @typedef {import("@earendil-works/pi-ai").ProviderHeaders} ProviderHeaders */
17
+
18
+ /**
19
+ * Match Pi's OpenCode attribution boundary without accepting deceptive suffixes
20
+ * or subdomains. Provider ids remain authoritative for callers that deliberately
21
+ * route OpenCode through a nonstandard endpoint.
22
+ *
23
+ * @param {{provider?: string, baseUrl?: string}} model
24
+ */
25
+ export function isOpenCodeModel(model) {
26
+ if (OPENCODE_PROVIDERS.has(model?.provider)) return true;
27
+ if (typeof model?.baseUrl !== "string") return false;
28
+ try {
29
+ return new URL(model.baseUrl).hostname === OPENCODE_HOST;
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Add one default without replacing an auth/model/request value, including a
37
+ * caller's case-insensitive null suppression.
38
+ *
39
+ * @param {ProviderHeaders} headers
40
+ * @param {string} name
41
+ * @param {string} value
42
+ * @returns {ProviderHeaders}
43
+ */
44
+ function addDefaultHeader(headers, name, value) {
45
+ const lowerName = name.toLowerCase();
46
+ if (Object.keys(headers).some((existingName) => existingName.toLowerCase() === lowerName)) {
47
+ return headers;
48
+ }
49
+ return { ...headers, [name]: value };
50
+ }
51
+
52
+ /**
53
+ * @param {Object<string, *>|undefined} options
54
+ * @param {string} sessionId
55
+ * @returns {Object<string, *>}
56
+ */
57
+ function withOpenCodeHeaderTransform(options, sessionId) {
58
+ const previousTransform = options?.transformHeaders;
59
+ return {
60
+ ...(options ?? {}),
61
+ transformHeaders: async (headers) => {
62
+ let attributed = addDefaultHeader(headers, "x-opencode-session", sessionId);
63
+ attributed = addDefaultHeader(attributed, "x-opencode-client", "mono-agent");
64
+ return typeof previousTransform === "function"
65
+ ? await previousTransform(attributed)
66
+ : attributed;
67
+ },
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Decorate every Pi Models request path for one run. Non-request methods stay
73
+ * bound to the original Models instance because its state lives in private
74
+ * fields; matching is performed on the actual model dispatched by Pi, covering
75
+ * builtin, custom-provider, compaction, deferred, and advanced/test models.
76
+ *
77
+ * @param {Models} models
78
+ * @param {string} sessionId
79
+ * @returns {Models}
80
+ */
81
+ export function withOpenCodeSessionHeaders(models, sessionId) {
82
+ const wrappers = new Map();
83
+ return /** @type {Models} */ (new Proxy(models, {
84
+ get(target, property) {
85
+ const value = Reflect.get(target, property, target);
86
+ if (typeof property !== "string" || typeof value !== "function") return value;
87
+ if (!REQUEST_METHODS.has(property)) return value.bind(target);
88
+
89
+ let wrapper = wrappers.get(property);
90
+ if (wrapper === undefined) {
91
+ wrapper = (model, input, options) => value.call(
92
+ target,
93
+ model,
94
+ input,
95
+ isOpenCodeModel(model) ? withOpenCodeHeaderTransform(options, sessionId) : options,
96
+ );
97
+ wrappers.set(property, wrapper);
98
+ }
99
+ return wrapper;
100
+ },
101
+ }));
102
+ }