@agwab/pi-workflow 0.2.1 → 0.3.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 (70) hide show
  1. package/dist/compiler.js +6 -8
  2. package/dist/dynamic-decision.d.ts +0 -1
  3. package/dist/dynamic-decision.js +0 -7
  4. package/dist/dynamic-profiles.d.ts +0 -1
  5. package/dist/dynamic-profiles.js +0 -3
  6. package/dist/engine-run-graph.d.ts +1 -0
  7. package/dist/engine-run-graph.js +142 -2
  8. package/dist/engine.d.ts +5 -0
  9. package/dist/engine.js +112 -27
  10. package/dist/extension.d.ts +2 -1
  11. package/dist/extension.js +27 -6
  12. package/dist/index.d.ts +3 -3
  13. package/dist/index.js +2 -1
  14. package/dist/store.js +55 -11
  15. package/dist/subagent-backend.js +155 -29
  16. package/dist/types.d.ts +6 -0
  17. package/dist/workflow-runtime.js +10 -1
  18. package/dist/workflow-view.js +3 -1
  19. package/dist/workflow-web-source-extension.js +167 -48
  20. package/dist/workflow-web-source.d.ts +2 -1
  21. package/dist/workflow-web-source.js +84 -19
  22. package/node_modules/@agwab/pi-subagent/README.md +3 -3
  23. package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
  24. package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
  25. package/node_modules/@agwab/pi-subagent/package.json +2 -2
  26. package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
  27. package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
  28. package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
  29. package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
  30. package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
  31. package/node_modules/@agwab/pi-subagent/src/index.ts +995 -573
  32. package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
  33. package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
  34. package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
  35. package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
  36. package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
  37. package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
  38. package/node_modules/@agwab/pi-subagent/src/panel.ts +1352 -560
  39. package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
  40. package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
  41. package/package.json +2 -2
  42. package/src/compiler.ts +14 -9
  43. package/src/dynamic-decision.ts +0 -11
  44. package/src/dynamic-profiles.ts +0 -4
  45. package/src/engine-run-graph.ts +185 -2
  46. package/src/engine.ts +145 -24
  47. package/src/extension.ts +33 -4
  48. package/src/index.ts +3 -1
  49. package/src/store.ts +74 -11
  50. package/src/subagent-backend.ts +201 -28
  51. package/src/types.ts +6 -0
  52. package/src/workflow-runtime.ts +18 -2
  53. package/src/workflow-view.ts +2 -1
  54. package/src/workflow-web-source-extension.ts +621 -228
  55. package/src/workflow-web-source.ts +118 -28
  56. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +56 -16
  57. package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
  58. package/workflows/deep-research/helpers/normalize-input-packet.mjs +1 -1
  59. package/workflows/deep-research/helpers/render-executive.mjs +8 -21
  60. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
  61. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +0 -1
  62. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +4 -1
  63. package/workflows/impact-review/spec.json +3 -3
  64. package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
  65. package/dist/dynamic-loader.d.ts +0 -25
  66. package/dist/dynamic-loader.js +0 -13
  67. package/src/dynamic-loader.ts +0 -49
  68. package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
  69. package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
  70. package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
@@ -3,678 +3,1100 @@ import { Type } from "typebox";
3
3
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
4
  import { loadAgentByName, type AgentDefinition } from "./agents.ts";
5
5
  import {
6
- appendRunEvent,
7
- createAttemptArtifactStore,
8
- setRunDependency,
9
- type ArtifactRef,
10
- type ResultEnvelope,
6
+ appendRunEvent,
7
+ createAttemptArtifactStore,
8
+ setRunDependency,
9
+ type ArtifactRef,
10
+ type ResultEnvelope,
11
11
  } from "./artifacts/index.ts";
12
12
  import {
13
- AGENT_SCOPES,
14
- ASYNC_DEPENDENCIES,
15
- BACKENDS,
16
- EXECUTION_MODES,
17
- ON_COMPLETE_ACTIONS,
18
- THINKING_LEVELS,
19
- WORKSPACE_MODES,
20
- WORKTREE_POLICIES,
21
- type ExecutionMode,
22
- type ResolveInput,
23
- type ResolveValidationFailure,
24
- type ResolvedBackend,
13
+ AGENT_SCOPES,
14
+ ASYNC_DEPENDENCIES,
15
+ BACKENDS,
16
+ EXECUTION_MODES,
17
+ ON_COMPLETE_ACTIONS,
18
+ THINKING_LEVELS,
19
+ WORKSPACE_MODES,
20
+ WORKTREE_POLICIES,
21
+ type ExecutionMode,
22
+ type ResolveInput,
23
+ type ResolveValidationFailure,
24
+ type ResolvedBackend,
25
25
  } from "./core/constants.ts";
26
26
  import { resolveBackend } from "./core/resolver.ts";
27
27
  import { validateResolveInput } from "./core/validation.ts";
28
- import { startAsyncParallelSubagentRuns, startAsyncSubagentRun } from "./orchestrate/async.ts";
28
+ import {
29
+ startAsyncParallelSubagentRuns,
30
+ startAsyncSubagentRun,
31
+ } from "./orchestrate/async.ts";
29
32
  import { interruptRun } from "./orchestrate/interrupt.ts";
30
33
  import { reconcileSubagentRun } from "./orchestrate/reconcile.ts";
31
- import { DEFAULT_PARALLEL_CONCURRENCY, runParallelSubagentTasks, runSubagentTask } from "./orchestrate/run.ts";
34
+ import { resolveRunRef } from "./orchestrate/run-ref.ts";
35
+ import {
36
+ DEFAULT_PARALLEL_CONCURRENCY,
37
+ runParallelSubagentTasks,
38
+ runSubagentTask,
39
+ } from "./orchestrate/run.ts";
32
40
  import { getRunLogs, getRunStatus, waitForRun } from "./orchestrate/status.ts";
33
41
  import { showSubagentPanel } from "./panel.ts";
34
42
  import { WorkspacePolicyError } from "./workspace/worktree.ts";
35
43
 
36
44
  const TOOL_NAME = "subagent";
37
45
  const SUPPORTED_KEYS = new Set([
38
- "backend",
39
- "visible",
40
- "sandbox",
41
- "agent",
42
- "task",
43
- "roleContext",
44
- "agentScope",
45
- "confirmProjectAgents",
46
- "mode",
47
- "tasks",
48
- "concurrency",
49
- "asyncDependency",
50
- "workspace",
51
- "worktree",
52
- "worktreePolicy",
53
- "cwd",
54
- "async",
55
- "onComplete",
56
- "timeoutMs",
57
- "model",
58
- "tools",
59
- "systemPrompt",
60
- "skills",
61
- "extensions",
62
- "runsDir",
63
- "correlationId",
64
- "captureToolCalls",
65
- "thinking",
66
- "thinkingLevel",
67
- "reasoningLevel",
68
- "action",
69
- "runId",
70
- "attemptId",
71
- "taskId",
72
- "pollIntervalMs",
73
- "reason",
74
- "signal",
75
- "escalateAfterMs",
76
- "killAfterMs",
46
+ "backend",
47
+ "visible",
48
+ "sandbox",
49
+ "agent",
50
+ "task",
51
+ "roleContext",
52
+ "agentScope",
53
+ "confirmProjectAgents",
54
+ "mode",
55
+ "tasks",
56
+ "concurrency",
57
+ "failFast",
58
+ "cancelSiblingsOnFailure",
59
+ "asyncDependency",
60
+ "workspace",
61
+ "worktree",
62
+ "worktreePolicy",
63
+ "cwd",
64
+ "async",
65
+ "onComplete",
66
+ "timeoutMs",
67
+ "model",
68
+ "tools",
69
+ "systemPrompt",
70
+ "skills",
71
+ "extensions",
72
+ "runsDir",
73
+ "correlationId",
74
+ "captureToolCalls",
75
+ "thinking",
76
+ "thinkingLevel",
77
+ "reasoningLevel",
78
+ "action",
79
+ "runId",
80
+ "attemptId",
81
+ "taskId",
82
+ "pollIntervalMs",
83
+ "reason",
84
+ "signal",
85
+ "escalateAfterMs",
86
+ "killAfterMs",
77
87
  ]);
78
- const AGENT_TASK_KEYS = ["agent", "task", "roleContext", "agentScope", "confirmProjectAgents"];
88
+ const AGENT_TASK_KEYS = [
89
+ "agent",
90
+ "task",
91
+ "roleContext",
92
+ "agentScope",
93
+ "confirmProjectAgents",
94
+ ];
79
95
  const SANDBOX_SCHEMA = Type.Union(
80
- [
81
- Type.Boolean(),
82
- Type.Null(),
83
- Type.Object({
84
- allowedDomains: Type.Optional(
85
- Type.Array(Type.String({ minLength: 1 }), {
86
- description: 'Network domains the sandboxed child may reach, e.g. "api.anthropic.com" or "*.npmjs.org". Model-backed sandboxed runs must include their provider endpoint. Omitted means deny-all network.',
87
- }),
88
- ),
89
- }),
90
- ],
91
- { description: "true = offline OS sandbox; { allowedDomains: [...] } = sandbox with explicit network egress; false/null = no sandbox." },
96
+ [
97
+ Type.Boolean(),
98
+ Type.Null(),
99
+ Type.Object({
100
+ allowedDomains: Type.Optional(
101
+ Type.Array(Type.String({ minLength: 1 }), {
102
+ description:
103
+ 'Network domains the sandboxed child may reach, e.g. "api.anthropic.com" or "*.npmjs.org". Model-backed sandboxed runs must include their provider endpoint. Omitted means deny-all network.',
104
+ }),
105
+ ),
106
+ }),
107
+ ],
108
+ {
109
+ description:
110
+ "true = offline OS sandbox; { allowedDomains: [...] } = sandbox with explicit network egress; false/null = no sandbox.",
111
+ },
92
112
  );
