agent
Helpers for building user-facing agents. This module offers three independent tools:
routedispatches each user message to one of several specialist agents (and lets the LLM hand off between them),questionasks the user something through an interrupt the host UI can render, andtodoWrite/todoListgive the LLM a small todo list to track multi-step work across turns.tsimport { route } from "std::agent" // Two specialists, each with its own system prompt and tools. const codeAgent = { systemPrompt: "You write and edit code...", tools: [readFile, writeFile] } const researchAgent = { systemPrompt: "You answer questions from docs...", tools: [fetchUrl] } node main() { let msg = input("> ") while (msg != "exit") { const reply = route({ start: "code", agents: { code: codeAgent, research: researchAgent }, maxHops: 3, }, msg) print(reply) msg = input("> ") } }
Types
PromptSpec
- Configuration for one specialist registered with
route. systemPrompt— system message seeded into the specialist's
- thread on first entry. The router appends
RouterConfig.context - (if set) to this string before seeding.
tools— the tools this specialist sees. The router automatically
- appends a
handofftool PFA-bound to the other categories, so - do NOT add
handoffhere yourself. memory(optional) — whentrue, the router calls
setMemoryId(<category>)before each LLM call so this- specialist's
remember/recalllands in a per-specialist graph. - You must
enableMemory(...)separately for this to do anything.
/**
* Configuration for one specialist registered with `route`.
*
* - `systemPrompt` — system message seeded into the specialist's
* thread on first entry. The router appends `RouterConfig.context`
* (if set) to this string before seeding.
* - `tools` — the tools this specialist sees. The router automatically
* appends a `handoff` tool PFA-bound to the other categories, so
* do NOT add `handoff` here yourself.
* - `memory` (optional) — when `true`, the router calls
* `setMemoryId(<category>)` before each LLM call so this
* specialist's `remember`/`recall` lands in a per-specialist graph.
* You must `enableMemory(...)` separately for this to do anything.
*/
export type PromptSpec = {
type: "prompt";
systemPrompt: string;
tools: any[];
memory?: boolean
}(source)
AgentSpec
export type AgentSpec = {
type: "agent";
name: string;
agent: any
}(source)
RouterConfig
- Top-level config for a single
route(config, msg)call. start— the category to dispatch the user's message to first.
- Must be a key of
agents. agents— map of category name →PromptSpec | AgentSpec. Every key is a potential handoff target.
maxHops— bound on per-turn handoffs (specialist A → B → A → …).
- When the cap is reached, the next LLM call drops the
handoff - tool so the agent is forced to answer in place. 3 is a sensible
- default for most agents.
context(optional) — extra text appended to every specialist's
systemPrompton first entry. Use this for shared grounding- (current date, working directory, project conventions) that
- every specialist should see.
/**
* Top-level config for a single `route(config, msg)` call.
*
* - `start` — the category to dispatch the user's message to first.
* Must be a key of `agents`.
* - `agents` — map of category name → `PromptSpec | AgentSpec`.
Every key is a potential handoff target.
* - `maxHops` — bound on per-turn handoffs (specialist A → B → A → …).
* When the cap is reached, the next LLM call drops the `handoff`
* tool so the agent is forced to answer in place. 3 is a sensible
* default for most agents.
* - `context` (optional) — extra text appended to every specialist's
* `systemPrompt` on first entry. Use this for shared grounding
* (current date, working directory, project conventions) that
* every specialist should see.
*/
export type RouterConfig = {
start: string;
agents: Record<string, PromptSpec | AgentSpec>;
maxHops: number;
context?: string
}(source)
Effects
std::question
effect std::question {
prompt: string
}(source)
Functions
todoAdd
todoAdd(todo: Todo): Todo[]Add a new todo to the list.
Parameters:
| Name | Type | Default |
|---|---|---|
| todo | Todo |
Returns: Todo[]
(source)
todoUpdate
todoUpdate(id: string, status: "pending" | "in_progress" | "completed"): Todo[]Update the status of a todo by id.
Parameters:
| Name | Type | Default |
|---|---|---|
| id | string | |
| status | "pending" | "in_progress" | "completed" |
Returns: Todo[]
(source)
todoWrite
todoWrite(todos: Todo[]): Todo[]Replace the current todo list.
Parameters:
| Name | Type | Default |
|---|---|---|
| todos | Todo[] |
Returns: Todo[]
(source)
todoList
todoList(): Todo[]Return the current todo list.
Returns: Todo[]
(source)
question
question(prompt: string): stringAsk the user a question and wait for their reply.
@param prompt - The question to show the user
Alternative to the input function, raises an interrupt instead, so it can be used anywhere (eg a web server) instead of just the CLI.
Parameters:
| Name | Type | Default |
|---|---|---|
| prompt | string |
Returns: string
Throws: std::question
(source)
handoff
handoff(category: string, reason: string, validCategories: string[]): stringRe-route the user's current message to a different specialist. After you call this, your turn closes and the original message is re-routed to the chosen specialist. Anything you say after handoff() is discarded, so return immediately.
Bias toward staying: most follow-ups are continuations of the current topic. Only call this when the message clearly needs a different specialist's tools.
@param category - The specialist to re-route to @param reason - Brief explanation (recorded for debugging) @param validCategories - PFA-bound by route() — do not set manually
- Tool function the router injects into every specialist's tool
- set. You should not register this manually —
routeadds it for - you with
validCategoriesPFA-bound to the other registered - categories.
- When called, sets the module-level handoff target. The current
- LLM call returns.
routereads (and clears) the target and - re-runs the user's original message in the new specialist's
- thread. If
categoryisn't invalidCategories, the handoff is - rejected and the LLM sees an error string as the tool's result.
- This keeps the LLM from accidentally handing off to itself or to
- an unregistered specialist.
Parameters:
| Name | Type | Default |
|---|---|---|
| category | string | |
| reason | string | |
| validCategories | string[] |
Returns: string
(source)
route
route(config: RouterConfig, userMsg: string): stringRun one user turn through a multi-specialist agent. Owns the hop loop, handoff signal, and force-answer fallback. The router injects a handoff(category, reason) tool into each specialist's toolset so the LLM can re-route the message when it's out of scope. When maxHops is reached, the handoff tool is stripped from the last call so the LLM is forced to answer.
@param config - Start category, per-category agent specs, max hops, optional context string @param userMsg - The user's input message for this turn
- Run one user turn through a multi-specialist agent. Returns the
- final reply (the text the user sees) after all handoffs settle.
- Lifecycle of a single call:
- Dispatch
userMsgtoconfig.start's specialist. The
- Dispatch
- specialist's
thread(session: <category>)block is opened, - its system message is seeded on first entry, memory is scoped
- if requested, and `llm(userMsg, { tools: [...spec.tools,
- handoff] })` runs.
- After the LLM call returns, check whether
handoffwas called
- After the LLM call returns, check whether
- during the turn.
- No → done, return the LLM's reply.
- Yes → loop with the new category, up to
config.maxHops
- Yes → loop with the new category, up to
total iterations.- If the hop cap is reached without a final answer, run one more
- LLM call WITHOUT the
handofftool so the agent must answer. - Call once per user turn —
routedoes not own the outer REPL - loop. Threads, memory state, and the policy file (if you install
cliPolicyHandler) persist across calls so the user can have a- stateful conversation.
- @param config - Specialists, start category, hop cap, optional
- shared context appended to every system prompt.
- @param userMsg - The user's input for this turn.
Parameters:
| Name | Type | Default |
|---|---|---|
| config | RouterConfig | |
| userMsg | string |
Returns: string
(source)