@llblab/pi-actors 0.14.2 → 0.15.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 (67) hide show
  1. package/AGENTS.md +5 -1
  2. package/BACKLOG.md +18 -32
  3. package/CHANGELOG.md +25 -0
  4. package/README.md +24 -20
  5. package/docs/actor-messages.md +1 -1
  6. package/docs/async-runs.md +4 -4
  7. package/docs/command-templates.md +11 -11
  8. package/docs/recipe-library.md +7 -3
  9. package/docs/task-first-recipes.md +44 -43
  10. package/docs/template-recipes.md +7 -2
  11. package/docs/tool-registry.md +7 -5
  12. package/lib/actor-messages.ts +20 -7
  13. package/lib/async-runs.ts +25 -12
  14. package/lib/command-templates.ts +6 -1
  15. package/lib/config.ts +2 -2
  16. package/lib/execution.ts +9 -5
  17. package/lib/observability.ts +20 -10
  18. package/lib/prompts.ts +13 -20
  19. package/lib/tools.ts +196 -64
  20. package/package.json +17 -9
  21. package/recipes/coordinator-locker.json +46 -0
  22. package/recipes/music-player.json +16 -2
  23. package/recipes/pipeline-architect-coordinator.json +11 -3
  24. package/recipes/pipeline-artifact-bundle.json +12 -3
  25. package/recipes/pipeline-artifact-report.json +9 -3
  26. package/recipes/pipeline-artifact-write.json +9 -3
  27. package/recipes/pipeline-async-run-ops.json +18 -9
  28. package/recipes/pipeline-checkpoint-continuation.json +14 -3
  29. package/recipes/pipeline-development-tasking.json +12 -3
  30. package/recipes/pipeline-docs-maintenance.json +12 -3
  31. package/recipes/pipeline-media-library.json +12 -3
  32. package/recipes/pipeline-quorum-review.json +12 -9
  33. package/recipes/pipeline-release-readiness.json +27 -9
  34. package/recipes/pipeline-release-summary.json +89 -0
  35. package/recipes/pipeline-repo-health.json +12 -3
  36. package/recipes/pipeline-research-synthesis.json +11 -3
  37. package/recipes/pipeline-review-readiness.json +12 -6
  38. package/recipes/subagent-artifact.json +9 -3
  39. package/recipes/subagent-checkpoint.json +10 -3
  40. package/recipes/subagent-conflict-report.json +11 -3
  41. package/recipes/subagent-contradiction-map.json +11 -3
  42. package/recipes/subagent-critic.json +11 -3
  43. package/recipes/subagent-evidence-map.json +11 -3
  44. package/recipes/subagent-followup.json +10 -3
  45. package/recipes/subagent-judge.json +11 -3
  46. package/recipes/subagent-merge.json +11 -3
  47. package/recipes/subagent-message.json +8 -3
  48. package/recipes/subagent-normalize.json +11 -3
  49. package/recipes/subagent-plan.json +11 -3
  50. package/recipes/subagent-prompt.json +10 -3
  51. package/recipes/subagent-quorum.json +10 -7
  52. package/recipes/subagent-review-coordinator.json +14 -6
  53. package/recipes/subagent-review.json +11 -3
  54. package/recipes/subagent-task-card.json +11 -3
  55. package/recipes/subagent-tools.json +10 -3
  56. package/recipes/subagent-verify.json +11 -3
  57. package/recipes/subagents-prompts.json +10 -3
  58. package/recipes/utility-coordinator-lock-snapshot.json +14 -0
  59. package/recipes/utility-run-ops-snapshot.json +3 -3
  60. package/recipes/utility-skill-summary.json +14 -0
  61. package/scripts/coordinator-locker.mjs +272 -0
  62. package/scripts/music-player.mjs +2 -1
  63. package/scripts/recipe-utils.mjs +239 -81
  64. package/scripts/validate-recipe.mjs +28 -10
  65. package/skills/actors/SKILL.md +283 -0
  66. package/skills/swarm/SKILL.md +451 -0
  67. package/skills/swarm/references/development-swarm.md +596 -0
