@llblab/pi-actors 0.12.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 (86) hide show
  1. package/AGENTS.md +72 -0
  2. package/BACKLOG.md +38 -0
  3. package/CHANGELOG.md +179 -0
  4. package/README.md +338 -0
  5. package/docs/README.md +21 -0
  6. package/docs/actor-messages.md +149 -0
  7. package/docs/async-runs.md +335 -0
  8. package/docs/command-templates.md +424 -0
  9. package/docs/component-recipes.md +148 -0
  10. package/docs/recipe-library.md +176 -0
  11. package/docs/task-first-recipes.md +233 -0
  12. package/docs/template-recipes.md +285 -0
  13. package/docs/tool-registry.md +142 -0
  14. package/index.ts +198 -0
  15. package/lib/actor-messages.ts +120 -0
  16. package/lib/async-runs.ts +688 -0
  17. package/lib/command-templates.ts +795 -0
  18. package/lib/config.ts +266 -0
  19. package/lib/execution.ts +720 -0
  20. package/lib/file-state.ts +24 -0
  21. package/lib/identity.ts +29 -0
  22. package/lib/observability.ts +525 -0
  23. package/lib/output.ts +123 -0
  24. package/lib/paths.ts +35 -0
  25. package/lib/prompts.ts +75 -0
  26. package/lib/recipe-references.ts +586 -0
  27. package/lib/registry.ts +302 -0
  28. package/lib/runtime.ts +101 -0
  29. package/lib/schema.ts +402 -0
  30. package/lib/temp.ts +44 -0
  31. package/lib/tools.ts +651 -0
  32. package/package.json +52 -0
  33. package/recipes/music-player.json +25 -0
  34. package/recipes/pipeline-architect-coordinator.json +88 -0
  35. package/recipes/pipeline-artifact-report.json +52 -0
  36. package/recipes/pipeline-artifact-write.json +66 -0
  37. package/recipes/pipeline-async-run-ops.json +67 -0
  38. package/recipes/pipeline-checkpoint-continuation.json +57 -0
  39. package/recipes/pipeline-development-tasking.json +73 -0
  40. package/recipes/pipeline-docs-maintenance.json +72 -0
  41. package/recipes/pipeline-media-library.json +51 -0
  42. package/recipes/pipeline-quorum-review.json +72 -0
  43. package/recipes/pipeline-release-readiness.json +83 -0
  44. package/recipes/pipeline-repo-health.json +81 -0
  45. package/recipes/pipeline-research-synthesis.json +87 -0
  46. package/recipes/pipeline-review-readiness.json +49 -0
  47. package/recipes/subagent-artifact.json +26 -0
  48. package/recipes/subagent-checkpoint.json +27 -0
  49. package/recipes/subagent-conflict-report.json +25 -0
  50. package/recipes/subagent-contradiction-map.json +26 -0
  51. package/recipes/subagent-critic.json +28 -0
  52. package/recipes/subagent-evidence-map.json +26 -0
  53. package/recipes/subagent-followup.json +27 -0
  54. package/recipes/subagent-judge.json +26 -0
  55. package/recipes/subagent-merge.json +26 -0
  56. package/recipes/subagent-message.json +29 -0
  57. package/recipes/subagent-normalize.json +24 -0
  58. package/recipes/subagent-plan.json +26 -0
  59. package/recipes/subagent-prompt.json +22 -0
  60. package/recipes/subagent-quorum.json +41 -0
  61. package/recipes/subagent-review-coordinator.json +107 -0
  62. package/recipes/subagent-review.json +30 -0
  63. package/recipes/subagent-task-card.json +28 -0
  64. package/recipes/subagent-tools.json +17 -0
  65. package/recipes/subagent-verify.json +27 -0
  66. package/recipes/subagents-prompts.json +32 -0
  67. package/recipes/utility-actor-message.json +24 -0
  68. package/recipes/utility-artifact-manifest.json +17 -0
  69. package/recipes/utility-artifact-write.json +17 -0
  70. package/recipes/utility-changelog-head.json +12 -0
  71. package/recipes/utility-changelog-section.json +14 -0
  72. package/recipes/utility-git-log.json +12 -0
  73. package/recipes/utility-git-status.json +10 -0
  74. package/recipes/utility-jsonl-tail.json +11 -0
  75. package/recipes/utility-markdown-index.json +15 -0
  76. package/recipes/utility-package-summary.json +12 -0
  77. package/recipes/utility-playlist-build.json +18 -0
  78. package/recipes/utility-playlist-scan.json +12 -0
  79. package/recipes/utility-run-state-files.json +14 -0
  80. package/recipes/utility-run-summary.json +12 -0
  81. package/recipes/utility-validate-recipe.json +14 -0
  82. package/recipes/utility-validation-wrapper.json +14 -0
  83. package/scripts/async-runner.mjs +170 -0
  84. package/scripts/music-player.mjs +637 -0
  85. package/scripts/recipe-utils.mjs +273 -0
  86. package/scripts/validate-recipe.mjs +89 -0
