Reference
Plugins
Built-in plugins that extend the Routecraft runtime, and the full plugin API: the descriptor, the plugin context, the lifecycle and its faults, ports, hooks, steps and facets.
- 01
llmPluginLanguage models.@routecraft/aiConfigure provider keys, default models, and global LLM defaults for every agent and llm() call in the context.
- 02
embeddingPluginVectors.@routecraft/aiWire an embedding provider for the embedding() destination and downstream clustering with cosine().
- 03
mcpPluginMCP server runtime.@routecraft/aiExpose mcp() capabilities over Model Context Protocol, with JWT, OAuth 2.1, and bearer-token verification built in.
- 04
agentPluginAgent registry and harness.@routecraft/aiRegister named agents, the tools they can call, and shared defaults like system prompt and principal context.
- 05
acpPluginEditor protocol mount.@routecraft/aiServe the Agent Client Protocol so a person can talk to this instance's agents from their editor, on the same server and behind the same wall as everything else.
- 06
httpPluginHTTP routing.@routecraft/routecraftExpose routes over HTTP via the http() source, mounted as the catch-all surface on a named server; JWT, JWKS, or API-key auth at the plugin boundary.
- 07
serversPluginNamed listeners.@routecraft/routecraftDeclare the HTTP listeners the app binds, by name. HTTP, MCP, and custom plugins mount paths on a shared listener; a surface gets a dedicated port by declaring another named server.
- 08
opsPluginHealth and readiness.@routecraft/routecraftServe liveness, readiness, and operational health, mounted on a named server. Route lifecycle and circuit-breaker state are derived from events the framework already emits; indicators cover dependencies it cannot see.
- 09
shellPluginshell() context defaults.@routecraft/osSet context-wide defaults for shell(): isolation tier, timeout, and output cap. The lowest of the three precedence layers, under the ROUTECRAFT_SHELL_ISOLATION operator override and the call site.
- 10
remotesPluginRoutes of other instances.@routecraft/routecraftName other running instances, and every dispatchable route they expose through the ops management API becomes a direct endpoint here, for direct(), forward(), agent tools and the local ops listing alike.
Core adapter defaults (cron, direct) are set with dedicated CraftConfig fields. Each field installs the adapter's plugin for you, so you never list it in plugins. See Configuration and Merged Options.
How to write a plugin, with a worked example for each part, is on the Plugins guide. This page lists every field, member and fault.
First-class config keys
A config key installs the matching plugin with its options. Importing @routecraft/ai augments CraftConfig with keys for the AI plugins: setting llm, mcp, embedding, agent, or sessions is equivalent to listing the corresponding plugin in plugins (sessions maps to sessionsPlugin, documented under Configuration). The plugin's lifecycle and events are identical either way. A key and an explicit plugin for the same feature are two installations of one id and fail with RC1101.
Recommended for declarative configuration:
import { defineProject } from '@routecraft/routecraft'
import '@routecraft/ai' // augments CraftConfig
export default defineProject({
llm: { providers: { openai: { apiKey: '...' } } },
mcp: { clients: {} },
})
The factories remain available, for a plugin instance you build once and reuse, or for programmatic composition:
import { defineProject } from '@routecraft/routecraft'
import { llmPlugin, mcpPlugin } from '@routecraft/ai'
export default defineProject({
plugins: [
llmPlugin({ providers: { openai: { apiKey: '...' } } }),
mcpPlugin({ clients: {} }),
],
})
defineProject
defineProject({ plugins, ...config }) declares a project and returns { craft, config, plugins }. craft.config.ts default-exports it, and craft start reads that export.
A misspelled config key is a compile error, as it is with defineConfig. A config key that installs a plugin with steps or a facet (deferral) types the project's routes when it is set: the package registering the key merges its plugin type into ConfigKeyPlugins, so a third-party key gets the same typing as a first-party one. The root craft() export is typed by the plugins @routecraft/routecraft ships; a route that uses an uninstalled plugin's step refuses to start with RC1111, and reading an uninstalled plugin's facet fails with the same code.
A plain config object still loads: craft start accepts a default-exported defineProject(...), a named craftConfig export, or a default-exported config object, in that order. craft run <file> reads the file's named craftConfig export.
Plugin descriptor
definePlugin(descriptor) returns the descriptor unchanged, keeping its literal types.
Plugin context
bind, start and stop receive a PluginContext. A plugin never receives the CraftContext.
Execution
Lifecycle
The stages an application goes through, and the fault each one raises:
Stages 1 to 3 run before any plugin binds, so their faults leave nothing to release. Each lifecycle function is awaited before the next plugin's runs. Events plugin:binding / plugin:bound bracket bind, plugin:starting / plugin:started bracket start, and plugin:stopping / plugin:stopped bracket stop (see plugin events).
A throwing start fails context.start() with the original error, and every bound plugin is stopped in reverse order before it surfaces, including plugins whose start never ran. A failure during bind or route registration unwinds the same way: only plugins whose bind returned are stopped, and the original error surfaces unchanged even if a stop throws on the way out.
A stop that lands mid-boot waits for the lifecycle function still running before it stops that plugin, so a plugin always observes its function entered, settled, then stop. No lifecycle function runs once the stop walk has begun.
To abort a boot, throw. To stand the application down without failing the boot, call c.execution.requestStop(), which returns at once. A lifecycle function must never wait for the application's own stop: the stop waits for the function, and neither settles.
stop receives a second argument describing how far the application got:
A plugin that only closes what bind opened can ignore both. A plugin that stops what start began reads started.
start is awaited, so the application is not ready until every start has resolved. Use it for startup work that finishes, and begin unbounded work (a poll loop, a subscription) there without awaiting it. A plugin descriptor may serve more than one application in a process, so per-run state is released with c.onDispose() or keyed by the PluginContext, never held in a closure slot.
Waiting for a context to be ready
ctx.start() resolves when the context stops, not when it comes up: a context with an indefinite route (an HTTP server, a direct() endpoint) keeps running. Await ctx.whenStarted(), or c.execution.whenStarted() from a plugin, for readiness. It resolves once no route is still coming up and every start has finished, and rejects if a start or the config refuses. A single route failing to come up is not observable there, since the context keeps the others running.
Ports
port<T>(name) declares a port. The name is owner.capability@version in lowercase dotted segments; anything else is RC1103. Call it once at module scope and export the token: the token is the identity, and two tokens with one name in one application is RC1103.
Adapters read a port at runtime through the context they run in, with context.require(port) or context.lookup(port).
The ports the framework ships:
RESILIENCE, CACHE and ENFORCEMENT fill the chain's route-scope positions. The default providers are exported as resilienceProvider, cacheProvider and enforcementProvider, so a replacement can delegate to them. A step-scope wrapper (.retry() and the rest called after .from()) resolves the same provider when the exchange runs, so a replacement fills both scopes (see Slots). A plugin that replaces a port is named once at boot, at warn for AUTHORITY and ENFORCEMENT.
AUTHORITY decides who an exchange acts for and whether to trust it. It provides mint(claims), brand(principal), isAuthentic(principal), restore(record), isRestored(principal) and read(exchange). authorityOf(exchangeOrContext) returns the application's authority. Every mint and every check goes through it, so a replacement decides both sides; delegate() and the ingress mounts take the authority explicitly rather than assuming the default. defaultAuthority is exported for a replacement that decorates it.
Default plugins
Installed in every application, ahead of its own plugins:
An application plugin with the same id takes a default's place. A plugin that declares replaces for a default's port is selected over it while the default still binds. The deferral plugin (routecraft.deferral, installed by the deferral config key) adds the defer and resume steps and the ex.deferral facet.
Hooks
Slots
The chain around every route runs in a fixed order. Positions belong to the framework and hold one thing each; a plugin can replace what fills a position through its port, but cannot move, remove or add one. Slots sit between positions, and any number of plugins add hooks there.
SLOTS exports the slot names in order. A hook in a slot or point that does not exist is RC1112.
The resilience methods and .cache() placed after .from() are the same positions around one step. The wrapper resolves the same provider when the exchange runs, with scope: "step" and the step's label on the PositionRun ("route" and "route" at route scope), so a plugin replacing RESILIENCE or CACHE fills both scopes; throttle(options, scope) takes the scope, and the step-scope cache runs through CachePositions.wrap. A step wrapped in a method no installed plugin provides fails with RC1111 when it runs.
Phases
A slot runs observe, then mutate, then validate. exit has no validate phase. In the error slot the phases are observe and mutate, and mutate is the deciding phase: a hook answers with a recovery body, recovery.drop(), recovery.defer(...) or recovery.rethrow(), or undefined to pass, and the first answer decides. Inside a phase, hooks run in the order the application lists the plugins. When two mutate hooks write the same header in one slot, the later wins and the framework warns once per route naming both.
perAttempt takes wrappers rather than phased hooks: { wrap(proceed, exchange, info) }. A wrapper surrounds every attempt of the route's work, inside retry and outside timeout; it may time, trace or guard an attempt and may throw, but does not change the exchange.
Hook fields
info carries routeId, tags, slot and kind. An error hook's info also carries execution (1 on the first run, 2 once the exchange is a resumed continuation) and forward, which sends to a direct() endpoint under the failing exchange's principal.
An error hook that throws is reported as route:error-handler:failed with scope: "slot" and the hook's id under hook, and the slot continues to the next hook. A failure an error hook decides reaches neither context:error nor route:exchange:failed.
Points
A plugin may declare points: [{ name: 'approvals.decided' }] and run the hooks other plugins place there from its own step, with ctx.invoke(name, exchange) in a step's execute. Another plugin places a hook there under hooks.points: hooks: { points: { 'approvals.decided': { id: 'recordDecision', phase: 'observe', run } } }. Point names are dotted under the declaring plugin's namespace; two plugins declaring one point, or a point named after a slot, is RC1113.
Hooks config
The application settles conflicts in config, under hooks:
A name in order or disable that matches no installed hook is RC1112.
Steps
Each entry of a plugin's steps becomes a builder method of that name. The entry takes the method's arguments and returns a step built with step<In, Out>().
A route that uses a plugin's step refuses to start unless the plugin is installed (RC1111). Two plugins declaring one step, or a step named after a builder method, is RC1116.
Facets
A plugin's facet is readable as ex.<namespace> in route callables, typed in routes built with the project's craft(). It is computed from the exchange on every read and never stored. A facet named after an exchange field (id, headers, body, logger, context) or method is RC1114. Reading a facet in an application that does not install its plugin is RC1111.
Code that holds a plain Exchange rather than a route's typed one (an adapter, a hook, a helper) uses the library function.
Related
Plugins guide
Write a plugin: ports, hooks, steps and facets with worked examples.
Configuration
Every craft.config.ts field, including the first-class keys that stand in for plugins.
Errors
RC1101 to RC1117 and RC5068, the plugin faults.
Agents and skills
Give a project a model, define an agent, and grant it capabilities as tools.