@gotgenes/pi-subagents 16.0.0 → 16.1.1

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.
@@ -0,0 +1,156 @@
1
+ /**
2
+ * subagent-state.ts — SubagentState value object: lifecycle status and metrics.
3
+ *
4
+ * Owns the passive, readable state of a subagent — status, result, error,
5
+ * timestamps, and stats (toolUses, lifetimeUsage, compactionCount) — together
6
+ * with the transition methods (markRunning, markCompleted, …) and accumulation
7
+ * methods (incrementToolUses, addUsage, incrementCompactions) that mutate it.
8
+ *
9
+ * State is encapsulated behind getters; external code reads through them but
10
+ * mutates only via the transition/accumulation methods. The value object owns
11
+ * all of its own mutations — no field is written from outside.
12
+ *
13
+ * Subagent holds one of these privately and delegates its getters and mutation
14
+ * methods to it. Extracting it lets the lifecycle state machine and the
15
+ * session-event observer be unit-tested without constructing an executor.
16
+ */
17
+
18
+ import type { LifetimeUsage } from "#src/lifecycle/usage";
19
+ import { addUsage } from "#src/lifecycle/usage";
20
+
21
+ export type SubagentStatus =
22
+ | "queued"
23
+ | "running"
24
+ | "completed"
25
+ | "steered"
26
+ | "aborted"
27
+ | "stopped"
28
+ | "error";
29
+
30
+ export interface SubagentStateInit {
31
+ status?: SubagentStatus;
32
+ result?: string;
33
+ error?: string;
34
+ startedAt?: number;
35
+ completedAt?: number;
36
+ }
37
+
38
+ export class SubagentState {
39
+ // Transition state — encapsulated behind getters, mutated only via transition methods
40
+ private _status: SubagentStatus;
41
+ get status(): SubagentStatus { return this._status; }
42
+
43
+ private _result?: string;
44
+ get result(): string | undefined { return this._result; }
45
+
46
+ private _error?: string;
47
+ get error(): string | undefined { return this._error; }
48
+
49
+ private _startedAt: number;
50
+ get startedAt(): number { return this._startedAt; }
51
+
52
+ private _completedAt?: number;
53
+ get completedAt(): number | undefined { return this._completedAt; }
54
+
55
+ // Stats — accumulated via mutation methods, readable via getters
56
+ private _toolUses = 0;
57
+ get toolUses(): number { return this._toolUses; }
58
+
59
+ private _lifetimeUsage: LifetimeUsage = { input: 0, output: 0, cacheWrite: 0 };
60
+ get lifetimeUsage(): Readonly<LifetimeUsage> { return this._lifetimeUsage; }
61
+
62
+ private _compactionCount = 0;
63
+ get compactionCount(): number { return this._compactionCount; }
64
+
65
+ constructor(init: SubagentStateInit = {}) {
66
+ this._status = init.status ?? "queued";
67
+ this._result = init.result;
68
+ this._error = init.error;
69
+ this._startedAt = init.startedAt ?? Date.now();
70
+ this._completedAt = init.completedAt;
71
+ }
72
+
73
+ /** Increment tool use count. Called by record-observer on tool_execution_end. */
74
+ incrementToolUses(): void {
75
+ this._toolUses++;
76
+ }
77
+
78
+ /** Accumulate a usage delta into lifetimeUsage. Called by record-observer on message_end. */
79
+ addUsage(delta: { input: number; output: number; cacheWrite: number }): void {
80
+ addUsage(this._lifetimeUsage, delta);
81
+ }
82
+
83
+ /** Increment compaction count. Called by record-observer on compaction_end. */
84
+ incrementCompactions(): void {
85
+ this._compactionCount++;
86
+ }
87
+
88
+ /** Transition to running state. Sets status and startedAt. */
89
+ markRunning(startedAt: number): void {
90
+ this._status = "running";
91
+ this._startedAt = startedAt;
92
+ }
93
+
94
+ /**
95
+ * Transition to completed state.
96
+ * Always sets result and completedAt (??=). Only changes status if not stopped.
97
+ */
98
+ markCompleted(result: string, completedAt?: number): void {
99
+ this._result = result;
100
+ this._completedAt ??= completedAt ?? Date.now();
101
+ if (this._status !== "stopped") {
102
+ this._status = "completed";
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Transition to aborted state.
108
+ * Always sets result and completedAt (??=). Only changes status if not stopped.
109
+ */
110
+ markAborted(result: string, completedAt?: number): void {
111
+ this._result = result;
112
+ this._completedAt ??= completedAt ?? Date.now();
113
+ if (this._status !== "stopped") {
114
+ this._status = "aborted";
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Transition to steered state.
120
+ * Always sets result and completedAt (??=). Only changes status if not stopped.
121
+ */
122
+ markSteered(result: string, completedAt?: number): void {
123
+ this._result = result;
124
+ this._completedAt ??= completedAt ?? Date.now();
125
+ if (this._status !== "stopped") {
126
+ this._status = "steered";
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Transition to error state.
132
+ * Always sets error (formatted) and completedAt (??=). Only changes status if not stopped.
133
+ */
134
+ markError(error: unknown, completedAt?: number): void {
135
+ this._error = error instanceof Error ? error.message : String(error);
136
+ this._completedAt ??= completedAt ?? Date.now();
137
+ if (this._status !== "stopped") {
138
+ this._status = "error";
139
+ }
140
+ }
141
+
142
+ /** Transition to stopped state. Always valid — no guard. */
143
+ markStopped(completedAt?: number): void {
144
+ this._status = "stopped";
145
+ this._completedAt = completedAt ?? Date.now();
146
+ }
147
+
148
+ /** Reset for resume: running status, new startedAt, clear completedAt/result/error. */
149
+ resetForResume(startedAt: number): void {
150
+ this._status = "running";
151
+ this._startedAt = startedAt;
152
+ this._completedAt = undefined;
153
+ this._result = undefined;
154
+ this._error = undefined;
155
+ }
156
+ }
@@ -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
- export interface SubagentInit {
57
- // Identity
58
- id: string;
59
- type: SubagentType;
60
- description: string;
61
- invocation?: AgentInvocation;
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?: (params: CreateSubagentSessionParams) => Promise<SubagentSession>;
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,55 +93,28 @@ export class Subagent {
95
93
  readonly description: string;
96
94
  readonly invocation?: AgentInvocation;
97
95
 
98
- // Transition stateencapsulated behind getters, mutated only via transition methods
99
- private _status: SubagentStatus;
100
- get status(): SubagentStatus { return this._status; }
101
-
102
- private _result?: string;
103
- get result(): string | undefined { return this._result; }
104
-
105
- private _error?: string;
106
- get error(): string | undefined { return this._error; }
107
-
108
- private _startedAt: number;
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
110
  /** Promise for the full agent run (including post-processing). Set by run(). */
127
111
  promise?: Promise<void>;
128
112
 
129
- // Shared depsoptional (required for run())
130
- private readonly _createSubagentSession?: (params: CreateSubagentSessionParams) => Promise<SubagentSession>;
131
- readonly observer?: SubagentLifecycleObserver;
132
- private readonly _getRunConfig?: () => RunConfig;
133
- private readonly _getWorkspaceProvider?: () => WorkspaceProvider | undefined;
134
- private readonly _baseCwd: string;
113
+ // Execution machinerya single mandatory collaborator (no per-field fallbacks).
114
+ private readonly execution: SubagentExecution;
135
115
  /** Workspace prepared at run-start by a provider — undefined when none is registered. */
136
116
  private _workspace?: Workspace;
137
117
 
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
118
  // Phase-specific collaborators — each born complete when their info becomes available
148
119
  /** The born-complete child session — set when the factory returns inside run(). */
149
120
  subagentSession?: SubagentSession;
@@ -207,40 +178,19 @@ export class Subagent {
207
178
  this.description = init.description;
208
179
  this.invocation = init.invocation;
209
180
 
210
- // Status
211
- this._status = init.status ?? "queued";
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;
181
+ // Lifecycle status and metrics — fresh queued state unless one is supplied
182
+ this.state = init.state ?? new SubagentState();
221
183
 
222
184
  // Abort controller — always created, never injected
223
185
  this.abortController = new AbortController();
224
186
 
225
- // Shared deps
226
- this._createSubagentSession = init.createSubagentSession;
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;
187
+ // Execution machinery — a single mandatory collaborator
188
+ this.execution = init.execution;
240
189
 
241
190
  // Notification state — created from parentSession.toolCallId if present
242
- if (init.parentSession?.toolCallId) {
243
- this.notification = new NotificationState(init.parentSession.toolCallId);
191
+ const toolCallId = init.execution.parentSession?.toolCallId;
192
+ if (toolCallId) {
193
+ this.notification = new NotificationState(toolCallId);
244
194
  }
245
195
  }
246
196
 
@@ -249,31 +199,25 @@ export class Subagent {
249
199
  * via the factory, observer wiring, the turn loop, workspace disposal, and
250
200
  * status transitions.
251
201
  *
252
- * Requires the session factory and snapshot to be set at construction.
253
- * The returned promise always resolves (errors are captured internally).
202
+ * Execution is supplied at construction (mandatory), so run() needs no
203
+ * "not configured" guards. The returned promise always resolves (errors are
204
+ * captured internally).
254
205
  */
255
206
  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
207
  this.markRunning(Date.now());
264
- this.observer?.onStarted?.(this);
265
- this.wireSignal(this._signal, () => this.abort());
208
+ this.execution.observer?.onStarted?.(this);
209
+ this.wireSignal(this.execution.signal, () => this.abort());
266
210
 
267
211
  let cwd: string | undefined;
268
212
  try {
269
213
  // A registered workspace provider supplies the child's cwd and owns its
270
214
  // teardown; with no provider the child runs in the parent cwd.
271
- const provider = this._getWorkspaceProvider?.();
215
+ const provider = this.execution.getWorkspaceProvider?.();
272
216
  if (provider) {
273
217
  this._workspace = await provider.prepare({
274
218
  agentId: this.id,
275
219
  agentType: this.type,
276
- baseCwd: this._baseCwd,
220
+ baseCwd: this.execution.baseCwd,
277
221
  invocation: this.invocation,
278
222
  });
279
223
  cwd = this._workspace?.cwd;
@@ -281,18 +225,18 @@ export class Subagent {
281
225
  } catch (err) {
282
226
  this.markError(err);
283
227
  this.releaseListeners();
284
- this.observer?.onRunFinished?.(this);
228
+ this.execution.observer?.onRunFinished?.(this);
285
229
  return;
286
230
  }
287
231
 
288
232
  try {
289
- this.subagentSession = await this._createSubagentSession({
290
- snapshot: this._snapshot,
233
+ this.subagentSession = await this.execution.createSubagentSession({
234
+ snapshot: this.execution.snapshot,
291
235
  type: this.type,
292
236
  cwd,
293
- parentSession: this._parentSession,
294
- model: this._model,
295
- thinkingLevel: this._thinkingLevel,
237
+ parentSession: this.execution.parentSession,
238
+ model: this.execution.model,
239
+ thinkingLevel: this.execution.thinkingLevel,
296
240
  });
297
241
  } catch (err) {
298
242
  // The factory disposed its own session on a post-creation failure.
@@ -301,15 +245,15 @@ export class Subagent {
301
245
  }
302
246
 
303
247
  this.flushPendingSteers();
304
- this.attachObserver(subscribeSubagentObserver(this.subagentSession, this, {
305
- onCompact: (r, info) => this.observer?.onCompacted?.(r, info),
248
+ this.attachObserver(subscribeSubagentObserver(this.subagentSession, this.state, {
249
+ onCompact: (info) => this.execution.observer?.onCompacted?.(this, info),
306
250
  }));
307
- this.observer?.onSessionCreated?.(this);
251
+ this.execution.observer?.onSessionCreated?.(this);
308
252
 
309
- const runConfig = this._getRunConfig?.();
253
+ const runConfig = this.execution.getRunConfig?.();
310
254
  try {
311
- const result = await this.subagentSession.runTurnLoop(this._prompt, {
312
- maxTurns: this._maxTurns,
255
+ const result = await this.subagentSession.runTurnLoop(this.execution.prompt, {
256
+ maxTurns: this.execution.maxTurns,
313
257
  defaultMaxTurns: runConfig?.defaultMaxTurns,
314
258
  graceTurns: runConfig?.graceTurns,
315
259
  signal: this.abortController.signal,
@@ -336,8 +280,8 @@ export class Subagent {
336
280
  }
337
281
 
338
282
  this.resetForResume(Date.now());
339
- this.attachObserver(subscribeSubagentObserver(subagentSession, this, {
340
- onCompact: (r, info) => this.observer?.onCompacted?.(r, info),
283
+ this.attachObserver(subscribeSubagentObserver(subagentSession, this.state, {
284
+ onCompact: (info) => this.execution.observer?.onCompacted?.(this, info),
341
285
  }));
342
286
 
343
287
  try {
@@ -352,23 +296,22 @@ export class Subagent {
352
296
 
353
297
  /** Increment tool use count. Called by record-observer on tool_execution_end. */
354
298
  incrementToolUses(): void {
355
- this._toolUses++;
299
+ this.state.incrementToolUses();
356
300
  }
357
301
 
358
302
  /** Accumulate a usage delta into lifetimeUsage. Called by record-observer on message_end. */
359
303
  addUsage(delta: { input: number; output: number; cacheWrite: number }): void {
360
- addUsage(this._lifetimeUsage, delta);
304
+ this.state.addUsage(delta);
361
305
  }
362
306
 
363
307
  /** Increment compaction count. Called by record-observer on compaction_end. */
364
308
  incrementCompactions(): void {
365
- this._compactionCount++;
309
+ this.state.incrementCompactions();
366
310
  }
367
311
 
368
312
  /** Transition to running state. Sets status and startedAt. */
369
313
  markRunning(startedAt: number): void {
370
- this._status = "running";
371
- this._startedAt = startedAt;
314
+ this.state.markRunning(startedAt);
372
315
  }
373
316
 
374
317
  /**
@@ -376,11 +319,7 @@ export class Subagent {
376
319
  * Always sets result and completedAt (??=). Only changes status if not stopped.
377
320
  */
378
321
  markCompleted(result: string, completedAt?: number): void {
379
- this._result = result;
380
- this._completedAt ??= completedAt ?? Date.now();
381
- if (this._status !== "stopped") {
382
- this._status = "completed";
383
- }
322
+ this.state.markCompleted(result, completedAt);
384
323
  }
385
324
 
386
325
  /**
@@ -388,11 +327,7 @@ export class Subagent {
388
327
  * Always sets result and completedAt (??=). Only changes status if not stopped.
389
328
  */
390
329
  markAborted(result: string, completedAt?: number): void {
391
- this._result = result;
392
- this._completedAt ??= completedAt ?? Date.now();
393
- if (this._status !== "stopped") {
394
- this._status = "aborted";
395
- }
330
+ this.state.markAborted(result, completedAt);
396
331
  }
397
332
 
398
333
  /**
@@ -400,11 +335,7 @@ export class Subagent {
400
335
  * Always sets result and completedAt (??=). Only changes status if not stopped.
401
336
  */
402
337
  markSteered(result: string, completedAt?: number): void {
403
- this._result = result;
404
- this._completedAt ??= completedAt ?? Date.now();
405
- if (this._status !== "stopped") {
406
- this._status = "steered";
407
- }
338
+ this.state.markSteered(result, completedAt);
408
339
  }
409
340
 
410
341
  /**
@@ -412,26 +343,22 @@ export class Subagent {
412
343
  * Always sets error (formatted) and completedAt (??=). Only changes status if not stopped.
413
344
  */
414
345
  markError(error: unknown, completedAt?: number): void {
415
- this._error = error instanceof Error ? error.message : String(error);
416
- this._completedAt ??= completedAt ?? Date.now();
417
- if (this._status !== "stopped") {
418
- this._status = "error";
419
- }
346
+ this.state.markError(error, completedAt);
420
347
  }
421
348
 
422
349
  /** Transition to stopped state. Always valid — no guard. */
423
350
  markStopped(completedAt?: number): void {
424
- this._status = "stopped";
425
- this._completedAt = completedAt ?? Date.now();
351
+ this.state.markStopped(completedAt);
426
352
  }
427
353
 
428
354
  /**
429
355
  * Abort a running agent: fire AbortController and transition to stopped.
430
356
  * Returns false if the agent is not running.
431
- * Queue removal is handled by SubagentManager via ConcurrencyQueue.dequeue().
357
+ * A still-queued agent is stopped by SubagentManager; its scheduled thunk
358
+ * then no-ops on the queued-status guard.
432
359
  */
433
360
  abort(): boolean {
434
- if (this._status !== "running") return false;
361
+ if (this.status !== "running") return false;
435
362
  this.abortController.abort();
436
363
  this.markStopped();
437
364
  return true;
@@ -458,11 +385,7 @@ export class Subagent {
458
385
 
459
386
  /** Reset for resume: running status, new startedAt, clear completedAt/result/error/listeners. */
460
387
  resetForResume(startedAt: number): void {
461
- this._status = "running";
462
- this._startedAt = startedAt;
463
- this._completedAt = undefined;
464
- this._result = undefined;
465
- this._error = undefined;
388
+ this.state.resetForResume(startedAt);
466
389
  this.releaseListeners();
467
390
  }
468
391
 
@@ -510,7 +433,7 @@ export class Subagent {
510
433
  else if (result.steered) this.markSteered(finalResult);
511
434
  else this.markCompleted(finalResult);
512
435
 
513
- this.observer?.onRunFinished?.(this);
436
+ this.execution.observer?.onRunFinished?.(this);
514
437
  }
515
438
 
516
439
  /** Dispose the wrapped session, firing the `disposed` lifecycle event. */
@@ -527,6 +450,6 @@ export class Subagent {
527
450
  if (this._workspace) this._workspace.dispose({ status: "error", description: this.description });
528
451
  } catch (cleanupErr) { debugLog("workspace dispose on agent error", cleanupErr); }
529
452
 
530
- this.observer?.onRunFinished?.(this);
453
+ this.execution.observer?.onRunFinished?.(this);
531
454
  }
532
455
  }
@@ -1,40 +1,42 @@
1
1
  /**
2
- * record-observer.ts — Subscribes to session events and updates Subagent stats.
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 { Subagent } from "#src/lifecycle/subagent";
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?: (record: Subagent, info: CompactionInfo) => void;
14
+ onCompact?: (info: CompactionInfo) => void;
13
15
  }
14
16
 
15
17
  /**
16
- * Subscribe to session events and accumulate stats on the subagent record.
18
+ * Subscribe to session events and accumulate stats on the subagent state.
17
19
  *
18
20
  * Handles:
19
- * - `tool_execution_end` → `record.incrementToolUses()`
20
- * - `message_end` (assistant, with usage) → `record.addUsage(…)`
21
- * - `compaction_end` (not aborted) → `record.incrementCompactions()`, call `onCompact`
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
- record: Subagent,
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
- record.incrementToolUses();
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
- record.addUsage({
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
- record.incrementCompactions();
46
- options?.onCompact?.(record, {
47
+ state.incrementCompactions();
48
+ options?.onCompact?.({
47
49
  reason: event.reason,
48
50
  tokensBefore: event.result.tokensBefore,
49
51
  });