pi-subagents 0.34.0 → 0.35.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 (98) hide show
  1. package/CHANGELOG.md +78 -9
  2. package/README.md +213 -32
  3. package/index.ts +1 -0
  4. package/install.mjs +1 -1
  5. package/package.json +23 -8
  6. package/prompts/review-loop.md +3 -1
  7. package/skills/pi-subagents/SKILL.md +87 -25
  8. package/src/agents/agent-management.ts +82 -15
  9. package/src/agents/agent-serializer.ts +19 -0
  10. package/src/agents/agents.ts +91 -49
  11. package/src/agents/frontmatter.ts +67 -13
  12. package/src/agents/skills.ts +25 -12
  13. package/src/api/background-work.ts +197 -0
  14. package/src/api/delegation.ts +158 -0
  15. package/src/extension/chain-validation.ts +165 -0
  16. package/src/extension/doctor.ts +15 -0
  17. package/src/extension/fanout-child.ts +3 -1
  18. package/src/extension/index.ts +65 -124
  19. package/src/extension/rpc.ts +10 -2
  20. package/src/extension/schemas.ts +18 -14
  21. package/src/extension/steering-notices.ts +35 -0
  22. package/src/extension/tool-description.ts +20 -9
  23. package/src/intercom/intercom-bridge.ts +3 -2
  24. package/src/intercom/native-supervisor-channel.ts +9 -1
  25. package/src/intercom/result-intercom.ts +4 -0
  26. package/src/runs/background/async-execution.ts +293 -44
  27. package/src/runs/background/async-job-tracker.ts +56 -9
  28. package/src/runs/background/async-resume.ts +159 -52
  29. package/src/runs/background/async-status.ts +25 -18
  30. package/src/runs/background/auto-drain.ts +67 -0
  31. package/src/runs/background/chain-root-attachment.ts +16 -8
  32. package/src/runs/background/control-channel.ts +260 -13
  33. package/src/runs/background/fleet-view.ts +23 -2
  34. package/src/runs/background/notify.ts +79 -10
  35. package/src/runs/background/result-watcher.ts +12 -9
  36. package/src/runs/background/run-id-resolver.ts +14 -2
  37. package/src/runs/background/run-status.ts +23 -15
  38. package/src/runs/background/scheduled-runs.ts +3 -0
  39. package/src/runs/background/stale-run-reconciler.ts +32 -10
  40. package/src/runs/background/steering.ts +237 -0
  41. package/src/runs/background/subagent-runner.ts +898 -236
  42. package/src/runs/background/subagent-wait.ts +484 -0
  43. package/src/runs/background/top-level-async.ts +2 -1
  44. package/src/runs/background/wait-config.ts +36 -0
  45. package/src/runs/background/wait-tool.ts +26 -0
  46. package/src/runs/foreground/async-steering-action.ts +230 -0
  47. package/src/runs/foreground/chain-clarify.ts +22 -6
  48. package/src/runs/foreground/chain-execution.ts +50 -32
  49. package/src/runs/foreground/execution.ts +308 -94
  50. package/src/runs/foreground/subagent-executor.ts +592 -268
  51. package/src/runs/shared/acceptance.ts +355 -97
  52. package/src/runs/shared/child-protocol.ts +121 -0
  53. package/src/runs/shared/completion-guard.ts +8 -127
  54. package/src/runs/shared/dynamic-fanout.ts +6 -4
  55. package/src/runs/shared/model-fallback.ts +36 -0
  56. package/src/runs/shared/nested-events.ts +9 -4
  57. package/src/runs/shared/nested-render.ts +4 -1
  58. package/src/runs/shared/parallel-utils.ts +7 -0
  59. package/src/runs/shared/pi-args.ts +34 -7
  60. package/src/runs/shared/pi-spawn.ts +18 -12
  61. package/src/runs/shared/session-lease.ts +279 -0
  62. package/src/runs/shared/single-output.ts +61 -6
  63. package/src/runs/shared/spawn-budget.ts +128 -0
  64. package/src/runs/shared/subagent-control.ts +10 -6
  65. package/src/runs/shared/subagent-prompt-runtime.ts +127 -26
  66. package/src/runs/shared/task-intent.ts +176 -0
  67. package/src/runs/shared/tool-availability.ts +65 -0
  68. package/src/runs/shared/turn-budget.ts +49 -4
  69. package/src/shared/atomic-json.ts +4 -1
  70. package/src/shared/fork-context.ts +28 -3
  71. package/src/shared/model-info.ts +7 -4
  72. package/src/shared/status-format.ts +7 -1
  73. package/src/shared/types.ts +203 -25
  74. package/src/shared/utils.ts +35 -7
  75. package/src/slash/delegation-adapters.ts +457 -0
  76. package/src/slash/delegation-request.ts +103 -0
  77. package/src/slash/prompt-template-bridge.ts +167 -344
  78. package/src/slash/slash-commands.ts +239 -6
  79. package/src/slash/subagents-admin.ts +428 -0
  80. package/src/slash/subagents-editor.ts +86 -0
  81. package/src/tui/fleet.ts +405 -0
  82. package/src/tui/render.ts +90 -16
  83. package/src/watchdog/change-signature.ts +127 -0
  84. package/src/watchdog/child-status.ts +205 -0
  85. package/src/watchdog/emission-guard.ts +123 -0
  86. package/src/watchdog/lsp-diagnostics.ts +532 -0
  87. package/src/watchdog/model-selection.ts +167 -0
  88. package/src/watchdog/register-child.ts +117 -0
  89. package/src/watchdog/register-main.ts +433 -0
  90. package/src/watchdog/render.ts +54 -0
  91. package/src/watchdog/review.ts +293 -0
  92. package/src/watchdog/runtime.ts +712 -0
  93. package/src/watchdog/settings.ts +528 -0
  94. package/src/watchdog/tool-actions.ts +155 -0
  95. package/src/watchdog/turn-delta.ts +161 -0
  96. package/src/watchdog/types.ts +188 -0
  97. package/src/watchdog/warning-format.ts +73 -0
  98. package/src/runs/background/wait.ts +0 -394
