@gotgenes/pi-subagents 16.1.0 → 16.2.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.
- package/CHANGELOG.md +14 -0
- package/dist/public.d.ts +19 -22
- package/docs/architecture/architecture.md +52 -19
- package/docs/plans/0373-extract-subagent-state.md +250 -0
- package/docs/plans/0374-encapsulate-subagent-start-notification.md +268 -0
- package/docs/plans/0403-abort-subagents-on-interrupt.md +180 -0
- package/docs/retro/0373-extract-subagent-state.md +94 -0
- package/docs/retro/0374-encapsulate-subagent-start-notification.md +38 -0
- package/docs/retro/0381-replace-concurrency-queue-with-limiter.md +46 -0
- package/docs/retro/0403-abort-subagents-on-interrupt.md +90 -0
- package/package.json +1 -1
- package/src/handlers/index.ts +1 -0
- package/src/handlers/interrupt.ts +49 -0
- package/src/index.ts +5 -1
- package/src/lifecycle/subagent-manager.ts +22 -23
- package/src/lifecycle/subagent-state.ts +156 -0
- package/src/lifecycle/subagent.ts +108 -166
- package/src/observation/record-observer.ts +15 -13
|
@@ -24,8 +24,8 @@ import { debugLog } from "#src/debug";
|
|
|
24
24
|
import type { CreateSubagentSessionParams } from "#src/lifecycle/create-subagent-session";
|
|
25
25
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
26
26
|
import type { SubagentSession, TurnLoopResult } from "#src/lifecycle/subagent-session";
|
|
27
|
+
import { SubagentState, type SubagentStatus } from "#src/lifecycle/subagent-state";
|
|
27
28
|
import type { LifetimeUsage } from "#src/lifecycle/usage";
|
|
28
|
-
import { addUsage } from "#src/lifecycle/usage";
|
|
29
29
|
import type { Workspace, WorkspaceProvider } from "#src/lifecycle/workspace";
|
|
30
30
|
import { NotificationState } from "#src/observation/notification-state";
|
|
31
31
|
import { subscribeSubagentObserver } from "#src/observation/record-observer";
|
|
@@ -44,50 +44,48 @@ export interface SubagentLifecycleObserver {
|
|
|
44
44
|
onCompacted?(agent: Subagent, info: CompactionInfo): void;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export type SubagentStatus
|
|
48
|
-
| "queued"
|
|
49
|
-
| "running"
|
|
50
|
-
| "completed"
|
|
51
|
-
| "steered"
|
|
52
|
-
| "aborted"
|
|
53
|
-
| "stopped"
|
|
54
|
-
| "error";
|
|
47
|
+
export type { SubagentStatus } from "#src/lifecycle/subagent-state";
|
|
55
48
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// Status (for tests and restore scenarios)
|
|
64
|
-
status?: SubagentStatus;
|
|
65
|
-
startedAt?: number;
|
|
66
|
-
completedAt?: number;
|
|
67
|
-
result?: string;
|
|
68
|
-
error?: string;
|
|
69
|
-
|
|
70
|
-
// Shared deps (required for run(), optional for tests)
|
|
49
|
+
/**
|
|
50
|
+
* The execution machinery a Subagent needs to run. A single mandatory
|
|
51
|
+
* collaborator: production (SubagentManager.spawn) always supplies it, so run()
|
|
52
|
+
* needs no "not configured" guards. The genuinely-optional behavior knobs stay
|
|
53
|
+
* optional; the four inputs run() cannot proceed without are required.
|
|
54
|
+
*/
|
|
55
|
+
export interface SubagentExecution {
|
|
71
56
|
/** Assembly factory that produces a born-complete SubagentSession. */
|
|
72
|
-
createSubagentSession
|
|
57
|
+
createSubagentSession: (params: CreateSubagentSessionParams) => Promise<SubagentSession>;
|
|
58
|
+
/** Immutable spawn-time parent snapshot handed to the session factory. */
|
|
59
|
+
snapshot: ParentSnapshot;
|
|
60
|
+
/** Initial prompt for the turn loop. */
|
|
61
|
+
prompt: string;
|
|
62
|
+
/** Parent working directory handed to a workspace provider's prepare(). */
|
|
63
|
+
baseCwd: string;
|
|
73
64
|
observer?: SubagentLifecycleObserver;
|
|
74
65
|
getRunConfig?: () => RunConfig;
|
|
75
66
|
/** Resolves the registered workspace provider (if any) at run-start. */
|
|
76
67
|
getWorkspaceProvider?: () => WorkspaceProvider | undefined;
|
|
77
|
-
/** Parent working directory handed to a workspace provider's prepare(). */
|
|
78
|
-
baseCwd?: string;
|
|
79
|
-
|
|
80
|
-
// Run config (required for run(), optional for tests)
|
|
81
|
-
snapshot?: ParentSnapshot;
|
|
82
|
-
prompt?: string;
|
|
83
68
|
model?: Model<any>;
|
|
84
69
|
maxTurns?: number;
|
|
85
70
|
thinkingLevel?: ThinkingLevel;
|
|
86
71
|
parentSession?: ParentSessionInfo;
|
|
87
|
-
isBackground?: boolean;
|
|
88
72
|
signal?: AbortSignal;
|
|
89
73
|
}
|
|
90
74
|
|
|
75
|
+
export interface SubagentInit {
|
|
76
|
+
// Identity
|
|
77
|
+
id: string;
|
|
78
|
+
type: SubagentType;
|
|
79
|
+
description: string;
|
|
80
|
+
invocation?: AgentInvocation;
|
|
81
|
+
|
|
82
|
+
/** Execution machinery — always supplied; construct-complete, no test fallbacks. */
|
|
83
|
+
execution: SubagentExecution;
|
|
84
|
+
|
|
85
|
+
/** Lifecycle status and metrics. Defaults to a fresh queued state. */
|
|
86
|
+
state?: SubagentState;
|
|
87
|
+
}
|
|
88
|
+
|
|
91
89
|
export class Subagent {
|
|
92
90
|
// Identity — set once at construction
|
|
93
91
|
readonly id: string;
|
|
@@ -95,59 +93,36 @@ export class Subagent {
|
|
|
95
93
|
readonly description: string;
|
|
96
94
|
readonly invocation?: AgentInvocation;
|
|
97
95
|
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
get
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
get
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
get startedAt(): number { return this._startedAt; }
|
|
110
|
-
|
|
111
|
-
private _completedAt?: number;
|
|
112
|
-
get completedAt(): number | undefined { return this._completedAt; }
|
|
113
|
-
|
|
114
|
-
// Stats — accumulated via mutation methods, readable via getters
|
|
115
|
-
private _toolUses: number;
|
|
116
|
-
get toolUses(): number { return this._toolUses; }
|
|
117
|
-
|
|
118
|
-
private _lifetimeUsage: LifetimeUsage;
|
|
119
|
-
get lifetimeUsage(): Readonly<LifetimeUsage> { return this._lifetimeUsage; }
|
|
120
|
-
|
|
121
|
-
private _compactionCount: number;
|
|
122
|
-
get compactionCount(): number { return this._compactionCount; }
|
|
96
|
+
// Lifecycle status and metrics — owned by a private value object; getters and
|
|
97
|
+
// mutation methods below delegate to it one line.
|
|
98
|
+
private readonly state: SubagentState;
|
|
99
|
+
get status(): SubagentStatus { return this.state.status; }
|
|
100
|
+
get result(): string | undefined { return this.state.result; }
|
|
101
|
+
get error(): string | undefined { return this.state.error; }
|
|
102
|
+
get startedAt(): number { return this.state.startedAt; }
|
|
103
|
+
get completedAt(): number | undefined { return this.state.completedAt; }
|
|
104
|
+
get toolUses(): number { return this.state.toolUses; }
|
|
105
|
+
get lifetimeUsage(): Readonly<LifetimeUsage> { return this.state.lifetimeUsage; }
|
|
106
|
+
get compactionCount(): number { return this.state.compactionCount; }
|
|
123
107
|
|
|
124
108
|
/** AbortController for cancelling this agent. Created at construction. */
|
|
125
109
|
readonly abortController: AbortController;
|
|
126
|
-
/**
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
private readonly
|
|
133
|
-
private readonly _getWorkspaceProvider?: () => WorkspaceProvider | undefined;
|
|
134
|
-
private readonly _baseCwd: string;
|
|
110
|
+
/** Backing store for the run promise. Set by start(). */
|
|
111
|
+
private _promise?: Promise<void>;
|
|
112
|
+
/** Awaitable handle to the running promise. Set by start(). */
|
|
113
|
+
get promise(): Promise<void> | undefined { return this._promise; }
|
|
114
|
+
|
|
115
|
+
// Execution machinery — a single mandatory collaborator (no per-field fallbacks).
|
|
116
|
+
private readonly execution: SubagentExecution;
|
|
135
117
|
/** Workspace prepared at run-start by a provider — undefined when none is registered. */
|
|
136
118
|
private _workspace?: Workspace;
|
|
137
119
|
|
|
138
|
-
// Run config — optional (required for run())
|
|
139
|
-
private readonly _snapshot?: ParentSnapshot;
|
|
140
|
-
private readonly _prompt?: string;
|
|
141
|
-
private readonly _model?: Model<any>;
|
|
142
|
-
private readonly _maxTurns?: number;
|
|
143
|
-
private readonly _thinkingLevel?: ThinkingLevel;
|
|
144
|
-
private readonly _parentSession?: ParentSessionInfo;
|
|
145
|
-
private readonly _signal?: AbortSignal;
|
|
146
|
-
|
|
147
120
|
// Phase-specific collaborators — each born complete when their info becomes available
|
|
148
121
|
/** The born-complete child session — set when the factory returns inside run(). */
|
|
149
122
|
subagentSession?: SubagentSession;
|
|
150
|
-
|
|
123
|
+
private _notification?: NotificationState;
|
|
124
|
+
/** Notification state for background agents — wired from parentSession.toolCallId. */
|
|
125
|
+
get notification(): NotificationState | undefined { return this._notification; }
|
|
151
126
|
|
|
152
127
|
// Steer buffer — messages queued before the session is ready
|
|
153
128
|
private _pendingSteers: string[] = [];
|
|
@@ -207,40 +182,19 @@ export class Subagent {
|
|
|
207
182
|
this.description = init.description;
|
|
208
183
|
this.invocation = init.invocation;
|
|
209
184
|
|
|
210
|
-
//
|
|
211
|
-
this.
|
|
212
|
-
this._result = init.result;
|
|
213
|
-
this._error = init.error;
|
|
214
|
-
this._startedAt = init.startedAt ?? Date.now();
|
|
215
|
-
this._completedAt = init.completedAt;
|
|
216
|
-
|
|
217
|
-
// Stats
|
|
218
|
-
this._toolUses = 0;
|
|
219
|
-
this._lifetimeUsage = { input: 0, output: 0, cacheWrite: 0 };
|
|
220
|
-
this._compactionCount = 0;
|
|
185
|
+
// Lifecycle status and metrics — fresh queued state unless one is supplied
|
|
186
|
+
this.state = init.state ?? new SubagentState();
|
|
221
187
|
|
|
222
188
|
// Abort controller — always created, never injected
|
|
223
189
|
this.abortController = new AbortController();
|
|
224
190
|
|
|
225
|
-
//
|
|
226
|
-
this.
|
|
227
|
-
this.observer = init.observer;
|
|
228
|
-
this._getRunConfig = init.getRunConfig;
|
|
229
|
-
this._getWorkspaceProvider = init.getWorkspaceProvider;
|
|
230
|
-
this._baseCwd = init.baseCwd ?? "";
|
|
231
|
-
|
|
232
|
-
// Run config
|
|
233
|
-
this._snapshot = init.snapshot;
|
|
234
|
-
this._prompt = init.prompt;
|
|
235
|
-
this._model = init.model;
|
|
236
|
-
this._maxTurns = init.maxTurns;
|
|
237
|
-
this._thinkingLevel = init.thinkingLevel;
|
|
238
|
-
this._parentSession = init.parentSession;
|
|
239
|
-
this._signal = init.signal;
|
|
191
|
+
// Execution machinery — a single mandatory collaborator
|
|
192
|
+
this.execution = init.execution;
|
|
240
193
|
|
|
241
194
|
// Notification state — created from parentSession.toolCallId if present
|
|
242
|
-
|
|
243
|
-
|
|
195
|
+
const toolCallId = init.execution.parentSession?.toolCallId;
|
|
196
|
+
if (toolCallId) {
|
|
197
|
+
this._notification = new NotificationState(toolCallId);
|
|
244
198
|
}
|
|
245
199
|
}
|
|
246
200
|
|
|
@@ -249,31 +203,25 @@ export class Subagent {
|
|
|
249
203
|
* via the factory, observer wiring, the turn loop, workspace disposal, and
|
|
250
204
|
* status transitions.
|
|
251
205
|
*
|
|
252
|
-
*
|
|
253
|
-
* The returned promise always resolves (errors are
|
|
206
|
+
* Execution is supplied at construction (mandatory), so run() needs no
|
|
207
|
+
* "not configured" guards. The returned promise always resolves (errors are
|
|
208
|
+
* captured internally).
|
|
254
209
|
*/
|
|
255
210
|
async run(): Promise<void> {
|
|
256
|
-
if (!this._createSubagentSession) {
|
|
257
|
-
throw new Error("Subagent not configured for execution — missing session factory");
|
|
258
|
-
}
|
|
259
|
-
if (!this._snapshot || !this._prompt) {
|
|
260
|
-
throw new Error("Subagent not configured for execution — missing snapshot or prompt");
|
|
261
|
-
}
|
|
262
|
-
|
|
263
211
|
this.markRunning(Date.now());
|
|
264
|
-
this.observer?.onStarted?.(this);
|
|
265
|
-
this.wireSignal(this.
|
|
212
|
+
this.execution.observer?.onStarted?.(this);
|
|
213
|
+
this.wireSignal(this.execution.signal, () => this.abort());
|
|
266
214
|
|
|
267
215
|
let cwd: string | undefined;
|
|
268
216
|
try {
|
|
269
217
|
// A registered workspace provider supplies the child's cwd and owns its
|
|
270
218
|
// teardown; with no provider the child runs in the parent cwd.
|
|
271
|
-
const provider = this.
|
|
219
|
+
const provider = this.execution.getWorkspaceProvider?.();
|
|
272
220
|
if (provider) {
|
|
273
221
|
this._workspace = await provider.prepare({
|
|
274
222
|
agentId: this.id,
|
|
275
223
|
agentType: this.type,
|
|
276
|
-
baseCwd: this.
|
|
224
|
+
baseCwd: this.execution.baseCwd,
|
|
277
225
|
invocation: this.invocation,
|
|
278
226
|
});
|
|
279
227
|
cwd = this._workspace?.cwd;
|
|
@@ -281,18 +229,18 @@ export class Subagent {
|
|
|
281
229
|
} catch (err) {
|
|
282
230
|
this.markError(err);
|
|
283
231
|
this.releaseListeners();
|
|
284
|
-
this.observer?.onRunFinished?.(this);
|
|
232
|
+
this.execution.observer?.onRunFinished?.(this);
|
|
285
233
|
return;
|
|
286
234
|
}
|
|
287
235
|
|
|
288
236
|
try {
|
|
289
|
-
this.subagentSession = await this.
|
|
290
|
-
snapshot: this.
|
|
237
|
+
this.subagentSession = await this.execution.createSubagentSession({
|
|
238
|
+
snapshot: this.execution.snapshot,
|
|
291
239
|
type: this.type,
|
|
292
240
|
cwd,
|
|
293
|
-
parentSession: this.
|
|
294
|
-
model: this.
|
|
295
|
-
thinkingLevel: this.
|
|
241
|
+
parentSession: this.execution.parentSession,
|
|
242
|
+
model: this.execution.model,
|
|
243
|
+
thinkingLevel: this.execution.thinkingLevel,
|
|
296
244
|
});
|
|
297
245
|
} catch (err) {
|
|
298
246
|
// The factory disposed its own session on a post-creation failure.
|
|
@@ -301,15 +249,15 @@ export class Subagent {
|
|
|
301
249
|
}
|
|
302
250
|
|
|
303
251
|
this.flushPendingSteers();
|
|
304
|
-
this.attachObserver(subscribeSubagentObserver(this.subagentSession, this, {
|
|
305
|
-
onCompact: (
|
|
252
|
+
this.attachObserver(subscribeSubagentObserver(this.subagentSession, this.state, {
|
|
253
|
+
onCompact: (info) => this.execution.observer?.onCompacted?.(this, info),
|
|
306
254
|
}));
|
|
307
|
-
this.observer?.onSessionCreated?.(this);
|
|
255
|
+
this.execution.observer?.onSessionCreated?.(this);
|
|
308
256
|
|
|
309
|
-
const runConfig = this.
|
|
257
|
+
const runConfig = this.execution.getRunConfig?.();
|
|
310
258
|
try {
|
|
311
|
-
const result = await this.subagentSession.runTurnLoop(this.
|
|
312
|
-
maxTurns: this.
|
|
259
|
+
const result = await this.subagentSession.runTurnLoop(this.execution.prompt, {
|
|
260
|
+
maxTurns: this.execution.maxTurns,
|
|
313
261
|
defaultMaxTurns: runConfig?.defaultMaxTurns,
|
|
314
262
|
graceTurns: runConfig?.graceTurns,
|
|
315
263
|
signal: this.abortController.signal,
|
|
@@ -320,6 +268,22 @@ export class Subagent {
|
|
|
320
268
|
}
|
|
321
269
|
}
|
|
322
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Start execution: call run(), store the promise internally, and return it.
|
|
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.
|
|
277
|
+
*/
|
|
278
|
+
start(): Promise<void> {
|
|
279
|
+
if (this.status !== "queued" && this.status !== "running") {
|
|
280
|
+
this._promise = Promise.resolve();
|
|
281
|
+
return this._promise;
|
|
282
|
+
}
|
|
283
|
+
this._promise = this.run();
|
|
284
|
+
return this._promise;
|
|
285
|
+
}
|
|
286
|
+
|
|
323
287
|
/**
|
|
324
288
|
* Resume an existing session with a new prompt, managing the observer
|
|
325
289
|
* subscription lifecycle internally (same wiring as run()).
|
|
@@ -336,8 +300,8 @@ export class Subagent {
|
|
|
336
300
|
}
|
|
337
301
|
|
|
338
302
|
this.resetForResume(Date.now());
|
|
339
|
-
this.attachObserver(subscribeSubagentObserver(subagentSession, this, {
|
|
340
|
-
onCompact: (
|
|
303
|
+
this.attachObserver(subscribeSubagentObserver(subagentSession, this.state, {
|
|
304
|
+
onCompact: (info) => this.execution.observer?.onCompacted?.(this, info),
|
|
341
305
|
}));
|
|
342
306
|
|
|
343
307
|
try {
|
|
@@ -352,23 +316,22 @@ export class Subagent {
|
|
|
352
316
|
|
|
353
317
|
/** Increment tool use count. Called by record-observer on tool_execution_end. */
|
|
354
318
|
incrementToolUses(): void {
|
|
355
|
-
this.
|
|
319
|
+
this.state.incrementToolUses();
|
|
356
320
|
}
|
|
357
321
|
|
|
358
322
|
/** Accumulate a usage delta into lifetimeUsage. Called by record-observer on message_end. */
|
|
359
323
|
addUsage(delta: { input: number; output: number; cacheWrite: number }): void {
|
|
360
|
-
addUsage(
|
|
324
|
+
this.state.addUsage(delta);
|
|
361
325
|
}
|
|
362
326
|
|
|
363
327
|
/** Increment compaction count. Called by record-observer on compaction_end. */
|
|
364
328
|
incrementCompactions(): void {
|
|
365
|
-
this.
|
|
329
|
+
this.state.incrementCompactions();
|
|
366
330
|
}
|
|
367
331
|
|
|
368
332
|
/** Transition to running state. Sets status and startedAt. */
|
|
369
333
|
markRunning(startedAt: number): void {
|
|
370
|
-
this.
|
|
371
|
-
this._startedAt = startedAt;
|
|
334
|
+
this.state.markRunning(startedAt);
|
|
372
335
|
}
|
|
373
336
|
|
|
374
337
|
/**
|
|
@@ -376,11 +339,7 @@ export class Subagent {
|
|
|
376
339
|
* Always sets result and completedAt (??=). Only changes status if not stopped.
|
|
377
340
|
*/
|
|
378
341
|
markCompleted(result: string, completedAt?: number): void {
|
|
379
|
-
this.
|
|
380
|
-
this._completedAt ??= completedAt ?? Date.now();
|
|
381
|
-
if (this._status !== "stopped") {
|
|
382
|
-
this._status = "completed";
|
|
383
|
-
}
|
|
342
|
+
this.state.markCompleted(result, completedAt);
|
|
384
343
|
}
|
|
385
344
|
|
|
386
345
|
/**
|
|
@@ -388,11 +347,7 @@ export class Subagent {
|
|
|
388
347
|
* Always sets result and completedAt (??=). Only changes status if not stopped.
|
|
389
348
|
*/
|
|
390
349
|
markAborted(result: string, completedAt?: number): void {
|
|
391
|
-
this.
|
|
392
|
-
this._completedAt ??= completedAt ?? Date.now();
|
|
393
|
-
if (this._status !== "stopped") {
|
|
394
|
-
this._status = "aborted";
|
|
395
|
-
}
|
|
350
|
+
this.state.markAborted(result, completedAt);
|
|
396
351
|
}
|
|
397
352
|
|
|
398
353
|
/**
|
|
@@ -400,11 +355,7 @@ export class Subagent {
|
|
|
400
355
|
* Always sets result and completedAt (??=). Only changes status if not stopped.
|
|
401
356
|
*/
|
|
402
357
|
markSteered(result: string, completedAt?: number): void {
|
|
403
|
-
this.
|
|
404
|
-
this._completedAt ??= completedAt ?? Date.now();
|
|
405
|
-
if (this._status !== "stopped") {
|
|
406
|
-
this._status = "steered";
|
|
407
|
-
}
|
|
358
|
+
this.state.markSteered(result, completedAt);
|
|
408
359
|
}
|
|
409
360
|
|
|
410
361
|
/**
|
|
@@ -412,17 +363,12 @@ export class Subagent {
|
|
|
412
363
|
* Always sets error (formatted) and completedAt (??=). Only changes status if not stopped.
|
|
413
364
|
*/
|
|
414
365
|
markError(error: unknown, completedAt?: number): void {
|
|
415
|
-
this.
|
|
416
|
-
this._completedAt ??= completedAt ?? Date.now();
|
|
417
|
-
if (this._status !== "stopped") {
|
|
418
|
-
this._status = "error";
|
|
419
|
-
}
|
|
366
|
+
this.state.markError(error, completedAt);
|
|
420
367
|
}
|
|
421
368
|
|
|
422
369
|
/** Transition to stopped state. Always valid — no guard. */
|
|
423
370
|
markStopped(completedAt?: number): void {
|
|
424
|
-
this.
|
|
425
|
-
this._completedAt = completedAt ?? Date.now();
|
|
371
|
+
this.state.markStopped(completedAt);
|
|
426
372
|
}
|
|
427
373
|
|
|
428
374
|
/**
|
|
@@ -432,7 +378,7 @@ export class Subagent {
|
|
|
432
378
|
* then no-ops on the queued-status guard.
|
|
433
379
|
*/
|
|
434
380
|
abort(): boolean {
|
|
435
|
-
if (this.
|
|
381
|
+
if (this.status !== "running") return false;
|
|
436
382
|
this.abortController.abort();
|
|
437
383
|
this.markStopped();
|
|
438
384
|
return true;
|
|
@@ -459,11 +405,7 @@ export class Subagent {
|
|
|
459
405
|
|
|
460
406
|
/** Reset for resume: running status, new startedAt, clear completedAt/result/error/listeners. */
|
|
461
407
|
resetForResume(startedAt: number): void {
|
|
462
|
-
this.
|
|
463
|
-
this._startedAt = startedAt;
|
|
464
|
-
this._completedAt = undefined;
|
|
465
|
-
this._result = undefined;
|
|
466
|
-
this._error = undefined;
|
|
408
|
+
this.state.resetForResume(startedAt);
|
|
467
409
|
this.releaseListeners();
|
|
468
410
|
}
|
|
469
411
|
|
|
@@ -511,7 +453,7 @@ export class Subagent {
|
|
|
511
453
|
else if (result.steered) this.markSteered(finalResult);
|
|
512
454
|
else this.markCompleted(finalResult);
|
|
513
455
|
|
|
514
|
-
this.observer?.onRunFinished?.(this);
|
|
456
|
+
this.execution.observer?.onRunFinished?.(this);
|
|
515
457
|
}
|
|
516
458
|
|
|
517
459
|
/** Dispose the wrapped session, firing the `disposed` lifecycle event. */
|
|
@@ -528,6 +470,6 @@ export class Subagent {
|
|
|
528
470
|
if (this._workspace) this._workspace.dispose({ status: "error", description: this.description });
|
|
529
471
|
} catch (cleanupErr) { debugLog("workspace dispose on agent error", cleanupErr); }
|
|
530
472
|
|
|
531
|
-
this.observer?.onRunFinished?.(this);
|
|
473
|
+
this.execution.observer?.onRunFinished?.(this);
|
|
532
474
|
}
|
|
533
475
|
}
|
|
@@ -1,40 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* record-observer.ts — Subscribes to session events and
|
|
2
|
+
* record-observer.ts — Subscribes to session events and accumulates SubagentState stats.
|
|
3
3
|
*
|
|
4
4
|
* Replaces the scattered callback-wrapping logic in SubagentManager's startAgent()
|
|
5
|
-
* and resume() with a single direct subscription.
|
|
5
|
+
* and resume() with a single direct subscription. The observer targets the
|
|
6
|
+
* SubagentState value object directly, so it carries no dependency on Subagent;
|
|
7
|
+
* the caller forwards itself to its own lifecycle observer via onCompact.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
|
-
import type {
|
|
10
|
+
import type { SubagentState } from "#src/lifecycle/subagent-state";
|
|
9
11
|
import type { CompactionInfo, SubscribableSession } from "#src/types";
|
|
10
12
|
|
|
11
13
|
export interface SubagentObserverOptions {
|
|
12
|
-
onCompact?: (
|
|
14
|
+
onCompact?: (info: CompactionInfo) => void;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
|
-
* Subscribe to session events and accumulate stats on the subagent
|
|
18
|
+
* Subscribe to session events and accumulate stats on the subagent state.
|
|
17
19
|
*
|
|
18
20
|
* Handles:
|
|
19
|
-
* - `tool_execution_end` → `
|
|
20
|
-
* - `message_end` (assistant, with usage) → `
|
|
21
|
-
* - `compaction_end` (not aborted) → `
|
|
21
|
+
* - `tool_execution_end` → `state.incrementToolUses()`
|
|
22
|
+
* - `message_end` (assistant, with usage) → `state.addUsage(…)`
|
|
23
|
+
* - `compaction_end` (not aborted) → `state.incrementCompactions()`, call `onCompact`
|
|
22
24
|
*
|
|
23
25
|
* @returns An unsubscribe function.
|
|
24
26
|
*/
|
|
25
27
|
export function subscribeSubagentObserver(
|
|
26
28
|
session: SubscribableSession,
|
|
27
|
-
|
|
29
|
+
state: SubagentState,
|
|
28
30
|
options?: SubagentObserverOptions,
|
|
29
31
|
): () => void {
|
|
30
32
|
return session.subscribe((event) => {
|
|
31
33
|
if (event.type === "tool_execution_end") {
|
|
32
|
-
|
|
34
|
+
state.incrementToolUses();
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
36
38
|
const u = event.message.usage;
|
|
37
|
-
|
|
39
|
+
state.addUsage({
|
|
38
40
|
input: u.input,
|
|
39
41
|
output: u.output,
|
|
40
42
|
cacheWrite: u.cacheWrite,
|
|
@@ -42,8 +44,8 @@ export function subscribeSubagentObserver(
|
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
if (event.type === "compaction_end" && !event.aborted && event.result) {
|
|
45
|
-
|
|
46
|
-
options?.onCompact?.(
|
|
47
|
+
state.incrementCompactions();
|
|
48
|
+
options?.onCompact?.({
|
|
47
49
|
reason: event.reason,
|
|
48
50
|
tokensBefore: event.result.tokensBefore,
|
|
49
51
|
});
|