@henryqw/pi-subagent 22.2.1 → 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.js +8 -12
- package/extensions/result-transport.ts +7 -8
- package/extensions/subagent.ts +13 -30
- package/package.json +1 -1
package/dist/herdr-runtime.js
CHANGED
|
@@ -152,9 +152,6 @@ function worktreeIntent(attempt) {
|
|
|
152
152
|
export function workspaceLabel(attempt) {
|
|
153
153
|
return attempt.correlationToken.slice(-6);
|
|
154
154
|
}
|
|
155
|
-
function legacyWorkspaceLabel(requestId, task, attempt) {
|
|
156
|
-
return `${requestId}/${task.id}#${attempt.number}`;
|
|
157
|
-
}
|
|
158
155
|
function expectedWorkerLabel(task) {
|
|
159
156
|
return `${task.role}/${task.modelClass}`;
|
|
160
157
|
}
|
|
@@ -174,10 +171,9 @@ function requireIntentIdentity(intent, attempt) {
|
|
|
174
171
|
throw new Error(`${intent.kind} allocation intent does not match the task attempt.`);
|
|
175
172
|
}
|
|
176
173
|
}
|
|
177
|
-
function assertWorkspaceIntent(intent,
|
|
174
|
+
function assertWorkspaceIntent(intent, attempt) {
|
|
178
175
|
const worktree = worktreeIntent(attempt);
|
|
179
|
-
|
|
180
|
-
if ((label !== workspaceLabel(attempt) && label !== legacyWorkspaceLabel(requestId, task, attempt))
|
|
176
|
+
if (exactString(intent.label, "workspace label") !== workspaceLabel(attempt)
|
|
181
177
|
|| exactAbsolutePath(intent.worktreeCwd, "workspace worktree cwd") !== worktree.cwd
|
|
182
178
|
|| exactAbsolutePath(intent.mainRoot, "workspace Main root") !== worktree.repoRoot) {
|
|
183
179
|
throw new Error("Workspace allocation plan drifted from the exact owned worktree.");
|
|
@@ -368,7 +364,7 @@ export class HerdrHostRuntime {
|
|
|
368
364
|
if (input.intent.kind === "workspace") {
|
|
369
365
|
if (input.acquireLaunch)
|
|
370
366
|
throw new Error("Role launch acquisition is valid only at the exact agent allocation boundary.");
|
|
371
|
-
return await this.allocateWorkspace(input.intent, input.
|
|
367
|
+
return await this.allocateWorkspace(input.intent, input.attempt, context);
|
|
372
368
|
}
|
|
373
369
|
if (input.intent.kind === "worker_tab") {
|
|
374
370
|
if (input.acquireLaunch)
|
|
@@ -381,7 +377,7 @@ export class HerdrHostRuntime {
|
|
|
381
377
|
requireIntentIdentity(input.intent, input.attempt);
|
|
382
378
|
const allocation = input.intent;
|
|
383
379
|
if (allocation.kind === "workspace") {
|
|
384
|
-
assertWorkspaceIntent(allocation, input.
|
|
380
|
+
assertWorkspaceIntent(allocation, input.attempt);
|
|
385
381
|
await this.assertRepositoryIdentity(allocation, context);
|
|
386
382
|
const response = await this.herdr.json(["worktree", "list", "--cwd", allocation.worktreeCwd], this.processOptions(allocation.herdrRepoRoot, context, HERDR_OPERATION_CAP_MS));
|
|
387
383
|
const result = resultRecord(response, "Herdr worktree list response");
|
|
@@ -656,7 +652,7 @@ export class HerdrHostRuntime {
|
|
|
656
652
|
assertWorkerTabIntent(allocation, input.task, input.attempt);
|
|
657
653
|
const leasePath = exactString(allocation.leasePath, "saved worker lease path");
|
|
658
654
|
this.assertLeasePath(leasePath, input.attempt.correlationToken);
|
|
659
|
-
assertWorkspaceIntent(workspace, input.
|
|
655
|
+
assertWorkspaceIntent(workspace, input.attempt);
|
|
660
656
|
await this.assertRepositoryIdentity(workspace, context);
|
|
661
657
|
const tabId = exactString(allocation.tabId, "saved worker tab ID");
|
|
662
658
|
const workspaceId = exactString(workspace.workspaceId, "saved workspace ID");
|
|
@@ -688,7 +684,7 @@ export class HerdrHostRuntime {
|
|
|
688
684
|
return { outcome: "blocked", failure: "Workspace cleanup must follow worker-tab reconciliation." };
|
|
689
685
|
const allocation = ownedIntent(input.attempt, "workspace");
|
|
690
686
|
requireIntentIdentity(allocation, input.attempt);
|
|
691
|
-
assertWorkspaceIntent(allocation, input.
|
|
687
|
+
assertWorkspaceIntent(allocation, input.attempt);
|
|
692
688
|
await this.assertRepositoryIdentity(allocation, context);
|
|
693
689
|
const workspaceId = exactString(allocation.workspaceId, "saved workspace ID");
|
|
694
690
|
if (await this.workspaceAbsent(workspaceId, allocation, context))
|
|
@@ -708,8 +704,8 @@ export class HerdrHostRuntime {
|
|
|
708
704
|
return { outcome: "blocked", failure: safeText(error) };
|
|
709
705
|
}
|
|
710
706
|
}
|
|
711
|
-
async allocateWorkspace(allocation,
|
|
712
|
-
assertWorkspaceIntent(allocation,
|
|
707
|
+
async allocateWorkspace(allocation, attempt, context) {
|
|
708
|
+
assertWorkspaceIntent(allocation, attempt);
|
|
713
709
|
await this.assertRepositoryIdentity(allocation, context);
|
|
714
710
|
const args = ["worktree", "open", "--cwd", allocation.herdrRepoRoot, "--path", allocation.worktreeCwd, "--label", allocation.label, "--no-focus"];
|
|
715
711
|
const response = await this.herdr.exec(args, this.processOptions(allocation.herdrRepoRoot, context, HERDR_OPERATION_CAP_MS));
|
|
@@ -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;
|