@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
package/lib/inspector.ts
CHANGED
|
@@ -107,9 +107,12 @@ export function readActorInspectorRuns(
|
|
|
107
107
|
: {}),
|
|
108
108
|
}];
|
|
109
109
|
})
|
|
110
|
-
.sort((left, right) =>
|
|
111
|
-
String(
|
|
112
|
-
|
|
110
|
+
.sort((left, right) => {
|
|
111
|
+
const recency = String(right.updatedAt ?? "").localeCompare(
|
|
112
|
+
String(left.updatedAt ?? ""),
|
|
113
|
+
);
|
|
114
|
+
return recency || right.run.localeCompare(left.run);
|
|
115
|
+
});
|
|
113
116
|
} catch (error) {
|
|
114
117
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
115
118
|
return [];
|
package/lib/observability.ts
CHANGED
|
@@ -101,15 +101,19 @@ export function createRunUiObservationState(): RunUiObservationState {
|
|
|
101
101
|
export function readRunUiSnapshot(
|
|
102
102
|
state: RunUiObservationState,
|
|
103
103
|
ownerId: string,
|
|
104
|
+
options: { includeOutbox?: boolean; stateRoot?: string } = {},
|
|
104
105
|
): RunUiSnapshot {
|
|
105
|
-
const summary = summarizeRuns(
|
|
106
|
+
const summary = summarizeRuns(options.stateRoot, ownerId);
|
|
106
107
|
const status = renderRunStatus(summary, state.frame++);
|
|
107
108
|
return {
|
|
108
|
-
outboxEvents:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
outboxEvents:
|
|
110
|
+
options.includeOutbox === false
|
|
111
|
+
? []
|
|
112
|
+
: detectRunOutboxEvents(
|
|
113
|
+
state.eventLines,
|
|
114
|
+
summary,
|
|
115
|
+
state.outboxEventIds,
|
|
116
|
+
),
|
|
113
117
|
status,
|
|
114
118
|
summary,
|
|
115
119
|
transitions: detectRunTransitions(state.observed, summary),
|
|
@@ -134,27 +138,55 @@ export function pruneRunUiObservationState(
|
|
|
134
138
|
export function deliverRunTransitionNotifications(
|
|
135
139
|
transitions: RunTransition[],
|
|
136
140
|
sink: RunUiNotificationSink,
|
|
141
|
+
inFlight: Set<string> = new Set(),
|
|
137
142
|
): void {
|
|
138
143
|
for (const transition of transitions) {
|
|
139
144
|
if (!shouldNotifyRunTransition(transition)) continue;
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
transition
|
|
152
|
-
|
|
153
|
-
)
|
|
145
|
+
const key = transition.stateDir ?? transition.run;
|
|
146
|
+
if (inFlight.has(key)) continue;
|
|
147
|
+
inFlight.add(key);
|
|
148
|
+
try {
|
|
149
|
+
const text = formatRunTransitionMessage(transition);
|
|
150
|
+
sink.notify(text, getRunTransitionNotificationType(transition));
|
|
151
|
+
if (!shouldSendRunTransitionFollowUp(transition)) continue;
|
|
152
|
+
sink.sendFollowUp({
|
|
153
|
+
customType: "pi-actors-run",
|
|
154
|
+
content: text,
|
|
155
|
+
display: true,
|
|
156
|
+
details: transition,
|
|
157
|
+
});
|
|
158
|
+
if (transition.stateDir) {
|
|
159
|
+
AsyncRuns.markRunTerminalNotificationHandled(
|
|
160
|
+
transition.stateDir,
|
|
161
|
+
transition.to,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
} finally {
|
|
165
|
+
inFlight.delete(key);
|
|
154
166
|
}
|
|
155
167
|
}
|
|
156
168
|
}
|
|
157
169
|
|
|
170
|
+
export function reconcileRunTerminalNotifications(input: {
|
|
171
|
+
inFlight?: Set<string>;
|
|
172
|
+
ownerId: string;
|
|
173
|
+
sink: RunUiNotificationSink;
|
|
174
|
+
state: RunUiObservationState;
|
|
175
|
+
stateRoot?: string;
|
|
176
|
+
}): RunUiSnapshot {
|
|
177
|
+
const snapshot = readRunUiSnapshot(input.state, input.ownerId, {
|
|
178
|
+
includeOutbox: false,
|
|
179
|
+
stateRoot: input.stateRoot,
|
|
180
|
+
});
|
|
181
|
+
deliverRunTransitionNotifications(
|
|
182
|
+
snapshot.transitions,
|
|
183
|
+
input.sink,
|
|
184
|
+
input.inFlight,
|
|
185
|
+
);
|
|
186
|
+
pruneRunUiObservationState(input.state, snapshot);
|
|
187
|
+
return snapshot;
|
|
188
|
+
}
|
|
189
|
+
|
|
158
190
|
export function deliverRunOutboxNotifications(
|
|
159
191
|
events: RunOutboxEvent[],
|
|
160
192
|
sink: RunUiNotificationSink,
|
|
@@ -189,18 +221,94 @@ export interface RunRetirementExecution {
|
|
|
189
221
|
stateDir: string;
|
|
190
222
|
}
|
|
191
223
|
|
|
224
|
+
export type RunStateWatcherDiagnosticCode =
|
|
225
|
+
| "attach_failed"
|
|
226
|
+
| "error"
|
|
227
|
+
| "removed"
|
|
228
|
+
| "rearmed";
|
|
229
|
+
|
|
230
|
+
export interface RunStateWatcherDiagnostic {
|
|
231
|
+
code: RunStateWatcherDiagnosticCode;
|
|
232
|
+
id: number;
|
|
233
|
+
message: string;
|
|
234
|
+
path: string;
|
|
235
|
+
scope: "root" | "run";
|
|
236
|
+
ts: string;
|
|
237
|
+
}
|
|
238
|
+
|
|
192
239
|
export interface RunStateWatcher {
|
|
193
240
|
close(): void;
|
|
241
|
+
getDiagnostics(): RunStateWatcherDiagnostic[];
|
|
194
242
|
refresh(): void;
|
|
195
243
|
}
|
|
196
244
|
|
|
245
|
+
const RUN_WATCHER_DIAGNOSTIC_LIMIT = 32;
|
|
246
|
+
|
|
197
247
|
export function createRunStateWatcher(input: {
|
|
198
|
-
|
|
248
|
+
exists?: (path: string) => boolean;
|
|
249
|
+
listDirectories?: (path: string) => string[];
|
|
199
250
|
onChange: () => void;
|
|
251
|
+
stateRoot?: string;
|
|
252
|
+
watchPath?: (path: string, onChange: () => void) => FSWatcher;
|
|
200
253
|
}): RunStateWatcher {
|
|
201
254
|
const stateRoot = input.stateRoot ?? Paths.getRunStateRoot();
|
|
255
|
+
const pathExists = input.exists ?? existsSync;
|
|
256
|
+
const listDirectories =
|
|
257
|
+
input.listDirectories ??
|
|
258
|
+
((path: string) =>
|
|
259
|
+
readdirSync(path, { withFileTypes: true })
|
|
260
|
+
.filter((entry) => entry.isDirectory())
|
|
261
|
+
.map((entry) => join(path, entry.name)));
|
|
262
|
+
const watchPath = input.watchPath ?? ((path, onChange) => watch(path, onChange));
|
|
263
|
+
let diagnosticId = 0;
|
|
264
|
+
let rootDegraded = false;
|
|
202
265
|
let stateRootWatcher: FSWatcher | undefined;
|
|
266
|
+
const degradedRunDirs = new Set<string>();
|
|
267
|
+
const diagnostics: RunStateWatcherDiagnostic[] = [];
|
|
268
|
+
const lastDiagnosticSignatures = new Map<string, string>();
|
|
203
269
|
const runDirWatchers = new Map<string, FSWatcher>();
|
|
270
|
+
const record = (
|
|
271
|
+
code: RunStateWatcherDiagnosticCode,
|
|
272
|
+
scope: "root" | "run",
|
|
273
|
+
path: string,
|
|
274
|
+
error?: unknown,
|
|
275
|
+
): void => {
|
|
276
|
+
const detail = error instanceof Error ? `: ${error.message}` : "";
|
|
277
|
+
const signature = `${code}${detail}`;
|
|
278
|
+
const signatureKey = `${scope}:${path}`;
|
|
279
|
+
if (lastDiagnosticSignatures.get(signatureKey) === signature) return;
|
|
280
|
+
lastDiagnosticSignatures.set(signatureKey, signature);
|
|
281
|
+
const recovery =
|
|
282
|
+
code === "rearmed"
|
|
283
|
+
? "; terminal watch acceleration restored"
|
|
284
|
+
: "; terminal reconciliation remains active and watcher rearm will retry";
|
|
285
|
+
diagnostics.push({
|
|
286
|
+
code,
|
|
287
|
+
id: ++diagnosticId,
|
|
288
|
+
message: `Run-state ${scope} watcher ${code.replace("_", " ")} for ${path}${detail}${recovery}`,
|
|
289
|
+
path,
|
|
290
|
+
scope,
|
|
291
|
+
ts: new Date().toISOString(),
|
|
292
|
+
});
|
|
293
|
+
if (diagnostics.length > RUN_WATCHER_DIAGNOSTIC_LIMIT) diagnostics.shift();
|
|
294
|
+
};
|
|
295
|
+
const removeRunWatcher = (
|
|
296
|
+
stateDir: string,
|
|
297
|
+
watcher: FSWatcher,
|
|
298
|
+
options: { degraded?: boolean; error?: unknown } = {},
|
|
299
|
+
): void => {
|
|
300
|
+
if (runDirWatchers.get(stateDir) !== watcher) return;
|
|
301
|
+
watcher.close();
|
|
302
|
+
runDirWatchers.delete(stateDir);
|
|
303
|
+
if (options.degraded === false) {
|
|
304
|
+
degradedRunDirs.delete(stateDir);
|
|
305
|
+
lastDiagnosticSignatures.delete(`run:${stateDir}`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
degradedRunDirs.add(stateDir);
|
|
309
|
+
if (options.error) record("error", "run", stateDir, options.error);
|
|
310
|
+
record("removed", "run", stateDir);
|
|
311
|
+
};
|
|
204
312
|
const close = (): void => {
|
|
205
313
|
stateRootWatcher?.close();
|
|
206
314
|
stateRootWatcher = undefined;
|
|
@@ -208,37 +316,95 @@ export function createRunStateWatcher(input: {
|
|
|
208
316
|
runDirWatchers.clear();
|
|
209
317
|
};
|
|
210
318
|
const watchRunDir = (stateDir: string): void => {
|
|
211
|
-
if (runDirWatchers.has(stateDir) || !
|
|
319
|
+
if (runDirWatchers.has(stateDir) || !pathExists(stateDir)) return;
|
|
212
320
|
try {
|
|
213
|
-
const watcher =
|
|
214
|
-
watcher.on("error", () =>
|
|
215
|
-
watcher
|
|
216
|
-
|
|
217
|
-
});
|
|
321
|
+
const watcher = watchPath(stateDir, input.onChange);
|
|
322
|
+
watcher.on("error", (error) =>
|
|
323
|
+
removeRunWatcher(stateDir, watcher, { error }),
|
|
324
|
+
);
|
|
218
325
|
runDirWatchers.set(stateDir, watcher);
|
|
219
|
-
|
|
220
|
-
|
|
326
|
+
if (degradedRunDirs.delete(stateDir)) record("rearmed", "run", stateDir);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
degradedRunDirs.add(stateDir);
|
|
329
|
+
record("attach_failed", "run", stateDir, error);
|
|
221
330
|
}
|
|
222
331
|
};
|
|
223
332
|
function refresh(): void {
|
|
224
|
-
if (!
|
|
333
|
+
if (!pathExists(stateRoot)) return;
|
|
225
334
|
if (!stateRootWatcher) {
|
|
226
335
|
try {
|
|
227
|
-
|
|
228
|
-
stateRootWatcher
|
|
229
|
-
|
|
336
|
+
const watcher = watchPath(stateRoot, input.onChange);
|
|
337
|
+
stateRootWatcher = watcher;
|
|
338
|
+
watcher.on("error", (error) => {
|
|
339
|
+
if (stateRootWatcher !== watcher) return;
|
|
340
|
+
watcher.close();
|
|
230
341
|
stateRootWatcher = undefined;
|
|
342
|
+
rootDegraded = true;
|
|
343
|
+
record("error", "root", stateRoot, error);
|
|
344
|
+
record("removed", "root", stateRoot);
|
|
231
345
|
});
|
|
232
|
-
|
|
233
|
-
|
|
346
|
+
if (rootDegraded) {
|
|
347
|
+
rootDegraded = false;
|
|
348
|
+
record("rearmed", "root", stateRoot);
|
|
349
|
+
}
|
|
350
|
+
} catch (error) {
|
|
351
|
+
rootDegraded = true;
|
|
352
|
+
record("attach_failed", "root", stateRoot, error);
|
|
234
353
|
}
|
|
235
354
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
355
|
+
let stateDirs: string[];
|
|
356
|
+
try {
|
|
357
|
+
stateDirs = listDirectories(stateRoot);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
record("attach_failed", "root", stateRoot, error);
|
|
360
|
+
return;
|
|
239
361
|
}
|
|
362
|
+
const present = new Set(stateDirs);
|
|
363
|
+
for (const [stateDir, watcher] of runDirWatchers) {
|
|
364
|
+
if (present.has(stateDir) && pathExists(stateDir)) continue;
|
|
365
|
+
removeRunWatcher(stateDir, watcher, { degraded: false });
|
|
366
|
+
}
|
|
367
|
+
for (const stateDir of stateDirs) watchRunDir(stateDir);
|
|
240
368
|
}
|
|
241
|
-
return {
|
|
369
|
+
return {
|
|
370
|
+
close,
|
|
371
|
+
getDiagnostics: () => [...diagnostics],
|
|
372
|
+
refresh,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export interface RunTerminalReconciliationLoop {
|
|
377
|
+
close(): void;
|
|
378
|
+
reconcileNow(): void;
|
|
379
|
+
start(): void;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function createRunTerminalReconciliationLoop(input: {
|
|
383
|
+
intervalMs?: number;
|
|
384
|
+
onError?: (error: unknown) => void;
|
|
385
|
+
reconcile: () => void;
|
|
386
|
+
refreshWatcher: () => void;
|
|
387
|
+
}): RunTerminalReconciliationLoop {
|
|
388
|
+
const intervalMs = input.intervalMs ?? 10_000;
|
|
389
|
+
let interval: NodeJS.Timeout | undefined;
|
|
390
|
+
const reconcileNow = (): void => {
|
|
391
|
+
try {
|
|
392
|
+
input.refreshWatcher();
|
|
393
|
+
input.reconcile();
|
|
394
|
+
} catch (error) {
|
|
395
|
+
input.onError?.(error);
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
const close = (): void => {
|
|
399
|
+
if (interval) clearInterval(interval);
|
|
400
|
+
interval = undefined;
|
|
401
|
+
};
|
|
402
|
+
const start = (): void => {
|
|
403
|
+
close();
|
|
404
|
+
interval = setInterval(reconcileNow, intervalMs);
|
|
405
|
+
interval.unref?.();
|
|
406
|
+
};
|
|
407
|
+
return { close, reconcileNow, start };
|
|
242
408
|
}
|
|
243
409
|
|
|
244
410
|
export interface RunRetirementExecutorOptions {
|
package/lib/tools-response.ts
CHANGED
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
import * as Limits from "./limits.ts";
|
|
8
8
|
import * as ToolsMailbox from "./tools-mailbox.ts";
|
|
9
9
|
|
|
10
|
-
export function
|
|
11
|
-
|
|
12
|
-
return text.startsWith("\n") ? `\n${text}` : `\n\n${text}`;
|
|
10
|
+
export function withLeadingLineBreak(text: string): string {
|
|
11
|
+
return `\n${text.replace(/^\n+/, "")}`;
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
export function spaceToolResult<T>(result: T): T {
|
|
@@ -25,7 +24,7 @@ export function spaceToolResult<T>(result: T): T {
|
|
|
25
24
|
typeof (item as { text?: unknown }).text === "string"
|
|
26
25
|
? {
|
|
27
26
|
...item,
|
|
28
|
-
text:
|
|
27
|
+
text: withLeadingLineBreak((item as { text: string }).text),
|
|
29
28
|
}
|
|
30
29
|
: item,
|
|
31
30
|
),
|
|
@@ -34,10 +33,10 @@ export function spaceToolResult<T>(result: T): T {
|
|
|
34
33
|
|
|
35
34
|
export function spaceToolError(error: unknown): unknown {
|
|
36
35
|
if (error instanceof Error) {
|
|
37
|
-
error.message =
|
|
36
|
+
error.message = withLeadingLineBreak(error.message);
|
|
38
37
|
return error;
|
|
39
38
|
}
|
|
40
|
-
return new Error(
|
|
39
|
+
return new Error(withLeadingLineBreak(String(error)));
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
export function asRecord(value: unknown): Record<string, unknown> {
|
package/package.json
CHANGED
package/skills/actors/SKILL.md
CHANGED
|
@@ -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.
|
package/skills/swarm/SKILL.md
CHANGED
|
@@ -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
|