@llblab/pi-actors 0.39.0 → 0.40.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/AGENTS.md +10 -3
  2. package/BACKLOG.md +4 -11
  3. package/CHANGELOG.md +51 -0
  4. package/README.md +5 -5
  5. package/dist/index.js +6 -4
  6. package/dist/lib/async-runs.d.ts +4 -0
  7. package/dist/lib/async-runs.js +112 -17
  8. package/dist/lib/command-templates.d.ts +9 -0
  9. package/dist/lib/command-templates.js +92 -11
  10. package/dist/lib/config.js +0 -5
  11. package/dist/lib/execution.d.ts +31 -0
  12. package/dist/lib/execution.js +145 -12
  13. package/dist/lib/file-state.d.ts +1 -0
  14. package/dist/lib/file-state.js +91 -3
  15. package/dist/lib/observability.js +5 -2
  16. package/dist/lib/prompts.d.ts +1 -2
  17. package/dist/lib/prompts.js +3 -4
  18. package/dist/lib/recipes-context.js +17 -9
  19. package/dist/lib/recipes-discovery.js +11 -5
  20. package/dist/lib/recipes-references.d.ts +1 -1
  21. package/dist/lib/recipes-references.js +4 -5
  22. package/dist/lib/recipes-usage.d.ts +2 -0
  23. package/dist/lib/recipes-usage.js +35 -21
  24. package/dist/lib/registry.d.ts +0 -2
  25. package/dist/lib/registry.js +33 -10
  26. package/dist/lib/runs-ownership.d.ts +7 -0
  27. package/dist/lib/runs-ownership.js +82 -0
  28. package/dist/lib/runs-process.d.ts +17 -2
  29. package/dist/lib/runs-process.js +99 -11
  30. package/dist/lib/runs-retention.d.ts +3 -0
  31. package/dist/lib/runs-retention.js +18 -3
  32. package/dist/lib/runs-start.d.ts +2 -2
  33. package/dist/lib/runs-start.js +51 -17
  34. package/dist/lib/runs-status.d.ts +1 -1
  35. package/dist/lib/runs-status.js +8 -6
  36. package/dist/lib/runtime.js +69 -13
  37. package/dist/lib/tools-inspect.d.ts +2 -0
  38. package/dist/lib/tools-inspect.js +39 -2
  39. package/dist/lib/tools-register.js +0 -1
  40. package/dist/lib/tools-spawn.js +3 -2
  41. package/dist/lib/tools.d.ts +1 -0
  42. package/dist/lib/tools.js +3 -0
  43. package/dist/pi-actors/index.js +1 -0
  44. package/dist/recipes/subagent-judge.json +2 -1
  45. package/dist/recipes/subagent-merge.json +2 -1
  46. package/dist/recipes/subagent-normalize.json +2 -1
  47. package/dist/recipes/subagent-review-coordinator.json +1 -1
  48. package/dist/recipes/subagent-review.json +2 -1
  49. package/dist/recipes/subagent-verify.json +2 -1
  50. package/dist/scripts/async-runner.mjs +274 -6
  51. package/dist/scripts/build-dist.mjs +14 -1
  52. package/dist/skills/actors/SKILL.md +15 -8
  53. package/dist/skills/swarm/SKILL.md +4 -2
  54. package/docs/actor-messages.md +1 -1
  55. package/docs/async-runs.md +14 -5
  56. package/docs/command-templates.md +4 -2
  57. package/docs/recipe-library.md +1 -0
  58. package/docs/template-recipes.md +5 -7
  59. package/docs/tool-registry.md +4 -2
  60. package/index.ts +18 -7
  61. package/lib/async-runs.ts +138 -19
  62. package/lib/command-templates.ts +132 -13
  63. package/lib/config.ts +0 -4
  64. package/lib/execution.ts +198 -13
  65. package/lib/file-state.ts +106 -3
  66. package/lib/observability.ts +8 -2
  67. package/lib/prompts.ts +3 -5
  68. package/lib/recipes-context.ts +17 -9
  69. package/lib/recipes-discovery.ts +10 -5
  70. package/lib/recipes-references.ts +5 -6
  71. package/lib/recipes-usage.ts +36 -20
  72. package/lib/registry.ts +43 -13
  73. package/lib/runs-ownership.ts +117 -0
  74. package/lib/runs-process.ts +138 -16
  75. package/lib/runs-retention.ts +22 -2
  76. package/lib/runs-start.ts +89 -31
  77. package/lib/runs-status.ts +15 -6
  78. package/lib/runtime.ts +64 -12
  79. package/lib/tools-inspect.ts +46 -4
  80. package/lib/tools-register.ts +0 -3
  81. package/lib/tools-spawn.ts +5 -5
  82. package/lib/tools.ts +8 -0
  83. package/package.json +2 -2
  84. package/recipes/subagent-judge.json +2 -1
  85. package/recipes/subagent-merge.json +2 -1
  86. package/recipes/subagent-normalize.json +2 -1
  87. package/recipes/subagent-review-coordinator.json +1 -1
  88. package/recipes/subagent-review.json +2 -1
  89. package/recipes/subagent-verify.json +2 -1
  90. package/scripts/async-runner.mjs +274 -6
  91. package/scripts/build-dist.mjs +14 -1
  92. package/skills/actors/SKILL.md +15 -8
  93. package/skills/swarm/SKILL.md +4 -2
