@llblab/pi-actors 0.43.0 → 0.43.1

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 (95) hide show
  1. package/AGENTS.md +12 -6
  2. package/BACKLOG.md +189 -1
  3. package/CHANGELOG.md +180 -301
  4. package/README.md +9 -7
  5. package/dist/index.js +1 -1
  6. package/dist/lib/async-runs.d.ts +2 -1
  7. package/dist/lib/async-runs.js +16 -4
  8. package/dist/lib/automatic-review-runtime.d.ts +1 -1
  9. package/dist/lib/automatic-review-runtime.js +5 -5
  10. package/dist/lib/command-templates.d.ts +2 -0
  11. package/dist/lib/command-templates.js +38 -4
  12. package/dist/lib/control-projection.d.ts +20 -0
  13. package/dist/lib/control-projection.js +66 -0
  14. package/dist/lib/control.d.ts +3 -0
  15. package/dist/lib/control.js +27 -14
  16. package/dist/lib/draft-sleep.js +3 -3
  17. package/dist/lib/file-state.d.ts +1 -0
  18. package/dist/lib/file-state.js +98 -42
  19. package/dist/lib/inspector-overlay.d.ts +2 -0
  20. package/dist/lib/inspector-overlay.js +92 -53
  21. package/dist/lib/limits.d.ts +5 -3
  22. package/dist/lib/limits.js +5 -3
  23. package/dist/lib/prompts.d.ts +1 -1
  24. package/dist/lib/prompts.js +1 -1
  25. package/dist/lib/recipe-control.js +6 -2
  26. package/dist/lib/review-control.d.ts +1 -1
  27. package/dist/lib/review-control.js +4 -5
  28. package/dist/lib/runs-control-delivery.d.ts +8 -1
  29. package/dist/lib/runs-control-delivery.js +38 -15
  30. package/dist/lib/runs-controls.d.ts +2 -0
  31. package/dist/lib/runs-controls.js +5 -3
  32. package/dist/lib/runs-trace.d.ts +2 -2
  33. package/dist/lib/runs-trace.js +24 -20
  34. package/dist/lib/runtime-identity.d.ts +7 -0
  35. package/dist/lib/runtime-identity.js +35 -0
  36. package/dist/lib/runtime-triage.d.ts +29 -0
  37. package/dist/lib/runtime-triage.js +76 -0
  38. package/dist/lib/tool-review-scheduler.js +7 -7
  39. package/dist/lib/tools-inspect.js +53 -14
  40. package/dist/lib/tools-message.d.ts +1 -2
  41. package/dist/lib/tools-message.js +6 -6
  42. package/dist/lib/tools-response.d.ts +0 -1
  43. package/dist/lib/tools-response.js +0 -9
  44. package/dist/lib/tools.d.ts +1 -1
  45. package/dist/lib/tools.js +1 -1
  46. package/dist/lib/trace-projection.js +30 -10
  47. package/dist/scripts/locker.mjs +9 -16
  48. package/dist/scripts/music-player.mjs +7 -13
  49. package/dist/scripts/release-gates.mjs +33 -2
  50. package/dist/scripts/validate-recipe.mjs +5 -4
  51. package/dist/skills/actors/SKILL.md +12 -7
  52. package/dist/skills/swarm/SKILL.md +0 -2
  53. package/docs/0.43-baseline.md +18 -23
  54. package/docs/README.md +2 -4
  55. package/docs/actor-inspector.md +5 -5
  56. package/docs/async-runs.md +2 -2
  57. package/docs/command-templates.md +6 -116
  58. package/docs/recipe-library.md +2 -4
  59. package/docs/releasing.md +28 -0
  60. package/docs/template-recipes.md +1 -1
  61. package/docs/tool-registry.md +2 -2
  62. package/index.ts +1 -1
  63. package/lib/async-runs.ts +18 -5
  64. package/lib/automatic-review-runtime.ts +7 -7
  65. package/lib/command-templates.ts +44 -4
  66. package/lib/control-projection.ts +105 -0
  67. package/lib/control.ts +33 -18
  68. package/lib/draft-sleep.ts +3 -3
  69. package/lib/file-state.ts +68 -61
  70. package/lib/inspector-overlay.ts +93 -58
  71. package/lib/limits.ts +5 -3
  72. package/lib/prompts.ts +1 -1
  73. package/lib/recipe-control.ts +9 -2
  74. package/lib/review-control.ts +4 -5
  75. package/lib/runs-control-delivery.ts +45 -17
  76. package/lib/runs-controls.ts +12 -3
  77. package/lib/runs-trace.ts +23 -19
  78. package/lib/runtime-identity.ts +39 -0
  79. package/lib/runtime-triage.ts +120 -0
  80. package/lib/tool-review-scheduler.ts +7 -7
  81. package/lib/tools-inspect.ts +60 -16
  82. package/lib/tools-message.ts +7 -8
  83. package/lib/tools-response.ts +0 -12
  84. package/lib/tools.ts +4 -4
  85. package/lib/trace-projection.ts +33 -10
  86. package/package.json +1 -1
  87. package/scripts/locker.mjs +9 -16
  88. package/scripts/music-player.mjs +7 -13
  89. package/scripts/release-gates.mjs +33 -2
  90. package/scripts/validate-recipe.mjs +5 -4
  91. package/skills/actors/SKILL.md +12 -7
  92. package/skills/swarm/SKILL.md +0 -2
  93. package/docs/actors-deep-reference.md +0 -108
  94. package/docs/component-recipes.md +0 -45
  95. package/docs/task-first-recipes.md +0 -261
