@narumitw/pi-subagents 0.49.2 → 0.51.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 (81) hide show
  1. package/README.md +313 -53
  2. package/package.json +11 -8
  3. package/src/adaptive-scheduler.ts +196 -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 +848 -158
  24. package/src/in-process-transport.ts +269 -25
  25. package/src/inspect-render.ts +101 -1
  26. package/src/inspect.ts +296 -3
  27. package/src/integration-controller.ts +98 -0
  28. package/src/limits.ts +3 -0
  29. package/src/orchestration-metrics.ts +78 -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 +772 -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 +172 -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 +17 -0
  77. package/src/work-item-ledger.ts +682 -0
  78. package/src/work-item-persistence.ts +218 -0
  79. package/src/workflow-planning.ts +150 -0
  80. package/src/workflow-ui.ts +61 -0
  81. 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,41 @@ 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";
10
24
  import {
11
25
  assertDelegationTargetAllowed,
12
26
  type ResolvedSubagentTarget,
13
27
  resolveSubagentTarget,
14
28
  targetPolicyAudit,
15
29
  } from "./cwd-policy.js";
16
- import { DEFAULT_MAX_CONTEXT_BYTES, truncateUtf8 } from "./limits.js";
30
+ import {
31
+ appendDelegationContract,
32
+ type DelegationContract,
33
+ normalizeDelegationContract,
34
+ } from "./delegation-contract.js";
35
+ import {
36
+ acknowledgeExecutionPlan,
37
+ createExecutionPlan,
38
+ resolveContractTools,
39
+ } from "./execution-plan.js";
40
+ import {
41
+ DEFAULT_MAX_CONTEXT_BYTES,
42
+ MAX_BLOCKING_PARALLEL_CONCURRENCY,
43
+ MAX_SUBAGENT_TIMEOUT_MS,
44
+ truncateUtf8,
45
+ } from "./limits.js";
46
+ import { calculateOrchestrationMetrics } from "./orchestration-metrics.js";
47
+ import { executePanel, preflightPanelExecution } from "./panel-execution.js";
48
+ import { validatePanelRequest } from "./panel-planning.js";
17
49
  import { hasUsableAggregator, type SubagentParams } from "./params.js";
