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
@@ -17,6 +17,18 @@ const DEFAULT_CONFIG_DIR_NAME = ".pi";
17
17
  const PI_CODING_AGENT_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
18
18
  export const PI_CODING_AGENT_PACKAGE_ROOT_ENV = "PI_SUBAGENTS_PI_CODING_AGENT_PACKAGE_ROOT";
19
19
 
20
+ export function resolveWatchPath(
21
+ watchPath: string,
22
+ nativeRealpath: (filePath: string) => string = fs.realpathSync.native,
23
+ ): string {
24
+ // libuv's Windows watcher cannot mix 8.3 registration paths with long event paths.
25
+ try {
26
+ return nativeRealpath(watchPath);
27
+ } catch {
28
+ return watchPath;
29
+ }
30
+ }
31
+
20
32
  function validConfigDirName(value: unknown): string | undefined {
21
33
  return typeof value === "string" && value.trim() ? value : undefined;
22
34
  }
@@ -76,7 +88,7 @@ export function getAgentDir(): string {
76
88
  return configured || path.join(os.homedir(), getConfigDirName(), "agent");
77
89
  }
78
90
 
79
- const statusCache = new Map<string, { mtime: number; status: AsyncStatus }>();
91
+ const statusCache = new Map<string, { mtime: number; ctime: number; size: number; ino: number; status: AsyncStatus }>();
80
92
 
81
93
  function getErrorMessage(error: unknown): string {
82
94
  return error instanceof Error ? error.message : String(error);
@@ -111,7 +123,13 @@ export function readStatus(asyncDir: string): AsyncStatus | null {
111
123
  }
112
124
 
113
125
  const cached = statusCache.get(statusPath);
114
- if (cached && cached.mtime === stat.mtimeMs) {
126
+ if (
127
+ cached
128
+ && cached.mtime === stat.mtimeMs
129
+ && cached.ctime === stat.ctimeMs
130
+ && cached.size === stat.size
131
+ && cached.ino === stat.ino
132
+ ) {
115
133
  return cached.status;
116
134
  }
117
135
 
@@ -134,7 +152,13 @@ export function readStatus(asyncDir: string): AsyncStatus | null {
134
152
  });
135
153
  }
136
154
 
