@llblab/pi-actors 0.33.0 → 0.34.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 (155) hide show
  1. package/AGENTS.md +22 -11
  2. package/BACKLOG.md +1 -1
  3. package/CHANGELOG.md +22 -1
  4. package/dist/index.js +10 -10
  5. package/dist/lib/async-runs.d.ts +23 -75
  6. package/dist/lib/async-runs.js +49 -801
  7. package/dist/lib/command-templates.d.ts +3 -3
  8. package/dist/lib/command-templates.js +3 -3
  9. package/dist/lib/config.d.ts +2 -2
  10. package/dist/lib/config.js +5 -5
  11. package/dist/lib/{output.d.ts → execution-output.d.ts} +3 -3
  12. package/dist/lib/{output.js → execution-output.js} +3 -3
  13. package/dist/lib/execution.js +1 -1
  14. package/dist/lib/{actor-inspector-tui.d.ts → inspector.d.ts} +3 -2
  15. package/dist/lib/{actor-inspector-tui.js → inspector.js} +3 -2
  16. package/dist/lib/mailbox-loop.d.ts +3 -3
  17. package/dist/lib/mailbox-loop.js +3 -3
  18. package/dist/lib/{actor-messages.d.ts → messages.d.ts} +3 -3
  19. package/dist/lib/{actor-messages.js → messages.js} +3 -3
  20. package/dist/lib/{actor-recipe-context.d.ts → recipes-context.d.ts} +3 -2
  21. package/dist/lib/{actor-recipe-context.js → recipes-context.js} +2 -1
  22. package/dist/lib/{recipe-discovery.d.ts → recipes-discovery.d.ts} +8 -8
  23. package/dist/lib/{recipe-discovery.js → recipes-discovery.js} +6 -6
  24. package/dist/lib/registry.d.ts +1 -1
  25. package/dist/lib/registry.js +22 -22
  26. package/dist/lib/{actor-rooms.d.ts → rooms.d.ts} +4 -4
  27. package/dist/lib/{actor-rooms.js → rooms.js} +3 -3
  28. package/dist/lib/runs-artifacts.d.ts +21 -0
  29. package/dist/lib/runs-artifacts.js +68 -0
  30. package/dist/lib/runs-control.d.ts +13 -0
  31. package/dist/lib/runs-control.js +59 -0
  32. package/dist/lib/runs-identity.d.ts +5 -0
  33. package/dist/lib/runs-identity.js +11 -0
  34. package/dist/lib/runs-index.d.ts +18 -0
  35. package/dist/lib/runs-index.js +119 -0
  36. package/dist/lib/runs-mailbox.d.ts +25 -0
  37. package/dist/lib/runs-mailbox.js +146 -0
  38. package/dist/lib/runs-messages.d.ts +15 -0
  39. package/dist/lib/runs-messages.js +179 -0
  40. package/dist/lib/runs-outbox.d.ts +41 -0
  41. package/dist/lib/runs-outbox.js +87 -0
  42. package/dist/lib/runs-process.d.ts +7 -0
  43. package/dist/lib/runs-process.js +36 -0
  44. package/dist/lib/runs-retention.d.ts +8 -0
  45. package/dist/lib/runs-retention.js +56 -0
  46. package/dist/lib/runs-start.d.ts +9 -0
  47. package/dist/lib/runs-start.js +67 -0
  48. package/dist/lib/runs-status.d.ts +8 -0
  49. package/dist/lib/runs-status.js +60 -0
  50. package/dist/lib/runtime.d.ts +2 -2
  51. package/dist/lib/runtime.js +5 -5
  52. package/dist/lib/schema.d.ts +9 -0
  53. package/dist/lib/schema.js +40 -0
  54. package/dist/lib/tools-access.d.ts +19 -0
  55. package/dist/lib/tools-access.js +44 -0
  56. package/dist/lib/tools-inspect.d.ts +11 -0
  57. package/dist/lib/tools-inspect.js +534 -0
  58. package/dist/lib/tools-local.d.ts +14 -0
  59. package/dist/lib/tools-local.js +156 -0
  60. package/dist/lib/tools-mailbox.d.ts +8 -0
  61. package/dist/lib/tools-mailbox.js +48 -0
  62. package/dist/lib/tools-message.d.ts +9 -0
  63. package/dist/lib/tools-message.js +346 -0
  64. package/dist/lib/tools-register.d.ts +18 -0
  65. package/dist/lib/tools-register.js +40 -0
  66. package/dist/lib/tools-response.d.ts +21 -0
  67. package/dist/lib/tools-response.js +219 -0
  68. package/dist/lib/tools-spawn.d.ts +12 -0
  69. package/dist/lib/tools-spawn.js +159 -0
  70. package/dist/lib/tools.d.ts +8 -39
  71. package/dist/lib/tools.js +11 -1490
  72. package/dist/recipes/utility-coordinator-lock-snapshot.json +1 -1
  73. package/dist/scripts/actor-worker.mjs +186 -7
  74. package/dist/scripts/async-runner.mjs +171 -8
  75. package/dist/scripts/build-dist.mjs +1 -1
  76. package/dist/scripts/conformance.mjs +3 -3
  77. package/dist/scripts/coordinator.mjs +733 -11
  78. package/dist/scripts/locker.mjs +345 -16
  79. package/dist/scripts/recipe-utils.mjs +478 -14
  80. package/dist/scripts/validate-recipe.mjs +110 -10
  81. package/dist/skills/actors/SKILL.md +1 -1
  82. package/dist/skills/swarm/SKILL.md +1 -1
  83. package/docs/recipe-library.md +1 -1
  84. package/docs/template-recipes.md +1 -1
  85. package/index.ts +11 -18
  86. package/lib/async-runs.ts +124 -1016
  87. package/lib/command-templates.ts +3 -3
  88. package/lib/config.ts +7 -7
  89. package/lib/{output.ts → execution-output.ts} +3 -3
  90. package/lib/execution.ts +5 -1
  91. package/lib/{actor-inspector-tui.ts → inspector.ts} +4 -3
  92. package/lib/mailbox-loop.ts +3 -3
  93. package/lib/{actor-messages.ts → messages.ts} +3 -3
  94. package/lib/{actor-recipe-context.ts → recipes-context.ts} +3 -2
  95. package/lib/{recipe-discovery.ts → recipes-discovery.ts} +19 -19
  96. package/lib/{recipe-usage.ts → recipes-usage.ts} +2 -2
  97. package/lib/registry.ts +37 -24
  98. package/lib/{actor-rooms.ts → rooms.ts} +4 -4
  99. package/lib/runs-artifacts.ts +96 -0
  100. package/lib/runs-control.ts +85 -0
  101. package/lib/runs-identity.ts +14 -0
  102. package/lib/runs-index.ts +156 -0
  103. package/lib/runs-mailbox.ts +208 -0
  104. package/lib/runs-messages.ts +252 -0
  105. package/lib/runs-outbox.ts +144 -0
  106. package/lib/runs-process.ts +51 -0
  107. package/lib/runs-retention.ts +77 -0
  108. package/lib/runs-start.ts +98 -0
  109. package/lib/runs-status.ts +78 -0
  110. package/lib/runtime.ts +6 -6
  111. package/lib/schema.ts +55 -0
  112. package/lib/tools-access.ts +68 -0
  113. package/lib/tools-inspect.ts +735 -0
  114. package/lib/tools-local.ts +234 -0
  115. package/lib/tools-mailbox.ts +56 -0
  116. package/lib/tools-message.ts +483 -0
  117. package/lib/tools-register.ts +64 -0
  118. package/lib/tools-response.ts +267 -0
  119. package/lib/tools-spawn.ts +240 -0
  120. package/lib/tools.ts +20 -2040
  121. package/package.json +1 -1
  122. package/recipes/utility-coordinator-lock-snapshot.json +1 -1
  123. package/scripts/actor-worker.mjs +186 -7
  124. package/scripts/async-runner.mjs +171 -8
  125. package/scripts/build-dist.mjs +1 -1
  126. package/scripts/conformance.mjs +3 -3
  127. package/scripts/coordinator.mjs +733 -11
  128. package/scripts/locker.mjs +345 -16
  129. package/scripts/recipe-utils.mjs +478 -14
  130. package/scripts/validate-recipe.mjs +110 -10
  131. package/skills/actors/SKILL.md +1 -1
  132. package/skills/swarm/SKILL.md +1 -1
  133. package/dist/lib/actor-worker.d.ts +0 -16
  134. package/dist/lib/actor-worker.js +0 -134
  135. package/dist/lib/async-runner.d.ts +0 -5
  136. package/dist/lib/async-runner.js +0 -134
  137. package/dist/lib/coordinator.d.ts +0 -5
  138. package/dist/lib/coordinator.js +0 -660
  139. package/dist/lib/locker.d.ts +0 -5
  140. package/dist/lib/locker.js +0 -310
  141. package/dist/lib/recipe-utils.d.ts +0 -5
  142. package/dist/lib/recipe-utils.js +0 -385
  143. package/dist/lib/validate-recipe.d.ts +0 -6
  144. package/dist/lib/validate-recipe.js +0 -104
  145. package/lib/actor-worker.ts +0 -174
  146. package/lib/async-runner.ts +0 -173
  147. package/lib/coordinator.ts +0 -742
  148. package/lib/locker.ts +0 -340
  149. package/lib/recipe-utils.ts +0 -486
  150. package/lib/validate-recipe.ts +0 -110
  151. /package/dist/lib/{recipe-references.d.ts → recipes-references.d.ts} +0 -0
  152. /package/dist/lib/{recipe-references.js → recipes-references.js} +0 -0
  153. /package/dist/lib/{recipe-usage.d.ts → recipes-usage.d.ts} +0 -0
  154. /package/dist/lib/{recipe-usage.js → recipes-usage.js} +0 -0
  155. /package/lib/{recipe-references.ts → recipes-references.ts} +0 -0
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Public tool response formatting
3
+ * Zones: compact text summaries, verbose JSON switching, next-action rendering
4
+ * Owns model-facing response helpers shared by public tool execution paths
5
+ */
6
+
7
+ import * as Limits from "./limits.ts";
8
+ import * as ToolsMailbox from "./tools-mailbox.ts";
9
+
10
+ export function asRecord(value: unknown): Record<string, unknown> {
11
+ return value && typeof value === "object" && !Array.isArray(value)
12
+ ? (value as Record<string, unknown>)
13
+ : {};
14
+ }
15
+
16
+ export function jsonText(value: unknown): string {
17
+ return `\n${JSON.stringify(value, null, 2)}`;
18
+ }
19
+
20
+ export function compactPreview(
21
+ value: unknown,
22
+ maxLength = Limits.COMPACT_PREVIEW_CHARS,
23
+ ): string | undefined {
24
+ if (value === undefined) return undefined;
25
+ const text =
26
+ typeof value === "string" ? value : JSON.stringify(value, undefined, 0);
27
+ const compact = text.replaceAll(/\s+/g, "_");
28
+ return compact.length > maxLength
29
+ ? `${compact.slice(0, Math.max(0, maxLength - 1))}…`
30
+ : compact;
31
+ }
32
+
33
+ export function compactNextActions(actions: string[]): string {
34
+ return actions.length
35
+ ? ` next=${actions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`
36
+ : "";
37
+ }
38
+
39
+ function formatFailureCount(value: unknown): number | undefined {
40
+ return Array.isArray(value) ? value.length : undefined;
41
+ }
42
+
43
+ export function actorRunNextActions(run: unknown): string[] {
44
+ const id = String(run ?? "").trim();
45
+ if (!id) return [];
46
+ return [
47
+ `inspect target=run:${id} view=status`,
48
+ `inspect target=run:${id} view=messages`,
49
+ `message to=run:${id} type=<actor.action>`,
50
+ ];
51
+ }
52
+
53
+ export function compactAsyncRunStatus(value: unknown): string {
54
+ const status = asRecord(value);
55
+ const progress = asRecord(status.progress);
56
+ const result = asRecord(status.result);
57
+ const run = String(status.run ?? "<unknown>");
58
+ const tokens = [`run=${run}`, `status=${String(status.status ?? "unknown")}`];
59
+ if (status.tool) tokens.push(`tool=${String(status.tool)}`);
60
+ if (status.recipe) tokens.push(`recipe=${String(status.recipe)}`);
61
+ if (status.retire_when)
62
+ tokens.push(`retire_when=${String(status.retire_when)}`);
63
+ if (Number(status.pid) > 0) tokens.push(`pid=${Number(status.pid)}`);
64
+ if (progress.phase && progress.phase !== status.status)
65
+ tokens.push(`phase=${String(progress.phase)}`);
66
+ if (Number(progress.activeSubagents) > 0)
67
+ tokens.push(`active=${Number(progress.activeSubagents)}`);
68
+ if (Number(progress.completed) > 0)
69
+ tokens.push(`completed=${Number(progress.completed)}`);
70
+ const failures = formatFailureCount(progress.failures);
71
+ if (failures !== undefined && failures > 0)
72
+ tokens.push(`failures=${failures}`);
73
+ if (result.code !== undefined) tokens.push(`code=${String(result.code)}`);
74
+ if (result.killed === true) tokens.push("killed=true");
75
+ const draftRecipe = status.draft_recipe;
76
+ if (draftRecipe) tokens.push(`draft_recipe=${String(draftRecipe)}`);
77
+ const nextActions = actorRunNextActions(run);
78
+ if (nextActions.length > 0)
79
+ tokens.push(
80
+ `next=${nextActions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`,
81
+ );
82
+ return `\n${tokens.join(" ")}`;
83
+ }
84
+
85
+ export function compactInboxMessages(
86
+ messages: Array<Record<string, unknown>>,
87
+ emptyLabel: string,
88
+ ): string {
89
+ if (messages.length === 0) return `\n(no ${emptyLabel} messages)`;
90
+ return `\n${messages
91
+ .map((message) =>
92
+ [
93
+ ...(message.id ? [`id=${String(message.id)}`] : []),
94
+ `status=${String(message.status ?? "")}`,
95
+ `type=${String(message.type ?? "")}`,
96
+ `from=${String(message.from ?? "")}`,
97
+ `to=${String(message.to ?? "")}`,
98
+ ...(message.queued_at
99
+ ? [`queued_at=${String(message.queued_at)}`]
100
+ : []),
101
+ ...(message.sent_at ? [`sent_at=${String(message.sent_at)}`] : []),
102
+ ...(message.claimed_at
103
+ ? [`claimed_at=${String(message.claimed_at)}`]
104
+ : []),
105
+ ...(message.handled_at
106
+ ? [`handled_at=${String(message.handled_at)}`]
107
+ : []),
108
+ ...(message.failed_at
109
+ ? [`failed_at=${String(message.failed_at)}`]
110
+ : []),
111
+ ].join(" "),
112
+ )
113
+ .join("\n")}`;
114
+ }
115
+
116
+ export function compactBranchInbox(
117
+ messages: Array<Record<string, unknown>>,
118
+ ): string {
119
+ return compactInboxMessages(messages, "branch inbox");
120
+ }
121
+
122
+ export function compactRunMailbox(
123
+ run: string,
124
+ mailbox: Record<string, unknown>,
125
+ messages: Array<Record<string, unknown>>,
126
+ ): string {
127
+ const normalized = ToolsMailbox.normalizeMailboxContracts(mailbox);
128
+ return `\nrun=${run} accepts=${ToolsMailbox.mailboxTypes(normalized.accepts).join(",")} emits=${ToolsMailbox.mailboxTypes(normalized.emits).join(",")}${compactInboxMessages(messages, "run inbox")}`;
129
+ }
130
+
131
+ export function artifactNextActions(
132
+ run: unknown,
133
+ artifacts: Record<string, unknown>,
134
+ ): string[] {
135
+ const id = String(run ?? "").trim();
136
+ if (!id || Object.keys(artifacts).length === 0) return [];
137
+ return [
138
+ `inspect target=run:${id} view=artifacts verbose=true`,
139
+ `inspect target=run:${id} view=messages`,
140
+ ];
141
+ }
142
+
143
+ export function maybeJsonText(
144
+ value: unknown,
145
+ verbose: boolean | undefined,
146
+ compact: string,
147
+ ): string {
148
+ return verbose ? jsonText(value) : compact;
149
+ }
150
+
151
+ export function compactRecipeImports(summary: Record<string, unknown>): string {
152
+ const active = Array.isArray(summary.active)
153
+ ? (summary.active as Array<Record<string, unknown>>)
154
+ : [];
155
+ const lines = active.flatMap((entry) => {
156
+ const imports = asRecord(entry.imports);
157
+ return Object.entries(imports).map(([alias, value]) => {
158
+ const binding =
159
+ typeof value === "string" ? { from: value } : asRecord(value);
160
+ return `recipe=${String(entry.id ?? "<unknown>")} alias=${alias} from=${String(binding.from ?? value)}`;
161
+ });
162
+ });
163
+ return lines.length ? `\n${lines.join("\n")}` : "\n(no recipe imports)";
164
+ }
165
+
166
+ export function compactRecipeDoctor(summary: Record<string, unknown>): string {
167
+ const details = Array.isArray(summary.diagnostic_details)
168
+ ? (summary.diagnostic_details as Array<Record<string, unknown>>)
169
+ : [];
170
+ const remediations = Array.isArray(summary.remediations)
171
+ ? (summary.remediations as Array<Record<string, unknown>>)
172
+ : [];
173
+ const recommendations = Array.isArray(summary.recommendations)
174
+ ? (summary.recommendations as Array<Record<string, unknown>>)
175
+ : [];
176
+ const counts = { error: 0, info: 0, warning: 0 };
177
+ for (const detail of details) {
178
+ const severity = String(detail.severity ?? "info");
179
+ if (severity === "error" || severity === "warning" || severity === "info")
180
+ counts[severity] += 1;
181
+ }
182
+ const topAction = asRecord(summary.top_action);
183
+ const lines = [
184
+ `recipes doctor errors=${counts.error} warnings=${counts.warning} info=${counts.info} actions=${remediations.length} recommendations=${recommendations.length}`,
185
+ ];
186
+ if (Object.keys(topAction).length > 0) {
187
+ const action = compactPreview(
188
+ topAction.action,
189
+ Limits.DOCTOR_ACTION_PREVIEW_CHARS,
190
+ );
191
+ lines.push(
192
+ `top severity=${String(topAction.severity ?? "info")} kind=${String(topAction.kind ?? "inspect")} id=${String(topAction.id ?? "root")} action=${action ?? "inspect"}`,
193
+ );
194
+ }
195
+ for (const item of remediations.slice(0, 8)) {
196
+ const action = compactPreview(
197
+ item.action,
198
+ Limits.DOCTOR_ACTION_PREVIEW_CHARS,
199
+ );
200
+ const blocked = item.blocked_fallback
201
+ ? ` blocked=${compactPreview(item.blocked_fallback, Limits.DOCTOR_ACTION_PREVIEW_CHARS)}`
202
+ : "";
203
+ lines.push(
204
+ `${String(item.severity ?? "info")} kind=${String(item.kind ?? "inspect")} id=${String(item.id ?? "root")}${blocked} action=${action ?? "inspect"}`,
205
+ );
206
+ }
207
+ const nextActions = Array.isArray(summary.next_actions)
208
+ ? (summary.next_actions as string[])
209
+ : [];
210
+ if (nextActions.length > 0)
211
+ lines[0] = `${lines[0]}${compactNextActions(nextActions)}`;
212
+ return `\n${lines.join("\n")}`;
213
+ }
214
+
215
+ export function recipeRegistryNextActions(
216
+ summary: Record<string, unknown>,
217
+ view: string,
218
+ ): string[] {
219
+ const actions: string[] = [];
220
+ const drafts = Array.isArray(summary.drafts)
221
+ ? (summary.drafts as Array<Record<string, unknown>>)
222
+ : [];
223
+ const invalid = Array.isArray(summary.invalid) ? summary.invalid.length : 0;
224
+ const diagnostics = Array.isArray(summary.diagnostics)
225
+ ? summary.diagnostics.length
226
+ : 0;
227
+ const topAction = asRecord(summary.top_action);
228
+ if (view !== "doctor" && (invalid > 0 || diagnostics > 0)) {
229
+ actions.push("inspect target=recipes view=doctor");
230
+ }
231
+ if (view === "doctor" && typeof topAction.action === "string") {
232
+ actions.push(String(topAction.action));
233
+ }
234
+ if (drafts.length > 0) {
235
+ actions.push("inspect target=recipes view=summary verbose=true");
236
+ const firstPath =
237
+ typeof drafts[0]?.path === "string" ? drafts[0].path : undefined;
238
+ if (firstPath) actions.push(`spawn file=${firstPath}`);
239
+ }
240
+ return [...new Set(actions)].slice(0, 4);
241
+ }
242
+
243
+ export function compactRecipeRegistry(
244
+ summary: Record<string, unknown>,
245
+ ): string {
246
+ const active = Array.isArray(summary.active) ? summary.active.length : 0;
247
+ const shadowed = Array.isArray(summary.shadowed)
248
+ ? summary.shadowed.length
249
+ : 0;
250
+ const invalid = Array.isArray(summary.invalid) ? summary.invalid.length : 0;
251
+ const disabled = Array.isArray(summary.disabled)
252
+ ? summary.disabled.length
253
+ : 0;
254
+ const diagnostics = Array.isArray(summary.diagnostics)
255
+ ? summary.diagnostics.length
256
+ : 0;
257
+ const drafts = Array.isArray(summary.drafts) ? summary.drafts.length : 0;
258
+ const recommendations = Array.isArray(summary.recommendations)
259
+ ? summary.recommendations.length
260
+ : 0;
261
+ const nextActions = Array.isArray(summary.next_actions)
262
+ ? (summary.next_actions as string[])
263
+ : [];
264
+ return `\nrecipes active=${active} drafts=${drafts} shadowed=${shadowed} invalid=${invalid} disabled=${disabled} recommendations=${recommendations} diagnostics=${diagnostics}${compactNextActions(nextActions)}`;
265
+ }
266
+
267
+ export const DEFAULT_INSPECT_LINES = Limits.DEFAULT_INSPECT_LINES;
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Public spawn tool behavior
3
+ * Zones: actor launch, draft recipe capture, launch diagnostics
4
+ * Owns the public spawn execution path for run-backed actors
5
+ */
6
+
7
+ import { mkdirSync, writeFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+
10
+ import * as AsyncRuns from "./async-runs.ts";
11
+ import * as Messages from "./messages.ts";
12
+ import * as Paths from "./paths.ts";
13
+ import * as RecipesDiscovery from "./recipes-discovery.ts";
14
+ import * as Rooms from "./rooms.ts";
15
+ import * as Schema from "./schema.ts";
16
+ import * as ToolsResponse from "./tools-response.ts";
17
+
18
+ export interface SpawnToolContext {
19
+ cwd: string;
20
+ sessionManager?: { getSessionId?: () => string };
21
+ }
22
+
23
+ const asRecord = ToolsResponse.asRecord;
24
+ const maybeJsonText = ToolsResponse.maybeJsonText;
25
+
26
+ function getRunOwnerId(ctx: SpawnToolContext): string | undefined {
27
+ return ctx.sessionManager?.getSessionId?.();
28
+ }
29
+
30
+ function shadowedRecipeLaunchDiagnostic(
31
+ recipe: unknown,
32
+ ): Record<string, unknown> | undefined {
33
+ if (
34
+ typeof recipe !== "string" ||
35
+ recipe.includes("/") ||
36
+ recipe.includes("~")
37
+ )
38
+ return undefined;
39
+ const discovery = RecipesDiscovery.discoverRecipeSources([
40
+ { root: Paths.getRecipeRoot(), defaultTool: true, mutableUsage: true },
41
+ { root: Paths.getPackagedRecipeRoot(), defaultTool: false },
42
+ ]);
43
+ return RecipesDiscovery.getShadowedLaunchDiagnostic(discovery, recipe);
44
+ }
45
+
46
+ function draftRecipeName(run: string): string {
47
+ return `${run.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "spawn"}.json`;
48
+ }
49
+
50
+ function draftRecipeDefaults(
51
+ values: Record<string, unknown>,
52
+ ): Record<string, unknown> | undefined {
53
+ const ignored = new Set([
54
+ "actor_address",
55
+ "communication_file",
56
+ "default_room",
57
+ "run_id",
58
+ "state_dir",
59
+ ]);
60
+ const defaults = Object.fromEntries(
61
+ Object.entries(values).filter(([key]) => !ignored.has(key)),
62
+ );
63
+ return Object.keys(defaults).length > 0 ? defaults : undefined;
64
+ }
65
+
66
+ function writeSpawnDraftRecipe(
67
+ input: Record<string, unknown>,
68
+ meta: AsyncRuns.AsyncRunMeta,
69
+ ): string | undefined {
70
+ if (
71
+ process.env.NODE_TEST_CONTEXT &&
72
+ process.env.PI_ACTORS_ENABLE_SPAWN_DRAFTS_IN_TEST !== "1"
73
+ )
74
+ return undefined;
75
+ if (
76
+ input.template === undefined ||
77
+ input.file !== undefined ||
78
+ input.recipe !== undefined
79
+ )
80
+ return undefined;
81
+ const root = Paths.getRecipeDraftRoot();
82
+ mkdirSync(root, { recursive: true });
83
+ const path = join(root, draftRecipeName(String(meta.run)));
84
+ const defaults = draftRecipeDefaults(meta.values);
85
+ const recipe = {
86
+ async: true,
87
+ description: `Draft recipe captured from spawn run ${String(meta.run)}`,
88
+ ...(meta.artifacts ? { artifacts: meta.artifacts } : {}),
89
+ ...(defaults ? { defaults } : {}),
90
+ template: input.template,
91
+ };
92
+ writeFileSync(path, `${JSON.stringify(recipe, null, 2)}\n`, { flag: "wx" });
93
+ return path;
94
+ }
95
+
96
+ function enhanceSpawnRecipeError(error: unknown, recipe: unknown): Error {
97
+ const diagnostic = shadowedRecipeLaunchDiagnostic(recipe);
98
+ if (!diagnostic)
99
+ return error instanceof Error ? error : new Error(String(error));
100
+ const original = error instanceof Error ? error.message : String(error);
101
+ return Object.assign(
102
+ new Error(
103
+ `${original} reason=${diagnostic.reason} active_path=${diagnostic.active_path} blocked_fallback=${diagnostic.blocked_fallback} hint=${diagnostic.hint}`,
104
+ ),
105
+ {
106
+ ...diagnostic,
107
+ original_error: original,
108
+ },
109
+ );
110
+ }
111
+
112
+ function runIdFromActorAddress(
113
+ address: string | undefined,
114
+ ): string | undefined {
115
+ if (!address) return undefined;
116
+ const parsed = Messages.parseActorAddress(address);
117
+ if (parsed.kind !== "run" || !parsed.value) {
118
+ throw new Error(`Expected run:<id> actor address, received: ${address}`);
119
+ }
120
+ return parsed.value;
121
+ }
122
+
123
+ export function createSpawnToolDefinition<
124
+ TContext extends SpawnToolContext,
125
+ >(): any {
126
+ return {
127
+ name: "spawn",
128
+ label: "Spawn",
129
+ description:
130
+ "Create an addressable actor from a recipe file or inline command template. Use instead of ad hoc shell backgrounding for work that may outlive this turn, needs steering/follow-up/artifacts, runs as a service, fans out, or should be inspected later. Currently spawns run:<id> actors backed by async runs.",
131
+ parameters: Schema.objectSchema(
132
+ {
133
+ artifacts: Schema.looseObjectSchema(
134
+ "Optional named artifact paths for the spawned actor.",
135
+ ),
136
+ as: Schema.stringSchema(
137
+ "Optional actor address for the spawned run, e.g. run:<id>.",
138
+ ),
139
+ file: Schema.stringSchema(
140
+ "Optional template recipe JSON file. Bare names resolve under ~/.pi/agent/recipes.",
141
+ ),
142
+ recipe: Schema.stringSchema(
143
+ "Alias for file; template recipe JSON file/name to spawn.",
144
+ ),
145
+ state_dir: Schema.stringSchema(
146
+ "Optional explicit run state directory.",
147
+ ),
148
+ template: Schema.unionSchema([
149
+ Schema.stringSchema("Inline command template string"),
150
+ Schema.arraySchema(
151
+ "Inline command-template sequence or parallel tree",
152
+ ),
153
+ Schema.looseObjectSchema(
154
+ "Inline command-template object with flags such as parallel, repeat, retry, failure, and nested template.",
155
+ ),
156
+ ]),
157
+ values: Schema.looseObjectSchema(
158
+ "Runtime placeholder values passed to the actor.",
159
+ ),
160
+ verbose: Schema.booleanSchema(
161
+ "Return full JSON instead of compact text.",
162
+ ),
163
+ },
164
+ [],
165
+ ),
166
+ async execute(
167
+ _toolCallId: string,
168
+ params: unknown,
169
+ _signal: AbortSignal | undefined,
170
+ _onUpdate: unknown,
171
+ ctx: TContext,
172
+ ) {
173
+ const input = asRecord(params);
174
+ const runId = runIdFromActorAddress(
175
+ typeof input.as === "string" ? input.as : undefined,
176
+ );
177
+ const recipe =
178
+ typeof input.file === "string"
179
+ ? input.file
180
+ : typeof input.recipe === "string"
181
+ ? input.recipe
182
+ : undefined;
183
+ let meta: AsyncRuns.AsyncRunMeta;
184
+ try {
185
+ meta = AsyncRuns.startRun(
186
+ {
187
+ file: recipe,
188
+ launch_source: "spawn",
189
+ ownerId: getRunOwnerId(ctx),
190
+ run_id: runId,
191
+ state_dir:
192
+ typeof input.state_dir === "string" ? input.state_dir : undefined,
193
+ ...(input.template !== undefined
194
+ ? {
195
+ template:
196
+ input.template as AsyncRuns.AsyncRunStartParams["template"],
197
+ }
198
+ : {}),
199
+ values: asRecord(input.values),
200
+ ...(input.artifacts &&
201
+ typeof input.artifacts === "object" &&
202
+ !Array.isArray(input.artifacts)
203
+ ? {
204
+ artifacts: input.artifacts as Record<
205
+ string,
206
+ AsyncRuns.RunArtifactDeclaration
207
+ >,
208
+ }
209
+ : {}),
210
+ },
211
+ ctx.cwd,
212
+ );
213
+ } catch (error) {
214
+ throw enhanceSpawnRecipeError(error, recipe);
215
+ }
216
+ const draftRecipe = writeSpawnDraftRecipe(input, meta);
217
+ const nextActions = ToolsResponse.actorRunNextActions(meta.run);
218
+ const details = {
219
+ ...meta,
220
+ ...(draftRecipe ? { draft_recipe: draftRecipe } : {}),
221
+ next_actions: nextActions,
222
+ };
223
+ Rooms.ensureDefaultRoom(meta.state_dir, String(meta.run));
224
+ Rooms.writeCommunicationSnapshot(meta.state_dir, String(meta.run));
225
+ return {
226
+ content: [
227
+ {
228
+ type: "text" as const,
229
+ text: maybeJsonText(
230
+ details,
231
+ input.verbose === true,
232
+ ToolsResponse.compactAsyncRunStatus(details),
233
+ ),
234
+ },
235
+ ],
236
+ details,
237
+ };
238
+ },
239
+ };
240
+ }