Providers
A provider is one agent CLI as the host knows it: its models, what it can do, how it is started and how its session is continued. Two are built in.
| Provider | Started as | Config folder |
|---|---|---|
| Claude Code | claude -p with stream-json in and out | CLAUDE_CONFIG_DIR, else ~/.claude |
| Codex | codex app-server, JSON-RPC over stdio | CODEX_HOME, else ~/.codex |
The CLI stays up between turns. Claude Code reports a file change as the text before and after and folds its context with a prompt; Codex reports unified diffs, streams tool output, folds its context natively and has questions that can wait, but reports no cost and takes no reason with a denial. provider.list answers each one's capabilities: offer a control only when the capability is there. The test fixtures were captured from particular versions of each CLI; they show what the backends were built against, not a minimum version.
The registry
ProviderRegistry holds the providers of a host, the built-ins by default. list() answers them with whether each is installed and its version, which it caches for a minute; invalidate(kind) forgets one. get(kind) answers the provider of a kind, or a stand-in that is not installed for a kind the host does not offer. oneShotProvider(preferred?) answers an installed CLI that can answer a single prompt, which is how a chat gets a title, or null.
AgentHost and wireAgents always use the two built-ins. A host with other providers builds the registry, the ChatCore and the services around it itself.
Models
Each provider has a ModelCatalog, shipped as providers/claude-models.json and providers/codex-models.json. A selection a client sends is made complete by the catalog:
import { ModelCatalogDataSchema } from '@adecore/agent-contracts/model';
import { ModelCatalog } from '@adecore/agents/providers/catalog';
const catalog = new ModelCatalog(
ModelCatalogDataSchema.parse({
updatedAt: '2026-10-05T00:00:00Z',
defaultModel: 'standard-model',
profiles: {
standard: {
options: [{ id: 'effort', label: 'Effort', type: 'select', choices: [{ id: 'medium', label: 'Medium' }], defaultChoice: 'medium' }],
contextWindowTokens: { '*': 200000 }
}
},
models: [{ slug: 'standard-model', name: 'Standard', profile: 'standard', aliases: ['standard'] }]
})
);
catalog.normalize({ model: 'standard', options: { effort: 'unknown', other: true } });
// { model: 'standard-model', options: { effort: 'medium' } }normalize takes a slug or an alias, falls back to the default model for one it does not know, puts each option at its default when it is missing or does not fit, and drops options the model does not have. resolveModel(name) answers the slug, or null for an unknown name, for a caller that would rather refuse. contextWindowFor(selection) answers the context size of a selection, or null. replace(data) takes a newer catalog, but never one older than the catalog it was made with.
Runtime modes
A chat's runtime mode becomes the CLI's own permission settings:
| Mode | Claude Code --permission-mode | Codex approval policy and sandbox |
|---|---|---|
supervised | The CLI's default | untrusted, read-only |
auto-accept-edits | acceptEdits | untrusted, workspace-write |
auto | auto | on-request, workspace-write |
full-access | bypassPermissions | never, danger-full-access |
Codex has no policy that matches every mode of Claude Code exactly; this is the closest. A new chat runs in full-access unless it names a mode, so name one, and narrow it in runtimeModeFor when the host has a ceiling. MODE_ORDER is the modes from narrowest to widest and narrowerMode(mode, ceiling) the narrower of two. ceilingForOpening(opener, requested) refuses a mode wider than the opener's, for an agent that opens another.
claude.allowedTools lets Claude Code use the named tools without asking, in every mode. codexRules writes command prefixes Codex may run without asking into the rules of every Codex home; a folder it cannot write to only costs an approval per call.
A provider of your own
A ChatProvider is data plus a backend: its kind, name, catalog, capabilities, command, resumeCommand, home for accounts, detect, the arguments for a first prompt and for a one-shot prompt, and createBackend(launch, host). The ChatBackend owns the process, speaks the CLI's protocol, and reports what happens as BackendEvents through host.onEvent; the session turns those into thread items. See chat/backend for the whole interface, and the built-ins for two that work.