50
+ import { appendResultInstruction, type SubagentResultFormat } from "./result-contract.js";
18
51
  import {
19
52
  buildFanInContext,
20
53
  formatResultFailure,
@@ -30,14 +63,21 @@ import { safeTerminalLine } from "./safe-text.js";
30
63
  import {
31
64
  DEFAULT_DELEGATION_CWD_POLICY,
32
65
  readSubagentSettings,
66
+ resolveBlockingMaxParallelTasks,
33
67
  resolveSubagentThinkingLevel,
34
68
  } from "./settings.js";
69
+ import { isRetryableResult, runHedgedAttempt, supervisionDelay } from "./supervision.js";
70
+ import { TimeoutProgressJournal, TURN_TERMINATION_VERSION } from "./timeout-checkpoint.js";
71
+ import type { TurnLimits } from "./turn-budget.js";
72
+ import { requiresIndependentVerification } from "./verification-policy.js";
73
+ import type { WorkItemLedger } from "./work-item-ledger.js";
74
+ import {
75
+ createSessionWorkItemPersistence,
76
+ type WorkItemPersistence,
77
+ } from "./work-item-persistence.js";
78
+ import { createBlockingWorkLedger, resolveWorkflowTasks } from "./workflow-planning.js";
35
79
 
36
- const MAX_PARALLEL_TASKS = 8;
37
- const MAX_CONCURRENCY = 4;
38
80
  export const FALLBACK_TIMEOUT_MS = 10 * 60 * 1000;
39
- const STATUS_KEY = "subagents";
40
- const activeStatuses = new Map<string, string>();
41
81
 
42
82
  export function parsePositiveInteger(value: string | undefined): number | undefined {
43
83
  if (!value) return undefined;
@@ -57,59 +97,6 @@ export function assertSubagentDepthAllowed(): void {
57
97
  }
58
98
  }
59
99
 
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
100
  export async function executeSubagent(
114
101
  toolCallId: string,
115
102
  params: SubagentParams,
@@ -125,8 +112,10 @@ export async function executeSubagent(
125
112
  }
126
113
  const aggregator = hasUsableAggregator(params.aggregator) ? params.aggregator : undefined;
127
114
  const config = settingsOverride ?? readSubagentSettings();
115
+ const maxParallelTasks = resolveBlockingMaxParallelTasks(config);
128
116
  const discovery = discoverAgents(ctx.cwd, agentScope, config);
129
117
  const agents = discovery.agents;
118
+ const resolvedWorkflowTasks = resolveWorkflowTasks(params, agents);
130
119
  const confirmProjectAgents = params.confirmProjectAgents ?? true;
131
120
  const resolveTimeoutMs = (agentName: string, localTimeoutMs?: number) =>
132
121
  localTimeoutMs ??
@@ -135,21 +124,112 @@ export async function executeSubagent(
135
124
  resolveDefaultSubagentTimeoutMs();
136
125
  const resolveThinkingLevel = (agentName: string, localThinkingLevel?: SubagentThinkingLevel) =>
137
126
  resolveSubagentThinkingLevel(agents, agentName, params.thinkingLevel, localThinkingLevel);
127
+ let orchestrationDeadline: number | undefined;
128
+ const resolveTurnLimits = (local?: TurnLimits): TurnLimits => ({
129
+ idleTimeoutMs: local?.idleTimeoutMs ?? params.idleTimeoutMs,
130
+ maxTurns: local?.maxTurns ?? params.maxTurns,
131
+ maxToolCalls: local?.maxToolCalls ?? params.maxToolCalls,
132
+ });
133
+ const resolveExecutionBudget = (
134
+ agentName: string,
135
+ localTimeoutMs?: number,
136
+ ):
137
+ | {
138
+ timeoutMs: number;
139
+ workTimeoutReason: "work_timeout" | "orchestration_timeout";
140
+ workTimeoutReportLimit: number;
141
+ }
142
+ | undefined => {
143
+ const requested = resolveTimeoutMs(agentName, localTimeoutMs);
144
+ if (orchestrationDeadline === undefined) {
145
+ return {
146
+ timeoutMs: requested,
147
+ workTimeoutReason: "work_timeout",
148
+ workTimeoutReportLimit: requested,
149
+ };
150
+ }
151
+ const remaining = Math.floor(orchestrationDeadline - Date.now());
152
+ if (remaining < 1) return undefined;
153
+ const orchestrationLimited = remaining < requested;
154
+ return {
155
+ timeoutMs: Math.min(requested, remaining),
156
+ workTimeoutReason: orchestrationLimited ? "orchestration_timeout" : "work_timeout",
157
+ workTimeoutReportLimit: orchestrationLimited
158
+ ? Math.floor(params.totalTimeoutMs as number)
159
+ : requested,
160
+ };
161
+ };
138
162
 
139
163
  const hasChain = (params.chain?.length ?? 0) > 0;
140
164
  const hasTasks = (params.tasks?.length ?? 0) > 0;
165
+ const hasWorkflow = (params.workflow?.tasks.length ?? 0) > 0;
166
+ const hasPanel = params.panel !== undefined;
141
167
  const hasSingle = Boolean(params.agent && params.task);
142
- const modeCount = Number(hasChain) + Number(hasTasks) + Number(hasSingle);
168
+ const modeCount =
169
+ Number(hasChain) +
170
+ Number(hasTasks) +
171
+ Number(hasWorkflow) +
172
+ Number(hasPanel) +
173
+ Number(hasSingle);
174
+ let workLedger: WorkItemLedger | undefined;
175
+ const workflowScheduling: ReturnType<AdaptiveScheduler["decide"]>[] = [];
143
176
 
144
177
  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
- });
178
+ (mode: "single" | "parallel" | "chain" | "workflow" | "panel") =>
179
+ (results: SingleResult[], aggregator?: SingleResult): SubagentDetails => {
180
+ const workflow = workLedger?.snapshot();
181
+ const metricResults = aggregator ? [...results, aggregator] : results;
182
+ return {
183
+ mode,
184
+ agentScope,
185
+ projectAgentsDir: discovery.projectAgentsDir,
186
+ results,
187
+ aggregator,
188
+ workflow,
189
+ schedulerDecisions:
190
+ workflowScheduling.length > 0 ? workflowScheduling.slice(-64) : undefined,
191
+ metrics: calculateOrchestrationMetrics(workflow, metricResults),
192
+ };
193
+ };
194
+ const exhaustedResult = (
195
+ agentName: string,
196
+ task: string,
197
+ thinkingLevel: SubagentThinkingLevel | undefined,
198
+ step?: number,
199
+ ): SingleResult => {
200
+ const limit = Math.floor(params.totalTimeoutMs as number);
201
+ const message = `Subagent orchestration deadline expired after ${limit}ms`;
202
+ return {
203
+ agent: agentName,
204
+ agentSource: agents.find((agent) => agent.name === agentName)?.source ?? "unknown",
205
+ task,
206
+ exitCode: 124,
207
+ messages: [],
208
+ stderr: message,
209
+ errorMessage: message,
210
+ usage: {
211
+ input: 0,
212
+ output: 0,
213
+ cacheRead: 0,
214
+ cacheWrite: 0,
215
+ cost: 0,
216
+ contextTokens: 0,
217
+ turns: 0,
218
+ },
219
+ thinkingLevel,
220
+ step,
221
+ finalOutput: "",
222
+ timedOut: true,
223
+ stopReason: "timeout",
224
+ termination: {
225
+ version: TURN_TERMINATION_VERSION,
226
+ reason: "orchestration_timeout",
227
+ limit,
228
+ checkpoint: new TimeoutProgressJournal().checkpoint(task),
229
+ finalization: { attempted: false, status: "skipped", durationMs: 0 },
230
+ },
231
+ };
232
+ };
153
233
 
154
234
  if (modeCount !== 1 || (aggregator && !hasTasks)) {
155
235
  const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
@@ -167,6 +247,117 @@ export async function executeSubagent(
167
247
  details: makeDetails("single")([]),
168
248
  };
169
249
  }
250
+ if (
251
+ (hasWorkflow || hasPanel) &&
252
+ (Number.parseInt(process.env.PI_SUBAGENT_DEPTH ?? "0", 10) || 0) > 0
253
+ ) {
254
+ throw new Error("Explicit workflow and panel recursion is disabled until separately evaluated");
255
+ }
256
+ if (params.panel) {
257
+ validatePanelRequest(params.panel, maxParallelTasks);
258
+ for (const agentName of [
259
+ ...params.panel.reviewers.map((reviewer) => reviewer.agent),
260
+ params.panel.synthesizer.agent,
261
+ ]) {
262
+ if (!agents.some((agent) => agent.name === agentName)) {
263
+ throw new Error(`Unknown panel agent: ${agentName}`);
264
+ }
265
+ }
266
+ }
267
+ if (
268
+ (hasTasks && (params.tasks?.length ?? 0) > maxParallelTasks) ||
269
+ (hasWorkflow && (params.workflow?.tasks.length ?? 0) > maxParallelTasks) ||
270
+ (hasPanel && (params.panel?.reviewers.length ?? 0) > maxParallelTasks)
271
+ ) {
272
+ const count = hasWorkflow
273
+ ? (params.workflow?.tasks.length ?? 0)
274
+ : hasPanel
275
+ ? (params.panel?.reviewers.length ?? 0)
276
+ : (params.tasks?.length ?? 0);
277
+ throw new Error(`Too many delegated tasks (${count}). Configured max is ${maxParallelTasks}.`);
278
+ }
279
+
280
+ const nonWorkflowRetryConfigured =
281
+ !hasWorkflow &&
282
+ Boolean(
283
+ params.retryPolicy ||
284
+ params.hedgeAfterMs ||
285
+ params.tasks?.some((task) => task.retryPolicy || task.hedgeAfterMs) ||
286
+ params.chain?.some((task) => task.retryPolicy || task.hedgeAfterMs) ||
287
+ aggregator?.retryPolicy ||
288
+ aggregator?.hedgeAfterMs,
289
+ );
290
+ if (nonWorkflowRetryConfigured) {
291
+ throw new Error("Retry and hedge policies are supported only by explicit workflow tasks");
292
+ }
293
+ if (hasWorkflow && params.workflow) {
294
+ for (const task of resolvedWorkflowTasks) {
295
+ const contract = normalizeDelegationContract(task.contract);
296
+ if (params.workflow.honorAdmission) {
297
+ const admission = contract?.admission;
298
+ const decision = evaluateDelegationAdmission({
299
+ contextPressure: admission?.contextPressure ?? "low",
300
+ independentWorkItems: admission?.independentWorkItems ?? 1,
301
+ coupling: admission?.coupling ?? "dense",
302
+ verificationRequired: admission?.verificationRequired ?? false,
303
+ verificationAvailable: admission?.verificationAvailable ?? false,
304
+ capabilitiesSupported: true,
305
+ budgetAllowsChildren: admission?.budgetAllowsChildren ?? false,
306
+ generationCurrent: true,
307
+ requirementsComplete: admission?.requirementsComplete ?? false,
308
+ });
309
+ if (
310
+ decision.recommendation === "parent-owned-direct" ||
311
+ decision.recommendation === "abstain-insufficient-evidence"
312
+ ) {
313
+ throw new Error(
314
+ `Admission declined workflow task ${task.id}: ${decision.reasonCodes.join(", ")}`,
315
+ );
316
+ }
317
+ }
318
+ if (
319
+ requiresIndependentVerification({
320
+ contract,
321
+ integrationOwner: task.integrationOwner === true,
322
+ requiredCapabilities: task.requiredCapabilities ?? [],
323
+ })
324
+ ) {
325
+ const verifier = resolvedWorkflowTasks.find(
326
+ (candidate) =>
327
+ candidate.verifierFor === task.id &&
328
+ candidate.dependsOn?.includes(task.id) &&
329
+ candidate.agent !== task.agent,
330
+ );
331
+ if (!verifier) {
332
+ throw new Error(
333
+ `Workflow task ${task.id} requires a distinct dependent verifier before launch`,
334
+ );
335
+ }
336
+ }
337
+ if (!task.retryPolicy && !task.hedgeAfterMs) continue;
338
+ const policy = contract?.sideEffectPolicy;
339
+ if (
340
+ (task.retryPolicy && policy !== "read-only" && policy !== "idempotent") ||
341
+ (task.hedgeAfterMs && policy !== "read-only")
342
+ ) {
343
+ throw new Error(
344
+ `Workflow task ${task.id} must declare an idempotent retry or read-only hedge delegation contract`,
345
+ );
346
+ }
347
+ }
348
+ }
349
+ workLedger = createBlockingWorkLedger(params, resolvedWorkflowTasks, aggregator);
350
+ let workflowPersistence: WorkItemPersistence | undefined;
351
+ if (hasWorkflow && workLedger) {
352
+ const owner =
353
+ ctx.sessionManager.getSessionId?.() ??
354
+ ctx.sessionManager.getSessionFile?.() ??
355
+ `ephemeral:${ctx.cwd}`;
356
+ workflowPersistence = createSessionWorkItemPersistence(owner, workLedger.workflowId);
357
+ }
358
+ const persistWorkLedger = async () => {
359
+ if (workflowPersistence && workLedger) await workflowPersistence.save(workLedger.snapshot());
360
+ };
170
361
 
171
362
  const delegationPolicy = config?.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY;
172
363
  const resolveTarget = (cwd: string | undefined): ResolvedSubagentTarget => {
@@ -179,22 +370,215 @@ export async function executeSubagent(
179
370
  return target;
180
371
  };
181
372
  const singleTarget = hasSingle ? resolveTarget(params.cwd) : undefined;
373
+ const panelTarget = hasPanel ? resolveTarget(undefined) : undefined;
182
374
  const chainTargets = params.chain?.map((step) => resolveTarget(step.cwd)) ?? [];
183
375
  const parallelTargets = params.tasks?.map((task) => resolveTarget(task.cwd)) ?? [];
376
+ const workflowTargets = resolvedWorkflowTasks.map((task) => resolveTarget(task.cwd));
184
377
  const aggregatorTarget = aggregator ? resolveTarget(aggregator.cwd) : undefined;
378
+ if (params.panel && panelTarget) {
379
+ await preflightPanelExecution({
380
+ panel: params.panel,
381
+ agents,
382
+ signal,
383
+ target: panelTarget,
384
+ resolveThinkingLevel,
385
+ resolveTimeoutMs,
386
+ });
387
+ }
185
388
  const attachTarget = (result: SingleResult, target: ResolvedSubagentTarget): SingleResult => {
186
389
  result.target = targetPolicyAudit(target);
187
390
  return result;
188
391
  };
189
- const launchPolicy = (target: ResolvedSubagentTarget) => ({
190
- projectTrust: target.trust.projectTrusted,
191
- });
392
+ type ContractRequest = {
393
+ contract?: unknown;
394
+ resultFormat?: SubagentResultFormat;
395
+ };
396
+ const prepareTask = (task: string, local?: ContractRequest) => {
397
+ const contracted = appendDelegationContract(task, local?.contract ?? params.contract);
398
+ const resultFormat = local?.resultFormat ?? params.resultFormat;
399
+ return {
400
+ text: appendResultInstruction(contracted.text, resultFormat, DEFAULT_MAX_CONTEXT_BYTES),
401
+ contract: contracted.contract,
402
+ resultFormat,
403
+ };
404
+ };
405
+ const launchPolicy = (
406
+ target: ResolvedSubagentTarget,
407
+ prepared: { contract?: DelegationContract; resultFormat?: SubagentResultFormat },
408
+ displayTask: string,
409
+ agentName: string,
410
+ thinkingLevel: SubagentThinkingLevel | undefined,
411
+ timeoutMs: number,
412
+ taskGeneration = 0,
413
+ budget?: NonNullable<ReturnType<typeof resolveExecutionBudget>>,
414
+ turnLimits?: TurnLimits,
415
+ ) => {
416
+ const agent = agents.find((candidate) => candidate.name === agentName);
417
+ const effectiveTools = agent ? resolveContractTools(agent.tools, prepared.contract) : undefined;
418
+ const executionPlan = agent
419
+ ? createExecutionPlan({
420
+ contract: prepared.contract,
421
+ agent,
422
+ effectiveTools,
423
+ target: targetPolicyAudit(target),
424
+ workspaceMode: "shared",
425
+ transport: "subprocess",
426
+ resultFormat: prepared.resultFormat ?? "text",
427
+ model: agent.model,
428
+ thinkingLevel,
429
+ timeoutMs,
430
+ taskGeneration,
431
+ })
432
+ : undefined;
433
+ if (executionPlan) {
434
+ const acknowledgement = acknowledgeExecutionPlan(executionPlan);
435
+ if (acknowledgement.status === "rejected") {
436
+ throw new Error(`Execution plan rejected: ${JSON.stringify(acknowledgement)}`);
437
+ }
438
+ }
439
+ const capabilityGrant = executionPlan
440
+ ? issueCapabilityGrant(executionPlan, Date.now(), Math.max(1, timeoutMs + 60_000))
441
+ : undefined;
442
+ return {
443
+ projectTrust: target.trust.projectTrusted,
444
+ turnLimits,
445
+ workTimeoutReason: budget?.workTimeoutReason,
446
+ workTimeoutReportLimit: budget?.workTimeoutReportLimit,
447
+ orchestrationDeadlineAt: budget ? orchestrationDeadline : undefined,
448
+ tools: effectiveTools,
449
+ contract: prepared.contract,
450
+ resultFormat: prepared.resultFormat,
451
+ displayTask,
452
+ executionPlan,
453
+ capabilityGrant,
454
+ };
455
+ };
456
+
457
+ // Build and acknowledge every contracted plan before confirmation or child launch.
458
+ if (hasSingle && singleTarget && params.agent && params.task) {
459
+ const prepared = prepareTask(params.task, params);
460
+ launchPolicy(
461
+ singleTarget,
462
+ prepared,
463
+ params.task,
464
+ params.agent,
465
+ resolveThinkingLevel(params.agent, params.thinkingLevel),
466
+ resolveTimeoutMs(params.agent, params.timeoutMs),
467
+ );
468
+ }
469
+ for (const [index, step] of (params.chain ?? []).entries()) {
470
+ const prepared = prepareTask(step.task, step);
471
+ launchPolicy(
472
+ chainTargets[index],
473
+ prepared,
474
+ step.task,
475
+ step.agent,
476
+ resolveThinkingLevel(step.agent, step.thinkingLevel),
477
+ resolveTimeoutMs(step.agent, step.timeoutMs),
478
+ );
479
+ }
480
+ for (const [index, task] of (params.tasks ?? []).entries()) {
481
+ const prepared = prepareTask(task.task, task);
482
+ launchPolicy(
483
+ parallelTargets[index],
484
+ prepared,
485
+ task.task,
486
+ task.agent,
487
+ resolveThinkingLevel(task.agent, task.thinkingLevel),
488
+ resolveTimeoutMs(task.agent, task.timeoutMs),
489
+ );
490
+ }
491
+ for (const [index, task] of resolvedWorkflowTasks.entries()) {
492
+ const prepared = prepareTask(task.task, task);
493
+ launchPolicy(
494
+ workflowTargets[index],
495
+ prepared,
496
+ task.task,
497
+ task.agent,
498
+ resolveThinkingLevel(task.agent, task.thinkingLevel),
499
+ resolveTimeoutMs(task.agent, task.timeoutMs),
500
+ workLedger?.get(task.id)?.taskGeneration ?? 0,
501
+ );
502
+ }
503
+ if (aggregator && aggregatorTarget) {
504
+ const prepared = prepareTask(aggregator.task, aggregator);
505
+ launchPolicy(
506
+ aggregatorTarget,
507
+ prepared,
508
+ aggregator.task,
509
+ aggregator.agent,
510
+ resolveThinkingLevel(aggregator.agent, aggregator.thinkingLevel),
511
+ resolveTimeoutMs(aggregator.agent, aggregator.timeoutMs),
512
+ );
513
+ }
514
+
515
+ const startWorkItem = (id: string, agentName: string) => {
516
+ if (workLedger?.get(id)?.state === "ready") {
517
+ return workLedger.start(id, `agent:${agentName}`);
518
+ }
519
+ return workLedger?.get(id);
520
+ };
521
+ const settleWorkItem = (id: string, result: SingleResult, taskGeneration: number) => {
522
+ if (!workLedger) return;
523
+ if (workLedger.get(id)?.taskGeneration !== taskGeneration) {
524
+ result.outcome = {
525
+ status: "stale",
526
+ reasonCode: "stale-task-generation",
527
+ recoveryActions: ["discard", "replan"],
528
+ retryable: false,
529
+ };
530
+ }
531
+ if (result.outcome?.status === "stale") {
532
+ workLedger.invalidate(id, result.outcome.reasonCode ?? "stale-result");
533
+ return;
534
+ }
535
+ if (isResultError(result)) {
536
+ const state =
537
+ result.outcome?.status === "blocked"
538
+ ? "blocked"
539
+ : result.outcome?.status === "needs-input"
540
+ ? "needs-input"
541
+ : result.aborted || result.outcome?.status === "interrupted"
542
+ ? "interrupted"
543
+ : "failed";
544
+ workLedger.settle(
545
+ id,
546
+ state,
547
+ result.outcome?.reasonCode ?? result.errorMessage ?? result.stopReason,
548
+ );
549
+ return;
550
+ }
551
+ const structured =
552
+ result.structuredResult?.version === "pi-subagents:result:v2"
553
+ ? result.structuredResult
554
+ : undefined;
555
+ workLedger.complete(id, {
556
+ taskGeneration,
557
+ executionPlanId: result.executionPlan?.id,
558
+ artifacts: (structured?.artifacts ?? []).map((artifact) => ({
559
+ id: artifact.id,
560
+ kind: artifact.kind,
561
+ version: artifact.version ?? artifact.digest ?? "unversioned",
562
+ digest: artifact.digest,
563
+ verified:
564
+ structured?.verification.some((verification) => verification.status === "passed") ??
565
+ false,
566
+ })),
567
+ verificationAccepted:
568
+ structured?.verification.some((verification) => verification.status === "passed") ?? false,
569
+ });
570
+ };
192
571
 
193
572
  if (agentScope === "project" || agentScope === "both") {
194
573
  const requestedAgentNames = new Set<string>();
195
574
  if (params.chain) for (const step of params.chain) requestedAgentNames.add(step.agent);
196
575
  if (params.tasks) for (const t of params.tasks) requestedAgentNames.add(t.agent);
576
+ for (const task of resolvedWorkflowTasks) requestedAgentNames.add(task.agent);
197
577
  if (aggregator) requestedAgentNames.add(aggregator.agent);
578
+ if (params.panel) {
579
+ for (const reviewer of params.panel.reviewers) requestedAgentNames.add(reviewer.agent);
580
+ requestedAgentNames.add(params.panel.synthesizer.agent);
581
+ }
198
582
  if (params.agent) requestedAgentNames.add(params.agent);
199
583
 
200
584
  const projectAgentsRequested = Array.from(requestedAgentNames)
@@ -222,13 +606,258 @@ export async function executeSubagent(
222
606
  if (!ok) {
223
607
  return {
224
608
  content: [{ type: "text", text: "Canceled: project-local agents not approved." }],
225
- details: makeDetails(hasChain ? "chain" : hasTasks ? "parallel" : "single")([]),
609
+ details: makeDetails(
610
+ hasChain
611
+ ? "chain"
612
+ : hasTasks
613
+ ? "parallel"
614
+ : hasWorkflow
615
+ ? "workflow"
616
+ : hasPanel
617
+ ? "panel"
618
+ : "single",
619
+ )([]),
226
620
  };
227
621
  }
228
622
  }
229
623
  }
230
624
  }
231
625
 
626
+ orchestrationDeadline =
627
+ params.totalTimeoutMs === undefined
628
+ ? undefined
629
+ : Date.now() + Math.floor(params.totalTimeoutMs);
630
+ if (params.panel && panelTarget) {
631
+ return executePanel({
632
+ toolCallId,
633
+ params,
634
+ panel: params.panel,
635
+ signal,
636
+ onUpdate,
637
+ ctx,
638
+ agents,
639
+ agentScope,
640
+ projectAgentsDir: discovery.projectAgentsDir,
641
+ maxParallelTasks,
642
+ target: panelTarget,
643
+ resolveThinkingLevel,
644
+ resolveTimeoutMs,
645
+ });
646
+ }
647
+ if (params.workflow && resolvedWorkflowTasks.length > 0 && workLedger) {
648
+ await persistWorkLedger();
649
+ const status = startSubagentStatus(
650
+ ctx,
651
+ toolCallId,
652
+ parallelStatus(0, resolvedWorkflowTasks.length, 0),
653
+ );
654
+ const scheduler = new AdaptiveScheduler();
655
+ const taskById = new Map(
656
+ resolvedWorkflowTasks.map((task, index) => [task.id, { task, index }]),
657
+ );
658
+ const resultsById = new Map<string, SingleResult>();
659
+ const deadline = orchestrationDeadline;
660
+ const cancelWorkflowGeneration = () => {
661
+ for (const item of workLedger.snapshot().items) {
662
+ if (item.state === "running") workLedger.invalidate(item.id, "parent-aborted");
663
+ }
664
+ };
665
+ signal?.addEventListener("abort", cancelWorkflowGeneration, { once: true });
666
+ try {
667
+ while (true) {
668
+ const snapshot = workLedger.snapshot();
669
+ const remainingBudgetMs =
670
+ deadline === undefined
671
+ ? MAX_SUBAGENT_TIMEOUT_MS
672
+ : Math.max(0, Math.floor(deadline - Date.now()));
673
+ const decision = scheduler.decide(snapshot, {
674
+ maxConcurrency: Math.min(MAX_BLOCKING_PARALLEL_CONCURRENCY, maxParallelTasks),
675
+ activeCount: 0,
676
+ transportCapacity: MAX_BLOCKING_PARALLEL_CONCURRENCY,
677
+ remainingBudgetMs,
678
+ });
679
+ workflowScheduling.push(decision);
680
+ if (decision.selected.length === 0) break;
681
+ status.update(
682
+ parallelStatus(resultsById.size, resolvedWorkflowTasks.length, decision.selected.length),
683
+ );
684
+ const batch = await mapWithConcurrencyLimit(
685
+ decision.selected,
686
+ decision.effectiveConcurrency,
687
+ async (workItemId) => {
688
+ const entry = taskById.get(workItemId);
689
+ if (!entry) throw new Error(`Missing workflow task ${workItemId}`);
690
+ const { task, index } = entry;
691
+ const dependencies = (task.dependsOn ?? [])
692
+ .map((dependency) => resultsById.get(dependency))
693
+ .filter((result): result is SingleResult => result !== undefined);
694
+ const dependencyContext = dependencies.length
695
+ ? `\n\nDependency results:\n${buildFanInContext(dependencies)}`
696
+ : "";
697
+ const displayTask = task.task;
698
+ const taskWithContext = truncateUtf8(
699
+ `${task.task}${dependencyContext}`,
700
+ DEFAULT_MAX_CONTEXT_BYTES,
701
+ ).text;
702
+ const prepared = prepareTask(taskWithContext, task);
703
+ const target = workflowTargets[index];
704
+ const thinkingLevel = resolveThinkingLevel(task.agent, task.thinkingLevel);
705
+ const startedItem = startWorkItem(workItemId, task.agent);
706
+ const acceptedTaskGeneration = startedItem?.taskGeneration ?? 0;
707
+ await persistWorkLedger();
708
+ const runAttempt = (attemptSignal: AbortSignal | undefined) => {
709
+ const budget = resolveExecutionBudget(task.agent, task.timeoutMs);
710
+ if (!budget) {
711
+ return Promise.resolve(exhaustedResult(task.agent, displayTask, thinkingLevel));
712
+ }
713
+ return runSingleAgent(
714
+ ctx.cwd,
715
+ agents,
716
+ task.agent,
717
+ prepared.text,
718
+ target.cwd,
719
+ undefined,
720
+ attemptSignal,
721
+ thinkingLevel,
722
+ budget.timeoutMs,
723
+ undefined,
724
+ makeDetails("workflow"),
725
+ undefined,
726
+ launchPolicy(
727
+ target,
728
+ prepared,
729
+ displayTask,
730
+ task.agent,
731
+ thinkingLevel,
732
+ budget.timeoutMs,
733
+ acceptedTaskGeneration,
734
+ budget,
735
+ resolveTurnLimits(task),
736
+ ),
737
+ );
738
+ };
739
+ const maxAttempts = task.retryPolicy?.maxAttempts ?? 1;
740
+ let result: SingleResult | undefined;
741
+ let hedged = false;
742
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
743
+ if (attempt > 1 && deadline !== undefined && Date.now() >= deadline) break;
744
+ const attempted = await runHedgedAttempt(runAttempt, signal, task.hedgeAfterMs);
745
+ hedged ||= attempted.hedged;
746
+ result = attachTarget(
747
+ {
748
+ ...attempted.result,
749
+ attemptCount: attempt,
750
+ hedged: hedged || undefined,
751
+ },
752
+ target,
753
+ );
754
+ if (!isRetryableResult(result) || attempt >= maxAttempts) break;
755
+ if (deadline !== undefined && Date.now() >= deadline) break;
756
+ await supervisionDelay(task.retryPolicy?.backoffMs ?? 0, signal);
757
+ }
758
+ if (!result) throw new Error(`Workflow task ${workItemId} produced no result`);
759
+ if (workLedger.get(workItemId)?.taskGeneration !== acceptedTaskGeneration) {
760
+ result.outcome = {
761
+ status: "stale",
762
+ reasonCode: "cancelled-generation",
763
+ recoveryActions: ["discard", "replan"],
764
+ retryable: false,
765
+ };
766
+ }
767
+ resultsById.set(workItemId, result);
768
+ settleWorkItem(workItemId, result, acceptedTaskGeneration);
769
+ await persistWorkLedger();
770
+ return result;
771
+ },
772
+ signal,
773
+ );
774
+ if (batch.length === 0 || signal?.aborted) break;
775
+ }
776
+ for (const item of workLedger.snapshot().items) {
777
+ if (item.state !== "pending" && item.state !== "ready") continue;
778
+ if (signal?.aborted) {
779
+ workLedger.settle(item.id, "interrupted", "parent-aborted");
780
+ } else if (deadline !== undefined && Date.now() >= deadline) {
781
+ workLedger.settle(item.id, "blocked", "budget-exhausted");
782
+ } else {
783
+ const dependencyBlocked = item.dependencies.some(
784
+ (dependency) => workLedger.get(dependency)?.state !== "completed",
785
+ );
786
+ workLedger.settle(
787
+ item.id,
788
+ dependencyBlocked ? "blocked" : "needs-input",
789
+ dependencyBlocked ? "dependency-not-completed" : "artifact-version-mismatch",
790
+ );
791
+ }
792
+ }
793
+ await persistWorkLedger();
794
+ const results = resolvedWorkflowTasks.map((task) => {
795
+ const completed = resultsById.get(task.id);
796
+ if (completed) return completed;
797
+ const item = workLedger.get(task.id);
798
+ const outcomeStatus =
799
+ item?.state === "interrupted"
800
+ ? "interrupted"
801
+ : item?.state === "needs-input"
802
+ ? "needs-input"
803
+ : "blocked";
804
+ const reasonCode = item?.outcomeReason ?? "dependency-not-satisfied";
805
+ return {
806
+ agent: task.agent,
807
+ agentSource: agents.find((agent) => agent.name === task.agent)?.source ?? "unknown",
808
+ task: task.task,
809
+ exitCode: 1,
810
+ messages: [],
811
+ stderr: "Workflow dependency was not satisfied",
812
+ errorMessage: `Workflow task did not start: ${reasonCode}`,
813
+ aborted: outcomeStatus === "interrupted",
814
+ outcome: {
815
+ status: outcomeStatus,
816
+ reasonCode,
817
+ recoveryActions:
818
+ outcomeStatus === "needs-input"
819
+ ? ["supply-input"]
820
+ : outcomeStatus === "interrupted"
821
+ ? ["retry"]
822
+ : ["resolve-dependency"],
823
+ retryable: outcomeStatus === "interrupted",
824
+ },
825
+ usage: {
826
+ input: 0,
827
+ output: 0,
828
+ cacheRead: 0,
829
+ cacheWrite: 0,
830
+ cost: 0,
831
+ contextTokens: 0,
832
+ turns: 0,
833
+ },
834
+ finalOutput: "",
835
+ } satisfies SingleResult;
836
+ });
837
+ const successCount = results.filter((result) => !isResultError(result)).length;
838
+ const attemptCount = results.reduce((sum, result) => sum + (result.attemptCount ?? 1), 0);
839
+ const hedgeCount = results.filter((result) => result.hedged).length;
840
+ const isError = successCount !== results.length;
841
+ return {
842
+ content: [
843
+ {
844
+ type: "text",
845
+ text: `Workflow: ${successCount}/${results.length} succeeded; ${attemptCount} attempt(s), ${hedgeCount} hedged task(s).`,
846
+ },
847
+ ],
848
+ details: { ...makeDetails("workflow")(results), isError },
849
+ isError: isError || undefined,
850
+ };
851
+ } finally {
852
+ signal?.removeEventListener("abort", cancelWorkflowGeneration);
853
+ try {
854
+ await persistWorkLedger();
855
+ } finally {
856
+ status.clear();
857
+ }
858
+ }
859
+ }
860
+
232
861
  if (params.chain && params.chain.length > 0) {
233
862
  const results: SingleResult[] = [];
234
863
  let previousOutput = "";
@@ -242,6 +871,7 @@ export async function executeSubagent(
242
871
  step.task.replace(/\{previous\}/g, previousOutput),
243
872
  DEFAULT_MAX_CONTEXT_BYTES,
244
873
  ).text;
874
+ const prepared = prepareTask(taskWithContext, step);
245
875
 
246
876
  // Create update callback that includes all previous results
247
877
  const chainUpdate: OnUpdateCallback | undefined = onUpdate
@@ -259,25 +889,41 @@ export async function executeSubagent(
259
889
  : undefined;
260
890
 
261
891
  const target = chainTargets[i];
892
+ const thinkingLevel = resolveThinkingLevel(step.agent, step.thinkingLevel);
893
+ const budget = resolveExecutionBudget(step.agent, step.timeoutMs);
894
+ const taskGeneration = startWorkItem(`step-${i + 1}`, step.agent)?.taskGeneration ?? 0;
262
895
  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
- ),
896
+ budget
897
+ ? await runSingleAgent(
898
+ ctx.cwd,
899
+ agents,
900
+ step.agent,
901
+ prepared.text,
902
+ target.cwd,
903
+ i + 1,
904
+ signal,
905
+ thinkingLevel,
906
+ budget.timeoutMs,
907
+ chainUpdate,
908
+ makeDetails("chain"),
909
+ undefined,
910
+ launchPolicy(
911
+ target,
912
+ prepared,
913
+ taskWithContext,
914
+ step.agent,
915
+ thinkingLevel,
916
+ budget.timeoutMs,
917
+ taskGeneration,
918
+ budget,
919
+ resolveTurnLimits(step),
920
+ ),
921
+ )
922
+ : exhaustedResult(step.agent, taskWithContext, thinkingLevel, i + 1),
278
923
  target,
279
924
  );
