@mono-agent/agent-runtime 0.15.3 → 0.15.4

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 (36) hide show
  1. package/README.md +43 -6
  2. package/package.json +5 -1
  3. package/src/agent/tools/agent-tool.js +859 -0
  4. package/src/agent/tools/bash.js +241 -123
  5. package/src/agent/tools/exec.js +238 -0
  6. package/src/agent/tools/index.js +10 -3
  7. package/src/agent/tools/node-repl.js +231 -95
  8. package/src/agent/tools/pi-bridge.js +115 -24
  9. package/src/agent/tools/shared/process-runner.js +162 -0
  10. package/src/agent/tools/shared/semaphore.js +73 -0
  11. package/src/agent/tools/web-browser-render.js +221 -0
  12. package/src/agent/tools/web-controller.js +160 -0
  13. package/src/agent/tools/web-fetch.js +653 -68
  14. package/src/agent/tools/web-search.js +568 -16
  15. package/src/ai/providers/pi-native/stream-subscriber.js +37 -0
  16. package/src/ai/providers/pi-native/turn-runner.js +60 -5
  17. package/src/ai/providers/pi-native.js +49 -5
  18. package/src/ai/runtime/router.js +302 -166
  19. package/src/ai/types.js +52 -1
  20. package/src/runtime.js +51 -1
  21. package/types/agent/tools/agent-tool.d.ts +60 -0
  22. package/types/agent/tools/bash.d.ts +55 -7
  23. package/types/agent/tools/exec.d.ts +53 -0
  24. package/types/agent/tools/index.d.ts +5 -3
  25. package/types/agent/tools/node-repl.d.ts +28 -3
  26. package/types/agent/tools/pi-bridge.d.ts +6 -2
  27. package/types/agent/tools/shared/process-runner.d.ts +33 -0
  28. package/types/agent/tools/shared/semaphore.d.ts +29 -0
  29. package/types/agent/tools/web-browser-render.d.ts +16 -0
  30. package/types/agent/tools/web-controller.d.ts +20 -0
  31. package/types/agent/tools/web-fetch.d.ts +74 -5
  32. package/types/agent/tools/web-search.d.ts +81 -5
  33. package/types/ai/providers/pi-native/turn-runner.d.ts +34 -2
  34. package/types/ai/providers/pi-native.d.ts +12 -0
  35. package/types/ai/runtime/router.d.ts +23 -3
  36. package/types/ai/types.d.ts +163 -1
@@ -15,6 +15,7 @@ import {
15
15
  initPiMcpTools,
16
16
  } from "../../../agent/tools/pi-bridge.js";
17
17
  import { createNodeReplController } from "../../../agent/tools/node-repl.js";
18
+ import { createWebToolController } from "../../../agent/tools/web-controller.js";
18
19
  import { readToolRuntime } from "../../../agent/tools/shared/runtime-context.js";
19
20
  import { formatLiveInputGuidance } from "../../live-input-prompt.js";
20
21
  import { appendStructuredOutputInstruction } from "./structured-output.js";
