Three-way picker maps to {provider, escalationEnabled} pairs. Wired
to ConfigStore for hot-reload. Local-first is the new default
(default-config.json).
- ProviderMode enum (Local-first / Anthropic only / Ollama only) +
bidirectional mapping to {provider, escalationEnabled}.
- New Provider section in SettingsView, hydrated from ConfigStore
on .onAppear, persists through ConfigStore.updatePerTurn(_:) on
.onChange.
- PerTurnSnapshot.with(provider:escalationEnabled:) — multi-field
copy so the picker can mutate both fields atomically.
- ConfigStore threaded through SettingsWindowController.open(...)
from AppDelegate.openSettings().
- ProviderModeTests covers canonical mapping, (anthropic, true)
edge case normalization, and round-trip via .pair.
Refs design spec §6.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When a chat turn is escalated, the bus client attaches the
EscalationDecision to the most-recent assistant message; the message
renderer shows an inline badge with friendly reason text. Six
OllamaFailureKind reasons mapped to plain English.
If escalation fires before any assistant tokens have streamed (e.g.
connectionFailure on the very first response), the decision is parked
on the store as pendingEscalation and stamped onto the first text part
created by a subsequent tokenDelta — so the badge is never dropped.
Refs design spec §4.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Local-first LLM routing Task 7. Carries an EscalationDecision from the
orchestrator to the HUD so the escalation badge can render the
ollama->anthropic transition + reason. BusForwarder gains an .escalated
arm that routes the AgentCore-side decision through a new
BusForwarderSink.sendEscalated hook; AppBusForwarderSink translates the
AgentCore types into the dependency-free Bus mirror types
(BusEscalationDecision / BusProviderSelection / BusOllamaFailureKind),
preserving the lightweight-bridge-layer invariant.
Bus protocol bumped 2.3.0 -> 2.4.0 (MINOR, additive). TS decoder gains
matching guards; HUD client adds a no-op acknowledgment for the
exhaustiveness sentinel (the EscalationBadge component lands in Task 8).
Round-trip coverage:
- packages/Bus: fixture-driven Codable round-trip + new BusForwarder
tests asserting sendEscalated fires without spurious lifecycle events.
- webview/packages/bus: fixture parity + protocol-version test bumped.
Refs spec docs/superpowers/specs/2026-05-14-local-first-llm-routing-design.md §4.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extend the per-turn retry hook to handle OllamaFailureKind-bearing
provider errors via providerError.ollamaFailureKind. When the local
Ollama provider emits a malformedToolCall / emptyResponse on a turn
with escalationEnabled, the orchestrator allocates a fresh turn row
(retryOf the original), swaps to Anthropic, and re-runs the turn from
initialMessages. Enforces at-most-one escalation per turn — a second
provider failure surfaces a terminal `.error` event. The
`escalatedThisTurn` flag is local to `runTurnLoop` so it resets across
turns naturally (no sticky cross-turn state).
Adds:
- `OrchestratorEvent.escalated(EscalationDecision)` — surfaced once
per escalation; Task 7 wires this to BusOutbound for the HUD badge.
- `PerTurnSnapshot.with(provider:)` — convenience copy used by the
orchestrator to swap providers per-turn without mutating ConfigStore.
The existing AGENT-09 streamTruncated retry (in `.stopReason`) is left
parallel; it triggers on a different LLMEvent shape (transport-mid-
stream EOF) and was independently validated by OrchestratorRetryTests.
Refs design spec §2, §3. Tests: LocalFirstEscalationTests A/B/C
(reactive fire-once / budget honored / disabled passthrough).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wire detection into the NDJSON parser. Adds two new LLMProviderError
cases (`.malformedToolCall(reason:)`, `.emptyResponse`) and an
`ollamaFailureKind: OllamaFailureKind?` accessor so the orchestrator
(Task 6) can route to Anthropic on these failure modes.
NDJSONDecoder tracks `sawAnyTextDelta` / `sawAnyToolUseRequested`; on
the `done:true` terminator it emits `.providerError(.emptyResponse)`
before `.messageStop` when neither was observed. The tool_calls path
now validates string-form `arguments` via `JSONSerialization` and
emits `.malformedToolCall(reason:)` on parse failure instead of
silently forwarding garbage bytes.
OpenAI-compat path is scoped to follow-up — `/api/chat` is the
default transport per CLAUDE.md and where reactive escalation lands
first.
Refs design spec §2.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes the policy half of the local-first routing spec. Pinned-anthropic
and pinned-ollama remain valid via Settings (Task 9). Memory extraction
and Anthropic API key wiring untouched.
Also updates ConfigLoaderTests.test_loadDefaultConfigFromBundle to
assert the new ollama default (it asserted .anthropic against the same
bundled resource — a mirror of the policy being flipped).
Refs design spec §1, §6.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Guard against a future regression where a custom encode(to:) drops the
field. Today encoder symmetry holds via Swift's Codable synthesis;
this test pins it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Defaults to true. Custom decoder uses decodeIfPresent so older configs
without the field opt into the new local-first behavior on upgrade.
Init parameter default keeps existing call sites source-compatible.
Refs design spec §6.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Code-quality review feedback. Loop OllamaFailureKind.allCases so adding
a case fails the test by construction. Strengthen EscalationDecision
test to assert whole-struct equality and firedAt round-trip.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closed enum of six detectable Ollama failure modes that trigger reactive
escalation to Anthropic per the local-first routing spec. EscalationDecision
is the record emitted once per escalated turn.
Refs design spec §2-§3.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>