@mono-agent/agent-runtime 0.15.2 → 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.
- package/README.md +55 -7
- package/package.json +5 -1
- package/src/agent/tools/agent-tool.js +859 -0
- package/src/agent/tools/bash.js +241 -123
- package/src/agent/tools/exec.js +238 -0
- package/src/agent/tools/index.js +10 -3
- package/src/agent/tools/node-repl.js +231 -95
- package/src/agent/tools/pi-bridge.js +115 -24
- package/src/agent/tools/shared/process-runner.js +162 -0
- package/src/agent/tools/shared/semaphore.js +73 -0
- package/src/agent/tools/web-browser-render.js +221 -0
- package/src/agent/tools/web-controller.js +160 -0
- package/src/agent/tools/web-fetch.js +653 -68
- package/src/agent/tools/web-search.js +568 -16
- package/src/ai/providers/codex-app.js +18 -0
- package/src/ai/providers/pi-native/stream-subscriber.js +37 -0
- package/src/ai/providers/pi-native/turn-runner.js +60 -5
- package/src/ai/providers/pi-native.js +49 -5
- package/src/ai/runtime/router.js +302 -166
- package/src/ai/types.js +52 -1
- package/src/runtime.js +51 -1
- package/types/agent/tools/agent-tool.d.ts +60 -0
- package/types/agent/tools/bash.d.ts +55 -7
- package/types/agent/tools/exec.d.ts +53 -0
- package/types/agent/tools/index.d.ts +5 -3
- package/types/agent/tools/node-repl.d.ts +28 -3
- package/types/agent/tools/pi-bridge.d.ts +6 -2
- package/types/agent/tools/shared/process-runner.d.ts +33 -0
- package/types/agent/tools/shared/semaphore.d.ts +29 -0
- package/types/agent/tools/web-browser-render.d.ts +16 -0
- package/types/agent/tools/web-controller.d.ts +20 -0
- package/types/agent/tools/web-fetch.d.ts +74 -5
- package/types/agent/tools/web-search.d.ts +81 -5
- package/types/ai/backend.d.ts +57 -0
- package/types/ai/providers/pi-native/turn-runner.d.ts +34 -2
- package/types/ai/providers/pi-native.d.ts +12 -0
- package/types/ai/registry.d.ts +1 -0
- package/types/ai/runtime/router.d.ts +23 -3
- package/types/ai/types.d.ts +163 -1
|
@@ -7,14 +7,14 @@ import { passthroughSandbox } from "../sandbox-seam.js";
|
|
|
7
7
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
8
8
|
import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
bashToolRun,
|
|
11
11
|
editToolImpl,
|
|
12
|
+
execToolRun,
|
|
12
13
|
globToolImpl,
|
|
13
14
|
grepToolImpl,
|
|
14
15
|
normalizeBashTimeoutMs,
|
|
16
|
+
normalizeProcessTimeoutMs,
|
|
15
17
|
readToolImpl,
|
|
16
|
-
webFetchToolImpl,
|
|
17
|
-
webSearchToolImpl,
|
|
18
18
|
writeToolImpl,
|
|
19
19
|
} from "./index.js";
|
|
20
20
|
import {
|
|
@@ -28,6 +28,7 @@ import { wrapToolsWithApprovalGate } from "../approval.js";
|
|
|
28
28
|
import { isInsidePath } from "./shared/path-resolver.js";
|
|
29
29
|
import { readToolRuntime } from "./shared/runtime-context.js";
|
|
30
30
|
import { resolveSandboxPolicy } from "./shared/tool-context.js";
|
|
31
|
+
import { createAgentTool } from "./agent-tool.js";
|
|
31
32
|
|
|
32
33
|
function textResult(text, details = {}) {
|
|
33
34
|
return {
|
|
@@ -114,7 +115,7 @@ function withAbsolutePaths(name, params, cwd, ctx) {
|
|
|
114
115
|
const next = { ...(params || {}) };
|
|
115
116
|
if (["Read", "Write", "Edit"].includes(name)) next.file_path = absolutizePath(next.file_path, cwd);
|
|
116
117
|
if (["Glob", "Grep"].includes(name)) next.path = absolutizePath(next.path, cwd);
|
|
117
|
-
if (["Read", "Write", "Edit", "Glob", "Grep", "Bash"].includes(name)) {
|
|
118
|
+
if (["Read", "Write", "Edit", "Glob", "Grep", "Bash", "Exec"].includes(name)) {
|
|
118
119
|
next.workdir = normalizeWorkdir(next.workdir, cwd, ctx);
|
|
119
120
|
}
|
|
120
121
|
return next;
|
|
@@ -198,9 +199,14 @@ function withToolLimits(name, params, limits = {}) {
|
|
|
198
199
|
next.output_mode = next.output_mode || "files_with_matches";
|
|
199
200
|
delete next.max_matches;
|
|
200
201
|
}
|
|
201
|
-
if (name === "Bash") {
|
|
202
|
+
if (name === "Bash" || name === "Exec") {
|
|
203
|
+
const timeoutLimit = limits.bashTimeoutMs || DEFAULT_BASH_TIMEOUT_MS;
|
|
202
204
|
next.max_output_chars = limitedNumber(next.max_output_chars, limits.bashOutputLimitChars || limits.toolTextLimitChars || 20000);
|
|
203
|
-
next.
|
|
205
|
+
if (name === "Bash" && next.timeout_ms === undefined && next.timeout !== undefined) {
|
|
206
|
+
next.timeout = normalizeBashTimeoutMs(next.timeout, timeoutLimit);
|
|
207
|
+
} else {
|
|
208
|
+
next.timeout_ms = normalizeProcessTimeoutMs(next.timeout_ms, timeoutLimit);
|
|
209
|
+
}
|
|
204
210
|
}
|
|
205
211
|
return next;
|
|
206
212
|
}
|
|
@@ -238,21 +244,41 @@ function isReadOnlyShellCommand(command) {
|
|
|
238
244
|
].some((pattern) => pattern.test(text));
|
|
239
245
|
}
|
|
240
246
|
|
|
247
|
+
const ALWAYS_SEQUENTIAL_BUILTINS = new Set(["Write", "Edit", "Bash", "Exec", "NodeRepl"]);
|
|
248
|
+
const SENSITIVE_RESULT_PARAMS = new Set(["Bash", "Exec", "WebFetch", "WebSearch"]);
|
|
249
|
+
|
|
250
|
+
function isStructuredToolRun(value) {
|
|
251
|
+
return Boolean(value)
|
|
252
|
+
&& typeof value === "object"
|
|
253
|
+
&& typeof value.text === "string"
|
|
254
|
+
&& value.outcome
|
|
255
|
+
&& typeof value.outcome === "object";
|
|
256
|
+
}
|
|
257
|
+
|
|
241
258
|
/**
|
|
242
259
|
* @param {any} name
|
|
243
260
|
* @param {any} label
|
|
244
261
|
* @param {any} description
|
|
245
262
|
* @param {any} parameters
|
|
246
263
|
* @param {any} execute
|
|
247
|
-
* @param {{cwd?: any, onEvent?: (event: any) => void, toolLimits?: any, toolPolicy?: any, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any}} [options]
|
|
264
|
+
* @param {{cwd?: any, onEvent?: (event: any) => void, toolLimits?: any, toolPolicy?: any, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, forceSequential?: boolean}} [options]
|
|
248
265
|
*/
|
|
249
|
-
function createBuiltinTool(name, label, description, parameters, execute, {
|
|
266
|
+
function createBuiltinTool(name, label, description, parameters, execute, {
|
|
267
|
+
cwd,
|
|
268
|
+
onEvent,
|
|
269
|
+
toolLimits,
|
|
270
|
+
toolPolicy,
|
|
271
|
+
sandboxPolicy,
|
|
272
|
+
sandboxEngine,
|
|
273
|
+
ctx,
|
|
274
|
+
forceSequential = false,
|
|
275
|
+
} = {}) {
|
|
250
276
|
return {
|
|
251
277
|
name,
|
|
252
278
|
label,
|
|
253
279
|
description,
|
|
254
280
|
parameters,
|
|
255
|
-
executionMode:
|
|
281
|
+
executionMode: forceSequential || ALWAYS_SEQUENTIAL_BUILTINS.has(name) ? "sequential" : undefined,
|
|
256
282
|
async execute(toolCallId, params, signal) {
|
|
257
283
|
if (signal?.aborted) throw new Error("tool execution aborted");
|
|
258
284
|
const normalized = normalizePiBuiltinToolParams(name, params, { cwd, toolLimits, ctx });
|
|
@@ -268,9 +294,21 @@ function createBuiltinTool(name, label, description, parameters, execute, { cwd,
|
|
|
268
294
|
if (isImageToolResult(raw)) {
|
|
269
295
|
return imageResult(raw.data, raw.mimeType, { tool: name, params: normalized });
|
|
270
296
|
}
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
297
|
+
const structured = isStructuredToolRun(raw) ? raw : null;
|
|
298
|
+
const text = structured ? structured.text : toolText(raw);
|
|
299
|
+
if (!structured && isErrorText(text)) throw new Error(text);
|
|
300
|
+
if (structured?.outcome?.legacyTimeoutUsed) {
|
|
301
|
+
onEvent?.({
|
|
302
|
+
type: "runtime_warning",
|
|
303
|
+
warning_kind: "deprecated_bash_timeout",
|
|
304
|
+
message: "Bash.timeout is deprecated; use timeout_ms for exact millisecond semantics.",
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
const details = /** @type {any} */ ({
|
|
308
|
+
tool: name,
|
|
309
|
+
...(SENSITIVE_RESULT_PARAMS.has(name) ? {} : { params: normalized }),
|
|
310
|
+
...(structured ? { outcome: structured.outcome } : {}),
|
|
311
|
+
});
|
|
274
312
|
if (shouldTrackWrite) {
|
|
275
313
|
details.file_change = writeFileChangeDetails(normalized.file_path, beforeWrite, readFileChangeSnapshot(normalized.file_path));
|
|
276
314
|
}
|
|
@@ -379,7 +417,7 @@ export function createStructuredOutputTool(outputSchema, onStructuredOutput) {
|
|
|
379
417
|
|
|
380
418
|
/**
|
|
381
419
|
* @param {any} allowedTools
|
|
382
|
-
* @param {{disallowedTools?: any[], skillNames?: any[], skills?: any[], skillsRoot?: any, dataDir?: any, cwd?: any, onEvent?: (event: any) => void, toolLimits?: any, persistArtifact?: any, onTruncate?: any, toolPayloadMaxBytes?: number, imageInlineMaxBytes?: any, toolPolicy?: any, sandboxPolicy?: any, sandboxEngine?: any, approvalManager?: any, approvalModel?: any, nodeReplController?: any, ctx?: any}} [options]
|
|
420
|
+
* @param {{disallowedTools?: any[], skillNames?: any[], skills?: any[], skillsRoot?: any, dataDir?: any, cwd?: any, onEvent?: (event: any) => void, toolLimits?: any, persistArtifact?: any, onTruncate?: any, toolPayloadMaxBytes?: number, imageInlineMaxBytes?: any, toolPolicy?: any, sandboxPolicy?: any, sandboxEngine?: any, approvalManager?: any, approvalModel?: any, nodeReplController?: any, webController?: any, toolExecutionMode?: "sequential"|"safe-parallel", subagents?: any, subagentContext?: any, ctx?: any}} [options]
|
|
383
421
|
*/
|
|
384
422
|
export function getPiBuiltinTools(allowedTools, {
|
|
385
423
|
disallowedTools = [],
|
|
@@ -400,17 +438,35 @@ export function getPiBuiltinTools(allowedTools, {
|
|
|
400
438
|
approvalManager = null,
|
|
401
439
|
approvalModel = null,
|
|
402
440
|
nodeReplController = null,
|
|
441
|
+
webController = null,
|
|
442
|
+
subagents = null,
|
|
443
|
+
subagentContext = null,
|
|
444
|
+
toolExecutionMode = "safe-parallel",
|
|
403
445
|
ctx = null,
|
|
404
446
|
} = {}) {
|
|
405
447
|
const textLimitSchema = integerSchema();
|
|
406
448
|
const bashLimitSchema = integerSchema();
|
|
407
|
-
const
|
|
449
|
+
const legacyBashTimeoutSchema = {
|
|
450
|
+
type: "integer",
|
|
451
|
+
description: "Deprecated compatibility timeout. Values up to 600 mean seconds and larger values mean milliseconds; use timeout_ms instead.",
|
|
452
|
+
};
|
|
453
|
+
const processTimeoutSchema = {
|
|
408
454
|
type: "integer",
|
|
409
|
-
|
|
455
|
+
minimum: 1,
|
|
456
|
+
description: "Exact timeout in milliseconds.",
|
|
410
457
|
};
|
|
411
458
|
// Per-tool closure config (cwd/event sink/limits/policy) plus the per-instance
|
|
412
459
|
// ToolContext `ctx` that the tool impls and shared helpers read from.
|
|
413
|
-
const toolContext = {
|
|
460
|
+
const toolContext = {
|
|
461
|
+
cwd,
|
|
462
|
+
onEvent,
|
|
463
|
+
toolLimits,
|
|
464
|
+
toolPolicy,
|
|
465
|
+
sandboxPolicy,
|
|
466
|
+
sandboxEngine,
|
|
467
|
+
forceSequential: toolExecutionMode === "sequential",
|
|
468
|
+
ctx,
|
|
469
|
+
};
|
|
414
470
|
const all = {
|
|
415
471
|
Read: createBuiltinTool("Read", "Read", "Read a local file. Text files return line-numbered content; image files (PNG, JPEG, GIF, WebP, BMP) are returned as a viewable image you can see directly — use this to look at image attachments.", objectSchema({
|
|
416
472
|
file_path: { type: "string" },
|
|
@@ -451,32 +507,64 @@ export function getPiBuiltinTools(allowedTools, {
|
|
|
451
507
|
max_matches: { type: "integer" },
|
|
452
508
|
max_output_chars: textLimitSchema,
|
|
453
509
|
}, ["pattern"]), grepToolImpl, toolContext),
|
|
454
|
-
Bash: createBuiltinTool("Bash", "Bash", "Execute a shell command
|
|
510
|
+
Bash: createBuiltinTool("Bash", "Bash", "Execute a shell command for pipelines, redirection, conditionals, or other shell syntax. Prefer Exec for one executable with an argv array. This is macOS: do not assume GNU-only commands or flags.", objectSchema({
|
|
455
511
|
command: { type: "string" },
|
|
456
512
|
workdir: { type: "string" },
|
|
457
513
|
description: { type: "string" },
|
|
458
|
-
|
|
514
|
+
timeout_ms: processTimeoutSchema,
|
|
515
|
+
timeout: legacyBashTimeoutSchema,
|
|
516
|
+
max_output_chars: bashLimitSchema,
|
|
517
|
+
}, ["command"]), bashToolRun, toolContext),
|
|
518
|
+
Exec: createBuiltinTool("Exec", "Exec", "Execute one program directly from an argv array without shell parsing. Prefer this for ordinary commands; use Bash only when shell syntax is required.", objectSchema({
|
|
519
|
+
executable: { type: "string", minLength: 1 },
|
|
520
|
+
args: { type: "array", items: { type: "string" }, maxItems: 256 },
|
|
521
|
+
workdir: { type: "string" },
|
|
522
|
+
timeout_ms: processTimeoutSchema,
|
|
459
523
|
max_output_chars: bashLimitSchema,
|
|
460
|
-
}, ["
|
|
524
|
+
}, ["executable"]), execToolRun, toolContext),
|
|
461
525
|
NodeRepl: nodeReplController
|
|
462
526
|
? createBuiltinTool(
|
|
463
527
|
"NodeRepl",
|
|
464
528
|
"Node REPL",
|
|
465
529
|
"Evaluate JavaScript in a run-scoped Node.js REPL. Variables persist across NodeRepl calls in this run.",
|
|
466
530
|
objectSchema({ code: { type: "string", minLength: 1 } }, ["code"]),
|
|
467
|
-
(params, { signal }) => nodeReplController.
|
|
531
|
+
(params, { signal }) => typeof nodeReplController.executeDetailed === "function"
|
|
532
|
+
? nodeReplController.executeDetailed(params, { signal })
|
|
533
|
+
: nodeReplController.execute(params, { signal }),
|
|
468
534
|
toolContext,
|
|
469
535
|
)
|
|
470
536
|
: null,
|
|
471
|
-
|
|
537
|
+
// Built directly (not via createBuiltinTool) so a subagent answer starting
|
|
538
|
+
// with "Error:" is not reclassified as a tool failure, discarding its log.
|
|
539
|
+
Agent: createAgentTool(subagents, { onEvent, ...(subagentContext || {}) }),
|
|
540
|
+
WebFetch: createBuiltinTool("WebFetch", "Web Fetch", "Fetch and extract one HTTP(S) URL locally. Static extraction is preferred; browser rendering is available only through the configured render policy.", objectSchema({
|
|
472
541
|
url: { type: "string" },
|
|
473
542
|
headers: { type: "object", additionalProperties: { type: "string" } },
|
|
474
543
|
max_output_chars: textLimitSchema,
|
|
475
|
-
|
|
476
|
-
|
|
544
|
+
format: { type: "string", enum: ["markdown", "text", "raw"] },
|
|
545
|
+
render: { type: "string", enum: ["never", "auto", "always"] },
|
|
546
|
+
}, ["url"]), webController
|
|
547
|
+
? (params, execution) => webController.fetch(params, execution)
|
|
548
|
+
: async () => ({
|
|
549
|
+
text: "Error: WebFetch controller is unavailable.",
|
|
550
|
+
outcome: { status: "error", code: "controller_unavailable", retryable: false, attempts: 0 },
|
|
551
|
+
error: true,
|
|
552
|
+
}), toolContext),
|
|
553
|
+
WebSearch: createBuiltinTool("WebSearch", "Web Search", "Search the public web with an operator-owned SearXNG endpoint when available, otherwise a keyless fallback, and return deduplicated ranked results.", objectSchema({
|
|
477
554
|
query: { type: "string" },
|
|
478
555
|
limit: { type: "integer" },
|
|
479
|
-
|
|
556
|
+
alternate_queries: { type: "array", items: { type: "string" }, maxItems: 3 },
|
|
557
|
+
domains: { type: "array", items: { type: "string" } },
|
|
558
|
+
exclude_domains: { type: "array", items: { type: "string" } },
|
|
559
|
+
language: { type: "string" },
|
|
560
|
+
time_range: { type: "string", enum: ["day", "month", "year"] },
|
|
561
|
+
}, ["query"]), webController
|
|
562
|
+
? (params, execution) => webController.search(params, execution)
|
|
563
|
+
: async () => ({
|
|
564
|
+
text: "Error: WebSearch controller is unavailable.",
|
|
565
|
+
outcome: { status: "error", code: "controller_unavailable", retryable: false, attempts: 0 },
|
|
566
|
+
error: true,
|
|
567
|
+
}), toolContext),
|
|
480
568
|
};
|
|
481
569
|
// allowedTools honors the `"*"` allow-all sentinel (and undefined) as "every
|
|
482
570
|
// built-in"; disallowedTools is the deny-wins filter applied to the final set.
|
|
@@ -489,6 +577,9 @@ export function getPiBuiltinTools(allowedTools, {
|
|
|
489
577
|
// Deny-check the canonical PascalCase name AND the legacy snake_case alias so
|
|
490
578
|
// an old denylist keeps disabling the tool after the rename.
|
|
491
579
|
if (skillTool && !denied.has("ReadSkill") && !denied.has("read_skill" /* legacy alias */)) tools.push(skillTool);
|
|
580
|
+
if (toolExecutionMode === "sequential") {
|
|
581
|
+
for (const tool of tools) tool.executionMode = "sequential";
|
|
582
|
+
}
|
|
492
583
|
const gated = approvalManager
|
|
493
584
|
? wrapToolsWithApprovalGate(tools, approvalManager, { model: approvalModel })
|
|
494
585
|
: tools;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_PROCESS_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
6
|
+
const KILL_GRACE_MS = 1_000;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Run one already-prepared executable without adding a shell.
|
|
10
|
+
*
|
|
11
|
+
* The result is deliberately loss-aware: stdout/stderr are retained up to the
|
|
12
|
+
* shared byte cap even when the child times out, is aborted, exits by signal,
|
|
13
|
+
* or exceeds that cap.
|
|
14
|
+
*
|
|
15
|
+
* @param {{command: string, args?: string[], cwd?: string, env?: Record<string, string|undefined>}} commandSpec
|
|
16
|
+
* @param {{timeoutMs?: number, signal?: AbortSignal, maxBufferBytes?: number}} [options]
|
|
17
|
+
*/
|
|
18
|
+
export function runPreparedProcess(
|
|
19
|
+
commandSpec,
|
|
20
|
+
{
|
|
21
|
+
timeoutMs,
|
|
22
|
+
signal,
|
|
23
|
+
maxBufferBytes = DEFAULT_PROCESS_BUFFER_BYTES,
|
|
24
|
+
} = {},
|
|
25
|
+
) {
|
|
26
|
+
const startedAt = Date.now();
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
let child;
|
|
29
|
+
try {
|
|
30
|
+
const env = commandSpec.env ? mergedProcessEnv(commandSpec.env) : process.env;
|
|
31
|
+
child = spawn(commandSpec.command, commandSpec.args || [], {
|
|
32
|
+
cwd: commandSpec.cwd,
|
|
33
|
+
detached: process.platform !== "win32",
|
|
34
|
+
env,
|
|
35
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
36
|
+
});
|
|
37
|
+
} catch (error) {
|
|
38
|
+
resolve({
|
|
39
|
+
code: null,
|
|
40
|
+
signal: null,
|
|
41
|
+
stdout: "",
|
|
42
|
+
stderr: "",
|
|
43
|
+
aborted: false,
|
|
44
|
+
timedOut: false,
|
|
45
|
+
bufferExceeded: false,
|
|
46
|
+
truncated: false,
|
|
47
|
+
bytes: 0,
|
|
48
|
+
storedBytes: 0,
|
|
49
|
+
spawnError: error,
|
|
50
|
+
durationMs: Date.now() - startedAt,
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const stdout = [];
|
|
56
|
+
const stderr = [];
|
|
57
|
+
const state = {
|
|
58
|
+
aborted: false,
|
|
59
|
+
bufferExceeded: false,
|
|
60
|
+
bytes: 0,
|
|
61
|
+
storedBytes: 0,
|
|
62
|
+
spawnError: null,
|
|
63
|
+
timedOut: false,
|
|
64
|
+
truncated: false,
|
|
65
|
+
};
|
|
66
|
+
let killTimer = null;
|
|
67
|
+
let timeoutTimer = null;
|
|
68
|
+
let settled = false;
|
|
69
|
+
|
|
70
|
+
function terminate() {
|
|
71
|
+
killProcessGroup(child, "SIGTERM");
|
|
72
|
+
if (killTimer === null) {
|
|
73
|
+
killTimer = setTimeout(() => killProcessGroup(child, "SIGKILL"), KILL_GRACE_MS);
|
|
74
|
+
killTimer.unref?.();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function append(target, chunk) {
|
|
79
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
80
|
+
state.bytes += buffer.length;
|
|
81
|
+
const remaining = Math.max(0, maxBufferBytes - state.storedBytes);
|
|
82
|
+
if (remaining > 0) {
|
|
83
|
+
const stored = buffer.length > remaining ? buffer.subarray(0, remaining) : buffer;
|
|
84
|
+
target.push(stored);
|
|
85
|
+
state.storedBytes += stored.length;
|
|
86
|
+
}
|
|
87
|
+
if (buffer.length > remaining) {
|
|
88
|
+
state.bufferExceeded = true;
|
|
89
|
+
state.truncated = true;
|
|
90
|
+
terminate();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (Number.isFinite(timeoutMs) && Number(timeoutMs) > 0) {
|
|
95
|
+
timeoutTimer = setTimeout(() => {
|
|
96
|
+
state.timedOut = true;
|
|
97
|
+
terminate();
|
|
98
|
+
}, Number(timeoutMs));
|
|
99
|
+
timeoutTimer.unref?.();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const onAbort = () => {
|
|
103
|
+
state.aborted = true;
|
|
104
|
+
terminate();
|
|
105
|
+
};
|
|
106
|
+
if (signal?.aborted) onAbort();
|
|
107
|
+
else signal?.addEventListener?.("abort", onAbort, { once: true });
|
|
108
|
+
|
|
109
|
+
child.stdout?.on("data", (chunk) => append(stdout, chunk));
|
|
110
|
+
child.stderr?.on("data", (chunk) => append(stderr, chunk));
|
|
111
|
+
child.once("error", (error) => {
|
|
112
|
+
state.spawnError = error;
|
|
113
|
+
});
|
|
114
|
+
child.once("close", (code, closeSignal) => {
|
|
115
|
+
if (settled) return;
|
|
116
|
+
settled = true;
|
|
117
|
+
if (timeoutTimer !== null) clearTimeout(timeoutTimer);
|
|
118
|
+
if (killTimer !== null) clearTimeout(killTimer);
|
|
119
|
+
signal?.removeEventListener?.("abort", onAbort);
|
|
120
|
+
resolve({
|
|
121
|
+
code,
|
|
122
|
+
signal: closeSignal,
|
|
123
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
124
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
125
|
+
...state,
|
|
126
|
+
durationMs: Date.now() - startedAt,
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function mergedProcessEnv(overrides) {
|
|
133
|
+
const env = { ...process.env };
|
|
134
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
135
|
+
if (value === undefined) delete env[key];
|
|
136
|
+
else env[key] = value;
|
|
137
|
+
}
|
|
138
|
+
return env;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {import("node:child_process").ChildProcess} child
|
|
143
|
+
* @param {NodeJS.Signals} signal
|
|
144
|
+
*/
|
|
145
|
+
export function killProcessGroup(child, signal) {
|
|
146
|
+
if (!child?.pid) return;
|
|
147
|
+
try {
|
|
148
|
+
process.kill(process.platform === "win32" ? child.pid : -child.pid, signal);
|
|
149
|
+
} catch {
|
|
150
|
+
try { process.kill(child.pid, signal); } catch { /* already gone */ }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {{stdout?: string, stderr?: string}} result
|
|
156
|
+
*/
|
|
157
|
+
export function combinedProcessOutput(result) {
|
|
158
|
+
const stdout = String(result.stdout || "");
|
|
159
|
+
const stderr = String(result.stderr || "");
|
|
160
|
+
if (stdout && stderr) return `STDOUT:\n${stdout}\nSTDERR:\n${stderr}`;
|
|
161
|
+
return stdout || stderr || "(no output)";
|
|
162
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// FIFO counting semaphore for run-scoped tool concurrency.
|
|
2
|
+
//
|
|
3
|
+
// agent-harness owns the equivalent helper for provider-execution width, but
|
|
4
|
+
// the kernel cannot depend on the harness, so the shape is duplicated here for
|
|
5
|
+
// the in-process tools that need to bound their own fan-out.
|
|
6
|
+
|
|
7
|
+
// @ts-check
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} CountingSemaphore
|
|
11
|
+
* @property {(signal?: AbortSignal) => Promise<() => void>} acquire Resolves with
|
|
12
|
+
* a single-use release function once a slot is free. Rejects if `signal`
|
|
13
|
+
* aborts while queued; an already-acquired slot is never leaked.
|
|
14
|
+
* @property {() => number} inFlight Slots currently held.
|
|
15
|
+
* @property {() => number} queued Waiters not yet admitted.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {number} limit Maximum simultaneous holders. Values below 1 are clamped.
|
|
20
|
+
* @returns {CountingSemaphore}
|
|
21
|
+
*/
|
|
22
|
+
export function createCountingSemaphore(limit) {
|
|
23
|
+
const max = Number.isInteger(limit) && limit > 0 ? limit : 1;
|
|
24
|
+
let active = 0;
|
|
25
|
+
/** @type {Array<{resolve: (release: () => void) => void, reject: (error: Error) => void, settled: boolean}>} */
|
|
26
|
+
const waiters = [];
|
|
27
|
+
|
|
28
|
+
const release = () => {
|
|
29
|
+
// A release function is single-use: a double call would hand out a slot the
|
|
30
|
+
// holder no longer owns and let the limit drift upward for the whole run.
|
|
31
|
+
let released = false;
|
|
32
|
+
return () => {
|
|
33
|
+
if (released) return;
|
|
34
|
+
released = true;
|
|
35
|
+
active -= 1;
|
|
36
|
+
pump();
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const pump = () => {
|
|
41
|
+
while (active < max && waiters.length > 0) {
|
|
42
|
+
const waiter = /** @type {*} */ (waiters.shift());
|
|
43
|
+
if (waiter.settled) continue;
|
|
44
|
+
waiter.settled = true;
|
|
45
|
+
active += 1;
|
|
46
|
+
waiter.resolve(release());
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
acquire(signal) {
|
|
52
|
+
if (signal?.aborted) {
|
|
53
|
+
return Promise.reject(new Error("tool execution aborted"));
|
|
54
|
+
}
|
|
55
|
+
if (active < max) {
|
|
56
|
+
active += 1;
|
|
57
|
+
return Promise.resolve(release());
|
|
58
|
+
}
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
/** @type {*} */
|
|
61
|
+
const waiter = { resolve, reject, settled: false };
|
|
62
|
+
waiters.push(waiter);
|
|
63
|
+
signal?.addEventListener("abort", () => {
|
|
64
|
+
if (waiter.settled) return;
|
|
65
|
+
waiter.settled = true;
|
|
66
|
+
reject(new Error("tool execution aborted"));
|
|
67
|
+
}, { once: true });
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
inFlight: () => active,
|
|
71
|
+
queued: () => waiters.filter((waiter) => !waiter.settled).length,
|
|
72
|
+
};
|
|
73
|
+
}
|