@henryqw/pi-subagent 5.0.0 → 6.1.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/CONTEXT.md +8 -8
- package/README.md +15 -11
- package/dist/ephemeral.d.ts +13 -0
- package/dist/ephemeral.js +85 -6
- package/dist/index.d.ts +1 -1
- package/docs/adr/001-composable-ephemeral-execution.md +3 -1
- package/docs/adr/002-package-owned-delegate-flow-orchestration.md +8 -8
- package/docs/orchestration.md +30 -17
- package/examples/roles/reviewer.md +2 -2
- package/extensions/delegate-flow.ts +162 -68
- package/extensions/subagent.ts +139 -26
- package/extensions/tool-render.ts +16 -0
- package/package.json +2 -2
- package/skills/pi-subagent-delegated-development/SKILL.md +7 -7
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { StringEnum, type Usage } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { PROFILE_NAMES } from "@henryqw/pi-task-models";
|
|
3
4
|
import {
|
|
4
5
|
addUsage,
|
|
5
6
|
capEphemeralSubagentOutput as capOutput,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
inspectWorktreeDirty,
|
|
10
11
|
prepareExactReviewEvidence,
|
|
11
12
|
WorktreeSetupError,
|
|
13
|
+
type EphemeralSubagentActivityEvent,
|
|
12
14
|
type EphemeralSubagentExecutor,
|
|
13
15
|
type EphemeralSubagentResult,
|
|
14
16
|
type ResolvedRoleLaunch,
|
|
@@ -18,6 +20,7 @@ import {
|
|
|
18
20
|
import { Type, type Static } from "typebox";
|
|
19
21
|
import { Check } from "typebox/value";
|
|
20
22
|
import { runDelegation } from "./delegation.ts";
|
|
23
|
+
import { renderToolLines } from "./tool-render.ts";
|
|
21
24
|
|
|
22
25
|
const MAX_UNITS = 8;
|
|
23
26
|
const GIT_TIMEOUT_MS = 30_000;
|
|
@@ -28,10 +31,14 @@ const ValidationSchema = Type.Object({
|
|
|
28
31
|
args: Type.Array(Type.String()),
|
|
29
32
|
}, { additionalProperties: false });
|
|
30
33
|
|
|
34
|
+
const ModelClassSchema = StringEnum(PROFILE_NAMES, { description: "Task model profile" });
|
|
35
|
+
|
|
31
36
|
const UnitSchema = Type.Object({
|
|
32
37
|
id: Type.String({ minLength: 1 }),
|
|
33
38
|
task: Type.String({ minLength: 1 }),
|
|
34
39
|
validation: Type.Array(ValidationSchema, { minItems: 1 }),
|
|
40
|
+
modelClass: Type.Optional(ModelClassSchema),
|
|
41
|
+
review: Type.Optional(Type.String({ minLength: 1 })),
|
|
35
42
|
}, { additionalProperties: false });
|
|
36
43
|
|
|
37
44
|
export const DelegateFlowSchema = Type.Object({
|
|
@@ -40,10 +47,12 @@ export const DelegateFlowSchema = Type.Object({
|
|
|
40
47
|
|
|
41
48
|
export const DelegateFlowContinueSchema = Type.Object({
|
|
42
49
|
guidance: Type.String({ minLength: 1 }),
|
|
50
|
+
modelClass: Type.Optional(ModelClassSchema),
|
|
43
51
|
}, { additionalProperties: false });
|
|
44
52
|
|
|
45
53
|
type FlowRequest = Static<typeof DelegateFlowSchema>;
|
|
46
54
|
type FlowUnitRequest = Static<typeof UnitSchema>;
|
|
55
|
+
type FlowModelClass = FlowUnitRequest["modelClass"];
|
|
47
56
|
type FlowClassification = "setup" | "implementer" | "validation" | "reviewer_findings" | "main" | "infrastructure" | "integration";
|
|
48
57
|
type FlowPhase = "running" | "blocked";
|
|
49
58
|
type WidgetStatus = "success" | "failure" | "aborted";
|
|
@@ -54,6 +63,7 @@ type ChildSettlement =
|
|
|
54
63
|
|
|
55
64
|
type UnitState = {
|
|
56
65
|
request: FlowUnitRequest;
|
|
66
|
+
modelClass: FlowModelClass;
|
|
57
67
|
worktree: WorktreeInfo;
|
|
58
68
|
base: string;
|
|
59
69
|
implementation?: ChildSettlement;
|
|
@@ -87,7 +97,7 @@ type FlowState = {
|
|
|
87
97
|
generation: number;
|
|
88
98
|
sessionController: AbortController;
|
|
89
99
|
implementer: Role;
|
|
90
|
-
reviewer
|
|
100
|
+
reviewer?: Role;
|
|
91
101
|
main?: MainState;
|
|
92
102
|
units: UnitState[];
|
|
93
103
|
setupRecoveries: SetupRecovery[];
|
|
@@ -111,7 +121,7 @@ export interface DelegateFlowRuntime {
|
|
|
111
121
|
maxRuntimeMs: number;
|
|
112
122
|
getSessionGeneration: () => number;
|
|
113
123
|
loadRoles: () => Role[];
|
|
114
|
-
resolveLaunch: (role: Role, ctx: ExtensionContext) => ResolvedRoleLaunch;
|
|
124
|
+
resolveLaunch: (role: Role, modelClass: FlowModelClass, ctx: ExtensionContext) => ResolvedRoleLaunch;
|
|
115
125
|
startWidget: (
|
|
116
126
|
id: string,
|
|
117
127
|
role: string,
|
|
@@ -121,6 +131,7 @@ export interface DelegateFlowRuntime {
|
|
|
121
131
|
ctx: ExtensionContext,
|
|
122
132
|
) => void;
|
|
123
133
|
updateWidgetTokens: (id: string, tokens: number) => void;
|
|
134
|
+
updateWidgetActivity: (id: string, event: EphemeralSubagentActivityEvent) => void;
|
|
124
135
|
finishWidget: (id: string, status: WidgetStatus) => void;
|
|
125
136
|
}
|
|
126
137
|
|
|
@@ -150,6 +161,8 @@ export function parseDelegateFlow(value: unknown): FlowRequest {
|
|
|
150
161
|
command: text(validation.command, `units[${unitIndex}].validation[${validationIndex}].command`),
|
|
151
162
|
args: validation.args.map((value, argumentIndex) => argument(value, `units[${unitIndex}].validation[${validationIndex}].args[${argumentIndex}]`)),
|
|
152
163
|
})),
|
|
164
|
+
...(unit.modelClass === undefined ? {} : { modelClass: unit.modelClass }),
|
|
165
|
+
...(unit.review === undefined ? {} : { review: text(unit.review, `units[${unitIndex}].review`) }),
|
|
153
166
|
};
|
|
154
167
|
}),
|
|
155
168
|
};
|
|
@@ -157,7 +170,39 @@ export function parseDelegateFlow(value: unknown): FlowRequest {
|
|
|
157
170
|
|
|
158
171
|
export function parseDelegateFlowContinue(value: unknown): Static<typeof DelegateFlowContinueSchema> {
|
|
159
172
|
if (!Check(DelegateFlowContinueSchema, value)) throw new Error("delegate_flow_continue must match the declared tool schema.");
|
|
160
|
-
return {
|
|
173
|
+
return {
|
|
174
|
+
guidance: text(value.guidance, "guidance"),
|
|
175
|
+
...(value.modelClass === undefined ? {} : { modelClass: value.modelClass }),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function flowCallLabel(args: { units?: unknown }): string {
|
|
180
|
+
const count = Array.isArray(args.units) ? args.units.length : 0;
|
|
181
|
+
return `delegate_flow · working: ${count} unit${count === 1 ? "" : "s"}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function flowResultLines(text: string): string[] {
|
|
185
|
+
const lines = text.split(/\r?\n/).filter((line) => line.trim());
|
|
186
|
+
const diagnosticHeader = lines.findIndex((line) => line.trim() === "Diagnostic:");
|
|
187
|
+
const diagnostic = diagnosticHeader === -1 ? undefined : lines[diagnosticHeader + 1];
|
|
188
|
+
const recoveryHeader = lines.findIndex((line) => line.trim() === "Retained Flow state:" || line.trim() === "Attempted allocations preserved without cleanup:");
|
|
189
|
+
const recovery = recoveryHeader === -1 || !lines[recoveryHeader + 1]?.trim().startsWith("- unit=")
|
|
190
|
+
? undefined
|
|
191
|
+
: lines[recoveryHeader + 1];
|
|
192
|
+
if (diagnostic === undefined) {
|
|
193
|
+
if (recovery === undefined) return lines;
|
|
194
|
+
const recoveryIndex = lines.indexOf(recovery);
|
|
195
|
+
return [lines[0]!, recovery, ...lines.filter((_, index) => index !== 0 && index !== recoveryIndex)];
|
|
196
|
+
}
|
|
197
|
+
const leading = lines.slice(0, Math.min(1, diagnosticHeader));
|
|
198
|
+
if (recovery !== undefined) return [...leading, `Diagnostic: ${diagnostic}`, recovery];
|
|
199
|
+
// Promote the first diagnostic ahead of the result cap.
|
|
200
|
+
return [
|
|
201
|
+
...leading,
|
|
202
|
+
`Diagnostic: ${diagnostic}`,
|
|
203
|
+
...lines.slice(leading.length, diagnosticHeader),
|
|
204
|
+
...lines.slice(diagnosticHeader + 2),
|
|
205
|
+
];
|
|
161
206
|
}
|
|
162
207
|
|
|
163
208
|
function errorText(error: unknown): string {
|
|
@@ -176,6 +221,11 @@ function implementerTask(unit: FlowUnitRequest): string {
|
|
|
176
221
|
return [
|
|
177
222
|
`Flow Unit ${JSON.stringify(unit.id)} requirements:`,
|
|
178
223
|
unit.task,
|
|
224
|
+
...(unit.review === undefined ? [] : [
|
|
225
|
+
"",
|
|
226
|
+
"Review criterion to satisfy; the Reviewer alone decides approval:",
|
|
227
|
+
unit.review,
|
|
228
|
+
]),
|
|
179
229
|
"",
|
|
180
230
|
"Authoritative Flow validation (do not duplicate this final gate):",
|
|
181
231
|
...unit.validation.map((validation) => `- ${JSON.stringify(validation)}`),
|
|
@@ -188,6 +238,11 @@ function repairTask(unit: FlowUnitRequest, blocked: BlockedState, guidance: stri
|
|
|
188
238
|
"",
|
|
189
239
|
"Original requirements:",
|
|
190
240
|
unit.task,
|
|
241
|
+
...(unit.review === undefined ? [] : [
|
|
242
|
+
"",
|
|
243
|
+
"Review criterion to satisfy; the Reviewer alone decides approval:",
|
|
244
|
+
unit.review,
|
|
245
|
+
]),
|
|
191
246
|
"",
|
|
192
247
|
"Authoritative Flow validation (do not duplicate this final gate):",
|
|
193
248
|
...unit.validation.map((validation) => `- ${JSON.stringify(validation)}`),
|
|
@@ -200,16 +255,19 @@ function repairTask(unit: FlowUnitRequest, blocked: BlockedState, guidance: stri
|
|
|
200
255
|
].join("\n");
|
|
201
256
|
}
|
|
202
257
|
|
|
203
|
-
function reviewerTask(unit: FlowUnitRequest, packet: { base: string; tip: string; patchPath: string }): string {
|
|
258
|
+
function reviewerTask(unit: FlowUnitRequest, review: string, packet: { base: string; tip: string; patchPath: string }): string {
|
|
204
259
|
return [
|
|
205
|
-
`Review Flow Unit ${JSON.stringify(unit.id)}
|
|
260
|
+
`Review Flow Unit ${JSON.stringify(unit.id)} for this explicit judgment criterion:`,
|
|
261
|
+
review,
|
|
262
|
+
"",
|
|
263
|
+
"Original requirements (context only):",
|
|
206
264
|
unit.task,
|
|
207
265
|
"",
|
|
208
|
-
"Declared validation already passed:",
|
|
266
|
+
"Declared validation already passed and is authoritative for objective verification:",
|
|
209
267
|
...unit.validation.map((validation) => `- ${JSON.stringify(validation)}`),
|
|
210
268
|
"",
|
|
211
269
|
`Review Packet: ${JSON.stringify(packet)}`,
|
|
212
|
-
"Read the exact patch as authoritative and emit exactly PASS only when there are zero findings.",
|
|
270
|
+
"Review only the criterion above. Read the exact patch as authoritative and emit exactly PASS only when there are zero findings.",
|
|
213
271
|
].join("\n");
|
|
214
272
|
}
|
|
215
273
|
|
|
@@ -308,7 +366,9 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
308
366
|
const runChild = async (
|
|
309
367
|
flow: FlowState,
|
|
310
368
|
role: Role,
|
|
369
|
+
modelClass: FlowModelClass,
|
|
311
370
|
task: string,
|
|
371
|
+
widgetTask: string,
|
|
312
372
|
cwd: string,
|
|
313
373
|
widgetId: string,
|
|
314
374
|
signal: AbortSignal | undefined,
|
|
@@ -321,13 +381,14 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
321
381
|
const result = await runDelegation(runtime.executor, {
|
|
322
382
|
signal,
|
|
323
383
|
onTokens: (tokens) => runtime.updateWidgetTokens(widgetId, tokens),
|
|
384
|
+
onActivity: (event) => runtime.updateWidgetActivity(widgetId, event),
|
|
324
385
|
prepare: async () => {
|
|
325
386
|
assertCurrent(flow);
|
|
326
|
-
const launch = runtime.resolveLaunch(role, ctx);
|
|
387
|
+
const launch = runtime.resolveLaunch(role, modelClass, ctx);
|
|
327
388
|
if (launch.missingSkills.length) {
|
|
328
389
|
ctx.ui.notify(`Subagent role ${role.name} skipped unavailable Pi skills: ${launch.missingSkills.join(", ")}.`, "warning");
|
|
329
390
|
}
|
|
330
|
-
runtime.startWidget(widgetId, role.name, launch.model.id, launch.thinkingLevel,
|
|
391
|
+
runtime.startWidget(widgetId, role.name, launch.model.id, launch.thinkingLevel, widgetTask, ctx);
|
|
331
392
|
started = true;
|
|
332
393
|
return { launch, task, cwd };
|
|
333
394
|
},
|
|
@@ -437,6 +498,14 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
437
498
|
const lines = [
|
|
438
499
|
`Flow ${outcome}.`,
|
|
439
500
|
flow.completed.length ? `Completed units: ${flow.completed.map(({ id, noOp }) => `${JSON.stringify(id)}${noOp ? " (no-op)" : ""}`).join(", ")}` : "Completed units: none.",
|
|
501
|
+
...(flow.setupRecoveries.length ? [
|
|
502
|
+
"Attempted allocations preserved without cleanup:",
|
|
503
|
+
...flow.setupRecoveries.map((recovery) => `- unit=${JSON.stringify(recovery.id)} path=${JSON.stringify(recovery.path)} branch=${JSON.stringify(recovery.branch)} base=${recovery.base}`),
|
|
504
|
+
] : []),
|
|
505
|
+
...(retainedUnits.length ? [
|
|
506
|
+
"Retained Flow state:",
|
|
507
|
+
...retainedUnits.map((unit) => `- unit=${JSON.stringify(unit.id)} path=${JSON.stringify(unit.path)} branch=${JSON.stringify(unit.branch)} base=${unit.base} worktree=${unit.worktreeRetained} branch_ref=${unit.branchRetained}`),
|
|
508
|
+
] : []),
|
|
440
509
|
...(blocked ? [
|
|
441
510
|
`Blocked unit: ${JSON.stringify(blocked.unit.request.id)}.`,
|
|
442
511
|
`Classification: ${blocked.classification}.`,
|
|
@@ -446,14 +515,6 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
446
515
|
] : []),
|
|
447
516
|
...(failure ? [`Classification: ${failure.classification}.`, `Diagnostic:\n${failure.diagnostic}`] : []),
|
|
448
517
|
...(flow.warnings.length ? ["Warnings:", ...flow.warnings.map((warning) => `- ${warning}`)] : []),
|
|
449
|
-
...(flow.setupRecoveries.length ? [
|
|
450
|
-
"Attempted allocations preserved without cleanup:",
|
|
451
|
-
...flow.setupRecoveries.map((recovery) => `- unit=${JSON.stringify(recovery.id)} path=${JSON.stringify(recovery.path)} branch=${JSON.stringify(recovery.branch)} base=${recovery.base}`),
|
|
452
|
-
] : []),
|
|
453
|
-
...(retainedUnits.length ? [
|
|
454
|
-
"Retained Flow state:",
|
|
455
|
-
...retainedUnits.map((unit) => `- unit=${JSON.stringify(unit.id)} path=${JSON.stringify(unit.path)} branch=${JSON.stringify(unit.branch)} base=${unit.base} worktree=${unit.worktreeRetained} branch_ref=${unit.branchRetained}`),
|
|
456
|
-
] : []),
|
|
457
518
|
];
|
|
458
519
|
return {
|
|
459
520
|
content: [{ type: "text" as const, text: capOutput(lines.join("\n")) }],
|
|
@@ -624,45 +685,54 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
624
685
|
continue;
|
|
625
686
|
}
|
|
626
687
|
|
|
627
|
-
let
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
return terminal(flow, "infrastructure",
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
let review: ChildSettlement;
|
|
635
|
-
let cleanupError: unknown;
|
|
636
|
-
try {
|
|
637
|
-
assertCurrent(flow);
|
|
638
|
-
review = await runChild(
|
|
639
|
-
flow,
|
|
640
|
-
flow.reviewer,
|
|
641
|
-
reviewerTask(unit.request, { base: evidence.base, tip: evidence.tip, patchPath: evidence.patchPath }),
|
|
642
|
-
unit.worktree.cwd,
|
|
643
|
-
`${toolCallId}:flow:${flow.index}:review`,
|
|
644
|
-
signal,
|
|
645
|
-
ctx,
|
|
646
|
-
meter,
|
|
647
|
-
);
|
|
648
|
-
} finally {
|
|
688
|
+
let approvedTip = tip;
|
|
689
|
+
const reviewCriterion = unit.request.review;
|
|
690
|
+
if (reviewCriterion !== undefined) {
|
|
691
|
+
const reviewer = flow.reviewer;
|
|
692
|
+
if (!reviewer) return terminal(flow, "infrastructure", "Flow Reviewer was not resolved for a unit that requires review.", meter);
|
|
693
|
+
let evidence;
|
|
649
694
|
try {
|
|
650
|
-
await
|
|
695
|
+
evidence = await prepareExactReviewEvidence({ base: main.expectedHead, tip, worktree: unit.worktree.path }, signal);
|
|
651
696
|
} catch (error) {
|
|
652
|
-
|
|
697
|
+
return terminal(flow, "infrastructure", errorText(error), meter);
|
|
653
698
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
699
|
+
|
|
700
|
+
let review: ChildSettlement;
|
|
701
|
+
let cleanupError: unknown;
|
|
702
|
+
try {
|
|
703
|
+
assertCurrent(flow);
|
|
704
|
+
review = await runChild(
|
|
705
|
+
flow,
|
|
706
|
+
reviewer,
|
|
707
|
+
unit.modelClass,
|
|
708
|
+
reviewerTask(unit.request, reviewCriterion, { base: evidence.base, tip: evidence.tip, patchPath: evidence.patchPath }),
|
|
709
|
+
unit.request.task,
|
|
710
|
+
unit.worktree.cwd,
|
|
711
|
+
`${toolCallId}:flow:${flow.index}:review`,
|
|
712
|
+
signal,
|
|
713
|
+
ctx,
|
|
714
|
+
meter,
|
|
715
|
+
);
|
|
716
|
+
} finally {
|
|
717
|
+
try {
|
|
718
|
+
await evidence.cleanup();
|
|
719
|
+
} catch (error) {
|
|
720
|
+
cleanupError = error;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (cleanupError !== undefined) return terminal(flow, "infrastructure", errorText(cleanupError), meter);
|
|
724
|
+
assertCurrent(flow);
|
|
725
|
+
if ("error" in review) return terminal(flow, "infrastructure", errorText(review.error), meter);
|
|
726
|
+
if (review.result.outcome !== "success") {
|
|
727
|
+
return terminal(flow, "infrastructure", settlementFailure(review)!, meter);
|
|
728
|
+
}
|
|
729
|
+
if (TRUNCATED_OUTPUT.test(review.result.output)) {
|
|
730
|
+
return terminal(flow, "infrastructure", "Reviewer transport output was truncated; approval is invalid.", meter);
|
|
731
|
+
}
|
|
732
|
+
if (review.result.output.trim() !== "PASS") {
|
|
733
|
+
return block(flow, unit, "reviewer_findings", review.result.output || "Reviewer returned no PASS approval.", meter);
|
|
734
|
+
}
|
|
735
|
+
approvedTip = evidence.tip;
|
|
666
736
|
}
|
|
667
737
|
|
|
668
738
|
try {
|
|
@@ -671,12 +741,12 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
671
741
|
return terminal(flow, "main", errorText(error), meter);
|
|
672
742
|
}
|
|
673
743
|
assertCurrent(flow);
|
|
674
|
-
const merged = await git(["merge", "--no-overwrite-ignore", "--ff-only",
|
|
744
|
+
const merged = await git(["merge", "--no-overwrite-ignore", "--ff-only", approvedTip], main.root, signal);
|
|
675
745
|
assertCurrent(flow);
|
|
676
746
|
if (merged.code !== 0 || merged.killed) {
|
|
677
|
-
const diagnostic = commandFailure(`git merge --no-overwrite-ignore --ff-only ${
|
|
747
|
+
const diagnostic = commandFailure(`git merge --no-overwrite-ignore --ff-only ${approvedTip}`, merged);
|
|
678
748
|
const previousHead = main.expectedHead;
|
|
679
|
-
main.expectedHead =
|
|
749
|
+
main.expectedHead = approvedTip;
|
|
680
750
|
try {
|
|
681
751
|
await checkMain(main);
|
|
682
752
|
} catch (error) {
|
|
@@ -688,7 +758,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
688
758
|
].join("\n")), meter);
|
|
689
759
|
}
|
|
690
760
|
flow.warnings.push(capOutput(`Unit ${JSON.stringify(unit.request.id)} integrated after merge reported failure: ${diagnostic}`));
|
|
691
|
-
} else main.expectedHead =
|
|
761
|
+
} else main.expectedHead = approvedTip;
|
|
692
762
|
flow.completed.push({ id: unit.request.id, noOp: false });
|
|
693
763
|
try {
|
|
694
764
|
await checkMain(main);
|
|
@@ -696,7 +766,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
696
766
|
return terminal(flow, "integration", errorText(error), meter);
|
|
697
767
|
}
|
|
698
768
|
assertCurrent(flow);
|
|
699
|
-
const cleanupWarning = await cleanupUnit(unit, main,
|
|
769
|
+
const cleanupWarning = await cleanupUnit(unit, main, approvedTip, flow.sessionController.signal);
|
|
700
770
|
assertCurrent(flow);
|
|
701
771
|
if (cleanupWarning) flow.warnings.push(`Unit ${JSON.stringify(unit.request.id)} integrated, but cleanup refused: ${cleanupWarning}`);
|
|
702
772
|
flow.index += 1;
|
|
@@ -709,28 +779,38 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
709
779
|
pi.registerTool({
|
|
710
780
|
name: "delegate_flow",
|
|
711
781
|
label: "Delegate Flow",
|
|
712
|
-
description: "Run 1–8 independent Implementers in isolated Unit Worktrees,
|
|
713
|
-
promptSnippet: "Run a deterministic parallel-implementation, serial-
|
|
782
|
+
description: "Run 1–8 independent Implementers in isolated Unit Worktrees, validate and serially fast-forward each tip, with exact review only for units that declare a judgment criterion.",
|
|
783
|
+
promptSnippet: "Run a deterministic parallel-implementation, serial-verification Flow",
|
|
714
784
|
promptGuidelines: [
|
|
715
785
|
"Use delegate_flow only for cohesive units expected to commute; combine work that overlaps files, APIs, schemas, generated output, package metadata, lockfiles, or invariants.",
|
|
716
|
-
"Each unit must include explicit bounded requirements and its authoritative direct command/argument validation gate.",
|
|
717
|
-
"If a Flow blocks, inspect its classification and call delegate_flow_continue once with explicit repair guidance.",
|
|
786
|
+
"Each unit must include explicit bounded requirements and its authoritative direct command/argument validation gate. Add review only for an explicit judgment that validation cannot establish.",
|
|
787
|
+
"If a Flow blocks, inspect its classification and call delegate_flow_continue once with explicit repair guidance; modelClass may replace that one repair's current class.",
|
|
718
788
|
],
|
|
719
789
|
parameters: DelegateFlowSchema,
|
|
790
|
+
renderShell: "self",
|
|
791
|
+
renderCall(args, theme, _context) {
|
|
792
|
+
return renderToolLines([theme.fg("toolTitle", flowCallLabel(args))], theme);
|
|
793
|
+
},
|
|
794
|
+
renderResult(result, _options, theme, _context) {
|
|
795
|
+
const text = result.content.find((part) => part.type === "text")?.text ?? "(no output)";
|
|
796
|
+
return renderToolLines(flowResultLines(text), theme);
|
|
797
|
+
},
|
|
720
798
|
prepareArguments: parseDelegateFlow,
|
|
721
799
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
722
800
|
const request = parseDelegateFlow(params);
|
|
723
801
|
if (active) throw new Error("delegate_flow rejected because another Flow is active.");
|
|
724
802
|
const roles = runtime.loadRoles();
|
|
725
803
|
const implementer = roles.find(({ name }) => name === "implementer");
|
|
726
|
-
const
|
|
727
|
-
|
|
804
|
+
const needsReviewer = request.units.some(({ review }) => review !== undefined);
|
|
805
|
+
const reviewer = needsReviewer ? roles.find(({ name }) => name === "reviewer") : undefined;
|
|
806
|
+
if (!implementer) throw new Error("delegate_flow requires an implementer Role.");
|
|
807
|
+
if (needsReviewer && !reviewer) throw new Error("delegate_flow requires a reviewer Role when a unit declares review.");
|
|
728
808
|
const flow: FlowState = {
|
|
729
809
|
phase: "running",
|
|
730
810
|
generation: runtime.getSessionGeneration(),
|
|
731
811
|
sessionController: new AbortController(),
|
|
732
812
|
implementer,
|
|
733
|
-
reviewer,
|
|
813
|
+
...(reviewer === undefined ? {} : { reviewer }),
|
|
734
814
|
units: [],
|
|
735
815
|
setupRecoveries: [],
|
|
736
816
|
index: 0,
|
|
@@ -762,6 +842,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
762
842
|
if (!worktree) throw new Error("Flow Unit Worktrees require a Git repository with a committed HEAD; generic cwd fallback is disabled.");
|
|
763
843
|
flow.units.push({
|
|
764
844
|
request: unit,
|
|
845
|
+
modelClass: unit.modelClass,
|
|
765
846
|
worktree,
|
|
766
847
|
base: worktree.baseCommit,
|
|
767
848
|
repairUsed: false,
|
|
@@ -777,7 +858,9 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
777
858
|
const settlements = await Promise.all(flow.units.map((unit, index) => runChild(
|
|
778
859
|
flow,
|
|
779
860
|
flow.implementer,
|
|
861
|
+
unit.modelClass,
|
|
780
862
|
implementerTask(unit.request),
|
|
863
|
+
unit.request.task,
|
|
781
864
|
unit.worktree.cwd,
|
|
782
865
|
`${toolCallId}:flow:${index}:implement`,
|
|
783
866
|
operationSignal,
|
|
@@ -805,13 +888,21 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
805
888
|
pi.registerTool({
|
|
806
889
|
name: "delegate_flow_continue",
|
|
807
890
|
label: "Continue Delegate Flow",
|
|
808
|
-
description: "Repair the one blocked Flow Unit in its existing Unit Worktree, then resume declared-order validation, exact review, and integration.",
|
|
891
|
+
description: "Repair the one blocked Flow Unit in its existing Unit Worktree, optionally replace its model class, then resume declared-order validation, conditional exact review, and integration.",
|
|
809
892
|
promptSnippet: "Repair and continue the blocked deterministic Flow",
|
|
810
893
|
promptGuidelines: ["Call delegate_flow_continue only after delegate_flow reports a repairable block, with explicit guidance addressing that block."],
|
|
811
894
|
parameters: DelegateFlowContinueSchema,
|
|
895
|
+
renderShell: "self",
|
|
896
|
+
renderCall(_args, theme, _context) {
|
|
897
|
+
return renderToolLines([theme.fg("toolTitle", "delegate_flow_continue · working: repair continuation")], theme);
|
|
898
|
+
},
|
|
899
|
+
renderResult(result, _options, theme, _context) {
|
|
900
|
+
const text = result.content.find((part) => part.type === "text")?.text ?? "(no output)";
|
|
901
|
+
return renderToolLines(flowResultLines(text), theme);
|
|
902
|
+
},
|
|
812
903
|
prepareArguments: parseDelegateFlowContinue,
|
|
813
904
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
814
|
-
const { guidance } = parseDelegateFlowContinue(params);
|
|
905
|
+
const { guidance, modelClass } = parseDelegateFlowContinue(params);
|
|
815
906
|
const flow = active;
|
|
816
907
|
if (!flow) throw new Error("delegate_flow_continue requires an active blocked Flow.");
|
|
817
908
|
assertCurrent(flow);
|
|
@@ -822,13 +913,16 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
822
913
|
flow.phase = "running";
|
|
823
914
|
flow.blocked = undefined;
|
|
824
915
|
unit.repairUsed = true;
|
|
916
|
+
if (modelClass !== undefined) unit.modelClass = modelClass;
|
|
825
917
|
const operationSignal = bindSignal(flow, signal);
|
|
826
918
|
const meter: UsageMeter = {};
|
|
827
919
|
try {
|
|
828
920
|
unit.implementation = await runChild(
|
|
829
921
|
flow,
|
|
830
922
|
flow.implementer,
|
|
923
|
+
unit.modelClass,
|
|
831
924
|
repairTask(unit.request, blocked, guidance),
|
|
925
|
+
unit.request.task,
|
|
832
926
|
unit.worktree.cwd,
|
|
833
927
|
`${toolCallId}:flow:${flow.index}:repair`,
|
|
834
928
|
operationSignal,
|