pi-famulus 0.1.2 → 0.1.4
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 +3 -1
- package/package.json +31 -12
- package/src/agents/builtins.ts +5 -22
- package/src/agents/definition.ts +16 -12
- package/src/bash-override.ts +8 -21
- package/src/behavior-guidelines.ts +40 -15
- package/src/clock.ts +5 -0
- package/src/comms/comms.ts +7 -3
- package/src/comms/tools.ts +11 -36
- package/src/config.ts +25 -1
- package/src/format.ts +83 -5
- package/src/index.ts +155 -30
- package/src/manager-client.ts +265 -114
- package/src/monitor.ts +14 -33
- package/src/notify.ts +7 -7
- package/src/prompts.generated.ts +183 -0
- package/src/shell-exec.ts +6 -2
- package/src/subagent/admission.ts +224 -0
- package/src/subagent/agent-records.ts +66 -3
- package/src/subagent/child-bash.ts +65 -56
- package/src/subagent/model-spec.ts +134 -16
- package/src/subagent/overrun.ts +191 -0
- package/src/subagent/pi-runtime.ts +118 -33
- package/src/subagent/registry.ts +235 -34
- package/src/subagent/runner.ts +565 -99
- package/src/subagent/tool.ts +111 -61
- package/src/subagent/types.ts +71 -7
- package/src/subagent/usage.ts +48 -0
- package/src/task-tools.ts +15 -25
- package/src/thinking-levels.ts +26 -0
- package/src/tui/message-renderers.ts +52 -2
- package/src/wake-delivery.ts +137 -0
- package/src/wake.ts +270 -7
- package/src/work-index.ts +4 -0
package/src/subagent/registry.ts
CHANGED
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
* - lineage: v1 = same runId.
|
|
11
11
|
*
|
|
12
12
|
* Wiring contract: the runner must be constructed with
|
|
13
|
-
* `acquire: (req) => registry.admitChild(req.childId)` so every
|
|
14
|
-
* (initial start and resume) passes through admission.
|
|
13
|
+
* `acquire: (req, ticket) => registry.admitChild(req.childId, ticket)` so every
|
|
14
|
+
* generation (initial start and resume) passes through admission. Forward the
|
|
15
|
+
* ticket: without it admitChild falls back to the handle's current status and
|
|
16
|
+
* a stale queued request can admit a newer resume. startChild() wires the
|
|
15
17
|
* first generation's result; admitChild() wires subsequent generations.
|
|
16
18
|
*
|
|
17
19
|
* Zero pi dependency.
|
|
@@ -24,9 +26,17 @@ import type {
|
|
|
24
26
|
ChildRunRequest,
|
|
25
27
|
ChildRunner,
|
|
26
28
|
ChildStatus,
|
|
29
|
+
ChildTokenUsage,
|
|
30
|
+
ChildWallUsage,
|
|
27
31
|
DisposableChildHandle,
|
|
28
32
|
} from "./types";
|
|
29
33
|
|
|
34
|
+
async function awaitDisposals(disposals: Array<void | Promise<void>>): Promise<void> {
|
|
35
|
+
const results = await Promise.allSettled(disposals);
|
|
36
|
+
const errors = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
37
|
+
if (errors.length > 0) throw new AggregateError(errors, "Child session cleanup failed");
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
// ---------------------------------------------------------------------------
|
|
31
41
|
// Appendix B public record shape
|
|
32
42
|
// ---------------------------------------------------------------------------
|
|
@@ -38,6 +48,8 @@ export interface RunRecord {
|
|
|
38
48
|
childId: string;
|
|
39
49
|
name: string;
|
|
40
50
|
agent: string;
|
|
51
|
+
workKind?: string;
|
|
52
|
+
queueMs?: number;
|
|
41
53
|
model?: string;
|
|
42
54
|
status: ChildStatus;
|
|
43
55
|
/** User-authored prompt (after chain interpolation), without injected preamble. */
|
|
@@ -47,6 +59,10 @@ export interface RunRecord {
|
|
|
47
59
|
result?: ChildResult;
|
|
48
60
|
startedAt: number;
|
|
49
61
|
endedAt?: number;
|
|
62
|
+
/** User turns so far: 1 for the launch, +1 per accepted resume. */
|
|
63
|
+
turn?: number;
|
|
64
|
+
tokenUsage?: ChildTokenUsage;
|
|
65
|
+
wallUsage?: ChildWallUsage;
|
|
50
66
|
}[];
|
|
51
67
|
status: "running" | "completed" | "partial" | "failed" | "interrupted";
|
|
52
68
|
createdAt: number;
|
|
@@ -60,7 +76,7 @@ export interface RunRegistry {
|
|
|
60
76
|
findChild(runId: string, childIdOrName: string): ChildHandle | undefined;
|
|
61
77
|
lineage(runIdA: string, runIdB: string): boolean; // v1: same runId
|
|
62
78
|
onTransition(cb: (run: RunRecord) => void): void; // fleet widget / notifications
|
|
63
|
-
disposeRun(runId: string): void
|
|
79
|
+
disposeRun(runId: string): Promise<void>;
|
|
64
80
|
}
|
|
65
81
|
|
|
66
82
|
// ---------------------------------------------------------------------------
|
|
@@ -90,6 +106,8 @@ export interface ActiveChildInfo {
|
|
|
90
106
|
runId: string;
|
|
91
107
|
name: string;
|
|
92
108
|
agent: string;
|
|
109
|
+
workKind?: string;
|
|
110
|
+
queueMs?: number;
|
|
93
111
|
model?: string;
|
|
94
112
|
status: ChildStatus;
|
|
95
113
|
startedAt: number;
|
|
@@ -101,6 +119,7 @@ interface InternalChild {
|
|
|
101
119
|
runId: string;
|
|
102
120
|
name: string;
|
|
103
121
|
agent: string;
|
|
122
|
+
workKind?: string;
|
|
104
123
|
/** Best-effort model id for fleet / ls (set when startChild runs). */
|
|
105
124
|
model?: string;
|
|
106
125
|
status: ChildStatus;
|
|
@@ -109,8 +128,13 @@ interface InternalChild {
|
|
|
109
128
|
result?: ChildResult;
|
|
110
129
|
startedAt: number;
|
|
111
130
|
endedAt?: number;
|
|
131
|
+
turn: number;
|
|
112
132
|
handle?: ChildHandle;
|
|
133
|
+
starting?: Promise<ChildHandle>;
|
|
113
134
|
shouldStart?: () => boolean;
|
|
135
|
+
/** Initial admission wait while runner.start() has not returned its handle. */
|
|
136
|
+
queueStartedAt?: number;
|
|
137
|
+
queueMs: number;
|
|
114
138
|
}
|
|
115
139
|
|
|
116
140
|
interface InternalRun {
|
|
@@ -157,6 +181,10 @@ class FailedChildHandle implements ChildHandle {
|
|
|
157
181
|
return Promise.reject(this.dead());
|
|
158
182
|
}
|
|
159
183
|
|
|
184
|
+
extend(): never {
|
|
185
|
+
throw this.dead();
|
|
186
|
+
}
|
|
187
|
+
|
|
160
188
|
interrupt(): Promise<void> {
|
|
161
189
|
return Promise.resolve();
|
|
162
190
|
}
|
|
@@ -176,6 +204,14 @@ class FailedChildHandle implements ChildHandle {
|
|
|
176
204
|
conversation() {
|
|
177
205
|
return [];
|
|
178
206
|
}
|
|
207
|
+
|
|
208
|
+
tokenUsage() {
|
|
209
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
wallUsage(): ChildWallUsage {
|
|
213
|
+
return { llmMs: 0, toolMs: 0, queueMs: 0, otherMs: 0, approximate: false };
|
|
214
|
+
}
|
|
179
215
|
}
|
|
180
216
|
|
|
181
217
|
export class SubagentRegistry implements RunRegistry {
|
|
@@ -220,16 +256,17 @@ export class SubagentRegistry implements RunRegistry {
|
|
|
220
256
|
};
|
|
221
257
|
this.runs.set(run.runId, run);
|
|
222
258
|
this.emit(run);
|
|
223
|
-
return snapshot(run);
|
|
259
|
+
return snapshot(run, this.now());
|
|
224
260
|
}
|
|
225
261
|
|
|
226
262
|
get(runId: string): RunRecord | undefined {
|
|
227
263
|
const run = this.runs.get(runId);
|
|
228
|
-
return run ? snapshot(run) : undefined;
|
|
264
|
+
return run ? snapshot(run, this.now()) : undefined;
|
|
229
265
|
}
|
|
230
266
|
|
|
231
267
|
list(): RunRecord[] {
|
|
232
|
-
|
|
268
|
+
const now = this.now();
|
|
269
|
+
return [...this.runs.values()].map((run) => snapshot(run, now));
|
|
233
270
|
}
|
|
234
271
|
|
|
235
272
|
handle(childId: string): ChildHandle | undefined {
|
|
@@ -253,7 +290,7 @@ export class SubagentRegistry implements RunRegistry {
|
|
|
253
290
|
this.transitionCbs.add(cb);
|
|
254
291
|
}
|
|
255
292
|
|
|
256
|
-
disposeRun(runId: string): void {
|
|
293
|
+
async disposeRun(runId: string): Promise<void> {
|
|
257
294
|
const run = this.runs.get(runId);
|
|
258
295
|
if (!run) return;
|
|
259
296
|
const now = this.now();
|
|
@@ -262,6 +299,10 @@ export class SubagentRegistry implements RunRegistry {
|
|
|
262
299
|
// "running" record (ghost agents in ls).
|
|
263
300
|
for (const child of run.children) {
|
|
264
301
|
if (child.status === "pending" || child.status === "running") {
|
|
302
|
+
if (child.status === "pending" && child.queueStartedAt !== undefined) {
|
|
303
|
+
child.queueMs = Math.max(0, now - child.queueStartedAt);
|
|
304
|
+
child.queueStartedAt = undefined;
|
|
305
|
+
}
|
|
265
306
|
child.status = "interrupted";
|
|
266
307
|
child.endedAt = now;
|
|
267
308
|
child.result = {
|
|
@@ -275,17 +316,26 @@ export class SubagentRegistry implements RunRegistry {
|
|
|
275
316
|
this.recomputeRunStatus(run);
|
|
276
317
|
this.emit(run);
|
|
277
318
|
this.runs.delete(runId);
|
|
319
|
+
const disposals: Array<void | Promise<void>> = [];
|
|
278
320
|
for (const child of run.children) {
|
|
279
321
|
this.children.delete(child.childId);
|
|
280
322
|
const handle = child.handle as DisposableChildHandle | undefined;
|
|
281
|
-
if (!handle)
|
|
323
|
+
if (!handle) {
|
|
324
|
+
// Custom runners may not expose a handle until startup completes.
|
|
325
|
+
if (child.starting) disposals.push(child.starting.then(async (lateHandle) => {
|
|
326
|
+
await lateHandle.interrupt().catch(() => {});
|
|
327
|
+
await (lateHandle as DisposableChildHandle).dispose?.();
|
|
328
|
+
}, () => {}));
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
282
331
|
void handle.interrupt().catch(() => {});
|
|
283
332
|
try {
|
|
284
|
-
handle.dispose?.();
|
|
285
|
-
} catch {
|
|
286
|
-
|
|
333
|
+
disposals.push(handle.dispose?.());
|
|
334
|
+
} catch (error) {
|
|
335
|
+
disposals.push(Promise.reject(error));
|
|
287
336
|
}
|
|
288
337
|
}
|
|
338
|
+
await awaitDisposals(disposals);
|
|
289
339
|
}
|
|
290
340
|
|
|
291
341
|
// -------------------------------------------------------------------------
|
|
@@ -293,7 +343,7 @@ export class SubagentRegistry implements RunRegistry {
|
|
|
293
343
|
// -------------------------------------------------------------------------
|
|
294
344
|
|
|
295
345
|
/** Allocate a pending child inside a run. Returns the child id. */
|
|
296
|
-
addChild(runId: string, info: { name: string; agent: string }): string {
|
|
346
|
+
addChild(runId: string, info: { name: string; agent: string; workKind?: string }): string {
|
|
297
347
|
const run = this.runs.get(runId);
|
|
298
348
|
if (!run) throw new Error(`unknown run ${runId}`);
|
|
299
349
|
const child: InternalChild = {
|
|
@@ -301,8 +351,12 @@ export class SubagentRegistry implements RunRegistry {
|
|
|
301
351
|
runId,
|
|
302
352
|
name: info.name,
|
|
303
353
|
agent: info.agent,
|
|
354
|
+
workKind: info.workKind ?? "other",
|
|
304
355
|
status: "pending",
|
|
305
356
|
startedAt: this.now(),
|
|
357
|
+
queueStartedAt: this.now(),
|
|
358
|
+
turn: 1,
|
|
359
|
+
queueMs: 0,
|
|
306
360
|
};
|
|
307
361
|
run.children.push(child);
|
|
308
362
|
this.children.set(child.childId, child);
|
|
@@ -319,6 +373,7 @@ export class SubagentRegistry implements RunRegistry {
|
|
|
319
373
|
const child = this.children.get(req.childId);
|
|
320
374
|
if (!child) throw new Error(`unknown child ${req.childId} (addChild first)`);
|
|
321
375
|
child.shouldStart = opts?.shouldStart;
|
|
376
|
+
child.workKind = req.workKind ?? child.workKind ?? "other";
|
|
322
377
|
child.prompt = req.taskPrompt ?? req.prompt;
|
|
323
378
|
child.preamble = req.agent.systemPrompt || undefined;
|
|
324
379
|
child.model = req.model ?? req.agent.model;
|
|
@@ -333,12 +388,28 @@ export class SubagentRegistry implements RunRegistry {
|
|
|
333
388
|
return this.failWithoutSession(child, (err as Error).message);
|
|
334
389
|
}
|
|
335
390
|
|
|
336
|
-
|
|
391
|
+
child.queueStartedAt = this.now();
|
|
392
|
+
let handle: ChildHandle;
|
|
393
|
+
try {
|
|
394
|
+
child.starting = runner.start(req, (startingHandle) => { child.handle = startingHandle; });
|
|
395
|
+
handle = await child.starting;
|
|
396
|
+
} catch (error) {
|
|
397
|
+
this.closeInitialQueue(child, this.now());
|
|
398
|
+
throw error;
|
|
399
|
+
} finally {
|
|
400
|
+
child.starting = undefined;
|
|
401
|
+
}
|
|
402
|
+
// Teardown already owns this handle and its asynchronous cleanup.
|
|
403
|
+
if (!this.children.has(child.childId)) return handle;
|
|
404
|
+
this.closeInitialQueue(child, this.now());
|
|
337
405
|
child.handle = handle;
|
|
338
|
-
//
|
|
339
|
-
//
|
|
406
|
+
// Preserve the session's effective model and thinking setting, not a raw
|
|
407
|
+
// request suffix that may have fallen back to a different parent model.
|
|
340
408
|
const resolved = handle.resolvedModel();
|
|
341
|
-
if (resolved)
|
|
409
|
+
if (resolved) {
|
|
410
|
+
const thinking = handle.effectiveThinkingLevel?.();
|
|
411
|
+
child.model = `${resolved}${thinking ? `:${thinking}` : ""}`;
|
|
412
|
+
}
|
|
342
413
|
// Generation-1 result wiring (later generations are wired in admitChild,
|
|
343
414
|
// where the handle is already visible).
|
|
344
415
|
handle.result.then((result) => this.settleChild(child.childId, result));
|
|
@@ -360,7 +431,10 @@ export class SubagentRegistry implements RunRegistry {
|
|
|
360
431
|
* and wires result settlement for resumed generations. Returns the slot
|
|
361
432
|
* releaser.
|
|
362
433
|
*/
|
|
363
|
-
async
|
|
434
|
+
async reserveChildSlot(
|
|
435
|
+
childId: string,
|
|
436
|
+
ticket?: { current: () => boolean },
|
|
437
|
+
): Promise<{ admit: () => void; release: () => void }> {
|
|
364
438
|
await this.acquireSlot();
|
|
365
439
|
let released = false;
|
|
366
440
|
const release = () => {
|
|
@@ -373,18 +447,111 @@ export class SubagentRegistry implements RunRegistry {
|
|
|
373
447
|
release();
|
|
374
448
|
throw new Error(`unknown child ${childId}`);
|
|
375
449
|
}
|
|
376
|
-
|
|
450
|
+
const checkCurrent = () => {
|
|
451
|
+
if (child.shouldStart && !child.shouldStart()) throw new ChildCancelledError();
|
|
452
|
+
// A request settled while waiting (interrupt/dispose or a newer resume
|
|
453
|
+
// queued behind other children) must hand this local reservation on.
|
|
454
|
+
const stale = ticket ? !ticket.current() : (child.handle?.status() ?? "pending") !== "pending";
|
|
455
|
+
if (stale) throw new ChildCancelledError("settled while queued");
|
|
456
|
+
};
|
|
457
|
+
try {
|
|
458
|
+
checkCurrent();
|
|
459
|
+
} catch (error) {
|
|
460
|
+
this.closeInitialQueue(child, this.now());
|
|
377
461
|
release();
|
|
378
|
-
throw
|
|
462
|
+
throw error;
|
|
379
463
|
}
|
|
380
|
-
|
|
464
|
+
let admitted = false;
|
|
465
|
+
return {
|
|
466
|
+
release,
|
|
467
|
+
admit: () => {
|
|
468
|
+
if (admitted) return;
|
|
469
|
+
checkCurrent();
|
|
470
|
+
this.closeInitialQueue(child, this.now());
|
|
471
|
+
admitted = true;
|
|
472
|
+
this.transitionChild(child, "running");
|
|
473
|
+
const handle = child.handle;
|
|
474
|
+
if (handle) {
|
|
475
|
+
// Resume generations: wire settlement only once global admission also succeeds.
|
|
476
|
+
handle.result.then((result) => this.settleChild(childId, result));
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async admitChild(childId: string, ticket?: { current: () => boolean }): Promise<() => void> {
|
|
483
|
+
const reservation = await this.reserveChildSlot(childId, ticket);
|
|
484
|
+
try {
|
|
485
|
+
reservation.admit();
|
|
486
|
+
return reservation.release;
|
|
487
|
+
} catch (error) {
|
|
488
|
+
reservation.release();
|
|
489
|
+
throw error;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Resume an ended child: a new user turn on its session. Returns once the
|
|
495
|
+
* request is accepted; admission runs in the background (design.md §4.6
|
|
496
|
+
* "Child lifecycle", resume rows). Until a slot is granted the child is
|
|
497
|
+
* pending. `queuedBehind` is null when a slot was free at request time,
|
|
498
|
+
* otherwise how many children were already waiting for one.
|
|
499
|
+
*/
|
|
500
|
+
async resumeChild(
|
|
501
|
+
childId: string,
|
|
502
|
+
message: string,
|
|
503
|
+
opts: { timeoutMs?: number } = {},
|
|
504
|
+
): Promise<{ queuedBehind: number | null }> {
|
|
505
|
+
const child = this.children.get(childId);
|
|
506
|
+
if (!child) throw new Error(`unknown child ${childId}`);
|
|
381
507
|
const handle = child.handle;
|
|
382
|
-
if (handle) {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
508
|
+
if (!handle) throw new Error(`subagent ${childId} has no live session to resume`);
|
|
509
|
+
const queuedBehind = this.activeSlots >= this.maxChildren ? this.slotWaiters.length : null;
|
|
510
|
+
const previous = {
|
|
511
|
+
status: child.status,
|
|
512
|
+
result: child.result,
|
|
513
|
+
startedAt: child.startedAt,
|
|
514
|
+
endedAt: child.endedAt,
|
|
515
|
+
queueMs: child.queueMs,
|
|
516
|
+
queueStartedAt: child.queueStartedAt,
|
|
517
|
+
};
|
|
518
|
+
const accepted = handle.resume(message, opts);
|
|
519
|
+
// Mark the child queued before awaiting: with a free slot, admitChild's
|
|
520
|
+
// continuation can run first and move it to running, which must win.
|
|
521
|
+
child.status = "pending";
|
|
522
|
+
child.result = undefined;
|
|
523
|
+
child.endedAt = undefined;
|
|
524
|
+
child.startedAt = this.now();
|
|
525
|
+
child.queueMs = 0;
|
|
526
|
+
child.queueStartedAt = child.startedAt;
|
|
527
|
+
child.turn += 1;
|
|
528
|
+
try {
|
|
529
|
+
await accepted;
|
|
530
|
+
} catch (err) {
|
|
531
|
+
// Undo this attempt's +1 even if a concurrent accepted resume has
|
|
532
|
+
// already moved the child to running (the pending guard would skip).
|
|
533
|
+
child.turn -= 1;
|
|
534
|
+
if (child.status === "pending") {
|
|
535
|
+
child.status = previous.status;
|
|
536
|
+
child.result = previous.result;
|
|
537
|
+
child.startedAt = previous.startedAt;
|
|
538
|
+
child.endedAt = previous.endedAt;
|
|
539
|
+
child.queueMs = previous.queueMs;
|
|
540
|
+
child.queueStartedAt = previous.queueStartedAt;
|
|
541
|
+
}
|
|
542
|
+
throw err;
|
|
386
543
|
}
|
|
387
|
-
|
|
544
|
+
// Wired at request time, not admission: a turn that never gets a slot
|
|
545
|
+
// (interrupt/dispose while queued, abort drain bound) still settles here.
|
|
546
|
+
handle.result.then((result) => this.settleChild(childId, result));
|
|
547
|
+
if (child.status === "pending") {
|
|
548
|
+
const run = this.runs.get(child.runId);
|
|
549
|
+
if (run) {
|
|
550
|
+
this.recomputeRunStatus(run);
|
|
551
|
+
this.emit(run);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return { queuedBehind };
|
|
388
555
|
}
|
|
389
556
|
|
|
390
557
|
/** Current generation's result promise for a child (post-resume aware). */
|
|
@@ -400,12 +567,17 @@ export class SubagentRegistry implements RunRegistry {
|
|
|
400
567
|
finalizeRun(runId: string, error: string): void {
|
|
401
568
|
const run = this.runs.get(runId);
|
|
402
569
|
if (!run) return;
|
|
570
|
+
const now = this.now();
|
|
403
571
|
let changed = false;
|
|
404
572
|
for (const child of run.children) {
|
|
405
573
|
if (child.status === "pending" && !child.handle) {
|
|
574
|
+
if (child.queueStartedAt !== undefined) {
|
|
575
|
+
child.queueMs = (child.queueMs ?? 0) + Math.max(0, now - child.queueStartedAt);
|
|
576
|
+
child.queueStartedAt = undefined;
|
|
577
|
+
}
|
|
406
578
|
child.status = "interrupted";
|
|
407
579
|
child.result = { status: "interrupted", text: "", error, durationMs: 0 };
|
|
408
|
-
child.endedAt =
|
|
580
|
+
child.endedAt = now;
|
|
409
581
|
changed = true;
|
|
410
582
|
}
|
|
411
583
|
}
|
|
@@ -425,6 +597,11 @@ export class SubagentRegistry implements RunRegistry {
|
|
|
425
597
|
runId: child.runId,
|
|
426
598
|
name: child.name,
|
|
427
599
|
agent: child.agent,
|
|
600
|
+
workKind: child.workKind,
|
|
601
|
+
queueMs:
|
|
602
|
+
child.status === "pending" && child.queueStartedAt !== undefined
|
|
603
|
+
? (child.queueMs ?? 0) + Math.max(0, this.now() - child.queueStartedAt)
|
|
604
|
+
: child.queueMs,
|
|
428
605
|
model: child.model,
|
|
429
606
|
status: child.status,
|
|
430
607
|
startedAt: child.startedAt,
|
|
@@ -436,10 +613,8 @@ export class SubagentRegistry implements RunRegistry {
|
|
|
436
613
|
}
|
|
437
614
|
|
|
438
615
|
/** Dispose every run (session shutdown). */
|
|
439
|
-
disposeAll(): void {
|
|
440
|
-
|
|
441
|
-
this.disposeRun(runId);
|
|
442
|
-
}
|
|
616
|
+
async disposeAll(): Promise<void> {
|
|
617
|
+
await awaitDisposals([...this.runs.keys()].map((runId) => this.disposeRun(runId)));
|
|
443
618
|
}
|
|
444
619
|
|
|
445
620
|
// -------------------------------------------------------------------------
|
|
@@ -458,9 +633,15 @@ export class SubagentRegistry implements RunRegistry {
|
|
|
458
633
|
const child = this.children.get(childId);
|
|
459
634
|
if (!child) return;
|
|
460
635
|
if (child.status !== "pending" && child.status !== "running") return; // already terminal
|
|
636
|
+
const settledAt = this.now();
|
|
637
|
+
if (child.status === "pending" && child.queueStartedAt !== undefined) {
|
|
638
|
+
child.queueMs = Math.max(0, settledAt - child.queueStartedAt);
|
|
639
|
+
child.queueStartedAt = undefined;
|
|
640
|
+
}
|
|
641
|
+
if (child.queueMs === undefined && result.queueMs !== undefined) child.queueMs = result.queueMs;
|
|
461
642
|
child.status = result.status;
|
|
462
643
|
child.result = result;
|
|
463
|
-
child.endedAt =
|
|
644
|
+
child.endedAt = settledAt;
|
|
464
645
|
child.shouldStart = undefined;
|
|
465
646
|
const run = this.runs.get(child.runId);
|
|
466
647
|
if (run) {
|
|
@@ -473,8 +654,11 @@ export class SubagentRegistry implements RunRegistry {
|
|
|
473
654
|
if (child.status === status) return;
|
|
474
655
|
child.status = status;
|
|
475
656
|
if (status === "running") {
|
|
657
|
+
const startedAt = this.now();
|
|
658
|
+
child.queueMs = Math.max(0, startedAt - (child.queueStartedAt ?? child.startedAt));
|
|
659
|
+
child.queueStartedAt = undefined;
|
|
476
660
|
// (Re-)start: elapsed time and any previous generation's result reset.
|
|
477
|
-
child.startedAt =
|
|
661
|
+
child.startedAt = startedAt;
|
|
478
662
|
child.result = undefined;
|
|
479
663
|
child.endedAt = undefined;
|
|
480
664
|
}
|
|
@@ -502,9 +686,15 @@ export class SubagentRegistry implements RunRegistry {
|
|
|
502
686
|
else run.status = "partial";
|
|
503
687
|
}
|
|
504
688
|
|
|
689
|
+
private closeInitialQueue(child: InternalChild, at: number): void {
|
|
690
|
+
if (child.queueStartedAt === undefined) return;
|
|
691
|
+
child.queueMs += Math.max(0, at - child.queueStartedAt);
|
|
692
|
+
child.queueStartedAt = undefined;
|
|
693
|
+
}
|
|
694
|
+
|
|
505
695
|
private emit(run: InternalRun): void {
|
|
506
696
|
if (this.transitionCbs.size === 0) return;
|
|
507
|
-
const record = snapshot(run);
|
|
697
|
+
const record = snapshot(run, this.now());
|
|
508
698
|
for (const cb of this.transitionCbs) {
|
|
509
699
|
try {
|
|
510
700
|
cb(record);
|
|
@@ -553,7 +743,7 @@ export class SubagentRegistry implements RunRegistry {
|
|
|
553
743
|
}
|
|
554
744
|
}
|
|
555
745
|
|
|
556
|
-
function snapshot(run: InternalRun): RunRecord {
|
|
746
|
+
function snapshot(run: InternalRun, now: number): RunRecord {
|
|
557
747
|
return {
|
|
558
748
|
runId: run.runId,
|
|
559
749
|
kind: run.kind,
|
|
@@ -563,6 +753,8 @@ function snapshot(run: InternalRun): RunRecord {
|
|
|
563
753
|
childId: c.childId,
|
|
564
754
|
name: c.name,
|
|
565
755
|
agent: c.agent,
|
|
756
|
+
...(c.workKind !== undefined ? { workKind: c.workKind } : {}),
|
|
757
|
+
...(c.queueMs !== undefined ? { queueMs: c.queueMs } : {}),
|
|
566
758
|
...(c.model !== undefined ? { model: c.model } : {}),
|
|
567
759
|
status: c.status,
|
|
568
760
|
...(c.prompt !== undefined ? { prompt: c.prompt } : {}),
|
|
@@ -570,6 +762,15 @@ function snapshot(run: InternalRun): RunRecord {
|
|
|
570
762
|
result: c.result,
|
|
571
763
|
startedAt: c.startedAt,
|
|
572
764
|
endedAt: c.endedAt,
|
|
765
|
+
turn: c.turn,
|
|
766
|
+
tokenUsage: c.handle?.tokenUsage() ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
767
|
+
wallUsage: c.handle?.wallUsage() ?? {
|
|
768
|
+
llmMs: 0,
|
|
769
|
+
toolMs: 0,
|
|
770
|
+
queueMs: c.queueMs + (c.queueStartedAt === undefined ? 0 : Math.max(0, now - c.queueStartedAt)),
|
|
771
|
+
otherMs: c.status === "running" ? Math.max(0, now - c.startedAt) : 0,
|
|
772
|
+
approximate: c.status === "running",
|
|
773
|
+
},
|
|
573
774
|
})),
|
|
574
775
|
};
|
|
575
776
|
}
|