@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/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`;
|
|
@@ -50,7 +50,7 @@ export interface RunUiSnapshot {
|
|
|
50
50
|
}
|
|
51
51
|
export interface RunUiNotificationSink {
|
|
52
52
|
notify(message: string, level: "info" | "warning" | "error"): void;
|
|
53
|
-
|
|
53
|
+
sendSteering(message: {
|
|
54
54
|
customType: string;
|
|
55
55
|
content: string;
|
|
56
56
|
display: true;
|
|
@@ -37,12 +37,15 @@ export function deliverRunTransitionNotifications(transitions, sink) {
|
|
|
37
37
|
sink.notify(text, getRunTransitionNotificationType(transition));
|
|
38
38
|
if (!shouldSendRunTransitionFollowUp(transition))
|
|
39
39
|
continue;
|
|
40
|
-
sink.
|
|
40
|
+
sink.sendSteering({
|
|
41
41
|
customType: "pi-actors-run",
|
|
42
42
|
content: text,
|
|
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) {
|
|
@@ -53,7 +56,7 @@ export function deliverRunOutboxNotifications(events, sink) {
|
|
|
53
56
|
sink.notify(text, getRunOutboxNotificationType(event));
|
|
54
57
|
if (!shouldSendRunOutboxFollowUp(event))
|
|
55
58
|
continue;
|
|
56
|
-
sink.
|
|
59
|
+
sink.sendSteering({
|
|
57
60
|
customType: "pi-actors-run-message",
|
|
58
61
|
content: text,
|
|
59
62
|
display: true,
|
|
@@ -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/pi.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
7
7
|
export type { ExtensionAPI, ExtensionContext };
|
|
8
8
|
export interface PiNotificationSink {
|
|
9
9
|
notify(message: string, level: "info" | "warning" | "error"): void;
|
|
10
|
-
|
|
10
|
+
sendSteering(message: {
|
|
11
11
|
customType: string;
|
|
12
12
|
content: string;
|
|
13
13
|
display: true;
|
package/dist/lib/pi.js
CHANGED
|
@@ -9,8 +9,8 @@ export function getSessionId(ctx) {
|
|
|
9
9
|
export function createNotificationSink(pi, ctx) {
|
|
10
10
|
return {
|
|
11
11
|
notify: (message, level) => ctx.ui.notify(message, level),
|
|
12
|
-
|
|
13
|
-
deliverAs: "
|
|
12
|
+
sendSteering: (message) => pi.sendMessage(message, {
|
|
13
|
+
deliverAs: "steer",
|
|
14
14
|
triggerTurn: true,
|
|
15
15
|
}),
|
|
16
16
|
};
|
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 leaf, array sequence, object node; flags include args/defaults, parallel, concurrency, min_successful, when, timeout, delay, retry, failure, recover, repeat, 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
|
|
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 leaf, array sequence, object node; 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. 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.\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; 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.";
|
package/dist/lib/prompts.js
CHANGED
|
@@ -16,14 +16,14 @@ export const REGISTER_TOOL_GUIDELINES = [
|
|
|
16
16
|
export const ONBOARDING_SYSTEM_PROMPT = `pi-actors quick model:
|
|
17
17
|
- Local-first actor memory: persist trusted local capabilities instead of rebuilding shell recipes.
|
|
18
18
|
- Layers: task -> command template -> recipe/tool -> spawn -> run:<id>; tool:<name> wraps registered capabilities.
|
|
19
|
-
- 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.
|
|
19
|
+
- 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.
|
|
20
20
|
- Placeholders support typed/default args plus {value??fallback} and {flag?yes:no}.
|
|
21
21
|
- ~/.pi/agent/recipes/*.json is actor muscle memory: every recipe there is auto-registered as an agent tool across sessions; register_tool writes there.
|
|
22
22
|
- Recipes own template directly and may declare metadata/defaults/imports/mailbox/artifacts; files >1 MiB or import depth >32 fail closed.
|
|
23
23
|
- Recipe imports are local variables; imported recipes are definitions, not nested async runs; parent async:true creates one run.
|
|
24
24
|
- 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.
|
|
25
25
|
- 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.
|
|
26
|
-
- Run state lives under ~/.pi/agent/tmp/pi-actors/runs
|
|
26
|
+
- 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.
|
|
27
27
|
- 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.
|
|
28
28
|
- 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.
|
|
29
29
|
- 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.`;
|
|
@@ -32,7 +32,6 @@ export const REGISTER_TOOL_PARAM_DESCRIPTIONS = {
|
|
|
32
32
|
description: "Describe what the tool does for the LLM. Required unless deleting; omitted updates keep the old description.",
|
|
33
33
|
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.",
|
|
34
34
|
async: "Set true for a co-located async template recipe. Omit for ordinary command templates or file-backed recipe references.",
|
|
35
|
-
state_dir: "Optional async run state directory for a co-located template recipe.",
|
|
36
35
|
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.",
|
|
37
36
|
templateArray: "Sequential command-template composition array. Leaves may be strings or objects with template/defaults/timeout/retry/failure/recover.",
|
|
38
37
|
templateNull: "Delete the tool when template is null.",
|
|
@@ -47,10 +47,10 @@ function piOptionConsumesNextArg(arg) {
|
|
|
47
47
|
function isPiFileArgument(arg) {
|
|
48
48
|
return arg.startsWith("@") && arg.length > 1;
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
function findPiPrintPromptIndexes(args) {
|
|
51
51
|
let printMode = false;
|
|
52
52
|
let positionalOnly = false;
|
|
53
|
-
|
|
53
|
+
const promptIndexes = [];
|
|
54
54
|
for (let index = 0; index < args.length; index += 1) {
|
|
55
55
|
const arg = args[index];
|
|
56
56
|
if (!positionalOnly && arg === "--") {
|
|
@@ -68,9 +68,12 @@ export function findPiPrintPromptIndex(args) {
|
|
|
68
68
|
}
|
|
69
69
|
if (!printMode || isPiFileArgument(arg))
|
|
70
70
|
continue;
|
|
71
|
-
|
|
71
|
+
promptIndexes.push(index);
|
|
72
72
|
}
|
|
73
|
-
return
|
|
73
|
+
return promptIndexes;
|
|
74
|
+
}
|
|
75
|
+
export function findPiPrintPromptIndex(args) {
|
|
76
|
+
return findPiPrintPromptIndexes(args).at(-1);
|
|
74
77
|
}
|
|
75
78
|
function matchesActorContext(record, context) {
|
|
76
79
|
if (!context)
|
|
@@ -134,14 +137,19 @@ export function appendRecipeContextToPiArgs(command, args, records, context) {
|
|
|
134
137
|
export function materializePiPrintPromptArg(command, args, promptFile) {
|
|
135
138
|
if (!isPiCommand(command))
|
|
136
139
|
return { args };
|
|
137
|
-
const
|
|
138
|
-
if (
|
|
140
|
+
const promptIndexes = findPiPrintPromptIndexes(args);
|
|
141
|
+
if (promptIndexes.length === 0)
|
|
139
142
|
return { args };
|
|
140
|
-
const prompt = args[
|
|
143
|
+
const prompt = promptIndexes.map((index) => args[index]).join(" ");
|
|
141
144
|
const path = typeof promptFile === "function" ? promptFile() : promptFile;
|
|
142
145
|
writeFileSync(path, prompt, "utf8");
|
|
143
|
-
const
|
|
144
|
-
|
|
146
|
+
const promptIndexSet = new Set(promptIndexes);
|
|
147
|
+
const firstPromptIndex = promptIndexes[0];
|
|
148
|
+
const next = args.flatMap((arg, index) => {
|
|
149
|
+
if (index === firstPromptIndex)
|
|
150
|
+
return [`@${path}`];
|
|
151
|
+
return promptIndexSet.has(index) ? [] : [arg];
|
|
152
|
+
});
|
|
145
153
|
return {
|
|
146
154
|
args: next,
|
|
147
155
|
promptBytes: Buffer.byteLength(prompt),
|