93
113
  const SUBAGENT_TASK_SCHEMA = Type.Object({
94
- agent: Type.Optional(Type.String({ minLength: 1 })),
95
- task: Type.Optional(Type.String({ minLength: 1 })),
96
- roleContext: Type.Optional(Type.String({ minLength: 1 })),
97
- agentScope: Type.Optional(Type.Union(AGENT_SCOPES.map((value) => Type.Literal(value)))),
98
- confirmProjectAgents: Type.Optional(Type.Boolean()),
99
- sandbox: Type.Optional(SANDBOX_SCHEMA),
100
- visible: Type.Optional(Type.Boolean()),
101
- cwd: Type.Optional(Type.String({ minLength: 1 })),
102
- timeoutMs: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
103
- model: Type.Optional(Type.String({ minLength: 1 })),
104
- thinking: Type.Optional(Type.Union(THINKING_LEVELS.map((value) => Type.Literal(value)))),
105
- tools: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
106
- systemPrompt: Type.Optional(Type.String({ minLength: 1 })),
107
- skills: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
108
- extensions: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
109
- captureToolCalls: Type.Optional(Type.Boolean({ description: "Capture redacted child tool-call telemetry as artifacts. Default false." })),
114
+ agent: Type.Optional(Type.String({ minLength: 1 })),
115
+ task: Type.Optional(Type.String({ minLength: 1 })),
116
+ roleContext: Type.Optional(Type.String({ minLength: 1 })),
117
+ agentScope: Type.Optional(
118
+ Type.Union(AGENT_SCOPES.map((value) => Type.Literal(value))),
119
+ ),
120
+ confirmProjectAgents: Type.Optional(Type.Boolean()),
121
+ sandbox: Type.Optional(SANDBOX_SCHEMA),
122
+ visible: Type.Optional(Type.Boolean()),
123
+ cwd: Type.Optional(Type.String({ minLength: 1 })),
124
+ timeoutMs: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
125
+ model: Type.Optional(Type.String({ minLength: 1 })),
126
+ thinking: Type.Optional(
127
+ Type.Union(THINKING_LEVELS.map((value) => Type.Literal(value))),
128
+ ),
129
+ tools: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
130
+ systemPrompt: Type.Optional(Type.String({ minLength: 1 })),
131
+ skills: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
132
+ extensions: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
133
+ captureToolCalls: Type.Optional(
134
+ Type.Boolean({
135
+ description:
136
+ "Capture redacted child tool-call telemetry as artifacts. Default false.",
137
+ }),
138
+ ),
110
139
  });
111
140
 
112
141
  interface ToolTextContent {
113
- type: "text";
114
- text: string;
142
+ type: "text";
143
+ text: string;
115
144
  }
116
145
 
117
146
  interface ToolResult {
118
- content: ToolTextContent[];
119
- details: unknown;
120
- isError: boolean;
147
+ content: ToolTextContent[];
148
+ details: unknown;
149
+ isError: boolean;
121
150
  }
122
151
 
