@henryqw/pi-subagent 27.5.4 → 27.6.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/README.md +11 -3
- package/dist/direct-herdr.d.ts +10 -5
- package/dist/direct-herdr.js +9 -4
- package/dist/integration-git.d.ts +3 -1
- package/dist/integration-git.js +2 -1
- package/dist/launch-runtime.d.ts +3 -1
- package/dist/launch-runtime.js +7 -4
- package/dist/runner.d.ts +16 -0
- package/dist/runner.js +42 -16
- package/dist/schema.d.ts +143 -2
- package/dist/schema.js +38 -6
- package/docs/orchestration.md +35 -6
- package/extensions/isolated.ts +56 -3
- package/extensions/result-transport.ts +55 -2
- package/extensions/subagent-command.ts +8 -4
- package/extensions/subagent.ts +89 -23
- package/extensions/workflow.ts +144 -4
- package/package.json +1 -1
package/dist/schema.d.ts
CHANGED
|
@@ -10,6 +10,11 @@ export declare const CheckCommandSchema: Type.TObject<{
|
|
|
10
10
|
command: Type.TString;
|
|
11
11
|
args: Type.TArray<Type.TString>;
|
|
12
12
|
}>;
|
|
13
|
+
/** One display form for a check command; quoted args keep argv boundaries visible. */
|
|
14
|
+
export declare function formatCheckCommand({ command, args }: {
|
|
15
|
+
command: string;
|
|
16
|
+
args: readonly string[];
|
|
17
|
+
}): string;
|
|
13
18
|
export declare const JudgmentSchema: Type.TObject<{
|
|
14
19
|
role: Type.TString;
|
|
15
20
|
modelClass: Type.TUnion<[Type.TLiteral<"fast">, Type.TLiteral<"balanced">, Type.TLiteral<"frontier">, Type.TLiteral<"fav">]>;
|
|
@@ -291,6 +296,7 @@ export type HostAllocationPlan = WorkspaceAllocationPlan | WorkerTabAllocationPl
|
|
|
291
296
|
export type PromptRecord = Static<typeof PromptRecordSchema>;
|
|
292
297
|
export type CommandEvidence = Static<typeof CommandEvidenceSchema>;
|
|
293
298
|
export type CheckBatchEvidence = Static<typeof CheckBatchEvidenceSchema>;
|
|
299
|
+
export type ReusedCheckSource = NonNullable<CommandEvidence["reusedFrom"]>;
|
|
294
300
|
export type ReviewEvidence = Static<typeof ReviewEvidenceSchema>;
|
|
295
301
|
export type WorkerTermination = Static<typeof WorkerTerminationSchema>;
|
|
296
302
|
export declare const CLEANUP_KINDS: readonly ["worker_tab", "workspace", "worktree", "branch"];
|
|
@@ -430,6 +436,16 @@ declare const CommandEvidenceSchema: Type.TObject<{
|
|
|
430
436
|
killed: Type.TBoolean;
|
|
431
437
|
stdout: Type.TString;
|
|
432
438
|
stderr: Type.TString;
|
|
439
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
440
|
+
taskId: Type.TString;
|
|
441
|
+
attempt: Type.TInteger;
|
|
442
|
+
candidate: Type.TObject<{
|
|
443
|
+
branch: Type.TString;
|
|
444
|
+
head: Type.TString;
|
|
445
|
+
index: Type.TString;
|
|
446
|
+
tree: Type.TString;
|
|
447
|
+
}>;
|
|
448
|
+
}>>;
|
|
433
449
|
}>;
|
|
434
450
|
declare const CheckBatchEvidenceSchema: Type.TObject<{
|
|
435
451
|
phase: Type.TUnion<[Type.TLiteral<"preliminary">, Type.TLiteral<"final">]>;
|
|
@@ -452,6 +468,16 @@ declare const CheckBatchEvidenceSchema: Type.TObject<{
|
|
|
452
468
|
killed: Type.TBoolean;
|
|
453
469
|
stdout: Type.TString;
|
|
454
470
|
stderr: Type.TString;
|
|
471
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
472
|
+
taskId: Type.TString;
|
|
473
|
+
attempt: Type.TInteger;
|
|
474
|
+
candidate: Type.TObject<{
|
|
475
|
+
branch: Type.TString;
|
|
476
|
+
head: Type.TString;
|
|
477
|
+
index: Type.TString;
|
|
478
|
+
tree: Type.TString;
|
|
479
|
+
}>;
|
|
480
|
+
}>>;
|
|
455
481
|
}>>;
|
|
456
482
|
passed: Type.TBoolean;
|
|
457
483
|
at: Type.TInteger;
|
|
@@ -619,6 +645,16 @@ declare const TaskAttemptSchema: Type.TObject<{
|
|
|
619
645
|
killed: Type.TBoolean;
|
|
620
646
|
stdout: Type.TString;
|
|
621
647
|
stderr: Type.TString;
|
|
648
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
649
|
+
taskId: Type.TString;
|
|
650
|
+
attempt: Type.TInteger;
|
|
651
|
+
candidate: Type.TObject<{
|
|
652
|
+
branch: Type.TString;
|
|
653
|
+
head: Type.TString;
|
|
654
|
+
index: Type.TString;
|
|
655
|
+
tree: Type.TString;
|
|
656
|
+
}>;
|
|
657
|
+
}>>;
|
|
622
658
|
}>>;
|
|
623
659
|
passed: Type.TBoolean;
|
|
624
660
|
at: Type.TInteger;
|
|
@@ -807,6 +843,16 @@ declare const ChangesetTaskStateSchema: Type.TObject<{
|
|
|
807
843
|
killed: Type.TBoolean;
|
|
808
844
|
stdout: Type.TString;
|
|
809
845
|
stderr: Type.TString;
|
|
846
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
847
|
+
taskId: Type.TString;
|
|
848
|
+
attempt: Type.TInteger;
|
|
849
|
+
candidate: Type.TObject<{
|
|
850
|
+
branch: Type.TString;
|
|
851
|
+
head: Type.TString;
|
|
852
|
+
index: Type.TString;
|
|
853
|
+
tree: Type.TString;
|
|
854
|
+
}>;
|
|
855
|
+
}>>;
|
|
810
856
|
}>>;
|
|
811
857
|
passed: Type.TBoolean;
|
|
812
858
|
at: Type.TInteger;
|
|
@@ -1086,6 +1132,16 @@ declare const TaskStateSchema: Type.TUnion<[Type.TObject<{
|
|
|
1086
1132
|
killed: Type.TBoolean;
|
|
1087
1133
|
stdout: Type.TString;
|
|
1088
1134
|
stderr: Type.TString;
|
|
1135
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
1136
|
+
taskId: Type.TString;
|
|
1137
|
+
attempt: Type.TInteger;
|
|
1138
|
+
candidate: Type.TObject<{
|
|
1139
|
+
branch: Type.TString;
|
|
1140
|
+
head: Type.TString;
|
|
1141
|
+
index: Type.TString;
|
|
1142
|
+
tree: Type.TString;
|
|
1143
|
+
}>;
|
|
1144
|
+
}>>;
|
|
1089
1145
|
}>>;
|
|
1090
1146
|
passed: Type.TBoolean;
|
|
1091
1147
|
at: Type.TInteger;
|
|
@@ -1206,6 +1262,16 @@ declare const IntegrationCandidateSchema: Type.TObject<{
|
|
|
1206
1262
|
killed: Type.TBoolean;
|
|
1207
1263
|
stdout: Type.TString;
|
|
1208
1264
|
stderr: Type.TString;
|
|
1265
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
1266
|
+
taskId: Type.TString;
|
|
1267
|
+
attempt: Type.TInteger;
|
|
1268
|
+
candidate: Type.TObject<{
|
|
1269
|
+
branch: Type.TString;
|
|
1270
|
+
head: Type.TString;
|
|
1271
|
+
index: Type.TString;
|
|
1272
|
+
tree: Type.TString;
|
|
1273
|
+
}>;
|
|
1274
|
+
}>>;
|
|
1209
1275
|
}>>;
|
|
1210
1276
|
passed: Type.TBoolean;
|
|
1211
1277
|
at: Type.TInteger;
|
|
@@ -1350,6 +1416,16 @@ declare const IntegrationGenerationSchema: Type.TObject<{
|
|
|
1350
1416
|
killed: Type.TBoolean;
|
|
1351
1417
|
stdout: Type.TString;
|
|
1352
1418
|
stderr: Type.TString;
|
|
1419
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
1420
|
+
taskId: Type.TString;
|
|
1421
|
+
attempt: Type.TInteger;
|
|
1422
|
+
candidate: Type.TObject<{
|
|
1423
|
+
branch: Type.TString;
|
|
1424
|
+
head: Type.TString;
|
|
1425
|
+
index: Type.TString;
|
|
1426
|
+
tree: Type.TString;
|
|
1427
|
+
}>;
|
|
1428
|
+
}>>;
|
|
1353
1429
|
}>>;
|
|
1354
1430
|
passed: Type.TBoolean;
|
|
1355
1431
|
at: Type.TInteger;
|
|
@@ -1470,6 +1546,16 @@ export declare const IntegrationStateSchema: Type.TObject<{
|
|
|
1470
1546
|
killed: Type.TBoolean;
|
|
1471
1547
|
stdout: Type.TString;
|
|
1472
1548
|
stderr: Type.TString;
|
|
1549
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
1550
|
+
taskId: Type.TString;
|
|
1551
|
+
attempt: Type.TInteger;
|
|
1552
|
+
candidate: Type.TObject<{
|
|
1553
|
+
branch: Type.TString;
|
|
1554
|
+
head: Type.TString;
|
|
1555
|
+
index: Type.TString;
|
|
1556
|
+
tree: Type.TString;
|
|
1557
|
+
}>;
|
|
1558
|
+
}>>;
|
|
1473
1559
|
}>>;
|
|
1474
1560
|
passed: Type.TBoolean;
|
|
1475
1561
|
at: Type.TInteger;
|
|
@@ -1590,6 +1676,16 @@ export declare const IntegrationStateSchema: Type.TObject<{
|
|
|
1590
1676
|
killed: Type.TBoolean;
|
|
1591
1677
|
stdout: Type.TString;
|
|
1592
1678
|
stderr: Type.TString;
|
|
1679
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
1680
|
+
taskId: Type.TString;
|
|
1681
|
+
attempt: Type.TInteger;
|
|
1682
|
+
candidate: Type.TObject<{
|
|
1683
|
+
branch: Type.TString;
|
|
1684
|
+
head: Type.TString;
|
|
1685
|
+
index: Type.TString;
|
|
1686
|
+
tree: Type.TString;
|
|
1687
|
+
}>;
|
|
1688
|
+
}>>;
|
|
1593
1689
|
}>>;
|
|
1594
1690
|
passed: Type.TBoolean;
|
|
1595
1691
|
at: Type.TInteger;
|
|
@@ -1918,6 +2014,16 @@ declare const RunStateSchema: Type.TObject<{
|
|
|
1918
2014
|
killed: Type.TBoolean;
|
|
1919
2015
|
stdout: Type.TString;
|
|
1920
2016
|
stderr: Type.TString;
|
|
2017
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
2018
|
+
taskId: Type.TString;
|
|
2019
|
+
attempt: Type.TInteger;
|
|
2020
|
+
candidate: Type.TObject<{
|
|
2021
|
+
branch: Type.TString;
|
|
2022
|
+
head: Type.TString;
|
|
2023
|
+
index: Type.TString;
|
|
2024
|
+
tree: Type.TString;
|
|
2025
|
+
}>;
|
|
2026
|
+
}>>;
|
|
1921
2027
|
}>>;
|
|
1922
2028
|
passed: Type.TBoolean;
|
|
1923
2029
|
at: Type.TInteger;
|
|
@@ -2031,6 +2137,16 @@ declare const RunStateSchema: Type.TObject<{
|
|
|
2031
2137
|
killed: Type.TBoolean;
|
|
2032
2138
|
stdout: Type.TString;
|
|
2033
2139
|
stderr: Type.TString;
|
|
2140
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
2141
|
+
taskId: Type.TString;
|
|
2142
|
+
attempt: Type.TInteger;
|
|
2143
|
+
candidate: Type.TObject<{
|
|
2144
|
+
branch: Type.TString;
|
|
2145
|
+
head: Type.TString;
|
|
2146
|
+
index: Type.TString;
|
|
2147
|
+
tree: Type.TString;
|
|
2148
|
+
}>;
|
|
2149
|
+
}>>;
|
|
2034
2150
|
}>>;
|
|
2035
2151
|
passed: Type.TBoolean;
|
|
2036
2152
|
at: Type.TInteger;
|
|
@@ -2151,6 +2267,16 @@ declare const RunStateSchema: Type.TObject<{
|
|
|
2151
2267
|
killed: Type.TBoolean;
|
|
2152
2268
|
stdout: Type.TString;
|
|
2153
2269
|
stderr: Type.TString;
|
|
2270
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
2271
|
+
taskId: Type.TString;
|
|
2272
|
+
attempt: Type.TInteger;
|
|
2273
|
+
candidate: Type.TObject<{
|
|
2274
|
+
branch: Type.TString;
|
|
2275
|
+
head: Type.TString;
|
|
2276
|
+
index: Type.TString;
|
|
2277
|
+
tree: Type.TString;
|
|
2278
|
+
}>;
|
|
2279
|
+
}>>;
|
|
2154
2280
|
}>>;
|
|
2155
2281
|
passed: Type.TBoolean;
|
|
2156
2282
|
at: Type.TInteger;
|
|
@@ -2281,6 +2407,16 @@ declare const RunStateSchema: Type.TObject<{
|
|
|
2281
2407
|
killed: Type.TBoolean;
|
|
2282
2408
|
stdout: Type.TString;
|
|
2283
2409
|
stderr: Type.TString;
|
|
2410
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
2411
|
+
taskId: Type.TString;
|
|
2412
|
+
attempt: Type.TInteger;
|
|
2413
|
+
candidate: Type.TObject<{
|
|
2414
|
+
branch: Type.TString;
|
|
2415
|
+
head: Type.TString;
|
|
2416
|
+
index: Type.TString;
|
|
2417
|
+
tree: Type.TString;
|
|
2418
|
+
}>;
|
|
2419
|
+
}>>;
|
|
2284
2420
|
}>>;
|
|
2285
2421
|
passed: Type.TBoolean;
|
|
2286
2422
|
at: Type.TInteger;
|
|
@@ -2445,11 +2581,16 @@ export declare function parseIdOnly(value: unknown): IdOnly;
|
|
|
2445
2581
|
export declare function parseResumeRequest(value: unknown): ResumeRequest;
|
|
2446
2582
|
export declare function sameIdentity(left: WorkspaceIdentity, right: WorkspaceIdentity): boolean;
|
|
2447
2583
|
export declare function isCleanCommitted(identity: WorkspaceIdentity): boolean;
|
|
2448
|
-
|
|
2584
|
+
/** Reused results pass only when they still cite one of `sources` exactly; no sources accept no reuse. */
|
|
2585
|
+
export declare function checkBatchPasses(evidence: CheckBatchEvidence | undefined, checks: readonly CheckCommand[], candidate: WorkspaceIdentity, sources?: readonly IntegrationCandidate[]): boolean;
|
|
2586
|
+
/** Candidates exactly staged in this generation; only they can supply reused final results. */
|
|
2587
|
+
export declare function stagedCheckSources(generation: IntegrationGeneration, candidates: readonly IntegrationCandidate[]): IntegrationCandidate[];
|
|
2588
|
+
/** A passed preliminary result stands in for a final check only on the same tree with identical command and argv. */
|
|
2589
|
+
export declare function reusableCheckSource(check: CheckCommand, tip: WorkspaceIdentity, sources: readonly IntegrationCandidate[]): ReusedCheckSource | undefined;
|
|
2449
2590
|
export declare function reviewEvidencePasses(evidence: ReviewEvidence | undefined, phase: ReviewEvidence["phase"], criterion: string, base: WorkspaceIdentity, tip: WorkspaceIdentity): boolean;
|
|
2450
2591
|
/** Check the persisted integration projection against the request before recovery or promotion. */
|
|
2451
2592
|
export declare function parseIntegrationState(value: unknown, request: ExecuteRequest): IntegrationState;
|
|
2452
2593
|
/** Passing final evidence is bound to the exact assembled identity, not any worker tip. */
|
|
2453
|
-
export declare function integrationGenerationPasses(generation: IntegrationGeneration, request: ExecuteRequest): boolean;
|
|
2594
|
+
export declare function integrationGenerationPasses(generation: IntegrationGeneration, request: ExecuteRequest, candidates: readonly IntegrationCandidate[]): boolean;
|
|
2454
2595
|
export declare function parseRunState(value: unknown): RunState;
|
|
2455
2596
|
export {};
|
package/dist/schema.js
CHANGED
|
@@ -26,6 +26,10 @@ export const CheckCommandSchema = Type.Object({
|
|
|
26
26
|
command: TextSchema,
|
|
27
27
|
args: Type.Array(Type.String({ maxLength: 16_000 }), { maxItems: 128 }),
|
|
28
28
|
}, { additionalProperties: false });
|
|
29
|
+
/** One display form for a check command; quoted args keep argv boundaries visible. */
|
|
30
|
+
export function formatCheckCommand({ command, args }) {
|
|
31
|
+
return [command, ...args.map((arg) => JSON.stringify(arg))].join(" ");
|
|
32
|
+
}
|
|
29
33
|
export const JudgmentSchema = Type.Object({
|
|
30
34
|
role: RoleNameSchema,
|
|
31
35
|
modelClass: ModelClassSchema,
|
|
@@ -219,6 +223,12 @@ const CommandEvidenceSchema = Type.Object({
|
|
|
219
223
|
killed: Type.Boolean(),
|
|
220
224
|
stdout: Type.String({ maxLength: MAX_PERSISTED_RUNTIME_TEXT_BYTES }),
|
|
221
225
|
stderr: Type.String({ maxLength: MAX_PERSISTED_RUNTIME_TEXT_BYTES }),
|
|
226
|
+
// A final result copied from a staged candidate's passing preliminary result on the same tree.
|
|
227
|
+
reusedFrom: Type.Optional(Type.Object({
|
|
228
|
+
taskId: IdSchema,
|
|
229
|
+
attempt: Type.Integer({ minimum: 1, maximum: 2 }),
|
|
230
|
+
candidate: WorkspaceSchema,
|
|
231
|
+
}, { additionalProperties: false })),
|
|
222
232
|
}, { additionalProperties: false });
|
|
223
233
|
const CheckBatchEvidenceSchema = Type.Object({
|
|
224
234
|
phase: Type.Union([Type.Literal("preliminary"), Type.Literal("final")]),
|
|
@@ -672,6 +682,9 @@ function validateCheckBatchEvidence(evidence, checks, field) {
|
|
|
672
682
|
|| evidence.results.some((result, index) => !sameCheck(result, checks[index]))) {
|
|
673
683
|
throw new Error(`${field} does not retain every exact declared command and argv.`);
|
|
674
684
|
}
|
|
685
|
+
if (evidence.results.some((result) => result.reusedFrom && (evidence.phase !== "final" || result.code !== 0 || result.killed))) {
|
|
686
|
+
throw new Error(`${field} has invalid reused check evidence.`);
|
|
687
|
+
}
|
|
675
688
|
const commandsPassed = evidence.results.every((result) => result.code === 0 && !result.killed);
|
|
676
689
|
const identityMatches = sameIdentity(evidence.candidate, evidence.identityAfter);
|
|
677
690
|
if (evidence.passed !== (commandsPassed && identityMatches)) {
|
|
@@ -686,13 +699,32 @@ function validateCheckBatchEvidence(evidence, checks, field) {
|
|
|
686
699
|
throw new Error(`${field} retains non-diagnostic command output.`);
|
|
687
700
|
}
|
|
688
701
|
}
|
|
689
|
-
|
|
702
|
+
/** Reused results pass only when they still cite one of `sources` exactly; no sources accept no reuse. */
|
|
703
|
+
export function checkBatchPasses(evidence, checks, candidate, sources = []) {
|
|
690
704
|
return Boolean(evidence
|
|
691
705
|
&& evidence.passed
|
|
692
706
|
&& sameIdentity(evidence.candidate, candidate)
|
|
693
707
|
&& sameIdentity(evidence.identityAfter, candidate)
|
|
694
708
|
&& evidence.results.length === checks.length
|
|
695
|
-
&& evidence.results.every((result, index) => sameCheck(result, checks[index]) && result.code === 0 && !result.killed
|
|
709
|
+
&& evidence.results.every((result, index) => sameCheck(result, checks[index]) && result.code === 0 && !result.killed
|
|
710
|
+
&& (!result.reusedFrom || reusedCheckExact(result, result.reusedFrom, candidate, sources))));
|
|
711
|
+
}
|
|
712
|
+
/** Candidates exactly staged in this generation; only they can supply reused final results. */
|
|
713
|
+
export function stagedCheckSources(generation, candidates) {
|
|
714
|
+
return generation.stages.flatMap((stage) => candidates.filter((candidate) => stage.status === "staged"
|
|
715
|
+
&& candidate.taskId === stage.taskId && candidate.attempt === stage.attempt && sameIdentity(candidate.tip, stage.source)));
|
|
716
|
+
}
|
|
717
|
+
/** A passed preliminary result stands in for a final check only on the same tree with identical command and argv. */
|
|
718
|
+
export function reusableCheckSource(check, tip, sources) {
|
|
719
|
+
const source = sources.find(({ tip: candidate, checks }) => candidate.tree === tip.tree
|
|
720
|
+
&& checks.phase === "preliminary" && checks.passed
|
|
721
|
+
&& sameIdentity(checks.candidate, candidate) && sameIdentity(checks.identityAfter, candidate)
|
|
722
|
+
&& checks.results.some((result) => !result.reusedFrom && sameCheck(result, check) && result.code === 0 && !result.killed));
|
|
723
|
+
return source && { taskId: source.taskId, attempt: source.attempt, candidate: { ...source.tip } };
|
|
724
|
+
}
|
|
725
|
+
function reusedCheckExact(check, reused, tip, sources) {
|
|
726
|
+
return Boolean(reusableCheckSource(check, tip, sources.filter((source) => source.taskId === reused.taskId
|
|
727
|
+
&& source.attempt === reused.attempt && sameIdentity(source.tip, reused.candidate))));
|
|
696
728
|
}
|
|
697
729
|
export function reviewEvidencePasses(evidence, phase, criterion, base, tip) {
|
|
698
730
|
return Boolean(evidence
|
|
@@ -895,7 +927,7 @@ export function parseIntegrationState(value, request) {
|
|
|
895
927
|
if (generation.checks && !sameIdentity(generation.checks.candidate, generation.combinedTip)) {
|
|
896
928
|
throw new Error(`Generation ${generation.number} checks target another tip.`);
|
|
897
929
|
}
|
|
898
|
-
const passes = integrationGenerationPasses(generation, request);
|
|
930
|
+
const passes = integrationGenerationPasses(generation, request, state.candidates);
|
|
899
931
|
const promotion = generation.promotion;
|
|
900
932
|
if (promotion && (!passes || generation.failure !== undefined
|
|
901
933
|
|| !sameIdentity(promotion.expectedMain, generation.expectedMain)
|
|
@@ -958,12 +990,12 @@ export function parseIntegrationState(value, request) {
|
|
|
958
990
|
return state;
|
|
959
991
|
}
|
|
960
992
|
/** Passing final evidence is bound to the exact assembled identity, not any worker tip. */
|
|
961
|
-
export function integrationGenerationPasses(generation, request) {
|
|
993
|
+
export function integrationGenerationPasses(generation, request, candidates) {
|
|
962
994
|
return Boolean(generation.combinedTip && isCleanCommitted(generation.combinedTip)
|
|
963
995
|
&& generation.stages.length === generation.order.length
|
|
964
996
|
&& generation.stages.every((stage, index) => stage.status === "staged" && stage.taskId === generation.order[index])
|
|
965
997
|
&& generation.checks?.phase === "final"
|
|
966
|
-
&& checkBatchPasses(generation.checks, request.finalChecks, generation.combinedTip)
|
|
998
|
+
&& checkBatchPasses(generation.checks, request.finalChecks, generation.combinedTip, stagedCheckSources(generation, candidates))
|
|
967
999
|
&& (!request.finalJudgment || reviewEvidencePasses(generation.review, "final", request.finalJudgment.criterion, generation.expectedMain, generation.combinedTip)));
|
|
968
1000
|
}
|
|
969
1001
|
export function parseRunState(value) {
|
|
@@ -1297,7 +1329,7 @@ export function parseRunState(value) {
|
|
|
1297
1329
|
&& generation.combinedTip.tree === state.final.identity.tree
|
|
1298
1330
|
&& generation.promotion?.mainAfter && sameIdentity(generation.promotion.mainAfter, state.final.identity)
|
|
1299
1331
|
&& JSON.stringify(generation.checks) === JSON.stringify(state.final.checks)
|
|
1300
|
-
&& integrationGenerationPasses(generation, request))
|
|
1332
|
+
&& integrationGenerationPasses(generation, request, state.integration.candidates))
|
|
1301
1333
|
: checkBatchPasses(state.final.checks, request.finalChecks, state.final.identity);
|
|
1302
1334
|
if (!acceptedChecks)
|
|
1303
1335
|
throw new Error("Accepted request lacks passing final checks on its exact identity.");
|
package/docs/orchestration.md
CHANGED
|
@@ -69,6 +69,7 @@ Each entry has:
|
|
|
69
69
|
| `name` | yes | Short display name without terminal controls |
|
|
70
70
|
| `task` | yes | Non-empty task packet |
|
|
71
71
|
| `access` | yes | `read` or `write`; Main's honest classification, not enforced |
|
|
72
|
+
| `paths` | no | Write tasks only: 1 to 32 repo-relative globs (`path.matchesGlob` syntax, such as `src/**`). The change report flags changed paths outside them. A report only, not enforced |
|
|
72
73
|
| `continueFrom` | no | Direct reference such as `D7.1`; continues that finished worker's Pi session. `role` must equal the recorded Role |
|
|
73
74
|
| `kind` | no | `text` only (default); direct `changeset` is rejected |
|
|
74
75
|
| `modelClass` | no | `fast`, `balanced`, `frontier`, or `fav` route |
|
|
@@ -78,6 +79,12 @@ An explicit model must support the route's thinking level. A launched child is n
|
|
|
78
79
|
|
|
79
80
|
Direct `changeset` is rejected; use isolated mode for checked integration. Resource validation and recursive-delegation exclusions apply to every task. A request becomes a write request when any task declares `access: "write"`.
|
|
80
81
|
|
|
82
|
+
### Dry run
|
|
83
|
+
|
|
84
|
+
Set `"dryRun": true` on a direct or isolated `delegate_task` request to check it without a launch. The dry run does the same checks as a launch: request shape, Role lookup, route and model with thinking level, Skills, tools, MCP allowlist, Role extensions, config, and `continueFrom` eligibility. For an isolated request, it also checks the task graph and that Main is clean and committed. If the request has a changeset task, it also checks the canonical root `package.json` test script and adds `pnpm test` to the final checks. A failed check gives the same error as a launch.
|
|
85
|
+
|
|
86
|
+
The result is a bounded plan. It shows each task's Role, resolved `provider/model`, thinking level, access or kind, dependencies, and checks. An isolated plan also shows the final checks. The dry run makes no Herdr call, Herdr tab, session record, saved state, worktree, lease, checkout admission, or follow-up. The plan lists each check it cannot do without such an effect as `Not checked: <reason>`. Examples are the Herdr host preflight, checkout admission, and the stop of a continued agent.
|
|
87
|
+
|
|
81
88
|
Read requests skip checkout admission and return plain text. Parallel `tasks` run concurrently up to `maxSubagents`; chains stay ordered. The extension does not enforce `read`. Results stay in request order.
|
|
82
89
|
|
|
83
90
|
Direct tasks use the declared tools, extensions, Skills, and MCP servers of the selected Role, including resources that can write. Authorization comes from your request and Main's bounded task packet, not from the capabilities of the Role. Name the permitted paths, actions, and exclusions. Preserve unrelated edits and staged changes. Authorize commits or external actions explicitly.
|
|
@@ -134,18 +141,31 @@ The extension retrieves each final answer from its exact Pi session file, not it
|
|
|
134
141
|
|
|
135
142
|
The tool returns a task ID and the first Herdr tab after launch, not the answer. The extension observes each worker and sends one result to Main when the workflow finishes. If Main is busy, Pi queues the result after the current turn. If Main is idle, the result starts a turn. The extension does not report a blocked, stalled, unknown, or truncated result as success.
|
|
136
143
|
|
|
137
|
-
In the result, each launched worker has a line with its model, thinking level, measured task tokens, and elapsed time. For example: `✓ [1/2] Check · worker · provider/model · thinking high · 1.5k tok · 42s`. If Pi recorded no usage, the line shows `tokens unknown`. Failed and stopped workers show the usage that the extension measured until they ended. A worker that the extension skipped before launch shows no usage.
|
|
144
|
+
In the result, each launched worker has a line with its model, thinking level, measured task tokens, cost, and elapsed time. For example: `✓ [1/2] Check · worker · provider/model · thinking high · 1.5k tok · $0.12 · 42s`. The first line also shows the request total, such as `total $0.12`. Unknown and zero cost are not shown. If Pi recorded no usage, the line shows `tokens unknown`. Failed and stopped workers show the usage that the extension measured until they ended. A worker that the extension skipped before launch shows no usage.
|
|
138
145
|
|
|
139
|
-
The result details contain the same values in each entry as `model`, `thinkingLevel`, `tokens`, and `elapsedMs`. When
|
|
146
|
+
The result details contain the same values in each entry as `model`, `thinkingLevel`, `tokens`, `cost`, and `elapsedMs`, and the request total as `cost`. When a value is unknown, its field is absent. If a task fails after its worker produced text, the result keeps a bounded excerpt marked as partial output. This excerpt is never success.
|
|
140
147
|
|
|
141
148
|
Every worker result delivered to Main starts with a header. The header states that the text is the output of the worker and carries no user authority. The extension inserts a backslash in text that imitates harness framing. Examples are `<system-reminder>`-style tags and lines that start with `Human:` or `Assistant:`. Nothing else changes. Isolated status and follow-up answer previews use the same header.
|
|
142
149
|
|
|
143
150
|
A session switch or shutdown stops observation and preserves tab identities for recovery. To inspect exact tabs, agents, and Pi session files, open `/subagent` on the session branch. This includes tabs launched after the first. The menu has one row for each workflow.
|
|
144
151
|
|
|
145
|
-
A single worker shows its reference, Role, and name (`D7.1 · worker · Search audit`). A multi-worker workflow shows its reference and worker count (`D8 · 3 workers`). When you select it, the menu lists each recorded worker by reference, Role, and name with its exact IDs. References and names help you match rows. The extension never uses them to decide ownership.
|
|
152
|
+
A single worker shows its reference, Role, and name (`D7.1 · worker · Search audit`). A multi-worker workflow shows its reference and worker count (`D8 · 3 workers`). When you select it, the menu lists each recorded worker by reference, Role, and name with its exact IDs. After Main receives the result, the row shows the request cost and each worker line shows its cost. References and names help you match rows. The extension never uses them to decide ownership.
|
|
146
153
|
|
|
147
154
|
Older records without saved references show their saved allocation label or exact task ID. A `recorded only` row does not prove that its workers run or finished. The menu labels locally observed work separately.
|
|
148
155
|
|
|
156
|
+
### Checkout change reports
|
|
157
|
+
|
|
158
|
+
For each task in a write request, the extension takes a Git snapshot of Main's checkout before the worker starts and again when the task ends (success, failure, blocked work, or stop). A snapshot contains the `HEAD` OID and `git status --porcelain=v1 -z --untracked-files=all`, with the content identity of each dirty path. The result of the task then lists:
|
|
159
|
+
|
|
160
|
+
- New commits from `git rev-list before..after`, as a short OID and subject. If `HEAD` moved to a commit that does not descend from the start `HEAD`, the report says so.
|
|
161
|
+
- Paths with a changed status or content, and the paths that the new commits changed. If the task declares `paths`, each changed path outside them shows `outside declared paths`.
|
|
162
|
+
|
|
163
|
+
The report shows at most 10 commits and 20 paths, and it counts the omitted items. Paths outside the declaration come first. The same report is in the `checkout` field of the result details entry. The report does not enforce scope. Checkout admission blocks Main writes during a write request, so the report assigns changes to the worker. It cannot exclude changes from other processes.
|
|
164
|
+
|
|
165
|
+
For a read request, the extension compares the same snapshot at launch and when each task ends. If `HEAD`, status, or dirty content changed, the result of that task shows `checkout changed during task; result may be stale`.
|
|
166
|
+
|
|
167
|
+
Outside a Git repository, the extension makes no report and the task continues. If a snapshot fails, the task does not fail; the result shows `change report unavailable: <reason>` (or `stale-read check unavailable: <reason>` for a read request).
|
|
168
|
+
|
|
149
169
|
### Status widgets
|
|
150
170
|
|
|
151
171
|
The TUI has two separate compact status widgets. Each widget has one row for each visible worker or task. `D` marks direct work, and `I` marks isolated work. Badges such as `[S]` and `[I3]` identify the Role initial. Isolated badges also include the model class: `1` fast, `2` balanced, `3` frontier, `*` fav.
|
|
@@ -154,13 +174,13 @@ Each direct row starts with a saved direct reference such as `D7.1`. `D7` is the
|
|
|
154
174
|
|
|
155
175
|
A session switch can move a workflow into a session that already uses its number. In that case, the workflow gets a new number there. An existing Herdr tab keeps its original launch label.
|
|
156
176
|
|
|
157
|
-
The direct widget shows the launched model, thinking level, elapsed time,
|
|
177
|
+
The direct widget shows the launched model, thinking level, elapsed time, measured task tokens (`— tok` until usage is available), and cost next to the tokens (such as `$0.12`; not shown when unknown or zero). Failed rows show before working and completed rows. When rows overflow, `… N more` includes counts by status.
|
|
158
178
|
|
|
159
179
|
The isolated widget shows the recorded task state and any retained workspace. It does not show reliable live model, thinking, or token readings from Herdr. Attention shows before ordinary work, and overflow uses `+N more · /subagent`. An aborted request with visible tasks has its own `request aborted` row. This row does not change the recorded statuses of the tasks.
|
|
160
180
|
|
|
161
181
|
A committed worker without a checked candidate can show attention, not readiness. Status glyphs use the active TUI theme. The text also names the status without color.
|
|
162
182
|
|
|
163
|
-
The direct widget and result summaries show measured tokens for the current task turn. This count adds input and output tokens from completed assistant usage records. It excludes cache-read and cache-write tokens. A continued worker counts only its new task turn. Missing usage stays unknown. This display count does not change token-limit accounting.
|
|
183
|
+
The direct widget and result summaries show measured tokens for the current task turn. This count adds input and output tokens from completed assistant usage records. It excludes cache-read and cache-write tokens. A continued worker counts only its new task turn. Missing usage stays unknown. This display count does not change token-limit accounting. The cost is the sum of `usage.cost.total` from the same records, read in the same session read. Isolated task state records no usage, so isolated rows show no tokens or cost.
|
|
164
184
|
|
|
165
185
|
A ready isolated candidate is **not promoted** to Main. To reload the menu and the isolated widget from saved state, choose **Refresh** in `/subagent`. Refresh does not recover or change a request. Use `/subagent` or `subagent_status` for exact identities, failures, and recovery.
|
|
166
186
|
|
|
@@ -231,7 +251,7 @@ Admission and worker-wave dispatch pin Main's exact committed branch, HEAD, and
|
|
|
231
251
|
|
|
232
252
|
Main selects exact candidates through `subagent_stage` into an owned integration checkout in explicit order. Each stage is a committed merge of the candidate, not a mutation of Main. Conflicts retain the checkout for Main to resolve; `resolve` verifies the exact two-parent merge instead of replaying an uncertain merge. Main can explicitly run dependent tasks from an exact staged tip with `subagent_integrate advance` and stage their results afterward. Independent candidates may be staged without re-running workers.
|
|
233
253
|
|
|
234
|
-
Once all selected work is staged, Main calls `subagent_integrate validate` on the exact clean combined tip. It acknowledges the durably saved validation intent and continues in the session-owned background, independent of later turn cancellation; session replacement/shutdown still cancels it. A follow-up reports completion; status remains authoritative if delivery is missed. This runs every final check, including `pnpm test`, and optional final judgment in the integration checkout before any Main mutation. A failed check/review can be followed by one Main-authored committed correction in that checkout, recorded with `correct`, then another `validate`.
|
|
254
|
+
Once all selected work is staged, Main calls `subagent_integrate validate` on the exact clean combined tip. It acknowledges the durably saved validation intent and continues in the session-owned background, independent of later turn cancellation; session replacement/shutdown still cancels it. A follow-up reports completion; status remains authoritative if delivery is missed. This runs every final check, including `pnpm test`, unless [exact check reuse](#validate-and-promote) applies, and optional final judgment in the integration checkout before any Main mutation. A failed check/review can be followed by one Main-authored committed correction in that checkout, recorded with `correct`, then another `validate`.
|
|
235
255
|
|
|
236
256
|
Staging, combined validation, and `promote` require clean Main at the recorded committed identity. Dirty edits block these actions even when the checked candidate stays retained. The extension never replays or merges dirty work automatically. `promote` requires exact passing combined evidence and unchanged Main and uses a guarded update to Main. Only proven promotion leads to selected-worker termination and verified cleanup. A changed candidate, integration tip, or Main identity invalidates prior evidence or blocks promotion; review accepts only exact `PASS`.
|
|
237
257
|
|
|
@@ -299,6 +319,15 @@ Post-promotion cleanup handles each promoted stage separately. A blocked termina
|
|
|
299
319
|
|
|
300
320
|
Validation acknowledges only after saving the exact generation's validation intent. Checks and final judgment then run in the background. Later turn cancellation does not cancel acknowledged validation; session replacement or shutdown does. Acknowledgement is not passing evidence. Inspect status if the completion follow-up fails.
|
|
301
321
|
|
|
322
|
+
Validation reuses a passed preliminary result for a final check, and does not run that check again, only when all of these conditions are true:
|
|
323
|
+
|
|
324
|
+
- The candidate is staged in this generation.
|
|
325
|
+
- The tree OID of the combined tip is equal to the tree OID of the candidate. This usually occurs when you stage one candidate onto unchanged pinned Main.
|
|
326
|
+
- The saved preliminary checks of the candidate passed, and their `candidate` and `identityAfter` are equal to the candidate tip.
|
|
327
|
+
- The preliminary result has the identical command and argv, exit code 0, and no timeout or signal.
|
|
328
|
+
|
|
329
|
+
Any mismatch runs the check normally. Each reused result records `reusedFrom` with `taskId`, `attempt`, and `candidate`. Status text and the validation follow-up show each reused command. Promotion, reconciliation, and state loading accept a reused result only after they prove these conditions again from saved identities. Final judgment still runs. Reuse assumes that checks use only tracked content. Ignored files, for example `node_modules`, can be different between the worker and integration worktrees.
|
|
330
|
+
|
|
302
331
|
#### Refresh
|
|
303
332
|
|
|
304
333
|
```text
|
package/extensions/isolated.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { basename } from "node:path";
|
|
|
2
2
|
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
3
3
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
5
|
-
import { utf8Prefix, type EphemeralSubagentExecutor } from "../dist/ephemeral.js";
|
|
5
|
+
import { capEphemeralSubagentOutput, utf8Prefix, type EphemeralSubagentExecutor } from "../dist/ephemeral.js";
|
|
6
6
|
import type { EffectiveExecutionPolicy } from "./config.ts";
|
|
7
7
|
import {
|
|
8
8
|
createCanonicalGitRootResolver,
|
|
@@ -16,7 +16,7 @@ import { HerdrHostRuntime } from "../dist/herdr-runtime.js";
|
|
|
16
16
|
import { inspectPruneCandidates, listHerdrPanes, removePruneCandidates, type PruneGuards, type PruneItem } from "../dist/prune.js";
|
|
17
17
|
import { runProcess as directRunProcess } from "../dist/process.js";
|
|
18
18
|
import { RoleLaunchRuntime, type LaunchRuntimeOptions } from "../dist/launch-runtime.js";
|
|
19
|
-
import { IsolatedRunner, nextIntegrationAction, type OperationContext, type RunResponse } from "../dist/runner.js";
|
|
19
|
+
import { IsolatedRunner, nextIntegrationAction, type ExecutePlan, type OperationContext, type RunResponse } from "../dist/runner.js";
|
|
20
20
|
import {
|
|
21
21
|
IdOnlySchema,
|
|
22
22
|
ResumeRequestParameters,
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
parseIntegrationAction,
|
|
26
26
|
parseStageRequest,
|
|
27
27
|
parseIdOnly,
|
|
28
|
+
formatCheckCommand,
|
|
28
29
|
parseResumeRequest,
|
|
29
30
|
sameIdentity,
|
|
30
31
|
StatusOutputSchema,
|
|
@@ -208,7 +209,7 @@ function publicFailedCheck(evidence: CheckBatchEvidence | undefined) {
|
|
|
208
209
|
phase: evidence.phase,
|
|
209
210
|
identityChanged: !sameIdentity(evidence.candidate, evidence.identityAfter),
|
|
210
211
|
...(result ? {
|
|
211
|
-
command: boundedPublicText(
|
|
212
|
+
command: boundedPublicText(formatCheckCommand(result)),
|
|
212
213
|
code: result.code,
|
|
213
214
|
killed: result.killed,
|
|
214
215
|
...(result.stdout ? { stdout: boundedPublicText(result.stdout) } : {}),
|
|
@@ -334,6 +335,8 @@ function publicState(state: RunState, verdictMaxBytes: number, preferredTaskId?:
|
|
|
334
335
|
...(combinedTip ? { combinedTip } : {}),
|
|
335
336
|
...(correction ? { correction } : {}),
|
|
336
337
|
...(checks ? { checked: checks.passed, failedCheck: publicFailedCheck(checks) } : {}),
|
|
338
|
+
...(checks?.results.some((result) => result.reusedFrom) ? { reusedChecks: checks.results.flatMap((result) =>
|
|
339
|
+
result.reusedFrom ? [{ command: boundedPublicText(formatCheckCommand(result)), reusedFrom: result.reusedFrom }] : []) } : {}),
|
|
337
340
|
...(review ? { reviewed: review.passed, failedReview: publicFailedReview(review, verdictMaxBytes) } : {}),
|
|
338
341
|
...(promotion ? { promotion: promotion.status, ...(promotion.mainAfter ? { mainAfter: promotion.mainAfter } : {}),
|
|
339
342
|
...(promotion.failure ? { promotionFailure: boundedPublicText(promotion.failure) } : {}) } : {}),
|
|
@@ -469,7 +472,50 @@ function toolResult(response: RunResponse, ctx: ExtensionContext, rowsByRequest:
|
|
|
469
472
|
};
|
|
470
473
|
}
|
|
471
474
|
|
|
475
|
+
/** Bounded dry-run plan; a route missing from the coordinator report shows as unresolved. */
|
|
476
|
+
export function formatExecutePlan({ request, root, main, routes }: ExecutePlan, hostChecked: boolean): { content: [{ type: "text"; text: string }]; details: unknown } {
|
|
477
|
+
const route = (role: string, modelClass: ModelClass) => {
|
|
478
|
+
const found = routes.find((candidate) => candidate.role === role && candidate.modelClass === modelClass);
|
|
479
|
+
return found ? { model: found.model, thinkingLevel: found.thinkingLevel } : undefined;
|
|
480
|
+
};
|
|
481
|
+
const label = (role: string, modelClass: ModelClass) => {
|
|
482
|
+
const resolved = route(role, modelClass);
|
|
483
|
+
return `${role} · ${modelClass} → ${resolved ? `${resolved.model} · thinking ${resolved.thinkingLevel}` : "route not reported"}`;
|
|
484
|
+
};
|
|
485
|
+
const changesets = request.tasks.some((task) => task.kind === "changeset");
|
|
486
|
+
const notChecked = [
|
|
487
|
+
...(changesets && !hostChecked ? ["Herdr host preflight (client and server versions, API schema, lsof, current workspace identity): it calls Herdr"] : []),
|
|
488
|
+
"request ID uniqueness and saved-state admission: they need the state lock",
|
|
489
|
+
];
|
|
490
|
+
const lines = [
|
|
491
|
+
`Dry run passed: isolated request ${request.id} in ${root} at Main ${main.head.slice(0, 12)} (${main.branch}). No state, worktree, Herdr tab, lease, or follow-up was created.`,
|
|
492
|
+
...request.tasks.map((task, index) => [
|
|
493
|
+
`${index + 1}. ${task.id} · ${task.kind} · ${label(task.role, task.modelClass)}`,
|
|
494
|
+
` depends on: ${task.dependsOn.join(", ") || "none"} · context from: ${task.contextFrom.join(", ") || "none"}`,
|
|
495
|
+
...(task.kind === "changeset" ? [` checks: ${task.checks.map(formatCheckCommand).join("; ")}`,
|
|
496
|
+
...(task.judgment ? [` judgment: ${label(task.judgment.role, task.judgment.modelClass)}`] : [])] : []),
|
|
497
|
+
].join("\n")),
|
|
498
|
+
`Final checks: ${request.finalChecks.map(formatCheckCommand).join("; ") || "none"}`,
|
|
499
|
+
...(request.finalJudgment ? [`Final judgment: ${label(request.finalJudgment.role, request.finalJudgment.modelClass)}`] : []),
|
|
500
|
+
...notChecked.map((reason) => `Not checked: ${reason}.`),
|
|
501
|
+
];
|
|
502
|
+
return {
|
|
503
|
+
content: [{ type: "text", text: capEphemeralSubagentOutput(lines.join("\n")) }],
|
|
504
|
+
details: {
|
|
505
|
+
dryRun: true, mode: "isolated", id: request.id, root, main,
|
|
506
|
+
tasks: request.tasks.map((task) => ({ id: task.id, kind: task.kind, role: task.role, modelClass: task.modelClass,
|
|
507
|
+
...route(task.role, task.modelClass), dependsOn: task.dependsOn, contextFrom: task.contextFrom,
|
|
508
|
+
...(task.kind === "changeset" ? { checks: task.checks, ...(task.judgment ? { judgment: { ...task.judgment, ...route(task.judgment.role, task.judgment.modelClass) } } : {}) } : {}) })),
|
|
509
|
+
finalChecks: request.finalChecks,
|
|
510
|
+
...(request.finalJudgment ? { finalJudgment: { ...request.finalJudgment, ...route(request.finalJudgment.role, request.finalJudgment.modelClass) } } : {}),
|
|
511
|
+
notChecked,
|
|
512
|
+
},
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
472
516
|
export interface IsolatedSurface {
|
|
517
|
+
/** Dry run of execute: validates and resolves without durable state or Herdr calls. */
|
|
518
|
+
plan(request: ExecuteRequest, signal: AbortSignal | undefined, ctx: ExtensionContext): Promise<ReturnType<typeof formatExecutePlan>>;
|
|
473
519
|
execute(request: ExecuteRequest, signal: AbortSignal | undefined, ctx: ExtensionContext): Promise<ReturnType<typeof toolResult>>;
|
|
474
520
|
inventory(cwd: string, current?: () => boolean): Promise<IsolatedInventory>;
|
|
475
521
|
recover(cwd: string): Promise<string>;
|
|
@@ -877,6 +923,13 @@ export function registerIsolatedExtension(pi: ExtensionAPI, options: RegisterIso
|
|
|
877
923
|
requireCurrent(current);
|
|
878
924
|
return getComponents().runner.drainFollowups(root, requestId, taskId);
|
|
879
925
|
},
|
|
926
|
+
async plan(request, signal, ctx) {
|
|
927
|
+
latestCtx = ctx;
|
|
928
|
+
latestContext();
|
|
929
|
+
const root = await lookupRoot(ctx.cwd, signal);
|
|
930
|
+
if (jobOwners.has(`${root}\0${request.id}`)) throw new Error(`Pi Subagent request ${request.id} is already active in this session runtime.`);
|
|
931
|
+
return formatExecutePlan(await getComponents().runner.plan(request, ctx.cwd, signal), false);
|
|
932
|
+
},
|
|
880
933
|
async execute(request, signal, ctx) {
|
|
881
934
|
latestCtx = ctx;
|
|
882
935
|
latestContext();
|