@pify/swarm 0.9.1 → 0.9.2
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/extensions/swarm.ts +61 -31
- package/package.json +1 -1
- package/src/mailbox.ts +24 -0
package/extensions/swarm.ts
CHANGED
|
@@ -38,7 +38,17 @@ import {
|
|
|
38
38
|
import { LiveChildren, cancelNote, type CancelReason } from "../src/cancel.ts";
|
|
39
39
|
import { DELIVERY_TYPE, deliveryMessage, pendingResult } from "../src/pending.ts";
|
|
40
40
|
import { createIsolationWorktree, isolationNote, removeIfUnchanged } from "../src/isolate.ts";
|
|
41
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
MAILBOX_INBOX_TOOL,
|
|
43
|
+
MAILBOX_POST_TOOL,
|
|
44
|
+
MAILBOX_TOOL_NAMES,
|
|
45
|
+
formatInbox,
|
|
46
|
+
mailboxDir,
|
|
47
|
+
mailboxKey,
|
|
48
|
+
mailboxPrompt,
|
|
49
|
+
postMessage,
|
|
50
|
+
readInbox,
|
|
51
|
+
} from "../src/mailbox.ts";
|
|
42
52
|
import { parseAgentFile } from "../src/frontmatter.ts";
|
|
43
53
|
import { buildReport, buildStatusLine } from "../src/report.ts";
|
|
44
54
|
import { routeItem } from "../src/routing.ts";
|
|
@@ -53,7 +63,7 @@ import {
|
|
|
53
63
|
type ItemState,
|
|
54
64
|
type SwarmRun,
|
|
55
65
|
} from "../src/types.ts";
|
|
56
|
-
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
66
|
+
import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
57
67
|
import { basename, join } from "node:path";
|
|
58
68
|
|
|
59
69
|
const RUN_ENTRY = "swarm-run";
|
|
@@ -136,7 +146,7 @@ export default function swarm(pi: ExtensionAPI) {
|
|
|
136
146
|
let seen = 0;
|
|
137
147
|
return [
|
|
138
148
|
{
|
|
139
|
-
name:
|
|
149
|
+
name: MAILBOX_POST_TOOL,
|
|
140
150
|
label: "Post to swarm",
|
|
141
151
|
description:
|
|
142
152
|
"Tell the other agents in this swarm something that changes their work: a shared file you " +
|
|
@@ -154,7 +164,7 @@ export default function swarm(pi: ExtensionAPI) {
|
|
|
154
164
|
},
|
|
155
165
|
},
|
|
156
166
|
{
|
|
157
|
-
name:
|
|
167
|
+
name: MAILBOX_INBOX_TOOL,
|
|
158
168
|
label: "Read swarm inbox",
|
|
159
169
|
description:
|
|
160
170
|
"Read what the other agents in this swarm have posted since you last checked. Call it " +
|
|
@@ -227,7 +237,11 @@ export default function swarm(pi: ExtensionAPI) {
|
|
|
227
237
|
sessionManager: SessionManager.inMemory(workDir ?? ctx.cwd),
|
|
228
238
|
model,
|
|
229
239
|
thinkingLevel: (def.thinking ?? pi.getThinkingLevel()) as never,
|
|
230
|
-
tools
|
|
240
|
+
// `tools` is an allowlist and it filters customTools too, so a mailbox
|
|
241
|
+
// tool that is not named here is registered and then dropped — the
|
|
242
|
+
// child is told it has no such tool, and mailbox:true does nothing.
|
|
243
|
+
// Admit them exactly as @pify/subagent admits ask_supervisor.
|
|
244
|
+
tools: mailbox ? [...def.tools, ...MAILBOX_TOOL_NAMES] : def.tools,
|
|
231
245
|
...(mailbox ? { customTools: mailboxTools(mailbox, item.agent + "-" + item.index) } : {}),
|
|
232
246
|
resourceLoader: loader,
|
|
233
247
|
});
|
|
@@ -343,8 +357,12 @@ export default function swarm(pi: ExtensionAPI) {
|
|
|
343
357
|
isolate?: boolean,
|
|
344
358
|
useMailbox?: boolean,
|
|
345
359
|
): Promise<void> {
|
|
346
|
-
// One shared log per run; only created when the caller asked for it.
|
|
347
|
-
|
|
360
|
+
// One shared log per run; only created when the caller asked for it. Keyed
|
|
361
|
+
// on a per-run token (not the reused "s1" run id), so one run never reads a
|
|
362
|
+
// previous run's stale messages, and removed at the end so it never leaks.
|
|
363
|
+
const mailbox = useMailbox
|
|
364
|
+
? mailboxDir(getAgentDir(), mailboxKey(run.runId, run.startedAt))
|
|
365
|
+
: undefined;
|
|
348
366
|
|
|
349
367
|
// A dependent structurally receives each upstream's output — the thing a
|
|
350
368
|
// hand-sequenced coordinator forgets. Prepended to the shared preamble.
|
|
@@ -359,33 +377,45 @@ export default function swarm(pi: ExtensionAPI) {
|
|
|
359
377
|
|
|
360
378
|
// Run each item once its needs finish, up to the concurrency cap; a flat
|
|
361
379
|
// run (no needs) has everything ready at once, exactly like the old pool.
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
380
|
+
try {
|
|
381
|
+
await runGraph(
|
|
382
|
+
run.items,
|
|
383
|
+
DEFAULT_CONCURRENCY,
|
|
384
|
+
async (item, upstream) => {
|
|
385
|
+
const def = routeItem(item.item, defs, fixed);
|
|
386
|
+
item.agent = def.name;
|
|
387
|
+
const itemContext = contextFor(upstream);
|
|
388
|
+
if (isolate) {
|
|
389
|
+
try {
|
|
390
|
+
const iso = createIsolationWorktree(ctx.cwd, run.runId + "-i" + (item.index + 1));
|
|
391
|
+
await runItem(ctx, run.runId, def, item, itemContext, iso.path, mailbox);
|
|
392
|
+
// Remove the worktree when the item changed nothing (the leak
|
|
393
|
+
// removeIfUnchanged fixes); keep it when there is work to merge.
|
|
394
|
+
const removed = removeIfUnchanged(ctx.cwd, iso);
|
|
395
|
+
if (item.result !== null) {
|
|
396
|
+
item.result = `${item.result}\n\n${removed ? CLEAN_WORKTREE_NOTE : isolationNote(iso)}`;
|
|
397
|
+
}
|
|
398
|
+
} catch (err) {
|
|
399
|
+
item.status = "error";
|
|
400
|
+
item.error = err instanceof Error ? err.message : String(err);
|
|
378
401
|
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
item.error = err instanceof Error ? err.message : String(err);
|
|
402
|
+
} else {
|
|
403
|
+
await runItem(ctx, run.runId, def, item, itemContext, undefined, mailbox);
|
|
382
404
|
}
|
|
383
|
-
}
|
|
384
|
-
|
|
405
|
+
},
|
|
406
|
+
() => run.status === "cancelled",
|
|
407
|
+
);
|
|
408
|
+
} finally {
|
|
409
|
+
// The mailbox is per-run scratch: reclaim it however the run ends
|
|
410
|
+
// (finished, cancelled, or thrown) so a directory never accumulates.
|
|
411
|
+
if (mailbox) {
|
|
412
|
+
try {
|
|
413
|
+
rmSync(mailbox, { recursive: true, force: true });
|
|
414
|
+
} catch {
|
|
415
|
+
// best-effort: an unremovable scratch dir is not worth failing a run
|
|
385
416
|
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
389
419
|
|
|
390
420
|
if (run.status !== "cancelled") run.status = "done";
|
|
391
421
|
run.finishedAt = Date.now();
|
package/package.json
CHANGED
package/src/mailbox.ts
CHANGED
|
@@ -23,6 +23,30 @@ export interface MailMessage {
|
|
|
23
23
|
export const MAX_MESSAGE_CHARS = 1200;
|
|
24
24
|
export const MAX_INBOX_MESSAGES = 30;
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* The two mailbox tool names. pi's child-session `tools` allowlist filters
|
|
28
|
+
* customTools too, so a mailbox tool that is not also named in the child's
|
|
29
|
+
* allowlist is registered and then silently dropped — the child is told it has
|
|
30
|
+
* no such tool. Naming them here keeps the allowlist and the tool definitions
|
|
31
|
+
* from drifting apart (mirrors how @pify/subagent wires ask_supervisor).
|
|
32
|
+
*/
|
|
33
|
+
export const MAILBOX_POST_TOOL = "swarm_post";
|
|
34
|
+
export const MAILBOX_INBOX_TOOL = "swarm_inbox";
|
|
35
|
+
export const MAILBOX_TOOL_NAMES = [MAILBOX_POST_TOOL, MAILBOX_INBOX_TOOL] as const;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A per-run key for the mailbox directory. The run id ("s1", "s2", …) restarts
|
|
39
|
+
* at "s1" every session, so keying the dir on it alone made two different runs
|
|
40
|
+
* — a fresh "s1" and a previous session's "s1" — share one directory: the new
|
|
41
|
+
* run read the old run's stale messages, and the old dir was never reclaimed.
|
|
42
|
+
* Folding in the run's start time (base36-compact, and derived from the clock
|
|
43
|
+
* rather than Math.random so it stays deterministic and testable) gives every
|
|
44
|
+
* run its own directory, even two "s1"s from different sessions.
|
|
45
|
+
*/
|
|
46
|
+
export function mailboxKey(runId: string, startedAt: number): string {
|
|
47
|
+
return `${runId}-${Math.trunc(startedAt).toString(36)}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
26
50
|
/**
|
|
27
51
|
* One directory per run under the agent dir. Run ids are generated locally,
|
|
28
52
|
* but this builds a filesystem path, so it stays a single flat segment: no
|