@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.
Files changed (97) hide show
  1. package/AGENTS.md +10 -3
  2. package/BACKLOG.md +2 -11
  3. package/CHANGELOG.md +45 -0
  4. package/README.md +4 -4
  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.d.ts +1 -1
  16. package/dist/lib/observability.js +7 -4
  17. package/dist/lib/pi.d.ts +1 -1
  18. package/dist/lib/pi.js +2 -2
  19. package/dist/lib/prompts.d.ts +1 -2
  20. package/dist/lib/prompts.js +2 -3
  21. package/dist/lib/recipes-context.js +17 -9
  22. package/dist/lib/recipes-discovery.js +11 -5
  23. package/dist/lib/recipes-references.d.ts +1 -1
  24. package/dist/lib/recipes-references.js +4 -5
  25. package/dist/lib/recipes-usage.d.ts +2 -0
  26. package/dist/lib/recipes-usage.js +35 -21
  27. package/dist/lib/registry.d.ts +0 -2
  28. package/dist/lib/registry.js +33 -10
  29. package/dist/lib/runs-ownership.d.ts +7 -0
  30. package/dist/lib/runs-ownership.js +82 -0
  31. package/dist/lib/runs-process.d.ts +17 -2
  32. package/dist/lib/runs-process.js +99 -11
  33. package/dist/lib/runs-retention.d.ts +3 -0
  34. package/dist/lib/runs-retention.js +18 -3
  35. package/dist/lib/runs-start.d.ts +2 -2
  36. package/dist/lib/runs-start.js +51 -17
  37. package/dist/lib/runs-status.d.ts +1 -1
  38. package/dist/lib/runs-status.js +8 -6
  39. package/dist/lib/runtime.js +69 -13
  40. package/dist/lib/tools-inspect.d.ts +2 -0
  41. package/dist/lib/tools-inspect.js +39 -2
  42. package/dist/lib/tools-register.js +0 -1
  43. package/dist/lib/tools-spawn.js +3 -2
  44. package/dist/lib/tools.d.ts +1 -0
  45. package/dist/lib/tools.js +3 -0
  46. package/dist/pi-actors/index.js +1 -0
  47. package/dist/recipes/subagent-judge.json +2 -1
  48. package/dist/recipes/subagent-merge.json +2 -1
  49. package/dist/recipes/subagent-normalize.json +2 -1
  50. package/dist/recipes/subagent-review-coordinator.json +1 -1
  51. package/dist/recipes/subagent-review.json +2 -1
  52. package/dist/recipes/subagent-verify.json +2 -1
  53. package/dist/scripts/async-runner.mjs +274 -6
  54. package/dist/scripts/build-dist.mjs +14 -1
  55. package/dist/skills/actors/SKILL.md +11 -7
  56. package/dist/skills/swarm/SKILL.md +1 -1
  57. package/docs/actor-messages.md +1 -1
  58. package/docs/async-runs.md +14 -5
  59. package/docs/command-templates.md +4 -2
  60. package/docs/recipe-library.md +1 -0
  61. package/docs/template-recipes.md +5 -7
  62. package/docs/tool-registry.md +4 -2
  63. package/index.ts +18 -7
  64. package/lib/async-runs.ts +138 -19
  65. package/lib/command-templates.ts +132 -13
  66. package/lib/config.ts +0 -4
  67. package/lib/execution.ts +198 -13
  68. package/lib/file-state.ts +106 -3
  69. package/lib/observability.ts +11 -5
  70. package/lib/pi.ts +3 -3
  71. package/lib/prompts.ts +2 -4
  72. package/lib/recipes-context.ts +17 -9
  73. package/lib/recipes-discovery.ts +10 -5
  74. package/lib/recipes-references.ts +5 -6
  75. package/lib/recipes-usage.ts +36 -20
  76. package/lib/registry.ts +43 -13
  77. package/lib/runs-ownership.ts +117 -0
  78. package/lib/runs-process.ts +138 -16
  79. package/lib/runs-retention.ts +22 -2
  80. package/lib/runs-start.ts +89 -31
  81. package/lib/runs-status.ts +15 -6
  82. package/lib/runtime.ts +64 -12
  83. package/lib/tools-inspect.ts +46 -4
  84. package/lib/tools-register.ts +0 -3
  85. package/lib/tools-spawn.ts +5 -5
  86. package/lib/tools.ts +8 -0
  87. package/package.json +2 -2
  88. package/recipes/subagent-judge.json +2 -1
  89. package/recipes/subagent-merge.json +2 -1
  90. package/recipes/subagent-normalize.json +2 -1
  91. package/recipes/subagent-review-coordinator.json +1 -1
  92. package/recipes/subagent-review.json +2 -1
  93. package/recipes/subagent-verify.json +2 -1
  94. package/scripts/async-runner.mjs +274 -6
  95. package/scripts/build-dist.mjs +14 -1
  96. package/skills/actors/SKILL.md +11 -7
  97. package/skills/swarm/SKILL.md +1 -1
