@dylanrussell/agent-router 1.0.6 → 1.1.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/README.md CHANGED
@@ -29,28 +29,29 @@ opencode agents are markdown files (`~/.config/opencode/agents/*.md`) whose YAML
29
29
  ---
30
30
  description: High-reasoning review, debugging, and architecture counsel
31
31
  mode: subagent
32
- model: anthropic/claude-opus-4-8 # ← the ONLY line agent-router touches
32
+ model: anthropic/claude-opus-4-8 # ← agent-router rewrites this
33
+ reasoningEffort: high # ← and any option keys a stack names
33
34
  temperature: 0.1
34
35
  tools: { write: false, edit: false }
35
36
  ---
36
37
  <prompt body — owned by you, never touched by agent-router>
37
38
  ```
38
39
 
39
- `agent-router` keeps named **stacks** — JSON files mapping agent names to models — and applies them to those `model:` lines on demand. Premium models for the workday, cheap ones for bulk chores, one command to swap the whole crew:
40
+ `agent-router` keeps named **stacks** — JSON files mapping agent names to models (and optionally to provider options) — and applies them to those frontmatter lines on demand. Premium models for the workday, cheap ones for bulk chores, one command to swap the whole crew:
40
41
 
41
42
  ```json
42
43
  {
43
44
  "agents": {
44
45
  "Omni": { "model": "anthropic/claude-fable-5" },
45
- "oracle": { "model": "openai/gpt-5.5" },
46
+ "oracle": { "model": "openai/gpt-5.5", "reasoningEffort": "high" },
46
47
  "explorer": { "model": "openai/gpt-5.4-mini" },
47
48
  "librarian": { "model": "openai/gpt-5.4-mini" },
48
- "fixer": { "model": "openai/gpt-5.5" }
49
+ "fixer": { "model": "openai/gpt-5.5", "thinking": { "effort": "low" } }
49
50
  }
50
51
  }
51
52
  ```
52
53
 
53
- The prompt body and every other frontmatter key are yours; agent-router rewrites exactly one line per agent, atomically, through symlinks (dotfile-manager setups survive intact).
54
+ The prompt body and opencode framework keys (`description`, `mode`, `permission`, `tools`, …) are yours; agent-router rewrites only the `model:` line and the option keys a stack entry names, atomically, through symlinks (dotfile-manager setups survive intact).
54
55
 
55
56
  ```
56
57
  ~/.config/opencode/
@@ -104,11 +105,83 @@ The everyday loop: tune your agents until you like them → `capture <name>` →
104
105
 
105
106
  ## Stacks
106
107
 
107
- A stack file needs one thing: an `agents` record whose entries carry a `model` string. Agent names are the `.md` basenames in your agents dir (`Omni` ↔ `Omni.md`). Unknown keys are preserved round-trip.
108
+ A stack file needs one thing: an `agents` record whose entries carry a `model` string. Agent names are the `.md` basenames in your agents dir (`Omni` ↔ `Omni.md`). Except for router-owned `fallbacks`, other keys on an entry are provider pass-through options (`reasoningEffort`, `thinking`, `temperature`, `topP`, `maxOutputTokens`, …) that opencode forwards to the model — agent-router transcribes these to the agent's frontmatter alongside `model:` and captures them back. Unknown keys are preserved round-trip.
109
+
110
+ ```json
111
+ {
112
+ "agents": {
113
+ "oracle": {
114
+ "model": "openai/gpt-5.5",
115
+ "reasoningEffort": "high",
116
+ "thinking": { "effort": "low" }
117
+ }
118
+ }
119
+ }
120
+ ```
121
+
122
+ - **Apply** writes/updates only the option keys a stack entry names; options the stack omits are left in place. To **clear** an option, set it to `null` in the stack entry.
123
+ - Reserved opencode framework keys (`description`, `mode`, `permission`, `tools`, `prompt`, `steps`, `color`, `name`) are never transcribed — a stack entry can't clobber them.
124
+ - Option values serialize to a single frontmatter line: scalars bare (quoted only when needed), objects/arrays as JSON flow. Block-style values an apply replaces are collapsed to flow; `capture` parses them back into typed JSON.
108
125
 
109
126
  `use` is strict by design: if a stack references an agent file that doesn't exist, or one without a `model:` line, the switch fails **before anything is written** — your suite is never left half-switched. It also validates every model ID against `opencode models` first (skip with `--no-validate`, override with `--force-invalid`).
110
127
 
