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.