@@ -41,6 +41,7 @@ export declare class ActorInspectorOverlay {
41
41
  private killDialogChoice;
42
42
  private rowIndex;
43
43
  private runCache?;
44
+ private selectedTraceId?;
44
45
  private runIndex;
45
46
  private selectorIndex;
46
47
  private selectorMode?;
@@ -81,6 +82,7 @@ export declare class ActorInspectorOverlay {
81
82
  private readRunMeta;
82
83
  private documentProjection;
83
84
  private inlineDocumentValue;
85
+ private labeledDocumentLines;
84
86
  private labeledScalarLines;
85
87
  private document;
86
88
  private wrapDocument;
@@ -7,6 +7,7 @@ import { readFileSync } from "node:fs";
7
7
  import * as path from "node:path";
8
8
  import { matchesKey, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
9
9
  import * as ActorInspector from "./inspector.js";
10
+ import * as ControlProjection from "./control-projection.js";
10
11
  import * as RunsControls from "./runs-controls.js";
11
12
  import * as RunControlDelivery from "./runs-control-delivery.js";
12
13
  import * as TraceProjection from "./trace-projection.js";
@@ -20,6 +21,9 @@ const TRACE_SOURCES = [
20
21
  "artifact",
21
22
  "runtime",
22
23
  ];
24
+ function newestTraceItems(items) {
25
+ return [...items].sort((left, right) => right.ts.localeCompare(left.ts) || right.id.localeCompare(left.id));
26
+ }
23
27
  export class ActorInspectorOverlay {
24
28
  done;
25
29
  killRun;
@@ -39,6 +43,7 @@ export class ActorInspectorOverlay {
39
43
  killDialogChoice = "cancel";
40
44
  rowIndex = 0;
41
45
  runCache;
46
+ selectedTraceId;
42
47
  runIndex = 0;
43
48
  selectorIndex = 0;
44
49
  selectorMode;
@@ -56,9 +61,7 @@ export class ActorInspectorOverlay {
56
61
  this.theme = options.theme;
57
62
  this.tui = options.tui;
58
63
  this.refreshTimer = setInterval(() => {
59
- this.runCache = undefined;
60
- if (this.focus !== "list" && this.focus !== "detail")
61
- this.traceCache = undefined;
64
+ this.invalidate();
62
65
  this.tui.requestRender();
63
66
  }, 1_000);
64
67
  this.refreshTimer.unref?.();
@@ -66,7 +69,10 @@ export class ActorInspectorOverlay {
66
69
  dispose() {
67
70
  clearInterval(this.refreshTimer);
68
71
  }
69
- invalidate() { }
72
+ invalidate() {
73
+ this.runCache = undefined;
74
+ this.traceCache = undefined;
75
+ }
70
76
  handleInput(data) {
71
77
  const runs = this.runs();
72
78
  this.runIndex = Math.min(this.runIndex, Math.max(0, runs.length - 1));
@@ -90,11 +96,14 @@ export class ActorInspectorOverlay {
90
96
  this.focus = this.tab === "trace" ? "tabs" : "runs";
91
97
  }
92
98
  else if (this.focus === "detail") {
99
+ this.selectedTraceId = this.traceDetail?.id;
93
100
  this.traceDetail = undefined;
94
101
  this.detailScroll = 0;
95
102
  this.focus = "list";
96
103
  }
97
104
  else if (this.focus === "document" || this.focus === "list") {
105
+ if (this.focus === "list")
106
+ this.selectedTraceId = undefined;
98
107
  this.focus = "tabs";
99
108
  }
100
109
  else {
@@ -146,9 +155,12 @@ export class ActorInspectorOverlay {
146
155
  this.focus = "runs";
147
156
  }
148
157
  else if (matchesKey(data, "down")) {
149
- this.focus = this.tab === "trace" && this.traceItems(runs[this.runIndex]).length > 0
150
- ? "list"
151
- : "document";
158
+ const items = this.tab === "trace" ? this.traceItems(runs[this.runIndex]) : [];
159
+ this.focus = items.length > 0 ? "list" : "document";
160
+ if (this.focus === "list") {
161
+ this.rowIndex = 0;
162
+ this.selectedTraceId = items[0]?.id;
163
+ }
152
164
  }
153
165
  else if (matchesKey(data, "return")) {
154
166
  if (this.tab === "trace")
@@ -158,12 +170,17 @@ export class ActorInspectorOverlay {
158
170
  }
159
171
  }
160
172
  else if (this.focus === "list") {
161
- const count = this.traceItems(runs[this.runIndex]).length;
162
- if (matchesKey(data, "left"))
173
+ const items = this.traceItems(runs[this.runIndex]);
174
+ const count = items.length;
175
+ if (matchesKey(data, "left")) {
176
+ this.selectedTraceId = undefined;
163
177
  this.focus = "tabs";
178
+ }
164
179
  else if (matchesKey(data, "up")) {
165
- if (this.rowIndex === 0)
180
+ if (this.rowIndex === 0) {
181
+ this.selectedTraceId = undefined;
166
182
  this.focus = "tabs";
183
+ }
167
184
  else
168
185
  this.rowIndex -= 1;
169
186
  }
@@ -177,13 +194,15 @@ export class ActorInspectorOverlay {
177
194
  this.rowIndex = Math.min(Math.max(0, count - 1), this.rowIndex + this.contentViewportRows());
178
195
  }
179
196
  else if (matchesKey(data, "return") || matchesKey(data, "right")) {
180
- const item = this.traceItems(runs[this.runIndex])[this.rowIndex];
197
+ const item = items[this.rowIndex];
181
198
  if (item) {
182
199
  this.traceDetail = item;
183
200
  this.detailScroll = 0;
184
201
  this.focus = "detail";
185
202
  }
186
203
  }
204
+ if (this.focus === "list")
205
+ this.selectedTraceId = items[this.rowIndex]?.id;
187
206
  }
188
207
  if (data.toLowerCase() === "f" && this.tab === "trace") {
189
208
  this.cycleTraceSource(runs[this.runIndex]);
@@ -239,6 +258,7 @@ export class ActorInspectorOverlay {
239
258
  this.documentScroll = 0;
240
259
  this.detailScroll = 0;
241
260
  this.rowIndex = 0;
261
+ this.selectedTraceId = undefined;
242
262
  this.traceDetail = undefined;
243
263
  }
244
264
  cycleRun(runs, direction) {
@@ -260,6 +280,7 @@ export class ActorInspectorOverlay {
260
280
  this[key] += this.contentViewportRows();
261
281
  }
262
282
  else if (matchesKey(data, "left") && target === "detail") {
283
+ this.selectedTraceId = this.traceDetail?.id;
263
284
  this.traceDetail = undefined;
264
285
  this.detailScroll = 0;
265
286
  this.focus = "list";
@@ -361,7 +382,10 @@ export class ActorInspectorOverlay {
361
382
  if (!confirmation || !this.killRun)
362
383
  return;
363
384
  try {
364
- this.feedback = this.killRun(confirmation.run, confirmation.runInstanceId);
385
+ const result = this.killRun(confirmation.run, confirmation.runInstanceId);
386
+ this.feedback = result.ok ? undefined : result;
387
+ if (result.ok)
388
+ this.invalidate();
365
389
  }
366
390
  catch (error) {
367
391
  const message = error instanceof Error ? error.message : String(error);
@@ -387,7 +411,6 @@ export class ActorInspectorOverlay {
387
411
  `${this.theme.fg("muted", "Current status:")} ${this.theme.fg("warning", confirmation.status)}`,
388
412
  "",
389
413
  this.theme.fg("text", "This sends canonical kill Control."),
390
- this.theme.fg("error", "The action is destructive and cannot be undone."),
391
414
  "",
392
415
  `${cancel} ${kill}`,
393
416
  "",
@@ -480,7 +503,17 @@ export class ActorInspectorOverlay {
480
503
  const items = this.traceItems(run);
481
504
  if (items.length === 0)
482
505
  return [this.theme.fg("muted", " No Trace evidence")];
506
+ if (this.selectedTraceId) {
507
+ const selectedIndex = items.findIndex((item) => item.id === this.selectedTraceId);
508
+ if (selectedIndex >= 0)
509
+ this.rowIndex = selectedIndex;
510
+ else
511
+ this.selectedTraceId = undefined;
512
+ }
483
513
  this.rowIndex = Math.min(this.rowIndex, items.length - 1);
514
+ if (this.focus === "list" && !this.selectedTraceId) {
515
+ this.selectedTraceId = items[this.rowIndex]?.id;
516
+ }
484
517
  const viewportRows = this.contentViewportRows();
485
518
  const maxStart = Math.max(0, items.length - viewportRows);
486
519
  const start = Math.max(0, Math.min(this.rowIndex - Math.floor(viewportRows / 2), maxStart));
@@ -495,12 +528,12 @@ export class ActorInspectorOverlay {
495
528
  const marker = item.level === "error"
496
529
  ? this.theme.fg("error", "!")
497
530
  : detail.attention
498
- ? this.theme.fg("warning", "•")
499
- : this.theme.fg("muted", "·");
531
+ ? this.theme.fg("warning", "A")
532
+ : " ";
500
533
  const prefix = this.focus === "list" && index === this.rowIndex
501
534
  ? this.theme.fg("accent", " ▶ ")
502
535
  : " ";
503
- const row = `${prefix}${this.theme.fg("text", `#${index + 1}`)} ${marker} ${this.theme.fg("muted", item.source)}/${this.theme.fg(item.level === "error" ? "error" : "accent", item.kind)} ${this.theme.fg("text", item.summary)}`;
536
+ const row = `${prefix}${this.theme.fg("text", `#${items.length - index}`)} ${marker} ${this.theme.fg("muted", item.source)}/${this.theme.fg(item.level === "error" ? "error" : "accent", item.kind)} ${this.theme.fg("text", item.summary)}`;
504
537
  return this.focus === "list" && index === this.rowIndex
505
538
  ? this.theme.fg("accent", row)
506
539
  : row;
@@ -526,7 +559,10 @@ export class ActorInspectorOverlay {
526
559
  actor_actions: Array.isArray(meta.control) ? meta.control : [],
527
560
  runtime_actions: run.status === "running" ? ["kill"] : ["archive", "prune"],
528
561
  endpoint,
529
- recent_controls: RunsControls.readRunControlsFromStateDir(stateDir).slice(-20).reverse(),
562
+ recent_controls: RunsControls.readRunControlsFromStateDir(stateDir)
563
+ .slice(-20)
564
+ .reverse()
565
+ .map(ControlProjection.projectRunControl),
530
566
  };
531
567
  }
532
568
  allTraceItems(run) {
@@ -536,10 +572,10 @@ export class ActorInspectorOverlay {
536
572
  this.traceCache.runInstanceId === run.runInstanceId) {
537
573
  return this.traceCache.items;
538
574
  }
539
- const items = this.readTrace(path.join(this.stateRoot, run.run), {
575
+ const items = newestTraceItems(this.readTrace(path.join(this.stateRoot, run.run), {
540
576
  limit: 100,
541
577
  source: "all",
542
- });
578
+ }));
543
579
  this.traceCache = { run: run.run, runInstanceId: run.runInstanceId, items };
544
580
  return items;
545
581
  }
@@ -567,14 +603,7 @@ export class ActorInspectorOverlay {
567
603
  }
568
604
  documentProjection(value, width) {
569
605
  const sections = value && typeof value === "object" && !Array.isArray(value)
570
- ? Object.entries(value).map(([key, item]) => {
571
- const inline = this.inlineDocumentValue(item);
572
- return inline !== undefined
573
- ? [`${key}: ${inline}`]
574
- : item && typeof item === "object"
575
- ? [`${key}:`, ...this.document(item, 1)]
576
- : this.labeledScalarLines(key, item);
577
- })
606
+ ? Object.entries(value).map(([key, item]) => this.labeledDocumentLines(key, item))
578
607
  : [this.document(value)];
579
608
  const lines = [];
580
609
  const stripes = [];
@@ -596,19 +625,22 @@ export class ActorInspectorOverlay {
596
625
  return undefined;
597
626
  }
598
627
  if (value && typeof value === "object") {
599
- const entries = Object.entries(value);
600
- if (entries.length === 0)
628
+ if (Object.keys(value).length === 0)
601
629
  return "{}";
602
- if (entries.length <= 3 &&
603
- entries.every(([, item]) => (item === null || typeof item !== "object") &&
604
- !(typeof item === "string" && /[\r\n]/u.test(item)))) {
605
- return `{ ${entries
606
- .map(([key, item]) => `${key}: ${String(item ?? "none")}`)
607
- .join(", ")} }`;
608
- }
609
630
  }
610
631
  return undefined;
611
632
  }
633
+ labeledDocumentLines(key, value, depth = 0) {
634
+ const inline = this.inlineDocumentValue(value);
635
+ if (inline !== undefined)
636
+ return [`${" ".repeat(depth)}${key}: ${inline}`];
637
+ if (value && typeof value === "object") {
638
+ const indent = " ".repeat(depth);
639
+ const nested = this.document(value, depth);
640
+ return [`${indent}${key}: ${nested[0].slice(indent.length)}`, ...nested.slice(1)];
641
+ }
642
+ return this.labeledScalarLines(key, value, depth);
643
+ }
612
644
  labeledScalarLines(key, value, depth = 0) {
613
645
  const indent = " ".repeat(depth);
614
646
  const [first = "", ...rest] = String(value ?? "none").split(/\r?\n/u);
@@ -624,25 +656,32 @@ export class ActorInspectorOverlay {
624
656
  if (inline !== undefined)
625
657
  return [`${" ".repeat(depth)}${inline}`];
626
658
  if (Array.isArray(value)) {
627
- return value.flatMap((item, index) => {
628
- const marker = `${" ".repeat(depth)}- #${index + 1}`;
629
- const itemInline = this.inlineDocumentValue(item);
630
- return itemInline !== undefined
631
- ? [`${marker}: ${itemInline}`]
632
- : [marker, ...this.document(item, depth + 1)];
633
- });
659
+ const indent = " ".repeat(depth);
660
+ return [
661
+ `${indent}[`,
662
+ ...value.flatMap((item, index) => {
663
+ const marker = `${indent} - #${index + 1}`;
664
+ const itemInline = this.inlineDocumentValue(item);
665
+ return itemInline !== undefined
666
+ ? [`${marker}: ${itemInline}`]
667
+ : [marker, ...this.document(item, depth + 2)];
668
+ }),
669
+ `${indent}]`,
670
+ ];
634
671
  }
635
672
  if (typeof value === "object") {
636
- return Object.entries(value).flatMap(([key, item]) => {
637
- const itemInline = this.inlineDocumentValue(item);
638
- if (itemInline !== undefined) {
639
- return [`${" ".repeat(depth)}${key}: ${itemInline}`];
640
- }
641
- if (item && typeof item === "object") {
642
- return [`${" ".repeat(depth)}${key}:`, ...this.document(item, depth + 1)];
643
- }
644
- return this.labeledScalarLines(key, item, depth);
645
- });
673
+ const indent = " ".repeat(depth);
674
+ const entries = Object.entries(value);
675
+ return [
676
+ `${indent}{`,
677
+ ...entries.flatMap(([key, item], index) => {
678
+ const lines = this.labeledDocumentLines(key, item, depth + 1);
679
+ if (index < entries.length - 1)
680
+ lines[lines.length - 1] += ",";
681
+ return lines;
682
+ }),
683
+ `${indent}}`,
684
+ ];
646
685
  }
647
686
  return String(value)
648
687
  .split(/\r?\n/u)
@@ -1,12 +1,14 @@
1
1
  /**
2
- * Shared output and preview size limits.
3
- * Zones: output governance, Trace previews, inspect defaults
2
+ * Shared output, preview, and Control envelope limits.
3
+ * Zones: output governance, Trace previews, Inspect defaults, Control portability
4
4
  */
5
5
  export declare const DEFAULT_INSPECT_LINES = 40;
6
6
  export declare const TOOL_OUTPUT_MAX_BYTES: number;
7
7
  export declare const TOOL_OUTPUT_MAX_LINES = 2000;
8
8
  export declare const COMPACT_PREVIEW_CHARS = 160;
9
- export declare const CONTROL_INPUT_MAX_BYTES: number;
9
+ export declare const CONTROL_ACTION_MAX_LENGTH = 64;
10
+ export declare const CONTROL_INPUT_MAX_BYTES = 380;
11
+ export declare const CONTROL_WIRE_MAX_BYTES = 512;
10
12
  export declare const INSPECTOR_BODY_PREVIEW_CHARS = 320;
11
13
  export declare const DOCTOR_ACTION_PREVIEW_CHARS = 72;
12
14
  export declare const SESSION_EVIDENCE_MAX_TURNS = 100;
@@ -1,12 +1,14 @@
1
1
  /**
2
- * Shared output and preview size limits.
3
- * Zones: output governance, Trace previews, inspect defaults
2
+ * Shared output, preview, and Control envelope limits.
3
+ * Zones: output governance, Trace previews, Inspect defaults, Control portability
4
4
  */
5
5
  export const DEFAULT_INSPECT_LINES = 40;
6
6
  export const TOOL_OUTPUT_MAX_BYTES = 50 * 1024;
7
7
  export const TOOL_OUTPUT_MAX_LINES = 2_000;
8
8
  export const COMPACT_PREVIEW_CHARS = 160;
9
- export const CONTROL_INPUT_MAX_BYTES = 64 * 1024;
9
+ export const CONTROL_ACTION_MAX_LENGTH = 64;
10
+ export const CONTROL_INPUT_MAX_BYTES = 380;
11
+ export const CONTROL_WIRE_MAX_BYTES = 512;
10
12
  export const INSPECTOR_BODY_PREVIEW_CHARS = 320;
11
13
  export const DOCTOR_ACTION_PREVIEW_CHARS = 72;
12
14
  export const SESSION_EVIDENCE_MAX_TURNS = 100;
@@ -6,7 +6,7 @@
6
6
  export declare const REGISTER_TOOL_DESCRIPTION: string;
7
7
  export declare const REGISTER_TOOL_PROMPT_SNIPPET = "Register persistent command templates as agent-callable tools";
8
8
  export declare const REGISTER_TOOL_GUIDELINES: string[];
9
- export declare const ONBOARDING_SYSTEM_PROMPT = "pi-actors quick model:\n- Local-first actor memory: persist trusted local capabilities instead of rebuilding shell recipes.\n- Layers: task -> command template -> recipe/tool -> spawn -> run:<id>; tool:<name> wraps registered capabilities.\n- Command templates stay sync and shell-free: string leaves split into executable + argv, so operators such as && are literal arguments; use template arrays for sequencing or an explicit trusted shell/script when shell semantics are required. Flags include args/defaults, parallel, concurrency, min_successful, when, timeout, delay, retry, failure, recover, repeat, accept_output, output.\n- Placeholders support typed/default args plus {value??fallback} and {flag?yes:no}.\n- ~/.pi/agent/recipes/*.json is actor muscle memory: every recipe there is auto-registered as an agent tool across sessions; register_tool writes there.\n- Recipes own template directly and may declare metadata/defaults/imports/control/artifacts; files >1 MiB or import depth >32 fail closed.\n- Recipe imports are local variables; imported recipes are definitions, not nested async runs; parent async:true creates one run.\n- Actor-mode trigger: if work may outlive this turn, need steering/follow-up/artifacts, run as a service, fan out, or be resumed/inspected later, use spawn -> message -> inspect instead of ad hoc shell backgrounding.\n- Use spawn/message/inspect for actor-level start/send/observe; short foreground checks can stay ordinary tools/templates; avoid internal transport vocabulary in public guidance.\n- Run state lives under ~/.pi/agent/tmp/pi-actors/runs. Inspect intentionally and avoid busy-polling. Terminal and coordinator-bound notifications queue as Pi follow-ups so concurrently completed actors can reach the coordinator after current work instead of steering between tool calls. Terminal follow-up content stays minimal: run, status, one base path, and relative artifact names only; semantic output stays in non-LLM details and run state. When a deferred actor result gates the next step, wait for its terminal follow-up; do not schedule continuation loops, repeatedly inspect, or mutate its reviewed scope while it runs. Inspect early only for an operator request, a meaningful actor event, or diagnosis of an overdue/stuck run.\n- Maintain ~/.pi/agent/recipes like MEMORY.md for capabilities: keep useful tools, curate stale ones, and fix/remove/disable invalid recipes flagged by registry warnings; packaged/ad hoc recipes are lower-priority components; offer to save successful recurring patterns only after confirmation.\n- Prefer maintained packaged recipes/pipelines with spawn file=<recipe> before ad hoc scripts/wrappers; review swarms inherit current model/thinking, preflight before fanout, and expose quorum/concurrency/TTL knobs unless explicit args are passed.\n- For any non-trivial actor use or pi-actors change, read the bundled actors skill first. Before launching multiple actors/subagents for parallel implementation, independent artifact generation, delegated audit, or review, also read the bundled swarm skill; the coordinator owns decomposition, disjoint scopes, launch correctness, integration, and final validation. For deeper guidance, inspect installed extension sources/docs/recipes because README/docs are not automatically in context.";
9
+ export declare const ONBOARDING_SYSTEM_PROMPT = "pi-actors quick model:\n- Local-first actor memory: persist trusted local capabilities instead of rebuilding shell recipes.\n- Layers: task -> command template -> recipe/tool -> spawn -> run:<id>; tool:<name> wraps registered capabilities.\n- Command templates stay sync and shell-free: string leaves split into executable + argv, infer .js/.mjs through node\u2192bun\u2192deno run and .sh through bash, and treat operators such as && as literal arguments; use template arrays for sequencing or an explicit trusted shell/script when shell semantics are required. Flags include args/defaults, parallel, concurrency, min_successful, when, timeout, delay, retry, failure, recover, repeat, accept_output, output.\n- Placeholders support typed/default args plus {value??fallback} and {flag?yes:no}.\n- ~/.pi/agent/recipes/*.json is actor muscle memory: every recipe there is auto-registered as an agent tool across sessions; register_tool writes there.\n- Recipes own template directly and may declare metadata/defaults/imports/control/artifacts; files >1 MiB or import depth >32 fail closed.\n- Recipe imports are local variables; imported recipes are definitions, not nested async runs; parent async:true creates one run.\n- Actor-mode trigger: if work may outlive this turn, need steering/follow-up/artifacts, run as a service, fan out, or be resumed/inspected later, use spawn -> message -> inspect instead of ad hoc shell backgrounding.\n- Use spawn/message/inspect for actor-level start/send/observe; short foreground checks can stay ordinary tools/templates; avoid internal transport vocabulary in public guidance.\n- Run state lives under ~/.pi/agent/tmp/pi-actors/runs. Inspect intentionally and avoid busy-polling. Terminal and coordinator-bound notifications queue as Pi follow-ups so concurrently completed actors can reach the coordinator after current work instead of steering between tool calls. Terminal follow-up content stays minimal: run, status, one base path, and relative artifact names only; semantic output stays in non-LLM details and run state. When a deferred actor result gates the next step, wait for its terminal follow-up; do not schedule continuation loops, repeatedly inspect, or mutate its reviewed scope while it runs. Inspect early only for an operator request, a meaningful actor event, or diagnosis of an overdue/stuck run.\n- Maintain ~/.pi/agent/recipes like MEMORY.md for capabilities: keep useful tools, curate stale ones, and fix/remove/disable invalid recipes flagged by registry warnings; packaged/ad hoc recipes are lower-priority components; offer to save successful recurring patterns only after confirmation.\n- Prefer maintained packaged recipes/pipelines with spawn file=<recipe> before ad hoc scripts/wrappers; review swarms inherit current model/thinking, preflight before fanout, and expose quorum/concurrency/TTL knobs unless explicit args are passed.\n- For any non-trivial actor use or pi-actors change, read the bundled actors skill first. Before launching multiple actors/subagents for parallel implementation, independent artifact generation, delegated audit, or review, also read the bundled swarm skill; the coordinator owns decomposition, disjoint scopes, launch correctness, integration, and final validation. For deeper guidance, inspect installed extension sources/docs/recipes because README/docs are not automatically in context.";
10
10
  export declare const REGISTER_TOOL_PARAM_DESCRIPTIONS: {
11
11
  readonly name: "Tool name in snake_case (e.g., 'transcribe')";
12
12
  readonly description: "Describe what the tool does for the LLM. Required unless deleting; omitted updates keep the old description.";
@@ -16,7 +16,7 @@ export const REGISTER_TOOL_GUIDELINES = [
16
16
  export const ONBOARDING_SYSTEM_PROMPT = `pi-actors quick model:
17
17
  - Local-first actor memory: persist trusted local capabilities instead of rebuilding shell recipes.
18
18
  - Layers: task -> command template -> recipe/tool -> spawn -> run:<id>; tool:<name> wraps registered capabilities.
19
- - Command templates stay sync and shell-free: string leaves split into executable + argv, so operators such as && are literal arguments; use template arrays for sequencing or an explicit trusted shell/script when shell semantics are required. Flags include args/defaults, parallel, concurrency, min_successful, when, timeout, delay, retry, failure, recover, repeat, accept_output, output.
19
+ - Command templates stay sync and shell-free: string leaves split into executable + argv, infer .js/.mjs through node→bun→deno run and .sh through bash, and treat operators such as && as literal arguments; use template arrays for sequencing or an explicit trusted shell/script when shell semantics are required. Flags include args/defaults, parallel, concurrency, min_successful, when, timeout, delay, retry, failure, recover, repeat, accept_output, output.
20
20
  - Placeholders support typed/default args plus {value??fallback} and {flag?yes:no}.
21
21
  - ~/.pi/agent/recipes/*.json is actor muscle memory: every recipe there is auto-registered as an agent tool across sessions; register_tool writes there.
22
22
  - Recipes own template directly and may declare metadata/defaults/imports/control/artifacts; files >1 MiB or import depth >32 fail closed.
@@ -3,7 +3,8 @@
3
3
  * Zones: actor-local action declarations, normalization, reserved-action fencing
4
4
  * Owns pure Recipe control validation; Recipe loading and Run capture stay in recipe/run domains.
5
5
  */
6
- const ACTION_PATTERN = /^[a-z][a-z0-9_-]*(?:\.[a-z0-9_-]+)*$/;
6
+ import * as Control from "./control.js";
7
+ import * as Limits from "./limits.js";
7
8
  const RUNTIME_RUN_ACTIONS = new Set(["archive", "kill", "prune"]);
8
9
  export function normalizeRecipeControl(value) {
9
10
  if (value === undefined)
@@ -18,9 +19,12 @@ export function normalizeRecipeControl(value) {
18
19
  throw new Error("recipe.control actions must be non-empty strings");
19
20
  }
20
21
  const action = raw.trim();
21
- if (!ACTION_PATTERN.test(action)) {
22
+ if (!Control.isControlAction(action)) {
22
23
  throw new Error(`invalid recipe.control action: ${action}`);
23
24
  }
25
+ if (action.length > Limits.CONTROL_ACTION_MAX_LENGTH) {
26
+ throw new Error(`recipe.control action exceeds ${Limits.CONTROL_ACTION_MAX_LENGTH} ASCII characters: ${action}`);
27
+ }
24
28
  if (RUNTIME_RUN_ACTIONS.has(action)) {
25
29
  throw new Error(`recipe.control action is runtime-reserved and must not be declared: ${action}`);
26
30
  }
@@ -11,4 +11,4 @@ export interface AutomaticReviewControlOptions {
11
11
  toolStatePath?: string;
12
12
  }
13
13
  export declare function controlAutomaticReview(action: AutomaticReviewControlAction, scope: AutomaticReviewScope, options?: AutomaticReviewControlOptions): Record<string, unknown>;
14
- export declare function parseAutomaticReviewScope(body: unknown): AutomaticReviewScope;
14
+ export declare function parseAutomaticReviewScope(input: unknown): AutomaticReviewScope;
@@ -98,14 +98,13 @@ export function controlAutomaticReview(action, scope, options = {}) {
98
98
  return {
99
99
  ...result,
100
100
  next_actions: nextActions,
101
- sent: result.changed === true,
102
101
  };
103
102
  }
104
- export function parseAutomaticReviewScope(body) {
105
- const record = body && typeof body === "object" && !Array.isArray(body)
106
- ? body
103
+ export function parseAutomaticReviewScope(input) {
104
+ const record = input && typeof input === "object" && !Array.isArray(input)
105
+ ? input
107
106
  : {};
108
107
  if (record.scope === "draft" || record.scope === "tool")
109
108
  return record.scope;
110
- throw new Error("review control requires body.scope=draft or body.scope=tool.");
109
+ throw new Error("review Control requires input.scope=draft or input.scope=tool.");
111
110
  }
@@ -16,6 +16,13 @@ export interface DeliverRunControlRequest {
16
16
  input?: unknown;
17
17
  run_instance_id: string;
18
18
  }
19
- export declare const FIFO_ATOMIC_CONTROL_MAX_BYTES = 512;
19
+ export declare function encodeRunControlWire(wire: {
20
+ action: string;
21
+ id: string;
22
+ input?: unknown;
23
+ }): {
24
+ bytes: number;
25
+ payload: string;
26
+ };
20
27
  export declare function readRunControlEndpoint(stateDir: string, runInstanceId: string): RunControlEndpoint | undefined;
21
28
  export declare function deliverRunControl(run: string, stateDir: string, request: DeliverRunControlRequest, options?: DeliverRunControlOptions): Promise<Record<string, unknown>>;
@@ -6,9 +6,23 @@
6
6
  import { closeSync, constants, existsSync, openSync, statSync, writeSync, } from "node:fs";
7
7
  import { createConnection } from "node:net";
8
8
  import { join } from "node:path";
9
+ import * as Control from "./control.js";
10
+ import * as Limits from "./limits.js";
9
11
  import { appendRunControlInStateDir, updateRunControlStatusInStateDir, } from "./runs-controls.js";
10
12
  import { readJsonFileResilient } from "./state-readers.js";
11
- export const FIFO_ATOMIC_CONTROL_MAX_BYTES = 512;
13
+ const PREFLIGHT_CONTROL_ID = "00000000-0000-4000-8000-000000000000";
14
+ export function encodeRunControlWire(wire) {
15
+ const payload = `${JSON.stringify({
16
+ id: wire.id,
17
+ action: wire.action,
18
+ ...(wire.input !== undefined ? { input: wire.input } : {}),
19
+ })}\n`;
20
+ const bytes = Buffer.byteLength(payload);
21
+ if (bytes > Limits.CONTROL_WIRE_MAX_BYTES) {
22
+ throw new Error(`Run Control wire record exceeds the ${Limits.CONTROL_WIRE_MAX_BYTES}-byte portable bound`);
23
+ }
24
+ return { bytes, payload };
25
+ }
12
26
  export function readRunControlEndpoint(stateDir, runInstanceId) {
13
27
  const endpoint = readJsonFileResilient(join(stateDir, "control-endpoint.json"), {}).value;
14
28
  if (endpoint.run_instance_id !== runInstanceId)
@@ -65,34 +79,43 @@ function sendToNamedPipe(endpoint, payload, send) {
65
79
  });
66
80
  }
67
81
  export async function deliverRunControl(run, stateDir, request, options = {}) {
68
- const control = appendRunControlInStateDir(stateDir, request);
82
+ const action = Control.normalizeControlAction(request.action);
83
+ const input = request.input === undefined
84
+ ? undefined
85
+ : Control.normalizeControlInput(request.input);
86
+ const normalizedRequest = {
87
+ action,
88
+ ...(input !== undefined ? { input } : {}),
89
+ run_instance_id: request.run_instance_id,
90
+ };
91
+ encodeRunControlWire({
92
+ id: PREFLIGHT_CONTROL_ID,
93
+ action,
94
+ ...(input !== undefined ? { input } : {}),
95
+ });
96
+ const control = appendRunControlInStateDir(stateDir, normalizedRequest);
69
97
  const endpoint = readRunControlEndpoint(stateDir, request.run_instance_id);
70
98
  if (!endpoint) {
71
99
  updateRunControlStatusInStateDir(stateDir, control.id, "failed", { error: "control endpoint is not ready for this Run generation" }, ["queued", "delivered"]);
72
100
  throw Object.assign(new Error("Run Control endpoint is not ready."), {
73
- action: request.action,
101
+ action,
74
102
  control_id: control.id,
75
103
  reason: "endpoint_not_ready",
76
104
  run,
77
105
  run_instance_id: request.run_instance_id,
78
106
  });
79
107
  }
80
- const wire = {
81
- id: control.id,
82
- action: request.action,
83
- ...(request.input !== undefined ? { input: request.input } : {}),
84
- };
85
- const payload = `${JSON.stringify(wire)}\n`;
86
- const bytes = Buffer.byteLength(payload);
87
108
  try {
109
+ const { bytes, payload } = encodeRunControlWire({
110
+ id: control.id,
111
+ action,
112
+ ...(input !== undefined ? { input } : {}),
113
+ });
88
114
  let written;
89
115
  if (endpoint.type === "fifo") {
90
116
  if ((options.platform ?? process.platform) === "win32") {
91
117
  throw new Error("FIFO Control delivery is unsupported on native Windows");
92
118
  }
93
- if (bytes > FIFO_ATOMIC_CONTROL_MAX_BYTES) {
94
- throw new Error(`FIFO Control payload exceeds the ${FIFO_ATOMIC_CONTROL_MAX_BYTES}-byte portable atomic-write bound`);
95
- }
96
119
  written = sendToFifo(endpoint, payload);
97
120
  }
98
121
  else {
@@ -103,7 +126,7 @@ export async function deliverRunControl(run, stateDir, request, options = {}) {
103
126
  }
104
127
  updateRunControlStatusInStateDir(stateDir, control.id, "delivered");
105
128
  return {
106
- action: request.action,
129
+ action,
107
130
  bytes,
108
131
  control_id: control.id,
109
132
  delivery: "delivered",
@@ -116,7 +139,7 @@ export async function deliverRunControl(run, stateDir, request, options = {}) {
116
139
  const reason = error instanceof Error ? error.message : String(error);
117
140
  updateRunControlStatusInStateDir(stateDir, control.id, "failed", { error: reason }, ["queued", "delivered"]);
118
141
  throw Object.assign(new Error(`Run Control delivery failed: ${reason}`), {
119
- action: request.action,
142
+ action,
120
143
  control_id: control.id,
121
144
  endpoint_type: endpoint.type,
122
145
  reason: "delivery_failed",
@@ -3,6 +3,7 @@
3
3
  * Zones: durable Control records, claim locks, status transitions, bounded compaction
4
4
  * Owns Run-local Control persistence; owner/generation authorization and transport delivery stay in lifecycle adapters.
5
5
  */
6
+ import { type JsonlReadResult } from "./state-readers.ts";
6
7
  export declare const RUN_CONTROL_TERMINAL_LIMIT = 128;
7
8
  export type RunControlStatus = "queued" | "delivered" | "claimed" | "handled" | "failed";
8
9
  export interface RunControlRecord {
@@ -24,6 +25,7 @@ export interface ProcessRunControlsResult {
24
25
  handled: number;
25
26
  }
26
27
  export declare function runControlsFile(stateDir: string): string;
28
+ export declare function readRunControlJournalFromStateDir(stateDir: string): JsonlReadResult<unknown>;
27
29
  export declare function readRunControlsFromStateDir(stateDir: string): RunControlRecord[];
28
30
  export declare function appendRunControlInStateDir(stateDir: string, request: {
29
31
  run_instance_id: string;
@@ -7,15 +7,17 @@ import { randomUUID } from "node:crypto";
7
7
  import { writeFileSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
  import { acquireFileMutationLock, writeTextAtomic } from "./file-state.js";
10
- import { readJsonlFileResilient } from "./state-readers.js";
10
+ import { readJsonlFileResilient, } from "./state-readers.js";
11
11
  export const RUN_CONTROL_TERMINAL_LIMIT = 128;
12
12
  const TERMINAL_STATUSES = new Set(["handled", "failed"]);
13
13
  export function runControlsFile(stateDir) {
14
14
  return join(stateDir, "controls.jsonl");
15
15
  }
16
+ export function readRunControlJournalFromStateDir(stateDir) {
17
+ return readJsonlFileResilient(runControlsFile(stateDir));
18
+ }
16
19
  export function readRunControlsFromStateDir(stateDir) {
17
- return readJsonlFileResilient(runControlsFile(stateDir))
18
- .records;
20
+ return readRunControlJournalFromStateDir(stateDir).records;
19
21
  }
20
22
  function acquireRunControlsLock(stateDir) {
21
23
  return acquireFileMutationLock(runControlsFile(stateDir));
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Run Trace event journal.
3
- * Zones: structured event validation, bounded append, resilient bounded reads
4
- * Owns Run-local semantic Trace events; lifecycle projection and owner attention delivery stay in adapters.
3
+ * Zones: structured event validation, token-locked append-only writes, resilient bounded reads
4
+ * Owns canonical Run-local Trace persistence; lifecycle projection and owner attention delivery stay in adapters.
5
5
  */
6
6
  export interface TraceEvent {
7
7
  id: string;