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 — exactly as written. What you export is what the agent sees.

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.

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").

Default export — bound under the filename:

// src/tools/github.ts
export default {
    listOpenPrs: async () => { ... },
    openPr: async (title: string, body: string, head: string) => { ... },
}

The agent gets github as a global, named after the file.

Calling tools

The agent calls tools like any other code. Scripts and routes can do the same — tools are globals in all execution contexts:

// In a script or route handler
const tasks = await kanban.list("open")
const pr = await github.openPr("fix: auth", body, "feat/auth")
const ts = await now()

No axon.tools.* prefix. The export name is the call name.

What the agent sees

Tool types come from your TypeScript signatures directly. 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 }
}

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!)
    // ...
}

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.