@@ -1,290 +1,51 @@
1
- export const PROMPT_TEMPLATE_SUBAGENT_REQUEST_EVENT = "prompt-template:subagent:request";
2
- export const PROMPT_TEMPLATE_SUBAGENT_STARTED_EVENT = "prompt-template:subagent:started";
3
- export const PROMPT_TEMPLATE_SUBAGENT_RESPONSE_EVENT = "prompt-template:subagent:response";
4
- export const PROMPT_TEMPLATE_SUBAGENT_UPDATE_EVENT = "prompt-template:subagent:update";
5
- export const PROMPT_TEMPLATE_SUBAGENT_CANCEL_EVENT = "prompt-template:subagent:cancel";
6
-
7
- interface PromptTemplateDelegationTask {
8
- agent: string;
9
- task: string;
10
- model?: string;
11
- cwd?: string;
12
- }
13
-
14
- interface PromptTemplateDelegationParallelResult {
15
- agent: string;
16
- messages: unknown[];
17
- isError: boolean;
18
- errorText?: string;
19
- }
20
-
21
- interface PromptTemplateDelegationRequest {
22
- requestId: string;
23
- agent: string;
24
- task: string;
25
- tasks?: PromptTemplateDelegationTask[];
26
- context: "fresh" | "fork";
27
- model: string;
28
- cwd: string;
29
- worktree?: boolean;
30
- }
31
-
32
- interface PromptTemplateDelegationResponse extends PromptTemplateDelegationRequest {
33
- messages: unknown[];
34
- parallelResults?: PromptTemplateDelegationParallelResult[];
35
- contentText?: string;
36
- isError: boolean;
37
- errorText?: string;
38
- }
39
-
40
- interface PromptTemplateDelegationTaskProgress {
41
- index?: number;
42
- agent: string;
43
- status?: string;
44
- currentTool?: string;
45
- currentToolArgs?: string;
46
- recentOutput?: string;
47
- recentOutputLines?: string[];
48
- recentTools?: Array<{ tool: string; args: string }>;
49
- model?: string;
50
- toolCount?: number;
51
- durationMs?: number;
52
- tokens?: number;
53
- }
54
-
55
- interface PromptTemplateDelegationUpdate {
56
- requestId: string;
57
- currentTool?: string;
58
- currentToolArgs?: string;
59
- recentOutput?: string;
60
- recentOutputLines?: string[];
61
- recentTools?: Array<{ tool: string; args: string }>;
62
- model?: string;
63
- toolCount?: number;
64
- durationMs?: number;
65
- tokens?: number;
66
- taskProgress?: PromptTemplateDelegationTaskProgress[];
67
- }
1
+ import {
2
+ SUBAGENT_DELEGATION_CANCEL_EVENT,
3
+ SUBAGENT_DELEGATION_PROTOCOL_VERSION,
4
+ SUBAGENT_DELEGATION_REQUEST_EVENT,
5
+ SUBAGENT_DELEGATION_RESPONSE_EVENT,
6
+ SUBAGENT_DELEGATION_STARTED_EVENT,
7
+ SUBAGENT_DELEGATION_UPDATE_EVENT,
8
+ type SubagentDelegationRequest,
9
+ type SubagentDelegationResponse,
10
+ } from "../api/delegation.ts";
11
+ import { parseSubagentDelegationRequest } from "./delegation-request.ts";
12
+ import {
13
+ parsePromptTemplateRequest,
14
+ toDelegationUpdate,
15
+ toLegacyExecutionParams,
16
+ toPromptTemplateResponse,
17
+ toSubagentDelegationExecutionParams,
18
+ toSubagentDelegationResponse,
19
+ toSubagentDelegationUpdate,
20
+ type DelegatedSubagentExecutionParams,
21
+ type PromptTemplateBridgeResult,
22
+ type PromptTemplateDelegationRequest,
23
+ type PromptTemplateDelegationResponse,
24
+ } from "./delegation-adapters.ts";
25
+
26
+ export const PROMPT_TEMPLATE_SUBAGENT_REQUEST_EVENT = SUBAGENT_DELEGATION_REQUEST_EVENT;
27
+ export const PROMPT_TEMPLATE_SUBAGENT_STARTED_EVENT = SUBAGENT_DELEGATION_STARTED_EVENT;
28
+ export const PROMPT_TEMPLATE_SUBAGENT_RESPONSE_EVENT = SUBAGENT_DELEGATION_RESPONSE_EVENT;
29
+ export const PROMPT_TEMPLATE_SUBAGENT_UPDATE_EVENT = SUBAGENT_DELEGATION_UPDATE_EVENT;
30
+ export const PROMPT_TEMPLATE_SUBAGENT_CANCEL_EVENT = SUBAGENT_DELEGATION_CANCEL_EVENT;
68
31
 
