@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
package/lib/tools.ts CHANGED
@@ -1,31 +1,17 @@
1
1
  /**
2
- * Pi-facing tool definition helpers
3
- * Zones: pi tools, registry tools, async run launchers
4
- * Owns generated runtime tool schemas and the register_tool management tool schema
2
+ * Public tool family composition
3
+ * Zones: tool set, reserved tool names, pi-facing tool wiring
4
+ * Owns public tool-family composition without owning individual tool behavior
5
5
  */
6
6
 
7
- import { execFileSync } from "node:child_process";
8
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
- import { dirname, join } from "node:path";
7
+ import * as ToolsInspect from "./tools-inspect.ts";
8
+ import type { RuntimeToolContext } from "./tools-local.ts";
9
+ import * as ToolsMessage from "./tools-message.ts";
10
+ import * as ToolsRegister from "./tools-register.ts";
11
+ import * as ToolsSpawn from "./tools-spawn.ts";
10
12
 
11
- import * as ActorMessages from "./actor-messages.ts";
12
- import * as ActorRooms from "./actor-rooms.ts";
13
- import * as AsyncRuns from "./async-runs.ts";
14
- import * as CommandTemplates from "./command-templates.ts";
15
- import type { RegisteredTool } from "./config.ts";
16
- import * as Execution from "./execution.ts";
17
- import * as Limits from "./limits.ts";
18
- import * as Paths from "./paths.ts";
19
- import * as Prompts from "./prompts.ts";
20
- import * as RecipeDiscovery from "./recipe-discovery.ts";
21
- import * as RecipeReferences from "./recipe-references.ts";
22
- import * as RecipeUsage from "./recipe-usage.ts";
23
- import * as Registry from "./registry.ts";
24
- import * as Schema from "./schema.ts";
25
-
26
- export type RegisterToolInput = Registry.RegisterToolInput;
27
13
  export type RegisterToolRuntimeDeps<TContext> =
28
- Registry.RegisterToolRuntimeDeps<TContext>;
14
+ ToolsRegister.RegisterToolRuntimeDeps<TContext>;
29
15
 
