pi-herdr-agents 0.0.4 → 0.1.1
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
CHANGED
|
@@ -78,12 +78,6 @@ pi
|
|
|
78
78
|
|
|
79
79
|
herdr is the only supported terminal environment. The extension requires `HERDR_ENV=1` and the `herdr` CLI to be available.
|
|
80
80
|
|
|
81
|
-
If your shell startup is slow and subagent commands sometimes get dropped before the prompt is ready, set `PI_SUBAGENT_SHELL_READY_DELAY_MS` to a higher value (defaults to `500`):
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
export PI_SUBAGENT_SHELL_READY_DELAY_MS=2500
|
|
85
|
-
```
|
|
86
|
-
|
|
87
81
|
### Troubleshooting completion delivery
|
|
88
82
|
|
|
89
83
|
If a child finishes but the parent returns an empty or unrelated response, first verify that the result reached the parent session:
|
|
@@ -314,7 +308,14 @@ exact IDs from your authenticated model catalog:
|
|
|
314
308
|
`subagent({ agent: ... })`. Explicit `model` tool arguments take precedence,
|
|
315
309
|
followed by agent frontmatter, per-agent config, the global default, and finally
|
|
316
310
|
the parent model. Model values must be exact authenticated `provider/model-id`
|
|
317
|
-
references.
|
|
311
|
+
references. A value can contain an ordered comma-separated fallback list, for
|
|
312
|
+
example `provider/preferred, provider/fallback`. The extension validates every
|
|
313
|
+
candidate before launch, retries the preferred model normally, then launches
|
|
314
|
+
later candidates only after a provider/agent request failure. A completed child
|
|
315
|
+
result, including a negative task result, never switches models. Completion
|
|
316
|
+
metadata and the status widget report the model actually used; an exhausted
|
|
317
|
+
list reports every attempted model. Workflow metadata accepts one exact model
|
|
318
|
+
only, to keep approved workflow runtimes deterministic.
|
|
318
319
|
|
|
319
320
|
`config.json` is gitignored in the source tree so local overrides are not
|
|
320
321
|
committed from a checkout. On an installed package root, treat it as disposable
|
|
@@ -356,7 +357,7 @@ subagent({
|
|
|
356
357
|
| `agent` | string | — | Load defaults from agent definition |
|
|
357
358
|
| `fork` | boolean | `false` | Force the full-context fork mode for this spawn, overriding any agent `session-mode` frontmatter |
|
|
358
359
|
| `interactive` | boolean | derived | Mark this spawn as interactive (don't wake the parent on stall/recovery). Defaults to the agent's `interactive` frontmatter, otherwise the inverse of `auto-exit`. |
|
|
359
|
-
| `model` | string | configured or parent | Exact authenticated `provider/model-id
|
|
360
|
+
| `model` | string | configured or parent | Exact authenticated `provider/model-id`, or an ordered comma-separated Pi fallback list; unavailable for Claude CLI and worktree spawns. Resolution is tool argument → agent frontmatter → per-agent config → global config → parent |
|
|
360
361
|
| `thinking` | string | parent level | Pi thinking level (`off` through `max`); omit to inherit the parent |
|
|
361
362
|
| `systemPrompt` | string | — | Role/system-prompt text for a bare spawn; overrides the body for Claude CLI agents, while named Pi agents keep their definition body |
|
|
362
363
|
| `resumeSessionId` | string | — | Claude CLI session ID to resume; separate from the Pi `subagent_resume` tool |
|
|
@@ -422,7 +423,7 @@ herdr_workflow({ action: "cancel", runId: "run-1" });
|
|
|
422
423
|
|
|
423
424
|
- Cancel claims a process-global terminal gate. Completion, failure, interruption, and cancellation cannot each produce a terminal outcome.
|
|
424
425
|
- Queued `agent()` calls resolve as cancelled; no later reviewer or synthesizer starts.
|
|
425
|
-
-
|
|
426
|
+
- New panes are queried through Herdr process-info until their interactive shell is ready before launch. Active panes are queried again before close so foreground process identities can be waited on.
|
|
426
427
|
- After synchronous pane close, cancel waits for pane absence and captured process exit before disposing the reader checkout.
|
|
427
428
|
- If process identity cannot be captured for an active pane, the pane remains present after close, or any captured process still lives after the bounded wait, the checkout is retained and the run ends `failed` with `cancel_termination_failed`. Successful cancellation is not reported in that case.
|
|
428
429
|
- A successful cancel writes one `cancelled` terminal journal event and one result-free delivery. Repeated cancel is idempotent and returns the authoritative terminal outcome (including a prior fail-closed result).
|
|
@@ -683,7 +684,7 @@ and verify them with `/subagent list` plus a smoke launch.
|
|
|
683
684
|
| ------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
684
685
|
| `name` | string | Optional explicit agent name used in `agent: "my-agent"`; defaults to the filename stem and must match it in role packs |
|
|
685
686
|
| `description` | string | Shown in `subagents_list` output |
|
|
686
|
-
| `model` | string | Optional exact authenticated Pi model default; omit to use per-agent config, global config, then the parent
|
|
687
|
+
| `model` | string | Optional exact authenticated Pi model default or ordered comma-separated fallback list; omit to use per-agent config, global config, then the parent |
|
|
687
688
|
| `cli` | string | Set to `claude` to launch the Claude CLI instead of Pi |
|
|
688
689
|
| `cli-model` | string | Optional model name passed to a Claude CLI agent; separate from Pi model routing |
|
|
689
690
|
| `thinking` | string | Optional Pi thinking default (`off` through `max`); omit to inherit the parent. Thinking overrides are not supported for Claude CLI agents |
|
|
@@ -46,7 +46,7 @@ For a worktree launch:
|
|
|
46
46
|
- Uncommitted and untracked files from the parent checkout are not copied. Commit anything the child must see before spawning it, or pass the needed context in the task.
|
|
47
47
|
- Worktree creation does not steal terminal focus.
|
|
48
48
|
|
|
49
|
-
`worktree` cannot be set in agent frontmatter and is not exposed by the `/subagent <agent> <task>` shorthand. It is selected per call to the `subagent` tool.
|
|
49
|
+
`worktree` cannot be set in agent frontmatter and is not exposed by the `/subagent <agent> <task>` shorthand. It is selected per call to the `subagent` tool. Ordered model fallback lists are not supported for worktree subagents: a failed attempt retains its worktree and branch for review, so a retry cannot safely reuse the requested branch.
|
|
50
50
|
|
|
51
51
|
## Parent and worker responsibilities
|
|
52
52
|
|
package/package.json
CHANGED
|
@@ -129,6 +129,10 @@ function getHerdrParentPaneId(): string {
|
|
|
129
129
|
return paneId;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
function buildCurrentPaneArgs(): string[] {
|
|
133
|
+
return ["pane", "current", "--current"];
|
|
134
|
+
}
|
|
135
|
+
|
|
132
136
|
function getHerdrCurrentPaneInfo(): {
|
|
133
137
|
pane_id: string;
|
|
134
138
|
tab_id: string;
|
|
@@ -141,7 +145,7 @@ function getHerdrCurrentPaneInfo(): {
|
|
|
141
145
|
// Fall back to `herdr pane current` if any identity env var is missing —
|
|
142
146
|
// older herdr versions may not set all three.
|
|
143
147
|
if (!paneId || !tabId || !workspaceId) {
|
|
144
|
-
const output = herdrExec(
|
|
148
|
+
const output = herdrExec(buildCurrentPaneArgs());
|
|
145
149
|
const parsed = parseHerdrJson(output);
|
|
146
150
|
const pane = (parsed as { result?: { pane?: unknown } } | null)?.result
|
|
147
151
|
?.pane as
|
|
@@ -449,6 +453,46 @@ export function getHerdrPaneProcessInfo(surface: string): HerdrPaneProcessInfo {
|
|
|
449
453
|
);
|
|
450
454
|
}
|
|
451
455
|
|
|
456
|
+
async function getHerdrPaneProcessInfoAsync(
|
|
457
|
+
surface: string,
|
|
458
|
+
): Promise<HerdrPaneProcessInfo> {
|
|
459
|
+
return parsePaneProcessInfo(
|
|
460
|
+
await herdrExecAsync(["pane", "process-info", "--pane", surface]),
|
|
461
|
+
surface,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function isHerdrShellReady(info: HerdrPaneProcessInfo): boolean {
|
|
466
|
+
return (
|
|
467
|
+
info.shellPid != null &&
|
|
468
|
+
info.foregroundProcessGroupId === info.shellPid
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export async function waitForHerdrShellReady(
|
|
473
|
+
surface: string,
|
|
474
|
+
options: { timeoutMs?: number; intervalMs?: number; signal?: AbortSignal } = {},
|
|
475
|
+
): Promise<void> {
|
|
476
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
477
|
+
const intervalMs = options.intervalMs ?? 50;
|
|
478
|
+
const deadline = Date.now() + timeoutMs;
|
|
479
|
+
let lastError = "no interactive shell foreground process";
|
|
480
|
+
|
|
481
|
+
while (Date.now() <= deadline) {
|
|
482
|
+
if (options.signal?.aborted) throw new Error("Shell readiness wait cancelled.");
|
|
483
|
+
try {
|
|
484
|
+
if (isHerdrShellReady(await getHerdrPaneProcessInfoAsync(surface))) return;
|
|
485
|
+
} catch (error) {
|
|
486
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
487
|
+
}
|
|
488
|
+
if (Date.now() >= deadline) break;
|
|
489
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
490
|
+
}
|
|
491
|
+
throw new Error(
|
|
492
|
+
`Timed out waiting for interactive shell in Herdr pane ${surface}: ${lastError}`,
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
452
496
|
export function isProcessAlive(pid: number): boolean {
|
|
453
497
|
try {
|
|
454
498
|
process.kill(pid, 0);
|
|
@@ -529,6 +573,7 @@ export function renameHerdrWorkspace(title: string): void {
|
|
|
529
573
|
}
|
|
530
574
|
|
|
531
575
|
export const __herdrTest__ = {
|
|
576
|
+
buildCurrentPaneArgs,
|
|
532
577
|
buildTabCreateArgs,
|
|
533
578
|
buildWorktreeCreateArgs,
|
|
534
579
|
parseHerdrJson,
|
|
@@ -538,4 +583,5 @@ export const __herdrTest__ = {
|
|
|
538
583
|
parsePaneGetOutput,
|
|
539
584
|
parsePaneGetError,
|
|
540
585
|
parsePaneProcessInfo,
|
|
586
|
+
isHerdrShellReady,
|
|
541
587
|
};
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
readPaneAsync,
|
|
40
40
|
inspectPane,
|
|
41
41
|
getPaneProcessInfo,
|
|
42
|
+
waitForShellReady,
|
|
42
43
|
waitForPaneAbsence,
|
|
43
44
|
waitForProcessesExit,
|
|
44
45
|
} from "./terminal.ts";
|
|
@@ -53,6 +54,7 @@ import {
|
|
|
53
54
|
import {
|
|
54
55
|
buildAuthenticatedModelCatalog,
|
|
55
56
|
resolveRuntimePlan,
|
|
57
|
+
resolveRuntimePlans,
|
|
56
58
|
wrapPiModelRegistry,
|
|
57
59
|
THINKING_LEVELS,
|
|
58
60
|
isThinkingLevel,
|
|
@@ -202,7 +204,7 @@ const SubagentParams = Type.Object({
|
|
|
202
204
|
model: Type.Optional(
|
|
203
205
|
Type.String({
|
|
204
206
|
description:
|
|
205
|
-
"Exact authenticated provider/model-id. Omit to inherit the parent model.
|
|
207
|
+
"Exact authenticated provider/model-id, or an ordered comma-separated fallback list. Omit to inherit the parent model. Fallbacks are Pi-backed only and cannot be used with worktrees.",
|
|
206
208
|
}),
|
|
207
209
|
),
|
|
208
210
|
thinking: Type.Optional(ThinkingLevelSchema),
|
|
@@ -802,19 +804,6 @@ function formatElapsed(seconds: number): string {
|
|
|
802
804
|
return `${m}m ${s}s`;
|
|
803
805
|
}
|
|
804
806
|
|
|
805
|
-
/**
|
|
806
|
-
* Wait long enough for a freshly created pane to finish shell startup.
|
|
807
|
-
*
|
|
808
|
-
* Some environments do extra shell-init work before the prompt is ready
|
|
809
|
-
* (for example direnv/devenv), so the delay is configurable for users who hit
|
|
810
|
-
* dropped commands. Keep the historical default at 500ms.
|
|
811
|
-
*/
|
|
812
|
-
function getShellReadyDelayMs(): number {
|
|
813
|
-
const raw = process.env.PI_SUBAGENT_SHELL_READY_DELAY_MS?.trim();
|
|
814
|
-
const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
|
|
815
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 500;
|
|
816
|
-
}
|
|
817
|
-
|
|
818
807
|
function muxUnavailableResult() {
|
|
819
808
|
return {
|
|
820
809
|
content: [
|
|
@@ -1147,6 +1136,7 @@ function resolveResultPresentation(
|
|
|
1147
1136
|
| "summary"
|
|
1148
1137
|
| "sessionFile"
|
|
1149
1138
|
| "errorMessage"
|
|
1139
|
+
| "fallbackAttempts"
|
|
1150
1140
|
| "worktree"
|
|
1151
1141
|
>,
|
|
1152
1142
|
name: string,
|
|
@@ -1173,6 +1163,9 @@ function resolveResultPresentation(
|
|
|
1173
1163
|
: `Sub-agent "${name}" completed (${formatElapsed(result.elapsed)}).\n\n${result.summary}`;
|
|
1174
1164
|
}
|
|
1175
1165
|
|
|
1166
|
+
if (result.fallbackAttempts && result.fallbackAttempts.length > 1) {
|
|
1167
|
+
body += `\n\nModels attempted: ${result.fallbackAttempts.join(", ")}`;
|
|
1168
|
+
}
|
|
1176
1169
|
if (result.worktree) body += `\n\n${formatWorktreeHandoff(result.worktree)}`;
|
|
1177
1170
|
const runtimeWarning = runtimeMismatch
|
|
1178
1171
|
? `\n\nRuntime warning: ${runtimeMismatch}`
|
|
@@ -1215,6 +1208,8 @@ interface SubagentResult {
|
|
|
1215
1208
|
error?: string;
|
|
1216
1209
|
/** Provider/agent error message when auto-retry exhausted (overload, rate limit, etc.). */
|
|
1217
1210
|
errorMessage?: string;
|
|
1211
|
+
/** Ordered models launched for this run, including failed fallback attempts. */
|
|
1212
|
+
fallbackAttempts?: string[];
|
|
1218
1213
|
ping?: { name: string; message: string };
|
|
1219
1214
|
worktree?: WorktreeHandoff;
|
|
1220
1215
|
}
|
|
@@ -1977,7 +1972,6 @@ function buildWorkflowChildCommand(params: {
|
|
|
1977
1972
|
|
|
1978
1973
|
export const __test__ = {
|
|
1979
1974
|
borderLine,
|
|
1980
|
-
getShellReadyDelayMs,
|
|
1981
1975
|
renderSubagentWidgetLines,
|
|
1982
1976
|
loadAgentDefaults,
|
|
1983
1977
|
discoverAgentDefinitions,
|
|
@@ -2045,10 +2039,10 @@ async function launchSubagent(
|
|
|
2045
2039
|
};
|
|
2046
2040
|
},
|
|
2047
2041
|
parentThinking: ThinkingLevel,
|
|
2048
|
-
options?: { surface?: string },
|
|
2042
|
+
options?: { surface?: string; runtimePlan?: ResolvedRuntimePlan; id?: string },
|
|
2049
2043
|
): Promise<RunningSubagent> {
|
|
2050
2044
|
const startTime = Date.now();
|
|
2051
|
-
const id = Math.random().toString(16).slice(2, 10);
|
|
2045
|
+
const id = options?.id ?? Math.random().toString(16).slice(2, 10);
|
|
2052
2046
|
|
|
2053
2047
|
const agentDefs = params.agent
|
|
2054
2048
|
? loadAgentDefaults(params.agent, runtime.pi)
|
|
@@ -2064,19 +2058,21 @@ async function launchSubagent(
|
|
|
2064
2058
|
requireClaudeAdapter(agentDefs?.cli);
|
|
2065
2059
|
if (!ctx.model)
|
|
2066
2060
|
throw new Error("Subagent launch requires a resolved parent model");
|
|
2067
|
-
const runtimePlan =
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
model:
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2061
|
+
const runtimePlan =
|
|
2062
|
+
options?.runtimePlan ??
|
|
2063
|
+
resolveRuntimePlan(
|
|
2064
|
+
{ model: params.model, thinking: params.thinking },
|
|
2065
|
+
{
|
|
2066
|
+
model: resolveModelDefault(params.agent, agentDefs?.model, modelConfig),
|
|
2067
|
+
thinking: agentDefs?.thinking,
|
|
2068
|
+
},
|
|
2069
|
+
{
|
|
2070
|
+
provider: ctx.model.provider,
|
|
2071
|
+
modelId: ctx.model.id,
|
|
2072
|
+
thinking: parentThinking,
|
|
2073
|
+
},
|
|
2074
|
+
wrapPiModelRegistry(ctx.modelRegistry),
|
|
2075
|
+
);
|
|
2080
2076
|
const effectiveModel = runtimePlan.model;
|
|
2081
2077
|
const effectiveTools = params.tools ?? agentDefs?.tools;
|
|
2082
2078
|
const effectiveSkills = params.skills ?? agentDefs?.skills;
|
|
@@ -2209,13 +2205,8 @@ async function launchSubagent(
|
|
|
2209
2205
|
});
|
|
2210
2206
|
}
|
|
2211
2207
|
|
|
2212
|
-
//
|
|
2213
|
-
|
|
2214
|
-
if (!options?.surface) {
|
|
2215
|
-
await new Promise<void>((resolve) =>
|
|
2216
|
-
setTimeout(resolve, getShellReadyDelayMs()),
|
|
2217
|
-
);
|
|
2218
|
-
}
|
|
2208
|
+
// `pane run` is safe only after the shell owns the foreground process group.
|
|
2209
|
+
await waitForShellReady(surface);
|
|
2219
2210
|
|
|
2220
2211
|
const launchBehavior = resolveLaunchBehavior(params, agentDefs);
|
|
2221
2212
|
|
|
@@ -2484,6 +2475,67 @@ async function launchSubagent(
|
|
|
2484
2475
|
* the summary from the session file, and closes ordinary panes. Worktree
|
|
2485
2476
|
* workspaces are retained for parent review.
|
|
2486
2477
|
*/
|
|
2478
|
+
function resolveSubagentRuntimePlans(
|
|
2479
|
+
params: typeof SubagentParams.static,
|
|
2480
|
+
ctx: Parameters<typeof launchSubagent>[1],
|
|
2481
|
+
parentThinking: ThinkingLevel,
|
|
2482
|
+
): ResolvedRuntimePlan[] {
|
|
2483
|
+
const agentDefs = params.agent
|
|
2484
|
+
? loadAgentDefaults(params.agent, runtime.pi)
|
|
2485
|
+
: null;
|
|
2486
|
+
if (params.agent && !agentDefs) {
|
|
2487
|
+
const diagnostic = discoverAgentCatalog(runtime.pi).diagnostics.find(
|
|
2488
|
+
(candidate) => candidate.agentName === params.agent,
|
|
2489
|
+
);
|
|
2490
|
+
throw new Error(
|
|
2491
|
+
diagnostic?.message ?? `Agent "${params.agent}" was not found.`,
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2494
|
+
if (!ctx.model) throw new Error("Subagent launch requires a resolved parent model");
|
|
2495
|
+
const plans = resolveRuntimePlans(
|
|
2496
|
+
{ model: params.model, thinking: params.thinking },
|
|
2497
|
+
{
|
|
2498
|
+
model: resolveModelDefault(params.agent, agentDefs?.model, modelConfig),
|
|
2499
|
+
thinking: agentDefs?.thinking,
|
|
2500
|
+
},
|
|
2501
|
+
{
|
|
2502
|
+
provider: ctx.model.provider,
|
|
2503
|
+
modelId: ctx.model.id,
|
|
2504
|
+
thinking: parentThinking,
|
|
2505
|
+
},
|
|
2506
|
+
wrapPiModelRegistry(ctx.modelRegistry),
|
|
2507
|
+
);
|
|
2508
|
+
if (agentDefs?.cli === "claude" && plans.length > 1) {
|
|
2509
|
+
throw new Error("Model fallbacks are supported only for Pi-backed subagents.");
|
|
2510
|
+
}
|
|
2511
|
+
if (params.worktree && plans.length > 1) {
|
|
2512
|
+
throw new Error("Model fallbacks are not supported for worktree subagents.");
|
|
2513
|
+
}
|
|
2514
|
+
return plans;
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
async function launchSubagentWithFallbacks(
|
|
2518
|
+
params: typeof SubagentParams.static,
|
|
2519
|
+
ctx: Parameters<typeof launchSubagent>[1],
|
|
2520
|
+
parentThinking: ThinkingLevel,
|
|
2521
|
+
plans: ResolvedRuntimePlan[],
|
|
2522
|
+
): Promise<{ running: RunningSubagent; index: number }> {
|
|
2523
|
+
const failures: string[] = [];
|
|
2524
|
+
for (const [index, plan] of plans.entries()) {
|
|
2525
|
+
try {
|
|
2526
|
+
return {
|
|
2527
|
+
running: await launchSubagent(params, ctx, parentThinking, { runtimePlan: plan }),
|
|
2528
|
+
index,
|
|
2529
|
+
};
|
|
2530
|
+
} catch (error) {
|
|
2531
|
+
failures.push(`${plan.model}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
throw new Error(
|
|
2535
|
+
`Subagent could not launch with any configured model. Attempted: ${plans.map((plan) => plan.model).join(", ")}. ${failures.join("; ")}`,
|
|
2536
|
+
);
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2487
2539
|
async function watchSubagent(
|
|
2488
2540
|
running: RunningSubagent,
|
|
2489
2541
|
signal: AbortSignal,
|
|
@@ -2676,6 +2728,64 @@ async function watchSubagent(
|
|
|
2676
2728
|
}
|
|
2677
2729
|
}
|
|
2678
2730
|
|
|
2731
|
+
async function watchSubagentWithFallbacks(
|
|
2732
|
+
initial: RunningSubagent,
|
|
2733
|
+
initialPlanIndex: number,
|
|
2734
|
+
params: typeof SubagentParams.static,
|
|
2735
|
+
ctx: Parameters<typeof launchSubagent>[1],
|
|
2736
|
+
parentThinking: ThinkingLevel,
|
|
2737
|
+
plans: ResolvedRuntimePlan[],
|
|
2738
|
+
signal: AbortSignal,
|
|
2739
|
+
): Promise<{ running: RunningSubagent; result: SubagentResult }> {
|
|
2740
|
+
let running = initial;
|
|
2741
|
+
let nextPlan = initialPlanIndex + 1;
|
|
2742
|
+
const attempts = [running.runtimePlan?.model].filter(
|
|
2743
|
+
(model): model is string => !!model,
|
|
2744
|
+
);
|
|
2745
|
+
|
|
2746
|
+
for (;;) {
|
|
2747
|
+
const result = await watchSubagent(running, signal);
|
|
2748
|
+
const shouldRetry = !!result.errorMessage && nextPlan < plans.length;
|
|
2749
|
+
if (!shouldRetry) {
|
|
2750
|
+
return { running, result: { ...result, fallbackAttempts: attempts } };
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
runningSubagents.delete(running.id);
|
|
2754
|
+
updateWidget();
|
|
2755
|
+
const launchErrors: string[] = [];
|
|
2756
|
+
let launchedFallback = false;
|
|
2757
|
+
while (nextPlan < plans.length) {
|
|
2758
|
+
const plan = plans[nextPlan++];
|
|
2759
|
+
attempts.push(plan.model);
|
|
2760
|
+
try {
|
|
2761
|
+
running = await launchSubagent(params, ctx, parentThinking, {
|
|
2762
|
+
runtimePlan: plan,
|
|
2763
|
+
id: initial.id,
|
|
2764
|
+
});
|
|
2765
|
+
running.abortController = initial.abortController;
|
|
2766
|
+
launchedFallback = true;
|
|
2767
|
+
startWidgetRefresh();
|
|
2768
|
+
startStatusRefresh(runtime.pi!);
|
|
2769
|
+
break;
|
|
2770
|
+
} catch (error) {
|
|
2771
|
+
launchErrors.push(
|
|
2772
|
+
`${plan.model}: ${error instanceof Error ? error.message : String(error)}`,
|
|
2773
|
+
);
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
if (!launchedFallback) {
|
|
2777
|
+
return {
|
|
2778
|
+
running,
|
|
2779
|
+
result: {
|
|
2780
|
+
...result,
|
|
2781
|
+
errorMessage: `${result.errorMessage}\n\nFallback launch failures: ${launchErrors.join("; ")}`,
|
|
2782
|
+
fallbackAttempts: attempts,
|
|
2783
|
+
},
|
|
2784
|
+
};
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2679
2789
|
export default function subagentsExtension(pi: ExtensionAPI) {
|
|
2680
2790
|
runtime.pi = pi;
|
|
2681
2791
|
let btwChild: BtwChild | undefined;
|
|
@@ -2881,9 +2991,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
2881
2991
|
mkdirSync(dirname(sessionFile), { recursive: true });
|
|
2882
2992
|
surface = createSubagentPane(`${candidate.runId}: ${roleName}`);
|
|
2883
2993
|
owner.children.set(id, { controller: childController, surface });
|
|
2884
|
-
await
|
|
2885
|
-
setTimeout(done, getShellReadyDelayMs()),
|
|
2886
|
-
);
|
|
2994
|
+
await waitForShellReady(surface, { signal: childController.signal });
|
|
2887
2995
|
if (childController.signal.aborted)
|
|
2888
2996
|
return workflowFailure("cancelled", "Workflow cancelled.");
|
|
2889
2997
|
const command = buildWorkflowChildCommand({
|
|
@@ -3604,7 +3712,17 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
3604
3712
|
`Unsupported parent thinking level: ${parentThinking}`,
|
|
3605
3713
|
);
|
|
3606
3714
|
}
|
|
3607
|
-
const
|
|
3715
|
+
const runtimePlans = resolveSubagentRuntimePlans(
|
|
3716
|
+
params,
|
|
3717
|
+
ctx,
|
|
3718
|
+
parentThinking,
|
|
3719
|
+
);
|
|
3720
|
+
const { running, index: initialPlanIndex } = await launchSubagentWithFallbacks(
|
|
3721
|
+
params,
|
|
3722
|
+
ctx,
|
|
3723
|
+
parentThinking,
|
|
3724
|
+
runtimePlans,
|
|
3725
|
+
);
|
|
3608
3726
|
|
|
3609
3727
|
// Create a separate AbortController for the watcher
|
|
3610
3728
|
// (the tool's signal completes when we return)
|
|
@@ -3616,16 +3734,24 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
3616
3734
|
startStatusRefresh(pi);
|
|
3617
3735
|
|
|
3618
3736
|
// Fire-and-forget: start watching in background
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3737
|
+
watchSubagentWithFallbacks(
|
|
3738
|
+
running,
|
|
3739
|
+
initialPlanIndex,
|
|
3740
|
+
params,
|
|
3741
|
+
ctx,
|
|
3742
|
+
parentThinking,
|
|
3743
|
+
runtimePlans,
|
|
3744
|
+
watcherAbort.signal,
|
|
3745
|
+
)
|
|
3746
|
+
.then(({ running: completedRunning, result }) => {
|
|
3747
|
+
if (!shouldDeliverSubagentCompletion(completedRunning)) {
|
|
3748
|
+
completedRunning.lifecycle = markDelivery(completedRunning.lifecycle, "suppressed");
|
|
3749
|
+
runningSubagents.delete(completedRunning.id);
|
|
3624
3750
|
updateWidget();
|
|
3625
3751
|
return;
|
|
3626
3752
|
}
|
|
3627
|
-
|
|
3628
|
-
runningSubagents.delete(
|
|
3753
|
+
completedRunning.lifecycle = markDelivery(completedRunning.lifecycle, "delivered");
|
|
3754
|
+
runningSubagents.delete(completedRunning.id);
|
|
3629
3755
|
updateWidget();
|
|
3630
3756
|
const completionApi = selectCompletionApi(pi, runtime.pi);
|
|
3631
3757
|
|
|
@@ -3655,26 +3781,29 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
3655
3781
|
|
|
3656
3782
|
const presentation = resolveResultPresentation(
|
|
3657
3783
|
result,
|
|
3658
|
-
|
|
3659
|
-
|
|
3784
|
+
completedRunning.name,
|
|
3785
|
+
completedRunning.runtimePlan?.runtimeMismatch,
|
|
3660
3786
|
);
|
|
3661
3787
|
|
|
3662
3788
|
sendSubagentResult(completionApi, presentation, {
|
|
3663
|
-
name:
|
|
3664
|
-
task:
|
|
3665
|
-
agent:
|
|
3789
|
+
name: completedRunning.name,
|
|
3790
|
+
task: completedRunning.task,
|
|
3791
|
+
agent: completedRunning.agent,
|
|
3666
3792
|
exitCode: result.exitCode,
|
|
3667
3793
|
elapsed: result.elapsed,
|
|
3668
3794
|
sessionFile: result.sessionFile,
|
|
3669
3795
|
...(result.errorMessage
|
|
3670
3796
|
? { errorMessage: result.errorMessage }
|
|
3671
3797
|
: {}),
|
|
3798
|
+
...(result.fallbackAttempts
|
|
3799
|
+
? { fallbackAttempts: result.fallbackAttempts }
|
|
3800
|
+
: {}),
|
|
3672
3801
|
...(result.claudeSessionId
|
|
3673
3802
|
? { claudeSessionId: result.claudeSessionId }
|
|
3674
3803
|
: {}),
|
|
3675
3804
|
...(result.worktree ? { worktree: result.worktree } : {}),
|
|
3676
|
-
...(
|
|
3677
|
-
? { runtimePlan:
|
|
3805
|
+
...(completedRunning.runtimePlan
|
|
3806
|
+
? { runtimePlan: completedRunning.runtimePlan }
|
|
3678
3807
|
: {}),
|
|
3679
3808
|
});
|
|
3680
3809
|
})
|
|
@@ -4033,9 +4162,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
4033
4162
|
const entryCountBefore = getNewEntries(params.sessionPath, 0).length;
|
|
4034
4163
|
|
|
4035
4164
|
const surface = createSubagentPane(name);
|
|
4036
|
-
await
|
|
4037
|
-
setTimeout(resolve, getShellReadyDelayMs()),
|
|
4038
|
-
);
|
|
4165
|
+
await waitForShellReady(surface);
|
|
4039
4166
|
|
|
4040
4167
|
// Build pi resume command
|
|
4041
4168
|
const parts = ["pi", "--session", shellQuote(params.sessionPath)];
|
|
@@ -4269,9 +4396,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
4269
4396
|
|
|
4270
4397
|
sessionFile = createBtwSessionSnapshot(parentSessionFile, leafId);
|
|
4271
4398
|
surface = createSubagentPane("BTW");
|
|
4272
|
-
await
|
|
4273
|
-
setTimeout(resolve, getShellReadyDelayMs()),
|
|
4274
|
-
);
|
|
4399
|
+
await waitForShellReady(surface);
|
|
4275
4400
|
|
|
4276
4401
|
const artifactDir = getArtifactDir(
|
|
4277
4402
|
ctx.sessionManager.getSessionDir(),
|
|
@@ -190,6 +190,16 @@ function selectField(
|
|
|
190
190
|
return { source: "parent" };
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
export function parseModelFallbacks(reference: string): string[] {
|
|
194
|
+
const candidates = reference.split(",").map((candidate) => candidate.trim());
|
|
195
|
+
if (candidates.some((candidate) => candidate === "")) {
|
|
196
|
+
throw new RuntimeResolutionError(
|
|
197
|
+
`model fallback list ${JSON.stringify(reference)} cannot contain an empty candidate`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return candidates;
|
|
201
|
+
}
|
|
202
|
+
|
|
193
203
|
export function resolveRuntimePlan(
|
|
194
204
|
request: RuntimeRequest,
|
|
195
205
|
agentDefaults: RuntimeRequest,
|
|
@@ -272,6 +282,30 @@ export function resolveRuntimePlan(
|
|
|
272
282
|
};
|
|
273
283
|
}
|
|
274
284
|
|
|
285
|
+
/** Resolve every configured fallback before launching the first child. */
|
|
286
|
+
export function resolveRuntimePlans(
|
|
287
|
+
request: RuntimeRequest,
|
|
288
|
+
agentDefaults: RuntimeRequest,
|
|
289
|
+
parent: ParentRuntime,
|
|
290
|
+
registry: ModelRegistryAdapter,
|
|
291
|
+
): ResolvedRuntimePlan[] {
|
|
292
|
+
const selection = selectField(request.model, agentDefaults.model);
|
|
293
|
+
if (!selection.value) {
|
|
294
|
+
return [resolveRuntimePlan(request, agentDefaults, parent, registry)];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return parseModelFallbacks(selection.value).map((model) =>
|
|
298
|
+
resolveRuntimePlan(
|
|
299
|
+
selection.source === "request" ? { ...request, model } : { ...request, model: undefined },
|
|
300
|
+
selection.source === "agent"
|
|
301
|
+
? { ...agentDefaults, model }
|
|
302
|
+
: agentDefaults,
|
|
303
|
+
parent,
|
|
304
|
+
registry,
|
|
305
|
+
),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
275
309
|
function formatTokenCount(value: number | undefined): string | undefined {
|
|
276
310
|
if (!value || value <= 0) return undefined;
|
|
277
311
|
if (value >= 1_000_000) return `${Number((value / 1_000_000).toFixed(1))}m`;
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
createHerdrSurfaceSplit,
|
|
8
8
|
createHerdrWorktree,
|
|
9
9
|
getHerdrPaneProcessInfo,
|
|
10
|
+
waitForHerdrShellReady,
|
|
10
11
|
isHerdrAvailable,
|
|
11
12
|
isProcessAlive,
|
|
12
13
|
readHerdrScreen,
|
|
@@ -152,6 +153,14 @@ export function getPaneProcessInfo(paneId: PaneId): HerdrPaneProcessInfo {
|
|
|
152
153
|
return getHerdrPaneProcessInfo(paneId);
|
|
153
154
|
}
|
|
154
155
|
|
|
156
|
+
export async function waitForShellReady(
|
|
157
|
+
paneId: PaneId,
|
|
158
|
+
options?: { timeoutMs?: number; intervalMs?: number; signal?: AbortSignal },
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
assertTerminalAvailable();
|
|
161
|
+
return waitForHerdrShellReady(paneId, options);
|
|
162
|
+
}
|
|
163
|
+
|
|
155
164
|
export async function waitForPaneAbsence(
|
|
156
165
|
paneId: PaneId,
|
|
157
166
|
options?: { timeoutMs?: number; intervalMs?: number },
|