69
32
  export interface PromptTemplateBridgeEvents {
70
33
  on(event: string, handler: (data: unknown) => void): (() => void) | void;
71
34
  emit(event: string, data: unknown): void;
72
35
  }
73
36
 
74
- interface PromptTemplateBridgeResult {
75
- isError?: boolean;
76
- content?: unknown;
77
- details?: {
78
- results?: Array<{
79
- agent?: string;
80
- messages?: unknown[];
81
- finalOutput?: string;
82
- toolCalls?: Array<{ text?: string; expandedText?: string }>;
83
- exitCode?: number;
84
- error?: string;
85
- model?: string;
86
- }>;
87
- progress?: Array<{
88
- index?: number;
89
- agent?: string;
90
- status?: string;
91
- currentTool?: string;
92
- currentToolArgs?: string;
93
- recentOutput?: string[];
94
- recentTools?: Array<{ tool?: string; args?: string }>;
95
- toolCount?: number;
96
- durationMs?: number;
97
- tokens?: number;
98
- }>;
99
- };
100
- }
101
-
102
37
  interface PromptTemplateBridgeOptions<Ctx extends { cwd?: string }> {
103
38
  events: PromptTemplateBridgeEvents;
104
39
  getContext: () => Ctx | null;
105
40
  execute: (
106
41
  requestId: string,
107
- request: PromptTemplateDelegationRequest,
42
+ params: DelegatedSubagentExecutionParams,
108
43
  signal: AbortSignal,
109
44
  ctx: Ctx,
110
45
  onUpdate: (result: PromptTemplateBridgeResult) => void,
111
46
  ) => Promise<PromptTemplateBridgeResult>;
112
47
  }
