@narumitw/pi-subagents 0.49.3 → 0.52.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 (83) hide show
  1. package/README.md +362 -53
  2. package/package.json +10 -7
  3. package/src/adaptive-scheduler.ts +224 -0
  4. package/src/admission-benchmark.ts +95 -0
  5. package/src/admission-policy.ts +78 -0
  6. package/src/agent-projection.ts +53 -0
  7. package/src/agents.ts +58 -1
  8. package/src/auto-transport.ts +114 -0
  9. package/src/blocking-status.ts +63 -0
  10. package/src/capabilities.ts +145 -0
  11. package/src/capability-grant.ts +115 -0
  12. package/src/capability-router.ts +107 -0
  13. package/src/completion-delivery.ts +257 -0
  14. package/src/config-status.ts +221 -0
  15. package/src/config-ui.ts +215 -236
  16. package/src/consult-resources.ts +4 -27
  17. package/src/consult.ts +9 -1
  18. package/src/create-stateful-transport.ts +55 -0
  19. package/src/delegation-contract.ts +417 -0
  20. package/src/execution-plan.ts +322 -0
  21. package/src/execution-profiles.ts +95 -0
  22. package/src/execution-ui.ts +320 -0
  23. package/src/execution.ts +1098 -158
  24. package/src/in-process-transport.ts +269 -25
  25. package/src/inspect-render.ts +101 -1
  26. package/src/inspect.ts +321 -3
  27. package/src/integration-controller.ts +98 -0
  28. package/src/limits.ts +3 -0
  29. package/src/orchestration-metrics.ts +109 -0
  30. package/src/outcome.ts +61 -0
  31. package/src/panel-child-group.ts +35 -0
  32. package/src/panel-contract.ts +343 -0
  33. package/src/panel-evidence.ts +59 -0
  34. package/src/panel-execution.ts +770 -0
  35. package/src/panel-failure.ts +56 -0
  36. package/src/panel-planning.ts +175 -0
  37. package/src/panel-prompts.ts +132 -0
  38. package/src/panel-reconciliation.ts +57 -0
  39. package/src/panel-render.ts +103 -0
  40. package/src/parallel-limit-ui.ts +112 -0
  41. package/src/params.ts +179 -3
  42. package/src/persistence.ts +182 -32
  43. package/src/prompt-resources.ts +38 -0
  44. package/src/registry-types.ts +175 -0
  45. package/src/registry.ts +466 -143
  46. package/src/render.ts +72 -6
  47. package/src/result-contract.ts +416 -0
  48. package/src/retained-semantic-state.ts +100 -0
  49. package/src/rpc-timeout-finalization.ts +207 -0
  50. package/src/rpc-transport-metadata.ts +65 -0
  51. package/src/rpc-transport.ts +990 -0
  52. package/src/rpc-turn-capture.ts +142 -0
  53. package/src/runner-result.ts +55 -0
  54. package/src/runner-usage.ts +48 -0
  55. package/src/runner.ts +325 -73
  56. package/src/semantic-snapshot.ts +214 -0
  57. package/src/settings.ts +254 -35
  58. package/src/spawn-idempotency.ts +61 -0
  59. package/src/stateful-config.ts +13 -0
  60. package/src/stateful-guidance.ts +1 -0
  61. package/src/stateful-lifecycle.ts +45 -2
  62. package/src/stateful-limit-ui.ts +246 -0
  63. package/src/stateful-limits.ts +96 -0
  64. package/src/stateful-prompt.ts +11 -2
  65. package/src/stateful-render.ts +48 -3
  66. package/src/stateful.ts +467 -357
  67. package/src/subagents.ts +114 -46
  68. package/src/subprocess-transport.ts +64 -5
  69. package/src/supervision.ts +103 -0
  70. package/src/timeout-checkpoint.ts +305 -0
  71. package/src/timeout-finalization.ts +75 -0
  72. package/src/transport-types.ts +68 -0
  73. package/src/transport-ui.ts +169 -0
  74. package/src/transport.ts +16 -4
  75. package/src/turn-budget.ts +109 -0
  76. package/src/verification-policy.ts +67 -0
  77. package/src/work-item-ledger.ts +931 -0
  78. package/src/work-item-persistence.ts +223 -0
  79. package/src/workflow-planning.ts +162 -0
  80. package/src/workflow-tree-identity.ts +289 -0
  81. package/src/workflow-ui.ts +61 -0
  82. package/src/workflow-verification.ts +296 -0
  83. package/src/workspace.ts +69 -12
package/src/execution.ts CHANGED
@@ -1,5 +1,11 @@
1
+ /**
2
+ * Blocking execution stays in one module so preflight, confirmation, cancellation generation,
3
+ * launch, and settlement retain one ordered lifecycle owner across every mode.
4
+ */
1
5
  import type { AgentToolResult, AgentToolUpdateCallback } from "@earendil-works/pi-agent-core";
