@llblab/pi-actors 0.42.0 → 0.42.2
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 +2 -2
- package/BACKLOG.md +1 -5
- package/CHANGELOG.md +15 -0
- package/README.md +1 -1
- package/dist/lib/async-runs.d.ts +12 -0
- package/dist/lib/async-runs.js +53 -12
- package/dist/lib/command-templates.js +1 -1
- package/dist/lib/inspector-overlay.d.ts +5 -1
- package/dist/lib/inspector-overlay.js +104 -36
- package/dist/lib/inspector.js +1 -1
- package/dist/lib/observability.d.ts +12 -1
- package/dist/lib/observability.js +159 -80
- package/dist/lib/prompts.d.ts +1 -1
- package/dist/lib/prompts.js +1 -1
- package/dist/lib/runs-control.d.ts +2 -0
- package/dist/lib/runs-control.js +14 -1
- package/dist/lib/runs-ownership.js +17 -3
- package/dist/lib/runs-process.js +4 -3
- package/dist/lib/runs-start.js +1 -0
- package/dist/lib/runs-status.js +3 -0
- package/dist/lib/tools-inspect.js +2 -1
- package/dist/lib/tools-local.js +17 -2
- package/dist/lib/tools-spawn.js +10 -1
- package/dist/scripts/async-runner.mjs +24 -24
- package/dist/scripts/build-dist.mjs +6 -1
- package/dist/scripts/conformance.mjs +6 -1
- package/dist/scripts/recipe-utils.mjs +3 -3
- package/dist/skills/actors/SKILL.md +3 -3
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/actor-inspector.md +3 -3
- package/docs/async-runs.md +10 -4
- package/docs/recipe-library.md +1 -1
- package/docs/tool-registry.md +2 -0
- package/lib/async-runs.ts +72 -12
- package/lib/command-templates.ts +1 -1
- package/lib/inspector-overlay.ts +129 -36
- package/lib/inspector.ts +1 -1
- package/lib/observability.ts +194 -76
- package/lib/prompts.ts +1 -1
- package/lib/runs-control.ts +20 -1
- package/lib/runs-ownership.ts +22 -3
- package/lib/runs-process.ts +4 -3
- package/lib/runs-start.ts +1 -0
- package/lib/runs-status.ts +5 -0
- package/lib/tools-inspect.ts +2 -1
- package/lib/tools-local.ts +21 -2
- package/lib/tools-spawn.ts +14 -1
- package/package.json +4 -3
- package/scripts/async-runner.mjs +24 -24
- package/scripts/build-dist.mjs +6 -1
- package/scripts/conformance.mjs +6 -1
- package/scripts/recipe-utils.mjs +3 -3
- package/skills/actors/SKILL.md +3 -3
- package/skills/swarm/SKILL.md +1 -1
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Zones: async runtime, ambient UI, diagnostics
|
|
4
4
|
* Owns ambient summaries, terminal events, and run outbox delivery for detached command-template runs
|
|
5
5
|
*/
|
|
6
|
-
import { existsSync, readdirSync, readFileSync, watch, } from "node:fs";
|
|
7
|
-
import { basename, dirname, isAbsolute, join, relative,
|
|
6
|
+
import { closeSync, existsSync, fstatSync, openSync, readdirSync, readFileSync, readSync, watch, } from "node:fs";
|
|
7
|
+
import { basename, dirname, isAbsolute, join, relative, } from "node:path";
|
|
8
8
|
import * as AsyncRuns from "./async-runs.js";
|
|
9
9
|
import * as Paths from "./paths.js";
|
|
10
10
|
import { readJsonlFileResilient } from "./state-readers.js";
|
|
@@ -54,6 +54,13 @@ export function deliverRunTransitionNotifications(transitions, sink, inFlight =
|
|
|
54
54
|
AsyncRuns.markRunTerminalNotificationHandled(transition.stateDir, transition.to);
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (transition.stateDir) {
|
|
59
|
+
AsyncRuns.recordRunTerminalDeliveryFailure(transition.stateDir, transition.to, error);
|
|
60
|
+
}
|
|
61
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
62
|
+
sink.notify(`Actor terminal delivery failed for run:${transition.run}: ${message.replaceAll(/\s+/g, " ").slice(0, 240)}`, "error");
|
|
63
|
+
}
|
|
57
64
|
finally {
|
|
58
65
|
inFlight.delete(key);
|
|
59
66
|
}
|
|
@@ -282,6 +289,121 @@ function scanRunStateDirs(stateRoot, depth = 0, seen = new Set()) {
|
|
|
282
289
|
}
|
|
283
290
|
return result;
|
|
284
291
|
}
|
|
292
|
+
const TERMINAL_RESULT_BYTES = 8 * 1024;
|
|
293
|
+
const TERMINAL_RESULT_CHARS = 4_000;
|
|
294
|
+
function readBoundedStart(path) {
|
|
295
|
+
if (!existsSync(path))
|
|
296
|
+
return "";
|
|
297
|
+
const fd = openSync(path, "r");
|
|
298
|
+
try {
|
|
299
|
+
const size = Math.min(fstatSync(fd).size, TERMINAL_RESULT_BYTES);
|
|
300
|
+
const buffer = Buffer.alloc(size);
|
|
301
|
+
const bytes = readSync(fd, buffer, 0, size, 0);
|
|
302
|
+
const text = buffer.subarray(0, bytes).toString("utf8").trim();
|
|
303
|
+
return text.length > TERMINAL_RESULT_CHARS
|
|
304
|
+
? `${text.slice(0, TERMINAL_RESULT_CHARS - 1)}…`
|
|
305
|
+
: text;
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
closeSync(fd);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function semanticBodyFromStdout(stateDir) {
|
|
312
|
+
const text = readBoundedStart(join(stateDir, "stdout.log"));
|
|
313
|
+
if (!text)
|
|
314
|
+
return undefined;
|
|
315
|
+
const withoutMarker = text.replace(/^ACTOR_REVIEW_RESULT\s*(?:\r?\n)?/, "").trim();
|
|
316
|
+
return withoutMarker || undefined;
|
|
317
|
+
}
|
|
318
|
+
function terminalSemanticResult(status, stateDir, observedStatus) {
|
|
319
|
+
const correlation = status.launch_correlation &&
|
|
320
|
+
typeof status.launch_correlation === "object" &&
|
|
321
|
+
!Array.isArray(status.launch_correlation)
|
|
322
|
+
? status.launch_correlation
|
|
323
|
+
: {};
|
|
324
|
+
const correlationId = typeof correlation.correlation_id === "string"
|
|
325
|
+
? correlation.correlation_id
|
|
326
|
+
: typeof correlation.tool_call_id === "string"
|
|
327
|
+
? correlation.tool_call_id
|
|
328
|
+
: undefined;
|
|
329
|
+
const mailbox = status.mailbox &&
|
|
330
|
+
typeof status.mailbox === "object" &&
|
|
331
|
+
!Array.isArray(status.mailbox)
|
|
332
|
+
? status.mailbox
|
|
333
|
+
: {};
|
|
334
|
+
const emits = Array.isArray(mailbox.emits)
|
|
335
|
+
? mailbox.emits.filter((item) => typeof item === "string")
|
|
336
|
+
: [];
|
|
337
|
+
const outbox = readJsonlFileResilient(join(stateDir, "outbox.jsonl")).records;
|
|
338
|
+
const explicit = outbox.findLast((record) => {
|
|
339
|
+
const type = String(record.type ?? record.event ?? "");
|
|
340
|
+
return emits.includes(type) && !["command.done", "run.done", "run.failed"].includes(type);
|
|
341
|
+
});
|
|
342
|
+
if (explicit) {
|
|
343
|
+
const type = String(explicit.type ?? explicit.event);
|
|
344
|
+
return {
|
|
345
|
+
...(explicit.body === undefined
|
|
346
|
+
? {} : { body: formatSemanticBody(explicit.body) }),
|
|
347
|
+
...(typeof explicit.correlation_id === "string"
|
|
348
|
+
? { correlationId: explicit.correlation_id } : correlationId ? { correlationId } : {}),
|
|
349
|
+
metadata: {
|
|
350
|
+
...(explicit.metadata &&
|
|
351
|
+
typeof explicit.metadata === "object" &&
|
|
352
|
+
!Array.isArray(explicit.metadata)
|
|
353
|
+
? explicit.metadata : {}),
|
|
354
|
+
...(status.transport_context &&
|
|
355
|
+
typeof status.transport_context === "object" &&
|
|
356
|
+
!Array.isArray(status.transport_context)
|
|
357
|
+
? {
|
|
358
|
+
transport_context: status.transport_context,
|
|
359
|
+
} : {}),
|
|
360
|
+
run: String(status.run ?? ""),
|
|
361
|
+
status: observedStatus,
|
|
362
|
+
},
|
|
363
|
+
summary: String(explicit.summary ?? type),
|
|
364
|
+
synthesized: false,
|
|
365
|
+
type,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
const reviewCompleted = observedStatus === "done" && emits.includes("review.completed");
|
|
369
|
+
const result = status.result &&
|
|
370
|
+
typeof status.result === "object" &&
|
|
371
|
+
!Array.isArray(status.result)
|
|
372
|
+
? status.result
|
|
373
|
+
: {};
|
|
374
|
+
const type = reviewCompleted
|
|
375
|
+
? "review.completed"
|
|
376
|
+
: observedStatus === "done" ? "run.done" : "run.failed";
|
|
377
|
+
const stdoutBody = observedStatus === "done"
|
|
378
|
+
? semanticBodyFromStdout(stateDir) : undefined;
|
|
379
|
+
return {
|
|
380
|
+
...(stdoutBody
|
|
381
|
+
? { body: stdoutBody }
|
|
382
|
+
: typeof result.error === "string" ? { body: result.error } : {}),
|
|
383
|
+
...(correlationId ? { correlationId } : {}),
|
|
384
|
+
metadata: {
|
|
385
|
+
...(status.transport_context &&
|
|
386
|
+
typeof status.transport_context === "object" &&
|
|
387
|
+
!Array.isArray(status.transport_context)
|
|
388
|
+
? {
|
|
389
|
+
transport_context: status.transport_context,
|
|
390
|
+
} : {}),
|
|
391
|
+
run: String(status.run ?? ""), status: observedStatus,
|
|
392
|
+
},
|
|
393
|
+
summary: reviewCompleted ? "Review completed." :
|
|
394
|
+
observedStatus === "done" ? "Run completed." : `Run ${observedStatus}.`,
|
|
395
|
+
synthesized: true,
|
|
396
|
+
type,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function formatSemanticBody(body) {
|
|
400
|
+
const rendered = typeof body === "string" ? body : JSON.stringify(body);
|
|
401
|
+
const text = typeof rendered === "string" ? rendered : String(body);
|
|
402
|
+
const compact = text.trim();
|
|
403
|
+
return compact.length > TERMINAL_RESULT_CHARS
|
|
404
|
+
? `${compact.slice(0, TERMINAL_RESULT_CHARS - 1)}…`
|
|
405
|
+
: compact;
|
|
406
|
+
}
|
|
285
407
|
function observeRun(stateDir) {
|
|
286
408
|
try {
|
|
287
409
|
const status = AsyncRuns.getRunStatus(stateDir);
|
|
@@ -289,9 +411,15 @@ function observeRun(stateDir) {
|
|
|
289
411
|
const run = typeof status.run === "string" ? status.run : undefined;
|
|
290
412
|
if (!run)
|
|
291
413
|
return undefined;
|
|
414
|
+
const observedStatus = status.status;
|
|
292
415
|
return {
|
|
293
416
|
activeSubagents: toNumber(progress.activeSubagents),
|
|
294
417
|
completed: toNumber(progress.completed),
|
|
418
|
+
...(status.launch_correlation &&
|
|
419
|
+
typeof status.launch_correlation === "object" &&
|
|
420
|
+
!Array.isArray(status.launch_correlation)
|
|
421
|
+
? { launchCorrelation: status.launch_correlation }
|
|
422
|
+
: {}),
|
|
295
423
|
failures: Array.isArray(progress.failures)
|
|
296
424
|
? progress.failures.length
|
|
297
425
|
: undefined,
|
|
@@ -321,9 +449,12 @@ function observeRun(stateDir) {
|
|
|
321
449
|
...(typeof status.retire_when === "string"
|
|
322
450
|
? { retireWhen: status.retire_when }
|
|
323
451
|
: {}),
|
|
452
|
+
...(TERMINAL.has(observedStatus)
|
|
453
|
+
? { semanticResult: terminalSemanticResult(status, stateDir, observedStatus) }
|
|
454
|
+
: {}),
|
|
324
455
|
run,
|
|
325
456
|
stateDir,
|
|
326
|
-
status:
|
|
457
|
+
status: observedStatus,
|
|
327
458
|
...(typeof status.tool === "string" ? { tool: status.tool } : {}),
|
|
328
459
|
updatedAt: getUpdatedAt(status),
|
|
329
460
|
};
|
|
@@ -626,9 +757,11 @@ export function detectRunTransitions(previous, summary) {
|
|
|
626
757
|
run: run.run,
|
|
627
758
|
...(run.stateDir ? { stateDir: run.stateDir } : {}),
|
|
628
759
|
...(run.artifacts ? { artifacts: run.artifacts } : {}),
|
|
760
|
+
...(run.launchCorrelation ? { launchCorrelation: run.launchCorrelation } : {}),
|
|
629
761
|
...(run.launchSource ? { launchSource: run.launchSource } : {}),
|
|
630
762
|
...(run.modelPolicy ? { modelPolicy: run.modelPolicy } : {}),
|
|
631
763
|
...(run.recipeFile ? { recipeFile: run.recipeFile } : {}),
|
|
764
|
+
...(run.semanticResult ? { semanticResult: run.semanticResult } : {}),
|
|
632
765
|
...(run.terminalHandled ? { terminalHandled: true } : {}),
|
|
633
766
|
to: run.status,
|
|
634
767
|
...(run.tool ? { tool: run.tool } : {}),
|
|
@@ -825,83 +958,29 @@ export function shouldNotifyRunTransition(transition) {
|
|
|
825
958
|
export function shouldSendRunTransitionFollowUp(transition) {
|
|
826
959
|
return shouldNotifyRunTransition(transition);
|
|
827
960
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
if (
|
|
841
|
-
return
|
|
842
|
-
const
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
if (isUserRecipeFile(transition.recipeFile))
|
|
850
|
-
return false;
|
|
851
|
-
return Boolean(transition.recipeFile) || transition.launchSource === "spawn";
|
|
852
|
-
}
|
|
853
|
-
function formatRecipePersistenceSuggestion(transition) {
|
|
854
|
-
if (!shouldSuggestRecipePersistence(transition))
|
|
855
|
-
return "";
|
|
856
|
-
if (transition.recipeFile) {
|
|
857
|
-
return `\nAgent note: this actor completed successfully from recipe ${transition.recipeFile}. If this recipe fits this machine's recurring workflow, ask the operator whether to copy or register it as a durable tool recipe under ~/.pi/agent/recipes. Do not auto-save without confirmation.`;
|
|
858
|
-
}
|
|
859
|
-
return `\nAgent note: this actor was spawned directly and completed successfully. If this pattern fits this machine's recurring workflow, ask the operator whether to save it as a durable recipe/tool under ~/.pi/agent/recipes with register_tool. Do not auto-save without confirmation.`;
|
|
860
|
-
}
|
|
861
|
-
function formatTransitionPolicy(transition) {
|
|
862
|
-
if (!transition.modelPolicy)
|
|
863
|
-
return "";
|
|
864
|
-
const axis = (key, label) => {
|
|
865
|
-
const value = transition.modelPolicy?.[key];
|
|
866
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
867
|
-
return undefined;
|
|
868
|
-
const record = value;
|
|
869
|
-
const source = typeof record.source === "string" ? record.source : "unused";
|
|
870
|
-
if (source === "unused")
|
|
871
|
-
return undefined;
|
|
872
|
-
const renderedValue = typeof record.value === "string" && record.value.trim()
|
|
873
|
-
? ` (${record.value.trim()})`
|
|
874
|
-
: "";
|
|
875
|
-
return `${label}: ${source}${renderedValue}`;
|
|
876
|
-
};
|
|
877
|
-
const lines = [axis("model", "Model"), axis("thinking", "Thinking")].filter((line) => Boolean(line));
|
|
878
|
-
return lines.length ? `\nPolicy:\n- ${lines.join("\n- ")}` : "";
|
|
879
|
-
}
|
|
880
|
-
function formatTransitionNextActions(transition) {
|
|
881
|
-
const actions = [
|
|
882
|
-
`inspect target=run:${transition.run} view=status`,
|
|
883
|
-
transition.to === "done" && Object.keys(transition.artifacts ?? {}).length > 0
|
|
884
|
-
? `inspect target=run:${transition.run} view=artifacts`
|
|
885
|
-
: `inspect target=run:${transition.run} view=tail`,
|
|
886
|
-
`inspect target=run:${transition.run} view=messages`,
|
|
887
|
-
].filter(Boolean);
|
|
888
|
-
return `\nNext actions: ${actions.join(" | ")}`;
|
|
961
|
+
const TERMINAL_FOLLOW_UP_ARTIFACT_LIMIT = 4;
|
|
962
|
+
const TERMINAL_FOLLOW_UP_IDENTIFIER_CHARS = 120;
|
|
963
|
+
const TERMINAL_FOLLOW_UP_PATH_CHARS = 320;
|
|
964
|
+
function compactTerminalText(value, limit) {
|
|
965
|
+
const compact = value.replaceAll(/\s+/g, " ").trim();
|
|
966
|
+
return compact.length > limit ? `${compact.slice(0, limit - 1)}…` : compact;
|
|
967
|
+
}
|
|
968
|
+
function formatTerminalPath(path) {
|
|
969
|
+
return `\`${compactTerminalText(path, TERMINAL_FOLLOW_UP_PATH_CHARS)}\``;
|
|
970
|
+
}
|
|
971
|
+
function formatTerminalResultLocations(transition) {
|
|
972
|
+
const artifactPaths = [...new Set(Object.values(transition.artifacts ?? {}).filter((path) => typeof path === "string" && path.length > 0))];
|
|
973
|
+
if (artifactPaths.length === 0)
|
|
974
|
+
return transition.stateDir ? `\nBase: ${formatTerminalPath(transition.stateDir)}` : "";
|
|
975
|
+
const base = commonDirectory(artifactPaths);
|
|
976
|
+
const artifactPreview = artifactPaths
|
|
977
|
+
.slice(0, TERMINAL_FOLLOW_UP_ARTIFACT_LIMIT)
|
|
978
|
+
.map((path) => formatTerminalPath(base ? relativeName(base, path) : path));
|
|
979
|
+
const omitted = artifactPaths.length - artifactPreview.length;
|
|
980
|
+
const artifacts = `${artifactPreview.join(", ")}${omitted > 0 ? ` (+${omitted} more)` : ""}`;
|
|
981
|
+
return `${base ? `\nBase: ${formatTerminalPath(base)}` : ""}\nArtifacts: ${artifacts}`;
|
|
889
982
|
}
|
|
890
983
|
export function formatRunTransitionMessage(transition) {
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
const persistenceSuggestion = formatRecipePersistenceSuggestion(transition);
|
|
894
|
-
const policy = formatTransitionPolicy(transition);
|
|
895
|
-
const nextActions = formatTransitionNextActions(transition);
|
|
896
|
-
if (transition.to === "done")
|
|
897
|
-
return `Run ${transition.run} completed successfully.${policy}${artifacts}${runFiles}${nextActions}${persistenceSuggestion}`;
|
|
898
|
-
if (transition.to === "failed")
|
|
899
|
-
return `Run ${transition.run} failed.${policy}${artifacts}${runFiles}${nextActions}`;
|
|
900
|
-
if (transition.to === "cancelled")
|
|
901
|
-
return `Run ${transition.run} was cancelled.${policy}${nextActions}`;
|
|
902
|
-
if (transition.to === "killed")
|
|
903
|
-
return `Run ${transition.run} was force-killed.${policy}${nextActions}`;
|
|
904
|
-
if (transition.to === "exited")
|
|
905
|
-
return `Run ${transition.run} exited before writing a result.${policy}${nextActions}`;
|
|
906
|
-
return `Run ${transition.run} finished with status ${transition.to}.${policy}${nextActions}`;
|
|
984
|
+
const run = compactTerminalText(transition.run, TERMINAL_FOLLOW_UP_IDENTIFIER_CHARS);
|
|
985
|
+
return `Run: \`${run}\`\nStatus: \`${transition.to}\`${formatTerminalResultLocations(transition)}`;
|
|
907
986
|
}
|
package/dist/lib/prompts.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
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 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.";
|
|
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. Terminal follow-up content stays minimal: run, status, one base path, and relative artifact names only; semantic output stays in non-LLM details and run state. 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.";
|
package/dist/lib/prompts.js
CHANGED
|
@@ -23,7 +23,7 @@ export const ONBOARDING_SYSTEM_PROMPT = `pi-actors quick model:
|
|
|
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. 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.
|
|
26
|
+
- 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. Terminal follow-up content stays minimal: run, status, one base path, and relative artifact names only; semantic output stays in non-LLM details and run state. 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.
|
|
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. 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.`;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Async run process control primitives.
|
|
3
3
|
* Owns: platform signal planning, owned-process signalling, and terminal control markers.
|
|
4
4
|
*/
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
5
6
|
import { type RunProcessIdentity, type RunProcessIdentityResult } from "./runs-process.ts";
|
|
6
7
|
export interface RunProcessSignalPlan {
|
|
7
8
|
args?: string[];
|
|
@@ -12,6 +13,7 @@ export declare function getRunProcessSignalPlan(pid: number, signal: NodeJS.Sign
|
|
|
12
13
|
export interface RunProcessSignalDeps {
|
|
13
14
|
killProcess?: typeof process.kill;
|
|
14
15
|
runtimePlatform?: NodeJS.Platform;
|
|
16
|
+
spawnProcess?: typeof spawnSync;
|
|
15
17
|
verifyIdentity?: (pid: number, expected: RunProcessIdentity, runtimePlatform: NodeJS.Platform) => RunProcessIdentityResult;
|
|
16
18
|
}
|
|
17
19
|
export declare function signalOwnedRunProcess(pid: number, signal: NodeJS.Signals, expectedIdentity?: RunProcessIdentity, deps?: RunProcessSignalDeps): RunProcessSignalPlan;
|
package/dist/lib/runs-control.js
CHANGED
|
@@ -31,8 +31,21 @@ export function signalOwnedRunProcess(pid, signal, expectedIdentity, deps = {})
|
|
|
31
31
|
}
|
|
32
32
|
const plan = getRunProcessSignalPlan(pid, signal, runtimePlatform);
|
|
33
33
|
if (plan.command && plan.args) {
|
|
34
|
-
const
|
|
34
|
+
const spawnProcess = deps.spawnProcess ?? spawnSync;
|
|
35
|
+
let result = spawnProcess(plan.command, plan.args, { encoding: "utf8" });
|
|
36
|
+
if (runtimePlatform === "win32" &&
|
|
37
|
+
result.status !== 0 &&
|
|
38
|
+
!plan.args.includes("/F")) {
|
|
39
|
+
result = spawnProcess(plan.command, [...plan.args, "/F"], {
|
|
40
|
+
encoding: "utf8",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
35
43
|
if (result.status !== 0) {
|
|
44
|
+
if (expectedIdentity) {
|
|
45
|
+
const finalProof = (deps.verifyIdentity ?? verifyRunProcessIdentity)(pid, expectedIdentity, runtimePlatform);
|
|
46
|
+
if (finalProof.status === "dead_pid")
|
|
47
|
+
return plan;
|
|
48
|
+
}
|
|
36
49
|
throw new Error(result.stderr?.trim() ||
|
|
37
50
|
result.stdout?.trim() ||
|
|
38
51
|
`${plan.command} failed`);
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, } from "node:fs";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
-
import {
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
8
9
|
import { writeJsonAtomic } from "./file-state.js";
|
|
9
10
|
export const RUN_STATE_OWNERSHIP_FILE = ".pi-actors-run-state.json";
|
|
10
11
|
function markerPath(stateDir) {
|
|
@@ -14,13 +15,26 @@ function comparablePath(path) {
|
|
|
14
15
|
const resolved = resolve(path);
|
|
15
16
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
16
17
|
}
|
|
18
|
+
function isSystemTempRootAlias(resolved, canonical) {
|
|
19
|
+
const tempRoot = resolve(tmpdir());
|
|
20
|
+
const relativeStateDir = relative(tempRoot, resolved);
|
|
21
|
+
if (relativeStateDir === ".." ||
|
|
22
|
+
relativeStateDir.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) ||
|
|
23
|
+
isAbsolute(relativeStateDir)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const canonicalTempRoot = realpathSync.native(tempRoot);
|
|
27
|
+
const expectedCanonical = resolve(canonicalTempRoot, relativeStateDir);
|
|
28
|
+
return comparablePath(canonical) === comparablePath(expectedCanonical);
|
|
29
|
+
}
|
|
17
30
|
function assertCanonicalDirectory(stateDir) {
|
|
18
31
|
const resolved = resolve(stateDir);
|
|
19
32
|
if (lstatSync(resolved).isSymbolicLink()) {
|
|
20
33
|
throw new Error(`Run state directory cannot be a symlink: ${resolved}`);
|
|
21
34
|
}
|
|
22
|
-
const canonical = realpathSync(resolved);
|
|
23
|
-
if (comparablePath(canonical) !== comparablePath(resolved)
|
|
35
|
+
const canonical = realpathSync.native(resolved);
|
|
36
|
+
if (comparablePath(canonical) !== comparablePath(resolved) &&
|
|
37
|
+
!isSystemTempRootAlias(resolved, canonical)) {
|
|
24
38
|
throw new Error(`Run state directory has an ambiguous symlink alias: ${resolved}`);
|
|
25
39
|
}
|
|
26
40
|
return resolved;
|
package/dist/lib/runs-process.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import { existsSync, readFileSync, readlinkSync, realpathSync } from "node:fs";
|
|
7
7
|
import { platform } from "node:os";
|
|
8
|
-
import {
|
|
8
|
+
import { posix, win32 } from "node:path";
|
|
9
9
|
export function isAlive(pid) {
|
|
10
10
|
try {
|
|
11
11
|
process.kill(pid, 0);
|
|
@@ -90,8 +90,9 @@ export function captureRunProcessIdentity(pid, cwd, stateDir, runnerPath, runtim
|
|
|
90
90
|
if (!identity.command.includes(runnerPath) || !identity.command.includes(stateDir)) {
|
|
91
91
|
return undefined;
|
|
92
92
|
}
|
|
93
|
-
const
|
|
94
|
-
const
|
|
93
|
+
const pathApi = runtimePlatform === "win32" ? win32 : posix;
|
|
94
|
+
const resolvedCwd = pathApi.resolve(cwd);
|
|
95
|
+
const canonicalCwd = runtimePlatform === platform() && existsSync(resolvedCwd)
|
|
95
96
|
? realpathSync.native(resolvedCwd)
|
|
96
97
|
: resolvedCwd;
|
|
97
98
|
const expectedCwd = runtimePlatform === "win32" ? canonicalCwd.toLowerCase() : canonicalCwd;
|
package/dist/lib/runs-start.js
CHANGED
package/dist/lib/runs-status.js
CHANGED
|
@@ -30,6 +30,7 @@ export function buildRunStatus(stateDir, runOrDir, meta, readJson, _runnerPath,
|
|
|
30
30
|
? "running"
|
|
31
31
|
: (getInterruptedRunStatus(stateDir) ?? "exited");
|
|
32
32
|
const terminalHandled = readJson(join(stateDir, "terminal-handled.json"));
|
|
33
|
+
const terminalDeliveryFailure = readJson(join(stateDir, "terminal-delivery-failure.json"));
|
|
33
34
|
return {
|
|
34
35
|
...meta,
|
|
35
36
|
eventsFile: join(stateDir, "events.jsonl"),
|
|
@@ -39,6 +40,8 @@ export function buildRunStatus(stateDir, runOrDir, meta, readJson, _runnerPath,
|
|
|
39
40
|
process_identity_status: processIdentity.status,
|
|
40
41
|
progress: readJson(join(stateDir, "progress.json")) || null,
|
|
41
42
|
result: result || null,
|
|
43
|
+
...(terminalDeliveryFailure
|
|
44
|
+
? { terminal_delivery_failure: terminalDeliveryFailure } : {}),
|
|
42
45
|
...(terminalHandled ? { terminal_handled: terminalHandled } : {}),
|
|
43
46
|
state_dir: String(meta.state_dir ?? stateDir),
|
|
44
47
|
stderrLog: join(stateDir, "stderr.log"),
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
9
10
|
import * as AsyncRuns from "./async-runs.js";
|
|
10
11
|
import * as Limits from "./limits.js";
|
|
11
12
|
import * as Messages from "./messages.js";
|
|
@@ -229,7 +230,7 @@ function getPiActorsRuntimeStatus() {
|
|
|
229
230
|
catch {
|
|
230
231
|
git_commit = undefined;
|
|
231
232
|
}
|
|
232
|
-
const entrypoint =
|
|
233
|
+
const entrypoint = fileURLToPath(import.meta.url);
|
|
233
234
|
return {
|
|
234
235
|
automatic_recipe_review: Paths.isAutomaticRecipeReviewEnabled(),
|
|
235
236
|
entrypoint,
|
package/dist/lib/tools-local.js
CHANGED
|
@@ -100,6 +100,10 @@ export function createRuntimeToolDefinition(cfg, exec) {
|
|
|
100
100
|
}
|
|
101
101
|
if (isAsyncRecipe)
|
|
102
102
|
paramSchema.run_id = Schema.stringSchema("Optional run id override for this async template recipe invocation.");
|
|
103
|
+
if (isAsyncRecipe) {
|
|
104
|
+
paramSchema.correlation_id = Schema.stringSchema("Optional workflow correlation id preserved in terminal follow-up delivery.");
|
|
105
|
+
paramSchema.transport_context = Schema.looseObjectSchema("Optional originating transport route preserved for detached terminal follow-up.");
|
|
106
|
+
}
|
|
103
107
|
return {
|
|
104
108
|
name: cfg.name,
|
|
105
109
|
label: cfg.name,
|
|
@@ -108,7 +112,7 @@ export function createRuntimeToolDefinition(cfg, exec) {
|
|
|
108
112
|
promptSnippet: isRecipe
|
|
109
113
|
? Prompts.formatRecipeToolPromptSnippet(cfg.recipe?.name ?? String(cfg.template), isAsyncRecipe)
|
|
110
114
|
: Prompts.formatRegisteredToolPromptSnippet(cfg.template),
|
|
111
|
-
async execute(
|
|
115
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
112
116
|
try {
|
|
113
117
|
if (cfg.sourcePath &&
|
|
114
118
|
!RecipesUsage.recordRecipeLaunch(cfg.sourcePath, new Date(), "tool")) {
|
|
@@ -116,7 +120,7 @@ export function createRuntimeToolDefinition(cfg, exec) {
|
|
|
116
120
|
}
|
|
117
121
|
if (isAsyncRecipe) {
|
|
118
122
|
const input = params;
|
|
119
|
-
const { run_id, ...values } = input;
|
|
123
|
+
const { correlation_id, run_id, transport_context, ...values } = input;
|
|
120
124
|
const base = cfg.recipe ? cfg.recipe : { file: String(cfg.template) };
|
|
121
125
|
const runId = typeof run_id === "string" && run_id.trim()
|
|
122
126
|
? run_id.trim()
|
|
@@ -124,6 +128,17 @@ export function createRuntimeToolDefinition(cfg, exec) {
|
|
|
124
128
|
const meta = AsyncRuns.startRun({
|
|
125
129
|
...base,
|
|
126
130
|
launch_source: "tool",
|
|
131
|
+
launch_correlation: {
|
|
132
|
+
...(typeof correlation_id === "string"
|
|
133
|
+
? { correlation_id } : {}),
|
|
134
|
+
tool_call_id: toolCallId,
|
|
135
|
+
},
|
|
136
|
+
...(transport_context &&
|
|
137
|
+
typeof transport_context === "object" &&
|
|
138
|
+
!Array.isArray(transport_context)
|
|
139
|
+
? {
|
|
140
|
+
transport_context: transport_context,
|
|
141
|
+
} : {}),
|
|
127
142
|
ownerId: getRunOwnerId(ctx),
|
|
128
143
|
run_id: runId,
|
|
129
144
|
tool: cfg.name,
|
package/dist/lib/tools-spawn.js
CHANGED
|
@@ -95,6 +95,7 @@ export function createSpawnToolDefinition() {
|
|
|
95
95
|
parameters: Schema.objectSchema({
|
|
96
96
|
artifacts: Schema.looseObjectSchema("Optional named artifact paths for the spawned actor."),
|
|
97
97
|
as: Schema.stringSchema("Optional actor address for the spawned run, e.g. run:<id>."),
|
|
98
|
+
correlation_id: Schema.stringSchema("Optional workflow correlation id preserved in terminal follow-up delivery."),
|
|
98
99
|
file: Schema.stringSchema("Optional template recipe JSON file. Bare names resolve under ~/.pi/agent/recipes."),
|
|
99
100
|
recipe: Schema.stringSchema("Alias for file; template recipe JSON file/name to spawn."),
|
|
100
101
|
template: Schema.unionSchema([
|
|
@@ -103,9 +104,10 @@ export function createSpawnToolDefinition() {
|
|
|
103
104
|
Schema.looseObjectSchema("Inline command-template object with flags such as parallel, repeat, retry, failure, and nested template."),
|
|
104
105
|
]),
|
|
105
106
|
values: Schema.looseObjectSchema("Runtime placeholder values passed to the actor."),
|
|
107
|
+
transport_context: Schema.looseObjectSchema("Optional originating transport route preserved for detached terminal follow-up, e.g. Telegram chat_id and thread_id."),
|
|
106
108
|
verbose: Schema.booleanSchema("Return full JSON instead of compact text."),
|
|
107
109
|
}, []),
|
|
108
|
-
async execute(
|
|
110
|
+
async execute(toolCallId, params, _signal, _onUpdate, ctx) {
|
|
109
111
|
const input = asRecord(params);
|
|
110
112
|
if (input.state_dir !== undefined) {
|
|
111
113
|
throw new Error("spawn.state_dir is not supported; run state is runtime-owned so run:<id> remains addressable and retention-safe.");
|
|
@@ -121,7 +123,14 @@ export function createSpawnToolDefinition() {
|
|
|
121
123
|
meta = AsyncRuns.startRun({
|
|
122
124
|
file: recipe,
|
|
123
125
|
launch_source: "spawn",
|
|
126
|
+
launch_correlation: {
|
|
127
|
+
...(typeof input.correlation_id === "string"
|
|
128
|
+
? { correlation_id: input.correlation_id } : {}),
|
|
129
|
+
tool_call_id: toolCallId,
|
|
130
|
+
},
|
|
124
131
|
ownerId: getRunOwnerId(ctx),
|
|
132
|
+
...(input.transport_context
|
|
133
|
+
? { transport_context: asRecord(input.transport_context) } : {}),
|
|
125
134
|
run_id: runId,
|
|
126
135
|
...(input.template !== undefined
|
|
127
136
|
? {
|