Building Agents
An agent is the combination of four things you author. Everything else — how the loop runs, how context is assembled, how tools are dispatched, how sessions persist — is the runtime's job.
Identity
src/boot.vue is the agent's standing system prompt. It's a Vue component that renders
to Markdown: compose sections, import partials, load knowledge files. Whatever you put
here is present for every session, every thread, every invocation.
<!-- src/boot.vue -->
<template>
<h1>Barry</h1>
<p>
You are a senior engineering partner embedded in this codebase.
You have full access to the repo, the issue tracker, and CI.
</p>
<WorkingPractices />
<KanbanContext />
</template>
<WorkingPractices /> and <KanbanContext /> are components defined in
src/prompts/ — reusable sections you compose here or load per-invocation.
Identity is authored in source and committed to git. In local development, edits
hot-reload for future work without rewriting the thread history already produced.
Tools
Tools are async TypeScript functions in src/tools/. Export a function and the agent can
call it. No registration, no schema definitions, no wiring.
// src/tools/github.ts
/** Open a pull request against the base branch. Returns the PR number and URL. */
export async function openPr(title: string, body: string, head: string) {
const { data } = await octokit.pulls.create({ title, body, head, ...repo() })
return { number: data.number, url: data.html_url }
}
/** Get the diff for a pull request. */
export async function getPrDiff(number: number): Promise<string> {
const { data } = await octokit.pulls.get({ pull_number: number, ...repo() })
return data.diff_url
}
Axon reads the TypeScript signatures and JSDoc at boot and generates tool declarations for
the model. The agent sees github.openPr and github.getPrDiff as typed, documented
calls. You write the function. The agent knows how to use it.
Tools run in the capsule — an isolated subprocess separate from the agent process. A tool that crashes doesn't crash the agent. See Capsule & Policy.
Scripts
Scripts are TypeScript files in src/scripts/. They orchestrate work: load prompts, call
the agent, process results, write files. The primary authoring unit.
// src/scripts/code-review.ts
const diff = await github.getPrDiff(prNumber)
const context = await axon.prompt("code-review", {
diff: diff
})
const { stream } = axon.stream({ prompt: context, thread: `pr-${prNumber}` })
for await (const entry of stream) {
if (entry.type === "text") process.stdout.write(entry.content)
}
A script can do anything TypeScript can do. Calling the agent is one option, not the default. The script decides when reasoning is needed.
The same script runs four ways without changing:
axon run code-review # headless from the terminal
From the TUI: press !, type the script name, select it.
From a route: axon.scripts.stream("code-review").
From another script: axon.scripts.request("code-review").
Policy
Policy declares what the capsule is allowed to do. Declared in axon.config.ts, enforced
on every tool call and subprocess spawn — before the function runs.
export default defineAgent({
policy: {
fs: {
read: ["./src/**", "./data/**"],
write: ["./data/**"],
deny: [".env"],
},
network: {
allow: ["api.github.com", "api.linear.app"],
},
proc: {
allow: ["git *", "bun test"],
deny: ["git push --force*"],
},
},
})
Policy is not a prompt hint. The agent process has no direct access to the filesystem, network, or shell — every call goes through the capsule's policy gate. Violations are rejected before execution.
Routes handling untrusted input can narrow the base policy further for a single invocation:
const result = await axon.request({
prompt,
policy: { fs: { read: false, write: false }, proc: { allow: [] } },
})
Narrowing can only restrict. A route cannot grant access beyond what the base policy allows.
The server
server/ is optional but makes the agent addressable. File-based routing in server/api/
maps to HTTP endpoints. Routes are thin — they call scripts and return the stream.
// server/api/review.post.ts
export default defineEventHandler(async () => {
const { stream } = axon.scripts.stream("code-review")
return stream
})
Plugins in server/plugins/ run once at boot. This is where you subscribe to module
hooks — an event your code reacts to when something external happens.
// server/plugins/triage.ts
export default defineAxonPlugin(async axon => {
axon.hooks.on("github:issue.opened", async ({ number, title, body }) => {
const prompt = await axon.prompt("triage", { number, title, body })
void axon.request({ prompt, thread: `issue-${number}` })
})
})
What you don't write
No loop. No context window management. No retry logic for tool failures. No session persistence. No stop condition detection. No model API client.
You write what only you can write. The runtime handles the rest.
The Managed Runtime — what the runtime owns and why the boundary exists where it does.
How It Works — state model, threads, sessions, and routes. The mental models that make the structural choices click.
Agent Structure — every file and directory, what it does, what Axon does with it.