pi-subagents 0.25.0 → 0.28.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 (40) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +175 -19
  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/skills/pi-subagents/SKILL.md +60 -17
  7. package/src/agents/agent-management.ts +71 -15
  8. package/src/agents/agent-serializer.ts +13 -2
  9. package/src/agents/agents.ts +88 -17
  10. package/src/agents/chain-serializer.ts +120 -0
  11. package/src/extension/fanout-child.ts +2 -0
  12. package/src/extension/index.ts +5 -2
  13. package/src/extension/schemas.ts +132 -6
  14. package/src/intercom/result-intercom.ts +5 -0
  15. package/src/runs/background/async-execution.ts +88 -6
  16. package/src/runs/background/async-status.ts +11 -1
  17. package/src/runs/background/run-status.ts +10 -1
  18. package/src/runs/background/subagent-runner.ts +665 -39
  19. package/src/runs/foreground/chain-execution.ts +369 -118
  20. package/src/runs/foreground/execution.ts +392 -19
  21. package/src/runs/foreground/subagent-executor.ts +126 -3
  22. package/src/runs/shared/acceptance-contract.ts +318 -0
  23. package/src/runs/shared/acceptance-evaluation.ts +221 -0
  24. package/src/runs/shared/acceptance-finalization.ts +173 -0
  25. package/src/runs/shared/acceptance-reports.ts +127 -0
  26. package/src/runs/shared/acceptance.ts +22 -0
  27. package/src/runs/shared/chain-outputs.ts +101 -0
  28. package/src/runs/shared/completion-guard.ts +26 -3
  29. package/src/runs/shared/dynamic-fanout.ts +293 -0
  30. package/src/runs/shared/parallel-utils.ts +33 -1
  31. package/src/runs/shared/pi-args.ts +11 -0
  32. package/src/runs/shared/structured-output.ts +77 -0
  33. package/src/runs/shared/subagent-prompt-runtime.ts +53 -3
  34. package/src/runs/shared/workflow-graph.ts +210 -0
  35. package/src/shared/formatters.ts +2 -2
  36. package/src/shared/settings.ts +53 -4
  37. package/src/shared/types.ts +265 -1
  38. package/src/shared/utils.ts +7 -0
  39. package/src/slash/slash-commands.ts +41 -3
  40. package/src/tui/render.ts +178 -45
