# Migration Guide — GENC-1262 (Per-Agent AI Provider)

This document covers the breaking changes introduced by the `mw/GENC-1262-per-agent-model` branch and the new per-agent provider capability that motivated them.

The old single-provider DI token (`AIProvider`) is gone. Hosts now register a **named registry of providers** (`AIProviderRegistry`), and each agent can declare which named provider it wants.

---

## 1. Single-provider DI token replaced by a registry

The `AIProvider` DI token (a const exported by `@genesislcap/foundation-ai`) has been **removed**. So has the `getAIProvider()` helper. They're replaced by a new token `AIProviderRegistry` and a registration helper `registerAIProviders`.

The `AIProvider` **interface** still exists and is still exported (concrete provider classes implement it). Only the DI-token const and `getAIProvider` helper are gone.

### Bootstrap: single-provider host

**Before**

```ts
import { AIProvider, createAIProvider, resolveAIConfig } from '@genesislcap/foundation-ai';
import { Registration } from '@microsoft/fast-foundation';

const aiConfig = await resolveAIConfig({ provider: 'gemini', model: 'gemini-2.5-flash' });

this.container.register(
  Registration.instance(AIProvider, createAIProvider(aiConfig)),
);
```

**After**

```ts
import { createAIProvider, registerAIProviders, resolveAIConfig } from '@genesislcap/foundation-ai';

const aiConfig = await resolveAIConfig({ provider: 'gemini', model: 'gemini-2.5-flash' });

registerAIProviders(this.container, { gemini: createAIProvider(aiConfig) });
```

Notes:

- `registerAIProviders` is a function — call it directly with the container; **do not** wrap it in `container.register(...)`.
- With one provider in the map, the default is inferred automatically. The key (`'gemini'` here) becomes both the registered name and the default name.
- Pick a stable, descriptive key. It's how agents will reference this provider in their config, and it's what the settings panel will show.

### Bootstrap: multi-provider host

```ts
import {
  AnthropicProvider,
  AnthropicTransport,
  GeminiProvider,
  GeminiTransport,
  registerAIProviders,
} from '@genesislcap/foundation-ai';

const fast = new GeminiProvider({}, new GeminiTransport({ model: 'gemini-2.5-flash-lite' }));
const deep = new AnthropicProvider({}, new AnthropicTransport({ model: 'claude-sonnet-4-6' }));

registerAIProviders(this.container, { fast, deep }, { default: 'fast' });
```

- With **more than one** entry, `options.default` is **required**. Omitting it throws — there's no implicit ordering.
- Unknown `options.default` (a name not present in `providers`) throws.
- Empty `providers` map throws.

### What happens if you register nothing?

A built-in empty registry resolves from the DI container. Every lookup returns a no-op provider; the assistant renders inert. This preserves today's "AI feature flag off / nothing configured" behaviour — no need to special-case it.

---

## 2. New `BaseAgentConfig.provider` — per-agent provider override

Every agent (specialist, fallback, or stateful) can now declare which registered provider it uses by name. Omit the field to use the registry's default.

```ts
import { defineAgent } from '@genesislcap/ai-assistant';

const classifier = defineAgent({
  name: 'classifier',
  description: 'Quick triage and routing.',
  provider: 'fast', // resolves against the AIProviderRegistry
});

const analyst = defineAgent({
  name: 'analyst',
  description: 'Deep analysis with long context.',
  provider: 'deep',
});
```

Type: `provider?: string | ((ctx: SystemPromptContext) => string | Promise<string>)`.

### Function form (per-turn resolution)

The function form mirrors `systemPrompt`, `toolDefinitions`, `toolHandlers` — resolved each tool-loop iteration with the same `SystemPromptContext`. On a plain `defineAgent` config this looks like:

```ts
const triage = defineAgent({
  name: 'triage',
  description: 'Routes intents.',
  provider: (ctx) => (ctx.history.length > 8 ? 'deep' : 'fast'),
});
```

The argument is just `SystemPromptContext` — `agentName`, `history`, `turnIndex`, `signal`. No `state` field, because plain agents don't own per-turn state.

### Stateful agents (`defineStatefulAgent`)

For agents built with `defineStatefulAgent<S>`, the `provider` resolver receives `StatefulAgentContext<S>` — the same context plus the live `state` value. This lets the agent flip provider based on whatever it owns (a state machine, an accumulator, anything in `S`):

```ts
const guidedBooking = defineStatefulAgent<{ machine: GuidedBookingMachine }>({
  name: 'guided_booking',
  description: 'Walks the user through a multi-step booking flow.',
  init: ({ /* ... */ }) => ({ machine: createMachine(...) }),

  provider: ({ state }) =>
    state.machine.state === GuidedBookingStates.EnterDetails ? 'deep' : 'fast',

  systemPrompt: ({ state }) => state.machine.meta?.systemPrompt ?? '...',
  // ...
});
```

`defineStatefulAgent` wraps the function so `state` is threaded in before each call — you don't write the wrapping yourself.

