@llblab/pi-actors 0.23.0 → 0.24.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/AGENTS.md +125 -60
- package/BACKLOG.md +93 -188
- package/CHANGELOG.md +22 -0
- package/README.md +13 -0
- package/dist/fixtures/protocol/actor-message-branch.json +13 -0
- package/dist/fixtures/protocol/artifact-manifest.json +9 -0
- package/dist/fixtures/protocol/mailbox-contract.json +15 -0
- package/dist/fixtures/protocol/recipe-summary.json +16 -0
- package/dist/fixtures/protocol/room-message.json +11 -0
- package/dist/fixtures/protocol/room-roster.json +11 -0
- package/dist/fixtures/protocol/run-inbox-message.json +9 -0
- package/dist/fixtures/protocol/run-outbox-event.json +9 -0
- package/dist/fixtures/protocol/run-state.json +10 -0
- package/dist/index.js +3 -2
- package/dist/lib/actor-inspector-tui.js +5 -32
- package/dist/lib/actor-rooms.d.ts +8 -0
- package/dist/lib/actor-rooms.js +64 -20
- package/dist/lib/actor-worker.d.ts +13 -0
- package/dist/lib/actor-worker.js +86 -0
- package/dist/lib/async-runner.d.ts +5 -0
- package/dist/lib/async-runner.js +134 -0
- package/dist/lib/async-runs.js +9 -28
- package/dist/lib/build-dist.d.ts +5 -0
- package/dist/lib/build-dist.js +24 -0
- package/dist/lib/conformance.d.ts +12 -0
- package/dist/lib/conformance.js +28 -0
- package/dist/lib/coordinator.d.ts +5 -0
- package/dist/lib/coordinator.js +557 -0
- package/dist/lib/locker.d.ts +5 -0
- package/dist/lib/locker.js +310 -0
- package/dist/lib/mailbox-loop.d.ts +41 -0
- package/dist/lib/mailbox-loop.js +62 -0
- package/dist/lib/observability.d.ts +2 -2
- package/dist/lib/observability.js +51 -53
- package/dist/lib/prompts.d.ts +1 -1
- package/dist/lib/prompts.js +1 -1
- package/dist/lib/recipe-references.js +25 -2
- package/dist/lib/recipe-utils.d.ts +5 -0
- package/dist/lib/recipe-utils.js +385 -0
- package/dist/lib/runtime-notifier.js +3 -7
- package/dist/lib/state-readers.d.ts +21 -0
- package/dist/lib/state-readers.js +74 -0
- package/dist/lib/tools.js +1 -1
- package/dist/lib/validate-recipe.d.ts +6 -0
- package/dist/lib/validate-recipe.js +104 -0
- package/dist/recipes/actor-worker.json +35 -0
- package/dist/recipes/coordinator-locker.json +45 -0
- package/dist/recipes/lens-swarm.json +66 -0
- package/dist/recipes/locker.json +45 -0
- package/dist/recipes/music-player.json +38 -0
- package/dist/recipes/pipeline-architect-coordinator.json +95 -0
- package/dist/recipes/pipeline-artifact-bundle.json +100 -0
- package/dist/recipes/pipeline-artifact-report.json +58 -0
- package/dist/recipes/pipeline-artifact-write.json +72 -0
- package/dist/recipes/pipeline-async-run-ops.json +70 -0
- package/dist/recipes/pipeline-checkpoint-continuation.json +67 -0
- package/dist/recipes/pipeline-development-tasking.json +81 -0
- package/dist/recipes/pipeline-docs-maintenance.json +80 -0
- package/dist/recipes/pipeline-media-library.json +59 -0
- package/dist/recipes/pipeline-quorum-review.json +79 -0
- package/dist/recipes/pipeline-release-readiness.json +110 -0
- package/dist/recipes/pipeline-release-summary.json +88 -0
- package/dist/recipes/pipeline-repo-health.json +89 -0
- package/dist/recipes/pipeline-research-synthesis.json +94 -0
- package/dist/recipes/pipeline-review-readiness.json +54 -0
- package/dist/recipes/pipeline-room-swarm.json +50 -0
- package/dist/recipes/subagent-artifact.json +32 -0
- package/dist/recipes/subagent-checkpoint.json +33 -0
- package/dist/recipes/subagent-conflict-report.json +32 -0
- package/dist/recipes/subagent-contradiction-map.json +33 -0
- package/dist/recipes/subagent-critic.json +35 -0
- package/dist/recipes/subagent-evidence-map.json +33 -0
- package/dist/recipes/subagent-followup.json +33 -0
- package/dist/recipes/subagent-judge.json +33 -0
- package/dist/recipes/subagent-merge.json +33 -0
- package/dist/recipes/subagent-message.json +34 -0
- package/dist/recipes/subagent-normalize.json +31 -0
- package/dist/recipes/subagent-plan.json +33 -0
- package/dist/recipes/subagent-prompt.json +28 -0
- package/dist/recipes/subagent-quorum.json +43 -0
- package/dist/recipes/subagent-review-coordinator.json +114 -0
- package/dist/recipes/subagent-review.json +37 -0
- package/dist/recipes/subagent-task-card.json +35 -0
- package/dist/recipes/subagent-tools.json +27 -0
- package/dist/recipes/subagent-verify.json +34 -0
- package/dist/recipes/subagents-prompts.json +51 -0
- package/dist/recipes/utility-actor-message.json +23 -0
- package/dist/recipes/utility-artifact-manifest.json +16 -0
- package/dist/recipes/utility-artifact-write.json +16 -0
- package/dist/recipes/utility-changelog-head.json +11 -0
- package/dist/recipes/utility-changelog-section.json +13 -0
- package/dist/recipes/utility-coordinator-lock-snapshot.json +13 -0
- package/dist/recipes/utility-git-log.json +11 -0
- package/dist/recipes/utility-git-status.json +9 -0
- package/dist/recipes/utility-jsonl-tail.json +10 -0
- package/dist/recipes/utility-markdown-index.json +14 -0
- package/dist/recipes/utility-package-summary.json +11 -0
- package/dist/recipes/utility-playlist-build.json +17 -0
- package/dist/recipes/utility-playlist-scan.json +11 -0
- package/dist/recipes/utility-run-ops-snapshot.json +17 -0
- package/dist/recipes/utility-run-state-files.json +13 -0
- package/dist/recipes/utility-run-summary.json +11 -0
- package/dist/recipes/utility-skill-summary.json +13 -0
- package/dist/recipes/utility-validate-recipe.json +13 -0
- package/dist/recipes/utility-validation-wrapper.json +13 -0
- package/dist/scripts/actor-worker.mjs +31 -0
- package/dist/scripts/async-runner.mjs +31 -0
- package/dist/scripts/build-dist.mjs +25 -0
- package/dist/scripts/conformance.mjs +33 -0
- package/dist/scripts/coordinator.mjs +31 -0
- package/dist/scripts/locker.mjs +33 -0
- package/dist/scripts/music-player.mjs +964 -0
- package/dist/scripts/recipe-utils.mjs +31 -0
- package/dist/scripts/validate-recipe.mjs +34 -0
- package/dist/skills/actors/SKILL.md +375 -0
- package/dist/skills/swarm/SKILL.md +467 -0
- package/dist/skills/swarm/references/development-swarm.md +596 -0
- package/docs/actor-messages.md +2 -2
- package/docs/async-runs.md +11 -0
- package/docs/template-recipes.md +1 -1
- package/fixtures/protocol/actor-message-branch.json +13 -0
- package/fixtures/protocol/artifact-manifest.json +9 -0
- package/fixtures/protocol/mailbox-contract.json +15 -0
- package/fixtures/protocol/recipe-summary.json +16 -0
- package/fixtures/protocol/room-message.json +11 -0
- package/fixtures/protocol/room-roster.json +11 -0
- package/fixtures/protocol/run-inbox-message.json +9 -0
- package/fixtures/protocol/run-outbox-event.json +9 -0
- package/fixtures/protocol/run-state.json +10 -0
- package/index.ts +3 -0
- package/lib/actor-inspector-tui.ts +11 -34
- package/lib/actor-rooms.ts +88 -18
- package/lib/actor-worker.ts +118 -0
- package/lib/async-runner.ts +173 -0
- package/lib/async-runs.ts +12 -22
- package/lib/build-dist.ts +30 -0
- package/lib/conformance.ts +46 -0
- package/lib/coordinator.ts +649 -0
- package/lib/locker.ts +340 -0
- package/lib/mailbox-loop.ts +148 -0
- package/lib/observability.ts +24 -19
- package/lib/prompts.ts +1 -1
- package/lib/recipe-references.ts +37 -2
- package/lib/recipe-utils.ts +486 -0
- package/lib/runtime-notifier.ts +4 -6
- package/lib/state-readers.ts +93 -0
- package/lib/tools.ts +1 -1
- package/lib/validate-recipe.ts +110 -0
- package/package.json +10 -2
- package/recipes/actor-worker.json +35 -0
- package/recipes/pipeline-quorum-review.json +12 -7
- package/scripts/actor-worker.mjs +31 -0
- package/scripts/async-runner.mjs +11 -201
- package/scripts/build-dist.mjs +25 -0
- package/scripts/conformance.mjs +21 -35
- package/scripts/coordinator.mjs +15 -625
- package/scripts/locker.mjs +20 -332
- package/scripts/recipe-utils.mjs +17 -477
- package/scripts/validate-recipe.mjs +18 -121
- package/skills/actors/SKILL.md +5 -2
- package/skills/swarm/SKILL.md +3 -1
package/README.md
CHANGED
|
@@ -43,6 +43,8 @@ Or from git:
|
|
|
43
43
|
pi install git:github.com/llblab/pi-actors
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
The npm package is dist-first for JavaScript-only runtimes: default Pi metadata points at compiled `dist/` entrypoints and mirrored runtime assets. Source TypeScript and source skills remain in the package for TypeScript-native runtimes through optional source metadata.
|
|
47
|
+
|
|
46
48
|
## Address Surface
|
|
47
49
|
|
|
48
50
|
Actors and coordination endpoints are addressed with compact route strings:
|
|
@@ -255,6 +257,17 @@ Use mailbox declarations when an actor has a stable conversational surface.
|
|
|
255
257
|
|
|
256
258
|
Core actor state, inspection, foreground tools, and basic async runs are portable Node.js behavior. Run-local messaging and stop/kill use a platform adapter under the same `message` API: Unix-compatible recipes can use their existing local control endpoint, while native Windows recipes can expose a Windows-native endpoint in run state. Some packaged scripts still depend on Unix tools and are WSL/Linux/macOS-only until migrated; their public recipe surface should stay `spawn` / `message` / `inspect` either way.
|
|
257
259
|
|
|
260
|
+
| Surface | Linux/macOS/WSL | Native Windows |
|
|
261
|
+
| --- | --- | --- |
|
|
262
|
+
| Foreground tools, recipe discovery, inspect | Supported | Supported |
|
|
263
|
+
| Async runs and file-backed state | Supported | Supported |
|
|
264
|
+
| Mailbox-only actors and worker recipe | Supported | Supported |
|
|
265
|
+
| FIFO control endpoints | Supported | Not supported; use mailbox or named pipe |
|
|
266
|
+
| Named-pipe control endpoints | Not needed | Supported when recipe exposes one |
|
|
267
|
+
| Process cancel/kill | Process group signal with pid fallback | Windows process-tree adapter |
|
|
268
|
+
|
|
269
|
+
Packaged recipes should prefer mailbox/wake behavior for portable control. Recipes that require FIFO, Unix shell tools, or platform-specific media backends should make that limitation visible in docs or diagnostics before launch.
|
|
270
|
+
|
|
258
271
|
## Safety Boundary
|
|
259
272
|
|
|
260
273
|
`pi-actors` is local-first, not sandbox-first.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"to": "branch:demo/reviewer",
|
|
3
|
+
"from": "run:demo",
|
|
4
|
+
"type": "task.assign",
|
|
5
|
+
"summary": "Review the current slice",
|
|
6
|
+
"body": {
|
|
7
|
+
"task": "Check mailbox loop semantics"
|
|
8
|
+
},
|
|
9
|
+
"correlation_id": "task-001",
|
|
10
|
+
"metadata": {
|
|
11
|
+
"requires_response": true
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "actor-worker",
|
|
3
|
+
"path": "recipes/actor-worker.json",
|
|
4
|
+
"location": "packaged",
|
|
5
|
+
"active": true,
|
|
6
|
+
"args": [
|
|
7
|
+
"run",
|
|
8
|
+
"branch",
|
|
9
|
+
"poll_ms",
|
|
10
|
+
"state_dir"
|
|
11
|
+
],
|
|
12
|
+
"mailbox": {
|
|
13
|
+
"kind": "branch",
|
|
14
|
+
"accepts": ["task.assign", "control.stop"]
|
|
15
|
+
}
|
|
16
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -39,6 +39,7 @@ export default function toolRegistryExtension(pi) {
|
|
|
39
39
|
const runDirWatchers = new Map();
|
|
40
40
|
const observedRuns = new Map();
|
|
41
41
|
const observedRunEventLines = new Map();
|
|
42
|
+
const observedRunOutboxEventIds = new Map();
|
|
42
43
|
const retirementAttempts = new Set();
|
|
43
44
|
let runStatusFrame = 0;
|
|
44
45
|
let communicationWidgetVisible = false;
|
|
@@ -102,7 +103,7 @@ export default function toolRegistryExtension(pi) {
|
|
|
102
103
|
}
|
|
103
104
|
: undefined, { placement: "belowEditor" });
|
|
104
105
|
const transitions = Observability.detectRunTransitions(observedRuns, summary);
|
|
105
|
-
const outboxEvents = Observability.detectRunOutboxEvents(observedRunEventLines, summary);
|
|
106
|
+
const outboxEvents = Observability.detectRunOutboxEvents(observedRunEventLines, summary, observedRunOutboxEventIds);
|
|
106
107
|
if (!notify)
|
|
107
108
|
return;
|
|
108
109
|
retireCandidateRuns(ctx, summary);
|
|
@@ -121,7 +122,7 @@ export default function toolRegistryExtension(pi) {
|
|
|
121
122
|
details: transition,
|
|
122
123
|
}, { deliverAs: "followUp", triggerTurn: true });
|
|
123
124
|
}
|
|
124
|
-
Observability.pruneRunObservationState(observedRuns, observedRunEventLines, summary, transitions.map((transition) => transition.stateDir ?? transition.run));
|
|
125
|
+
Observability.pruneRunObservationState(observedRuns, observedRunEventLines, summary, transitions.map((transition) => transition.stateDir ?? transition.run), observedRunOutboxEventIds);
|
|
125
126
|
for (const event of outboxEvents) {
|
|
126
127
|
if (!Observability.shouldNotifyRunOutboxEvent(event))
|
|
127
128
|
continue;
|
|
@@ -7,31 +7,14 @@ import * as path from "node:path";
|
|
|
7
7
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
8
8
|
import * as Limits from "./limits.js";
|
|
9
9
|
import * as Paths from "./paths.js";
|
|
10
|
+
import { readJsonFileResilient, readJsonlFileResilient } from "./state-readers.js";
|
|
10
11
|
function asRecord(value) {
|
|
11
12
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
12
13
|
? value
|
|
13
14
|
: {};
|
|
14
15
|
}
|
|
15
16
|
function readJsonLines(file) {
|
|
16
|
-
|
|
17
|
-
return fs
|
|
18
|
-
.readFileSync(file, "utf8")
|
|
19
|
-
.split("\n")
|
|
20
|
-
.filter(Boolean)
|
|
21
|
-
.flatMap((line) => {
|
|
22
|
-
try {
|
|
23
|
-
return [JSON.parse(line)];
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return [];
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
catch (error) {
|
|
31
|
-
if (error.code === "ENOENT")
|
|
32
|
-
return [];
|
|
33
|
-
return [];
|
|
34
|
-
}
|
|
17
|
+
return readJsonlFileResilient(file).records;
|
|
35
18
|
}
|
|
36
19
|
function previewValue(value, maxLength = Limits.INSPECTOR_BODY_PREVIEW_CHARS) {
|
|
37
20
|
if (value === undefined)
|
|
@@ -85,12 +68,7 @@ function previewFromMessage(run, message, timestamp, displayNames = {}) {
|
|
|
85
68
|
};
|
|
86
69
|
}
|
|
87
70
|
function readRoomRosterRecords(stateDir, room) {
|
|
88
|
-
|
|
89
|
-
return JSON.parse(fs.readFileSync(path.join(stateDir, "rooms", room, "roster.json"), "utf8"));
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
return {};
|
|
93
|
-
}
|
|
71
|
+
return readJsonFileResilient(path.join(stateDir, "rooms", room, "roster.json"), {}).value;
|
|
94
72
|
}
|
|
95
73
|
function memberDisplay(_address, member) {
|
|
96
74
|
const display = typeof member.display === "string" ? member.display.trim() : "";
|
|
@@ -170,13 +148,8 @@ function readOutboxPreviews(run, stateDir) {
|
|
|
170
148
|
.filter((preview) => Boolean(preview));
|
|
171
149
|
}
|
|
172
150
|
function getRunOwnerId(stateDir) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
return typeof meta.ownerId === "string" ? meta.ownerId : undefined;
|
|
176
|
-
}
|
|
177
|
-
catch {
|
|
178
|
-
return undefined;
|
|
179
|
-
}
|
|
151
|
+
const meta = readJsonFileResilient(path.join(stateDir, "run.json"), undefined).value;
|
|
152
|
+
return typeof meta?.ownerId === "string" ? meta.ownerId : undefined;
|
|
180
153
|
}
|
|
181
154
|
function matchesOwner(stateDir, ownerId) {
|
|
182
155
|
return ownerId === undefined || getRunOwnerId(stateDir) === ownerId;
|
|
@@ -44,6 +44,8 @@ export interface RoomStatus {
|
|
|
44
44
|
message_count: number;
|
|
45
45
|
roster_count: number;
|
|
46
46
|
compaction?: RoomCompactionInfo;
|
|
47
|
+
diagnostics?: string[];
|
|
48
|
+
diagnostics_count?: number;
|
|
47
49
|
last_message_at?: string;
|
|
48
50
|
last_message_from?: string;
|
|
49
51
|
last_message_summary?: string;
|
|
@@ -71,6 +73,11 @@ export interface ActorCommunicationSnapshot {
|
|
|
71
73
|
}
|
|
72
74
|
export declare function readRoomRoster(stateDir: string, room: string): Record<string, RoomMember>;
|
|
73
75
|
export interface BranchInboxRecord extends ActorMessage {
|
|
76
|
+
claimed_at?: string;
|
|
77
|
+
claimed_by?: string;
|
|
78
|
+
error?: string;
|
|
79
|
+
failed_at?: string;
|
|
80
|
+
handled_at?: string;
|
|
74
81
|
id?: string;
|
|
75
82
|
queued_at?: string;
|
|
76
83
|
status?: string;
|
|
@@ -83,6 +90,7 @@ export declare function readBranchInboxMessages(stateDir: string, run: string, a
|
|
|
83
90
|
export declare function readBranchInboxDiagnostics(stateDir: string, run: string, address: string, limit?: number): BranchInboxReadResult;
|
|
84
91
|
export declare function getBranchInboxTerminalRetainLimit(): number;
|
|
85
92
|
export declare function appendBranchInboxMessage(stateDir: string, run: string, address: string, message: ActorMessage): void;
|
|
93
|
+
export declare function claimBranchInboxMessage(stateDir: string, run: string, address: string, owner?: string, statuses?: string[]): BranchInboxRecord | undefined;
|
|
86
94
|
export declare function updateBranchInboxMessageStatus(stateDir: string, run: string, address: string, id: string, status: "claimed" | "handled" | "failed", metadata?: Record<string, unknown>): boolean;
|
|
87
95
|
export declare function appendRoomMessage(stateDir: string, room: string, message: ActorMessage): RoomAppendResult;
|
|
88
96
|
export declare function readRoomMessages(stateDir: string, room: string, limit?: number): RoomTimelineEntry[];
|
package/dist/lib/actor-rooms.js
CHANGED
|
@@ -8,6 +8,7 @@ import { randomUUID } from "node:crypto";
|
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import * as Limits from "./limits.js";
|
|
10
10
|
import { notifyRuntimeWake } from "./runtime-notifier.js";
|
|
11
|
+
import { formatStateReadDiagnostics, readJsonFileResilient, readJsonlFileResilient, } from "./state-readers.js";
|
|
11
12
|
const STATE_LOCK_MAX_AGE_MS = 5 * 60 * 1000;
|
|
12
13
|
const STATE_LOCK_TIMEOUT_MS = 5000;
|
|
13
14
|
const DEFAULT_ROOM_MAX_MESSAGES = 10000;
|
|
@@ -87,14 +88,7 @@ function runFromRoomAddress(address) {
|
|
|
87
88
|
return match?.[1];
|
|
88
89
|
}
|
|
89
90
|
function readJsonFile(file, fallback) {
|
|
90
|
-
|
|
91
|
-
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
92
|
-
}
|
|
93
|
-
catch (error) {
|
|
94
|
-
if (error.code === "ENOENT")
|
|
95
|
-
return fallback;
|
|
96
|
-
throw error;
|
|
97
|
-
}
|
|
91
|
+
return readJsonFileResilient(file, fallback).value;
|
|
98
92
|
}
|
|
99
93
|
function writeJsonFile(file, value) {
|
|
100
94
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -394,6 +388,43 @@ export function appendBranchInboxMessage(stateDir, run, address, message) {
|
|
|
394
388
|
releaseLock();
|
|
395
389
|
}
|
|
396
390
|
}
|
|
391
|
+
export function claimBranchInboxMessage(stateDir, run, address, owner = "runtime", statuses = ["queued"]) {
|
|
392
|
+
const branch = branchIdFromAddress(address, run);
|
|
393
|
+
if (!branch)
|
|
394
|
+
throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
|
|
395
|
+
const releaseLock = acquireBranchInboxLock(stateDir, branch);
|
|
396
|
+
try {
|
|
397
|
+
const file = branchInboxFile(stateDir, branch);
|
|
398
|
+
const records = readAllBranchInboxLines(stateDir, run, address);
|
|
399
|
+
let claimed;
|
|
400
|
+
const now = new Date().toISOString();
|
|
401
|
+
const updated = records.map((record) => {
|
|
402
|
+
if (claimed || !("message" in record))
|
|
403
|
+
return record;
|
|
404
|
+
const status = record.message.status ?? "queued";
|
|
405
|
+
if (!statuses.includes(status))
|
|
406
|
+
return record;
|
|
407
|
+
claimed = {
|
|
408
|
+
...record.message,
|
|
409
|
+
claimed_at: now,
|
|
410
|
+
claimed_by: owner,
|
|
411
|
+
status: "claimed",
|
|
412
|
+
};
|
|
413
|
+
return { message: claimed };
|
|
414
|
+
});
|
|
415
|
+
if (!claimed)
|
|
416
|
+
return undefined;
|
|
417
|
+
fs.writeFileSync(file, `${updated.map((record) => ("raw" in record ? record.raw : JSON.stringify(record.message))).join("\n")}\n`);
|
|
418
|
+
notifyActorWake(stateDir, address, "branch.inbox.claim", {
|
|
419
|
+
id: claimed.id,
|
|
420
|
+
owner,
|
|
421
|
+
});
|
|
422
|
+
return claimed;
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
releaseLock();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
397
428
|
export function updateBranchInboxMessageStatus(stateDir, run, address, id, status, metadata = {}) {
|
|
398
429
|
const branch = branchIdFromAddress(address, run);
|
|
399
430
|
if (!branch)
|
|
@@ -469,7 +500,14 @@ export function appendRoomMessage(stateDir, room, message) {
|
|
|
469
500
|
export function readRoomMessages(stateDir, room, limit = 40) {
|
|
470
501
|
try {
|
|
471
502
|
const lines = readJsonlTailLines(messagesFile(stateDir, room), limit);
|
|
472
|
-
return lines.
|
|
503
|
+
return lines.flatMap((line) => {
|
|
504
|
+
try {
|
|
505
|
+
return [JSON.parse(line)];
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
return [];
|
|
509
|
+
}
|
|
510
|
+
});
|
|
473
511
|
}
|
|
474
512
|
catch (error) {
|
|
475
513
|
if (error.code === "ENOENT")
|
|
@@ -503,7 +541,14 @@ export function readRoomMessagePreviews(stateDir, room, limit = 40) {
|
|
|
503
541
|
export function getRoomStatus(stateDir, room) {
|
|
504
542
|
const messageCount = readRoomMessageCount(stateDir, room);
|
|
505
543
|
const [last] = readRoomMessages(stateDir, room, 1);
|
|
506
|
-
const
|
|
544
|
+
const compactionRead = readJsonFileResilient(path.join(roomDir(stateDir, room), "compaction.json"), undefined);
|
|
545
|
+
const rosterRead = readJsonFileResilient(rosterFile(stateDir, room), {});
|
|
546
|
+
const messageRead = readJsonlFileResilient(messagesFile(stateDir, room));
|
|
547
|
+
const diagnostics = [
|
|
548
|
+
...messageRead.diagnostics,
|
|
549
|
+
...rosterRead.diagnostics,
|
|
550
|
+
...compactionRead.diagnostics,
|
|
551
|
+
];
|
|
507
552
|
return {
|
|
508
553
|
...(last
|
|
509
554
|
? {
|
|
@@ -513,10 +558,16 @@ export function getRoomStatus(stateDir, room) {
|
|
|
513
558
|
last_message_type: last.type,
|
|
514
559
|
}
|
|
515
560
|
: {}),
|
|
516
|
-
...(
|
|
561
|
+
...(compactionRead.value ? { compaction: compactionRead.value } : {}),
|
|
562
|
+
...(diagnostics.length
|
|
563
|
+
? {
|
|
564
|
+
diagnostics: formatStateReadDiagnostics(diagnostics),
|
|
565
|
+
diagnostics_count: diagnostics.length,
|
|
566
|
+
}
|
|
567
|
+
: {}),
|
|
517
568
|
message_count: messageCount,
|
|
518
569
|
room,
|
|
519
|
-
roster_count: Object.keys(
|
|
570
|
+
roster_count: Object.keys(rosterRead.value).length,
|
|
520
571
|
};
|
|
521
572
|
}
|
|
522
573
|
export function ensureRoomMember(stateDir, run, room, address, body, summary) {
|
|
@@ -541,14 +592,7 @@ export function ensureDefaultRoom(stateDir, run) {
|
|
|
541
592
|
return ensureRoomMember(stateDir, run, "main", `run:${run}`, { role: "run", status: "present" }, "Run joined default room");
|
|
542
593
|
}
|
|
543
594
|
export function readCommunicationSnapshot(stateDir) {
|
|
544
|
-
|
|
545
|
-
return JSON.parse(fs.readFileSync(snapshotFile(stateDir), "utf8"));
|
|
546
|
-
}
|
|
547
|
-
catch (error) {
|
|
548
|
-
if (error.code === "ENOENT")
|
|
549
|
-
return undefined;
|
|
550
|
-
throw error;
|
|
551
|
-
}
|
|
595
|
+
return readJsonFileResilient(snapshotFile(stateDir), undefined).value;
|
|
552
596
|
}
|
|
553
597
|
export function readRoomContacts(stateDir, room, self) {
|
|
554
598
|
return Object.values(readRoomRoster(stateDir, room))
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical mailbox-backed actor worker entrypoint logic.
|
|
3
|
+
* Zones: worker demo, mailbox loop recipe runtime
|
|
4
|
+
*/
|
|
5
|
+
interface ActorWorkerArgs {
|
|
6
|
+
branch?: string;
|
|
7
|
+
poll_ms?: string | number;
|
|
8
|
+
run?: string;
|
|
9
|
+
state_dir?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function parseActorWorkerArgs(argv: string[]): ActorWorkerArgs;
|
|
12
|
+
export declare function runActorWorker(argv?: string[]): Promise<void>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical mailbox-backed actor worker entrypoint logic.
|
|
3
|
+
* Zones: worker demo, mailbox loop recipe runtime
|
|
4
|
+
*/
|
|
5
|
+
import { appendFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { appendRoomMessage, ensureRoomMember } from "./actor-rooms.js";
|
|
8
|
+
import { handleMailboxLoopOnce, isMailboxLoopStopMessage } from "./mailbox-loop.js";
|
|
9
|
+
export function parseActorWorkerArgs(argv) {
|
|
10
|
+
const args = {};
|
|
11
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
12
|
+
const token = argv[index];
|
|
13
|
+
if (!token.startsWith("--"))
|
|
14
|
+
continue;
|
|
15
|
+
const key = token.slice(2).replaceAll("-", "_");
|
|
16
|
+
const next = argv[index + 1];
|
|
17
|
+
if (!next || next.startsWith("--")) {
|
|
18
|
+
args[key] = true;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
args[key] = next;
|
|
22
|
+
index += 1;
|
|
23
|
+
}
|
|
24
|
+
return args;
|
|
25
|
+
}
|
|
26
|
+
function sleep(ms) {
|
|
27
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
28
|
+
}
|
|
29
|
+
function text(value) {
|
|
30
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
31
|
+
}
|
|
32
|
+
export async function runActorWorker(argv = process.argv.slice(2)) {
|
|
33
|
+
const args = parseActorWorkerArgs(argv);
|
|
34
|
+
const stateDir = String(args.state_dir ?? "");
|
|
35
|
+
const run = String(args.run ?? "");
|
|
36
|
+
const branch = String(args.branch ?? "worker");
|
|
37
|
+
const pollMs = Math.max(50, Number(args.poll_ms ?? 1000));
|
|
38
|
+
if (!stateDir || !run) {
|
|
39
|
+
throw new Error("usage: actor-worker.mjs --state-dir <dir> --run <id> [--branch worker] [--poll-ms 1000]");
|
|
40
|
+
}
|
|
41
|
+
const branchAddress = `branch:${run}/${branch}`;
|
|
42
|
+
const roomAddress = `room:${run}`;
|
|
43
|
+
const journalPath = join(stateDir, "worker-events.jsonl");
|
|
44
|
+
function journal(event, data = {}) {
|
|
45
|
+
appendFileSync(journalPath, `${JSON.stringify({ event, ts: new Date().toISOString(), ...data })}\n`);
|
|
46
|
+
}
|
|
47
|
+
function room(type, summary, body = {}) {
|
|
48
|
+
appendRoomMessage(stateDir, "main", {
|
|
49
|
+
body,
|
|
50
|
+
from: branchAddress,
|
|
51
|
+
summary,
|
|
52
|
+
to: roomAddress,
|
|
53
|
+
type,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
ensureRoomMember(stateDir, run, "main", branchAddress, { display: branch, role: "worker", status: "present" }, `${branch} joined as mailbox worker`);
|
|
57
|
+
room("awaiting_assignment", `${branch} awaiting assignment`, { branch });
|
|
58
|
+
journal("worker.started", { branch, run });
|
|
59
|
+
let stopping = false;
|
|
60
|
+
while (!stopping) {
|
|
61
|
+
const result = await handleMailboxLoopOnce({ address: branchAddress, kind: "branch", run, stateDir }, (message) => {
|
|
62
|
+
if (isMailboxLoopStopMessage(message)) {
|
|
63
|
+
stopping = true;
|
|
64
|
+
room("actor.leave", `${branch} stopping`, { branch, reason: message.type });
|
|
65
|
+
journal("worker.stopping", { id: message.id, type: message.type });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
room("task.claim", `${branch} claimed ${message.type}`, {
|
|
69
|
+
branch,
|
|
70
|
+
id: message.id,
|
|
71
|
+
type: message.type,
|
|
72
|
+
});
|
|
73
|
+
room("task.result", `${branch} handled ${message.type}`, {
|
|
74
|
+
branch,
|
|
75
|
+
id: message.id,
|
|
76
|
+
result: text(message.body),
|
|
77
|
+
type: message.type,
|
|
78
|
+
});
|
|
79
|
+
room("awaiting_assignment", `${branch} awaiting assignment`, { branch });
|
|
80
|
+
journal("task.handled", { id: message.id, type: message.type });
|
|
81
|
+
}, { owner: branchAddress });
|
|
82
|
+
if (!result.handled)
|
|
83
|
+
await sleep(pollMs);
|
|
84
|
+
}
|
|
85
|
+
journal("worker.done", { branch, run });
|
|
86
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detached async-runner entrypoint logic.
|
|
3
|
+
* Zones: async run process, command-template execution telemetry
|
|
4
|
+
*/
|
|
5
|
+
import { appendFileSync, readFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { appendRecipeContextToPiArgs } from "./actor-recipe-context.js";
|
|
8
|
+
import { execCommandTemplate } from "./command-templates.js";
|
|
9
|
+
import { executeRegisteredTool } from "./execution.js";
|
|
10
|
+
import { writeJsonAtomic } from "./file-state.js";
|
|
11
|
+
function quoteCommandDetailPart(value) {
|
|
12
|
+
if (value === "")
|
|
13
|
+
return "''";
|
|
14
|
+
if (/^[A-Za-z0-9_/:=.,@%+\-]+$/.test(value))
|
|
15
|
+
return value;
|
|
16
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
17
|
+
}
|
|
18
|
+
function formatCommandDetail(command, args) {
|
|
19
|
+
return [command, ...args].map(quoteCommandDetailPart).join(" ");
|
|
20
|
+
}
|
|
21
|
+
function summarizeCommandDetail(commandDetail) {
|
|
22
|
+
return commandDetail.length > 160
|
|
23
|
+
? `${commandDetail.slice(0, 157)}...`
|
|
24
|
+
: commandDetail;
|
|
25
|
+
}
|
|
26
|
+
export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
27
|
+
if (!stateDir)
|
|
28
|
+
throw new Error("missing state dir");
|
|
29
|
+
const runPath = join(stateDir, "run.json");
|
|
30
|
+
const progressPath = join(stateDir, "progress.json");
|
|
31
|
+
const resultPath = join(stateDir, "result.json");
|
|
32
|
+
const eventsPath = join(stateDir, "events.jsonl");
|
|
33
|
+
const outboxPath = join(stateDir, "outbox.jsonl");
|
|
34
|
+
const stdoutPath = join(stateDir, "stdout.log");
|
|
35
|
+
const stderrPath = join(stateDir, "stderr.log");
|
|
36
|
+
const meta = JSON.parse(readFileSync(runPath, "utf8"));
|
|
37
|
+
function event(name, data = {}) {
|
|
38
|
+
appendFileSync(eventsPath, `${JSON.stringify({ event: name, ts: new Date().toISOString(), ...data })}\n`);
|
|
39
|
+
}
|
|
40
|
+
function outbox(name, summary, data = {}, delivery = "log", level = "info") {
|
|
41
|
+
appendFileSync(outboxPath, `${JSON.stringify({ body: data, data, delivery, event: name, from: `run:${meta.run}`, level, summary, to: "coordinator", ts: new Date().toISOString(), type: name })}\n`);
|
|
42
|
+
}
|
|
43
|
+
function progress(phase, extra = {}) {
|
|
44
|
+
writeJsonAtomic(progressPath, {
|
|
45
|
+
phase,
|
|
46
|
+
updatedAt: new Date().toISOString(),
|
|
47
|
+
...extra,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
let activeSubagents = 0;
|
|
51
|
+
let completedSubagents = 0;
|
|
52
|
+
const subagentFailures = [];
|
|
53
|
+
function getCommandDoneDelivery(result) {
|
|
54
|
+
return result.code !== 0 || activeSubagents > 0 ? "followup" : "log";
|
|
55
|
+
}
|
|
56
|
+
function progressRunning() {
|
|
57
|
+
progress("running", {
|
|
58
|
+
activeSubagents,
|
|
59
|
+
completed: completedSubagents,
|
|
60
|
+
failures: subagentFailures,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async function observedExec(command, args, options) {
|
|
64
|
+
const commandDetail = formatCommandDetail(command, args);
|
|
65
|
+
const execArgs = appendRecipeContextToPiArgs(command, args, meta.recipe_context_records, options?.actorRecipeContext);
|
|
66
|
+
activeSubagents += 1;
|
|
67
|
+
event("command.start", { activeSubagents, command: commandDetail });
|
|
68
|
+
progressRunning();
|
|
69
|
+
const result = await execCommandTemplate(command, execArgs, options);
|
|
70
|
+
activeSubagents = Math.max(0, activeSubagents - 1);
|
|
71
|
+
completedSubagents += 1;
|
|
72
|
+
if (result.code !== 0) {
|
|
73
|
+
subagentFailures.push({
|
|
74
|
+
code: result.code,
|
|
75
|
+
command: commandDetail,
|
|
76
|
+
killed: result.killed,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
event("command.done", {
|
|
80
|
+
activeSubagents,
|
|
81
|
+
code: result.code,
|
|
82
|
+
command: commandDetail,
|
|
83
|
+
killed: result.killed,
|
|
84
|
+
});
|
|
85
|
+
outbox("command.done", `Command ${summarizeCommandDetail(commandDetail)} completed with code ${result.code}`, {
|
|
86
|
+
activeSubagents,
|
|
87
|
+
...(meta.artifacts ? { artifacts: meta.artifacts } : {}),
|
|
88
|
+
run_files: [stdoutPath, stderrPath, resultPath, eventsPath, outboxPath],
|
|
89
|
+
code: result.code,
|
|
90
|
+
command: commandDetail,
|
|
91
|
+
killed: result.killed,
|
|
92
|
+
}, getCommandDoneDelivery(result), result.code === 0 ? "info" : "error");
|
|
93
|
+
progressRunning();
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
event("run.runner.start", { pid: process.pid });
|
|
98
|
+
progressRunning();
|
|
99
|
+
const result = await executeRegisteredTool({
|
|
100
|
+
name: "run_actor",
|
|
101
|
+
description: "Detached command-template run actor",
|
|
102
|
+
template: meta.template,
|
|
103
|
+
args: [],
|
|
104
|
+
defaults: {},
|
|
105
|
+
}, meta.values || {}, observedExec, meta.cwd);
|
|
106
|
+
const text = result.content?.[0]?.text || "";
|
|
107
|
+
appendFileSync(stdoutPath, text);
|
|
108
|
+
writeJsonAtomic(resultPath, {
|
|
109
|
+
code: result.details.code,
|
|
110
|
+
command: result.details.command,
|
|
111
|
+
killed: result.details.killed,
|
|
112
|
+
truncated: result.details.truncated,
|
|
113
|
+
completedAt: new Date().toISOString(),
|
|
114
|
+
});
|
|
115
|
+
progress("done", {
|
|
116
|
+
completed: 1,
|
|
117
|
+
failures: result.details.nonCriticalFailures || [],
|
|
118
|
+
});
|
|
119
|
+
event("run.done", { code: result.details.code });
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
123
|
+
appendFileSync(stderrPath, `${message}\n`);
|
|
124
|
+
writeJsonAtomic(resultPath, {
|
|
125
|
+
code: 1,
|
|
126
|
+
error: message,
|
|
127
|
+
killed: false,
|
|
128
|
+
completedAt: new Date().toISOString(),
|
|
129
|
+
});
|
|
130
|
+
progress("failed", { completed: 0, failures: [{ message }] });
|
|
131
|
+
event("run.failed", { error: message });
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|