@fradser/pi-agent-teams 0.5.1 → 0.5.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/README.md +1 -0
- package/index.ts +1 -0
- package/package.json +4 -3
- package/src/run-machine.ts +7 -14
- package/src/spawner.ts +75 -10
package/README.md
CHANGED
|
@@ -86,6 +86,7 @@ State is session-scoped and dies with the session (`~/.pi/agent/teammate/<sessio
|
|
|
86
86
|
|
|
87
87
|
```
|
|
88
88
|
agent-teams/
|
|
89
|
+
├── index.ts — package-root extension entry point
|
|
89
90
|
├── package.json — Pi package manifest
|
|
90
91
|
├── agents/ — bundled agent definitions (worker/reviewer/specialist/observer)
|
|
91
92
|
├── src/
|
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./src/index.ts";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fradser/pi-agent-teams",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Multi-agent team system for Pi — DAG orchestration, leader inbox, and one-way worker reports",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
|
+
"index.ts",
|
|
25
26
|
"src",
|
|
26
27
|
"agents",
|
|
27
28
|
".memory",
|
|
@@ -34,10 +35,10 @@
|
|
|
34
35
|
},
|
|
35
36
|
"pi": {
|
|
36
37
|
"extensions": [
|
|
37
|
-
"./
|
|
38
|
+
"./index.ts"
|
|
38
39
|
]
|
|
39
40
|
},
|
|
40
41
|
"dependencies": {
|
|
41
|
-
"@fradser/pi-kit": "0.1.
|
|
42
|
+
"@fradser/pi-kit": "0.1.1"
|
|
42
43
|
}
|
|
43
44
|
}
|
package/src/run-machine.ts
CHANGED
|
@@ -2,25 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { truncateTail, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@earendil-works/pi-coding-agent";
|
|
6
5
|
import { resolveAgent } from "./agents";
|
|
7
6
|
import { isWorkerEvent } from "./worker";
|
|
8
7
|
import {
|
|
9
8
|
cancelBlockedDependents, cancelNode, cancelRun, clearWorkerRunEvents, clearStateDirty, isStateDirty,
|
|
10
|
-
deliverToLeader, failRunTimeout, findSharedWorkspaceWriteConflict, getNode,
|
|
9
|
+
deliverToLeader, failRunTimeout, findSharedWorkspaceWriteConflict, getNode,
|
|
11
10
|
getRun, getState, listNodes, listRuns, markNodeRunning, markStateDirty,
|
|
12
|
-
readyPendingNodes, receiveWorkerMessage,
|
|
11
|
+
readyPendingNodes, receiveWorkerMessage, runningNodeCount, settleRun, setNodeSpawnInfo,
|
|
13
12
|
SUMMARY_NODE_ID, updateNodeSpawnProgress, updateNodeStatus,
|
|
14
13
|
} from "./state";
|
|
15
14
|
import {
|
|
16
15
|
CancellationIntents, buildAutonomousPrompt, finishReportedWorker, isCompletedWorkerExit,
|
|
17
|
-
POST_REPORT_GRACE_MS, spawnPiWorker,
|
|
16
|
+
POST_REPORT_GRACE_MS, spawnPiWorker, terminateWorker,
|
|
18
17
|
type WorkerProcessResult,
|
|
19
18
|
} from "./spawner";
|
|
20
19
|
import { buildNodeTerminalResult } from "./terminal";
|
|
21
20
|
import { captureWorktreeDiff, cleanupWorktree, createWorktree, discardWorktree } from "./worktree";
|
|
22
21
|
import { readWorkerEvents, removeWorkerOutbox, stateFilePath, workerOutboxPath, writeStateFile } from "./statefile";
|
|
23
|
-
import type { Run } from "./types";
|
|
24
22
|
|
|
25
23
|
export interface DispatchCtx {
|
|
26
24
|
ui: ExtensionContext["ui"];
|
|
@@ -184,7 +182,7 @@ function enforceRunTimeouts(): void {
|
|
|
184
182
|
const node = getNode(run.id, nodeId);
|
|
185
183
|
if (node) void terminateWorker(node.workerKey).catch(() => false);
|
|
186
184
|
}
|
|
187
|
-
onRunSettled(run.id
|
|
185
|
+
onRunSettled(run.id);
|
|
188
186
|
// failRunTimeout clears spawns, so those nodes' close events skip their
|
|
189
187
|
// finalize path — release write-deferred nodes in OTHER runs here.
|
|
190
188
|
if (machineCtx) scheduleAllRuns(machineCtx);
|
|
@@ -194,10 +192,6 @@ function enforceRunTimeouts(): void {
|
|
|
194
192
|
|
|
195
193
|
// ── Run dispatch machinery ────────────────────────────────────────
|
|
196
194
|
|
|
197
|
-
function sleep(ms: number): Promise<void> {
|
|
198
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
199
|
-
}
|
|
200
|
-
|
|
201
195
|
/** Compact run summary for tool returns and follow-ups. When the run has a
|
|
202
196
|
* synthesized __summary node result, that is shown instead of per-node
|
|
203
197
|
* headlines (no truncation heuristics). */
|
|
@@ -231,14 +225,13 @@ export function buildRunSummary(runId: string): string {
|
|
|
231
225
|
for (const node of nodes) {
|
|
232
226
|
lines.push(`- [${node.id}] ${node.status} (${node.agent})`);
|
|
233
227
|
}
|
|
234
|
-
lines.push("", "Console: /teammate");
|
|
235
228
|
return lines.join("\n");
|
|
236
229
|
}
|
|
237
230
|
|
|
238
231
|
/** Called once a run reaches a terminal status. Idempotent: the leader summary
|
|
239
232
|
* and follow-up fire only on the first settled observation (a run can transition
|
|
240
233
|
* through settleRun once per node close). */
|
|
241
|
-
export function onRunSettled(runId: string
|
|
234
|
+
export function onRunSettled(runId: string): void {
|
|
242
235
|
const run = getRun(runId);
|
|
243
236
|
if (!run) return;
|
|
244
237
|
if (run.settledMessageSent) return;
|
|
@@ -266,7 +259,7 @@ export function scheduleRun(runId: string, ctx: DispatchCtx): void {
|
|
|
266
259
|
if (!run || run.status !== "running") return;
|
|
267
260
|
const settled = settleRun(runId);
|
|
268
261
|
if (settled !== "running") {
|
|
269
|
-
onRunSettled(runId
|
|
262
|
+
onRunSettled(runId);
|
|
270
263
|
return;
|
|
271
264
|
}
|
|
272
265
|
const sessionBudget = MAX_SESSION_WORKERS - listNodes().filter((node) => node.status === "running").length;
|
|
@@ -346,7 +339,7 @@ export async function cancelRunAndTerminate(
|
|
|
346
339
|
cancellationIntents.resolve(spawnId, terminated);
|
|
347
340
|
}
|
|
348
341
|
publishStateSnapshot();
|
|
349
|
-
onRunSettled(runId
|
|
342
|
+
onRunSettled(runId);
|
|
350
343
|
scheduleAllRuns(ctx);
|
|
351
344
|
notifyChange();
|
|
352
345
|
return { ok: true };
|
package/src/spawner.ts
CHANGED
|
@@ -152,10 +152,68 @@ export class CancellationIntents {
|
|
|
152
152
|
/** Live workers by teammate name — lets the panel interrupt/stop a running worker. */
|
|
153
153
|
const workers = new Map<string, ReturnType<typeof spawn>>();
|
|
154
154
|
|
|
155
|
+
export interface WorkerTimeoutHandle {
|
|
156
|
+
cancel(): void;
|
|
157
|
+
dispose(): void;
|
|
158
|
+
wasCancelled(): boolean;
|
|
159
|
+
didTimeout(): boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const workerTimeouts = new WeakMap<ReturnType<typeof spawn>, WorkerTimeoutHandle>();
|
|
163
|
+
|
|
155
164
|
function isChildRunning(child: ReturnType<typeof spawn>): boolean {
|
|
156
165
|
return child.exitCode === null && child.signalCode === null;
|
|
157
166
|
}
|
|
158
167
|
|
|
168
|
+
export function registerWorkerTimeout(
|
|
169
|
+
child: ReturnType<typeof spawn>,
|
|
170
|
+
timeoutMs: number,
|
|
171
|
+
): WorkerTimeoutHandle {
|
|
172
|
+
let cancelled = false;
|
|
173
|
+
let disposed = false;
|
|
174
|
+
let timedOut = false;
|
|
175
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
176
|
+
|
|
177
|
+
const handle: WorkerTimeoutHandle = {
|
|
178
|
+
cancel() {
|
|
179
|
+
if (disposed) return;
|
|
180
|
+
cancelled = true;
|
|
181
|
+
if (timer) clearTimeout(timer);
|
|
182
|
+
disposed = true;
|
|
183
|
+
workerTimeouts.delete(child);
|
|
184
|
+
},
|
|
185
|
+
dispose() {
|
|
186
|
+
if (disposed) return;
|
|
187
|
+
if (timer) clearTimeout(timer);
|
|
188
|
+
disposed = true;
|
|
189
|
+
workerTimeouts.delete(child);
|
|
190
|
+
},
|
|
191
|
+
wasCancelled: () => cancelled,
|
|
192
|
+
didTimeout: () => timedOut,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
timer = setTimeout(() => {
|
|
196
|
+
if (disposed || cancelled) return;
|
|
197
|
+
if (!isChildRunning(child)) {
|
|
198
|
+
handle.dispose();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
if (child.kill("SIGKILL")) timedOut = true;
|
|
203
|
+
} catch {
|
|
204
|
+
// A failed kill is not a timeout outcome; close/error will report the
|
|
205
|
+
// actual child result.
|
|
206
|
+
}
|
|
207
|
+
}, timeoutMs);
|
|
208
|
+
timer.unref?.();
|
|
209
|
+
workerTimeouts.set(child, handle);
|
|
210
|
+
return handle;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function cancelWorkerTimeout(child: ReturnType<typeof spawn>): void {
|
|
214
|
+
workerTimeouts.get(child)?.cancel();
|
|
215
|
+
}
|
|
216
|
+
|
|
159
217
|
function waitForClose(child: ReturnType<typeof spawn>, timeoutMs: number): Promise<boolean> {
|
|
160
218
|
return new Promise((resolve) => {
|
|
161
219
|
let settled = false;
|
|
@@ -180,6 +238,7 @@ function waitForClose(child: ReturnType<typeof spawn>, timeoutMs: number): Promi
|
|
|
180
238
|
* resolve only after a bounded wait observes the child `close` event.
|
|
181
239
|
*/
|
|
182
240
|
export async function terminateChildProcess(child: ReturnType<typeof spawn>, graceMs = CANCEL_GRACE_MS): Promise<boolean> {
|
|
241
|
+
cancelWorkerTimeout(child);
|
|
183
242
|
if (!isChildRunning(child)) return false;
|
|
184
243
|
|
|
185
244
|
const closedAfterTerm = waitForClose(child, graceMs);
|
|
@@ -431,7 +490,17 @@ export function spawnPiWorker(options: SpawnPiWorkerOptions): SpawnedWorker | {
|
|
|
431
490
|
// resolvePiCli always resolves (worst case a "pi" binary on PATH); a missing
|
|
432
491
|
// binary surfaces later as a child "error" event through onError.
|
|
433
492
|
const cli = resolvePiCli();
|
|
434
|
-
const
|
|
493
|
+
const workerExtension = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "index.ts");
|
|
494
|
+
const args: string[] = [
|
|
495
|
+
...cli.args,
|
|
496
|
+
"--print",
|
|
497
|
+
"--mode",
|
|
498
|
+
"json",
|
|
499
|
+
"--no-session",
|
|
500
|
+
"--no-extensions",
|
|
501
|
+
"--extension",
|
|
502
|
+
workerExtension,
|
|
503
|
+
];
|
|
435
504
|
if (options.model) args.push("--model", options.model);
|
|
436
505
|
// Capability tools cannot be removed; execution tools are selected by the
|
|
437
506
|
// leader from the teammate's explicit configuration or role default.
|
|
@@ -487,7 +556,7 @@ export function spawnPiWorker(options: SpawnPiWorkerOptions): SpawnedWorker | {
|
|
|
487
556
|
const stderrChunks: string[] = [];
|
|
488
557
|
const streamState = createWorkerStreamState();
|
|
489
558
|
let stdoutBuffer = "";
|
|
490
|
-
let
|
|
559
|
+
let timeoutHandle: WorkerTimeoutHandle | undefined;
|
|
491
560
|
let settled = false;
|
|
492
561
|
const emitProgress = () => options.onUpdate?.({
|
|
493
562
|
text: truncate(streamState.text, OUTPUT_CAP),
|
|
@@ -508,17 +577,12 @@ export function spawnPiWorker(options: SpawnPiWorkerOptions): SpawnedWorker | {
|
|
|
508
577
|
});
|
|
509
578
|
child.stderr.on("data", (chunk: Buffer) => stderrChunks.push(chunk.toString()));
|
|
510
579
|
|
|
511
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
512
580
|
if (options.timeoutMs) {
|
|
513
|
-
|
|
514
|
-
timedOut = true;
|
|
515
|
-
child.kill("SIGKILL");
|
|
516
|
-
}, options.timeoutMs);
|
|
517
|
-
timer.unref?.();
|
|
581
|
+
timeoutHandle = registerWorkerTimeout(child, options.timeoutMs);
|
|
518
582
|
}
|
|
519
583
|
|
|
520
584
|
child.on("error", (error) => {
|
|
521
|
-
|
|
585
|
+
timeoutHandle?.dispose();
|
|
522
586
|
cleanupTempDir();
|
|
523
587
|
settled = true;
|
|
524
588
|
if (options.workerName && workers.get(options.workerName) === child) workers.delete(options.workerName);
|
|
@@ -526,7 +590,8 @@ export function spawnPiWorker(options: SpawnPiWorkerOptions): SpawnedWorker | {
|
|
|
526
590
|
});
|
|
527
591
|
|
|
528
592
|
child.on("close", (code, signal) => {
|
|
529
|
-
|
|
593
|
+
const timedOut = timeoutHandle?.didTimeout() ?? false;
|
|
594
|
+
timeoutHandle?.dispose();
|
|
530
595
|
cleanupTempDir();
|
|
531
596
|
// A spawn failure already reported via onError (Node fires error then
|
|
532
597
|
// close) — do not double-report through onExit, which would let a failed
|