@@ -40,6 +41,7 @@ export async function buildTurnTools(runState, {
40
41
  resolved,
41
42
  onEvent,
42
43
  runtimeWarnings,
44
+ toolExecutionMode = "safe-parallel",
43
45
  }) {
44
46
  const onTruncate = (info) => {
45
47
  try {
@@ -73,6 +75,15 @@ export async function buildTurnTools(runState, {
73
75
  sandboxEngine,
74
76
  ctx: runCtx,
75
77
  });
78
+ const webController = capabilities.tool_use === false
79
+ ? null
80
+ : createWebToolController({
81
+ searchConfig: options.webSearchConfig,
82
+ fetchConfig: options.webFetchConfig,
83
+ sandboxPolicy: options.sandboxPolicy,
84
+ sandboxEngine,
85
+ ctx: runCtx,
86
+ });
76
87
 
77
88
  // REUSED custom pieces: built-in tool sandboxing + allowlist/bloat filter +
78
89
  // approval gates. These are identical to the legacy bridge.
@@ -109,6 +120,21 @@ export async function buildTurnTools(runState, {
109
120
  approvalManager,
110
121
  approvalModel: runtime.model?.id || runtime.model?.name || resolved.model,
111
122
  nodeReplController,
123
+ webController,
124
+ toolExecutionMode,
125
+ subagents: options.subagents,
126
+ // The child inherits the parent's route and workspace unless its profile
127
+ // pins a model; the tool closure reads these to build each child request.
128
+ subagentContext: {
129
+ model: options.model,
130
+ executionMode: options.executionMode,
131
+ cwd: options.cwd,
132
+ parentRunId: runCtx?.runId,
133
+ // Same policy + engine this turn's own tools are confined by, so a
134
+ // child is never less sandboxed than the parent that spawned it.
135
+ sandboxPolicy: options.sandboxPolicy,
136
+ sandboxEngine,
137
+ },
112
138
  ctx: runCtx,
113
139
  }));
114
140
 
@@ -143,14 +169,19 @@ export async function buildTurnTools(runState, {
143
169
 
144
170
  const tools = [
145
171
  ...builtIns,
146
- ...mcpInit.tools,
172
+ ...mcpInit.tools.map((tool) => ({ ...tool, executionMode: "sequential" })),
147
173
  ...(structuredTool ? [structuredTool] : []),
148
174
  ];
149
175
  return {
150
176
  tools,
151
177
  structuredTool,
152
178
  mcpClients: mcpInit.clients,
153
- closeRunTools: async () => { await nodeReplController?.close(); },
179
+ closeRunTools: async () => {
180
+ await Promise.allSettled([
181
+ nodeReplController?.close(),
182
+ webController?.close(),
183
+ ].filter(Boolean));
184
+ },
154
185
  };
155
186
  }
156
187
 
@@ -189,6 +220,26 @@ export function thinkingLevelForEffort(effort, capabilities) {
189
220
  * @param {any} params
190
221
  * @returns {any}
191
222
  */
223
+ /**
224
+ * Restore the error flag on a tool result pi resolved successfully.
225
+ *
226
+ * pi hardcodes `isError: false` for every `execute()` that returns rather than
227
+ * throws, so any tool that reports failure in its payload needs this hook or
228
+ * the model is told the call succeeded.
229
+ *
230
+ * @param {*} details Tool-result details recorded by the bridge.
231
+ * @returns {{isError: true}|undefined}
232
+ */
233
+ export function toolResultErrorOverride(details) {
234
+ const subagentStatus = details?.subagent?.status;
235
+ const failed = details?.mcp_result_is_error === true
236
+ || details?.outcome?.status === "error"
237
+ // A failed subagent returns its answer-plus-log instead of throwing,
238
+ // precisely so a failed delegation keeps its activity log.
239
+ || (typeof subagentStatus === "string" && subagentStatus !== "ok");
240
+ return failed ? { isError: true } : undefined;
241
+ }
242
+
192
243
  export function buildTurnHarness(runState, {
193
244
  cwd,
194
245
  session,
@@ -225,9 +276,13 @@ export function buildTurnHarness(runState, {
225
276
  // this after-tool hook restores the error flag while preserving the already
226
277
  // bounded content/details verbatim. Downstream tool_execution_end and timing
227
278
  // events therefore report the failure accurately.
228
- harness.on("tool_result", (event) => /** @type {any} */ (event?.details)?.mcp_result_is_error === true
229
- ? { isError: true }
230
- : undefined);
279
+ //
280
+ // A failed subagent is the same shape of problem: the `Agent` tool returns
281
+ // its formatted answer-plus-activity-log rather than throwing, precisely so a
282
+ // failed delegation keeps its log — but pi hardcodes `isError: false` for
283
+ // every resolved execute(), so without this the model would be told a failed,
284
+ // timed-out, or empty delegation succeeded.
285
+ harness.on("tool_result", (event) => toolResultErrorOverride(event?.details));
231
286
  runState.harness = harness;
232
287
 
233
288
  harness.subscribe(createStreamSubscriber(runState, {
@@ -78,6 +78,37 @@ import {
78
78
  } from "./pi-native/turn-runner.js";
79
79
  import { resolvePiTransport } from "./pi-native/transport.js";
80
80
 
81
+ /**
82
+ * Pi 0.80.6 exposes per-tool execution markers but not AgentHarness's global
83
+ * toolExecution option. Resolve mono-agent's programmatic mode once per run;
84
+ * individual tool builders then mark stateful/mutating tools sequential.
85
+ */
86
+ export function resolvePiToolExecutionMode(options = {}) {
87
+ const warnings = [];
88
+ const requested = options.piToolExecutionMode;
89
+ let mode = requested === "sequential" || requested === "safe-parallel"
90
+ ? requested
91
+ : null;
92
+ if (requested !== undefined && mode === null) {
93
+ warnings.push({
94
+ warning_kind: "invalid_pi_tool_execution_mode",
95
+ message: `Unknown piToolExecutionMode ${JSON.stringify(requested)}; using safe-parallel.`,
96
+ });
97
+ }
98
+ if (options.piToolParallelismMode !== undefined) {
99
+ warnings.push({
100
+ warning_kind: "deprecated_pi_tool_parallelism_mode",
101
+ message: "piToolParallelismMode is deprecated; use piToolExecutionMode (sequential or safe-parallel).",
102
+ });
103
+ if (mode === null) {
104
+ mode = options.piToolParallelismMode === "one-at-a-time"
105
+ ? "sequential"
106
+ : "safe-parallel";
107
+ }
108
+ }
109
+ return { mode: mode || "safe-parallel", warnings };
110
+ }
111
+
81
112
  async function resolveApiKey(provider, { apiKeys, resolvePiApiKey, runtimeWarnings }) {
82
113
  if (apiKeys?.has(provider)) return apiKeys.get(provider);
83
114
  if (typeof resolvePiApiKey !== "function") return undefined;
@@ -307,13 +338,21 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
307
338
  const piTransport = resolvePiTransport(options.piTransport);
308
339
 
309
340
  const onEvent = (event) => emitCaptured(events, options.onEvent, event);
341
+ const approvalRiskTiers = {
342
+ ...(options.toolRiskTiers || {}),
343
+ ...(
344
+ options.toolRiskTiers?.Exec === undefined && options.toolRiskTiers?.Bash !== undefined
345
+ ? { Exec: options.toolRiskTiers.Bash }
346
+ : {}
347
+ ),
348
+ };
310
349
  const approvalManager = options.onToolApprovalRequest
311
350
  ? createApprovalManager({
312
351
  onToolApprovalRequest: options.onToolApprovalRequest,
313
352
  defaultRiskTier: options.approvalDefaultRiskTier,
314
353
  timeoutMs: options.approvalTimeoutMs,
315
354
  onEvent,
316
- riskTiersByTool: options.toolRiskTiers,
355
+ riskTiersByTool: approvalRiskTiers,
317
356
  alwaysAllowTools: options.approvalAlwaysAllowTools,
318
357
  })
319
358
  : null;
@@ -396,6 +435,11 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
396
435
  // normalization. Restores configurable clamping (toolTextLimitChars,
397
436
  // searchResultLimit, ...) on top of the 256KB hard ceiling.
398
437
  const toolLimits = resolveAgentCompactionPolicy(settingsLike, runtime.model);
438
+ const toolExecution = resolvePiToolExecutionMode(options);
439
+ for (const warning of toolExecution.warnings) {
440
+ runtimeWarnings.push(warning);
441
+ onEvent({ type: "runtime_warning", ...warning });
442
+ }
399
443
 
400
444
  // Build the turn's tools (builtins + MCP bridge + StructuredOutput). The
401
445
  // StructuredOutput callback writes runState.structuredResult; the MCP clients
@@ -414,6 +458,7 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
414
458
  resolved,
415
459
  onEvent,
416
460
  runtimeWarnings,
461
+ toolExecutionMode: toolExecution.mode,
417
462
  });
418
463
  mcpClients = builtMcpClients;
419
464
  closeRunTools = builtCloseRunTools;
@@ -426,10 +471,9 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
426
471
  const maxRetryDelayMs = Number.isFinite(Number(options.maxRetryDelayMs))
427
472
  ? Number(options.maxRetryDelayMs)
428
473
  : 60_000;
429
- // Tool steering: default "one-at-a-time" (safe, deterministic ordering).
430
- // Opt-in "all" lets pi-agent-core run a model step's tool calls concurrently
431
- // (QueueMode). Only enable when tools in a step are independent.
432
- const toolSteeringMode = options.piToolParallelismMode === "all" ? "all" : "one-at-a-time";
474
+ // Steering controls user follow-up delivery, not tool scheduling. Keep it
475
+ // independent from piToolExecutionMode; tools carry their own executionMode.
476
+ const toolSteeringMode = "one-at-a-time";
433
477
 
434
478
  const piModels = buildRunModels(runtime, options, runtimeWarnings);
435
479