137
- statusCache.set(statusPath, { mtime: stat.mtimeMs, status });
155
+ statusCache.set(statusPath, {
156
+ mtime: stat.mtimeMs,
157
+ ctime: stat.ctimeMs,
158
+ size: stat.size,
159
+ ino: stat.ino,
160
+ status,
161
+ });
138
162
  if (statusCache.size > 50) {
139
163
  const firstKey = statusCache.keys().next().value;
140
164
  if (firstKey) statusCache.delete(firstKey);
@@ -249,18 +273,22 @@ export function getFinalOutput(messages: Message[]): string {
249
273
  const hasAssistantError = ("errorMessage" in msg && typeof msg.errorMessage === "string" && msg.errorMessage.length > 0)
250
274
  || ("stopReason" in msg && msg.stopReason === "error");
251
275
  if (hasAssistantError) continue;
276
+ const messageText = msg.content
277
+ .filter((part) => part.type === "text" && part.text.trim().length > 0)
278
+ .map((part) => part.type === "text" ? part.text : "")
279
+ .join("\n");
252
280
  for (let j = msg.content.length - 1; j >= 0; j--) {
253
281
  const part = msg.content[j];
254
282
  if (part.type !== "text" || part.text.trim().length === 0) continue;
255
283
  validTextParts.push(part.text);
256
- if (/```acceptance-report\s*\n[\s\S]*?```/i.test(part.text)) return part.text;
284
+ if (/```acceptance[-_]report\s*\n[\s\S]*?```/i.test(part.text)) return messageText;
257
285
  for (const match of part.text.matchAll(/```(?:json|jsonc|json5)\s*\n([\s\S]*?)```/gi)) {
258
286
  const body = match[1] ?? "";
259
- if (/"criteriaSatisfied"/.test(body) && /"(?:changedFiles|testsAddedOrUpdated|commandsRun|validationOutput|residualRisks|noStagedFiles|diffSummary|reviewFindings|manualNotes)"/.test(body)) {
260
- return part.text;
287
+ if (/"(?:criteriaSatisfied|criteria_satisfied)"/.test(body) && /"(?:changedFiles|changed_files|testsAddedOrUpdated|tests_added_or_updated|commandsRun|commands_run|validationOutput|validation_output|residualRisks|residual_risks|noStagedFiles|no_staged_files|diffSummary|diff_summary|reviewFindings|review_findings|manualNotes|manual_notes)"/.test(body)) {
288
+ return messageText;
261
289
  }
262
290
  }
263
- if (/ACCEPTANCE_REPORT\s*:/i.test(part.text)) return part.text;
291
+ if (/ACCEPTANCE_REPORT\s*:/i.test(part.text)) return messageText;
264
292
  }
265
293
  }
266
294
  return validTextParts[0] ?? "";
@@ -0,0 +1,457 @@
1
+ import {
2
+ SUBAGENT_DELEGATION_PROTOCOL_VERSION,
3
+ type SubagentDelegationAcceptanceResult,
4
+ type SubagentDelegationRequest,
5
+ type SubagentDelegationResponse,
6
+ type SubagentDelegationStatus,
7
+ type SubagentDelegationUpdate,
8
+ } from "../api/delegation.ts";
9
+ import type { AcceptanceInput, ToolBudgetConfig, TurnBudgetConfig } from "../shared/types.ts";
10
+
11
+ export interface PromptTemplateDelegationTask {
12
+ agent: string;
13
+ task: string;
14
+ model?: string;
15
+ cwd?: string;
16
+ }
17
+
18
+ export interface PromptTemplateDelegationParallelResult {
19
+ agent: string;
20
+ messages: unknown[];
21
+ isError: boolean;
22
+ errorText?: string;
23
+ }
24
+
25
+ export interface PromptTemplateDelegationRequest {
26
+ requestId: string;
27
+ agent: string;
28
+ task: string;
29
+ tasks?: PromptTemplateDelegationTask[];
30
+ context: "fresh" | "fork";
31
+ model: string;
32
+ cwd: string;
33
+ worktree?: boolean;
34
+ }
35
+
36
+ export interface PromptTemplateDelegationResponse extends PromptTemplateDelegationRequest {
37
+ messages: unknown[];
38
+ parallelResults?: PromptTemplateDelegationParallelResult[];
39
+ contentText?: string;
40
+ isError: boolean;
41
+ errorText?: string;
42
+ }
43
+
44
+ interface PromptTemplateDelegationTaskProgress {
45
+ index?: number;
46
+ agent: string;
47
+ status?: string;
48
+ currentTool?: string;
49
+ currentToolArgs?: string;
50
+ recentOutput?: string;
51
+ recentOutputLines?: string[];
52
+ recentTools?: Array<{ tool: string; args: string }>;
53
+ model?: string;
54
+ toolCount?: number;
55
+ durationMs?: number;
56
+ tokens?: number;
57
+ }
58
+
59
+ export interface PromptTemplateDelegationUpdate {
60
+ requestId: string;
61
+ currentTool?: string;
62
+ currentToolArgs?: string;
63
+ recentOutput?: string;
64
+ recentOutputLines?: string[];
65
+ recentTools?: Array<{ tool: string; args: string }>;
66
+ model?: string;
67
+ toolCount?: number;
68
+ durationMs?: number;
69
+ tokens?: number;
70
+ taskProgress?: PromptTemplateDelegationTaskProgress[];
71
+ }
72
+
73
+ export interface PromptTemplateBridgeResult {
74
+ isError?: boolean;
75
+ content?: unknown;
76
+ details?: {
77
+ mode?: "single" | "parallel" | "chain" | "management";
78
+ runId?: string;
79
+ timedOut?: boolean;
80
+ stopped?: boolean;
81
+ results?: Array<{
82
+ agent?: string;
83
+ messages?: unknown[];
84
+ finalOutput?: string;
85
+ toolCalls?: Array<{ text?: string; expandedText?: string }>;
86
+ exitCode?: number;
87
+ error?: string;
88
+ model?: string;
89
+ interrupted?: boolean;
90
+ timedOut?: boolean;
91
+ stopped?: boolean;
92
+ turnBudgetExceeded?: boolean;
93
+ toolBudgetBlocked?: boolean;
94
+ savedOutputPath?: string;
95
+ sessionFile?: string;
96
+ acceptance?: SubagentDelegationAcceptanceResult;
97
+ usage?: { turns?: number };
98
+ progressSummary?: { toolCount?: number; durationMs?: number; tokens?: number };
99
+ skillsWarning?: string;
100
+ outputSaveError?: string;
101
+ transcriptError?: string;
102
+ }>;
103
+ progress?: Array<{
104
+ index?: number;
105
+ agent?: string;
106
+ status?: string;
107
+ currentTool?: string;
108
+ currentToolArgs?: string;
109
+ recentOutput?: string[];
110
+ recentTools?: Array<{ tool?: string; args?: string }>;
111
+ toolCount?: number;
112
+ durationMs?: number;
113
+ tokens?: number;
114
+ }>;
115
+ };
116
+ }
117
+
118
+ export interface DelegatedSubagentExecutionParams {
119
+ agent?: string;
120
+ task?: string;
121
+ tasks?: PromptTemplateDelegationTask[];
122
+ context: "fresh" | "fork";
123
+ model?: string;
124
+ cwd: string;
125
+ worktree?: boolean;
126
+ timeoutMs?: number;
127
+ turnBudget?: TurnBudgetConfig;
128
+ toolBudget?: ToolBudgetConfig;
129
+ skill?: string | string[] | boolean;
130
+ output?: string | boolean;
131
+ outputMode?: "inline" | "file-only";
132
+ acceptance?: AcceptanceInput;
133
+ artifacts?: boolean;
134
+ async: false;
135
+ foregroundOnly: true;
136
+ clarify: false;
137
+ }
138
+
139
+ function parseDelegationTasks(tasks: unknown): PromptTemplateDelegationTask[] {
140
+ if (!Array.isArray(tasks)) return [];
141
+ const parsed: PromptTemplateDelegationTask[] = [];
142
+ for (const item of tasks) {
143
+ if (!item || typeof item !== "object") return [];
144
+ const value = item as Partial<PromptTemplateDelegationTask>;
145
+ if (typeof value.agent !== "string" || !value.agent.trim()) return [];
146
+ if (typeof value.task !== "string" || !value.task.trim()) return [];
147
+ const model = typeof value.model === "string" && value.model.trim().length > 0 ? value.model : undefined;
148
+ const cwd = typeof value.cwd === "string" && value.cwd.trim().length > 0 ? value.cwd : undefined;
149
+ parsed.push({
150
+ agent: value.agent,
151
+ task: value.task,
152
+ ...(model ? { model } : {}),
153
+ ...(cwd ? { cwd } : {}),
154
+ });
155
+ }
156
+ return parsed;
157
+ }
158
+
159
+ export function parsePromptTemplateRequest(data: unknown): PromptTemplateDelegationRequest | undefined {
160
+ if (!data || typeof data !== "object") return undefined;
161
+ const value = data as Partial<PromptTemplateDelegationRequest> & { tasks?: unknown };
162
+ if (typeof value.requestId !== "string" || !value.requestId) return undefined;
163
+ if (typeof value.model !== "string" || !value.model) return undefined;
164
+ if (typeof value.cwd !== "string" || !value.cwd) return undefined;
165
+ if (value.context !== "fresh" && value.context !== "fork") return undefined;
166
+ const tasks = parseDelegationTasks(value.tasks);
167
+ const worktree = value.worktree === true ? true : undefined;
168
+ const hasSingle =
169
+ typeof value.agent === "string" &&
170
+ value.agent.length > 0 &&
171
+ typeof value.task === "string" &&
172
+ value.task.length > 0;
173
+ if (!hasSingle && tasks.length === 0) return undefined;
174
+
175
+ const fallbackTask = tasks[0];
176
+ return {
177
+ requestId: value.requestId,
178
+ agent: hasSingle ? value.agent : fallbackTask!.agent,
179
+ task: hasSingle ? value.task : fallbackTask!.task,
180
+ ...(tasks.length > 0 ? { tasks } : {}),
181
+ context: value.context,
182
+ model: value.model,
183
+ cwd: value.cwd,
184
+ ...(worktree ? { worktree } : {}),
185
+ };
186
+ }
187
+
188
+ function firstTextContent(content: unknown): string | undefined {
189
+ if (!Array.isArray(content)) return undefined;
190
+ for (const part of content) {
191
+ if (!part || typeof part !== "object") continue;
192
+ if ((part as { type?: string }).type !== "text") continue;
193
+ const text = (part as { text?: unknown }).text;
194
+ if (typeof text === "string" && text.trim()) return text.trim();
195
+ }
196
+ return undefined;
197
+ }
198
+
199
+ function filterRecentOutput(lines: string[] | undefined): string[] | undefined {
200
+ if (!lines || lines.length === 0) return undefined;
201
+ const filtered = lines.filter((line) => typeof line === "string" && line.trim() && line.trim() !== "(running...)");
202
+ if (filtered.length === 0) return undefined;
203
+ return filtered;
204
+ }
205
+
206
+ function sanitizeRecentTools(
207
+ tools: Array<{ tool?: string; args?: string }> | undefined,
208
+ ): Array<{ tool: string; args: string }> | undefined {
209
+ if (!tools || tools.length === 0) return undefined;
210
+ const sanitized = tools.flatMap((entry) => {
211
+ if (typeof entry.tool !== "string" || entry.tool.trim().length === 0) return [];
212
+ return [{
213
+ tool: entry.tool,
214
+ args: typeof entry.args === "string" ? entry.args : String(entry.args ?? ""),
215
+ }];
216
+ });
217
+ return sanitized.length > 0 ? sanitized : undefined;
218
+ }
219
+
220
+ function resolveProgressModel(
221
+ update: PromptTemplateBridgeResult,
222
+ entry: { index?: number; agent?: string },
223
+ ): string | undefined {
224
+ const results = update.details?.results;
225
+ if (!results || results.length === 0) return undefined;
226
+ if (typeof entry.index === "number" && entry.index >= 0) {
227
+ const byIndex = results[entry.index];
228
+ if (typeof byIndex?.model === "string") return byIndex.model;
229
+ }
230
+ if (entry.agent) {
231
+ const byAgent = results.find((result) => result.agent === entry.agent && typeof result.model === "string");
232
+ if (byAgent?.model) return byAgent.model;
233
+ }
234
+ const firstWithModel = results.find((result) => typeof result.model === "string");
235
+ return firstWithModel?.model;
236
+ }
237
+
238
+ function toolCallNameFromSummary(summary: { text?: string; expandedText?: string }): string | undefined {
239
+ const text = typeof summary.expandedText === "string" && summary.expandedText.trim().length > 0
240
+ ? summary.expandedText.trim()
241
+ : typeof summary.text === "string"
242
+ ? summary.text.trim()
243
+ : "";
244
+ if (!text) return undefined;
245
+ if (text.startsWith("$ ")) return "bash";
246
+ return text.match(/^[A-Za-z_][\w.-]*/)?.[0];
247
+ }
248
+
249
+ function buildDelegationMessages(
250
+ result: { messages?: unknown[]; finalOutput?: string; toolCalls?: Array<{ text?: string; expandedText?: string }> },
251
+ fallbackText?: string,
252
+ ): unknown[] {
253
+ if (Array.isArray(result.messages) && result.messages.length > 0) return result.messages;
254
+ const toolCallParts = (result.toolCalls ?? []).flatMap((summary) => {
255
+ const name = toolCallNameFromSummary(summary);
256
+ return name ? [{ type: "toolCall", name, arguments: { summary: summary.expandedText ?? summary.text ?? "" } }] : [];
257
+ });
258
+ const text = typeof result.finalOutput === "string" && result.finalOutput.trim().length > 0
259
+ ? result.finalOutput.trim()
260
+ : fallbackText;
261
+ const content = [
262
+ ...toolCallParts,
263
+ ...(text ? [{ type: "text", text }] : []),
264
+ ];
265
+ if (content.length === 0) return [];
266
+ return [{ role: "assistant", content }];
267
+ }
268
+
269
+ export function toDelegationUpdate(requestId: string, update: PromptTemplateBridgeResult): PromptTemplateDelegationUpdate | undefined {
270
+ const progress = update.details?.progress?.[0];
271
+ const taskProgress = update.details?.progress?.map((entry) => {
272
+ const lastOutput = entry.recentOutput?.[entry.recentOutput.length - 1];
273
+ const safeLastOutput =
274
+ typeof lastOutput === "string" && lastOutput.trim() && lastOutput !== "(running...)"
275
+ ? lastOutput
276
+ : undefined;
277
+ return {
278
+ index: entry.index,
279
+ agent: entry.agent ?? "delegate",
280
+ status: entry.status,
281
+ currentTool: entry.currentTool,
282
+ currentToolArgs: entry.currentToolArgs,
283
+ recentOutput: safeLastOutput,
284
+ recentOutputLines: filterRecentOutput(entry.recentOutput),
285
+ recentTools: sanitizeRecentTools(entry.recentTools),
286
+ model: resolveProgressModel(update, entry),
287
+ toolCount: entry.toolCount,
288
+ durationMs: entry.durationMs,
289
+ tokens: entry.tokens,
290
+ };
291
+ });
292
+ if (!progress && (!taskProgress || taskProgress.length === 0)) return undefined;
293
+ const lastOutput = progress?.recentOutput?.[progress.recentOutput.length - 1];
294
+ const safeLastOutput =
295
+ typeof lastOutput === "string" && lastOutput.trim() && lastOutput !== "(running...)"
296
+ ? lastOutput
297
+ : undefined;
298
+ return {
299
+ requestId,
300
+ currentTool: progress?.currentTool,
301
+ currentToolArgs: progress?.currentToolArgs,
302
+ recentOutput: safeLastOutput,
303
+ recentOutputLines: filterRecentOutput(progress?.recentOutput),
304
+ recentTools: sanitizeRecentTools(progress?.recentTools),
305
+ model: progress ? resolveProgressModel(update, progress) : undefined,
306
+ toolCount: progress?.toolCount,
307
+ durationMs: progress?.durationMs,
308
+ tokens: progress?.tokens,
309
+ taskProgress,
310
+ };
311
+ }
312
+
313
+ export function toLegacyExecutionParams(request: PromptTemplateDelegationRequest): DelegatedSubagentExecutionParams {
314
+ if (request.tasks && request.tasks.length > 0) {
315
+ return {
316
+ tasks: request.tasks,
317
+ context: request.context,
318
+ cwd: request.cwd,
319
+ worktree: request.worktree,
320
+ async: false,
321
+ foregroundOnly: true,
322
+ clarify: false,
323
+ };
324
+ }
325
+ return {
326
+ agent: request.agent,
327
+ task: request.task,
328
+ context: request.context,
329
+ model: request.model,
330
+ cwd: request.cwd,
331
+ async: false,
332
+ foregroundOnly: true,
333
+ clarify: false,
334
+ };
335
+ }
336
+
337
+ export function toSubagentDelegationExecutionParams(request: SubagentDelegationRequest): DelegatedSubagentExecutionParams {
338
+ return {
339
+ agent: request.agent,
340
+ task: request.task,
341
+ context: request.context,
342
+ cwd: request.cwd,
343
+ model: request.model,
344
+ timeoutMs: request.timeoutMs,
345
+ turnBudget: request.turnBudget,
346
+ toolBudget: request.toolBudget,
347
+ skill: request.skill,
348
+ output: request.output,
349
+ outputMode: request.outputMode,
350
+ acceptance: request.acceptance,
351
+ artifacts: request.artifacts,
352
+ async: false,
353
+ foregroundOnly: true,
354
+ clarify: false,
355
+ };
356
+ }
357
+
358
+ export function toSubagentDelegationUpdate(requestId: string, result: PromptTemplateBridgeResult): SubagentDelegationUpdate | undefined {
359
+ const legacy = toDelegationUpdate(requestId, result);
360
+ if (!legacy) return undefined;
361
+ return {
362
+ version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
363
+ requestId,
364
+ ...(legacy.currentTool ? { currentTool: legacy.currentTool } : {}),
365
+ ...(legacy.currentToolArgs ? { currentToolArgs: legacy.currentToolArgs } : {}),
366
+ ...(legacy.recentOutput ? { recentOutput: legacy.recentOutput } : {}),
367
+ ...(legacy.recentOutputLines ? { recentOutputLines: legacy.recentOutputLines } : {}),
368
+ ...(legacy.recentTools ? { recentTools: legacy.recentTools } : {}),
369
+ ...(legacy.model ? { model: legacy.model } : {}),
370
+ ...(typeof legacy.toolCount === "number" ? { toolCount: legacy.toolCount } : {}),
371
+ ...(typeof legacy.durationMs === "number" ? { durationMs: legacy.durationMs } : {}),
372
+ ...(typeof legacy.tokens === "number" ? { tokens: legacy.tokens } : {}),
373
+ };
374
+ }
375
+
376
+ function resolveSubagentDelegationStatus(
377
+ result: PromptTemplateBridgeResult,
378
+ aborted: boolean,
379
+ ): SubagentDelegationStatus {
380
+ if (aborted) return "cancelled";
381
+ const child = result.details?.results?.[0];
382
+ if (!child) return "failed";
383
+ if (result.details?.timedOut || child.timedOut) return "timed_out";
384
+ if (child?.turnBudgetExceeded) return "turn_budget_exhausted";
385
+ if (child?.toolBudgetBlocked) return "tool_budget_exhausted";
386
+ if (child?.acceptance?.status === "rejected" && child.acceptance.explicit) return "acceptance_failed";
387
+ if (result.details?.stopped || child?.stopped || child?.interrupted) return "interrupted";
388
+ if (result.isError || child?.error || (typeof child?.exitCode === "number" && child.exitCode !== 0)) return "failed";
389
+ return "completed";
390
+ }
391
+
392
+ export function toSubagentDelegationResponse(
393
+ requestId: string,
394
+ result: PromptTemplateBridgeResult,
395
+ aborted: boolean,
396
+ ): SubagentDelegationResponse {
397
+ const child = result.details?.results?.[0];
398
+ const progress = child?.progressSummary ?? result.details?.progress?.[0];
399
+ const warnings = [child?.skillsWarning, child?.outputSaveError, child?.transcriptError]
400
+ .filter((warning): warning is string => typeof warning === "string" && warning.length > 0);
401
+ const status = resolveSubagentDelegationStatus(result, aborted);
402
+ const fallbackError = status === "failed" ? firstTextContent(result.content) : undefined;
403
+ return {
404
+ version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
405
+ requestId,
406
+ status,
407
+ ...(child?.error || fallbackError ? { error: child?.error ?? fallbackError } : {}),
408
+ ...(result.details?.runId ? { runId: result.details.runId } : {}),
409
+ ...(child ? { childIndex: 0 } : {}),
410
+ ...(child?.agent ? { agent: child.agent } : {}),
411
+ ...(child?.model ? { model: child.model } : {}),
412
+ ...(typeof child?.exitCode === "number" ? { exitCode: child.exitCode } : {}),
413
+ ...(child?.finalOutput ? { output: child.finalOutput } : {}),
414
+ ...(child?.savedOutputPath ? { outputPath: child.savedOutputPath } : {}),
415
+ ...(child?.sessionFile ? { sessionFile: child.sessionFile } : {}),
416
+ ...(child?.acceptance ? { acceptance: { status: child.acceptance.status, explicit: child.acceptance.explicit } } : {}),
417
+ ...(typeof child?.usage?.turns === "number" ? { turns: child.usage.turns } : {}),
418
+ ...(typeof progress?.toolCount === "number" ? { toolCount: progress.toolCount } : {}),
419
+ ...(typeof progress?.durationMs === "number" ? { durationMs: progress.durationMs } : {}),
420
+ ...(typeof progress?.tokens === "number" ? { tokens: progress.tokens } : {}),
421
+ ...(warnings.length > 0 ? { warnings } : {}),
422
+ };
423
+ }
424
+
425
+ export function toPromptTemplateResponse(
426
+ request: PromptTemplateDelegationRequest,
427
+ result: PromptTemplateBridgeResult,
428
+ ): PromptTemplateDelegationResponse {
429
+ const contentText = firstTextContent(result.content);
430
+ const messages = buildDelegationMessages(result.details?.results?.[0] ?? {}, contentText);
431
+ const parallelResults = request.tasks?.map<PromptTemplateDelegationParallelResult>((task, index) => {
432
+ const step = result.details?.results?.[index];
433
+ if (!step) {
434
+ return {
435
+ agent: task.agent,
436
+ messages: [],
437
+ isError: true,
438
+ errorText: "Missing result for delegated parallel task.",
439
+ };
440
+ }
441
+ const exitCode = typeof step.exitCode === "number" ? step.exitCode : undefined;
442
+ return {
443
+ agent: step.agent ?? task.agent,
444
+ messages: buildDelegationMessages(step),
445
+ isError: (exitCode !== undefined && exitCode !== 0) || !!step.error,
446
+ errorText: step.error || undefined,
447
+ };
448
+ });
449
+ return {
450
+ ...request,
451
+ messages,
452
+ ...(parallelResults ? { parallelResults } : {}),
453
+ ...(contentText ? { contentText } : {}),
454
+ isError: result.isError === true,
455
+ errorText: result.isError ? contentText : undefined,
456
+ };
457
+ }
@@ -0,0 +1,103 @@
1
+ import {
2
+ SUBAGENT_DELEGATION_PROTOCOL_VERSION,
3
+ type SubagentDelegationRequest,
4
+ } from "../api/delegation.ts";
5
+ import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
6
+ import { validateToolBudgetConfig } from "../runs/shared/tool-budget.ts";
7
+ import { resolveTurnBudgetConfig } from "../runs/shared/turn-budget.ts";
8
+
9
+ export type SubagentDelegationParseResult =
10
+ | { ok: true; request: SubagentDelegationRequest }
11
+ | { ok: false; requestId?: string; error: string };
12
+
13
+ const supportedFields = new Set([
14
+ "version",
15
+ "requestId",
16
+ "agent",
17
+ "task",
18
+ "context",
19
+ "cwd",
20
+ "model",
21
+ "timeoutMs",
22
+ "turnBudget",
23
+ "toolBudget",
24
+ "skill",
25
+ "output",
26
+ "outputMode",
27
+ "acceptance",
28
+ "artifacts",
29
+ ]);
30
+
31
+ function nonEmptyString(value: unknown): value is string {
32
+ return typeof value === "string" && value.trim().length > 0;
33
+ }
34
+
35
+ function validateRequestId(value: unknown): string | undefined {
36
+ if (!nonEmptyString(value) || value.length > 256 || /[\r\n]/.test(value)) return undefined;
37
+ return value;
38
+ }
39
+
40
+ export function parseSubagentDelegationRequest(data: unknown): SubagentDelegationParseResult {
41
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
42
+ return { ok: false, error: "Delegation request must be an object." };
43
+ }
44
+ const value = data as Record<string, unknown>;
45
+ const requestId = validateRequestId(value.requestId);
46
+ if (value.version !== SUBAGENT_DELEGATION_PROTOCOL_VERSION) {
47
+ return {
48
+ ...(requestId ? { requestId } : {}),
49
+ ok: false,
50
+ error: `Unsupported delegation protocol version: ${String(value.version)}.`,
51
+ };
52
+ }
53
+ if (!requestId) {
54
+ return { ok: false, error: "Delegation requestId must be a non-empty string of at most 256 characters without newlines." };
55
+ }
56
+ const unsupportedField = Object.keys(value).find((key) => !supportedFields.has(key));
57
+ if (unsupportedField) return { ok: false, requestId, error: `Unsupported delegation field: ${unsupportedField}.` };
58
+ if (!nonEmptyString(value.agent)) return { ok: false, requestId, error: "Delegation agent must be a non-empty string." };
59
+ if (!nonEmptyString(value.task)) return { ok: false, requestId, error: "Delegation task must be a non-empty string." };
60
+ if (value.context !== "fresh" && value.context !== "fork") {
61
+ return { ok: false, requestId, error: "Delegation context must be fresh or fork." };
62
+ }
63
+ if (!nonEmptyString(value.cwd)) return { ok: false, requestId, error: "Delegation cwd must be a non-empty string." };
64
+ if (value.model !== undefined && !nonEmptyString(value.model)) {
65
+ return { ok: false, requestId, error: "model must be a non-empty string when provided." };
66
+ }
67
+ if (value.timeoutMs !== undefined && (typeof value.timeoutMs !== "number" || !Number.isInteger(value.timeoutMs) || value.timeoutMs < 1)) {
68
+ return { ok: false, requestId, error: "timeoutMs must be an integer >= 1." };
69
+ }
70
+ const turnBudget = resolveTurnBudgetConfig(value.turnBudget);
71
+ if (turnBudget.error) return { ok: false, requestId, error: turnBudget.error };
72
+ if (value.toolBudget && typeof value.toolBudget === "object" && !Array.isArray(value.toolBudget)) {
73
+ const unsupportedToolBudgetField = Object.keys(value.toolBudget).find((key) => key !== "soft" && key !== "hard" && key !== "block");
74
+ if (unsupportedToolBudgetField) {
75
+ return { ok: false, requestId, error: `toolBudget.${unsupportedToolBudgetField} is not supported.` };
76
+ }
77
+ }
78
+ const toolBudget = validateToolBudgetConfig(value.toolBudget);
79
+ if (toolBudget.error) return { ok: false, requestId, error: toolBudget.error };
80
+ if (value.skill !== undefined) {
81
+ const validSkill = typeof value.skill === "boolean"
82
+ || nonEmptyString(value.skill)
83
+ || (Array.isArray(value.skill) && value.skill.length > 0 && value.skill.every(nonEmptyString));
84
+ if (!validSkill) {
85
+ return { ok: false, requestId, error: "skill must be a boolean, non-empty string, or non-empty string array." };
86
+ }
87
+ }
88
+ if (value.output !== undefined && typeof value.output !== "boolean" && !nonEmptyString(value.output)) {
89
+ return { ok: false, requestId, error: "output must be a boolean or non-empty string." };
90
+ }
91
+ if (value.outputMode !== undefined && value.outputMode !== "inline" && value.outputMode !== "file-only") {
92
+ return { ok: false, requestId, error: "outputMode must be inline or file-only." };
93
+ }
94
+ if (value.outputMode === "file-only" && !nonEmptyString(value.output)) {
95
+ return { ok: false, requestId, error: 'outputMode "file-only" requires output to be a non-empty path.' };
96
+ }
97
+ const acceptanceErrors = validateAcceptanceInput(value.acceptance);
98
+ if (acceptanceErrors.length > 0) return { ok: false, requestId, error: acceptanceErrors.join(" ") };
99
+ if (value.artifacts !== undefined && typeof value.artifacts !== "boolean") {
100
+ return { ok: false, requestId, error: "artifacts must be a boolean." };
101
+ }
102
+ return { ok: true, request: value as unknown as SubagentDelegationRequest };
103
+ }