123
152
  const ANSI_PATTERN = /\u001b\[[0-?]*[ -/]*[@-~]/g;
124
153
 
125
154
  function visibleLength(text: string): number {
126
- return text.replace(ANSI_PATTERN, "").length;
155
+ return text.replace(ANSI_PATTERN, "").length;
127
156
  }
128
157
 
129
158
  function clip(text: string, width: number): string {
130
- if (width <= 0) return "";
131
- if (visibleLength(text) <= width) return text;
132
- if (width <= 1) return "…";
133
-
134
- let output = "";
135
- let visible = 0;
136
- for (let index = 0; index < text.length && visible < width - 1; ) {
137
- if (text.charCodeAt(index) === 0x1b) {
138
- ANSI_PATTERN.lastIndex = index;
139
- const match = ANSI_PATTERN.exec(text);
140
- if (match && match.index === index) {
141
- output += match[0];
142
- index = ANSI_PATTERN.lastIndex;
143
- continue;
144
- }
145
- }
146
- output += text[index];
147
- visible += 1;
148
- index += 1;
149
- }
150
- return `${output}…`;
159
+ if (width <= 0) return "";
160
+ if (visibleLength(text) <= width) return text;
161
+ if (width <= 1) return "…";
162
+
163
+ let output = "";
164
+ let visible = 0;
165
+ for (let index = 0; index < text.length && visible < width - 1; ) {
166
+ if (text.charCodeAt(index) === 0x1b) {
167
+ ANSI_PATTERN.lastIndex = index;
168
+ const match = ANSI_PATTERN.exec(text);
169
+ if (match && match.index === index) {
170
+ output += match[0];
171
+ index = ANSI_PATTERN.lastIndex;
172
+ continue;
173
+ }
174
+ }
175
+ output += text[index];
176
+ visible += 1;
177
+ index += 1;
178
+ }
179
+ return `${output}…`;
151
180
  }
152
181
 
153
182
  class SingleLineComponent {
154
- constructor(private readonly text: string) {}
183
+ constructor(private readonly text: string) {}
155
184
 
156
- invalidate(): void {
157
- // Static one-line component.
158
- }
185
+ invalidate(): void {
186
+ // Static one-line component.
187
+ }
159
188
 
160
- render(width: number): string[] {
161
- return [clip(this.text, width)];
162
- }
189
+ render(width: number): string[] {
190
+ return [clip(this.text, width)];
191
+ }
163
192
  }
164
193
 
165
194
  function isRecord(value: unknown): value is Record<string, unknown> {
166
- return typeof value === "object" && value !== null && !Array.isArray(value);
195
+ return typeof value === "object" && value !== null && !Array.isArray(value);
167
196
  }
168
197
 
169
- function hasAnyKey(input: Record<string, unknown>, keys: readonly string[]): boolean {
170
- return keys.some((key) => Object.hasOwn(input, key));
198
+ function hasAnyKey(
199
+ input: Record<string, unknown>,
200
+ keys: readonly string[],
201
+ ): boolean {
202
+ return keys.some((key) => Object.hasOwn(input, key));
171
203
  }
172
204
 
173
205
  function formatKeyList(keys: readonly string[]): string {
174
- return keys.map((key) => `"${key}"`).join(", ");
206
+ return keys.map((key) => `"${key}"`).join(", ");
175
207
  }
176
208
 
177
209
  function getExecuteParams(first: unknown, second: unknown): unknown {
178
- return second === undefined ? first : second;
210
+ return second === undefined ? first : second;
179
211
  }
180
212
 
181
213
  function getCwd(ctx: unknown): string {
182
- if (isRecord(ctx) && typeof ctx.cwd === "string" && ctx.cwd.length > 0) return ctx.cwd;
183
- return process.cwd();
214
+ if (isRecord(ctx) && typeof ctx.cwd === "string" && ctx.cwd.length > 0)
215
+ return ctx.cwd;
216
+ return process.cwd();
184
217
  }
185
218
 
186
219
  function parentSessionIdFromCtx(ctx: unknown): string | undefined {
187
- if (!isRecord(ctx)) return undefined;
188
- const sessionManager = ctx.sessionManager;
189
- if (!isRecord(sessionManager) || typeof sessionManager.getSessionId !== "function") return undefined;
190
- try {
191
- const id = (sessionManager.getSessionId as () => unknown)();
192
- return typeof id === "string" && id.length > 0 ? id : undefined;
193
- } catch {
194
- return undefined;
195
- }
220
+ if (!isRecord(ctx)) return undefined;
221
+ const sessionManager = ctx.sessionManager;
222
+ if (
223
+ !isRecord(sessionManager) ||
224
+ typeof sessionManager.getSessionId !== "function"
225
+ )
226
+ return undefined;
227
+ try {
228
+ const id = (sessionManager.getSessionId as () => unknown)();
229
+ return typeof id === "string" && id.length > 0 ? id : undefined;
230
+ } catch {
231
+ return undefined;
232
+ }
196
233
  }
197
234
 
198
- function textResult(payload: unknown, isError: boolean, details?: unknown): ToolResult {
199
- return {
200
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
201
- details,
202
- isError,
203
- };
235
+ function textResult(
236
+ payload: unknown,
237
+ isError: boolean,
238
+ details?: unknown,
239
+ ): ToolResult {
240
+ return {
241
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
242
+ details,
243
+ isError,
244
+ };
204
245
  }
205
246
 
206
247
  function artifactSummary(artifacts: readonly ArtifactRef[]) {
207
- return artifacts.map((artifact) => ({
208
- type: artifact.type,
209
- path: artifact.path,
210
- ...(artifact.bytes === undefined ? {} : { bytes: artifact.bytes }),
211
- }));
248
+ return artifacts.map((artifact) => ({
249
+ type: artifact.type,
250
+ path: artifact.path,
251
+ ...(artifact.bytes === undefined ? {} : { bytes: artifact.bytes }),
252
+ }));
212
253
  }
213
254
 
214
255
  function compactResult(result: ResultEnvelope, error?: string) {
215
- return {
216
- tool: TOOL_NAME,
217
- backend: result.backend,
218
- status: result.status,
219
- failureKind: result.failureKind,
220
- ...(error === undefined ? {} : { error }),
221
- runId: result.runId,
222
- attemptId: result.attemptId,
223
- ...(result.taskId === undefined ? {} : { taskId: result.taskId }),
224
- ...(result.correlationId === undefined ? {} : { correlationId: result.correlationId }),
225
- durationMs: result.durationMs,
226
- exitCode: result.exitCode,
227
- signal: result.signal,
228
- sandbox: result.sandbox,
229
- workspace: result.workspace,
230
- ...(result.tmux === undefined ? {} : { tmux: result.tmux }),
231
- ...(result.completion === undefined ? {} : { completion: result.completion }),
232
- metadata: result.metadata,
233
- artifacts: artifactSummary(result.artifacts),
234
- };
256
+ return {
257
+ tool: TOOL_NAME,
258
+ backend: result.backend,
259
+ status: result.status,
260
+ failureKind: result.failureKind,
261
+ ...(error === undefined ? {} : { error }),
262
+ runId: result.runId,
263
+ attemptId: result.attemptId,
264
+ ...(result.taskId === undefined ? {} : { taskId: result.taskId }),
265
+ ...(result.correlationId === undefined
266
+ ? {}
267
+ : { correlationId: result.correlationId }),
268
+ durationMs: result.durationMs,
269
+ exitCode: result.exitCode,
270
+ signal: result.signal,
271
+ sandbox: result.sandbox,
272
+ workspace: result.workspace,
273
+ ...(result.tmux === undefined ? {} : { tmux: result.tmux }),
274
+ ...(result.completion === undefined
275
+ ? {}
276
+ : { completion: result.completion }),
277
+ metadata: result.metadata,
278
+ artifacts: artifactSummary(result.artifacts),
279
+ };
235
280
  }
236
281
 
237
282
  function displayText(value: unknown, maxLength: number): string | undefined {
238
- if (typeof value !== "string") return undefined;
239
- const normalized = value.replace(/\s+/g, " ").trim();
240
- if (!normalized) return undefined;
241
- return normalized.length <= maxLength ? normalized : `${normalized.slice(0, Math.max(0, maxLength - 1))}…`;
283
+ if (typeof value !== "string") return undefined;
284
+ const normalized = value.replace(/\s+/g, " ").trim();
285
+ if (!normalized) return undefined;
286
+ return normalized.length <= maxLength
287
+ ? normalized
288
+ : `${normalized.slice(0, Math.max(0, maxLength - 1))}…`;
242
289
  }
243
290
 
244
291
  function subagentCallSummary(input: unknown): string {
245
- const args = isRecord(input) ? input : {};
246
- const action = displayText(args.action, 16) ?? "run";
247
- const mode = displayText(args.mode, 16) ?? (Array.isArray(args.tasks) ? "parallel" : "single");
248
- const pieces = [`subagent ${action}`];
249
-
250
- if (action === "run") {
251
- pieces.push(mode);
252
- if (Array.isArray(args.tasks)) pieces.push(`${args.tasks.length} run${args.tasks.length === 1 ? "" : "s"}`);
253
- const agent = displayText(args.agent, 24);
254
- if (agent) pieces.push(agent);
255
- const task = displayText(args.task, 48);
256
- if (task) pieces.push(task);
257
- const asyncMode = args.async === true ? "async" : displayText(args.onComplete, 16);
258
- if (asyncMode) pieces.push(asyncMode);
259
- } else {
260
- const runId = displayText(args.runId, 28);
261
- if (runId) pieces.push(runId);
262
- const attemptId = displayText(args.attemptId, 16) ?? displayText(args.taskId, 16);
263
- if (attemptId) pieces.push(attemptId);
264
- }
265
-
266
- return pieces.filter(Boolean).join(" · ");
292
+ const args = isRecord(input) ? input : {};
293
+ const action = displayText(args.action, 16) ?? "run";
294
+ const mode =
295
+ displayText(args.mode, 16) ??
296
+ (Array.isArray(args.tasks) ? "parallel" : "single");
297
+ const pieces = [`subagent ${action}`];
298
+
299
+ if (action === "run") {
300
+ pieces.push(mode);
301
+ if (Array.isArray(args.tasks))
302
+ pieces.push(
303
+ `${args.tasks.length} run${args.tasks.length === 1 ? "" : "s"}`,
304
+ );
305
+ const agent = displayText(args.agent, 24);
306
+ if (agent) pieces.push(agent);
307
+ const task = displayText(args.task, 48);
308
+ if (task) pieces.push(task);
309
+ const asyncMode =
310
+ args.async === true ? "async" : displayText(args.onComplete, 16);
311
+ if (args.failFast === true || args.cancelSiblingsOnFailure === true)
312
+ pieces.push("fail-fast");
313
+ if (asyncMode) pieces.push(asyncMode);
314
+ } else {
315
+ const runId = displayText(args.runId, 28);
316
+ if (runId) pieces.push(runId);
317
+ const attemptId =
318
+ displayText(args.attemptId, 16) ?? displayText(args.taskId, 16);
319
+ if (attemptId) pieces.push(attemptId);
320
+ }
321
+
322
+ return pieces.filter(Boolean).join(" · ");
267
323
  }
268
324
 
269
325
  function validationFailure(failure: ResolveValidationFailure): ToolResult {
270
- return textResult(
271
- {
272
- tool: TOOL_NAME,
273
- backend: failure.backend,
274
- status: failure.status,
275
- failureKind: failure.failureKind,
276
- error: failure.error,
277
- },
278
- true,
279
- { resolved: failure },
280
- );
326
+ return textResult(
327
+ {
328
+ tool: TOOL_NAME,
329
+ backend: failure.backend,
330
+ status: failure.status,
331
+ failureKind: failure.failureKind,
332
+ error: failure.error,
333
+ },
334
+ true,
335
+ { resolved: failure },
336
+ );
281
337
  }
282
338
 
283
339
  class InputValidationError extends Error {
284
- readonly failureKind = "validation" as const;
340
+ readonly failureKind = "validation" as const;
285
341
  }
286
342
 
287
343
  function optionalString(value: unknown, fieldName: string): string | undefined {
288
- if (value === undefined) return undefined;
289
- if (typeof value !== "string" || value.length === 0) throw new InputValidationError(`${fieldName} must be a non-empty string when provided.`);
290
- return value;
291
- }
292
-
293
- function optionalPositiveNumber(value: unknown, fieldName: string): number | undefined {
294
- if (value === undefined) return undefined;
295
- if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw new InputValidationError(`${fieldName} must be a positive finite number when provided.`);
296
- return value;
297
- }
298
-
299
- async function lifecycleAction(raw: Record<string, unknown>, cwd: string): Promise<ToolResult | null> {
300
- const action = raw.action ?? "run";
301
- if (action === "run") return null;
302
- if (action !== "status" && action !== "logs" && action !== "wait" && action !== "interrupt" && action !== "mark-background" && action !== "reconcile") {
303
- throw new InputValidationError('action must be one of "run", "status", "logs", "wait", "interrupt", "mark-background", or "reconcile" when provided.');
304
- }
305
-
306
- const runId = optionalString(raw.runId, "runId");
307
- if (runId === undefined) throw new InputValidationError(`${String(action)} action requires a non-empty runId.`);
308
- const ref = {
309
- cwd: optionalString(raw.cwd, "cwd") ?? cwd,
310
- runId,
311
- attemptId: optionalString(raw.attemptId, "attemptId") ?? optionalString(raw.taskId, "taskId"),
312
- runsDir: optionalString(raw.runsDir, "runsDir"),
313
- };
314
-
315
- if (action === "status") {
316
- const snapshot = await getRunStatus(ref);
317
- return textResult({ tool: TOOL_NAME, action, status: snapshot === null ? "failed" : snapshot.status, snapshot }, snapshot === null, { snapshot });
318
- }
319
-
320
- if (action === "logs") {
321
- const snapshot = await getRunLogs(ref);
322
- return textResult({ tool: TOOL_NAME, action, status: snapshot === null ? "failed" : snapshot.status, snapshot }, snapshot === null, { snapshot });
323
- }
324
-
325
- if (action === "mark-background") {
326
- const record = await setRunDependency(ref, "background");
327
- await appendRunEvent(ref, { type: "run.mark_background", status: record.status, message: "run marked background" });
328
- const snapshot = await getRunStatus(ref);
329
- return textResult({ tool: TOOL_NAME, action, status: snapshot?.status ?? record.status, snapshot }, false, { snapshot, record });
330
- }
331
-
332
- if (action === "interrupt") {
333
- const signal = optionalString(raw.signal, "signal") as NodeJS.Signals | undefined;
334
- if (signal !== undefined && signal !== "SIGINT" && signal !== "SIGTERM" && signal !== "SIGKILL") {
335
- throw new InputValidationError('signal must be one of "SIGINT", "SIGTERM", or "SIGKILL" when provided.');
336
- }
337
- const interrupted = await interruptRun({
338
- cwd: ref.cwd,
339
- runId,
340
- runsDir: ref.runsDir,
341
- attemptId: ref.attemptId,
342
- reason: optionalString(raw.reason, "reason"),
343
- signal,
344
- escalateAfterMs: optionalPositiveNumber(raw.escalateAfterMs, "escalateAfterMs"),
345
- killAfterMs: optionalPositiveNumber(raw.killAfterMs, "killAfterMs"),
346
- });
347
- const snapshot = await getRunStatus(ref);
348
- const isError = interrupted.status === "not-found" || interrupted.status === "unsupported";
349
- return textResult({ tool: TOOL_NAME, action, status: interrupted.status, interrupted, snapshot }, isError, { interrupted, snapshot });
350
- }
351
-
352
- if (action === "reconcile") {
353
- const reconciled = await reconcileSubagentRun(ref);
354
- const snapshot = await getRunStatus(ref);
355
- return textResult({ tool: TOOL_NAME, action, status: reconciled.status, reconciled, snapshot }, reconciled.status === "not-found", { reconciled, snapshot });
356
- }
357
-
358
- const waited = await waitForRun({ ...ref, timeoutMs: optionalPositiveNumber(raw.timeoutMs, "timeoutMs"), pollIntervalMs: optionalPositiveNumber(raw.pollIntervalMs, "pollIntervalMs") });
359
- const isError = waited.status !== "completed" || waited.snapshot?.status !== "completed";
360
- return textResult({ tool: TOOL_NAME, action, status: waited.status, snapshot: waited.snapshot }, isError, { waited });
344
+ if (value === undefined) return undefined;
345
+ if (typeof value !== "string" || value.length === 0)
346
+ throw new InputValidationError(
347
+ `${fieldName} must be a non-empty string when provided.`,
348
+ );
349
+ return value;
350
+ }
351
+
352
+ function optionalPositiveNumber(
353
+ value: unknown,
354
+ fieldName: string,
355
+ ): number | undefined {
356
+ if (value === undefined) return undefined;
357
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0)
358
+ throw new InputValidationError(
359
+ `${fieldName} must be a positive finite number when provided.`,
360
+ );
361
+ return value;
362
+ }
363
+
364
+ async function lifecycleAction(
365
+ raw: Record<string, unknown>,
366
+ cwd: string,
367
+ ): Promise<ToolResult | null> {
368
+ const action = raw.action ?? "run";
369
+ if (action === "run") return null;
370
+ if (
371
+ action !== "status" &&
372
+ action !== "logs" &&
373
+ action !== "wait" &&
374
+ action !== "interrupt" &&
375
+ action !== "mark-background" &&
376
+ action !== "reconcile"
377
+ ) {
378
+ throw new InputValidationError(
379
+ 'action must be one of "run", "status", "logs", "wait", "interrupt", "mark-background", or "reconcile" when provided.',
380
+ );
381
+ }
382
+
383
+ const runId = optionalString(raw.runId, "runId");
384
+ if (runId === undefined)
385
+ throw new InputValidationError(
386
+ `${String(action)} action requires a non-empty runId.`,
387
+ );
388
+ const ref = await resolveRunRef(
389
+ {
390
+ cwd: optionalString(raw.cwd, "cwd"),
391
+ runId,
392
+ attemptId:
393
+ optionalString(raw.attemptId, "attemptId") ??
394
+ optionalString(raw.taskId, "taskId"),
395
+ runsDir: optionalString(raw.runsDir, "runsDir"),
396
+ },
397
+ cwd,
398
+ );
399
+
400
+ if (action === "status") {
401
+ const snapshot = await getRunStatus(ref);
402
+ return textResult(
403
+ {
404
+ tool: TOOL_NAME,
405
+ action,
406
+ status: snapshot === null ? "failed" : snapshot.status,
407
+ snapshot,
408
+ },
409
+ snapshot === null,
410
+ { snapshot },
411
+ );
412
+ }
413
+
414
+ if (action === "logs") {
415
+ const snapshot = await getRunLogs(ref);
416
+ return textResult(
417
+ {
418
+ tool: TOOL_NAME,
419
+ action,
420
+ status: snapshot === null ? "failed" : snapshot.status,
421
+ snapshot,
422
+ },
423
+ snapshot === null,
424
+ { snapshot },
425
+ );
426
+ }
427
+
428
+ if (action === "mark-background") {
429
+ const record = await setRunDependency(ref, "background");
430
+ await appendRunEvent(ref, {
431
+ type: "run.mark_background",
432
+ status: record.status,
433
+ message: "run marked background",
434
+ });
435
+ const snapshot = await getRunStatus(ref);
436
+ return textResult(
437
+ {
438
+ tool: TOOL_NAME,
439
+ action,
440
+ status: snapshot?.status ?? record.status,
441
+ snapshot,
442
+ },
443
+ false,
444
+ { snapshot, record },
445
+ );
446
+ }
447
+
448
+ if (action === "interrupt") {
449
+ const signal = optionalString(raw.signal, "signal") as
450
+ | NodeJS.Signals
451
+ | undefined;
452
+ if (
453
+ signal !== undefined &&
454
+ signal !== "SIGINT" &&
455
+ signal !== "SIGTERM" &&
456
+ signal !== "SIGKILL"
457
+ ) {
458
+ throw new InputValidationError(
459
+ 'signal must be one of "SIGINT", "SIGTERM", or "SIGKILL" when provided.',
460
+ );
461
+ }
462
+ const interrupted = await interruptRun({
463
+ cwd: ref.cwd,
464
+ runId,
465
+ runsDir: ref.runsDir,
466
+ attemptId: ref.attemptId,
467
+ reason: optionalString(raw.reason, "reason"),
468
+ signal,
469
+ escalateAfterMs: optionalPositiveNumber(
470
+ raw.escalateAfterMs,
471
+ "escalateAfterMs",
472
+ ),
473
+ killAfterMs: optionalPositiveNumber(raw.killAfterMs, "killAfterMs"),
474
+ });
475
+ const snapshot = await getRunStatus(ref);
476
+ const isError =
477
+ interrupted.status === "not-found" ||
478
+ interrupted.status === "unsupported";
479
+ return textResult(
480
+ {
481
+ tool: TOOL_NAME,
482
+ action,
483
+ status: interrupted.status,
484
+ interrupted,
485
+ snapshot,
486
+ },
487
+ isError,
488
+ { interrupted, snapshot },
489
+ );
490
+ }
491
+
492
+ if (action === "reconcile") {
493
+ const reconciled = await reconcileSubagentRun(ref);
494
+ const snapshot = await getRunStatus(ref);
495
+ return textResult(
496
+ {
497
+ tool: TOOL_NAME,
498
+ action,
499
+ status: reconciled.status,
500
+ reconciled,
501
+ snapshot,
502
+ },
503
+ reconciled.status === "not-found",
504
+ { reconciled, snapshot },
505
+ );
506
+ }
507
+
508
+ const waited = await waitForRun({
509
+ ...ref,
510
+ timeoutMs: optionalPositiveNumber(raw.timeoutMs, "timeoutMs"),
511
+ pollIntervalMs: optionalPositiveNumber(
512
+ raw.pollIntervalMs,
513
+ "pollIntervalMs",
514
+ ),
515
+ });
516
+ const isError =
517
+ waited.status !== "completed" || waited.snapshot?.status !== "completed";
518
+ return textResult(
519
+ {
520
+ tool: TOOL_NAME,
521
+ action,
522
+ status: waited.status,
523
+ snapshot: waited.snapshot,
524
+ },
525
+ isError,
526
+ { waited },
527
+ );
361
528
  }
362
529
 
363
530
  function executionMode(input: ResolveInput): ExecutionMode {
364
- if (input.mode !== undefined) return input.mode;
365
- if (input.tasks !== undefined) return "parallel";
366
- return "single";
367
- }
368
-
369
- function unsupportedPathError(raw: Record<string, unknown>, input: ResolveInput, backend: ResolvedBackend): string | undefined {
370
- const mode = executionMode(input);
371
- const unknownKeys = Object.keys(raw).filter((key) => !SUPPORTED_KEYS.has(key));
372
- if (unknownKeys.length > 0) {
373
- return `unsupported subagent option(s): ${formatKeyList(unknownKeys)}.`;
374
- }
375
-
376
- if (mode === "parallel") {
377
- return input.tasks === undefined ? "parallel mode requires a non-empty tasks array." : undefined;
378
- }
379
-
380
- if (backend !== "inline" && backend !== "headless" && backend !== "tmux") {
381
- return `backend "${backend}" is not implemented in this MVP; only inline, headless, and tmux execution are supported.`;
382
- }
383
-
384
- if (hasAnyKey(raw, AGENT_TASK_KEYS) && input.task === undefined) {
385
- return `${backend} agent/task execution requires a non-empty "task".`;
386
- }
387
-
388
- if (!hasAnyKey(raw, AGENT_TASK_KEYS)) {
389
- return `${backend} execution requires agent/task input.`;
390
- }
391
-
392
- return undefined;
393
- }
394
-
395
- async function writeUnsupportedResult(cwd: string, backend: ResolvedBackend, input: ResolveInput): Promise<ResultEnvelope> {
396
- const startedAt = new Date();
397
- const store = await createAttemptArtifactStore({ cwd, runsDir: input.runsDir });
398
- const sandboxed = input.sandbox !== undefined && input.sandbox !== null;
399
- return await store.writeResult({
400
- backend,
401
- status: "failed",
402
- failureKind: "validation",
403
- cwd,
404
- startedAt,
405
- completedAt: new Date(),
406
- workspace: { mode: "shared", cwd },
407
- sandbox: { enabled: sandboxed },
408
- exitCode: null,
409
- signal: null,
410
- artifacts: [],
411
- correlationId: input.correlationId,
412
- metadata: { contextLengthExceeded: false },
413
- });
531
+ if (input.mode !== undefined) return input.mode;
532
+ if (input.tasks !== undefined) return "parallel";
533
+ return "single";
534
+ }
535
+
536
+ function unsupportedPathError(
537
+ raw: Record<string, unknown>,
538
+ input: ResolveInput,
539
+ backend: ResolvedBackend,
540
+ ): string | undefined {
541
+ const mode = executionMode(input);
542
+ const unknownKeys = Object.keys(raw).filter(
543
+ (key) => !SUPPORTED_KEYS.has(key),
544
+ );
545
+ if (unknownKeys.length > 0) {
546
+ return `unsupported subagent option(s): ${formatKeyList(unknownKeys)}.`;
547
+ }
548
+
549
+ if (mode === "parallel") {
550
+ return input.tasks === undefined
551
+ ? "parallel mode requires a non-empty tasks array."
552
+ : undefined;
553
+ }
554
+
555
+ if (backend !== "inline" && backend !== "headless" && backend !== "tmux") {
556
+ return `backend "${backend}" is not implemented in this MVP; only inline, headless, and tmux execution are supported.`;
557
+ }
558
+
559
+ if (hasAnyKey(raw, AGENT_TASK_KEYS) && input.task === undefined) {
560
+ return `${backend} agent/task execution requires a non-empty "task".`;
561
+ }
562
+
563
+ if (!hasAnyKey(raw, AGENT_TASK_KEYS)) {
564
+ return `${backend} execution requires agent/task input.`;
565
+ }
566
+
567
+ return undefined;
568
+ }
569
+
570
+ async function writeUnsupportedResult(
571
+ cwd: string,
572
+ backend: ResolvedBackend,
573
+ input: ResolveInput,
574
+ ): Promise<ResultEnvelope> {
575
+ const startedAt = new Date();
576
+ const store = await createAttemptArtifactStore({
577
+ cwd,
578
+ runsDir: input.runsDir,
579
+ });
580
+ const sandboxed = input.sandbox !== undefined && input.sandbox !== null;
581
+ return await store.writeResult({
582
+ backend,
583
+ status: "failed",
584
+ failureKind: "validation",
585
+ cwd,
586
+ startedAt,
587
+ completedAt: new Date(),
588
+ workspace: { mode: "shared", cwd },
589
+ sandbox: { enabled: sandboxed },
590
+ exitCode: null,
591
+ signal: null,
592
+ artifacts: [],
593
+ correlationId: input.correlationId,
594
+ metadata: { contextLengthExceeded: false },
595
+ });
414
596
  }
415
597
 
416
598
  interface ToolUpdateCallback {
417
- (update: { content: ToolTextContent[]; details: unknown }): void;
599
+ (update: { content: ToolTextContent[]; details: unknown }): void;
418
600
  }
419
601
 
420
602
  interface NotificationContext {
421
- ui?: {
422
- notify?: (message: string, level?: "info" | "warning" | "error") => void;
423
- };
603
+ ui?: {
604
+ notify?: (message: string, level?: "info" | "warning" | "error") => void;
605
+ };
424
606
  }
425
607
 
426
608
  interface ProjectAgentApprovalContext extends NotificationContext {
427
- hasUI?: boolean;
428
- ui?: NotificationContext["ui"] & {
429
- confirm?: (title: string, message?: string) => Promise<boolean> | boolean;
430
- };
609
+ hasUI?: boolean;
610
+ ui?: NotificationContext["ui"] & {
611
+ confirm?: (title: string, message?: string) => Promise<boolean> | boolean;
612
+ };
431
613
  }
432
614
 
433
615
  interface AgentRequest {
434
- agent: string;
435
- cwd?: string;
436
- agentScope?: ResolveInput["agentScope"];
437
- confirmProjectAgents?: boolean;
616
+ agent: string;
617
+ cwd?: string;
618
+ agentScope?: ResolveInput["agentScope"];
619
+ confirmProjectAgents?: boolean;
438
620
  }
439
621
 
440
622
  function agentRequests(input: ResolveInput): AgentRequest[] {
441
- if (input.tasks !== undefined) {
442
- return input.tasks
443
- .filter((task): task is typeof task & { agent: string } => typeof task.agent === "string" && task.agent.length > 0)
444
- .map((task) => ({
445
- agent: task.agent,
446
- cwd: task.cwd,
447
- agentScope: task.agentScope ?? input.agentScope,
448
- confirmProjectAgents: task.confirmProjectAgents ?? input.confirmProjectAgents ?? false,
449
- }));
450
- }
451
- return typeof input.agent === "string" && input.agent.length > 0
452
- ? [{ agent: input.agent, cwd: input.cwd, agentScope: input.agentScope, confirmProjectAgents: input.confirmProjectAgents ?? false }]
453
- : [];
454
- }
455
-
456
- async function maybeConfirmProjectAgents(input: ResolveInput, cwd: string, ctx?: ProjectAgentApprovalContext): Promise<void> {
457
- const projectAgents: AgentDefinition[] = [];
458
- for (const request of agentRequests(input)) {
459
- if (request.confirmProjectAgents === false || request.agentScope === "global") continue;
460
- const requestCwd = resolve(cwd, request.cwd ?? ".");
461
- const agent = await loadAgentByName(request.agent, requestCwd, request.agentScope);
462
- if (agent?.source === "project" && !projectAgents.some((candidate) => candidate.sourcePath === agent.sourcePath)) {
463
- projectAgents.push(agent);
464
- }
465
- }
466
- if (projectAgents.length === 0) return;
467
-
468
- const names = projectAgents.map((agent) => agent.displayName).join(", ");
469
- const sources = projectAgents.map((agent) => agent.sourcePath).join("\n");
470
- if (ctx?.hasUI && ctx.ui?.confirm) {
471
- const approved = await ctx.ui.confirm(
472
- "Run project-local subagent definitions?",
473
- `Agents: ${names}\nSources:\n${sources}\n\nProject agents are repository-controlled. Continue only for trusted repositories.`,
474
- );
475
- if (!approved) throw new Error("Canceled: project-local subagent definitions were not approved.");
476
- return;
477
- }
478
-
479
- throw new Error('Project-local subagent definitions require interactive approval or confirmProjectAgents:false.');
623
+ if (input.tasks !== undefined) {
624
+ return input.tasks
625
+ .filter(
626
+ (task): task is typeof task & { agent: string } =>
627
+ typeof task.agent === "string" && task.agent.length > 0,
628
+ )
629
+ .map((task) => ({
630
+ agent: task.agent,
631
+ cwd: task.cwd,
632
+ agentScope: task.agentScope ?? input.agentScope,
633
+ confirmProjectAgents:
634
+ task.confirmProjectAgents ?? input.confirmProjectAgents ?? false,
635
+ }));
636
+ }
637
+ return typeof input.agent === "string" && input.agent.length > 0
638
+ ? [
639
+ {
640
+ agent: input.agent,
641
+ cwd: input.cwd,
642
+ agentScope: input.agentScope,
643
+ confirmProjectAgents: input.confirmProjectAgents ?? false,
644
+ },
645
+ ]
646
+ : [];
647
+ }
648
+
649
+ async function maybeConfirmProjectAgents(
650
+ input: ResolveInput,
651
+ cwd: string,
652
+ ctx?: ProjectAgentApprovalContext,
653
+ ): Promise<void> {
654
+ const projectAgents: AgentDefinition[] = [];
655
+ for (const request of agentRequests(input)) {
656
+ if (
657
+ request.confirmProjectAgents === false ||
658
+ request.agentScope === "global"
659
+ )
660
+ continue;
661
+ const requestCwd = resolve(cwd, request.cwd ?? ".");
662
+ const agent = await loadAgentByName(
663
+ request.agent,
664
+ requestCwd,
665
+ request.agentScope,
666
+ );
667
+ if (
668
+ agent?.source === "project" &&
669
+ !projectAgents.some(
670
+ (candidate) => candidate.sourcePath === agent.sourcePath,
671
+ )
672
+ ) {
673
+ projectAgents.push(agent);
674
+ }
675
+ }
676
+ if (projectAgents.length === 0) return;
677
+
678
+ const names = projectAgents.map((agent) => agent.displayName).join(", ");
679
+ const sources = projectAgents.map((agent) => agent.sourcePath).join("\n");
680
+ if (ctx?.hasUI && ctx.ui?.confirm) {
681
+ const approved = await ctx.ui.confirm(
682
+ "Run project-local subagent definitions?",
683
+ `Agents: ${names}\nSources:\n${sources}\n\nProject agents are repository-controlled. Continue only for trusted repositories.`,
684
+ );
685
+ if (!approved)
686
+ throw new Error(
687
+ "Canceled: project-local subagent definitions were not approved.",
688
+ );
689
+ return;
690
+ }
691
+
692
+ throw new Error(
693
+ "Project-local subagent definitions require interactive approval or confirmProjectAgents:false.",
694
+ );
480
695
  }
481
696
 
482
697
  function completionPayload(result: ResultEnvelope, mode: ExecutionMode) {
483
- return {
484
- tool: TOOL_NAME,
485
- event: "complete",
486
- mode,
487
- runId: result.runId,
488
- attemptId: result.attemptId,
489
- backend: result.backend,
490
- status: result.status,
491
- failureKind: result.failureKind,
492
- artifacts: artifactSummary(result.artifacts),
493
- };
494
- }
495
-
496
- function notifyCompletion(input: ResolveInput, result: ResultEnvelope, mode: ExecutionMode, onUpdate?: ToolUpdateCallback, ctx?: NotificationContext): number {
497
- if (input.onComplete !== "notify") return 0;
498
- const payload = completionPayload(result, mode);
499
- let updatesSent = 0;
500
- try {
501
- onUpdate?.({ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], details: payload });
502
- if (onUpdate) updatesSent += 1;
503
- } catch {
504
- // Completion notifications must not change the task result.
505
- }
506
- try {
507
- ctx?.ui?.notify?.(`subagent ${result.runId}/${result.attemptId} ${result.status}`, result.status === "completed" ? "info" : "warning");
508
- if (ctx?.ui?.notify) updatesSent += 1;
509
- } catch {
510
- // Completion notifications must not change the task result.
511
- }
512
- return updatesSent;
698
+ return {
699
+ tool: TOOL_NAME,
700
+ event: "complete",
701
+ mode,
702
+ runId: result.runId,
703
+ attemptId: result.attemptId,
704
+ backend: result.backend,
705
+ status: result.status,
706
+ failureKind: result.failureKind,
707
+ artifacts: artifactSummary(result.artifacts),
708
+ };
513
709
  }
514
710
 
711
+ function notifyCompletion(
712
+ input: ResolveInput,
713
+ result: ResultEnvelope,
714
+ mode: ExecutionMode,
715
+ onUpdate?: ToolUpdateCallback,
716
+ ctx?: NotificationContext,
717
+ ): number {
718
+ if (input.onComplete !== "notify") return 0;
719
+ const payload = completionPayload(result, mode);
720
+ let updatesSent = 0;
721
+ try {
722
+ onUpdate?.({
723
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
724
+ details: payload,
725
+ });
726
+ if (onUpdate) updatesSent += 1;
727
+ } catch {
728
+ // Completion notifications must not change the task result.
729
+ }
730
+ try {
731
+ ctx?.ui?.notify?.(
732
+ `subagent ${result.runId}/${result.attemptId} ${result.status}`,
733
+ result.status === "completed" ? "info" : "warning",
734
+ );
735
+ if (ctx?.ui?.notify) updatesSent += 1;
736
+ } catch {
737
+ // Completion notifications must not change the task result.
738
+ }
739
+ return updatesSent;
740
+ }
515
741
 
516
742
  export default function registerSubagentEngine(pi: ExtensionAPI) {
517
- if (typeof pi.registerCommand === "function") {
518
- pi.registerCommand("subagent", {
519
- description: "Subagent utilities. Use `/subagent panel` to open the live status panel.",
520
- getArgumentCompletions(prefix) {
521
- const items = [{ value: "panel", label: "panel", description: "Open the live Subagents status panel" }];
522
- const filtered = items.filter((item) => item.value.startsWith(prefix.trim()));
523
- return filtered.length > 0 ? filtered : null;
524
- },
525
- async handler(args, ctx) {
526
- if (args.trim() !== "panel") {
527
- ctx.ui.notify?.('Usage: /subagent panel', "warning");
528
- return;
529
- }
530
- await showSubagentPanel(ctx);
531
- },
532
- });
533
- }
534
-
535
- pi.registerTool({
536
- name: TOOL_NAME,
537
- label: "Subagent",
538
- description:
539
- "Subagent engine. Executes headless/tmux/inline workers; supports workspace:auto/worktree isolation, bounded parallel fanout, async lifecycle lookup, mark-background, reconcile, and conservative interrupt. Workspaces default to shared; set worktree:true for parallel tasks that mutate files.",
540
- parameters: Type.Object({
541
- backend: Type.Optional(Type.Union(BACKENDS.map((value) => Type.Literal(value)))),
542
- visible: Type.Optional(Type.Boolean()),
543
- sandbox: Type.Optional(SANDBOX_SCHEMA),
544
- agent: Type.Optional(Type.String({ minLength: 1 })),
545
- task: Type.Optional(Type.String({ minLength: 1 })),
546
- roleContext: Type.Optional(Type.String({ minLength: 1 })),
547
- agentScope: Type.Optional(Type.Union(AGENT_SCOPES.map((value) => Type.Literal(value)))),
548
- confirmProjectAgents: Type.Optional(Type.Boolean()),
549
- mode: Type.Optional(Type.Union(EXECUTION_MODES.map((value) => Type.Literal(value)))),
550
- tasks: Type.Optional(Type.Array(SUBAGENT_TASK_SCHEMA, { minItems: 1 })),
551
- concurrency: Type.Optional(Type.Number({ minimum: 1, description: `Maximum parallel runs to launch at once. Default ${DEFAULT_PARALLEL_CONCURRENCY}.` })),
552
- asyncDependency: Type.Optional(Type.Union(ASYNC_DEPENDENCIES.map((value) => Type.Literal(value)), { description: "Whether an async run is needed before final, background, or unclassified." })),
553
- workspace: Type.Optional(
554
- Type.Union([
555
- Type.Union(WORKSPACE_MODES.map((value) => Type.Literal(value))),
556
- Type.Object({
557
- mode: Type.Optional(Type.Union(WORKSPACE_MODES.map((value) => Type.Literal(value)))),
558
- path: Type.Optional(Type.String({ minLength: 1 })),
559
- }),
560
- ]),
561
- ),
562
- worktree: Type.Optional(Type.Union([Type.Boolean(), Type.String({ minLength: 1 })])),
563
- worktreePolicy: Type.Optional(Type.Union(WORKTREE_POLICIES.map((value) => Type.Literal(value)))),
564
- cwd: Type.Optional(Type.String({ minLength: 1 })),
565
- async: Type.Optional(Type.Boolean()),
566
- onComplete: Type.Optional(Type.Union(ON_COMPLETE_ACTIONS.map((value) => Type.Literal(value)))),
567
- timeoutMs: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
568
- model: Type.Optional(Type.String({ minLength: 1, description: "Optional Pi model pattern or provider/model id for model-backed subagents." })),
569
- tools: Type.Optional(Type.Array(Type.String({ minLength: 1 }), { description: "Optional tool allowlist. With a named agent this may only narrow the agent-declared tools. Use [] to disable tools." })),
570
- systemPrompt: Type.Optional(Type.String({ minLength: 1, description: "Optional compiled system prompt. When provided, it replaces the named agent prompt body but not agent frontmatter policy." })),
571
- skills: Type.Optional(Type.Array(Type.String({ minLength: 1 }), { description: "Additional Pi skill paths to load. Omit to use ambient discovery; pass [] to disable child skills." })),
572
- extensions: Type.Optional(Type.Array(Type.String({ minLength: 1 }), { description: "Additional Pi extension paths to load. Omit to use ambient discovery; pass [] to disable child extensions." })),
573
- runsDir: Type.Optional(Type.String({ minLength: 1, description: "Safe relative run/artifact root under cwd." })),
574
- correlationId: Type.Optional(Type.String({ minLength: 1, description: "External correlation label; no aggregation semantics." })),
575
- captureToolCalls: Type.Optional(Type.Boolean({ description: "Capture redacted child tool-call telemetry (tool names, durations, statuses; no args/results) as run artifacts. Default false." })),
576
- thinking: Type.Optional(Type.Union(THINKING_LEVELS.map((value) => Type.Literal(value)), { description: "Optional Pi thinking/reasoning level." })),
577
- thinkingLevel: Type.Optional(Type.Union(THINKING_LEVELS.map((value) => Type.Literal(value)), { description: "Alias for thinking." })),
578
- reasoningLevel: Type.Optional(Type.Union(THINKING_LEVELS.map((value) => Type.Literal(value)), { description: "Alias for thinking." })),
579
- action: Type.Optional(
580
- Type.Union(
581
- [Type.Literal("run"), Type.Literal("status"), Type.Literal("logs"), Type.Literal("wait"), Type.Literal("interrupt"), Type.Literal("mark-background"), Type.Literal("reconcile")],
582
- { default: "run", description: "What to do. Default \"run\" starts a new subagent. status/logs/wait/interrupt/mark-background/reconcile operate on an existing runId." },
583
- ),
584
- ),
585
- runId: Type.Optional(Type.String({ minLength: 1 })),
586
- attemptId: Type.Optional(Type.String({ minLength: 1 })),
587
- taskId: Type.Optional(Type.String({ minLength: 1, description: "Deprecated alias for attemptId when reading old runs." })),
588
- pollIntervalMs: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
589
- reason: Type.Optional(Type.String({ minLength: 1 })),
590
- signal: Type.Optional(Type.Union([Type.Literal("SIGINT"), Type.Literal("SIGTERM"), Type.Literal("SIGKILL")])),
591
- escalateAfterMs: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
592
- killAfterMs: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
593
- }),
594
- renderCall(args, theme) {
595
- const title = theme.fg("toolTitle", theme.bold("subagent"));
596
- const summary = subagentCallSummary(args);
597
- const rest = summary.startsWith("subagent ") ? summary.slice("subagent ".length) : summary;
598
- return new SingleLineComponent(`${title} ${theme.fg("muted", rest)}`);
599
- },
600
- async execute(toolCallIdOrArgs, maybeParams, signal, onUpdate, ctx) {
601
- const params = getExecuteParams(toolCallIdOrArgs, maybeParams);
602
- const cwd = getCwd(ctx);
603
-
604
- try {
605
- const raw = isRecord(params) ? params : {};
606
- const lifecycle = await lifecycleAction(raw, cwd);
607
- if (lifecycle !== null) return lifecycle;
608
-
609
- const validation = validateResolveInput(params);
610
- if (!validation.ok) return validationFailure(validation.failure);
611
-
612
- const parentSessionId = parentSessionIdFromCtx(ctx);
613
- if (parentSessionId !== undefined) validation.input.parentSessionId = parentSessionId;
614
-
615
- const resolved = resolveBackend(validation.input);
616
- if (resolved.status === "failed") return validationFailure(resolved);
617
-
618
- const unsupportedError = unsupportedPathError(raw, validation.input, resolved.backend);
619
- if (unsupportedError) {
620
- const result = await writeUnsupportedResult(cwd, resolved.backend, validation.input);
621
- return textResult(compactResult(result, unsupportedError), true, { result, resolved });
622
- }
623
-
624
- const runCwd = validation.input.cwd ?? cwd;
625
- await maybeConfirmProjectAgents(validation.input, runCwd, ctx as ProjectAgentApprovalContext);
626
- const mode = executionMode(validation.input);
627
- const asyncRequested = validation.input.async === true || validation.input.onComplete === "detach" || validation.input.onComplete === "notify";
628
- if (mode === "parallel") {
629
- const parallel = asyncRequested
630
- ? await startAsyncParallelSubagentRuns(validation.input, runCwd, signal, (completed, completedMode) => notifyCompletion(validation.input, completed, completedMode, onUpdate, ctx as NotificationContext))
631
- : await runParallelSubagentTasks(validation.input, runCwd, signal);
632
- const runs = parallel.results.map((result) => compactResult(result));
633
- const failed = !asyncRequested && parallel.results.some((result) => result.status !== "completed");
634
- return textResult(
635
- {
636
- tool: TOOL_NAME,
637
- mode: "parallel",
638
- status: failed ? "failed" : asyncRequested ? "running" : "completed",
639
- runIds: parallel.runIds,
640
- concurrencyLimit: parallel.concurrency,
641
- runs,
642
- },
643
- failed,
644
- { results: parallel.results, resolved },
645
- );
646
- }
647
-
648
- if (asyncRequested) {
649
- const result = await startAsyncSubagentRun({
650
- input: validation.input,
651
- cwd: runCwd,
652
- backend: resolved.backend,
653
- signal,
654
- onComplete: (completed, completedMode) => notifyCompletion(validation.input, completed, completedMode, onUpdate, ctx as NotificationContext),
655
- });
656
- return textResult(compactResult(result), false, { result, resolved });
657
- }
658
-
659
- const result = await runSubagentTask({ input: validation.input, cwd: runCwd, signal });
660
- return textResult(compactResult(result), result.status !== "completed", { result, resolved });
661
- } catch (error) {
662
- const message = error instanceof Error ? error.message : String(error);
663
- const failureKind = error instanceof WorkspacePolicyError || error instanceof InputValidationError
664
- ? error.failureKind
665
- : typeof error === "object" && error !== null && (error as { failureKind?: unknown }).failureKind === "validation"
666
- ? "validation"
667
- : "internal";
668
- return textResult(
669
- {
670
- tool: TOOL_NAME,
671
- status: "failed",
672
- failureKind,
673
- error: message,
674
- },
675
- true,
676
- );
677
- }
678
- },
679
- });
743
+ if (typeof pi.registerCommand === "function") {
744
+ pi.registerCommand("subagent", {
745
+ description:
746
+ "Subagent utilities. Use `/subagent panel` to open the live status panel.",
747
+ getArgumentCompletions(prefix) {
748
+ const items = [
749
+ {
750
+ value: "panel",
751
+ label: "panel",
752
+ description: "Open the live Subagents status panel",
753
+ },
754
+ ];
755
+ const filtered = items.filter((item) =>
756
+ item.value.startsWith(prefix.trim()),
757
+ );
758
+ return filtered.length > 0 ? filtered : null;
759
+ },
760
+ async handler(args, ctx) {
761
+ const commandArgs = args.trim();
762
+ const normalizedArgs = commandArgs
763
+ .replace(/^\/?subagent\b\s*/, "")
764
+ .trim();
765
+ if (normalizedArgs !== "panel") {
766
+ ctx.ui.notify?.("Usage: /subagent panel", "warning");
767
+ return;
768
+ }
769
+ await showSubagentPanel(ctx);
770
+ },
771
+ });
772
+ }
773
+
774
+ pi.registerTool({
775
+ name: TOOL_NAME,
776
+ label: "Subagent",
777
+ description:
778
+ "Subagent engine. Executes headless/tmux/inline workers; supports workspace:auto/worktree isolation, bounded parallel fanout, async lifecycle lookup, mark-background, reconcile, and conservative interrupt. Workspaces default to shared; set worktree:true for parallel tasks that mutate files.",
779
+ parameters: Type.Object({
780
+ backend: Type.Optional(
781
+ Type.Union(BACKENDS.map((value) => Type.Literal(value))),
782
+ ),
783
+ visible: Type.Optional(Type.Boolean()),
784
+ sandbox: Type.Optional(SANDBOX_SCHEMA),
785
+ agent: Type.Optional(Type.String({ minLength: 1 })),
786
+ task: Type.Optional(Type.String({ minLength: 1 })),
787
+ roleContext: Type.Optional(Type.String({ minLength: 1 })),
788
+ agentScope: Type.Optional(
789
+ Type.Union(AGENT_SCOPES.map((value) => Type.Literal(value))),
790
+ ),
791
+ confirmProjectAgents: Type.Optional(Type.Boolean()),
792
+ mode: Type.Optional(
793
+ Type.Union(EXECUTION_MODES.map((value) => Type.Literal(value))),
794
+ ),
795
+ tasks: Type.Optional(Type.Array(SUBAGENT_TASK_SCHEMA, { minItems: 1 })),
796
+ concurrency: Type.Optional(
797
+ Type.Number({
798
+ minimum: 1,
799
+ description: `Maximum parallel runs to launch at once. Default ${DEFAULT_PARALLEL_CONCURRENCY}.`,
800
+ }),
801
+ ),
802
+ failFast: Type.Optional(
803
+ Type.Boolean({
804
+ description:
805
+ "For synchronous parallel runs, stop scheduling additional siblings after the first failed result.",
806
+ }),
807
+ ),
808
+ cancelSiblingsOnFailure: Type.Optional(
809
+ Type.Boolean({
810
+ description:
811
+ "For synchronous parallel runs, abort already-running siblings after the first failed result. Implies fail-fast scheduling.",
812
+ }),
813
+ ),
814
+ asyncDependency: Type.Optional(
815
+ Type.Union(
816
+ ASYNC_DEPENDENCIES.map((value) => Type.Literal(value)),
817
+ {
818
+ description:
819
+ "Whether an async run is needed before final, background, or unclassified.",
820
+ },
821
+ ),
822
+ ),
823
+ workspace: Type.Optional(
824
+ Type.Union([
825
+ Type.Union(WORKSPACE_MODES.map((value) => Type.Literal(value))),
826
+ Type.Object({
827
+ mode: Type.Optional(
828
+ Type.Union(WORKSPACE_MODES.map((value) => Type.Literal(value))),
829
+ ),
830
+ path: Type.Optional(Type.String({ minLength: 1 })),
831
+ }),
832
+ ]),
833
+ ),
834
+ worktree: Type.Optional(
835
+ Type.Union([Type.Boolean(), Type.String({ minLength: 1 })]),
836
+ ),
837
+ worktreePolicy: Type.Optional(
838
+ Type.Union(WORKTREE_POLICIES.map((value) => Type.Literal(value))),
839
+ ),
840
+ cwd: Type.Optional(Type.String({ minLength: 1 })),
841
+ async: Type.Optional(Type.Boolean()),
842
+ onComplete: Type.Optional(
843
+ Type.Union(ON_COMPLETE_ACTIONS.map((value) => Type.Literal(value))),
844
+ ),
845
+ timeoutMs: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
846
+ model: Type.Optional(
847
+ Type.String({
848
+ minLength: 1,
849
+ description:
850
+ "Optional Pi model pattern or provider/model id for model-backed subagents.",
851
+ }),
852
+ ),
853
+ tools: Type.Optional(
854
+ Type.Array(Type.String({ minLength: 1 }), {
855
+ description:
856
+ "Optional tool allowlist. With a named agent this may only narrow the agent-declared tools. Use [] to disable tools.",
857
+ }),
858
+ ),
859
+ systemPrompt: Type.Optional(
860
+ Type.String({
861
+ minLength: 1,
862
+ description:
863
+ "Optional compiled system prompt. When provided, it replaces the named agent prompt body but not agent frontmatter policy.",
864
+ }),
865
+ ),
866
+ skills: Type.Optional(
867
+ Type.Array(Type.String({ minLength: 1 }), {
868
+ description:
869
+ "Additional Pi skill paths to load. Omit to use ambient discovery; pass [] to disable child skills.",
870
+ }),
871
+ ),
872
+ extensions: Type.Optional(
873
+ Type.Array(Type.String({ minLength: 1 }), {
874
+ description:
875
+ "Additional Pi extension paths to load. Omit to use ambient discovery; pass [] to disable child extensions.",
876
+ }),
877
+ ),
878
+ runsDir: Type.Optional(
879
+ Type.String({
880
+ minLength: 1,
881
+ description: "Safe relative run/artifact root under cwd.",
882
+ }),
883
+ ),
884
+ correlationId: Type.Optional(
885
+ Type.String({
886
+ minLength: 1,
887
+ description: "External correlation label; no aggregation semantics.",
888
+ }),
889
+ ),
890
+ captureToolCalls: Type.Optional(
891
+ Type.Boolean({
892
+ description:
893
+ "Capture redacted child tool-call telemetry (tool names, durations, statuses; no args/results) as run artifacts. Default false.",
894
+ }),
895
+ ),
896
+ thinking: Type.Optional(
897
+ Type.Union(
898
+ THINKING_LEVELS.map((value) => Type.Literal(value)),
899
+ { description: "Optional Pi thinking/reasoning level." },
900
+ ),
901
+ ),
902
+ thinkingLevel: Type.Optional(
903
+ Type.Union(
904
+ THINKING_LEVELS.map((value) => Type.Literal(value)),
905
+ { description: "Alias for thinking." },
906
+ ),
907
+ ),
908
+ reasoningLevel: Type.Optional(
909
+ Type.Union(
910
+ THINKING_LEVELS.map((value) => Type.Literal(value)),
911
+ { description: "Alias for thinking." },
912
+ ),
913
+ ),
914
+ action: Type.Optional(
915
+ Type.Union(
916
+ [
917
+ Type.Literal("run"),
918
+ Type.Literal("status"),
919
+ Type.Literal("logs"),
920
+ Type.Literal("wait"),
921
+ Type.Literal("interrupt"),
922
+ Type.Literal("mark-background"),
923
+ Type.Literal("reconcile"),
924
+ ],
925
+ {
926
+ default: "run",
927
+ description:
928
+ 'What to do. Default "run" starts a new subagent. status/logs/wait/interrupt/mark-background/reconcile operate on an existing runId.',
929
+ },
930
+ ),
931
+ ),
932
+ runId: Type.Optional(Type.String({ minLength: 1 })),
933
+ attemptId: Type.Optional(Type.String({ minLength: 1 })),
934
+ taskId: Type.Optional(
935
+ Type.String({
936
+ minLength: 1,
937
+ description: "Deprecated alias for attemptId when reading old runs.",
938
+ }),
939
+ ),
940
+ pollIntervalMs: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
941
+ reason: Type.Optional(Type.String({ minLength: 1 })),
942
+ signal: Type.Optional(
943
+ Type.Union([
944
+ Type.Literal("SIGINT"),
945
+ Type.Literal("SIGTERM"),
946
+ Type.Literal("SIGKILL"),
947
+ ]),
948
+ ),
949
+ escalateAfterMs: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
950
+ killAfterMs: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
951
+ }),
952
+ renderCall(args, theme) {
953
+ const title = theme.fg("toolTitle", theme.bold("subagent"));
954
+ const summary = subagentCallSummary(args);
955
+ const rest = summary.startsWith("subagent ")
956
+ ? summary.slice("subagent ".length)
957
+ : summary;
958
+ return new SingleLineComponent(`${title} ${theme.fg("muted", rest)}`);
959
+ },
960
+ async execute(toolCallIdOrArgs, maybeParams, signal, onUpdate, ctx) {
961
+ const params = getExecuteParams(toolCallIdOrArgs, maybeParams);
962
+ const cwd = getCwd(ctx);
963
+
964
+ try {
965
+ const raw = isRecord(params) ? params : {};
966
+ const lifecycle = await lifecycleAction(raw, cwd);
967
+ if (lifecycle !== null) return lifecycle;
968
+
969
+ const validation = validateResolveInput(params);
970
+ if (!validation.ok) return validationFailure(validation.failure);
971
+
972
+ const parentSessionId = parentSessionIdFromCtx(ctx);
973
+ if (parentSessionId !== undefined)
974
+ validation.input.parentSessionId = parentSessionId;
975
+
976
+ const resolved = resolveBackend(validation.input);
977
+ if (resolved.status === "failed") return validationFailure(resolved);
978
+
979
+ const unsupportedError = unsupportedPathError(
980
+ raw,
981
+ validation.input,
982
+ resolved.backend,
983
+ );
984
+ if (unsupportedError) {
985
+ const result = await writeUnsupportedResult(
986
+ cwd,
987
+ resolved.backend,
988
+ validation.input,
989
+ );
990
+ return textResult(compactResult(result, unsupportedError), true, {
991
+ result,
992
+ resolved,
993
+ });
994
+ }
995
+
996
+ const runCwd = validation.input.cwd ?? cwd;
997
+ await maybeConfirmProjectAgents(
998
+ validation.input,
999
+ runCwd,
1000
+ ctx as ProjectAgentApprovalContext,
1001
+ );
1002
+ const mode = executionMode(validation.input);
1003
+ const asyncRequested =
1004
+ validation.input.async === true ||
1005
+ validation.input.onComplete === "detach" ||
1006
+ validation.input.onComplete === "notify";
1007
+ if (mode === "parallel") {
1008
+ const parallel = asyncRequested
1009
+ ? await startAsyncParallelSubagentRuns(
1010
+ validation.input,
1011
+ runCwd,
1012
+ signal,
1013
+ (completed, completedMode) =>
1014
+ notifyCompletion(
1015
+ validation.input,
1016
+ completed,
1017
+ completedMode,
1018
+ onUpdate,
1019
+ ctx as NotificationContext,
1020
+ ),
1021
+ )
1022
+ : await runParallelSubagentTasks(validation.input, runCwd, signal);
1023
+ const runs = parallel.results.map((result) => compactResult(result));
1024
+ const failed =
1025
+ !asyncRequested &&
1026
+ (parallel.failFastTriggered ||
1027
+ parallel.results.some((result) => result.status !== "completed"));
1028
+ return textResult(
1029
+ {
1030
+ tool: TOOL_NAME,
1031
+ mode: "parallel",
1032
+ status: failed
1033
+ ? "failed"
1034
+ : asyncRequested
1035
+ ? "running"
1036
+ : "completed",
1037
+ runIds: parallel.runIds,
1038
+ concurrencyLimit: parallel.concurrency,
1039
+ totalTasks: parallel.totalTasks,
1040
+ startedCount: parallel.startedCount,
1041
+ skippedCount: parallel.skippedCount,
1042
+ failFastTriggered: parallel.failFastTriggered,
1043
+ runs,
1044
+ },
1045
+ failed,
1046
+ { results: parallel.results, resolved },
1047
+ );
1048
+ }
1049
+
1050
+ if (asyncRequested) {
1051
+ const result = await startAsyncSubagentRun({
1052
+ input: validation.input,
1053
+ cwd: runCwd,
1054
+ backend: resolved.backend,
1055
+ signal,
1056
+ onComplete: (completed, completedMode) =>
1057
+ notifyCompletion(
1058
+ validation.input,
1059
+ completed,
1060
+ completedMode,
1061
+ onUpdate,
1062
+ ctx as NotificationContext,
1063
+ ),
1064
+ });
1065
+ return textResult(compactResult(result), false, { result, resolved });
1066
+ }
1067
+
1068
+ const result = await runSubagentTask({
1069
+ input: validation.input,
1070
+ cwd: runCwd,
1071
+ signal,
1072
+ });
1073
+ return textResult(
1074
+ compactResult(result),
1075
+ result.status !== "completed",
1076
+ { result, resolved },
1077
+ );
1078
+ } catch (error) {
1079
+ const message = error instanceof Error ? error.message : String(error);
1080
+ const failureKind =
1081
+ error instanceof WorkspacePolicyError ||
1082
+ error instanceof InputValidationError
1083
+ ? error.failureKind
1084
+ : typeof error === "object" &&
1085
+ error !== null &&
1086
+ (error as { failureKind?: unknown }).failureKind ===
1087
+ "validation"
1088
+ ? "validation"
1089
+ : "internal";
1090
+ return textResult(
1091
+ {
1092
+ tool: TOOL_NAME,
1093
+ status: "failed",
1094
+ failureKind,
1095
+ error: message,
1096
+ },
1097
+ true,
1098
+ );
1099
+ }
1100
+ },
1101
+ });
680
1102
  }