package/lib/runs-start.ts CHANGED
@@ -4,10 +4,21 @@
4
4
  * preparation before a new runner process is spawned.
5
5
  */
6
6
 
7
- import { mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";
7
+ import {
8
+ mkdirSync,
9
+ readFileSync,
10
+ rmSync,
11
+ statSync,
12
+ writeFileSync,
13
+ } from "node:fs";
8
14
  import { join } from "node:path";
9
15
 
10
- import { isAlive, pidMatchesRun } from "./runs-process.ts";
16
+ import {
17
+ isAlive,
18
+ readProcessIdentity,
19
+ verifyRunProcessIdentity,
20
+ type RunProcessIdentity,
21
+ } from "./runs-process.ts";
11
22
 
12
23
  const START_LOCK_MAX_AGE_MS = 5 * 60 * 1000;
13
24
 
@@ -16,39 +27,79 @@ export type RunJsonReader = (path: string) => Record<string, unknown> | undefine
16
27
  export function assertNoActiveRunState(
17
28
  stateDir: string,
18
29
  readJson: RunJsonReader,
19
- runnerPath: string,
30
+ _runnerPath: string,
20
31
  ): void {
21
32
  const meta = readJson(join(stateDir, "run.json"));
22
33
  if (!meta) return;
23
34
  const pid = Number(meta.pid || 0);
24
35
  const cwd = String(meta.cwd ?? "");
25
- if (!pid || !isAlive(pid) || !pidMatchesRun(pid, cwd, stateDir, runnerPath))
26
- return;
27
- throw new Error(
28
- `Run state already has an active owned process: ${String(meta.run ?? stateDir)}. Stop it before reusing the same run_id or state_dir.`,
36
+ if (!pid || !isAlive(pid)) return;
37
+ const identity = verifyRunProcessIdentity(
38
+ pid,
39
+ meta.process_identity as RunProcessIdentity | undefined,
29
40
  );
41
+ if (identity.status === "owner_mismatch") {
42
+ throw new Error(
43
+ `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.`,
44
+ );
45
+ }
46
+ if (identity.status === "unsupported_proof") {
47
+ throw new Error(
48
+ `Run state process identity proof is unavailable: ${String(meta.run ?? stateDir)}. Refusing to reuse the state directory while pid ${pid} is alive.`,
49
+ );
50
+ }
51
+ if (identity.valid) {
52
+ throw new Error(
53
+ `Run state already has an active owned process: ${String(meta.run ?? stateDir)}. Stop it before reusing the same run_id or state_dir.`,
54
+ );
55
+ }
56
+ }
57
+
58
+ function writeStartLockOwner(lockDir: string, recovered = false): void {
59
+ const processIdentity = readProcessIdentity(process.pid);
60
+ writeFileSync(
61
+ join(lockDir, "owner.json"),
62
+ `${JSON.stringify({
63
+ pid: process.pid,
64
+ createdAt: new Date().toISOString(),
65
+ ...(processIdentity ? { process_identity: processIdentity } : {}),
66
+ ...(recovered ? { recovered: true } : {}),
67
+ })}\n`,
68
+ "utf8",
69
+ );
70
+ }
71
+
72
+ function isStartLockOwnerProvenDead(lockDir: string): boolean {
73
+ try {
74
+ const owner = JSON.parse(
75
+ readFileSync(join(lockDir, "owner.json"), "utf8"),
76
+ ) as Record<string, unknown>;
77
+ const pid = Number(owner.pid || 0);
78
+ if (!pid) return false;
79
+ return verifyRunProcessIdentity(
80
+ pid,
81
+ owner.process_identity as RunProcessIdentity | undefined,
82
+ ).status === "dead_pid";
83
+ } catch {
84
+ return false;
85
+ }
30
86
  }
31
87
 
32
88
  export function acquireStateStartLock(stateDir: string): () => void {
33
89
  const lockDir = join(stateDir, ".start.lock");
34
90
  try {
35
91
  mkdirSync(lockDir);
36
- writeFileSync(
37
- join(lockDir, "owner.json"),
38
- `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\n`,
39
- "utf8",
40
- );
92
+ writeStartLockOwner(lockDir);
41
93
  } catch (error) {
42
94
  try {
43
95
  const stat = statSync(lockDir);
44
- if (Date.now() - stat.mtimeMs > START_LOCK_MAX_AGE_MS) {
96
+ if (
97
+ Date.now() - stat.mtimeMs > START_LOCK_MAX_AGE_MS &&
98
+ isStartLockOwnerProvenDead(lockDir)
99
+ ) {
45
100
  rmSync(lockDir, { recursive: true, force: true });
46
101
  mkdirSync(lockDir);
47
- writeFileSync(
48
- join(lockDir, "owner.json"),
49
- `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString(), recovered: true })}\n`,
50
- "utf8",
51
- );
102
+ writeStartLockOwner(lockDir, true);
52
103
  return () => rmSync(lockDir, { recursive: true, force: true });
53
104
  }
54
105
  } catch {
@@ -65,23 +116,30 @@ export function acquireStateStartLock(stateDir: string): () => void {
65
116
  export function prepareStateDirForStart(
66
117
  stateDir: string,
67
118
  readJson: RunJsonReader,
68
- runnerPath: string,
119
+ _runnerPath: string,
69
120
  ): void {
70
121
  const existing = readJson(join(stateDir, "run.json"));
71
122
  const existingPid = Number(existing?.pid || 0);
72
- const existingCwd =
73
- typeof existing?.cwd === "string" ? existing.cwd : undefined;
74
- const existingResult = readJson(join(stateDir, "result.json"));
75
- if (
76
- !existingResult &&
77
- existingPid &&
78
- existingCwd &&
79
- isAlive(existingPid) &&
80
- pidMatchesRun(existingPid, existingCwd, stateDir, runnerPath)
81
- ) {
82
- throw new Error(
83
- `Run state already has an active owned process: ${String(existing?.run ?? stateDir)}. Stop it before restarting.`,
123
+ if (existingPid && isAlive(existingPid)) {
124
+ const identity = verifyRunProcessIdentity(
125
+ existingPid,
126
+ existing?.process_identity as RunProcessIdentity | undefined,
84
127
  );
128
+ if (identity.status === "owner_mismatch") {
129
+ throw new Error(
130
+ `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.`,
131
+ );
132
+ }
133
+ if (identity.status === "unsupported_proof") {
134
+ throw new Error(
135
+ `Run state process identity proof is unavailable: ${String(existing?.run ?? stateDir)}. Refusing to prepare the state directory while pid ${existingPid} is alive.`,
136
+ );
137
+ }
138
+ if (identity.valid) {
139
+ throw new Error(
140
+ `Run state already has an active owned process: ${String(existing?.run ?? stateDir)}. Stop it before restarting.`,
141
+ );
142
+ }
85
143
  }
86
144
  for (const file of [
87
145
  "events.jsonl",
@@ -7,7 +7,11 @@ import { existsSync, readFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
 
9
9
  import type { AsyncRunStatus } from "./async-runs.ts";
10
- import { isAlive, isWithinRunnerIdentityGrace, pidMatchesRun } from "./runs-process.ts";
10
+ import {
11
+ isWithinRunnerIdentityGrace,
12
+ verifyRunProcessIdentity,
13
+ type RunProcessIdentity,
14
+ } from "./runs-process.ts";
11
15
  import { readJsonlFileResilient } from "./state-readers.ts";
12
16
 
13
17
  export type RunJsonReader = (path: string) => Record<string, unknown> | undefined;
@@ -30,17 +34,20 @@ export function buildRunStatus(
30
34
  runOrDir: string,
31
35
  meta: Record<string, unknown>,
32
36
  readJson: RunJsonReader,
33
- runnerPath: string,
37
+ _runnerPath: string,
34
38
  runnerIdentityGraceMs: number,
35
39
  ): Record<string, unknown> {
36
40
  const result = readJson(join(stateDir, "result.json"));
37
41
  const pid = Number(meta.pid || 0);
42
+ const processIdentity = verifyRunProcessIdentity(
43
+ pid,
44
+ meta.process_identity as RunProcessIdentity | undefined,
45
+ );
38
46
  const aliveOwnedRunner = Boolean(
39
47
  pid &&
40
- isAlive(pid) &&
41
- (!Array.isArray(meta.argv) ||
42
- pidMatchesRun(pid, String(meta.cwd ?? ""), stateDir, runnerPath) ||
43
- isWithinRunnerIdentityGrace(meta, runnerIdentityGraceMs)),
48
+ (processIdentity.valid ||
49
+ (processIdentity.status === "unsupported_proof" &&
50
+ isWithinRunnerIdentityGrace(meta, runnerIdentityGraceMs))),
44
51
  );
45
52
  const status: AsyncRunStatus = result
46
53
  ? Number(result.code ?? 0) === 0
@@ -53,8 +60,10 @@ export function buildRunStatus(
53
60
  return {
54
61
  ...meta,
55
62
  eventsFile: join(stateDir, "events.jsonl"),
63
+ evidenceFile: join(stateDir, "review-evidence.json"),
56
64
  inboxFile: join(stateDir, "inbox.jsonl"),
57
65
  outboxFile: join(stateDir, "outbox.jsonl"),
66
+ process_identity_status: processIdentity.status,
58
67
  progress: readJson(join(stateDir, "progress.json")) || null,
59
68
  result: result || null,
60
69
  ...(terminalHandled ? { terminal_handled: terminalHandled } : {}),
package/lib/runtime.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { existsSync, watch, type FSWatcher } from "node:fs";
8
+ import { basename, dirname } from "node:path";
8
9
 
9
10
  import * as Config from "./config.ts";
10
11
  import type { RegisteredToolExec } from "./execution.ts";
@@ -186,10 +187,13 @@ export function createRecipeToolReloadWatcher(
186
187
  ): RecipeToolReloadWatcher {
187
188
  let reloadTimeout: NodeJS.Timeout | undefined;
188
189
  let rootWatcher: FSWatcher | undefined;
190
+ let parentWatcher: FSWatcher | undefined;
189
191
  let failureNotified = false;
190
192
  const close = (): void => {
191
193
  rootWatcher?.close();
192
194
  rootWatcher = undefined;
195
+ parentWatcher?.close();
196
+ parentWatcher = undefined;
193
197
  if (reloadTimeout) clearTimeout(reloadTimeout);
194
198
  reloadTimeout = undefined;
195
199
  };
@@ -210,21 +214,69 @@ export function createRecipeToolReloadWatcher(
210
214
  }, 150);
211
215
  reloadTimeout.unref?.();
212
216
  };
217
+ const watchParent = (ctx: RuntimeContext, recipeRoot: string): void => {
218
+ if (parentWatcher || rootWatcher) return;
219
+ const parent = dirname(recipeRoot);
220
+ if (!existsSync(parent)) {
221
+ notifyFailure(ctx);
222
+ return;
223
+ }
224
+ try {
225
+ parentWatcher = watch(parent, (_event, changedFile) => {
226
+ if (
227
+ changedFile &&
228
+ String(changedFile) !== basename(recipeRoot) &&
229
+ !existsSync(recipeRoot)
230
+ ) {
231
+ return;
232
+ }
233
+ if (!existsSync(recipeRoot)) return;
234
+ parentWatcher?.close();
235
+ parentWatcher = undefined;
236
+ watchRoot(ctx, recipeRoot);
237
+ scheduleReload(ctx);
238
+ });
239
+ parentWatcher.on("error", () => {
240
+ parentWatcher?.close();
241
+ parentWatcher = undefined;
242
+ notifyFailure(ctx);
243
+ });
244
+ } catch {
245
+ notifyFailure(ctx);
246
+ }
247
+ };
248
+ const watchRoot = (ctx: RuntimeContext, recipeRoot: string): void => {
249
+ if (rootWatcher) return;
250
+ if (!existsSync(recipeRoot)) {
251
+ watchParent(ctx, recipeRoot);
252
+ return;
253
+ }
254
+ try {
255
+ rootWatcher = watch(recipeRoot, () => {
256
+ if (!existsSync(recipeRoot)) {
257
+ rootWatcher?.close();
258
+ rootWatcher = undefined;
259
+ scheduleReload(ctx);
260
+ watchParent(ctx, recipeRoot);
261
+ return;
262
+ }
263
+ scheduleReload(ctx);
264
+ });
265
+ rootWatcher.on("error", () => {
266
+ rootWatcher?.close();
267
+ rootWatcher = undefined;
268
+ if (existsSync(recipeRoot)) notifyFailure(ctx);
269
+ else watchParent(ctx, recipeRoot);
270
+ });
271
+ } catch {
272
+ notifyFailure(ctx);
273
+ }
274
+ };
213
275
  return {
214
276
  close,
215
277
  watch(ctx: RuntimeContext): void {
216
- const recipeRoot = Paths.getRecipeRoot();
217
- if (rootWatcher || !existsSync(recipeRoot)) return;
218
- try {
219
- rootWatcher = watch(recipeRoot, () => scheduleReload(ctx));
220
- rootWatcher.on("error", () => {
221
- rootWatcher?.close();
222
- rootWatcher = undefined;
223
- notifyFailure(ctx);
224
- });
225
- } catch {
226
- notifyFailure(ctx);
227
- }
278
+ if (rootWatcher || parentWatcher) return;
279
+ watchRoot(ctx, Paths.getRecipeRoot());
228
280
  },
229
281
  };
230
282
  }
@@ -139,6 +139,31 @@ function compactArtifactPath(value: unknown): string {
139
139
  return String(record.path ?? "<missing>");
140
140
  }
141
141
 
142
+ function readBoundedReviewEvidence(
143
+ status: Record<string, unknown>,
144
+ limit: number,
145
+ ): Record<string, unknown> | undefined {
146
+ const evidenceFile = status.evidenceFile;
147
+ if (typeof evidenceFile !== "string" || !existsSync(evidenceFile))
148
+ return undefined;
149
+ try {
150
+ const manifest = asRecord(JSON.parse(readFileSync(evidenceFile, "utf8")));
151
+ const commands = Array.isArray(manifest.commands) ? manifest.commands : [];
152
+ const bounded = commands.slice(-Math.max(1, limit));
153
+ return {
154
+ ...manifest,
155
+ commands: bounded,
156
+ commands_total: commands.length,
157
+ ...(bounded.length < commands.length ? { commands_truncated: true } : {}),
158
+ };
159
+ } catch {
160
+ return {
161
+ status: "invalid",
162
+ error: "review evidence manifest is unreadable",
163
+ };
164
+ }
165
+ }
166
+
142
167
  function compactActorFiles(status: Record<string, unknown>): string {
143
168
  const run = String(status.run ?? "<unknown>");
144
169
  const artifacts = asRecord(status.artifacts);
@@ -147,6 +172,7 @@ function compactActorFiles(status: Record<string, unknown>): string {
147
172
  status.stderrLog,
148
173
  status.eventsFile,
149
174
  status.outboxFile,
175
+ status.evidenceFile,
150
176
  status.state_dir
151
177
  ? `${String(status.state_dir)}/communication.json`
152
178
  : undefined,
@@ -305,9 +331,15 @@ function getPiActorsTriage(
305
331
  ): Record<string, unknown> {
306
332
  const runtime = getPiActorsRuntimeStatus();
307
333
  const currentSession = ToolsAccess.getContextSessionId(ctx);
308
- const allRuns = AsyncRuns.listRuns().map((run) =>
309
- AsyncRuns.getRunStatus(String(run.state_dir)),
310
- );
334
+ const listRuns = deps.listRuns ?? AsyncRuns.listRuns;
335
+ const getRunStatus = deps.getRunStatus ?? AsyncRuns.getRunStatus;
336
+ const allRuns = listRuns().flatMap((run) => {
337
+ try {
338
+ return [getRunStatus(String(run.state_dir))];
339
+ } catch {
340
+ return [];
341
+ }
342
+ });
311
343
  const visibleRuns = currentSession
312
344
  ? allRuns.filter(
313
345
  (run) => !run.ownerId || run.ownerId === currentSession,
@@ -433,7 +465,9 @@ function compactToolActor(name: string, tool: Record<string, unknown>): string {
433
465
  }
434
466
 
435
467
  export interface InspectToolDeps<TContext = unknown> {
468
+ getRunStatus?: (runOrDir: string) => Record<string, any>;
436
469
  getTool?: (name: string) => any | undefined;
470
+ listRuns?: () => Array<Record<string, any>>;
437
471
  packagedRecipeRoot?: string;
438
472
  recipeRoot?: string;
439
473
  }
@@ -832,16 +866,24 @@ export function createInspectToolDefinition<TContext = unknown>(
832
866
  | Record<string, AsyncRuns.RunArtifactDeclaration>
833
867
  | undefined,
834
868
  );
869
+ const reviewEvidence = readBoundedReviewEvidence(
870
+ status,
871
+ Number(input.lines || Limits.DEFAULT_INSPECT_LINES),
872
+ );
835
873
  const details = artifactManifest
836
874
  ? {
837
875
  ...status,
876
+ ...(reviewEvidence ? { review_evidence: reviewEvidence } : {}),
838
877
  artifact_manifest: artifactManifest,
839
878
  next_actions: ToolsResponse.artifactNextActions(
840
879
  status.run ?? runId,
841
880
  asRecord(status.artifacts),
842
881
  ),
843
882
  }
844
- : status;
883
+ : {
884
+ ...status,
885
+ ...(reviewEvidence ? { review_evidence: reviewEvidence } : {}),
886
+ };
845
887
  return {
846
888
  content: [
847
889
  {
@@ -38,9 +38,6 @@ export function createRegisterToolDefinition<TContext>(
38
38
  ),
39
39
  draft: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.draft),
40
40
  name: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.name),
41
- state_dir: stringSchema(
42
- Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.state_dir,
43
- ),
44
41
  template: unionSchema([
45
42
  stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.template),
46
43
  looseObjectSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.template),
@@ -143,9 +143,6 @@ export function createSpawnToolDefinition<
143
143
  recipe: Schema.stringSchema(
144
144
  "Alias for file; template recipe JSON file/name to spawn.",
145
145
  ),
146
- state_dir: Schema.stringSchema(
147
- "Optional explicit run state directory.",
148
- ),
149
146
  template: Schema.unionSchema([
150
147
  Schema.stringSchema("Inline command template string"),
151
148
  Schema.arraySchema(
@@ -172,6 +169,11 @@ export function createSpawnToolDefinition<
172
169
  ctx: TContext,
173
170
  ) {
174
171
  const input = asRecord(params);
172
+ if (input.state_dir !== undefined) {
173
+ throw new Error(
174
+ "spawn.state_dir is not supported; run state is runtime-owned so run:<id> remains addressable and retention-safe.",
175
+ );
176
+ }
175
177
  const runId = runIdFromActorAddress(
176
178
  typeof input.as === "string" ? input.as : undefined,
177
179
  );
@@ -189,8 +191,6 @@ export function createSpawnToolDefinition<
189
191
  launch_source: "spawn",
190
192
  ownerId: getRunOwnerId(ctx),
191
193
  run_id: runId,
192
- state_dir:
193
- typeof input.state_dir === "string" ? input.state_dir : undefined,
194
194
  ...(input.template !== undefined
195
195
  ? {
196
196
  template:
package/lib/tools.ts CHANGED
@@ -32,6 +32,14 @@ export interface CoreActorToolDefinitionDeps<
32
32
  setActiveTools: (toolNames: string[]) => void;
33
33
  }
34
34
 
35
+ export function resolveActiveRuntimeTool(
36
+ name: string,
37
+ activeTools: Pick<Map<string, unknown>, "has">,
38
+ getDefinition: (name: string) => unknown,
39
+ ): unknown {
40
+ return activeTools.has(name) ? getDefinition(name) : undefined;
41
+ }
42
+
35
43
  export const RESERVED_TOOL_NAMES = new Set([
36
44
  "read",
37
45
  "write",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
4
4
  "private": false,
5
5
  "description": "Local Actor Kernel for Pi",
6
6
  "keywords": [
@@ -51,7 +51,7 @@
51
51
  ],
52
52
  "pi": {
53
53
  "extensions": [
54
- "./dist/index.js"
54
+ "./dist/pi-actors/index.js"
55
55
  ],
56
56
  "sourceExtensions": [
57
57
  "./index.ts"
@@ -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
  }