@llblab/pi-actors 0.40.0 → 0.41.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 (45) hide show
  1. package/AGENTS.md +3 -3
  2. package/BACKLOG.md +22 -1
  3. package/CHANGELOG.md +31 -0
  4. package/README.md +5 -4
  5. package/dist/index.js +30 -51
  6. package/dist/lib/inspector-overlay.d.ts +84 -0
  7. package/dist/lib/inspector-overlay.js +627 -0
  8. package/dist/lib/inspector.d.ts +17 -33
  9. package/dist/lib/inspector.js +98 -151
  10. package/dist/lib/limits.d.ts +3 -0
  11. package/dist/lib/limits.js +3 -0
  12. package/dist/lib/observability.d.ts +1 -1
  13. package/dist/lib/observability.js +2 -2
  14. package/dist/lib/pi.d.ts +1 -1
  15. package/dist/lib/pi.js +2 -2
  16. package/dist/lib/prompts.d.ts +1 -1
  17. package/dist/lib/prompts.js +3 -3
  18. package/dist/lib/recipes-context.d.ts +5 -0
  19. package/dist/lib/recipes-context.js +15 -0
  20. package/dist/lib/session-evidence.d.ts +45 -0
  21. package/dist/lib/session-evidence.js +204 -0
  22. package/dist/lib/tools-response.d.ts +3 -0
  23. package/dist/lib/tools-response.js +31 -0
  24. package/dist/scripts/async-runner.mjs +40 -5
  25. package/dist/scripts/coordinator.mjs +55 -9
  26. package/dist/skills/actors/SKILL.md +10 -9
  27. package/dist/skills/swarm/SKILL.md +4 -2
  28. package/docs/README.md +1 -0
  29. package/docs/actor-inspector.md +72 -0
  30. package/docs/async-runs.md +2 -2
  31. package/index.ts +32 -63
  32. package/lib/inspector-overlay.ts +735 -0
  33. package/lib/inspector.ts +132 -204
  34. package/lib/limits.ts +3 -0
  35. package/lib/observability.ts +3 -3
  36. package/lib/pi.ts +3 -3
  37. package/lib/prompts.ts +3 -3
  38. package/lib/recipes-context.ts +25 -0
  39. package/lib/session-evidence.ts +302 -0
  40. package/lib/tools-response.ts +33 -0
  41. package/package.json +1 -1
  42. package/scripts/async-runner.mjs +40 -5
  43. package/scripts/coordinator.mjs +55 -9
  44. package/skills/actors/SKILL.md +10 -9
  45. package/skills/swarm/SKILL.md +4 -2