113
48
 
114
- function parseDelegationTasks(tasks: unknown): PromptTemplateDelegationTask[] {
115
- if (!Array.isArray(tasks)) return [];
116
- const parsed: PromptTemplateDelegationTask[] = [];
117
- for (const item of tasks) {
118
- if (!item || typeof item !== "object") return [];
119
- const value = item as Partial<PromptTemplateDelegationTask>;
120
- if (typeof value.agent !== "string" || !value.agent.trim()) return [];
121
- if (typeof value.task !== "string" || !value.task.trim()) return [];
122
- const model = typeof value.model === "string" && value.model.trim().length > 0 ? value.model : undefined;
123
- const cwd = typeof value.cwd === "string" && value.cwd.trim().length > 0 ? value.cwd : undefined;
124
- parsed.push({
125
- agent: value.agent,
126
- task: value.task,
127
- ...(model ? { model } : {}),
128
- ...(cwd ? { cwd } : {}),
129
- });
130
- }
131
- return parsed;
132
- }
133
-
134
- function parsePromptTemplateRequest(data: unknown): PromptTemplateDelegationRequest | undefined {
135
- if (!data || typeof data !== "object") return undefined;
136
- const value = data as Partial<PromptTemplateDelegationRequest> & { tasks?: unknown };
137
- if (typeof value.requestId !== "string" || !value.requestId) return undefined;
138
- if (typeof value.model !== "string" || !value.model) return undefined;
139
- if (typeof value.cwd !== "string" || !value.cwd) return undefined;
140
- if (value.context !== "fresh" && value.context !== "fork") return undefined;
141
- const tasks = parseDelegationTasks(value.tasks);
142
- const worktree = value.worktree === true ? true : undefined;
143
- const hasSingle =
144
- typeof value.agent === "string" &&
145
- value.agent.length > 0 &&
146
- typeof value.task === "string" &&
147
- value.task.length > 0;
148
- if (!hasSingle && tasks.length === 0) return undefined;
149
-
150
- const fallbackTask = tasks[0];
151
- return {
152
- requestId: value.requestId,
153
- agent: hasSingle ? value.agent : fallbackTask!.agent,
154
- task: hasSingle ? value.task : fallbackTask!.task,
155
- ...(tasks.length > 0 ? { tasks } : {}),
156
- context: value.context,
157
- model: value.model,
158
- cwd: value.cwd,
159
- ...(worktree ? { worktree } : {}),
160
- };
161
- }
162
-
163
- function firstTextContent(content: unknown): string | undefined {
164
- if (!Array.isArray(content)) return undefined;
165
- for (const part of content) {
166
- if (!part || typeof part !== "object") continue;
167
- if ((part as { type?: string }).type !== "text") continue;
168
- const text = (part as { text?: unknown }).text;
169
- if (typeof text === "string" && text.trim()) return text.trim();
170
- }
171
- return undefined;
172
- }
173
-
174
- function filterRecentOutput(lines: string[] | undefined): string[] | undefined {
175
- if (!lines || lines.length === 0) return undefined;
176
- const filtered = lines.filter((line) => typeof line === "string" && line.trim() && line.trim() !== "(running...)");
177
- if (filtered.length === 0) return undefined;
178
- return filtered;
179
- }
180
-
181
- function sanitizeRecentTools(
182
- tools: Array<{ tool?: string; args?: string }> | undefined,
183
- ): Array<{ tool: string; args: string }> | undefined {
184
- if (!tools || tools.length === 0) return undefined;
185
- const sanitized = tools.flatMap((entry) => {
186
- if (typeof entry.tool !== "string" || entry.tool.trim().length === 0) return [];
187
- return [{
188
- tool: entry.tool,
189
- args: typeof entry.args === "string" ? entry.args : String(entry.args ?? ""),
190
- }];
191
- });
192
- return sanitized.length > 0 ? sanitized : undefined;
193
- }
194
-
195
- function resolveProgressModel(
196
- update: PromptTemplateBridgeResult,
197
- entry: { index?: number; agent?: string },
198
- ): string | undefined {
199
- const results = update.details?.results;
200
- if (!results || results.length === 0) return undefined;
201
- if (typeof entry.index === "number" && entry.index >= 0) {
202
- const byIndex = results[entry.index];
203
- if (typeof byIndex?.model === "string") return byIndex.model;
204
- }
205
- if (entry.agent) {
206
- const byAgent = results.find((result) => result.agent === entry.agent && typeof result.model === "string");
207
- if (byAgent?.model) return byAgent.model;
208
- }
209
- const firstWithModel = results.find((result) => typeof result.model === "string");
210
- return firstWithModel?.model;
211
- }
212
-
213
- function toolCallNameFromSummary(summary: { text?: string; expandedText?: string }): string | undefined {
214
- const text = typeof summary.expandedText === "string" && summary.expandedText.trim().length > 0
215
- ? summary.expandedText.trim()
216
- : typeof summary.text === "string"
217
- ? summary.text.trim()
218
- : "";
219
- if (!text) return undefined;
220
- if (text.startsWith("$ ")) return "bash";
221
- return text.match(/^[A-Za-z_][\w.-]*/)?.[0];
222
- }
223
-
224
- function buildDelegationMessages(
225
- result: { messages?: unknown[]; finalOutput?: string; toolCalls?: Array<{ text?: string; expandedText?: string }> },
226
- fallbackText?: string,
227
- ): unknown[] {
228
- if (Array.isArray(result.messages) && result.messages.length > 0) return result.messages;
229
- const toolCallParts = (result.toolCalls ?? []).flatMap((summary) => {
230
- const name = toolCallNameFromSummary(summary);
231
- return name ? [{ type: "toolCall", name, arguments: { summary: summary.expandedText ?? summary.text ?? "" } }] : [];
232
- });
233
- const text = typeof result.finalOutput === "string" && result.finalOutput.trim().length > 0
234
- ? result.finalOutput.trim()
235
- : fallbackText;
236
- const content = [
237
- ...toolCallParts,
238
- ...(text ? [{ type: "text", text }] : []),
239
- ];
240
- if (content.length === 0) return [];
241
- return [{ role: "assistant", content }];
242
- }
243
-
244
- function toDelegationUpdate(requestId: string, update: PromptTemplateBridgeResult): PromptTemplateDelegationUpdate | undefined {
245
- const progress = update.details?.progress?.[0];
246
- const taskProgress = update.details?.progress?.map((entry) => {
247
- const lastOutput = entry.recentOutput?.[entry.recentOutput.length - 1];
248
- const safeLastOutput =
249
- typeof lastOutput === "string" && lastOutput.trim() && lastOutput !== "(running...)"
250
- ? lastOutput
251
- : undefined;
252
- return {
253
- index: entry.index,
254
- agent: entry.agent ?? "delegate",
255
- status: entry.status,
256
- currentTool: entry.currentTool,
257
- currentToolArgs: entry.currentToolArgs,
258
- recentOutput: safeLastOutput,
259
- recentOutputLines: filterRecentOutput(entry.recentOutput),
260
- recentTools: sanitizeRecentTools(entry.recentTools),
261
- model: resolveProgressModel(update, entry),
262
- toolCount: entry.toolCount,
263
- durationMs: entry.durationMs,
264
- tokens: entry.tokens,
265
- };
266
- });
267
- if (!progress && (!taskProgress || taskProgress.length === 0)) return undefined;
268
- const lastOutput = progress?.recentOutput?.[progress.recentOutput.length - 1];
269
- const safeLastOutput =
270
- typeof lastOutput === "string" && lastOutput.trim() && lastOutput !== "(running...)"
271
- ? lastOutput
272
- : undefined;
273
- return {
274
- requestId,
275
- currentTool: progress?.currentTool,
276
- currentToolArgs: progress?.currentToolArgs,
277
- recentOutput: safeLastOutput,
278
- recentOutputLines: filterRecentOutput(progress?.recentOutput),
279
- recentTools: sanitizeRecentTools(progress?.recentTools),
280
- model: progress ? resolveProgressModel(update, progress) : undefined,
281
- toolCount: progress?.toolCount,
282
- durationMs: progress?.durationMs,
283
- tokens: progress?.tokens,
284
- taskProgress,
285
- };
286
- }
287
-
288
49
  export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: string }>(
289
50
  options: PromptTemplateBridgeOptions<Ctx>,
290
51
  ): {
@@ -292,126 +53,188 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
292
53
  dispose: () => void;
293
54
  } {
294
55
  const controllers = new Map<string, AbortController>();
295
- const pendingCancels = new Set<string>();
56
+ const pendingCancels = new Map<string, true>();
296
57
  const subscriptions: Array<() => void> = [];
58
+ let disposed = false;
297
59
 
298
60
  const subscribe = (event: string, handler: (data: unknown) => void): void => {
299
61
  const unsubscribe = options.events.on(event, handler);
300
62
  if (typeof unsubscribe === "function") subscriptions.push(unsubscribe);
301
63
  };
64
+ const ownsRequest = (requestId: string, controller: AbortController): boolean =>
65
+ !disposed && controllers.get(requestId) === controller;
66
+ const rememberPendingCancel = (requestId: string): void => {
67
+ pendingCancels.delete(requestId);
68
+ pendingCancels.set(requestId, true);
69
+ while (pendingCancels.size > 256) {
70
+ const oldest = pendingCancels.keys().next().value;
71
+ if (typeof oldest !== "string") break;
72
+ pendingCancels.delete(oldest);
73
+ }
74
+ };
302
75
 
303
76
  subscribe(PROMPT_TEMPLATE_SUBAGENT_CANCEL_EVENT, (data) => {
304
- if (!data || typeof data !== "object") return;
77
+ if (!data || typeof data !== "object" || Array.isArray(data)) return;
78
+ if (Object.hasOwn(data, "version")) {
79
+ const value = data as Record<string, unknown>;
80
+ if (value.version !== SUBAGENT_DELEGATION_PROTOCOL_VERSION) return;
81
+ if (Object.keys(value).some((key) => key !== "version" && key !== "requestId")) return;
82
+ }
305
83
  const requestId = (data as { requestId?: unknown }).requestId;
306
- if (typeof requestId !== "string") return;
84
+ if (typeof requestId !== "string" || !requestId.trim() || requestId.length > 256 || /[\r\n]/.test(requestId)) return;
307
85
  const controller = controllers.get(requestId);
308
86
  if (controller) {
309
87
  controller.abort();
310
88
  return;
311
89
  }
312
- pendingCancels.add(requestId);
90
+ rememberPendingCancel(requestId);
313
91
  });
314
92
 
315
93
  subscribe(PROMPT_TEMPLATE_SUBAGENT_REQUEST_EVENT, async (data) => {
316
- const request = parsePromptTemplateRequest(data);
317
- if (!request) return;
94
+ const isVersioned = !!data && typeof data === "object" && Object.hasOwn(data, "version");
95
+ let requestId: string;
96
+ let params: DelegatedSubagentExecutionParams;
97
+ let versionedRequest: SubagentDelegationRequest | undefined;
98
+ let legacyRequest: PromptTemplateDelegationRequest | undefined;
99
+
100
+ if (isVersioned) {
101
+ const parsed = parseSubagentDelegationRequest(data);
102
+ if (parsed.ok === false) {
103
+ if (!disposed && parsed.requestId && !controllers.has(parsed.requestId)) {
104
+ options.events.emit(SUBAGENT_DELEGATION_RESPONSE_EVENT, {
105
+ version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
106
+ requestId: parsed.requestId,
107
+ status: "invalid_request",
108
+ error: parsed.error,
109
+ } satisfies SubagentDelegationResponse);
110
+ }
111
+ return;
112
+ }
113
+ versionedRequest = parsed.request;
114
+ requestId = parsed.request.requestId;
115
+ params = toSubagentDelegationExecutionParams(parsed.request);
116
+ } else {
117
+ legacyRequest = parsePromptTemplateRequest(data);
118
+ if (!legacyRequest) return;
119
+ requestId = legacyRequest.requestId;
120
+ params = toLegacyExecutionParams(legacyRequest);
121
+ }
318
122
 
123
+ // The first request owns its correlation ID until it settles. A duplicate
124
+ // cannot receive a terminal response without stealing the original caller's.
125
+ if (controllers.has(requestId)) return;
319
126
  const ctx = options.getContext();
320
127
  if (!ctx) {
321
- const response: PromptTemplateDelegationResponse = {
322
- ...request,
323
- messages: [],
324
- isError: true,
325
- errorText: "No active extension context for delegated subagent execution.",
326
- };
327
- options.events.emit(PROMPT_TEMPLATE_SUBAGENT_RESPONSE_EVENT, response);
128
+ if (versionedRequest) {
129
+ options.events.emit(SUBAGENT_DELEGATION_RESPONSE_EVENT, {
130
+ version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
131
+ requestId,
132
+ status: "unavailable_context",
133
+ error: "No active extension context for delegated subagent execution.",
134
+ } satisfies SubagentDelegationResponse);
135
+ } else if (legacyRequest) {
136
+ options.events.emit(PROMPT_TEMPLATE_SUBAGENT_RESPONSE_EVENT, {
137
+ ...legacyRequest,
138
+ messages: [],
139
+ isError: true,
140
+ errorText: "No active extension context for delegated subagent execution.",
141
+ } satisfies PromptTemplateDelegationResponse);
142
+ }
328
143
  return;
329
144
  }
330
145
 
331
146
  const controller = new AbortController();
332
- controllers.set(request.requestId, controller);
333
-
334
- if (pendingCancels.delete(request.requestId)) {
335
- controller.abort();
336
- const response: PromptTemplateDelegationResponse = {
337
- ...request,
338
- messages: [],
339
- isError: true,
340
- errorText: "Delegated prompt cancelled.",
341
- };
342
- options.events.emit(PROMPT_TEMPLATE_SUBAGENT_RESPONSE_EVENT, response);
343
- controllers.delete(request.requestId);
147
+ controllers.set(requestId, controller);
148
+ if (pendingCancels.delete(requestId)) controller.abort();
149
+ if (controller.signal.aborted) {
150
+ if (versionedRequest) {
151
+ options.events.emit(SUBAGENT_DELEGATION_RESPONSE_EVENT, {
152
+ version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
153
+ requestId,
154
+ status: "cancelled",
155
+ } satisfies SubagentDelegationResponse);
156
+ } else if (legacyRequest) {
157
+ options.events.emit(PROMPT_TEMPLATE_SUBAGENT_RESPONSE_EVENT, {
158
+ ...legacyRequest,
159
+ messages: [],
160
+ isError: true,
161
+ errorText: "Delegated prompt cancelled.",
162
+ } satisfies PromptTemplateDelegationResponse);
163
+ }
164
+ controllers.delete(requestId);
344
165
  return;
345
166
  }
346
167
 
347
- options.events.emit(PROMPT_TEMPLATE_SUBAGENT_STARTED_EVENT, { requestId: request.requestId });
168
+ options.events.emit(
169
+ versionedRequest ? SUBAGENT_DELEGATION_STARTED_EVENT : PROMPT_TEMPLATE_SUBAGENT_STARTED_EVENT,
170
+ versionedRequest
171
+ ? { version: SUBAGENT_DELEGATION_PROTOCOL_VERSION, requestId }
172
+ : { requestId },
173
+ );
348
174
 
349
175
  try {
350
176
  const result = await options.execute(
351
- request.requestId,
352
- request,
177
+ requestId,
178
+ params,
353
179
  controller.signal,
354
180
  ctx,
355
181
  (update) => {
356
- const payload = toDelegationUpdate(request.requestId, update);
357
- if (!payload) return;
358
- options.events.emit(PROMPT_TEMPLATE_SUBAGENT_UPDATE_EVENT, payload);
182
+ if (!ownsRequest(requestId, controller)) return;
183
+ if (versionedRequest) {
184
+ const payload = toSubagentDelegationUpdate(requestId, update);
185
+ if (payload) options.events.emit(SUBAGENT_DELEGATION_UPDATE_EVENT, payload);
186
+ return;
187
+ }
188
+ const payload = toDelegationUpdate(requestId, update);
189
+ if (payload) options.events.emit(PROMPT_TEMPLATE_SUBAGENT_UPDATE_EVENT, payload);
359
190
  },
360
191
  );
361
- const contentText = firstTextContent(result.content);
362
- const messages = buildDelegationMessages(result.details?.results?.[0] ?? {}, contentText);
363
- const parallelResults = request.tasks
364
- ? request.tasks.map<PromptTemplateDelegationParallelResult>((task, index) => {
365
- const step = result.details?.results?.[index];
366
- if (!step) {
367
- return {
368
- agent: task.agent,
369
- messages: [],
370
- isError: true,
371
- errorText: "Missing result for delegated parallel task.",
372
- };
373
- }
374
- const exitCode = typeof step.exitCode === "number" ? step.exitCode : undefined;
375
- const errorText = step.error;
376
- return {
377
- agent: step.agent ?? task.agent,
378
- messages: buildDelegationMessages(step),
379
- isError: (exitCode !== undefined && exitCode !== 0) || !!errorText,
380
- errorText: errorText || undefined,
381
- };
382
- })
383
- : undefined;
384
- const response: PromptTemplateDelegationResponse = {
385
- ...request,
386
- messages,
387
- ...(parallelResults ? { parallelResults } : {}),
388
- ...(contentText ? { contentText } : {}),
389
- isError: result.isError === true,
390
- errorText: result.isError ? contentText : undefined,
391
- };
392
- options.events.emit(PROMPT_TEMPLATE_SUBAGENT_RESPONSE_EVENT, response);
192
+ if (!ownsRequest(requestId, controller)) return;
193
+ if (versionedRequest) {
194
+ options.events.emit(
195
+ SUBAGENT_DELEGATION_RESPONSE_EVENT,
196
+ toSubagentDelegationResponse(requestId, result, controller.signal.aborted),
197
+ );
198
+ } else if (legacyRequest) {
199
+ options.events.emit(
200
+ PROMPT_TEMPLATE_SUBAGENT_RESPONSE_EVENT,
201
+ controller.signal.aborted
202
+ ? { ...legacyRequest, messages: [], isError: true, errorText: "Delegated prompt cancelled." }
203
+ : toPromptTemplateResponse(legacyRequest, result),
204
+ );
205
+ }
393
206
  } catch (error) {
394
- const response: PromptTemplateDelegationResponse = {
395
- ...request,
396
- messages: [],
397
- isError: true,
398
- errorText: error instanceof Error ? error.message : String(error),
399
- };
400
- options.events.emit(PROMPT_TEMPLATE_SUBAGENT_RESPONSE_EVENT, response);
207
+ if (!ownsRequest(requestId, controller)) return;
208
+ if (versionedRequest) {
209
+ options.events.emit(SUBAGENT_DELEGATION_RESPONSE_EVENT, {
210
+ version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
211
+ requestId,
212
+ status: controller.signal.aborted ? "cancelled" : "failed",
213
+ ...(controller.signal.aborted ? {} : { error: error instanceof Error ? error.message : String(error) }),
214
+ } satisfies SubagentDelegationResponse);
215
+ } else if (legacyRequest) {
216
+ options.events.emit(PROMPT_TEMPLATE_SUBAGENT_RESPONSE_EVENT, {
217
+ ...legacyRequest,
218
+ messages: [],
219
+ isError: true,
220
+ errorText: error instanceof Error ? error.message : String(error),
221
+ } satisfies PromptTemplateDelegationResponse);
222
+ }
401
223
  } finally {
402
- controllers.delete(request.requestId);
224
+ if (controllers.get(requestId) === controller) controllers.delete(requestId);
403
225
  }
404
226
  });
405
227
 
406
228
  return {
407
229
  cancelAll: () => {
408
- for (const controller of controllers.values()) {
409
- controller.abort();
410
- }
230
+ for (const controller of controllers.values()) controller.abort();
411
231
  controllers.clear();
412
232
  pendingCancels.clear();
413
233
  },
414
234
  dispose: () => {
235
+ disposed = true;
236
+ for (const controller of controllers.values()) controller.abort();
237
+ controllers.clear();
415
238
  for (const unsubscribe of subscriptions) unsubscribe();
416
239
  subscriptions.length = 0;
417
240
  pendingCancels.clear();