pi-subagents 0.24.4 → 0.27.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 (48) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +145 -27
  3. package/package.json +1 -1
  4. package/prompts/parallel-context-build.md +3 -1
  5. package/prompts/parallel-handoff-plan.md +3 -1
  6. package/prompts/review-loop.md +1 -1
  7. package/skills/pi-subagents/SKILL.md +71 -20
  8. package/src/agents/agent-management.ts +57 -15
  9. package/src/agents/agent-serializer.ts +3 -2
  10. package/src/agents/agents.ts +47 -16
  11. package/src/agents/chain-serializer.ts +120 -0
  12. package/src/extension/fanout-child.ts +171 -0
  13. package/src/extension/index.ts +7 -2
  14. package/src/extension/schemas.ts +138 -5
  15. package/src/intercom/result-intercom.ts +108 -0
  16. package/src/runs/background/async-execution.ts +185 -10
  17. package/src/runs/background/async-job-tracker.ts +41 -6
  18. package/src/runs/background/async-resume.ts +28 -15
  19. package/src/runs/background/async-status.ts +71 -31
  20. package/src/runs/background/result-watcher.ts +111 -54
  21. package/src/runs/background/run-id-resolver.ts +83 -0
  22. package/src/runs/background/run-status.ts +89 -4
  23. package/src/runs/background/stale-run-reconciler.ts +46 -1
  24. package/src/runs/background/subagent-runner.ts +648 -42
  25. package/src/runs/foreground/chain-execution.ts +331 -118
  26. package/src/runs/foreground/execution.ts +226 -10
  27. package/src/runs/foreground/subagent-executor.ts +377 -14
  28. package/src/runs/shared/acceptance-contract.ts +291 -0
  29. package/src/runs/shared/acceptance-evaluation.ts +221 -0
  30. package/src/runs/shared/acceptance-finalization.ts +161 -0
  31. package/src/runs/shared/acceptance-reports.ts +127 -0
  32. package/src/runs/shared/acceptance.ts +22 -0
  33. package/src/runs/shared/chain-outputs.ts +101 -0
  34. package/src/runs/shared/completion-guard.ts +26 -3
  35. package/src/runs/shared/dynamic-fanout.ts +293 -0
  36. package/src/runs/shared/nested-events.ts +819 -0
  37. package/src/runs/shared/nested-path.ts +52 -0
  38. package/src/runs/shared/nested-render.ts +115 -0
  39. package/src/runs/shared/parallel-utils.ts +31 -1
  40. package/src/runs/shared/pi-args.ts +73 -5
  41. package/src/runs/shared/structured-output.ts +77 -0
  42. package/src/runs/shared/subagent-prompt-runtime.ts +77 -7
  43. package/src/runs/shared/workflow-graph.ts +206 -0
  44. package/src/shared/formatters.ts +2 -2
  45. package/src/shared/settings.ts +53 -4
  46. package/src/shared/types.ts +345 -0
  47. package/src/slash/slash-commands.ts +41 -3
  48. package/src/tui/render.ts +268 -43
