@llblab/pi-actors 0.39.0 → 0.40.1
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 +10 -3
- package/BACKLOG.md +4 -11
- package/CHANGELOG.md +51 -0
- package/README.md +5 -5
- 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.js +5 -2
- package/dist/lib/prompts.d.ts +1 -2
- package/dist/lib/prompts.js +3 -4
- 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 +15 -8
- package/dist/skills/swarm/SKILL.md +4 -2
- 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 +8 -2
- package/lib/prompts.ts +3 -5
- 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 +15 -8
- package/skills/swarm/SKILL.md +4 -2
|
@@ -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>;
|
package/dist/lib/execution.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Zones: tool execution, command templates, output formatting
|
|
4
4
|
* Owns command-template invocation execution and pi tool-result payload formatting
|
|
5
5
|
*/
|
|
6
|
+
import { readFile } from "node:fs/promises";
|
|
6
7
|
import * as CommandTemplates from "./command-templates.js";
|
|
7
8
|
import { formatFailureOutput, formatOutput, formatToolText, } from "./execution-output.js";
|
|
8
9
|
import * as Schema from "./schema.js";
|
|
@@ -62,6 +63,63 @@ function getBranchStatus(result) {
|
|
|
62
63
|
return "done";
|
|
63
64
|
return result.killed ? "timeout" : "failed";
|
|
64
65
|
}
|
|
66
|
+
const REVIEW_RESULT_MARKER = "ACTOR_REVIEW_RESULT";
|
|
67
|
+
export async function applyOutputAcceptancePolicy(result, output) {
|
|
68
|
+
if (result.code !== 0 || output !== "review_evidence")
|
|
69
|
+
return result;
|
|
70
|
+
let semanticStdout = result.pipelineStdout ?? result.stdout;
|
|
71
|
+
if (result.pipelineStdout === undefined && result.stdoutTruncated) {
|
|
72
|
+
if (!result.stdoutFile)
|
|
73
|
+
semanticStdout = "";
|
|
74
|
+
else {
|
|
75
|
+
try {
|
|
76
|
+
semanticStdout = await readFile(result.stdoutFile, "utf8");
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
semanticStdout = "";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const firstNonWhitespaceLine = semanticStdout
|
|
84
|
+
.split(/\r?\n/)
|
|
85
|
+
.find((line) => line.trim().length > 0);
|
|
86
|
+
if (firstNonWhitespaceLine?.trim() === REVIEW_RESULT_MARKER)
|
|
87
|
+
return result;
|
|
88
|
+
return {
|
|
89
|
+
...result,
|
|
90
|
+
code: 65,
|
|
91
|
+
stderr: [
|
|
92
|
+
result.stderr,
|
|
93
|
+
`review evidence rejected: missing ${REVIEW_RESULT_MARKER} marker`,
|
|
94
|
+
].filter(Boolean).join("\n"),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async function resolvePipelineStdout(result) {
|
|
98
|
+
let stdout = result.pipelineStdout ?? result.stdout;
|
|
99
|
+
if (result.pipelineStdout === undefined && result.stdoutTruncated) {
|
|
100
|
+
if (!result.stdoutFile)
|
|
101
|
+
return undefined;
|
|
102
|
+
try {
|
|
103
|
+
stdout = await readFile(result.stdoutFile, "utf8");
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return result.evidenceRef
|
|
110
|
+
? `${stdout}\nACTOR_EVIDENCE_REF: ${result.evidenceRef}`
|
|
111
|
+
: stdout;
|
|
112
|
+
}
|
|
113
|
+
function rejectIncompletePipelineOutput(result) {
|
|
114
|
+
return {
|
|
115
|
+
...result,
|
|
116
|
+
code: 74,
|
|
117
|
+
stderr: [
|
|
118
|
+
result.stderr,
|
|
119
|
+
`incomplete pipeline stdin: complete stdout unavailable${result.stdoutFile ? `; capture path unreadable: ${result.stdoutFile}` : ""}`,
|
|
120
|
+
].filter(Boolean).join("\n"),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
65
123
|
function getBranchFailureReason(result) {
|
|
66
124
|
if (result.code === 0) {
|
|
67
125
|
return result.stdout.trim() ? undefined : "empty_output";
|
|
@@ -80,13 +138,21 @@ function createBranchReport(label, command, result) {
|
|
|
80
138
|
label,
|
|
81
139
|
status: getBranchStatus(result),
|
|
82
140
|
...(result.stderr ? { stderr: result.stderr.slice(-1000) } : {}),
|
|
83
|
-
stderrBytes: Buffer.byteLength(result.stderr),
|
|
141
|
+
stderrBytes: result.stderrBytes ?? Buffer.byteLength(result.stderr),
|
|
142
|
+
stderrCapturedBytes: Buffer.byteLength(result.stderr),
|
|
143
|
+
...(result.stderrFile ? { stderrFile: result.stderrFile } : {}),
|
|
144
|
+
...(result.stderrTruncated ? { stderrTruncated: true } : {}),
|
|
84
145
|
...(result.stdout ? { stdout: result.stdout.slice(-1000) } : {}),
|
|
85
|
-
stdoutBytes: Buffer.byteLength(result.stdout),
|
|
146
|
+
stdoutBytes: result.stdoutBytes ?? Buffer.byteLength(result.stdout),
|
|
147
|
+
stdoutCapturedBytes: Buffer.byteLength(result.stdout),
|
|
148
|
+
...(result.stdoutFile ? { stdoutFile: result.stdoutFile } : {}),
|
|
149
|
+
...(result.stdoutTruncated ? { stdoutTruncated: true } : {}),
|
|
86
150
|
};
|
|
87
151
|
}
|
|
88
152
|
function isUsableBranch(branch) {
|
|
89
|
-
return branch.status === "done" &&
|
|
153
|
+
return (branch.status === "done" &&
|
|
154
|
+
branch.stdoutBytes > 0 &&
|
|
155
|
+
branch.stdoutTruncated !== true);
|
|
90
156
|
}
|
|
91
157
|
function createSoftQuorum(branches) {
|
|
92
158
|
if (branches.length === 0)
|
|
@@ -231,15 +297,24 @@ function formatParallelStatusHeader(branches, minSuccessful) {
|
|
|
231
297
|
return undefined;
|
|
232
298
|
return `--- parallel_status: ${getParallelStatus(branches, minSuccessful)} usable: ${countUsableBranches(branches)} expected: ${branches.length} minimum: ${minSuccessful} ---`;
|
|
233
299
|
}
|
|
300
|
+
function stdoutWithEvidenceReference(result) {
|
|
301
|
+
return result.evidenceRef
|
|
302
|
+
? `${result.stdout}\nACTOR_EVIDENCE_REF: ${result.evidenceRef}`
|
|
303
|
+
: result.stdout;
|
|
304
|
+
}
|
|
234
305
|
function joinParallelStdout(branches, results, minSuccessful) {
|
|
235
306
|
const body = results
|
|
236
307
|
.map((result, index) => {
|
|
237
308
|
const branch = branches[index];
|
|
238
|
-
const
|
|
309
|
+
const evidence = result.evidenceRef
|
|
310
|
+
? ` evidence_ref: ${result.evidenceRef}`
|
|
311
|
+
: "";
|
|
312
|
+
const header = `--- branch: ${branch.label} status: ${branch.status}${evidence} ---`;
|
|
239
313
|
if (branch.status === "done")
|
|
240
|
-
return `${header}\n${result
|
|
314
|
+
return `${header}\n${stdoutWithEvidenceReference(result)}`;
|
|
315
|
+
const stdout = result.stdout ? `\nrejected_stdout: ${result.stdout}` : "";
|
|
241
316
|
const stderr = branch.stderr ? `\nstderr: ${branch.stderr}` : "";
|
|
242
|
-
return `${header}\nexit: ${branch.code}${stderr}`;
|
|
317
|
+
return `${header}\nexit: ${branch.code}${stdout}${stderr}`;
|
|
243
318
|
})
|
|
244
319
|
.join("\n");
|
|
245
320
|
return [formatParallelStatusHeader(branches, minSuccessful), body]
|
|
@@ -369,8 +444,18 @@ async function executeTemplateConfig(config, inherited, params, exec, cwd, signa
|
|
|
369
444
|
if (!Array.isArray(normalized.template)) {
|
|
370
445
|
const leaf = { ...normalized, ...context };
|
|
371
446
|
const invocation = CommandTemplates.buildCommandTemplateInvocation(leaf, params, cwd, { emptyMessage: "Tool template produced an empty command." });
|
|
372
|
-
const
|
|
447
|
+
const evidenceContext = {
|
|
448
|
+
...(normalized.accept_output
|
|
449
|
+
? { acceptOutput: normalized.accept_output }
|
|
450
|
+
: {}),
|
|
451
|
+
...(normalized.label ? { label: normalized.label } : {}),
|
|
452
|
+
...(typeof controlValues.index === "string"
|
|
453
|
+
? { repeatIndex: controlValues.index }
|
|
454
|
+
: {}),
|
|
455
|
+
};
|
|
456
|
+
const rawResult = await exec(invocation.command, invocation.args, {
|
|
373
457
|
...(actorRecipeContext ? { actorRecipeContext } : {}),
|
|
458
|
+
...(Object.keys(evidenceContext).length > 0 ? { evidenceContext } : {}),
|
|
374
459
|
cwd,
|
|
375
460
|
signal,
|
|
376
461
|
stdin,
|
|
@@ -383,6 +468,7 @@ async function executeTemplateConfig(config, inherited, params, exec, cwd, signa
|
|
|
383
468
|
? { retry: normalizeRetry(normalized.retry, controlValues) }
|
|
384
469
|
: {}),
|
|
385
470
|
});
|
|
471
|
+
const result = await applyOutputAcceptancePolicy(rawResult, normalized.accept_output);
|
|
386
472
|
return {
|
|
387
473
|
branches: [],
|
|
388
474
|
commands: [formatInvocationDetail(invocation)],
|
|
@@ -398,6 +484,12 @@ async function executeTemplateConfig(config, inherited, params, exec, cwd, signa
|
|
|
398
484
|
const concurrency = normalizeConcurrency(normalized.concurrency, controlValues, steps.length);
|
|
399
485
|
const minSuccessful = normalizeMinSuccessful(normalized.min_successful, controlValues, steps.length);
|
|
400
486
|
const branchResults = await mapConcurrent(steps, concurrency, (step) => executeTemplateConfig(step, context, params, exec, cwd, signal, stdin, false, actorRecipeContext));
|
|
487
|
+
const branchPipelineStdouts = await Promise.all(branchResults.map((item) => resolvePipelineStdout(item.result)));
|
|
488
|
+
for (const [index, item] of branchResults.entries()) {
|
|
489
|
+
if (item.result.stdoutTruncated && branchPipelineStdouts[index] === undefined) {
|
|
490
|
+
item.result = rejectIncompletePipelineOutput(item.result);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
401
493
|
const commands = branchResults.flatMap((item) => item.commands);
|
|
402
494
|
const failures = branchResults.flatMap((item) => item.failures);
|
|
403
495
|
const branches = branchResults.map((item, index) => createBranchReport(getNodeLabel(steps[index], index), item.commands.at(-1) ?? "<template>", item.result));
|
|
@@ -432,8 +524,13 @@ async function executeTemplateConfig(config, inherited, params, exec, cwd, signa
|
|
|
432
524
|
if (item.result.code === 0)
|
|
433
525
|
return item.result;
|
|
434
526
|
addResultFailure(failures, item);
|
|
435
|
-
return { ...item.result, code: 0
|
|
527
|
+
return { ...item.result, code: 0 };
|
|
436
528
|
});
|
|
529
|
+
const completeSuccessful = successful.map((item, index) => ({
|
|
530
|
+
...item,
|
|
531
|
+
stdout: branchPipelineStdouts[index] ?? item.stdout,
|
|
532
|
+
evidenceRef: undefined,
|
|
533
|
+
}));
|
|
437
534
|
const result = {
|
|
438
535
|
code: 0,
|
|
439
536
|
killed: successful.some((item) => item.killed),
|
|
@@ -442,6 +539,11 @@ async function executeTemplateConfig(config, inherited, params, exec, cwd, signa
|
|
|
442
539
|
.filter(Boolean)
|
|
443
540
|
.join("\n"),
|
|
444
541
|
stdout: joinParallelStdout(branches, successful, minSuccessful),
|
|
542
|
+
pipelineStdout: joinParallelStdout(branches, completeSuccessful, minSuccessful),
|
|
543
|
+
stdoutBytes: successful.reduce((total, item) => total + (item.stdoutBytes ?? Buffer.byteLength(item.stdout)), 0),
|
|
544
|
+
...(successful.some((item) => item.stdoutTruncated)
|
|
545
|
+
? { stdoutTruncated: true }
|
|
546
|
+
: {}),
|
|
445
547
|
};
|
|
446
548
|
if (quorumUnmet && nodeFailure === "root") {
|
|
447
549
|
return {
|
|
@@ -504,12 +606,28 @@ async function executeTemplateConfig(config, inherited, params, exec, cwd, signa
|
|
|
504
606
|
const failures = [];
|
|
505
607
|
let nextStdin = stdin;
|
|
506
608
|
let result;
|
|
507
|
-
for (const step of steps) {
|
|
609
|
+
for (const [stepIndex, step] of steps.entries()) {
|
|
508
610
|
const executed = await executeTemplateConfig(step, context, params, exec, cwd, signal, nextStdin, false, actorRecipeContext);
|
|
509
611
|
branches.push(...executed.branches);
|
|
510
612
|
commands.push(...executed.commands);
|
|
511
613
|
failures.push(...executed.failures);
|
|
512
|
-
|
|
614
|
+
const pipelineStdout = await resolvePipelineStdout(executed.result);
|
|
615
|
+
const incompletePipelineInput = stepIndex < steps.length - 1 && pipelineStdout === undefined;
|
|
616
|
+
result = incompletePipelineInput
|
|
617
|
+
? rejectIncompletePipelineOutput(executed.result)
|
|
618
|
+
: executed.result;
|
|
619
|
+
executed.result = result;
|
|
620
|
+
if (incompletePipelineInput) {
|
|
621
|
+
addResultFailure(failures, executed);
|
|
622
|
+
return {
|
|
623
|
+
branches,
|
|
624
|
+
commands,
|
|
625
|
+
criticalFailure: true,
|
|
626
|
+
failureScope: "root",
|
|
627
|
+
failures,
|
|
628
|
+
result,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
513
631
|
if (result.code !== 0) {
|
|
514
632
|
const failureScope = maxFailureScope(executed.failureScope, executed.criticalFailure ? "root" : undefined, getFailureScope(step), getFailureScope(normalized), isRoot && steps.length === 1 ? "root" : undefined);
|
|
515
633
|
if (failureScope === "root") {
|
|
@@ -537,13 +655,25 @@ async function executeTemplateConfig(config, inherited, params, exec, cwd, signa
|
|
|
537
655
|
nextStdin = "";
|
|
538
656
|
continue;
|
|
539
657
|
}
|
|
540
|
-
nextStdin =
|
|
658
|
+
nextStdin = pipelineStdout;
|
|
541
659
|
}
|
|
542
660
|
return { branches, commands, failures, result: result };
|
|
543
661
|
}
|
|
544
662
|
async function executeTemplateSteps(cfg, params, exec, cwd, signal) {
|
|
545
663
|
return executeTemplateConfig(createTemplateConfig(cfg), {}, params, exec, cwd, signal, undefined, true, undefined);
|
|
546
664
|
}
|
|
665
|
+
function getCaptureDetails(result) {
|
|
666
|
+
return {
|
|
667
|
+
stdoutBytes: result.stdoutBytes ?? Buffer.byteLength(result.stdout),
|
|
668
|
+
stdoutCapturedBytes: Buffer.byteLength(result.stdout),
|
|
669
|
+
stderrBytes: result.stderrBytes ?? Buffer.byteLength(result.stderr),
|
|
670
|
+
stderrCapturedBytes: Buffer.byteLength(result.stderr),
|
|
671
|
+
...(result.stdoutFile ? { stdoutFile: result.stdoutFile } : {}),
|
|
672
|
+
...(result.stderrFile ? { stderrFile: result.stderrFile } : {}),
|
|
673
|
+
...(result.stdoutTruncated ? { stdoutTruncated: true } : {}),
|
|
674
|
+
...(result.stderrTruncated ? { stderrTruncated: true } : {}),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
547
677
|
export async function executeRegisteredTool(cfg, params, exec, cwd, signal) {
|
|
548
678
|
const executed = await executeTemplateSteps(cfg, Schema.normalizeRuntimeValues(params, cfg.argTypes), exec, cwd, signal);
|
|
549
679
|
const command = formatCommandDetail(executed.commands);
|
|
@@ -556,7 +686,9 @@ export async function executeRegisteredTool(cfg, params, exec, cwd, signal) {
|
|
|
556
686
|
branches: executed.branches,
|
|
557
687
|
code: result.code,
|
|
558
688
|
command,
|
|
689
|
+
fullOutputPath: result.stdoutFile ?? formatted.fullOutputPath,
|
|
559
690
|
killed: result.killed,
|
|
691
|
+
...getCaptureDetails(result),
|
|
560
692
|
...(executed.failures.length > 0
|
|
561
693
|
? { nonCriticalFailures: executed.failures }
|
|
562
694
|
: {}),
|
|
@@ -575,8 +707,9 @@ export async function executeRegisteredTool(cfg, params, exec, cwd, signal) {
|
|
|
575
707
|
details: {
|
|
576
708
|
code: result.code,
|
|
577
709
|
command,
|
|
578
|
-
fullOutputPath: formatted.fullOutputPath,
|
|
710
|
+
fullOutputPath: result.stdoutFile ?? formatted.fullOutputPath,
|
|
579
711
|
killed: result.killed,
|
|
712
|
+
...getCaptureDetails(result),
|
|
580
713
|
...(executed.branches.length > 0 ? { branches: executed.branches } : {}),
|
|
581
714
|
...(executed.failures.length > 0
|
|
582
715
|
? { nonCriticalFailures: executed.failures }
|
package/dist/lib/file-state.d.ts
CHANGED
|
@@ -3,4 +3,5 @@
|
|
|
3
3
|
* Zones: file persistence, atomic writes, runtime state support
|
|
4
4
|
* Owns generic durable JSON file writes shared by registry config and async run state.
|
|
5
5
|
*/
|
|
6
|
+
export declare function withFileMutationLock<T>(path: string, mutate: () => T): T;
|
|
6
7
|
export declare function writeJsonAtomic(path: string, value: unknown): void;
|
package/dist/lib/file-state.js
CHANGED
|
@@ -3,9 +3,97 @@
|
|
|
3
3
|
* Zones: file persistence, atomic writes, runtime state support
|
|
4
4
|
* Owns generic durable JSON file writes shared by registry config and async run state.
|
|
5
5
|
*/
|
|
6
|
-
import { randomUUID } from "node:crypto";
|
|
7
|
-
import { mkdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
8
|
-
import {
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { basename, dirname, join, parse, resolve } from "node:path";
|
|
10
|
+
const FILE_MUTATION_LOCK_TIMEOUT_MS = 5000;
|
|
11
|
+
const FILE_MUTATION_LOCK_STALE_MS = 30000;
|
|
12
|
+
const FILE_MUTATION_LOCK_ROOT = join(tmpdir(), "pi-actors-file-locks");
|
|
13
|
+
function canonicalMutationPath(path) {
|
|
14
|
+
const absolute = resolve(path);
|
|
15
|
+
const suffix = [];
|
|
16
|
+
let existing = absolute;
|
|
17
|
+
while (!existsSync(existing)) {
|
|
18
|
+
const parent = dirname(existing);
|
|
19
|
+
if (parent === existing || existing === parse(existing).root)
|
|
20
|
+
break;
|
|
21
|
+
suffix.unshift(basename(existing));
|
|
22
|
+
existing = parent;
|
|
23
|
+
}
|
|
24
|
+
const canonicalAncestor = existsSync(existing)
|
|
25
|
+
? realpathSync.native(existing)
|
|
26
|
+
: existing;
|
|
27
|
+
const canonical = resolve(canonicalAncestor, ...suffix);
|
|
28
|
+
return process.platform === "win32" ? canonical.toLowerCase() : canonical;
|
|
29
|
+
}
|
|
30
|
+
function mutationLockPath(path) {
|
|
31
|
+
const key = createHash("sha256")
|
|
32
|
+
.update(canonicalMutationPath(path))
|
|
33
|
+
.digest("hex");
|
|
34
|
+
return join(FILE_MUTATION_LOCK_ROOT, `${key}.lock`);
|
|
35
|
+
}
|
|
36
|
+
function lockOwnerIsDead(lockPath) {
|
|
37
|
+
try {
|
|
38
|
+
const owner = JSON.parse(readFileSync(join(lockPath, "owner.json"), "utf8"));
|
|
39
|
+
const pid = Number(owner.pid);
|
|
40
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
41
|
+
return false;
|
|
42
|
+
try {
|
|
43
|
+
process.kill(pid, 0);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
return error.code === "ESRCH";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function withFileMutationLock(path, mutate) {
|
|
55
|
+
mkdirSync(FILE_MUTATION_LOCK_ROOT, { recursive: true });
|
|
56
|
+
const lockPath = mutationLockPath(path);
|
|
57
|
+
const deadline = Date.now() + FILE_MUTATION_LOCK_TIMEOUT_MS;
|
|
58
|
+
for (;;) {
|
|
59
|
+
try {
|
|
60
|
+
mkdirSync(lockPath);
|
|
61
|
+
try {
|
|
62
|
+
writeFileSync(join(lockPath, "owner.json"), `${JSON.stringify({ pid: process.pid, acquired_at: new Date().toISOString() })}\n`, "utf8");
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
try {
|
|
72
|
+
if (Date.now() - statSync(lockPath).mtimeMs >
|
|
73
|
+
FILE_MUTATION_LOCK_STALE_MS &&
|
|
74
|
+
lockOwnerIsDead(lockPath)) {
|
|
75
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (Date.now() >= deadline) {
|
|
83
|
+
throw new Error(`Timed out waiting for file mutation lock: ${canonicalMutationPath(path)}`, {
|
|
84
|
+
cause: error,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
return mutate();
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
9
97
|
export function writeJsonAtomic(path, value) {
|
|
10
98
|
mkdirSync(dirname(path), { recursive: true });
|
|
11
99
|
const tempPath = `${path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
@@ -43,6 +43,9 @@ export function deliverRunTransitionNotifications(transitions, sink) {
|
|
|
43
43
|
display: true,
|
|
44
44
|
details: transition,
|
|
45
45
|
});
|
|
46
|
+
if (transition.stateDir) {
|
|
47
|
+
AsyncRuns.markRunTerminalNotificationHandled(transition.stateDir, transition.to);
|
|
48
|
+
}
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
51
|
export function deliverRunOutboxNotifications(events, sink) {
|
|
@@ -494,9 +497,9 @@ export function detectRunTransitions(previous, summary) {
|
|
|
494
497
|
for (const run of summary.runs) {
|
|
495
498
|
const key = runObservationKey(run);
|
|
496
499
|
const old = previous.get(key);
|
|
497
|
-
if (
|
|
500
|
+
if (!run.terminalHandled && TERMINAL.has(run.status)) {
|
|
498
501
|
transitions.push({
|
|
499
|
-
from: old,
|
|
502
|
+
from: old ?? "running",
|
|
500
503
|
run: run.run,
|
|
501
504
|
...(run.stateDir ? { stateDir: run.stateDir } : {}),
|
|
502
505
|
...(run.artifacts ? { artifacts: run.artifacts } : {}),
|
package/dist/lib/prompts.d.ts
CHANGED
|
@@ -6,13 +6,12 @@
|
|
|
6
6
|
export declare const REGISTER_TOOL_DESCRIPTION: string;
|
|
7
7
|
export declare const REGISTER_TOOL_PROMPT_SNIPPET = "Register persistent command templates as agent-callable tools";
|
|
8
8
|
export declare const REGISTER_TOOL_GUIDELINES: string[];
|
|
9
|
-
export declare const ONBOARDING_SYSTEM_PROMPT = "pi-actors quick model:\n- Local-first actor memory: persist trusted local capabilities instead of rebuilding shell recipes.\n- Layers: task -> command template -> recipe/tool -> spawn -> run:<id>; tool:<name> wraps registered capabilities.\n- Command templates stay sync: string
|
|
9
|
+
export declare const ONBOARDING_SYSTEM_PROMPT = "pi-actors quick model:\n- Local-first actor memory: persist trusted local capabilities instead of rebuilding shell recipes.\n- Layers: task -> command template -> recipe/tool -> spawn -> run:<id>; tool:<name> wraps registered capabilities.\n- Command templates stay sync and shell-free: string leaves split into executable + argv, so operators such as && are literal arguments; use template arrays for sequencing or an explicit trusted shell/script when shell semantics are required. Flags include args/defaults, parallel, concurrency, min_successful, when, timeout, delay, retry, failure, recover, repeat, accept_output, output.\n- Placeholders support typed/default args plus {value??fallback} and {flag?yes:no}.\n- ~/.pi/agent/recipes/*.json is actor muscle memory: every recipe there is auto-registered as an agent tool across sessions; register_tool writes there.\n- Recipes own template directly and may declare metadata/defaults/imports/mailbox/artifacts; files >1 MiB or import depth >32 fail closed.\n- Recipe imports are local variables; imported recipes are definitions, not nested async runs; parent async:true creates one run.\n- Actor-mode trigger: if work may outlive this turn, need steering/follow-up/artifacts, run as a service, fan out, or be resumed/inspected later, use spawn -> message -> inspect instead of ad hoc shell backgrounding.\n- Use spawn/message/inspect for actor-level start/send/observe; short foreground checks can stay ordinary tools/templates; avoid runtime/FIFO/outbox vocabulary in public guidance.\n- Run state lives under ~/.pi/agent/tmp/pi-actors/runs. Inspect intentionally and avoid busy-polling. Terminal and coordinator-bound notifications queue as Pi follow-ups so concurrently completed actors can reach the coordinator after current work instead of steering between tool calls. When a deferred actor result gates the next step, wait for its terminal follow-up; do not schedule continuation loops, repeatedly inspect, or mutate its reviewed scope while it runs. Inspect early only for an operator request, a meaningful actor event, or diagnosis of an overdue/stuck run.\n- Maintain ~/.pi/agent/recipes like MEMORY.md for capabilities: keep useful tools, curate stale ones, and fix/remove/disable invalid recipes flagged by registry warnings; packaged/ad hoc recipes are lower-priority components; offer to save successful recurring patterns only after confirmation.\n- Prefer maintained packaged recipes/pipelines with spawn file=<recipe> before ad hoc scripts/wrappers; review swarms inherit current model/thinking, preflight before fanout, and expose quorum/concurrency/TTL knobs unless explicit args are passed.\n- For any non-trivial actor use or pi-actors change, read the bundled actors skill first. Before launching multiple actors/subagents for parallel implementation, independent artifact generation, delegated audit, or review, also read the bundled swarm skill; the coordinator owns decomposition, disjoint scopes, launch correctness, integration, and final validation. For deeper guidance, inspect installed extension sources/docs/recipes because README/docs are not automatically in context.";
|
|
10
10
|
export declare const REGISTER_TOOL_PARAM_DESCRIPTIONS: {
|
|
11
11
|
readonly name: "Tool name in snake_case (e.g., 'transcribe')";
|
|
12
12
|
readonly description: "Describe what the tool does for the LLM. Required unless deleting; omitted updates keep the old description.";
|
|
13
13
|
readonly draft: "Promote a draft recipe path from ~/.pi/agent/recipes/drafts into an active named recipe under ~/.pi/agent/recipes. Requires name; use update=true to overwrite.";
|
|
14
14
|
readonly async: "Set true for a co-located async template recipe. Omit for ordinary command templates or file-backed recipe references.";
|
|
15
|
-
readonly state_dir: "Optional async run state directory for a co-located template recipe.";
|
|
16
15
|
readonly template: "Command template with {arg} or {arg=default} placeholders, or a template recipe JSON path/name. With async, this is the co-located recipe body. Bare recipe names resolve under ~/.pi/agent/recipes. Omitted updates keep the old template. Empty string deletes the tool.";
|
|
17
16
|
readonly templateArray: "Sequential command-template composition array. Leaves may be strings or objects with template/defaults/timeout/retry/failure/recover.";
|
|
18
17
|
readonly templateNull: "Delete the tool when template is null.";
|