30
16
  export interface ActorToolDefinition {
31
17
  name: string;
@@ -33,16 +19,15 @@ export interface ActorToolDefinition {
33
19
  [key: string]: unknown;
34
20
  }
35
21
 
36
- export interface CoreActorToolDefinitionDeps<TContext extends AsyncRunToolContext> {
22
+ export interface CoreActorToolDefinitionDeps<
23
+ TContext extends RuntimeToolContext,
24
+ > {
37
25
  configPath: string;
38
26
  getActiveTools: () => string[];
39
27
  getRuntimeTool: (name: string) => unknown;
40
28
  registryRuntime: Pick<
41
29
  RegisterToolRuntimeDeps<TContext>,
42
- | "getExternalToolConflict"
43
- | "getTools"
44
- | "notify"
45
- | "registerRuntimeTool"
30
+ "getExternalToolConflict" | "getTools" | "notify" | "registerRuntimeTool"
46
31
  >;
47
32
  setActiveTools: (toolNames: string[]) => void;
48
33
  }
@@ -61,1886 +46,11 @@ export const RESERVED_TOOL_NAMES = new Set([
61
46
  "inspect",
62
47
  ]);
63
48
 
64
- type JsonSchema = Record<string, unknown>;
65
-
66
- function stringSchema(description: string): JsonSchema {
67
- return { description, type: "string" };
68
- }
69
-
70
- function typedArgSchema(
71
- arg: string,
72
- type: Schema.ToolArgType | undefined,
73
- ): JsonSchema {
74
- if (!type || type.kind === "string") return stringSchema(`Argument: ${arg}`);
75
- if (type.kind === "path") return stringSchema(`Path argument: ${arg}`);
76
- if (type.kind === "int")
77
- return { description: `Integer argument: ${arg}`, type: "integer" };
78
- if (type.kind === "number")
79
- return { description: `Number argument: ${arg}`, type: "number" };
80
- if (type.kind === "bool")
81
- return { description: `Boolean argument: ${arg}`, type: "boolean" };
82
- if (type.kind === "array")
83
- return { description: `Array argument: ${arg}`, items: {}, type: "array" };
84
- return {
85
- description: `Enum argument: ${arg}`,
86
- enum: type.values,
87
- type: "string",
88
- };
89
- }
90
-
91
- function booleanSchema(description: string): JsonSchema {
92
- return { description, type: "boolean" };
93
- }
94
-
95
- function nullSchema(description: string): JsonSchema {
96
- return { description, type: "null" };
97
- }
98
-
99
- function arraySchema(description: string): JsonSchema {
100
- return { description, items: {}, type: "array" };
101
- }
102
-
103
- function unionSchema(anyOf: JsonSchema[]): JsonSchema {
104
- return { anyOf };
105
- }
106
-
107
- function objectSchema(
108
- properties: Record<string, JsonSchema>,
109
- required: string[],
110
- ): JsonSchema {
111
- return { additionalProperties: false, properties, required, type: "object" };
112
- }
113
-
114
- function sampleValueForArg(
115
- arg: string,
116
- type: Schema.ToolArgType | undefined,
117
- defaults: Record<string, unknown>,
118
- ): unknown {
119
- if (Object.hasOwn(defaults, arg)) return defaults[arg];
120
- if (!type || type.kind === "string") return `<${arg}>`;
121
- if (type.kind === "path") return `./${arg}`;
122
- if (type.kind === "int") return 1;
123
- if (type.kind === "number") return 1.5;
124
- if (type.kind === "bool") return true;
125
- if (type.kind === "array") return [`<${arg}>`];
126
- return type.values[0] ?? `<${arg}>`;
127
- }
128
-
129
- function shouldAddRuntimeToolUsageHint(error: unknown): boolean {
130
- const message = error instanceof Error ? error.message : String(error);
131
- return (
132
- /^Argument \S+ must /.test(message) || /^Missing .* value: /.test(message)
133
- );
134
- }
135
-
136
- function formatRuntimeToolUsageHint(
137
- cfg: RegisteredTool,
138
- required: string[],
139
- includeRunId: boolean,
140
- ): string {
141
- const optional = cfg.args.filter((arg) => !required.includes(arg));
142
- const example: Record<string, unknown> = {};
143
- for (const arg of required)
144
- example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], cfg.defaults);
145
- for (const arg of optional)
146
- example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], cfg.defaults);
147
- if (includeRunId) example.run_id = `${cfg.name}-1`;
148
- const lines = [
149
- `Expected call shape for ${cfg.name}:`,
150
- `${cfg.name}(${JSON.stringify(example, null, 2)})`,
151
- ];
152
- if (required.length) lines.push(`Required: ${required.join(", ")}`);
153
- if (optional.length || includeRunId)
154
- lines.push(
155
- `Optional: ${[...optional, ...(includeRunId ? ["run_id"] : [])].join(", ")}`,
156
- );
157
- return lines.join("\n");
158
- }
159
-
160
- function formatRuntimeToolArgumentError(
161
- cfg: RegisteredTool,
162
- error: unknown,
163
- required: string[],
164
- includeRunId: boolean,
165
- ): Error {
166
- const message = error instanceof Error ? error.message : String(error);
167
- if (!shouldAddRuntimeToolUsageHint(error))
168
- return error instanceof Error ? error : new Error(message);
169
- return new Error(
170
- `Invalid arguments for tool "${cfg.name}": ${message}\n\n${formatRuntimeToolUsageHint(
171
- cfg,
172
- required,
173
- includeRunId,
174
- )}`,
175
- );
176
- }
177
-
178
- function looseObjectSchema(description: string): JsonSchema {
179
- return { additionalProperties: true, description, type: "object" };
180
- }
181
-
182
- function jsonText(value: unknown): string {
183
- return `\n${JSON.stringify(value, null, 2)}`;
184
- }
185
-
186
- function asRecord(value: unknown): Record<string, unknown> {
187
- return value && typeof value === "object" && !Array.isArray(value)
188
- ? (value as Record<string, unknown>)
189
- : {};
190
- }
191
-
192
- function formatFailureCount(value: unknown): number | undefined {
193
- return Array.isArray(value) ? value.length : undefined;
194
- }
195
-
196
- function actorRunNextActions(run: unknown): string[] {
197
- const id = String(run ?? "").trim();
198
- if (!id) return [];
199
- return [
200
- `inspect target=run:${id} view=status`,
201
- `inspect target=run:${id} view=messages`,
202
- `message to=run:${id} type=<actor.action>`,
203
- ];
204
- }
205
-
206
- function compactAsyncRunStatus(value: unknown): string {
207
- const status = asRecord(value);
208
- const progress = asRecord(status.progress);
209
- const result = asRecord(status.result);
210
- const run = String(status.run ?? "<unknown>");
211
- const tokens = [
212
- `run=${run}`,
213
- `status=${String(status.status ?? "unknown")}`,
214
- ];
215
- if (status.tool) tokens.push(`tool=${String(status.tool)}`);
216
- if (status.recipe) tokens.push(`recipe=${String(status.recipe)}`);
217
- if (status.retire_when)
218
- tokens.push(`retire_when=${String(status.retire_when)}`);
219
- if (Number(status.pid) > 0) tokens.push(`pid=${Number(status.pid)}`);
220
- if (progress.phase && progress.phase !== status.status)
221
- tokens.push(`phase=${String(progress.phase)}`);
222
- if (Number(progress.activeSubagents) > 0)
223
- tokens.push(`active=${Number(progress.activeSubagents)}`);
224
- if (Number(progress.completed) > 0)
225
- tokens.push(`completed=${Number(progress.completed)}`);
226
- const failures = formatFailureCount(progress.failures);
227
- if (failures !== undefined && failures > 0)
228
- tokens.push(`failures=${failures}`);
229
- if (result.code !== undefined) tokens.push(`code=${String(result.code)}`);
230
- if (result.killed === true) tokens.push("killed=true");
231
- const draftRecipe = status.draft_recipe;
232
- if (draftRecipe) tokens.push(`draft_recipe=${String(draftRecipe)}`);
233
- const nextActions = actorRunNextActions(run);
234
- if (nextActions.length > 0)
235
- tokens.push(`next=${nextActions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`);
236
- return `\n${tokens.join(" ")}`;
237
- }
238
-
239
- function compactRunMessages(messages: AsyncRuns.RunOutboxEvent[]): string {
240
- if (messages.length === 0) return "\n(no actor messages)";
241
- return `\n${messages
242
- .map((message) =>
243
- [
244
- `run=${message.run}`,
245
- `type=${message.event}`,
246
- `level=${message.level}`,
247
- `summary=${message.summary.replaceAll(/\s+/g, "_")}`,
248
- ].join(" "),
249
- )
250
- .join("\n")}`;
251
- }
252
-
253
- function compactPreview(
254
- value: unknown,
255
- maxLength = Limits.COMPACT_PREVIEW_CHARS,
256
- ): string | undefined {
257
- if (value === undefined) return undefined;
258
- const text =
259
- typeof value === "string" ? value : JSON.stringify(value, undefined, 0);
260
- const compact = text.replaceAll(/\s+/g, "_");
261
- return compact.length > maxLength
262
- ? `${compact.slice(0, Math.max(0, maxLength - 1))}…`
263
- : compact;
264
- }
265
-
266
- function compactRoomPreviews(
267
- previews: ActorRooms.RoomMessagePreview[],
268
- ): string {
269
- if (previews.length === 0) return "\n(no room message previews)";
270
- return `\n${previews
271
- .map((preview) =>
272
- [
273
- `ts=${preview.timestamp}`,
274
- preview.from ? `from=${preview.from}` : "",
275
- `to=${preview.to}`,
276
- `type=${preview.type}`,
277
- preview.summary ? `summary=${compactPreview(preview.summary)}` : "",
278
- preview.body_preview
279
- ? `body=${compactPreview(preview.body_preview)}`
280
- : "",
281
- ]
282
- .filter(Boolean)
283
- .join(" "),
284
- )
285
- .join("\n")}`;
286
- }
287
-
288
- function compactRoomMessages(messages: ActorRooms.RoomTimelineEntry[]): string {
289
- if (messages.length === 0) return "\n(no room messages)";
290
- return `\n${messages
291
- .map((message) =>
292
- [
293
- `ts=${message.received_at}`,
294
- `from=${String(message.from ?? "<unknown>")}`,
295
- `to=${message.to}`,
296
- `type=${message.type}`,
297
- `summary=${String(message.summary ?? "").replaceAll(/\s+/g, "_")}`,
298
- compactPreview(message.body)
299
- ? `body=${compactPreview(message.body)}`
300
- : "",
301
- ]
302
- .filter(Boolean)
303
- .join(" "),
304
- )
305
- .join("\n")}`;
306
- }
307
-
308
- function compactRoomContacts(contacts: ActorRooms.RoomContact[]): string {
309
- if (contacts.length === 0) return "\n(no room contacts)";
310
- return `\n${contacts
311
- .map((contact) =>
312
- [
313
- `address=${contact.address}`,
314
- contact.role !== undefined ? `role=${String(contact.role)}` : "",
315
- contact.parent !== undefined ? `parent=${String(contact.parent)}` : "",
316
- contact.caps !== undefined
317
- ? `caps=${Array.isArray(contact.caps) ? contact.caps.join(",") : String(contact.caps)}`
318
- : "",
319
- contact.claim !== undefined
320
- ? `claim=${String(contact.claim).replaceAll(/\s+/g, "_")}`
321
- : "",
322
- contact.status !== undefined ? `status=${String(contact.status)}` : "",
323
- ]
324
- .filter(Boolean)
325
- .join(" "),
326
- )
327
- .join("\n")}`;
328
- }
329
-
330
- function compactRoomRoster(
331
- roster: Record<string, ActorRooms.RoomMember>,
332
- ): string {
333
- const members = Object.values(roster);
334
- if (members.length === 0) return "\n(no room members)";
335
- return `\n${members
336
- .map((member) =>
337
- [
338
- `address=${member.address}`,
339
- `role=${String(member.role ?? "")}`,
340
- member.parent !== undefined ? `parent=${String(member.parent)}` : "",
341
- member.caps !== undefined
342
- ? `caps=${Array.isArray(member.caps) ? member.caps.join(",") : String(member.caps)}`
343
- : "",
344
- member.claim !== undefined
345
- ? `claim=${String(member.claim).replaceAll(/\s+/g, "_")}`
346
- : "",
347
- `status=${String(member.status ?? "")}`,
348
- `last_seen=${member.last_seen}`,
349
- ].join(" "),
350
- )
351
- .join("\n")}`;
352
- }
353
-
354
- function compactRoomStatus(status: ActorRooms.RoomStatus): string {
355
- return `\nroom=${status.room} messages=${status.message_count} roster=${status.roster_count}${status.diagnostics_count ? ` diagnostics=${status.diagnostics_count}` : ""}${status.last_message_at ? ` last_message_at=${status.last_message_at}` : ""}${status.last_message_from ? ` last_from=${status.last_message_from}` : ""}${status.last_message_type ? ` last_type=${status.last_message_type}` : ""}${status.last_message_summary ? ` last_summary=${compactPreview(status.last_message_summary)}` : ""}`;
356
- }
357
-
358
- function compactCommunicationSnapshot(
359
- snapshot: ActorRooms.ActorCommunicationSnapshot | undefined,
360
- ): string {
361
- if (!snapshot) return "\n(no communication snapshot)";
362
- return `\nself=${snapshot.self} root=${snapshot.root} rooms=${snapshot.rooms.length} updated_at=${snapshot.updated_at}`;
363
- }
364
-
365
- function compactInboxMessages(
366
- messages: Array<Record<string, unknown>>,
367
- emptyLabel: string,
368
- ): string {
369
- if (messages.length === 0) return `\n(no ${emptyLabel} messages)`;
370
- return `\n${messages
371
- .map((message) =>
372
- [
373
- ...(message.id ? [`id=${String(message.id)}`] : []),
374
- `status=${String(message.status ?? "")}`,
375
- `type=${String(message.type ?? "")}`,
376
- `from=${String(message.from ?? "")}`,
377
- `to=${String(message.to ?? "")}`,
378
- ...(message.queued_at
379
- ? [`queued_at=${String(message.queued_at)}`]
380
- : []),
381
- ...(message.sent_at ? [`sent_at=${String(message.sent_at)}`] : []),
382
- ...(message.claimed_at
383
- ? [`claimed_at=${String(message.claimed_at)}`]
384
- : []),
385
- ...(message.handled_at
386
- ? [`handled_at=${String(message.handled_at)}`]
387
- : []),
388
- ...(message.failed_at
389
- ? [`failed_at=${String(message.failed_at)}`]
390
- : []),
391
- ].join(" "),
392
- )
393
- .join("\n")}`;
394
- }
395
-
396
- function compactBranchInbox(messages: Array<Record<string, unknown>>): string {
397
- return compactInboxMessages(messages, "branch inbox");
398
- }
399
-
400
- function normalizeMailboxEntry(
401
- value: unknown,
402
- ): Record<string, unknown> | undefined {
403
- if (typeof value === "string") return { type: value };
404
- const record = asRecord(value);
405
- if (typeof record.type !== "string" || !record.type.trim()) return undefined;
406
- return {
407
- type: record.type.trim(),
408
- ...(record.body_schema !== undefined
409
- ? { body_schema: record.body_schema }
410
- : {}),
411
- ...(record.ack !== undefined ? { ack: record.ack } : {}),
412
- ...(typeof record.idempotency === "string"
413
- ? { idempotency: record.idempotency }
414
- : {}),
415
- ...(typeof record.level === "string" ? { level: record.level } : {}),
416
- ...(record.requires_response === true ? { requires_response: true } : {}),
417
- ...(typeof record.summary === "string" ? { summary: record.summary } : {}),
418
- };
419
- }
420
-
421
- function normalizeMailboxContracts(
422
- mailbox: Record<string, unknown>,
423
- ): Record<string, unknown[]> {
424
- const accepts = Array.isArray(mailbox.accepts)
425
- ? mailbox.accepts
426
- .map(normalizeMailboxEntry)
427
- .filter((entry): entry is Record<string, unknown> => Boolean(entry))
428
- : [];
429
- const emits = Array.isArray(mailbox.emits)
430
- ? mailbox.emits
431
- .map(normalizeMailboxEntry)
432
- .filter((entry): entry is Record<string, unknown> => Boolean(entry))
433
- : [];
434
- return { accepts, emits };
435
- }
436
-
437
- function mailboxTypes(entries: unknown[]): string[] {
438
- return entries.flatMap((entry) =>
439
- typeof asRecord(entry).type === "string"
440
- ? [String(asRecord(entry).type)]
441
- : [],
442
- );
443
- }
444
-
445
- function compactRunMailbox(
446
- run: string,
447
- mailbox: Record<string, unknown>,
448
- messages: Array<Record<string, unknown>>,
449
- ): string {
450
- const normalized = normalizeMailboxContracts(mailbox);
451
- return `\nrun=${run} accepts=${mailboxTypes(normalized.accepts).join(",")} emits=${mailboxTypes(normalized.emits).join(",")}${compactInboxMessages(messages, "run inbox")}`;
452
- }
453
-
454
- function compactArtifactPath(value: unknown): string {
455
- if (typeof value === "string") return value;
456
- const record = asRecord(value);
457
- return String(record.path ?? "<missing>");
458
- }
459
-
460
- function artifactNextActions(run: unknown, artifacts: Record<string, unknown>): string[] {
461
- const id = String(run ?? "").trim();
462
- if (!id || Object.keys(artifacts).length === 0) return [];
463
- return [
464
- `inspect target=run:${id} view=artifacts verbose=true`,
465
- `inspect target=run:${id} view=messages`,
466
- ];
467
- }
468
-
469
- function compactActorFiles(status: Record<string, unknown>): string {
470
- const run = String(status.run ?? "<unknown>");
471
- const artifacts = asRecord(status.artifacts);
472
- const files = [
473
- status.stdoutLog,
474
- status.stderrLog,
475
- status.eventsFile,
476
- status.outboxFile,
477
- status.state_dir
478
- ? `${String(status.state_dir)}/communication.json`
479
- : undefined,
480
- status.state_dir ? `${String(status.state_dir)}/result.json` : undefined,
481
- ].filter((file): file is string => typeof file === "string");
482
- const artifactText = Object.keys(artifacts).length
483
- ? ` artifacts=${Object.entries(artifacts)
484
- .map(([key, value]) => `${key}:${compactArtifactPath(value)}`)
485
- .join(",")}`
486
- : "";
487
- const nextActions = artifactNextActions(run, artifacts);
488
- const nextText = nextActions.length
489
- ? ` next=${nextActions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`
490
- : "";
491
- return `\nrun=${run}${artifactText}${files.length ? ` files=${files.join(",")}` : ""}${nextText}`;
492
- }
493
-
494
- function summarizeOtherSessions(
495
- currentSession: string,
496
- allRuns: Array<Record<string, unknown>>,
497
- ): Record<string, unknown> {
498
- const otherRuns = allRuns.filter(
499
- (run) => run.ownerId && run.ownerId !== currentSession,
500
- );
501
- return {
502
- other_runs: otherRuns.length,
503
- other_sessions: new Set(otherRuns.map((run) => run.ownerId)).size,
504
- };
505
- }
506
-
507
- function compactSessionRuns(
508
- session: string,
509
- runs: Array<Record<string, unknown>>,
510
- summary: Record<string, unknown> = {},
511
- ): string {
512
- const suffix = [
513
- summary.other_sessions !== undefined
514
- ? `other_sessions=${String(summary.other_sessions)}`
515
- : "",
516
- summary.other_runs !== undefined
517
- ? `other_runs=${String(summary.other_runs)}`
518
- : "",
519
- ]
520
- .filter(Boolean)
521
- .join(" ");
522
- if (runs.length === 0)
523
- return `\nsession=${session} runs=0${suffix ? ` ${suffix}` : ""}`;
524
- return `\nsession=${session} runs=${runs.length}${suffix ? ` ${suffix}` : ""}\n${runs
525
- .map((run) => {
526
- const tokens = [
527
- `run=${String(run.run ?? "")}`,
528
- `status=${String(run.status ?? "")}`,
529
- ];
530
- if (run.recipe) tokens.push(`recipe=${String(run.recipe)}`);
531
- if (run.retire_when)
532
- tokens.push(`retire_when=${String(run.retire_when)}`);
533
- return tokens.join(" ");
534
- })
535
- .join("\n")}`;
536
- }
537
-
538
- function getPiActorsRuntimeStatus(): Record<string, unknown> {
539
- const packagedRecipeRoot = Paths.getPackagedRecipeRoot();
540
- const packageRoot = dirname(packagedRecipeRoot);
541
- const packageJsonPath = join(packageRoot, "package.json");
542
- const packageJson = existsSync(packageJsonPath)
543
- ? (JSON.parse(readFileSync(packageJsonPath, "utf8")) as Record<
544
- string,
545
- unknown
546
- >)
547
- : {};
548
- let git_commit: string | undefined;
549
- try {
550
- git_commit = execFileSync(
551
- "git",
552
- ["-C", packageRoot, "rev-parse", "--short", "HEAD"],
553
- {
554
- encoding: "utf8",
555
- stdio: ["ignore", "pipe", "ignore"],
556
- },
557
- ).trim();
558
- } catch {
559
- git_commit = undefined;
560
- }
561
- const entrypoint = new URL(import.meta.url).pathname;
562
- return {
563
- entrypoint,
564
- git_commit,
565
- mode: entrypoint.includes("/dist/") ? "dist" : "source",
566
- package_name: packageJson.name ?? "@llblab/pi-actors",
567
- package_root: packageRoot,
568
- recipe_root: Paths.getRecipeRoot(),
569
- packaged_recipe_root: packagedRecipeRoot,
570
- version: packageJson.version ?? "unknown",
571
- };
572
- }
573
-
574
- function compactPiActorsRuntimeStatus(status: Record<string, unknown>): string {
575
- return `\npi-actors version=${String(status.version)} mode=${String(status.mode)} path=${String(status.package_root)} entrypoint=${String(status.entrypoint)}${status.git_commit ? ` git=${String(status.git_commit)}` : ""}`;
576
- }
577
-
578
- function compactToolActor(name: string, tool: Record<string, unknown>): string {
579
- const parameters = asRecord(tool.parameters);
580
- const required = Array.isArray(parameters.required)
581
- ? parameters.required.join(",")
582
- : "";
583
- const properties = asRecord(parameters.properties);
584
- return `\ntool=${name} description=${String(tool.description ?? "").replaceAll(/\s+/g, "_")} args=${Object.keys(properties).join(",")} required=${required}`;
585
- }
586
-
587
- function compactRecipeImports(summary: Record<string, unknown>): string {
588
- const active = Array.isArray(summary.active)
589
- ? (summary.active as Array<Record<string, unknown>>)
590
- : [];
591
- const lines = active.flatMap((entry) => {
592
- const imports = asRecord(entry.imports);
593
- return Object.entries(imports).map(([alias, value]) => {
594
- const binding =
595
- typeof value === "string" ? { from: value } : asRecord(value);
596
- return `recipe=${String(entry.id ?? "<unknown>")} alias=${alias} from=${String(binding.from ?? value)}`;
597
- });
598
- });
599
- return lines.length ? `\n${lines.join("\n")}` : "\n(no recipe imports)";
600
- }
601
-
602
- function compactRecipeDoctor(summary: Record<string, unknown>): string {
603
- const details = Array.isArray(summary.diagnostic_details)
604
- ? (summary.diagnostic_details as Array<Record<string, unknown>>)
605
- : [];
606
- const remediations = Array.isArray(summary.remediations)
607
- ? (summary.remediations as Array<Record<string, unknown>>)
608
- : [];
609
- const recommendations = Array.isArray(summary.recommendations)
610
- ? (summary.recommendations as Array<Record<string, unknown>>)
611
- : [];
612
- const counts = { error: 0, info: 0, warning: 0 };
613
- for (const detail of details) {
614
- const severity = String(detail.severity ?? "info");
615
- if (severity === "error" || severity === "warning" || severity === "info")
616
- counts[severity] += 1;
617
- }
618
- const topAction = asRecord(summary.top_action);
619
- const lines = [
620
- `recipes doctor errors=${counts.error} warnings=${counts.warning} info=${counts.info} actions=${remediations.length} recommendations=${recommendations.length}`,
621
- ];
622
- if (Object.keys(topAction).length > 0) {
623
- const action = compactPreview(
624
- topAction.action,
625
- Limits.DOCTOR_ACTION_PREVIEW_CHARS,
626
- );
627
- lines.push(
628
- `top severity=${String(topAction.severity ?? "info")} kind=${String(topAction.kind ?? "inspect")} id=${String(topAction.id ?? "root")} action=${action ?? "inspect"}`,
629
- );
630
- }
631
- for (const item of remediations.slice(0, 8)) {
632
- const action = compactPreview(
633
- item.action,
634
- Limits.DOCTOR_ACTION_PREVIEW_CHARS,
635
- );
636
- const blocked = item.blocked_fallback
637
- ? ` blocked=${compactPreview(item.blocked_fallback, Limits.DOCTOR_ACTION_PREVIEW_CHARS)}`
638
- : "";
639
- lines.push(
640
- `${String(item.severity ?? "info")} kind=${String(item.kind ?? "inspect")} id=${String(item.id ?? "root")}${blocked} action=${action ?? "inspect"}`,
641
- );
642
- }
643
- const nextActions = Array.isArray(summary.next_actions)
644
- ? (summary.next_actions as string[])
645
- : [];
646
- if (nextActions.length > 0) lines[0] = `${lines[0]}${compactNextActions(nextActions)}`;
647
- return `\n${lines.join("\n")}`;
648
- }
649
-
650
- function recipeRegistryNextActions(summary: Record<string, unknown>, view: string): string[] {
651
- const actions: string[] = [];
652
- const drafts = Array.isArray(summary.drafts)
653
- ? (summary.drafts as Array<Record<string, unknown>>)
654
- : [];
655
- const invalid = Array.isArray(summary.invalid) ? summary.invalid.length : 0;
656
- const diagnostics = Array.isArray(summary.diagnostics)
657
- ? summary.diagnostics.length
658
- : 0;
659
- const topAction = asRecord(summary.top_action);
660
- if (view !== "doctor" && (invalid > 0 || diagnostics > 0)) {
661
- actions.push("inspect target=recipes view=doctor");
662
- }
663
- if (view === "doctor" && typeof topAction.action === "string") {
664
- actions.push(String(topAction.action));
665
- }
666
- if (drafts.length > 0) {
667
- actions.push("inspect target=recipes view=summary verbose=true");
668
- const firstPath = typeof drafts[0]?.path === "string" ? drafts[0].path : undefined;
669
- if (firstPath) actions.push(`spawn file=${firstPath}`);
670
- }
671
- return [...new Set(actions)].slice(0, 4);
672
- }
673
-
674
- function compactNextActions(actions: string[]): string {
675
- return actions.length
676
- ? ` next=${actions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`
677
- : "";
678
- }
679
-
680
- function compactRecipeRegistry(summary: Record<string, unknown>): string {
681
- const active = Array.isArray(summary.active) ? summary.active.length : 0;
682
- const shadowed = Array.isArray(summary.shadowed)
683
- ? summary.shadowed.length
684
- : 0;
685
- const invalid = Array.isArray(summary.invalid) ? summary.invalid.length : 0;
686
- const disabled = Array.isArray(summary.disabled)
687
- ? summary.disabled.length
688
- : 0;
689
- const diagnostics = Array.isArray(summary.diagnostics)
690
- ? summary.diagnostics.length
691
- : 0;
692
- const drafts = Array.isArray(summary.drafts) ? summary.drafts.length : 0;
693
- const recommendations = Array.isArray(summary.recommendations)
694
- ? summary.recommendations.length
695
- : 0;
696
- const nextActions = Array.isArray(summary.next_actions)
697
- ? (summary.next_actions as string[])
698
- : [];
699
- return `\nrecipes active=${active} drafts=${drafts} shadowed=${shadowed} invalid=${invalid} disabled=${disabled} recommendations=${recommendations} diagnostics=${diagnostics}${compactNextActions(nextActions)}`;
700
- }
701
-
702
- function actorMessageNextActions(
703
- message: ActorMessages.ActorMessage,
704
- result: Record<string, unknown>,
705
- ): string[] {
706
- const actions: string[] = [];
707
- const address = ActorMessages.parseActorAddress(message.to);
708
- if (result.delivery_error || result.sent === false) {
709
- if (address.kind === "run" && address.value) {
710
- actions.push(`inspect target=run:${address.value} view=status`);
711
- actions.push(`inspect target=run:${address.value} view=mailbox`);
712
- } else if (address.kind === "branch" && address.value) {
713
- actions.push(`inspect target=branch:${address.value}/${address.branch ?? "main"} view=mailbox`);
714
- actions.push(`inspect target=run:${address.value} view=status`);
715
- }
716
- }
717
- if (result.queued === true) {
718
- if (address.kind === "branch" && address.value) {
719
- actions.push(`inspect target=branch:${address.value}/${address.branch ?? "main"} view=mailbox`);
720
- } else if (address.kind === "run" && address.value) {
721
- actions.push(`inspect target=run:${address.value} view=mailbox`);
722
- }
723
- }
724
- return [...new Set(actions)].slice(0, 3);
725
- }
726
-
727
- function compactActorMessageResult(
728
- message: ActorMessages.ActorMessage,
729
- result: Record<string, unknown>,
730
- ): string {
731
- const tokens = [
732
- `to=${message.to}`,
733
- `type=${message.type}`,
734
- `message=${result.sent === true || result.stopped === true ? "sent" : "not_sent"}`,
735
- ];
736
- if (result.bytes !== undefined) tokens.push(`bytes=${String(result.bytes)}`);
737
- if (result.queued === true) tokens.push("queued=true");
738
- if (result.control) tokens.push(`control=${String(result.control)}`);
739
- if (result.outbox) tokens.push(`messages=${String(result.outbox)}`);
740
- if (result.message_count !== undefined)
741
- tokens.push(`messages=${String(result.message_count)}`);
742
- if (result.roster_count !== undefined)
743
- tokens.push(`roster=${String(result.roster_count)}`);
744
- if (result.room) tokens.push(`room=${String(result.room)}`);
745
- if (result.tool) tokens.push(`tool=${String(result.tool)}`);
746
- if (result.stopped === true) tokens.push("stopped=true");
747
- if (result.signal) tokens.push(`signal=${String(result.signal)}`);
748
- if (result.invoked === true) tokens.push("invoked=true");
749
- if (result.delivery_error) {
750
- tokens.push(`delivery_error=${compactPreview(result.delivery_error, 96)}`);
751
- }
752
- const nextActions = Array.isArray(result.next_actions)
753
- ? (result.next_actions as string[])
754
- : actorMessageNextActions(message, result);
755
- if (nextActions.length > 0)
756
- tokens.push(`next=${nextActions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`);
757
- return `\n${tokens.join(" ")}`;
758
- }
759
-
760
- function maybeJsonText(
761
- value: unknown,
762
- verbose: boolean | undefined,
763
- compact: string,
764
- ): string {
765
- return verbose ? jsonText(value) : compact;
766
- }
767
-
768
- export function createRegisterToolDefinition<TContext>(
769
- deps: RegisterToolRuntimeDeps<TContext>,
770
- ) {
771
- return {
772
- name: "register_tool",
773
- label: "Register Tool",
774
- description: Prompts.REGISTER_TOOL_DESCRIPTION,
775
- promptSnippet: Prompts.REGISTER_TOOL_PROMPT_SNIPPET,
776
- promptGuidelines: Prompts.REGISTER_TOOL_GUIDELINES,
777
- parameters: objectSchema(
778
- {
779
- args: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.args),
780
- async: booleanSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.async),
781
- description: stringSchema(
782
- Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.description,
783
- ),
784
- name: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.name),
785
- state_dir: stringSchema(
786
- Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.state_dir,
787
- ),
788
- template: unionSchema([
789
- stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.template),
790
- looseObjectSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.template),
791
- arraySchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.templateArray),
792
- nullSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.templateNull),
793
- ]),
794
- update: booleanSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.update),
795
- values: looseObjectSchema(
796
- Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.values,
797
- ),
798
- },
799
- [],
800
- ),
801
- execute: async (
802
- _toolCallId: string,
803
- params: unknown,
804
- _signal: AbortSignal | undefined,
805
- _onUpdate: unknown,
806
- ctx: TContext,
807
- ) => Registry.executeRegisterTool(params, ctx, deps),
808
- };
809
- }
810
-
811
- export interface AsyncRunToolContext {
812
- cwd: string;
813
- sessionManager?: { getSessionId?: () => string };
814
- }
815
-
816
- function getRunOwnerId(ctx: AsyncRunToolContext): string | undefined {
817
- return ctx.sessionManager?.getSessionId?.();
818
- }
819
-
820
- function messageBodyToRunLine(message: ActorMessages.ActorMessage): string {
821
- if (message.type !== "run.message") return JSON.stringify(message);
822
- if (typeof message.body === "string") return message.body;
823
- if (message.body === undefined) return message.type;
824
- return JSON.stringify(message.body);
825
- }
826
-
827
- function messageBodyToToolParams(
828
- message: ActorMessages.ActorMessage,
829
- ): Record<string, unknown> {
830
- if (
831
- message.body &&
832
- typeof message.body === "object" &&
833
- !Array.isArray(message.body)
834
- ) {
835
- return message.body as Record<string, unknown>;
836
- }
837
- if (message.body === undefined) return {};
838
- return { input: message.body };
839
- }
840
-
841
- function formatToolActorFailure(
842
- tool: string,
843
- message: ActorMessages.ActorMessage,
844
- params: Record<string, unknown>,
845
- error: unknown,
846
- ): Error {
847
- const original = error instanceof Error ? error.message : String(error);
848
- const paramsPreview = compactPreview(params, 240) ?? "{}";
849
- return Object.assign(
850
- new Error(
851
- `tool actor ${tool} failed for message type ${message.type}: ${original}; params=${paramsPreview}`,
852
- ),
853
- {
854
- message_type: message.type,
855
- original_error: original,
856
- params_preview: paramsPreview,
857
- tool,
858
- },
859
- );
860
- }
861
-
862
- function shadowedRecipeLaunchDiagnostic(
863
- recipe: unknown,
864
- ): Record<string, unknown> | undefined {
865
- if (
866
- typeof recipe !== "string" ||
867
- recipe.includes("/") ||
868
- recipe.includes("~")
869
- )
870
- return undefined;
871
- const discovery = RecipeDiscovery.discoverRecipeSources([
872
- { root: Paths.getRecipeRoot(), defaultTool: true, mutableUsage: true },
873
- { root: Paths.getPackagedRecipeRoot(), defaultTool: false },
874
- ]);
875
- return RecipeDiscovery.getShadowedLaunchDiagnostic(discovery, recipe);
876
- }
877
-
878
- function draftRecipeName(run: string): string {
879
- return `${run.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "spawn"}.json`;
880
- }
881
-
882
- function draftRecipeDefaults(
883
- values: Record<string, unknown>,
884
- ): Record<string, unknown> | undefined {
885
- const ignored = new Set([
886
- "actor_address",
887
- "communication_file",
888
- "default_room",
889
- "run_id",
890
- "state_dir",
891
- ]);
892
- const defaults = Object.fromEntries(
893
- Object.entries(values).filter(([key]) => !ignored.has(key)),
894
- );
895
- return Object.keys(defaults).length > 0 ? defaults : undefined;
896
- }
897
-
898
- function writeSpawnDraftRecipe(
899
- input: Record<string, unknown>,
900
- meta: AsyncRuns.AsyncRunMeta,
901
- ): string | undefined {
902
- if (
903
- process.env.NODE_TEST_CONTEXT &&
904
- process.env.PI_ACTORS_ENABLE_SPAWN_DRAFTS_IN_TEST !== "1"
905
- )
906
- return undefined;
907
- if (
908
- input.template === undefined ||
909
- input.file !== undefined ||
910
- input.recipe !== undefined
911
- )
912
- return undefined;
913
- const root = Paths.getRecipeDraftRoot();
914
- mkdirSync(root, { recursive: true });
915
- const path = join(root, draftRecipeName(String(meta.run)));
916
- const defaults = draftRecipeDefaults(meta.values);
917
- const recipe = {
918
- async: true,
919
- description: `Draft recipe captured from spawn run ${String(meta.run)}`,
920
- ...(meta.artifacts ? { artifacts: meta.artifacts } : {}),
921
- ...(defaults ? { defaults } : {}),
922
- template: input.template,
923
- };
924
- writeFileSync(path, `${JSON.stringify(recipe, null, 2)}\n`, { flag: "wx" });
925
- return path;
926
- }
927
-
928
- function enhanceSpawnRecipeError(error: unknown, recipe: unknown): Error {
929
- const diagnostic = shadowedRecipeLaunchDiagnostic(recipe);
930
- if (!diagnostic)
931
- return error instanceof Error ? error : new Error(String(error));
932
- const original = error instanceof Error ? error.message : String(error);
933
- return Object.assign(
934
- new Error(
935
- `${original} reason=${diagnostic.reason} active_path=${diagnostic.active_path} blocked_fallback=${diagnostic.blocked_fallback} hint=${diagnostic.hint}`,
936
- ),
937
- {
938
- ...diagnostic,
939
- original_error: original,
940
- },
941
- );
942
- }
943
-
944
- function runIdFromActorAddress(
945
- address: string | undefined,
946
- ): string | undefined {
947
- if (!address) return undefined;
948
- const parsed = ActorMessages.parseActorAddress(address);
949
- if (parsed.kind !== "run" || !parsed.value) {
950
- throw new Error(`Expected run:<id> actor address, received: ${address}`);
951
- }
952
- return parsed.value;
953
- }
954
-
955
- function assertMessageSenderBelongsToRun(
956
- message: ActorMessages.ActorMessage,
957
- run: string,
958
- routeLabel: string,
959
- ): void {
960
- if (!message.from) {
961
- throw new Error(`message to ${message.to} requires from=<actor address>.`);
962
- }
963
- const sender = ActorMessages.parseActorAddress(message.from);
964
- if (
965
- (sender.kind !== "run" && sender.kind !== "branch") ||
966
- sender.value !== run
967
- ) {
968
- throw new Error(
969
- `message to ${routeLabel} requires from=run:${run} or branch:${run}/<branch>; got ${message.from}.`,
970
- );
971
- }
972
- }
973
-
974
- async function routeBranchEnvelope(
975
- stateDir: string,
976
- runId: string,
977
- recipient: string,
978
- message: ActorMessages.ActorMessage,
979
- _options: { source: "direct" | "room-multicast" },
980
- ): Promise<Record<string, unknown>> {
981
- const branchMessage = { ...message, to: recipient };
982
- ActorRooms.appendBranchInboxMessage(
983
- stateDir,
984
- runId,
985
- recipient,
986
- branchMessage,
987
- );
988
- try {
989
- return await AsyncRuns.sendRunMessage(runId, JSON.stringify(branchMessage));
990
- } catch (error) {
991
- const record =
992
- error && typeof error === "object"
993
- ? (error as Record<string, unknown>)
994
- : {};
995
- if (record.queued === true) {
996
- return {
997
- control_path: record.control_path,
998
- control_type: record.control_type,
999
- delivery_error:
1000
- record.delivery_error ??
1001
- (error instanceof Error ? error.message : String(error)),
1002
- inbox_id: record.inbox_id,
1003
- queued: true,
1004
- run: runId,
1005
- sent: false,
1006
- state_dir: stateDir,
1007
- };
1008
- }
1009
- throw error;
1010
- }
1011
- }
1012
-
1013
- function getRoomMulticastRecipients(
1014
- message: ActorMessages.ActorMessage,
1015
- run: string,
1016
- ): string[] {
1017
- const raw = message.metadata?.recipients;
1018
- if (raw === undefined) return [];
1019
- if (!Array.isArray(raw)) {
1020
- throw new Error("room multicast metadata.recipients must be an array.");
1021
- }
1022
- return raw.map((recipient) => {
1023
- if (typeof recipient !== "string") {
1024
- throw new Error("room multicast recipients must be actor addresses.");
1025
- }
1026
- const parsed = ActorMessages.parseActorAddress(recipient);
1027
- if (parsed.kind !== "branch" || parsed.value !== run) {
1028
- throw new Error(
1029
- `room multicast recipient must be branch:${run}/<branch>; got ${recipient}.`,
1030
- );
1031
- }
1032
- return ActorMessages.formatActorAddress(parsed);
1033
- });
1034
- }
1035
-
1036
- export function createSpawnToolDefinition<
1037
- TContext extends AsyncRunToolContext,
1038
- >(): any {
1039
- return {
1040
- name: "spawn",
1041
- label: "Spawn",
1042
- description:
1043
- "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.",
1044
- parameters: objectSchema(
1045
- {
1046
- artifacts: looseObjectSchema(
1047
- "Optional named artifact paths for the spawned actor.",
1048
- ),
1049
- as: stringSchema(
1050
- "Optional actor address for the spawned run, e.g. run:<id>.",
1051
- ),
1052
- file: stringSchema(
1053
- "Optional template recipe JSON file. Bare names resolve under ~/.pi/agent/recipes.",
1054
- ),
1055
- recipe: stringSchema(
1056
- "Alias for file; template recipe JSON file/name to spawn.",
1057
- ),
1058
- state_dir: stringSchema("Optional explicit run state directory."),
1059
- template: unionSchema([
1060
- stringSchema("Inline command template string"),
1061
- arraySchema("Inline command-template sequence or parallel tree"),
1062
- looseObjectSchema(
1063
- "Inline command-template object with flags such as parallel, repeat, retry, failure, and nested template.",
1064
- ),
1065
- ]),
1066
- values: looseObjectSchema(
1067
- "Runtime placeholder values passed to the actor.",
1068
- ),
1069
- verbose: booleanSchema("Return full JSON instead of compact text."),
1070
- },
1071
- [],
1072
- ),
1073
- async execute(
1074
- _toolCallId: string,
1075
- params: unknown,
1076
- _signal: AbortSignal | undefined,
1077
- _onUpdate: unknown,
1078
- ctx: TContext,
1079
- ) {
1080
- const input = asRecord(params);
1081
- const runId = runIdFromActorAddress(
1082
- typeof input.as === "string" ? input.as : undefined,
1083
- );
1084
- const recipe =
1085
- typeof input.file === "string"
1086
- ? input.file
1087
- : typeof input.recipe === "string"
1088
- ? input.recipe
1089
- : undefined;
1090
- let meta: AsyncRuns.AsyncRunMeta;
1091
- try {
1092
- meta = AsyncRuns.startRun(
1093
- {
1094
- file: recipe,
1095
- launch_source: "spawn",
1096
- ownerId: getRunOwnerId(ctx),
1097
- run_id: runId,
1098
- state_dir:
1099
- typeof input.state_dir === "string" ? input.state_dir : undefined,
1100
- ...(input.template !== undefined
1101
- ? {
1102
- template:
1103
- input.template as AsyncRuns.AsyncRunStartParams["template"],
1104
- }
1105
- : {}),
1106
- values: asRecord(input.values),
1107
- ...(input.artifacts &&
1108
- typeof input.artifacts === "object" &&
1109
- !Array.isArray(input.artifacts)
1110
- ? {
1111
- artifacts: input.artifacts as Record<
1112
- string,
1113
- AsyncRuns.RunArtifactDeclaration
1114
- >,
1115
- }
1116
- : {}),
1117
- },
1118
- ctx.cwd,
1119
- );
1120
- } catch (error) {
1121
- throw enhanceSpawnRecipeError(error, recipe);
1122
- }
1123
- const draftRecipe = writeSpawnDraftRecipe(input, meta);
1124
- const nextActions = actorRunNextActions(meta.run);
1125
- const details = {
1126
- ...meta,
1127
- ...(draftRecipe ? { draft_recipe: draftRecipe } : {}),
1128
- next_actions: nextActions,
1129
- };
1130
- ActorRooms.ensureDefaultRoom(meta.state_dir, String(meta.run));
1131
- ActorRooms.writeCommunicationSnapshot(meta.state_dir, String(meta.run));
1132
- return {
1133
- content: [
1134
- {
1135
- type: "text" as const,
1136
- text: maybeJsonText(
1137
- details,
1138
- input.verbose === true,
1139
- compactAsyncRunStatus(details),
1140
- ),
1141
- },
1142
- ],
1143
- details,
1144
- };
1145
- },
1146
- };
1147
- }
1148
-
1149
- export interface InspectToolDeps<TContext = unknown> {
1150
- getTool?: (name: string) => any | undefined;
1151
- packagedRecipeRoot?: string;
1152
- recipeRoot?: string;
1153
- }
1154
-
1155
- function getContextSessionId(ctx: unknown): string | undefined {
1156
- return (
1157
- ctx as AsyncRunToolContext | undefined
1158
- )?.sessionManager?.getSessionId?.();
1159
- }
1160
-
1161
- function requireContextSessionId(ctx: unknown, actor: string): string {
1162
- const sessionId = getContextSessionId(ctx);
1163
- if (!sessionId) {
1164
- throw new Error(
1165
- `${actor} requires a current coordinator session; use session:<id> or session:all for explicit session inventory.`,
1166
- );
1167
- }
1168
- return sessionId;
1169
- }
1170
-
1171
- function sessionMismatchError(input: {
1172
- currentSession?: string;
1173
- expectedSession?: string;
1174
- run?: string;
1175
- target?: string;
1176
- }): Error {
1177
- const ownerSession = input.expectedSession ?? "none";
1178
- const currentSession = input.currentSession ?? "none";
1179
- const actor = input.run ? `run:${input.run}` : (input.target ?? "session");
1180
- const hintTarget = input.expectedSession
1181
- ? `session:${input.expectedSession}`
1182
- : "session:all";
1183
- return Object.assign(
1184
- new Error(
1185
- `${actor} reason=session_mismatch owner_session=${ownerSession} current_session=${currentSession} hint=inspect_session:${input.expectedSession ?? "all"}`,
1186
- ),
1187
- {
1188
- current_session: input.currentSession,
1189
- hint: `inspect target=${hintTarget} view=status`,
1190
- owner_session: input.expectedSession,
1191
- reason: "session_mismatch",
1192
- run: input.run,
1193
- target: input.target,
1194
- },
1195
- );
1196
- }
1197
-
1198
- function assertRunAccessibleToContext(
1199
- runId: string,
1200
- ctx: unknown,
1201
- ): Record<string, unknown> {
1202
- const status = AsyncRuns.getRunStatus(runId);
1203
- const sessionId = getContextSessionId(ctx);
1204
- if (sessionId && status.ownerId && status.ownerId !== sessionId) {
1205
- throw sessionMismatchError({
1206
- currentSession: sessionId,
1207
- expectedSession: String(status.ownerId),
1208
- run: runId,
1209
- });
1210
- }
1211
- return status;
1212
- }
1213
-
1214
- function assertRunExistsForActorMessage(
1215
- runId: string,
1216
- ): Record<string, unknown> {
1217
- return AsyncRuns.getRunStatus(runId);
1218
- }
1219
-
1220
- export function createInspectToolDefinition<TContext = unknown>(
1221
- deps: InspectToolDeps<TContext> = {},
1222
- ): any {
1223
- return {
1224
- name: "inspect",
1225
- label: "Inspect",
1226
- description:
1227
- "Intentionally inspect actors at decision points, after follow-ups, or during diagnosis instead of polling. Core targets are run:<id> and tool:<name>; advanced targets include branch:<run>/<branch>, room:<run>, coordinator, session:<id>, and session:all.",
1228
- parameters: objectSchema(
1229
- {
1230
- lines: stringSchema("Line count for tail/messages views. Default 40."),
1231
- status: stringSchema(
1232
- "Optional session run filter: all, running, active, terminal, done, failed, cancelled, killed, or exited.",
1233
- ),
1234
- target: stringSchema(
1235
- "Actor address to inspect, e.g. run:<id> or tool:<name>; advanced: branch:<run>/<branch>, room:<run>, coordinator, session:<id>, session:all.",
1236
- ),
1237
- verbose: booleanSchema(
1238
- "Return full JSON instead of compact text where available.",
1239
- ),
1240
- view: stringSchema(
1241
- "Inspection view. Core run views: status, tail, messages, artifacts, files, mailbox. Advanced views include communication, roster, and contacts.",
1242
- ),
1243
- },
1244
- ["target", "view"],
1245
- ),
1246
- async execute(
1247
- _toolCallId: string,
1248
- params: unknown,
1249
- _signal: AbortSignal | undefined,
1250
- _onUpdate: unknown,
1251
- ctx: TContext,
1252
- ) {
1253
- const input = asRecord(params);
1254
- const target = String(input.target ?? "");
1255
- const view = String(input.view ?? "");
1256
- if (target === "recipes" || target === "recipe-registry") {
1257
- if (
1258
- view !== "status" &&
1259
- view !== "summary" &&
1260
- view !== "doctor" &&
1261
- view !== "imports"
1262
- ) {
1263
- throw new Error(
1264
- "inspect recipes supports view=status, view=summary, view=doctor, or view=imports.",
1265
- );
1266
- }
1267
- const discovered = RecipeDiscovery.discoverRecipeSources([
1268
- {
1269
- root: deps.recipeRoot ?? Paths.getRecipeRoot(),
1270
- defaultTool: true,
1271
- mutableUsage: true,
1272
- },
1273
- { root: deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot() },
1274
- ]);
1275
- const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
1276
- const summaryBase = {
1277
- ...RecipeDiscovery.summarizeDiscovery(discovered),
1278
- drafts: RecipeDiscovery.listDraftRecipes(join(recipeRoot, "drafts")),
1279
- };
1280
- const summary = {
1281
- ...summaryBase,
1282
- next_actions: recipeRegistryNextActions(summaryBase, view),
1283
- };
1284
- return {
1285
- content: [
1286
- {
1287
- type: "text" as const,
1288
- text: maybeJsonText(
1289
- summary,
1290
- input.verbose === true,
1291
- view === "doctor"
1292
- ? compactRecipeDoctor(summary)
1293
- : view === "imports"
1294
- ? compactRecipeImports(summary)
1295
- : compactRecipeRegistry(summary),
1296
- ),
1297
- },
1298
- ],
1299
- details: summary,
1300
- };
1301
- }
1302
- const address = ActorMessages.parseActorAddress(target);
1303
- if (address.kind === "coordinator") {
1304
- if (view !== "status" && view !== "runs") {
1305
- throw new Error(
1306
- "inspect coordinator supports view=status or view=runs.",
1307
- );
1308
- }
1309
- const session = requireContextSessionId(ctx, "inspect coordinator");
1310
- const allRuns = AsyncRuns.listRuns(
1311
- undefined,
1312
- typeof input.status === "string" ? input.status : undefined,
1313
- ).map((run) => AsyncRuns.getRunStatus(String(run.state_dir)));
1314
- const runs = allRuns.filter((run) => run.ownerId === session);
1315
- const sessionSummary = summarizeOtherSessions(session, allRuns);
1316
- return {
1317
- content: [
1318
- {
1319
- type: "text" as const,
1320
- text: maybeJsonText(
1321
- { session, runs, ...sessionSummary },
1322
- input.verbose === true,
1323
- compactSessionRuns(session, runs, sessionSummary),
1324
- ),
1325
- },
1326
- ],
1327
- details: { session, runs, ...sessionSummary },
1328
- };
1329
- }
1330
- if (address.kind === "session") {
1331
- if (view !== "status" && view !== "runs") {
1332
- throw new Error(
1333
- "inspect session:<id> supports view=status or view=runs.",
1334
- );
1335
- }
1336
- const allRuns = AsyncRuns.listRuns(
1337
- undefined,
1338
- typeof input.status === "string" ? input.status : undefined,
1339
- ).map((run) => AsyncRuns.getRunStatus(String(run.state_dir)));
1340
- const runs = allRuns.filter(
1341
- (run) => address.value === "all" || run.ownerId === address.value,
1342
- );
1343
- const sessionSummary =
1344
- address.value === "all"
1345
- ? {}
1346
- : summarizeOtherSessions(address.value || "", allRuns);
1347
- return {
1348
- content: [
1349
- {
1350
- type: "text" as const,
1351
- text: maybeJsonText(
1352
- { session: address.value, runs, ...sessionSummary },
1353
- input.verbose === true,
1354
- compactSessionRuns(address.value || "", runs, sessionSummary),
1355
- ),
1356
- },
1357
- ],
1358
- details: { session: address.value, runs, ...sessionSummary },
1359
- };
1360
- }
1361
- if (address.kind === "tool" && address.value) {
1362
- if (address.value === "pi-actors") {
1363
- if (view !== "status") {
1364
- throw new Error("inspect tool:pi-actors supports view=status.");
1365
- }
1366
- const details = getPiActorsRuntimeStatus();
1367
- return {
1368
- content: [
1369
- {
1370
- type: "text" as const,
1371
- text: maybeJsonText(
1372
- details,
1373
- input.verbose === true,
1374
- compactPiActorsRuntimeStatus(details),
1375
- ),
1376
- },
1377
- ],
1378
- details,
1379
- };
1380
- }
1381
- if (view !== "status" && view !== "schema") {
1382
- throw new Error(
1383
- "inspect tool:<name> supports view=status or view=schema.",
1384
- );
1385
- }
1386
- const tool = deps.getTool?.(address.value);
1387
- if (!tool) throw new Error(`tool actor not found: ${address.value}`);
1388
- const details = {
1389
- name: address.value,
1390
- description: tool.description,
1391
- parameters: tool.parameters,
1392
- promptSnippet: tool.promptSnippet,
1393
- };
1394
- return {
1395
- content: [
1396
- {
1397
- type: "text" as const,
1398
- text: maybeJsonText(
1399
- details,
1400
- input.verbose === true || view === "schema",
1401
- compactToolActor(address.value, details),
1402
- ),
1403
- },
1404
- ],
1405
- details,
1406
- };
1407
- }
1408
- if (address.kind === "room" && address.value && address.room) {
1409
- const status = assertRunAccessibleToContext(address.value, ctx);
1410
- const stateDir = String(status.state_dir ?? "");
1411
- if (!stateDir)
1412
- throw new Error(`room:${address.value} has no run state directory.`);
1413
- if (view === "status") {
1414
- const status = ActorRooms.getRoomStatus(stateDir, address.room);
1415
- return {
1416
- content: [
1417
- {
1418
- type: "text" as const,
1419
- text: maybeJsonText(
1420
- status,
1421
- input.verbose === true,
1422
- compactRoomStatus(status),
1423
- ),
1424
- },
1425
- ],
1426
- details: status,
1427
- };
1428
- }
1429
- if (view === "previews") {
1430
- const previews = ActorRooms.readRoomMessagePreviews(
1431
- stateDir,
1432
- address.room,
1433
- Number(input.lines || Limits.DEFAULT_INSPECT_LINES),
1434
- );
1435
- return {
1436
- content: [
1437
- {
1438
- type: "text" as const,
1439
- text: maybeJsonText(
1440
- previews,
1441
- input.verbose === true,
1442
- compactRoomPreviews(previews),
1443
- ),
1444
- },
1445
- ],
1446
- details: { previews },
1447
- };
1448
- }
1449
- if (view === "messages") {
1450
- const messages = ActorRooms.readRoomMessages(
1451
- stateDir,
1452
- address.room,
1453
- Number(input.lines || Limits.DEFAULT_INSPECT_LINES),
1454
- );
1455
- return {
1456
- content: [
1457
- {
1458
- type: "text" as const,
1459
- text: maybeJsonText(
1460
- messages,
1461
- input.verbose === true,
1462
- compactRoomMessages(messages),
1463
- ),
1464
- },
1465
- ],
1466
- details: { messages },
1467
- };
1468
- }
1469
- if (view === "contacts") {
1470
- const contacts = ActorRooms.readRoomContacts(stateDir, address.room);
1471
- return {
1472
- content: [
1473
- {
1474
- type: "text" as const,
1475
- text: maybeJsonText(
1476
- contacts,
1477
- input.verbose === true,
1478
- compactRoomContacts(contacts),
1479
- ),
1480
- },
1481
- ],
1482
- details: { contacts },
1483
- };
1484
- }
1485
- if (view === "roster") {
1486
- const roster = ActorRooms.readRoomRoster(stateDir, address.room);
1487
- return {
1488
- content: [
1489
- {
1490
- type: "text" as const,
1491
- text: maybeJsonText(
1492
- roster,
1493
- input.verbose === true,
1494
- compactRoomRoster(roster),
1495
- ),
1496
- },
1497
- ],
1498
- details: { roster },
1499
- };
1500
- }
1501
- throw new Error(
1502
- "inspect room:<run> supports view=status, view=messages, view=previews, view=roster, or view=contacts.",
1503
- );
1504
- }
1505
- const runId =
1506
- address.kind === "run" || address.kind === "branch"
1507
- ? address.value
1508
- : undefined;
1509
- if (!runId)
1510
- throw new Error(
1511
- "inspect target must be run:<id>, branch:<run>/<branch>, coordinator, session:<id>, or tool:<name>.",
1512
- );
1513
- if (address.kind === "branch") {
1514
- if (view !== "mailbox")
1515
- throw new Error(
1516
- "inspect branch:<run>/<branch> supports view=mailbox.",
1517
- );
1518
- const status = assertRunAccessibleToContext(runId, ctx);
1519
- const branchInbox = ActorRooms.readBranchInboxDiagnostics(
1520
- String(status.state_dir ?? ""),
1521
- runId,
1522
- target,
1523
- Number(input.lines || Limits.DEFAULT_INSPECT_LINES),
1524
- );
1525
- const messages = branchInbox.messages;
1526
- return {
1527
- content: [
1528
- {
1529
- type: "text" as const,
1530
- text: maybeJsonText(
1531
- input.verbose === true ? branchInbox : messages,
1532
- input.verbose === true,
1533
- `${compactBranchInbox(messages.map((message) => ({ ...message })))}${branchInbox.corrupted > 0 ? `\ncorrupted=${branchInbox.corrupted}` : ""}`,
1534
- ),
1535
- },
1536
- ],
1537
- details: { corrupted: branchInbox.corrupted, messages },
1538
- };
1539
- }
1540
- switch (view) {
1541
- case "status": {
1542
- const status = assertRunAccessibleToContext(runId, ctx);
1543
- return {
1544
- content: [
1545
- {
1546
- type: "text" as const,
1547
- text: maybeJsonText(
1548
- status,
1549
- input.verbose === true,
1550
- compactAsyncRunStatus(status),
1551
- ),
1552
- },
1553
- ],
1554
- details: status,
1555
- };
1556
- }
1557
- case "tail": {
1558
- assertRunAccessibleToContext(runId, ctx);
1559
- const text = AsyncRuns.tailRun(
1560
- runId,
1561
- Number(input.lines || Limits.DEFAULT_INSPECT_LINES),
1562
- );
1563
- return {
1564
- content: [{ type: "text" as const, text: `\n${text}` }],
1565
- details: {},
1566
- };
1567
- }
1568
- case "messages": {
1569
- assertRunAccessibleToContext(runId, ctx);
1570
- const messages = AsyncRuns.readRunEvents(
1571
- runId,
1572
- Number(input.lines || Limits.DEFAULT_INSPECT_LINES),
1573
- );
1574
- return {
1575
- content: [
1576
- {
1577
- type: "text" as const,
1578
- text: maybeJsonText(
1579
- messages,
1580
- input.verbose === true,
1581
- compactRunMessages(messages),
1582
- ),
1583
- },
1584
- ],
1585
- details: { messages },
1586
- };
1587
- }
1588
- case "artifacts":
1589
- case "files": {
1590
- const status = assertRunAccessibleToContext(runId, ctx);
1591
- const artifactManifest = AsyncRuns.resolveArtifactManifest(
1592
- status.artifacts as
1593
- | Record<string, AsyncRuns.RunArtifactDeclaration>
1594
- | undefined,
1595
- );
1596
- const details = artifactManifest
1597
- ? {
1598
- ...status,
1599
- artifact_manifest: artifactManifest,
1600
- next_actions: artifactNextActions(
1601
- status.run ?? runId,
1602
- asRecord(status.artifacts),
1603
- ),
1604
- }
1605
- : status;
1606
- return {
1607
- content: [
1608
- {
1609
- type: "text" as const,
1610
- text: maybeJsonText(
1611
- details,
1612
- input.verbose === true,
1613
- compactActorFiles(status),
1614
- ),
1615
- },
1616
- ],
1617
- details,
1618
- };
1619
- }
1620
- case "mailbox": {
1621
- const status = assertRunAccessibleToContext(runId, ctx);
1622
- const mailbox = asRecord(status.mailbox);
1623
- const normalizedMailbox = normalizeMailboxContracts(mailbox);
1624
- const messages = AsyncRuns.readRunInboxMessages(
1625
- runId,
1626
- Number(input.lines || Limits.DEFAULT_INSPECT_LINES),
1627
- );
1628
- const details = {
1629
- mailbox,
1630
- normalized_mailbox: normalizedMailbox,
1631
- messages,
1632
- };
1633
- return {
1634
- content: [
1635
- {
1636
- type: "text" as const,
1637
- text: maybeJsonText(
1638
- details,
1639
- input.verbose === true,
1640
- compactRunMailbox(
1641
- String(status.run ?? runId),
1642
- mailbox,
1643
- messages,
1644
- ),
1645
- ),
1646
- },
1647
- ],
1648
- details,
1649
- };
1650
- }
1651
- case "communication": {
1652
- const status = assertRunAccessibleToContext(runId, ctx);
1653
- const snapshot = ActorRooms.readCommunicationSnapshot(
1654
- String(status.state_dir ?? ""),
1655
- );
1656
- return {
1657
- content: [
1658
- {
1659
- type: "text" as const,
1660
- text: maybeJsonText(
1661
- snapshot ?? {},
1662
- input.verbose === true,
1663
- compactCommunicationSnapshot(snapshot),
1664
- ),
1665
- },
1666
- ],
1667
- details: { communication: snapshot },
1668
- };
1669
- }
1670
- default:
1671
- throw new Error(
1672
- "inspect view must be one of: status, tail, messages, artifacts, files, mailbox, communication; branch targets support mailbox.",
1673
- );
1674
- }
1675
- },
1676
- };
1677
- }
1678
-
1679
- export interface ActorMessageToolDeps<TContext = unknown> {
1680
- getTool?: (name: string) => any | undefined;
1681
- }
1682
-
1683
- export function createActorMessageToolDefinition<TContext = unknown>(
1684
- deps: ActorMessageToolDeps<TContext> = {},
1685
- ): any {
1686
- return {
1687
- name: "message",
1688
- label: "Message",
1689
- description:
1690
- "Send one typed addressed message to steer an existing actor instead of restarting it. Core routes are run:<id> and tool:<name>; advanced routes include branch:<run>/<branch>, room:<run> group timelines, coordinator, and session:<id>.",
1691
- parameters: objectSchema(
1692
- {
1693
- body: unionSchema([
1694
- stringSchema(
1695
- "Message body. For run:<id>, this is the run-local command line.",
1696
- ),
1697
- looseObjectSchema("Structured JSON message body."),
1698
- arraySchema("Structured JSON message body array."),
1699
- ]),
1700
- correlation_id: stringSchema(
1701
- "Optional correlation id for workflow/task linkage.",
1702
- ),
1703
- from: stringSchema(
1704
- "Optional sender address, such as coordinator or run:<id>.",
1705
- ),
1706
- metadata: looseObjectSchema(
1707
- "Optional structured metadata for routing or domain hints.",
1708
- ),
1709
- reply_to: stringSchema("Optional message id this message replies to."),
1710
- summary: stringSchema("Optional short human-facing summary."),
1711
- to: stringSchema(
1712
- "Destination actor address, e.g. run:<id> or tool:<name>; advanced: branch:<run>/<branch>, room:<run>, coordinator, session:<id>.",
1713
- ),
1714
- type: stringSchema(
1715
- "Semantic message type, e.g. control.approve or checkpoint.needs_scope.",
1716
- ),
1717
- verbose: booleanSchema("Return full JSON instead of compact text."),
1718
- },
1719
- ["to", "type"],
1720
- ),
1721
- async execute(
1722
- _toolCallId: string,
1723
- params: unknown,
1724
- _signal: AbortSignal | undefined,
1725
- _onUpdate: unknown,
1726
- ctx: TContext,
1727
- ) {
1728
- const input = asRecord(params);
1729
- const message = ActorMessages.normalizeActorMessage(input);
1730
- const address = ActorMessages.parseActorAddress(message.to);
1731
- let result: Record<string, unknown>;
1732
- if (address.kind === "run" && address.value) {
1733
- const status = assertRunAccessibleToContext(address.value, ctx);
1734
- const normalizedMailbox = normalizeMailboxContracts(
1735
- asRecord(status.mailbox),
1736
- );
1737
- const acceptedTypes = new Set(mailboxTypes(normalizedMailbox.accepts));
1738
- const advisoryWarnings =
1739
- acceptedTypes.size > 0 && !acceptedTypes.has(message.type)
1740
- ? [
1741
- `Message type ${message.type} is not declared in mailbox.accepts for run:${address.value}.`,
1742
- ]
1743
- : [];
1744
- if (message.type === "control.kill") {
1745
- result = AsyncRuns.killRun(address.value);
1746
- } else if (message.type === "control.archive") {
1747
- result = AsyncRuns.archiveRun(address.value);
1748
- } else if (message.type === "control.prune") {
1749
- const body = asRecord(message.body);
1750
- result = AsyncRuns.pruneRun(address.value, {
1751
- preserveArtifacts:
1752
- body.preserve_artifacts === true ||
1753
- body.preserveArtifacts === true,
1754
- });
1755
- } else {
1756
- result = await AsyncRuns.sendRunMessage(
1757
- address.value,
1758
- messageBodyToRunLine(message),
1759
- );
1760
- }
1761
- if (advisoryWarnings.length > 0)
1762
- result = { ...result, warnings: advisoryWarnings };
1763
- } else if (address.kind === "branch" && address.value) {
1764
- const runId = address.value;
1765
- if (message.from)
1766
- assertMessageSenderBelongsToRun(
1767
- message,
1768
- runId,
1769
- `branch:${runId}/<branch>`,
1770
- );
1771
- const status = assertRunAccessibleToContext(runId, ctx);
1772
- const stateDir = String(status.state_dir ?? "");
1773
- if (stateDir && address.branch) {
1774
- const ensureBranchMember = (actorAddress: string) => {
1775
- ActorRooms.ensureRoomMember(
1776
- stateDir,
1777
- runId,
1778
- "main",
1779
- actorAddress,
1780
- {
1781
- parent: `run:${runId}`,
1782
- role: "branch",
1783
- status: "present",
1784
- },
1785
- "Branch joined default room",
1786
- );
1787
- ActorRooms.writeBranchCommunicationSnapshot(
1788
- stateDir,
1789
- runId,
1790
- actorAddress,
1791
- );
1792
- };
1793
- ensureBranchMember(message.to);
1794
- if (message.from) {
1795
- const sender = ActorMessages.parseActorAddress(message.from);
1796
- if (sender.kind === "branch" && sender.value === runId) {
1797
- ensureBranchMember(message.from);
1798
- }
1799
- }
1800
- ActorRooms.writeCommunicationSnapshot(stateDir, runId);
1801
- }
1802
- result = await routeBranchEnvelope(
1803
- stateDir,
1804
- runId,
1805
- message.to,
1806
- message,
1807
- {
1808
- source: "direct",
1809
- },
1810
- );
1811
- } else if (address.kind === "room" && address.value && address.room) {
1812
- const runId = address.value;
1813
- assertMessageSenderBelongsToRun(message, runId, `room:${runId}`);
1814
- const status = assertRunAccessibleToContext(runId, ctx);
1815
- const stateDir = String(status.state_dir ?? "");
1816
- if (!stateDir)
1817
- throw new Error(`${message.to} has no run state directory.`);
1818
- const recipients = getRoomMulticastRecipients(message, runId);
1819
- const roomResult = ActorRooms.appendRoomMessage(
1820
- stateDir,
1821
- address.room,
1822
- message,
1823
- );
1824
- await Promise.all(
1825
- recipients.map((recipient) =>
1826
- routeBranchEnvelope(stateDir, runId, recipient, message, {
1827
- source: "room-multicast",
1828
- }),
1829
- ),
1830
- );
1831
- result = {
1832
- ...roomResult,
1833
- ...(recipients.length > 0
1834
- ? { multicast: recipients, multicast_count: recipients.length }
1835
- : {}),
1836
- };
1837
- } else if (address.kind === "tool" && address.value) {
1838
- const tool = deps.getTool?.(address.value);
1839
- if (!tool || typeof tool.execute !== "function") {
1840
- throw new Error(
1841
- `tool actor not found or not executable: ${address.value}`,
1842
- );
1843
- }
1844
- const toolParams = messageBodyToToolParams(message);
1845
- let toolResult: unknown;
1846
- try {
1847
- toolResult = await tool.execute(
1848
- `message:${message.type}`,
1849
- toolParams,
1850
- _signal,
1851
- _onUpdate,
1852
- ctx,
1853
- );
1854
- } catch (error) {
1855
- throw formatToolActorFailure(
1856
- address.value,
1857
- message,
1858
- toolParams,
1859
- error,
1860
- );
1861
- }
1862
- result = {
1863
- invoked: true,
1864
- sent: true,
1865
- tool: address.value,
1866
- tool_result: toolResult,
1867
- };
1868
- } else if (address.kind === "coordinator" || address.kind === "session") {
1869
- if (!message.from) {
1870
- throw new Error(`message to ${address.kind} requires from=run:<id>.`);
1871
- }
1872
- const sender = ActorMessages.parseActorAddress(message.from);
1873
- if (sender.kind !== "run" || !sender.value) {
1874
- throw new Error(
1875
- `message to ${address.kind} currently requires from=run:<id>.`,
1876
- );
1877
- }
1878
- const senderStatus = assertRunAccessibleToContext(sender.value, ctx);
1879
- if (address.kind === "session") {
1880
- if (!senderStatus.ownerId) {
1881
- throw sessionMismatchError({
1882
- currentSession: undefined,
1883
- expectedSession: address.value,
1884
- run: sender.value,
1885
- target: `session:${address.value}`,
1886
- });
1887
- }
1888
- if (senderStatus.ownerId !== address.value) {
1889
- throw sessionMismatchError({
1890
- currentSession: String(senderStatus.ownerId),
1891
- expectedSession: address.value,
1892
- run: sender.value,
1893
- target: `session:${address.value}`,
1894
- });
1895
- }
1896
- }
1897
- result = AsyncRuns.appendRunOutboxEvent(sender.value, {
1898
- body: message.body,
1899
- correlation_id: message.correlation_id,
1900
- delivery:
1901
- message.metadata?.requires_response === true ||
1902
- address.kind === "session"
1903
- ? "followup"
1904
- : undefined,
1905
- event: message.type,
1906
- from: message.from,
1907
- metadata: message.metadata,
1908
- reply_to: message.reply_to,
1909
- summary: message.summary,
1910
- to: message.to,
1911
- type: message.type,
1912
- });
1913
- } else {
1914
- throw new Error(
1915
- `message currently supports run:<id>, branch:<run>/<branch>, room:<run>, tool:<name>, coordinator, and session:<id> destinations; unsupported destination: ${message.to}`,
1916
- );
1917
- }
1918
- const nextActions = actorMessageNextActions(message, result);
1919
- const resultWithNext = nextActions.length
1920
- ? { ...result, next_actions: nextActions }
1921
- : result;
1922
- return {
1923
- content: [
1924
- {
1925
- type: "text" as const,
1926
- text: maybeJsonText(
1927
- { message, result: resultWithNext },
1928
- input.verbose === true,
1929
- compactActorMessageResult(message, resultWithNext),
1930
- ),
1931
- },
1932
- ],
1933
- details: { message, result: resultWithNext },
1934
- };
1935
- },
1936
- };
1937
- }
1938
-
1939
- export function createCoreActorToolDefinitions<TContext extends AsyncRunToolContext>(
1940
- deps: CoreActorToolDefinitionDeps<TContext>,
1941
- ): ActorToolDefinition[] {
49
+ export function createCoreActorToolDefinitions<
50
+ TContext extends RuntimeToolContext,
51
+ >(deps: CoreActorToolDefinitionDeps<TContext>): ActorToolDefinition[] {
1942
52
  return [
1943
- createRegisterToolDefinition<TContext>({
53
+ ToolsRegister.createRegisterToolDefinition<TContext>({
1944
54
  configPath: deps.configPath,
1945
55
  getActiveTools: deps.getActiveTools,
1946
56
  getExternalToolConflict: deps.registryRuntime.getExternalToolConflict,
@@ -1950,142 +60,12 @@ export function createCoreActorToolDefinitions<TContext extends AsyncRunToolCont
1950
60
  reservedToolNames: RESERVED_TOOL_NAMES,
1951
61
  setActiveTools: deps.setActiveTools,
1952
62
  }),
1953
- createSpawnToolDefinition<TContext>(),
1954
- createActorMessageToolDefinition<TContext>({
63
+ ToolsSpawn.createSpawnToolDefinition<TContext>(),
64
+ ToolsMessage.createActorMessageToolDefinition<TContext>({
1955
65
  getTool: (name) => deps.getRuntimeTool(name),
1956
66
  }),
1957
- createInspectToolDefinition<TContext>({
67
+ ToolsInspect.createInspectToolDefinition<TContext>({
1958
68
  getTool: (name) => deps.getRuntimeTool(name),
1959
69
  }),
1960
70
  ];
1961
71
  }
1962
-
1963
- export function createRuntimeToolDefinition(
1964
- cfg: RegisteredTool,
1965
- exec: Execution.RegisteredToolExec,
1966
- ): any {
1967
- const paramSchema: Record<string, JsonSchema> = {};
1968
- const required: string[] = [];
1969
- const isRecipe = RecipeReferences.isRecipeTool(cfg.template, cfg.recipe);
1970
- const isAsyncRecipe =
1971
- cfg.recipe?.async === true ||
1972
- RecipeReferences.isAsyncRecipeReference(cfg.template);
1973
- const recipeTemplate =
1974
- cfg.recipe?.template ?? RecipeReferences.getRecipeTemplate(cfg.template);
1975
- const requiredTemplate = recipeTemplate ?? cfg.template!;
1976
- const requiredTemplateConfig: CommandTemplates.CommandTemplateConfig =
1977
- typeof requiredTemplate === "object" && !Array.isArray(requiredTemplate)
1978
- ? {
1979
- ...requiredTemplate,
1980
- args: cfg.args,
1981
- defaults: { ...(requiredTemplate.defaults ?? {}), ...cfg.defaults },
1982
- }
1983
- : {
1984
- args: cfg.args,
1985
- defaults: cfg.defaults,
1986
- template: requiredTemplate,
1987
- };
1988
- const requiredArgs =
1989
- isRecipe && cfg.storedArgs !== undefined
1990
- ? new Set(cfg.args.filter((arg) => !Object.hasOwn(cfg.defaults, arg)))
1991
- : RecipeReferences.isRecipeReference(cfg.template) && !recipeTemplate
1992
- ? new Set(cfg.args.filter((arg) => !Object.hasOwn(cfg.defaults, arg)))
1993
- : Schema.getRequiredToolArgNames(requiredTemplateConfig);
1994
- for (const arg of cfg.args) {
1995
- paramSchema[arg] = typedArgSchema(arg, cfg.argTypes?.[arg]);
1996
- if (requiredArgs.has(arg)) required.push(arg);
1997
- }
1998
- if (isAsyncRecipe)
1999
- paramSchema.run_id = stringSchema(
2000
- "Optional run id override for this async template recipe invocation.",
2001
- );
2002
- return {
2003
- name: cfg.name,
2004
- label: cfg.name,
2005
- description: cfg.description,
2006
- parameters: objectSchema(paramSchema, required),
2007
- promptSnippet: isRecipe
2008
- ? Prompts.formatRecipeToolPromptSnippet(
2009
- cfg.recipe?.name ?? String(cfg.template),
2010
- isAsyncRecipe,
2011
- )
2012
- : Prompts.formatRegisteredToolPromptSnippet(cfg.template),
2013
- async execute(
2014
- _toolCallId: string,
2015
- params: unknown,
2016
- signal: AbortSignal | undefined,
2017
- _onUpdate: unknown,
2018
- ctx: AsyncRunToolContext,
2019
- ) {
2020
- try {
2021
- if (cfg.sourcePath)
2022
- RecipeUsage.recordRecipeLaunch(cfg.sourcePath, new Date(), "tool");
2023
- if (isAsyncRecipe) {
2024
- const input = params as Record<string, unknown>;
2025
- const { run_id, ...values } = input;
2026
- const base = cfg.recipe ? cfg.recipe : { file: String(cfg.template) };
2027
- const runId =
2028
- typeof run_id === "string" && run_id.trim()
2029
- ? run_id.trim()
2030
- : `${cfg.name}-${Date.now()}`;
2031
- const meta = AsyncRuns.startRun(
2032
- {
2033
- ...base,
2034
- launch_source: "tool",
2035
- ownerId: getRunOwnerId(ctx),
2036
- run_id: runId,
2037
- tool: cfg.name,
2038
- values: Schema.normalizeRuntimeValues(
2039
- { ...(cfg.recipe?.values ?? {}), ...cfg.defaults, ...values },
2040
- cfg.argTypes,
2041
- ),
2042
- },
2043
- ctx.cwd,
2044
- );
2045
- ActorRooms.ensureDefaultRoom(meta.state_dir, String(meta.run));
2046
- ActorRooms.writeCommunicationSnapshot(
2047
- meta.state_dir,
2048
- String(meta.run),
2049
- );
2050
- return {
2051
- content: [
2052
- { type: "text" as const, text: compactAsyncRunStatus(meta) },
2053
- ],
2054
- details: meta,
2055
- };
2056
- }
2057
- if (isRecipe && recipeTemplate) {
2058
- const paramsWithDefaults = {
2059
- ...(cfg.recipe?.values ?? {}),
2060
- ...cfg.defaults,
2061
- ...(params as Record<string, unknown>),
2062
- };
2063
- return await Execution.executeRegisteredTool(
2064
- { ...cfg, template: recipeTemplate },
2065
- Schema.normalizeRuntimeValues(paramsWithDefaults, cfg.argTypes),
2066
- exec,
2067
- ctx.cwd,
2068
- signal,
2069
- );
2070
- }
2071
- return await Execution.executeRegisteredTool(
2072
- cfg,
2073
- Schema.normalizeRuntimeValues(
2074
- params as Record<string, unknown>,
2075
- cfg.argTypes,
2076
- ),
2077
- exec,
2078
- ctx.cwd,
2079
- signal,
2080
- );
2081
- } catch (error) {
2082
- throw formatRuntimeToolArgumentError(
2083
- cfg,
2084
- error,
2085
- required,
2086
- isAsyncRecipe,
2087
- );
2088
- }
2089
- },
2090
- };
2091
- }