pi-subagents 0.12.5 → 0.13.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/CHANGELOG.md +22 -0
- package/README.md +46 -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/index.ts +1 -1
- package/intercom-bridge.ts +115 -0
- 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/subagent-executor.ts +59 -30
- package/subagent-runner.ts +163 -32
- package/subagents-status.ts +8 -1
- package/types.ts +24 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.13.1] - 2026-04-13
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added optional intercom orchestration bridge for delegated runs. When enabled via `intercomBridge` (default `fork-only`) and `pi-intercom` is available, child subagents get runtime coordination instructions for contacting the orchestrator session via `intercom`, and `intercom` is auto-added to the child tool allowlist when needed.
|
|
9
|
+
- Added unit coverage for intercom bridge activation, config handling, and extension allowlist behavior.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Normalized `subagent-executor.ts` relative imports to `.ts` specifiers to match direct TypeScript runtime loading.
|
|
13
|
+
- Documented `pi-intercom` installation and activation requirements in README.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Tightened intercom extension allowlist matching to avoid false positives from similarly named extension paths.
|
|
17
|
+
|
|
18
|
+
## [0.13.0] - 2026-04-11
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- 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.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- 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.
|
|
25
|
+
- 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.
|
|
26
|
+
|
|
5
27
|
## [0.12.5] - 2026-04-09
|
|
6
28
|
|
|
7
29
|
### Fixed
|
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:
|
|
@@ -324,6 +331,9 @@ Chains can be created from the Agents Manager template picker ("Blank Chain"), o
|
|
|
324
331
|
| Parallel | Yes | `{ tasks: [{agent, task}...] }` - via TUI toggle or converted to chain for async |
|
|
325
332
|
|
|
326
333
|
Execution context defaults to `context: "fresh"`, which starts each child run from a clean session. Set `context: "fork"` to start each child from a real branched session created from the parent's current leaf.
|
|
334
|
+
When `intercomBridge` is enabled (default: `fork-only`) and `pi-intercom` is installed/enabled, forked children get runtime instructions for contacting the orchestrator session via `intercom({ action: "ask"|"send", ... })`.
|
|
335
|
+
|
|
336
|
+
> **Note:** Intercom bridging requires the [pi-intercom](https://github.com/nicobailon/pi-intercom) extension. Install it with `pi install npm:pi-intercom`.
|
|
327
337
|
|
|
328
338
|
All modes support foreground and background execution. Foreground is the default (the call waits and streams progress). For programmatic background launch, use `clarify: false, async: true`. For interactive background launch, use `clarify: true` and press `b` in the TUI before running. Chains with parallel steps (`{ parallel: [...] }`) run concurrently with configurable `concurrency` and `failFast` options.
|
|
329
339
|
|
|
@@ -552,6 +562,7 @@ Agent definitions are not loaded into LLM context by default. Management actions
|
|
|
552
562
|
scope: "user",
|
|
553
563
|
systemPrompt: "You are a code scout...",
|
|
554
564
|
model: "anthropic/claude-sonnet-4",
|
|
565
|
+
fallbackModels: ["openai/gpt-5-mini", "anthropic/claude-haiku-4-5"],
|
|
555
566
|
tools: "read, bash, mcp:github/search_repositories",
|
|
556
567
|
extensions: "", // empty = no extensions
|
|
557
568
|
skills: "parallel-scout",
|
|
@@ -590,7 +601,7 @@ Agent definitions are not loaded into LLM context by default. Management actions
|
|
|
590
601
|
Notes:
|
|
591
602
|
- `create` uses `config.scope` (`"user"` or `"project"`), not `agentScope`.
|
|
592
603
|
- `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.
|
|
604
|
+
- 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
605
|
- To clear any optional field, set it to `false` or `""` (e.g., `{ model: false }` or `{ skills: "" }`). Both work for all string-typed fields.
|
|
595
606
|
|
|
596
607
|
## Parameters
|
|
@@ -601,10 +612,11 @@ Notes:
|
|
|
601
612
|
| `task` | string | - | Task string (single mode) |
|
|
602
613
|
| `action` | string | - | Management action: `list`, `get`, `create`, `update`, `delete` |
|
|
603
614
|
| `chainName` | string | - | Chain name for management get/update/delete |
|
|
604
|
-
| `config` | object | - | Agent or chain config for management create/update |
|
|
615
|
+
| `config` | object | - | Agent or chain config for management create/update. Agent configs also accept `fallbackModels` (comma-separated string or string array). |
|
|
605
616
|
| `output` | `string \| false` | agent default | Override output file for single agent (absolute path as-is, relative path resolved against cwd) |
|
|
606
617
|
| `skill` | `string \| string[] \| false` | agent default | Override skills (comma-separated string, array, or false to disable) |
|
|
607
618
|
| `model` | string | agent default | Override model for single agent |
|
|
619
|
+
| `fallbackModels` | `string \| string[]` | agent default | Management/config-only field for ordered backup models. Markdown frontmatter uses a comma-separated string. |
|
|
608
620
|
| `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
621
|
| `worktree` | boolean | false | Create isolated git worktrees for each parallel task. Requires clean git state. Per-worktree diffs included in output. |
|
|
610
622
|
| `chain` | ChainItem[] | - | Sequential steps with behavior overrides (see below) |
|
|
@@ -637,6 +649,8 @@ Notes:
|
|
|
637
649
|
| `skill` | `string \| string[] \| false` | agent default | Override skills or disable all |
|
|
638
650
|
| `model` | string | agent default | Override model for this step |
|
|
639
651
|
|
|
652
|
+
Fallbacks are inherited from the selected agent for that step. There is no per-step `fallbackModels` override in v1.
|
|
653
|
+
|
|
640
654
|
*Parallel step fields:*
|
|
641
655
|
|
|
642
656
|
| Field | Type | Default | Description |
|
|
@@ -660,6 +674,8 @@ Notes:
|
|
|
660
674
|
| `skill` | `string \| string[] \| false` | agent default | Override skills or disable all |
|
|
661
675
|
| `model` | string | agent default | Override model for this task |
|
|
662
676
|
|
|
677
|
+
Fallbacks are inherited from the selected agent for that task. There is no per-task `fallbackModels` override in v1.
|
|
678
|
+
|
|
663
679
|
Status tool:
|
|
664
680
|
|
|
665
681
|
| Tool | Description |
|
|
@@ -765,6 +781,27 @@ Sessions are always enabled — every subagent run gets a session directory for
|
|
|
765
781
|
|
|
766
782
|
Per-agent `maxSubagentDepth` can tighten that limit further for child runs, but it does not relax an already inherited stricter limit.
|
|
767
783
|
|
|
784
|
+
### `intercomBridge`
|
|
785
|
+
|
|
786
|
+
Controls whether subagents receive runtime intercom coordination instructions (and `intercom` is auto-added to their tool allowlist when needed).
|
|
787
|
+
|
|
788
|
+
```json
|
|
789
|
+
{
|
|
790
|
+
"intercomBridge": "fork-only"
|
|
791
|
+
}
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
Values:
|
|
795
|
+
- `"fork-only"` (default): inject intercom bridge only when `context: "fork"`
|
|
796
|
+
- `"always"`: inject bridge in both `fresh` and `fork`
|
|
797
|
+
- `"off"`: disable bridge entirely
|
|
798
|
+
|
|
799
|
+
Bridge activation also requires all of the following:
|
|
800
|
+
- [pi-intercom](https://github.com/nicobailon/pi-intercom) is installed (`pi install npm:pi-intercom`)
|
|
801
|
+
- `~/.pi/agent/intercom/config.json` is not set to `"enabled": false`
|
|
802
|
+
- the current session has a name to target as orchestrator
|
|
803
|
+
- if agent `extensions` is an explicit allowlist, it must include `pi-intercom`
|
|
804
|
+
|
|
768
805
|
### `worktreeSetupHook`
|
|
769
806
|
|
|
770
807
|
`worktreeSetupHook` configures an optional setup hook for worktree-isolated parallel runs. The hook runs once per created worktree, after `git worktree add` succeeds and before the agent starts.
|
|
@@ -821,7 +858,9 @@ Files per task:
|
|
|
821
858
|
- `{runId}_{agent}_input.md` - Task prompt
|
|
822
859
|
- `{runId}_{agent}_output.md` - Full output (untruncated)
|
|
823
860
|
- `{runId}_{agent}.jsonl` - Event stream (sync only)
|
|
824
|
-
- `{runId}_{agent}_meta.json` - Timing, usage, exit code
|
|
861
|
+
- `{runId}_{agent}_meta.json` - Timing, usage, exit code, final model, attempted models, and per-attempt outcomes
|
|
862
|
+
|
|
863
|
+
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
864
|
|
|
826
865
|
## Session Logs
|
|
827
866
|
|
|
@@ -904,6 +943,8 @@ Async runs write a dedicated observability folder:
|
|
|
904
943
|
|
|
905
944
|
`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
945
|
|
|
946
|
+
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.
|
|
947
|
+
|
|
907
948
|
For programmatic access:
|
|
908
949
|
|
|
909
950
|
```typescript
|
|
@@ -934,7 +975,7 @@ Async events:
|
|
|
934
975
|
├── chain-serializer.ts # Parse/serialize .chain.md files
|
|
935
976
|
├── async-execution.ts # Async/background execution support
|
|
936
977
|
├── async-status.ts # Async run discovery, listing, and formatting
|
|
937
|
-
├── execution.ts # Core runSync
|
|
978
|
+
├── execution.ts # Core runSync and sync fallback handling
|
|
938
979
|
├── render.ts # TUI rendering (widget, tool result display)
|
|
939
980
|
├── subagents-status.ts # Async status overlay component
|
|
940
981
|
├── artifacts.ts # Artifact management
|
|
@@ -942,6 +983,7 @@ Async events:
|
|
|
942
983
|
├── schemas.ts # TypeBox parameter schemas
|
|
943
984
|
├── utils.ts # Shared utility functions (mapConcurrent, readStatus, etc.)
|
|
944
985
|
├── types.ts # Shared types and constants
|
|
986
|
+
├── model-fallback.ts # Fallback candidate resolution and retry classification
|
|
945
987
|
├── subagent-runner.ts # Async runner (detached process)
|
|
946
988
|
├── parallel-utils.ts # Parallel execution utilities for async runner
|
|
947
989
|
├── 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) => {
|