@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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mono-agent/agent-runtime",
3
- "version": "0.20.11",
4
- "description": "Agent runtime supporting Claude SDK/CLI, Codex, OpenCode, Pi SDK, and ACP v1 bridges out of the box",
3
+ "version": "0.21.0",
4
+ "description": "Agent runtime backed by the Pi SDK provider catalog",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-only",
7
7
  "repository": {
@@ -47,18 +47,6 @@
47
47
  "types": "./types/ai/runtime/registry.d.ts",
48
48
  "default": "./src/ai/runtime/registry.js"
49
49
  },
50
- "./ai/runtime/context-windows.js": {
51
- "types": "./types/ai/runtime/context-windows.d.ts",
52
- "default": "./src/ai/runtime/context-windows.js"
53
- },
54
- "./ai/runtime/fast-mode.js": {
55
- "types": "./types/ai/runtime/fast-mode.d.ts",
56
- "default": "./src/ai/runtime/fast-mode.js"
57
- },
58
- "./ai/streaming/codex-events.js": {
59
- "types": "./types/ai/streaming/codex-events.d.ts",
60
- "default": "./src/ai/streaming/codex-events.js"
61
- },
62
50
  "./ai/live-input-prompt.js": {
63
51
  "types": "./types/ai/live-input-prompt.d.ts",
64
52
  "default": "./src/ai/live-input-prompt.js"
@@ -67,29 +55,9 @@
67
55
  "types": "./types/ai/file-change-stats.d.ts",
68
56
  "default": "./src/ai/file-change-stats.js"
69
57
  },