@@ -0,0 +1,72 @@
1
+ # Actor Inspector
2
+
3
+ The actor inspector is a manually opened, read-only TUI navigator for owned actor runs. It keeps communication evidence and persisted subagent execution evidence in one hierarchy without merging their meanings.
4
+
5
+ ```text
6
+ owned run
7
+ → messages | turns
8
+ → filtered timeline
9
+ → bounded detail
10
+ ```
11
+
12
+ ## Navigation
13
+
14
+ `/actors-inspector-toggle` opens one centered overlay and remains the only command to remember. The first run owned by the current Pi session becomes the active run automatically; an empty session still exposes functional tabs and filters.
15
+
16
+ The overlay exposes an explicit focus hierarchy:
17
+
18
+ ```text
19
+ Run Enter opens owned runs, ↓ enters tabs
20
+ Tabs ←/→ chooses Messages or Turns, Enter opens filter parameters
21
+ Filters ↑/↓ chooses Channel/State or Subagent, Enter opens values to the right
22
+ Values ↑/↓ hovers, Enter applies, Escape returns one menu level
23
+ List ↑/↓ chooses, Enter/→ opens detail
24
+ Detail ↑/↓ scroll, Escape/← returns
25
+ Escape Close (or cancel the active options popup)
26
+ ```
27
+
28
+ Navigation stays bounded by available actions. `↑` on Run does nothing because no higher control exists. `↓` on Tabs enters the timeline only when it contains rows. Empty timelines therefore never receive focus.
29
+
30
+ Selection and focus remain separate visual states. Accent-blue text marks the current tab, active filter popup, and applied option. A light neutral background plus `▶` marks every selectable control or timeline row that currently owns keyboard focus. Opening a popup keeps its parent filter blue so the relationship remains visible. The footer uses accent color only for key names and arrows; descriptions remain muted.
31
+
32
+ The top Run control aligns vertically with the tab labels, names the selected owned run, and colors its textual lifecycle status semantically. Enter opens available owned runs immediately beneath it, overlaying the tab row rather than leaving a detached gap. The timeline no longer renders run metadata as a data row.
33
+
34
+ Filters live behind their tab rather than occupying a permanent row. Non-default filters remain visible as compact suffixes in the tab label, so hidden state never silently changes the timeline. Enter on Messages opens `Channel: <current>`, `State: <current>`, and `From: <current>`; `From` draws its values from the selected run's roster and limits rows to one actor. Enter on Turns opens `Subagent: <current>`. Enter on a parameter opens its alternative values as a second menu to the right while the parent and current value remain visible. Parent and child share their touching border rather than leaving or doubling a spacer column. Escape returns one level at a time. Moving focus never applies a value.
35
+
36
+ Nested menus overlay rather than replace the timeline. Only rows and columns containing menu borders or values occlude underlying cells. When adjacent menus have different heights, the unused corner remains transparent and preserves the separator, striped background, and timeline data beneath it.
37
+
38
+ The overlay uses most of the available terminal width and height. The bordered header keeps both tabs visible, while the list body shows the selected run and its current status above the evidence rows. Evidence rows retain stable alternating backgrounds based on their absolute timeline position, including while scrolling: even rows keep the dark overlay background, while odd rows use the neutral `customMessageBg` stripe. The footer exposes the active keys. Messages retain attention markers and unread filtering and open into bounded detail without leaving the overlay. The overlay refreshes while visible and distinguishes true empty timelines from filtered-empty results; filtered-empty copy points back to Enter on the active tab without moving focus.
39
+
40
+ ## Communication Timeline
41
+
42
+ The communication timeline reads run-local room, direct, branch-inbox, and coordinator/session message evidence. It preserves channel/sender filters, unread state, attention markers, roster-derived sender options, and bounded body previews. Unread remains filterable but does not consume a row column with a separate dot marker.
43
+
44
+ Communication evidence describes messages between actors. It does not prove model execution.
45
+
46
+ ## Turns Timeline
47
+
48
+ Detached child `pi -p` commands receive isolated session storage under their owned run state:
49
+
50
+ ```text
51
+ <run-state>/sessions/command-NNN/*.jsonl
52
+ ```
53
+
54
+ The runner records direct command-template session files in `review-evidence.json`. Coordinator-managed room/swarm participants also persist role/phase-scoped directories under the same `sessions/` root; the inspector discovers those owned files even though the coordinator, rather than the command-template runner, launched them. Explicit caller session policy (`--no-session`, `--session`, `--session-id`, `--session-dir`, or `--fork`) remains authoritative and is not replaced. A command may therefore have no inspector-visible session.
55
+
56
+ The turns timeline follows the latest persisted entry branch in each recorded Pi session and groups:
57
+
58
+ - User input associated with the response;
59
+ - Assistant text and host-persisted thinking blocks;
60
+ - Provider, model, stop reason, usage, and error metadata;
61
+ - Tool calls in assistant source order;
62
+ - Tool results correlated by `toolCallId`, regardless of completion order.
63
+
64
+ Enter opens the selected turn inside the overlay. Detail adds command/stage identity, session and prompt paths, recipe-context reference, tool arguments/results, truncation state, unmatched result counts, and parse diagnostics. ↑/↓ scroll long detail while the footer keeps return/close keys visible.
65
+
66
+ ## Evidence And Privacy Boundary
67
+
68
+ The inspector reads file-backed evidence; it does not reconstruct hidden provider reasoning or claim access to data Pi did not persist. When no explicit thinking block exists, detail shows `reasoning unavailable`.
69
+
70
+ Session text, communication bodies, and structured values remain bounded. Common secret-bearing keys, camelCase/private-key credentials, serialized JSON credentials, and inline credential patterns are redacted before rendering. Malformed JSONL lines, missing parents, cycles, missing sessions, and incomplete tool correlation remain diagnostic states rather than inferred data.
71
+
72
+ Ownership filtering happens before run summaries, communication previews, roster data, or session evidence become visible. Selection and read state reset across Pi sessions. Manifest session paths must resolve canonically beneath the selected owned run's `sessions/` directory; absolute paths, traversal, and symlink escapes remain invisible. The inspector never scans another coordinator session's run state into the current view.
@@ -216,9 +216,9 @@ Runtime wake notifications are now modeled separately from durable queues. Messa
216
216
 
