@gotgenes/pi-subagents 16.2.0 → 16.2.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/CHANGELOG.md +14 -0
- package/README.md +9 -67
- package/docs/architecture/architecture.md +27 -20
- package/docs/comparison-with-upstream.md +76 -0
- package/docs/plans/0375-extract-run-listener-workspace-bracket.md +300 -0
- package/docs/plans/0376-extract-manager-observer.md +232 -0
- package/docs/retro/0374-encapsulate-subagent-start-notification.md +133 -0
- package/docs/retro/0375-extract-run-listener-workspace-bracket.md +104 -0
- package/docs/retro/0376-extract-manager-observer.md +40 -0
- package/package.json +1 -1
- package/src/index.ts +8 -57
- package/src/lifecycle/run-listeners.ts +37 -0
- package/src/lifecycle/subagent-manager.ts +5 -3
- package/src/lifecycle/subagent.ts +67 -94
- package/src/lifecycle/workspace-bracket.ts +59 -0
- package/src/observation/subagent-events-observer.ts +97 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-listeners.ts — Per-run observer-unsubscribe and signal-detach handles.
|
|
3
|
+
*
|
|
4
|
+
* Owns the two teardown handles that a Subagent wires at run start (signal
|
|
5
|
+
* listener) and after session creation (record-observer unsub), releasing
|
|
6
|
+
* both atomically when the run ends or the agent is resumed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Owns the per-run observer-unsubscribe and signal-detach handles. */
|
|
10
|
+
export class RunListeners {
|
|
11
|
+
private unsub?: () => void;
|
|
12
|
+
private detach?: () => void;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Wire a parent AbortSignal so it triggers onAbort when fired.
|
|
16
|
+
* No-op when signal is undefined.
|
|
17
|
+
*/
|
|
18
|
+
wireSignal(signal: AbortSignal | undefined, onAbort: () => void): void {
|
|
19
|
+
if (!signal) return;
|
|
20
|
+
const listener = () => onAbort();
|
|
21
|
+
signal.addEventListener("abort", listener, { once: true });
|
|
22
|
+
this.detach = () => signal.removeEventListener("abort", listener);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Store the record-observer unsubscribe handle. */
|
|
26
|
+
attachObserver(unsub: () => void): void {
|
|
27
|
+
this.unsub = unsub;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Release the observer + signal handles. Idempotent. */
|
|
31
|
+
release(): void {
|
|
32
|
+
this.unsub?.();
|
|
33
|
+
this.unsub = undefined;
|
|
34
|
+
this.detach?.();
|
|
35
|
+
this.detach = undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -168,12 +168,14 @@ export class SubagentManager {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
if (options.isBackground && !options.bypassQueue) {
|
|
171
|
-
// Schedule on the limiter —
|
|
172
|
-
|
|
171
|
+
// Schedule on the limiter — scheduleVia captures the limiter promise
|
|
172
|
+
// eagerly, so a queued agent is awaitable from spawn; guardedRun guards
|
|
173
|
+
// against abort-while-queued when the slot frees.
|
|
174
|
+
record.scheduleVia((thunk) => this.limiter.schedule(thunk));
|
|
173
175
|
return id;
|
|
174
176
|
}
|
|
175
177
|
|
|
176
|
-
|
|
178
|
+
record.start();
|
|
177
179
|
return id;
|
|
178
180
|
}
|
|
179
181
|
|
|
@@ -1,21 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* subagent.ts — Subagent class
|
|
2
|
+
* subagent.ts — Subagent class: identity, lifecycle status, and per-subagent behavior.
|
|
3
3
|
*
|
|
4
|
-
* Status
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Stats (toolUses, lifetimeUsage, compactionCount) are owned by the class and
|
|
9
|
-
* accumulated via mutation methods (incrementToolUses, addUsage, incrementCompactions).
|
|
10
|
-
*
|
|
11
|
-
* Behavior (abort, steer buffering) lives on the subagent rather than on
|
|
12
|
-
* SubagentManager — each subagent manages its own lifecycle concerns.
|
|
13
|
-
*
|
|
14
|
-
* The child's working directory is supplied by a registered WorkspaceProvider
|
|
15
|
-
* (the workspace seam); with no provider the child runs in the parent cwd.
|
|
16
|
-
*
|
|
17
|
-
* Phase-specific collaborators (subagentSession, notification) are attached
|
|
18
|
-
* after construction as lifecycle information becomes available.
|
|
4
|
+
* Status/stats are delegated to the SubagentState value object; listener
|
|
5
|
+
* lifecycle to RunListeners; workspace prepare/dispose to WorkspaceBracket.
|
|
6
|
+
* Behavior (abort, steer buffering) lives here rather than on SubagentManager.
|
|
19
7
|
*/
|
|
20
8
|
|
|
21
9
|
import type { Model } from "@earendil-works/pi-ai";
|
|
@@ -23,10 +11,12 @@ import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
|
23
11
|
import { debugLog } from "#src/debug";
|
|
24
12
|
import type { CreateSubagentSessionParams } from "#src/lifecycle/create-subagent-session";
|
|
25
13
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
14
|
+
import { RunListeners } from "#src/lifecycle/run-listeners";
|
|
26
15
|
import type { SubagentSession, TurnLoopResult } from "#src/lifecycle/subagent-session";
|
|
27
16
|
import { SubagentState, type SubagentStatus } from "#src/lifecycle/subagent-state";
|
|
28
17
|
import type { LifetimeUsage } from "#src/lifecycle/usage";
|
|
29
|
-
import type {
|
|
18
|
+
import type { WorkspaceProvider } from "#src/lifecycle/workspace";
|
|
19
|
+
import { WorkspaceBracket } from "#src/lifecycle/workspace-bracket";
|
|
30
20
|
import { NotificationState } from "#src/observation/notification-state";
|
|
31
21
|
import { subscribeSubagentObserver } from "#src/observation/record-observer";
|
|
32
22
|
import type { RunConfig } from "#src/runtime";
|
|
@@ -105,23 +95,16 @@ export class Subagent {
|
|
|
105
95
|
get lifetimeUsage(): Readonly<LifetimeUsage> { return this.state.lifetimeUsage; }
|
|
106
96
|
get compactionCount(): number { return this.state.compactionCount; }
|
|
107
97
|
|
|
108
|
-
/** AbortController for cancelling this agent. Created at construction. */
|
|
109
98
|
readonly abortController: AbortController;
|
|
110
|
-
/** Backing store for the run promise. Set by start(). */
|
|
111
99
|
private _promise?: Promise<void>;
|
|
112
|
-
/** Awaitable handle to the running promise. Set by start(). */
|
|
113
100
|
get promise(): Promise<void> | undefined { return this._promise; }
|
|
114
101
|
|
|
115
|
-
// Execution machinery — a single mandatory collaborator (no per-field fallbacks).
|
|
116
102
|
private readonly execution: SubagentExecution;
|
|
117
|
-
|
|
118
|
-
private
|
|
103
|
+
private readonly listeners = new RunListeners();
|
|
104
|
+
private readonly workspaceBracket: WorkspaceBracket;
|
|
119
105
|
|
|
120
|
-
// Phase-specific collaborators — each born complete when their info becomes available
|
|
121
|
-
/** The born-complete child session — set when the factory returns inside run(). */
|
|
122
106
|
subagentSession?: SubagentSession;
|
|
123
107
|
private _notification?: NotificationState;
|
|
124
|
-
/** Notification state for background agents — wired from parentSession.toolCallId. */
|
|
125
108
|
get notification(): NotificationState | undefined { return this._notification; }
|
|
126
109
|
|
|
127
110
|
// Steer buffer — messages queued before the session is ready
|
|
@@ -191,6 +174,11 @@ export class Subagent {
|
|
|
191
174
|
// Execution machinery — a single mandatory collaborator
|
|
192
175
|
this.execution = init.execution;
|
|
193
176
|
|
|
177
|
+
// Per-run lifecycle collaborators
|
|
178
|
+
this.workspaceBracket = new WorkspaceBracket(
|
|
179
|
+
this.execution.getWorkspaceProvider ?? (() => undefined),
|
|
180
|
+
);
|
|
181
|
+
|
|
194
182
|
// Notification state — created from parentSession.toolCallId if present
|
|
195
183
|
const toolCallId = init.execution.parentSession?.toolCallId;
|
|
196
184
|
if (toolCallId) {
|
|
@@ -210,27 +198,26 @@ export class Subagent {
|
|
|
210
198
|
async run(): Promise<void> {
|
|
211
199
|
this.markRunning(Date.now());
|
|
212
200
|
this.execution.observer?.onStarted?.(this);
|
|
213
|
-
this.wireSignal(this.execution.signal, () => this.abort());
|
|
201
|
+
this.listeners.wireSignal(this.execution.signal, () => this.abort());
|
|
214
202
|
|
|
203
|
+
// Guard the await so the no-provider path stays synchronous, preserving
|
|
204
|
+
// the original run() timing: the factory is called in the same turn as
|
|
205
|
+
// spawn() when no workspace provider is registered.
|
|
215
206
|
let cwd: string | undefined;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const provider = this.execution.getWorkspaceProvider?.();
|
|
220
|
-
if (provider) {
|
|
221
|
-
this._workspace = await provider.prepare({
|
|
207
|
+
if (this.workspaceBracket.hasProvider()) {
|
|
208
|
+
try {
|
|
209
|
+
cwd = await this.workspaceBracket.prepare({
|
|
222
210
|
agentId: this.id,
|
|
223
211
|
agentType: this.type,
|
|
224
212
|
baseCwd: this.execution.baseCwd,
|
|
225
213
|
invocation: this.invocation,
|
|
226
214
|
});
|
|
227
|
-
|
|
215
|
+
} catch (err) {
|
|
216
|
+
this.markError(err);
|
|
217
|
+
this.listeners.release();
|
|
218
|
+
this.execution.observer?.onRunFinished?.(this);
|
|
219
|
+
return;
|
|
228
220
|
}
|
|
229
|
-
} catch (err) {
|
|
230
|
-
this.markError(err);
|
|
231
|
-
this.releaseListeners();
|
|
232
|
-
this.execution.observer?.onRunFinished?.(this);
|
|
233
|
-
return;
|
|
234
221
|
}
|
|
235
222
|
|
|
236
223
|
try {
|
|
@@ -249,7 +236,7 @@ export class Subagent {
|
|
|
249
236
|
}
|
|
250
237
|
|
|
251
238
|
this.flushPendingSteers();
|
|
252
|
-
this.attachObserver(subscribeSubagentObserver(this.subagentSession, this.state, {
|
|
239
|
+
this.listeners.attachObserver(subscribeSubagentObserver(this.subagentSession, this.state, {
|
|
253
240
|
onCompact: (info) => this.execution.observer?.onCompacted?.(this, info),
|
|
254
241
|
}));
|
|
255
242
|
this.execution.observer?.onSessionCreated?.(this);
|
|
@@ -269,19 +256,32 @@ export class Subagent {
|
|
|
269
256
|
}
|
|
270
257
|
|
|
271
258
|
/**
|
|
272
|
-
* Start execution
|
|
273
|
-
*
|
|
274
|
-
* Guards against non-active states (e.g. abort-while-queued): if the agent
|
|
275
|
-
* is neither queued nor running, the promise resolves immediately (no-op).
|
|
276
|
-
* This folds the inline status guard out of SubagentManager's limiter callback.
|
|
259
|
+
* Start execution immediately (foreground / bypassQueue paths).
|
|
260
|
+
* Stores the run promise so it is awaitable via the `promise` getter.
|
|
277
261
|
*/
|
|
278
|
-
start():
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
262
|
+
start(): void {
|
|
263
|
+
this._promise = this.guardedRun();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Schedule execution through an external concurrency scheduler (the limiter).
|
|
268
|
+
* Captures the scheduler's promise eagerly, so a still-queued agent is
|
|
269
|
+
* awaitable via the `promise` getter from spawn — not only once its slot opens.
|
|
270
|
+
* The guard in guardedRun() makes an abort-while-queued run a no-op when the
|
|
271
|
+
* slot finally frees.
|
|
272
|
+
*/
|
|
273
|
+
scheduleVia(schedule: (thunk: () => Promise<void>) => Promise<void>): void {
|
|
274
|
+
this._promise = schedule(() => this.guardedRun());
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Run unless the agent left the active set before its slot opened
|
|
279
|
+
* (e.g. abort-while-queued): a non-queued, non-running status resolves
|
|
280
|
+
* immediately without running.
|
|
281
|
+
*/
|
|
282
|
+
private guardedRun(): Promise<void> {
|
|
283
|
+
if (this.status !== "queued" && this.status !== "running") return Promise.resolve();
|
|
284
|
+
return this.run();
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
/**
|
|
@@ -300,7 +300,7 @@ export class Subagent {
|
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
this.resetForResume(Date.now());
|
|
303
|
-
this.attachObserver(subscribeSubagentObserver(subagentSession, this.state, {
|
|
303
|
+
this.listeners.attachObserver(subscribeSubagentObserver(subagentSession, this.state, {
|
|
304
304
|
onCompact: (info) => this.execution.observer?.onCompacted?.(this, info),
|
|
305
305
|
}));
|
|
306
306
|
|
|
@@ -310,7 +310,7 @@ export class Subagent {
|
|
|
310
310
|
} catch (err) {
|
|
311
311
|
this.markError(err);
|
|
312
312
|
} finally {
|
|
313
|
-
this.
|
|
313
|
+
this.listeners.release();
|
|
314
314
|
}
|
|
315
315
|
}
|
|
316
316
|
|
|
@@ -406,48 +406,21 @@ export class Subagent {
|
|
|
406
406
|
/** Reset for resume: running status, new startedAt, clear completedAt/result/error/listeners. */
|
|
407
407
|
resetForResume(startedAt: number): void {
|
|
408
408
|
this.state.resetForResume(startedAt);
|
|
409
|
-
this.
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// --- Per-run listener state (released on completion or resume reset) ---
|
|
413
|
-
private _unsub?: () => void;
|
|
414
|
-
private _detachFn?: () => void;
|
|
415
|
-
|
|
416
|
-
/** Wire a parent AbortSignal so it stops this agent when fired. */
|
|
417
|
-
wireSignal(signal: AbortSignal | undefined, onAbort: () => void): void {
|
|
418
|
-
if (!signal) return;
|
|
419
|
-
const listener = () => onAbort();
|
|
420
|
-
signal.addEventListener("abort", listener, { once: true });
|
|
421
|
-
this._detachFn = () => signal.removeEventListener("abort", listener);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/** Store the record-observer unsubscribe handle. */
|
|
425
|
-
attachObserver(unsub: () => void): void {
|
|
426
|
-
this._unsub = unsub;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/** Release observer + signal listener handles. */
|
|
430
|
-
releaseListeners(): void {
|
|
431
|
-
this._unsub?.();
|
|
432
|
-
this._unsub = undefined;
|
|
433
|
-
this._detachFn?.();
|
|
434
|
-
this._detachFn = undefined;
|
|
409
|
+
this.listeners.release();
|
|
435
410
|
}
|
|
436
411
|
|
|
437
412
|
/** Complete a run: release listeners, dispose the workspace, status transition, notify observer. */
|
|
438
413
|
completeRun(result: TurnLoopResult): void {
|
|
439
|
-
this.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
? "
|
|
445
|
-
:
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
if (disposeResult?.resultAddendum) finalResult += disposeResult.resultAddendum;
|
|
450
|
-
}
|
|
414
|
+
this.listeners.release();
|
|
415
|
+
|
|
416
|
+
const finalStatus: SubagentStatus = result.aborted
|
|
417
|
+
? "aborted"
|
|
418
|
+
: result.steered
|
|
419
|
+
? "steered"
|
|
420
|
+
: "completed";
|
|
421
|
+
const finalResult =
|
|
422
|
+
result.responseText +
|
|
423
|
+
this.workspaceBracket.dispose({ status: finalStatus, description: this.description });
|
|
451
424
|
|
|
452
425
|
if (result.aborted) this.markAborted(finalResult);
|
|
453
426
|
else if (result.steered) this.markSteered(finalResult);
|
|
@@ -464,10 +437,10 @@ export class Subagent {
|
|
|
464
437
|
/** Fail a run: mark error, release listeners, best-effort workspace dispose, notify observer. */
|
|
465
438
|
failRun(err: unknown): void {
|
|
466
439
|
this.markError(err);
|
|
467
|
-
this.
|
|
440
|
+
this.listeners.release();
|
|
468
441
|
|
|
469
442
|
try {
|
|
470
|
-
|
|
443
|
+
this.workspaceBracket.dispose({ status: "error", description: this.description });
|
|
471
444
|
} catch (cleanupErr) { debugLog("workspace dispose on agent error", cleanupErr); }
|
|
472
445
|
|
|
473
446
|
this.execution.observer?.onRunFinished?.(this);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workspace-bracket.ts — Owned prepare/dispose lifecycle for a child workspace.
|
|
3
|
+
*
|
|
4
|
+
* Captures the provider resolver (not the provider itself) so provider
|
|
5
|
+
* resolution stays lazy at run-start. The prepared Workspace is held
|
|
6
|
+
* privately; dispose() centralises the guard and addendum-unwrap so callers
|
|
7
|
+
* never reach through to workspace.dispose().resultAddendum directly.
|
|
8
|
+
*
|
|
9
|
+
* dispose() deliberately does NOT catch errors — the best-effort try/catch
|
|
10
|
+
* for failRun() belongs at the call site, preserving the per-caller semantics.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
Workspace,
|
|
15
|
+
WorkspaceDisposeOutcome,
|
|
16
|
+
WorkspacePrepareContext,
|
|
17
|
+
WorkspaceProvider,
|
|
18
|
+
} from "#src/lifecycle/workspace";
|
|
19
|
+
|
|
20
|
+
/** Owns the child workspace lifecycle: prepare at run-start, dispose at run-end. */
|
|
21
|
+
export class WorkspaceBracket {
|
|
22
|
+
private prepared?: Workspace;
|
|
23
|
+
|
|
24
|
+
constructor(private readonly resolveProvider: () => WorkspaceProvider | undefined) {}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Returns true when a workspace provider is currently registered.
|
|
28
|
+
* Use to guard the `await prepare(...)` call and avoid an unnecessary
|
|
29
|
+
* microtask boundary in the no-provider path.
|
|
30
|
+
*/
|
|
31
|
+
hasProvider(): boolean {
|
|
32
|
+
return this.resolveProvider() !== undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the registered provider and prepare the child workspace.
|
|
37
|
+
* Returns the workspace's cwd, or undefined when no provider is registered
|
|
38
|
+
* or the provider resolves to undefined.
|
|
39
|
+
*/
|
|
40
|
+
async prepare(ctx: WorkspacePrepareContext): Promise<string | undefined> {
|
|
41
|
+
const provider = this.resolveProvider();
|
|
42
|
+
if (!provider) return undefined;
|
|
43
|
+
this.prepared = await provider.prepare(ctx);
|
|
44
|
+
return this.prepared?.cwd;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Dispose the prepared workspace (if any) and return the result addendum
|
|
49
|
+
* verbatim. Returns an empty string when no workspace was prepared or when
|
|
50
|
+
* the workspace returns no addendum.
|
|
51
|
+
*
|
|
52
|
+
* Throws propagate — wrap in try/catch at the call site when best-effort
|
|
53
|
+
* disposal is desired (e.g. failRun).
|
|
54
|
+
*/
|
|
55
|
+
dispose(outcome: WorkspaceDisposeOutcome): string {
|
|
56
|
+
if (!this.prepared) return "";
|
|
57
|
+
return this.prepared.dispose(outcome)?.resultAddendum ?? "";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { SubagentManagerObserver } from "#src/lifecycle/subagent-manager";
|
|
2
|
+
import { buildEventData, type NotificationSystem } from "#src/observation/notification";
|
|
3
|
+
import type { CompactionInfo, Subagent } from "#src/types";
|
|
4
|
+
|
|
5
|
+
/** Emit callback — a subset of `pi.events.emit`. */
|
|
6
|
+
export type EventEmit = (channel: string, data: unknown) => void;
|
|
7
|
+
|
|
8
|
+
/** Append callback — a subset of `pi.appendEntry`. */
|
|
9
|
+
export type AppendEntry = (customType: string, data: unknown) => void;
|
|
10
|
+
|
|
11
|
+
export interface SubagentEventsObserverDeps {
|
|
12
|
+
emit: EventEmit;
|
|
13
|
+
appendEntry: AppendEntry;
|
|
14
|
+
notifications: NotificationSystem;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Receives agent lifecycle notifications from SubagentManager and dispatches
|
|
19
|
+
* them to three concerns: pi.events lifecycle events, session-entry persistence,
|
|
20
|
+
* and completion notifications.
|
|
21
|
+
*
|
|
22
|
+
* Constructed with narrow deps (emit, appendEntry, NotificationSystem) so all
|
|
23
|
+
* three concerns are unit-testable without booting the extension.
|
|
24
|
+
*/
|
|
25
|
+
export class SubagentEventsObserver implements SubagentManagerObserver {
|
|
26
|
+
private readonly emit: EventEmit;
|
|
27
|
+
private readonly appendEntry: AppendEntry;
|
|
28
|
+
private readonly notifications: NotificationSystem;
|
|
29
|
+
|
|
30
|
+
constructor(deps: SubagentEventsObserverDeps) {
|
|
31
|
+
this.emit = deps.emit;
|
|
32
|
+
this.appendEntry = deps.appendEntry;
|
|
33
|
+
this.notifications = deps.notifications;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
onSubagentStarted(record: Subagent): void {
|
|
37
|
+
// Emit started event when agent transitions to running (including from queue).
|
|
38
|
+
this.emit("subagents:started", {
|
|
39
|
+
id: record.id,
|
|
40
|
+
type: record.type,
|
|
41
|
+
description: record.description,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onSubagentCompleted(record: Subagent): void {
|
|
46
|
+
// Emit lifecycle event based on terminal status.
|
|
47
|
+
const isError = record.status === "error" || record.status === "stopped" || record.status === "aborted";
|
|
48
|
+
const eventData = buildEventData(record);
|
|
49
|
+
if (isError) {
|
|
50
|
+
this.emit("subagents:failed", eventData);
|
|
51
|
+
} else {
|
|
52
|
+
this.emit("subagents:completed", eventData);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Persist final record for cross-extension history reconstruction.
|
|
56
|
+
this.appendEntry("subagents:record", {
|
|
57
|
+
id: record.id,
|
|
58
|
+
type: record.type,
|
|
59
|
+
description: record.description,
|
|
60
|
+
status: record.status,
|
|
61
|
+
result: record.result,
|
|
62
|
+
error: record.error,
|
|
63
|
+
startedAt: record.startedAt,
|
|
64
|
+
completedAt: record.completedAt,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Skip notification if result was already consumed via get_subagent_result.
|
|
68
|
+
if (record.notification?.resultConsumed) {
|
|
69
|
+
this.notifications.cleanupCompleted(record.id);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.notifications.sendCompletion(record);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
onSubagentCompacted(record: Subagent, info: CompactionInfo): void {
|
|
77
|
+
// Emit compacted event when agent's session compacts (preserves count on record).
|
|
78
|
+
this.emit("subagents:compacted", {
|
|
79
|
+
id: record.id,
|
|
80
|
+
type: record.type,
|
|
81
|
+
description: record.description,
|
|
82
|
+
reason: info.reason,
|
|
83
|
+
tokensBefore: info.tokensBefore,
|
|
84
|
+
compactionCount: record.compactionCount,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
onSubagentCreated(record: Subagent): void {
|
|
89
|
+
// Emit created event for background agents (before limiter admission).
|
|
90
|
+
this.emit("subagents:created", {
|
|
91
|
+
id: record.id,
|
|
92
|
+
type: record.type,
|
|
93
|
+
description: record.description,
|
|
94
|
+
isBackground: true,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|