70
- "./ai/providers/claude-sdk.js": {
71
- "types": "./types/ai/providers/claude-sdk.d.ts",
72
- "default": "./src/ai/providers/claude-sdk.js"
73
- },
74
- "./ai/providers/claude-sdk-discovery.js": {
75
- "types": "./types/ai/providers/claude-sdk-discovery.d.ts",
76
- "default": "./src/ai/providers/claude-sdk-discovery.js"
77
- },
78
- "./ai/providers/claude-cli.js": {
79
- "types": "./types/ai/providers/claude-cli.d.ts",
80
- "default": "./src/ai/providers/claude-cli.js"
81
- },
82
- "./ai/providers/codex-app.js": {
83
- "types": "./types/ai/providers/codex-app.d.ts",
84
- "default": "./src/ai/providers/codex-app.js"
85
- },
86
- "./ai/providers/opencode-discovery.js": {
87
- "types": "./types/ai/providers/opencode-discovery.d.ts",
88
- "default": "./src/ai/providers/opencode-discovery.js"
89
- },
90
- "./ai/providers/acp.js": {
91
- "types": "./types/ai/providers/acp.d.ts",
92
- "default": "./src/ai/providers/acp.js"
58
+ "./ai/providers/codex/app-server-client.js": {
59
+ "types": "./types/ai/providers/codex/app-server-client.d.ts",
60
+ "default": "./src/ai/providers/codex/app-server-client.js"
93
61
  },
94
62
  "./agent/tools/index.js": {
95
63
  "types": "./types/agent/tools/index.d.ts",
@@ -132,26 +100,27 @@
132
100
  "LICENSE"
133
101
  ],
134
102
  "engines": {
135
- "node": ">=22.19.0"
103
+ "node": ">=24.15.0"
136
104
  },
137
105
  "dependencies": {
138
- "@agentclientprotocol/sdk": "1.3.0",
139
- "@anthropic-ai/claude-agent-sdk": "0.3.206",
140
- "@anthropic-ai/sdk": "^0.110.0",
141
- "@earendil-works/pi-agent-core": "0.83.0",
142
- "@earendil-works/pi-ai": "0.84.3",
106
+ "@earendil-works/pi-agent-core": "0.85.1",
107
+ "@earendil-works/pi-ai": "0.85.1",
143
108
  "@modelcontextprotocol/sdk": "^1.29.0",
144
109
  "@mozilla/readability": "0.6.0",
145
- "@opencode-ai/sdk": "^1.15.13",
146
110
  "@vscode/ripgrep": "1.18.0",
147
111
  "bmp-ts": "1.0.9",
148
112
  "cross-spawn": "^7.0.6",
149
113
  "defuddle": "0.19.2",
114
+ "fast-xml-parser": "5.7.3",
150
115
  "linkedom": "0.18.13",
151
116
  "sharp": "0.35.3",
117
+ "turndown": "7.2.4",
152
118
  "unpdf": "1.8.0",
153
119
  "zod": "^4.3.6"
154
120
  },
121
+ "devDependencies": {
122
+ "@types/turndown": "5.0.6"
123
+ },
155
124
  "scripts": {
156
125
  "build": "tsc -p tsconfig.types.json",
157
126
  "typecheck": "tsc -p tsconfig.types.json --noEmit",
@@ -6,9 +6,9 @@
6
6
  // still cite the artifact.
7
7
  //
8
8
  // Persistence is delegated to the host via a `persistArtifact({ filename, buffer,
9
- // toolName, toolUseId }) -> path | null` callback. Host output sinks
10
- // in src/core/tool-artifacts.js writes to {runArtifactDir}/tool-output/<file>.
11
- // Hosts that don't care can pass null and the truncated payload is dropped.
9
+ // toolName, toolUseId }) -> path | null` callback. The app binds that callback
10
+ // per run and stores payloads under its configured tool-output artifact root.
11
+ // Hosts that don't provide a sink still receive bounded retained text.
12
12
 
13
13
  export const MAX_TOOL_RESULT_BYTES = 262144;
14
14
 
@@ -22,6 +22,11 @@ export const DEFAULT_TOOL_BLOAT_CONFIG = Object.freeze({
22
22
  binaryBloatTools: BINARY_BLOAT_TOOLS,
23
23
  });
24
24
 
25
+ const RETAINED_UNTRUSTED_BEGIN = "[BEGIN RETAINED UNTRUSTED TOOL RESULT]";
26
+ const RETAINED_UNTRUSTED_END = "[END RETAINED UNTRUSTED TOOL RESULT]";
27
+ const RETAINED_MIDDLE_NOTICE = "[Omitted middle may contain additional source content; retained tail is not the source ending.]";
28
+ const RETAINED_OMISSION_RE = /\[\.\.\. (\d+) source bytes omitted \.\.\.\]/gu;
29
+
25
30
  function blockBytes(block) {
26
31
  if (!block || typeof block !== "object") return 0;
27
32
  if (block.type === "text") return Buffer.byteLength(String(block.text || ""), "utf8");
@@ -74,17 +79,144 @@ function persistBlock(toolName, block, idx, idTag, persistArtifact) {
74
79
  }
75
80
  }
76
81
 
77
- function summaryText(toolName, originalBytes, maxBytes, savedPaths) {
82
+ function controlFree(value) {
83
+ return String(value || "").replace(/[\u0000-\u001f\u007f]+/gu, " ").trim();
84
+ }
85
+
86
+ function persistenceSummary(savedPaths, includePath = true) {
87
+ if (savedPaths.length === 0) return "persistence unavailable";
88
+ if (!includePath) return `saved_files=${savedPaths.length}`;
89
+ if (savedPaths.length === 1) return `saved_to=${controlFree(savedPaths[0])}`;
90
+ return `saved_to=${controlFree(savedPaths[0])}; saved_files=${savedPaths.length}`;
91
+ }
92
+
93
+ function summaryText(toolName, originalBytes, retainedBytes, maxBytes, savedPaths, includePath = true) {
78
94
  const parts = [
79
95
  `[truncated tool_result: ${originalBytes} bytes exceeded ${maxBytes} byte cap`,
80
- `tool=${toolName}`,
96
+ `retained=${retainedBytes} bytes`,
97
+ `tool=${controlFree(toolName)}`,
81
98
  ];
82
- if (savedPaths.length === 1) parts.push(`saved_to=${savedPaths[0]}`);
83
- else if (savedPaths.length > 1) parts.push(`saved_to=[${savedPaths.length} files]`);
84
- else parts.push("persistence unavailable");
99
+ parts.push(persistenceSummary(savedPaths, includePath));
85
100
  return `${parts.join("; ")}]`;
86
101
  }
87
102
 
103
+ function capSummary(text, maxBytes) {
104
+ for (const candidate of [text, "[truncated tool_result]", "[truncated]", "[]"]) {
105
+ if (Buffer.byteLength(candidate, "utf8") <= maxBytes) return candidate;
106
+ }
107
+ return "";
108
+ }
109
+
110
+ function summaryOnly(toolName, originalBytes, maxBytes, savedPaths) {
111
+ const parts = [
112
+ `[truncated tool_result: ${originalBytes} bytes exceeded ${maxBytes} byte cap`,
113
+ `tool=${controlFree(toolName)}`,
114
+ ];
115
+ parts.push(persistenceSummary(savedPaths));
116
+ return `${parts.join("; ")}]`;
117
+ }
118
+
119
+ function neutralizeRetainedFrames(value) {
120
+ return wellFormedText(value)
121
+ .replaceAll(RETAINED_UNTRUSTED_BEGIN, `(BEGIN RETAINED UNTRUSTED TOOL RESULT)`)
122
+ .replaceAll(RETAINED_UNTRUSTED_END, `(END RETAINED UNTRUSTED TOOL RESULT)`)
123
+ .replaceAll(RETAINED_MIDDLE_NOTICE, `(Omitted middle may contain additional source content; retained tail is not the source ending.)`)
124
+ .replace(RETAINED_OMISSION_RE, `(... $1 source bytes omitted ...)`);
125
+ }
126
+
127
+ function wellFormedText(value) {
128
+ let out = "";
129
+ for (const point of String(value || "")) {
130
+ const unit = point.charCodeAt(0);
131
+ out += point.length === 1 && unit >= 0xd800 && unit <= 0xdfff ? "\uFFFD" : point;
132
+ }
133
+ return out;
134
+ }
135
+
136
+ function utf8Head(segments, maxBytes) {
137
+ if (maxBytes <= 0) return { text: "", sourceBytes: 0 };
138
+ let bytes = 0;
139
+ let out = "";
140
+ let sourceBytes = 0;
141
+ outer: for (const segment of segments) {
142
+ for (const point of segment.text) {
143
+ const size = Buffer.byteLength(point, "utf8");
144
+ if (bytes + size > maxBytes) break outer;
145
+ out += point;
146
+ bytes += size;
147
+ if (segment.source) sourceBytes += size;
148
+ }
149
+ }
150
+ return { text: out, sourceBytes };
151
+ }
152
+
153
+ function utf8Tail(segments, maxBytes) {
154
+ if (maxBytes <= 0) return { text: "", sourceBytes: 0 };
155
+ let bytes = 0;
156
+ const out = [];
157
+ let sourceBytes = 0;
158
+ outer: for (const segment of [...segments].reverse()) {
159
+ for (const point of [...segment.text].reverse()) {
160
+ const size = Buffer.byteLength(point, "utf8");
161
+ if (bytes + size > maxBytes) break outer;
162
+ out.push(point);
163
+ bytes += size;
164
+ if (segment.source) sourceBytes += size;
165
+ }
166
+ }
167
+ return { text: out.reverse().join(""), sourceBytes };
168
+ }
169
+
170
+ function retainedTextPayload(toolName, blocks, originalBytes, maxBytes, savedPaths) {
171
+ const segments = blocks.flatMap((block, index) => [
172
+ ...(index === 0 ? [] : [{ text: "\n\n", source: false }]),
173
+ { text: neutralizeRetainedFrames(block.text), source: true },
174
+ ]);
175
+ const maximumDigits = String(originalBytes).length;
176
+ const retainedPlaceholder = "9".repeat(maximumDigits);
177
+ const omittedPlaceholder = "9".repeat(maximumDigits);
178
+ let summary = summaryText(toolName, originalBytes, Number(retainedPlaceholder), maxBytes, savedPaths, true);
179
+ const fixedBody = [
180
+ summary,
181
+ RETAINED_UNTRUSTED_BEGIN,
182
+ "",
183
+ `[... ${omittedPlaceholder} source bytes omitted ...]`,
184
+ RETAINED_MIDDLE_NOTICE,
185
+ "",
186
+ RETAINED_UNTRUSTED_END,
187
+ ].join("\n");
188
+ if (Buffer.byteLength(fixedBody, "utf8") > maxBytes) {
189
+ summary = summaryText(toolName, originalBytes, 0, maxBytes, savedPaths, false);
190
+ return capSummary(summary, maxBytes);
191
+ }
192
+
193
+ const sourceBudget = maxBytes - Buffer.byteLength(fixedBody, "utf8");
194
+ const head = utf8Head(segments, Math.floor(sourceBudget * 0.6));
195
+ const tail = utf8Tail(segments, sourceBudget - Buffer.byteLength(head.text, "utf8"));
196
+ const retainedBytes = head.sourceBytes + tail.sourceBytes;
197
+ const omittedBytes = Math.max(0, originalBytes - retainedBytes);
198
+ summary = summaryText(toolName, originalBytes, retainedBytes, maxBytes, savedPaths, true);
199
+ const rendered = [
200
+ summary,
201
+ RETAINED_UNTRUSTED_BEGIN,
202
+ head.text,
203
+ `[... ${omittedBytes} source bytes omitted ...]`,
204
+ RETAINED_MIDDLE_NOTICE,
205
+ tail.text,
206
+ RETAINED_UNTRUSTED_END,
207
+ ].join("\n");
208
+ if (Buffer.byteLength(rendered, "utf8") <= maxBytes) return rendered;
209
+
210
+ // A long saved path can make the exact-path summary larger than the reserved
211
+ // placeholder. Preserve the trust frame and source slices by falling back to
212
+ // the honest saved-file count, never by returning a partial path.
213
+ const countSummary = summaryText(toolName, originalBytes, retainedBytes, maxBytes, savedPaths, false);
214
+ const countRendered = [countSummary, ...rendered.split("\n").slice(1)].join("\n");
215
+ return Buffer.byteLength(countRendered, "utf8") <= maxBytes
216
+ ? countRendered
217
+ : capSummary(countSummary, maxBytes);
218
+ }
219
+
88
220
  export function summarisePayload(toolName, contentBlocks, persistArtifact, options = {}) {
89
221
  const {
90
222
  maxBytes = MAX_TOOL_RESULT_BYTES,
@@ -109,8 +241,12 @@ export function summarisePayload(toolName, contentBlocks, persistArtifact, optio
109
241
  const path = persistBlock(toolName, block, idx, idTag, persistArtifact);
110
242
  if (path) savedPaths.push(path);
111
243
  });
244
+ const textOnly = blocks.length > 0 && blocks.every((block) => block?.type === "text");
245
+ const rewrittenText = textOnly && otherBytes > maxBytes && imageBytes === 0
246
+ ? retainedTextPayload(toolName, blocks, originalBytes, maxBytes, savedPaths)
247
+ : summaryOnly(toolName, originalBytes, maxBytes, savedPaths);
112
248
  return {
113
- rewrittenBlocks: [{ type: "text", text: summaryText(toolName, originalBytes, maxBytes, savedPaths) }],
249
+ rewrittenBlocks: [{ type: "text", text: rewrittenText }],
114
250
  savedPaths,
115
251
  originalBytes,
116
252
  truncated: true,
@@ -203,7 +203,7 @@ function positiveInt(value, fallback) {
203
203
  * Build the `Agent` tool, or null when subagents are unavailable for this run.
204
204
  *
205
205
  * @param {RuntimeSubagentsOptions|null|undefined} subagents
206
- * @param {{model?: *, executionMode?: string, cwd?: string, parentRunId?: string, sandboxPolicy?: *, sandboxEngine?: *, skills?: {name: string, description?: string}[], skillsRoot?: string, toolEnvironment?: *, onEvent?: (event: *) => void}} [context]
206
+ * @param {{model?: *, cwd?: string, parentRunId?: string, sandboxPolicy?: *, sandboxEngine?: *, skills?: {name: string, description?: string}[], skillsRoot?: string, toolEnvironment?: *, webSearchConfig?: *, webRequestCoordinator?: *, webFetchConfig?: *, onEvent?: (event: *) => void}} [context]
207
207
  * @returns {*|null}
208
208
  */
209
209
  export function createAgentTool(subagents, context = {}) {
@@ -291,9 +291,10 @@ export function createAgentTool(subagents, context = {}) {
291
291
  label: "Agent",
292
292
  description: toolDescription(subagents, definitions, ceiling),
293
293
  parameters,
294
- // MUST stay undefined. pi-agent-core's agent loop makes the ENTIRE batch
295
- // sequential when any tool in it declares executionMode "sequential"
296
- // (dist/agent-loop.js:289), which would serialize every parallel Agent call.
294
+ // MUST stay undefined. Agent-only batches can overlap when the offered tool
295
+ // set contains no sequential tool. Pi 0.85 exposes only a global harness
296
+ // mode, however, so offering Bash/Write/MCP/etc. serializes Agent calls too;
297
+ // see the documented provider limitation rather than marking Agent itself.
297
298
  executionMode: undefined,
298
299
  /**
299
300
  * @param {string} toolCallId
@@ -398,7 +399,6 @@ export function createAgentTool(subagents, context = {}) {
398
399
  prompt: params.prompt,
399
400
  definition: profile,
400
401
  ...(context.model === undefined ? {} : { model: context.model }),
401
- ...(context.executionMode === undefined ? {} : { executionMode: context.executionMode }),
402
402
  ...(context.cwd === undefined ? {} : { cwd: context.cwd }),
403
403
  ...(context.parentRunId === undefined ? {} : { parentRunId: context.parentRunId }),
404
404
  // Inherited, never widened: a profile cannot loosen confinement.
@@ -410,6 +410,9 @@ export function createAgentTool(subagents, context = {}) {
410
410
  ...(context.skills === undefined ? {} : { skills: context.skills }),
411
411
  ...(context.skillsRoot === undefined ? {} : { skillsRoot: context.skillsRoot }),
412
412
  ...(context.toolEnvironment === undefined ? {} : { toolEnvironment: context.toolEnvironment }),
413
+ ...(context.webSearchConfig === undefined ? {} : { webSearchConfig: context.webSearchConfig }),
414
+ ...(context.webRequestCoordinator === undefined ? {} : { webRequestCoordinator: context.webRequestCoordinator }),
415
+ ...(context.webFetchConfig === undefined ? {} : { webFetchConfig: context.webFetchConfig }),
413
416
  abortSignal: controller.signal,
414
417
  maxTurns,
415
418
  callId: toolCallId,
@@ -447,7 +450,7 @@ export function createAgentTool(subagents, context = {}) {
447
450
 
448
451
  const durationMs = Date.now() - startedAt;
449
452
  const outcome = classifyOutcome({ result, thrown, timedOut, abandoned });
450
- collector.finished({ status: outcome.status, durationMs });
453
+ collector.finished({ status: outcome.status, durationMs, result });
451
454
  // Each result is individually capped, but the parent's context sees the
452
455
  // SUM. The description encourages parallel calls, so twenty valid results
453
456
  // would otherwise land ~480KB in one batch. Later calls get whatever
@@ -598,6 +601,22 @@ function normalizeProfile(definition) {
598
601
 
599
602
  /** Hard cap on any single forwarded payload, so the operator wire's binary-search reducer never has to run. */
600
603
  const WIRE_CONTENT_MAX_CHARS = 2_000;
604
+ const ROUTE_HISTORY_MAX_ENTRIES = 32;
605
+
606
+ function boundedRouteString(value, max = 256) {
607
+ return typeof value === "string" && value.length > 0 ? value.slice(0, max) : undefined;
608
+ }
609
+
610
+ function boundedRouteIndex(value) {
611
+ return Number.isFinite(value) && value >= 0 ? Math.floor(value) : undefined;
612
+ }
613
+
614
+ function appendRouteEntry(entries, entry, routeState) {
615
+ entries.push(entry);
616
+ if (entries.length <= ROUTE_HISTORY_MAX_ENTRIES) return;
617
+ entries.splice(1, entries.length - ROUTE_HISTORY_MAX_ENTRIES);
618
+ routeState.truncated = true;
619
+ }
601
620
 
602
621
  /**
603
622
  * Translates the child's raw provider events into (a) a bounded per-tool-call
@@ -624,6 +643,7 @@ function createActivityCollector({ callId, profileName, callIndex, label, emit,
624
643
  /** What the child reported spending, so the parent run can own it. */
625
644
  const usage = emptyUsage();
626
645
  const subagent = { id: callId, name: profileName, callIndex, ...(label === undefined ? {} : { label }) };
646
+ const routeState = { requested: {}, attempted: undefined, transitions: [], retries: [], truncated: false };
627
647
 
628
648
  /** @param {*} event */
629
649
  const publish = (event) => {
@@ -646,11 +666,32 @@ function createActivityCollector({ callId, profileName, callIndex, label, emit,
646
666
  arguments: { name: profileName, ...(label === undefined ? {} : { description: label }) },
647
667
  });
648
668
  },
649
- /** @param {{status: string, durationMs: number}} outcome */
650
- finished({ status, durationMs }) {
669
+ /** @param {{status: string, durationMs: number, result?: *}} outcome */
670
+ finished({ status, durationMs, result }) {
651
671
  // Before the bookend, so the run's own usage report can already include
652
672
  // it, and so an abandoned child still hands over whatever it spent.
653
673
  recordUsage?.(usage);
674
+ const executed = status === "ok" && result && typeof result === "object"
675
+ ? {
676
+ ...(boundedRouteString(result.model) === undefined ? {} : { model: boundedRouteString(result.model) }),
677
+ ...(boundedRouteString(result.effort, 64) === undefined ? {} : { effort: boundedRouteString(result.effort, 64) }),
678
+ ...(boundedRouteString(result.effectiveEffort, 64) === undefined ? {} : { effectiveEffort: boundedRouteString(result.effectiveEffort, 64) }),
679
+ }
680
+ : undefined;
681
+ const requestedModel = routeState.requested.model;
682
+ const fallback = routeState.transitions.length > 0
683
+ || (executed?.model !== undefined && requestedModel !== undefined && executed.model !== requestedModel);
684
+ const attribution = requestedModel === undefined && routeState.attempted === undefined && executed === undefined
685
+ ? undefined
686
+ : {
687
+ requested: routeState.requested,
688
+ ...(routeState.attempted === undefined ? {} : { attempted: routeState.attempted }),
689
+ ...(executed === undefined ? {} : { executed }),
690
+ disposition: requestedModel === undefined ? "unknown" : fallback ? "fallback" : "requested",
691
+ transitions: routeState.transitions,
692
+ retries: routeState.retries,
693
+ ...(routeState.truncated ? { truncated: true } : {}),
694
+ };
654
695
  publish({
655
696
  phase: "agent_completed",
656
697
  id: `agent:${callId}`,
@@ -658,7 +699,13 @@ function createActivityCollector({ callId, profileName, callIndex, label, emit,
658
699
  // The one place the child's price is knowable per delegation: operator
659
700
  // surfaces show it on the row so an expensive one is identifiable, not
660
701
  // just visible in the run total it disappears into.
661
- ...(usage.costUsd > 0 ? { subagent: { ...subagent, costUsd: usage.costUsd } } : {}),
702
+ ...((usage.costUsd > 0 || attribution !== undefined)
703
+ ? { subagent: {
704
+ ...subagent,
705
+ ...(usage.costUsd > 0 ? { costUsd: usage.costUsd } : {}),
706
+ ...(attribution === undefined ? {} : { attribution }),
707
+ } }
708
+ : {}),
662
709
  isError: status !== "ok",
663
710
  executionMs: durationMs,
664
711
  content: `${status} · ${done.length} tool call${done.length === 1 ? "" : "s"}`,
@@ -681,6 +728,58 @@ function createActivityCollector({ callId, profileName, callIndex, label, emit,
681
728
  /** @param {*} event */
682
729
  observe(event) {
683
730
  const type = event?.type;
731
+ if (type === "provider_execution_config") {
732
+ const model = boundedRouteString(event.model);
733
+ const effort = boundedRouteString(event.effort, 64);
734
+ const effectiveEffort = boundedRouteString(event.effectiveEffort, 64);
735
+ routeState.attempted = {
736
+ ...(model === undefined ? {} : { model }),
737
+ ...(effort === undefined ? {} : { effort }),
738
+ ...(effectiveEffort === undefined ? {} : { effectiveEffort }),
739
+ };
740
+ if (routeState.requested.model === undefined && routeState.transitions.length === 0) {
741
+ routeState.requested = {
742
+ ...(model === undefined ? {} : { model }),
743
+ ...(effort === undefined ? {} : { effort }),
744
+ };
745
+ }
746
+ return;
747
+ }
748
+ if (type === "provider_failover_started") {
749
+ const from = boundedRouteString(event.from);
750
+ const to = boundedRouteString(event.to);
751
+ if (from !== undefined && to !== undefined) {
752
+ if (routeState.requested.model === undefined) routeState.requested = { model: from };
753
+ const attemptIndex = boundedRouteIndex(event.attemptIndex);
754
+ const reason = boundedRouteString(event.reason, 128);
755
+ appendRouteEntry(routeState.transitions, {
756
+ from,
757
+ to,
758
+ ...(attemptIndex === undefined ? {} : { attemptIndex }),
759
+ ...(reason === undefined ? {} : { reason }),
760
+ }, routeState);
761
+ routeState.attempted = { model: to };
762
+ }
763
+ return;
764
+ }
765
+ if (type === "provider_retry_started") {
766
+ const model = boundedRouteString(event.model);
767
+ const retryIndex = boundedRouteIndex(event.retryIndex);
768
+ const attempts = boundedRouteIndex(event.attempts);
769
+ const reason = boundedRouteString(event.reason, 128);
770
+ appendRouteEntry(routeState.retries, {
771
+ ...(model === undefined ? {} : { model }),
772
+ ...(retryIndex === undefined ? {} : { retryIndex }),
773
+ ...(attempts === undefined ? {} : { attempts }),
774
+ ...(reason === undefined ? {} : { reason }),
775
+ }, routeState);
776
+ return;
777
+ }
778
+ if (type === "provider_failover_completed") {
779
+ const model = boundedRouteString(event.model);
780
+ if (model !== undefined) routeState.attempted = { ...(routeState.attempted || {}), model };
781
+ return;
782
+ }
684
783
  // Providers legitimately forward multiple blocks in one message; the
685
784
  // Claude bridges do routinely. Inspecting only content[0] dropped every
686
785
  // tool call after the first.
@@ -14,22 +14,12 @@ import {
14
14
  DEFAULT_PROCESS_BUFFER_BYTES,
15
15
  runPreparedProcess,
16
16
  } from "./shared/process-runner.js";
17
+ import { cleanBashEnvironment } from "./shared/bash-environment.js";
17
18
  import { handOffProcessJob } from "./shared/process-jobs.js";
18
19
  import { readToolRuntime } from "./shared/runtime-context.js";
19
20
  import { requestToolProcessEnvironment, resolveSandboxPolicy } from "./shared/tool-context.js";
20
21
 
21
22
  const DEFAULT_BASH_TIMEOUT_MS = 120_000;
22
- const BASH_STARTUP_ENV_KEYS = new Set([
23
- "BASHOPTS",
24
- "BASH_COMPAT",
25
- "BASH_XTRACEFD",
26
- "CDPATH",
27
- "GLOBIGNORE",
28
- "POSIXLY_CORRECT",
29
- "PROMPT_COMMAND",
30
- "PS4",
31
- "SHELLOPTS",
32
- ]);
33
23
 
34
24
  /**
35
25
  * Legacy Bash timeout normalization. Values up to 600 are seconds; larger
@@ -82,7 +72,7 @@ export function normalizeBackgroundBashTimeoutMs(value) {
82
72
  /**
83
73
  * Compatibility wrapper retained for direct callers and tests.
84
74
  *
85
- * @param {{command: string, description?: string, timeout?: number, timeout_ms?: number, max_output_chars?: number, workdir?: string, background?: boolean}} params
75
+ * @param {{command: string, description?: string, timeout?: number, timeout_ms?: number, max_output_chars?: number, workdir?: string, background?: boolean, wake_on_completion?: boolean}} params
86
76
  * @param {{signal?: AbortSignal, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, processJobsController?: import("./shared/process-jobs.js").ProcessJobsController}} [options]
87
77
  */
88
78
  export async function bashToolImpl(params, options = {}) {
@@ -92,7 +82,7 @@ export async function bashToolImpl(params, options = {}) {
92
82
  /**
93
83
  * Structured Bash execution used by the Pi bridge.
94
84
  *
95
- * @param {{command: string, description?: string, timeout?: number, timeout_ms?: number, max_output_chars?: number, workdir?: string, background?: boolean}} params
85
+ * @param {{command: string, description?: string, timeout?: number, timeout_ms?: number, max_output_chars?: number, workdir?: string, background?: boolean, wake_on_completion?: boolean}} params
96
86
  * @param {{signal?: AbortSignal, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, processJobsController?: import("./shared/process-jobs.js").ProcessJobsController}} [options]
97
87
  */
98
88
  export async function bashToolRun(
@@ -104,6 +94,7 @@ export async function bashToolRun(
104
94
  max_output_chars,
105
95
  workdir,
106
96
  background,
97
+ wake_on_completion,
107
98
  },
108
99
  {
109
100
  signal,
@@ -114,6 +105,12 @@ export async function bashToolRun(
114
105
  } = {},
115
106
  ) {
116
107
  const startedAt = Date.now();
108
+ if (wake_on_completion !== undefined && (background !== true || typeof wake_on_completion !== "boolean")) {
109
+ return failed("Error: wake_on_completion requires background=true and a boolean value.", "process_job_invalid", startedAt);
110
+ }
111
+ if (background === true && !processJobsController) {
112
+ return failed("Error: Background process jobs are unavailable for this request.", "background_unsupported", startedAt);
113
+ }
117
114
  if (typeof command !== "string") {
118
115
  return failed("Error: Bash command must be a string.", "invalid_command", startedAt);
119
116
  }
@@ -168,6 +165,7 @@ export async function bashToolRun(
168
165
  prepared,
169
166
  summary: `Bash command (${command.length} characters; content redacted)`,
170
167
  description,
168
+ wakeOnCompletion: wake_on_completion,
171
169
  timeoutMs: requestedTimeoutMs,
172
170
  maxOutputChars: max_output_chars === undefined ? undefined : maxChars,
173
171
  startedAt,
@@ -260,19 +258,6 @@ export async function bashToolRun(
260
258
  return completed(partial, baseOutcome, maxChars, "Bash", resolvedCtx);
261
259
  }
262
260
 
263
- function cleanBashEnvironment() {
264
- const env = {
265
- BASH_ENV: "/dev/null",
266
- ENV: "/dev/null",
267
- };
268
- for (const key of Object.keys(process.env)) {
269
- if (BASH_STARTUP_ENV_KEYS.has(key) || key.startsWith("BASH_FUNC_")) {
270
- env[key] = undefined;
271
- }
272
- }
273
- return env;
274
- }
275
-
276
261
  function finitePositiveInteger(value, fallback) {
277
262
  const number = Number(value);
278
263
  return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;