pi-cohort 7.2.2 → 7.3.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.
- package/CHANGELOG.md +12 -0
- package/README.md +31 -2
- package/package.json +1 -1
- package/skills/pi-cohort/SKILL.md +10 -0
- package/skills/pi-cohort/reference/config-fields.md +1 -0
- package/src/agents/agent-management.ts +9 -1
- package/src/agents/agents.ts +2 -1
- package/src/agents/chain-serializer.ts +14 -0
- package/src/extension/schemas.ts +4 -0
- package/src/runs/background/async-execution.ts +3 -0
- package/src/runs/background/notify.ts +8 -2
- package/src/runs/background/subagent-runner.ts +57 -21
- package/src/runs/foreground/chain-execution.ts +148 -54
- package/src/runs/foreground/subagent-executor.ts +3 -2
- package/src/runs/shared/chain-outputs.ts +49 -9
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/parallel-utils.ts +3 -0
- package/src/runs/shared/pi-spawn.ts +38 -33
- package/src/runs/shared/stop-conditions.ts +100 -0
- package/src/runs/shared/worktree.ts +7 -1
- package/src/shared/formatters.ts +64 -15
- package/src/shared/settings.ts +4 -1
- package/src/shared/types.ts +37 -0
- package/src/slash/slash-commands.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [7.3.0] - 2026-10-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Chain steps accept an optional `stopOn: { path, in }` condition over schema-validated structured output. A match ends the chain after that step as a controlled stop (`isError: false`) with a fixed `## Stop` JSON record naming every matching task and a `## Outputs` section carrying every finished task's text and structured output; a sibling failure in the same step keeps the failed status but still attaches the record and the finished outputs. Accepted on call-time `chain:` items, `.chain.json`, management `config.steps[]`, and `.chain.md` (`stopOn: {"path":"/verdict","in":["FAIL"]}`); rejected before any child starts when the step has no `outputSchema`. Background runs persist `stop` in the result file and the completion notice renders it. Worktree parallel tasks expose their patch to later steps as `{outputs.name.patchPath}`. Chains without `stopOn` are unchanged. (#23)
|
|
8
|
+
|
|
9
|
+
## [7.2.3] - 2026-10-18
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Subagents start from SDK-hosted sessions (Pi Web, standalone SDK scripts) whose `process.argv[1]` is not Pi's CLI. `capturePiRuntime` still identifies the parent from `argv[1]` first; when that entry is not Pi's declared CLI, the executor supplies the installation the extension was loaded from (`getPackageDir()` from `@earendil-works/pi-coding-agent`), validated against that package's `bin.pi` through the same ownership check, so a sibling Pi earlier on `PATH` is never selected. Sessions launched through Pi's CLI capture exactly as before. (#22)
|
|
14
|
+
|
|
3
15
|
## [7.2.2] - 2026-10-16
|
|
4
16
|
|
|
5
17
|
### Fixed
|
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ pi install npm:pi-cohort
|
|
|
39
39
|
|
|
40
40
|
That is the only required step.
|
|
41
41
|
|
|
42
|
-
Children and detached coordinators use the originating parent's resolved Node executable and Pi entry path directly, bypassing `PATH`. Revival keeps that original runtime even when requested from a different parent installation.
|
|
42
|
+
Children and detached coordinators use the originating parent's resolved Node executable and Pi entry path directly, bypassing `PATH`. Revival keeps that original runtime even when requested from a different parent installation. When the parent is not launched through Pi's CLI (an SDK-hosted session such as Pi Web, where `process.argv[1]` is the host's entry), children use the Pi installation this extension was loaded from: the package directory Pi reports for itself, accepted only when it is `@earendil-works/pi-coding-agent` and its declared `bin.pi` resolves inside that package. Single-file Pi binaries and hosts whose `process.execPath` is not a Node binary remain unsupported. These paths are not a version lock: replacing Node or Pi in place changes what runs. If the original installation is removed, restore it at the recorded paths or start a new run; there is no fallback to the current parent's installation. Legacy records without an original runtime identity remain inspectable but cannot be revived - start a new run.
|
|
43
43
|
|
|
44
44
|
## Execution backend extension API
|
|
45
45
|
|
|
@@ -117,6 +117,35 @@ Ask oracle for a second opinion on my current plan.
|
|
|
117
117
|
Run parallel reviewers: one for correctness, one for tests, and one for unnecessary complexity.
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
+
Run implementers and spec reviewers in the first call, stopping the chain on `FAIL`:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
subagent({ async: false, chain: [
|
|
124
|
+
{ parallel: [
|
|
125
|
+
{ agent: "implementer", as: "impl_1", task: "Implement task 1", outputSchema: statusSchema },
|
|
126
|
+
{ agent: "implementer", as: "impl_2", task: "Implement task 2", outputSchema: statusSchema },
|
|
127
|
+
], worktree: true, stopOn: { path: "/status", in: ["NEEDS_CONTEXT", "BLOCKED"] } },
|
|
128
|
+
{ parallel: [
|
|
129
|
+
{ agent: "spec-reviewer", as: "sr_1", task: "Review {outputs.impl_1.patchPath}", outputSchema: verdictSchema },
|
|
130
|
+
{ agent: "spec-reviewer", as: "sr_2", task: "Review {outputs.impl_2.patchPath}", outputSchema: verdictSchema },
|
|
131
|
+
], stopOn: { path: "/verdict", in: ["FAIL"] } },
|
|
132
|
+
]})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The parent applies the accepted patches, runs its tests, and issues a second call for code review over the integrated diff:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
subagent({ async: false, agent: "code-reviewer", task: "Review the integrated diff" })
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
A match ends the chain after that step as a controlled stop (not an error): the result carries a `## Stop` JSON record naming every matching task and a `## Outputs` section with every finished task's output. A chain without `stopOn` behaves exactly as before.
|
|
142
|
+
|
|
143
|
+
In a saved `.chain.md` the condition is one line per step, the object as JSON:
|
|
144
|
+
|
|
145
|
+
```md
|
|
146
|
+
stopOn: {"path":"/verdict","in":["FAIL"]}
|
|
147
|
+
```
|
|
148
|
+
|
|
120
149
|
That's the whole surface for day-to-day use. More phrasing patterns: [doc/commands.md](doc/commands.md#prompt-cookbook-appendix).
|
|
121
150
|
|
|
122
151
|
Review and delivery workflows live in pi-gauntlet; pi-cohort ships delegation primitives plus `/investigate` and `/skill:handoff [--out <path> | --key <stem>]` (the brief lands in `<tmpdir>/pi-handoff/<primary>--<leaf>.md` by default; contract in [doc/handoff-template.md](doc/handoff-template.md)).
|
|
@@ -142,7 +171,7 @@ Full reference: agent/chain authoring in [doc/agents-and-chains.md](doc/agents-a
|
|
|
142
171
|
|---|---|
|
|
143
172
|
| Subagent | A focused child Pi session with one job and (by default) a fresh context. |
|
|
144
173
|
| Agent (persona) | A markdown file with frontmatter defining a specialist: `scout`, `planner`, `worker`, `reviewer`, `context-builder`, `oracle`, `delegate`, `monitor`. Full table: [doc/agents-and-chains.md](doc/agents-and-chains.md#builtin-agents-in-plain-english). |
|
|
145
|
-
| Chain | A saved or inline sequence of agent steps, with fan-out/fan-in support. |
|
|
174
|
+
| Chain | A saved or inline sequence of agent steps, with fan-out/fan-in support and an optional per-step `stopOn` condition that ends the chain when a child's structured output matches (for example a reviewer's `FAIL` verdict). Field reference: [doc/agents-and-chains.md](doc/agents-and-chains.md#stop-conditions-stopon). |
|
|
146
175
|
| Fresh vs. forked context | Fresh = clean slate; forked = a real branch of the parent's session history. |
|
|
147
176
|
| Recursion guard | Depth cap on nested delegation so a child can only fan out if explicitly allowed. |
|
|
148
177
|
| `Σ$` / `cost:external` | The session-wide cost rollup, and the protocol other extensions use to report into it. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cohort",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.3.0",
|
|
4
4
|
"description": "Delegate Pi work to focused child agents: code review, scouting, implementation, parallel audits, saved chains, and background jobs.",
|
|
5
5
|
"author": "Jacek Juraszek",
|
|
6
6
|
"license": "MIT",
|
|
@@ -65,6 +65,7 @@ A child saying "done" is evidence, not verification. `reviewed` means a reviewer
|
|
|
65
65
|
| `cwd` | child working directory; pass the worktree path or the child inherits pi's launch dir |
|
|
66
66
|
| `acceptance` | per-task override, same shape as top-level |
|
|
67
67
|
| `as`, `outputSchema` | the child must call `structured_output` with schema-valid JSON or the step fails; later steps read `{outputs.<as>}` |
|
|
68
|
+
| `stopOn` | Use step-level `{ path, in }` to end the chain after this step when any task's structured output at the JSON Pointer `path` equals a member of `in`; supply `outputSchema` on every task of the step |
|
|
68
69
|
|
|
69
70
|
## Control actions
|
|
70
71
|
|
|
@@ -98,6 +99,15 @@ subagent({ async: false, chain: [
|
|
|
98
99
|
{ agent: "worker", task: "Apply accepted fixes from {previous}" }
|
|
99
100
|
]})
|
|
100
101
|
|
|
102
|
+
// Run implementers and spec reviewers in the first call (stop on FAIL).
|
|
103
|
+
subagent({ async: false, chain: [
|
|
104
|
+
{ parallel: [ { agent: "implementer", as: "impl_1", task: "...", outputSchema: statusSchema } ], worktree: true, stopOn: { path: "/status", in: ["NEEDS_CONTEXT", "BLOCKED"] } },
|
|
105
|
+
{ parallel: [ { agent: "spec-reviewer", as: "sr_1", task: "Review {outputs.impl_1.patchPath}", outputSchema: verdictSchema } ], stopOn: { path: "/verdict", in: ["FAIL"] } },
|
|
106
|
+
]})
|
|
107
|
+
// Read a controlled match (isError false) in the `## Stop` JSON record and every finished output under `## Outputs`.
|
|
108
|
+
// Apply accepted patches in the parent, run tests, then issue a second call for code review over the integrated diff.
|
|
109
|
+
subagent({ async: false, agent: "code-reviewer", task: "Review the integrated diff" })
|
|
110
|
+
|
|
101
111
|
// background job, then control (pair long jobs with a monitor - see Control actions)
|
|
102
112
|
subagent({ agent: "worker", async: true, task: "<long job - emit progress lines as you work>" })
|
|
103
113
|
subagent({ action: "status" })
|
|
@@ -53,6 +53,7 @@ Per step:
|
|
|
53
53
|
| `model` | string | Model override. |
|
|
54
54
|
| `skills` | string[] \| false | Skills to inject into this step's agent (management config, plural); the execution-time `chain[].skill` param is singular. |
|
|
55
55
|
| `progress` | boolean | progress.md tracking. |
|
|
56
|
+
| `stopOn` | `{ path: string, in: string[] }` | End the chain after this step when the structured output at JSON Pointer `path` equals a member of `in`; supply `outputSchema`. Write to `.chain.md` as `stopOn: {"path":"/verdict","in":["FAIL"]}`. |
|
|
56
57
|
|
|
57
58
|
### Call-time `control` overrides
|
|
58
59
|
|
|
@@ -20,7 +20,8 @@ import {
|
|
|
20
20
|
import { serializeAgent } from "./agent-serializer.ts";
|
|
21
21
|
import { serializeChain, serializeJsonChain } from "./chain-serializer.ts";
|
|
22
22
|
import { discoverAvailableSkills } from "./skills.ts";
|
|
23
|
-
import type { Details } from "../shared/types.ts";
|
|
23
|
+
import type { Details, StopCondition } from "../shared/types.ts";
|
|
24
|
+
import { stopConditionShapeError, validateStopConditions } from "../runs/shared/stop-conditions.ts";
|
|
24
25
|
|
|
25
26
|
type ManagementAction = "list" | "get" | "create" | "update" | "delete";
|
|
26
27
|
type ManagementScope = "user" | "project";
|
|
@@ -213,8 +214,15 @@ function parseStepList(raw: unknown): { steps?: ChainStepConfig[]; error?: strin
|
|
|
213
214
|
if (typeof s.progress === "boolean") step.progress = s.progress;
|
|
214
215
|
else return { error: `config.steps[${i}].progress must be a boolean.` };
|
|
215
216
|
}
|
|
217
|
+
if (hasKey(s, "stopOn")) {
|
|
218
|
+
const shapeError = stopConditionShapeError(s.stopOn, `config.steps[${i}].stopOn`);
|
|
219
|
+
if (shapeError) return { error: shapeError };
|
|
220
|
+
step.stopOn = s.stopOn as StopCondition;
|
|
221
|
+
}
|
|
216
222
|
steps.push(step);
|
|
217
223
|
}
|
|
224
|
+
const stopError = validateStopConditions(steps);
|
|
225
|
+
if (stopError) return { error: stopError };
|
|
218
226
|
return { steps };
|
|
219
227
|
}
|
|
220
228
|
|
package/src/agents/agents.ts
CHANGED
|
@@ -6,7 +6,7 @@ import * as fs from "node:fs";
|
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import type { AcceptanceInput, OutputMode } from "../shared/types.ts";
|
|
9
|
+
import type { AcceptanceInput, OutputMode, StopCondition } from "../shared/types.ts";
|
|
10
10
|
import { getAgentDir } from "../shared/utils.ts";
|
|
11
11
|
import { KNOWN_FIELDS } from "./agent-serializer.ts";
|
|
12
12
|
import { parseChain, parseJsonChain } from "./chain-serializer.ts";
|
|
@@ -142,6 +142,7 @@ export interface ChainStepConfig {
|
|
|
142
142
|
failFast?: boolean;
|
|
143
143
|
worktree?: boolean;
|
|
144
144
|
acceptance?: AcceptanceInput;
|
|
145
|
+
stopOn?: StopCondition;
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
export interface ChainConfig {
|
|
@@ -4,6 +4,8 @@ import { parseFrontmatter } from "./frontmatter.ts";
|
|
|
4
4
|
import { ChainOutputValidationError, validateChainOutputBindings } from "../runs/shared/chain-outputs.ts";
|
|
5
5
|
import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
|
|
6
6
|
import type { ChainStep } from "../shared/settings.ts";
|
|
7
|
+
import { stopConditionShapeError } from "../runs/shared/stop-conditions.ts";
|
|
8
|
+
import type { StopCondition } from "../shared/types.ts";
|
|
7
9
|
|
|
8
10
|
function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
|
|
9
11
|
const lines = sectionBody.split("\n");
|
|
@@ -78,6 +80,17 @@ function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
|
|
|
78
80
|
if (rawValue === "true") step.progress = true;
|
|
79
81
|
else if (rawValue === "false") step.progress = false;
|
|
80
82
|
}
|
|
83
|
+
if (key === "stopon") {
|
|
84
|
+
const invalid = `stopOn line '${line.trim()}' must be a JSON object {"path": "<json-pointer>", "in": ["<value>", ...]}.`;
|
|
85
|
+
let parsed: unknown;
|
|
86
|
+
try {
|
|
87
|
+
parsed = JSON.parse(rawValue);
|
|
88
|
+
} catch {
|
|
89
|
+
throw new Error(invalid);
|
|
90
|
+
}
|
|
91
|
+
if (stopConditionShapeError(parsed, "stopOn")) throw new Error(invalid);
|
|
92
|
+
step.stopOn = parsed as StopCondition;
|
|
93
|
+
}
|
|
81
94
|
}
|
|
82
95
|
|
|
83
96
|
return step;
|
|
@@ -242,6 +255,7 @@ export function serializeChain(config: ChainConfig): string {
|
|
|
242
255
|
if (step.skills === false) lines.push("skills: false");
|
|
243
256
|
else if (Array.isArray(step.skills) && step.skills.length > 0) lines.push(`skills: ${step.skills.join(", ")}`);
|
|
244
257
|
if (step.progress !== undefined) lines.push(`progress: ${step.progress ? "true" : "false"}`);
|
|
258
|
+
if (step.stopOn) lines.push(`stopOn: ${JSON.stringify(step.stopOn)}`);
|
|
245
259
|
lines.push("");
|
|
246
260
|
lines.push(step.task ?? "");
|
|
247
261
|
if (i < config.steps.length - 1) lines.push("");
|
package/src/extension/schemas.ts
CHANGED
|
@@ -233,6 +233,10 @@ const ChainItem = Type.Object({
|
|
|
233
233
|
worktree: Type.Optional(Type.Boolean({
|
|
234
234
|
description: "Isolated worktree per task."
|
|
235
235
|
})),
|
|
236
|
+
stopOn: Type.Optional(Type.Object({
|
|
237
|
+
path: Type.String({ description: "JSON Pointer into the step's structured output." }),
|
|
238
|
+
in: Type.Array(Type.String(), { minItems: 1, description: "Stop the chain when the value equals any member." }),
|
|
239
|
+
}, { additionalProperties: false, description: "Stop after this step when any task's structured output matches; requires outputSchema." })),
|
|
236
240
|
}, {
|
|
237
241
|
description: "{agent,task?} seq; {parallel:[..]} concurrent; {expand,parallel,collect} fanout.",
|
|
238
242
|
additionalProperties: false,
|
|
@@ -519,6 +519,7 @@ export function executeAsyncChain(
|
|
|
519
519
|
async: true,
|
|
520
520
|
dynamic: false,
|
|
521
521
|
}),
|
|
522
|
+
...(s.stopOn ? { stopOn: s.stopOn } : {}),
|
|
522
523
|
...(s.outputSchema ? { structuredOutputSchema: s.outputSchema } : {}),
|
|
523
524
|
...(s.outputSchema ? { structuredOutput: createStructuredOutputRuntime(s.outputSchema, path.join(asyncDir, "structured-output")) } : {}),
|
|
524
525
|
};
|
|
@@ -566,6 +567,7 @@ export function executeAsyncChain(
|
|
|
566
567
|
concurrency: s.concurrency,
|
|
567
568
|
failFast: s.failFast,
|
|
568
569
|
worktree: s.worktree,
|
|
570
|
+
stopOn: s.stopOn,
|
|
569
571
|
};
|
|
570
572
|
}
|
|
571
573
|
if (isDynamicParallelStep(s)) {
|
|
@@ -584,6 +586,7 @@ export function executeAsyncChain(
|
|
|
584
586
|
failFast: s.failFast,
|
|
585
587
|
phase: s.phase,
|
|
586
588
|
label: s.label,
|
|
589
|
+
stopOn: s.stopOn,
|
|
587
590
|
effectiveAcceptance: resolveEffectiveAcceptance({
|
|
588
591
|
explicit: s.acceptance,
|
|
589
592
|
agentName: s.parallel.agent,
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { buildCompletionKey, getGlobalSeenMap, markSeenWithTtl } from "./completion-dedupe.ts";
|
|
7
7
|
import { SUBAGENT_ASYNC_COMPLETE_EVENT } from "../../shared/types.ts";
|
|
8
|
+
import { buildChainStopSection } from "../../shared/formatters.ts";
|
|
9
|
+
import type { StopRecord } from "../../shared/types.ts";
|
|
8
10
|
|
|
9
11
|
interface ChainStepResult {
|
|
10
12
|
agent: string;
|
|
@@ -14,7 +16,7 @@ interface ChainStepResult {
|
|
|
14
16
|
|
|
15
17
|
export interface SubagentNotifyDetails {
|
|
16
18
|
agent: string;
|
|
17
|
-
status: "completed" | "failed" | "paused";
|
|
19
|
+
status: "completed" | "failed" | "paused" | "stopped";
|
|
18
20
|
taskInfo?: string;
|
|
19
21
|
resultPreview: string;
|
|
20
22
|
durationMs?: number;
|
|
@@ -36,6 +38,7 @@ interface SubagentResult {
|
|
|
36
38
|
gistUrl?: string;
|
|
37
39
|
shareError?: string;
|
|
38
40
|
results?: ChainStepResult[];
|
|
41
|
+
stop?: StopRecord;
|
|
39
42
|
taskIndex?: number;
|
|
40
43
|
totalTasks?: number;
|
|
41
44
|
}
|
|
@@ -57,7 +60,7 @@ export default function registerSubagentNotify(pi: ExtensionAPI): () => void {
|
|
|
57
60
|
|| result.state === "paused"
|
|
58
61
|
|| summary.startsWith("Paused after interrupt.")
|
|
59
62
|
);
|
|
60
|
-
const status = paused ? "paused" : result.success ? "completed" : "failed";
|
|
63
|
+
const status = paused ? "paused" : result.success ? (result.stop ? "stopped" : "completed") : "failed";
|
|
61
64
|
|
|
62
65
|
const taskInfo =
|
|
63
66
|
result.taskIndex !== undefined && result.totalTasks !== undefined
|
|
@@ -73,10 +76,13 @@ export default function registerSubagentNotify(pi: ExtensionAPI): () => void {
|
|
|
73
76
|
: undefined;
|
|
74
77
|
|
|
75
78
|
const displaySummary = summary.trim() ? summary : "(no output)";
|
|
79
|
+
const stopBlock = result.stop ? buildChainStopSection(result.stop) : undefined;
|
|
76
80
|
const content = [
|
|
77
81
|
`Background task ${status}: **${agent}**${taskInfo}`,
|
|
78
82
|
"",
|
|
79
83
|
displaySummary,
|
|
84
|
+
stopBlock ? "" : undefined,
|
|
85
|
+
stopBlock,
|
|
80
86
|
sessionLine ? "" : undefined,
|
|
81
87
|
sessionLine,
|
|
82
88
|
]
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type AsyncParallelGroupStatus,
|
|
16
16
|
type AsyncStatus,
|
|
17
17
|
type ChainOutputMap,
|
|
18
|
+
type StopRecord,
|
|
18
19
|
type ModelAttempt,
|
|
19
20
|
type NestedRouteInfo,
|
|
20
21
|
type ResolvedControlConfig,
|
|
@@ -45,7 +46,8 @@ import {
|
|
|
45
46
|
MAX_PARALLEL_CONCURRENCY,
|
|
46
47
|
} from "../shared/parallel-utils.ts";
|
|
47
48
|
import { buildPiArgs, cleanupTempDir, runDirEnv } from "../shared/pi-args.ts";
|
|
48
|
-
import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
|
|
49
|
+
import { ChainOutputValidationError, outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
|
|
50
|
+
import { buildStopRecord, evaluateStopCondition } from "../shared/stop-conditions.ts";
|
|
49
51
|
import { createStructuredOutputRuntime, readStructuredOutput } from "../shared/structured-output.ts";
|
|
50
52
|
import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
|
|
51
53
|
import { nestedSummaryFromAsyncStatus, writeNestedEvent } from "../shared/nested-events.ts";
|
|
@@ -74,6 +76,8 @@ import {
|
|
|
74
76
|
formatWorktreeDiffSummary,
|
|
75
77
|
formatWorktreeTaskCwdConflict,
|
|
76
78
|
type WorktreeSetup,
|
|
79
|
+
type WorktreeDiff,
|
|
80
|
+
patchPathForDiff,
|
|
77
81
|
} from "../shared/worktree.ts";
|
|
78
82
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
79
83
|
import { writeInitialProgressFile } from "../../shared/settings.ts";
|
|
@@ -633,7 +637,12 @@ export async function runSingleStep(
|
|
|
633
637
|
: undefined);
|
|
634
638
|
const placeholderRegex = new RegExp(ctx.placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
|
|
635
639
|
let task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
|
|
636
|
-
|
|
640
|
+
try {
|
|
641
|
+
task = resolveOutputReferences(task, ctx.outputs ?? {});
|
|
642
|
+
} catch (error) {
|
|
643
|
+
if (!(error instanceof ChainOutputValidationError)) throw error;
|
|
644
|
+
return { agent: step.agent, output: error.message, exitCode: 1, error: error.message, sessionFile: step.sessionFile };
|
|
645
|
+
}
|
|
637
646
|
const taskForCompletionGuard = task;
|
|
638
647
|
if (step.effectiveAcceptance) {
|
|
639
648
|
const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance);
|
|
@@ -1203,16 +1212,23 @@ function prepareParallelTaskRun(
|
|
|
1203
1212
|
};
|
|
1204
1213
|
}
|
|
1205
1214
|
|
|
1206
|
-
function
|
|
1207
|
-
previousOutput: string,
|
|
1215
|
+
function captureParallelWorktreeDiffs(
|
|
1208
1216
|
worktreeSetup: WorktreeSetup | undefined,
|
|
1209
1217
|
asyncDir: string,
|
|
1210
1218
|
stepIndex: number,
|
|
1211
1219
|
group: Extract<RunnerStep, { parallel: SubagentStep[] }>,
|
|
1212
|
-
):
|
|
1213
|
-
if (!worktreeSetup) return
|
|
1220
|
+
): WorktreeDiff[] {
|
|
1221
|
+
if (!worktreeSetup) return [];
|
|
1214
1222
|
const diffsDir = path.join(asyncDir, "worktree-diffs", `step-${stepIndex}`);
|
|
1215
|
-
|
|
1223
|
+
return diffWorktrees(worktreeSetup, group.parallel.map((task) => task.agent), diffsDir);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function patchInfoForDiff(diff: WorktreeDiff | undefined) {
|
|
1227
|
+
if (!diff) return undefined;
|
|
1228
|
+
return { patchPath: patchPathForDiff(diff), patchError: diff.captureError };
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
function appendParallelWorktreeSummary(previousOutput: string, diffs: WorktreeDiff[]): string {
|
|
1216
1232
|
const diffSummary = formatWorktreeDiffSummary(diffs);
|
|
1217
1233
|
if (!diffSummary) return previousOutput;
|
|
1218
1234
|
return `${previousOutput}\n\n${diffSummary}`;
|
|
@@ -1231,6 +1247,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1231
1247
|
let previousOutput = "";
|
|
1232
1248
|
const outputs: ChainOutputMap = {};
|
|
1233
1249
|
const results: StepResult[] = [];
|
|
1250
|
+
let stopRecord: StopRecord | undefined;
|
|
1234
1251
|
const overallStartTime = Date.now();
|
|
1235
1252
|
const shareEnabled = config.share === true;
|
|
1236
1253
|
const asyncDir = config.asyncDir;
|
|
@@ -1931,16 +1948,22 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1931
1948
|
}
|
|
1932
1949
|
const collection = collectDynamicResults(step as Parameters<typeof collectDynamicResults>[0], materialized.items, parallelResults);
|
|
1933
1950
|
const failures = parallelResults.filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
|
|
1951
|
+
if (!interrupted && step.stopOn) {
|
|
1952
|
+
const matches = evaluateStopCondition(step.stopOn, stepIndex, parallelResults.map((r, i) => ({
|
|
1953
|
+
agent: r.agent, label: materialized.parallel[i]?.label, exitCode: r.exitCode, structuredOutput: r.structuredOutput,
|
|
1954
|
+
})));
|
|
1955
|
+
if (matches.length > 0) stopRecord = buildStopRecord(step.stopOn, stepIndex, matches);
|
|
1956
|
+
}
|
|
1957
|
+
outputs[step.collect.as] = {
|
|
1958
|
+
text: JSON.stringify(collection),
|
|
1959
|
+
structured: collection,
|
|
1960
|
+
agent: step.parallel.agent,
|
|
1961
|
+
stepIndex,
|
|
1962
|
+
};
|
|
1963
|
+
statusPayload.outputs = outputs;
|
|
1934
1964
|
if (failures.length === 0) {
|
|
1935
1965
|
try {
|
|
1936
1966
|
validateDynamicCollection(step.collect.outputSchema, collection);
|
|
1937
|
-
outputs[step.collect.as] = {
|
|
1938
|
-
text: JSON.stringify(collection),
|
|
1939
|
-
structured: collection,
|
|
1940
|
-
agent: step.parallel.agent,
|
|
1941
|
-
stepIndex,
|
|
1942
|
-
};
|
|
1943
|
-
statusPayload.outputs = outputs;
|
|
1944
1967
|
const groupAcceptance = step.effectiveAcceptance
|
|
1945
1968
|
? await evaluateAcceptance({
|
|
1946
1969
|
acceptance: step.effectiveAcceptance,
|
|
@@ -1993,7 +2016,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1993
2016
|
if (failures.length > 0) markDynamicGraphGroup(stepIndex, "failed", failures[0]?.error ?? "Dynamic fanout child failed.");
|
|
1994
2017
|
statusPayload.lastUpdate = Date.now();
|
|
1995
2018
|
writeStatusPayload();
|
|
1996
|
-
if (failures.length > 0 || statusPayload.error) break;
|
|
2019
|
+
if (stopRecord || failures.length > 0 || statusPayload.error) break;
|
|
1997
2020
|
continue;
|
|
1998
2021
|
}
|
|
1999
2022
|
|
|
@@ -2217,15 +2240,22 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2217
2240
|
executionSurface: pr.executionSurface,
|
|
2218
2241
|
});
|
|
2219
2242
|
}
|
|
2243
|
+
const diffs = captureParallelWorktreeDiffs(worktreeSetup, asyncDir, stepIndex, group);
|
|
2220
2244
|
for (let t = 0; t < group.parallel.length; t++) {
|
|
2221
2245
|
const outputName = group.parallel[t]?.outputName;
|
|
2222
|
-
if (outputName) outputs[outputName] = outputEntryFromAsyncResult({
|
|
2246
|
+
if (outputName && parallelResults[t]!.exitCode === 0) outputs[outputName] = outputEntryFromAsyncResult({
|
|
2223
2247
|
agent: parallelResults[t]!.agent,
|
|
2224
2248
|
output: parallelResults[t]!.output,
|
|
2225
2249
|
structuredOutput: parallelResults[t]!.structuredOutput,
|
|
2226
|
-
}, stepIndex);
|
|
2250
|
+
}, stepIndex, patchInfoForDiff(diffs[t]));
|
|
2227
2251
|
}
|
|
2228
2252
|
statusPayload.outputs = outputs;
|
|
2253
|
+
if (!interrupted && group.stopOn) {
|
|
2254
|
+
const matches = evaluateStopCondition(group.stopOn, stepIndex, parallelResults.map((r, i) => ({
|
|
2255
|
+
agent: r.agent, as: group.parallel[i]?.outputName, label: group.parallel[i]?.label, exitCode: r.exitCode, structuredOutput: r.structuredOutput,
|
|
2256
|
+
})));
|
|
2257
|
+
if (matches.length > 0) stopRecord = buildStopRecord(group.stopOn, stepIndex, matches);
|
|
2258
|
+
}
|
|
2229
2259
|
|
|
2230
2260
|
previousOutput = aggregateParallelOutputs(
|
|
2231
2261
|
parallelResults.map((r) => ({
|
|
@@ -2237,7 +2267,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2237
2267
|
attemptedModels: r.attemptedModels,
|
|
2238
2268
|
})),
|
|
2239
2269
|
);
|
|
2240
|
-
previousOutput = appendParallelWorktreeSummary(previousOutput,
|
|
2270
|
+
previousOutput = appendParallelWorktreeSummary(previousOutput, diffs);
|
|
2241
2271
|
|
|
2242
2272
|
appendJsonl(eventsPath, JSON.stringify({
|
|
2243
2273
|
type: "subagent.parallel.completed",
|
|
@@ -2247,7 +2277,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2247
2277
|
success: parallelResults.every((r) => r.exitCode === 0 || r.exitCode === -1),
|
|
2248
2278
|
}));
|
|
2249
2279
|
|
|
2250
|
-
if (parallelResults.some((r) => r.exitCode !== 0 && r.exitCode !== -1)) {
|
|
2280
|
+
if (stopRecord || parallelResults.some((r) => r.exitCode !== 0 && r.exitCode !== -1)) {
|
|
2251
2281
|
break;
|
|
2252
2282
|
}
|
|
2253
2283
|
} finally {
|
|
@@ -2382,8 +2412,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2382
2412
|
}
|
|
2383
2413
|
|
|
2384
2414
|
flatIndex++;
|
|
2385
|
-
if (singleResult.exitCode !== 0)
|
|
2386
|
-
|
|
2415
|
+
if (singleResult.exitCode !== 0) break;
|
|
2416
|
+
if (!interrupted && seqStep.stopOn) {
|
|
2417
|
+
const matches = evaluateStopCondition(seqStep.stopOn, stepIndex, [{ agent: singleResult.agent, as: seqStep.outputName, label: seqStep.label, exitCode: singleResult.exitCode, structuredOutput: singleResult.structuredOutput }], { sequential: true });
|
|
2418
|
+
if (matches.length > 0) {
|
|
2419
|
+
stopRecord = buildStopRecord(seqStep.stopOn, stepIndex, matches);
|
|
2420
|
+
break;
|
|
2421
|
+
}
|
|
2387
2422
|
}
|
|
2388
2423
|
}
|
|
2389
2424
|
}
|
|
@@ -2509,6 +2544,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
2509
2544
|
executionSurface: r.executionSurface,
|
|
2510
2545
|
})),
|
|
2511
2546
|
outputs,
|
|
2547
|
+
...(stopRecord ? { stop: stopRecord } : {}),
|
|
2512
2548
|
workflowGraph: statusPayload.workflowGraph,
|
|
2513
2549
|
exitCode: interrupted || results.every((r) => r.success) ? 0 : 1,
|
|
2514
2550
|
timestamp: runEndedAt,
|