pi-subagents 0.12.4 → 0.13.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/CHANGELOG.md +15 -0
- package/README.md +22 -4
- package/agent-management.ts +27 -0
- package/agent-manager-edit.ts +4 -2
- package/agent-serializer.ts +3 -0
- package/agents.ts +6 -0
- package/async-execution.ts +15 -2
- package/async-status.ts +7 -0
- package/chain-execution.ts +7 -21
- package/execution.ts +203 -119
- package/model-fallback.ts +100 -0
- package/package.json +1 -1
- package/parallel-utils.ts +3 -0
- package/pi-args.ts +1 -4
- package/render.ts +6 -0
- package/slash-commands.ts +6 -1
- package/subagent-executor.ts +30 -12
- package/subagent-runner.ts +163 -32
- package/subagents-status.ts +8 -1
- package/types.ts +23 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.13.0] - 2026-04-11
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added native agent `fallbackModels` support. Agents can now declare ordered backup models, and single, chain, parallel, and async/background runs retry on provider/model-style failures such as quota, auth, timeout, or provider/model unavailability.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Fallback attempts now preserve observability across sync and async execution: results, artifact metadata, async status, and run logs record attempted models and per-attempt outcomes instead of only the final pass.
|
|
12
|
+
- Child subagent runs now pass model selections through `--model` instead of `--models`, so live execution pins the intended model correctly and end-to-end fallback behavior matches the validated test path.
|
|
13
|
+
|
|
14
|
+
## [0.12.5] - 2026-04-09
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Slash-command result cards now finalize through the extension's own snapshot timing instead of relying on core to treat hidden custom messages as in-place updates. The final slash snapshot and hidden persisted message are written before the last status-clear redraw, so live `/run`, `/chain`, and `/parallel` cards update to their final state more reliably.
|
|
18
|
+
- Added focused slash-command regression coverage for the success/error ordering around visible placeholder messages, hidden final messages, and the final status-clear redraw.
|
|
19
|
+
|
|
5
20
|
## [0.12.4] - 2026-04-04
|
|
6
21
|
|
|
7
22
|
### Added
|
package/README.md
CHANGED
|
@@ -63,6 +63,7 @@ description: Fast codebase recon
|
|
|
63
63
|
tools: read, grep, find, ls, bash, mcp:chrome-devtools # mcp: requires pi-mcp-adapter
|
|
64
64
|
extensions: # absent=all, empty=none, csv=allowlist
|
|
65
65
|
model: claude-haiku-4-5
|
|
66
|
+
fallbackModels: openai/gpt-5-mini, anthropic/claude-sonnet-4 # optional ordered fallbacks
|
|
66
67
|
thinking: high # off, minimal, low, medium, high, xhigh
|
|
67
68
|
skill: safe-bash, chrome-devtools # comma-separated skills to inject
|
|
68
69
|
output: context.md # writes to {chain_dir}/context.md
|
|
@@ -77,6 +78,12 @@ Your system prompt goes here (the markdown body after frontmatter).
|
|
|
77
78
|
|
|
78
79
|
The `thinking` field sets a default extended thinking level for the agent. At runtime it's appended as a `:level` suffix to the model string (e.g., `claude-sonnet-4-5:high`). If the model already has a thinking suffix (from a chain-clarify override), the agent's default is not double-applied.
|
|
79
80
|
|
|
81
|
+
`fallbackModels` is an optional ordered list of backup models to try when the primary model fails with a provider/model-style error such as quota, auth, timeout, or provider/model unavailable. In markdown frontmatter, declare it as a comma-separated string. In management `config` objects, you can pass either a comma-separated string or a string array.
|
|
82
|
+
|
|
83
|
+
Fallback resolution follows the same conservative model lookup as normal execution. Explicit `provider/model` values are used as-is. Bare model IDs are only upgraded to a full `provider/model` when they map cleanly to a single registry entry. If a bare ID is ambiguous, it stays bare.
|
|
84
|
+
|
|
85
|
+
Fallback is only used for provider/model availability failures. Ordinary task failures such as bad `bash` commands, missing files, or other tool/runtime errors do not trigger a model hop.
|
|
86
|
+
|
|
80
87
|
**Extension sandboxing**
|
|
81
88
|
|
|
82
89
|
Use `extensions` in frontmatter to control which extensions a subagent can access:
|
|
@@ -552,6 +559,7 @@ Agent definitions are not loaded into LLM context by default. Management actions
|
|
|
552
559
|
scope: "user",
|
|
553
560
|
systemPrompt: "You are a code scout...",
|
|
554
561
|
model: "anthropic/claude-sonnet-4",
|
|
562
|
+
fallbackModels: ["openai/gpt-5-mini", "anthropic/claude-haiku-4-5"],
|
|
555
563
|
tools: "read, bash, mcp:github/search_repositories",
|
|
556
564
|
extensions: "", // empty = no extensions
|
|
557
565
|
skills: "parallel-scout",
|
|
@@ -590,7 +598,7 @@ Agent definitions are not loaded into LLM context by default. Management actions
|
|
|
590
598
|
Notes:
|
|
591
599
|
- `create` uses `config.scope` (`"user"` or `"project"`), not `agentScope`.
|
|
592
600
|
- `update`/`delete` use `agentScope` only for scope disambiguation when the same name exists in both scopes.
|
|
593
|
-
- Agent config mapping: `reads -> defaultReads`, `progress -> defaultProgress`, `extensions` controls extension sandboxing, `maxSubagentDepth` maps directly to agent frontmatter, and `tools` supports `mcp:` entries that map to direct MCP tools.
|
|
601
|
+
- Agent config mapping: `reads -> defaultReads`, `progress -> defaultProgress`, `extensions` controls extension sandboxing, `maxSubagentDepth` maps directly to agent frontmatter, `fallbackModels` maps directly to agent frontmatter, and `tools` supports `mcp:` entries that map to direct MCP tools.
|
|
594
602
|
- To clear any optional field, set it to `false` or `""` (e.g., `{ model: false }` or `{ skills: "" }`). Both work for all string-typed fields.
|
|
595
603
|
|
|
596
604
|
## Parameters
|
|
@@ -601,10 +609,11 @@ Notes:
|
|
|
601
609
|
| `task` | string | - | Task string (single mode) |
|
|
602
610
|
| `action` | string | - | Management action: `list`, `get`, `create`, `update`, `delete` |
|
|
603
611
|
| `chainName` | string | - | Chain name for management get/update/delete |
|
|
604
|
-
| `config` | object | - | Agent or chain config for management create/update |
|
|
612
|
+
| `config` | object | - | Agent or chain config for management create/update. Agent configs also accept `fallbackModels` (comma-separated string or string array). |
|
|
605
613
|
| `output` | `string \| false` | agent default | Override output file for single agent (absolute path as-is, relative path resolved against cwd) |
|
|
606
614
|
| `skill` | `string \| string[] \| false` | agent default | Override skills (comma-separated string, array, or false to disable) |
|
|
607
615
|
| `model` | string | agent default | Override model for single agent |
|
|
616
|
+
| `fallbackModels` | `string \| string[]` | agent default | Management/config-only field for ordered backup models. Markdown frontmatter uses a comma-separated string. |
|
|
608
617
|
| `tasks` | `{agent, task, cwd?, count?, skill?}[]` | - | Parallel tasks. Foreground runs directly; background requests are converted to an equivalent chain. `count` repeats one task entry N times with the same settings. |
|
|
609
618
|
| `worktree` | boolean | false | Create isolated git worktrees for each parallel task. Requires clean git state. Per-worktree diffs included in output. |
|
|
610
619
|
| `chain` | ChainItem[] | - | Sequential steps with behavior overrides (see below) |
|
|
@@ -637,6 +646,8 @@ Notes:
|
|
|
637
646
|
| `skill` | `string \| string[] \| false` | agent default | Override skills or disable all |
|
|
638
647
|
| `model` | string | agent default | Override model for this step |
|
|
639
648
|
|
|
649
|
+
Fallbacks are inherited from the selected agent for that step. There is no per-step `fallbackModels` override in v1.
|
|
650
|
+
|
|
640
651
|
*Parallel step fields:*
|
|
641
652
|
|
|
642
653
|
| Field | Type | Default | Description |
|
|
@@ -660,6 +671,8 @@ Notes:
|
|
|
660
671
|
| `skill` | `string \| string[] \| false` | agent default | Override skills or disable all |
|
|
661
672
|
| `model` | string | agent default | Override model for this task |
|
|
662
673
|
|
|
674
|
+
Fallbacks are inherited from the selected agent for that task. There is no per-task `fallbackModels` override in v1.
|
|
675
|
+
|
|
663
676
|
Status tool:
|
|
664
677
|
|
|
665
678
|
| Tool | Description |
|
|
@@ -821,7 +834,9 @@ Files per task:
|
|
|
821
834
|
- `{runId}_{agent}_input.md` - Task prompt
|
|
822
835
|
- `{runId}_{agent}_output.md` - Full output (untruncated)
|
|
823
836
|
- `{runId}_{agent}.jsonl` - Event stream (sync only)
|
|
824
|
-
- `{runId}_{agent}_meta.json` - Timing, usage, exit code
|
|
837
|
+
- `{runId}_{agent}_meta.json` - Timing, usage, exit code, final model, attempted models, and per-attempt outcomes
|
|
838
|
+
|
|
839
|
+
When fallback is used, metadata records both the ordered `attemptedModels` list and `modelAttempts` entries with success/failure, exit code, error, and usage per attempt.
|
|
825
840
|
|
|
826
841
|
## Session Logs
|
|
827
842
|
|
|
@@ -904,6 +919,8 @@ Async runs write a dedicated observability folder:
|
|
|
904
919
|
|
|
905
920
|
`status.json` is the source of truth for async progress and powers both the TUI widget and `/subagents-status`. Async status and result files are written atomically, so readers do not observe partial JSON during background updates.
|
|
906
921
|
|
|
922
|
+
When fallback is used in async/background mode, `status.json` and the final result JSON include the final selected model, ordered attempted models, and per-attempt outcomes so background runs are as debuggable as sync runs.
|
|
923
|
+
|
|
907
924
|
For programmatic access:
|
|
908
925
|
|
|
909
926
|
```typescript
|
|
@@ -934,7 +951,7 @@ Async events:
|
|
|
934
951
|
├── chain-serializer.ts # Parse/serialize .chain.md files
|
|
935
952
|
├── async-execution.ts # Async/background execution support
|
|
936
953
|
├── async-status.ts # Async run discovery, listing, and formatting
|
|
937
|
-
├── execution.ts # Core runSync
|
|
954
|
+
├── execution.ts # Core runSync and sync fallback handling
|
|
938
955
|
├── render.ts # TUI rendering (widget, tool result display)
|
|
939
956
|
├── subagents-status.ts # Async status overlay component
|
|
940
957
|
├── artifacts.ts # Artifact management
|
|
@@ -942,6 +959,7 @@ Async events:
|
|
|
942
959
|
├── schemas.ts # TypeBox parameter schemas
|
|
943
960
|
├── utils.ts # Shared utility functions (mapConcurrent, readStatus, etc.)
|
|
944
961
|
├── types.ts # Shared types and constants
|
|
962
|
+
├── model-fallback.ts # Fallback candidate resolution and retry classification
|
|
945
963
|
├── subagent-runner.ts # Async runner (detached process)
|
|
946
964
|
├── parallel-utils.ts # Parallel execution utilities for async runner
|
|
947
965
|
├── worktree.ts # Git worktree isolation for parallel execution
|
package/agent-management.ts
CHANGED
|
@@ -130,6 +130,13 @@ function modelWarning(ctx: ManagementContext, model: string | undefined): string
|
|
|
130
130
|
return found ? undefined : `Warning: model '${model}' is not in the current model registry.`;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
function fallbackModelsWarning(ctx: ManagementContext, fallbackModels: string[] | undefined): string | undefined {
|
|
134
|
+
if (!fallbackModels || fallbackModels.length === 0) return undefined;
|
|
135
|
+
const available = new Set(ctx.modelRegistry.getAvailable().flatMap((m) => [`${m.provider}/${m.id}`, m.id]));
|
|
136
|
+
const missing = fallbackModels.filter((model) => !available.has(model));
|
|
137
|
+
return missing.length ? `Warning: fallback models not in the current model registry: ${missing.join(", ")}.` : undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
133
140
|
function skillsWarning(cwd: string, skills: string[] | undefined): string | undefined {
|
|
134
141
|
if (!skills || skills.length === 0) return undefined;
|
|
135
142
|
const available = new Set(discoverAvailableSkills(cwd).map((s) => s.name));
|
|
@@ -198,6 +205,19 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
|
|
|
198
205
|
else if (typeof cfg.model === "string") target.model = cfg.model.trim() || undefined;
|
|
199
206
|
else return "config.model must be a string or false when provided.";
|
|
200
207
|
}
|
|
208
|
+
if (hasKey(cfg, "fallbackModels")) {
|
|
209
|
+
if (cfg.fallbackModels === false || cfg.fallbackModels === "") target.fallbackModels = undefined;
|
|
210
|
+
else if (typeof cfg.fallbackModels === "string") {
|
|
211
|
+
const models = parseCsv(cfg.fallbackModels);
|
|
212
|
+
target.fallbackModels = models.length ? models : undefined;
|
|
213
|
+
} else if (Array.isArray(cfg.fallbackModels)) {
|
|
214
|
+
const models = cfg.fallbackModels
|
|
215
|
+
.filter((value): value is string => typeof value === "string")
|
|
216
|
+
.map((value) => value.trim())
|
|
217
|
+
.filter(Boolean);
|
|
218
|
+
target.fallbackModels = models.length ? [...new Set(models)] : undefined;
|
|
219
|
+
} else return "config.fallbackModels must be a comma-separated string, string array, or false when provided.";
|
|
220
|
+
}
|
|
201
221
|
if (hasKey(cfg, "tools")) {
|
|
202
222
|
if (cfg.tools === false || cfg.tools === "") { target.tools = undefined; target.mcpDirectTools = undefined; }
|
|
203
223
|
else if (typeof cfg.tools === "string") { const parsed = parseTools(cfg.tools); target.tools = parsed.tools; target.mcpDirectTools = parsed.mcpDirectTools; }
|
|
@@ -292,6 +312,7 @@ export function formatAgentDetail(agent: AgentConfig): string {
|
|
|
292
312
|
const tools = [...(agent.tools ?? []), ...(agent.mcpDirectTools ?? []).map((t) => `mcp:${t}`)];
|
|
293
313
|
const lines: string[] = [`Agent: ${agent.name} (${agent.source})`, `Path: ${agent.filePath}`, `Description: ${agent.description}`];
|
|
294
314
|
if (agent.model) lines.push(`Model: ${agent.model}`);
|
|
315
|
+
if (agent.fallbackModels?.length) lines.push(`Fallback models: ${agent.fallbackModels.join(", ")}`);
|
|
295
316
|
if (tools.length) lines.push(`Tools: ${tools.join(", ")}`);
|
|
296
317
|
if (agent.skills?.length) lines.push(`Skills: ${agent.skills.join(", ")}`);
|
|
297
318
|
if (agent.extensions !== undefined) lines.push(`Extensions: ${agent.extensions.length ? agent.extensions.join(", ") : "(none)"}`);
|
|
@@ -395,6 +416,8 @@ export function handleCreate(params: ManagementParams, ctx: ManagementContext):
|
|
|
395
416
|
if (applyError) return result(applyError, true);
|
|
396
417
|
const mw = modelWarning(ctx, agent.model);
|
|
397
418
|
if (mw) warnings.push(mw);
|
|
419
|
+
const fmw = fallbackModelsWarning(ctx, agent.fallbackModels);
|
|
420
|
+
if (fmw) warnings.push(fmw);
|
|
398
421
|
const sw = skillsWarning(ctx.cwd, agent.skills);
|
|
399
422
|
if (sw) warnings.push(sw);
|
|
400
423
|
fs.writeFileSync(targetPath, serializeAgent(agent), "utf-8");
|
|
@@ -431,6 +454,10 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
|
|
|
431
454
|
const mw = modelWarning(ctx, updated.model);
|
|
432
455
|
if (mw) warnings.push(mw);
|
|
433
456
|
}
|
|
457
|
+
if (hasKey(cfg, "fallbackModels")) {
|
|
458
|
+
const fmw = fallbackModelsWarning(ctx, updated.fallbackModels);
|
|
459
|
+
if (fmw) warnings.push(fmw);
|
|
460
|
+
}
|
|
434
461
|
if (hasKey(cfg, "skills")) {
|
|
435
462
|
const sw = skillsWarning(ctx.cwd, updated.skills);
|
|
436
463
|
if (sw) warnings.push(sw);
|
package/agent-manager-edit.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface EditState {
|
|
|
16
16
|
export interface EditInputResult { action?: "save" | "discard"; nextScreen?: EditScreen; }
|
|
17
17
|
|
|
18
18
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
19
|
-
const FIELD_ORDER = ["name", "description", "model", "thinking", "tools", "extensions", "skills", "output", "reads", "progress", "interactive", "prompt"] as const;
|
|
19
|
+
const FIELD_ORDER = ["name", "description", "model", "fallbackModels", "thinking", "tools", "extensions", "skills", "output", "reads", "progress", "interactive", "prompt"] as const;
|
|
20
20
|
type EditField = typeof FIELD_ORDER[number];
|
|
21
21
|
type ThinkingLevel = typeof THINKING_LEVELS[number];
|
|
22
22
|
const PROMPT_VIEWPORT_HEIGHT = 16;
|
|
@@ -29,7 +29,7 @@ function parseCommaList(value: string): string[] | undefined { const items = val
|
|
|
29
29
|
|
|
30
30
|
export function createEditState(draft: AgentConfig, isNew: boolean, models: ModelInfo[], skills: SkillInfo[]): EditState {
|
|
31
31
|
return {
|
|
32
|
-
draft: { ...draft, tools: draft.tools ? [...draft.tools] : undefined, mcpDirectTools: draft.mcpDirectTools ? [...draft.mcpDirectTools] : undefined, skills: draft.skills ? [...draft.skills] : undefined, extensions: draft.extensions ? [...draft.extensions] : draft.extensions, defaultReads: draft.defaultReads ? [...draft.defaultReads] : undefined, extraFields: draft.extraFields ? { ...draft.extraFields } : undefined },
|
|
32
|
+
draft: { ...draft, tools: draft.tools ? [...draft.tools] : undefined, mcpDirectTools: draft.mcpDirectTools ? [...draft.mcpDirectTools] : undefined, skills: draft.skills ? [...draft.skills] : undefined, fallbackModels: draft.fallbackModels ? [...draft.fallbackModels] : undefined, extensions: draft.extensions ? [...draft.extensions] : draft.extensions, defaultReads: draft.defaultReads ? [...draft.defaultReads] : undefined, extraFields: draft.extraFields ? { ...draft.extraFields } : undefined },
|
|
33
33
|
isNew, fieldIndex: 0, fieldMode: null, fieldEditor: createEditorState(), promptEditor: createEditorState(draft.systemPrompt ?? ""),
|
|
34
34
|
modelSearchQuery: "", modelCursor: 0, filteredModels: [...models], thinkingCursor: 0, skillSearchQuery: "", skillCursor: 0, filteredSkills: [...skills], skillSelected: new Set(draft.skills ?? []),
|
|
35
35
|
};
|
|
@@ -41,6 +41,7 @@ function renderFieldValue(field: EditField, state: EditState): string {
|
|
|
41
41
|
case "name": return draft.name;
|
|
42
42
|
case "description": return draft.description;
|
|
43
43
|
case "model": return draft.model ?? "default";
|
|
44
|
+
case "fallbackModels": return draft.fallbackModels && draft.fallbackModels.length > 0 ? draft.fallbackModels.join(", ") : "";
|
|
44
45
|
case "thinking": return draft.thinking ?? "off";
|
|
45
46
|
case "tools": return formatTools(draft);
|
|
46
47
|
case "extensions": return draft.extensions !== undefined ? (draft.extensions.length > 0 ? draft.extensions.join(", ") : "") : "(all)";
|
|
@@ -59,6 +60,7 @@ function applyFieldValue(field: EditField, state: EditState, value: string): voi
|
|
|
59
60
|
case "name": draft.name = value.trim(); break;
|
|
60
61
|
case "description": draft.description = value.trim(); break;
|
|
61
62
|
case "model": draft.model = value.trim() || undefined; break;
|
|
63
|
+
case "fallbackModels": draft.fallbackModels = parseCommaList(value); break;
|
|
62
64
|
case "tools": { const parsed = parseTools(value); draft.tools = parsed.tools; draft.mcpDirectTools = parsed.mcp; break; }
|
|
63
65
|
case "extensions": { const trimmed = value.trim(); draft.extensions = trimmed === "(all)" ? undefined : parseCommaList(trimmed) ?? []; break; }
|
|
64
66
|
case "skills": draft.skills = parseCommaList(value); break;
|
package/agent-serializer.ts
CHANGED
|
@@ -6,6 +6,7 @@ export const KNOWN_FIELDS = new Set([
|
|
|
6
6
|
"description",
|
|
7
7
|
"tools",
|
|
8
8
|
"model",
|
|
9
|
+
"fallbackModels",
|
|
9
10
|
"thinking",
|
|
10
11
|
"skill",
|
|
11
12
|
"skills",
|
|
@@ -36,6 +37,8 @@ export function serializeAgent(config: AgentConfig): string {
|
|
|
36
37
|
if (toolsValue) lines.push(`tools: ${toolsValue}`);
|
|
37
38
|
|
|
38
39
|
if (config.model) lines.push(`model: ${config.model}`);
|
|
40
|
+
const fallbackModelsValue = joinComma(config.fallbackModels);
|
|
41
|
+
if (fallbackModelsValue) lines.push(`fallbackModels: ${fallbackModelsValue}`);
|
|
39
42
|
if (config.thinking && config.thinking !== "off") lines.push(`thinking: ${config.thinking}`);
|
|
40
43
|
|
|
41
44
|
const skillsValue = joinComma(config.skills);
|
package/agents.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface AgentConfig {
|
|
|
21
21
|
tools?: string[];
|
|
22
22
|
mcpDirectTools?: string[];
|
|
23
23
|
model?: string;
|
|
24
|
+
fallbackModels?: string[];
|
|
24
25
|
thinking?: string;
|
|
25
26
|
systemPrompt: string;
|
|
26
27
|
source: AgentSource;
|
|
@@ -121,6 +122,10 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
121
122
|
?.split(",")
|
|
122
123
|
.map((s) => s.trim())
|
|
123
124
|
.filter(Boolean);
|
|
125
|
+
const fallbackModels = frontmatter.fallbackModels
|
|
126
|
+
?.split(",")
|
|
127
|
+
.map((model) => model.trim())
|
|
128
|
+
.filter(Boolean);
|
|
124
129
|
|
|
125
130
|
let extensions: string[] | undefined;
|
|
126
131
|
if (frontmatter.extensions !== undefined) {
|
|
@@ -143,6 +148,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
143
148
|
tools: tools.length > 0 ? tools : undefined,
|
|
144
149
|
mcpDirectTools: mcpDirectTools.length > 0 ? mcpDirectTools : undefined,
|
|
145
150
|
model: frontmatter.model,
|
|
151
|
+
fallbackModels: fallbackModels && fallbackModels.length > 0 ? fallbackModels : undefined,
|
|
146
152
|
thinking: frontmatter.thinking,
|
|
147
153
|
systemPrompt: body,
|
|
148
154
|
source,
|
package/async-execution.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { isParallelStep, resolveStepBehavior, type ChainStep, type SequentialSte
|
|
|
16
16
|
import type { RunnerStep } from "./parallel-utils.ts";
|
|
17
17
|
import { resolvePiPackageRoot } from "./pi-spawn.ts";
|
|
18
18
|
import { buildSkillInjection, normalizeSkillInput, resolveSkills } from "./skills.ts";
|
|
19
|
+
import { buildModelCandidates, resolveModelCandidate, type AvailableModelInfo } from "./model-fallback.ts";
|
|
19
20
|
import {
|
|
20
21
|
type ArtifactConfig,
|
|
21
22
|
type Details,
|
|
@@ -58,6 +59,7 @@ export interface AsyncChainParams {
|
|
|
58
59
|
chain: ChainStep[];
|
|
59
60
|
agents: AgentConfig[];
|
|
60
61
|
ctx: AsyncExecutionContext;
|
|
62
|
+
availableModels?: AvailableModelInfo[];
|
|
61
63
|
cwd?: string;
|
|
62
64
|
maxOutput?: MaxOutputConfig;
|
|
63
65
|
artifactsDir?: string;
|
|
@@ -85,6 +87,8 @@ export interface AsyncSingleParams {
|
|
|
85
87
|
sessionFile?: string;
|
|
86
88
|
skills?: string[];
|
|
87
89
|
output?: string | false;
|
|
90
|
+
modelOverride?: string;
|
|
91
|
+
availableModels?: AvailableModelInfo[];
|
|
88
92
|
maxSubagentDepth: number;
|
|
89
93
|
worktreeSetupHook?: string;
|
|
90
94
|
worktreeSetupHookTimeoutMs?: number;
|
|
@@ -146,6 +150,7 @@ export function executeAsyncChain(
|
|
|
146
150
|
worktreeSetupHookTimeoutMs,
|
|
147
151
|
} = params;
|
|
148
152
|
const chainSkills = params.chainSkills ?? [];
|
|
153
|
+
const availableModels = params.availableModels;
|
|
149
154
|
|
|
150
155
|
// Validate all agents exist before building steps
|
|
151
156
|
for (const s of chain) {
|
|
@@ -195,11 +200,15 @@ export function executeAsyncChain(
|
|
|
195
200
|
const outputPath = resolveSingleOutputPath(s.output, ctx.cwd, s.cwd ?? cwd);
|
|
196
201
|
const task = injectSingleOutputInstruction(s.task ?? "{previous}", outputPath);
|
|
197
202
|
|
|
203
|
+
const primaryModel = resolveModelCandidate(s.model ?? a.model, availableModels);
|
|
198
204
|
return {
|
|
199
205
|
agent: s.agent,
|
|
200
206
|
task,
|
|
201
207
|
cwd: s.cwd,
|
|
202
|
-
model: applyThinkingSuffix(
|
|
208
|
+
model: applyThinkingSuffix(primaryModel, a.thinking),
|
|
209
|
+
modelCandidates: buildModelCandidates(s.model ?? a.model, a.fallbackModels, availableModels).map((candidate) =>
|
|
210
|
+
applyThinkingSuffix(candidate, a.thinking),
|
|
211
|
+
),
|
|
203
212
|
tools: a.tools,
|
|
204
213
|
extensions: a.extensions,
|
|
205
214
|
mcpDirectTools: a.mcpDirectTools,
|
|
@@ -319,6 +328,7 @@ export function executeAsyncSingle(
|
|
|
319
328
|
worktreeSetupHookTimeoutMs,
|
|
320
329
|
} = params;
|
|
321
330
|
const skillNames = params.skills ?? agentConfig.skills ?? [];
|
|
331
|
+
const availableModels = params.availableModels;
|
|
322
332
|
const { resolved: resolvedSkills } = resolveSkills(skillNames, ctx.cwd);
|
|
323
333
|
let systemPrompt = agentConfig.systemPrompt?.trim() || null;
|
|
324
334
|
if (resolvedSkills.length > 0) {
|
|
@@ -349,7 +359,10 @@ export function executeAsyncSingle(
|
|
|
349
359
|
agent,
|
|
350
360
|
task: taskWithOutputInstruction,
|
|
351
361
|
cwd,
|
|
352
|
-
model: applyThinkingSuffix(agentConfig.model, agentConfig.thinking),
|
|
362
|
+
model: applyThinkingSuffix(resolveModelCandidate(params.modelOverride ?? agentConfig.model, availableModels), agentConfig.thinking),
|
|
363
|
+
modelCandidates: buildModelCandidates(params.modelOverride ?? agentConfig.model, agentConfig.fallbackModels, availableModels).map((candidate) =>
|
|
364
|
+
applyThinkingSuffix(candidate, agentConfig.thinking),
|
|
365
|
+
),
|
|
353
366
|
tools: agentConfig.tools,
|
|
354
367
|
extensions: agentConfig.extensions,
|
|
355
368
|
mcpDirectTools: agentConfig.mcpDirectTools,
|
package/async-status.ts
CHANGED
|
@@ -11,6 +11,9 @@ export interface AsyncRunStepSummary {
|
|
|
11
11
|
durationMs?: number;
|
|
12
12
|
tokens?: TokenUsage;
|
|
13
13
|
skills?: string[];
|
|
14
|
+
model?: string;
|
|
15
|
+
attemptedModels?: string[];
|
|
16
|
+
error?: string;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export interface AsyncRunSummary {
|
|
@@ -81,6 +84,9 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
81
84
|
...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
|
|
82
85
|
...(step.tokens ? { tokens: step.tokens } : {}),
|
|
83
86
|
...(step.skills ? { skills: step.skills } : {}),
|
|
87
|
+
...(step.model ? { model: step.model } : {}),
|
|
88
|
+
...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
|
|
89
|
+
...(step.error ? { error: step.error } : {}),
|
|
84
90
|
})),
|
|
85
91
|
...(status.sessionDir ? { sessionDir: status.sessionDir } : {}),
|
|
86
92
|
...(status.outputFile ? { outputFile: status.outputFile } : {}),
|
|
@@ -147,6 +153,7 @@ export function listAsyncRunsForOverlay(asyncDirRoot: string, recentLimit = 5):
|
|
|
147
153
|
|
|
148
154
|
function formatStepLine(step: AsyncRunStepSummary): string {
|
|
149
155
|
const parts = [`${step.index + 1}. ${step.agent}`, step.status];
|
|
156
|
+
if (step.model) parts.push(step.model);
|
|
150
157
|
if (step.durationMs !== undefined) parts.push(formatDuration(step.durationMs));
|
|
151
158
|
if (step.tokens) parts.push(`${formatTokens(step.tokens.total)} tok`);
|
|
152
159
|
return parts.join(" | ");
|
package/chain-execution.ts
CHANGED
|
@@ -48,23 +48,7 @@ import {
|
|
|
48
48
|
MAX_CONCURRENCY,
|
|
49
49
|
resolveChildMaxSubagentDepth,
|
|
50
50
|
} from "./types.ts";
|
|
51
|
-
|
|
52
|
-
/** Resolve a model name to its full provider/model format */
|
|
53
|
-
function resolveModelFullId(modelName: string | undefined, availableModels: ModelInfo[]): string | undefined {
|
|
54
|
-
if (!modelName) return undefined;
|
|
55
|
-
if (modelName.includes("/")) return modelName;
|
|
56
|
-
|
|
57
|
-
const colonIdx = modelName.lastIndexOf(":");
|
|
58
|
-
const baseModel = colonIdx !== -1 ? modelName.substring(0, colonIdx) : modelName;
|
|
59
|
-
const thinkingSuffix = colonIdx !== -1 ? modelName.substring(colonIdx) : "";
|
|
60
|
-
|
|
61
|
-
const match = availableModels.find((m) => m.id === baseModel);
|
|
62
|
-
if (match) {
|
|
63
|
-
return thinkingSuffix ? `${match.fullId}${thinkingSuffix}` : match.fullId;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return modelName;
|
|
67
|
-
}
|
|
51
|
+
import { resolveModelCandidate } from "./model-fallback.ts";
|
|
68
52
|
|
|
69
53
|
interface ChainExecutionDetailsInput {
|
|
70
54
|
results: SingleResult[];
|
|
@@ -191,8 +175,8 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
191
175
|
|
|
192
176
|
const taskAgentConfig = input.agents.find((agent) => agent.name === task.agent);
|
|
193
177
|
const effectiveModel =
|
|
194
|
-
(task.model ?
|
|
195
|
-
??
|
|
178
|
+
(task.model ? resolveModelCandidate(task.model, input.availableModels) : null)
|
|
179
|
+
?? resolveModelCandidate(taskAgentConfig?.model, input.availableModels);
|
|
196
180
|
const maxSubagentDepth = resolveChildMaxSubagentDepth(input.maxSubagentDepth, taskAgentConfig?.maxSubagentDepth);
|
|
197
181
|
|
|
198
182
|
const taskCwd = input.worktreeSetup
|
|
@@ -216,6 +200,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
216
200
|
outputPath,
|
|
217
201
|
maxSubagentDepth,
|
|
218
202
|
modelOverride: effectiveModel,
|
|
203
|
+
availableModels: input.availableModels,
|
|
219
204
|
skills: behavior.skills === false ? [] : behavior.skills,
|
|
220
205
|
onUpdate: input.onUpdate
|
|
221
206
|
? (progressUpdate) => {
|
|
@@ -643,8 +628,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
643
628
|
// Resolve model: TUI override (already full format) or agent's model resolved to full format
|
|
644
629
|
const effectiveModel =
|
|
645
630
|
tuiOverride?.model
|
|
646
|
-
?? (seqStep.model ?
|
|
647
|
-
??
|
|
631
|
+
?? (seqStep.model ? resolveModelCandidate(seqStep.model, availableModels) : null)
|
|
632
|
+
?? resolveModelCandidate(agentConfig.model, availableModels);
|
|
648
633
|
|
|
649
634
|
// Run step
|
|
650
635
|
const outputPath = typeof behavior.output === "string"
|
|
@@ -665,6 +650,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
665
650
|
outputPath,
|
|
666
651
|
maxSubagentDepth,
|
|
667
652
|
modelOverride: effectiveModel,
|
|
653
|
+
availableModels,
|
|
668
654
|
skills: behavior.skills === false ? [] : behavior.skills,
|
|
669
655
|
onUpdate: onUpdate
|
|
670
656
|
? (p) => {
|