@llblab/pi-actors 0.38.1 → 0.40.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.
- package/AGENTS.md +11 -4
- package/BACKLOG.md +2 -11
- package/CHANGELOG.md +51 -0
- package/README.md +120 -131
- package/dist/index.js +6 -4
- package/dist/lib/async-runs.d.ts +4 -0
- package/dist/lib/async-runs.js +112 -17
- package/dist/lib/command-templates.d.ts +9 -0
- package/dist/lib/command-templates.js +92 -11
- package/dist/lib/config.js +0 -5
- package/dist/lib/execution.d.ts +31 -0
- package/dist/lib/execution.js +145 -12
- package/dist/lib/file-state.d.ts +1 -0
- package/dist/lib/file-state.js +91 -3
- package/dist/lib/observability.d.ts +1 -1
- package/dist/lib/observability.js +7 -4
- package/dist/lib/pi.d.ts +1 -1
- package/dist/lib/pi.js +2 -2
- package/dist/lib/prompts.d.ts +1 -2
- package/dist/lib/prompts.js +2 -3
- package/dist/lib/recipes-context.js +17 -9
- package/dist/lib/recipes-discovery.js +11 -5
- package/dist/lib/recipes-references.d.ts +1 -1
- package/dist/lib/recipes-references.js +4 -5
- package/dist/lib/recipes-usage.d.ts +2 -0
- package/dist/lib/recipes-usage.js +35 -21
- package/dist/lib/registry.d.ts +0 -2
- package/dist/lib/registry.js +33 -10
- package/dist/lib/runs-ownership.d.ts +7 -0
- package/dist/lib/runs-ownership.js +82 -0
- package/dist/lib/runs-process.d.ts +17 -2
- package/dist/lib/runs-process.js +99 -11
- package/dist/lib/runs-retention.d.ts +3 -0
- package/dist/lib/runs-retention.js +18 -3
- package/dist/lib/runs-start.d.ts +2 -2
- package/dist/lib/runs-start.js +51 -17
- package/dist/lib/runs-status.d.ts +1 -1
- package/dist/lib/runs-status.js +8 -6
- package/dist/lib/runtime.js +69 -13
- package/dist/lib/tools-inspect.d.ts +2 -0
- package/dist/lib/tools-inspect.js +39 -2
- package/dist/lib/tools-register.js +0 -1
- package/dist/lib/tools-spawn.js +3 -2
- package/dist/lib/tools.d.ts +1 -0
- package/dist/lib/tools.js +3 -0
- package/dist/pi-actors/index.js +1 -0
- package/dist/recipes/subagent-judge.json +2 -1
- package/dist/recipes/subagent-merge.json +2 -1
- package/dist/recipes/subagent-normalize.json +2 -1
- package/dist/recipes/subagent-review-coordinator.json +1 -1
- package/dist/recipes/subagent-review.json +2 -1
- package/dist/recipes/subagent-verify.json +2 -1
- package/dist/scripts/async-runner.mjs +274 -6
- package/dist/scripts/build-dist.mjs +14 -1
- package/dist/skills/actors/SKILL.md +11 -7
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/actor-messages.md +1 -1
- package/docs/async-runs.md +14 -5
- package/docs/command-templates.md +4 -2
- package/docs/recipe-library.md +1 -0
- package/docs/template-recipes.md +5 -7
- package/docs/tool-registry.md +4 -2
- package/index.ts +18 -7
- package/lib/async-runs.ts +138 -19
- package/lib/command-templates.ts +132 -13
- package/lib/config.ts +0 -4
- package/lib/execution.ts +198 -13
- package/lib/file-state.ts +106 -3
- package/lib/observability.ts +11 -5
- package/lib/pi.ts +3 -3
- package/lib/prompts.ts +2 -4
- package/lib/recipes-context.ts +17 -9
- package/lib/recipes-discovery.ts +10 -5
- package/lib/recipes-references.ts +5 -6
- package/lib/recipes-usage.ts +36 -20
- package/lib/registry.ts +43 -13
- package/lib/runs-ownership.ts +117 -0
- package/lib/runs-process.ts +138 -16
- package/lib/runs-retention.ts +22 -2
- package/lib/runs-start.ts +89 -31
- package/lib/runs-status.ts +15 -6
- package/lib/runtime.ts +64 -12
- package/lib/tools-inspect.ts +46 -4
- package/lib/tools-register.ts +0 -3
- package/lib/tools-spawn.ts +5 -5
- package/lib/tools.ts +8 -0
- package/package.json +2 -2
- package/recipes/subagent-judge.json +2 -1
- package/recipes/subagent-merge.json +2 -1
- package/recipes/subagent-normalize.json +2 -1
- package/recipes/subagent-review-coordinator.json +1 -1
- package/recipes/subagent-review.json +2 -1
- package/recipes/subagent-verify.json +2 -1
- package/scripts/async-runner.mjs +274 -6
- package/scripts/build-dist.mjs +14 -1
- package/skills/actors/SKILL.md +11 -7
- package/skills/swarm/SKILL.md +1 -1
package/dist/lib/async-runs.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { type CurrentPolicyProvenance } from "./model-context.ts";
|
|
|
7
7
|
import * as RecipesReferences from "./recipes-references.ts";
|
|
8
8
|
import { type RunArtifactDeclaration } from "./runs-artifacts.ts";
|
|
9
9
|
import { type RunOutboxEvent } from "./runs-outbox.ts";
|
|
10
|
+
import { type RunProcessIdentity } from "./runs-process.ts";
|
|
10
11
|
import * as RunsIndex from "./runs-index.ts";
|
|
11
12
|
import { type ProcessRunInboxResult, type RunInboxMessage, type RunInboxStatus } from "./runs-mailbox.ts";
|
|
12
13
|
import { type SendRunMessageOptions } from "./runs-messages.ts";
|
|
@@ -35,6 +36,7 @@ export interface AsyncRunStartParams {
|
|
|
35
36
|
when?: boolean | string;
|
|
36
37
|
timeout?: number | string;
|
|
37
38
|
delay?: number | string;
|
|
39
|
+
accept_output?: "review_evidence";
|
|
38
40
|
output?: string;
|
|
39
41
|
artifacts?: Record<string, RunArtifactDeclaration>;
|
|
40
42
|
mailbox?: RecipesReferences.TemplateRecipeMailbox;
|
|
@@ -68,6 +70,7 @@ export interface AsyncRunMeta {
|
|
|
68
70
|
control?: AsyncRunControlEndpoint;
|
|
69
71
|
mailbox?: RecipesReferences.TemplateRecipeMailbox;
|
|
70
72
|
model_policy?: CurrentPolicyProvenance;
|
|
73
|
+
process_identity?: RunProcessIdentity;
|
|
71
74
|
recipe_context_records?: RecipesReferences.TemplateRecipeContextRecord[];
|
|
72
75
|
retire_when?: "children_terminal";
|
|
73
76
|
}
|
|
@@ -112,6 +115,7 @@ export type { SendRunMessageOptions } from "./runs-messages.ts";
|
|
|
112
115
|
export declare function sendRunMessage(runOrDir: string, message: string, options?: SendRunMessageOptions): Promise<Record<string, unknown>>;
|
|
113
116
|
export { getRunProcessSignalPlan } from "./runs-control.ts";
|
|
114
117
|
export type { RunProcessSignalPlan } from "./runs-control.ts";
|
|
118
|
+
export declare function markRunTerminalNotificationHandled(stateDir: string, status: string): void;
|
|
115
119
|
export declare function cancelRun(runOrDir: string): Record<string, unknown>;
|
|
116
120
|
export declare function archiveRun(runOrDir: string): Record<string, unknown>;
|
|
117
121
|
export declare function pruneRun(runOrDir: string, options?: {
|
package/dist/lib/async-runs.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Owns launch, state observation, listing, message/control facade methods, and retention while runs-* subdomains own narrower run internals.
|
|
4
4
|
*/
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
-
import { closeSync, existsSync, mkdirSync, openSync, writeFileSync, } from "node:fs";
|
|
7
|
-
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
6
|
+
import { closeSync, existsSync, mkdirSync, openSync, readdirSync, statSync, writeFileSync, } from "node:fs";
|
|
7
|
+
import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { writeJsonAtomic } from "./file-state.js";
|
|
10
10
|
import { CURRENT_MODEL_VALUE_KEY, CURRENT_THINKING_VALUE_KEY, describeCurrentPolicyProvenance, } from "./model-context.js";
|
|
@@ -15,8 +15,9 @@ import { resolveArtifactPaths, } from "./runs-artifacts.js";
|
|
|
15
15
|
import { safeRunId } from "./runs-identity.js";
|
|
16
16
|
import { buildTerminalProgress, markTerminalHandled, signalOwnedRunProcess, } from "./runs-control.js";
|
|
17
17
|
import { buildRunOutboxEventPayload, parseRunOutboxEventLine, } from "./runs-outbox.js";
|
|
18
|
+
import { claimRunStateDirectory } from "./runs-ownership.js";
|
|
18
19
|
import { archiveTerminalRun, pruneTerminalRun } from "./runs-retention.js";
|
|
19
|
-
import {
|
|
20
|
+
import { captureRunProcessIdentity, verifyRunProcessIdentity, } from "./runs-process.js";
|
|
20
21
|
import * as RunsStart from "./runs-start.js";
|
|
21
22
|
import * as RunsIndex from "./runs-index.js";
|
|
22
23
|
import { claimRunInboxMessageInStateDir, parseRunInboxLine, processRunInboxMessagesInStateDir, runInboxFile, updateRunInboxMessageStatusInStateDir, } from "./runs-mailbox.js";
|
|
@@ -57,6 +58,7 @@ function resolveRunTemplate(params) {
|
|
|
57
58
|
"when",
|
|
58
59
|
"timeout",
|
|
59
60
|
"delay",
|
|
61
|
+
"accept_output",
|
|
60
62
|
"output",
|
|
61
63
|
"retry",
|
|
62
64
|
"failure",
|
|
@@ -228,6 +230,7 @@ export function startRun(params, cwd) {
|
|
|
228
230
|
mkdirSync(stateDir, { recursive: true });
|
|
229
231
|
const releaseStartLock = acquireStateStartLock(stateDir);
|
|
230
232
|
try {
|
|
233
|
+
claimRunStateDirectory(stateDir, run);
|
|
231
234
|
assertNoActiveRunState(stateDir);
|
|
232
235
|
prepareStateDirForStart(stateDir);
|
|
233
236
|
const stdout = join(stateDir, "stdout.log");
|
|
@@ -289,6 +292,9 @@ export function startRun(params, cwd) {
|
|
|
289
292
|
closeSync(outFd);
|
|
290
293
|
closeSync(errFd);
|
|
291
294
|
meta.pid = child.pid ?? 0;
|
|
295
|
+
const processIdentity = captureRunProcessIdentity(meta.pid, cwd, stateDir, RUNNER_PATH);
|
|
296
|
+
if (processIdentity)
|
|
297
|
+
meta.process_identity = processIdentity;
|
|
292
298
|
writeJsonAtomic(join(stateDir, "run.json"), meta);
|
|
293
299
|
writeJsonAtomic(join(stateDir, "progress.json"), {
|
|
294
300
|
completed: 0,
|
|
@@ -384,14 +390,18 @@ export async function sendRunMessage(runOrDir, message, options = {}) {
|
|
|
384
390
|
const status = getRunStatus(runOrDir);
|
|
385
391
|
const stateDir = String(status.state_dir);
|
|
386
392
|
const run = String(status.run ?? runOrDir);
|
|
387
|
-
if (status.status !== "running")
|
|
388
|
-
throw new Error(`Run is not running: ${run}`);
|
|
389
393
|
const pid = Number(status.pid || 0);
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
394
|
+
const identity = verifyRunProcessIdentity(pid, status.process_identity);
|
|
395
|
+
if (status.status !== "running") {
|
|
396
|
+
if (identity.status === "owner_mismatch" ||
|
|
397
|
+
identity.status === "unsupported_proof") {
|
|
398
|
+
throw new Error(`Run process identity ${identity.status}: ${run}`);
|
|
399
|
+
}
|
|
400
|
+
throw new Error(`Run is not running: ${run}`);
|
|
401
|
+
}
|
|
402
|
+
if (!identity.valid) {
|
|
403
|
+
throw new Error(`Run process identity ${identity.status}: ${run}`);
|
|
404
|
+
}
|
|
395
405
|
return deliverRunMessage(status, run, stateDir, message, options);
|
|
396
406
|
}
|
|
397
407
|
export { getRunProcessSignalPlan } from "./runs-control.js";
|
|
@@ -402,26 +412,111 @@ function markTerminalProgress(stateDir, phase) {
|
|
|
402
412
|
: undefined;
|
|
403
413
|
writeJsonAtomic(join(stateDir, "progress.json"), buildTerminalProgress(progress, phase));
|
|
404
414
|
}
|
|
415
|
+
function finalizeInterruptedReviewEvidence(stateDir, phase, signal) {
|
|
416
|
+
const evidencePath = join(stateDir, "review-evidence.json");
|
|
417
|
+
const manifest = readJson(evidencePath);
|
|
418
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest))
|
|
419
|
+
return;
|
|
420
|
+
const record = manifest;
|
|
421
|
+
if (!Array.isArray(record.commands))
|
|
422
|
+
return;
|
|
423
|
+
const completedAt = new Date().toISOString();
|
|
424
|
+
const effectiveExitCode = signal === "SIGKILL" ? 137 : 143;
|
|
425
|
+
const commands = record.commands.map((command) => {
|
|
426
|
+
if (!command || typeof command !== "object" || Array.isArray(command)) {
|
|
427
|
+
return command;
|
|
428
|
+
}
|
|
429
|
+
const entry = command;
|
|
430
|
+
if (entry.status !== "running" || typeof entry.id !== "string")
|
|
431
|
+
return entry;
|
|
432
|
+
const captureDir = join(stateDir, "captures", entry.id);
|
|
433
|
+
const attempts = existsSync(captureDir)
|
|
434
|
+
? readdirSync(captureDir)
|
|
435
|
+
.filter((name) => /^attempt-\d+$/.test(name))
|
|
436
|
+
.sort()
|
|
437
|
+
.map((name, index) => {
|
|
438
|
+
const attemptDir = join(captureDir, name);
|
|
439
|
+
const stdoutFile = join(attemptDir, "stdout.log");
|
|
440
|
+
const stderrFile = join(attemptDir, "stderr.log");
|
|
441
|
+
return {
|
|
442
|
+
attempt: index + 1,
|
|
443
|
+
stdout: {
|
|
444
|
+
path: relative(stateDir, stdoutFile),
|
|
445
|
+
bytes: existsSync(stdoutFile) ? statSync(stdoutFile).size : 0,
|
|
446
|
+
},
|
|
447
|
+
stderr: {
|
|
448
|
+
path: relative(stateDir, stderrFile),
|
|
449
|
+
bytes: existsSync(stderrFile) ? statSync(stderrFile).size : 0,
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
})
|
|
453
|
+
: [];
|
|
454
|
+
return {
|
|
455
|
+
...entry,
|
|
456
|
+
status: phase,
|
|
457
|
+
completed_at: completedAt,
|
|
458
|
+
attempts,
|
|
459
|
+
effective_exit_code: effectiveExitCode,
|
|
460
|
+
killed: true,
|
|
461
|
+
...(entry.semantic_acceptance === "pending"
|
|
462
|
+
? { semantic_acceptance: "interrupted" }
|
|
463
|
+
: {}),
|
|
464
|
+
};
|
|
465
|
+
});
|
|
466
|
+
writeJsonAtomic(evidencePath, {
|
|
467
|
+
...record,
|
|
468
|
+
status: phase,
|
|
469
|
+
commands,
|
|
470
|
+
updated_at: completedAt,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
405
473
|
function stopRun(runOrDir, signal, event) {
|
|
406
474
|
const status = getRunStatus(runOrDir);
|
|
407
475
|
const pid = Number(status.pid || 0);
|
|
408
476
|
const stateDir = String(status.state_dir);
|
|
409
|
-
if (status.status !== "running")
|
|
477
|
+
if (status.status !== "running" && status.status !== "exited") {
|
|
410
478
|
return { stopped: false, reason: "not running", status };
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
if (
|
|
414
|
-
|
|
479
|
+
}
|
|
480
|
+
const identity = verifyRunProcessIdentity(pid, status.process_identity);
|
|
481
|
+
if (status.status === "exited") {
|
|
482
|
+
if (identity.status === "owner_mismatch" ||
|
|
483
|
+
identity.status === "unsupported_proof") {
|
|
484
|
+
return {
|
|
485
|
+
stopped: false,
|
|
486
|
+
reason: identity.status.replaceAll("_", " "),
|
|
487
|
+
process_identity_status: identity.status,
|
|
488
|
+
status,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
return { stopped: false, reason: "not running", status };
|
|
492
|
+
}
|
|
493
|
+
if (!identity.valid) {
|
|
494
|
+
return {
|
|
495
|
+
stopped: false,
|
|
496
|
+
reason: identity.status.replaceAll("_", " "),
|
|
497
|
+
process_identity_status: identity.status,
|
|
498
|
+
status,
|
|
499
|
+
};
|
|
415
500
|
}
|
|
416
501
|
const signalResult = signalOwnedRunProcess(pid, signal);
|
|
417
502
|
writeFileSync(join(stateDir, "events.jsonl"), `${JSON.stringify({ event, pid, signal, ...signalResult, ts: new Date().toISOString() })}\n`, { flag: "a" });
|
|
418
503
|
markTerminalHandled(stateDir, { event, signal });
|
|
419
|
-
if (event === "run.kill")
|
|
504
|
+
if (event === "run.kill") {
|
|
505
|
+
finalizeInterruptedReviewEvidence(stateDir, "killed", signal);
|
|
420
506
|
markTerminalProgress(stateDir, "killed");
|
|
421
|
-
|
|
507
|
+
}
|
|
508
|
+
if (event === "run.cancel") {
|
|
509
|
+
finalizeInterruptedReviewEvidence(stateDir, "cancelled", signal);
|
|
422
510
|
markTerminalProgress(stateDir, "cancelled");
|
|
511
|
+
}
|
|
423
512
|
return { stopped: true, pid, signal, ...signalResult, state_dir: stateDir };
|
|
424
513
|
}
|
|
514
|
+
export function markRunTerminalNotificationHandled(stateDir, status) {
|
|
515
|
+
markTerminalHandled(stateDir, {
|
|
516
|
+
event: "run.notification",
|
|
517
|
+
status,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
425
520
|
export function cancelRun(runOrDir) {
|
|
426
521
|
const result = stopRun(runOrDir, "SIGTERM", "run.cancel");
|
|
427
522
|
return Object.hasOwn(result, "stopped")
|
|
@@ -23,6 +23,7 @@ export interface CommandTemplateObjectConfig {
|
|
|
23
23
|
defaults?: Record<string, unknown>;
|
|
24
24
|
timeout?: number | string;
|
|
25
25
|
delay?: number | string;
|
|
26
|
+
accept_output?: "review_evidence";
|
|
26
27
|
output?: string;
|
|
27
28
|
retry?: number | string;
|
|
28
29
|
failure?: CommandTemplateFailureScope;
|
|
@@ -45,12 +46,20 @@ export interface CommandTemplateExecOptions {
|
|
|
45
46
|
stdin?: string;
|
|
46
47
|
killGrace?: number;
|
|
47
48
|
retry?: number;
|
|
49
|
+
captureDir?: string;
|
|
50
|
+
captureLimitBytes?: number;
|
|
48
51
|
}
|
|
49
52
|
export interface CommandTemplateExecResult {
|
|
50
53
|
stdout: string;
|
|
51
54
|
stderr: string;
|
|
52
55
|
code: number;
|
|
53
56
|
killed: boolean;
|
|
57
|
+
stdoutBytes?: number;
|
|
58
|
+
stderrBytes?: number;
|
|
59
|
+
stdoutFile?: string;
|
|
60
|
+
stderrFile?: string;
|
|
61
|
+
stdoutTruncated?: boolean;
|
|
62
|
+
stderrTruncated?: boolean;
|
|
54
63
|
}
|
|
55
64
|
export type CommandTemplateRiskLabel = "risk.shell" | "risk.eval" | "risk.broad_fs_write" | "risk.destructive_fs" | "risk.network" | "risk.external_side_effect" | "risk.long_running" | "risk.platform_specific" | "risk.secret_touching";
|
|
56
65
|
export type CommandTemplateExecCommand = (command: string, args: string[], options?: CommandTemplateExecOptions) => Promise<CommandTemplateExecResult>;
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* Owns portable command-template parsing, expansion, risk checks, retries, timeouts, and direct execution.
|
|
5
5
|
*/
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { appendFileSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { homedir, tmpdir } from "node:os";
|
|
9
|
+
import { isAbsolute, join, resolve as resolvePath } from "node:path";
|
|
10
|
+
const DEFAULT_COMMAND_CAPTURE_LIMIT_BYTES = 1024 * 1024;
|
|
9
11
|
const COMMAND_TEMPLATE_RISK_LABEL_ORDER = [
|
|
10
12
|
"risk.shell",
|
|
11
13
|
"risk.eval",
|
|
@@ -386,9 +388,9 @@ export function expandCommandTemplateExecutable(command, cwd) {
|
|
|
386
388
|
if (command === "~")
|
|
387
389
|
return homedir();
|
|
388
390
|
if (command.startsWith("~/"))
|
|
389
|
-
return
|
|
391
|
+
return resolvePath(homedir(), command.slice(2));
|
|
390
392
|
if (command.includes("/") && !isAbsolute(command))
|
|
391
|
-
return
|
|
393
|
+
return resolvePath(cwd, command);
|
|
392
394
|
return command;
|
|
393
395
|
}
|
|
394
396
|
function evaluateCommandTemplateExpression(expression, values) {
|
|
@@ -592,13 +594,67 @@ export async function execCommandTemplate(command, args, options = {}) {
|
|
|
592
594
|
killed: false,
|
|
593
595
|
};
|
|
594
596
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
595
|
-
const
|
|
597
|
+
const attemptOptions = options.captureDir
|
|
598
|
+
? {
|
|
599
|
+
...options,
|
|
600
|
+
captureDir: join(options.captureDir, `attempt-${String(attempt).padStart(3, "0")}`),
|
|
601
|
+
}
|
|
602
|
+
: options;
|
|
603
|
+
const result = await execCommandTemplateOnce(command, args, attemptOptions);
|
|
596
604
|
if (result.code === 0)
|
|
597
605
|
return result;
|
|
598
606
|
lastResult = result;
|
|
599
607
|
}
|
|
600
608
|
return lastResult;
|
|
601
609
|
}
|
|
610
|
+
function trimCaptureTail(value, limit) {
|
|
611
|
+
if (value.length <= limit)
|
|
612
|
+
return value;
|
|
613
|
+
let start = value.length - limit;
|
|
614
|
+
while (start < value.length && (value[start] & 0xc0) === 0x80)
|
|
615
|
+
start += 1;
|
|
616
|
+
return value.subarray(start);
|
|
617
|
+
}
|
|
618
|
+
function createBoundedCommandCapture(stream, limit, getCaptureDir, persistCompleteStream) {
|
|
619
|
+
let bytes = 0;
|
|
620
|
+
let content = Buffer.alloc(0);
|
|
621
|
+
let file = persistCompleteStream
|
|
622
|
+
? join(getCaptureDir(), `${stream}.log`)
|
|
623
|
+
: undefined;
|
|
624
|
+
if (file)
|
|
625
|
+
writeFileSync(file, Buffer.alloc(0));
|
|
626
|
+
let truncated = false;
|
|
627
|
+
return {
|
|
628
|
+
append(value) {
|
|
629
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
630
|
+
bytes += chunk.length;
|
|
631
|
+
if (bytes > limit)
|
|
632
|
+
truncated = true;
|
|
633
|
+
if (!file && bytes <= limit) {
|
|
634
|
+
content = Buffer.concat([content, chunk]);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (!file) {
|
|
638
|
+
file = join(getCaptureDir(), `${stream}.log`);
|
|
639
|
+
writeFileSync(file, content);
|
|
640
|
+
}
|
|
641
|
+
appendFileSync(file, chunk);
|
|
642
|
+
content = trimCaptureTail(Buffer.concat([content, chunk]), limit);
|
|
643
|
+
},
|
|
644
|
+
result() {
|
|
645
|
+
if (!file && persistCompleteStream) {
|
|
646
|
+
file = join(getCaptureDir(), `${stream}.log`);
|
|
647
|
+
writeFileSync(file, content);
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
bytes,
|
|
651
|
+
content: content.toString("utf8"),
|
|
652
|
+
...(file ? { file } : {}),
|
|
653
|
+
truncated,
|
|
654
|
+
};
|
|
655
|
+
},
|
|
656
|
+
};
|
|
657
|
+
}
|
|
602
658
|
function execCommandTemplateOnce(command, args, options = {}) {
|
|
603
659
|
return new Promise((resolve) => {
|
|
604
660
|
const proc = spawn(command, args, {
|
|
@@ -606,8 +662,20 @@ function execCommandTemplateOnce(command, args, options = {}) {
|
|
|
606
662
|
shell: false,
|
|
607
663
|
stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
608
664
|
});
|
|
609
|
-
|
|
610
|
-
let
|
|
665
|
+
const captureLimit = Math.max(1, options.captureLimitBytes ?? DEFAULT_COMMAND_CAPTURE_LIMIT_BYTES);
|
|
666
|
+
let captureDir;
|
|
667
|
+
const getCaptureDir = () => {
|
|
668
|
+
if (captureDir)
|
|
669
|
+
return captureDir;
|
|
670
|
+
captureDir = options.captureDir
|
|
671
|
+
? resolvePath(options.captureDir)
|
|
672
|
+
: mkdtempSync(join(tmpdir(), "pi-actors-command-"));
|
|
673
|
+
mkdirSync(captureDir, { recursive: true });
|
|
674
|
+
return captureDir;
|
|
675
|
+
};
|
|
676
|
+
const persistCompleteStreams = options.captureDir !== undefined;
|
|
677
|
+
const stdoutCapture = createBoundedCommandCapture("stdout", captureLimit, getCaptureDir, persistCompleteStreams);
|
|
678
|
+
const stderrCapture = createBoundedCommandCapture("stderr", captureLimit, getCaptureDir, persistCompleteStreams);
|
|
611
679
|
let killed = false;
|
|
612
680
|
let settled = false;
|
|
613
681
|
let timeoutId;
|
|
@@ -632,7 +700,20 @@ function execCommandTemplateOnce(command, args, options = {}) {
|
|
|
632
700
|
clearTimeout(killTimeoutId);
|
|
633
701
|
if (options.signal)
|
|
634
702
|
options.signal.removeEventListener("abort", killProcess);
|
|
635
|
-
|
|
703
|
+
const stdout = stdoutCapture.result();
|
|
704
|
+
const stderr = stderrCapture.result();
|
|
705
|
+
resolve({
|
|
706
|
+
stdout: stdout.content,
|
|
707
|
+
stderr: stderr.content,
|
|
708
|
+
code,
|
|
709
|
+
killed,
|
|
710
|
+
stdoutBytes: stdout.bytes,
|
|
711
|
+
stderrBytes: stderr.bytes,
|
|
712
|
+
...(stdout.file ? { stdoutFile: stdout.file } : {}),
|
|
713
|
+
...(stderr.file ? { stderrFile: stderr.file } : {}),
|
|
714
|
+
...(stdout.truncated ? { stdoutTruncated: true } : {}),
|
|
715
|
+
...(stderr.truncated ? { stderrTruncated: true } : {}),
|
|
716
|
+
});
|
|
636
717
|
};
|
|
637
718
|
if (options.signal) {
|
|
638
719
|
if (options.signal.aborted)
|
|
@@ -643,16 +724,16 @@ function execCommandTemplateOnce(command, args, options = {}) {
|
|
|
643
724
|
if (options.timeout !== undefined && options.timeout > 0)
|
|
644
725
|
timeoutId = setTimeout(killProcess, options.timeout);
|
|
645
726
|
proc.stdout?.on("data", (data) => {
|
|
646
|
-
|
|
727
|
+
stdoutCapture.append(data);
|
|
647
728
|
});
|
|
648
729
|
proc.stderr?.on("data", (data) => {
|
|
649
|
-
|
|
730
|
+
stderrCapture.append(data);
|
|
650
731
|
});
|
|
651
732
|
proc.stdin?.on("error", () => { });
|
|
652
733
|
if (options.stdin !== undefined)
|
|
653
734
|
proc.stdin?.end(options.stdin);
|
|
654
735
|
proc.on("error", (error) => {
|
|
655
|
-
|
|
736
|
+
stderrCapture.append(error instanceof Error ? error.message : String(error));
|
|
656
737
|
settle(1);
|
|
657
738
|
});
|
|
658
739
|
proc.on("close", (code) => {
|
package/dist/lib/config.js
CHANGED
|
@@ -24,8 +24,6 @@ export function serializeTools(source) {
|
|
|
24
24
|
entry.name = cfg.recipe.name;
|
|
25
25
|
if (cfg.recipe?.async !== undefined)
|
|
26
26
|
entry.async = cfg.recipe.async;
|
|
27
|
-
if (cfg.recipe?.state_dir)
|
|
28
|
-
entry.state_dir = cfg.recipe.state_dir;
|
|
29
27
|
if (cfg.recipe?.values)
|
|
30
28
|
entry.values = cfg.recipe.values;
|
|
31
29
|
if (cfg.template)
|
|
@@ -113,9 +111,6 @@ export function normalizeStoredTool(key, value, reservedToolNames) {
|
|
|
113
111
|
? {
|
|
114
112
|
name: recipeName,
|
|
115
113
|
...(typeof record.async === "boolean" ? { async: record.async } : {}),
|
|
116
|
-
...(typeof record.state_dir === "string" && record.state_dir.trim()
|
|
117
|
-
? { state_dir: record.state_dir.trim() }
|
|
118
|
-
: {}),
|
|
119
114
|
template,
|
|
120
115
|
...(record.values &&
|
|
121
116
|
typeof record.values === "object" &&
|
package/dist/lib/execution.d.ts
CHANGED
|
@@ -7,17 +7,33 @@ import * as CommandTemplates from "./command-templates.ts";
|
|
|
7
7
|
import type { RegisteredTool } from "./config.ts";
|
|
8
8
|
export interface ToolExecOptions {
|
|
9
9
|
actorRecipeContext?: CommandTemplates.CommandTemplateActorRecipeContext;
|
|
10
|
+
evidenceContext?: {
|
|
11
|
+
acceptOutput?: "review_evidence";
|
|
12
|
+
label?: string;
|
|
13
|
+
repeatIndex?: string;
|
|
14
|
+
};
|
|
10
15
|
cwd?: string;
|
|
11
16
|
signal?: AbortSignal;
|
|
12
17
|
stdin?: string;
|
|
13
18
|
timeout?: number;
|
|
14
19
|
retry?: number;
|
|
20
|
+
captureDir?: string;
|
|
21
|
+
captureLimitBytes?: number;
|
|
15
22
|
}
|
|
16
23
|
export interface ToolExecResult {
|
|
17
24
|
stdout: string;
|
|
18
25
|
stderr: string;
|
|
19
26
|
code: number;
|
|
20
27
|
killed: boolean;
|
|
28
|
+
stdoutBytes?: number;
|
|
29
|
+
stderrBytes?: number;
|
|
30
|
+
stdoutFile?: string;
|
|
31
|
+
stderrFile?: string;
|
|
32
|
+
evidenceRef?: string;
|
|
33
|
+
stdoutTruncated?: boolean;
|
|
34
|
+
stderrTruncated?: boolean;
|
|
35
|
+
/** Complete internal stdout for downstream pipeline stdin; never returned to model-facing output. */
|
|
36
|
+
pipelineStdout?: string;
|
|
21
37
|
}
|
|
22
38
|
export interface BranchReport {
|
|
23
39
|
code: number;
|
|
@@ -28,8 +44,14 @@ export interface BranchReport {
|
|
|
28
44
|
status: "done" | "failed" | "timeout";
|
|
29
45
|
stderr?: string;
|
|
30
46
|
stderrBytes: number;
|
|
47
|
+
stderrCapturedBytes: number;
|
|
48
|
+
stderrFile?: string;
|
|
49
|
+
stderrTruncated?: boolean;
|
|
31
50
|
stdout?: string;
|
|
32
51
|
stdoutBytes: number;
|
|
52
|
+
stdoutCapturedBytes: number;
|
|
53
|
+
stdoutFile?: string;
|
|
54
|
+
stdoutTruncated?: boolean;
|
|
33
55
|
}
|
|
34
56
|
export interface SoftQuorumReport {
|
|
35
57
|
coverage: number;
|
|
@@ -50,6 +72,14 @@ export interface RegisteredToolExecutionResult {
|
|
|
50
72
|
command: string;
|
|
51
73
|
fullOutputPath?: string;
|
|
52
74
|
killed: boolean;
|
|
75
|
+
stderrBytes?: number;
|
|
76
|
+
stderrCapturedBytes?: number;
|
|
77
|
+
stderrFile?: string;
|
|
78
|
+
stderrTruncated?: boolean;
|
|
79
|
+
stdoutBytes?: number;
|
|
80
|
+
stdoutCapturedBytes?: number;
|
|
81
|
+
stdoutFile?: string;
|
|
82
|
+
stdoutTruncated?: boolean;
|
|
53
83
|
nonCriticalFailures?: Array<{
|
|
54
84
|
code: number;
|
|
55
85
|
command: string;
|
|
@@ -64,4 +94,5 @@ export interface RegisteredToolExecutionResult {
|
|
|
64
94
|
};
|
|
65
95
|
}
|
|
66
96
|
export type RegisteredToolExec = (command: string, args: string[], options?: ToolExecOptions) => Promise<ToolExecResult>;
|
|
97
|
+
export declare function applyOutputAcceptancePolicy(result: ToolExecResult, output: string | undefined): Promise<ToolExecResult>;
|
|
67
98
|
export declare function executeRegisteredTool(cfg: RegisteredTool, params: Record<string, unknown>, exec: RegisteredToolExec, cwd: string, signal?: AbortSignal): Promise<RegisteredToolExecutionResult>;
|