217
217
  ## Coordinator Notifications
218
218
 
219
- The launching coordinator should not busy-poll long-running async runs. The extension watches run state directories and steers terminal `done`/`failed`/unhandled `killed`/`exited` transitions back to the owning session with `triggerTurn: true`; a busy coordinator receives completion at the next safe tool boundary, while an idle coordinator starts a normal turn without a racy manual idle check. Script-authored `notify`/`followup` actor messages still follow their declared outbox delivery policy. Terminal notifications include recipe-level named `artifacts` when declared. The generic runner also emits compact `command.done` actor messages for completed leaf commands; recipe authors declare that capability in `mailbox.emits` rather than configuring a separate delivery policy. Failures and in-flight parallel branch completions can bubble according to outbox policy, while successful final leaf completions stay diagnostic to avoid flooding long sequential pipelines. Intentional `control.kill` and recipe-local stop commands stay out of coordinator context because the initiating message already returns synchronously or is handled by actor-local policy. If a notification asks for direction, answer with `message` rather than starting a polling loop. Use explicit `inspect` only when a delivered notification requests inspection, a real decision depends on state, or a suspected stuck run needs diagnosis — never merely because a timeout elapsed.
219
+ The launching coordinator should not busy-poll long-running async runs. The extension watches run state directories and queues terminal `done`/`failed`/unhandled `killed`/`exited` transitions back to the owning session through Pi's `followUp` delivery mode with `triggerTurn: true`; a busy coordinator finishes its current work before queued actor results arrive, while an idle coordinator starts a normal turn without a racy manual idle check. Pi's configured `followUpMode` determines whether concurrently queued results arrive together or one at a time. Script-authored `notify`/`followup` actor messages still follow their declared outbox delivery policy. Terminal notifications include recipe-level named `artifacts` when declared. The generic runner also emits compact `command.done` actor messages for completed leaf commands; recipe authors declare that capability in `mailbox.emits` rather than configuring a separate delivery policy. Failures and in-flight parallel branch completions can bubble according to outbox policy, while successful final leaf completions stay diagnostic to avoid flooding long sequential pipelines. Intentional `control.kill` and recipe-local stop commands stay out of coordinator context because the initiating message already returns synchronously or is handled by actor-local policy. If a notification asks for direction, answer with `message` rather than starting a polling loop. Use explicit `inspect` only when a delivered notification requests inspection, a real decision depends on state, or a suspected stuck run needs diagnosis — never merely because a timeout elapsed.
220
220
 