**Use typed constants for state names.** Comparing against a string literal like `'enterDetails'` is fragile — renames in the machine don't break the resolver. Export a typed const map from the machine file and reference it from the agent:

```ts
// guided-booking.machine.ts
export const GuidedBookingStates = {
  PickCounterparty: 'pickCounterparty',
  PickInstrument: 'pickInstrument',
  EnterDetails: 'enterDetails',
  Confirm: 'confirm',
  Done: 'done',
  Cancelled: 'cancelled',
} as const satisfies Record<string, GuidedBookingState>;
```

Renaming a state now requires updating the map, which surfaces every consumer in one place. The `satisfies Record<string, GuidedBookingState>` keeps the values constrained to the state-name union.

For dynamic stateful agents (no machine — see [`trade-operations.agent.ts`](../../../../../showcase/client-app/client/src/routes/ai/trade-operations/trade-operations.agent.ts)), the same pattern applies but the predicate is whatever derives "current phase" from the state shape — e.g. `provider: ({ state }) => isPlanning(state) ? 'deep' : 'fast'`.

### Sub-agents

`BaseAgentConfig.provider` is inherited by sub-agents. A sub-agent declared in `subAgents: [...]` can have its own `provider` field; it's validated alongside its parent.

---

## 3. Validation — unknown names and missing capabilities hard-error

There are no silent fallbacks for misconfigured providers. The validation runs at two points:

- **Static names** (`provider: 'fast'`) — validated at `OrchestratingDriver` construction time. Walks every agent and every nested sub-agent. Unknown names throw with the list of registered names. Providers missing `chat()` throw too.
- **Function-form names** (`provider: ({ state }) => '...'`) — validated on first resolution per name (cached after that). Subsequent state transitions that return a different name validate again.

Sample errors:

```
OrchestratingDriver: agent "analyst" references unknown provider "deep". Registered: fast
ChatDriver: agent "analyst" resolved to provider "deep" which does not implement chat() — required by ChatDriver.
```

If you see one of these, the fix is in either the registration (`registerAIProviders(...)`) or the agent config (`provider: '...'`), not in this code.

---

## 4. Settings panel — multi-provider display

The settings cog inside the assistant displays providers in two modes:

- **1 provider** — same single "Model" row as before. No visible change for single-provider hosts.
- **2+ providers** — a list of every registered provider with the model id from `getStatus()`, plus `default` and `current` badges. `current` follows the active agent's resolved provider.

Status is fetched once on connect via `AIProviderRegistry.listStatuses()` and refreshed when the active provider changes.

Nothing about the multi-provider list leaks outside the cog — the header, halo, and message rows are unchanged.

---

## 5. Consumer behaviour outside the assistant

Other foundation-ui components that consume the AI layer use **the registry's default provider only** — they're not bound to any agent, so per-agent overrides don't apply:

- `ai-criteria-search` (the natural-language criteria filter, including when embedded in `foundation-entity-management`) — calls `interpretCriteria` on the default provider.
- `ai-indicator` (chrome status indicator) — reads status / triggers Chrome model download on the default provider.

If your host needs criteria search backed by a specific provider, make sure that provider is your default.

---

## 6. Things that always use the default provider

Inside the assistant itself, a few non-turn paths always run against the registry default rather than the active agent's provider:

- **Suggestion generation** (`ChatDriver.getSuggestions`) — UI helper not bound to any agent.
- **Classifier** (`OrchestratingDriver`'s intent routing) — orchestrator-level decision, runs before any specialist is picked.

This is intentional: keep those paths cheap and predictable, and pick a default provider that's cheap enough for them.

---

## 7. Known limitations

- **Context-window flip.** If an agent switches provider mid-conversation (e.g. a stateful agent moves from a long-window provider to a short-window one), history accumulated under the larger window can blow the smaller window. The driver does not auto-trim. Either keep providers compatible in context size, or design state transitions that reset / summarise history.
- **Capability check is `chat()` only.** `ChatDriver` validates that the resolved provider implements `chat()`. It does **not** validate `streamChat()` or `prompt()`. If an agent's flow specifically needs streaming, ensure the provider implements it; otherwise calls will fall through to non-streaming.

---

## 8. Checklist for migrating a host app

1. Replace `Registration.instance(AIProvider, createAIProvider(...))` with `registerAIProviders(container, { '<name>': createAIProvider(...) })`. Call it **outside** `container.register(...)`.
2. Drop `import { AIProvider }` value imports; keep `import type { AIProvider }` if you still need the interface.
3. Drop any `getAIProvider()` calls — pull the registry from DI (`@AIProviderRegistry providerRegistry!: AIProviderRegistry`) and use `providerRegistry.default()` or `providerRegistry.get(name)`.
4. If you want per-agent providers, register multiple under distinct names with an explicit `default`, then add `provider: '<name>'` to the relevant `AgentConfig` entries.
5. Re-run typecheck. The breakage surface is narrow: only sites that used the removed const or helper. Type-only uses of the `AIProvider` interface continue to work unchanged.
