@llblab/pi-actors 0.32.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/AGENTS.md +23 -11
  2. package/BACKLOG.md +2 -30
  3. package/CHANGELOG.md +30 -1
  4. package/dist/index.js +10 -10
  5. package/dist/lib/async-runs.d.ts +23 -75
  6. package/dist/lib/async-runs.js +49 -801
  7. package/dist/lib/command-templates.d.ts +3 -3
  8. package/dist/lib/command-templates.js +3 -3
  9. package/dist/lib/config.d.ts +2 -2
  10. package/dist/lib/config.js +5 -5
  11. package/dist/lib/{output.d.ts → execution-output.d.ts} +3 -3
  12. package/dist/lib/{output.js → execution-output.js} +3 -3
  13. package/dist/lib/execution.js +1 -1
  14. package/dist/lib/{actor-inspector-tui.d.ts → inspector.d.ts} +3 -2
  15. package/dist/lib/{actor-inspector-tui.js → inspector.js} +3 -2
  16. package/dist/lib/mailbox-loop.d.ts +3 -3
  17. package/dist/lib/mailbox-loop.js +3 -3
  18. package/dist/lib/{actor-messages.d.ts → messages.d.ts} +3 -3
  19. package/dist/lib/{actor-messages.js → messages.js} +3 -3
  20. package/dist/lib/paths.d.ts +1 -1
  21. package/dist/lib/paths.js +3 -3
  22. package/dist/lib/{actor-recipe-context.d.ts → recipes-context.d.ts} +3 -2
  23. package/dist/lib/{actor-recipe-context.js → recipes-context.js} +2 -1
  24. package/dist/lib/{recipe-discovery.d.ts → recipes-discovery.d.ts} +9 -9
  25. package/dist/lib/{recipe-discovery.js → recipes-discovery.js} +17 -17
  26. package/dist/lib/registry.d.ts +1 -1
  27. package/dist/lib/registry.js +22 -22
  28. package/dist/lib/{actor-rooms.d.ts → rooms.d.ts} +4 -4
  29. package/dist/lib/{actor-rooms.js → rooms.js} +3 -3
  30. package/dist/lib/runs-artifacts.d.ts +21 -0
  31. package/dist/lib/runs-artifacts.js +68 -0
  32. package/dist/lib/runs-control.d.ts +13 -0
  33. package/dist/lib/runs-control.js +59 -0
  34. package/dist/lib/runs-identity.d.ts +5 -0
  35. package/dist/lib/runs-identity.js +11 -0
  36. package/dist/lib/runs-index.d.ts +18 -0
  37. package/dist/lib/runs-index.js +119 -0
  38. package/dist/lib/runs-mailbox.d.ts +25 -0
  39. package/dist/lib/runs-mailbox.js +146 -0
  40. package/dist/lib/runs-messages.d.ts +15 -0
  41. package/dist/lib/runs-messages.js +179 -0
  42. package/dist/lib/runs-outbox.d.ts +41 -0
  43. package/dist/lib/runs-outbox.js +87 -0
  44. package/dist/lib/runs-process.d.ts +7 -0
  45. package/dist/lib/runs-process.js +36 -0
  46. package/dist/lib/runs-retention.d.ts +8 -0
  47. package/dist/lib/runs-retention.js +56 -0
  48. package/dist/lib/runs-start.d.ts +9 -0
  49. package/dist/lib/runs-start.js +67 -0
  50. package/dist/lib/runs-status.d.ts +8 -0
  51. package/dist/lib/runs-status.js +60 -0
  52. package/dist/lib/runtime.d.ts +2 -2
  53. package/dist/lib/runtime.js +5 -5
  54. package/dist/lib/schema.d.ts +9 -0
  55. package/dist/lib/schema.js +40 -0
  56. package/dist/lib/tools-access.d.ts +19 -0
  57. package/dist/lib/tools-access.js +44 -0
  58. package/dist/lib/tools-inspect.d.ts +11 -0
  59. package/dist/lib/tools-inspect.js +534 -0
  60. package/dist/lib/tools-local.d.ts +14 -0
  61. package/dist/lib/tools-local.js +156 -0
  62. package/dist/lib/tools-mailbox.d.ts +8 -0
  63. package/dist/lib/tools-mailbox.js +48 -0
  64. package/dist/lib/tools-message.d.ts +9 -0
  65. package/dist/lib/tools-message.js +346 -0
  66. package/dist/lib/tools-register.d.ts +18 -0
  67. package/dist/lib/tools-register.js +40 -0
  68. package/dist/lib/tools-response.d.ts +21 -0
  69. package/dist/lib/tools-response.js +219 -0
  70. package/dist/lib/tools-spawn.d.ts +12 -0
  71. package/dist/lib/tools-spawn.js +159 -0
  72. package/dist/lib/tools.d.ts +13 -39
  73. package/dist/lib/tools.js +11 -1497
  74. package/dist/recipes/utility-coordinator-lock-snapshot.json +1 -1
  75. package/dist/scripts/actor-worker.mjs +186 -7
  76. package/dist/scripts/async-runner.mjs +171 -8
  77. package/dist/scripts/build-dist.mjs +1 -1
  78. package/dist/scripts/conformance.mjs +34 -18
  79. package/dist/scripts/coordinator.mjs +733 -11
  80. package/dist/scripts/locker.mjs +345 -16
  81. package/dist/scripts/recipe-utils.mjs +478 -14
  82. package/dist/scripts/validate-recipe.mjs +110 -10
  83. package/dist/skills/actors/SKILL.md +2 -2
  84. package/dist/skills/swarm/SKILL.md +2 -2
  85. package/docs/recipe-library.md +1 -1
  86. package/docs/template-recipes.md +1 -1
  87. package/docs/tool-registry.md +3 -3
  88. package/index.ts +12 -19
  89. package/lib/async-runs.ts +124 -1016
  90. package/lib/command-templates.ts +3 -3
  91. package/lib/config.ts +7 -7
  92. package/lib/{output.ts → execution-output.ts} +3 -3
  93. package/lib/execution.ts +5 -1
  94. package/lib/{actor-inspector-tui.ts → inspector.ts} +4 -3
  95. package/lib/mailbox-loop.ts +3 -3
  96. package/lib/{actor-messages.ts → messages.ts} +3 -3
  97. package/lib/paths.ts +3 -3
  98. package/lib/{actor-recipe-context.ts → recipes-context.ts} +3 -2
  99. package/lib/{recipe-discovery.ts → recipes-discovery.ts} +30 -30
  100. package/lib/{recipe-usage.ts → recipes-usage.ts} +2 -2
  101. package/lib/registry.ts +37 -24
  102. package/lib/{actor-rooms.ts → rooms.ts} +4 -4
  103. package/lib/runs-artifacts.ts +96 -0
  104. package/lib/runs-control.ts +85 -0
  105. package/lib/runs-identity.ts +14 -0
  106. package/lib/runs-index.ts +156 -0
  107. package/lib/runs-mailbox.ts +208 -0
  108. package/lib/runs-messages.ts +252 -0
  109. package/lib/runs-outbox.ts +144 -0
  110. package/lib/runs-process.ts +51 -0
  111. package/lib/runs-retention.ts +77 -0
  112. package/lib/runs-start.ts +98 -0
  113. package/lib/runs-status.ts +78 -0
  114. package/lib/runtime.ts +6 -6
  115. package/lib/schema.ts +55 -0
  116. package/lib/tools-access.ts +68 -0
  117. package/lib/tools-inspect.ts +735 -0
  118. package/lib/tools-local.ts +234 -0
  119. package/lib/tools-mailbox.ts +56 -0
  120. package/lib/tools-message.ts +483 -0
  121. package/lib/tools-register.ts +64 -0
  122. package/lib/tools-response.ts +267 -0
  123. package/lib/tools-spawn.ts +240 -0
  124. package/lib/tools.ts +26 -2051
  125. package/package.json +1 -1
  126. package/recipes/utility-coordinator-lock-snapshot.json +1 -1
  127. package/scripts/actor-worker.mjs +186 -7
  128. package/scripts/async-runner.mjs +171 -8
  129. package/scripts/build-dist.mjs +1 -1
  130. package/scripts/conformance.mjs +34 -18
  131. package/scripts/coordinator.mjs +733 -11
  132. package/scripts/locker.mjs +345 -16
  133. package/scripts/recipe-utils.mjs +478 -14
  134. package/scripts/validate-recipe.mjs +110 -10
  135. package/skills/actors/SKILL.md +2 -2
  136. package/skills/swarm/SKILL.md +2 -2
  137. package/dist/lib/actor-worker.d.ts +0 -16
  138. package/dist/lib/actor-worker.js +0 -134
  139. package/dist/lib/async-runner.d.ts +0 -5
  140. package/dist/lib/async-runner.js +0 -134
  141. package/dist/lib/conformance.d.ts +0 -12
  142. package/dist/lib/conformance.js +0 -28
  143. package/dist/lib/coordinator.d.ts +0 -5
  144. package/dist/lib/coordinator.js +0 -660
  145. package/dist/lib/locker.d.ts +0 -5
  146. package/dist/lib/locker.js +0 -310
  147. package/dist/lib/recipe-utils.d.ts +0 -5
  148. package/dist/lib/recipe-utils.js +0 -385
  149. package/dist/lib/validate-recipe.d.ts +0 -6
  150. package/dist/lib/validate-recipe.js +0 -104
  151. package/lib/actor-worker.ts +0 -174
  152. package/lib/async-runner.ts +0 -173
  153. package/lib/conformance.ts +0 -46
  154. package/lib/coordinator.ts +0 -742
  155. package/lib/locker.ts +0 -340
  156. package/lib/recipe-utils.ts +0 -486
  157. package/lib/validate-recipe.ts +0 -110
  158. /package/dist/lib/{recipe-references.d.ts → recipes-references.d.ts} +0 -0
  159. /package/dist/lib/{recipe-references.js → recipes-references.js} +0 -0
  160. /package/dist/lib/{recipe-usage.d.ts → recipes-usage.d.ts} +0 -0
  161. /package/dist/lib/{recipe-usage.js → recipes-usage.js} +0 -0
  162. /package/lib/{recipe-references.ts → recipes-references.ts} +0 -0