221
- Ambient status indicators may refresh while work is active, but coordinator attention is driven from run-state changes rather than a coordinator agent loop. This lets the coordinator continue other work after `spawn`; the run signals back through lifecycle state, results, and actor messages. An owned terminal run without `terminal-handled.json` remains retry-eligible during same-runtime and extension/session replacement reconciliation; the marker is written only after successful steering delivery, and initial reconciliation does not replay historical outbox traffic. This is an at-least-once contract: a process crash after send but before marker persistence can produce a duplicate notification, while a failed send remains durably retryable. The ambient triangle count represents active async work units: each running async run contributes at least one triangle, and a run with multiple active parallel command/subagent branches contributes the reported active branch count. If a coordinator starts one parent run with four active parallel branches, four triangles are shown; if the same coordinator starts five independent single-branch runs, five triangles are shown.
221
+ Ambient status indicators may refresh while work is active, but coordinator attention is driven from run-state changes rather than a coordinator agent loop. This lets the coordinator continue other work after `spawn`; the run signals back through lifecycle state, results, and actor messages. An owned terminal run without `terminal-handled.json` remains retry-eligible during same-runtime and extension/session replacement reconciliation; the marker is written only after successful follow-up delivery, and initial reconciliation does not replay historical outbox traffic. This is an at-least-once contract: a process crash after send but before marker persistence can produce a duplicate notification, while a failed send remains durably retryable. The ambient triangle count represents active async work units: each running async run contributes at least one triangle, and a run with multiple active parallel command/subagent branches contributes the reported active branch count. If a coordinator starts one parent run with four active parallel branches, four triangles are shown; if the same coordinator starts five independent single-branch runs, five triangles are shown.
222
222
 
223
223
  ## Run Actor Messages
224
224
 
package/index.ts CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import * as AsyncRuns from "./lib/async-runs.ts";
9
9
  import * as CommandTemplates from "./lib/command-templates.ts";
10
- import * as Inspector from "./lib/inspector.ts";
10
+ import * as InspectorOverlay from "./lib/inspector-overlay.ts";
11
11
  import * as Observability from "./lib/observability.ts";
12
12
  import * as Paths from "./lib/paths.ts";
13
13
  import * as Pi from "./lib/pi.ts";
@@ -15,6 +15,7 @@ import * as Prompts from "./lib/prompts.ts";
15
15
  import * as Runtime from "./lib/runtime.ts";
16
16
  import * as Temp from "./lib/temp.ts";
17
17
  import * as Tools from "./lib/tools.ts";
18
+ import * as ToolsResponse from "./lib/tools-response.ts";
18
19
 
19
20
  export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
20
21
  let runsAnimationInterval: NodeJS.Timeout | undefined;
@@ -22,7 +23,6 @@ export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
22
23
  let activeRunContext: Pi.ExtensionContext | undefined;
23
24
  const runUi = Observability.createRunUiObservationState();
24
25
  const retirementAttempts = new Set<string>();
25
- const actorInspector = Inspector.createActorInspectorControllerState();
26
26
  const getRunOwnerId = Pi.getSessionId;
27
27
  const retireCandidateRuns = (
28
28
  ctx: Pi.ExtensionContext,
@@ -47,33 +47,6 @@ export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
47
47
  "zz-pi-actors-runs",
48
48
  snapshot.status ? ctx.ui.theme.fg("dim", snapshot.status) : undefined,
49
49
  );
50
- ctx.ui.setWidget(
51
- "zz-pi-actors-comms",
52
- actorInspector.visible
53
- ? () => ({
54
- invalidate() {},
55
- render(width: number) {
56
- return Inspector.renderActorInspectorPanel({
57
- stateRoot: Paths.EXTENSION_RUNTIME_PATHS.runStateRoot,
58
- state: actorInspector,
59
- ownerId,
60
- width,
61
- style: {
62
- actor: (text: string) => ctx.ui.theme.fg("accent", text),
63
- muted: (text: string) => ctx.ui.theme.fg("dim", text),
64
- preview: (text: string) => ctx.ui.theme.fg("text", text),
65
- stripe: (text: string) => text,
66
- stripeAlt: (text: string) =>
67
- ctx.ui.theme.bg("customMessageBg", text),
68
- target: (text: string) => ctx.ui.theme.fg("success", text),
69
- type: (text: string) => ctx.ui.theme.fg("warning", text),
70
- },
71
- });
72
- },
73
- })
74
- : undefined,
75
- { placement: "belowEditor" },
76
- );
77
50
  if (!notify) return;
78
51
  const notificationSink = Pi.createNotificationSink(pi, ctx);
79
52
  retireCandidateRuns(ctx, snapshot.summary);
@@ -115,7 +88,7 @@ export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
115
88
  const execute = definition.execute as (...args: unknown[]) => unknown;
