@llblab/pi-actors 0.26.2 → 0.27.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.
- package/BACKLOG.md +24 -1
- package/CHANGELOG.md +9 -0
- package/dist/lib/tools.js +107 -15
- package/dist/skills/actors/SKILL.md +3 -2
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/tool-registry.md +5 -2
- package/lib/tools.ts +127 -21
- package/package.json +1 -1
- package/skills/actors/SKILL.md +3 -2
- package/skills/swarm/SKILL.md +1 -1
package/BACKLOG.md
CHANGED
|
@@ -225,12 +225,35 @@ The backlog is intentionally pruned to the 20% of work most likely to deliver 80
|
|
|
225
225
|
- Packaged recipes declare `stop`/`cancel` only when the actor-specific behavior is meaningful.
|
|
226
226
|
- Tests assert that generic mailbox-loop termination is not triggered by `control.stop` or `control.cancel`.
|
|
227
227
|
|
|
228
|
+
### M-12 Runtime and Session Observability UX
|
|
229
|
+
|
|
230
|
+
- Priority: High.
|
|
231
|
+
- Status: Planned.
|
|
232
|
+
- Goal: Make reload/session/runtime mismatches visible without changing ownership or lifecycle policy.
|
|
233
|
+
- Why now: 0.26 dogfood showed that actors, mailbox workers, and hotfixes work after a full Pi restart, but ordinary reloads can leave operators unsure which extension code is live. Session ownership mismatches also surface as terse strings instead of structured diagnostics or navigation hints.
|
|
234
|
+
- Direction:
|
|
235
|
+
- Add an intentional runtime/version inspection surface that reports loaded package version, entrypoint path, source/dist mode, package root, recipe roots, and git commit when available.
|
|
236
|
+
- Make session ownership denials structured in tool details with compact `reason=session_mismatch owner_session=... current_session=...` text and hints to inspect the owning session.
|
|
237
|
+
- Make coordinator/session status show when other sessions or other-session runs exist so `runs=0` is not misleading after reload or resume.
|
|
238
|
+
- Document reload vs full restart verification in the actors/swarm guidance.
|
|
239
|
+
- Opportunistically improve invalid shadowing recipe launch hints only if it stays diagnostic-only.
|
|
240
|
+
- Non-goals:
|
|
241
|
+
- No cross-session force kill.
|
|
242
|
+
- No session attach/adopt/reparent policy.
|
|
243
|
+
- No relaxation of current ownership gates.
|
|
244
|
+
- Acceptance:
|
|
245
|
+
- Operators can verify the loaded pi-actors version/path from an inspect/tool surface after reload.
|
|
246
|
+
- Session mismatch responses carry structured details and a compact human hint.
|
|
247
|
+
- Coordinator/session status exposes other-session counts without leaking unrelated run details by default.
|
|
248
|
+
- Tests cover version inspection, session mismatch shape, and other-session count summaries.
|
|
249
|
+
|
|
228
250
|
## Explicitly Deferred
|
|
229
251
|
|
|
230
252
|
These are valid ideas but not current focus. Reintroduce only with concrete evidence from real actor workflows.
|
|
231
253
|
|
|
232
254
|
- Spawn preflight mode: useful later, but lower value than resilient inspect and mailbox-loop consolidation.
|
|
233
255
|
- Run restart/reattach policy: risky for isolation; defer until corruption recovery and protocol fixtures are stronger.
|
|
256
|
+
- Cross-session force kill or attach/adopt/reparent: useful later, but ownership policy should not change until observability makes current boundaries clear.
|
|
234
257
|
- Actor address helper CLI: keep diagnostics improving opportunistically inside existing parser/tests.
|
|
235
258
|
- Documentation refactor: defer until the canonical mailbox loop and worker recipe exist; avoid rewriting docs twice.
|
|
236
259
|
- Host-level tool unregistration: blocked on host API support.
|
|
@@ -240,5 +263,5 @@ These are valid ideas but not current focus. Reintroduce only with concrete evid
|
|
|
240
263
|
## Suggested Milestone Order
|
|
241
264
|
|
|
242
265
|
```text
|
|
243
|
-
Next milestone:
|
|
266
|
+
Next milestone: M-12 Runtime and Session Observability UX.
|
|
244
267
|
```
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- `[Inspect]` Added `inspect target=tool:pi-actors view=status` as a runtime verification surface with loaded version, package root, source/dist mode, entrypoint path, recipe roots, and git commit when available.
|
|
6
|
+
- `[Sessions]` Started structured session mismatch diagnostics with `reason=session_mismatch`, owner/current session fields, and compact inspect-session hints while preserving current ownership gates.
|
|
7
|
+
- `[Sessions]` Added other-session run counts to coordinator/session status summaries so reload/resume states do not misleadingly report only `runs=0` without nearby context.
|
|
8
|
+
|
|
9
|
+
## 0.26.3: Branch Message Delivery UX Hotfix
|
|
10
|
+
|
|
11
|
+
- `[Actor Messages]` Treat branch mailbox persistence as a successful queued outcome even when the parent run control endpoint is unavailable, returning compact `queued=true` delivery diagnostics instead of throwing an unframed tool error.
|
|
12
|
+
- `[UX]` Preserve the compact action-output contract for branch `message` calls by keeping delivery diagnostics in the normal result formatter, including the leading blank-line separation used by other tool actions.
|
|
13
|
+
|
|
5
14
|
## 0.26.2: Terminal Progress Hotfix
|
|
6
15
|
|
|
7
16
|
- `[Async Runs]` Keep `progress.json` aligned with terminal kill/cancel handling by writing `phase=killed` or `phase=cancelled` and clearing active subagent counts when a run is stopped by runtime control.
|
package/dist/lib/tools.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* Zones: pi tools, registry tools, async run launchers
|
|
4
4
|
* Owns generated runtime tool schemas and the register_tool management tool schema
|
|
5
5
|
*/
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
6
9
|
import * as ActorMessages from "./actor-messages.js";
|
|
7
10
|
import * as ActorRooms from "./actor-rooms.js";
|
|
8
11
|
import * as AsyncRuns from "./async-runs.js";
|
|
@@ -345,10 +348,21 @@ function compactActorFiles(status) {
|
|
|
345
348
|
: "";
|
|
346
349
|
return `\nrun=${run}${artifactText}${files.length ? ` files=${files.join(",")}` : ""}`;
|
|
347
350
|
}
|
|
348
|
-
function
|
|
351
|
+
function summarizeOtherSessions(currentSession, allRuns) {
|
|
352
|
+
const otherRuns = allRuns.filter((run) => run.ownerId && run.ownerId !== currentSession);
|
|
353
|
+
return {
|
|
354
|
+
other_runs: otherRuns.length,
|
|
355
|
+
other_sessions: new Set(otherRuns.map((run) => run.ownerId)).size,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function compactSessionRuns(session, runs, summary = {}) {
|
|
359
|
+
const suffix = [
|
|
360
|
+
summary.other_sessions !== undefined ? `other_sessions=${String(summary.other_sessions)}` : "",
|
|
361
|
+
summary.other_runs !== undefined ? `other_runs=${String(summary.other_runs)}` : "",
|
|
362
|
+
].filter(Boolean).join(" ");
|
|
349
363
|
if (runs.length === 0)
|
|
350
|
-
return `\nsession=${session} runs=0`;
|
|
351
|
-
return `\nsession=${session} runs=${runs.length}\n${runs
|
|
364
|
+
return `\nsession=${session} runs=0${suffix ? ` ${suffix}` : ""}`;
|
|
365
|
+
return `\nsession=${session} runs=${runs.length}${suffix ? ` ${suffix}` : ""}\n${runs
|
|
352
366
|
.map((run) => {
|
|
353
367
|
const tokens = [
|
|
354
368
|
`run=${String(run.run ?? "")}`,
|
|
@@ -362,6 +376,38 @@ function compactSessionRuns(session, runs) {
|
|
|
362
376
|
})
|
|
363
377
|
.join("\n")}`;
|
|
364
378
|
}
|
|
379
|
+
function getPiActorsRuntimeStatus() {
|
|
380
|
+
const packagedRecipeRoot = Paths.getPackagedRecipeRoot();
|
|
381
|
+
const packageRoot = dirname(packagedRecipeRoot);
|
|
382
|
+
const packageJsonPath = join(packageRoot, "package.json");
|
|
383
|
+
const packageJson = existsSync(packageJsonPath)
|
|
384
|
+
? JSON.parse(readFileSync(packageJsonPath, "utf8"))
|
|
385
|
+
: {};
|
|
386
|
+
let git_commit;
|
|
387
|
+
try {
|
|
388
|
+
git_commit = execFileSync("git", ["-C", packageRoot, "rev-parse", "--short", "HEAD"], {
|
|
389
|
+
encoding: "utf8",
|
|
390
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
391
|
+
}).trim();
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
git_commit = undefined;
|
|
395
|
+
}
|
|
396
|
+
const entrypoint = new URL(import.meta.url).pathname;
|
|
397
|
+
return {
|
|
398
|
+
entrypoint,
|
|
399
|
+
git_commit,
|
|
400
|
+
mode: entrypoint.includes("/dist/") ? "dist" : "source",
|
|
401
|
+
package_name: packageJson.name ?? "@llblab/pi-actors",
|
|
402
|
+
package_root: packageRoot,
|
|
403
|
+
recipe_root: Paths.getRecipeRoot(),
|
|
404
|
+
packaged_recipe_root: packagedRecipeRoot,
|
|
405
|
+
version: packageJson.version ?? "unknown",
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function compactPiActorsRuntimeStatus(status) {
|
|
409
|
+
return `\npi-actors version=${String(status.version)} mode=${String(status.mode)} path=${String(status.package_root)} entrypoint=${String(status.entrypoint)}${status.git_commit ? ` git=${String(status.git_commit)}` : ""}`;
|
|
410
|
+
}
|
|
365
411
|
function compactToolActor(name, tool) {
|
|
366
412
|
const parameters = asRecord(tool.parameters);
|
|
367
413
|
const required = Array.isArray(parameters.required)
|
|
@@ -441,6 +487,8 @@ function compactActorMessageResult(message, result) {
|
|
|
441
487
|
];
|
|
442
488
|
if (result.bytes !== undefined)
|
|
443
489
|
tokens.push(`bytes=${String(result.bytes)}`);
|
|
490
|
+
if (result.queued === true)
|
|
491
|
+
tokens.push("queued=true");
|
|
444
492
|
if (result.control)
|
|
445
493
|
tokens.push(`control=${String(result.control)}`);
|
|
446
494
|
if (result.outbox)
|
|
@@ -459,6 +507,9 @@ function compactActorMessageResult(message, result) {
|
|
|
459
507
|
tokens.push(`signal=${String(result.signal)}`);
|
|
460
508
|
if (result.invoked === true)
|
|
461
509
|
tokens.push("invoked=true");
|
|
510
|
+
if (result.delivery_error) {
|
|
511
|
+
tokens.push(`delivery_error=${compactPreview(result.delivery_error, 96)}`);
|
|
512
|
+
}
|
|
462
513
|
return `\n${tokens.join(" ")}`;
|
|
463
514
|
}
|
|
464
515
|
function maybeJsonText(value, verbose, compact) {
|
|
@@ -543,7 +594,25 @@ function assertMessageSenderBelongsToRun(message, run, routeLabel) {
|
|
|
543
594
|
async function routeBranchEnvelope(stateDir, runId, recipient, message, _options) {
|
|
544
595
|
const branchMessage = { ...message, to: recipient };
|
|
545
596
|
ActorRooms.appendBranchInboxMessage(stateDir, runId, recipient, branchMessage);
|
|
546
|
-
|
|
597
|
+
try {
|
|
598
|
+
return await AsyncRuns.sendRunMessage(runId, JSON.stringify(branchMessage));
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
const record = error && typeof error === "object" ? error : {};
|
|
602
|
+
if (record.queued === true) {
|
|
603
|
+
return {
|
|
604
|
+
control_path: record.control_path,
|
|
605
|
+
control_type: record.control_type,
|
|
606
|
+
delivery_error: record.delivery_error ?? (error instanceof Error ? error.message : String(error)),
|
|
607
|
+
inbox_id: record.inbox_id,
|
|
608
|
+
queued: true,
|
|
609
|
+
run: runId,
|
|
610
|
+
sent: false,
|
|
611
|
+
state_dir: stateDir,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
throw error;
|
|
615
|
+
}
|
|
547
616
|
}
|
|
548
617
|
function getRoomMulticastRecipients(message, run) {
|
|
549
618
|
const raw = message.metadata?.recipients;
|
|
@@ -637,7 +706,13 @@ function assertRunAccessibleToContext(runId, ctx) {
|
|
|
637
706
|
const status = AsyncRuns.getRunStatus(runId);
|
|
638
707
|
const sessionId = getContextSessionId(ctx);
|
|
639
708
|
if (sessionId && status.ownerId && status.ownerId !== sessionId) {
|
|
640
|
-
throw new Error(`run:${runId}
|
|
709
|
+
throw Object.assign(new Error(`run:${runId} reason=session_mismatch owner_session=${status.ownerId} current_session=${sessionId} hint=inspect_session:${status.ownerId}`), {
|
|
710
|
+
current_session: sessionId,
|
|
711
|
+
hint: `inspect target=session:${status.ownerId} view=status`,
|
|
712
|
+
owner_session: status.ownerId,
|
|
713
|
+
reason: "session_mismatch",
|
|
714
|
+
run: runId,
|
|
715
|
+
});
|
|
641
716
|
}
|
|
642
717
|
return status;
|
|
643
718
|
}
|
|
@@ -693,37 +768,54 @@ export function createInspectToolDefinition(deps = {}) {
|
|
|
693
768
|
throw new Error("inspect coordinator supports view=status or view=runs.");
|
|
694
769
|
}
|
|
695
770
|
const session = requireContextSessionId(ctx, "inspect coordinator");
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
771
|
+
const allRuns = AsyncRuns.listRuns(undefined, typeof input.status === "string" ? input.status : undefined).map((run) => AsyncRuns.getRunStatus(String(run.state_dir)));
|
|
772
|
+
const runs = allRuns.filter((run) => run.ownerId === session);
|
|
773
|
+
const sessionSummary = summarizeOtherSessions(session, allRuns);
|
|
699
774
|
return {
|
|
700
775
|
content: [
|
|
701
776
|
{
|
|
702
777
|
type: "text",
|
|
703
|
-
text: maybeJsonText({ session, runs }, input.verbose === true, compactSessionRuns(session, runs)),
|
|
778
|
+
text: maybeJsonText({ session, runs, ...sessionSummary }, input.verbose === true, compactSessionRuns(session, runs, sessionSummary)),
|
|
704
779
|
},
|
|
705
780
|
],
|
|
706
|
-
details: { session, runs },
|
|
781
|
+
details: { session, runs, ...sessionSummary },
|
|
707
782
|
};
|
|
708
783
|
}
|
|
709
784
|
if (address.kind === "session") {
|
|
710
785
|
if (view !== "status" && view !== "runs") {
|
|
711
786
|
throw new Error("inspect session:<id> supports view=status or view=runs.");
|
|
712
787
|
}
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
|
|
788
|
+
const allRuns = AsyncRuns.listRuns(undefined, typeof input.status === "string" ? input.status : undefined).map((run) => AsyncRuns.getRunStatus(String(run.state_dir)));
|
|
789
|
+
const runs = allRuns.filter((run) => address.value === "all" || run.ownerId === address.value);
|
|
790
|
+
const sessionSummary = address.value === "all"
|
|
791
|
+
? {}
|
|
792
|
+
: summarizeOtherSessions(address.value || "", allRuns);
|
|
716
793
|
return {
|
|
717
794
|
content: [
|
|
718
795
|
{
|
|
719
796
|
type: "text",
|
|
720
|
-
text: maybeJsonText({ session: address.value, runs }, input.verbose === true, compactSessionRuns(address.value || "", runs)),
|
|
797
|
+
text: maybeJsonText({ session: address.value, runs, ...sessionSummary }, input.verbose === true, compactSessionRuns(address.value || "", runs, sessionSummary)),
|
|
721
798
|
},
|
|
722
799
|
],
|
|
723
|
-
details: { session: address.value, runs },
|
|
800
|
+
details: { session: address.value, runs, ...sessionSummary },
|
|
724
801
|
};
|
|
725
802
|
}
|
|
726
803
|
if (address.kind === "tool" && address.value) {
|
|
804
|
+
if (address.value === "pi-actors") {
|
|
805
|
+
if (view !== "status") {
|
|
806
|
+
throw new Error("inspect tool:pi-actors supports view=status.");
|
|
807
|
+
}
|
|
808
|
+
const details = getPiActorsRuntimeStatus();
|
|
809
|
+
return {
|
|
810
|
+
content: [
|
|
811
|
+
{
|
|
812
|
+
type: "text",
|
|
813
|
+
text: maybeJsonText(details, input.verbose === true, compactPiActorsRuntimeStatus(details)),
|
|
814
|
+
},
|
|
815
|
+
],
|
|
816
|
+
details,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
727
819
|
if (view !== "status" && view !== "schema") {
|
|
728
820
|
throw new Error("inspect tool:<name> supports view=status or view=schema.");
|
|
729
821
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: actors
|
|
3
3
|
description: Highest-density practical guide for pi-actors. Read this skill whenever prompt and tools are not enough for spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
|
|
4
4
|
metadata:
|
|
5
|
-
version: 0.
|
|
5
|
+
version: 0.27.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Actors (pi-actors)
|
|
@@ -41,7 +41,7 @@ Trusted local capability
|
|
|
41
41
|
- **Recipe**: saved JSON definition wrapping a template with args, defaults, imports, mailbox, artifacts, metadata, and optional `async: true`.
|
|
42
42
|
- **Run actor**: one detached execution instance addressable as `run:<id>` with status, logs, messages, mailbox metadata, files, communication snapshot, and artifacts.
|
|
43
43
|
- **Room actor**: shared timeline + roster endpoint addressable as `room:<run>`; every spawned run gets `room:<run>`.
|
|
44
|
-
- **Tool actor**: registered persistent capability addressable as `tool:<name>` and callable through the generated tool or `message`.
|
|
44
|
+
- **Tool actor**: registered persistent capability addressable as `tool:<name>` and callable through the generated tool or `message`. `tool:pi-actors` is reserved for runtime/status inspection.
|
|
45
45
|
- **Coordinator/session**: the current pi session endpoint that receives bounded actor follow-ups.
|
|
46
46
|
- **Mailbox**: public interaction contract: message types the actor accepts/emits.
|
|
47
47
|
- **Artifact**: named durable output path declared by a recipe/run.
|
|
@@ -102,6 +102,7 @@ Check `inspect view=mailbox` before domain-specific messages.
|
|
|
102
102
|
{ "target": "room:repo-health", "view": "roster" }
|
|
103
103
|
{ "target": "room:repo-health", "view": "contacts" }
|
|
104
104
|
{ "target": "room:repo-health", "view": "previews" }
|
|
105
|
+
{ "target": "tool:pi-actors", "view": "status" }
|
|
105
106
|
{ "target": "tool:music_player", "view": "status" }
|
|
106
107
|
{ "target": "recipes", "view": "status" }
|
|
107
108
|
{ "target": "coordinator", "view": "status" }
|
package/docs/tool-registry.md
CHANGED
|
@@ -19,15 +19,18 @@ Because the user recipe directory is sticky agent muscle memory, runtime launche
|
|
|
19
19
|
|
|
20
20
|
`register_tool` is the preferred agent-facing mutation API. It creates, updates, and deletes recipe files in `~/.pi/agent/recipes`; agents do not need to edit the files directly for normal registration. Direct file edits are still valid for operators and advanced agents. Runtime behavior is reactive: file creation, deletion, or edits in the user recipe root trigger validation and tool-set refresh, with invalid recipes surfaced as diagnostics rather than silently ignored.
|
|
21
21
|
|
|
22
|
-
Inspect the discovered registry with:
|
|
22
|
+
Inspect the loaded pi-actors runtime and discovered registry with:
|
|
23
23
|
|
|
24
24
|
```text
|
|
25
|
+
inspect target=tool:pi-actors view=status
|
|
25
26
|
inspect target=recipes view=status
|
|
26
27
|
inspect target=recipes view=doctor
|
|
27
28
|
inspect target=recipes view=summary verbose=true
|
|
28
29
|
```
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
`tool:pi-actors` is a reserved runtime-status actor: it reports the loaded package version, package root, source/dist mode, entrypoint path, recipe roots, and git commit when available. Use it after reloads to confirm which extension code is actually live.
|
|
32
|
+
|
|
33
|
+
The recipe summary reports active, shadowed, invalid, disabled, and diagnostic entries so operators can answer why a tool is present, hidden, broken, or disabled. The doctor view keeps the same registry evidence but promotes an advisory action surface: compact output includes the highest-priority `top` remediation plus ordered actions for invalid/blocking, disabled, risky shell-boundary, and shadowed recipes. Verbose inspection keeps the structured `remediations`, `top_action`, diagnostic details, and blocked lower-priority candidate paths when a broken or disabled higher-priority recipe masks a fallback.
|
|
31
34
|
|
|
32
35
|
## Registering Tools
|
|
33
36
|
|
package/lib/tools.ts
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* Owns generated runtime tool schemas and the register_tool management tool schema
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
|
|
7
11
|
import * as ActorMessages from "./actor-messages.ts";
|
|
8
12
|
import * as ActorRooms from "./actor-rooms.ts";
|
|
9
13
|
import * as AsyncRuns from "./async-runs.ts";
|
|
@@ -424,12 +428,30 @@ function compactActorFiles(status: Record<string, unknown>): string {
|
|
|
424
428
|
return `\nrun=${run}${artifactText}${files.length ? ` files=${files.join(",")}` : ""}`;
|
|
425
429
|
}
|
|
426
430
|
|
|
431
|
+
function summarizeOtherSessions(
|
|
432
|
+
currentSession: string,
|
|
433
|
+
allRuns: Array<Record<string, unknown>>,
|
|
434
|
+
): Record<string, unknown> {
|
|
435
|
+
const otherRuns = allRuns.filter(
|
|
436
|
+
(run) => run.ownerId && run.ownerId !== currentSession,
|
|
437
|
+
);
|
|
438
|
+
return {
|
|
439
|
+
other_runs: otherRuns.length,
|
|
440
|
+
other_sessions: new Set(otherRuns.map((run) => run.ownerId)).size,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
427
444
|
function compactSessionRuns(
|
|
428
445
|
session: string,
|
|
429
446
|
runs: Array<Record<string, unknown>>,
|
|
447
|
+
summary: Record<string, unknown> = {},
|
|
430
448
|
): string {
|
|
431
|
-
|
|
432
|
-
|
|
449
|
+
const suffix = [
|
|
450
|
+
summary.other_sessions !== undefined ? `other_sessions=${String(summary.other_sessions)}` : "",
|
|
451
|
+
summary.other_runs !== undefined ? `other_runs=${String(summary.other_runs)}` : "",
|
|
452
|
+
].filter(Boolean).join(" ");
|
|
453
|
+
if (runs.length === 0) return `\nsession=${session} runs=0${suffix ? ` ${suffix}` : ""}`;
|
|
454
|
+
return `\nsession=${session} runs=${runs.length}${suffix ? ` ${suffix}` : ""}\n${runs
|
|
433
455
|
.map((run) => {
|
|
434
456
|
const tokens = [
|
|
435
457
|
`run=${String(run.run ?? "")}`,
|
|
@@ -443,6 +465,39 @@ function compactSessionRuns(
|
|
|
443
465
|
.join("\n")}`;
|
|
444
466
|
}
|
|
445
467
|
|
|
468
|
+
function getPiActorsRuntimeStatus(): Record<string, unknown> {
|
|
469
|
+
const packagedRecipeRoot = Paths.getPackagedRecipeRoot();
|
|
470
|
+
const packageRoot = dirname(packagedRecipeRoot);
|
|
471
|
+
const packageJsonPath = join(packageRoot, "package.json");
|
|
472
|
+
const packageJson = existsSync(packageJsonPath)
|
|
473
|
+
? JSON.parse(readFileSync(packageJsonPath, "utf8")) as Record<string, unknown>
|
|
474
|
+
: {};
|
|
475
|
+
let git_commit: string | undefined;
|
|
476
|
+
try {
|
|
477
|
+
git_commit = execFileSync("git", ["-C", packageRoot, "rev-parse", "--short", "HEAD"], {
|
|
478
|
+
encoding: "utf8",
|
|
479
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
480
|
+
}).trim();
|
|
481
|
+
} catch {
|
|
482
|
+
git_commit = undefined;
|
|
483
|
+
}
|
|
484
|
+
const entrypoint = new URL(import.meta.url).pathname;
|
|
485
|
+
return {
|
|
486
|
+
entrypoint,
|
|
487
|
+
git_commit,
|
|
488
|
+
mode: entrypoint.includes("/dist/") ? "dist" : "source",
|
|
489
|
+
package_name: packageJson.name ?? "@llblab/pi-actors",
|
|
490
|
+
package_root: packageRoot,
|
|
491
|
+
recipe_root: Paths.getRecipeRoot(),
|
|
492
|
+
packaged_recipe_root: packagedRecipeRoot,
|
|
493
|
+
version: packageJson.version ?? "unknown",
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function compactPiActorsRuntimeStatus(status: Record<string, unknown>): string {
|
|
498
|
+
return `\npi-actors version=${String(status.version)} mode=${String(status.mode)} path=${String(status.package_root)} entrypoint=${String(status.entrypoint)}${status.git_commit ? ` git=${String(status.git_commit)}` : ""}`;
|
|
499
|
+
}
|
|
500
|
+
|
|
446
501
|
function compactToolActor(name: string, tool: Record<string, unknown>): string {
|
|
447
502
|
const parameters = asRecord(tool.parameters);
|
|
448
503
|
const required = Array.isArray(parameters.required)
|
|
@@ -535,6 +590,7 @@ function compactActorMessageResult(
|
|
|
535
590
|
`message=${result.sent === true || result.stopped === true ? "sent" : "not_sent"}`,
|
|
536
591
|
];
|
|
537
592
|
if (result.bytes !== undefined) tokens.push(`bytes=${String(result.bytes)}`);
|
|
593
|
+
if (result.queued === true) tokens.push("queued=true");
|
|
538
594
|
if (result.control) tokens.push(`control=${String(result.control)}`);
|
|
539
595
|
if (result.outbox) tokens.push(`messages=${String(result.outbox)}`);
|
|
540
596
|
if (result.message_count !== undefined)
|
|
@@ -546,6 +602,9 @@ function compactActorMessageResult(
|
|
|
546
602
|
if (result.stopped === true) tokens.push("stopped=true");
|
|
547
603
|
if (result.signal) tokens.push(`signal=${String(result.signal)}`);
|
|
548
604
|
if (result.invoked === true) tokens.push("invoked=true");
|
|
605
|
+
if (result.delivery_error) {
|
|
606
|
+
tokens.push(`delivery_error=${compactPreview(result.delivery_error, 96)}`);
|
|
607
|
+
}
|
|
549
608
|
return `\n${tokens.join(" ")}`;
|
|
550
609
|
}
|
|
551
610
|
|
|
@@ -695,7 +754,24 @@ async function routeBranchEnvelope(
|
|
|
695
754
|
recipient,
|
|
696
755
|
branchMessage,
|
|
697
756
|
);
|
|
698
|
-
|
|
757
|
+
try {
|
|
758
|
+
return await AsyncRuns.sendRunMessage(runId, JSON.stringify(branchMessage));
|
|
759
|
+
} catch (error) {
|
|
760
|
+
const record = error && typeof error === "object" ? (error as Record<string, unknown>) : {};
|
|
761
|
+
if (record.queued === true) {
|
|
762
|
+
return {
|
|
763
|
+
control_path: record.control_path,
|
|
764
|
+
control_type: record.control_type,
|
|
765
|
+
delivery_error: record.delivery_error ?? (error instanceof Error ? error.message : String(error)),
|
|
766
|
+
inbox_id: record.inbox_id,
|
|
767
|
+
queued: true,
|
|
768
|
+
run: runId,
|
|
769
|
+
sent: false,
|
|
770
|
+
state_dir: stateDir,
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
throw error;
|
|
774
|
+
}
|
|
699
775
|
}
|
|
700
776
|
|
|
701
777
|
function getRoomMulticastRecipients(
|
|
@@ -850,8 +926,17 @@ function assertRunAccessibleToContext(
|
|
|
850
926
|
const status = AsyncRuns.getRunStatus(runId);
|
|
851
927
|
const sessionId = getContextSessionId(ctx);
|
|
852
928
|
if (sessionId && status.ownerId && status.ownerId !== sessionId) {
|
|
853
|
-
throw
|
|
854
|
-
|
|
929
|
+
throw Object.assign(
|
|
930
|
+
new Error(
|
|
931
|
+
`run:${runId} reason=session_mismatch owner_session=${status.ownerId} current_session=${sessionId} hint=inspect_session:${status.ownerId}`,
|
|
932
|
+
),
|
|
933
|
+
{
|
|
934
|
+
current_session: sessionId,
|
|
935
|
+
hint: `inspect target=session:${status.ownerId} view=status`,
|
|
936
|
+
owner_session: status.ownerId,
|
|
937
|
+
reason: "session_mismatch",
|
|
938
|
+
run: runId,
|
|
939
|
+
},
|
|
855
940
|
);
|
|
856
941
|
}
|
|
857
942
|
return status;
|
|
@@ -940,24 +1025,24 @@ export function createInspectToolDefinition<TContext = unknown>(
|
|
|
940
1025
|
);
|
|
941
1026
|
}
|
|
942
1027
|
const session = requireContextSessionId(ctx, "inspect coordinator");
|
|
943
|
-
const
|
|
1028
|
+
const allRuns = AsyncRuns.listRuns(
|
|
944
1029
|
undefined,
|
|
945
1030
|
typeof input.status === "string" ? input.status : undefined,
|
|
946
|
-
)
|
|
947
|
-
|
|
948
|
-
|
|
1031
|
+
).map((run) => AsyncRuns.getRunStatus(String(run.state_dir)));
|
|
1032
|
+
const runs = allRuns.filter((run) => run.ownerId === session);
|
|
1033
|
+
const sessionSummary = summarizeOtherSessions(session, allRuns);
|
|
949
1034
|
return {
|
|
950
1035
|
content: [
|
|
951
1036
|
{
|
|
952
1037
|
type: "text" as const,
|
|
953
1038
|
text: maybeJsonText(
|
|
954
|
-
{ session, runs },
|
|
1039
|
+
{ session, runs, ...sessionSummary },
|
|
955
1040
|
input.verbose === true,
|
|
956
|
-
compactSessionRuns(session, runs),
|
|
1041
|
+
compactSessionRuns(session, runs, sessionSummary),
|
|
957
1042
|
),
|
|
958
1043
|
},
|
|
959
1044
|
],
|
|
960
|
-
details: { session, runs },
|
|
1045
|
+
details: { session, runs, ...sessionSummary },
|
|
961
1046
|
};
|
|
962
1047
|
}
|
|
963
1048
|
if (address.kind === "session") {
|
|
@@ -966,29 +1051,50 @@ export function createInspectToolDefinition<TContext = unknown>(
|
|
|
966
1051
|
"inspect session:<id> supports view=status or view=runs.",
|
|
967
1052
|
);
|
|
968
1053
|
}
|
|
969
|
-
const
|
|
1054
|
+
const allRuns = AsyncRuns.listRuns(
|
|
970
1055
|
undefined,
|
|
971
1056
|
typeof input.status === "string" ? input.status : undefined,
|
|
972
|
-
)
|
|
973
|
-
|
|
974
|
-
.
|
|
975
|
-
|
|
976
|
-
|
|
1057
|
+
).map((run) => AsyncRuns.getRunStatus(String(run.state_dir)));
|
|
1058
|
+
const runs = allRuns.filter(
|
|
1059
|
+
(run) => address.value === "all" || run.ownerId === address.value,
|
|
1060
|
+
);
|
|
1061
|
+
const sessionSummary = address.value === "all"
|
|
1062
|
+
? {}
|
|
1063
|
+
: summarizeOtherSessions(address.value || "", allRuns);
|
|
977
1064
|
return {
|
|
978
1065
|
content: [
|
|
979
1066
|
{
|
|
980
1067
|
type: "text" as const,
|
|
981
1068
|
text: maybeJsonText(
|
|
982
|
-
{ session: address.value, runs },
|
|
1069
|
+
{ session: address.value, runs, ...sessionSummary },
|
|
983
1070
|
input.verbose === true,
|
|
984
|
-
compactSessionRuns(address.value || "", runs),
|
|
1071
|
+
compactSessionRuns(address.value || "", runs, sessionSummary),
|
|
985
1072
|
),
|
|
986
1073
|
},
|
|
987
1074
|
],
|
|
988
|
-
details: { session: address.value, runs },
|
|
1075
|
+
details: { session: address.value, runs, ...sessionSummary },
|
|
989
1076
|
};
|
|
990
1077
|
}
|
|
991
1078
|
if (address.kind === "tool" && address.value) {
|
|
1079
|
+
if (address.value === "pi-actors") {
|
|
1080
|
+
if (view !== "status") {
|
|
1081
|
+
throw new Error("inspect tool:pi-actors supports view=status.");
|
|
1082
|
+
}
|
|
1083
|
+
const details = getPiActorsRuntimeStatus();
|
|
1084
|
+
return {
|
|
1085
|
+
content: [
|
|
1086
|
+
{
|
|
1087
|
+
type: "text" as const,
|
|
1088
|
+
text: maybeJsonText(
|
|
1089
|
+
details,
|
|
1090
|
+
input.verbose === true,
|
|
1091
|
+
compactPiActorsRuntimeStatus(details),
|
|
1092
|
+
),
|
|
1093
|
+
},
|
|
1094
|
+
],
|
|
1095
|
+
details,
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
992
1098
|
if (view !== "status" && view !== "schema") {
|
|
993
1099
|
throw new Error(
|
|
994
1100
|
"inspect tool:<name> supports view=status or view=schema.",
|
package/package.json
CHANGED
package/skills/actors/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: actors
|
|
3
3
|
description: Highest-density practical guide for pi-actors. Read this skill whenever prompt and tools are not enough for spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
|
|
4
4
|
metadata:
|
|
5
|
-
version: 0.
|
|
5
|
+
version: 0.27.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Actors (pi-actors)
|
|
@@ -41,7 +41,7 @@ Trusted local capability
|
|
|
41
41
|
- **Recipe**: saved JSON definition wrapping a template with args, defaults, imports, mailbox, artifacts, metadata, and optional `async: true`.
|
|
42
42
|
- **Run actor**: one detached execution instance addressable as `run:<id>` with status, logs, messages, mailbox metadata, files, communication snapshot, and artifacts.
|
|
43
43
|
- **Room actor**: shared timeline + roster endpoint addressable as `room:<run>`; every spawned run gets `room:<run>`.
|
|
44
|
-
- **Tool actor**: registered persistent capability addressable as `tool:<name>` and callable through the generated tool or `message`.
|
|
44
|
+
- **Tool actor**: registered persistent capability addressable as `tool:<name>` and callable through the generated tool or `message`. `tool:pi-actors` is reserved for runtime/status inspection.
|
|
45
45
|
- **Coordinator/session**: the current pi session endpoint that receives bounded actor follow-ups.
|
|
46
46
|
- **Mailbox**: public interaction contract: message types the actor accepts/emits.
|
|
47
47
|
- **Artifact**: named durable output path declared by a recipe/run.
|
|
@@ -102,6 +102,7 @@ Check `inspect view=mailbox` before domain-specific messages.
|
|
|
102
102
|
{ "target": "room:repo-health", "view": "roster" }
|
|
103
103
|
{ "target": "room:repo-health", "view": "contacts" }
|
|
104
104
|
{ "target": "room:repo-health", "view": "previews" }
|
|
105
|
+
{ "target": "tool:pi-actors", "view": "status" }
|
|
105
106
|
{ "target": "tool:music_player", "view": "status" }
|
|
106
107
|
{ "target": "recipes", "view": "status" }
|
|
107
108
|
{ "target": "coordinator", "view": "status" }
|
package/skills/swarm/SKILL.md
CHANGED