@nklisch/pi-enhanced 0.2.7 → 0.3.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 +16 -0
- package/node_modules/@nklisch/pi-clearance/native/clearance-core.linux-x64-gnu.node +0 -0
- package/node_modules/@nklisch/pi-clearance/native/clearance-core.win32-x64-msvc.node +0 -0
- package/node_modules/@nklisch/pi-plugins/README.md +4 -2
- package/node_modules/@nklisch/pi-plugins/dist/hooks.js +81 -8
- package/node_modules/@nklisch/pi-plugins/dist/hooks.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/mcp.js +32 -1
- package/node_modules/@nklisch/pi-plugins/dist/mcp.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.d.ts +10 -4
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.js +144 -10
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/runtime-discovery.js +40 -14
- package/node_modules/@nklisch/pi-plugins/dist/runtime-discovery.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/CHANGELOG.md +27 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/README.md +76 -47
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/dist/public.d.ts +171 -140
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/dist/settings.d.ts +2 -2
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/FORK-MAINTENANCE.md +8 -7
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/VISION.md +12 -10
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/architecture.md +118 -104
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/comparison-with-upstream.md +3 -3
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/decisions/0004-reconsider-ui-direction.md +5 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/package.json +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/config/custom-agents.ts +31 -4
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/config/default-agents.ts +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/config/invocation-config.ts +7 -4
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/index.ts +18 -9
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/layered-settings.ts +2 -2
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/child-lifecycle.ts +10 -63
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/concurrency-limiter.ts +122 -76
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/create-subagent-session.ts +3 -5
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/lifecycle-interceptor.ts +4 -3
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/run-listeners.ts +2 -23
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent-manager.ts +177 -252
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent-session.ts +90 -280
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent-state.ts +153 -239
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent.ts +569 -700
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/workspace.ts +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/observation/notification.ts +78 -122
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/observation/renderer.ts +15 -42
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/observation/subagent-events-observer.ts +44 -95
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/service/service-adapter.ts +157 -90
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/service/service.ts +60 -68
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/session/conversation.ts +3 -3
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/session/query-source.ts +28 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/session/query.ts +488 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/settings.ts +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/agent-tool.ts +171 -249
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/get-result-tool.ts +51 -94
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/helpers.ts +9 -6
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/list-tool.ts +46 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/parent-tool-registry.ts +17 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/query-session-tool.ts +412 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/result-renderer.ts +27 -111
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/resume-tool.ts +105 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/spawn-config.ts +26 -8
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/steer-tool.ts +27 -111
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/stop-tool.ts +53 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/types.ts +11 -5
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/agent-widget.ts +33 -27
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/display.ts +6 -3
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/session-navigation.ts +121 -10
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/session-navigator.ts +430 -112
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/subagents-settings.ts +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/widget-renderer.ts +8 -9
- package/node_modules/@nklisch/pi-plugins/package.json +2 -2
- package/package.json +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/background-spawner.ts +0 -81
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/foreground-runner.ts +0 -149
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/get-result-report.ts +0 -77
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The authoritative subagent record and its run leases.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* A record remains active until the current lease's settlement promise resolves.
|
|
5
|
+
* In particular, requesting a stop never fabricates terminal state while child
|
|
6
|
+
* code, teardown, or an admission callback can still mutate the workspace.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { randomUUID } from "node:crypto";
|
|
10
9
|
import type { Model } from "@earendil-works/pi-ai";
|
|
11
10
|
import type { AgentSessionEvent, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
12
11
|
import { debugLog, runDetached, runSafely } from "#src/debug";
|
|
12
|
+
import type { AdmissionHandle } from "#src/lifecycle/concurrency-limiter";
|
|
13
13
|
import type { CreateSubagentSessionParams } from "#src/lifecycle/create-subagent-session";
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
import type {
|
|
15
|
+
LifecycleInterceptorRegistry,
|
|
16
|
+
SubagentLifecycleExecutionPath,
|
|
17
|
+
SubagentTurnLifecycle,
|
|
18
18
|
} from "#src/lifecycle/lifecycle-interceptor";
|
|
19
19
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
20
20
|
import { RunListeners } from "#src/lifecycle/run-listeners";
|
|
21
21
|
import type { SubagentSession, TurnLoopResult } from "#src/lifecycle/subagent-session";
|
|
22
|
-
import { SubagentState, type SubagentStatus } from "#src/lifecycle/subagent-state";
|
|
22
|
+
import { SubagentState, type SubagentStatus, type SubagentStopReason, type SubagentTerminalReason } from "#src/lifecycle/subagent-state";
|
|
23
23
|
import type { LifetimeUsage } from "#src/lifecycle/usage";
|
|
24
24
|
import type { WorkspaceProvider } from "#src/lifecycle/workspace";
|
|
25
25
|
import { WorkspaceBracket } from "#src/lifecycle/workspace-bracket";
|
|
@@ -27,712 +27,581 @@ import { subscribeSubagentObserver } from "#src/observation/record-observer";
|
|
|
27
27
|
import type { RunConfig } from "#src/runtime";
|
|
28
28
|
import { formatModelLabel } from "#src/session/model-label";
|
|
29
29
|
import { resolveEffectiveThinkingLevel } from "#src/session/thinking-level";
|
|
30
|
-
import type { AgentInvocation, CompactionInfo, ParentSessionInfo, SessionMessage, SubagentType, ThinkingLevel } from "#src/types";
|
|
30
|
+
import type { AgentInvocation, CompactionInfo, ParentSessionInfo, SessionMessage, SubagentMode, SubagentType, ThinkingLevel } from "#src/types";
|
|
31
31
|
|
|
32
|
-
/** Per-subagent lifecycle observer — created by SubagentManager for each spawn. */
|
|
33
32
|
export interface SubagentLifecycleObserver {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
/** Fires once when a resumed turn reaches a terminal state. */
|
|
41
|
-
onResumedFinished?(agent: Subagent): void;
|
|
42
|
-
/** Fires on compaction events during the run. */
|
|
43
|
-
onCompacted?(agent: Subagent, info: CompactionInfo): void;
|
|
33
|
+
onStarted?(agent: Subagent): void;
|
|
34
|
+
onSessionCreated?(agent: Subagent): void;
|
|
35
|
+
onRunFinished?(agent: Subagent): void;
|
|
36
|
+
onResumedStarted?(agent: Subagent): void;
|
|
37
|
+
onResumedFinished?(agent: Subagent): void;
|
|
38
|
+
onCompacted?(agent: Subagent, info: CompactionInfo): void;
|
|
44
39
|
}
|
|
45
40
|
|
|
46
|
-
export type { SubagentStatus } from "#src/lifecycle/subagent-state";
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* The result of a steer attempt. `Subagent.steer` owns the non-running
|
|
50
|
-
* rejection rule and reports it here, so coordinators switch on the outcome
|
|
51
|
-
* instead of pre-checking status (tell by id, with outcomes).
|
|
52
|
-
*/
|
|
53
41
|
export type SteerOutcome =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
/** A second prompt was requested while this record already owned an execution. */
|
|
59
|
-
export class SubagentBusyError extends Error {
|
|
60
|
-
constructor(agentId: string) {
|
|
61
|
-
super(`Subagent "${agentId}" is still processing a turn. Wait for it to settle before resuming.`);
|
|
62
|
-
this.name = "SubagentBusyError";
|
|
63
|
-
}
|
|
64
|
-
}
|
|
42
|
+
| { kind: "delivered"; runId: number }
|
|
43
|
+
| { kind: "buffered"; runId: number }
|
|
44
|
+
| { kind: "rejected"; runId: number; status: SubagentStatus };
|
|
65
45
|
|
|
66
|
-
/**
|
|
67
|
-
* The execution machinery a Subagent needs to run. A single mandatory
|
|
68
|
-
* collaborator: production (SubagentManager.spawn) always supplies it, so run()
|
|
69
|
-
* needs no "not configured" guards. The genuinely-optional behavior knobs stay
|
|
70
|
-
* optional; the four inputs run() cannot proceed without are required.
|
|
71
|
-
*/
|
|
72
46
|
export interface SubagentExecution {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
lifecycleParentSession?: ParentSessionInfo;
|
|
91
|
-
signal?: AbortSignal;
|
|
92
|
-
/** Owned by the manager; only a callback bridge reaches the child session. */
|
|
93
|
-
lifecycleInterceptors?: LifecycleInterceptorRegistry;
|
|
94
|
-
/** Spawn-path facts remain stable when the same child session resumes. */
|
|
95
|
-
executionPath?: SubagentLifecycleExecutionPath;
|
|
47
|
+
createSubagentSession: (params: CreateSubagentSessionParams) => Promise<SubagentSession>;
|
|
48
|
+
snapshot: ParentSnapshot;
|
|
49
|
+
prompt: string;
|
|
50
|
+
baseCwd: string;
|
|
51
|
+
mode: SubagentMode;
|
|
52
|
+
timeoutSeconds?: number;
|
|
53
|
+
observer?: SubagentLifecycleObserver;
|
|
54
|
+
getRunConfig?: () => RunConfig;
|
|
55
|
+
getWorkspaceProvider?: () => WorkspaceProvider | undefined;
|
|
56
|
+
model?: Model<any>;
|
|
57
|
+
maxTurns?: number;
|
|
58
|
+
thinkingLevel?: ThinkingLevel;
|
|
59
|
+
parentSession?: ParentSessionInfo;
|
|
60
|
+
lifecycleParentSession?: ParentSessionInfo;
|
|
61
|
+
signal?: AbortSignal;
|
|
62
|
+
lifecycleInterceptors?: LifecycleInterceptorRegistry;
|
|
63
|
+
executionPath?: Omit<SubagentLifecycleExecutionPath, "phase" | "mode"> & { admission?: "immediate" | "queued" };
|
|
96
64
|
}
|
|
97
65
|
|
|
98
66
|
export interface SubagentInit {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
67
|
+
id: string;
|
|
68
|
+
type: SubagentType;
|
|
69
|
+
description: string;
|
|
70
|
+
invocation?: AgentInvocation;
|
|
71
|
+
execution: SubagentExecution;
|
|
72
|
+
state?: SubagentState;
|
|
73
|
+
}
|
|
104
74
|
|
|
105
|
-
|
|
106
|
-
|
|
75
|
+
interface RunLease {
|
|
76
|
+
readonly runId: number;
|
|
77
|
+
readonly phase: "initial" | "resume";
|
|
78
|
+
readonly mode: SubagentMode;
|
|
79
|
+
readonly prompt: string;
|
|
80
|
+
readonly timeoutSeconds?: number;
|
|
81
|
+
readonly executionController: AbortController;
|
|
82
|
+
readonly settlement: Promise<void>;
|
|
83
|
+
readonly resolveSettlement: () => void;
|
|
84
|
+
admission?: AdmissionHandle;
|
|
85
|
+
admitted: boolean;
|
|
86
|
+
settled: boolean;
|
|
87
|
+
admissionPath: "immediate" | "queued";
|
|
88
|
+
startedAt?: number;
|
|
89
|
+
runtimeTimer?: ReturnType<typeof setTimeout>;
|
|
90
|
+
parentSignalCleanup?: () => void;
|
|
91
|
+
stopRequest?: SubagentStopReason;
|
|
92
|
+
}
|
|
107
93
|
|
|
108
|
-
|
|
109
|
-
|
|
94
|
+
function deferred(): { promise: Promise<void>; resolve: () => void } {
|
|
95
|
+
let resolve!: () => void;
|
|
96
|
+
const promise = new Promise<void>((res) => { resolve = res; });
|
|
97
|
+
return { promise, resolve };
|
|
110
98
|
}
|
|
111
99
|
|
|
112
100
|
export class Subagent {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
/** Transition to stopped state. Always valid — no guard. */
|
|
606
|
-
markStopped(completedAt?: number): void {
|
|
607
|
-
this.state.markStopped(completedAt);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
markConsumed(at?: number): void {
|
|
611
|
-
this.state.markConsumed(at);
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
/** Stop a queued agent through the same terminal observer funnel as a run. */
|
|
615
|
-
stopQueued(): void {
|
|
616
|
-
this.state.stopQueued();
|
|
617
|
-
runSafely("subagent onRunFinished observer", () => this.execution.observer?.onRunFinished?.(this));
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/**
|
|
621
|
-
* Abort a running agent: fire AbortController and transition to stopped.
|
|
622
|
-
* Returns false if the agent is not running.
|
|
623
|
-
* A still-queued agent is stopped by SubagentManager; its scheduled thunk
|
|
624
|
-
* then no-ops on the queued-status guard.
|
|
625
|
-
*/
|
|
626
|
-
abort(): boolean {
|
|
627
|
-
if (this.status !== "running" && !this.resumeReserved) return false;
|
|
628
|
-
this.abortController.abort();
|
|
629
|
-
this.pendingResumeAbort?.abort();
|
|
630
|
-
this.markStopped();
|
|
631
|
-
return true;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
/**
|
|
635
|
-
* Buffer a steer message for delivery once the session is ready.
|
|
636
|
-
* Called internally from steer() before the session is ready.
|
|
637
|
-
*/
|
|
638
|
-
private queueSteer(message: string): void {
|
|
639
|
-
this._pendingSteers.push(message);
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
/**
|
|
643
|
-
* Flush all buffered steer messages to the session and clear the buffer.
|
|
644
|
-
* Called once the session is available (inside run()).
|
|
645
|
-
*/
|
|
646
|
-
private flushPendingSteers(): void {
|
|
647
|
-
for (const msg of this._pendingSteers) {
|
|
648
|
-
runDetached("subagent buffered steer", () => this.subagentSession?.steer(msg));
|
|
649
|
-
}
|
|
650
|
-
this._pendingSteers = [];
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
/** Reset for resume: running status, new startedAt, clear completedAt/result/error/listeners. */
|
|
654
|
-
resetForResume(startedAt: number, controller = new AbortController()): void {
|
|
655
|
-
this.abortController = controller;
|
|
656
|
-
this.state.resetForResume(startedAt);
|
|
657
|
-
this.listeners.release();
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
/** Complete a run: release listeners, dispose the workspace, status transition, notify observer. */
|
|
661
|
-
completeRun(result: TurnLoopResult): void {
|
|
662
|
-
this.listeners.release();
|
|
663
|
-
|
|
664
|
-
const finalStatus: SubagentStatus = result.failure
|
|
665
|
-
? "error"
|
|
666
|
-
: result.aborted
|
|
667
|
-
? "aborted"
|
|
668
|
-
: result.steered
|
|
669
|
-
? "steered"
|
|
670
|
-
: "completed";
|
|
671
|
-
let finalResult = result.responseText;
|
|
672
|
-
try {
|
|
673
|
-
finalResult += this.workspaceBracket.dispose({ status: finalStatus, description: this.description });
|
|
674
|
-
} catch (error) {
|
|
675
|
-
// Workspace teardown belongs to the run's terminal boundary. If it
|
|
676
|
-
// fails, report an error once rather than re-entering disposal from the
|
|
677
|
-
// outer catch path.
|
|
678
|
-
this.failRun(error);
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
if (result.failure) this.markFailed(result.failure, finalResult);
|
|
683
|
-
else if (result.aborted) this.markAborted(finalResult);
|
|
684
|
-
else if (result.steered) this.markSteered(finalResult);
|
|
685
|
-
else this.markCompleted(finalResult);
|
|
686
|
-
|
|
687
|
-
runSafely("subagent onRunFinished observer", () => this.execution.observer?.onRunFinished?.(this));
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
/** Dispose the wrapped session, firing the `disposed` lifecycle event. */
|
|
691
|
-
async disposeSession(): Promise<void> {
|
|
692
|
-
const session = this.subagentSession;
|
|
693
|
-
if (!session) return;
|
|
694
|
-
// Detach first: callers must not admit a resume while asynchronous
|
|
695
|
-
// extension shutdown is in progress.
|
|
696
|
-
this.subagentSession = undefined;
|
|
697
|
-
await session.dispose();
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
/** Release heavy session state while preserving the transcript pointer and record. */
|
|
701
|
-
async releaseSession(): Promise<void> {
|
|
702
|
-
const session = this.subagentSession;
|
|
703
|
-
if (!session) return;
|
|
704
|
-
this.releasedOutputFile = session.outputFile;
|
|
705
|
-
// The record becomes non-resumable atomically at release admission, not
|
|
706
|
-
// after extension shutdown finishes.
|
|
707
|
-
this.subagentSession = undefined;
|
|
708
|
-
this._sessionReleased = true;
|
|
709
|
-
await session.dispose();
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
/** Fail a run: mark error, release listeners, best-effort workspace dispose, notify observer. */
|
|
713
|
-
failRun(err: unknown): void {
|
|
714
|
-
this.markError(err);
|
|
715
|
-
this.listeners.release();
|
|
716
|
-
|
|
717
|
-
try {
|
|
718
|
-
this.workspaceBracket.dispose({ status: "error", description: this.description });
|
|
719
|
-
} catch (cleanupErr) { debugLog("workspace dispose on agent error", cleanupErr); }
|
|
720
|
-
|
|
721
|
-
runSafely("subagent onRunFinished observer", () => this.execution.observer?.onRunFinished?.(this));
|
|
722
|
-
}
|
|
101
|
+
readonly id: string;
|
|
102
|
+
readonly type: SubagentType;
|
|
103
|
+
readonly description: string;
|
|
104
|
+
readonly invocation?: AgentInvocation;
|
|
105
|
+
|
|
106
|
+
private readonly state: SubagentState;
|
|
107
|
+
private readonly execution: SubagentExecution;
|
|
108
|
+
private readonly listeners = new RunListeners();
|
|
109
|
+
private readonly workspaceBracket: WorkspaceBracket;
|
|
110
|
+
private currentLease: RunLease;
|
|
111
|
+
private nextRunId = 1;
|
|
112
|
+
private _modelLabel: string;
|
|
113
|
+
private _effectiveThinkingLevel: ThinkingLevel;
|
|
114
|
+
private _sessionReleased = false;
|
|
115
|
+
private releasedOutputFile?: string;
|
|
116
|
+
private pendingResumeAbort?: AbortController;
|
|
117
|
+
private pendingSteers: string[] = [];
|
|
118
|
+
private readonly recordUpdateListeners = new Set<() => void>();
|
|
119
|
+
private runtimeMs = 0;
|
|
120
|
+
|
|
121
|
+
subagentSession?: SubagentSession;
|
|
122
|
+
|
|
123
|
+
get status(): SubagentStatus { return this.state.status; }
|
|
124
|
+
get result(): string | undefined { return this.state.result; }
|
|
125
|
+
get error(): string | undefined { return this.state.error; }
|
|
126
|
+
get startedAt(): number { return this.state.startedAt; }
|
|
127
|
+
get completedAt(): number | undefined { return this.state.completedAt; }
|
|
128
|
+
get consumedAt(): number | undefined { return this.state.consumedAt; }
|
|
129
|
+
get consumed(): boolean { return this.state.consumed; }
|
|
130
|
+
get toolUses(): number { return this.state.toolUses; }
|
|
131
|
+
get lifetimeUsage(): Readonly<LifetimeUsage> { return this.state.lifetimeUsage; }
|
|
132
|
+
get compactionCount(): number { return this.state.compactionCount; }
|
|
133
|
+
get turnCount(): number { return this.state.turnCount; }
|
|
134
|
+
get activeTools(): ReadonlyMap<string, string> { return this.state.activeTools; }
|
|
135
|
+
get responseText(): string { return this.state.responseText; }
|
|
136
|
+
get maxTurns(): number | undefined { return this.execution.maxTurns; }
|
|
137
|
+
get modelLabel(): string { return this._modelLabel; }
|
|
138
|
+
get effectiveThinkingLevel(): ThinkingLevel { return this._effectiveThinkingLevel; }
|
|
139
|
+
get runId(): number { return this.currentLease.runId; }
|
|
140
|
+
get mode(): SubagentMode { return this.currentLease.mode; }
|
|
141
|
+
get stopRequested(): boolean { return this.currentLease.stopRequest !== undefined; }
|
|
142
|
+
get stopReason(): SubagentStopReason | undefined { return this.currentLease.stopRequest; }
|
|
143
|
+
get stateTerminalReason(): SubagentTerminalReason | undefined { return this.state.terminalReason; }
|
|
144
|
+
get activeRuntimeMs(): number {
|
|
145
|
+
if (this.currentLease.admitted && !this.currentLease.settled && this.currentLease.startedAt != null) {
|
|
146
|
+
return Date.now() - this.currentLease.startedAt;
|
|
147
|
+
}
|
|
148
|
+
return this.currentLease.settled ? this.runtimeMs : 0;
|
|
149
|
+
}
|
|
150
|
+
get sessionReleased(): boolean { return this._sessionReleased; }
|
|
151
|
+
get pendingSteerCount(): number { return this.pendingSteers.length; }
|
|
152
|
+
get outputFile(): string | undefined { return this.subagentSession?.outputFile ?? this.releasedOutputFile; }
|
|
153
|
+
get toolCallId(): string | undefined { return this.execution.parentSession?.toolCallId; }
|
|
154
|
+
get settlement(): Promise<void> { return this.currentLease.settlement; }
|
|
155
|
+
get isSettled(): boolean { return this.currentLease.settled; }
|
|
156
|
+
|
|
157
|
+
constructor(init: SubagentInit) {
|
|
158
|
+
this.id = init.id;
|
|
159
|
+
this.type = init.type;
|
|
160
|
+
this.description = init.description;
|
|
161
|
+
this.invocation = init.invocation;
|
|
162
|
+
this.state = init.state ?? new SubagentState();
|
|
163
|
+
this.execution = init.execution;
|
|
164
|
+
this._modelLabel = formatModelLabel(this.execution.model ?? this.execution.snapshot.model);
|
|
165
|
+
this._effectiveThinkingLevel = resolveEffectiveThinkingLevel(
|
|
166
|
+
this.execution.model ?? this.execution.snapshot.model,
|
|
167
|
+
this.execution.thinkingLevel,
|
|
168
|
+
this.execution.snapshot.thinkingLevel,
|
|
169
|
+
);
|
|
170
|
+
this.workspaceBracket = new WorkspaceBracket(this.execution.getWorkspaceProvider ?? (() => undefined));
|
|
171
|
+
this.currentLease = this.createLease(1, "initial", this.execution.mode, this.execution.prompt, this.execution.timeoutSeconds);
|
|
172
|
+
// A terminal state can be supplied when a record is reconstructed for a
|
|
173
|
+
// read-only projection (for example, a service or renderer test). Keep
|
|
174
|
+
// the lease invariant aligned with that state instead of claiming that a
|
|
175
|
+
// terminal record still has work in flight. Manager-created records start
|
|
176
|
+
// queued and take the normal admission path below.
|
|
177
|
+
if (this.state.status === "completed" || this.state.status === "stopped" || this.state.status === "error") {
|
|
178
|
+
this.currentLease.settled = true;
|
|
179
|
+
this.currentLease.resolveSettlement();
|
|
180
|
+
} else if (this.state.status === "running") {
|
|
181
|
+
this.currentLease.admitted = true;
|
|
182
|
+
this.currentLease.startedAt = this.state.startedAt;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
isSessionReady(): boolean { return this.subagentSession != null; }
|
|
187
|
+
isActive(): boolean { return !this.currentLease.settled; }
|
|
188
|
+
isRunning(): boolean { return this.currentLease.admitted && !this.currentLease.settled; }
|
|
189
|
+
|
|
190
|
+
/** Schedule this lease through the one shared FIFO limiter. */
|
|
191
|
+
scheduleVia(schedule: (task: () => Promise<void>) => AdmissionHandle): AdmissionHandle {
|
|
192
|
+
const lease = this.currentLease;
|
|
193
|
+
const handle = schedule(() => this.admitAndRun(lease));
|
|
194
|
+
lease.admission = handle;
|
|
195
|
+
if (lease.stopRequest && !handle.admitted) handle.cancel();
|
|
196
|
+
return handle;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Arm cancellation before admission; detached callers intentionally pass no signal. */
|
|
200
|
+
armParentSignal(signal: AbortSignal | undefined): void {
|
|
201
|
+
if (!signal) return;
|
|
202
|
+
const lease = this.currentLease;
|
|
203
|
+
const onAbort = (): void => { this.requestStop("parent_cancelled"); };
|
|
204
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
205
|
+
lease.parentSignalCleanup = () => signal.removeEventListener("abort", onAbort);
|
|
206
|
+
if (signal.aborted) onAbort();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async steer(message: string): Promise<SteerOutcome> {
|
|
210
|
+
const lease = this.currentLease;
|
|
211
|
+
if (!this.isRunning() || lease.stopRequest) return { kind: "rejected", runId: lease.runId, status: this.status };
|
|
212
|
+
if (!this.subagentSession) {
|
|
213
|
+
this.pendingSteers.push(message);
|
|
214
|
+
return { kind: "buffered", runId: lease.runId };
|
|
215
|
+
}
|
|
216
|
+
await this.subagentSession.steer(message);
|
|
217
|
+
return { kind: "delivered", runId: lease.runId };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
getConversation(): string | undefined { return this.subagentSession?.getConversation(); }
|
|
221
|
+
getContextPercent(): number | null { return this.subagentSession?.getContextPercent() ?? null; }
|
|
222
|
+
subscribeToUpdates(fn: (event: AgentSessionEvent) => void): (() => void) | undefined { return this.subagentSession?.subscribe(fn); }
|
|
223
|
+
/** Read-only lifecycle notification for adapters that bridge retention release. */
|
|
224
|
+
subscribeToRecordUpdates(fn: () => void): () => void {
|
|
225
|
+
this.recordUpdateListeners.add(fn);
|
|
226
|
+
return () => { this.recordUpdateListeners.delete(fn); };
|
|
227
|
+
}
|
|
228
|
+
get messages(): readonly unknown[] { return this.subagentSession?.messages ?? []; }
|
|
229
|
+
get agentMessages(): readonly SessionMessage[] { return this.subagentSession?.agentMessages ?? []; }
|
|
230
|
+
getToolDefinition(name: string): ToolDefinition | undefined { return this.subagentSession?.getToolDefinition(name); }
|
|
231
|
+
|
|
232
|
+
/** Request cooperative cancellation. The first reason owns this lease. */
|
|
233
|
+
requestStop(reason: SubagentStopReason): boolean {
|
|
234
|
+
const lease = this.currentLease;
|
|
235
|
+
if (lease.settled) return false;
|
|
236
|
+
lease.stopRequest ??= reason;
|
|
237
|
+
lease.executionController.abort(lease.stopRequest);
|
|
238
|
+
this.pendingResumeAbort?.abort(lease.stopRequest);
|
|
239
|
+
lease.admission?.cancel();
|
|
240
|
+
if (!lease.admitted) void this.finalizePendingStop(lease);
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Reserve a retained session for resume before any asynchronous wait. */
|
|
245
|
+
reserveResume(
|
|
246
|
+
prompt: string,
|
|
247
|
+
mode: SubagentMode,
|
|
248
|
+
timeoutSeconds: number | undefined,
|
|
249
|
+
schedule: (task: () => Promise<void>) => AdmissionHandle,
|
|
250
|
+
signal?: AbortSignal,
|
|
251
|
+
admissionPath: "immediate" | "queued" = "immediate",
|
|
252
|
+
): { accepted: true; runId: number } | { accepted: false; kind: "not_found" | "wrong_state"; status?: SubagentStatus } {
|
|
253
|
+
if (!this.subagentSession || this._sessionReleased) return { accepted: false, kind: "wrong_state", status: this.status };
|
|
254
|
+
if (this.isActive()) return { accepted: false, kind: "wrong_state", status: this.status };
|
|
255
|
+
|
|
256
|
+
const previous = this.currentLease;
|
|
257
|
+
const runId = ++this.nextRunId;
|
|
258
|
+
const lease = this.createLease(runId, "resume", mode, prompt, timeoutSeconds, admissionPath);
|
|
259
|
+
this.currentLease = lease;
|
|
260
|
+
this.state.resetForResume();
|
|
261
|
+
const resumeAbort = new AbortController();
|
|
262
|
+
this.pendingResumeAbort = resumeAbort;
|
|
263
|
+
// A detached resume is independent of the caller even while waiting for
|
|
264
|
+
// the prior turn to become idle. The private controller remains available
|
|
265
|
+
// for an explicit stop request.
|
|
266
|
+
const waitSignal = mode === "joined" && signal
|
|
267
|
+
? AbortSignal.any([resumeAbort.signal, signal])
|
|
268
|
+
: resumeAbort.signal;
|
|
269
|
+
if (mode === "joined") this.armParentSignal(signal);
|
|
270
|
+
|
|
271
|
+
void this.waitAndScheduleResume(lease, previous, waitSignal, schedule);
|
|
272
|
+
return { accepted: true, runId };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private async waitAndScheduleResume(
|
|
276
|
+
lease: RunLease,
|
|
277
|
+
previous: RunLease,
|
|
278
|
+
signal: AbortSignal,
|
|
279
|
+
schedule: (task: () => Promise<void>) => AdmissionHandle,
|
|
280
|
+
): Promise<void> {
|
|
281
|
+
try {
|
|
282
|
+
await previous.settlement;
|
|
283
|
+
await this.subagentSession?.waitUntilIdle(signal);
|
|
284
|
+
signal.throwIfAborted();
|
|
285
|
+
if (this.currentLease !== lease || lease.settled) return;
|
|
286
|
+
// Resume only enters the limiter after Pi's idle boundary. Queue time is
|
|
287
|
+
// therefore excluded from both the deadline and the active runtime.
|
|
288
|
+
lease.admission = schedule(() => this.admitAndRun(lease));
|
|
289
|
+
if (lease.stopRequest && !lease.admission.admitted) lease.admission.cancel();
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if (this.currentLease !== lease || lease.settled) return;
|
|
292
|
+
if (lease.stopRequest || signal.aborted) {
|
|
293
|
+
lease.stopRequest ??= "parent_cancelled";
|
|
294
|
+
await this.finalizePendingStop(lease);
|
|
295
|
+
} else {
|
|
296
|
+
await this.finalizeError(lease, error, "execution_failure");
|
|
297
|
+
}
|
|
298
|
+
} finally {
|
|
299
|
+
if (this.currentLease === lease) this.pendingResumeAbort = undefined;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private async admitAndRun(lease: RunLease): Promise<void> {
|
|
304
|
+
if (this.currentLease !== lease || lease.settled) return;
|
|
305
|
+
if (lease.stopRequest) {
|
|
306
|
+
await this.finalizePendingStop(lease);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
lease.admitted = true;
|
|
310
|
+
lease.startedAt = Date.now();
|
|
311
|
+
this.runtimeMs = 0;
|
|
312
|
+
this.state.markRunning(lease.startedAt);
|
|
313
|
+
if (lease.timeoutSeconds != null) {
|
|
314
|
+
lease.runtimeTimer = setTimeout(() => this.requestStop("runtime_timeout"), lease.timeoutSeconds * 1000);
|
|
315
|
+
}
|
|
316
|
+
if (lease.phase === "resume") {
|
|
317
|
+
runSafely("subagent onResumedStarted observer", () => this.execution.observer?.onResumedStarted?.(this));
|
|
318
|
+
} else {
|
|
319
|
+
runSafely("subagent onStarted observer", () => this.execution.observer?.onStarted?.(this));
|
|
320
|
+
}
|
|
321
|
+
await this.executeLease(lease);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private async executeLease(lease: RunLease): Promise<void> {
|
|
325
|
+
try {
|
|
326
|
+
if (lease.phase === "resume") {
|
|
327
|
+
await this.executeResume(lease);
|
|
328
|
+
} else {
|
|
329
|
+
await this.executeInitial(lease);
|
|
330
|
+
}
|
|
331
|
+
} catch (error) {
|
|
332
|
+
// A stop request aborts both provider work and lifecycle callbacks. Those
|
|
333
|
+
// callbacks reject through the same cooperative signal, so this is a
|
|
334
|
+
// normal stopped run rather than an execution failure. Keep genuine
|
|
335
|
+
// failures classified below when no stop owns the lease.
|
|
336
|
+
if (lease.stopRequest && isCooperativeAbort(error, lease.executionController.signal)) {
|
|
337
|
+
await this.finalizeResult(lease, { responseText: "" }, lease.phase === "initial");
|
|
338
|
+
} else {
|
|
339
|
+
await this.finalizeError(lease, error, "execution_failure");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private async executeInitial(lease: RunLease): Promise<void> {
|
|
345
|
+
let cwd: string | undefined;
|
|
346
|
+
if (this.workspaceBracket.hasProvider()) {
|
|
347
|
+
cwd = await this.workspaceBracket.prepare({
|
|
348
|
+
agentId: this.id,
|
|
349
|
+
agentType: this.type,
|
|
350
|
+
baseCwd: this.execution.baseCwd,
|
|
351
|
+
invocation: this.invocation,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
if (lease.stopRequest) {
|
|
355
|
+
// Admission has already happened, so finalize through the full run
|
|
356
|
+
// boundary. In particular, a stop arriving while workspace preparation
|
|
357
|
+
// is pending must dispose the prepared workspace exactly once rather
|
|
358
|
+
// than taking the queued-only fast path.
|
|
359
|
+
await this.finalizeResult(lease, { responseText: "" }, true);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const session = await this.execution.createSubagentSession({
|
|
364
|
+
snapshot: this.execution.snapshot,
|
|
365
|
+
type: this.type,
|
|
366
|
+
cwd,
|
|
367
|
+
parentSession: this.execution.parentSession,
|
|
368
|
+
model: this.execution.model,
|
|
369
|
+
thinkingLevel: this.execution.thinkingLevel,
|
|
370
|
+
});
|
|
371
|
+
this.subagentSession = session;
|
|
372
|
+
this._modelLabel = formatModelLabel(session.model ?? this.execution.model ?? this.execution.snapshot.model);
|
|
373
|
+
this._effectiveThinkingLevel = session.thinkingLevel ?? this._effectiveThinkingLevel;
|
|
374
|
+
|
|
375
|
+
if (lease.stopRequest) {
|
|
376
|
+
this.releasedOutputFile = session.outputFile;
|
|
377
|
+
this.subagentSession = undefined;
|
|
378
|
+
await session.dispose();
|
|
379
|
+
return this.finalizeResult(lease, { responseText: "" }, true);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
this.flushPendingSteers();
|
|
383
|
+
this.listeners.attachObserver(subscribeSubagentObserver(session, this.state, {
|
|
384
|
+
onCompact: (info) => runSafely("subagent onCompacted observer", () => this.execution.observer?.onCompacted?.(this, info)),
|
|
385
|
+
}));
|
|
386
|
+
runSafely("subagent onSessionCreated observer", () => this.execution.observer?.onSessionCreated?.(this));
|
|
387
|
+
|
|
388
|
+
const runConfig = this.execution.getRunConfig?.();
|
|
389
|
+
const lifecycle = this.createTurnLifecycle(lease);
|
|
390
|
+
const result = await session.runTurnLoop(lease.prompt, {
|
|
391
|
+
maxTurns: this.execution.maxTurns,
|
|
392
|
+
defaultMaxTurns: runConfig?.defaultMaxTurns,
|
|
393
|
+
graceTurns: runConfig?.graceTurns,
|
|
394
|
+
signal: lease.executionController.signal,
|
|
395
|
+
...(lifecycle ? { lifecycle } : {}),
|
|
396
|
+
});
|
|
397
|
+
await this.finalizeResult(lease, result, true);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private async executeResume(lease: RunLease): Promise<void> {
|
|
401
|
+
const session = this.subagentSession;
|
|
402
|
+
if (!session) return this.finalizeError(lease, new Error("Subagent session was released while resuming"), "execution_failure");
|
|
403
|
+
this.listeners.attachObserver(subscribeSubagentObserver(session, this.state, {
|
|
404
|
+
onCompact: (info) => runSafely("subagent onCompacted observer", () => this.execution.observer?.onCompacted?.(this, info)),
|
|
405
|
+
}));
|
|
406
|
+
const lifecycle = this.createTurnLifecycle(lease);
|
|
407
|
+
if (lifecycle) {
|
|
408
|
+
const result = await session.resumeLifecycleTurnLoop(lease.prompt, lease.executionController.signal, lifecycle);
|
|
409
|
+
await this.finalizeResult(lease, result, false);
|
|
410
|
+
} else {
|
|
411
|
+
const result = await session.resumeTurnLoop(lease.prompt, lease.executionController.signal);
|
|
412
|
+
await this.finalizeResult(lease, {
|
|
413
|
+
responseText: result.text,
|
|
414
|
+
...(result.failure ? { failure: result.failure } : {}),
|
|
415
|
+
...(result.terminalReason ? { terminalReason: result.terminalReason } : {}),
|
|
416
|
+
}, false);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private createTurnLifecycle(lease: RunLease): SubagentTurnLifecycle | undefined {
|
|
421
|
+
const registry = this.execution.lifecycleInterceptors;
|
|
422
|
+
const session = this.subagentSession;
|
|
423
|
+
if (!registry?.hasInterceptors() || !session) return undefined;
|
|
424
|
+
const basePath = this.execution.executionPath ?? { origin: "service" as const, admission: "immediate" as const };
|
|
425
|
+
const execution: SubagentLifecycleExecutionPath = {
|
|
426
|
+
phase: lease.phase,
|
|
427
|
+
origin: basePath.origin,
|
|
428
|
+
mode: lease.mode,
|
|
429
|
+
admission: lease.admissionPath,
|
|
430
|
+
};
|
|
431
|
+
const lifecycleParent = this.execution.lifecycleParentSession ?? this.execution.parentSession;
|
|
432
|
+
return registry.createTurnLifecycle({
|
|
433
|
+
identity: {
|
|
434
|
+
agentId: this.id,
|
|
435
|
+
sessionId: session.sessionId,
|
|
436
|
+
runId: lease.runId,
|
|
437
|
+
agentType: this.type,
|
|
438
|
+
...(lifecycleParent?.parentSessionId ? { parentSessionId: lifecycleParent.parentSessionId } : {}),
|
|
439
|
+
},
|
|
440
|
+
execution,
|
|
441
|
+
signal: lease.executionController.signal,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private async finalizeResult(lease: RunLease, result: TurnLoopResult, disposeWorkspace: boolean): Promise<void> {
|
|
446
|
+
if (this.currentLease !== lease || lease.settled) return;
|
|
447
|
+
this.listeners.release();
|
|
448
|
+
let finalResult = result.responseText;
|
|
449
|
+
if (disposeWorkspace) {
|
|
450
|
+
const terminalStatus: "completed" | "stopped" | "error" = result.failure
|
|
451
|
+
? "error"
|
|
452
|
+
: lease.stopRequest || result.terminalReason === "turn_limit_hard" || result.terminalReason === "lifecycle_abort"
|
|
453
|
+
? "stopped"
|
|
454
|
+
: "completed";
|
|
455
|
+
try {
|
|
456
|
+
finalResult += this.workspaceBracket.dispose({ status: terminalStatus, description: this.description });
|
|
457
|
+
} catch (error) {
|
|
458
|
+
return this.finalizeError(lease, error, "workspace_teardown_failure", finalResult);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (result.failure) {
|
|
463
|
+
await this.finalizeTerminal(lease, "error", "provider_failure", finalResult, result.failure);
|
|
464
|
+
} else if (lease.stopRequest) {
|
|
465
|
+
await this.finalizeTerminal(lease, "stopped", lease.stopRequest, finalResult);
|
|
466
|
+
} else if (result.terminalReason === "lifecycle_abort") {
|
|
467
|
+
await this.finalizeTerminal(lease, "stopped", "lifecycle_abort", finalResult);
|
|
468
|
+
} else if (result.terminalReason === "turn_limit_hard") {
|
|
469
|
+
await this.finalizeTerminal(lease, "stopped", "turn_limit_hard", finalResult);
|
|
470
|
+
} else {
|
|
471
|
+
await this.finalizeTerminal(lease, "completed", result.terminalReason ?? "completed", finalResult);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private async finalizeError(
|
|
476
|
+
lease: RunLease,
|
|
477
|
+
error: unknown,
|
|
478
|
+
reason: "provider_failure" | "execution_failure" | "workspace_teardown_failure",
|
|
479
|
+
partialResult?: string,
|
|
480
|
+
): Promise<void> {
|
|
481
|
+
if (this.currentLease !== lease || lease.settled) return;
|
|
482
|
+
this.listeners.release();
|
|
483
|
+
let finalResult = partialResult;
|
|
484
|
+
if (reason !== "workspace_teardown_failure") {
|
|
485
|
+
try {
|
|
486
|
+
finalResult = (finalResult ?? "") + this.workspaceBracket.dispose({ status: lease.stopRequest ? "stopped" : "error", description: this.description });
|
|
487
|
+
} catch (cleanupError) {
|
|
488
|
+
debugLog("workspace dispose on agent error", cleanupError);
|
|
489
|
+
reason = "workspace_teardown_failure";
|
|
490
|
+
error = cleanupError;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// An actual execution failure outranks a cancellation request. A normal
|
|
494
|
+
// cooperative abort resolves through finalizeResult and therefore still
|
|
495
|
+
// reports the lease's first stop reason; this branch is reserved for a
|
|
496
|
+
// genuine thrown failure while running or tearing down the child.
|
|
497
|
+
await this.finalizeTerminal(lease, "error", reason, finalResult, error);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private async finalizePendingStop(lease: RunLease): Promise<void> {
|
|
501
|
+
if (this.currentLease !== lease || lease.settled || lease.admitted) return;
|
|
502
|
+
await this.finalizeTerminal(lease, "stopped", lease.stopRequest ?? "explicit_stop");
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private async finalizeTerminal(
|
|
506
|
+
lease: RunLease,
|
|
507
|
+
status: SubagentStatus,
|
|
508
|
+
reason: SubagentTerminalReason,
|
|
509
|
+
result?: string,
|
|
510
|
+
error?: unknown,
|
|
511
|
+
): Promise<void> {
|
|
512
|
+
if (this.currentLease !== lease || lease.settled) return;
|
|
513
|
+
lease.settled = true;
|
|
514
|
+
lease.parentSignalCleanup?.();
|
|
515
|
+
lease.parentSignalCleanup = undefined;
|
|
516
|
+
if (lease.runtimeTimer) clearTimeout(lease.runtimeTimer);
|
|
517
|
+
this.runtimeMs = lease.admitted && lease.startedAt != null ? Math.max(0, Date.now() - lease.startedAt) : 0;
|
|
518
|
+
|
|
519
|
+
if (status === "completed") this.state.markCompleted(result ?? "", reason as "completed" | "turn_limit_graceful");
|
|
520
|
+
else if (status === "stopped") this.state.markStopped(result, reason as SubagentStopReason | "turn_limit_hard" | "lifecycle_abort");
|
|
521
|
+
else this.state.markError(error ?? result ?? "subagent execution failed", reason as "provider_failure" | "execution_failure" | "workspace_teardown_failure", result);
|
|
522
|
+
|
|
523
|
+
if (lease.phase === "resume") {
|
|
524
|
+
runSafely("subagent onResumedFinished observer", () => this.execution.observer?.onResumedFinished?.(this));
|
|
525
|
+
} else {
|
|
526
|
+
runSafely("subagent onRunFinished observer", () => this.execution.observer?.onRunFinished?.(this));
|
|
527
|
+
}
|
|
528
|
+
lease.resolveSettlement();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private createLease(
|
|
532
|
+
runId: number,
|
|
533
|
+
phase: "initial" | "resume",
|
|
534
|
+
mode: SubagentMode,
|
|
535
|
+
prompt: string,
|
|
536
|
+
timeoutSeconds?: number,
|
|
537
|
+
admissionPath: "immediate" | "queued" = "immediate",
|
|
538
|
+
): RunLease {
|
|
539
|
+
const d = deferred();
|
|
540
|
+
return {
|
|
541
|
+
runId,
|
|
542
|
+
phase,
|
|
543
|
+
mode,
|
|
544
|
+
prompt,
|
|
545
|
+
timeoutSeconds,
|
|
546
|
+
executionController: new AbortController(),
|
|
547
|
+
settlement: d.promise,
|
|
548
|
+
resolveSettlement: d.resolve,
|
|
549
|
+
admitted: false,
|
|
550
|
+
settled: false,
|
|
551
|
+
admissionPath,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private flushPendingSteers(): void {
|
|
556
|
+
for (const message of this.pendingSteers) runDetached("subagent buffered steer", () => this.subagentSession?.steer(message));
|
|
557
|
+
this.pendingSteers = [];
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
incrementToolUses(): void { this.state.incrementToolUses(); }
|
|
561
|
+
addUsage(delta: { input: number; output: number; cacheWrite: number }): void { this.state.addUsage(delta); }
|
|
562
|
+
incrementCompactions(): void { this.state.incrementCompactions(); }
|
|
563
|
+
markConsumed(at?: number): void { this.state.markConsumed(at); }
|
|
564
|
+
|
|
565
|
+
async waitForSettlement(signal?: AbortSignal): Promise<void> {
|
|
566
|
+
if (!signal) return this.currentLease.settlement;
|
|
567
|
+
await settleOrAbort(this.currentLease.settlement, signal);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async disposeSession(): Promise<void> {
|
|
571
|
+
const session = this.subagentSession;
|
|
572
|
+
if (!session) return;
|
|
573
|
+
this.subagentSession = undefined;
|
|
574
|
+
await session.dispose();
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async releaseSession(): Promise<void> {
|
|
578
|
+
const session = this.subagentSession;
|
|
579
|
+
if (!session) return;
|
|
580
|
+
this.releasedOutputFile = session.outputFile;
|
|
581
|
+
this.subagentSession = undefined;
|
|
582
|
+
this._sessionReleased = true;
|
|
583
|
+
for (const listener of this.recordUpdateListeners) runSafely("subagent release update", listener);
|
|
584
|
+
await session.dispose();
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function isCooperativeAbort(error: unknown, signal: AbortSignal): boolean {
|
|
589
|
+
if (!signal.aborted) return false;
|
|
590
|
+
if (error === signal.reason) return true;
|
|
591
|
+
return error instanceof Error && /abort|cancel/i.test(error.message);
|
|
723
592
|
}
|
|
724
593
|
|
|
725
|
-
/** Resolve when either the run settles or the caller's wait is interrupted. */
|
|
726
594
|
async function settleOrAbort(run: Promise<void>, signal: AbortSignal): Promise<void> {
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
595
|
+
if (signal.aborted) return;
|
|
596
|
+
await new Promise<void>((resolve) => {
|
|
597
|
+
let done = false;
|
|
598
|
+
const finish = (): void => {
|
|
599
|
+
if (done) return;
|
|
600
|
+
done = true;
|
|
601
|
+
signal.removeEventListener("abort", finish);
|
|
602
|
+
resolve();
|
|
603
|
+
};
|
|
604
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
605
|
+
void run.then(finish, finish);
|
|
606
|
+
});
|
|
738
607
|
}
|