package/lib/tools.ts ADDED
@@ -0,0 +1,651 @@
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
5
+ */
6
+
7
+ import type { RegisteredTool } from "./config.ts";
8
+ import * as ActorMessages from "./actor-messages.ts";
9
+ import * as AsyncRuns from "./async-runs.ts";
10
+ import * as CommandTemplates from "./command-templates.ts";
11
+ import * as Execution from "./execution.ts";
12
+ import * as Prompts from "./prompts.ts";
13
+ import * as RecipeReferences from "./recipe-references.ts";
14
+ import * as Registry from "./registry.ts";
15
+ import * as Schema from "./schema.ts";
16
+
17
+ export type RegisterToolInput = Registry.RegisterToolInput;
18
+ export type RegisterToolRuntimeDeps<TContext> =
19
+ Registry.RegisterToolRuntimeDeps<TContext>;
20
+
21
+ type JsonSchema = Record<string, unknown>;
22
+
23
+ function stringSchema(description: string): JsonSchema {
24
+ return { description, type: "string" };
25
+ }
26
+
27
+ function typedArgSchema(
28
+ arg: string,
29
+ type: Schema.ToolArgType | undefined,
30
+ ): JsonSchema {
31
+ if (!type || type.kind === "string") return stringSchema(`Argument: ${arg}`);
32
+ if (type.kind === "path") return stringSchema(`Path argument: ${arg}`);
33
+ if (type.kind === "int")
34
+ return { description: `Integer argument: ${arg}`, type: "integer" };
35
+ if (type.kind === "number")
36
+ return { description: `Number argument: ${arg}`, type: "number" };
37
+ if (type.kind === "bool")
38
+ return { description: `Boolean argument: ${arg}`, type: "boolean" };
39
+ if (type.kind === "array")
40
+ return { description: `Array argument: ${arg}`, items: {}, type: "array" };
41
+ return {
42
+ description: `Enum argument: ${arg}`,
43
+ enum: type.values,
44
+ type: "string",
45
+ };
46
+ }
47
+
48
+ function booleanSchema(description: string): JsonSchema {
49
+ return { description, type: "boolean" };
50
+ }
51
+
52
+ function nullSchema(description: string): JsonSchema {
53
+ return { description, type: "null" };
54
+ }
55
+
56
+ function arraySchema(description: string): JsonSchema {
57
+ return { description, items: {}, type: "array" };
58
+ }
59
+
60
+ function unionSchema(anyOf: JsonSchema[]): JsonSchema {
61
+ return { anyOf };
62
+ }
63
+
64
+ function objectSchema(
65
+ properties: Record<string, JsonSchema>,
66
+ required: string[],
67
+ ): JsonSchema {
68
+ return { additionalProperties: false, properties, required, type: "object" };
69
+ }
70
+
71
+ function looseObjectSchema(description: string): JsonSchema {
72
+ return { additionalProperties: true, description, type: "object" };
73
+ }
74
+
75
+ function jsonText(value: unknown): string {
76
+ return `\n${JSON.stringify(value, null, 2)}`;
77
+ }
78
+
79
+ function asRecord(value: unknown): Record<string, unknown> {
80
+ return value && typeof value === "object" && !Array.isArray(value)
81
+ ? (value as Record<string, unknown>)
82
+ : {};
83
+ }
84
+
85
+ function formatFailureCount(value: unknown): number | undefined {
86
+ return Array.isArray(value) ? value.length : undefined;
87
+ }
88
+
89
+ function compactAsyncRunStatus(value: unknown): string {
90
+ const status = asRecord(value);
91
+ const progress = asRecord(status.progress);
92
+ const result = asRecord(status.result);
93
+ const tokens = [
94
+ `run=${String(status.run ?? "<unknown>")}`,
95
+ `status=${String(status.status ?? "unknown")}`,
96
+ ];
97
+ if (status.tool) tokens.push(`tool=${String(status.tool)}`);
98
+ if (status.recipe) tokens.push(`recipe=${String(status.recipe)}`);
99
+ if (Number(status.pid) > 0) tokens.push(`pid=${Number(status.pid)}`);
100
+ if (progress.phase && progress.phase !== status.status)
101
+ tokens.push(`phase=${String(progress.phase)}`);
102
+ if (Number(progress.activeSubagents) > 0)
103
+ tokens.push(`active=${Number(progress.activeSubagents)}`);
104
+ if (Number(progress.completed) > 0)
105
+ tokens.push(`completed=${Number(progress.completed)}`);
106
+ const failures = formatFailureCount(progress.failures);
107
+ if (failures !== undefined && failures > 0)
108
+ tokens.push(`failures=${failures}`);
109
+ if (result.code !== undefined) tokens.push(`code=${String(result.code)}`);
110
+ if (result.killed === true) tokens.push("killed=true");
111
+ return `\n${tokens.join(" ")}`;
112
+ }
113
+
114
+ function compactRunEvents(events: AsyncRuns.RunOutboxEvent[]): string {
115
+ if (events.length === 0) return "\n(no run events)";
116
+ return `\n${events
117
+ .map((event) =>
118
+ [
119
+ `run=${event.run}`,
120
+ `event=${event.event}`,
121
+ `level=${event.level}`,
122
+ `delivery=${event.delivery}`,
123
+ `summary=${event.summary.replaceAll(/\s+/g, "_")}`,
124
+ ].join(" "),
125
+ )
126
+ .join("\n")}`;
127
+ }
128
+
129
+ function compactActorFiles(status: Record<string, unknown>): string {
130
+ const run = String(status.run ?? "<unknown>");
131
+ const artifacts = asRecord(status.artifacts);
132
+ const files = [
133
+ status.stdoutLog,
134
+ status.stderrLog,
135
+ status.eventsFile,
136
+ status.outboxFile,
137
+ status.state_dir ? `${String(status.state_dir)}/result.json` : undefined,
138
+ ].filter((file): file is string => typeof file === "string");
139
+ const artifactText = Object.keys(artifacts).length
140
+ ? ` artifacts=${Object.entries(artifacts)
141
+ .map(([key, value]) => `${key}:${String(value)}`)
142
+ .join(",")}`
143
+ : "";
144
+ return `\nrun=${run}${artifactText}${files.length ? ` files=${files.join(",")}` : ""}`;
145
+ }
146
+
147
+ function compactSessionRuns(session: string, runs: Array<Record<string, unknown>>): string {
148
+ if (runs.length === 0) return `\nsession=${session} runs=0`;
149
+ return `\nsession=${session} runs=${runs.length}\n${runs
150
+ .map((run) => `run=${String(run.run ?? "")} status=${String(run.status ?? "")}${run.recipe ? ` recipe=${String(run.recipe)}` : ""}`)
151
+ .join("\n")}`;
152
+ }
153
+
154
+ function compactActorMessageResult(
155
+ message: ActorMessages.ActorMessage,
156
+ result: Record<string, unknown>,
157
+ ): string {
158
+ const tokens = [
159
+ `to=${message.to}`,
160
+ `type=${message.type}`,
161
+ `message=${result.sent === true || result.stopped === true ? "sent" : "not_sent"}`,
162
+ ];
163
+ if (result.bytes !== undefined) tokens.push(`bytes=${String(result.bytes)}`);
164
+ if (result.control) tokens.push(`control=${String(result.control)}`);
165
+ if (result.outbox) tokens.push(`outbox=${String(result.outbox)}`);
166
+ if (result.tool) tokens.push(`tool=${String(result.tool)}`);
167
+ if (result.stopped === true) tokens.push("stopped=true");
168
+ if (result.signal) tokens.push(`signal=${String(result.signal)}`);
169
+ if (result.invoked === true) tokens.push("invoked=true");
170
+ return `\n${tokens.join(" ")}`;
171
+ }
172
+
173
+ function maybeJsonText(
174
+ value: unknown,
175
+ verbose: boolean | undefined,
176
+ compact: string,
177
+ ): string {
178
+ return verbose ? jsonText(value) : compact;
179
+ }
180
+
181
+ export function createRegisterToolDefinition<TContext>(
182
+ deps: RegisterToolRuntimeDeps<TContext>,
183
+ ) {
184
+ return {
185
+ name: "register_tool",
186
+ label: "Register Tool",
187
+ description: Prompts.REGISTER_TOOL_DESCRIPTION,
188
+ promptSnippet: Prompts.REGISTER_TOOL_PROMPT_SNIPPET,
189
+ promptGuidelines: Prompts.REGISTER_TOOL_GUIDELINES,
190
+ parameters: objectSchema(
191
+ {
192
+ args: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.args),
193
+ async: booleanSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.async),
194
+ description: stringSchema(
195
+ Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.description,
196
+ ),
197
+ name: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.name),
198
+ state_dir: stringSchema(
199
+ Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.state_dir,
200
+ ),
201
+ template: unionSchema([
202
+ stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.template),
203
+ arraySchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.templateArray),
204
+ nullSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.templateNull),
205
+ ]),
206
+ update: booleanSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.update),
207
+ values: looseObjectSchema(
208
+ Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.values,
209
+ ),
210
+ },
211
+ [],
212
+ ),
213
+ execute: async (
214
+ _toolCallId: string,
215
+ params: unknown,
216
+ _signal: AbortSignal | undefined,
217
+ _onUpdate: unknown,
218
+ ctx: TContext,
219
+ ) => Registry.executeRegisterTool(params, ctx, deps),
220
+ };
221
+ }
222
+
223
+ export interface AsyncRunToolContext {
224
+ cwd: string;
225
+ sessionManager?: { getSessionId?: () => string };
226
+ }
227
+
228
+ function getRunOwnerId(ctx: AsyncRunToolContext): string | undefined {
229
+ return ctx.sessionManager?.getSessionId?.();
230
+ }
231
+
232
+ function messageBodyToRunLine(message: ActorMessages.ActorMessage): string {
233
+ if (typeof message.body === "string") return message.body;
234
+ if (message.body === undefined) return message.type;
235
+ return JSON.stringify(message.body);
236
+ }
237
+
238
+ function messageBodyToToolParams(message: ActorMessages.ActorMessage): Record<string, unknown> {
239
+ if (message.body && typeof message.body === "object" && !Array.isArray(message.body)) {
240
+ return message.body as Record<string, unknown>;
241
+ }
242
+ if (message.body === undefined) return {};
243
+ return { input: message.body };
244
+ }
245
+
246
+ function runIdFromActorAddress(address: string | undefined): string | undefined {
247
+ if (!address) return undefined;
248
+ const parsed = ActorMessages.parseActorAddress(address);
249
+ if (parsed.kind !== "run" || !parsed.value) {
250
+ throw new Error(`Expected run:<id> actor address, received: ${address}`);
251
+ }
252
+ return parsed.value;
253
+ }
254
+
255
+ export function createSpawnToolDefinition<
256
+ TContext extends AsyncRunToolContext,
257
+ >(): any {
258
+ return {
259
+ name: "spawn",
260
+ label: "Spawn",
261
+ description:
262
+ "Create an addressable actor from a recipe file or inline command template. Currently spawns run:<id> actors backed by async runs.",
263
+ parameters: objectSchema(
264
+ {
265
+ artifacts: looseObjectSchema("Optional named artifact paths for the spawned actor."),
266
+ as: stringSchema("Optional actor address for the spawned run, e.g. run:<id>."),
267
+ file: stringSchema("Optional template recipe JSON file. Bare names resolve under ~/.pi/agent/recipes."),
268
+ recipe: stringSchema("Alias for file; template recipe JSON file/name to spawn."),
269
+ state_dir: stringSchema("Optional explicit run state directory."),
270
+ template: unionSchema([
271
+ stringSchema("Inline command template string"),
272
+ arraySchema("Inline command-template sequence or parallel tree"),
273
+ ]),
274
+ values: looseObjectSchema("Runtime placeholder values passed to the actor."),
275
+ verbose: booleanSchema("Return full JSON instead of compact text."),
276
+ },
277
+ [],
278
+ ),
279
+ async execute(
280
+ _toolCallId: string,
281
+ params: unknown,
282
+ _signal: AbortSignal | undefined,
283
+ _onUpdate: unknown,
284
+ ctx: TContext,
285
+ ) {
286
+ const input = asRecord(params);
287
+ const runId = runIdFromActorAddress(
288
+ typeof input.as === "string" ? input.as : undefined,
289
+ );
290
+ const meta = AsyncRuns.startRun(
291
+ {
292
+ file: typeof input.file === "string" ? input.file : typeof input.recipe === "string" ? input.recipe : undefined,
293
+ ownerId: getRunOwnerId(ctx),
294
+ run_id: runId,
295
+ state_dir: typeof input.state_dir === "string" ? input.state_dir : undefined,
296
+ template: input.template as AsyncRuns.AsyncRunStartParams["template"],
297
+ values: asRecord(input.values),
298
+ ...(input.artifacts && typeof input.artifacts === "object" && !Array.isArray(input.artifacts)
299
+ ? { artifacts: input.artifacts as Record<string, string> }
300
+ : {}),
301
+ },
302
+ ctx.cwd,
303
+ );
304
+ return {
305
+ content: [
306
+ {
307
+ type: "text" as const,
308
+ text: maybeJsonText(meta, input.verbose === true, compactAsyncRunStatus(meta)),
309
+ },
310
+ ],
311
+ details: meta,
312
+ };
313
+ },
314
+ };
315
+ }
316
+
317
+ export function createInspectToolDefinition(): any {
318
+ return {
319
+ name: "inspect",
320
+ label: "Inspect",
321
+ description:
322
+ "Intentionally inspect an actor. Supports run:<id> views: status, tail, events, artifacts, files, mailbox; and session:<id> status.",
323
+ parameters: objectSchema(
324
+ {
325
+ lines: stringSchema("Line count for tail/events views. Default 40."),
326
+ status: stringSchema("Optional session run filter: all, running, active, terminal, done, failed, cancelled, killed, or exited."),
327
+ target: stringSchema("Actor address to inspect, e.g. run:<id>, session:<id>, or session:all."),
328
+ verbose: booleanSchema("Return full JSON instead of compact text where available."),
329
+ view: stringSchema("Inspection view: status, tail, events, artifacts, files, or mailbox."),
330
+ },
331
+ ["target", "view"],
332
+ ),
333
+ async execute(
334
+ _toolCallId: string,
335
+ params: unknown,
336
+ _signal: AbortSignal | undefined,
337
+ _onUpdate: unknown,
338
+ _ctx: unknown,
339
+ ) {
340
+ const input = asRecord(params);
341
+ const target = String(input.target ?? "");
342
+ const address = ActorMessages.parseActorAddress(target);
343
+ const view = String(input.view ?? "");
344
+ if (address.kind === "session") {
345
+ if (view !== "status" && view !== "runs") {
346
+ throw new Error("inspect session:<id> supports view=status or view=runs.");
347
+ }
348
+ const runs = AsyncRuns.listRuns(undefined, typeof input.status === "string" ? input.status : undefined)
349
+ .map((run) => AsyncRuns.getRunStatus(String(run.state_dir)))
350
+ .filter((run) => address.value === "all" || run.ownerId === address.value);
351
+ return {
352
+ content: [
353
+ {
354
+ type: "text" as const,
355
+ text: maybeJsonText({ session: address.value, runs }, input.verbose === true, compactSessionRuns(address.value || "", runs)),
356
+ },
357
+ ],
358
+ details: { session: address.value, runs },
359
+ };
360
+ }
361
+ const runId = address.kind === "run" ? address.value : undefined;
362
+ if (!runId) throw new Error("inspect target must be run:<id> or session:<id>.");
363
+ switch (view) {
364
+ case "status": {
365
+ const status = AsyncRuns.getRunStatus(runId);
366
+ return {
367
+ content: [
368
+ {
369
+ type: "text" as const,
370
+ text: maybeJsonText(status, input.verbose === true, compactAsyncRunStatus(status)),
371
+ },
372
+ ],
373
+ details: status,
374
+ };
375
+ }
376
+ case "tail": {
377
+ const text = AsyncRuns.tailRun(runId, Number(input.lines || 40));
378
+ return { content: [{ type: "text" as const, text: `\n${text}` }], details: {} };
379
+ }
380
+ case "events": {
381
+ const events = AsyncRuns.readRunEvents(runId, Number(input.lines || 40));
382
+ return {
383
+ content: [
384
+ {
385
+ type: "text" as const,
386
+ text: maybeJsonText(events, input.verbose === true, compactRunEvents(events)),
387
+ },
388
+ ],
389
+ details: { events },
390
+ };
391
+ }
392
+ case "artifacts":
393
+ case "files": {
394
+ const status = AsyncRuns.getRunStatus(runId);
395
+ return {
396
+ content: [
397
+ {
398
+ type: "text" as const,
399
+ text: maybeJsonText(status, input.verbose === true, compactActorFiles(status)),
400
+ },
401
+ ],
402
+ details: status,
403
+ };
404
+ }
405
+ case "mailbox": {
406
+ const status = AsyncRuns.getRunStatus(runId);
407
+ const mailbox = asRecord(status.mailbox);
408
+ return {
409
+ content: [
410
+ {
411
+ type: "text" as const,
412
+ text: maybeJsonText(mailbox, input.verbose === true, `\nrun=${String(status.run ?? runId)} accepts=${Array.isArray(mailbox.accepts) ? mailbox.accepts.join(",") : ""} emits=${Array.isArray(mailbox.emits) ? mailbox.emits.join(",") : ""}`),
413
+ },
414
+ ],
415
+ details: { mailbox },
416
+ };
417
+ }
418
+ default:
419
+ throw new Error("inspect view must be one of: status, tail, events, artifacts, files, mailbox.");
420
+ }
421
+ },
422
+ };
423
+ }
424
+
425
+ export interface ActorMessageToolDeps<TContext = unknown> {
426
+ getTool?: (name: string) => any | undefined;
427
+ }
428
+
429
+ export function createActorMessageToolDefinition<TContext = unknown>(
430
+ deps: ActorMessageToolDeps<TContext> = {},
431
+ ): any {
432
+ return {
433
+ name: "message",
434
+ label: "Message",
435
+ description:
436
+ "Send one typed addressed message. Routes to run:<id> mailboxes, branch:<run>/<branch> mailboxes, tool:<name> calls, and coordinator-bound run messages.",
437
+ parameters: objectSchema(
438
+ {
439
+ body: unionSchema([
440
+ stringSchema("Message body. For run:<id>, this is the run-local command line."),
441
+ looseObjectSchema("Structured JSON message body."),
442
+ arraySchema("Structured JSON message body array."),
443
+ ]),
444
+ correlation_id: stringSchema("Optional correlation id for workflow/task linkage."),
445
+ from: stringSchema("Optional sender address, such as coordinator or run:<id>."),
446
+ metadata: looseObjectSchema("Optional structured metadata for routing or domain hints."),
447
+ reply_to: stringSchema("Optional message id this message replies to."),
448
+ summary: stringSchema("Optional short human-facing summary."),
449
+ to: stringSchema("Destination actor address, e.g. run:<id>, branch:<run>/<branch>, coordinator, session:<id>, or tool:<name>."),
450
+ type: stringSchema("Semantic message type, e.g. control.approve or checkpoint.needs_scope."),
451
+ verbose: booleanSchema("Return full JSON instead of compact text."),
452
+ },
453
+ ["to", "type"],
454
+ ),
455
+ async execute(
456
+ _toolCallId: string,
457
+ params: unknown,
458
+ _signal: AbortSignal | undefined,
459
+ _onUpdate: unknown,
460
+ ctx: TContext,
461
+ ) {
462
+ const input = asRecord(params);
463
+ const message = ActorMessages.normalizeActorMessage(input);
464
+ const address = ActorMessages.parseActorAddress(message.to);
465
+ let result: Record<string, unknown>;
466
+ if (address.kind === "run" && address.value) {
467
+ if (message.type === "runtime.cancel") {
468
+ result = AsyncRuns.cancelRun(address.value);
469
+ } else if (message.type === "runtime.kill") {
470
+ result = AsyncRuns.killRun(address.value);
471
+ } else {
472
+ result = AsyncRuns.sendRunMessage(
473
+ address.value,
474
+ messageBodyToRunLine(message),
475
+ );
476
+ }
477
+ } else if (address.kind === "branch" && address.value) {
478
+ result = AsyncRuns.sendRunMessage(
479
+ address.value,
480
+ JSON.stringify(message),
481
+ );
482
+ } else if (address.kind === "tool" && address.value) {
483
+ const tool = deps.getTool?.(address.value);
484
+ if (!tool || typeof tool.execute !== "function") {
485
+ throw new Error(`tool actor not found or not executable: ${address.value}`);
486
+ }
487
+ const toolResult = await tool.execute(
488
+ `message:${message.type}`,
489
+ messageBodyToToolParams(message),
490
+ _signal,
491
+ _onUpdate,
492
+ ctx,
493
+ );
494
+ result = {
495
+ invoked: true,
496
+ sent: true,
497
+ tool: address.value,
498
+ tool_result: toolResult,
499
+ };
500
+ } else if (address.kind === "coordinator") {
501
+ if (!message.from) {
502
+ throw new Error("message to coordinator requires from=run:<id>.");
503
+ }
504
+ const sender = ActorMessages.parseActorAddress(message.from);
505
+ if (sender.kind !== "run" || !sender.value) {
506
+ throw new Error("message to coordinator currently requires from=run:<id>.");
507
+ }
508
+ result = AsyncRuns.appendRunOutboxEvent(sender.value, {
509
+ body: message.body,
510
+ correlation_id: message.correlation_id,
511
+ event: message.type,
512
+ from: message.from,
513
+ reply_to: message.reply_to,
514
+ summary: message.summary,
515
+ to: message.to,
516
+ type: message.type,
517
+ });
518
+ } else {
519
+ throw new Error(
520
+ `message currently supports run:<id>, branch:<run>/<branch>, tool:<name>, and coordinator destinations; unsupported destination: ${message.to}`,
521
+ );
522
+ }
523
+ return {
524
+ content: [
525
+ {
526
+ type: "text" as const,
527
+ text: maybeJsonText(
528
+ { message, result },
529
+ input.verbose === true,
530
+ compactActorMessageResult(message, result),
531
+ ),
532
+ },
533
+ ],
534
+ details: { message, result },
535
+ };
536
+ },
537
+ };
538
+ }
539
+
540
+ export function createRuntimeToolDefinition(
541
+ cfg: RegisteredTool,
542
+ exec: Execution.RegisteredToolExec,
543
+ ): any {
544
+ const paramSchema: Record<string, JsonSchema> = {};
545
+ const required: string[] = [];
546
+ const isRecipe = RecipeReferences.isRecipeTool(cfg.template, cfg.recipe);
547
+ const isAsyncRecipe =
548
+ cfg.recipe?.async === true ||
549
+ RecipeReferences.isAsyncRecipeReference(cfg.template);
550
+ const recipeTemplate =
551
+ cfg.recipe?.template ?? RecipeReferences.getRecipeTemplate(cfg.template);
552
+ const requiredTemplate = recipeTemplate ?? cfg.template!;
553
+ const requiredTemplateConfig: CommandTemplates.CommandTemplateConfig =
554
+ typeof requiredTemplate === "object" && !Array.isArray(requiredTemplate)
555
+ ? {
556
+ ...requiredTemplate,
557
+ args: cfg.args,
558
+ defaults: { ...(requiredTemplate.defaults ?? {}), ...cfg.defaults },
559
+ }
560
+ : {
561
+ args: cfg.args,
562
+ defaults: cfg.defaults,
563
+ template: requiredTemplate,
564
+ };
565
+ const requiredArgs =
566
+ isRecipe && cfg.storedArgs !== undefined
567
+ ? new Set(cfg.args.filter((arg) => !Object.hasOwn(cfg.defaults, arg)))
568
+ : RecipeReferences.isRecipeReference(cfg.template) && !recipeTemplate
569
+ ? new Set(cfg.args.filter((arg) => !Object.hasOwn(cfg.defaults, arg)))
570
+ : Schema.getRequiredToolArgNames(requiredTemplateConfig);
571
+ for (const arg of cfg.args) {
572
+ paramSchema[arg] = typedArgSchema(arg, cfg.argTypes?.[arg]);
573
+ if (requiredArgs.has(arg)) required.push(arg);
574
+ }
575
+ if (isAsyncRecipe)
576
+ paramSchema.run_id = stringSchema(
577
+ "Optional run id override for this async template recipe invocation.",
578
+ );
579
+ return {
580
+ name: cfg.name,
581
+ label: cfg.name,
582
+ description: cfg.description,
583
+ parameters: objectSchema(paramSchema, required),
584
+ promptSnippet: isRecipe
585
+ ? Prompts.formatRecipeToolPromptSnippet(
586
+ cfg.recipe?.name ?? String(cfg.template),
587
+ isAsyncRecipe,
588
+ )
589
+ : Prompts.formatRegisteredToolPromptSnippet(cfg.template),
590
+ async execute(
591
+ _toolCallId: string,
592
+ params: unknown,
593
+ signal: AbortSignal | undefined,
594
+ _onUpdate: unknown,
595
+ ctx: AsyncRunToolContext,
596
+ ) {
597
+ if (isAsyncRecipe) {
598
+ const input = params as Record<string, unknown>;
599
+ const { run_id, ...values } = input;
600
+ const base = cfg.recipe ? cfg.recipe : { file: String(cfg.template) };
601
+ const runId =
602
+ typeof run_id === "string" && run_id.trim()
603
+ ? run_id.trim()
604
+ : `${cfg.name}-${Date.now()}`;
605
+ const meta = AsyncRuns.startRun(
606
+ {
607
+ ...base,
608
+ ownerId: getRunOwnerId(ctx),
609
+ run_id: runId,
610
+ tool: cfg.name,
611
+ values: Schema.normalizeRuntimeValues(
612
+ { ...(cfg.recipe?.values ?? {}), ...cfg.defaults, ...values },
613
+ cfg.argTypes,
614
+ ),
615
+ },
616
+ ctx.cwd,
617
+ );
618
+ return {
619
+ content: [
620
+ { type: "text" as const, text: compactAsyncRunStatus(meta) },
621
+ ],
622
+ details: meta,
623
+ };
624
+ }
625
+ if (isRecipe && recipeTemplate) {
626
+ const paramsWithDefaults = {
627
+ ...(cfg.recipe?.values ?? {}),
628
+ ...cfg.defaults,
629
+ ...(params as Record<string, unknown>),
630
+ };
631
+ return Execution.executeRegisteredTool(
632
+ { ...cfg, template: recipeTemplate },
633
+ Schema.normalizeRuntimeValues(paramsWithDefaults, cfg.argTypes),
634
+ exec,
635
+ ctx.cwd,
636
+ signal,
637
+ );
638
+ }
639
+ return Execution.executeRegisteredTool(
640
+ cfg,
641
+ Schema.normalizeRuntimeValues(
642
+ params as Record<string, unknown>,
643
+ cfg.argTypes,
644
+ ),
645
+ exec,
646
+ ctx.cwd,
647
+ signal,
648
+ );
649
+ },
650
+ };
651
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@llblab/pi-actors",
3
+ "version": "0.12.0",
4
+ "private": false,
5
+ "description": "Persistent template-backed tools for pi",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/llblab/pi-actors.git"
11
+ },
12
+ "homepage": "https://github.com/llblab/pi-actors",
13
+ "bugs": {
14
+ "url": "https://github.com/llblab/pi-actors/issues"
15
+ },
16
+ "keywords": [
17
+ "pi-package",
18
+ "pi-extension",
19
+ "tools",
20
+ "automation",
21
+ "binding"
22
+ ],
23
+ "scripts": {
24
+ "check": "node --experimental-strip-types -e \"await import('./index.ts'); console.log('pi-actors: extension import ok')\"",
25
+ "test": "node --experimental-strip-types --test tests/*.test.ts",
26
+ "pack:dry": "npm pack --dry-run",
27
+ "validate": "npx tsc --noEmit && npm run check && npm test && npm run pack:dry"
28
+ },
29
+ "files": [
30
+ "index.ts",
31
+ "lib",
32
+ "scripts",
33
+ "recipes",
34
+ "README.md",
35
+ "AGENTS.md",
36
+ "BACKLOG.md",
37
+ "CHANGELOG.md",
38
+ "docs"
39
+ ],
40
+ "pi": {
41
+ "extensions": [
42
+ "./index.ts"
43
+ ]
44
+ },
45
+ "peerDependencies": {
46
+ "@earendil-works/pi-coding-agent": "*"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "latest",
50
+ "typescript": "latest"
51
+ }
52
+ }