The .agent Format
A .agent file is a small, indentation-based document (2 spaces, never tabs) — a strict superset of TOON. You describe what the agent is; toac emits the how.
How it works
Real logic (what a tool actually does) lives in plain TypeScript, in a co-located <agent>.tools.ts.
Keys
| Key | Req | Form | Meaning |
|---|---|---|---|
agent | yes | identifier | the agent's name (also the export + filename) |
model | yes | string | a Claude model id, e.g. claude-opus-4-7 |
description | no | string | one line on what it does |
inputs | no | inputs[N]{name,type}: + N rows | typed call parameters |
tools | no | tools[N]: a,b or tools[N]{name,input}: | tools, implemented in <agent>.tools.ts (see Tools) |
prompt | yes | prompt: | + block | the instruction prompt |
outputs | no | outputs[N]{name,type}: + N rows | typed structured result |
system | no | system: | + block | system prompt (defaults to the description) |
uses | no | uses[N]: a,b | sub-agents wired in as tools via asTool() |
maxTurns / retries | no | number | tool-use turn cap / model-call retries |
maxContextTokens | no | number | soft per-turn context budget; elides old tool results over it |
temperature | no | number 0–1 | sampling temperature (omit for the default) |
Counts are checked
A header's count must match its rows: inputs[2]{...} has exactly two rows; tools[2]: a,b lists exactly two names. The explicit [N] lengths are what make the format reliable for LLMs to author.
Types
string, number, boolean, an enum of literal values (draft|final), or a quoted object type like "{title:string;score:number}". Append [] for an array (string[], or "{...}[]"). Read object fields with {inputs.x.field} or, in a loop, {item.field}.
Enums compile to TypeScript string-literal unions and z.enum([...]) — the literal set reaches the model through the schema, which makes structured outputs markedly more reliable than a free string:
outputs[2]{name,type}:
verdict,approve|reject|escalate
reason,stringA trailing ? on a field name marks it optional — the generated TypeScript types it field?: and the zod schema gets .optional(). Omitted optionals interpolate as empty, iterate as an empty list (so {:else} renders), and test false in {#if}:
inputs[2]{name,type}:
topic,string
audience?,stringinputs[3]{name,type}:
sources,"{title:string;url:string}[]"
detailed,boolean
audience,stringTools
Tools take one of two forms. Bare names list the tools; each is a full defineTool (description, schema, body) in the co-located <agent>.tools.ts:
tools[2]: web_search,fetch_pageTyped tools declare each tool's input type right in the .agent file, so the file owns the schema. toac then generates the Zod schema and a typed defineTool, and <agent>.tools.ts supplies only the run body — type-checked against the declared input:
tools[2]{name,input}:
web_search,"{query:string}"
fetch_page,"{url:string}"// researcher.tools.ts — just the bodies
import type { WebSearchInput } from "./researcher";
export const web_search = (i: WebSearchInput) => search(i.query);Add a third column for a model-facing description: tools[N]{name,input,description}:. A tool's input must be an object type.
A complete example
A kitchen-sink agent using loops, conditionals, object fields, and destructuring — all type-checked:
agent: brief
model: claude-opus-4-7
description: Summarize sources for an audience, optionally in detail.
inputs[3]{name,type}:
sources,"{title:string;url:string}[]"
detailed,boolean
audience,string
prompt: |
Write a brief for {inputs.audience}.
{#each inputs.sources as {title, url}, i}
{i}. {title} — {url}
{:else}
No sources provided.
{/each}
{#if inputs.detailed}
Include a thorough analysis section.
{:else}
Keep it to a single paragraph.
{/if}
outputs[1]{name,type}:
brief,stringTry it live in the playground, or see the template constructs in detail in Prompt Templates.
