pi-subagents 0.25.0 → 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 (38) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +129 -17
  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 +32 -17
  7. package/src/agents/agent-management.ts +57 -15
  8. package/src/agents/agent-serializer.ts +3 -2
  9. package/src/agents/agents.ts +47 -16
  10. package/src/agents/chain-serializer.ts +120 -0
  11. package/src/extension/fanout-child.ts +1 -0
  12. package/src/extension/index.ts +1 -0
  13. package/src/extension/schemas.ts +138 -5
  14. package/src/runs/background/async-execution.ts +84 -6
  15. package/src/runs/background/async-status.ts +11 -1
  16. package/src/runs/background/run-status.ts +10 -1
  17. package/src/runs/background/subagent-runner.ts +600 -31
  18. package/src/runs/foreground/chain-execution.ts +325 -118
  19. package/src/runs/foreground/execution.ts +222 -10
  20. package/src/runs/foreground/subagent-executor.ts +67 -0
  21. package/src/runs/shared/acceptance-contract.ts +291 -0
  22. package/src/runs/shared/acceptance-evaluation.ts +221 -0
  23. package/src/runs/shared/acceptance-finalization.ts +161 -0
  24. package/src/runs/shared/acceptance-reports.ts +127 -0
  25. package/src/runs/shared/acceptance.ts +22 -0
  26. package/src/runs/shared/chain-outputs.ts +101 -0
  27. package/src/runs/shared/completion-guard.ts +26 -3
  28. package/src/runs/shared/dynamic-fanout.ts +293 -0
  29. package/src/runs/shared/parallel-utils.ts +31 -1
  30. package/src/runs/shared/pi-args.ts +11 -0
  31. package/src/runs/shared/structured-output.ts +77 -0
  32. package/src/runs/shared/subagent-prompt-runtime.ts +53 -3
  33. package/src/runs/shared/workflow-graph.ts +206 -0
  34. package/src/shared/formatters.ts +2 -2
  35. package/src/shared/settings.ts +53 -4
  36. package/src/shared/types.ts +250 -0
  37. package/src/slash/slash-commands.ts +41 -3
  38. package/src/tui/render.ts +162 -34
@@ -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;
@@ -194,6 +239,175 @@ 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
+
197
411
  export interface SingleResult {
198
412
  agent: string;
199
413
  task: string;
@@ -221,6 +435,10 @@ export interface SingleResult {
221
435
  savedOutputPath?: string;
222
436
  outputReference?: SavedOutputReference;
223
437
  outputSaveError?: string;
438
+ structuredOutput?: unknown;
439
+ structuredOutputPath?: string;
440
+ structuredOutputSchemaPath?: string;
441
+ acceptance?: AcceptanceLedger;
224
442
  }
225
443
 
226
444
  export interface Details {
@@ -247,6 +465,8 @@ export interface Details {
247
465
  chainAgents?: string[]; // Agent names in order, e.g., ["scout", "planner"]
248
466
  totalSteps?: number; // Total steps in chain
249
467
  currentStepIndex?: number; // 0-indexed current step (for running chains)
468
+ workflowGraph?: WorkflowGraphSnapshot;
469
+ outputs?: ChainOutputMap;
250
470
  }
251
471
 
252
472
  // ============================================================================
@@ -360,6 +580,7 @@ export interface AsyncStartedEvent {
360
580
  chain?: string[];
361
581
  chainStepCount?: number;
362
582
  parallelGroups?: AsyncParallelGroupStatus[];
583
+ workflowGraph?: WorkflowGraphSnapshot;
363
584
  nestedRoute?: NestedRouteInfo;
364
585
  }
365
586
 
@@ -383,8 +604,13 @@ export interface AsyncStatus {
383
604
  currentStep?: number;
384
605
  chainStepCount?: number;
385
606
  parallelGroups?: AsyncParallelGroupStatus[];
607
+ workflowGraph?: WorkflowGraphSnapshot;
386
608
  steps?: Array<{
387
609
  agent: string;
610
+ phase?: string;
611
+ label?: string;
612
+ outputName?: string;
613
+ structured?: boolean;
388
614
  status: "pending" | "running" | "complete" | "completed" | "failed" | "paused";
389
615
  children?: NestedRunSummary[];
390
616
  sessionFile?: string;
@@ -409,11 +635,16 @@ export interface AsyncStatus {
409
635
  attemptedModels?: string[];
410
636
  modelAttempts?: ModelAttempt[];
411
637
  error?: string;
638
+ structuredOutput?: unknown;
639
+ structuredOutputPath?: string;
640
+ structuredOutputSchemaPath?: string;
641
+ acceptance?: AcceptanceLedger;
412
642
  }>;
413
643
  sessionDir?: string;
414
644
  outputFile?: string;
415
645
  totalTokens?: TokenUsage;
416
646
  sessionFile?: string;
647
+ outputs?: ChainOutputMap;
417
648
  }
418
649
 
419
650
  export type AsyncJobStep = NonNullable<AsyncStatus["steps"]>[number] & {
@@ -576,6 +807,18 @@ export interface RunSyncOptions {
576
807
  preferredModelProvider?: string;
577
808
  /** Skills to inject (overrides agent default if provided) */
578
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
+ };
579
822
  }
580
823
 
581
824
  export type IntercomBridgeMode = "off" | "fork-only" | "always";
@@ -590,6 +833,12 @@ interface TopLevelParallelConfig {
590
833
  concurrency?: number;
591
834
  }
592
835
 
836
+ interface ExtensionChainConfig {
837
+ dynamicFanout?: {
838
+ maxItems?: number;
839
+ };
840
+ }
841
+
593
842
  export interface ExtensionConfig {
594
843
  asyncByDefault?: boolean;
595
844
  forceTopLevelAsync?: boolean;
@@ -597,6 +846,7 @@ export interface ExtensionConfig {
597
846
  maxSubagentDepth?: number;
598
847
  control?: ControlConfig;
599
848
  parallel?: TopLevelParallelConfig;
849
+ chain?: ExtensionChainConfig;
600
850
  worktreeSetupHook?: string;
601
851
  worktreeSetupHookTimeoutMs?: number;
602
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,