@shanepadgett/tau-agent 0.42.3 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +0 -1
  2. package/docs/context.md +1 -1
  3. package/docs/extending-tau-agent.md +1 -2
  4. package/extensions/attention/README.md +1 -1
  5. package/extensions/cache-diagnostics/index.ts +4 -0
  6. package/extensions/context/README.md +1 -24
  7. package/extensions/context/definitions.ts +1 -52
  8. package/extensions/context/index.ts +1 -150
  9. package/extensions/context/panel.ts +0 -72
  10. package/extensions/explore/guidance.ts +9 -1
  11. package/extensions/footer/README.md +1 -1
  12. package/extensions/footer/index.ts +3 -25
  13. package/extensions/qna/choice-question-body.ts +7 -2
  14. package/extensions/run-summary/README.md +1 -1
  15. package/extensions/run-summary/index.ts +18 -25
  16. package/extensions/runtime-context/README.md +1 -1
  17. package/extensions/runtime-context/context.ts +1 -1
  18. package/extensions/runtime-context/index.ts +10 -16
  19. package/extensions/silent-command-runner/README.md +0 -2
  20. package/extensions/silent-command-runner/index.ts +12 -41
  21. package/extensions/soul/README.md +5 -8
  22. package/extensions/soul/context.ts +115 -0
  23. package/extensions/soul/index.ts +141 -7
  24. package/extensions/soul/prompt.ts +47 -102
  25. package/extensions/soul/state.ts +114 -0
  26. package/extensions/soul/tools.ts +31 -0
  27. package/extensions/tau-help/help.md +6 -18
  28. package/extensions/tau-help/index.ts +10 -4
  29. package/extensions/tool-approval/README.md +2 -0
  30. package/extensions/tool-approval/index.ts +65 -44
  31. package/extensions/tool-approval/panel.ts +153 -0
  32. package/extensions/tool-loader/README.md +1 -1
  33. package/extensions/tool-loader/index.ts +91 -22
  34. package/package.json +2 -2
  35. package/schemas/tau.schema.json +0 -104
  36. package/shared/bounded-text-result.ts +0 -1
  37. package/shared/events.ts +19 -15
  38. package/shared/isolated-session.ts +1 -2
  39. package/shared/model-effort.ts +15 -21
  40. package/shared/prompt-contributions.ts +24 -0
  41. package/src/index.ts +1 -1
  42. package/docs/subagents.md +0 -92
  43. package/extensions/auto-compact/README.md +0 -9
  44. package/extensions/auto-compact/index.ts +0 -122
  45. package/extensions/auto-compact/settings.ts +0 -30
  46. package/extensions/context/settings.ts +0 -62
  47. package/extensions/context/sync.ts +0 -276
  48. package/extensions/context/validation.ts +0 -88
  49. package/extensions/effort/README.md +0 -7
  50. package/extensions/effort/index.ts +0 -134
  51. package/extensions/effort/state.ts +0 -18
  52. package/extensions/qna/inline-editor-row.ts +0 -56
  53. package/extensions/soul/overseer.ts +0 -273
  54. package/extensions/soul/settings.ts +0 -34
  55. package/extensions/subagent/README.md +0 -67
  56. package/extensions/subagent/agents/context-sync.md +0 -244
  57. package/extensions/subagent/agents/dormant/generalist.md +0 -33
  58. package/extensions/subagent/agents/scout.md +0 -104
  59. package/extensions/subagent/agents/web-research.md +0 -101
  60. package/extensions/subagent/agents.ts +0 -261
  61. package/extensions/subagent/cmux-dashboard.ts +0 -495
  62. package/extensions/subagent/index.ts +0 -425
  63. package/extensions/subagent/panel.ts +0 -124
  64. package/extensions/subagent/render.ts +0 -74
  65. package/extensions/subagent/resume.ts +0 -78
  66. package/extensions/subagent/run.ts +0 -641
  67. package/extensions/subagent/runtime.ts +0 -1296
  68. package/extensions/subagent/session-resource.ts +0 -61
  69. package/extensions/subagent/settings.ts +0 -18