@@ -1,660 +0,0 @@
1
- /**
2
- * Multi-actor coordinator helper entrypoint logic.
3
- * Zones: swarm coordination, worker pool orchestration
4
- */
5
- // @ts-nocheck
6
- import { spawn } from "node:child_process";
7
- import { existsSync } from "node:fs";
8
- import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
9
- import { createConnection } from "node:net";
10
- import { dirname, join } from "node:path";
11
- import { fileURLToPath } from "node:url";
12
- let currentArgv = process.argv.slice(2);
13
- function arg(name, fallback = "") {
14
- const prefix = `--${name}=`;
15
- const value = currentArgv.find((item) => item.startsWith(prefix));
16
- return value ? value.slice(prefix.length) : fallback;
17
- }
18
- function numberArg(name, fallback) {
19
- const value = Number(arg(name, String(fallback)));
20
- return Number.isFinite(value) && value >= 0 ? value : fallback;
21
- }
22
- function boolArg(name, fallback = false) {
23
- const value = arg(name, String(fallback)).trim().toLowerCase();
24
- return ["1", "true", "yes", "on"].includes(value);
25
- }
26
- function normalizeRoles(value) {
27
- if (!Array.isArray(value))
28
- throw new Error("roles must be an array");
29
- return value.map((role, index) => ({
30
- name: String(role.name ?? `actor-${index + 1}`),
31
- persona: String(role.persona ?? role.role ?? "room participant"),
32
- }));
33
- }
34
- function parseRoles(raw) {
35
- if (!raw.trim())
36
- return defaultRoles;
37
- try {
38
- return normalizeRoles(JSON.parse(raw));
39
- }
40
- catch {
41
- return raw.split(",").map((item, index) => ({
42
- name: item.trim() || `actor-${index + 1}`,
43
- persona: "room participant",
44
- }));
45
- }
46
- }
47
- async function loadRoles(raw, path) {
48
- if (path.trim())
49
- return normalizeRoles(JSON.parse(await readFile(path, "utf8")));
50
- return parseRoles(raw);
51
- }
52
- function shellQuote(value) {
53
- return `'${String(value).replaceAll("'", "'\\''")}'`;
54
- }
55
- function agentDir() {
56
- return process.env.PI_CODING_AGENT_DIR || `${process.env.HOME}/.pi/agent`;
57
- }
58
- function runStateDir(runId) {
59
- return `${agentDir()}/tmp/pi-actors/runs/${runId}`;
60
- }
61
- async function sleep(ms) {
62
- await new Promise((resolve) => setTimeout(resolve, ms));
63
- }
64
- const STATE_LOCK_MAX_AGE_MS = 5 * 60 * 1000;
65
- const STATE_LOCK_TIMEOUT_MS = 5000;
66
- async function waitForPath(path, timeoutMs = 5000) {
67
- const started = Date.now();
68
- while (!existsSync(path)) {
69
- if (Date.now() - started > timeoutMs)
70
- throw new Error(`timed out waiting for ${path}`);
71
- await sleep(50);
72
- }
73
- }
74
- async function acquireStateLock(parentDir, name, label) {
75
- await mkdir(parentDir, { recursive: true });
76
- const lockDir = `${parentDir}/${name}`;
77
- const started = Date.now();
78
- while (true) {
79
- try {
80
- await mkdir(lockDir);
81
- await writeFile(`${lockDir}/owner.json`, `${JSON.stringify({ pid: process.pid, created_at: new Date().toISOString() })}\n`, "utf8");
82
- return async () => rm(lockDir, { recursive: true, force: true });
83
- }
84
- catch (error) {
85
- try {
86
- const current = await stat(lockDir);
87
- if (Date.now() - current.mtimeMs > STATE_LOCK_MAX_AGE_MS) {
88
- await rm(lockDir, { recursive: true, force: true });
89
- continue;
90
- }
91
- }
92
- catch {
93
- continue;
94
- }
95
- if (Date.now() - started > STATE_LOCK_TIMEOUT_MS) {
96
- throw new Error(`${label} lock timed out.`, { cause: error });
97
- }
98
- await sleep(10);
99
- }
100
- }
101
- }
102
- async function acquireBranchInboxLock(runId, branchName) {
103
- return acquireStateLock(`${runStateDir(runId)}/branches/${branchName}`, ".inbox.lock", `Branch inbox ${branchName}`);
104
- }
105
- async function readLockerControl(locker) {
106
- const controlJson = `${locker.stateDir}/control.json`;
107
- await waitForPath(controlJson);
108
- return JSON.parse(await readFile(controlJson, "utf8"));
109
- }
110
- function writeNamedPipeLine(path, line) {
111
- return new Promise((resolve, reject) => {
112
- const socket = createConnection(path);
113
- let settled = false;
114
- const finish = (error) => {
115
- if (settled)
116
- return;
117
- settled = true;
118
- if (error)
119
- reject(error);
120
- else
121
- resolve();
122
- };
123
- socket.on("error", finish);
124
- socket.on("connect", () => socket.end(line, () => finish()));
125
- });
126
- }
127
- async function writeLockerMessage(locker, message) {
128
- if (!locker)
129
- return;
130
- const control = locker.control ?? (await readLockerControl(locker));
131
- locker.control = control;
132
- const line = `${JSON.stringify(message)}\n`;
133
- if (control.type === "named-pipe")
134
- await writeNamedPipeLine(control.path, line);
135
- else
136
- await writeFile(control.path, line, { flag: "a" });
137
- await sleep(50);
138
- }
139
- async function waitForLockerJournal(locker, pattern, timeoutMs = 2000) {
140
- if (!locker)
141
- return;
142
- const journalPath = `${locker.stateDir}/journal.jsonl`;
143
- const deadline = Date.now() + timeoutMs;
144
- while (Date.now() < deadline) {
145
- try {
146
- if (pattern.test(await readFile(journalPath, "utf8")))
147
- return;
148
- }
149
- catch {
150
- // Journal may not exist yet.
151
- }
152
- await sleep(25);
153
- }
154
- }
155
- function scriptPath(name) {
156
- const here = dirname(fileURLToPath(import.meta.url));
157
- const root = here.endsWith(`${join("dist", "lib")}`)
158
- ? dirname(here)
159
- : dirname(here);
160
- return join(root, "scripts", name);
161
- }
162
- async function startLocker(config) {
163
- if (!config.locker)
164
- return undefined;
165
- const lockerStateDir = `${runStateDir(config.runId)}/locker`;
166
- await mkdir(lockerStateDir, { recursive: true });
167
- const child = spawn(scriptPath("locker.mjs"), [
168
- "serve",
169
- "--state-dir",
170
- lockerStateDir,
171
- "--lease-ms",
172
- String(config.lockerLeaseMs),
173
- ], {
174
- env: { ...process.env, run_id: config.runId },
175
- stdio: ["ignore", "pipe", "pipe"],
176
- });
177
- child.stdout.on("data", (chunk) => process.stdout.write(`[locker] ${chunk}`));
178
- child.stderr.on("data", (chunk) => process.stderr.write(`[locker] ${chunk}`));
179
- const locker = { child, stateDir: lockerStateDir };
180
- locker.control = await readLockerControl(locker);
181
- await writeLockerMessage(locker, {
182
- type: "lock.enqueue",
183
- body: {
184
- id: "coordinator-artifact",
185
- task: "Own final artifact synthesis",
186
- resources: [config.artifactPath || "artifact"],
187
- },
188
- });
189
- return locker;
190
- }
191
- async function stopLocker(locker) {
192
- if (!locker)
193
- return;
194
- try {
195
- await writeLockerMessage(locker, { type: "control.stop", body: {} });
196
- await sleep(100);
197
- }
198
- finally {
199
- if (!locker.child.killed)
200
- locker.child.kill("SIGTERM");
201
- }
202
- }
203
- function terminateProcessGroup(child, signal) {
204
- if (!child.pid)
205
- return;
206
- try {
207
- if (process.platform !== "win32")
208
- process.kill(-child.pid, signal);
209
- else
210
- child.kill(signal);
211
- }
212
- catch {
213
- try {
214
- child.kill(signal);
215
- }
216
- catch {
217
- // Process already exited.
218
- }
219
- }
220
- }
221
- function runPi(prompt, model, thinking, ttlMs = 0) {
222
- return new Promise((resolve) => {
223
- const args = [
224
- "--tools",
225
- "inspect,message",
226
- "--no-context-files",
227
- "--no-skills",
228
- "--no-session",
229
- ];
230
- if (model) {
231
- args.push("--model", model);
232
- }
233
- if (thinking) {
234
- args.push("--thinking", thinking);
235
- }
236
- args.push("-p", prompt);
237
- const child = spawn("pi", args, {
238
- detached: process.platform !== "win32",
239
- stdio: ["ignore", "pipe", "pipe"],
240
- });
241
- let stdout = "";
242
- let stderr = "";
243
- let settled = false;
244
- let timedOut = false;
245
- let ttlTimer;
246
- let forceTimer;
247
- const clearTimers = () => {
248
- if (ttlTimer)
249
- clearTimeout(ttlTimer);
250
- if (forceTimer)
251
- clearTimeout(forceTimer);
252
- };
253
- if (ttlMs > 0) {
254
- ttlTimer = setTimeout(() => {
255
- timedOut = true;
256
- stderr += `\nSubagent TTL expired after ${ttlMs}ms; terminating process group.\n`;
257
- terminateProcessGroup(child, "SIGTERM");
258
- forceTimer = setTimeout(() => terminateProcessGroup(child, "SIGKILL"), 1000);
259
- }, ttlMs);
260
- }
261
- child.stdout.on("data", (chunk) => {
262
- stdout += chunk;
263
- });
264
- child.stderr.on("data", (chunk) => {
265
- stderr += chunk;
266
- });
267
- child.on("close", (code, signal) => {
268
- if (settled)
269
- return;
270
- settled = true;
271
- clearTimers();
272
- resolve({
273
- code: timedOut ? 124 : (code ?? (signal ? 1 : 0)),
274
- killed: timedOut,
275
- signal,
276
- stderr,
277
- stdout,
278
- timed_out: timedOut,
279
- });
280
- });
281
- child.on("error", (error) => {
282
- if (settled)
283
- return;
284
- settled = true;
285
- clearTimers();
286
- resolve({ code: 1, stdout, stderr: String(error) });
287
- });
288
- });
289
- }
290
- async function readRoomTranscript(config) {
291
- const messagesPath = `${runStateDir(config.runId)}/rooms/main/messages.jsonl`;
292
- try {
293
- const raw = await readFile(messagesPath, "utf8");
294
- return raw
295
- .split("\n")
296
- .filter(Boolean)
297
- .slice(-160)
298
- .map((line) => {
299
- try {
300
- const message = JSON.parse(line);
301
- const from = String(message.from || "unknown").replace(/^branch:[^/]+\//, "");
302
- const type = String(message.type || "message");
303
- const body = typeof message.body === "string"
304
- ? message.body
305
- : JSON.stringify(message.body ?? "");
306
- return `${from} [${type}]: ${body}`;
307
- }
308
- catch {
309
- return line;
310
- }
311
- })
312
- .join("\n");
313
- }
314
- catch {
315
- return "";
316
- }
317
- }
318
- async function synthesize(config, locker) {
319
- if (!config.artifactPath)
320
- return { code: 0, empty: false };
321
- if (locker) {
322
- await writeLockerMessage(locker, {
323
- type: "lock.claim",
324
- body: { owner: "coordinator:synthesizer" },
325
- });
326
- }
327
- const transcript = await readRoomTranscript(config);
328
- const prompt = `Synthesize this transcript into a concise Markdown artifact. Mission: ${config.mission}. Include: Title, Consensus, Roles, Protocol, Final Artifact Shape, Next Actions, Open Questions. Use only the transcript evidence below.\n\nTRANSCRIPT:\n${transcript.slice(-24000)}`;
329
- const result = await runPi(prompt, config.model, config.thinking, config.subagentTtlMs);
330
- const output = result.stdout.trim();
331
- const diagnostics = config.stats
332
- ? `\n\n## Diagnostics\n\nparticipant_attempts=${config.stats.participantAttempts}\nparticipant_success=${config.stats.participantSuccess}\nparticipant_failures=${config.stats.participantFailures}\nsynthesis_code=${result.code}\ntranscript_messages=${transcript.trim() ? transcript.split("\n").length : 0}\n`
333
- : "";
334
- await writeFile(config.artifactPath, output
335
- ? result.stdout
336
- : `# Synthesis Artifact\n\nNo synthesis output.${diagnostics}\n`, "utf8");
337
- if (locker) {
338
- await writeLockerMessage(locker, {
339
- type: "lock.complete",
340
- body: { id: "coordinator-artifact", artifact: config.artifactPath },
341
- });
342
- await waitForLockerJournal(locker, /lock\.complete/);
343
- await writeLockerMessage(locker, {
344
- type: "lock.release",
345
- body: { resource: config.artifactPath },
346
- });
347
- }
348
- process.stdout.write(`artifact=${config.artifactPath}\n`);
349
- return { code: result.code, empty: !output };
350
- }
351
- async function readInboxLines(inboxPath) {
352
- if (!existsSync(inboxPath))
353
- return [];
354
- const content = await readFile(inboxPath, "utf8");
355
- return content
356
- .split("\n")
357
- .filter(Boolean)
358
- .map((line) => JSON.parse(line));
359
- }
360
- async function writeInboxMessages(inboxPath, messages) {
361
- await writeFile(inboxPath, messages.map((message) => JSON.stringify(message)).join("\n") + "\n", "utf8");
362
- }
363
- async function mutateBranchInbox(runId, branchName, mutate, fallback) {
364
- const inboxPath = `${runStateDir(runId)}/branches/${branchName}/inbox.jsonl`;
365
- const releaseLock = await acquireBranchInboxLock(runId, branchName);
366
- try {
367
- const messages = await readInboxLines(inboxPath);
368
- const result = mutate(messages);
369
- if (result.messages)
370
- await writeInboxMessages(inboxPath, result.messages);
371
- return result.value;
372
- }
373
- catch {
374
- return fallback;
375
- }
376
- finally {
377
- await releaseLock();
378
- }
379
- }
380
- async function claimQueuedInboxMessages(runId, branchName) {
381
- return mutateBranchInbox(runId, branchName, (messages) => {
382
- const claimedAt = new Date().toISOString();
383
- const queuedMessages = [];
384
- const updated = messages.map((msg, index) => {
385
- if (msg.status !== "queued" && msg.status)
386
- return msg;
387
- const claimed = {
388
- ...msg,
389
- claimed_at: claimedAt,
390
- id: msg.id || `legacy-${Date.now()}-${index}`,
391
- status: "claimed",
392
- };
393
- queuedMessages.push(claimed);
394
- return claimed;
395
- });
396
- return {
397
- messages: queuedMessages.length > 0 ? updated : undefined,
398
- value: queuedMessages,
399
- };
400
- }, []);
401
- }
402
- async function updateInboxMessagesStatus(runId, branchName, ids, status) {
403
- await mutateBranchInbox(runId, branchName, (messages) => {
404
- const idSet = new Set(ids);
405
- const updated = messages.map((msg) => {
406
- if (!msg.id || !idSet.has(msg.id))
407
- return msg;
408
- return { ...msg, [`${status}_at`]: new Date().toISOString(), status };
409
- });
410
- return { messages: updated, value: undefined };
411
- });
412
- }
413
- async function executeParticipantPrompt(role, basePrompt, config) {
414
- const branchName = role.name;
415
- const queuedMessages = await claimQueuedInboxMessages(config.runId, branchName);
416
- let finalPrompt = basePrompt;
417
- const claimedIds = [];
418
- if (queuedMessages.length > 0) {
419
- let inboxSection = "\n\nDIRECT INBOX MESSAGES FOR YOU (queued mailbox work):\n";
420
- for (const msg of queuedMessages) {
421
- const bodyText = typeof msg.body === "string"
422
- ? msg.body
423
- : JSON.stringify(msg.body ?? "");
424
- inboxSection += `- From: ${msg.from || "unknown"} (Type: ${msg.type || "message"})\n Body: ${bodyText}\n`;
425
- if (msg.id)
426
- claimedIds.push(msg.id);
427
- }
428
- inboxSection +=
429
- "\nPlease acknowledge and address these direct messages in your response.\n";
430
- finalPrompt += inboxSection;
431
- }
432
- const result = await runPi(finalPrompt, config.model, config.thinking, config.subagentTtlMs);
433
- if (claimedIds.length > 0) {
434
- const finalStatus = result.code === 0 ? "handled" : "failed";
435
- await updateInboxMessagesStatus(config.runId, branchName, claimedIds, finalStatus);
436
- }
437
- return result;
438
- }
439
- async function participantRound(role, round, config) {
440
- const displayName = role.name;
441
- const address = `branch:${config.runId}/${role.name}`;
442
- const prompt = `You are ${displayName} (${address}), ${role.persona}. Mission: ${config.mission}. Round ${round}/${config.rounds}. First call inspect target=${config.room} view=previews lines=30 and inspect target=${config.room} view=contacts. Then call message once to ${config.room} from ${address} type=chat.message. Body: 2-4 sentences that react to a named participant, propose the next coordination step, and refine the shared artifact. Use contacts for peer names and addresses. End stdout with summary <=160 chars.`;
443
- const result = await executeParticipantPrompt(role, prompt, config);
444
- if (config.stats) {
445
- config.stats.participantAttempts += 1;
446
- if (result.code === 0)
447
- config.stats.participantSuccess += 1;
448
- else
449
- config.stats.participantFailures += 1;
450
- }
451
- process.stdout.write(`[${role.name} round ${round}] code=${result.code}\n${result.stdout}\n`);
452
- if (result.stderr.trim())
453
- process.stderr.write(`[${role.name} round ${round}] ${result.stderr}\n`);
454
- return result;
455
- }
456
- async function participantJoin(role, config) {
457
- const displayName = role.name;
458
- const address = `branch:${config.runId}/${role.name}`;
459
- const joinPrompt = `You are ${displayName}, ${role.persona}. Mission: ${config.mission}. Call tool message exactly once with to=${shellQuote(config.room)}, from=${shellQuote(address)}, type='actor.join', summary='${displayName} joined', body JSON {"role":${JSON.stringify(role.persona)},"display":${JSON.stringify(displayName)},"caps":["coordination","synthesis"],"claim":"coordinate on mission"}. Then print one short line.`;
460
- await runPi(joinPrompt, config.model, config.thinking, config.subagentTtlMs);
461
- }
462
- async function participantLeave(role, config) {
463
- const displayName = role.name;
464
- const address = `branch:${config.runId}/${role.name}`;
465
- const leavePrompt = `Call tool message exactly once with to=${shellQuote(config.room)}, from=${shellQuote(address)}, type='actor.leave', summary='${displayName} left', body='finished coordinated work'. Then print goodbye.`;
466
- await runPi(leavePrompt, config.model, config.thinking, config.subagentTtlMs);
467
- }
468
- // 1. consensus / swarm mode: iterative chat in a room
469
- async function runConsensus(config, locker) {
470
- await Promise.all(config.roles.map((role) => participantJoin(role, config)));
471
- for (let round = 1; round <= config.rounds; round += 1) {
472
- await Promise.all(config.roles.map((role) => participantRound(role, round, config)));
473
- if (round < config.rounds && config.delay > 0)
474
- await sleep(config.delay * 1000);
475
- }
476
- await Promise.all(config.roles.map((role) => participantLeave(role, config)));
477
- }
478
- // 2. pipeline mode: sequential step-by-step
479
- async function runPipeline(config, locker) {
480
- for (const role of config.roles) {
481
- await participantJoin(role, config);
482
- for (let round = 1; round <= config.rounds; round += 1) {
483
- await participantRound(role, round, config);
484
- if (round < config.rounds && config.delay > 0)
485
- await sleep(config.delay * 1000);
486
- }
487
- await participantLeave(role, config);
488
- }
489
- }
490
- // 3. fanout mode: run completely parallel independent processes
491
- async function runFanout(config, locker) {
492
- await Promise.all(config.roles.map(async (role) => {
493
- await participantJoin(role, config);
494
- for (let round = 1; round <= config.rounds; round += 1) {
495
- await participantRound(role, round, config);
496
- if (round < config.rounds && config.delay > 0)
497
- await sleep(config.delay * 1000);
498
- }
499
- await participantLeave(role, config);
500
- }));
501
- }
502
- // 4. pool mode: dynamic task pulling from the locker queue
503
- async function runPool(config, locker) {
504
- if (!locker) {
505
- throw new Error("Pool mode requires locker enabled (--locker=true)");
506
- }
507
- // Enqueue a set of sub-tasks based on the mission splits (for demo/simulation, we enqueue 3 sub-tasks)
508
- for (let index = 1; index <= 3; index += 1) {
509
- await writeLockerMessage(locker, {
510
- type: "lock.enqueue",
511
- body: {
512
- id: `subtask-${index}`,
513
- task: `Execute subtask ${index} under mission: ${config.mission}`,
514
- resources: [`resource-${index}`],
515
- },
516
- });
517
- }
518
- // Workers concurrently poll task queue
519
- await Promise.all(config.roles.map(async (role) => {
520
- const address = `branch:${config.runId}/${role.name}`;
521
- await participantJoin(role, config);
522
- while (true) {
523
- // Dequeue/Claim a task from the locker
524
- await writeLockerMessage(locker, {
525
- type: "lock.claim",
526
- body: { owner: role.name },
527
- });
528
- await sleep(100);
529
- // Check locks and queue state from locker snapshot
530
- const lockerData = await readJsonFile(`${locker.stateDir}/locks.json`);
531
- const assignedTask = Object.values(lockerData).find((lock) => lock.owner === role.name);
532
- if (!assignedTask) {
533
- // No task assigned or queue is empty
534
- break;
535
- }
536
- const taskId = assignedTask.task;
537
- const prompt = `You are ${role.name}, ${role.persona}. You have been assigned task ${taskId}: "${assignedTask.task}". Solve it as part of the overall mission: ${config.mission}. End your response with a clear summary.`;
538
- const result = await executeParticipantPrompt(role, prompt, config);
539
- process.stdout.write(`[${role.name} completed ${taskId}] code=${result.code}\n${result.stdout}\n`);
540
- // Report task completion back to locker and release resource locks
541
- await writeLockerMessage(locker, {
542
- type: "lock.complete",
543
- body: { id: taskId, result: result.stdout },
544
- });
545
- // Release any locks associated with this resource
546
- for (const [res, lock] of Object.entries(lockerData)) {
547
- if (lock.owner === role.name) {
548
- await writeLockerMessage(locker, {
549
- type: "lock.release",
550
- body: { resource: res },
551
- });
552
- }
553
- }
554
- }
555
- await participantLeave(role, config);
556
- }));
557
- }
558
- const coordinatorModes = {
559
- consensus: runConsensus,
560
- fanout: runFanout,
561
- pipeline: runPipeline,
562
- pool: runPool,
563
- };
564
- async function readJsonFile(path) {
565
- try {
566
- return JSON.parse(await readFile(path, "utf8"));
567
- }
568
- catch {
569
- return {};
570
- }
571
- }
572
- const defaultRoles = [
573
- {
574
- name: "mapper",
575
- persona: "systems mapper; tracks shared structure and dependencies",
576
- },
577
- {
578
- name: "memory",
579
- persona: "memory keeper; preserves decisions and unresolved questions",
580
- },
581
- {
582
- name: "risk",
583
- persona: "risk scout; challenges weak coordination and missing owners",
584
- },
585
- {
586
- name: "flow",
587
- persona: "flow designer; turns scattered ideas into process rhythm",
588
- },
589
- {
590
- name: "operator",
591
- persona: "operator; converts ideas into concrete next actions",
592
- },
593
- {
594
- name: "narrative",
595
- persona: "narrative synthesizer; keeps the artifact coherent",
596
- },
597
- {
598
- name: "interface",
599
- persona: "interface designer; makes outputs visible and usable",
600
- },
601
- {
602
- name: "facilitator",
603
- persona: "facilitator; asks for consensus and convergence",
604
- },
605
- ];
606
- export async function runCoordinator(argv = process.argv.slice(2)) {
607
- currentArgv = argv;
608
- const config = {
609
- runId: arg("run-id", process.env.run_id || process.env.RUN_ID || "coordinator-run"),
610
- mode: arg("mode", "consensus"), // "consensus" (default/swarm), "pipeline", "fanout", "pool"
611
- mission: arg("mission", "Coordinate a shared artifact and converge on next actions"),
612
- model: arg("model", ""),
613
- thinking: arg("thinking", "off"),
614
- roles: await loadRoles(arg("roles", ""), arg("roles-path", "")),
615
- rounds: numberArg("rounds", 4),
616
- delay: numberArg("delay", 10),
617
- artifactPath: arg("artifact-path", ""),
618
- locker: boolArg("locker", false),
619
- lockerLeaseMs: numberArg("locker-lease-ms", 600000),
620
- subagentTtlMs: numberArg("subagent-ttl-ms", 0),
621
- stats: {
622
- participantAttempts: 0,
623
- participantSuccess: 0,
624
- participantFailures: 0,
625
- },
626
- };
627
- if (!config.model) {
628
- console.error("--model is required");
629
- process.exit(2);
630
- }
631
- config.room = `room:${config.runId}`;
632
- const failures = [];
633
- const locker = await startLocker(config);
634
- try {
635
- const runMode = coordinatorModes[config.mode];
636
- if (!runMode)
637
- throw new Error(`Unknown coordinator mode: ${config.mode}`);
638
- await runMode(config, locker);
639
- const synthesis = await synthesize(config, locker);
640
- if (config.stats.participantAttempts > 0 && config.stats.participantSuccess === 0) {
641
- failures.push(`All participant rounds failed: attempts=${config.stats.participantAttempts}`);
642
- }
643
- if (synthesis?.empty && config.stats.participantFailures > 0) {
644
- failures.push(`Synthesis output was empty after participant failures: failures=${config.stats.participantFailures}`);
645
- }
646
- if (synthesis && synthesis.code !== 0) {
647
- failures.push(`Synthesis command failed: code=${synthesis.code}`);
648
- }
649
- }
650
- catch (globalError) {
651
- failures.push(`Global coordinator error: ${globalError.message}`);
652
- }
653
- finally {
654
- await stopLocker(locker);
655
- }
656
- if (failures.length > 0) {
657
- console.error(failures.join("\n"));
658
- process.exit(1);
659
- }
660
- }
@@ -1,5 +0,0 @@
1
- /**
2
- * Local coordination locker service entrypoint logic.
3
- * Zones: coordination locks, task leases, platform control endpoint
4
- */
5
- export declare function runLocker(argv?: string[]): Promise<void>;