tools/
Export anything from .ts files in src/tools/ and it becomes a global in the agent's execution scope. No registration, no schema definition, no defineTool.
my-agent/
└── src/
└── tools/
├── github.ts
├── kanban.ts
└── time.ts
Each file's exports land directly on the global scope. The filename groups the file; it is not a prefix. What you export is what the agent calls.
Export shapes
Any export form works.
Named functions — each function becomes its own global:
// src/tools/time.ts
export async function now() {
return new Date().toISOString()
}
export async function format(date: string, locale = "en-GB") {
return new Intl.DateTimeFormat(locale).format(new Date(date))
}
The agent gets now and format as top-level globals. Not time.now — the file is a
grouping for you, not a namespace for the agent.
Named object — the object itself becomes a global:
// src/tools/kanban.ts
export const kanban = {
list: async (status?: string) => db.tasks.findAll({ status }),
add: async (title: string) => db.tasks.create({ title }),
close: async (id: string) => db.tasks.update(id, { status: "done" }),
}
The agent gets kanban as a global. Calls it as kanban.add("task").
Calling tools
The agent calls tools like any other code — the export name is the call name, with no prefix:
const tasks = await kanban.list("open")
const pr = await openPr("fix: auth", body, "feat/auth")
const ts = await now()
(From a script or route you reach the same functions through axon.tools.* instead — see
Calling tools from your own code.)
Every tool is async at the call site
Write your tool sync or async — both work. It is always called with await.
// src/tools/math.ts — a perfectly valid tool
export function add(a: number, b: number) {
return a + b
}
const sum = await add(2, 3) // 5
This is not a style preference; it is structural. Every tool call is policy-mediated
before the function body runs, and a policy rule can be escalate — which asks the user
to approve the call and waits for the answer. That round trip cannot happen synchronously,
so the function Axon installs in the agent's scope is always an async wrapper around
yours. A sync tool body is fine. A sync call is not available, for any tool.
The practical consequence: add(2, 3) without await gives you a Promise, not 5.
const wrong = add(2, 3) // Promise<number>
const wrong2 = add(2, 3) * 2 // NaN — no error, just wrong
const right = await add(2, 3) // 5
If you don't need a sync body for anything, writing async function makes the call site
and the signature agree, and is the easier habit.
See Policy for what mediation checks and how escalation is configured.
What the agent sees
Tool types come from your TypeScript signatures directly — run through real TypeScript
declaration emission, so inferred return types are the compiler's actual inference, not
unknown. Write JSDoc as if explaining to someone who has never seen your codebase —
that's the model deciding when and how to call the function.
// src/tools/github.ts
/** List all open pull requests for the configured repository. */
export async function listOpenPrs(): Promise<{ number: number; title: string }[]> {
const { data } = await octokit.pulls.list({ state: "open", ...repo() })
return data.map(pr => ({ number: pr.number, title: pr.title }))
}
/** Open a pull request. Returns the PR number and URL. */
export async function openPr(
title: string,
body: string,
head: string,
base = "main"
): Promise<{ number: number; url: string }> {
const { data } = await octokit.pulls.create({ title, body, head, base, ...repo() })
return { number: data.number, url: data.html_url }
}
Types your signature references
When a signature mentions a type you declared elsewhere — an interface, a type alias — that declaration is followed and carried alongside the tool, so the agent sees the full shape rather than a bare name:
// src/tools/tasks.ts
type Task = { id: string; title: string; done: boolean }
/** Fetch the next unfinished task. */
export async function next(): Promise<Task | null> { ... }
The agent receives both the next() signature and the Task definition.
Prefer plain data — interfaces, type aliases, object shapes — for anything a tool returns.
The agent only ever receives the serialized value of a return, so a returned class
instance arrives as its plain fields: methods are not callable across the capsule boundary,
and instanceof means nothing on the other side. A tool returning a rich class is usually
better expressed as one returning a plain object, with the class kept internal:
// src/tools/dice.ts
import { Roll } from "../lib/roll"
type RollResult = { spec: string; dice: number[]; total: number }
/** Roll dice from a spec like "2d6+3". Returns the individual dice and the total. */
export async function roll(spec: string): Promise<RollResult> {
const r = new Roll(spec) // class stays internal
return { spec, dice: r.dice, total: r.total }
}
A class named in a signature is carried across — the model sees its declared shape, not a bare name. What it does not get is behavior: the agent receives the serialized value, so returning a plain object is still the clearer contract.
Two tool files declaring the same type name with different shapes is an error, not a silent pick. Rename one, or move the shared definition into a file both import.
Module scope is persistent
The capsule process starts at boot and stays alive for the session. Module-level code in tools/ runs once and persists for the entire session. Use this for clients, connections, and caches.
// src/tools/github.ts
import { Octokit } from "@octokit/rest"
// Instantiated once at boot — reused across every call
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN })
export async function listOpenPrs() { ... }
No re-authentication per call. No cold-start latency per invocation.
Sharing code between files
Files in tools/ can import from each other. A file with no exports contributes nothing to the global scope — use it as a shared internal module.
// src/tools/_http.ts — underscore prefix, nothing exported to global scope
export function buildHeaders(token: string) {
return { Authorization: `Bearer ${token}` }
}
// src/tools/github.ts
import { buildHeaders } from "./_http"
export async function openPr(title: string, body: string, head: string) {
const headers = buildHeaders(process.env.GITHUB_TOKEN!)
// ...
}
Calling tools from your own code
Tools aren't only for the model. Scripts, routes and hooks call them the same way the agent does — by name, no prefix:
// In a script or route handler
const prs = await listOpenPrs()
const sum = await add(2, 3)
The same functions are always also reachable explicitly:
const prs = await axon.tools.github.listOpenPrs()
Identical behaviour — same capsule, same policy, same tracing; the globals are bindings
onto that surface. Use the explicit form when a bare name would be ambiguous: a tool whose
name collides with a host builtin is not installed as a global (a tool called fetch will
not shadow the real one) and stays callable as axon.tools.<file>.fetch.
Both are awaited — see above.
IDE support
axon prepare generates .agent/tool-globals.d.ts, which declares each export as a typed global. Full autocomplete in scripts, routes, and hooks without any imports.
Run axon prepare after adding or changing files in tools/ to refresh types.