280
925
  results.push(result);
926
+ settleWorkItem(`step-${i + 1}`, result, taskGeneration);
281
927
 
282
928
  const isError = isResultError(result);
283
929
  if (isError) {
@@ -290,7 +936,9 @@ export async function executeSubagent(
290
936
  isError: true,
291
937
  };
292
938
  }
293
- previousOutput = getResultFinalOutput(result);
939
+ previousOutput = result.structuredResult
940
+ ? JSON.stringify(result.structuredResult)
941
+ : getResultFinalOutput(result);
294
942
  }
295
943
  return {
296
944
  content: [
@@ -307,17 +955,6 @@ export async function executeSubagent(
307
955
  }
308
956
 
309
957
  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
958
  const status = startSubagentStatus(
322
959
  ctx,
323
960
  toolCallId,
@@ -395,34 +1032,50 @@ export async function executeSubagent(
395
1032
 
396
1033
  const results = await mapWithConcurrencyLimit(
397
1034
  params.tasks,
398
- MAX_CONCURRENCY,
1035
+ MAX_BLOCKING_PARALLEL_CONCURRENCY,
399
1036
  async (t, index) => {
400
1037
  const target = parallelTargets[index];
1038
+ const prepared = prepareTask(t.task, t);
1039
+ const thinkingLevel = resolveThinkingLevel(t.agent, t.thinkingLevel);
1040
+ const budget = resolveExecutionBudget(t.agent, t.timeoutMs);
1041
+ const taskGeneration = startWorkItem(`task-${index + 1}`, t.agent)?.taskGeneration ?? 0;
401
1042
  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
- ),
1043
+ budget
1044
+ ? await runSingleAgent(
1045
+ ctx.cwd,
1046
+ agents,
1047
+ t.agent,
1048
+ prepared.text,
1049
+ target.cwd,
1050
+ undefined,
1051
+ signal,
1052
+ thinkingLevel,
1053
+ budget.timeoutMs,
1054
+ (partial) => {
1055
+ if (partial.details?.results[0]) {
1056
+ allResults[index] = { ...partial.details.results[0], exitCode: -1 };
1057
+ emitParallelUpdate();
1058
+ }
1059
+ },
1060
+ makeDetails("parallel"),
1061
+ undefined,
1062
+ launchPolicy(
1063
+ target,
1064
+ prepared,
1065
+ t.task,
1066
+ t.agent,
1067
+ thinkingLevel,
1068
+ budget.timeoutMs,
1069
+ taskGeneration,
1070
+ budget,
1071
+ resolveTurnLimits(t),
1072
+ ),
1073
+ )
1074
+ : exhaustedResult(t.agent, t.task, thinkingLevel),
423
1075
  target,
424
1076
  );
425
1077
  allResults[index] = result;
1078
+ settleWorkItem(`task-${index + 1}`, result, taskGeneration);
426
1079
  doneCount += 1;
427
1080
  runningCount -= 1;
428
1081
  emitParallelUpdate();
@@ -430,6 +1083,8 @@ export async function executeSubagent(
430
1083
  },
431
1084
  signal,
432
1085
  (task, index) => {
1086
+ const taskGeneration =
1087
+ startWorkItem(`task-${index + 1}`, task.agent)?.taskGeneration ?? 0;
433
1088
  const skipped: SingleResult = {
434
1089
  ...allResults[index],
435
1090
  task: task.task,
@@ -439,6 +1094,7 @@ export async function executeSubagent(
439
1094
  errorMessage: "Subagent was not started because the parent call was aborted",
440
1095
  };
441
1096
  allResults[index] = skipped;
1097
+ settleWorkItem(`task-${index + 1}`, skipped, taskGeneration);
442
1098
  doneCount += 1;
443
1099
  runningCount -= 1;
444
1100
  emitParallelUpdate();
@@ -456,33 +1112,50 @@ export async function executeSubagent(
456
1112
  : `${aggregator.task}\n\nParallel task outputs:\n\n${fanInContext}`,
457
1113
  DEFAULT_MAX_CONTEXT_BYTES,
458
1114
  ).text;
1115
+ const prepared = prepareTask(aggregatorTask, aggregator);
459
1116
  const target = aggregatorTarget as ResolvedSubagentTarget;
1117
+ const thinkingLevel = resolveThinkingLevel(aggregator.agent, aggregator.thinkingLevel);
1118
+ const budget = resolveExecutionBudget(aggregator.agent, aggregator.timeoutMs);
1119
+ const taskGeneration = startWorkItem("aggregator", aggregator.agent)?.taskGeneration ?? 0;
460
1120
  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
- ),
1121
+ budget
1122
+ ? await runSingleAgent(
1123
+ ctx.cwd,
1124
+ agents,
1125
+ aggregator.agent,
1126
+ prepared.text,
1127
+ target.cwd,
1128
+ undefined,
1129
+ signal,
1130
+ thinkingLevel,
1131
+ budget.timeoutMs,
1132
+ (partial) => {
1133
+ status.update(fanInStatus(aggregator.agent));
1134
+ if (onUpdate && partial.details?.results[0]) {
1135
+ onUpdate({
1136
+ content: partial.content,
1137
+ details: makeDetails("parallel")(results, partial.details.results[0]),
1138
+ });
1139
+ }
1140
+ },
1141
+ makeDetails("parallel"),
1142
+ undefined,
1143
+ launchPolicy(
1144
+ target,
1145
+ prepared,
1146
+ aggregatorTask,
1147
+ aggregator.agent,
1148
+ thinkingLevel,
1149
+ budget.timeoutMs,
1150
+ taskGeneration,
1151
+ budget,
1152
+ resolveTurnLimits(aggregator),
1153
+ ),
1154
+ )
1155
+ : exhaustedResult(aggregator.agent, aggregatorTask, thinkingLevel),
484
1156
  target,
485
1157
  );
1158
+ settleWorkItem("aggregator", aggregatorResult, taskGeneration);
486
1159
  }
487
1160
 
488
1161
  const successCount = results.filter((result) => !isResultError(result)).length;
@@ -527,24 +1200,41 @@ export async function executeSubagent(
527
1200
 
528
1201
  try {
529
1202
  const target = singleTarget as ResolvedSubagentTarget;
1203
+ const prepared = prepareTask(params.task, params);
1204
+ const thinkingLevel = resolveThinkingLevel(params.agent, params.thinkingLevel);
1205
+ const budget = resolveExecutionBudget(params.agent, params.timeoutMs);
1206
+ const taskGeneration = startWorkItem("task-1", params.agent)?.taskGeneration ?? 0;
530
1207
  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
- ),
1208
+ budget
1209
+ ? await runSingleAgent(
1210
+ ctx.cwd,
1211
+ agents,
1212
+ params.agent,
1213
+ prepared.text,
1214
+ target.cwd,
1215
+ undefined,
1216
+ signal,
1217
+ thinkingLevel,
1218
+ budget.timeoutMs,
1219
+ onUpdate,
1220
+ makeDetails("single"),
1221
+ undefined,
1222
+ launchPolicy(
1223
+ target,
1224
+ prepared,
1225
+ params.task,
1226
+ params.agent,
1227
+ thinkingLevel,
1228
+ budget.timeoutMs,
1229
+ taskGeneration,
1230
+ budget,
1231
+ resolveTurnLimits(params),
1232
+ ),
1233
+ )
1234
+ : exhaustedResult(params.agent, params.task, thinkingLevel),
546
1235
  target,
547
1236
  );
1237
+ settleWorkItem("task-1", result, taskGeneration);
548
1238
  const isError = isResultError(result);
549
1239
  if (isError) {
550
1240
  const errorMsg = formatResultFailure(result);