Agents
A definition, a loop, and a record of what it did.
Two ways to call one
An agent is a saved definition: the model it thinks with, its instructions, the tools it may reach, the collections it retrieves from and what it may spend. There are two ways to call one, and which you want depends on whether it uses tools.
If it does not, publish it as a model id and call it with the SDK you already have. It costs nothing new to learn and nothing new to deploy.
// An agent published as a model id. Any OpenAI SDK, no new endpoint. const answer = await openai.chat.completions.create({ model: "agent/standup", messages: [{ role: "user", content: "Summarise yesterday." }], }); // The response says which definition answered, so a caller can tell that a // publish happened without reading the usage log afterwards. // x-vatan-agent: standup // x-vatan-agent-version: 4
If it does use tools, a chat completion is one round trip with no loop to call them in, so that route refuses with agent_needs_runtime rather than quietly answering without them. An agent silently missing the one tool it was built around reads as a model that has got worse, which is the hardest kind of problem to find. Start a run instead.
// A run, for an agent that uses tools. Answers with an id, never the answer. const started = await fetch("https://agents.vatan.one/v1/agents/standup/runs", { method: "POST", headers: { authorization: `Bearer ${VATAN_KEY}`, "content-type": "application/json" }, body: JSON.stringify({ input: { message: "Summarise yesterday." } }), }); const { id } = await started.json(); // 202 Accepted // Poll it. A run can make dozens of model calls and can wait on a person for // days, so there is nothing to hold a connection open for. const run = await fetch(`https://agents.vatan.one/v1/runs/${id}`, { headers: { authorization: `Bearer ${VATAN_KEY}` }, }).then((r) => r.json()); // run.status: QUEUED | RUNNING | WAITING_APPROVAL | SUCCEEDED | FAILED | CANCELLED // run.failure: why it stopped, as a word you can filter on // Stop one. No console needed: a run you started from code you can stop from // code. It takes effect at the next step, so a tool call already sent still // happens; what it guarantees is that no further step is paid for. await fetch(`https://agents.vatan.one/v1/runs/${id}/cancel`, { method: "POST", headers: { authorization: `Bearer ${VATAN_KEY}` }, });
The definition
The whole agent is one object. It is data rather than code or a canvas, so it can be exported, committed to a repository and reviewed in a pull request: a prompt change should be a production change somebody can see. Import it into another organisation and what comes back produces the same definition, checksum included.
{ "name": "Standup summary", "model": "anthropic/claude-sonnet-5", "fallback": ["openai/gpt-4o"], "instructions": "Summarise the standup in three parts: what moved, what is blocked, what is at risk.", "tools": ["mcp://jira/search"], "knowledge": ["kb://handbook"], "memory": { "strategy": "summarise_after", "window": 20 }, "budget": { "max_steps": 12, "max_tokens": 200000, "max_wall_ms": 300000, "max_spend": "1.0" } }
Saving a draft changes nothing live. Behaviour changes when you publish, which is a separate action on purpose, and you can run any version without publishing it to see what it does first. Versions are never edited, only added, because a usage row records which version served it and a definition that could change underneath would make that record meaningless.
instructions can also point at the prompt registry as prompt://house-style/v3, which is version-pinned and resolved when the run starts.
What stops a runaway
Four limits bound a single run and all four have a value: steps, tokens, wall clock and spend. There is no unlimited, because a loop with a tool that always errors is the fastest anybody has emptied a prepaid wallet, and a schema where the limits are optional makes that the out-of-the-box behaviour rather than something somebody chose.
A fifth limit is on the agent rather than on a run, and it is the one worth setting: a spend cap over a rolling 24 hours. A schedule firing every minute inside a one-pound run budget satisfies every other control here and costs a thousand pounds a day. Concurrency is capped too, at one by default, because the common agent talks to a system that does not want to be talked to twice at once.
A run that hits a limit stops with a failure you can filter on, so "show me every run that hit its budget" is a query rather than a reading exercise.
Tools that wait for a person
A tool is granted to an agent one at a time, and each grant says whether the agent may call it without asking. A tool that writes somewhere usually should not: the run pauses before the call, records exactly what it intends to do, and waits. Answering it resumes the run from that step; rejecting it ends the run, recorded as cancelled rather than failed, because a person saying no is the control working.
Subscribe a webhook to agent.approval.pending so this reaches somebody. A run that nobody answers expires, and a human-in-the-loop control that only works while a console is open is a queue rather than a control.
An agent holds no authority of its own. A run borrows the key that started it and can never reach a tool, a collection or a wallet that key could not, so revoking the key stops every run it started, on the next step, with nothing to redeploy.
What it remembers
Two different things, and it is worth telling them apart. When a conversation grows past the window, the older turns are replaced by a summary we write for the model and store where you can read it. Silent truncation is the most common cause of an agent that was fine yesterday and has forgotten everything today, so what it was told is an artefact rather than a side effect.
Separately, the agent gets a remember tool: a short note it writes for itself, replacing whatever it wrote before. That survives the conversation being trimmed and survives the run ending, so a fresh run on an old thread starts with an empty window and the note. It is capped at four thousand characters and refused rather than truncated when it is longer, because a note that stops mid-word is one the next run reads as fact.
Both are on the conversation in the console. If an agent keeps believing something you did not tell it, its own note is usually where that came from.
The record
Every step is written as it finishes, with what it cost at the moment it happened, taken from the settled usage row rather than priced when you read it. Steps roll into runs and runs into conversations, which is the only shape that answers "what did this conversation cost".
Conversations are kept for as long as your plan allows, and you can ask for less on an agent or on a single thread. What falls out of the model's window is summarised into a record you can read, rather than silently dropped: an agent that was fine yesterday and has forgotten everything today is almost always truncation nobody saw.