@@ -2,11 +2,21 @@
2
2
  * Async run retention operations.
3
3
  * Owns terminal-run archive and prune filesystem behavior.
4
4
  */
5
+ import { createHash } from "node:crypto";
5
6
  import { cpSync, mkdirSync, renameSync, rmSync } from "node:fs";
6
7
  import { basename, dirname, join } from "node:path";
7
8
  import { writeJsonAtomic } from "./file-state.js";
8
9
  import { resolveArtifactManifest, } from "./runs-artifacts.js";
9
10
  import { safeRunId } from "./runs-identity.js";
11
+ import { assertOwnedRunStateDirectory } from "./runs-ownership.js";
12
+ function retainedArtifactFilename(name, path) {
13
+ const readableName = name.replace(/[^A-Za-z0-9_.-]+/g, "_") || "artifact";
14
+ const identity = createHash("sha256")
15
+ .update(`${name}\0${path}`)
16
+ .digest("hex")
17
+ .slice(0, 8);
18
+ return `${readableName}-${identity}--${basename(path)}`;
19
+ }
10
20
  function archivePathFor(run, stateDir) {
11
21
  const archiveRoot = join(dirname(stateDir), "archived");
12
22
  mkdirSync(archiveRoot, { recursive: true });
@@ -15,6 +25,7 @@ function archivePathFor(run, stateDir) {
15
25
  export function archiveTerminalRun(status) {
16
26
  const stateDir = String(status.state_dir);
17
27
  const run = String(status.run ?? basename(stateDir));
28
+ assertOwnedRunStateDirectory(stateDir, run);
18
29
  const archiveDir = archivePathFor(run, stateDir);
19
30
  renameSync(stateDir, archiveDir);
20
31
  mkdirSync(stateDir, { recursive: true });
@@ -29,9 +40,10 @@ export function archiveTerminalRun(status) {
29
40
  writeJsonAtomic(join(stateDir, "archive-tombstone.json"), tombstone);
30
41
  return tombstone;
31
42
  }
32
- export function pruneTerminalRun(status, options = {}) {
43
+ export function pruneTerminalRun(status, options = {}, deps = {}) {
33
44
  const stateDir = String(status.state_dir);
34
45
  const run = String(status.run ?? basename(stateDir));
46
+ assertOwnedRunStateDirectory(stateDir, run);
35
47
  const manifest = resolveArtifactManifest(status.artifacts);
36
48
  const preserved = {};
37
49
  if (options.preserveArtifacts && manifest) {
@@ -40,8 +52,11 @@ export function pruneTerminalRun(status, options = {}) {
40
52
  for (const [name, artifact] of Object.entries(manifest)) {
41
53
  if (!artifact.exists)
42
54
  continue;
43
- const target = join(preserveRoot, basename(artifact.path));
44
- cpSync(artifact.path, target, { force: true });
55
+ const target = join(preserveRoot, retainedArtifactFilename(name, artifact.path));
56
+ (deps.copyArtifact ?? cpSync)(artifact.path, target, {
57
+ force: true,
58
+ preserveTimestamps: true,
59
+ });
45
60
  preserved[name] = target;
46
61
  }
47
62
  }
@@ -4,6 +4,6 @@
4
4
  * preparation before a new runner process is spawned.
5
5
  */
6
6
  export type RunJsonReader = (path: string) => Record<string, unknown> | undefined;
7
- export declare function assertNoActiveRunState(stateDir: string, readJson: RunJsonReader, runnerPath: string): void;
7
+ export declare function assertNoActiveRunState(stateDir: string, readJson: RunJsonReader, _runnerPath: string): void;
8
8
  export declare function acquireStateStartLock(stateDir: string): () => void;
9
- export declare function prepareStateDirForStart(stateDir: string, readJson: RunJsonReader, runnerPath: string): void;
9
+ export declare function prepareStateDirForStart(stateDir: string, readJson: RunJsonReader, _runnerPath: string): void;
@@ -3,33 +3,64 @@
3
3
  * Owns active-run reuse checks, start lock acquisition, and safe state-dir
4
4
  * preparation before a new runner process is spawned.
5
5
  */
6
- import { mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";
6
+ import { mkdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
7
7
  import { join } from "node:path";
8
- import { isAlive, pidMatchesRun } from "./runs-process.js";
8
+ import { isAlive, readProcessIdentity, verifyRunProcessIdentity, } from "./runs-process.js";
9
9
  const START_LOCK_MAX_AGE_MS = 5 * 60 * 1000;
10
- export function assertNoActiveRunState(stateDir, readJson, runnerPath) {
10
+ export function assertNoActiveRunState(stateDir, readJson, _runnerPath) {
11
11
  const meta = readJson(join(stateDir, "run.json"));
12
12
  if (!meta)
13
13
  return;
14
14
  const pid = Number(meta.pid || 0);
15
15
  const cwd = String(meta.cwd ?? "");
16
- if (!pid || !isAlive(pid) || !pidMatchesRun(pid, cwd, stateDir, runnerPath))
16
+ if (!pid || !isAlive(pid))
17
17
  return;
18
- throw new Error(`Run state already has an active owned process: ${String(meta.run ?? stateDir)}. Stop it before reusing the same run_id or state_dir.`);
18
+ const identity = verifyRunProcessIdentity(pid, meta.process_identity);
19
+ if (identity.status === "owner_mismatch") {
20
+ throw new Error(`Run state process identity does not match the live pid: ${String(meta.run ?? stateDir)}. Refusing to reuse the state directory while pid ${pid} is alive.`);
21
+ }
22
+ if (identity.status === "unsupported_proof") {
23
+ throw new Error(`Run state process identity proof is unavailable: ${String(meta.run ?? stateDir)}. Refusing to reuse the state directory while pid ${pid} is alive.`);
24
+ }
25
+ if (identity.valid) {
26
+ throw new Error(`Run state already has an active owned process: ${String(meta.run ?? stateDir)}. Stop it before reusing the same run_id or state_dir.`);
27
+ }
28
+ }
29
+ function writeStartLockOwner(lockDir, recovered = false) {
30
+ const processIdentity = readProcessIdentity(process.pid);
31
+ writeFileSync(join(lockDir, "owner.json"), `${JSON.stringify({
32
+ pid: process.pid,
33
+ createdAt: new Date().toISOString(),
34
+ ...(processIdentity ? { process_identity: processIdentity } : {}),
35
+ ...(recovered ? { recovered: true } : {}),
36
+ })}\n`, "utf8");
37
+ }
38
+ function isStartLockOwnerProvenDead(lockDir) {
39
+ try {
40
+ const owner = JSON.parse(readFileSync(join(lockDir, "owner.json"), "utf8"));
41
+ const pid = Number(owner.pid || 0);
42
+ if (!pid)
43
+ return false;
44
+ return verifyRunProcessIdentity(pid, owner.process_identity).status === "dead_pid";
45
+ }
46
+ catch {
47
+ return false;
48
+ }
19
49
  }
20
50
  export function acquireStateStartLock(stateDir) {
21
51
  const lockDir = join(stateDir, ".start.lock");
22
52
  try {
23
53
  mkdirSync(lockDir);
24
- writeFileSync(join(lockDir, "owner.json"), `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\n`, "utf8");
54
+ writeStartLockOwner(lockDir);
25
55
  }
26
56
  catch (error) {
27
57
  try {
28
58
  const stat = statSync(lockDir);
29
- if (Date.now() - stat.mtimeMs > START_LOCK_MAX_AGE_MS) {
59
+ if (Date.now() - stat.mtimeMs > START_LOCK_MAX_AGE_MS &&
60
+ isStartLockOwnerProvenDead(lockDir)) {
30
61
  rmSync(lockDir, { recursive: true, force: true });
31
62
  mkdirSync(lockDir);
32
- writeFileSync(join(lockDir, "owner.json"), `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString(), recovered: true })}\n`, "utf8");
63
+ writeStartLockOwner(lockDir, true);
33
64
  return () => rmSync(lockDir, { recursive: true, force: true });
34
65
  }
35
66
  }
@@ -40,17 +71,20 @@ export function acquireStateStartLock(stateDir) {
40
71
  }
41
72
  return () => rmSync(lockDir, { recursive: true, force: true });
42
73
  }
43
- export function prepareStateDirForStart(stateDir, readJson, runnerPath) {
74
+ export function prepareStateDirForStart(stateDir, readJson, _runnerPath) {
44
75
  const existing = readJson(join(stateDir, "run.json"));
45
76
  const existingPid = Number(existing?.pid || 0);
46
- const existingCwd = typeof existing?.cwd === "string" ? existing.cwd : undefined;
47
- const existingResult = readJson(join(stateDir, "result.json"));
48
- if (!existingResult &&
49
- existingPid &&
50
- existingCwd &&
51
- isAlive(existingPid) &&
52
- pidMatchesRun(existingPid, existingCwd, stateDir, runnerPath)) {
53
- throw new Error(`Run state already has an active owned process: ${String(existing?.run ?? stateDir)}. Stop it before restarting.`);
77
+ if (existingPid && isAlive(existingPid)) {
78
+ const identity = verifyRunProcessIdentity(existingPid, existing?.process_identity);
79
+ if (identity.status === "owner_mismatch") {
80
+ throw new Error(`Run state process identity does not match the live pid: ${String(existing?.run ?? stateDir)}. Refusing to prepare the state directory while pid ${existingPid} is alive.`);
81
+ }
82
+ if (identity.status === "unsupported_proof") {
83
+ throw new Error(`Run state process identity proof is unavailable: ${String(existing?.run ?? stateDir)}. Refusing to prepare the state directory while pid ${existingPid} is alive.`);
84
+ }
85
+ if (identity.valid) {
86
+ throw new Error(`Run state already has an active owned process: ${String(existing?.run ?? stateDir)}. Stop it before restarting.`);
87
+ }
54
88
  }
55
89
  for (const file of [
56
90
  "events.jsonl",
@@ -3,6 +3,6 @@
3
3
  * Owns status derivation from run state files plus bounded log/event tails.
4
4
  */
5
5
  export type RunJsonReader = (path: string) => Record<string, unknown> | undefined;
6
- export declare function buildRunStatus(stateDir: string, runOrDir: string, meta: Record<string, unknown>, readJson: RunJsonReader, runnerPath: string, runnerIdentityGraceMs: number): Record<string, unknown>;
6
+ export declare function buildRunStatus(stateDir: string, runOrDir: string, meta: Record<string, unknown>, readJson: RunJsonReader, _runnerPath: string, runnerIdentityGraceMs: number): Record<string, unknown>;
7
7
  export declare function tailFile(path: string, lines: number): string;
8
8
  export declare function tailLines(path: string, lines: number): string[];
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { existsSync, readFileSync } from "node:fs";
6
6
  import { join } from "node:path";
7
- import { isAlive, isWithinRunnerIdentityGrace, pidMatchesRun } from "./runs-process.js";
7
+ import { isWithinRunnerIdentityGrace, verifyRunProcessIdentity, } from "./runs-process.js";
8
8
  import { readJsonlFileResilient } from "./state-readers.js";
9
9
  function getInterruptedRunStatus(stateDir) {
10
10
  const events = readJsonlFileResilient(join(stateDir, "events.jsonl")).records.slice(-200);
@@ -16,14 +16,14 @@ function getInterruptedRunStatus(stateDir) {
16
16
  }
17
17
  return undefined;
18
18
  }
19
- export function buildRunStatus(stateDir, runOrDir, meta, readJson, runnerPath, runnerIdentityGraceMs) {
19
+ export function buildRunStatus(stateDir, runOrDir, meta, readJson, _runnerPath, runnerIdentityGraceMs) {
20
20
  const result = readJson(join(stateDir, "result.json"));
21
21
  const pid = Number(meta.pid || 0);
22
+ const processIdentity = verifyRunProcessIdentity(pid, meta.process_identity);
22
23
  const aliveOwnedRunner = Boolean(pid &&
23
- isAlive(pid) &&
24
- (!Array.isArray(meta.argv) ||
25
- pidMatchesRun(pid, String(meta.cwd ?? ""), stateDir, runnerPath) ||
26
- isWithinRunnerIdentityGrace(meta, runnerIdentityGraceMs)));
24
+ (processIdentity.valid ||
25
+ (processIdentity.status === "unsupported_proof" &&
26
+ isWithinRunnerIdentityGrace(meta, runnerIdentityGraceMs))));
27
27
  const status = result
28
28
  ? Number(result.code ?? 0) === 0
29
29
  ? "done"
@@ -35,8 +35,10 @@ export function buildRunStatus(stateDir, runOrDir, meta, readJson, runnerPath, r
35
35
  return {
36
36
  ...meta,
37
37
  eventsFile: join(stateDir, "events.jsonl"),
38
+ evidenceFile: join(stateDir, "review-evidence.json"),
38
39
  inboxFile: join(stateDir, "inbox.jsonl"),
39
40
  outboxFile: join(stateDir, "outbox.jsonl"),
41
+ process_identity_status: processIdentity.status,
40
42
  progress: readJson(join(stateDir, "progress.json")) || null,
41
43
  result: result || null,
42
44
  ...(terminalHandled ? { terminal_handled: terminalHandled } : {}),
@@ -4,6 +4,7 @@
4
4
  * Owns persisted tool loading, reserved-name guards, runtime registration, and warning notification
5
5
  */
6
6
  import { existsSync, watch } from "node:fs";
7
+ import { basename, dirname } from "node:path";
7
8
  import * as Paths from "./paths.js";
8
9
  import * as RecipesDiscovery from "./recipes-discovery.js";
9
10
  import * as ToolsLocal from "./tools-local.js";
@@ -126,10 +127,13 @@ export function createAutoToolsRuntime(deps) {
126
127
  export function createRecipeToolReloadWatcher(runtime) {
127
128
  let reloadTimeout;
128
129
  let rootWatcher;
130
+ let parentWatcher;
129
131
  let failureNotified = false;
130
132
  const close = () => {
131
133
  rootWatcher?.close();
132
134
  rootWatcher = undefined;
135
+ parentWatcher?.close();
136
+ parentWatcher = undefined;
133
137
  if (reloadTimeout)
134
138
  clearTimeout(reloadTimeout);
135
139
  reloadTimeout = undefined;
@@ -150,23 +154,75 @@ export function createRecipeToolReloadWatcher(runtime) {
150
154
  }, 150);
151
155
  reloadTimeout.unref?.();
152
156
  };
157
+ const watchParent = (ctx, recipeRoot) => {
158
+ if (parentWatcher || rootWatcher)
159
+ return;
160
+ const parent = dirname(recipeRoot);
161
+ if (!existsSync(parent)) {
162
+ notifyFailure(ctx);
163
+ return;
164
+ }
165
+ try {
166
+ parentWatcher = watch(parent, (_event, changedFile) => {
167
+ if (changedFile &&
168
+ String(changedFile) !== basename(recipeRoot) &&
169
+ !existsSync(recipeRoot)) {
170
+ return;
171
+ }
172
+ if (!existsSync(recipeRoot))
173
+ return;
174
+ parentWatcher?.close();
175
+ parentWatcher = undefined;
176
+ watchRoot(ctx, recipeRoot);
177
+ scheduleReload(ctx);
178
+ });
179
+ parentWatcher.on("error", () => {
180
+ parentWatcher?.close();
181
+ parentWatcher = undefined;
182
+ notifyFailure(ctx);
183
+ });
184
+ }
185
+ catch {
186
+ notifyFailure(ctx);
187
+ }
188
+ };
189
+ const watchRoot = (ctx, recipeRoot) => {
190
+ if (rootWatcher)
191
+ return;
192
+ if (!existsSync(recipeRoot)) {
193
+ watchParent(ctx, recipeRoot);
194
+ return;
195
+ }
196
+ try {
197
+ rootWatcher = watch(recipeRoot, () => {
198
+ if (!existsSync(recipeRoot)) {
199
+ rootWatcher?.close();
200
+ rootWatcher = undefined;
201
+ scheduleReload(ctx);
202
+ watchParent(ctx, recipeRoot);
203
+ return;
204
+ }
205
+ scheduleReload(ctx);
206
+ });
207
+ rootWatcher.on("error", () => {
208
+ rootWatcher?.close();
209
+ rootWatcher = undefined;
210
+ if (existsSync(recipeRoot))
211
+ notifyFailure(ctx);
212
+ else
213
+ watchParent(ctx, recipeRoot);
214
+ });
215
+ }
216
+ catch {
217
+ notifyFailure(ctx);
218
+ }
219
+ };
153
220
  return {
154
221
  close,
155
222
  watch(ctx) {
156
- const recipeRoot = Paths.getRecipeRoot();
157
- if (rootWatcher || !existsSync(recipeRoot))
223
+ if (rootWatcher || parentWatcher)
158
224
  return;
159
- try {
160
- rootWatcher = watch(recipeRoot, () => scheduleReload(ctx));
161
- rootWatcher.on("error", () => {
162
- rootWatcher?.close();
163
- rootWatcher = undefined;
164
- notifyFailure(ctx);
165
- });
166
- }
167
- catch {
168
- notifyFailure(ctx);
169
- }
225
+ watchRoot(ctx, Paths.getRecipeRoot());
170
226
  },
171
227
  };
172
228
  }
@@ -4,7 +4,9 @@
4
4
  * Owns the public inspect execution path and compact observation responses
5
5
  */
6
6
  export interface InspectToolDeps<TContext = unknown> {
7
+ getRunStatus?: (runOrDir: string) => Record<string, any>;
7
8
  getTool?: (name: string) => any | undefined;
9
+ listRuns?: () => Array<Record<string, any>>;
8
10
  packagedRecipeRoot?: string;
9
11
  recipeRoot?: string;
10
12
  }
@@ -122,6 +122,28 @@ function compactArtifactPath(value) {
122
122
  const record = asRecord(value);
123
123
  return String(record.path ?? "<missing>");
124
124
  }
125
+ function readBoundedReviewEvidence(status, limit) {
126
+ const evidenceFile = status.evidenceFile;
127
+ if (typeof evidenceFile !== "string" || !existsSync(evidenceFile))
128
+ return undefined;
129
+ try {
130
+ const manifest = asRecord(JSON.parse(readFileSync(evidenceFile, "utf8")));
131
+ const commands = Array.isArray(manifest.commands) ? manifest.commands : [];
132
+ const bounded = commands.slice(-Math.max(1, limit));
133
+ return {
134
+ ...manifest,
135
+ commands: bounded,
136
+ commands_total: commands.length,
137
+ ...(bounded.length < commands.length ? { commands_truncated: true } : {}),
138
+ };
139
+ }
140
+ catch {
141
+ return {
142
+ status: "invalid",
143
+ error: "review evidence manifest is unreadable",
144
+ };
145
+ }
146
+ }
125
147
  function compactActorFiles(status) {
126
148
  const run = String(status.run ?? "<unknown>");
127
149
  const artifacts = asRecord(status.artifacts);
@@ -130,6 +152,7 @@ function compactActorFiles(status) {
130
152
  status.stderrLog,
131
153
  status.eventsFile,
132
154
  status.outboxFile,
155
+ status.evidenceFile,
133
156
  status.state_dir
134
157
  ? `${String(status.state_dir)}/communication.json`
135
158
  : undefined,
@@ -268,7 +291,16 @@ function isTriageHighRiskRecipe(recipe) {
268
291
  function getPiActorsTriage(ctx, deps) {
269
292
  const runtime = getPiActorsRuntimeStatus();
270
293
  const currentSession = ToolsAccess.getContextSessionId(ctx);
271
- const allRuns = AsyncRuns.listRuns().map((run) => AsyncRuns.getRunStatus(String(run.state_dir)));
294
+ const listRuns = deps.listRuns ?? AsyncRuns.listRuns;
295
+ const getRunStatus = deps.getRunStatus ?? AsyncRuns.getRunStatus;
296
+ const allRuns = listRuns().flatMap((run) => {
297
+ try {
298
+ return [getRunStatus(String(run.state_dir))];
299
+ }
300
+ catch {
301
+ return [];
302
+ }
303
+ });
272
304
  const visibleRuns = currentSession
273
305
  ? allRuns.filter((run) => !run.ownerId || run.ownerId === currentSession)
274
306
  : allRuns;
@@ -644,13 +676,18 @@ export function createInspectToolDefinition(deps = {}) {
644
676
  case "files": {
645
677
  const status = ToolsAccess.assertRunAccessibleToContext(runId, ctx);
646
678
  const artifactManifest = AsyncRuns.resolveArtifactManifest(status.artifacts);
679
+ const reviewEvidence = readBoundedReviewEvidence(status, Number(input.lines || Limits.DEFAULT_INSPECT_LINES));
647
680
  const details = artifactManifest
648
681
  ? {
649
682
  ...status,
683
+ ...(reviewEvidence ? { review_evidence: reviewEvidence } : {}),
650
684
  artifact_manifest: artifactManifest,
651
685
  next_actions: ToolsResponse.artifactNextActions(status.run ?? runId, asRecord(status.artifacts)),
652
686
  }
653
- : status;
687
+ : {
688
+ ...status,
689
+ ...(reviewEvidence ? { review_evidence: reviewEvidence } : {}),
690
+ };
654
691
  return {
655
692
  content: [
656
693
  {
@@ -26,7 +26,6 @@ export function createRegisterToolDefinition(deps) {
26
26
  description: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.description),
27
27
  draft: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.draft),
28
28
  name: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.name),
29
- state_dir: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.state_dir),
30
29
  template: unionSchema([
31
30
  stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.template),
32
31
  looseObjectSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.template),
@@ -94,7 +94,6 @@ export function createSpawnToolDefinition() {
94
94
  as: Schema.stringSchema("Optional actor address for the spawned run, e.g. run:<id>."),
95
95
  file: Schema.stringSchema("Optional template recipe JSON file. Bare names resolve under ~/.pi/agent/recipes."),
96
96
  recipe: Schema.stringSchema("Alias for file; template recipe JSON file/name to spawn."),
97
- state_dir: Schema.stringSchema("Optional explicit run state directory."),
98
97
  template: Schema.unionSchema([
99
98
  Schema.stringSchema("Inline command template string"),
100
99
  Schema.arraySchema("Inline command-template sequence or parallel tree"),
@@ -105,6 +104,9 @@ export function createSpawnToolDefinition() {
105
104
  }, []),
106
105
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
107
106
  const input = asRecord(params);
107
+ if (input.state_dir !== undefined) {
108
+ throw new Error("spawn.state_dir is not supported; run state is runtime-owned so run:<id> remains addressable and retention-safe.");
109
+ }
108
110
  const runId = runIdFromActorAddress(typeof input.as === "string" ? input.as : undefined);
109
111
  const recipe = typeof input.file === "string"
110
112
  ? input.file
@@ -118,7 +120,6 @@ export function createSpawnToolDefinition() {
118
120
  launch_source: "spawn",
119
121
  ownerId: getRunOwnerId(ctx),
120
122
  run_id: runId,
121
- state_dir: typeof input.state_dir === "string" ? input.state_dir : undefined,
122
123
  ...(input.template !== undefined
123
124
  ? {
124
125
  template: input.template,
@@ -18,5 +18,6 @@ export interface CoreActorToolDefinitionDeps<TContext extends RuntimeToolContext
18
18
  registryRuntime: Pick<RegisterToolRuntimeDeps<TContext>, "getToolNameBlocker" | "getTools" | "notify" | "registerRuntimeTool">;
19
19
  setActiveTools: (toolNames: string[]) => void;
20
20
  }
21
+ export declare function resolveActiveRuntimeTool(name: string, activeTools: Pick<Map<string, unknown>, "has">, getDefinition: (name: string) => unknown): unknown;
21
22
  export declare const RESERVED_TOOL_NAMES: Set<string>;
22
23
  export declare function createCoreActorToolDefinitions<TContext extends RuntimeToolContext>(deps: CoreActorToolDefinitionDeps<TContext>): ActorToolDefinition[];
package/dist/lib/tools.js CHANGED
@@ -7,6 +7,9 @@ import * as ToolsInspect from "./tools-inspect.js";
7
7
  import * as ToolsMessage from "./tools-message.js";
8
8
  import * as ToolsRegister from "./tools-register.js";
9
9
  import * as ToolsSpawn from "./tools-spawn.js";
10
+ export function resolveActiveRuntimeTool(name, activeTools, getDefinition) {
11
+ return activeTools.has(name) ? getDefinition(name) : undefined;
12
+ }
10
13
  export const RESERVED_TOOL_NAMES = new Set([
11
14
  "read",
12
15
  "write",
@@ -0,0 +1 @@
1
+ export { default } from "../index.js";
@@ -28,5 +28,6 @@
28
28
  "run.failed"
29
29
  ]
30
30
  },
31
- "template": "pi -p --model {model} --thinking {thinking} {tools?--tools:--no-tools} {tools} Judge this report: {report}. Rubric: {rubric}. Evidence: {evidence}. Output format: {output_format}"
31
+ "accept_output": "review_evidence",
32
+ "template": "pi -p --model {model} --thinking {thinking} {tools?--tools:--no-tools} {tools} Judge this report: {report}. Rubric: {rubric}. Evidence: {evidence}. Begin the response with ACTOR_REVIEW_RESULT on its own line. Output format: {output_format}"
32
33
  }
@@ -30,5 +30,6 @@
30
30
  "run.failed"
31
31
  ]
32
32
  },
33
- "template": "pi -p --model {model} --thinking {thinking} {tools?--tools:--no-tools} {tools} Merge these subagent outputs using mode {mode}: {inputs}. Merge policy: {merge_policy}. Risk policy: {risk_policy}. Output format: {output_format}"
33
+ "accept_output": "review_evidence",
34
+ "template": "pi -p --model {model} --thinking {thinking} {tools?--tools:--no-tools} {tools} Merge these subagent outputs using mode {mode}: {inputs}. Merge policy: {merge_policy}. Risk policy: {risk_policy}. Begin the response with ACTOR_REVIEW_RESULT on its own line. Output format: {output_format}"
34
35
  }
@@ -26,5 +26,6 @@
26
26
  "run.failed"
27
27
  ]
28
28
  },
29
- "template": "pi -p --model {model} --thinking {thinking} {tools?--tools:--no-tools} {tools} Normalize this subagent output. Target format: {format}. Preservation policy: {preservation_policy}. Input: {input}"
29
+ "accept_output": "review_evidence",
30
+ "template": "pi -p --model {model} --thinking {thinking} {tools?--tools:--no-tools} {tools} Normalize this subagent output. Begin the response with ACTOR_REVIEW_RESULT on its own line. Target format: {format}. Preservation policy: {preservation_policy}. Input: {input}"
30
31
  }
@@ -174,7 +174,7 @@
174
174
  "values": {
175
175
  "input": "Use merged and judged output from stdin.",
176
176
  "format": "{output_format}",
177
- "preservation_policy": "Preserve branch status and explicitly mark Status as complete, degraded, or insufficient_data.",
177
+ "preservation_policy": "Preserve branch status and explicitly mark Status as complete, degraded, or insufficient_data. Include stable ACTOR_EVIDENCE_REF values from stdin in the Evidence section for every reviewer, verifier, merger, and judge source used. Do not claim complete when any required evidence reference is absent or inconsistent.",
178
178
  "model": "{merger_model}",
179
179
  "thinking": "{thinking}",
180
180
  "tools": "{tools}"
@@ -32,5 +32,6 @@
32
32
  "run.failed"
33
33
  ]
34
34
  },
35
- "template": "pi -p --model {model} --thinking {thinking} {tools?--tools:--no-tools} {tools} Review {scope} through this lens: {lens}. Instructions: {instructions}. Evidence policy: {evidence_policy}. Risk policy: {risk_policy}. Output format: {output_format}"
35
+ "accept_output": "review_evidence",
36
+ "template": "pi -p --model {model} --thinking {thinking} {tools?--tools:--no-tools} {tools} Review {scope} through this lens: {lens}. Instructions: {instructions}. Evidence policy: {evidence_policy}. Risk policy: {risk_policy}. Begin the response with ACTOR_REVIEW_RESULT on its own line. Output format: {output_format}"
36
37
  }
@@ -29,5 +29,6 @@
29
29
  "run.failed"
30
30
  ]
31
31
  },
32
- "template": "pi -p --model {model} --thinking {thinking} {tools?--tools:--no-tools} {tools} Verify this claim against the provided evidence. Claim: {claim}. Evidence or artifact paths: {evidence}. Acceptance: {acceptance}. Evidence policy: {evidence_policy}. Output format: {output_format}"
32
+ "accept_output": "review_evidence",
33
+ "template": "pi -p --model {model} --thinking {thinking} {tools?--tools:--no-tools} {tools} Verify this claim against the provided evidence. Claim: {claim}. Evidence or artifact paths: {evidence}. Acceptance: {acceptance}. Evidence policy: {evidence_policy}. Begin the response with ACTOR_REVIEW_RESULT on its own line. Output format: {output_format}"
33
34
  }