@llblab/pi-actors 0.41.0 → 0.41.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.
- package/AGENTS.md +2 -2
- package/BACKLOG.md +0 -19
- package/CHANGELOG.md +7 -0
- package/dist/index.js +51 -8
- package/dist/lib/inspector-overlay.d.ts +11 -0
- package/dist/lib/inspector-overlay.js +321 -75
- package/dist/lib/inspector.js +4 -1
- package/dist/lib/observability.d.ts +38 -3
- package/dist/lib/observability.js +151 -33
- package/dist/lib/tools-response.d.ts +1 -1
- package/dist/lib/tools-response.js +5 -7
- package/dist/skills/actors/SKILL.md +3 -7
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/actor-inspector.md +18 -13
- package/docs/async-runs.md +1 -1
- package/index.ts +49 -8
- package/lib/inspector-overlay.ts +418 -82
- package/lib/inspector.ts +6 -3
- package/lib/observability.ts +205 -39
- package/lib/tools-response.ts +5 -6
- package/package.json +1 -1
- package/skills/actors/SKILL.md +3 -7
- package/skills/swarm/SKILL.md +1 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Zones: async runtime, ambient UI, diagnostics
|
|
4
4
|
* Owns ambient summaries, terminal events, and run outbox delivery for detached command-template runs
|
|
5
5
|
*/
|
|
6
|
+
import { type FSWatcher } from "node:fs";
|
|
6
7
|
import * as AsyncRuns from "./async-runs.ts";
|
|
7
8
|
export type RunObservedStatus = "running" | "done" | "failed" | "exited" | "cancelled" | "killed";
|
|
8
9
|
export type RunOutboxDelivery = "log" | "notify" | "followup";
|
|
@@ -58,9 +59,19 @@ export interface RunUiNotificationSink {
|
|
|
58
59
|
}): void;
|
|
59
60
|
}
|
|
60
61
|
export declare function createRunUiObservationState(): RunUiObservationState;
|
|
61
|
-
export declare function readRunUiSnapshot(state: RunUiObservationState, ownerId: string
|
|
62
|
+
export declare function readRunUiSnapshot(state: RunUiObservationState, ownerId: string, options?: {
|
|
63
|
+
includeOutbox?: boolean;
|
|
64
|
+
stateRoot?: string;
|
|
65
|
+
}): RunUiSnapshot;
|
|
62
66
|
export declare function pruneRunUiObservationState(state: RunUiObservationState, snapshot: Pick<RunUiSnapshot, "summary" | "transitions">): void;
|
|
63
|
-
export declare function deliverRunTransitionNotifications(transitions: RunTransition[], sink: RunUiNotificationSink): void;
|
|
67
|
+
export declare function deliverRunTransitionNotifications(transitions: RunTransition[], sink: RunUiNotificationSink, inFlight?: Set<string>): void;
|
|
68
|
+
export declare function reconcileRunTerminalNotifications(input: {
|
|
69
|
+
inFlight?: Set<string>;
|
|
70
|
+
ownerId: string;
|
|
71
|
+
sink: RunUiNotificationSink;
|
|
72
|
+
state: RunUiObservationState;
|
|
73
|
+
stateRoot?: string;
|
|
74
|
+
}): RunUiSnapshot;
|
|
64
75
|
export declare function deliverRunOutboxNotifications(events: RunOutboxEvent[], sink: RunUiNotificationSink): void;
|
|
65
76
|
export interface RunRetirementCandidate {
|
|
66
77
|
activeSubagents: number;
|
|
@@ -76,14 +87,38 @@ export interface RunRetirementExecution {
|
|
|
76
87
|
run: string;
|
|
77
88
|
stateDir: string;
|
|
78
89
|
}
|
|
90
|
+
export type RunStateWatcherDiagnosticCode = "attach_failed" | "error" | "removed" | "rearmed";
|
|
91
|
+
export interface RunStateWatcherDiagnostic {
|
|
92
|
+
code: RunStateWatcherDiagnosticCode;
|
|
93
|
+
id: number;
|
|
94
|
+
message: string;
|
|
95
|
+
path: string;
|
|
96
|
+
scope: "root" | "run";
|
|
97
|
+
ts: string;
|
|
98
|
+
}
|
|
79
99
|
export interface RunStateWatcher {
|
|
80
100
|
close(): void;
|
|
101
|
+
getDiagnostics(): RunStateWatcherDiagnostic[];
|
|
81
102
|
refresh(): void;
|
|
82
103
|
}
|
|
83
104
|
export declare function createRunStateWatcher(input: {
|
|
84
|
-
|
|
105
|
+
exists?: (path: string) => boolean;
|
|
106
|
+
listDirectories?: (path: string) => string[];
|
|
85
107
|
onChange: () => void;
|
|
108
|
+
stateRoot?: string;
|
|
109
|
+
watchPath?: (path: string, onChange: () => void) => FSWatcher;
|
|
86
110
|
}): RunStateWatcher;
|
|
111
|
+
export interface RunTerminalReconciliationLoop {
|
|
112
|
+
close(): void;
|
|
113
|
+
reconcileNow(): void;
|
|
114
|
+
start(): void;
|
|
115
|
+
}
|
|
116
|
+
export declare function createRunTerminalReconciliationLoop(input: {
|
|
117
|
+
intervalMs?: number;
|
|
118
|
+
onError?: (error: unknown) => void;
|
|
119
|
+
reconcile: () => void;
|
|
120
|
+
refreshWatcher: () => void;
|
|
121
|
+
}): RunTerminalReconciliationLoop;
|
|
87
122
|
export interface RunRetirementExecutorOptions {
|
|
88
123
|
attempted?: Set<string>;
|
|
89
124
|
cancelRun: (candidate: RunRetirementCandidate) => Record<string, unknown>;
|
|
@@ -16,11 +16,13 @@ export function createRunUiObservationState() {
|
|
|
16
16
|
outboxEventIds: new Map(),
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
|
-
export function readRunUiSnapshot(state, ownerId) {
|
|
20
|
-
const summary = summarizeRuns(
|
|
19
|
+
export function readRunUiSnapshot(state, ownerId, options = {}) {
|
|
20
|
+
const summary = summarizeRuns(options.stateRoot, ownerId);
|
|
21
21
|
const status = renderRunStatus(summary, state.frame++);
|
|
22
22
|
return {
|
|
23
|
-
outboxEvents:
|
|
23
|
+
outboxEvents: options.includeOutbox === false
|
|
24
|
+
? []
|
|
25
|
+
: detectRunOutboxEvents(state.eventLines, summary, state.outboxEventIds),
|
|
24
26
|
status,
|
|
25
27
|
summary,
|
|
26
28
|
transitions: detectRunTransitions(state.observed, summary),
|
|
@@ -29,25 +31,43 @@ export function readRunUiSnapshot(state, ownerId) {
|
|
|
29
31
|
export function pruneRunUiObservationState(state, snapshot) {
|
|
30
32
|
pruneRunObservationState(state.observed, state.eventLines, snapshot.summary, snapshot.transitions.map((transition) => transition.stateDir ?? transition.run), state.outboxEventIds);
|
|
31
33
|
}
|
|
32
|
-
export function deliverRunTransitionNotifications(transitions, sink) {
|
|
34
|
+
export function deliverRunTransitionNotifications(transitions, sink, inFlight = new Set()) {
|
|
33
35
|
for (const transition of transitions) {
|
|
34
36
|
if (!shouldNotifyRunTransition(transition))
|
|
35
37
|
continue;
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
if (!shouldSendRunTransitionFollowUp(transition))
|
|
38
|
+
const key = transition.stateDir ?? transition.run;
|
|
39
|
+
if (inFlight.has(key))
|
|
39
40
|
continue;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
inFlight.add(key);
|
|
42
|
+
try {
|
|
43
|
+
const text = formatRunTransitionMessage(transition);
|
|
44
|
+
sink.notify(text, getRunTransitionNotificationType(transition));
|
|
45
|
+
if (!shouldSendRunTransitionFollowUp(transition))
|
|
46
|
+
continue;
|
|
47
|
+
sink.sendFollowUp({
|
|
48
|
+
customType: "pi-actors-run",
|
|
49
|
+
content: text,
|
|
50
|
+
display: true,
|
|
51
|
+
details: transition,
|
|
52
|
+
});
|
|
53
|
+
if (transition.stateDir) {
|
|
54
|
+
AsyncRuns.markRunTerminalNotificationHandled(transition.stateDir, transition.to);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
inFlight.delete(key);
|
|
48
59
|
}
|
|
49
60
|
}
|
|
50
61
|
}
|
|
62
|
+
export function reconcileRunTerminalNotifications(input) {
|
|
63
|
+
const snapshot = readRunUiSnapshot(input.state, input.ownerId, {
|
|
64
|
+
includeOutbox: false,
|
|
65
|
+
stateRoot: input.stateRoot,
|
|
66
|
+
});
|
|
67
|
+
deliverRunTransitionNotifications(snapshot.transitions, input.sink, input.inFlight);
|
|
68
|
+
pruneRunUiObservationState(input.state, snapshot);
|
|
69
|
+
return snapshot;
|
|
70
|
+
}
|
|
51
71
|
export function deliverRunOutboxNotifications(events, sink) {
|
|
52
72
|
for (const event of events) {
|
|
53
73
|
if (!shouldNotifyRunOutboxEvent(event))
|
|
@@ -64,10 +84,58 @@ export function deliverRunOutboxNotifications(events, sink) {
|
|
|
64
84
|
});
|
|
65
85
|
}
|
|
66
86
|
}
|
|
87
|
+
const RUN_WATCHER_DIAGNOSTIC_LIMIT = 32;
|
|
67
88
|
export function createRunStateWatcher(input) {
|
|
68
89
|
const stateRoot = input.stateRoot ?? Paths.getRunStateRoot();
|
|
90
|
+
const pathExists = input.exists ?? existsSync;
|
|
91
|
+
const listDirectories = input.listDirectories ??
|
|
92
|
+
((path) => readdirSync(path, { withFileTypes: true })
|
|
93
|
+
.filter((entry) => entry.isDirectory())
|
|
94
|
+
.map((entry) => join(path, entry.name)));
|
|
95
|
+
const watchPath = input.watchPath ?? ((path, onChange) => watch(path, onChange));
|
|
96
|
+
let diagnosticId = 0;
|
|
97
|
+
let rootDegraded = false;
|
|
69
98
|
let stateRootWatcher;
|
|
99
|
+
const degradedRunDirs = new Set();
|
|
100
|
+
const diagnostics = [];
|
|
101
|
+
const lastDiagnosticSignatures = new Map();
|
|
70
102
|
const runDirWatchers = new Map();
|
|
103
|
+
const record = (code, scope, path, error) => {
|
|
104
|
+
const detail = error instanceof Error ? `: ${error.message}` : "";
|
|
105
|
+
const signature = `${code}${detail}`;
|
|
106
|
+
const signatureKey = `${scope}:${path}`;
|
|
107
|
+
if (lastDiagnosticSignatures.get(signatureKey) === signature)
|
|
108
|
+
return;
|
|
109
|
+
lastDiagnosticSignatures.set(signatureKey, signature);
|
|
110
|
+
const recovery = code === "rearmed"
|
|
111
|
+
? "; terminal watch acceleration restored"
|
|
112
|
+
: "; terminal reconciliation remains active and watcher rearm will retry";
|
|
113
|
+
diagnostics.push({
|
|
114
|
+
code,
|
|
115
|
+
id: ++diagnosticId,
|
|
116
|
+
message: `Run-state ${scope} watcher ${code.replace("_", " ")} for ${path}${detail}${recovery}`,
|
|
117
|
+
path,
|
|
118
|
+
scope,
|
|
119
|
+
ts: new Date().toISOString(),
|
|
120
|
+
});
|
|
121
|
+
if (diagnostics.length > RUN_WATCHER_DIAGNOSTIC_LIMIT)
|
|
122
|
+
diagnostics.shift();
|
|
123
|
+
};
|
|
124
|
+
const removeRunWatcher = (stateDir, watcher, options = {}) => {
|
|
125
|
+
if (runDirWatchers.get(stateDir) !== watcher)
|
|
126
|
+
return;
|
|
127
|
+
watcher.close();
|
|
128
|
+
runDirWatchers.delete(stateDir);
|
|
129
|
+
if (options.degraded === false) {
|
|
130
|
+
degradedRunDirs.delete(stateDir);
|
|
131
|
+
lastDiagnosticSignatures.delete(`run:${stateDir}`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
degradedRunDirs.add(stateDir);
|
|
135
|
+
if (options.error)
|
|
136
|
+
record("error", "run", stateDir, options.error);
|
|
137
|
+
record("removed", "run", stateDir);
|
|
138
|
+
};
|
|
71
139
|
const close = () => {
|
|
72
140
|
stateRootWatcher?.close();
|
|
73
141
|
stateRootWatcher = undefined;
|
|
@@ -76,42 +144,92 @@ export function createRunStateWatcher(input) {
|
|
|
76
144
|
runDirWatchers.clear();
|
|
77
145
|
};
|
|
78
146
|
const watchRunDir = (stateDir) => {
|
|
79
|
-
if (runDirWatchers.has(stateDir) || !
|
|
147
|
+
if (runDirWatchers.has(stateDir) || !pathExists(stateDir))
|
|
80
148
|
return;
|
|
81
149
|
try {
|
|
82
|
-
const watcher =
|
|
83
|
-
watcher.on("error", () => {
|
|
84
|
-
watcher.close();
|
|
85
|
-
runDirWatchers.delete(stateDir);
|
|
86
|
-
});
|
|
150
|
+
const watcher = watchPath(stateDir, input.onChange);
|
|
151
|
+
watcher.on("error", (error) => removeRunWatcher(stateDir, watcher, { error }));
|
|
87
152
|
runDirWatchers.set(stateDir, watcher);
|
|
153
|
+
if (degradedRunDirs.delete(stateDir))
|
|
154
|
+
record("rearmed", "run", stateDir);
|
|
88
155
|
}
|
|
89
|
-
catch {
|
|
90
|
-
|
|
156
|
+
catch (error) {
|
|
157
|
+
degradedRunDirs.add(stateDir);
|
|
158
|
+
record("attach_failed", "run", stateDir, error);
|
|
91
159
|
}
|
|
92
160
|
};
|
|
93
161
|
function refresh() {
|
|
94
|
-
if (!
|
|
162
|
+
if (!pathExists(stateRoot))
|
|
95
163
|
return;
|
|
96
164
|
if (!stateRootWatcher) {
|
|
97
165
|
try {
|
|
98
|
-
|
|
99
|
-
stateRootWatcher
|
|
100
|
-
|
|
166
|
+
const watcher = watchPath(stateRoot, input.onChange);
|
|
167
|
+
stateRootWatcher = watcher;
|
|
168
|
+
watcher.on("error", (error) => {
|
|
169
|
+
if (stateRootWatcher !== watcher)
|
|
170
|
+
return;
|
|
171
|
+
watcher.close();
|
|
101
172
|
stateRootWatcher = undefined;
|
|
173
|
+
rootDegraded = true;
|
|
174
|
+
record("error", "root", stateRoot, error);
|
|
175
|
+
record("removed", "root", stateRoot);
|
|
102
176
|
});
|
|
177
|
+
if (rootDegraded) {
|
|
178
|
+
rootDegraded = false;
|
|
179
|
+
record("rearmed", "root", stateRoot);
|
|
180
|
+
}
|
|
103
181
|
}
|
|
104
|
-
catch {
|
|
105
|
-
|
|
182
|
+
catch (error) {
|
|
183
|
+
rootDegraded = true;
|
|
184
|
+
record("attach_failed", "root", stateRoot, error);
|
|
106
185
|
}
|
|
107
186
|
}
|
|
108
|
-
|
|
109
|
-
|
|
187
|
+
let stateDirs;
|
|
188
|
+
try {
|
|
189
|
+
stateDirs = listDirectories(stateRoot);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
record("attach_failed", "root", stateRoot, error);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const present = new Set(stateDirs);
|
|
196
|
+
for (const [stateDir, watcher] of runDirWatchers) {
|
|
197
|
+
if (present.has(stateDir) && pathExists(stateDir))
|
|
110
198
|
continue;
|
|
111
|
-
|
|
199
|
+
removeRunWatcher(stateDir, watcher, { degraded: false });
|
|
112
200
|
}
|
|
201
|
+
for (const stateDir of stateDirs)
|
|
202
|
+
watchRunDir(stateDir);
|
|
113
203
|
}
|
|
114
|
-
return {
|
|
204
|
+
return {
|
|
205
|
+
close,
|
|
206
|
+
getDiagnostics: () => [...diagnostics],
|
|
207
|
+
refresh,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
export function createRunTerminalReconciliationLoop(input) {
|
|
211
|
+
const intervalMs = input.intervalMs ?? 10_000;
|
|
212
|
+
let interval;
|
|
213
|
+
const reconcileNow = () => {
|
|
214
|
+
try {
|
|
215
|
+
input.refreshWatcher();
|
|
216
|
+
input.reconcile();
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
input.onError?.(error);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
const close = () => {
|
|
223
|
+
if (interval)
|
|
224
|
+
clearInterval(interval);
|
|
225
|
+
interval = undefined;
|
|
226
|
+
};
|
|
227
|
+
const start = () => {
|
|
228
|
+
close();
|
|
229
|
+
interval = setInterval(reconcileNow, intervalMs);
|
|
230
|
+
interval.unref?.();
|
|
231
|
+
};
|
|
232
|
+
return { close, reconcileNow, start };
|
|
115
233
|
}
|
|
116
234
|
const TERMINAL = new Set([
|
|
117
235
|
"done",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Zones: compact text summaries, verbose JSON switching, next-action rendering
|
|
4
4
|
* Owns model-facing response helpers shared by public tool execution paths
|
|
5
5
|
*/
|
|
6
|
-
export declare function
|
|
6
|
+
export declare function withLeadingLineBreak(text: string): string;
|
|
7
7
|
export declare function spaceToolResult<T>(result: T): T;
|
|
8
8
|
export declare function spaceToolError(error: unknown): unknown;
|
|
9
9
|
export declare function asRecord(value: unknown): Record<string, unknown>;
|
|
@@ -5,10 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as Limits from "./limits.js";
|
|
7
7
|
import * as ToolsMailbox from "./tools-mailbox.js";
|
|
8
|
-
export function
|
|
9
|
-
|
|
10
|
-
return text;
|
|
11
|
-
return text.startsWith("\n") ? `\n${text}` : `\n\n${text}`;
|
|
8
|
+
export function withLeadingLineBreak(text) {
|
|
9
|
+
return `\n${text.replace(/^\n+/, "")}`;
|
|
12
10
|
}
|
|
13
11
|
export function spaceToolResult(result) {
|
|
14
12
|
if (!result || typeof result !== "object")
|
|
@@ -24,17 +22,17 @@ export function spaceToolResult(result) {
|
|
|
24
22
|
typeof item.text === "string"
|
|
25
23
|
? {
|
|
26
24
|
...item,
|
|
27
|
-
text:
|
|
25
|
+
text: withLeadingLineBreak(item.text),
|
|
28
26
|
}
|
|
29
27
|
: item),
|
|
30
28
|
};
|
|
31
29
|
}
|
|
32
30
|
export function spaceToolError(error) {
|
|
33
31
|
if (error instanceof Error) {
|
|
34
|
-
error.message =
|
|
32
|
+
error.message = withLeadingLineBreak(error.message);
|
|
35
33
|
return error;
|
|
36
34
|
}
|
|
37
|
-
return new Error(
|
|
35
|
+
return new Error(withLeadingLineBreak(String(error)));
|
|
38
36
|
}
|
|
39
37
|
export function asRecord(value) {
|
|
40
38
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: actors
|
|
3
3
|
description: Required practical guide for non-trivial pi-actors use, including parallel actor launches, subagent fanout, and autonomous coordinator workflows. Read before using or changing spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
|
|
4
4
|
metadata:
|
|
5
|
-
version: 0.41.
|
|
5
|
+
version: 0.41.1
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Actors (pi-actors)
|
|
@@ -125,17 +125,13 @@ Views:
|
|
|
125
125
|
- `artifacts`: declared artifact paths/status plus the same bounded owned review-evidence manifest when present.
|
|
126
126
|
- `recipes` target: registry summary for active, shadowed, invalid, disabled, and diagnostic recipe entries.
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
Communication rows retain bounded body previews, capped noisy room traffic, branch-local inbox state, stable event ids, attention markers, and compact roster summaries. Turn rows come only from persisted owned child-session evidence and show source model/text at a glance. Turn detail remains bounded and includes command/stage, session/prompt/recipe provenance, user/assistant text, persisted thinking or explicit reasoning unavailability, stop/usage/error metadata, correlated tool arguments/results, truncation, and parse diagnostics. Active roster members use the target color, departed members stay muted, and display names come from `actor.join` bodies or branch addresses.
|
|
131
|
-
|
|
132
|
-
Let terminal notifications arrive. They queue through Pi's follow-up delivery mode, so a busy coordinator finishes its current work before receiving concurrently completed actor results; the host's `followUpMode` controls whether queued results arrive together or one at a time. When a deferred actor result gates the next step, wait for that terminal follow-up instead of scheduling continuation loops, repeatedly inspecting, or mutating the actor's reviewed scope. Idle coordinators still start a normal turn through `triggerTurn: true`. Inspect early only for an operator request, a meaningful actor event, or diagnosis of an overdue or stuck run.
|
|
128
|
+
Let terminal notifications arrive. They queue through Pi's follow-up delivery mode, so a busy coordinator finishes its current work before receiving concurrently completed actor results; the host's `followUpMode` controls whether queued results arrive together or one at a time. File watching accelerates delivery, while a bounded ten-second terminal-only reconciliation pass recovers missed or failed watcher activity without replaying outbox traffic; watcher degradation and rearm remain visible diagnostics. When a deferred actor result gates the next step, wait for that terminal follow-up instead of scheduling continuation loops, repeatedly inspecting, or mutating the actor's reviewed scope. Idle coordinators still start a normal turn through `triggerTurn: true`. Inspect early only for an operator request, a meaningful actor event, or diagnosis of an overdue or stuck run.
|
|
133
129
|
|
|
134
130
|
## Runtime Communication Rules
|
|
135
131
|
|
|
136
132
|
- Keep one public communication model: `spawn` creates actors, `message` sends typed envelopes, and `inspect` observes. Avoid adding public side channels or storage nouns when a normal actor address/view can express the operation.
|
|
137
133
|
- Keep route and semantic type separate. Direct, room, coordinator, and session messages may share `type`; delivery behavior comes from `to`.
|
|
138
|
-
- Treat
|
|
134
|
+
- Treat persisted communication logs as recipe evidence. Use `inspect room:<run> view=messages|previews` and `inspect run:<id> view=communication` to improve mailbox/artifact conventions after real runs.
|
|
139
135
|
- Any UI, summary, or aggregate view that scans run directories must apply coordinator/session ownership filters before exposing summaries or body previews.
|
|
140
136
|
- Treat `communication.json` as visible actor context, not a global mutable truth table. Run-level snapshots should identify the run actor; branch-local snapshots should identify the branch actor.
|
|
141
137
|
- Prefer same-run provenance checks on lateral actor routes. If `from` is accepted for room or branch routes, validate that it belongs to the addressed run.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: swarm
|
|
3
3
|
description: Subagent and actor orchestration with scoped locks, fanout, and quorum consensus. Use before launching multiple parallel actors or subagents for independent implementation, artifact generation, review, delegated audit, coordinated execution, or any workflow that needs autonomous coordinator decomposition and integration.
|
|
4
4
|
metadata:
|
|
5
|
-
version: 0.41.
|
|
5
|
+
version: 0.41.1
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Swarm
|
package/docs/actor-inspector.md
CHANGED
|
@@ -11,35 +11,36 @@ owned run
|
|
|
11
11
|
|
|
12
12
|
## Navigation
|
|
13
13
|
|
|
14
|
-
`/actors-inspector
|
|
14
|
+
`/actors-inspector` opens one centered overlay and remains the only command to remember. The latest run owned by the current Pi session becomes active automatically; an empty session still exposes functional tabs and filters.
|
|
15
15
|
|
|
16
16
|
The overlay exposes an explicit focus hierarchy:
|
|
17
17
|
|
|
18
18
|
```text
|
|
19
|
-
Run Enter opens
|
|
19
|
+
Run ←/→ chooses the previous/next owned run, Enter opens runs, ↓ enters tabs
|
|
20
20
|
Tabs ←/→ chooses Messages or Turns, Enter opens filter parameters
|
|
21
21
|
Filters ↑/↓ chooses Channel/State or Subagent, Enter opens values to the right
|
|
22
22
|
Values ↑/↓ hovers, Enter applies, Escape returns one menu level
|
|
23
23
|
List ↑/↓ chooses, Enter/→ opens detail
|
|
24
|
-
Detail ↑/↓ scroll, Escape/← returns
|
|
24
|
+
Detail ↑/↓ scroll, Enter/→ opens readable transcript, Escape/← returns
|
|
25
|
+
Readable ↑/↓ scroll, Escape/← returns to evidence detail
|
|
25
26
|
Escape Close (or cancel the active options popup)
|
|
26
27
|
```
|
|
27
28
|
|
|
28
29
|
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
|
|
|
30
|
-
Selection and focus remain separate visual states. Accent-blue text marks the current tab, active filter popup, and applied option.
|
|
31
|
+
Selection and focus remain separate visual states. Accent-blue text marks the current tab, active filter popup, and applied option. The Run control uses `← … →` markers plus a light neutral background to show both focus and horizontal cycling; menus and timeline rows retain the single `▶` focus marker, while selected tabs retain brackets. 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
|
|
|
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
|
|
33
|
+
The top Run control aligns vertically with the tab labels, names the selected owned run, and colors its textual lifecycle status semantically. ←/→ cycles owned runs directly with wraparound, while Enter opens the complete owned-run list immediately beneath the control. That run list starts one cell farther left than the filter menus so its border aligns with the Run control rather than the tab/filter grid. It still overlays the tab row rather than leaving a detached gap. The timeline no longer renders run metadata as a data row.
|
|
33
34
|
|
|
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
|
+
Filters live behind their tab rather than occupying a permanent row. Non-default filters remain visible as compact parenthesized 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
|
|
|
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
|
+
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. Every run, filter, and nested value menu is viewport-bounded: ↑/↓ moves through the complete option set, the visible window follows focus, and `↑`/`↓` border markers disclose hidden options above or below without growing past the available inspector rows.
|
|
37
38
|
|
|
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
|
+
The overlay uses most of the available terminal width and height and reduces its content/menu viewport on shorter terminals. The bordered header keeps both tabs visible, while the list body shows the selected run and its current status above the evidence rows. Run, Message, and Turn lists place the newest retained item directly below their control; a newly opened Inspector therefore selects the latest owned run, and ↓ moves backward in time toward older entries. Run options, Messages, and Turns all use compact descending `#N` labels, providing one timestamp-free time axis without repeating type words on every row. 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. Unused viewport padding stays on the plain overlay background instead of drawing fake striped rows beneath the last item. 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
|
|
|
40
41
|
## Communication Timeline
|
|
41
42
|
|
|
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
|
+
The communication timeline reads run-local room, direct, branch-inbox, and coordinator/session message evidence. Rows display their stable `#N` sequence in newest-first order. 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
|
|
|
44
45
|
Communication evidence describes messages between actors. It does not prove model execution.
|
|
45
46
|
|
|
@@ -53,19 +54,23 @@ Detached child `pi -p` commands receive isolated session storage under their own
|
|
|
53
54
|
|
|
54
55
|
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
|
|
|
56
|
-
The turns timeline follows the latest persisted entry branch in each recorded Pi session and
|
|
57
|
+
The turns timeline follows the latest persisted entry branch in each recorded Pi session and displays numbered turns newest-first. Each list row begins with compact `#N`, then a humanized `Subagent N` derived from the internal `command-NNN` session owner, followed by an optional parenthesized semantic stage such as `(reviewer)`. The internal command id remains available in evidence detail for provenance but no longer acts as the unexplained primary list label. The visible model column shows only the model id, not its provider. Tool activity appears as a compact parenthesized action summary such as `(read)`, `(read, bash)`, or `(3 tools)`; `(error)` appears only when the turn or a tool result failed.
|
|
58
|
+
|
|
59
|
+
Each turn groups:
|
|
57
60
|
|
|
58
61
|
- User input associated with the response;
|
|
59
62
|
- Assistant text and host-persisted thinking blocks;
|
|
60
|
-
-
|
|
63
|
+
- Model, stop reason, usage, and error metadata;
|
|
61
64
|
- Tool calls in assistant source order;
|
|
62
65
|
- Tool results correlated by `toolCallId`, regardless of completion order.
|
|
63
66
|
|
|
64
|
-
Enter opens the selected turn inside the overlay.
|
|
67
|
+
Enter/→ opens the selected turn as structured evidence inside the overlay. A compact `Subagent N` heading with an optional meaningful role leads into meaning-first sections: User, persisted Thinking, Assistant, Tools, Execution, and Diagnostics. A final Provenance section retains session/prompt paths, truncation state, and recipe context without making transport metadata the first screen. Generic internal stages such as `command` and `subagent` stay hidden; technical `command-NNN` provenance remains available through the session and prompt paths without producing a redundant `Command / command-NNN (command)` block. Secondary qualifiers use parentheses rather than centered-dot separators. Long text, paths, and structured values wrap to subsequent terminal rows instead of receiving visual ellipsis; lines that already fit the available inner width remain intact, leading indentation is reserved before wrapping long unbroken paths so it cannot become a whitespace-only row, and every section plus all of its explicit or wrapped continuations keeps one background stripe. Blank-only source lines and trailing line breaks are omitted from both evidence and readable rendering. Section boundaries change the stripe without inserting separator rows, so the next heading follows the previous value immediately. ↑/↓ scrolls the resulting visual-row document while the footer remains visible. Source evidence remains bounded by the persisted session reader, but the detail view no longer truncates that retained evidence to one terminal row per field.
|
|
68
|
+
|
|
69
|
+
Enter/→ once more opens a plain readable transcript of the same turn. This second level removes provenance, model, usage, ids, and other evidence metadata, retaining only User, Thinking when persisted, Assistant, Tool input/result, and Error content in execution order. When Pi persisted the user prompt as one `<file name="…">…</file>` transport wrapper, readable mode removes that wrapper and shows only its actual prompt text. Structured values render as indented key/value text rather than one-line JSON. Escape/← returns from transcript to evidence detail, then from evidence detail to the Turns list.
|
|
65
70
|
|
|
66
71
|
## Evidence And Privacy Boundary
|
|
67
72
|
|
|
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,
|
|
73
|
+
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, Execution reports `thinking: not persisted`.
|
|
69
74
|
|
|
70
75
|
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
76
|
|
package/docs/async-runs.md
CHANGED
|
@@ -218,7 +218,7 @@ Runtime wake notifications are now modeled separately from durable queues. Messa
|
|
|
218
218
|
|
|
219
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 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.
|
|
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. File-system watchers accelerate live discovery, while a bounded ten-second terminal-only reconciliation pass scans owned unhandled terminal state without reading or replaying outbox traffic. Failed root or run-directory watcher attachment, runtime errors, error-driven watcher removal, and successful rearm remain available as bounded runtime diagnostics; normal run-directory deletion stays quiet; reconciliation rearms degraded watchers but does not depend on them. 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. Watch-triggered and periodic delivery share an in-flight guard so one live runtime sends one follow-up when both paths race. 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
|
@@ -21,8 +21,10 @@ export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
|
|
|
21
21
|
let runsAnimationInterval: NodeJS.Timeout | undefined;
|
|
22
22
|
let runsNotifyTimeout: NodeJS.Timeout | undefined;
|
|
23
23
|
let activeRunContext: Pi.ExtensionContext | undefined;
|
|
24
|
+
let lastRunWatcherDiagnosticId = 0;
|
|
24
25
|
const runUi = Observability.createRunUiObservationState();
|
|
25
26
|
const retirementAttempts = new Set<string>();
|
|
27
|
+
const terminalNotificationsInFlight = new Set<string>();
|
|
26
28
|
const getRunOwnerId = Pi.getSessionId;
|
|
27
29
|
const retireCandidateRuns = (
|
|
28
30
|
ctx: Pi.ExtensionContext,
|
|
@@ -53,6 +55,7 @@ export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
|
|
|
53
55
|
Observability.deliverRunTransitionNotifications(
|
|
54
56
|
snapshot.transitions,
|
|
55
57
|
notificationSink,
|
|
58
|
+
terminalNotificationsInFlight,
|
|
56
59
|
);
|
|
57
60
|
Observability.pruneRunUiObservationState(runUi, snapshot);
|
|
58
61
|
if (!terminalOnly) {
|
|
@@ -64,22 +67,56 @@ export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
|
|
|
64
67
|
};
|
|
65
68
|
const closeRunWatchers = (): void => {
|
|
66
69
|
runWatcher.close();
|
|
70
|
+
terminalReconciliation.close();
|
|
67
71
|
if (runsNotifyTimeout) clearTimeout(runsNotifyTimeout);
|
|
68
72
|
runsNotifyTimeout = undefined;
|
|
69
73
|
};
|
|
70
|
-
const
|
|
74
|
+
const reportRunWatcherDiagnostics = (ctx: Pi.ExtensionContext): void => {
|
|
75
|
+
for (const diagnostic of runWatcher.getDiagnostics()) {
|
|
76
|
+
if (diagnostic.id <= lastRunWatcherDiagnosticId) continue;
|
|
77
|
+
lastRunWatcherDiagnosticId = diagnostic.id;
|
|
78
|
+
ctx.ui.notify(
|
|
79
|
+
diagnostic.message,
|
|
80
|
+
diagnostic.code === "rearmed" ? "info" : "warning",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const scheduleRunEventUpdate = (): void => {
|
|
71
85
|
if (runsNotifyTimeout) clearTimeout(runsNotifyTimeout);
|
|
72
86
|
runsNotifyTimeout = setTimeout(() => {
|
|
87
|
+
const ctx = activeRunContext;
|
|
88
|
+
if (!ctx) return;
|
|
73
89
|
runWatcher.refresh();
|
|
74
90
|
updateRunUi(ctx, true);
|
|
91
|
+
reportRunWatcherDiagnostics(ctx);
|
|
75
92
|
}, 50);
|
|
76
93
|
runsNotifyTimeout.unref?.();
|
|
77
94
|
};
|
|
78
95
|
const runWatcher = Observability.createRunStateWatcher({
|
|
79
96
|
stateRoot: Paths.EXTENSION_RUNTIME_PATHS.runStateRoot,
|
|
80
|
-
onChange:
|
|
81
|
-
activeRunContext && scheduleRunEventUpdate(activeRunContext),
|
|
97
|
+
onChange: scheduleRunEventUpdate,
|
|
82
98
|
});
|
|
99
|
+
const terminalReconciliation =
|
|
100
|
+
Observability.createRunTerminalReconciliationLoop({
|
|
101
|
+
onError: (error) => {
|
|
102
|
+
const ctx = activeRunContext;
|
|
103
|
+
if (!ctx) return;
|
|
104
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
105
|
+
ctx.ui.notify(`Actor terminal reconciliation failed: ${message}`, "error");
|
|
106
|
+
},
|
|
107
|
+
reconcile: () => {
|
|
108
|
+
const ctx = activeRunContext;
|
|
109
|
+
if (!ctx) return;
|
|
110
|
+
Observability.reconcileRunTerminalNotifications({
|
|
111
|
+
inFlight: terminalNotificationsInFlight,
|
|
112
|
+
ownerId: getRunOwnerId(ctx),
|
|
113
|
+
sink: Pi.createNotificationSink(pi, ctx),
|
|
114
|
+
state: runUi,
|
|
115
|
+
});
|
|
116
|
+
reportRunWatcherDiagnostics(ctx);
|
|
117
|
+
},
|
|
118
|
+
refreshWatcher: () => runWatcher.refresh(),
|
|
119
|
+
});
|
|
83
120
|
const actorToolDefinitions = new Map<string, Tools.ActorToolDefinition>();
|
|
84
121
|
const withCurrentThinkingContext = <T extends Tools.ActorToolDefinition>(
|
|
85
122
|
definition: T,
|
|
@@ -127,15 +164,19 @@ export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
|
|
|
127
164
|
// Clear the pre-overlay widget after hot reloads from older pi-actors builds.
|
|
128
165
|
ctx.ui.setWidget("zz-pi-actors-comms", undefined);
|
|
129
166
|
activeRunContext = ctx;
|
|
167
|
+
closeRunWatchers();
|
|
168
|
+
recipeReload.close();
|
|
130
169
|
await Temp.prepareExtensionTempDir(Paths.EXTENSION_RUNTIME_PATHS.tempDir);
|
|
170
|
+
if (activeRunContext !== ctx) return;
|
|
131
171
|
runtime.loadTools(ctx);
|
|
132
172
|
updateRunUi(ctx, true, true);
|
|
133
|
-
closeRunWatchers();
|
|
134
|
-
recipeReload.close();
|
|
135
173
|
runWatcher.refresh();
|
|
174
|
+
terminalReconciliation.start();
|
|
136
175
|
recipeReload.watch(ctx);
|
|
137
176
|
if (runsAnimationInterval) clearInterval(runsAnimationInterval);
|
|
138
|
-
runsAnimationInterval = setInterval(() =>
|
|
177
|
+
runsAnimationInterval = setInterval(() => {
|
|
178
|
+
if (activeRunContext === ctx) updateRunUi(ctx, false);
|
|
179
|
+
}, 1000);
|
|
139
180
|
runsAnimationInterval.unref?.();
|
|
140
181
|
});
|
|
141
182
|
pi.on("session_shutdown", async () => {
|
|
@@ -145,8 +186,8 @@ export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
|
|
|
145
186
|
closeRunWatchers();
|
|
146
187
|
recipeReload.close();
|
|
147
188
|
});
|
|
148
|
-
pi.registerCommand("actors-inspector
|
|
149
|
-
description: "
|
|
189
|
+
pi.registerCommand("actors-inspector", {
|
|
190
|
+
description: "Open the keyboard-driven actor inspector overlay",
|
|
150
191
|
handler: async (_args, ctx) => {
|
|
151
192
|
ctx.ui.setWidget("zz-pi-actors-comms", undefined);
|
|
152
193
|
await ctx.ui.custom<void>(
|