116
89
  return {
117
90
  ...definition,
118
- execute: (...args: unknown[]) => {
91
+ execute: async (...args: unknown[]) => {
119
92
  const nextArgs = [...args];
120
93
  const ctx = nextArgs[4];
121
94
  if (ctx && typeof ctx === "object") {
@@ -124,7 +97,11 @@ export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
124
97
  getThinkingLevel: () => pi.getThinkingLevel(),
125
98
  };
126
99
  }
127
- return execute(...nextArgs);
100
+ try {
101
+ return ToolsResponse.spaceToolResult(await execute(...nextArgs));
102
+ } catch (error) {
103
+ throw ToolsResponse.spaceToolError(error);
104
+ }
128
105
  },
129
106
  } as T;
130
107
  };
@@ -147,6 +124,8 @@ export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
147
124
  return { skillPaths };
148
125
  });
149
126
  pi.on("session_start", async (_event, ctx) => {
127
+ // Clear the pre-overlay widget after hot reloads from older pi-actors builds.
128
+ ctx.ui.setWidget("zz-pi-actors-comms", undefined);
150
129
  activeRunContext = ctx;
151
130
  await Temp.prepareExtensionTempDir(Paths.EXTENSION_RUNTIME_PATHS.tempDir);
152
131
  runtime.loadTools(ctx);
@@ -167,39 +146,29 @@ export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
167
146
  recipeReload.close();
168
147
  });
169
148
  pi.registerCommand("actors-inspector-toggle", {
170
- description: Inspector.ACTOR_INSPECTOR_COMMAND_DESCRIPTIONS.toggle,
171
- handler: async (args, ctx) => {
172
- const result = Inspector.handleActorInspectorToggle(actorInspector, args);
173
- if (result.update) updateRunUi(ctx);
174
- ctx.ui.notify(result.notify, result.type);
175
- },
176
- });
177
- pi.registerCommand("actors-inspector-filter", {
178
- description: Inspector.ACTOR_INSPECTOR_COMMAND_DESCRIPTIONS.filter,
179
- handler: async (args, ctx) => {
180
- const result = Inspector.handleActorInspectorFilter(actorInspector, args);
181
- if (result.update) updateRunUi(ctx);
182
- ctx.ui.notify(result.notify, result.type);
183
- },
184
- });
185
- pi.registerCommand("actors-inspect", {
186
- description: Inspector.ACTOR_INSPECTOR_COMMAND_DESCRIPTIONS.inspect,
187
- handler: async (args, ctx) => {
188
- const previews = Inspector.readActorInspectorPreviews(
189
- Paths.EXTENSION_RUNTIME_PATHS.runStateRoot,
190
- actorInspector.rows,
191
- Inspector.getActorInspectorPreviewOptions(
192
- actorInspector,
193
- getRunOwnerId(ctx),
194
- ),
195
- );
196
- const result = Inspector.handleActorInspectorInspect(
197
- actorInspector,
198
- args,
199
- previews,
149
+ description: "Toggle the keyboard-driven actor inspector overlay",
150
+ handler: async (_args, ctx) => {
151
+ ctx.ui.setWidget("zz-pi-actors-comms", undefined);
152
+ await ctx.ui.custom<void>(
153
+ (tui, theme, _keybindings, done) =>
154
+ new InspectorOverlay.ActorInspectorOverlay({
155
+ done,
156
+ ownerId: getRunOwnerId(ctx),
157
+ stateRoot: Paths.EXTENSION_RUNTIME_PATHS.runStateRoot,
158
+ theme,
159
+ tui,
160
+ }),
161
+ {
162
+ overlay: true,
163
+ overlayOptions: {
164
+ anchor: "center",
165
+ width: "94%",
166
+ minWidth: 72,
167
+ maxHeight: "94%",
168
+ margin: 1,
169
+ },
170
+ },
200
171
  );
201
- if (result.update) updateRunUi(ctx);
202
- ctx.ui.notify(result.notify, result.type);
203
172
  },
204
173
  });
205
174
  pi.on("before_agent_start", async (event) => ({