@@ -1,1296 +0,0 @@
1
- import type { Usage } from "@earendil-works/pi-ai";
2
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { createIsolatedSessionResource, type IsolatedSessionResource } from "../../shared/isolated-session.ts";
4
- import type { AgentDefinition } from "./agents.ts";
5
- import { buildColdResumePrompt, retainSubagentTurn, type RetainedTurnOutcome } from "./resume.ts";
6
- import {
7
- cloneInvocationSnapshot,
8
- createSubagentThread,
9
- disposeSubagentThread,
10
- extensionPathsForTools,
11
- FifoGate,
12
- runSubagentTurn,
13
- type SubagentDetails,
14
- type SubagentInvocationSnapshot,
15
- type SubagentLifecycle,
16
- type SubagentPhase,
17
- type SubagentThread,
18
- } from "./run.ts";
19
-
20
- const MAX_RETAINED_THREADS = 16;
21
- const GLOBAL_CONCURRENCY = 4;
22
- const SUBAGENT_HOT_WINDOW_MS = 5 * 60 * 1000;
23
-
24
- export type SnapshotObserver = (snapshot: SubagentInvocationSnapshot) => void;
25
-
26
- export interface SubagentRuntimeOptions {
27
- now?: () => number;
28
- }
29
-
30
- export interface SubagentToolResult {
31
- content: Array<{ type: "text"; text: string }>;
32
- details: SubagentDetails;
33
- usage?: Usage;
34
- }
35
-
36
- interface ActiveInvocation {
37
- snapshot: SubagentInvocationSnapshot;
38
- controller: AbortController;
39
- generation: number;
40
- run: Promise<SubagentToolResult>;
41
- admitTicket: number;
42
- }
43
-
44
- interface TrackedThread extends SubagentThread {
45
- disposed: boolean;
46
- disposePromise?: Promise<void>;
47
- }
48
-
49
- type ResolveFreshDefinition = () => Promise<
50
- { ok: true; definition: AgentDefinition } | { ok: false; error: string; phase: SubagentPhase }
51
- >;
52
-
53
- interface ExecuteBag {
54
- agent: string;
55
- displayName: string;
56
- definition: AgentDefinition | undefined;
57
- thread: TrackedThread | undefined;
58
- threadId: string | undefined;
59
- releaseThread: (() => void) | undefined;
60
- releaseGlobal: (() => void) | undefined;
61
- reservedThread: TrackedThread | undefined;
62
- provisionalThread: TrackedThread | undefined;
63
- provisionalResource: IsolatedSessionResource | undefined;
64
- reservationToken: symbol | undefined;
65
- admitAdvanced: boolean;
66
- phase: SubagentPhase;
67
- lastPublishedStatus: SubagentLifecycle | undefined;
68
- combined: AbortSignal;
69
- task: string;
70
- continuing: boolean;
71
- threadKey: string | undefined;
72
- files: readonly string[] | undefined;
73
- ctx: ExtensionContext;
74
- parentModel: string;
75
- parentThinking: NonNullable<ExtensionContext["thinkingLevel"]>;
76
- onUpdate: ((details: SubagentDetails) => void | Promise<void>) | undefined;
77
- resolveFreshDefinition: ResolveFreshDefinition;
78
- generation: number;
79
- invocationId: string;
80
- controller: AbortController;
81
- startedAt: number;
82
- active: ActiveInvocation;
83
- admitTicket: number;
84
- }
85
-
86
- function emptyUsage(): SubagentDetails["usage"] {
87
- return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
88
- }
89
-
90
- function baseDetails(options: {
91
- agent: string;
92
- displayName?: string;
93
- task: string;
94
- status: SubagentLifecycle;
95
- phase: SubagentPhase;
96
- model: string;
97
- thinkingLevel: string;
98
- threadId?: string;
99
- invocationId?: string;
100
- error?: string;
101
- }): SubagentDetails {
102
- return {
103
- agent: options.agent,
104
- displayName: options.displayName ?? options.agent,
105
- ...(options.threadId === undefined ? {} : { threadId: options.threadId }),
106
- ...(options.invocationId === undefined ? {} : { invocationId: options.invocationId }),
107
- status: options.status,
108
- phase: options.phase,
109
- task: options.task,
110
- model: options.model,
111
- thinkingLevel: options.thinkingLevel,
112
- toolCalls: 0,
113
- actions: [],
114
- omittedActions: 0,
115
- omittedErrors: 0,
116
- usage: emptyUsage(),
117
- durationMs: 0,
118
- ...(options.error === undefined ? {} : { error: options.error }),
119
- };
120
- }
121
-
122
- export function failedToolResult(
123
- agent: string,
124
- task: string,
125
- phase: SubagentPhase,
126
- model: string,
127
- thinkingLevel: string,
128
- error: string,
129
- threadId?: string,
130
- status: "failed" | "aborted" = "failed",
131
- ): SubagentToolResult {
132
- return {
133
- content: [{ type: "text", text: error }],
134
- details: baseDetails({ agent, task, status, phase, model, thinkingLevel, threadId, error }),
135
- };
136
- }
137
-
138
- function asTracked(thread: SubagentThread): TrackedThread {
139
- if ("disposed" in thread) return thread as TrackedThread;
140
- const tracked = thread as TrackedThread;
141
- tracked.disposed = false;
142
- return tracked;
143
- }
144
-
145
- /**
146
- * Session-scoped subagent orchestration.
147
- * One instance per extension factory lifetime; reset on session_start, dispose on shutdown.
148
- */
149
- export class SubagentRuntime {
150
- private generation = 0;
151
- private nextThreadId = 1;
152
- private nextInvocationId = 1;
153
- private nextAdmitTicket = 1;
154
- private admitHead = 1;
155
- private readonly admitWaiters = new Map<number, Set<() => void>>();
156
- private disposed = false;
157
- /** True while reset/shutdown drains old work. New execute calls abort immediately. */
158
- private lifecycleFence = false;
159
- private globalGate = new FifoGate(GLOBAL_CONCURRENCY);
160
- private readonly threads = new Map<string, TrackedThread>();
161
- /** Startup reservation tokens. Never bulk-cleared; each invocation releases its own token. */
162
- private readonly startupReservations = new Set<symbol>();
163
- private readonly controllers = new Set<AbortController>();
164
- private readonly invocations = new Map<string, ActiveInvocation>();
165
- private readonly observers = new Set<SnapshotObserver>();
166
- private readonly runtimeWarnings = new Set<string>();
167
- private readonly nameOrdinals = new Map<string, number>();
168
- private readonly assignedNames = new Set<string>();
169
- private readonly pi: ExtensionAPI;
170
- private readonly now: () => number;
171
- private lifecycleChain = Promise.resolve();
172
-
173
- constructor(pi: ExtensionAPI, options: SubagentRuntimeOptions = {}) {
174
- this.pi = pi;
175
- this.now = options.now ?? Date.now;
176
- }
177
-
178
- subscribe(observer: SnapshotObserver): () => void {
179
- this.observers.add(observer);
180
- return () => {
181
- this.observers.delete(observer);
182
- };
183
- }
184
-
185
- listThreads(cwd: string): SubagentThread[] {
186
- return [...this.threads.values()]
187
- .filter((thread) => thread.cwd === cwd && !thread.disposed)
188
- .sort((a, b) => a.id.localeCompare(b.id));
189
- }
190
-
191
- threadIds(cwd: string): string[] {
192
- return this.listThreads(cwd).map((thread) => thread.id);
193
- }
194
-
195
- async reset(): Promise<void> {
196
- await this.drainLifecycle({ permanent: false });
197
- }
198
-
199
- async shutdown(): Promise<void> {
200
- await this.drainLifecycle({ permanent: true });
201
- }
202
-
203
- private async drainLifecycle(options: { permanent: boolean }): Promise<void> {
204
- await this.runLifecycle(async () => {
205
- if (options.permanent) {
206
- if (this.disposed) return;
207
- this.disposed = true;
208
- }
209
- this.lifecycleFence = true;
210
- this.generation += 1;
211
- this.abortAllControllers();
212
- this.wakeAllAdmitWaiters();
213
- const running = [...this.invocations.values()].map((item) => item.run.catch(() => undefined));
214
- await Promise.all(running);
215
- this.invocations.clear();
216
- // Replace gate only after old turns have released slots.
217
- this.globalGate = new FifoGate(GLOBAL_CONCURRENCY);
218
- if (options.permanent) this.observers.clear();
219
- else {
220
- this.nextAdmitTicket = 1;
221
- this.admitHead = 1;
222
- this.admitWaiters.clear();
223
- this.runtimeWarnings.clear();
224
- this.nameOrdinals.clear();
225
- this.assignedNames.clear();
226
- }
227
- await this.disposeAllThreads();
228
- if (!options.permanent) this.lifecycleFence = false;
229
- });
230
- }
231
-
232
- execute(options: {
233
- agent: string;
234
- task: string;
235
- files?: readonly string[];
236
- continuing: boolean;
237
- threadKey?: string;
238
- definition?: AgentDefinition;
239
- ctx: ExtensionContext;
240
- parentModel: string;
241
- parentThinking: NonNullable<ExtensionContext["thinkingLevel"]>;
242
- signal?: AbortSignal;
243
- onUpdate?: (details: SubagentDetails) => void | Promise<void>;
244
- resolveFreshDefinition: () => Promise<
245
- { ok: true; definition: AgentDefinition } | { ok: false; error: string; phase: SubagentPhase }
246
- >;
247
- }): Promise<SubagentToolResult> {
248
- const generation = this.generation;
249
- const invocationId = `inv-${this.nextInvocationId++}`;
250
- const admitTicket = this.nextAdmitTicket++;
251
- const controller = new AbortController();
252
- this.controllers.add(controller);
253
- const startedAt = this.now();
254
- const initialAgent = options.agent;
255
- const initialSnapshot: SubagentInvocationSnapshot = {
256
- ...baseDetails({
257
- agent: initialAgent,
258
- task: options.task,
259
- status: "waiting",
260
- phase: "queue",
261
- model: options.parentModel,
262
- thinkingLevel: options.parentThinking,
263
- threadId: options.continuing ? options.threadKey : undefined,
264
- invocationId,
265
- }),
266
- invocationId,
267
- startedAt,
268
- files: [...(options.files ?? [])],
269
- };
270
- const active: ActiveInvocation = {
271
- snapshot: initialSnapshot,
272
- controller,
273
- generation,
274
- admitTicket,
275
- run: Promise.resolve(
276
- failedToolResult(
277
- initialAgent,
278
- options.task,
279
- "queue",
280
- options.parentModel,
281
- options.parentThinking,
282
- "Subagent invocation failed to start",
283
- options.threadKey,
284
- ),
285
- ),
286
- };
287
- this.invocations.set(invocationId, active);
288
- this.publish(active.snapshot);
289
-
290
- const run = this.executeInner(options, {
291
- generation,
292
- invocationId,
293
- controller,
294
- startedAt,
295
- active,
296
- admitTicket,
297
- });
298
- active.run = run;
299
- return run;
300
- }
301
-
302
- private async executeInner(
303
- options: {
304
- agent: string;
305
- task: string;
306
- files?: readonly string[];
307
- continuing: boolean;
308
- threadKey?: string;
309
- ctx: ExtensionContext;
310
- parentModel: string;
311
- parentThinking: NonNullable<ExtensionContext["thinkingLevel"]>;
312
- definition?: AgentDefinition;
313
- signal?: AbortSignal;
314
- onUpdate?: (details: SubagentDetails) => void | Promise<void>;
315
- resolveFreshDefinition: ResolveFreshDefinition;
316
- },
317
- state: {
318
- generation: number;
319
- invocationId: string;
320
- controller: AbortController;
321
- startedAt: number;
322
- active: ActiveInvocation;
323
- admitTicket: number;
324
- },
325
- ): Promise<SubagentToolResult> {
326
- const bag = this.createExecuteBag(options, state);
327
- const fanOut = (details: SubagentDetails, force = false) => this.fanOutInvocation(bag, details, force);
328
- try {
329
- return await this.driveExecute(bag, fanOut);
330
- } catch (error) {
331
- const handled = await this.handleExecuteError({
332
- error,
333
- agent: bag.agent,
334
- displayName: bag.displayName,
335
- task: bag.task,
336
- phase: bag.phase,
337
- parentModel: bag.parentModel,
338
- parentThinking: bag.parentThinking,
339
- thread: bag.thread,
340
- threadId: bag.threadId,
341
- invocationId: bag.invocationId,
342
- combined: bag.combined,
343
- continuing: bag.continuing,
344
- provisionalResource: bag.provisionalResource,
345
- provisionalThread: bag.provisionalThread,
346
- fanOut,
347
- });
348
- bag.provisionalResource = undefined;
349
- bag.provisionalThread = undefined;
350
- return handled;
351
- } finally {
352
- await this.cleanupExecuteBag(bag);
353
- }
354
- }
355
-
356
- private createExecuteBag(
357
- options: {
358
- agent: string;
359
- task: string;
360
- files?: readonly string[];
361
- continuing: boolean;
362
- threadKey?: string;
363
- ctx: ExtensionContext;
364
- parentModel: string;
365
- parentThinking: NonNullable<ExtensionContext["thinkingLevel"]>;
366
- definition?: AgentDefinition;
367
- signal?: AbortSignal;
368
- onUpdate?: (details: SubagentDetails) => void | Promise<void>;
369
- resolveFreshDefinition: ResolveFreshDefinition;
370
- },
371
- state: {
372
- generation: number;
373
- invocationId: string;
374
- controller: AbortController;
375
- startedAt: number;
376
- active: ActiveInvocation;
377
- admitTicket: number;
378
- },
379
- ): ExecuteBag {
380
- return {
381
- agent: options.agent,
382
- displayName: options.agent,
383
- definition: options.definition,
384
- thread: undefined,
385
- threadId: options.continuing ? options.threadKey : undefined,
386
- releaseThread: undefined,
387
- releaseGlobal: undefined,
388
- reservedThread: undefined,
389
- provisionalThread: undefined,
390
- provisionalResource: undefined,
391
- reservationToken: undefined,
392
- admitAdvanced: false,
393
- phase: "queue",
394
- lastPublishedStatus: "waiting",
395
- combined: AbortSignal.any([state.controller.signal, ...(options.signal ? [options.signal] : [])]),
396
- task: options.task,
397
- continuing: options.continuing,
398
- threadKey: options.threadKey,
399
- files: options.files,
400
- ctx: options.ctx,
401
- parentModel: options.parentModel,
402
- parentThinking: options.parentThinking,
403
- onUpdate: options.onUpdate,
404
- resolveFreshDefinition: options.resolveFreshDefinition,
405
- generation: state.generation,
406
- invocationId: state.invocationId,
407
- controller: state.controller,
408
- startedAt: state.startedAt,
409
- active: state.active,
410
- admitTicket: state.admitTicket,
411
- };
412
- }
413
-
414
- private fanOutInvocation(bag: ExecuteBag, details: SubagentDetails, force = false): void {
415
- const current = this.invocations.get(bag.invocationId);
416
- if (!current || current.generation !== this.generation) return;
417
- if (!force && details.status === bag.lastPublishedStatus && details.status !== "running") return;
418
- bag.lastPublishedStatus = details.status;
419
- const next: SubagentInvocationSnapshot = {
420
- ...details,
421
- invocationId: bag.invocationId,
422
- startedAt: bag.startedAt,
423
- files: current.snapshot.files,
424
- threadId: details.threadId ?? bag.threadId,
425
- agent: details.agent || bag.agent,
426
- displayName: details.displayName || bag.displayName,
427
- };
428
- current.snapshot = next;
429
- this.publish(next);
430
- if (!bag.onUpdate) return;
431
- // Detached: presentation latency must not block admission or child turns.
432
- void Promise.resolve()
433
- .then(() => bag.onUpdate?.(cloneInvocationSnapshot(next)))
434
- .catch(() => undefined);
435
- }
436
-
437
- private finishInvocation(bag: ExecuteBag, details: SubagentDetails, text?: string): SubagentToolResult {
438
- this.fanOutInvocation(bag, details, true);
439
- return { content: [{ type: "text", text: text ?? details.error ?? details.response ?? "" }], details };
440
- }
441
-
442
- private failInvocation(
443
- bag: ExecuteBag,
444
- error: string,
445
- failPhase: SubagentPhase,
446
- overrides: Partial<SubagentDetails> = {},
447
- ): SubagentToolResult {
448
- return this.finishInvocation(
449
- bag,
450
- baseDetails({
451
- agent: overrides.agent ?? bag.agent,
452
- displayName: overrides.displayName ?? bag.thread?.displayName ?? bag.displayName,
453
- task: bag.task,
454
- status: overrides.status === "aborted" ? "aborted" : "failed",
455
- phase: failPhase,
456
- model: overrides.model ?? bag.thread?.model ?? bag.parentModel,
457
- thinkingLevel: overrides.thinkingLevel ?? bag.thread?.thinkingLevel ?? bag.parentThinking,
458
- threadId: overrides.threadId ?? bag.thread?.id ?? bag.threadId,
459
- invocationId: bag.invocationId,
460
- error,
461
- }),
462
- error,
463
- );
464
- }
465
-
466
- private abortInvocation(
467
- bag: ExecuteBag,
468
- failPhase: SubagentPhase = bag.phase,
469
- markAdmit = false,
470
- ): SubagentToolResult {
471
- if (markAdmit && !bag.admitAdvanced) {
472
- this.advanceAdmitTicket(bag.admitTicket);
473
- bag.admitAdvanced = true;
474
- }
475
- return this.finishInvocation(
476
- bag,
477
- this.terminalFromAbort(
478
- bag.agent,
479
- bag.displayName,
480
- bag.task,
481
- failPhase,
482
- bag.thread?.model ?? bag.parentModel,
483
- bag.thread?.thinkingLevel ?? bag.parentThinking,
484
- bag.threadId,
485
- bag.invocationId,
486
- ),
487
- );
488
- }
489
-
490
- private async driveExecute(
491
- bag: ExecuteBag,
492
- fanOut: (details: SubagentDetails, force?: boolean) => void,
493
- ): Promise<SubagentToolResult> {
494
- if (this.lifecycleFence || this.disposed || bag.generation !== this.generation) {
495
- return this.abortInvocation(bag);
496
- }
497
-
498
- const admitted = await this.admitExecuteTarget(bag, fanOut);
499
- if (admitted) return admitted;
500
-
501
- const gated = await this.gateExecute(bag);
502
- if (gated) return gated;
503
-
504
- return await this.runExecuteTurn(bag, fanOut);
505
- }
506
-
507
- private async admitExecuteTarget(
508
- bag: ExecuteBag,
509
- fanOut: (details: SubagentDetails, force?: boolean) => void,
510
- ): Promise<SubagentToolResult | undefined> {
511
- if (bag.continuing) {
512
- const bound = this.bindContinuingThread({
513
- threadKey: bag.threadKey ?? "",
514
- cwd: bag.ctx.cwd,
515
- parentModel: bag.parentModel,
516
- parentThinking: bag.parentThinking,
517
- active: bag.active,
518
- fanOut,
519
- });
520
- if (!bound.ok) return this.failInvocation(bag, bound.error, "discovery", bound.overrides);
521
- bag.reservedThread = bound.thread;
522
- bag.thread = bound.thread;
523
- bag.agent = bound.agent;
524
- bag.displayName = bound.displayName;
525
- bag.threadId = bound.threadId;
526
- bag.definition = bound.definition;
527
- return undefined;
528
- }
529
-
530
- bag.phase = "discovery";
531
- const prepared = await this.prepareFreshDiscovery({
532
- generation: bag.generation,
533
- combined: bag.combined,
534
- active: bag.active,
535
- resolveFreshDefinition: bag.resolveFreshDefinition,
536
- fanOut,
537
- });
538
- if (prepared.kind === "aborted") return this.abortInvocation(bag);
539
- if (prepared.kind === "failed") return this.failInvocation(bag, prepared.error, prepared.phase);
540
- bag.definition = prepared.definition;
541
- bag.agent = prepared.agent;
542
- bag.displayName = prepared.displayName;
543
- bag.threadId = prepared.threadId;
544
- bag.reservationToken = prepared.reservationToken;
545
- return undefined;
546
- }
547
-
548
- private async gateExecute(bag: ExecuteBag): Promise<SubagentToolResult | undefined> {
549
- const gates = await this.acquireExecutionGates({
550
- thread: bag.thread,
551
- admitTicket: bag.admitTicket,
552
- combined: bag.combined,
553
- generation: bag.generation,
554
- phase: bag.phase,
555
- });
556
- if (gates.kind === "aborted") {
557
- bag.admitAdvanced = gates.admitAdvanced;
558
- return this.abortInvocation(bag, gates.phase, gates.markAdmit);
559
- }
560
- if (gates.kind === "failed") {
561
- bag.admitAdvanced = gates.admitAdvanced;
562
- return this.failInvocation(bag, gates.error, "queue", gates.overrides);
563
- }
564
- bag.releaseThread = gates.releaseThread;
565
- bag.releaseGlobal = gates.releaseGlobal;
566
- bag.admitAdvanced = true;
567
- return undefined;
568
- }
569
-
570
- private async runExecuteTurn(
571
- bag: ExecuteBag,
572
- fanOut: (details: SubagentDetails, force?: boolean) => void,
573
- ): Promise<SubagentToolResult> {
574
- if (!bag.thread) {
575
- bag.phase = "startup";
576
- if (!bag.threadId || !bag.definition) throw new Error("Subagent startup state is incomplete");
577
- const started = await this.startFreshThread({
578
- threadId: bag.threadId,
579
- displayName: bag.displayName,
580
- definition: bag.definition,
581
- task: bag.task,
582
- ctx: bag.ctx,
583
- parentThinking: bag.parentThinking,
584
- combined: bag.combined,
585
- generation: bag.generation,
586
- agent: bag.agent,
587
- active: bag.active,
588
- fanOut,
589
- });
590
- if (started.kind === "aborted") {
591
- bag.thread = started.thread;
592
- return this.abortInvocation(bag, "startup");
593
- }
594
- bag.provisionalThread = started.thread;
595
- bag.thread = started.thread;
596
- bag.reservedThread = bag.thread;
597
- }
598
-
599
- const thread = bag.thread;
600
- const cold = await this.coldResumeIfNeeded({
601
- continuing: bag.continuing,
602
- thread,
603
- generation: bag.generation,
604
- combined: bag.combined,
605
- task: bag.task,
606
- files: bag.files,
607
- agent: bag.agent,
608
- active: bag.active,
609
- fanOut,
610
- });
611
- if (cold.kind === "aborted") {
612
- bag.provisionalResource = cold.provisionalResource;
613
- return this.abortInvocation(bag, "startup");
614
- }
615
- bag.phase = cold.phase ?? bag.phase;
616
- bag.provisionalResource = cold.provisionalResource;
617
- const resumePrompt = cold.resumePrompt;
618
-
619
- const result = await runSubagentTurn({
620
- thread,
621
- task: bag.task,
622
- files: bag.files,
623
- invocationId: bag.invocationId,
624
- initial: thread.turns === 0 || resumePrompt !== undefined,
625
- ...(resumePrompt === undefined ? {} : { resumePrompt }),
626
- signal: bag.combined,
627
- onUpdate: (details) => {
628
- fanOut({ ...details, invocationId: bag.invocationId, threadId: bag.thread?.id ?? bag.threadId });
629
- },
630
- });
631
-
632
- if (!this.isLive(bag.generation, bag.combined)) {
633
- const aborted = await this.abortAfterTurn({
634
- result,
635
- invocationId: bag.invocationId,
636
- fanOut,
637
- reservationToken: bag.reservationToken,
638
- provisionalThread: bag.provisionalThread,
639
- thread,
640
- });
641
- bag.reservationToken = undefined;
642
- bag.provisionalThread = undefined;
643
- return aborted;
644
- }
645
-
646
- bag.lastPublishedStatus = result.details.status;
647
- // Drop reservation before publish so capacity never reads above 16.
648
- this.releaseReservation(bag.reservationToken);
649
- bag.reservationToken = undefined;
650
- const finished = await this.retainAndFinishTurn({
651
- result,
652
- thread,
653
- task: bag.task,
654
- files: bag.files,
655
- generation: bag.generation,
656
- continuing: bag.continuing,
657
- provisionalThread: bag.provisionalThread,
658
- invocationId: bag.invocationId,
659
- });
660
- bag.provisionalThread = undefined;
661
- return finished;
662
- }
663
-
664
- private async cleanupExecuteBag(bag: ExecuteBag): Promise<void> {
665
- if (!bag.admitAdvanced) this.advanceAdmitTicket(bag.admitTicket);
666
- this.releaseReservation(bag.reservationToken);
667
- if (bag.provisionalThread && this.threads.get(bag.provisionalThread.id) !== bag.provisionalThread) {
668
- await this.disposeThread(bag.provisionalThread).catch(() => undefined);
669
- }
670
- if (bag.provisionalResource) await bag.provisionalResource.dispose().catch(() => undefined);
671
- if (bag.reservedThread) bag.reservedThread.pendingTurns = Math.max(0, bag.reservedThread.pendingTurns - 1);
672
- bag.releaseGlobal?.();
673
- bag.releaseThread?.();
674
- this.controllers.delete(bag.controller);
675
- this.invocations.delete(bag.invocationId);
676
- }
677
-
678
- private bindContinuingThread(options: {
679
- threadKey: string;
680
- cwd: string;
681
- parentModel: string;
682
- parentThinking: string;
683
- active: ActiveInvocation;
684
- fanOut: (details: SubagentDetails, force?: boolean) => void;
685
- }):
686
- | {
687
- ok: true;
688
- thread: TrackedThread;
689
- agent: string;
690
- displayName: string;
691
- threadId: string;
692
- definition: AgentDefinition;
693
- }
694
- | { ok: false; error: string; overrides: Partial<SubagentDetails> } {
695
- const { threadKey, cwd, parentModel, parentThinking, active, fanOut } = options;
696
- const existing = this.threads.get(threadKey);
697
- if (!existing || existing.cwd !== cwd || existing.disposed) {
698
- const names = this.threadIds(cwd).join(", ") || "none";
699
- return {
700
- ok: false,
701
- error: `Subagent thread ${threadKey} is unavailable. Active threads: ${names}`,
702
- overrides: {
703
- agent: threadKey,
704
- threadId: threadKey,
705
- model: parentModel,
706
- thinkingLevel: parentThinking,
707
- },
708
- };
709
- }
710
- existing.pendingTurns += 1;
711
- const agent = existing.definition.name;
712
- const displayName = existing.displayName;
713
- const threadId = existing.id;
714
- fanOut(
715
- {
716
- ...active.snapshot,
717
- agent,
718
- displayName,
719
- threadId,
720
- model: existing.model,
721
- thinkingLevel: existing.thinkingLevel,
722
- },
723
- true,
724
- );
725
- return {
726
- ok: true,
727
- thread: existing,
728
- agent,
729
- displayName,
730
- threadId,
731
- definition: existing.definition,
732
- };
733
- }
734
-
735
- private async prepareFreshDiscovery(options: {
736
- generation: number;
737
- combined: AbortSignal;
738
- active: ActiveInvocation;
739
- resolveFreshDefinition: () => Promise<
740
- { ok: true; definition: AgentDefinition } | { ok: false; error: string; phase: SubagentPhase }
741
- >;
742
- fanOut: (details: SubagentDetails, force?: boolean) => void;
743
- }): Promise<
744
- | {
745
- kind: "ok";
746
- definition: AgentDefinition;
747
- agent: string;
748
- displayName: string;
749
- threadId: string;
750
- reservationToken: symbol;
751
- }
752
- | { kind: "aborted" }
753
- | { kind: "failed"; error: string; phase: SubagentPhase }
754
- > {
755
- const { generation, combined, active, resolveFreshDefinition, fanOut } = options;
756
- const resolved = await resolveFreshDefinition();
757
- if (!this.isLive(generation, combined)) return { kind: "aborted" };
758
- if (!resolved.ok) return { kind: "failed", error: resolved.error, phase: resolved.phase };
759
- const definition = resolved.definition;
760
- const agent = definition.name;
761
- const displayName = this.assignDisplayName(definition);
762
- const threadId = `thread-${this.nextThreadId++}`;
763
- fanOut({ ...active.snapshot, agent, displayName, threadId }, true);
764
- const reserved = this.reserveFreshCapacity();
765
- if (!reserved.ok) return { kind: "failed", error: reserved.error, phase: "queue" };
766
- if (reserved.evicted) await this.disposeThread(reserved.evicted);
767
- return {
768
- kind: "ok",
769
- definition,
770
- agent,
771
- displayName,
772
- threadId,
773
- reservationToken: reserved.token,
774
- };
775
- }
776
-
777
- private async acquireExecutionGates(options: {
778
- thread: TrackedThread | undefined;
779
- admitTicket: number;
780
- combined: AbortSignal;
781
- generation: number;
782
- phase: SubagentPhase;
783
- }): Promise<
784
- | {
785
- kind: "ok";
786
- releaseThread: (() => void) | undefined;
787
- releaseGlobal: () => void;
788
- }
789
- | { kind: "aborted"; phase: SubagentPhase; markAdmit: boolean; admitAdvanced: boolean }
790
- | {
791
- kind: "failed";
792
- error: string;
793
- admitAdvanced: boolean;
794
- overrides: Partial<SubagentDetails>;
795
- }
796
- > {
797
- const { thread, admitTicket, combined, generation, phase } = options;
798
- // FIFO barrier assigned at execute() entry — earlier tickets go first regardless of discovery speed.
799
- const admitted = await this.waitAdmitTicket(admitTicket, combined, generation);
800
- if (!admitted) return { kind: "aborted", phase, markAdmit: true, admitAdvanced: false };
801
-
802
- let releaseThread: (() => void) | undefined;
803
- if (thread) {
804
- releaseThread = await thread.turnGate.acquire(combined);
805
- if (thread.disposed || this.threads.get(thread.id) !== thread) {
806
- this.advanceAdmitTicket(admitTicket);
807
- releaseThread();
808
- return {
809
- kind: "failed",
810
- error: `Subagent thread ${thread.id} is unavailable after queueing`,
811
- admitAdvanced: true,
812
- overrides: {
813
- status: combined.aborted ? "aborted" : "failed",
814
- model: thread.model,
815
- thinkingLevel: thread.thinkingLevel,
816
- threadId: thread.id,
817
- },
818
- };
819
- }
820
- }
821
- if (!this.isLive(generation, combined)) {
822
- releaseThread?.();
823
- return { kind: "aborted", phase, markAdmit: true, admitAdvanced: false };
824
- }
825
-
826
- let releaseGlobal: () => void;
827
- try {
828
- releaseGlobal = await this.globalGate.acquire(combined);
829
- } catch {
830
- releaseThread?.();
831
- return { kind: "aborted", phase, markAdmit: true, admitAdvanced: false };
832
- }
833
- // Next ticket may compete for remaining global slots.
834
- this.advanceAdmitTicket(admitTicket);
835
- if (!this.isLive(generation, combined)) {
836
- // Caller keeps releaseThread/releaseGlobal unset; free them here before abort return.
837
- releaseGlobal();
838
- releaseThread?.();
839
- return { kind: "aborted", phase, markAdmit: false, admitAdvanced: true };
840
- }
841
- return { kind: "ok", releaseThread, releaseGlobal };
842
- }
843
-
844
- private async startFreshThread(options: {
845
- threadId: string;
846
- displayName: string;
847
- definition: AgentDefinition;
848
- task: string;
849
- ctx: ExtensionContext;
850
- parentThinking: NonNullable<ExtensionContext["thinkingLevel"]>;
851
- combined: AbortSignal;
852
- generation: number;
853
- agent: string;
854
- active: ActiveInvocation;
855
- fanOut: (details: SubagentDetails, force?: boolean) => void;
856
- }): Promise<{ kind: "ok"; thread: TrackedThread } | { kind: "aborted"; thread: TrackedThread }> {
857
- const {
858
- threadId,
859
- displayName,
860
- definition,
861
- task,
862
- ctx,
863
- parentThinking,
864
- combined,
865
- generation,
866
- agent,
867
- active,
868
- fanOut,
869
- } = options;
870
- fanOut({ ...active.snapshot, status: "starting", phase: "startup", agent, threadId }, true);
871
- const created = asTracked(
872
- await createSubagentThread({
873
- id: threadId,
874
- displayName,
875
- definition,
876
- extensionPaths: extensionPathsForTools(this.pi, definition.tools),
877
- initialTask: task,
878
- ctx,
879
- thinkingLevel: parentThinking,
880
- signal: combined,
881
- onWarning: (warning) => {
882
- const message = `Subagent definition ${definition.path}: ${warning}`;
883
- if (this.runtimeWarnings.has(message)) return;
884
- this.runtimeWarnings.add(message);
885
- ctx.ui.notify(message, "warning");
886
- },
887
- }),
888
- );
889
- if (!this.isLive(generation, combined)) {
890
- await this.disposeThread(created);
891
- return { kind: "aborted", thread: created };
892
- }
893
- created.pendingTurns += 1;
894
- return { kind: "ok", thread: created };
895
- }
896
-
897
- private async coldResumeIfNeeded(options: {
898
- continuing: boolean;
899
- thread: TrackedThread;
900
- generation: number;
901
- combined: AbortSignal;
902
- task: string;
903
- files: readonly string[] | undefined;
904
- agent: string;
905
- active: ActiveInvocation;
906
- fanOut: (details: SubagentDetails, force?: boolean) => void;
907
- }): Promise<
908
- | { kind: "ok"; resumePrompt?: string; phase?: SubagentPhase; provisionalResource?: IsolatedSessionResource }
909
- | { kind: "aborted"; provisionalResource?: IsolatedSessionResource }
910
- > {
911
- const { continuing, thread, generation, combined, task, files, agent, active, fanOut } = options;
912
- const needsCold =
913
- continuing &&
914
- (thread.lastAssistantMessageAt === undefined ||
915
- this.now() - thread.lastAssistantMessageAt >= SUBAGENT_HOT_WINDOW_MS);
916
- if (!needsCold) return { kind: "ok" };
917
-
918
- fanOut({ ...active.snapshot, status: "starting", phase: "startup", agent, threadId: thread.id }, true);
919
- const oldResource = thread.resource;
920
- const provisionalResource = await createIsolatedSessionResource(thread.sessionInputs, combined);
921
- if (!this.isLive(generation, combined) || thread.disposed || this.threads.get(thread.id) !== thread) {
922
- await provisionalResource.dispose();
923
- if (this.threads.get(thread.id) === thread) this.threads.delete(thread.id);
924
- await this.disposeThread(thread);
925
- return { kind: "aborted" };
926
- }
927
- thread.resource = provisionalResource;
928
- await oldResource.dispose();
929
- if (!this.isLive(generation, combined)) {
930
- if (this.threads.get(thread.id) === thread) this.threads.delete(thread.id);
931
- await this.disposeThread(thread);
932
- return { kind: "aborted" };
933
- }
934
- return {
935
- kind: "ok",
936
- phase: "startup",
937
- resumePrompt: buildColdResumePrompt({
938
- definition: thread.definition,
939
- state: thread.resumeState,
940
- followUp: task,
941
- hasAutoreadFiles: (files?.length ?? 0) > 0,
942
- }),
943
- };
944
- }
945
-
946
- private async abortAfterTurn(options: {
947
- result: {
948
- details: SubagentDetails;
949
- usage?: Usage;
950
- };
951
- invocationId: string;
952
- fanOut: (details: SubagentDetails, force?: boolean) => void;
953
- reservationToken: symbol | undefined;
954
- provisionalThread: TrackedThread | undefined;
955
- thread: TrackedThread;
956
- }): Promise<SubagentToolResult> {
957
- const { result, invocationId, fanOut, reservationToken, provisionalThread, thread } = options;
958
- const details = {
959
- ...result.details,
960
- status: "aborted" as const,
961
- invocationId,
962
- error: result.details.error ?? "Subagent session reset",
963
- };
964
- fanOut(details, true);
965
- this.releaseReservation(reservationToken);
966
- if (provisionalThread) await this.disposeThread(provisionalThread);
967
- else if (this.threads.get(thread.id) === thread) {
968
- this.threads.delete(thread.id);
969
- await this.disposeThread(thread);
970
- }
971
- return {
972
- content: [{ type: "text", text: details.error ?? "aborted" }],
973
- details,
974
- ...(result.usage === undefined ? {} : { usage: result.usage }),
975
- };
976
- }
977
-
978
- private toolResultFromTurn(
979
- result: {
980
- content: string;
981
- details: SubagentDetails;
982
- usage?: Usage;
983
- },
984
- thread: TrackedThread,
985
- invocationId: string,
986
- retained: boolean,
987
- ): SubagentToolResult {
988
- const details = {
989
- ...result.details,
990
- displayName: thread.displayName,
991
- invocationId,
992
- threadId: thread.id,
993
- };
994
- const text = retained
995
- ? `Thread: ${thread.id}\nReuse with subagent({ thread: "${thread.id}", task: "..." })\n\n${result.content}`
996
- : result.content;
997
- return {
998
- content: [{ type: "text", text }],
999
- details,
1000
- ...(result.usage === undefined ? {} : { usage: result.usage }),
1001
- };
1002
- }
1003
-
1004
- private async retainAndFinishTurn(options: {
1005
- result: {
1006
- content: string;
1007
- details: SubagentDetails;
1008
- retainable: boolean;
1009
- terminalOutcome: RetainedTurnOutcome;
1010
- assistantMessageEndAt: readonly number[];
1011
- usage?: Usage;
1012
- };
1013
- thread: TrackedThread;
1014
- task: string;
1015
- files: readonly string[] | undefined;
1016
- generation: number;
1017
- continuing: boolean;
1018
- provisionalThread: TrackedThread | undefined;
1019
- invocationId: string;
1020
- }): Promise<SubagentToolResult> {
1021
- const { result, thread, task, files, generation, continuing, provisionalThread, invocationId } = options;
1022
- if (result.retainable) {
1023
- thread.resumeState = retainSubagentTurn(thread.resumeState, {
1024
- task,
1025
- outcome: result.terminalOutcome,
1026
- terminalText: result.content,
1027
- files: files ?? [],
1028
- });
1029
- thread.lastAssistantMessageAt = result.assistantMessageEndAt.at(-1);
1030
- }
1031
- const retained = await this.finalizeThreadRetention({
1032
- generation,
1033
- continuing,
1034
- thread,
1035
- provisional: provisionalThread === thread,
1036
- result,
1037
- });
1038
- return this.toolResultFromTurn(result, thread, invocationId, retained);
1039
- }
1040
-
1041
- private async handleExecuteError(options: {
1042
- error: unknown;
1043
- agent: string;
1044
- displayName: string;
1045
- task: string;
1046
- phase: SubagentPhase;
1047
- parentModel: string;
1048
- parentThinking: string;
1049
- thread: TrackedThread | undefined;
1050
- threadId: string | undefined;
1051
- invocationId: string;
1052
- combined: AbortSignal;
1053
- continuing: boolean;
1054
- provisionalResource: IsolatedSessionResource | undefined;
1055
- provisionalThread: TrackedThread | undefined;
1056
- fanOut: (details: SubagentDetails, force?: boolean) => void;
1057
- }): Promise<SubagentToolResult> {
1058
- const {
1059
- error,
1060
- agent,
1061
- displayName,
1062
- task,
1063
- phase,
1064
- parentModel,
1065
- parentThinking,
1066
- thread,
1067
- threadId,
1068
- invocationId,
1069
- combined,
1070
- continuing,
1071
- provisionalResource,
1072
- provisionalThread,
1073
- fanOut,
1074
- } = options;
1075
- const message = error instanceof Error ? error.message : `Agent ${agent} ${phase} failed`;
1076
- const status: SubagentLifecycle = combined.aborted ? "aborted" : "failed";
1077
- const details = baseDetails({
1078
- agent,
1079
- task,
1080
- status,
1081
- phase,
1082
- model: thread?.model ?? parentModel,
1083
- thinkingLevel: thread?.thinkingLevel ?? parentThinking,
1084
- displayName: thread?.displayName ?? displayName,
1085
- threadId: thread?.id ?? threadId,
1086
- invocationId,
1087
- error: message,
1088
- });
1089
- fanOut(details, true);
1090
- if (provisionalResource) await provisionalResource.dispose().catch(() => undefined);
1091
- if (provisionalThread) await this.disposeThread(provisionalThread);
1092
- else if (thread && continuing && this.threads.get(thread.id) === thread) {
1093
- this.threads.delete(thread.id);
1094
- await this.disposeThread(thread);
1095
- }
1096
- return { content: [{ type: "text", text: message }], details };
1097
- }
1098
-
1099
- private async waitAdmitTicket(ticket: number, signal: AbortSignal, generation: number): Promise<boolean> {
1100
- if (signal.aborted || generation !== this.generation || this.disposed || this.lifecycleFence) {
1101
- return false;
1102
- }
1103
- if (this.admitHead === ticket) return true;
1104
- return new Promise<boolean>((resolve) => {
1105
- const wake = () => {
1106
- if (signal.aborted || generation !== this.generation || this.disposed || this.lifecycleFence) {
1107
- cleanup();
1108
- resolve(false);
1109
- return;
1110
- }
1111
- if (this.admitHead === ticket) {
1112
- cleanup();
1113
- resolve(true);
1114
- }
1115
- };
1116
- const onAbort = () => wake();
1117
- const cleanup = () => {
1118
- signal.removeEventListener("abort", onAbort);
1119
- const waiters = this.admitWaiters.get(ticket);
1120
- if (!waiters) return;
1121
- waiters.delete(wake);
1122
- if (waiters.size === 0) this.admitWaiters.delete(ticket);
1123
- };
1124
- const waiters = this.admitWaiters.get(ticket) ?? new Set<() => void>();
1125
- waiters.add(wake);
1126
- this.admitWaiters.set(ticket, waiters);
1127
- signal.addEventListener("abort", onAbort, { once: true });
1128
- wake();
1129
- });
1130
- }
1131
-
1132
- private advanceAdmitTicket(ticket: number): void {
1133
- if (ticket !== this.admitHead) {
1134
- // Out-of-order completion: still must not stall the head forever.
1135
- // Mark by bumping only when head matches; otherwise queue skip via recursive advance when head reaches us.
1136
- // Store skipped tickets.
1137
- this.skippedAdmitTickets.add(ticket);
1138
- this.drainAdmitSkips();
1139
- return;
1140
- }
1141
- this.admitHead += 1;
1142
- this.drainAdmitSkips();
1143
- const waiters = this.admitWaiters.get(this.admitHead);
1144
- if (waiters) for (const wake of waiters) wake();
1145
- }
1146
-
1147
- private readonly skippedAdmitTickets = new Set<number>();
1148
-
1149
- private drainAdmitSkips(): void {
1150
- while (this.skippedAdmitTickets.has(this.admitHead)) {
1151
- this.skippedAdmitTickets.delete(this.admitHead);
1152
- this.admitHead += 1;
1153
- const waiters = this.admitWaiters.get(this.admitHead);
1154
- if (waiters) for (const wake of waiters) wake();
1155
- }
1156
- }
1157
-
1158
- private wakeAllAdmitWaiters(): void {
1159
- for (const waiters of this.admitWaiters.values()) for (const wake of waiters) wake();
1160
- this.admitWaiters.clear();
1161
- }
1162
-
1163
- private async finalizeThreadRetention(options: {
1164
- generation: number;
1165
- continuing: boolean;
1166
- thread: TrackedThread;
1167
- provisional: boolean;
1168
- result: { details: SubagentDetails; retainable: boolean };
1169
- }): Promise<boolean> {
1170
- const { generation, continuing, thread, provisional, result } = options;
1171
- if (generation !== this.generation || this.disposed) {
1172
- if (this.threads.get(thread.id) === thread) this.threads.delete(thread.id);
1173
- await this.disposeThread(thread);
1174
- return false;
1175
- }
1176
- if (provisional) {
1177
- if (result.details.status === "completed" && result.retainable) {
1178
- this.threads.set(thread.id, thread);
1179
- return true;
1180
- }
1181
- await this.disposeThread(thread);
1182
- return false;
1183
- }
1184
- if (!continuing) return this.threads.has(thread.id);
1185
- if (result.retainable && !thread.disposed) return true;
1186
- if (this.threads.get(thread.id) === thread) this.threads.delete(thread.id);
1187
- await this.disposeThread(thread);
1188
- return false;
1189
- }
1190
-
1191
- private reserveFreshCapacity(): { ok: true; token: symbol; evicted?: TrackedThread } | { ok: false; error: string } {
1192
- if (this.threads.size + this.startupReservations.size < MAX_RETAINED_THREADS) {
1193
- const token = Symbol("startup-reservation");
1194
- this.startupReservations.add(token);
1195
- return { ok: true, token };
1196
- }
1197
- const evicted = [...this.threads.values()]
1198
- .filter((item) => item.pendingTurns === 0 && !item.disposed)
1199
- .sort((a, b) => a.lastUsedAt - b.lastUsedAt)[0];
1200
- if (!evicted) {
1201
- return {
1202
- ok: false,
1203
- error: "Subagent thread limit reached while all retained threads are busy",
1204
- };
1205
- }
1206
- this.threads.delete(evicted.id);
1207
- const token = Symbol("startup-reservation");
1208
- this.startupReservations.add(token);
1209
- return { ok: true, token, evicted };
1210
- }
1211
-
1212
- private assignDisplayName(definition: AgentDefinition): string {
1213
- const ordinal = this.nameOrdinals.get(definition.name) ?? 0;
1214
- this.nameOrdinals.set(definition.name, ordinal + 1);
1215
- const base = definition.names[ordinal % definition.names.length] ?? definition.name;
1216
- let cycle = Math.floor(ordinal / definition.names.length) + 1;
1217
- let candidate = cycle === 1 ? base : `${base}-${cycle}`;
1218
- while (this.assignedNames.has(candidate)) {
1219
- cycle += 1;
1220
- candidate = `${base}-${cycle}`;
1221
- }
1222
- this.assignedNames.add(candidate);
1223
- return candidate;
1224
- }
1225
-
1226
- private releaseReservation(token: symbol | undefined): void {
1227
- if (!token) return;
1228
- this.startupReservations.delete(token);
1229
- }
1230
-
1231
- private isLive(generation: number, signal: AbortSignal): boolean {
1232
- return !this.disposed && !this.lifecycleFence && generation === this.generation && !signal.aborted;
1233
- }
1234
-
1235
- private terminalFromAbort(
1236
- agent: string,
1237
- displayName: string,
1238
- task: string,
1239
- phase: SubagentPhase,
1240
- model: string,
1241
- thinkingLevel: string,
1242
- threadId: string | undefined,
1243
- invocationId: string,
1244
- ): SubagentDetails {
1245
- return baseDetails({
1246
- agent,
1247
- displayName,
1248
- task,
1249
- status: "aborted",
1250
- phase,
1251
- model,
1252
- thinkingLevel,
1253
- threadId,
1254
- invocationId,
1255
- error: "Subagent call aborted",
1256
- });
1257
- }
1258
-
1259
- private publish(snapshot: SubagentInvocationSnapshot): void {
1260
- const immutable = cloneInvocationSnapshot(snapshot);
1261
- for (const observer of this.observers) {
1262
- try {
1263
- observer(cloneInvocationSnapshot(immutable));
1264
- } catch {
1265
- // Observers must not break the runtime.
1266
- }
1267
- }
1268
- }
1269
-
1270
- private abortAllControllers(): void {
1271
- for (const controller of this.controllers) controller.abort();
1272
- this.controllers.clear();
1273
- }
1274
-
1275
- private async disposeThread(thread: TrackedThread): Promise<void> {
1276
- if (thread.disposePromise) return thread.disposePromise;
1277
- thread.disposed = true;
1278
- thread.disposePromise = disposeSubagentThread(thread).catch(() => undefined);
1279
- return thread.disposePromise;
1280
- }
1281
-
1282
- private async disposeAllThreads(): Promise<void> {
1283
- const retained = [...this.threads.values()];
1284
- this.threads.clear();
1285
- await Promise.all(retained.map((thread) => this.disposeThread(thread)));
1286
- }
1287
-
1288
- private runLifecycle(work: () => Promise<void>): Promise<void> {
1289
- const next = this.lifecycleChain.then(work, work);
1290
- this.lifecycleChain = next.then(
1291
- () => undefined,
1292
- () => undefined,
1293
- );
1294
- return next;
1295
- }
1296
- }