111
- `capture` is the inverse: it reads the current `model:` line of every agent file (files without one are skipped) and writes a stack. There are no bundled seed stacks — your real setup is the seed.
128
+ `capture` is the inverse: it reads the current `model:` line and option keys of every agent file (files without a `model:` line are skipped) and writes a stack. There are no bundled seed stacks — your real setup is the seed.
129
+
130
+ ## Ordered Fallbacks
131
+
132
+ Optional per-agent fallback chains select a model for the **next explicitly submitted user turn**, not an automatic retry of the failed turn. Enable by adding `fallbacks` to a stack, applying that stack, and restarting opencode with the server plugin loaded:
133
+
134
+ ```json
135
+ {
136
+ "agents": {
137
+ "oracle": {
138
+ "model": "openai/gpt-5.5",
139
+ "variant": "high",
140
+ "fallbacks": [
141
+ { "model": "anthropic/claude-sonnet-4-6", "variant": "low" },
142
+ { "model": "openai/gpt-5.4-mini" }
143
+ ]
144
+ }
145
+ }
146
+ }
147
+ ```
148
+
149
+ Model IDs and variants above are illustrative; use models and variants supported by your providers.
150
+
151
+ - `model` remains the primary. Primary `variant` is an optional nonempty string; `null` clears it on apply.
152
+ - `fallbacks` is optional, or an array of 0 to 8 objects. Each object contains only `model` (non-whitespace `provider/model`) and optional `variant` (nonempty string). `null`, bare strings, unknown candidate fields, and longer chains are rejected.
153
+ - Candidate order is preserved. Exact `(model, variant)` duplicates, including the primary, are skipped at runtime. An omitted fallback variant clears the previous candidate's variant.
154
+ - Only model and variant change at runtime. Agent prompts, permissions, tools, and other provider options remain unchanged; ensure those options are compatible with every candidate.
155
+ - Validation checks every primary and fallback ID, reporting paths such as `agents.oracle.fallbacks.0.model`. It checks catalogue membership through `opencode models`, not live provider health, credentials, quota, or variant support. `validate --active` includes applied fallback metadata when its primary still matches frontmatter.
156
+ - Failover does not rewrite frontmatter, stacks, history, or router state. `use` stores the applied chains in optional `state.json.fallbackAgents`. Old state files without that field remain valid and have no enabled chains until the stack is reapplied.
157
+ - `capture` and displaced-history snapshots merge those applied chains with live frontmatter only when primary model and variant still match. Editing a stack alone does not alter applied metadata. Applying a stack without chains clears the applied chains, including agents omitted from that stack.
158
+ - `back` retains its existing semantics: reapply a previous **named stack as it exists now**, not restore historical snapshot bytes. Runtime failover never adds a history entry.
159
+
160
+ ### Runtime Behavior And Limits
161
+
162
+ The plugin watches correlated assistant `message.updated` errors, requiring the current user-message parent ID, agent, and selected provider/model. It stages the next candidate only for structured `APIError` with `isRetryable: true` and HTTP **429, 500, 502, 503, or 504**. Native opencode retries may occur before the terminal error reaches this hook. Router does not change their policy or timing.
163
+
164
+ Authentication/permission errors, context overflow, content filtering, output-length errors, unknown errors, transport errors without an HTTP status, and ambiguous `session.error` notifications do not advance the chain. Router never classifies errors by substring matching.
165
+
166
+ After a qualifying failure a 15-second warning toast (or log fallback) names the next model and asks for an explicit retry/continue instruction. Every notice is also logged under `agent-router`. **Review partial output and completed tools before continuing.** The mutable `chat.message` hook selects the fallback when the next prompt is admitted; no prompt is submitted by this plugin. The successful fallback stays selected for subsequent turns. Chains advance monotonically with at most eight transitions per agent/session; exhaustion produces a notice, never wraps to the primary. There are no timers, background retries, or automatic failback.
167
+
168
+ Observed cancellation (`MessageAbortedError`), explicit model/agent switch events, and an incoming model/variant different from both the configured primary and current candidate disable routing for that session until restart. A switch to the same model is distinguishable only when the host emits its switch event. Hosts/UI paths that omit both the event and a changed selection cannot expose that manual intent to this plugin. Switching stacks with router tools disables running failover immediately; external CLI/TUI state changes disable it at the next prompt admission. Restart to activate the newly applied stack.
169
+
170
+ Duplicate/concurrent error events advance only once per user turn. Selection and bookkeeping run synchronously before notification awaits, so delayed notices cannot overwrite cancellation/manual selection. Sessions and per-agent budgets are isolated; deleted/disabled sessions remain tombstones to prevent rearming. After 1,024 tracked sessions in one plugin instance, additional sessions receive no automatic routing until restart. Untracked sessions and subagents that bypass `chat.message` are deliberately left alone.
171
+
172
+ **Verified boundary:** inspected opencode **v1.17.15** `packages/opencode/src/session/prompt.ts`: `chat.message` mutates the message before `sessions.updateMessage(info)` and the legacy session loop resolves `lastUser.model`. A deterministic in-process provider harness tests those hook semantics, explicit next-turn continuation, variant selection, and no replay/persistent writes. This is **not a real-server end-to-end provider test** and does not prove that every new v2 execution path invokes these legacy hooks. Hosts that bypass them receive no failover. There is no transparent same-turn continuation guarantee, no automatic recovery of a failed `task` invocation, and no guarantee that an LLM will not choose to repeat a tool when explicitly asked to continue.
173
+
174
+ The installed SDK exposes `/v2` `client.v2.session.switchModel` for subsequent turns, but no atomic compare-and-switch/retry contract was verified. Router deliberately does **not** call it: an asynchronous model write could race a manual selection or cancellation. No direct SDK dependency is needed; plugin hooks are the integration boundary. The TUI model picker and router status still display stack/primary assignments, not session-local runtime candidates.
175
+
176
+ ### Install This Checkout
177
+
178
+ Ordered fallbacks require agent-router **1.1.0 or later**. To test a local checkout instead of the published package, run `npm run typecheck`, `npm test`, and `npm run build` using the installed dependencies (a fresh checkout uses `pnpm install --frozen-lockfile`). Replace `/absolute/path/to/agent-router` below with your checkout path, then replace the registry router entry in the **server** `opencode.json` plugin array with:
179
+
180
+ ```json
181
+ "file:///absolute/path/to/agent-router/dist/plugin.js"
182
+ ```
183
+
184
+ Do not load both registry and local server plugins. If using the router TUI plugin, also replace its registry entry in `tui.json` with `file:///absolute/path/to/agent-router/dist/tui.js`. Use the checkout's CLI too: older router builds strictly validate state and cannot read the new `fallbackAgents` field. Add a chain to your stack and run `node /absolute/path/to/agent-router/dist/cli.js use <stack-name>`, then quit and restart opencode. These installation/configuration steps are instructions only; implementation and tests do not modify your live agent configuration. Start a new session, and confirm you see the explicit next-turn notice after a qualifying terminal error. Headless clients must inspect the `agent-router` log.
112
185
 
