agent
import { agent } from '@routecraft/ai'
Run an LLM with a fixed system prompt on each incoming exchange. Replaces the body with AgentResult { text, usage? }. Two forms:
- Inline (
agent({ model, system, user? })) -- identity and description come from the enclosing route (.id(),.description()). Suitable when the route is the agent. - By name (
agent("summariser")) -- resolves a registered agent from the context. Register agents viaagentPlugin({ agents: { name: {...} } })(agentPluginreference).
import { agent, agentPlugin } from '@routecraft/ai'
import { readFileSync } from 'node:fs'
// Inline: the route IS the agent. Other routes call it via direct("zoe").
craft()
.id('zoe')
.description('Internal ops assistant')
.from(direct())
.to(agent({
model: 'anthropic:claude-opus-4-7',
system: readFileSync('./prompts/zoe.md', 'utf-8'),
}))
.to(direct('reply'))
// By name: register once, use from any route in the context. Per-agent
// fields can be omitted when defaultOptions supplies them.
agentPlugin({
defaultOptions: {
model: 'anthropic:claude-opus-4-7',
},
agents: {
summariser: {
description: 'Summarises documents into bullet points',
system: 'Be concise.',
// model inherited from defaultOptions
},
},
})
craft()
.id('periodic-summary')
.from(timer({ intervalMs: 60_000 }))
.to(agent('summariser'))
.to(log())
Model ID format: "provider:model-name" (same as llm()). The provider must be registered via llmPlugin({ providers: {...} }). There is no inline-credentials escape hatch on agent({...}); centralised wiring via llmPlugin is the only path.
Supported providers: openai, anthropic, ollama, openrouter, gemini, lmstudio, custom
AgentOptions (inline form):
AgentRegisteredOptions (entries in agentPlugin({ agents: {...} }), for by-name reuse): same as AgentOptions plus:
The id is the record key in agentPlugin({ agents: { [id]: {...} } }).
Result shape (body is replaced by .to()):
Resolution semantics:
agent("name")only resolves registered agents. To call a route-backed agent from another route, use.to(direct("route-id")).directruns the full pipeline of the target route;agent("name")runs the registered agent's LLM call inline.- Model resolution at dispatch is
instance value > defaultOptions.model > throw RC5003. - Duplicate registered agent ids, missing description, malformed model string when present, or a non-
ToolSelectiontoolsvalue fail at context init withRC5003(Adapter misconfigured). - Referencing an unknown registered agent name fails at dispatch with
RC5004(No handler available).
Provider credentials are configured once in llmPlugin() and shared across all agent() calls. See llmPlugin reference.
Telling the agent who the caller is
By default the only part of the exchange that reaches the model is the body (as the user prompt). The authenticated caller (exchange.principal) is not in the prompt, so the model does not know who it is serving unless you put that there yourself.
Set principal: true to append a ## Caller section to the system prompt. It is appended after your own prompt and any blocks, and it covers the unauthenticated case explicitly so the model never invents an identity:
agent({
model: 'anthropic:claude-opus-4-7',
system: 'You are a support assistant.',
principal: true,
});
When the request is authenticated, the model sees:
## Caller
The current request is authenticated.
- Name: Jane Doe
- Email: [email protected]
- Subject: user_2a9f
- Roles: admin, editor
When there is no principal:
## Caller
The current request is not authenticated. No verified user identity is
available. Do not assume, infer, or invent the caller's name, email, or
permissions.
Only the loggable identity fields (name, email, subject) and roles are surfaced; fields that are absent on the principal are omitted, and interpolated values have newlines collapsed so a subject-controlled field (a self-service display name, say) cannot forge prompt structure. Scopes, claims, userinfoClaims, and the bearer token are never injected. The block is informational context only: authorization is still enforced by .authorize() and tool guards, never by the model.
To control the wording or which fields are shown, pass a function instead of true. It receives the principal (undefined when unauthenticated) and the exchange, and returns the markdown to append (return '' to append nothing). Your renderer owns its own escaping and the same field exclusions apply:
agent({
model: 'anthropic:claude-opus-4-7',
system: 'You are a support assistant.',
principal: (p) =>
p ? `## Caller\n\nYou are assisting ${p.name ?? p.subject}.` : '',
});
To opt every agent in a context into caller-awareness at once, set principal on agentPlugin({ defaultOptions }); a per-agent principal (including false) overrides it.
Inside a tool handler, the same principal is available as ctx.principal (a deep-frozen, read-only snapshot).