@@ -0,0 +1,210 @@
1
+ import { isDynamicParallelStep, isParallelStep, type ChainStep, type SequentialStep } from "../../shared/settings.ts";
2
+ import type { SingleResult, SubagentRunMode, WorkflowGraphNode, WorkflowGraphSnapshot, WorkflowNodeStatus } from "../../shared/types.ts";
3
+
4
+ export interface WorkflowGraphBuildInput {
5
+ runId: string;
6
+ mode?: SubagentRunMode;
7
+ steps: ChainStep[];
8
+ results?: Array<Pick<SingleResult, "exitCode" | "detached" | "interrupted" | "timedOut" | "error" | "acceptance">>;
9
+ currentFlatIndex?: number;
10
+ currentStepIndex?: number;
11
+ stepStatuses?: Array<{ status?: string; error?: string }>;
12
+ dynamicChildren?: Record<number, Array<{ agent: string; label?: string; flatIndex: number; itemKey: string; outputName?: string; structured?: boolean; error?: string }>>;
13
+ dynamicGroupStatuses?: Record<number, { status: WorkflowNodeStatus; error?: string; acceptance?: SingleResult["acceptance"] }>;
14
+ }
15
+
16
+ function normalizeStatus(status: string | undefined): WorkflowNodeStatus | undefined {
17
+ switch (status) {
18
+ case "complete":
19
+ case "completed":
20
+ return "completed";
21
+ case "running":
22
+ return "running";
23
+ case "failed":
24
+ return "failed";
25
+ case "paused":
26
+ return "paused";
27
+ case "detached":
28
+ return "detached";
29
+ case "timed-out":
30
+ return "timed-out";
31
+ case "pending":
32
+ return "pending";
33
+ default:
34
+ return undefined;
35
+ }
36
+ }
37
+
38
+ function resultStatus(result: Pick<SingleResult, "exitCode" | "detached" | "interrupted" | "timedOut"> | undefined): WorkflowNodeStatus | undefined {
39
+ if (!result) return undefined;
40
+ if (result.detached) return "detached";
41
+ if (result.timedOut) return "timed-out";
42
+ if (result.interrupted) return "paused";
43
+ return result.exitCode === 0 ? "completed" : "failed";
44
+ }
45
+
46
+ function nodeStatus(input: WorkflowGraphBuildInput, flatIndex: number): WorkflowNodeStatus {
47
+ return normalizeStatus(input.stepStatuses?.[flatIndex]?.status)
48
+ ?? resultStatus(input.results?.[flatIndex])
49
+ ?? (input.currentFlatIndex === flatIndex ? "running" : "pending");
50
+ }
51
+
52
+ function pushPhase(phases: WorkflowGraphSnapshot["phases"], phase: string | undefined, nodeId: string): void {
53
+ if (!phase) return;
54
+ let group = phases.find((candidate) => candidate.title === phase);
55
+ if (!group) {
56
+ group = { title: phase, nodeIds: [] };
57
+ phases.push(group);
58
+ }
59
+ group.nodeIds.push(nodeId);
60
+ }
61
+
62
+ function seqLabel(step: SequentialStep, stepIndex: number): string {
63
+ return step.label?.trim() || step.agent || `Step ${stepIndex + 1}`;
64
+ }
65
+
66
+ function summarizeParallelStatuses(statuses: WorkflowNodeStatus[]): WorkflowNodeStatus {
67
+ if (statuses.some((status) => status === "running")) return "running";
68
+ if (statuses.some((status) => status === "failed")) return "failed";
69
+ if (statuses.some((status) => status === "timed-out")) return "timed-out";
70
+ if (statuses.some((status) => status === "paused")) return "paused";
71
+ if (statuses.some((status) => status === "detached")) return "detached";
72
+ if (statuses.length > 0 && statuses.every((status) => status === "completed")) return "completed";
73
+ if (statuses.some((status) => status === "completed")) return "running";
74
+ return "pending";
75
+ }
76
+
77
+ export function buildWorkflowGraphSnapshot(input: WorkflowGraphBuildInput): WorkflowGraphSnapshot {
78
+ const nodes: WorkflowGraphNode[] = [];
79
+ const phases: WorkflowGraphSnapshot["phases"] = [];
80
+ let flatIndex = 0;
81
+ let currentNodeId: string | undefined;
82
+
83
+ for (let stepIndex = 0; stepIndex < input.steps.length; stepIndex++) {
84
+ const step = input.steps[stepIndex]!;
85
+ if (isParallelStep(step)) {
86
+ const groupId = `step-${stepIndex}`;
87
+ const children: WorkflowGraphNode[] = [];
88
+ const childStatuses: WorkflowNodeStatus[] = [];
89
+ for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
90
+ const task = step.parallel[taskIndex]!;
91
+ const status = nodeStatus(input, flatIndex);
92
+ childStatuses.push(status);
93
+ const childId = `step-${stepIndex}-agent-${taskIndex}`;
94
+ const child: WorkflowGraphNode = {
95
+ id: childId,
96
+ kind: "agent",
97
+ agent: task.agent,
98
+ phase: task.phase,
99
+ label: task.label?.trim() || task.agent || `Agent ${taskIndex + 1}`,
100
+ status,
101
+ flatIndex,
102
+ stepIndex,
103
+ outputName: task.as,
104
+ structured: Boolean(task.outputSchema),
105
+ acceptanceStatus: input.results?.[flatIndex]?.acceptance?.status,
106
+ error: input.stepStatuses?.[flatIndex]?.error ?? input.results?.[flatIndex]?.error,
107
+ };
108
+ children.push(child);
109
+ pushPhase(phases, task.phase, childId);
110
+ if (status === "running" || input.currentFlatIndex === flatIndex) currentNodeId = childId;
111
+ flatIndex++;
112
+ }
113
+ const groupStatus = summarizeParallelStatuses(childStatuses);
114
+ if (input.currentStepIndex === stepIndex && !currentNodeId) currentNodeId = groupId;
115
+ nodes.push({
116
+ id: groupId,
117
+ kind: "parallel-group",
118
+ label: step.parallel.length === 1 ? "Parallel task" : `Parallel group (${step.parallel.length})`,
119
+ status: groupStatus,
120
+ stepIndex,
121
+ children,
122
+ });
123
+ continue;
124
+ }
125
+
126
+ if (isDynamicParallelStep(step)) {
127
+ const groupId = `step-${stepIndex}`;
128
+ const materialized = input.dynamicChildren?.[stepIndex] ?? [];
129
+ const groupOverride = input.dynamicGroupStatuses?.[stepIndex];
130
+ const children: WorkflowGraphNode[] = [];
131
+ const childStatuses: WorkflowNodeStatus[] = [];
132
+ for (let taskIndex = 0; taskIndex < materialized.length; taskIndex++) {
133
+ const task = materialized[taskIndex]!;
134
+ const status = nodeStatus(input, task.flatIndex);
135
+ childStatuses.push(status);
136
+ const childId = `step-${stepIndex}-item-${task.itemKey.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
137
+ const child: WorkflowGraphNode = {
138
+ id: childId,
139
+ kind: "agent",
140
+ agent: task.agent,
141
+ phase: step.parallel.phase ?? step.phase,
142
+ label: task.label?.trim() || step.parallel.label?.trim() || `${task.agent} ${task.itemKey}`,
143
+ status,
144
+ flatIndex: task.flatIndex,
145
+ stepIndex,
146
+ itemKey: task.itemKey,
147
+ outputName: task.outputName,
148
+ structured: task.structured,
149
+ acceptanceStatus: input.results?.[task.flatIndex]?.acceptance?.status,
150
+ error: input.stepStatuses?.[task.flatIndex]?.error ?? input.results?.[task.flatIndex]?.error ?? task.error,
151
+ };
152
+ children.push(child);
153
+ pushPhase(phases, child.phase, childId);
154
+ if (status === "running" || input.currentFlatIndex === task.flatIndex) currentNodeId = childId;
155
+ }
156
+ const groupStatus = groupOverride?.status ?? (children.length > 0 ? summarizeParallelStatuses(childStatuses) : (input.currentStepIndex === stepIndex ? "running" : "pending"));
157
+ if (input.currentStepIndex === stepIndex && !currentNodeId) currentNodeId = groupId;
158
+ nodes.push({
159
+ id: groupId,
160
+ kind: "dynamic-parallel-group",
161
+ label: step.label?.trim() || step.parallel.label?.trim() || `Dynamic fanout (${step.collect.as})`,
162
+ status: groupStatus,
163
+ stepIndex,
164
+ outputName: step.collect.as,
165
+ structured: Boolean(step.collect.outputSchema),
166
+ acceptanceStatus: groupOverride?.acceptance?.status,
167
+ error: groupOverride?.error,
168
+ dynamic: {
169
+ sourceOutput: step.expand.from.output,
170
+ sourcePath: step.expand.from.path,
171
+ itemName: step.expand.item ?? "item",
172
+ maxItems: step.expand.maxItems,
173
+ collectAs: step.collect.as,
174
+ },
175
+ children,
176
+ });
177
+ if (materialized.length > 0) flatIndex = Math.max(flatIndex, ...materialized.map((child) => child.flatIndex + 1));
178
+ continue;
179
+ }
180
+
181
+ const seq = step as SequentialStep;
182
+ const status = nodeStatus(input, flatIndex);
183
+ const id = `step-${stepIndex}`;
184
+ nodes.push({
185
+ id,
186
+ kind: "step",
187
+ agent: seq.agent,
188
+ phase: seq.phase,
189
+ label: seqLabel(seq, stepIndex),
190
+ status,
191
+ flatIndex,
192
+ stepIndex,
193
+ outputName: seq.as,
194
+ structured: Boolean(seq.outputSchema),
195
+ acceptanceStatus: input.results?.[flatIndex]?.acceptance?.status,
196
+ error: input.stepStatuses?.[flatIndex]?.error ?? input.results?.[flatIndex]?.error,
197
+ });
198
+ pushPhase(phases, seq.phase, id);
199
+ if (status === "running" || input.currentFlatIndex === flatIndex || input.currentStepIndex === stepIndex) currentNodeId = id;
200
+ flatIndex++;
201
+ }
202
+
203
+ return {
204
+ runId: input.runId,
205
+ mode: input.mode ?? "chain",
206
+ phases,
207
+ nodes,
208
+ currentNodeId,
209
+ };
210
+ }
@@ -6,7 +6,7 @@ import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
7
  import type { Usage, SingleResult } from "./types.ts";
8
8
  import type { ChainStep } from "./settings.ts";
9
- import { isParallelStep } from "./settings.ts";
9
+ import { isDynamicParallelStep, isParallelStep } from "./settings.ts";
10
10
  import { splitKnownThinkingSuffix, THINKING_LEVELS } from "./model-info.ts";
11
11
 
12
12
  /**
@@ -63,7 +63,7 @@ export function buildChainSummary(
63
63
  failedStep?: { index: number; error: string },
64
64
  ): string {
65
65
  const stepNames = steps
66
- .map((step) => (isParallelStep(step) ? `parallel[${step.parallel.length}]` : step.agent))
66
+ .map((step) => (isParallelStep(step) ? `parallel[${step.parallel.length}]` : isDynamicParallelStep(step) ? `expand:${step.parallel.agent}` : step.agent))
67
67
  .join(" → ");
68
68
 
69
69
  const totalDuration = results.reduce((sum, r) => sum + (r.progress?.durationMs || 0), 0);
@@ -6,7 +6,7 @@ import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
7
  import type { AgentConfig } from "../agents/agents.ts";
8
8
  import { normalizeSkillInput } from "../agents/skills.ts";
9
- import { CHAIN_RUNS_DIR, type OutputMode } from "./types.ts";
9
+ import { CHAIN_RUNS_DIR, type AcceptanceInput, type JsonSchemaObject, type OutputMode } from "./types.ts";
10
10
  const CHAIN_DIR_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
11
11
  const INITIAL_PROGRESS_CONTENT = "# Progress\n\n## Status\nIn Progress\n\n## Tasks\n\n## Files Changed\n\n## Notes\n";
12
12
 
@@ -44,6 +44,10 @@ function normalizeOutputOverride(output: string | false | undefined): string | f
44
44
  export interface SequentialStep {
45
45
  agent: string;
46
46
  task?: string;
47
+ phase?: string;
48
+ label?: string;
49
+ as?: string;
50
+ outputSchema?: JsonSchemaObject;
47
51
  cwd?: string;
48
52
  output?: string | false;
49
53
  outputMode?: OutputMode;
@@ -51,12 +55,17 @@ export interface SequentialStep {
51
55
  progress?: boolean;
52
56
  skill?: string | string[] | false;
53
57
  model?: string;
58
+ acceptance?: AcceptanceInput;
54
59
  }
55
60
 
56
61
  /** Parallel task item within a parallel step */
57
- interface ParallelTaskItem {
62
+ export interface ParallelTaskItem {
58
63
  agent: string;
59
64
  task?: string;
65
+ phase?: string;
66
+ label?: string;
67
+ as?: string;
68
+ outputSchema?: JsonSchemaObject;
60
69
  cwd?: string;
61
70
  count?: number;
62
71
  output?: string | false;
@@ -65,18 +74,48 @@ interface ParallelTaskItem {
65
74
  progress?: boolean;
66
75
  skill?: string | string[] | false;
67
76
  model?: string;
77
+ acceptance?: AcceptanceInput;
78
+ }
79
+
80
+ export interface DynamicExpandSpec {
81
+ from: {
82
+ output: string;
83
+ path: string;
84
+ };
85
+ item?: string;
86
+ key?: string;
87
+ maxItems?: number;
88
+ onEmpty?: "skip" | "fail";
89
+ }
90
+
91
+ export type DynamicParallelTemplate = Omit<ParallelTaskItem, "as" | "count">;
92
+
93
+ export interface DynamicCollectSpec {
94
+ as: string;
95
+ outputSchema?: JsonSchemaObject;
96
+ }
97
+
98
+ export interface DynamicParallelStep {
99
+ expand: DynamicExpandSpec;
100
+ parallel: DynamicParallelTemplate;
101
+ collect: DynamicCollectSpec;
102
+ concurrency?: number;
103
+ failFast?: boolean;
104
+ phase?: string;
105
+ label?: string;
68
106
  }
69
107
 
70
108
  /** Parallel step: multiple agents running concurrently */
71
- interface ParallelStep {
109
+ export interface ParallelStep {
72
110
  parallel: ParallelTaskItem[];
73
111
  concurrency?: number;
74
112
  failFast?: boolean;
75
113
  worktree?: boolean;
114
+ cwd?: string;
76
115
  }
77
116
 
78
117
  /** Union type for chain steps */
79
- export type ChainStep = SequentialStep | ParallelStep;
118
+ export type ChainStep = SequentialStep | ParallelStep | DynamicParallelStep;
80
119
 
81
120
  // =============================================================================
82
121
  // Type Guards
@@ -86,11 +125,18 @@ export function isParallelStep(step: ChainStep): step is ParallelStep {
86
125
  return "parallel" in step && Array.isArray((step as ParallelStep).parallel);
87
126
  }
88
127
 
128
+ export function isDynamicParallelStep(step: ChainStep): step is DynamicParallelStep {
129
+ return "expand" in step && "collect" in step && "parallel" in step && !Array.isArray((step as { parallel?: unknown }).parallel);
130
+ }
131
+
89
132
  /** Get all agent names in a step (single for sequential, multiple for parallel) */
90
133
  export function getStepAgents(step: ChainStep): string[] {
91
134
  if (isParallelStep(step)) {
92
135
  return step.parallel.map((t) => t.agent);
93
136
  }
137
+ if (isDynamicParallelStep(step)) {
138
+ return [step.parallel.agent];
139
+ }
94
140
  return [step.agent];
95
141
  }
96
142
 
@@ -160,6 +206,9 @@ export function resolveChainTemplates(
160
206
  return "{previous}";
161
207
  });
162
208
  }
209
+ if (isDynamicParallelStep(step)) {
210
+ return step.parallel.task ?? "{previous}";
211
+ }
163
212
  // Sequential step: existing logic
164
213
  const seq = step as SequentialStep;
165
214
  if (seq.task) return seq.task;
@@ -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" | "timed-out";
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;
@@ -97,7 +142,7 @@ export interface ControlEvent {
97
142
  recentFailureSummary?: string;
98
143
  }
99
144
 
100
- export type SubagentResultStatus = "completed" | "failed" | "paused" | "detached";
145
+ export type SubagentResultStatus = "completed" | "failed" | "paused" | "detached" | "timed-out";
101
146
  export type SubagentRunMode = "single" | "parallel" | "chain";
102
147
 
103
148
  export type PublicNestedStepSummary = Pick<
@@ -194,6 +239,182 @@ export interface ModelAttempt {
194
239
  usage?: Usage;
195
240
  }
196
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
+
411
+ export interface ResourceLimitExceeded {
412
+ kind: "maxExecutionTimeMs" | "maxTokens";
413
+ limit: number;
414
+ observed?: number;
415
+ message: string;
416
+ }
417
+
197
418
  export interface SingleResult {
198
419
  agent: string;
199
420
  task: string;
@@ -201,6 +422,8 @@ export interface SingleResult {
201
422
  detached?: boolean;
202
423
  detachedReason?: string;
203
424
  interrupted?: boolean;
425
+ timedOut?: boolean;
426
+ resourceLimitExceeded?: ResourceLimitExceeded;
204
427
  messages?: Message[];
205
428
  usage: Usage;
206
429
  model?: string;
@@ -221,6 +444,10 @@ export interface SingleResult {
221
444
  savedOutputPath?: string;
222
445
  outputReference?: SavedOutputReference;
223
446
  outputSaveError?: string;
447
+ structuredOutput?: unknown;
448
+ structuredOutputPath?: string;
449
+ structuredOutputSchemaPath?: string;
450
+ acceptance?: AcceptanceLedger;
224
451
  }
225
452
 
226
453
  export interface Details {
@@ -247,6 +474,8 @@ export interface Details {
247
474
  chainAgents?: string[]; // Agent names in order, e.g., ["scout", "planner"]
248
475
  totalSteps?: number; // Total steps in chain
249
476
  currentStepIndex?: number; // 0-indexed current step (for running chains)
477
+ workflowGraph?: WorkflowGraphSnapshot;
478
+ outputs?: ChainOutputMap;
250
479
  }
251
480
 
252
481
  // ============================================================================
@@ -360,6 +589,7 @@ export interface AsyncStartedEvent {
360
589
  chain?: string[];
361
590
  chainStepCount?: number;
362
591
  parallelGroups?: AsyncParallelGroupStatus[];
592
+ workflowGraph?: WorkflowGraphSnapshot;
363
593
  nestedRoute?: NestedRouteInfo;
364
594
  }
365
595
 
@@ -383,8 +613,13 @@ export interface AsyncStatus {
383
613
  currentStep?: number;
384
614
  chainStepCount?: number;
385
615
  parallelGroups?: AsyncParallelGroupStatus[];
616
+ workflowGraph?: WorkflowGraphSnapshot;
386
617
  steps?: Array<{
387
618
  agent: string;
619
+ phase?: string;
620
+ label?: string;
621
+ outputName?: string;
622
+ structured?: boolean;
388
623
  status: "pending" | "running" | "complete" | "completed" | "failed" | "paused";
389
624
  children?: NestedRunSummary[];
390
625
  sessionFile?: string;
@@ -409,11 +644,17 @@ export interface AsyncStatus {
409
644
  attemptedModels?: string[];
410
645
  modelAttempts?: ModelAttempt[];
411
646
  error?: string;
647
+ structuredOutput?: unknown;
648
+ structuredOutputPath?: string;
649
+ structuredOutputSchemaPath?: string;
650
+ acceptance?: AcceptanceLedger;
651
+ resourceLimitExceeded?: ResourceLimitExceeded;
412
652
  }>;
413
653
  sessionDir?: string;
414
654
  outputFile?: string;
415
655
  totalTokens?: TokenUsage;
416
656
  sessionFile?: string;
657
+ outputs?: ChainOutputMap;
417
658
  }
418
659
 
419
660
  export type AsyncJobStep = NonNullable<AsyncStatus["steps"]>[number] & {
@@ -549,6 +790,8 @@ export interface RunSyncOptions {
549
790
  cwd?: string;
550
791
  signal?: AbortSignal;
551
792
  interruptSignal?: AbortSignal;
793
+ timeoutMs?: number;
794
+ timeoutAt?: number;
552
795
  allowIntercomDetach?: boolean;
553
796
  intercomEvents?: IntercomEventBus;
554
797
  onUpdate?: (r: import("@earendil-works/pi-agent-core").AgentToolResult<Details>) => void;
@@ -567,6 +810,8 @@ export interface RunSyncOptions {
567
810
  outputPath?: string;
568
811
  outputMode?: OutputMode;
569
812
  maxSubagentDepth?: number;
813
+ maxExecutionTimeMs?: number;
814
+ maxTokens?: number;
570
815
  nestedRoute?: NestedRouteInfo;
571
816
  /** Override the agent's default model (format: "provider/id" or just "id") */
572
817
  modelOverride?: string;
@@ -576,6 +821,18 @@ export interface RunSyncOptions {
576
821
  preferredModelProvider?: string;
577
822
  /** Skills to inject (overrides agent default if provided) */
578
823
  skills?: string[];
824
+ structuredOutput?: {
825
+ schema: JsonSchemaObject;
826
+ schemaPath: string;
827
+ outputPath: string;
828
+ };
829
+ acceptance?: AcceptanceInput;
830
+ acceptanceContext?: {
831
+ mode?: SubagentRunMode;
832
+ async?: boolean;
833
+ dynamic?: boolean;
834
+ dynamicGroup?: boolean;
835
+ };
579
836
  }
580
837
 
581
838
  export type IntercomBridgeMode = "off" | "fork-only" | "always";
@@ -590,6 +847,12 @@ interface TopLevelParallelConfig {
590
847
  concurrency?: number;
591
848
  }
592
849
 
850
+ interface ExtensionChainConfig {
851
+ dynamicFanout?: {
852
+ maxItems?: number;
853
+ };
854
+ }
855
+
593
856
  export interface ExtensionConfig {
594
857
  asyncByDefault?: boolean;
595
858
  forceTopLevelAsync?: boolean;
@@ -597,6 +860,7 @@ export interface ExtensionConfig {
597
860
  maxSubagentDepth?: number;
598
861
  control?: ControlConfig;
599
862
  parallel?: TopLevelParallelConfig;
863
+ chain?: ExtensionChainConfig;
600
864
  worktreeSetupHook?: string;
601
865
  worktreeSetupHookTimeoutMs?: number;
602
866
  intercomBridge?: IntercomBridgeConfig;