@@ -19,6 +19,51 @@ export interface MaxOutputConfig {
19
19
 
20
20
  export type OutputMode = "inline" | "file-only";
21
21
 
22
+ export type JsonSchemaObject = Record<string, unknown>;
23
+
24
+ export interface ChainOutputMapEntry {
25
+ text: string;
26
+ structured?: unknown;
27
+ agent: string;
28
+ stepIndex: number;
29
+ }
30
+
31
+ export type ChainOutputMap = Record<string, ChainOutputMapEntry>;
32
+
33
+ export type WorkflowNodeStatus = "pending" | "running" | "completed" | "failed" | "paused" | "detached";
34
+
35
+ export interface WorkflowGraphNode {
36
+ id: string;
37
+ kind: "step" | "parallel-group" | "dynamic-parallel-group" | "agent";
38
+ agent?: string;
39
+ phase?: string;
40
+ label: string;
41
+ status: WorkflowNodeStatus;
42
+ flatIndex?: number;
43
+ stepIndex?: number;
44
+ children?: WorkflowGraphNode[];
45
+ dynamic?: {
46
+ sourceOutput: string;
47
+ sourcePath: string;
48
+ itemName: string;
49
+ maxItems?: number;
50
+ collectAs?: string;
51
+ };
52
+ itemKey?: string;
53
+ outputName?: string;
54
+ structured?: boolean;
55
+ acceptanceStatus?: AcceptanceLedgerStatus;
56
+ error?: string;
57
+ }
58
+
59
+ export interface WorkflowGraphSnapshot {
60
+ runId: string;
61
+ mode: "chain" | "parallel" | "single";
62
+ phases: Array<{ title: string; nodeIds: string[] }>;
63
+ nodes: WorkflowGraphNode[];
64
+ currentNodeId?: string;
65
+ }
66
+
22
67
  export interface SavedOutputReference {
23
68
  path: string;
24
69
  bytes: number;
@@ -83,6 +128,8 @@ export interface ControlEvent {
83
128
  agent: string;
84
129
  index?: number;
85
130
  runId: string;
131
+ nestedRunId?: string;
132
+ nestingPath?: NestedRunAddress["path"];
86
133
  message: string;
87
134
  reason?: "idle" | "completion_guard" | "active_long_running" | "tool_failures" | "time_threshold" | "turn_threshold" | "token_threshold";
88
135
  turns?: number;
@@ -98,6 +145,21 @@ export interface ControlEvent {
98
145
  export type SubagentResultStatus = "completed" | "failed" | "paused" | "detached";
99
146
  export type SubagentRunMode = "single" | "parallel" | "chain";
100
147
 
148
+ export type PublicNestedStepSummary = Pick<
149
+ NestedStepSummary,
150
+ "agent" | "status" | "sessionFile" | "activityState" | "lastActivityAt" | "currentTool" | "currentToolStartedAt" | "currentPath" | "turnCount" | "toolCount" | "startedAt" | "endedAt" | "error"
151
+ > & {
152
+ children?: PublicNestedRunSummary[];
153
+ };
154
+
155
+ export type PublicNestedRunSummary = Pick<
156
+ NestedRunSummary,
157
+ "id" | "parentRunId" | "parentStepIndex" | "parentAgent" | "depth" | "path" | "asyncDir" | "sessionId" | "sessionFile" | "intercomTarget" | "ownerIntercomTarget" | "leafIntercomTarget" | "ownerState" | "mode" | "state" | "agent" | "agents" | "currentStep" | "chainStepCount" | "parallelGroups" | "activityState" | "lastActivityAt" | "currentTool" | "currentToolStartedAt" | "currentPath" | "turnCount" | "toolCount" | "totalTokens" | "startedAt" | "endedAt" | "lastUpdate" | "error"
158
+ > & {
159
+ steps?: PublicNestedStepSummary[];
160
+ children?: PublicNestedRunSummary[];
161
+ };
162
+
101
163
  export interface SubagentResultIntercomChild {
102
164
  agent: string;
103
165
  status: SubagentResultStatus;
@@ -106,6 +168,7 @@ export interface SubagentResultIntercomChild {
106
168
  artifactPath?: string;
107
169
  sessionPath?: string;
108
170
  intercomTarget?: string;
171
+ children?: PublicNestedRunSummary[];
109
172
  }
110
173
 
111
174
  export interface SubagentResultIntercomPayload {
@@ -176,6 +239,175 @@ export interface ModelAttempt {
176
239
  usage?: Usage;
177
240
  }
178
241
 
242
+ export type AcceptanceProvenanceLevel = "none" | "attested" | "checked" | "verified" | "reviewed";
243
+
244
+ export type AcceptanceEvidenceKind =
245
+ | "changed-files"
246
+ | "tests-added"
247
+ | "commands-run"
248
+ | "validation-output"
249
+ | "residual-risks"
250
+ | "no-staged-files"
251
+ | "diff-summary"
252
+ | "review-findings"
253
+ | "manual-notes";
254
+
255
+ export interface AcceptanceGate {
256
+ id: string;
257
+ must: string;
258
+ evidence?: AcceptanceEvidenceKind[];
259
+ severity?: "required" | "recommended";
260
+ }
261
+
262
+ export interface AcceptanceVerifyCommand {
263
+ id: string;
264
+ command: string;
265
+ timeoutMs?: number;
266
+ cwd?: string;
267
+ env?: Record<string, string>;
268
+ allowFailure?: boolean;
269
+ }
270
+
271
+ export interface AcceptanceReviewGate {
272
+ agent?: string;
273
+ focus?: string;
274
+ required?: boolean;
275
+ }
276
+
277
+ export interface AcceptanceConfig {
278
+ criteria?: Array<string | AcceptanceGate>;
279
+ evidence?: AcceptanceEvidenceKind[];
280
+ verify?: AcceptanceVerifyCommand[];
281
+ review?: AcceptanceReviewGate;
282
+ stopRules?: string[];
283
+ maxFinalizationTurns?: number;
284
+ }
285
+
286
+ export type AcceptanceInput = AcceptanceConfig;
287
+
288
+ export interface ResolvedAcceptanceGate extends AcceptanceGate {
289
+ id: string;
290
+ must: string;
291
+ evidence: AcceptanceEvidenceKind[];
292
+ severity: "required" | "recommended";
293
+ }
294
+
295
+ export interface ResolvedAcceptanceConfig {
296
+ level: AcceptanceProvenanceLevel;
297
+ explicit: boolean;
298
+ inferredReason: string[];
299
+ criteria: ResolvedAcceptanceGate[];
300
+ evidence: AcceptanceEvidenceKind[];
301
+ verify: AcceptanceVerifyCommand[];
302
+ review?: AcceptanceReviewGate;
303
+ stopRules: string[];
304
+ finalization: {
305
+ mode: "none" | "self-review-loop";
306
+ maxTurns: number;
307
+ };
308
+ }
309
+
310
+ export interface AcceptanceReport {
311
+ criteriaSatisfied?: Array<{
312
+ id?: string;
313
+ status: "satisfied" | "not-satisfied" | "not-applicable";
314
+ evidence: string;
315
+ }>;
316
+ changedFiles?: string[];
317
+ testsAddedOrUpdated?: string[];
318
+ commandsRun?: Array<{
319
+ command: string;
320
+ result: "passed" | "failed" | "not-run";
321
+ summary: string;
322
+ }>;
323
+ validationOutput?: string[];
324
+ residualRisks?: string[];
325
+ noStagedFiles?: boolean;
326
+ diffSummary?: string;
327
+ reviewFindings?: string[];
328
+ manualNotes?: string;
329
+ notes?: string;
330
+ }
331
+
332
+ export type AcceptanceRuntimeCheckStatus = "passed" | "failed" | "not-applicable";
333
+
334
+ export interface AcceptanceRuntimeCheck {
335
+ id: string;
336
+ status: AcceptanceRuntimeCheckStatus;
337
+ message: string;
338
+ }
339
+
340
+ export interface AcceptanceVerifyResult {
341
+ id: string;
342
+ command: string;
343
+ cwd?: string;
344
+ exitCode: number | null;
345
+ status: "passed" | "failed" | "timed-out" | "allowed-failure";
346
+ stdout?: string;
347
+ stderr?: string;
348
+ durationMs: number;
349
+ }
350
+
351
+ export interface AcceptanceReviewResult {
352
+ status: "no-blockers" | "blockers" | "needs-parent-decision";
353
+ findings: Array<{
354
+ severity: "blocker" | "non-blocking";
355
+ file?: string;
356
+ issue: string;
357
+ rationale: string;
358
+ }>;
359
+ }
360
+
361
+ export type AcceptanceLedgerStatus =
362
+ | "not-required"
363
+ | "claimed"
364
+ | "attested"
365
+ | "checked"
366
+ | "verified"
367
+ | "reviewed"
368
+ | "accepted"
369
+ | "rejected";
370
+
371
+ export interface AcceptanceFinalizationTurn {
372
+ turn: number;
373
+ prompt: string;
374
+ status: AcceptanceLedgerStatus;
375
+ rawOutput?: string;
376
+ report?: AcceptanceReport;
377
+ parseError?: string;
378
+ runtimeChecks: AcceptanceRuntimeCheck[];
379
+ verifyRuns: AcceptanceVerifyResult[];
380
+ failureMessage?: string;
381
+ }
382
+
383
+ export interface AcceptanceFinalizationLedger {
384
+ mode: "self-review-loop";
385
+ status: "not-run" | "completed" | "failed";
386
+ maxTurns: number;
387
+ turns: AcceptanceFinalizationTurn[];
388
+ }
389
+
390
+ export interface AcceptanceLedger {
391
+ status: AcceptanceLedgerStatus;
392
+ explicit: boolean;
393
+ effectiveAcceptance: ResolvedAcceptanceConfig;
394
+ inferredReason: string[];
395
+ criteria: ResolvedAcceptanceGate[];
396
+ childReport?: AcceptanceReport;
397
+ childReportParseError?: string;
398
+ initialChildReport?: AcceptanceReport;
399
+ initialChildReportParseError?: string;
400
+ runtimeChecks: AcceptanceRuntimeCheck[];
401
+ verifyRuns: AcceptanceVerifyResult[];
402
+ reviewResult?: AcceptanceReviewResult;
403
+ finalization?: AcceptanceFinalizationLedger;
404
+ parentDecision?: {
405
+ status: "accepted" | "rejected";
406
+ at: string;
407
+ reason?: string;
408
+ };
409
+ }
410
+
179
411
  export interface SingleResult {
180
412
  agent: string;
181
413
  task: string;
@@ -203,6 +435,10 @@ export interface SingleResult {
203
435
  savedOutputPath?: string;
204
436
  outputReference?: SavedOutputReference;
205
437
  outputSaveError?: string;
438
+ structuredOutput?: unknown;
439
+ structuredOutputPath?: string;
440
+ structuredOutputSchemaPath?: string;
441
+ acceptance?: AcceptanceLedger;
206
442
  }
207
443
 
208
444
  export interface Details {
@@ -229,6 +465,8 @@ export interface Details {
229
465
  chainAgents?: string[]; // Agent names in order, e.g., ["scout", "planner"]
230
466
  totalSteps?: number; // Total steps in chain
231
467
  currentStepIndex?: number; // 0-indexed current step (for running chains)
468
+ workflowGraph?: WorkflowGraphSnapshot;
469
+ outputs?: ChainOutputMap;
232
470
  }
233
471
 
234
472
  // ============================================================================
@@ -261,6 +499,76 @@ export interface AsyncParallelGroupStatus {
261
499
  stepIndex: number;
262
500
  }
263
501
 
502
+ export type NestedRunState = "queued" | "running" | "complete" | "failed" | "paused";
503
+ export type NestedOwnerState = "live" | "gone" | "unknown";
504
+
505
+ export interface NestedRunAddress {
506
+ id: string;
507
+ parentRunId: string;
508
+ parentStepIndex?: number;
509
+ parentAgent?: string;
510
+ depth: number;
511
+ path: Array<{ runId: string; stepIndex?: number; agent?: string }>;
512
+ }
513
+
514
+ export interface NestedStepSummary {
515
+ agent: string;
516
+ status: "pending" | "running" | "complete" | "completed" | "failed" | "paused";
517
+ sessionFile?: string;
518
+ activityState?: ActivityState;
519
+ lastActivityAt?: number;
520
+ currentTool?: string;
521
+ currentToolStartedAt?: number;
522
+ currentPath?: string;
523
+ turnCount?: number;
524
+ toolCount?: number;
525
+ startedAt?: number;
526
+ endedAt?: number;
527
+ error?: string;
528
+ children?: NestedRunSummary[];
529
+ }
530
+
531
+ export interface NestedRunSummary extends NestedRunAddress {
532
+ asyncDir?: string;
533
+ pid?: number;
534
+ sessionId?: string;
535
+ sessionFile?: string;
536
+ intercomTarget?: string;
537
+ ownerIntercomTarget?: string;
538
+ leafIntercomTarget?: string;
539
+ ownerState?: NestedOwnerState;
540
+ controlInbox?: string;
541
+ capabilityToken?: string;
542
+ mode?: SubagentRunMode;
543
+ state: NestedRunState;
544
+ agent?: string;
545
+ agents?: string[];
546
+ currentStep?: number;
547
+ chainStepCount?: number;
548
+ parallelGroups?: AsyncParallelGroupStatus[];
549
+ steps?: NestedStepSummary[];
550
+ children?: NestedRunSummary[];
551
+ activityState?: ActivityState;
552
+ lastActivityAt?: number;
553
+ currentTool?: string;
554
+ currentToolStartedAt?: number;
555
+ currentPath?: string;
556
+ turnCount?: number;
557
+ toolCount?: number;
558
+ totalTokens?: TokenUsage;
559
+ startedAt?: number;
560
+ endedAt?: number;
561
+ lastUpdate?: number;
562
+ error?: string;
563
+ }
564
+
565
+ export interface NestedRouteInfo {
566
+ rootRunId: string;
567
+ eventSink: string;
568
+ controlInbox: string;
569
+ capabilityToken: string;
570
+ }
571
+
264
572
  export interface AsyncStartedEvent {
265
573
  id?: string;
266
574
  asyncDir?: string;
@@ -272,6 +580,8 @@ export interface AsyncStartedEvent {
272
580
  chain?: string[];
273
581
  chainStepCount?: number;
274
582
  parallelGroups?: AsyncParallelGroupStatus[];
583
+ workflowGraph?: WorkflowGraphSnapshot;
584
+ nestedRoute?: NestedRouteInfo;
275
585
  }
276
586
 
277
587
  export interface AsyncStatus {
@@ -294,9 +604,15 @@ export interface AsyncStatus {
294
604
  currentStep?: number;
295
605
  chainStepCount?: number;
296
606
  parallelGroups?: AsyncParallelGroupStatus[];
607
+ workflowGraph?: WorkflowGraphSnapshot;
297
608
  steps?: Array<{
298
609
  agent: string;
610
+ phase?: string;
611
+ label?: string;
612
+ outputName?: string;
613
+ structured?: boolean;
299
614
  status: "pending" | "running" | "complete" | "completed" | "failed" | "paused";
615
+ children?: NestedRunSummary[];
300
616
  sessionFile?: string;
301
617
  activityState?: ActivityState;
302
618
  lastActivityAt?: number;
@@ -319,11 +635,16 @@ export interface AsyncStatus {
319
635
  attemptedModels?: string[];
320
636
  modelAttempts?: ModelAttempt[];
321
637
  error?: string;
638
+ structuredOutput?: unknown;
639
+ structuredOutputPath?: string;
640
+ structuredOutputSchemaPath?: string;
641
+ acceptance?: AcceptanceLedger;
322
642
  }>;
323
643
  sessionDir?: string;
324
644
  outputFile?: string;
325
645
  totalTokens?: TokenUsage;
326
646
  sessionFile?: string;
647
+ outputs?: ChainOutputMap;
327
648
  }
328
649
 
329
650
  export type AsyncJobStep = NonNullable<AsyncStatus["steps"]>[number] & {
@@ -361,6 +682,8 @@ export interface AsyncJobState {
361
682
  totalTokens?: TokenUsage;
362
683
  sessionFile?: string;
363
684
  controlEventCursor?: number;
685
+ nestedRoute?: NestedRouteInfo;
686
+ nestedChildren?: NestedRunSummary[];
364
687
  }
365
688
 
366
689
  export interface ForegroundResumeChild {
@@ -398,6 +721,8 @@ export interface SubagentState {
398
721
  turnCount?: number;
399
722
  tokens?: number;
400
723
  toolCount?: number;
724
+ nestedRoute?: NestedRouteInfo;
725
+ nestedChildren?: NestedRunSummary[];
401
726
  interrupt?: () => boolean;
402
727
  }>;
403
728
  lastForegroundControlId: string | null;
@@ -473,6 +798,7 @@ export interface RunSyncOptions {
473
798
  outputPath?: string;
474
799
  outputMode?: OutputMode;
475
800
  maxSubagentDepth?: number;
801
+ nestedRoute?: NestedRouteInfo;
476
802
  /** Override the agent's default model (format: "provider/id" or just "id") */
477
803
  modelOverride?: string;
478
804
  /** Registry models available for heuristic bare-model resolution */
@@ -481,6 +807,18 @@ export interface RunSyncOptions {
481
807
  preferredModelProvider?: string;
482
808
  /** Skills to inject (overrides agent default if provided) */
483
809
  skills?: string[];
810
+ structuredOutput?: {
811
+ schema: JsonSchemaObject;
812
+ schemaPath: string;
813
+ outputPath: string;
814
+ };
815
+ acceptance?: AcceptanceInput;
816
+ acceptanceContext?: {
817
+ mode?: SubagentRunMode;
818
+ async?: boolean;
819
+ dynamic?: boolean;
820
+ dynamicGroup?: boolean;
821
+ };
484
822
  }
485
823
 
486
824
  export type IntercomBridgeMode = "off" | "fork-only" | "always";
@@ -495,6 +833,12 @@ interface TopLevelParallelConfig {
495
833
  concurrency?: number;
496
834
  }
497
835
 
836
+ interface ExtensionChainConfig {
837
+ dynamicFanout?: {
838
+ maxItems?: number;
839
+ };
840
+ }
841
+
498
842
  export interface ExtensionConfig {
499
843
  asyncByDefault?: boolean;
500
844
  forceTopLevelAsync?: boolean;
@@ -502,6 +846,7 @@ export interface ExtensionConfig {
502
846
  maxSubagentDepth?: number;
503
847
  control?: ControlConfig;
504
848
  parallel?: TopLevelParallelConfig;
849
+ chain?: ExtensionChainConfig;
505
850
  worktreeSetupHook?: string;
506
851
  worktreeSetupHookTimeoutMs?: number;
507
852
  intercomBridge?: IntercomBridgeConfig;
@@ -5,7 +5,8 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
5
5
  import { Key, matchesKey } from "@earendil-works/pi-tui";
6
6
  import { discoverAgents, discoverAgentsAll, type ChainConfig } from "../agents/agents.ts";
7
7
  import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
8
- import { isParallelStep, type ChainStep } from "../shared/settings.ts";
8
+ import { isDynamicParallelStep, isParallelStep, type ChainStep } from "../shared/settings.ts";
9
+ import { assertJsonSchemaObject } from "../runs/shared/structured-output.ts";
9
10
  import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
10
11
  import {
11
12
  applySlashUpdate,
@@ -20,6 +21,7 @@ import {
20
21
  SLASH_SUBAGENT_RESPONSE_EVENT,
21
22
  SLASH_SUBAGENT_STARTED_EVENT,
22
23
  SLASH_SUBAGENT_UPDATE_EVENT,
24
+ type JsonSchemaObject,
23
25
  type SingleResult,
24
26
  type SubagentState,
25
27
  } from "../shared/types.ts";
@@ -123,12 +125,48 @@ const makeChainCompletions = (state: SubagentState) => (prefix: string) => {
123
125
  .map((chain) => ({ value: chain.name, label: chain.name }));
124
126
  };
125
127
 
128
+ function loadSavedOutputSchema(chain: ChainConfig, stepAgent: string, outputSchema: unknown): JsonSchemaObject | undefined {
129
+ if (outputSchema === undefined) return undefined;
130
+ if (typeof outputSchema === "string") {
131
+ const schemaPath = path.isAbsolute(outputSchema)
132
+ ? outputSchema
133
+ : path.join(path.dirname(chain.filePath), outputSchema);
134
+ const parsed = JSON.parse(fs.readFileSync(schemaPath, "utf-8")) as unknown;
135
+ assertJsonSchemaObject(parsed, `outputSchema for chain '${chain.name}' step '${stepAgent}' (${schemaPath})`);
136
+ return parsed;
137
+ }
138
+ assertJsonSchemaObject(outputSchema, `outputSchema for chain '${chain.name}' step '${stepAgent}'`);
139
+ return outputSchema;
140
+ }
141
+
126
142
  const mapSavedChainSteps = (chain: ChainConfig, worktree = false): ChainStep[] => {
127
- return (chain.steps as Array<ChainStep & { skills?: string[] | false }>).map((step) => {
128
- if (isParallelStep(step)) return worktree ? { ...step, worktree: true } : { ...step };
143
+ return (chain.steps as unknown as Array<ChainStep & { skills?: string[] | false }>).map((step) => {
144
+ if (isParallelStep(step)) {
145
+ const parallel = step.parallel.map((task) => {
146
+ const { outputSchema: rawOutputSchema, ...rest } = task as typeof task & { outputSchema?: unknown };
147
+ const outputSchema = loadSavedOutputSchema(chain, task.agent, rawOutputSchema);
148
+ return { ...rest, ...(outputSchema ? { outputSchema } : {}) };
149
+ });
150
+ return { ...step, parallel, ...(worktree ? { worktree: true } : {}) };
151
+ }
152
+ if (isDynamicParallelStep(step)) {
153
+ const { outputSchema: rawOutputSchema, ...parallelRest } = step.parallel as typeof step.parallel & { outputSchema?: unknown };
154
+ const outputSchema = loadSavedOutputSchema(chain, step.parallel.agent, rawOutputSchema);
155
+ const collectSchema = loadSavedOutputSchema(chain, `${step.collect.as} collection`, step.collect.outputSchema);
156
+ return {
157
+ ...step,
158
+ parallel: { ...parallelRest, ...(outputSchema ? { outputSchema } : {}) },
159
+ collect: { ...step.collect, ...(collectSchema ? { outputSchema: collectSchema } : {}) },
160
+ };
161
+ }
162
+ const outputSchema = loadSavedOutputSchema(chain, step.agent, (step as { outputSchema?: unknown }).outputSchema);
129
163
  return {
130
164
  agent: step.agent,
131
165
  task: step.task || undefined,
166
+ ...(step.phase ? { phase: step.phase } : {}),
167
+ ...(step.label ? { label: step.label } : {}),
168
+ ...(step.as ? { as: step.as } : {}),
169
+ ...(outputSchema ? { outputSchema } : {}),
132
170
  output: step.output,
133
171
  outputMode: step.outputMode,
134
172
  reads: step.reads,