113
186
  ## Inside opencode
114
187
 
@@ -167,12 +240,12 @@ Paths resolve in this order: explicit option → `config.json` → env var → d
167
240
  ## ⚠ Things to know
168
241
 
169
242
  - **Restart required.** opencode reads agent files once at startup. After every `agent-router use`, restart opencode for the new models to take effect. The CLI reminds you.
170
- - **Hand-edits to frontmatter are not auto-saved into stacks.** If you hand-tune a model and want to keep it, `capture` it (or `capture <active> --force`). The next `use` that touches that agent overwrites the hand-edit.
243
+ - **Hand-edits to frontmatter are not auto-saved into stacks.** If you hand-tune a model or option and want to keep it, `capture` it (or `capture <active> --force`). The next `use` that touches that agent overwrites the hand-edit for keys the stack names; keys the stack omits are preserved (set an option to `null` in a stack entry to clear it).
171
244
  - **Validation is auth-state-dependent.** `agent-router validate` runs `opencode models`, which only lists models reachable through your current auth. If you revoke a key, previously-valid stacks may suddenly be invalid.
172
245
 
173
246
  ## FAQ
174
247
 
175
- **Why no variants/fallback models?** Native agent frontmatter has a single `model:` line. If you want a fallback, make it a stack (`cheap`, `free`) and switch to it.
248
+ **Why no variants/fallback models?** Native agent frontmatter has a single `model:` line. If you want a fallback, make it a stack (`cheap`, `free`) and switch to it. For per-stack *behavior* variants (reasoning effort, thinking budget, temperature, …), add the option as a sibling key on the stack entry — it transcribes to frontmatter on `use`.
176
249
 
177
250
  **Can I have per-project stacks?** Point `AGENT_ROUTER_STACKS_DIR` at a project-local directory in that project's shell env.
178
251