@@ -33,7 +33,7 @@ register_tool name=transcribe_groq \
33
33
  ```text
34
34
  register_tool name=call_subagent \
35
35
  description="Run pi as a non-interactive sub-agent" \
36
- template="pi -p --model {model=openai-codex/gpt-5.5} --no-tools {prompt}"
36
+ template="pi -p --model {model} --no-tools {prompt}" args="prompt:string,model:string"
37
37
  ```
38
38
 
39
39
  Use `update=true` to overwrite an existing tool. Omit `template` and co-located recipe fields during update to keep the previous execution binding.
@@ -53,7 +53,7 @@ For reusable actor workflows, register a small tool whose `template` points to a
53
53
  register_tool name=docs_review \
54
54
  description="Start an async docs review actor" \
55
55
  template="docs-review.json" \
56
- args="scope:path,model:string=openai-codex/gpt-5.5"
56
+ args="scope:path,model:string"
57
57
  ```
58
58
 
59
59
  This stores the recipe path in the registry as `template`. If `~/.pi/agent/recipes/docs-review.json` contains `async: true`, calling the tool starts a detached actor run and returns metadata immediately. If `async` is omitted or false, the same recipe runs foreground and returns normal tool output.
@@ -66,7 +66,8 @@ When co-location is clearer than a separate file, the registry entry may include
66
66
  "description": "Start an async docs review",
67
67
  "name": "review-docs",
68
68
  "async": true,
69
- "template": "pi -p --model openai-codex/gpt-5.5 --tools read,bash \"Review {scope}\""
69
+ "args": ["scope:path", "model:string"],
70
+ "template": "pi -p --model {model} --tools read,bash \"Review {scope}\""
70
71
  }
71
72
  }
72
73
  ```
@@ -91,11 +92,12 @@ Tool names come from the top-level registry keys. Tool entries define `template`
91
92
  },
92
93
  "call_subagent": {
93
94
  "description": "Run pi as a non-interactive sub-agent",
94
- "template": "pi -p --model {model=openai-codex/gpt-5.5} --no-tools {prompt}"
95
+ "args": ["prompt:string", "model:string"],
96
+ "template": "pi -p --model {model} --no-tools {prompt}"
95
97
  },
96
98
  "docs_review": {
97
99
  "description": "Start an async docs review actor",
98
- "args": ["scope:path", "model:string=openai-codex/gpt-5.5"],
100
+ "args": ["scope:path", "model:string"],
99
101
  "template": "docs-review.json"
100
102
  }
101
103
  }
