@henryqw/pi-subagent 22.2.0 → 22.2.2
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/dist/herdr-runtime.d.ts +3 -0
- package/dist/herdr-runtime.js +26 -14
- package/dist/integration-git.js +7 -4
- package/dist/runner.js +14 -7
- package/dist/worktree.d.ts +1 -1
- package/dist/worktree.js +2 -2
- package/extensions/isolated.ts +7 -6
- package/extensions/result-transport.ts +7 -8
- package/extensions/subagent.ts +13 -30
- package/package.json +1 -1
package/dist/herdr-runtime.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ExecuteRequest, type HostAllocationIntent, type HostAllocationPlan, type TaskAttempt, type TaskRequest, type WorkspaceIdentity } from "./schema.ts";
|
|
2
2
|
import { type AllocationReconciliation, type HostAllocationKind, type HostAllocationResult, type HostCleanupKind, type HostRuntime, type InFlightTaskCandidateInspector, type OperationContext, type TextTaskContext, type TransientLaunchHandle, type VerifiedLaunch, type WorkerResult } from "./runner.ts";
|
|
3
|
+
import { type EphemeralSubagentExecutionBudget } from "./ephemeral.ts";
|
|
3
4
|
export interface HostProcessOptions {
|
|
4
5
|
cwd: string;
|
|
5
6
|
signal: AbortSignal;
|
|
@@ -21,6 +22,7 @@ export interface HerdrHostRuntimeOptions {
|
|
|
21
22
|
env?: NodeJS.ProcessEnv;
|
|
22
23
|
leaseDirectory?: string;
|
|
23
24
|
lsofCommand?: string;
|
|
25
|
+
executionBudget?: () => Omit<EphemeralSubagentExecutionBudget, "startedAt">;
|
|
24
26
|
}
|
|
25
27
|
export declare function workspaceLabel(attempt: Pick<TaskAttempt, "correlationToken">): string;
|
|
26
28
|
export declare class HerdrHostRuntime implements HostRuntime {
|
|
@@ -34,6 +36,7 @@ export declare class HerdrHostRuntime implements HostRuntime {
|
|
|
34
36
|
private readonly env;
|
|
35
37
|
private readonly leaseDirectory;
|
|
36
38
|
private readonly lsofCommand;
|
|
39
|
+
private readonly executionBudget?;
|
|
37
40
|
constructor(options: HerdrHostRuntimeOptions);
|
|
38
41
|
/** Fail-closed Herdr caller/capability gate. This method creates no state or host resource. */
|
|
39
42
|
preflightHost(input: {
|
package/dist/herdr-runtime.js
CHANGED
|
@@ -8,6 +8,7 @@ import { createHerdrClient, hasHerdrErrorCode, herdrCommandFailure, startPiAgent
|
|
|
8
8
|
import { isCleanCommitted, } from "./schema.js";
|
|
9
9
|
import { buildChangesetTaskPrompt, withTransientLaunch, } from "./runner.js";
|
|
10
10
|
import { runProcess as defaultRunProcess } from "./process.js";
|
|
11
|
+
import { EXECUTION_BUDGET_ENV } from "./ephemeral.js";
|
|
11
12
|
const MIN_HERDR_VERSION = [0, 9, 0];
|
|
12
13
|
const MIN_HERDR_PROTOCOL = 22;
|
|
13
14
|
const HERDR_OPERATION_CAP_MS = 30_000;
|
|
@@ -151,9 +152,6 @@ function worktreeIntent(attempt) {
|
|
|
151
152
|
export function workspaceLabel(attempt) {
|
|
152
153
|
return attempt.correlationToken.slice(-6);
|
|
153
154
|
}
|
|
154
|
-
function legacyWorkspaceLabel(requestId, task, attempt) {
|
|
155
|
-
return `${requestId}/${task.id}#${attempt.number}`;
|
|
156
|
-
}
|
|
157
155
|
function expectedWorkerLabel(task) {
|
|
158
156
|
return `${task.role}/${task.modelClass}`;
|
|
159
157
|
}
|
|
@@ -173,10 +171,9 @@ function requireIntentIdentity(intent, attempt) {
|
|
|
173
171
|
throw new Error(`${intent.kind} allocation intent does not match the task attempt.`);
|
|
174
172
|
}
|
|
175
173
|
}
|
|
176
|
-
function assertWorkspaceIntent(intent,
|
|
174
|
+
function assertWorkspaceIntent(intent, attempt) {
|
|
177
175
|
const worktree = worktreeIntent(attempt);
|
|
178
|
-
|
|
179
|
-
if ((label !== workspaceLabel(attempt) && label !== legacyWorkspaceLabel(requestId, task, attempt))
|
|
176
|
+
if (exactString(intent.label, "workspace label") !== workspaceLabel(attempt)
|
|
180
177
|
|| exactAbsolutePath(intent.worktreeCwd, "workspace worktree cwd") !== worktree.cwd
|
|
181
178
|
|| exactAbsolutePath(intent.mainRoot, "workspace Main root") !== worktree.repoRoot) {
|
|
182
179
|
throw new Error("Workspace allocation plan drifted from the exact owned worktree.");
|
|
@@ -252,6 +249,7 @@ export class HerdrHostRuntime {
|
|
|
252
249
|
env;
|
|
253
250
|
leaseDirectory;
|
|
254
251
|
lsofCommand;
|
|
252
|
+
executionBudget;
|
|
255
253
|
constructor(options) {
|
|
256
254
|
this.inspectInFlightCandidate = options.inspectInFlightTaskCandidate;
|
|
257
255
|
this.execute = options.runProcess ?? defaultRunProcess;
|
|
@@ -263,6 +261,7 @@ export class HerdrHostRuntime {
|
|
|
263
261
|
this.env = options.env ?? process.env;
|
|
264
262
|
this.leaseDirectory = resolve(options.leaseDirectory ?? join(extensionConfigDir("pi-subagent"), "leases"));
|
|
265
263
|
this.lsofCommand = options.lsofCommand ?? "lsof";
|
|
264
|
+
this.executionBudget = options.executionBudget;
|
|
266
265
|
}
|
|
267
266
|
/** Fail-closed Herdr caller/capability gate. This method creates no state or host resource. */
|
|
268
267
|
async preflightHost(input, context) {
|
|
@@ -365,7 +364,7 @@ export class HerdrHostRuntime {
|
|
|
365
364
|
if (input.intent.kind === "workspace") {
|
|
366
365
|
if (input.acquireLaunch)
|
|
367
366
|
throw new Error("Role launch acquisition is valid only at the exact agent allocation boundary.");
|
|
368
|
-
return await this.allocateWorkspace(input.intent, input.
|
|
367
|
+
return await this.allocateWorkspace(input.intent, input.attempt, context);
|
|
369
368
|
}
|
|
370
369
|
if (input.intent.kind === "worker_tab") {
|
|
371
370
|
if (input.acquireLaunch)
|
|
@@ -378,7 +377,7 @@ export class HerdrHostRuntime {
|
|
|
378
377
|
requireIntentIdentity(input.intent, input.attempt);
|
|
379
378
|
const allocation = input.intent;
|
|
380
379
|
if (allocation.kind === "workspace") {
|
|
381
|
-
assertWorkspaceIntent(allocation, input.
|
|
380
|
+
assertWorkspaceIntent(allocation, input.attempt);
|
|
382
381
|
await this.assertRepositoryIdentity(allocation, context);
|
|
383
382
|
const response = await this.herdr.json(["worktree", "list", "--cwd", allocation.worktreeCwd], this.processOptions(allocation.herdrRepoRoot, context, HERDR_OPERATION_CAP_MS));
|
|
384
383
|
const result = resultRecord(response, "Herdr worktree list response");
|
|
@@ -653,7 +652,7 @@ export class HerdrHostRuntime {
|
|
|
653
652
|
assertWorkerTabIntent(allocation, input.task, input.attempt);
|
|
654
653
|
const leasePath = exactString(allocation.leasePath, "saved worker lease path");
|
|
655
654
|
this.assertLeasePath(leasePath, input.attempt.correlationToken);
|
|
656
|
-
assertWorkspaceIntent(workspace, input.
|
|
655
|
+
assertWorkspaceIntent(workspace, input.attempt);
|
|
657
656
|
await this.assertRepositoryIdentity(workspace, context);
|
|
658
657
|
const tabId = exactString(allocation.tabId, "saved worker tab ID");
|
|
659
658
|
const workspaceId = exactString(workspace.workspaceId, "saved workspace ID");
|
|
@@ -685,7 +684,7 @@ export class HerdrHostRuntime {
|
|
|
685
684
|
return { outcome: "blocked", failure: "Workspace cleanup must follow worker-tab reconciliation." };
|
|
686
685
|
const allocation = ownedIntent(input.attempt, "workspace");
|
|
687
686
|
requireIntentIdentity(allocation, input.attempt);
|
|
688
|
-
assertWorkspaceIntent(allocation, input.
|
|
687
|
+
assertWorkspaceIntent(allocation, input.attempt);
|
|
689
688
|
await this.assertRepositoryIdentity(allocation, context);
|
|
690
689
|
const workspaceId = exactString(allocation.workspaceId, "saved workspace ID");
|
|
691
690
|
if (await this.workspaceAbsent(workspaceId, allocation, context))
|
|
@@ -705,8 +704,8 @@ export class HerdrHostRuntime {
|
|
|
705
704
|
return { outcome: "blocked", failure: safeText(error) };
|
|
706
705
|
}
|
|
707
706
|
}
|
|
708
|
-
async allocateWorkspace(allocation,
|
|
709
|
-
assertWorkspaceIntent(allocation,
|
|
707
|
+
async allocateWorkspace(allocation, attempt, context) {
|
|
708
|
+
assertWorkspaceIntent(allocation, attempt);
|
|
710
709
|
await this.assertRepositoryIdentity(allocation, context);
|
|
711
710
|
const args = ["worktree", "open", "--cwd", allocation.herdrRepoRoot, "--path", allocation.worktreeCwd, "--label", allocation.label, "--no-focus"];
|
|
712
711
|
const response = await this.herdr.exec(args, this.processOptions(allocation.herdrRepoRoot, context, HERDR_OPERATION_CAP_MS));
|
|
@@ -741,9 +740,12 @@ export class HerdrHostRuntime {
|
|
|
741
740
|
assertWorkerTabIntent(allocation, task, attempt);
|
|
742
741
|
await this.createPrivateLease(allocation.leasePath, allocation.token);
|
|
743
742
|
await this.waitForStableWorkspacePanes(allocation.workspaceId, allocation.worktreeCwd, context);
|
|
743
|
+
const budget = this.executionBudget?.();
|
|
744
744
|
const args = [
|
|
745
745
|
"tab", "create", "--workspace", allocation.workspaceId, "--cwd", allocation.worktreeCwd,
|
|
746
|
-
"--label", allocation.label, "--env", `${PROCESS_LEASE_ENV}=${allocation.leasePath}`,
|
|
746
|
+
"--label", allocation.label, "--env", `${PROCESS_LEASE_ENV}=${allocation.leasePath}`,
|
|
747
|
+
...(budget ? ["--env", `${EXECUTION_BUDGET_ENV}=${JSON.stringify({ ...budget, startedAt: this.now() })}`] : []),
|
|
748
|
+
"--no-focus",
|
|
747
749
|
];
|
|
748
750
|
const response = await this.herdr.exec(args, this.processOptions(allocation.worktreeCwd, context, HERDR_OPERATION_CAP_MS));
|
|
749
751
|
if (response.code !== 0 || response.killed) {
|
|
@@ -783,8 +785,9 @@ export class HerdrHostRuntime {
|
|
|
783
785
|
return await withTransientLaunch(handle, async (launch) => {
|
|
784
786
|
if (launch.role !== expectedRole)
|
|
785
787
|
throw new Error("Agent start acquisition returned the wrong Role.");
|
|
786
|
-
if (Object.keys(launch.env).
|
|
788
|
+
if (Object.keys(launch.env).some((name) => name !== EXECUTION_BUDGET_ENV || !this.executionBudget)) {
|
|
787
789
|
throw new Error("Herdr agent launch must not receive caller Role environment variables.");
|
|
790
|
+
}
|
|
788
791
|
const response = await startPiAgent(this.herdr, {
|
|
789
792
|
name: allocation.agentName,
|
|
790
793
|
pane: allocation.paneId,
|
|
@@ -827,6 +830,15 @@ export class HerdrHostRuntime {
|
|
|
827
830
|
if (lifecycle.status === "unknown") {
|
|
828
831
|
return { outcome: "unknown", diagnostic: "Delivered prompt lifecycle is unknown." };
|
|
829
832
|
}
|
|
833
|
+
if (lifecycle.status === "working") {
|
|
834
|
+
try {
|
|
835
|
+
await this.delay(STALLED_PROMPT_POLL_MS, context.signal);
|
|
836
|
+
}
|
|
837
|
+
catch (error) {
|
|
838
|
+
return { outcome: this.contextInterrupted(context) ? "interrupted" : "unknown", diagnostic: `Delivered prompt polling failed: ${safeText(error)}` };
|
|
839
|
+
}
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
830
842
|
let inspection;
|
|
831
843
|
try {
|
|
832
844
|
inspection = await this.inspectInFlightCandidate({ root: allocation.worktreeCwd, task: input.task, attempt: input.attempt }, this.childContext(context, GIT_INSPECTION_CAP_MS));
|
package/dist/integration-git.js
CHANGED
|
@@ -45,12 +45,15 @@ async function sharedHistory(base, tip, cwd, signal) {
|
|
|
45
45
|
/** Check helper-derived names even after a safely removed checkout is no longer present. */
|
|
46
46
|
async function ownershipMetadata(root, info, signal) {
|
|
47
47
|
const canonical = await realpath(root);
|
|
48
|
-
const
|
|
49
|
-
const primary =
|
|
48
|
+
const listed = await requireGit(["worktree", "list", "--porcelain", "-z"], root, signal);
|
|
49
|
+
const primary = listed.split("\0", 1)[0];
|
|
50
|
+
if (!primary?.startsWith("worktree "))
|
|
51
|
+
throw new Error("Primary Git worktree is unavailable.");
|
|
52
|
+
const repoRoot = await realpath(primary.slice("worktree ".length));
|
|
50
53
|
if (await realpath(await requireGit(["rev-parse", "--show-toplevel"], root, signal)) !== canonical
|
|
51
|
-
|| info.repoRoot !==
|
|
54
|
+
|| info.repoRoot !== repoRoot || info.cwd !== info.path
|
|
52
55
|
|| !/^pi-subagent\/subagent-[0-9a-f]{24}$/.test(info.branch)
|
|
53
|
-
|| info.path !== join(
|
|
56
|
+
|| info.path !== join(repoRoot, ".worktrees", basename(info.branch))) {
|
|
54
57
|
throw new Error("Git worktree is not an exact owned checkout of Main.");
|
|
55
58
|
}
|
|
56
59
|
}
|
package/dist/runner.js
CHANGED
|
@@ -940,6 +940,7 @@ export class IsolatedRunner {
|
|
|
940
940
|
if (generation && generation.status !== "staging" && generation.status !== "conflict")
|
|
941
941
|
throw new Error("Stage action has a stale generation.");
|
|
942
942
|
const previousStage = generation?.stages.at(-1);
|
|
943
|
+
const pendingAllocation = action.action === "resolve" && previousStage?.status === "pending";
|
|
943
944
|
const expected = previousStage?.status === "staged"
|
|
944
945
|
? previousStage.tip : previousStage?.onto ?? generation?.integrationBase;
|
|
945
946
|
if (action.action === "stage" && generation?.stages.some((stage) => stage.status !== "staged")) {
|
|
@@ -974,6 +975,18 @@ export class IsolatedRunner {
|
|
|
974
975
|
const actualMain = await scope.call((context) => this.gitRuntime.inspectMain({ root }, context));
|
|
975
976
|
if (!sameIdentity(actualMain, state.main))
|
|
976
977
|
throw new Error("Main changed or became dirty; staging is blocked.");
|
|
978
|
+
if (pendingAllocation) {
|
|
979
|
+
if (!generation?.worktree || generation.status !== "staging" || generation.stages.length !== 1
|
|
980
|
+
|| !sameIdentity(action.expectedTip, generation.integrationBase)) {
|
|
981
|
+
throw new Error("Pending integration allocation is not an exact initial stage.");
|
|
982
|
+
}
|
|
983
|
+
const checkout = generation.worktree;
|
|
984
|
+
const base = await scope.call((context) => this.integrationGit.inspectCombined(root, checkout, state.main, [], context.signal));
|
|
985
|
+
if (!sameIdentity(base, generation.integrationBase))
|
|
986
|
+
throw new Error("Pending integration checkout is not the clean recorded base.");
|
|
987
|
+
previousStage.status = "staging";
|
|
988
|
+
await this.saveProductive(handle); // Explicit resolve proves allocation; merge intent precedes the first merge.
|
|
989
|
+
}
|
|
977
990
|
if (!generation) {
|
|
978
991
|
if (action.action !== "stage")
|
|
979
992
|
throw new Error("No integration generation exists to resolve.");
|
|
@@ -1018,15 +1031,9 @@ export class IsolatedRunner {
|
|
|
1018
1031
|
}
|
|
1019
1032
|
await this.saveProductive(handle); // Exact candidate and previous tip before merge.
|
|
1020
1033
|
}
|
|
1021
|
-
|
|
1034
|
+
const outcome = await scope.call((context) => action.action === "stage" || pendingAllocation
|
|
1022
1035
|
? this.integrationGit.stage(root, worktree, state.main, receipts, worker, candidate.tip, context.signal)
|
|
1023
1036
|
: this.integrationGit.reconcileStage(root, worktree, state.main, receipts, worker, candidate.tip, context.signal));
|
|
1024
|
-
if (action.action === "resolve" && stage.status === "pending"
|
|
1025
|
-
&& outcome.outcome === "ready" && outcome.value === "not_started") {
|
|
1026
|
-
stage.status = "staging";
|
|
1027
|
-
await this.saveProductive(handle); // Read-only proof before Main's explicit merge request.
|
|
1028
|
-
outcome = await scope.call((context) => this.integrationGit.stage(root, worktree, state.main, receipts, worker, candidate.tip, context.signal));
|
|
1029
|
-
}
|
|
1030
1037
|
if (outcome.outcome === "ready" && outcome.value !== "not_started") {
|
|
1031
1038
|
stage.status = "staged";
|
|
1032
1039
|
stage.tip = outcome.value.tip;
|
package/dist/worktree.d.ts
CHANGED
|
@@ -46,7 +46,7 @@ export declare function inspectIndexFlags(cwd: string, run?: GitRunner, signal?:
|
|
|
46
46
|
hidden: boolean;
|
|
47
47
|
failure?: string;
|
|
48
48
|
}>;
|
|
49
|
-
/** Proves a worktree has no tracked, untracked,
|
|
49
|
+
/** Proves a worktree has no tracked, untracked, index-hidden, or nested submodule work; cleanup also checks ignored files. */
|
|
50
50
|
export declare function inspectWorktreeDirty(cwd: string, run?: GitRunner, includeIgnored?: boolean): Promise<WorktreeDirtyInspection>;
|
|
51
51
|
/**
|
|
52
52
|
* Inspects and possibly prunes a child worktree after it finishes. Commit count
|
package/dist/worktree.js
CHANGED
|
@@ -187,7 +187,7 @@ async function inspectDirty(run, cwd, includeIgnored = true) {
|
|
|
187
187
|
return { dirty: false, failure: "assume-unchanged or skip-worktree index entries remain" };
|
|
188
188
|
return { dirty: false };
|
|
189
189
|
}
|
|
190
|
-
/** Proves a worktree has no tracked, untracked,
|
|
190
|
+
/** Proves a worktree has no tracked, untracked, index-hidden, or nested submodule work; cleanup also checks ignored files. */
|
|
191
191
|
export async function inspectWorktreeDirty(cwd, run = runGit, includeIgnored = true) {
|
|
192
192
|
const root = await inspectDirty(run, cwd, includeIgnored);
|
|
193
193
|
if (root.dirty || root.failure)
|
|
@@ -232,7 +232,7 @@ export async function finalizeChildWorktree(info, run = runGit) {
|
|
|
232
232
|
const countFailure = counted.code !== 0
|
|
233
233
|
? `rev-list exit ${counted.code}: ${counted.stderr.trim().slice(0, 200)}`
|
|
234
234
|
: Number.isNaN(commits) ? "rev-list produced non-numeric output" : undefined;
|
|
235
|
-
const inspection = checkoutExists ? await inspectDirty(run, info.path) : undefined;
|
|
235
|
+
const inspection = checkoutExists ? await inspectDirty(run, info.path, true) : undefined;
|
|
236
236
|
const measurements = {
|
|
237
237
|
...(countFailure === undefined ? { commits } : {}),
|
|
238
238
|
...(inspection?.failure === undefined && inspection !== undefined ? { dirty: inspection.dirty } : {}),
|
package/extensions/isolated.ts
CHANGED
|
@@ -65,18 +65,19 @@ export const createIsolatedComponents: CreateIsolatedComponents = ({
|
|
|
65
65
|
: await directRunProcess(command, args, options);
|
|
66
66
|
const resolveRoot = createCanonicalGitRootResolver({ runProcess });
|
|
67
67
|
const git = new CheckedGitRuntime({ runProcess, executeReview: createExactJudgmentExecutor(executor) });
|
|
68
|
-
const
|
|
68
|
+
const executionBudget = () => ({
|
|
69
|
+
maxTurns: policy.maxTurns,
|
|
70
|
+
maxMs: null,
|
|
71
|
+
...(policy.maxTokens === undefined ? {} : { maxTokens: policy.maxTokens }),
|
|
72
|
+
});
|
|
73
|
+
const host = new HerdrHostRuntime({ inspectInFlightTaskCandidate: git.inspectInFlightTaskCandidate.bind(git), runProcess, executionBudget });
|
|
69
74
|
const coordinator = new RoleLaunchRuntime({
|
|
70
75
|
pi,
|
|
71
76
|
context,
|
|
72
77
|
resolveRoot,
|
|
73
78
|
preflightHost: async ({ root }, operation) => await host.preflightHost({ root }, operation),
|
|
74
79
|
inspectMain: async (input, operation) => await git.inspectMain(input, operation),
|
|
75
|
-
executionBudget
|
|
76
|
-
maxTurns: policy.maxTurns,
|
|
77
|
-
maxMs: null,
|
|
78
|
-
...(policy.maxTokens === undefined ? {} : { maxTokens: policy.maxTokens }),
|
|
79
|
-
}),
|
|
80
|
+
executionBudget,
|
|
80
81
|
});
|
|
81
82
|
return {
|
|
82
83
|
resolveRoot,
|
|
@@ -3,7 +3,7 @@ import type { WorkflowEntry, WorkflowMode } from "./workflow.ts";
|
|
|
3
3
|
|
|
4
4
|
const EVIDENCE_PREVIEW_CODE_POINTS = 256;
|
|
5
5
|
|
|
6
|
-
export type WorkflowTransportStatus = "pending" | "running" | "succeeded" | "
|
|
6
|
+
export type WorkflowTransportStatus = "pending" | "running" | "succeeded" | "rejected" | "skipped";
|
|
7
7
|
|
|
8
8
|
type TransportEntryBase = {
|
|
9
9
|
id: WorkflowEntry["id"];
|
|
@@ -17,7 +17,7 @@ type TransportEntryBase = {
|
|
|
17
17
|
export type WorkflowTransportEntry =
|
|
18
18
|
| TransportEntryBase & { status: "pending" | "skipped"; assistantOutput?: never; failure?: never }
|
|
19
19
|
| TransportEntryBase & { status: "running" | "succeeded"; assistantOutput: string; failure?: never }
|
|
20
|
-
| TransportEntryBase & { status: "
|
|
20
|
+
| TransportEntryBase & { status: "rejected"; assistantOutput?: never; failure: string };
|
|
21
21
|
|
|
22
22
|
export type WorkflowTransportEntryDetails = {
|
|
23
23
|
id: string;
|
|
@@ -37,7 +37,7 @@ export type WorkflowTransportDetails = {
|
|
|
37
37
|
|
|
38
38
|
export type BackgroundWorkflowTransportDetails = WorkflowTransportDetails & {
|
|
39
39
|
taskId: string;
|
|
40
|
-
outcome: "completed" | "failed"
|
|
40
|
+
outcome: "completed" | "failed";
|
|
41
41
|
recovery?: true;
|
|
42
42
|
};
|
|
43
43
|
|
|
@@ -63,7 +63,7 @@ function workflowTitle(mode: WorkflowMode): string {
|
|
|
63
63
|
function statusCounts(entries: readonly WorkflowTransportEntry[]): string[] {
|
|
64
64
|
const count = (statuses: WorkflowTransportStatus[]) => entries.filter(({ status }) => statuses.includes(status)).length;
|
|
65
65
|
return ([
|
|
66
|
-
[["
|
|
66
|
+
[["rejected"], "failed"],
|
|
67
67
|
[["succeeded"], "completed"],
|
|
68
68
|
[["skipped"], "skipped"],
|
|
69
69
|
] as const).flatMap(([statuses, word]) => {
|
|
@@ -84,7 +84,6 @@ const ENTRY_STATUS_PRESENTATION = {
|
|
|
84
84
|
pending: { glyph: "○", fallback: "queued" },
|
|
85
85
|
running: { glyph: "◌", fallback: "working" },
|
|
86
86
|
succeeded: { glyph: "✓", fallback: "completed" },
|
|
87
|
-
failed: { glyph: "✗", fallback: "failed" },
|
|
88
87
|
rejected: { glyph: "✗", fallback: "failed" },
|
|
89
88
|
skipped: { glyph: "–", fallback: "skipped" },
|
|
90
89
|
} as const satisfies Record<WorkflowTransportEntryDetails["status"], WorkflowEntryStatusPresentation>;
|
|
@@ -94,7 +93,7 @@ export function presentWorkflowEntryStatus(status: WorkflowTransportEntryDetails
|
|
|
94
93
|
}
|
|
95
94
|
|
|
96
95
|
function sourceFor(entry: WorkflowTransportEntry): string | undefined {
|
|
97
|
-
if (entry.status === "
|
|
96
|
+
if (entry.status === "rejected") return entry.failure;
|
|
98
97
|
if (entry.status === "running" || entry.status === "succeeded") return entry.assistantOutput;
|
|
99
98
|
}
|
|
100
99
|
|
|
@@ -103,7 +102,7 @@ function evidenceFor(entry: WorkflowTransportEntry, position: number, total: num
|
|
|
103
102
|
if (source === undefined || !source) return;
|
|
104
103
|
const [preview, remainder] = splitEvidence(source);
|
|
105
104
|
return {
|
|
106
|
-
heading: `- [${position}/${total}] ${entry.name} · ${entry.role} · ${entry.status === "
|
|
105
|
+
heading: `- [${position}/${total}] ${entry.name} · ${entry.role} · ${entry.status === "rejected" ? "failure" : "result"}:`,
|
|
107
106
|
preview,
|
|
108
107
|
remainder,
|
|
109
108
|
};
|
|
@@ -114,7 +113,7 @@ export function formatWorkflowResult(mode: WorkflowMode, entries: readonly Workf
|
|
|
114
113
|
if (ordered.some(({ status }) => status === "pending" || status === "running")) {
|
|
115
114
|
throw new TypeError("Final workflow transport requires terminal entry states.");
|
|
116
115
|
}
|
|
117
|
-
const failed = ordered.some(({ status }) => status === "
|
|
116
|
+
const failed = ordered.some(({ status }) => status === "rejected");
|
|
118
117
|
const positioned = ordered.map((entry, index) => ({ entry, position: index + 1 }));
|
|
119
118
|
const evidence = positioned.flatMap(({ entry, position }) => {
|
|
120
119
|
const value = evidenceFor(entry, position, ordered.length);
|
package/extensions/subagent.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { type Component, Text, type TUI, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
4
4
|
import { availableTaskModels, loadTaskModelsConfig, modelReference, registerModelTask, resolveAvailableModel, type ResolvedTaskRoute, taskThinkingLevels } from "@henryqw/pi-task-models";
|
|
5
|
-
import { capEphemeralSubagentOutput as capOutput, createEphemeralSubagentExecutor, DELEGATE_TASK, formatDuration, loadRoles, prepareRoleLaunch, ROLE_TOOL_POLICY_FLAG, type
|
|
5
|
+
import { capEphemeralSubagentOutput as capOutput, createEphemeralSubagentExecutor, DELEGATE_TASK, formatDuration, loadRoles, prepareRoleLaunch, ROLE_TOOL_POLICY_FLAG, type Role } from "@henryqw/pi-subagent";
|
|
6
6
|
import { DEFAULT_EXECUTION_POLICY, DEFAULT_TIMEOUT_CONFIG, readSubagentConfig, resolveExecutionPolicy, type EffectiveExecutionPolicy } from "./config.ts";
|
|
7
7
|
import { createCheckoutAdmission, roleCanWrite } from "./admission.ts";
|
|
8
8
|
import { registerIsolatedExtension } from "./isolated.ts";
|
|
@@ -18,8 +18,7 @@ const MAX_WIDGET_ITEMS = 8;
|
|
|
18
18
|
const MAX_WIDGET_LINES = 6;
|
|
19
19
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
20
20
|
|
|
21
|
-
type
|
|
22
|
-
type WidgetStatus = "working" | "success" | "failure" | "aborted";
|
|
21
|
+
type WidgetStatus = "working" | "success" | "failure";
|
|
23
22
|
type WidgetItem = {
|
|
24
23
|
role: string;
|
|
25
24
|
model: string;
|
|
@@ -42,7 +41,6 @@ function statusGlyph(status: WidgetStatus, spinnerIndex: number, theme: Theme):
|
|
|
42
41
|
case "working": return theme.fg("accent", SPINNER_FRAMES[spinnerIndex % SPINNER_FRAMES.length]!);
|
|
43
42
|
case "success": return theme.fg("success", "✓");
|
|
44
43
|
case "failure": return theme.fg("error", "✗");
|
|
45
|
-
case "aborted": return theme.fg("warning", "■");
|
|
46
44
|
}
|
|
47
45
|
}
|
|
48
46
|
|
|
@@ -51,7 +49,6 @@ function statusLabel(status: WidgetStatus): string {
|
|
|
51
49
|
case "working": return "working";
|
|
52
50
|
case "success": return "complete";
|
|
53
51
|
case "failure": return "failed";
|
|
54
|
-
case "aborted": return "stopped";
|
|
55
52
|
}
|
|
56
53
|
}
|
|
57
54
|
|
|
@@ -87,11 +84,11 @@ function renderWidgetRows(
|
|
|
87
84
|
});
|
|
88
85
|
const hidden = ordered.slice(visible.length);
|
|
89
86
|
if (hidden.length) {
|
|
90
|
-
const counts: Record<WidgetStatus, number> = { working: 0, success: 0, failure: 0
|
|
87
|
+
const counts: Record<WidgetStatus, number> = { working: 0, success: 0, failure: 0 };
|
|
91
88
|
for (const { status } of hidden) counts[status] += 1;
|
|
92
89
|
lines.push(truncateToWidth(theme.fg("muted", [
|
|
93
90
|
`… ${hidden.length} more`,
|
|
94
|
-
...(["working", "success", "failure"
|
|
91
|
+
...(["working", "success", "failure"] as const).flatMap((status) =>
|
|
95
92
|
counts[status] ? [`${counts[status]} ${statusLabel(status)}`] : []),
|
|
96
93
|
].join(" · ")), width));
|
|
97
94
|
}
|
|
@@ -128,10 +125,7 @@ const roleSummary = (): string => {
|
|
|
128
125
|
}
|
|
129
126
|
};
|
|
130
127
|
|
|
131
|
-
export default function subagentExtension(
|
|
132
|
-
pi: ExtensionAPI,
|
|
133
|
-
overrideTimeoutPolicy?: TimeoutPolicy,
|
|
134
|
-
): void {
|
|
128
|
+
export default function subagentExtension(pi: ExtensionAPI): void {
|
|
135
129
|
if (process.argv.includes(`--${ROLE_TOOL_POLICY_FLAG}`)) return;
|
|
136
130
|
registerModelTask(pi, DELEGATE_TASK);
|
|
137
131
|
pi.registerMessageRenderer(DIRECT_RESULT_TYPE, (message, { expanded, outputPad }, theme) => {
|
|
@@ -142,11 +136,9 @@ export default function subagentExtension(
|
|
|
142
136
|
if (!details?.entries) return new Text(content, outputPad, 0);
|
|
143
137
|
const count = details.entries.length;
|
|
144
138
|
const subject = count === 1 ? "Direct subagent" : `${count} direct subagents`;
|
|
145
|
-
const state = details.recovery ? "stopped; recovery needed"
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const glyph = details.recovery || details.outcome === "aborted" ? "■" : details.outcome === "completed" ? "✓" : "✗";
|
|
149
|
-
const color = details.recovery || details.outcome === "aborted" ? "warning" : details.outcome === "completed" ? "success" : "error";
|
|
139
|
+
const state = details.recovery ? "stopped; recovery needed" : details.outcome;
|
|
140
|
+
const glyph = details.recovery ? "■" : details.outcome === "completed" ? "✓" : "✗";
|
|
141
|
+
const color = details.recovery ? "warning" : details.outcome === "completed" ? "success" : "error";
|
|
150
142
|
const rows = details.entries.map(({ name, role, status, summary }) => {
|
|
151
143
|
const { glyph, fallback } = presentWorkflowEntryStatus(status);
|
|
152
144
|
return `${glyph} ${name} · ${role} — ${details.recovery ? fallback : summary || fallback}`;
|
|
@@ -175,20 +167,11 @@ export default function subagentExtension(
|
|
|
175
167
|
maxCorrections: DEFAULT_EXECUTION_POLICY.maxCorrections,
|
|
176
168
|
};
|
|
177
169
|
}
|
|
178
|
-
const timeoutPolicy: TimeoutPolicy = overrideTimeoutPolicy ?? {
|
|
179
|
-
idleMs: initialPolicy.childIdleMs,
|
|
180
|
-
maxMs: initialPolicy.childMaxMs,
|
|
181
|
-
};
|
|
182
|
-
const effectiveInitialPolicy: EffectiveExecutionPolicy = {
|
|
183
|
-
...initialPolicy,
|
|
184
|
-
childIdleMs: timeoutPolicy.idleMs,
|
|
185
|
-
childMaxMs: timeoutPolicy.maxMs ?? initialPolicy.childMaxMs,
|
|
186
|
-
};
|
|
187
170
|
const executor = createEphemeralSubagentExecutor({
|
|
188
|
-
maxConcurrency:
|
|
189
|
-
maxTurns:
|
|
190
|
-
maxTokens:
|
|
191
|
-
timeout:
|
|
171
|
+
maxConcurrency: initialPolicy.maxSubagents,
|
|
172
|
+
maxTurns: initialPolicy.maxTurns,
|
|
173
|
+
maxTokens: initialPolicy.maxTokens,
|
|
174
|
+
timeout: { idleMs: initialPolicy.childIdleMs, maxMs: initialPolicy.childMaxMs },
|
|
192
175
|
});
|
|
193
176
|
const currentPolicy = (): EffectiveExecutionPolicy => resolveExecutionPolicy(readSubagentConfig());
|
|
194
177
|
const canWrite = (input: unknown): boolean => {
|
|
@@ -205,7 +188,7 @@ export default function subagentExtension(
|
|
|
205
188
|
admission.register(pi, canWrite);
|
|
206
189
|
const isolatedSurface = registerIsolatedExtension(pi, {
|
|
207
190
|
executor,
|
|
208
|
-
policy:
|
|
191
|
+
policy: initialPolicy,
|
|
209
192
|
currentPolicy,
|
|
210
193
|
});
|
|
211
194
|
let directSequence = 0;
|