2
6
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
7
+ import { AdaptiveScheduler } from "./adaptive-scheduler.js";
8
+ import { evaluateDelegationAdmission } from "./admission-policy.js";
3
9
  import {
4
10
  type AgentConfig,
5
11
  type AgentScope,
@@ -7,14 +13,42 @@ import {
7
13
  type SubagentSettings,
8
14
  type SubagentThinkingLevel,
9
15
  } from "./agents.js";
16
+ import {
17
+ chainStatus,
18
+ fanInStatus,
19
+ parallelStatus,
20
+ singleStatus,
21
+ startSubagentStatus,
22
+ } from "./blocking-status.js";
23
+ import { issueCapabilityGrant } from "./capability-grant.js";
24
+ import { redactPrivateText } from "./context.js";
10
25
  import {
11
26
  assertDelegationTargetAllowed,
12
27
  type ResolvedSubagentTarget,
13
28
  resolveSubagentTarget,
14
29
  targetPolicyAudit,
15
30
  } from "./cwd-policy.js";
16
- import { DEFAULT_MAX_CONTEXT_BYTES, truncateUtf8 } from "./limits.js";
31
+ import {
32
+ appendDelegationContract,
33
+ type DelegationContract,
34
+ normalizeDelegationContract,
35
+ } from "./delegation-contract.js";
36
+ import {
37
+ acknowledgeExecutionPlan,
38
+ createExecutionPlan,
39
+ resolveContractTools,
40
+ } from "./execution-plan.js";
41
+ import {
42
+ DEFAULT_MAX_CONTEXT_BYTES,
43
+ MAX_BLOCKING_PARALLEL_CONCURRENCY,
44
+ MAX_SUBAGENT_TIMEOUT_MS,
45
+ truncateUtf8,
46
+ } from "./limits.js";
47
+ import { calculateOrchestrationMetrics } from "./orchestration-metrics.js";
48
+ import { executePanel, preflightPanelExecution } from "./panel-execution.js";
49
+ import { validatePanelRequest } from "./panel-planning.js";
17
50
  import { hasUsableAggregator, type SubagentParams } from "./params.js";
51
+ import { appendResultInstruction, type SubagentResultFormat } from "./result-contract.js";
18
52
  import {
19
53
  buildFanInContext,
20
54
  formatResultFailure,
@@ -30,14 +64,29 @@ import { safeTerminalLine } from "./safe-text.js";
30
64
  import {
31
65
  DEFAULT_DELEGATION_CWD_POLICY,
32
66
  readSubagentSettings,
67
+ resolveBlockingMaxParallelTasks,
33
68
  resolveSubagentThinkingLevel,
34
69
  } from "./settings.js";
70
+ import { isRetryableResult, runHedgedAttempt, supervisionDelay } from "./supervision.js";
71
+ import { TimeoutProgressJournal, TURN_TERMINATION_VERSION } from "./timeout-checkpoint.js";
72
+ import type { TurnLimits } from "./turn-budget.js";
73
+ import {
74
+ requiresIndependentVerification,
75
+ validateWorkflowVerificationGraph,
76
+ } from "./verification-policy.js";
77
+ import type { WorkItemLedger } from "./work-item-ledger.js";
78
+ import {
79
+ createSessionWorkItemPersistence,
80
+ type WorkItemPersistence,
81
+ } from "./work-item-persistence.js";
82
+ import { createBlockingWorkLedger, resolveWorkflowTasks } from "./workflow-planning.js";
83
+ import { captureWorkflowTreeIdentity, sameWorkflowTreeIdentity } from "./workflow-tree-identity.js";
84
+ import {
85
+ createWorkflowVerificationReceipt,
86
+ workflowVerificationInstruction,
87
+ } from "./workflow-verification.js";
35
88
 
36
- const MAX_PARALLEL_TASKS = 8;
37
- const MAX_CONCURRENCY = 4;
38
89
  export const FALLBACK_TIMEOUT_MS = 10 * 60 * 1000;
39
- const STATUS_KEY = "subagents";
40
- const activeStatuses = new Map<string, string>();
41
90
 
42
91
  export function parsePositiveInteger(value: string | undefined): number | undefined {
43
92
  if (!value) return undefined;
@@ -57,59 +106,6 @@ export function assertSubagentDepthAllowed(): void {
57
106
  }
58
107
  }
59
108
 
60
- interface StatusContext {
61
- ui: { setStatus: (key: string, value: string | undefined) => void };
62
- }
63
-
64
- function startSubagentStatus(ctx: StatusContext, toolCallId: string, status: string) {
65
- let cleared = false;
66
-
67
- const update = (nextStatus: string) => {
68
- if (cleared) return;
69
- activeStatuses.set(toolCallId, nextStatus);
70
- publishSubagentStatus(ctx);
71
- };
72
-
73
- update(status);
74
-
75
- return {
76
- update,
77
- clear() {
78
- if (cleared) return;
79
- cleared = true;
80
- activeStatuses.delete(toolCallId);
81
- publishSubagentStatus(ctx);
82
- },
83
- };
84
- }
85
-
86
- function publishSubagentStatus(ctx: StatusContext) {
87
- const statuses = [...activeStatuses.values()];
88
- if (statuses.length === 0) {
89
- ctx.ui.setStatus(STATUS_KEY, undefined);
90
- return;
91
- }
92
-
93
- const suffix = statuses.length > 1 ? ` +${statuses.length - 1}` : "";
94
- ctx.ui.setStatus(STATUS_KEY, `${statuses[0]}${suffix}`);
95
- }
96
-
97
- function singleStatus(agent: string): string {
98
- return `${agent}`;
99
- }
100
-
101
- function chainStatus(step: number, total: number, agent?: string): string {
102
- return `chain ${step}/${total}${agent ? ` ${agent}` : ""}`;
103
- }
104
-
105
- function parallelStatus(done: number, total: number, running: number): string {
106
- return `parallel ${done}/${total} done${running > 0 ? ` ${running} running` : ""}`;
107
- }
108
-
109
- function fanInStatus(agent: string): string {
110
- return `fan-in ${agent}`;
111
- }
112
-
113
109
  export async function executeSubagent(
114
110
  toolCallId: string,
115
111
  params: SubagentParams,
@@ -125,8 +121,10 @@ export async function executeSubagent(
125
121
  }
126
122
  const aggregator = hasUsableAggregator(params.aggregator) ? params.aggregator : undefined;
127
123
  const config = settingsOverride ?? readSubagentSettings();
124
+ const maxParallelTasks = resolveBlockingMaxParallelTasks(config);
128
125
  const discovery = discoverAgents(ctx.cwd, agentScope, config);
129
126
  const agents = discovery.agents;
127
+ const resolvedWorkflowTasks = resolveWorkflowTasks(params, agents);
130
128
  const confirmProjectAgents = params.confirmProjectAgents ?? true;
131
129
  const resolveTimeoutMs = (agentName: string, localTimeoutMs?: number) =>
132
130
  localTimeoutMs ??
@@ -135,21 +133,145 @@ export async function executeSubagent(
135
133
  resolveDefaultSubagentTimeoutMs();
136
134
  const resolveThinkingLevel = (agentName: string, localThinkingLevel?: SubagentThinkingLevel) =>
137
135
  resolveSubagentThinkingLevel(agents, agentName, params.thinkingLevel, localThinkingLevel);
136
+ let orchestrationDeadline: number | undefined;
137
+ const resolveTurnLimits = (local?: TurnLimits): TurnLimits => ({
138
+ idleTimeoutMs: local?.idleTimeoutMs ?? params.idleTimeoutMs,
139
+ maxTurns: local?.maxTurns ?? params.maxTurns,
140
+ maxToolCalls: local?.maxToolCalls ?? params.maxToolCalls,
141
+ });
142
+ const resolveExecutionBudget = (
143
+ agentName: string,
144
+ localTimeoutMs?: number,
145
+ ):
146
+ | {
147
+ timeoutMs: number;
148
+ workTimeoutReason: "work_timeout" | "orchestration_timeout";
149
+ workTimeoutReportLimit: number;
150
+ }
151
+ | undefined => {
152
+ const requested = resolveTimeoutMs(agentName, localTimeoutMs);
153
+ if (orchestrationDeadline === undefined) {
154
+ return {
155
+ timeoutMs: requested,
156
+ workTimeoutReason: "work_timeout",
157
+ workTimeoutReportLimit: requested,
158
+ };
159
+ }
160
+ const remaining = Math.floor(orchestrationDeadline - Date.now());
161
+ if (remaining < 1) return undefined;
162
+ const orchestrationLimited = remaining < requested;
163
+ return {
164
+ timeoutMs: Math.min(requested, remaining),
165
+ workTimeoutReason: orchestrationLimited ? "orchestration_timeout" : "work_timeout",
166
+ workTimeoutReportLimit: orchestrationLimited
167
+ ? Math.floor(params.totalTimeoutMs as number)
168
+ : requested,
169
+ };
170
+ };
138
171
 
139
172
  const hasChain = (params.chain?.length ?? 0) > 0;
140
173
  const hasTasks = (params.tasks?.length ?? 0) > 0;
174
+ const hasWorkflow = (params.workflow?.tasks.length ?? 0) > 0;
175
+ const hasPanel = params.panel !== undefined;
141
176
  const hasSingle = Boolean(params.agent && params.task);
142
- const modeCount = Number(hasChain) + Number(hasTasks) + Number(hasSingle);
177
+ const modeCount =
178
+ Number(hasChain) +
179
+ Number(hasTasks) +
180
+ Number(hasWorkflow) +
181
+ Number(hasPanel) +
182
+ Number(hasSingle);
183
+ let workLedger: WorkItemLedger | undefined;
184
+ const workflowScheduling: ReturnType<AdaptiveScheduler["decide"]>[] = [];
185
+ const verificationTargetIds = new Set<string>();
143
186
 
144
187
  const makeDetails =
145
- (mode: "single" | "parallel" | "chain") =>
146
- (results: SingleResult[], aggregator?: SingleResult): SubagentDetails => ({
147
- mode,
148
- agentScope,
149
- projectAgentsDir: discovery.projectAgentsDir,
150
- results,
151
- aggregator,
152
- });
188
+ (mode: "single" | "parallel" | "chain" | "workflow" | "panel") =>
189
+ (results: SingleResult[], aggregator?: SingleResult): SubagentDetails => {
190
+ const workflow = workLedger?.snapshot();
191
+ const metricResults = aggregator ? [...results, aggregator] : results;
192
+ return {
193
+ mode,
194
+ agentScope,
195
+ projectAgentsDir: discovery.projectAgentsDir,
196
+ results,
197
+ aggregator,
198
+ workflow,
199
+ schedulerDecisions:
200
+ workflowScheduling.length > 0 ? workflowScheduling.slice(-64) : undefined,
201
+ metrics: calculateOrchestrationMetrics(workflow, metricResults),
202
+ };
203
+ };
204
+ const workflowFailureResult = (
205
+ agentName: string,
206
+ task: string,
207
+ reasonCode: string,
208
+ message: string,
209
+ thinkingLevel?: SubagentThinkingLevel,
210
+ ): SingleResult => ({
211
+ agent: agentName,
212
+ agentSource: agents.find((agent) => agent.name === agentName)?.source ?? "unknown",
213
+ task,
214
+ exitCode: 1,
215
+ messages: [],
216
+ stderr: message,
217
+ errorMessage: message,
218
+ usage: {
219
+ input: 0,
220
+ output: 0,
221
+ cacheRead: 0,
222
+ cacheWrite: 0,
223
+ cost: 0,
224
+ contextTokens: 0,
225
+ turns: 0,
226
+ },
227
+ thinkingLevel,
228
+ finalOutput: "",
229
+ outcome: {
230
+ status: "failed",
231
+ reasonCode,
232
+ recoveryActions: ["revalidate"],
233
+ retryable: false,
234
+ },
235
+ });
236
+ const exhaustedResult = (
237
+ agentName: string,
238
+ task: string,
239
+ thinkingLevel: SubagentThinkingLevel | undefined,
240
+ step?: number,
241
+ ): SingleResult => {
242
+ const limit = Math.floor(params.totalTimeoutMs as number);
243
+ const message = `Subagent orchestration deadline expired after ${limit}ms`;
244
+ return {
245
+ agent: agentName,
246
+ agentSource: agents.find((agent) => agent.name === agentName)?.source ?? "unknown",
247
+ task,
248
+ exitCode: 124,
249
+ messages: [],
250
+ stderr: message,
251
+ errorMessage: message,
252
+ usage: {
253
+ input: 0,
254
+ output: 0,
255
+ cacheRead: 0,
256
+ cacheWrite: 0,
257
+ cost: 0,
258
+ contextTokens: 0,
259
+ turns: 0,
260
+ },
261
+ thinkingLevel,
262
+ step,
263
+ finalOutput: "",
264
+ timedOut: true,
265
+ stopReason: "timeout",
266
+ termination: {
267
+ version: TURN_TERMINATION_VERSION,
268
+ reason: "orchestration_timeout",
269
+ limit,
270
+ checkpoint: new TimeoutProgressJournal().checkpoint(task),
271
+ finalization: { attempted: false, status: "skipped", durationMs: 0 },
272
+ },
273
+ };
274
+ };
153
275
 
154
276
  if (modeCount !== 1 || (aggregator && !hasTasks)) {
155
277
  const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
@@ -167,6 +289,115 @@ export async function executeSubagent(
167
289
  details: makeDetails("single")([]),
168
290
  };
169
291
  }
292
+ if (
293
+ (hasWorkflow || hasPanel) &&
294
+ (Number.parseInt(process.env.PI_SUBAGENT_DEPTH ?? "0", 10) || 0) > 0
295
+ ) {
296
+ throw new Error("Explicit workflow and panel recursion is disabled until separately evaluated");
297
+ }
298
+ if (params.panel) {
299
+ validatePanelRequest(params.panel, maxParallelTasks);
300
+ for (const agentName of [
301
+ ...params.panel.reviewers.map((reviewer) => reviewer.agent),
302
+ params.panel.synthesizer.agent,
303
+ ]) {
304
+ if (!agents.some((agent) => agent.name === agentName)) {
305
+ throw new Error(`Unknown panel agent: ${agentName}`);
306
+ }
307
+ }
308
+ }
309
+ if (
310
+ (hasTasks && (params.tasks?.length ?? 0) > maxParallelTasks) ||
311
+ (hasWorkflow && (params.workflow?.tasks.length ?? 0) > maxParallelTasks) ||
312
+ (hasPanel && (params.panel?.reviewers.length ?? 0) > maxParallelTasks)
313
+ ) {
314
+ const count = hasWorkflow
315
+ ? (params.workflow?.tasks.length ?? 0)
316
+ : hasPanel
317
+ ? (params.panel?.reviewers.length ?? 0)
318
+ : (params.tasks?.length ?? 0);
319
+ throw new Error(`Too many delegated tasks (${count}). Configured max is ${maxParallelTasks}.`);
320
+ }
321
+
322
+ const nonWorkflowRetryConfigured =
323
+ !hasWorkflow &&
324
+ Boolean(
325
+ params.retryPolicy ||
326
+ params.hedgeAfterMs ||
327
+ params.tasks?.some((task) => task.retryPolicy || task.hedgeAfterMs) ||
328
+ params.chain?.some((task) => task.retryPolicy || task.hedgeAfterMs) ||
329
+ aggregator?.retryPolicy ||
330
+ aggregator?.hedgeAfterMs,
331
+ );
332
+ if (nonWorkflowRetryConfigured) {
333
+ throw new Error("Retry and hedge policies are supported only by explicit workflow tasks");
334
+ }
335
+ if (hasWorkflow && params.workflow) {
336
+ for (const task of resolvedWorkflowTasks) {
337
+ if (task.verifierFor) verificationTargetIds.add(task.verifierFor);
338
+ const contract = normalizeDelegationContract(task.contract);
339
+ if (params.workflow.honorAdmission) {
340
+ const admission = contract?.admission;
341
+ const decision = evaluateDelegationAdmission({
342
+ contextPressure: admission?.contextPressure ?? "low",
343
+ independentWorkItems: admission?.independentWorkItems ?? 1,
344
+ coupling: admission?.coupling ?? "dense",
345
+ verificationRequired: admission?.verificationRequired ?? false,
346
+ verificationAvailable: admission?.verificationAvailable ?? false,
347
+ capabilitiesSupported: true,
348
+ budgetAllowsChildren: admission?.budgetAllowsChildren ?? false,
349
+ generationCurrent: true,
350
+ requirementsComplete: admission?.requirementsComplete ?? false,
351
+ });
352
+ if (
353
+ decision.recommendation === "parent-owned-direct" ||
354
+ decision.recommendation === "abstain-insufficient-evidence"
355
+ ) {
356
+ throw new Error(
357
+ `Admission declined workflow task ${task.id}: ${decision.reasonCodes.join(", ")}`,
358
+ );
359
+ }
360
+ }
361
+ if (
362
+ requiresIndependentVerification({
363
+ contract,
364
+ integrationOwner: task.integrationOwner === true,
365
+ requiredCapabilities: task.requiredCapabilities ?? [],
366
+ })
367
+ ) {
368
+ verificationTargetIds.add(task.id);
369
+ }
370
+ if (!task.retryPolicy && !task.hedgeAfterMs) continue;
371
+ const policy = contract?.sideEffectPolicy;
372
+ if (
373
+ (task.retryPolicy && policy !== "read-only" && policy !== "idempotent") ||
374
+ (task.hedgeAfterMs && policy !== "read-only")
375
+ ) {
376
+ throw new Error(
377
+ `Workflow task ${task.id} must declare an idempotent retry or read-only hedge delegation contract`,
378
+ );
379
+ }
380
+ }
381
+ validateWorkflowVerificationGraph(
382
+ resolvedWorkflowTasks.map((task) => ({
383
+ ...task,
384
+ resultFormat: task.resultFormat ?? params.resultFormat,
385
+ })),
386
+ verificationTargetIds,
387
+ );
388
+ }
389
+ workLedger = createBlockingWorkLedger(params, resolvedWorkflowTasks, aggregator);
390
+ let workflowPersistence: WorkItemPersistence | undefined;
391
+ if (hasWorkflow && workLedger) {
392
+ const owner =
393
+ ctx.sessionManager.getSessionId?.() ??
394
+ ctx.sessionManager.getSessionFile?.() ??
395
+ `ephemeral:${ctx.cwd}`;
396
+ workflowPersistence = createSessionWorkItemPersistence(owner, workLedger.workflowId);
397
+ }
398
+ const persistWorkLedger = async () => {
399
+ if (workflowPersistence && workLedger) await workflowPersistence.save(workLedger.snapshot());
400
+ };
170
401
 
171
402
  const delegationPolicy = config?.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY;
172
403
  const resolveTarget = (cwd: string | undefined): ResolvedSubagentTarget => {
@@ -179,22 +410,214 @@ export async function executeSubagent(
179
410
  return target;
180
411
  };
181
412
  const singleTarget = hasSingle ? resolveTarget(params.cwd) : undefined;
413
+ const panelTarget = hasPanel ? resolveTarget(undefined) : undefined;
182
414
  const chainTargets = params.chain?.map((step) => resolveTarget(step.cwd)) ?? [];
183
415
  const parallelTargets = params.tasks?.map((task) => resolveTarget(task.cwd)) ?? [];
416
+ const workflowTargets = resolvedWorkflowTasks.map((task) => resolveTarget(task.cwd));
184
417
  const aggregatorTarget = aggregator ? resolveTarget(aggregator.cwd) : undefined;
418
+ if (params.panel && panelTarget) {
419
+ await preflightPanelExecution({
420
+ panel: params.panel,
421
+ agents,
422
+ signal,
423
+ target: panelTarget,
424
+ resolveThinkingLevel,
425
+ resolveTimeoutMs,
426
+ });
427
+ }
185
428
  const attachTarget = (result: SingleResult, target: ResolvedSubagentTarget): SingleResult => {
186
429
  result.target = targetPolicyAudit(target);
187
430
  return result;
188
431
  };
189
- const launchPolicy = (target: ResolvedSubagentTarget) => ({
190
- projectTrust: target.trust.projectTrusted,
191
- });
432
+ type ContractRequest = {
433
+ contract?: unknown;
434
+ resultFormat?: SubagentResultFormat;
435
+ };
436
+ const prepareTask = (task: string, local?: ContractRequest) => {
437
+ const contracted = appendDelegationContract(task, local?.contract ?? params.contract);
438
+ const resultFormat = local?.resultFormat ?? params.resultFormat;
439
+ return {
440
+ text: appendResultInstruction(contracted.text, resultFormat, DEFAULT_MAX_CONTEXT_BYTES),
441
+ contract: contracted.contract,
442
+ resultFormat,
443
+ };
444
+ };
445
+ const launchPolicy = (
446
+ target: ResolvedSubagentTarget,
447
+ prepared: { contract?: DelegationContract; resultFormat?: SubagentResultFormat },
448
+ displayTask: string,
449
+ agentName: string,
450
+ thinkingLevel: SubagentThinkingLevel | undefined,
451
+ timeoutMs: number,
452
+ taskGeneration = 0,
453
+ budget?: NonNullable<ReturnType<typeof resolveExecutionBudget>>,
454
+ turnLimits?: TurnLimits,
455
+ ) => {
456
+ const agent = agents.find((candidate) => candidate.name === agentName);
457
+ const effectiveTools = agent ? resolveContractTools(agent.tools, prepared.contract) : undefined;
458
+ const executionPlan = agent
459
+ ? createExecutionPlan({
460
+ contract: prepared.contract,
461
+ agent,
462
+ effectiveTools,
463
+ target: targetPolicyAudit(target),
464
+ workspaceMode: "shared",
465
+ transport: "subprocess",
466
+ resultFormat: prepared.resultFormat ?? "text",
467
+ model: agent.model,
468
+ thinkingLevel,
469
+ timeoutMs,
470
+ taskGeneration,
471
+ })
472
+ : undefined;
473
+ if (executionPlan) {
474
+ const acknowledgement = acknowledgeExecutionPlan(executionPlan);
475
+ if (acknowledgement.status === "rejected") {
476
+ throw new Error(`Execution plan rejected: ${JSON.stringify(acknowledgement)}`);
477
+ }
478
+ }
479
+ const capabilityGrant = executionPlan
480
+ ? issueCapabilityGrant(executionPlan, Date.now(), Math.max(1, timeoutMs + 60_000))
481
+ : undefined;
482
+ return {
483
+ projectTrust: target.trust.projectTrusted,
484
+ turnLimits,
485
+ workTimeoutReason: budget?.workTimeoutReason,
486
+ workTimeoutReportLimit: budget?.workTimeoutReportLimit,
487
+ orchestrationDeadlineAt: budget ? orchestrationDeadline : undefined,
488
+ tools: effectiveTools,
489
+ contract: prepared.contract,
490
+ resultFormat: prepared.resultFormat,
491
+ displayTask,
492
+ executionPlan,
493
+ capabilityGrant,
494
+ };
495
+ };
496
+
497
+ // Build and acknowledge every contracted plan before confirmation or child launch.
498
+ if (hasSingle && singleTarget && params.agent && params.task) {
499
+ const prepared = prepareTask(params.task, params);
500
+ launchPolicy(
501
+ singleTarget,
502
+ prepared,
503
+ params.task,
504
+ params.agent,
505
+ resolveThinkingLevel(params.agent, params.thinkingLevel),
506
+ resolveTimeoutMs(params.agent, params.timeoutMs),
507
+ );
508
+ }
509
+ for (const [index, step] of (params.chain ?? []).entries()) {
510
+ const prepared = prepareTask(step.task, step);
511
+ launchPolicy(
512
+ chainTargets[index],
513
+ prepared,
514
+ step.task,
515
+ step.agent,
516
+ resolveThinkingLevel(step.agent, step.thinkingLevel),
517
+ resolveTimeoutMs(step.agent, step.timeoutMs),
518
+ );
519
+ }
520
+ for (const [index, task] of (params.tasks ?? []).entries()) {
521
+ const prepared = prepareTask(task.task, task);
522
+ launchPolicy(
523
+ parallelTargets[index],
524
+ prepared,
525
+ task.task,
526
+ task.agent,
527
+ resolveThinkingLevel(task.agent, task.thinkingLevel),
528
+ resolveTimeoutMs(task.agent, task.timeoutMs),
529
+ );
530
+ }
531
+ for (const [index, task] of resolvedWorkflowTasks.entries()) {
532
+ const prepared = prepareTask(task.task, task);
533
+ launchPolicy(
534
+ workflowTargets[index],
535
+ prepared,
536
+ task.task,
537
+ task.agent,
538
+ resolveThinkingLevel(task.agent, task.thinkingLevel),
539
+ resolveTimeoutMs(task.agent, task.timeoutMs),
540
+ workLedger?.get(task.id)?.taskGeneration ?? 0,
541
+ );
542
+ }
543
+ if (aggregator && aggregatorTarget) {
544
+ const prepared = prepareTask(aggregator.task, aggregator);
545
+ launchPolicy(
546
+ aggregatorTarget,
547
+ prepared,
548
+ aggregator.task,
549
+ aggregator.agent,
550
+ resolveThinkingLevel(aggregator.agent, aggregator.thinkingLevel),
551
+ resolveTimeoutMs(aggregator.agent, aggregator.timeoutMs),
552
+ );
553
+ }
554
+
555
+ const artifactsFromResult = (result: SingleResult) => {
556
+ const structured =
557
+ result.structuredResult?.version === "pi-subagents:result:v2"
558
+ ? result.structuredResult
559
+ : undefined;
560
+ return (structured?.artifacts ?? []).map((artifact) => ({
561
+ id: artifact.id,
562
+ kind: artifact.kind,
563
+ version: artifact.version ?? artifact.digest ?? "unversioned",
564
+ digest: artifact.digest,
565
+ verified: false,
566
+ }));
567
+ };
568
+ const startWorkItem = (id: string, agentName: string) => {
569
+ if (workLedger?.get(id)?.state === "ready") {
570
+ return workLedger.start(id, `agent:${agentName}`);
571
+ }
572
+ return workLedger?.get(id);
573
+ };
574
+ const settleWorkItem = (id: string, result: SingleResult, taskGeneration: number) => {
575
+ if (!workLedger) return;
576
+ if (workLedger.get(id)?.taskGeneration !== taskGeneration) {
577
+ result.outcome = {
578
+ status: "stale",
579
+ reasonCode: "stale-task-generation",
580
+ recoveryActions: ["discard", "replan"],
581
+ retryable: false,
582
+ };
583
+ }
584
+ if (result.outcome?.status === "stale") {
585
+ workLedger.invalidate(id, result.outcome.reasonCode ?? "stale-result");
586
+ return;
587
+ }
588
+ if (isResultError(result)) {
589
+ const state =
590
+ result.outcome?.status === "blocked"
591
+ ? "blocked"
592
+ : result.outcome?.status === "needs-input"
593
+ ? "needs-input"
594
+ : result.aborted || result.outcome?.status === "interrupted"
595
+ ? "interrupted"
596
+ : "failed";
597
+ workLedger.settle(
598
+ id,
599
+ state,
600
+ result.outcome?.reasonCode ?? result.errorMessage ?? result.stopReason,
601
+ );
602
+ return;
603
+ }
604
+ workLedger.complete(id, {
605
+ taskGeneration,
606
+ executionPlanId: result.executionPlan?.id,
607
+ artifacts: artifactsFromResult(result),
608
+ });
609
+ };
192
610
 
193
611
  if (agentScope === "project" || agentScope === "both") {
194
612
  const requestedAgentNames = new Set<string>();
195
613
  if (params.chain) for (const step of params.chain) requestedAgentNames.add(step.agent);
196
614
  if (params.tasks) for (const t of params.tasks) requestedAgentNames.add(t.agent);
615
+ for (const task of resolvedWorkflowTasks) requestedAgentNames.add(task.agent);
197
616
  if (aggregator) requestedAgentNames.add(aggregator.agent);
617
+ if (params.panel) {
618
+ for (const reviewer of params.panel.reviewers) requestedAgentNames.add(reviewer.agent);
619
+ requestedAgentNames.add(params.panel.synthesizer.agent);
620
+ }
198
621
  if (params.agent) requestedAgentNames.add(params.agent);
199
622
 
200
623
  const projectAgentsRequested = Array.from(requestedAgentNames)
@@ -222,13 +645,469 @@ export async function executeSubagent(
222
645
  if (!ok) {
223
646
  return {
224
647
  content: [{ type: "text", text: "Canceled: project-local agents not approved." }],
225
- details: makeDetails(hasChain ? "chain" : hasTasks ? "parallel" : "single")([]),
648
+ details: makeDetails(
649
+ hasChain
650
+ ? "chain"
651
+ : hasTasks
652
+ ? "parallel"
653
+ : hasWorkflow
654
+ ? "workflow"
655
+ : hasPanel
656
+ ? "panel"
657
+ : "single",
658
+ )([]),
226
659
  };
227
660
  }
228
661
  }
229
662
  }
230
663
  }
231
664
 
665
+ orchestrationDeadline =
666
+ params.totalTimeoutMs === undefined
667
+ ? undefined
668
+ : Date.now() + Math.floor(params.totalTimeoutMs);
669
+ if (params.panel && panelTarget) {
670
+ return executePanel({
671
+ toolCallId,
672
+ params,
673
+ panel: params.panel,
674
+ signal,
675
+ onUpdate,
676
+ ctx,
677
+ agents,
678
+ agentScope,
679
+ projectAgentsDir: discovery.projectAgentsDir,
680
+ maxParallelTasks,
681
+ target: panelTarget,
682
+ resolveThinkingLevel,
683
+ resolveTimeoutMs,
684
+ });
685
+ }
686
+ if (params.workflow && resolvedWorkflowTasks.length > 0 && workLedger) {
687
+ await persistWorkLedger();
688
+ const status = startSubagentStatus(
689
+ ctx,
690
+ toolCallId,
691
+ parallelStatus(0, resolvedWorkflowTasks.length, 0),
692
+ );
693
+ const scheduler = new AdaptiveScheduler();
694
+ const taskById = new Map(
695
+ resolvedWorkflowTasks.map((task, index) => [task.id, { task, index }]),
696
+ );
697
+ const resultsById = new Map<string, SingleResult>();
698
+ const deadline = orchestrationDeadline;
699
+ const cancelWorkflowGeneration = () => {
700
+ for (const item of workLedger.snapshot().items) {
701
+ if (item.state === "running" || item.state === "awaiting-verification") {
702
+ workLedger.invalidate(item.id, "parent-aborted");
703
+ }
704
+ }
705
+ };
706
+ signal?.addEventListener("abort", cancelWorkflowGeneration, { once: true });
707
+ try {
708
+ while (true) {
709
+ const snapshot = workLedger.snapshot();
710
+ const remainingBudgetMs =
711
+ deadline === undefined
712
+ ? MAX_SUBAGENT_TIMEOUT_MS
713
+ : Math.max(0, Math.floor(deadline - Date.now()));
714
+ const decision = scheduler.decide(snapshot, {
715
+ maxConcurrency: Math.min(MAX_BLOCKING_PARALLEL_CONCURRENCY, maxParallelTasks),
716
+ activeCount: 0,
717
+ transportCapacity: MAX_BLOCKING_PARALLEL_CONCURRENCY,
718
+ remainingBudgetMs,
719
+ });
720
+ workflowScheduling.push(decision);
721
+ if (decision.selected.length === 0) break;
722
+ status.update(
723
+ parallelStatus(resultsById.size, resolvedWorkflowTasks.length, decision.selected.length),
724
+ );
725
+ const batch = await mapWithConcurrencyLimit(
726
+ decision.selected,
727
+ decision.effectiveConcurrency,
728
+ async (workItemId) => {
729
+ const entry = taskById.get(workItemId);
730
+ if (!entry) throw new Error(`Missing workflow task ${workItemId}`);
731
+ const { task, index } = entry;
732
+ const dependencies = (task.dependsOn ?? [])
733
+ .map((dependency) => resultsById.get(dependency))
734
+ .filter((result): result is SingleResult => result !== undefined);
735
+ const verifierDependency = task.verifierFor
736
+ ? resultsById.get(task.verifierFor)
737
+ : undefined;
738
+ const verifierStructuredResult =
739
+ verifierDependency?.structuredResult?.version === "pi-subagents:result:v2"
740
+ ? verifierDependency.structuredResult
741
+ : undefined;
742
+ const dependencyContext = task.verifierFor
743
+ ? verifierStructuredResult
744
+ ? `\n\nStaged target result:\n${redactPrivateText(
745
+ JSON.stringify(verifierStructuredResult),
746
+ )}`
747
+ : ""
748
+ : dependencies.length
749
+ ? `\n\nDependency results:\n${buildFanInContext(dependencies)}`
750
+ : "";
751
+ const displayTask = task.task;
752
+ const target = workflowTargets[index];
753
+ const thinkingLevel = resolveThinkingLevel(task.agent, task.thinkingLevel);
754
+ let verifierTreeIdentity:
755
+ | Awaited<ReturnType<typeof captureWorkflowTreeIdentity>>
756
+ | undefined;
757
+ let verifierPreflightError: string | undefined;
758
+ let verifierPreflightCode: string | undefined;
759
+ if (task.verifierFor) {
760
+ const staged = workLedger.get(task.verifierFor);
761
+ if (!staged?.stagedTreeIdentity) {
762
+ verifierPreflightCode = "verification-tree-unavailable";
763
+ verifierPreflightError = `Verification target ${task.verifierFor} has no staged tree identity`;
764
+ } else {
765
+ try {
766
+ verifierTreeIdentity = await captureWorkflowTreeIdentity(target.cwd, { signal });
767
+ if (!sameWorkflowTreeIdentity(staged.stagedTreeIdentity, verifierTreeIdentity)) {
768
+ verifierPreflightCode = "verification-tree-mismatch";
769
+ verifierPreflightError =
770
+ "Workflow verification tree changed before verifier launch";
771
+ }
772
+ } catch (error) {
773
+ if (signal?.aborted) throw error;
774
+ verifierPreflightCode = "verification-tree-unavailable";
775
+ verifierPreflightError = error instanceof Error ? error.message : String(error);
776
+ }
777
+ }
778
+ }
779
+ const verificationTargetTask = task.verifierFor
780
+ ? taskById.get(task.verifierFor)?.task
781
+ : undefined;
782
+ const verificationTargetContract = normalizeDelegationContract(
783
+ verificationTargetTask?.contract,
784
+ );
785
+ const verificationSuffix =
786
+ task.verifierFor && verifierTreeIdentity
787
+ ? `\n\n${workflowVerificationInstruction(task.verifierFor, verifierTreeIdentity, {
788
+ acceptanceCriteria: [
789
+ ...(verificationTargetTask?.acceptanceCriteria ?? []),
790
+ ...(verificationTargetContract?.acceptanceCriteria ?? []),
791
+ ],
792
+ requiredEvidence: verificationTargetContract?.requiredEvidence ?? [],
793
+ })}`
794
+ : "";
795
+ const baseTask = `${task.task}${dependencyContext}`;
796
+ const baseBudget = Math.max(
797
+ 0,
798
+ DEFAULT_MAX_CONTEXT_BYTES - Buffer.byteLength(verificationSuffix, "utf8"),
799
+ );
800
+ const taskWithContext = `${truncateUtf8(baseTask, baseBudget).text}${verificationSuffix}`;
801
+ const prepared = prepareTask(taskWithContext, task);
802
+ const startedItem = startWorkItem(workItemId, task.agent);
803
+ const acceptedTaskGeneration = startedItem?.taskGeneration ?? 0;
804
+ await persistWorkLedger();
805
+ const runAttempt = (attemptSignal: AbortSignal | undefined) => {
806
+ const budget = resolveExecutionBudget(task.agent, task.timeoutMs);
807
+ if (!budget) {
808
+ return Promise.resolve(exhaustedResult(task.agent, displayTask, thinkingLevel));
809
+ }
810
+ return runSingleAgent(
811
+ ctx.cwd,
812
+ agents,
813
+ task.agent,
814
+ prepared.text,
815
+ target.cwd,
816
+ undefined,
817
+ attemptSignal,
818
+ thinkingLevel,
819
+ budget.timeoutMs,
820
+ undefined,
821
+ makeDetails("workflow"),
822
+ undefined,
823
+ launchPolicy(
824
+ target,
825
+ prepared,
826
+ displayTask,
827
+ task.agent,
828
+ thinkingLevel,
829
+ budget.timeoutMs,
830
+ acceptedTaskGeneration,
831
+ budget,
832
+ resolveTurnLimits(task),
833
+ ),
834
+ );
835
+ };
836
+ const maxAttempts = task.retryPolicy?.maxAttempts ?? 1;
837
+ let result: SingleResult | undefined = verifierPreflightError
838
+ ? attachTarget(
839
+ workflowFailureResult(
840
+ task.agent,
841
+ displayTask,
842
+ verifierPreflightCode ?? "verification-tree-unavailable",
843
+ verifierPreflightError,
844
+ thinkingLevel,
845
+ ),
846
+ target,
847
+ )
848
+ : undefined;
849
+ let hedged = false;
850
+ for (let attempt = 1; !verifierPreflightError && attempt <= maxAttempts; attempt++) {
851
+ if (attempt > 1 && deadline !== undefined && Date.now() >= deadline) break;
852
+ const attempted = await runHedgedAttempt(runAttempt, signal, task.hedgeAfterMs);
853
+ hedged ||= attempted.hedged;
854
+ result = attachTarget(
855
+ {
856
+ ...attempted.result,
857
+ attemptCount: attempt,
858
+ hedged: hedged || undefined,
859
+ },
860
+ target,
861
+ );
862
+ if (!isRetryableResult(result) || attempt >= maxAttempts) break;
863
+ if (deadline !== undefined && Date.now() >= deadline) break;
864
+ await supervisionDelay(task.retryPolicy?.backoffMs ?? 0, signal);
865
+ }
866
+ if (!result) throw new Error(`Workflow task ${workItemId} produced no result`);
867
+ if (workLedger.get(workItemId)?.taskGeneration !== acceptedTaskGeneration) {
868
+ result.outcome = {
869
+ status: "stale",
870
+ reasonCode: "cancelled-generation",
871
+ recoveryActions: ["discard", "replan"],
872
+ retryable: false,
873
+ };
874
+ }
875
+ resultsById.set(workItemId, result);
876
+ if (result.outcome?.status === "stale") {
877
+ // Cancellation or replacement already rotated and invalidated this generation.
878
+ } else if (task.verifierFor) {
879
+ const staged = workLedger.get(task.verifierFor);
880
+ const structured =
881
+ result.structuredResult?.version === "pi-subagents:result:v2"
882
+ ? result.structuredResult
883
+ : undefined;
884
+ let failureReason: string | undefined = verifierPreflightError;
885
+ let failureCode: string | undefined = verifierPreflightCode;
886
+ let postVerifierIdentity = verifierTreeIdentity;
887
+ if (!failureReason) {
888
+ try {
889
+ postVerifierIdentity = await captureWorkflowTreeIdentity(target.cwd, { signal });
890
+ if (
891
+ !verifierTreeIdentity ||
892
+ !staged?.stagedTreeIdentity ||
893
+ !sameWorkflowTreeIdentity(verifierTreeIdentity, postVerifierIdentity) ||
894
+ !sameWorkflowTreeIdentity(staged.stagedTreeIdentity, postVerifierIdentity)
895
+ ) {
896
+ failureCode = "verification-tree-mismatch";
897
+ failureReason = "Workflow verification tree changed during verifier execution";
898
+ }
899
+ } catch (error) {
900
+ if (signal?.aborted) throw error;
901
+ failureCode = "verification-tree-unavailable";
902
+ failureReason = error instanceof Error ? error.message : String(error);
903
+ }
904
+ }
905
+ if (
906
+ !failureReason &&
907
+ structured &&
908
+ staged?.acceptedExecutionPlanId &&
909
+ result.executionPlan?.id &&
910
+ postVerifierIdentity
911
+ ) {
912
+ try {
913
+ const receipt = createWorkflowVerificationReceipt(structured, {
914
+ targetTaskId: staged.id,
915
+ targetTaskGeneration: staged.taskGeneration,
916
+ targetExecutionPlanId: staged.acceptedExecutionPlanId,
917
+ verifierTaskId: workItemId,
918
+ verifierTaskGeneration: acceptedTaskGeneration,
919
+ verifierExecutionPlanId: result.executionPlan.id,
920
+ treeIdentity: postVerifierIdentity,
921
+ sourceTruncated: result.truncated === true,
922
+ });
923
+ workLedger.completeVerification(workItemId, {
924
+ taskGeneration: acceptedTaskGeneration,
925
+ executionPlanId: result.executionPlan.id,
926
+ receipt,
927
+ });
928
+ if (receipt.decision !== "accept") {
929
+ const targetResult = resultsById.get(staged.id);
930
+ if (targetResult) {
931
+ targetResult.outcome = {
932
+ status: receipt.decision === "rework" ? "blocked" : "failed",
933
+ reasonCode:
934
+ receipt.decision === "rework"
935
+ ? "verification-rework"
936
+ : "verification-rejected",
937
+ recoveryActions:
938
+ receipt.decision === "rework" ? ["replan", "verify"] : ["stop"],
939
+ retryable: false,
940
+ };
941
+ }
942
+ }
943
+ } catch (error) {
944
+ failureCode = "verification-receipt-invalid";
945
+ failureReason = error instanceof Error ? error.message : String(error);
946
+ }
947
+ } else if (!failureReason) {
948
+ failureCode = "verification-receipt-invalid";
949
+ failureReason = "Workflow verifier did not return a current structured-v2 result";
950
+ }
951
+ if (failureReason) {
952
+ const reasonCode = failureCode ?? "verification-receipt-invalid";
953
+ result.outcome = {
954
+ status:
955
+ reasonCode === "verification-receipt-invalid" ? "contract-invalid" : "failed",
956
+ reasonCode,
957
+ recoveryActions:
958
+ reasonCode === "verification-receipt-invalid"
959
+ ? ["repair-contract"]
960
+ : ["revalidate"],
961
+ retryable: false,
962
+ };
963
+ result.errorMessage = failureReason;
964
+ workLedger.failVerification(workItemId, reasonCode);
965
+ }
966
+ } else if (verificationTargetIds.has(workItemId) && !isResultError(result)) {
967
+ try {
968
+ const treeIdentity = await captureWorkflowTreeIdentity(target.cwd, { signal });
969
+ if (!result.executionPlan?.id) {
970
+ throw new Error("Verification-required producer has no accepted ExecutionPlan");
971
+ }
972
+ workLedger.stageForVerification(workItemId, {
973
+ taskGeneration: acceptedTaskGeneration,
974
+ executionPlanId: result.executionPlan.id,
975
+ artifacts: artifactsFromResult(result),
976
+ treeIdentity,
977
+ });
978
+ } catch (error) {
979
+ if (signal?.aborted) throw error;
980
+ const message = error instanceof Error ? error.message : String(error);
981
+ result.outcome = {
982
+ status: "failed",
983
+ reasonCode: "verification-tree-unavailable",
984
+ recoveryActions: ["revalidate"],
985
+ retryable: false,
986
+ };
987
+ result.errorMessage = message;
988
+ settleWorkItem(workItemId, result, acceptedTaskGeneration);
989
+ }
990
+ } else {
991
+ settleWorkItem(workItemId, result, acceptedTaskGeneration);
992
+ }
993
+ await persistWorkLedger();
994
+ return result;
995
+ },
996
+ signal,
997
+ );
998
+ if (batch.length === 0 || signal?.aborted) break;
999
+ }
1000
+ for (const item of workLedger.snapshot().items) {
1001
+ if (
1002
+ item.state !== "pending" &&
1003
+ item.state !== "ready" &&
1004
+ item.state !== "awaiting-verification"
1005
+ ) {
1006
+ continue;
1007
+ }
1008
+ if (signal?.aborted) {
1009
+ workLedger.settle(item.id, "interrupted", "parent-aborted");
1010
+ } else if (deadline !== undefined && Date.now() >= deadline) {
1011
+ workLedger.settle(item.id, "blocked", "budget-exhausted");
1012
+ } else if (item.state === "awaiting-verification") {
1013
+ workLedger.settle(item.id, "blocked", "verification-not-completed");
1014
+ } else {
1015
+ const dependencyBlocked = item.dependencies.some(
1016
+ (dependency) => workLedger.get(dependency)?.state !== "completed",
1017
+ );
1018
+ workLedger.settle(
1019
+ item.id,
1020
+ dependencyBlocked ? "blocked" : "needs-input",
1021
+ dependencyBlocked ? "dependency-not-completed" : "artifact-version-mismatch",
1022
+ );
1023
+ }
1024
+ }
1025
+ await persistWorkLedger();
1026
+ const results = resolvedWorkflowTasks.map((task) => {
1027
+ const completed = resultsById.get(task.id);
1028
+ const item = workLedger.get(task.id);
1029
+ if (completed) {
1030
+ if (item && item.state !== "completed" && !isResultError(completed)) {
1031
+ completed.outcome = {
1032
+ status:
1033
+ item.state === "needs-input"
1034
+ ? "needs-input"
1035
+ : item.state === "interrupted"
1036
+ ? "interrupted"
1037
+ : item.state === "failed"
1038
+ ? "failed"
1039
+ : "blocked",
1040
+ reasonCode: item.outcomeReason ?? "verification-not-accepted",
1041
+ recoveryActions:
1042
+ item.outcomeReason === "verification-rework" ? ["replan", "verify"] : ["stop"],
1043
+ retryable: false,
1044
+ };
1045
+ }
1046
+ return completed;
1047
+ }
1048
+ const outcomeStatus =
1049
+ item?.state === "interrupted"
1050
+ ? "interrupted"
1051
+ : item?.state === "needs-input"
1052
+ ? "needs-input"
1053
+ : "blocked";
1054
+ const reasonCode = item?.outcomeReason ?? "dependency-not-satisfied";
1055
+ return {
1056
+ agent: task.agent,
1057
+ agentSource: agents.find((agent) => agent.name === task.agent)?.source ?? "unknown",
1058
+ task: task.task,
1059
+ exitCode: 1,
1060
+ messages: [],
1061
+ stderr: "Workflow dependency was not satisfied",
1062
+ errorMessage: `Workflow task did not start: ${reasonCode}`,
1063
+ aborted: outcomeStatus === "interrupted",
1064
+ outcome: {
1065
+ status: outcomeStatus,
1066
+ reasonCode,
1067
+ recoveryActions:
1068
+ outcomeStatus === "needs-input"
1069
+ ? ["supply-input"]
1070
+ : outcomeStatus === "interrupted"
1071
+ ? ["retry"]
1072
+ : ["resolve-dependency"],
1073
+ retryable: outcomeStatus === "interrupted",
1074
+ },
1075
+ usage: {
1076
+ input: 0,
1077
+ output: 0,
1078
+ cacheRead: 0,
1079
+ cacheWrite: 0,
1080
+ cost: 0,
1081
+ contextTokens: 0,
1082
+ turns: 0,
1083
+ },
1084
+ finalOutput: "",
1085
+ } satisfies SingleResult;
1086
+ });
1087
+ const successCount = results.filter((result) => !isResultError(result)).length;
1088
+ const attemptCount = results.reduce((sum, result) => sum + (result.attemptCount ?? 1), 0);
1089
+ const hedgeCount = results.filter((result) => result.hedged).length;
1090
+ const isError = successCount !== results.length;
1091
+ return {
1092
+ content: [
1093
+ {
1094
+ type: "text",
1095
+ text: `Workflow: ${successCount}/${results.length} succeeded; ${attemptCount} attempt(s), ${hedgeCount} hedged task(s).`,
1096
+ },
1097
+ ],
1098
+ details: { ...makeDetails("workflow")(results), isError },
1099
+ isError: isError || undefined,
1100
+ };
1101
+ } finally {
1102
+ signal?.removeEventListener("abort", cancelWorkflowGeneration);
1103
+ try {
1104
+ await persistWorkLedger();
1105
+ } finally {
1106
+ status.clear();
1107
+ }
1108
+ }
1109
+ }
1110
+
232
1111
  if (params.chain && params.chain.length > 0) {
233
1112
  const results: SingleResult[] = [];
234
1113
  let previousOutput = "";
@@ -242,6 +1121,7 @@ export async function executeSubagent(
242
1121
  step.task.replace(/\{previous\}/g, previousOutput),
243
1122
  DEFAULT_MAX_CONTEXT_BYTES,
244
1123
  ).text;
1124
+ const prepared = prepareTask(taskWithContext, step);
245
1125
 
246
1126
  // Create update callback that includes all previous results
247
1127
  const chainUpdate: OnUpdateCallback | undefined = onUpdate
@@ -259,25 +1139,41 @@ export async function executeSubagent(
259
1139
  : undefined;
260
1140
 
261
1141
  const target = chainTargets[i];
1142
+ const thinkingLevel = resolveThinkingLevel(step.agent, step.thinkingLevel);
1143
+ const budget = resolveExecutionBudget(step.agent, step.timeoutMs);
1144
+ const taskGeneration = startWorkItem(`step-${i + 1}`, step.agent)?.taskGeneration ?? 0;
262
1145
  const result = attachTarget(
263
- await runSingleAgent(
264
- ctx.cwd,
265
- agents,
266
- step.agent,
267
- taskWithContext,
268
- target.cwd,
269
- i + 1,
270
- signal,
271
- resolveThinkingLevel(step.agent, step.thinkingLevel),
272
- resolveTimeoutMs(step.agent, step.timeoutMs),
273
- chainUpdate,
274
- makeDetails("chain"),
275
- undefined,
276
- launchPolicy(target),
277
- ),
1146
+ budget
1147
+ ? await runSingleAgent(
1148
+ ctx.cwd,
1149
+ agents,
1150
+ step.agent,
1151
+ prepared.text,
1152
+ target.cwd,
1153
+ i + 1,
1154
+ signal,
1155
+ thinkingLevel,
1156
+ budget.timeoutMs,
1157
+ chainUpdate,
1158
+ makeDetails("chain"),
1159
+ undefined,
1160
+ launchPolicy(
1161
+ target,
1162
+ prepared,
1163
+ taskWithContext,
1164
+ step.agent,
1165
+ thinkingLevel,
1166
+ budget.timeoutMs,
1167
+ taskGeneration,
1168
+ budget,
1169
+ resolveTurnLimits(step),
1170
+ ),
1171
+ )
1172
+ : exhaustedResult(step.agent, taskWithContext, thinkingLevel, i + 1),
278
1173
  target,
279
1174
  );
280
1175
  results.push(result);
1176
+ settleWorkItem(`step-${i + 1}`, result, taskGeneration);
281
1177
 
282
1178
  const isError = isResultError(result);
283
1179
  if (isError) {
@@ -290,7 +1186,9 @@ export async function executeSubagent(
290
1186
  isError: true,
291
1187
  };
292
1188
  }
293
- previousOutput = getResultFinalOutput(result);
1189
+ previousOutput = result.structuredResult
1190
+ ? JSON.stringify(result.structuredResult)
1191
+ : getResultFinalOutput(result);
294
1192
  }
295
1193
  return {
296
1194
  content: [
@@ -307,17 +1205,6 @@ export async function executeSubagent(
307
1205
  }
308
1206
 
309
1207
  if (params.tasks && params.tasks.length > 0) {
310
- if (params.tasks.length > MAX_PARALLEL_TASKS)
311
- return {
312
- content: [
313
- {
314
- type: "text",
315
- text: `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
316
- },
317
- ],
318
- details: makeDetails("parallel")([]),
319
- };
320
-
321
1208
  const status = startSubagentStatus(
322
1209
  ctx,
323
1210
  toolCallId,
@@ -395,34 +1282,50 @@ export async function executeSubagent(
395
1282
 
396
1283
  const results = await mapWithConcurrencyLimit(
397
1284
  params.tasks,
398
- MAX_CONCURRENCY,
1285
+ MAX_BLOCKING_PARALLEL_CONCURRENCY,
399
1286
  async (t, index) => {
400
1287
  const target = parallelTargets[index];
1288
+ const prepared = prepareTask(t.task, t);
1289
+ const thinkingLevel = resolveThinkingLevel(t.agent, t.thinkingLevel);
1290
+ const budget = resolveExecutionBudget(t.agent, t.timeoutMs);
1291
+ const taskGeneration = startWorkItem(`task-${index + 1}`, t.agent)?.taskGeneration ?? 0;
401
1292
  const result = attachTarget(
402
- await runSingleAgent(
403
- ctx.cwd,
404
- agents,
405
- t.agent,
406
- t.task,
407
- target.cwd,
408
- undefined,
409
- signal,
410
- resolveThinkingLevel(t.agent, t.thinkingLevel),
411
- resolveTimeoutMs(t.agent, t.timeoutMs),
412
- // Per-task update callback
413
- (partial) => {
414
- if (partial.details?.results[0]) {
415
- allResults[index] = { ...partial.details.results[0], exitCode: -1 };
416
- emitParallelUpdate();
417
- }
418
- },
419
- makeDetails("parallel"),
420
- undefined,
421
- launchPolicy(target),
422
- ),
1293
+ budget
1294
+ ? await runSingleAgent(
1295
+ ctx.cwd,
1296
+ agents,
1297
+ t.agent,
1298
+ prepared.text,
1299
+ target.cwd,
1300
+ undefined,
1301
+ signal,
1302
+ thinkingLevel,
1303
+ budget.timeoutMs,
1304
+ (partial) => {
1305
+ if (partial.details?.results[0]) {
1306
+ allResults[index] = { ...partial.details.results[0], exitCode: -1 };
1307
+ emitParallelUpdate();
1308
+ }
1309
+ },
1310
+ makeDetails("parallel"),
1311
+ undefined,
1312
+ launchPolicy(
1313
+ target,
1314
+ prepared,
1315
+ t.task,
1316
+ t.agent,
1317
+ thinkingLevel,
1318
+ budget.timeoutMs,
1319
+ taskGeneration,
1320
+ budget,
1321
+ resolveTurnLimits(t),
1322
+ ),
1323
+ )
1324
+ : exhaustedResult(t.agent, t.task, thinkingLevel),
423
1325
  target,
424
1326
  );
425
1327
  allResults[index] = result;
1328
+ settleWorkItem(`task-${index + 1}`, result, taskGeneration);
426
1329
  doneCount += 1;
427
1330
  runningCount -= 1;
428
1331
  emitParallelUpdate();
@@ -430,6 +1333,8 @@ export async function executeSubagent(
430
1333
  },
431
1334
  signal,
432
1335
  (task, index) => {
1336
+ const taskGeneration =
1337
+ startWorkItem(`task-${index + 1}`, task.agent)?.taskGeneration ?? 0;
433
1338
  const skipped: SingleResult = {
434
1339
  ...allResults[index],
435
1340
  task: task.task,
@@ -439,6 +1344,7 @@ export async function executeSubagent(
439
1344
  errorMessage: "Subagent was not started because the parent call was aborted",
440
1345
  };
441
1346
  allResults[index] = skipped;
1347
+ settleWorkItem(`task-${index + 1}`, skipped, taskGeneration);
442
1348
  doneCount += 1;
443
1349
  runningCount -= 1;
444
1350
  emitParallelUpdate();
@@ -456,33 +1362,50 @@ export async function executeSubagent(
456
1362
  : `${aggregator.task}\n\nParallel task outputs:\n\n${fanInContext}`,
457
1363
  DEFAULT_MAX_CONTEXT_BYTES,
458
1364
  ).text;
1365
+ const prepared = prepareTask(aggregatorTask, aggregator);
459
1366
  const target = aggregatorTarget as ResolvedSubagentTarget;
1367
+ const thinkingLevel = resolveThinkingLevel(aggregator.agent, aggregator.thinkingLevel);
1368
+ const budget = resolveExecutionBudget(aggregator.agent, aggregator.timeoutMs);
1369
+ const taskGeneration = startWorkItem("aggregator", aggregator.agent)?.taskGeneration ?? 0;
460
1370
  aggregatorResult = attachTarget(
461
- await runSingleAgent(
462
- ctx.cwd,
463
- agents,
464
- aggregator.agent,
465
- aggregatorTask,
466
- target.cwd,
467
- undefined,
468
- signal,
469
- resolveThinkingLevel(aggregator.agent, aggregator.thinkingLevel),
470
- resolveTimeoutMs(aggregator.agent, aggregator.timeoutMs),
471
- (partial) => {
472
- status.update(fanInStatus(aggregator.agent));
473
- if (onUpdate && partial.details?.results[0]) {
474
- onUpdate({
475
- content: partial.content,
476
- details: makeDetails("parallel")(results, partial.details.results[0]),
477
- });
478
- }
479
- },
480
- makeDetails("parallel"),
481
- undefined,
482
- launchPolicy(target),
483
- ),
1371
+ budget
1372
+ ? await runSingleAgent(
1373
+ ctx.cwd,
1374
+ agents,
1375
+ aggregator.agent,
1376
+ prepared.text,
1377
+ target.cwd,
1378
+ undefined,
1379
+ signal,
1380
+ thinkingLevel,
1381
+ budget.timeoutMs,
1382
+ (partial) => {
1383
+ status.update(fanInStatus(aggregator.agent));
1384
+ if (onUpdate && partial.details?.results[0]) {
1385
+ onUpdate({
1386
+ content: partial.content,
1387
+ details: makeDetails("parallel")(results, partial.details.results[0]),
1388
+ });
1389
+ }
1390
+ },
1391
+ makeDetails("parallel"),
1392
+ undefined,
1393
+ launchPolicy(
1394
+ target,
1395
+ prepared,
1396
+ aggregatorTask,
1397
+ aggregator.agent,
1398
+ thinkingLevel,
1399
+ budget.timeoutMs,
1400
+ taskGeneration,
1401
+ budget,
1402
+ resolveTurnLimits(aggregator),
1403
+ ),
1404
+ )
1405
+ : exhaustedResult(aggregator.agent, aggregatorTask, thinkingLevel),
484
1406
  target,
485
1407
  );
1408
+ settleWorkItem("aggregator", aggregatorResult, taskGeneration);
486
1409
  }
487
1410
 
488
1411
  const successCount = results.filter((result) => !isResultError(result)).length;
@@ -527,24 +1450,41 @@ export async function executeSubagent(
527
1450
 
528
1451
  try {
529
1452
  const target = singleTarget as ResolvedSubagentTarget;
1453
+ const prepared = prepareTask(params.task, params);
1454
+ const thinkingLevel = resolveThinkingLevel(params.agent, params.thinkingLevel);
1455
+ const budget = resolveExecutionBudget(params.agent, params.timeoutMs);
1456
+ const taskGeneration = startWorkItem("task-1", params.agent)?.taskGeneration ?? 0;
530
1457
  const result = attachTarget(
531
- await runSingleAgent(
532
- ctx.cwd,
533
- agents,
534
- params.agent,
535
- params.task,
536
- target.cwd,
537
- undefined,
538
- signal,
539
- resolveThinkingLevel(params.agent, params.thinkingLevel),
540
- resolveTimeoutMs(params.agent, params.timeoutMs),
541
- onUpdate,
542
- makeDetails("single"),
543
- undefined,
544
- launchPolicy(target),
545
- ),
1458
+ budget
1459
+ ? await runSingleAgent(
1460
+ ctx.cwd,
1461
+ agents,
1462
+ params.agent,
1463
+ prepared.text,
1464
+ target.cwd,
1465
+ undefined,
1466
+ signal,
1467
+ thinkingLevel,
1468
+ budget.timeoutMs,
1469
+ onUpdate,
1470
+ makeDetails("single"),
1471
+ undefined,
1472
+ launchPolicy(
1473
+ target,
1474
+ prepared,
1475
+ params.task,
1476
+ params.agent,
1477
+ thinkingLevel,
1478
+ budget.timeoutMs,
1479
+ taskGeneration,
1480
+ budget,
1481
+ resolveTurnLimits(params),
1482
+ ),
1483
+ )
1484
+ : exhaustedResult(params.agent, params.task, thinkingLevel),
546
1485
  target,
547
1486
  );
1487
+ settleWorkItem("task-1", result, taskGeneration);
548
1488
  const isError = isResultError(result);
549
1489
  if (isError) {
550
1490
  const errorMsg = formatResultFailure(result);