@llblab/pi-actors 0.39.0 → 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 +10 -3
- package/BACKLOG.md +2 -11
- package/CHANGELOG.md +45 -0
- package/README.md +4 -4
- 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/lib/execution.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Owns command-template invocation execution and pi tool-result payload formatting
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
8
|
+
|
|
7
9
|
import * as CommandTemplates from "./command-templates.ts";
|
|
8
10
|
import type { RegisteredTool } from "./config.ts";
|
|
9
11
|
import {
|
|
@@ -15,11 +17,18 @@ import * as Schema from "./schema.ts";
|
|
|
15
17
|
|
|
16
18
|
export interface ToolExecOptions {
|
|
17
19
|
actorRecipeContext?: CommandTemplates.CommandTemplateActorRecipeContext;
|
|
20
|
+
evidenceContext?: {
|
|
21
|
+
acceptOutput?: "review_evidence";
|
|
22
|
+
label?: string;
|
|
23
|
+
repeatIndex?: string;
|
|
24
|
+
};
|
|
18
25
|
cwd?: string;
|
|
19
26
|
signal?: AbortSignal;
|
|
20
27
|
stdin?: string;
|
|
21
28
|
timeout?: number;
|
|
22
29
|
retry?: number;
|
|
30
|
+
captureDir?: string;
|
|
31
|
+
captureLimitBytes?: number;
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
export interface ToolExecResult {
|
|
@@ -27,6 +36,15 @@ export interface ToolExecResult {
|
|
|
27
36
|
stderr: string;
|
|
28
37
|
code: number;
|
|
29
38
|
killed: boolean;
|
|
39
|
+
stdoutBytes?: number;
|
|
40
|
+
stderrBytes?: number;
|
|
41
|
+
stdoutFile?: string;
|
|
42
|
+
stderrFile?: string;
|
|
43
|
+
evidenceRef?: string;
|
|
44
|
+
stdoutTruncated?: boolean;
|
|
45
|
+
stderrTruncated?: boolean;
|
|
46
|
+
/** Complete internal stdout for downstream pipeline stdin; never returned to model-facing output. */
|
|
47
|
+
pipelineStdout?: string;
|
|
30
48
|
}
|
|
31
49
|
|
|
32
50
|
export interface BranchReport {
|
|
@@ -38,8 +56,14 @@ export interface BranchReport {
|
|
|
38
56
|
status: "done" | "failed" | "timeout";
|
|
39
57
|
stderr?: string;
|
|
40
58
|
stderrBytes: number;
|
|
59
|
+
stderrCapturedBytes: number;
|
|
60
|
+
stderrFile?: string;
|
|
61
|
+
stderrTruncated?: boolean;
|
|
41
62
|
stdout?: string;
|
|
42
63
|
stdoutBytes: number;
|
|
64
|
+
stdoutCapturedBytes: number;
|
|
65
|
+
stdoutFile?: string;
|
|
66
|
+
stdoutTruncated?: boolean;
|
|
43
67
|
}
|
|
44
68
|
|
|
45
69
|
export interface SoftQuorumReport {
|
|
@@ -59,6 +83,14 @@ export interface RegisteredToolExecutionResult {
|
|
|
59
83
|
command: string;
|
|
60
84
|
fullOutputPath?: string;
|
|
61
85
|
killed: boolean;
|
|
86
|
+
stderrBytes?: number;
|
|
87
|
+
stderrCapturedBytes?: number;
|
|
88
|
+
stderrFile?: string;
|
|
89
|
+
stderrTruncated?: boolean;
|
|
90
|
+
stdoutBytes?: number;
|
|
91
|
+
stdoutCapturedBytes?: number;
|
|
92
|
+
stdoutFile?: string;
|
|
93
|
+
stdoutTruncated?: boolean;
|
|
62
94
|
nonCriticalFailures?: Array<{
|
|
63
95
|
code: number;
|
|
64
96
|
command: string;
|
|
@@ -163,6 +195,66 @@ function getBranchStatus(result: ToolExecResult): BranchReport["status"] {
|
|
|
163
195
|
return result.killed ? "timeout" : "failed";
|
|
164
196
|
}
|
|
165
197
|
|
|
198
|
+
const REVIEW_RESULT_MARKER = "ACTOR_REVIEW_RESULT";
|
|
199
|
+
|
|
200
|
+
export async function applyOutputAcceptancePolicy(
|
|
201
|
+
result: ToolExecResult,
|
|
202
|
+
output: string | undefined,
|
|
203
|
+
): Promise<ToolExecResult> {
|
|
204
|
+
if (result.code !== 0 || output !== "review_evidence") return result;
|
|
205
|
+
let semanticStdout = result.pipelineStdout ?? result.stdout;
|
|
206
|
+
if (result.pipelineStdout === undefined && result.stdoutTruncated) {
|
|
207
|
+
if (!result.stdoutFile) semanticStdout = "";
|
|
208
|
+
else {
|
|
209
|
+
try {
|
|
210
|
+
semanticStdout = await readFile(result.stdoutFile, "utf8");
|
|
211
|
+
} catch {
|
|
212
|
+
semanticStdout = "";
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const firstNonWhitespaceLine = semanticStdout
|
|
217
|
+
.split(/\r?\n/)
|
|
218
|
+
.find((line) => line.trim().length > 0);
|
|
219
|
+
if (firstNonWhitespaceLine?.trim() === REVIEW_RESULT_MARKER) return result;
|
|
220
|
+
return {
|
|
221
|
+
...result,
|
|
222
|
+
code: 65,
|
|
223
|
+
stderr: [
|
|
224
|
+
result.stderr,
|
|
225
|
+
`review evidence rejected: missing ${REVIEW_RESULT_MARKER} marker`,
|
|
226
|
+
].filter(Boolean).join("\n"),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function resolvePipelineStdout(
|
|
231
|
+
result: ToolExecResult,
|
|
232
|
+
): Promise<string | undefined> {
|
|
233
|
+
let stdout = result.pipelineStdout ?? result.stdout;
|
|
234
|
+
if (result.pipelineStdout === undefined && result.stdoutTruncated) {
|
|
235
|
+
if (!result.stdoutFile) return undefined;
|
|
236
|
+
try {
|
|
237
|
+
stdout = await readFile(result.stdoutFile, "utf8");
|
|
238
|
+
} catch {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return result.evidenceRef
|
|
243
|
+
? `${stdout}\nACTOR_EVIDENCE_REF: ${result.evidenceRef}`
|
|
244
|
+
: stdout;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function rejectIncompletePipelineOutput(result: ToolExecResult): ToolExecResult {
|
|
248
|
+
return {
|
|
249
|
+
...result,
|
|
250
|
+
code: 74,
|
|
251
|
+
stderr: [
|
|
252
|
+
result.stderr,
|
|
253
|
+
`incomplete pipeline stdin: complete stdout unavailable${result.stdoutFile ? `; capture path unreadable: ${result.stdoutFile}` : ""}`,
|
|
254
|
+
].filter(Boolean).join("\n"),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
166
258
|
function getBranchFailureReason(
|
|
167
259
|
result: ToolExecResult,
|
|
168
260
|
): string | undefined {
|
|
@@ -187,14 +279,24 @@ function createBranchReport(
|
|
|
187
279
|
label,
|
|
188
280
|
status: getBranchStatus(result),
|
|
189
281
|
...(result.stderr ? { stderr: result.stderr.slice(-1000) } : {}),
|
|
190
|
-
stderrBytes: Buffer.byteLength(result.stderr),
|
|
282
|
+
stderrBytes: result.stderrBytes ?? Buffer.byteLength(result.stderr),
|
|
283
|
+
stderrCapturedBytes: Buffer.byteLength(result.stderr),
|
|
284
|
+
...(result.stderrFile ? { stderrFile: result.stderrFile } : {}),
|
|
285
|
+
...(result.stderrTruncated ? { stderrTruncated: true } : {}),
|
|
191
286
|
...(result.stdout ? { stdout: result.stdout.slice(-1000) } : {}),
|
|
192
|
-
stdoutBytes: Buffer.byteLength(result.stdout),
|
|
287
|
+
stdoutBytes: result.stdoutBytes ?? Buffer.byteLength(result.stdout),
|
|
288
|
+
stdoutCapturedBytes: Buffer.byteLength(result.stdout),
|
|
289
|
+
...(result.stdoutFile ? { stdoutFile: result.stdoutFile } : {}),
|
|
290
|
+
...(result.stdoutTruncated ? { stdoutTruncated: true } : {}),
|
|
193
291
|
};
|
|
194
292
|
}
|
|
195
293
|
|
|
196
294
|
function isUsableBranch(branch: BranchReport): boolean {
|
|
197
|
-
return
|
|
295
|
+
return (
|
|
296
|
+
branch.status === "done" &&
|
|
297
|
+
branch.stdoutBytes > 0 &&
|
|
298
|
+
branch.stdoutTruncated !== true
|
|
299
|
+
);
|
|
198
300
|
}
|
|
199
301
|
|
|
200
302
|
function createSoftQuorum(
|
|
@@ -399,6 +501,12 @@ function formatParallelStatusHeader(
|
|
|
399
501
|
)} usable: ${countUsableBranches(branches)} expected: ${branches.length} minimum: ${minSuccessful} ---`;
|
|
400
502
|
}
|
|
401
503
|
|
|
504
|
+
function stdoutWithEvidenceReference(result: ToolExecResult): string {
|
|
505
|
+
return result.evidenceRef
|
|
506
|
+
? `${result.stdout}\nACTOR_EVIDENCE_REF: ${result.evidenceRef}`
|
|
507
|
+
: result.stdout;
|
|
508
|
+
}
|
|
509
|
+
|
|
402
510
|
function joinParallelStdout(
|
|
403
511
|
branches: BranchReport[],
|
|
404
512
|
results: ToolExecResult[],
|
|
@@ -407,10 +515,15 @@ function joinParallelStdout(
|
|
|
407
515
|
const body = results
|
|
408
516
|
.map((result, index) => {
|
|
409
517
|
const branch = branches[index];
|
|
410
|
-
const
|
|
411
|
-
|
|
518
|
+
const evidence = result.evidenceRef
|
|
519
|
+
? ` evidence_ref: ${result.evidenceRef}`
|
|
520
|
+
: "";
|
|
521
|
+
const header = `--- branch: ${branch.label} status: ${branch.status}${evidence} ---`;
|
|
522
|
+
if (branch.status === "done")
|
|
523
|
+
return `${header}\n${stdoutWithEvidenceReference(result)}`;
|
|
524
|
+
const stdout = result.stdout ? `\nrejected_stdout: ${result.stdout}` : "";
|
|
412
525
|
const stderr = branch.stderr ? `\nstderr: ${branch.stderr}` : "";
|
|
413
|
-
return `${header}\nexit: ${branch.code}${stderr}`;
|
|
526
|
+
return `${header}\nexit: ${branch.code}${stdout}${stderr}`;
|
|
414
527
|
})
|
|
415
528
|
.join("\n");
|
|
416
529
|
return [formatParallelStatusHeader(branches, minSuccessful), body]
|
|
@@ -643,8 +756,18 @@ async function executeTemplateConfig(
|
|
|
643
756
|
cwd,
|
|
644
757
|
{ emptyMessage: "Tool template produced an empty command." },
|
|
645
758
|
);
|
|
646
|
-
const
|
|
759
|
+
const evidenceContext = {
|
|
760
|
+
...(normalized.accept_output
|
|
761
|
+
? { acceptOutput: normalized.accept_output }
|
|
762
|
+
: {}),
|
|
763
|
+
...(normalized.label ? { label: normalized.label } : {}),
|
|
764
|
+
...(typeof controlValues.index === "string"
|
|
765
|
+
? { repeatIndex: controlValues.index }
|
|
766
|
+
: {}),
|
|
767
|
+
};
|
|
768
|
+
const rawResult = await exec(invocation.command, invocation.args, {
|
|
647
769
|
...(actorRecipeContext ? { actorRecipeContext } : {}),
|
|
770
|
+
...(Object.keys(evidenceContext).length > 0 ? { evidenceContext } : {}),
|
|
648
771
|
cwd,
|
|
649
772
|
signal,
|
|
650
773
|
stdin,
|
|
@@ -665,6 +788,10 @@ async function executeTemplateConfig(
|
|
|
665
788
|
? { retry: normalizeRetry(normalized.retry, controlValues) }
|
|
666
789
|
: {}),
|
|
667
790
|
});
|
|
791
|
+
const result = await applyOutputAcceptancePolicy(
|
|
792
|
+
rawResult,
|
|
793
|
+
normalized.accept_output,
|
|
794
|
+
);
|
|
668
795
|
return {
|
|
669
796
|
branches: [],
|
|
670
797
|
commands: [formatInvocationDetail(invocation)],
|
|
@@ -700,6 +827,14 @@ async function executeTemplateConfig(
|
|
|
700
827
|
actorRecipeContext,
|
|
701
828
|
),
|
|
702
829
|
);
|
|
830
|
+
const branchPipelineStdouts = await Promise.all(
|
|
831
|
+
branchResults.map((item) => resolvePipelineStdout(item.result)),
|
|
832
|
+
);
|
|
833
|
+
for (const [index, item] of branchResults.entries()) {
|
|
834
|
+
if (item.result.stdoutTruncated && branchPipelineStdouts[index] === undefined) {
|
|
835
|
+
item.result = rejectIncompletePipelineOutput(item.result);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
703
838
|
const commands = branchResults.flatMap((item) => item.commands);
|
|
704
839
|
const failures = branchResults.flatMap((item) => item.failures);
|
|
705
840
|
const branches = branchResults.map((item, index) =>
|
|
@@ -748,9 +883,14 @@ async function executeTemplateConfig(
|
|
|
748
883
|
const successful = branchResults.map((item) => {
|
|
749
884
|
if (item.result.code === 0) return item.result;
|
|
750
885
|
addResultFailure(failures, item);
|
|
751
|
-
return { ...item.result, code: 0
|
|
886
|
+
return { ...item.result, code: 0 };
|
|
752
887
|
});
|
|
753
|
-
const
|
|
888
|
+
const completeSuccessful = successful.map((item, index) => ({
|
|
889
|
+
...item,
|
|
890
|
+
stdout: branchPipelineStdouts[index] ?? item.stdout,
|
|
891
|
+
evidenceRef: undefined,
|
|
892
|
+
}));
|
|
893
|
+
const result: ToolExecResult = {
|
|
754
894
|
code: 0,
|
|
755
895
|
killed: successful.some((item) => item.killed),
|
|
756
896
|
stderr: successful
|
|
@@ -758,6 +898,18 @@ async function executeTemplateConfig(
|
|
|
758
898
|
.filter(Boolean)
|
|
759
899
|
.join("\n"),
|
|
760
900
|
stdout: joinParallelStdout(branches, successful, minSuccessful),
|
|
901
|
+
pipelineStdout: joinParallelStdout(
|
|
902
|
+
branches,
|
|
903
|
+
completeSuccessful,
|
|
904
|
+
minSuccessful,
|
|
905
|
+
),
|
|
906
|
+
stdoutBytes: successful.reduce(
|
|
907
|
+
(total, item) => total + (item.stdoutBytes ?? Buffer.byteLength(item.stdout)),
|
|
908
|
+
0,
|
|
909
|
+
),
|
|
910
|
+
...(successful.some((item) => item.stdoutTruncated)
|
|
911
|
+
? { stdoutTruncated: true }
|
|
912
|
+
: {}),
|
|
761
913
|
};
|
|
762
914
|
if (quorumUnmet && nodeFailure === "root") {
|
|
763
915
|
return {
|
|
@@ -821,7 +973,7 @@ async function executeTemplateConfig(
|
|
|
821
973
|
[];
|
|
822
974
|
let nextStdin = stdin;
|
|
823
975
|
let result: ToolExecResult | undefined;
|
|
824
|
-
for (const step of steps) {
|
|
976
|
+
for (const [stepIndex, step] of steps.entries()) {
|
|
825
977
|
const executed = await executeTemplateConfig(
|
|
826
978
|
step,
|
|
827
979
|
context,
|
|
@@ -836,7 +988,24 @@ async function executeTemplateConfig(
|
|
|
836
988
|
branches.push(...executed.branches);
|
|
837
989
|
commands.push(...executed.commands);
|
|
838
990
|
failures.push(...executed.failures);
|
|
839
|
-
|
|
991
|
+
const pipelineStdout = await resolvePipelineStdout(executed.result);
|
|
992
|
+
const incompletePipelineInput =
|
|
993
|
+
stepIndex < steps.length - 1 && pipelineStdout === undefined;
|
|
994
|
+
result = incompletePipelineInput
|
|
995
|
+
? rejectIncompletePipelineOutput(executed.result)
|
|
996
|
+
: executed.result;
|
|
997
|
+
executed.result = result;
|
|
998
|
+
if (incompletePipelineInput) {
|
|
999
|
+
addResultFailure(failures, executed);
|
|
1000
|
+
return {
|
|
1001
|
+
branches,
|
|
1002
|
+
commands,
|
|
1003
|
+
criticalFailure: true,
|
|
1004
|
+
failureScope: "root",
|
|
1005
|
+
failures,
|
|
1006
|
+
result,
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
840
1009
|
if (result.code !== 0) {
|
|
841
1010
|
const failureScope = maxFailureScope(
|
|
842
1011
|
executed.failureScope,
|
|
@@ -870,7 +1039,7 @@ async function executeTemplateConfig(
|
|
|
870
1039
|
nextStdin = "";
|
|
871
1040
|
continue;
|
|
872
1041
|
}
|
|
873
|
-
nextStdin =
|
|
1042
|
+
nextStdin = pipelineStdout;
|
|
874
1043
|
}
|
|
875
1044
|
return { branches, commands, failures, result: result! };
|
|
876
1045
|
}
|
|
@@ -895,6 +1064,19 @@ async function executeTemplateSteps(
|
|
|
895
1064
|
);
|
|
896
1065
|
}
|
|
897
1066
|
|
|
1067
|
+
function getCaptureDetails(result: ToolExecResult): Record<string, unknown> {
|
|
1068
|
+
return {
|
|
1069
|
+
stdoutBytes: result.stdoutBytes ?? Buffer.byteLength(result.stdout),
|
|
1070
|
+
stdoutCapturedBytes: Buffer.byteLength(result.stdout),
|
|
1071
|
+
stderrBytes: result.stderrBytes ?? Buffer.byteLength(result.stderr),
|
|
1072
|
+
stderrCapturedBytes: Buffer.byteLength(result.stderr),
|
|
1073
|
+
...(result.stdoutFile ? { stdoutFile: result.stdoutFile } : {}),
|
|
1074
|
+
...(result.stderrFile ? { stderrFile: result.stderrFile } : {}),
|
|
1075
|
+
...(result.stdoutTruncated ? { stdoutTruncated: true } : {}),
|
|
1076
|
+
...(result.stderrTruncated ? { stderrTruncated: true } : {}),
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
|
|
898
1080
|
export async function executeRegisteredTool(
|
|
899
1081
|
cfg: RegisteredTool,
|
|
900
1082
|
params: Record<string, unknown>,
|
|
@@ -925,7 +1107,9 @@ export async function executeRegisteredTool(
|
|
|
925
1107
|
branches: executed.branches,
|
|
926
1108
|
code: result.code,
|
|
927
1109
|
command,
|
|
1110
|
+
fullOutputPath: result.stdoutFile ?? formatted.fullOutputPath,
|
|
928
1111
|
killed: result.killed,
|
|
1112
|
+
...getCaptureDetails(result),
|
|
929
1113
|
...(executed.failures.length > 0
|
|
930
1114
|
? { nonCriticalFailures: executed.failures }
|
|
931
1115
|
: {}),
|
|
@@ -950,8 +1134,9 @@ export async function executeRegisteredTool(
|
|
|
950
1134
|
details: {
|
|
951
1135
|
code: result.code,
|
|
952
1136
|
command,
|
|
953
|
-
fullOutputPath: formatted.fullOutputPath,
|
|
1137
|
+
fullOutputPath: result.stdoutFile ?? formatted.fullOutputPath,
|
|
954
1138
|
killed: result.killed,
|
|
1139
|
+
...getCaptureDetails(result),
|
|
955
1140
|
...(executed.branches.length > 0 ? { branches: executed.branches } : {}),
|
|
956
1141
|
...(executed.failures.length > 0
|
|
957
1142
|
? { nonCriticalFailures: executed.failures }
|
package/lib/file-state.ts
CHANGED
|
@@ -4,9 +4,112 @@
|
|
|
4
4
|
* Owns generic durable JSON file writes shared by registry config and async run state.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { randomUUID } from "node:crypto";
|
|
8
|
-
import {
|
|
9
|
-
|
|
7
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
8
|
+
import {
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
realpathSync,
|
|
13
|
+
renameSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
statSync,
|
|
16
|
+
unlinkSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
} from "node:fs";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import { basename, dirname, join, parse, resolve } from "node:path";
|
|
21
|
+
|
|
22
|
+
const FILE_MUTATION_LOCK_TIMEOUT_MS = 5000;
|
|
23
|
+
const FILE_MUTATION_LOCK_STALE_MS = 30000;
|
|
24
|
+
const FILE_MUTATION_LOCK_ROOT = join(tmpdir(), "pi-actors-file-locks");
|
|
25
|
+
|
|
26
|
+
function canonicalMutationPath(path: string): string {
|
|
27
|
+
const absolute = resolve(path);
|
|
28
|
+
const suffix: string[] = [];
|
|
29
|
+
let existing = absolute;
|
|
30
|
+
while (!existsSync(existing)) {
|
|
31
|
+
const parent = dirname(existing);
|
|
32
|
+
if (parent === existing || existing === parse(existing).root) break;
|
|
33
|
+
suffix.unshift(basename(existing));
|
|
34
|
+
existing = parent;
|
|
35
|
+
}
|
|
36
|
+
const canonicalAncestor = existsSync(existing)
|
|
37
|
+
? realpathSync.native(existing)
|
|
38
|
+
: existing;
|
|
39
|
+
const canonical = resolve(canonicalAncestor, ...suffix);
|
|
40
|
+
return process.platform === "win32" ? canonical.toLowerCase() : canonical;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function mutationLockPath(path: string): string {
|
|
44
|
+
const key = createHash("sha256")
|
|
45
|
+
.update(canonicalMutationPath(path))
|
|
46
|
+
.digest("hex");
|
|
47
|
+
return join(FILE_MUTATION_LOCK_ROOT, `${key}.lock`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function lockOwnerIsDead(lockPath: string): boolean {
|
|
51
|
+
try {
|
|
52
|
+
const owner = JSON.parse(
|
|
53
|
+
readFileSync(join(lockPath, "owner.json"), "utf8"),
|
|
54
|
+
) as { pid?: unknown };
|
|
55
|
+
const pid = Number(owner.pid);
|
|
56
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
57
|
+
try {
|
|
58
|
+
process.kill(pid, 0);
|
|
59
|
+
return false;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return (error as NodeJS.ErrnoException).code === "ESRCH";
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function withFileMutationLock<T>(path: string, mutate: () => T): T {
|
|
69
|
+
mkdirSync(FILE_MUTATION_LOCK_ROOT, { recursive: true });
|
|
70
|
+
const lockPath = mutationLockPath(path);
|
|
71
|
+
const deadline = Date.now() + FILE_MUTATION_LOCK_TIMEOUT_MS;
|
|
72
|
+
for (;;) {
|
|
73
|
+
try {
|
|
74
|
+
mkdirSync(lockPath);
|
|
75
|
+
try {
|
|
76
|
+
writeFileSync(
|
|
77
|
+
join(lockPath, "owner.json"),
|
|
78
|
+
`${JSON.stringify({ pid: process.pid, acquired_at: new Date().toISOString() })}\n`,
|
|
79
|
+
"utf8",
|
|
80
|
+
);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
try {
|
|
88
|
+
if (
|
|
89
|
+
Date.now() - statSync(lockPath).mtimeMs >
|
|
90
|
+
FILE_MUTATION_LOCK_STALE_MS &&
|
|
91
|
+
lockOwnerIsDead(lockPath)
|
|
92
|
+
) {
|
|
93
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (Date.now() >= deadline) {
|
|
100
|
+
throw new Error(`Timed out waiting for file mutation lock: ${canonicalMutationPath(path)}`, {
|
|
101
|
+
cause: error,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
return mutate();
|
|
109
|
+
} finally {
|
|
110
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
10
113
|
|
|
11
114
|
export function writeJsonAtomic(path: string, value: unknown): void {
|
|
12
115
|
mkdirSync(dirname(path), { recursive: true });
|
package/lib/observability.ts
CHANGED
|
@@ -81,7 +81,7 @@ export interface RunUiSnapshot {
|
|
|
81
81
|
|
|
82
82
|
export interface RunUiNotificationSink {
|
|
83
83
|
notify(message: string, level: "info" | "warning" | "error"): void;
|
|
84
|
-
|
|
84
|
+
sendSteering(message: {
|
|
85
85
|
customType: string;
|
|
86
86
|
content: string;
|
|
87
87
|
display: true;
|
|
@@ -140,12 +140,18 @@ export function deliverRunTransitionNotifications(
|
|
|
140
140
|
const text = formatRunTransitionMessage(transition);
|
|
141
141
|
sink.notify(text, getRunTransitionNotificationType(transition));
|
|
142
142
|
if (!shouldSendRunTransitionFollowUp(transition)) continue;
|
|
143
|
-
sink.
|
|
143
|
+
sink.sendSteering({
|
|
144
144
|
customType: "pi-actors-run",
|
|
145
145
|
content: text,
|
|
146
146
|
display: true,
|
|
147
147
|
details: transition,
|
|
148
148
|
});
|
|
149
|
+
if (transition.stateDir) {
|
|
150
|
+
AsyncRuns.markRunTerminalNotificationHandled(
|
|
151
|
+
transition.stateDir,
|
|
152
|
+
transition.to,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
149
155
|
}
|
|
150
156
|
}
|
|
151
157
|
|
|
@@ -158,7 +164,7 @@ export function deliverRunOutboxNotifications(
|
|
|
158
164
|
const text = formatRunOutboxMessage(event);
|
|
159
165
|
sink.notify(text, getRunOutboxNotificationType(event));
|
|
160
166
|
if (!shouldSendRunOutboxFollowUp(event)) continue;
|
|
161
|
-
sink.
|
|
167
|
+
sink.sendSteering({
|
|
162
168
|
customType: "pi-actors-run-message",
|
|
163
169
|
content: text,
|
|
164
170
|
display: true,
|
|
@@ -747,9 +753,9 @@ export function detectRunTransitions(
|
|
|
747
753
|
for (const run of summary.runs) {
|
|
748
754
|
const key = runObservationKey(run);
|
|
749
755
|
const old = previous.get(key);
|
|
750
|
-
if (
|
|
756
|
+
if (!run.terminalHandled && TERMINAL.has(run.status)) {
|
|
751
757
|
transitions.push({
|
|
752
|
-
from: old,
|
|
758
|
+
from: old ?? "running",
|
|
753
759
|
run: run.run,
|
|
754
760
|
...(run.stateDir ? { stateDir: run.stateDir } : {}),
|
|
755
761
|
...(run.artifacts ? { artifacts: run.artifacts } : {}),
|
package/lib/pi.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type { ExtensionAPI, ExtensionContext };
|
|
|
13
13
|
|
|
14
14
|
export interface PiNotificationSink {
|
|
15
15
|
notify(message: string, level: "info" | "warning" | "error"): void;
|
|
16
|
-
|
|
16
|
+
sendSteering(message: {
|
|
17
17
|
customType: string;
|
|
18
18
|
content: string;
|
|
19
19
|
display: true;
|
|
@@ -31,9 +31,9 @@ export function createNotificationSink(
|
|
|
31
31
|
): PiNotificationSink {
|
|
32
32
|
return {
|
|
33
33
|
notify: (message, level) => ctx.ui.notify(message, level),
|
|
34
|
-
|
|
34
|
+
sendSteering: (message) =>
|
|
35
35
|
pi.sendMessage(message, {
|
|
36
|
-
deliverAs: "
|
|
36
|
+
deliverAs: "steer",
|
|
37
37
|
triggerTurn: true,
|
|
38
38
|
}),
|
|
39
39
|
};
|
package/lib/prompts.ts
CHANGED
|
@@ -22,14 +22,14 @@ export const REGISTER_TOOL_GUIDELINES = [
|
|
|
22
22
|
export const ONBOARDING_SYSTEM_PROMPT = `pi-actors quick model:
|
|
23
23
|
- Local-first actor memory: persist trusted local capabilities instead of rebuilding shell recipes.
|
|
24
24
|
- Layers: task -> command template -> recipe/tool -> spawn -> run:<id>; tool:<name> wraps registered capabilities.
|
|
25
|
-
- Command templates stay sync: string leaf, array sequence, object node; flags include args/defaults, parallel, concurrency, min_successful, when, timeout, delay, retry, failure, recover, repeat, output.
|
|
25
|
+
- Command templates stay sync: string leaf, array sequence, object node; flags include args/defaults, parallel, concurrency, min_successful, when, timeout, delay, retry, failure, recover, repeat, accept_output, output.
|
|
26
26
|
- Placeholders support typed/default args plus {value??fallback} and {flag?yes:no}.
|
|
27
27
|
- ~/.pi/agent/recipes/*.json is actor muscle memory: every recipe there is auto-registered as an agent tool across sessions; register_tool writes there.
|
|
28
28
|
- Recipes own template directly and may declare metadata/defaults/imports/mailbox/artifacts; files >1 MiB or import depth >32 fail closed.
|
|
29
29
|
- Recipe imports are local variables; imported recipes are definitions, not nested async runs; parent async:true creates one run.
|
|
30
30
|
- 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.
|
|
31
31
|
- 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.
|
|
32
|
-
- Run state lives under ~/.pi/agent/tmp/pi-actors/runs
|
|
32
|
+
- Run state lives under ~/.pi/agent/tmp/pi-actors/runs. Inspect intentionally and avoid busy-polling. When a deferred actor result gates the next step, wait for its terminal steering notification; 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.
|
|
33
33
|
- 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.
|
|
34
34
|
- 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.
|
|
35
35
|
- For any non-trivial actor use or pi-actors change, read the bundled actors skill first; for deeper guidance, inspect installed extension sources/docs/recipes because README/docs are not automatically in context.`;
|
|
@@ -42,8 +42,6 @@ export const REGISTER_TOOL_PARAM_DESCRIPTIONS = {
|
|
|
42
42
|
"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.",
|
|
43
43
|
async:
|
|
44
44
|
"Set true for a co-located async template recipe. Omit for ordinary command templates or file-backed recipe references.",
|
|
45
|
-
state_dir:
|
|
46
|
-
"Optional async run state directory for a co-located template recipe.",
|
|
47
45
|
template:
|
|
48
46
|
"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.",
|
|
49
47
|
templateArray:
|
package/lib/recipes-context.ts
CHANGED
|
@@ -63,10 +63,10 @@ function isPiFileArgument(arg: string): boolean {
|
|
|
63
63
|
return arg.startsWith("@") && arg.length > 1;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
function findPiPrintPromptIndexes(args: string[]): number[] {
|
|
67
67
|
let printMode = false;
|
|
68
68
|
let positionalOnly = false;
|
|
69
|
-
|
|
69
|
+
const promptIndexes: number[] = [];
|
|
70
70
|
for (let index = 0; index < args.length; index += 1) {
|
|
71
71
|
const arg = args[index];
|
|
72
72
|
if (!positionalOnly && arg === "--") {
|
|
@@ -82,9 +82,13 @@ export function findPiPrintPromptIndex(args: string[]): number | undefined {
|
|
|
82
82
|
continue;
|
|
83
83
|
}
|
|
84
84
|
if (!printMode || isPiFileArgument(arg)) continue;
|
|
85
|
-
|
|
85
|
+
promptIndexes.push(index);
|
|
86
86
|
}
|
|
87
|
-
return
|
|
87
|
+
return promptIndexes;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function findPiPrintPromptIndex(args: string[]): number | undefined {
|
|
91
|
+
return findPiPrintPromptIndexes(args).at(-1);
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
function matchesActorContext(
|
|
@@ -175,13 +179,17 @@ export function materializePiPrintPromptArg(
|
|
|
175
179
|
promptFile: string | (() => string),
|
|
176
180
|
): MaterializedPiPrintPromptArgs {
|
|
177
181
|
if (!isPiCommand(command)) return { args };
|
|
178
|
-
const
|
|
179
|
-
if (
|
|
180
|
-
const prompt = args[
|
|
182
|
+
const promptIndexes = findPiPrintPromptIndexes(args);
|
|
183
|
+
if (promptIndexes.length === 0) return { args };
|
|
184
|
+
const prompt = promptIndexes.map((index) => args[index]).join(" ");
|
|
181
185
|
const path = typeof promptFile === "function" ? promptFile() : promptFile;
|
|
182
186
|
writeFileSync(path, prompt, "utf8");
|
|
183
|
-
const
|
|
184
|
-
|
|
187
|
+
const promptIndexSet = new Set(promptIndexes);
|
|
188
|
+
const firstPromptIndex = promptIndexes[0];
|
|
189
|
+
const next = args.flatMap((arg, index) => {
|
|
190
|
+
if (index === firstPromptIndex) return [`@${path}`];
|
|
191
|
+
return promptIndexSet.has(index) ? [] : [arg];
|
|
192
|
+
});
|
|
185
193
|
return {
|
|
186
194
|
args: next,
|
|
187
195
|
promptBytes: Buffer.byteLength(prompt),
|
package/lib/recipes-discovery.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { RegisteredTool } from "./config.ts";
|
|
|
14
14
|
import * as ModelContext from "./model-context.ts";
|
|
15
15
|
import type { TemplateRecipeConfig } from "./recipes-references.ts";
|
|
16
16
|
import * as RecipesReferences from "./recipes-references.ts";
|
|
17
|
+
import * as RecipesUsage from "./recipes-usage.ts";
|
|
17
18
|
import * as Schema from "./schema.ts";
|
|
18
19
|
|
|
19
20
|
export interface DiscoveredRecipe {
|
|
@@ -374,9 +375,13 @@ export function discoverRecipes(roots: string[]): RecipesDiscoveryResult {
|
|
|
374
375
|
}
|
|
375
376
|
|
|
376
377
|
function recipeUsage(
|
|
377
|
-
|
|
378
|
+
entry: DiscoveredRecipe,
|
|
378
379
|
): Record<string, unknown> | undefined {
|
|
379
|
-
const
|
|
380
|
+
const stored = entry.mutableUsage
|
|
381
|
+
? RecipesUsage.readRecipeUsage(entry.path)
|
|
382
|
+
: undefined;
|
|
383
|
+
if (stored) return stored;
|
|
384
|
+
const usage = (entry.config as { usage?: unknown } | undefined)?.usage;
|
|
380
385
|
return usage && typeof usage === "object" && !Array.isArray(usage)
|
|
381
386
|
? (usage as Record<string, unknown>)
|
|
382
387
|
: undefined;
|
|
@@ -409,7 +414,7 @@ function cleanupRecommendation(
|
|
|
409
414
|
actions: ["keep disabled", "delete", "archive"],
|
|
410
415
|
};
|
|
411
416
|
}
|
|
412
|
-
const usage = recipeUsage(entry
|
|
417
|
+
const usage = recipeUsage(entry);
|
|
413
418
|
const calls = Number(usage?.calls ?? 0);
|
|
414
419
|
if (entry.mutableUsage && entry.tool && calls === 0) {
|
|
415
420
|
return {
|
|
@@ -767,8 +772,8 @@ export function summarizeDiscovery(
|
|
|
767
772
|
}
|
|
768
773
|
: {}),
|
|
769
774
|
...(entry.config?.imports ? { imports: entry.config.imports } : {}),
|
|
770
|
-
...(recipeUsage(entry
|
|
771
|
-
? { usage: recipeUsage(entry
|
|
775
|
+
...(recipeUsage(entry)
|
|
776
|
+
? { usage: recipeUsage(entry) }
|
|
772
777
|
: {}),
|
|
773
778
|
}))
|
|
774
779
|
.sort((a, b) => a.id.localeCompare(b.id)),
|