@@ -44,13 +44,15 @@ export function parseActorAddress(address: string): ActorAddress {
44
44
  const value = address.trim();
45
45
  if (value === "coordinator") return { kind: "coordinator" };
46
46
  const separator = value.indexOf(":");
47
- if (separator < 0) throw new Error(`Actor address must include kind: ${address}`);
47
+ if (separator < 0)
48
+ throw new Error(`Actor address must include kind: ${address}`);
48
49
  const kind = value.slice(0, separator) as ActorAddressKind;
49
50
  const rest = value.slice(separator + 1);
50
51
  switch (kind) {
51
52
  case "branch": {
52
53
  const [run, branch, ...extra] = rest.split("/");
53
- if (extra.length > 0) throw new Error(`Branch address has too many parts: ${address}`);
54
+ if (extra.length > 0)
55
+ throw new Error(`Branch address has too many parts: ${address}`);
54
56
  return {
55
57
  kind,
56
58
  value: assertToken(run || "", "branch run"),
@@ -74,14 +76,19 @@ export function formatActorAddress(address: ActorAddress): string {
74
76
  return `${address.kind}:${assertToken(address.value || "", `${address.kind} address`)}`;
75
77
  }
76
78
 
77
- function normalizeOptionalString(value: unknown, label: string): string | undefined {
79
+ function normalizeOptionalString(
80
+ value: unknown,
81
+ label: string,
82
+ ): string | undefined {
78
83
  if (value === undefined || value === null) return undefined;
79
84
  if (typeof value !== "string") throw new Error(`${label} must be a string`);
80
85
  const normalized = value.trim();
81
86
  return normalized || undefined;
82
87
  }
83
88
 
84
- function normalizeMetadata(value: unknown): Record<string, unknown> | undefined {
89
+ function normalizeMetadata(
90
+ value: unknown,
91
+ ): Record<string, unknown> | undefined {
85
92
  if (value === undefined || value === null) return undefined;
86
93
  if (typeof value !== "object" || Array.isArray(value)) {
87
94
  throw new Error("message metadata must be an object");
@@ -113,8 +120,14 @@ export function normalizeActorMessage(input: unknown): ActorMessage {
113
120
  ? { correlation_id: String(record.correlation_id) }
114
121
  : {}),
115
122
  ...(from ? { from: formatActorAddress(parseActorAddress(from)) } : {}),
116
- ...(record.metadata !== undefined ? { metadata: normalizeMetadata(record.metadata) } : {}),
117
- ...(record.reply_to !== undefined ? { reply_to: String(record.reply_to) } : {}),
118
- ...(record.summary !== undefined ? { summary: String(record.summary) } : {}),
123
+ ...(record.metadata !== undefined
124
+ ? { metadata: normalizeMetadata(record.metadata) }
125
+ : {}),
126
+ ...(record.reply_to !== undefined
127
+ ? { reply_to: String(record.reply_to) }
128
+ : {}),
129
+ ...(record.summary !== undefined
130
+ ? { summary: String(record.summary) }
131
+ : {}),
119
132
  };
120
133
  }
package/lib/async-runs.ts CHANGED
@@ -19,8 +19,8 @@ import {
19
19
  writeFileSync,
20
20
  writeSync,
21
21
  } from "node:fs";
22
- import { basename, extname, join, resolve } from "node:path";
23
22
  import { platform } from "node:os";
23
+ import { basename, extname, join, resolve } from "node:path";
24
24
 
25
25
  import type {
26
26
  CommandTemplateFailureScope,
@@ -28,8 +28,8 @@ import type {
28
28
  } from "./command-templates.ts";
29
29
  import { substituteCommandTemplateToken } from "./command-templates.ts";
30
30
  import { writeJsonAtomic } from "./file-state.ts";
31
- import * as RecipeReferences from "./recipe-references.ts";
32
31
  import * as Paths from "./paths.ts";
32
+ import * as RecipeReferences from "./recipe-references.ts";
33
33
 
34
34
  export interface AsyncRunStartParams {
35
35
  async?: boolean;
@@ -139,8 +139,7 @@ function resolveArtifactPaths(
139
139
  function resolveRunTemplate(params: AsyncRunStartParams): {
140
140
  template: CommandTemplateValue;
141
141
  } {
142
- if (!params.template)
143
- throw new Error("spawn requires file or template.");
142
+ if (!params.template) throw new Error("spawn requires file or template.");
144
143
  const envelope: Record<string, unknown> = {};
145
144
  for (const key of [
146
145
  "args",
@@ -227,7 +226,8 @@ function isAlive(pid: number): boolean {
227
226
  }
228
227
 
229
228
  function pidMatchesRun(pid: number, cwd: string, stateDir: string): boolean {
230
- if (platform() !== "linux" || !existsSync(`/proc/${pid}`)) return isAlive(pid);
229
+ if (platform() !== "linux" || !existsSync(`/proc/${pid}`))
230
+ return isAlive(pid);
231
231
  try {
232
232
  const procCwd = readlinkSync(`/proc/${pid}/cwd`);
233
233
  const cmdline = readFileSync(`/proc/${pid}/cmdline`, "utf8");
@@ -405,15 +405,23 @@ function normalizeRunOutboxEvent(
405
405
  : `${run}:${index}`;
406
406
  return {
407
407
  ...(record.body !== undefined ? { body: record.body } : {}),
408
- ...(typeof record.correlation_id === "string" ? { correlation_id: record.correlation_id } : {}),
408
+ ...(typeof record.correlation_id === "string"
409
+ ? { correlation_id: record.correlation_id }
410
+ : {}),
409
411
  ...(record.data !== undefined ? { data: record.data } : {}),
410
412
  delivery: normalizeRunOutboxDelivery(record.delivery),
411
- ...(record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) ? { metadata: record.metadata as Record<string, unknown> } : {}),
413
+ ...(record.metadata &&
414
+ typeof record.metadata === "object" &&
415
+ !Array.isArray(record.metadata)
416
+ ? { metadata: record.metadata as Record<string, unknown> }
417
+ : {}),
412
418
  event,
413
419
  ...(typeof record.from === "string" ? { from: record.from } : {}),
414
420
  id,
415
421
  level: normalizeRunOutboxLevel(record.level),
416
- ...(typeof record.reply_to === "string" ? { reply_to: record.reply_to } : {}),
422
+ ...(typeof record.reply_to === "string"
423
+ ? { reply_to: record.reply_to }
424
+ : {}),
417
425
  run,
418
426
  state_dir: stateDir,
419
427
  summary,
@@ -448,8 +456,9 @@ export function getRunStatus(runOrDir: string): Record<string, unknown> {
448
456
  const pid = Number(meta.pid || 0);
449
457
  const aliveOwnedRunner = Boolean(
450
458
  pid &&
451
- isAlive(pid) &&
452
- (!Array.isArray(meta.argv) || pidMatchesRun(pid, String(meta.cwd ?? ""), stateDir)),
459
+ isAlive(pid) &&
460
+ (!Array.isArray(meta.argv) ||
461
+ pidMatchesRun(pid, String(meta.cwd ?? ""), stateDir)),
453
462
  );
454
463
  const status: AsyncRunStatus = result
455
464
  ? Number(result.code ?? 0) === 0
@@ -555,7 +564,9 @@ export function appendRunOutboxEvent(
555
564
  ...(event.body !== undefined ? { body: event.body } : {}),
556
565
  ...(event.correlation_id ? { correlation_id: event.correlation_id } : {}),
557
566
  ...(event.data !== undefined ? { data: event.data } : {}),
558
- delivery: normalizeRunOutboxDelivery(event.delivery ?? (to === "coordinator" ? "followup" : "log")),
567
+ delivery: normalizeRunOutboxDelivery(
568
+ event.delivery ?? (to === "coordinator" ? "followup" : "log"),
569
+ ),
559
570
  event: type,
560
571
  from: event.from || `run:${run}`,
561
572
  level: normalizeRunOutboxLevel(event.level),
@@ -608,7 +619,9 @@ export function sendRunMessage(
608
619
  fd = openSync(controlPath, constants.O_WRONLY | constants.O_NONBLOCK);
609
620
  const bytes = writeSync(fd, payload);
610
621
  const trimmedMessage = message.trim().toLowerCase();
611
- const terminalMessage = ["stop", "cancel", "quit", "exit"].includes(trimmedMessage);
622
+ const terminalMessage = ["stop", "cancel", "quit", "exit"].includes(
623
+ trimmedMessage,
624
+ );
612
625
  writeFileSync(
613
626
  join(stateDir, "events.jsonl"),
614
627
  `${JSON.stringify({ bytes, event: "run.message", terminal: terminalMessage || undefined, ts: new Date().toISOString() })}\n`,
@@ -541,7 +541,12 @@ function shouldResolveEmbeddedCommandTemplateToken(
541
541
  function isFalsyCommandTemplateValue(value: unknown): boolean {
542
542
  if (value === undefined || value === null || value === false) return true;
543
543
  const normalized = String(value).trim().toLowerCase();
544
- return normalized === "" || normalized === "0" || normalized === "false" || normalized === "no";
544
+ return (
545
+ normalized === "" ||
546
+ normalized === "0" ||
547
+ normalized === "false" ||
548
+ normalized === "no"
549
+ );
545
550
  }
546
551
 
547
552
  function resolveCommandTemplateCondition(
package/lib/config.ts CHANGED
@@ -6,12 +6,12 @@
6
6
 
7
7
  import { existsSync, readFileSync } from "node:fs";
8
8
 
9
+ import type { CommandTemplateValue } from "./command-templates.ts";
10
+ import * as CommandTemplates from "./command-templates.ts";
9
11
  import { writeJsonAtomic } from "./file-state.ts";
10
12
  import { normalizeToolName } from "./identity.ts";
11
- import * as CommandTemplates from "./command-templates.ts";
12
13
  import * as RecipeReferences from "./recipe-references.ts";
13
14
  import * as Schema from "./schema.ts";
14
- import type { CommandTemplateValue } from "./command-templates.ts";
15
15
 
16
16
  export interface RegisteredTool {
17
17
  name: string;
package/lib/execution.ts CHANGED
@@ -4,9 +4,9 @@
4
4
  * Owns command-template invocation execution and pi tool-result payload formatting
5
5
  */
6
6
 
7
+ import * as CommandTemplates from "./command-templates.ts";
7
8
  import type { RegisteredTool } from "./config.ts";
8
9
  import { formatFailureOutput, formatOutput, formatToolText } from "./output.ts";
9
- import * as CommandTemplates from "./command-templates.ts";
10
10
  import * as Schema from "./schema.ts";
11
11
 
12
12
  export interface ToolExecOptions {
@@ -268,9 +268,10 @@ function resolveNumericControlField(
268
268
  label: string,
269
269
  ): number | undefined {
270
270
  if (value === undefined) return undefined;
271
- const resolved = typeof value === "string"
272
- ? CommandTemplates.substituteCommandTemplateToken(value, values, label)
273
- : value;
271
+ const resolved =
272
+ typeof value === "string"
273
+ ? CommandTemplates.substituteCommandTemplateToken(value, values, label)
274
+ : value;
274
275
  if (resolved === "") return undefined;
275
276
  const numeric = Number(resolved);
276
277
  if (!Number.isFinite(numeric) || numeric < 0)
@@ -407,7 +408,10 @@ async function executeTemplateConfig(
407
408
  const controlValues = { ...(context.defaults ?? {}), ...params };
408
409
  await applyDelay(normalized.delay, controlValues, signal);
409
410
  if (
410
- !CommandTemplates.shouldRunCommandTemplateNode(normalized.when, controlValues)
411
+ !CommandTemplates.shouldRunCommandTemplateNode(
412
+ normalized.when,
413
+ controlValues,
414
+ )
411
415
  ) {
412
416
  return {
413
417
  branches: [],
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { existsSync, readdirSync, readFileSync } from "node:fs";
8
- import { basename, dirname, relative, join } from "node:path";
8
+ import { basename, dirname, join, relative } from "node:path";
9
9
 
10
10
  import * as AsyncRuns from "./async-runs.ts";
11
11
  import * as Paths from "./paths.ts";
@@ -114,7 +114,9 @@ function observeRun(stateDir: string): RunObservation | undefined {
114
114
  ...(typeof status.ownerId === "string"
115
115
  ? { ownerId: status.ownerId }
116
116
  : {}),
117
- ...(status.artifacts && typeof status.artifacts === "object" && !Array.isArray(status.artifacts)
117
+ ...(status.artifacts &&
118
+ typeof status.artifacts === "object" &&
119
+ !Array.isArray(status.artifacts)
118
120
  ? { artifacts: status.artifacts as Record<string, string> }
119
121
  : {}),
120
122
  ...(status.terminal_handled ? { terminalHandled: true } : {}),
@@ -359,7 +361,11 @@ function parseOutboxLine(
359
361
  event,
360
362
  id,
361
363
  level: normalizeOutboxLevel(raw.level),
362
- ...(raw.metadata && typeof raw.metadata === "object" && !Array.isArray(raw.metadata) ? { metadata: raw.metadata as Record<string, unknown> } : {}),
364
+ ...(raw.metadata &&
365
+ typeof raw.metadata === "object" &&
366
+ !Array.isArray(raw.metadata)
367
+ ? { metadata: raw.metadata as Record<string, unknown> }
368
+ : {}),
363
369
  run: run.run,
364
370
  stateDir: run.stateDir,
365
371
  summary,
@@ -413,7 +419,8 @@ export function shouldSendRunOutboxFollowUp(event: RunOutboxEvent): boolean {
413
419
 
414
420
  function commonDirectory(paths: string[]): string | undefined {
415
421
  if (paths.length === 0) return undefined;
416
- const split = (path: string): string[] => dirname(path).split("/").filter(Boolean);
422
+ const split = (path: string): string[] =>
423
+ dirname(path).split("/").filter(Boolean);
417
424
  const first = split(paths[0]);
418
425
  let length = first.length;
419
426
  for (const path of paths.slice(1)) {
@@ -440,7 +447,9 @@ function formatPathGroup(label: string, paths: string[]): string {
440
447
  const unique = [...new Set(paths.filter(Boolean))].slice(0, 8);
441
448
  if (unique.length === 0) return "";
442
449
  const base = commonDirectory(unique);
443
- const names = unique.map((path) => `\`${relativeName(base, path)}\``).join(", ");
450
+ const names = unique
451
+ .map((path) => `\`${relativeName(base, path)}\``)
452
+ .join(", ");
444
453
  return `\n${label}:\n- Base: ${base ? `\`${base}\`` : "current run"}\n- Files: ${names}`;
445
454
  }
446
455
 
@@ -464,7 +473,9 @@ function formatNamedArtifacts(artifacts: unknown): string {
464
473
  }
465
474
 
466
475
  function getOutboxField(event: RunOutboxEvent, key: string): unknown {
467
- return event.data && typeof event.data === "object" && !Array.isArray(event.data)
476
+ return event.data &&
477
+ typeof event.data === "object" &&
478
+ !Array.isArray(event.data)
468
479
  ? (event.data as Record<string, unknown>)[key]
469
480
  : undefined;
470
481
  }
@@ -478,7 +489,8 @@ function formatBodyPreview(body: unknown): string {
478
489
  }
479
490
 
480
491
  export function formatRunOutboxMessage(event: RunOutboxEvent): string {
481
- if (event.event === "command.done") return `Run ${event.run}: ${event.summary}`;
492
+ if (event.event === "command.done")
493
+ return `Run ${event.run}: ${event.summary}`;
482
494
  return `Run ${event.run}: ${event.summary}${formatBodyPreview(event.body)}${formatNamedArtifacts(getOutboxField(event, "artifacts"))}${formatRunFileList(getOutboxField(event, "run_files"))}`;
483
495
  }
484
496
 
@@ -491,9 +503,7 @@ export function getRunTransitionNotificationType(
491
503
  return "error";
492
504
  }
493
505
 
494
- export function shouldNotifyRunTransition(
495
- transition: RunTransition,
496
- ): boolean {
506
+ export function shouldNotifyRunTransition(transition: RunTransition): boolean {
497
507
  if (transition.terminalHandled) return false;
498
508
  return (
499
509
  transition.to === "done" ||
package/lib/prompts.ts CHANGED
@@ -20,25 +20,18 @@ export const REGISTER_TOOL_GUIDELINES = [
20
20
  ];
21
21
 
22
22
  export const ONBOARDING_SYSTEM_PROMPT = `pi-actors quick model:
23
- - Local-first cybernetic tool memory: agents persist trusted local capabilities instead of repeating shell recipes.
24
- - Task = user work; template = execution graph; recipe = saved JSON; run = execution instance.
25
- - Command templates stay sync: string leaf, array sequence, object flags, parallel: true fanout.
26
- - Template flags: args/defaults, parallel, when, timeout, delay, retry, failure, recover, repeat, output; placeholders support {value??fallback} and {flag?yes:no}.
27
- - Recipes live in ~/.pi/agent/recipes/*.json and wrap templates with metadata/defaults/imports/artifacts.
28
- - Recipe imports are local variables: imports.alias -> {"name":"alias"} nodes and {alias.defaults.key} refs.
29
- - Imported recipes are definitions, not nested async runs; parent async:true creates one run.
30
- - async:true = detached lifecycle; spawn creates run actors from recipes/templates.
31
- - Actor run state lives under ~/.pi/agent/tmp/pi-actors/runs.
32
- - Use spawn/message/inspect for actor-level start/send/observe; runtime action internals are absorbed into the actor API.
33
- - Run lifecycle = state files, logs, actor messages, mailbox send, cancel/kill, compact status; do not busy-poll runs, rely on message/follow-up notifications and use message for explicit run-local commands.
34
- - Tool template may be a command template, recipe path/name, or co-located recipe.
35
- - register_tool makes compact persistent buttons; args may be typed or derived from placeholders.
36
- - For single calls or short pipelines, use foreground templates/tools.
37
- - For subagents, swarms, background music, or long fanout, prefer async recipes/runs.
38
- - Long async fanout = parent async recipe wrapping template(parallel: true) and imports; packaged fanout recipes bubble branch completion follow-ups by default.
39
- - If asked to explore pi-actors, read README.md, docs/README.md, docs/template-recipes.md, docs/async-runs.md, and recipes/.
40
- - Ambient triangles show active async commands/subagents for the launching coordinator.
41
- - After async run finish, inspect status/tail/messages before final artifacts.`;
23
+ - Local-first actor memory: persist trusted local capabilities instead of rebuilding shell recipes.
24
+ - Layers: task -> command template -> recipe/tool -> spawn -> run:<id>; tool:<name> wraps registered capabilities.
25
+ - Command templates stay sync: string leaf, array sequence, object node; flags include args/defaults, parallel, when, timeout, delay, retry, failure, recover, repeat, output.
26
+ - Placeholders support typed/default args plus {value??fallback} and {flag?yes:no}.
27
+ - Recipes live in ~/.pi/agent/recipes/*.json, own template directly, and may declare metadata/defaults/imports/mailbox/artifacts.
28
+ - Recipe imports are local variables; imported recipes are definitions, not nested async runs; parent async:true creates one run.
29
+ - Use spawn/message/inspect for actor-level start/send/observe; avoid runtime/FIFO/outbox vocabulary in public guidance.
30
+ - Run state lives under ~/.pi/agent/tmp/pi-actors/runs; inspect status/tail/messages/mailbox/files/artifacts intentionally and avoid busy-polling.
31
+ - Register_tool makes persistent tools from command templates, recipe names/paths, or co-located recipes; args may be typed or placeholder-derived.
32
+ - Foreground tools/templates fit short work; async recipes/runs fit subagents, services, fanout, media, and long pipelines.
33
+ - Long fanout = parent async recipe wrapping template(parallel:true) and imports; packaged fanout recipes bubble branch completion messages.
34
+ - For deeper pi-actors guidance, inspect installed extension sources/docs/recipes; README and docs are not automatically in context.`;
42
35
 
43
36
  export const REGISTER_TOOL_PARAM_DESCRIPTIONS = {
44
37
  name: "Tool name in snake_case (e.g., 'transcribe')",
@@ -53,7 +46,7 @@ export const REGISTER_TOOL_PARAM_DESCRIPTIONS = {
53
46
  templateArray:
54
47
  "Sequential command-template composition array. Leaves may be strings or objects with template/defaults/timeout/retry/failure/recover.",
55
48
  templateNull: "Delete the tool when template is null.",
56
- args: "Optional comma-separated placeholder declarations. Usually omit because args are derived from template placeholders. Interactive shorthand defaults are accepted and normalized. Example: file,lang,model=openai-codex/gpt-5.5",
49
+ args: "Optional comma-separated placeholder declarations. Usually omit because args are derived from template placeholders. Interactive shorthand defaults are accepted and normalized. Example: file,lang,mode=fast",
57
50
  update: "Set to true to overwrite an existing tool registration.",
58
51
  values:
59
52
  "Optional default runtime placeholder values for a co-located template recipe.",