@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.
- package/CHANGELOG.md +14 -0
- package/dist/public.d.ts +19 -22
- package/docs/architecture/architecture.md +49 -17
- package/docs/plans/0373-extract-subagent-state.md +250 -0
- package/docs/plans/0381-replace-concurrency-queue-with-limiter.md +267 -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/0381-replace-concurrency-queue-with-limiter.md +95 -0
- package/docs/retro/0400-include-parent-prompt-in-replace-mode.md +40 -0
- package/docs/retro/0403-abort-subagents-on-interrupt.md +49 -0
- package/package.json +1 -1
- package/src/handlers/index.ts +1 -0
- package/src/handlers/interrupt.ts +49 -0
- package/src/index.ts +13 -16
- package/src/lifecycle/concurrency-limiter.ts +55 -0
- package/src/lifecycle/subagent-manager.ts +57 -51
- package/src/lifecycle/subagent-state.ts +156 -0
- package/src/lifecycle/subagent.ts +86 -163
- package/src/observation/record-observer.ts +15 -13
- package/src/lifecycle/concurrency-queue.ts +0 -63
|
@@ -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
|
-
|
|
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,55 +93,28 @@ 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
110
|
/** Promise for the full agent run (including post-processing). Set by run(). */
|
|
127
111
|
promise?: Promise<void>;
|
|
128
112
|
|
|
129
|
-
//
|
|
130
|
-
private readonly
|
|
131
|
-
readonly observer?: SubagentLifecycleObserver;
|
|
132
|
-
private readonly _getRunConfig?: () => RunConfig;
|
|
133
|
-
private readonly _getWorkspaceProvider?: () => WorkspaceProvider | undefined;
|
|
134
|
-
private readonly _baseCwd: string;
|
|
113
|
+
// Execution machinery — a 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
|
-
//
|
|
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;
|
|
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
|
-
//
|
|
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;
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
*
|
|
253
|
-
* The returned promise always resolves (errors are
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
290
|
-
snapshot: this.
|
|
233
|
+
this.subagentSession = await this.execution.createSubagentSession({
|
|
234
|
+
snapshot: this.execution.snapshot,
|
|
291
235
|
type: this.type,
|
|
292
236
|
cwd,
|
|
293
|
-
parentSession: this.
|
|
294
|
-
model: this.
|
|
295
|
-
thinkingLevel: this.
|
|
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: (
|
|
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.
|
|
253
|
+
const runConfig = this.execution.getRunConfig?.();
|
|
310
254
|
try {
|
|
311
|
-
const result = await this.subagentSession.runTurnLoop(this.
|
|
312
|
-
maxTurns: this.
|
|
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: (
|
|
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.
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
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.
|
|
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.
|
|
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
|
|
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
|
});
|