AntonAgent
Embed AntonAgent in a Dynamic app. Two halves: a backend function that calls ctx.anton.connect, and a client that speaks the WebSocket event protocol.
Audience: LLM agents or developers wiring AntonAgent into a Dynamic app.
Architecture
┌─────────┐ ┌──────────────┐ ┌─────────────┐
│ Client │────▶│ Web PubSub │────▶│ Anton Agent │
│ Widget │◀────│ (WebSocket) │◀────│ (Server) │
└─────────┘ └──────────────┘ └─────────────┘
Bidirectional WebSocket using the json.webpubsub.azure.v1 subprotocol. No REST polling. REST is only used during setup (_me, _rt/ws, anton-connect).
Prerequisites
- Dynamic app with auth configured
gg/ratavailable (the Anton environment ships withgg; see RestAPI Terminal for standalone install)- Client app that can host the chat widget
Backend
Pull the typed surface and let your IDE drive:
gg rat api types
ctx.anton.* exposes connect, wake, prompt, list, close, setBalance, ask. Every option is documented inline — hover the method or Cmd/Ctrl+click into the types for defaults, enum values, the identityId vs actAs distinction, and the rest. This page does not duplicate the reference.
You need one function. Here's the minimum:
gg rat api function create http \
--file functions/anton-connect.ts \
--endpoint /anton-connect \
--access POST
import type { FunctionContext } from "./types";
export default async function (ctx: FunctionContext) {
const { connectionId, message } = ctx.item as { connectionId: string; message?: string };
if (!connectionId) { ctx.res.status = 400; ctx.item = { error: "Missing connectionId" }; return; }
if (!ctx.me?.id) { ctx.res.status = 401; ctx.item = { error: "Not authenticated" }; return; }
ctx.item = await ctx.anton.connect({
identityId: ctx.me.id,
channelName: `anton:${ctx.me.id}`,
connectionId,
...(message ? { message } : {}),
});
}
Deploy:
gg rat api deploy
# or just the function:
gg rat api function update --file functions/anton-connect.ts
For anonymous sessions, expose the function with --access GET and pick an identityId from something the client sends but can't easily forge (signed cookie, Turnstile token, Vipps phone, device fingerprint). The function is the gatekeeper for whatever identityId it forwards.
Functions the agent calls must declare their inputs
Your app's REST API becomes the agent's tools by generating them from your OpenAPI (/api/_openapi). A tool can only pass arguments that the spec declares — so the rule is:
If the agent must supply a value, that value has to be in the contract: a typed request body (POST/PUT/PATCH) or a declared parameter. Never read an agent-supplied value ad-hoc from the query string.
A GET function's generated tool only carries the auto-injected collection params (filter, select, sortBy, pageNo, pageSize, count, include, lastKey). There is no way to add a custom query param to a function's generated tool — if your function reads ctx.req.query.myArg, the agent's tool has no slot for myArg and the call fails (400, or the function runs with the value missing). It works when you call it by hand (?myArg=…) but the agent can't, because the tool generator never saw myArg.
Anything the agent needs to choose at call time belongs in a POST body instead — a body is free-form JSON, so the agent's tool can carry arbitrary fields. Read it from ctx.req.bodyJson, and make sure the verb you read is in the function's access allow-list (a GET-only allow-list rejects the agent's POST with 403).
This is deliberately not something the AntonAgent binding can paper over: it has no way to know your function needs myArg, what values are valid, or which verb to use — none of that is in the spec. Keep the contract honest and the tools generate correctly for free.
// ❌ agent can't drive this — `path` is read from the query but never declared
// GET /api/spond -> ctx.req.query.path
const path = ctx.req?.query?.path;
// ✅ agent can drive this — `path` rides in the POST body the tool exposes
// POST /api/spond (access: POST) -> ctx.req.bodyJson.path
const { path } = (ctx.req?.bodyJson ?? {}) as { path?: string };
(A read-only proxy over POST looks odd, but the POST body is just the transport that lets the agent pass an argument — your function can still do a plain GET to whatever it proxies.)
Files — in and out
The agent has no file channel of its own, and does not need one: keep the files in the app's data, where the user and the agent already meet. A file the user uploads in your app is one the agent can open, and a file the agent produces is one your app can show — with the access rules you already have.
-
A collection with a
blobfield, one item per file. The examples below call the collectiondocumentsand the fieldfile. -
Access for the users who talk to the agent. The agent acts as the user it was connected for (
ctx.me, oractAs), so it reaches exactly the files that user can. On the collection,GETlets it read and download; to hand a file back it needs to create an item and write to it, so grantPOSTandPATCHas well;DELETEis separate and only for removing. A security policy narrows that to which items. A session with no user behind it — anonymous, and noactAs— reaches none of the app's data, whatever the collection allows. -
Tell the agent where files live, in the
promptyou pass toctx.anton.connect. It cannot guess that a collection holds its files. Add it to the call you already have:prompt:
"Files the user shares with you are items in the `documents` collection, in its `file` field. " +
"`gg rat data documents get <id>` shows a file's name and type; " +
"`gg rat data documents file download <id> -o <path>` saves it to disk for you to read. " +
"To hand a file back, create an item with `gg rat data documents add title=<title>`, take the id from the answer, " +
"and attach the file with `gg rat data documents file upload <id> @<path>`.",promptis stored peridentityIdand stays until it is replaced: leaving it out later keeps the old text,""clears it. Keep what is always true here, and say what changes — which file — in the conversation. -
Your app uploads to the same collection, then says which item. Create the item, then send the file as a multipart
POST /api/documents/{id}/file. The form part is always namedfile, whatever the blob field is called (Working with BLOBs). The agent is not told about the upload, so name the item in the user's next message — for example by appending "(attached: documents item<id>)" to the text you send — or it is left to guess among everything in the collection. Refresh onturn-completedto show what the agent added.
The command line, not the tools. The agent's tools carry JSON. Reading an item through them shows what a file is — its name, type and size — and never its contents. Writing through them means base64 inside the call (inline upload), which is only reasonable for something small. gg rat data runs as the same signed-in user and moves the bytes themselves; the file commands are listed under rat data.
A file is held in memory on its way to and from the agent, so this suits documents, spreadsheets and images — not large media.
Direct completion — ctx.anton.ask
When you just need an LLM response inside a function (classification, summarization, form validation, anything one-shot), skip connect entirely and call ctx.anton.ask — no container spins up.
messages is OpenAI chat-completions shape ({role, content}[]), so anything you'd hand an OpenAI SDK call drops straight in. The response is simplified to {text, usage, ...} — no choices[] envelope, no streaming. See the JSDoc on AntonAskOptions for the rest of the knobs.
Choosing a model — base, pro, max
Pass model on connect (or on ctx.anton.ask). It takes a rung, not a model name:
| Rung | For | Notes |
|---|---|---|
base | the default, and where most apps stay | cheap and fast, good enough for the large majority of turns |
pro | when answers are visibly better for the money | the full model |
max | hard reasoning, long chains, tricky code | the flagship, and the priciest per token |
A rung is a curated slot, not a pointer to a specific model. Anton decides which model sits on each one and moves that pick as the landscape changes, so your app follows along without a redeploy. That is the whole reason the option is not a model name — name a model and you own keeping up with it.
mini, smart, genius, nano, their global- forms, and global-base / global-pro / global-max no longer resolve to what they used to. They are gone, and anything we do not recognise now runs on the base rung.
So an app still sending genius is running base — two rungs below what it asks for — and nothing will tell it so. If you have one of those strings in your code, change it: mini/nano → base, smart → pro, genius → max.
They were removed because they described the model (how clever, how small) and the description kept ending up wrong: the cheapest model we offer is also the most capable one, and the model with the biggest context window sits on the bottom rung. base/pro/max name a position we control instead of a fact that drifts.
global-base / global-pro / global-max are the newest removals and the easiest to miss, because they were correct until recently and two of the three fail quietly: global-base already meant base, so it is a pure rename, but global-pro drops one rung and global-max drops two. Just delete the prefix — global-max → max. They existed to let a codebase state the region on every model string, which read well until it meant nine names for six models; norway-* earns its prefix because it changes where the turn runs, and the bare rungs never needed one.
Norway-hosted models
norway-* routes to the Telenor AI Factory instead of OpenAI — Norwegian data residency, useful when that is a requirement rather than a preference.
| Alias | Rung | Model |
|---|---|---|
norway-base | base | Nemotron |
norway-pro | pro | Qwen3-Coder |
norway-max | max | GLM-5.2 |
These are the same three rungs as everywhere else, and the alias now names the rung it sits on — which it did not always do, so if you have norway-genius, norway-smart, norway-code or norway-large anywhere, they are removed and covered by the warning below.
There is no prompt caching on these, so every turn bills full input — a long conversation costs meaningfully more than the same conversation on base, pro or max.
norway-* name leaves the regionAnything we do not recognise runs on the base rung, and that rule has no exception for the norway- prefix. A misspelled or retired Norway alias is therefore served by a global model, not a Norwegian one — with a perfectly normal-looking 200 and nothing in the response to say so.
If Norwegian data residency is a requirement rather than a preference, do not rely on the prefix to enforce it. Use exactly one of the three aliases above, and treat a typo as a residency incident rather than a performance one.
Balance — what AI usage costs and what stops it
AI usage is money. Every amount on this page — balance, sessionBalance, cost, turnCost — is a plain number in the API's currency: what the app is charged, excluding VAT. There are no credits.
Usage is charged to the APP, not to the caller. Every AntonAgent turn and every ctx.anton.ask is charged to the app that owns the session, whoever triggered it. That is why the payloads carry both an app-level balance and a per-user sessionBalance.
balance — what the app may still spend
balance is the app's AI limit less what it has spent toward it. It shows up on the connect and ask results, the stats callback, and the status/balance events below.
balance | Means |
|---|---|
| a number above 0 | the app can spend this much more on AI |
0 | AI is stopped for the app: the limit is used up, a cap is reached, an admin paused it, or the API is not online |
null | the app has no limit (a Business app, which has monthly caps instead), or the amount is not known yet |
Being stopped affects AI usage only — the app itself keeps running and serving; only agent turns and ctx.anton.ask are refused (ask fails with 402). It resumes when whatever stopped it is lifted: a raised limit or cap, a lifted pause, or a new month for a monthly cap.
sessionBalance — your own per-user budget
Separate from the app's balance, and yours to set. It is how you stop one end user spending the app's whole balance.
- Give a budget by passing
balanceonconnect, orsessionBalanceonctx.anton.asktogether with anidentityId. The user's AI usage is taken off it, and at 0 they are stopped while the rest of the app carries on. - It seeds, it does not overwrite. What a user has left is kept across reconnects; passing a new amount later changes nothing.
- Only an amount above 0 is a budget. Omit it, or pass 0, and the user has no budget of their own — they are not stopped.
- Change it with
ctx.anton.setBalance({ identityId, balance }), which sets what the user has left, whatever they had. Here 0 does stop them. The user must have been connected withconnectfirst; otherwise the call fails with404. - Read it from
sessionBalanceon the results and events, or fromctx.anton.list(), which givessessionBalance(what is left) andsessionBalanceLimit(the budget last given) for every session.
credits on connect, sessionCredits on ask and ctx.anton.setCredits are gone, and so are credits, sessionCredits and creditsUsed on the results.
The old options are ignored, not rejected: an app that still passes credits or sessionCredits runs with no per-user budget at all. ctx.anton.setCredits fails with an error. Amounts do not carry over either — 100,000 credits was one dollar, and a budget is now an amount in the API's currency — so pick new numbers rather than renaming the field.
Stats callback (optional)
Pass callbackUrl on connect. Anton POSTs JSON to it at turn lifecycle boundaries. Intended for billing/analytics on the app dev side.
URL forms accepted:
- Absolute:
https://example.com/anton-stats?key=… - Leading-slash relative:
/anton-stats?key=…— resolves against your app's own Dynamic base URL. Use this to point at one of your own functions without hardcoding the host.
No auth envelope. Bake your shared secret into the URL query string. Delivery is fire-and-forget — ~2s timeout, no retries, failures logged server-side only.
event | When |
|---|---|
turn-started | Codex begins a turn — what is left before the model runs |
turn-completed | Turn finished, failed or was interrupted — full stats |
Common fields on every payload: event, apiId, apiName, identityId, threadId.
turn-started:
{
"event":"turn-started",
"apiId":"…","apiName":"…","identityId":"…","threadId":"…",
"balance": 87.72,
"sessionBalance": 4.52
}
turn-completed:
{
"event":"turn-completed",
"apiId":"…","apiName":"…","identityId":"…","threadId":"…",
"status":"completed",
"error": null,
"balance": 87.4,
"sessionBalance": 4.2,
"sessionBalanceLimit": 10,
"sessionBalanceUsed": 5.8,
"turnCost": 0.32,
"turnDurationMs": 4820,
"toolCalls": [
{ "server":"parkly", "tool":"get-schema", "durationMs":120 },
{ "server":"parkly", "tool":"crt-item", "durationMs":380, "error":"…" }
]
}
statusiscompleted,failedorinterrupted.balanceandsessionBalancearenullwhen the app has no limit or the user has no budget;sessionBalanceLimitandsessionBalanceUsedare thennulltoo.turnCostis what the user's spending grew by during the turn. A cost that arrives late counts toward the next turn, and actx.anton.askfor the same user in the meantime is included.sessionBalanceUsedis the budget last given less what is left, so it starts over when the budget is set again. Add upturnCostfor totals.- A tool call that never reported back has no
durationMs.
Respond 2xx quickly. Bodies are dropped. >2s is aborted.
Client: connection flow
1. GET /api/_me → current user ID
2. GET /api/_rt/ws → WebSocket URL (one-time token)
3. new WebSocket(url, 'json.webpubsub.azure.v1')
4. Wait {type:'system', event:'connected'} → extract connectionId
5. POST /api/anton-connect → {connectionId}
→ {channel, balance, sessionBalance}
6. Ready — send/receive via channel
All fetch calls need credentials:'include' so access/refresh cookies are sent.
Tip: connect eagerly when the chat widget mounts (not on first user message). anton-connect wakes the agent container server-side, so doing it early shaves first-message latency.
Client: payload shape (read this before either transport)
Server-published frames (every event in the tables below — ack, text, turn-started, …) arrive with data as a JSON-encoded string, even though the envelope's dataType is "json". Only client-to-group echoes round-trip as objects. So clients MUST normalize:
const payload = typeof data === 'string' ? JSON.parse(data) : data;
Skip this and payload.type is undefined for every server event.
Server-published payloads also carry an extra _from: "anton" marker. Ignore it — and ignore any other unknown fields; the protocol may add more.
Client: SDK alternative
@azure/web-pubsub-client handles reconnect, subprotocol framing, token refresh, and group filtering. The event protocol is the same; only the transport changes.
npm install @azure/web-pubsub-client
import { WebPubSubClient } from "@azure/web-pubsub-client";
const client = new WebPubSubClient({
// Re-invoked on every (re)connect — always returns a fresh token.
getClientAccessUrl: async () => {
const r = await fetch("/api/_rt/ws", { credentials: "include" });
const { url } = (await r.json()) as { url: string };
return url;
},
});
client.on("group-message", (e) => {
if (e.message.dataType !== "json") return;
// Server frames are JSON-encoded strings; echoes are objects. Normalize.
const raw = e.message.data;
const payload = typeof raw === "string" ? JSON.parse(raw) : raw;
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return;
if (typeof payload.type !== "string") return;
handleMessage(payload as { type: string; [k: string]: unknown });
});
await client.start();
// anton-connect joins the connection to the group server-side.
const r = await fetch("/api/anton-connect", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ connectionId: client.connectionId }),
});
const { channel, balance } = await r.json();
// Re-call anton-connect on every reconnect — connectionId changes.
client.on("connected", async (e) => {
await fetch("/api/anton-connect", {
method: "POST", credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ connectionId: e.connectionId }),
});
});
Send:
await client.sendToGroup(
channel,
{ type: "message", text, id: crypto.randomUUID() },
"json",
{ noEcho: true }, // server drops your own copy — no fromUserId filter needed
);
The SDK does NOT remove app-level ACK recovery: if Anton's subscription dropped mid-turn (container restart), the message is gone even though the transport succeeded. Keep the ACK-timeout → anton-connect re-send flow from "ACK system" below.
Client: message envelope (raw WS path)
Skip this section if you're using the SDK above.
Send:
ws.send(JSON.stringify({
type: 'sendToGroup',
group: channel, // from anton-connect response
data: payload, // one of the payloads below
dataType: 'json',
}));
Receive — messages arrive on ws.onmessage:
{"type":"message","from":"group","fromUserId":"...","data":<payload>}
<payload> is either a JSON object (your own echoes) or a JSON-encoded string (every server event). See "payload shape" above — you MUST JSON.parse strings before dispatching.
ws.onmessage = (ev) => {
const m = JSON.parse(ev.data);
if (m.type !== 'message' || m.from !== 'group') return;
if (m.fromUserId === myUserId) return;
const payload = typeof m.data === 'string' ? JSON.parse(m.data) : m.data;
handleMessage(payload); // payload._from === "anton" on server frames; ignore it
};
Client: send events (client → server)
type | Fields | Purpose |
|---|---|---|
message | text, id? | Chat message; include id (UUID) to receive an ack |
status | — | Request current agent state + balances |
cancel | — | Cancel the current AI turn |
new-thread | — | Start a new conversation |
restart | — | Restart the agent container |
fast | — | Enable fast mode for next turn |
effort | level:'low'|'medium'|'high' | Set reasoning effort |
thread-list | — | Request past conversations |
thread-resume | threadId | Resume a conversation |
compact | — | Compact current thread context |
rollback | turns? | Undo turns (default 1) |
mcp-list | — | Request MCP server list |
tool-result | reqId, result?, error? | Only used if you configured uiTools (see end of doc). |
Client: receive events (server → client)
Anton envelope events
type | Fields | Notes |
|---|---|---|
ack | id | Server received your message — clear the pending ACK timer |
status | agent, balance?, sessionBalance? | Agent state, with the balances when it has them |
balance | balance, sessionBalance | Balances only — sent after every turn. Either can be null (see Balance) |
info | message | Info message |
idle-timeout | deadline, timeoutMs | Idle warning |
anton-event | event | Lifecycle event |
mcp-list | servers:[{name, status}] | MCP server list |
thread-list | threads:[{id, preview, createdAt}] | Past threads. createdAt is unix seconds (UTC), not ms or ISO. preview is the first user message, trimmed to 200 chars. Sorted newest first, capped at 50. |
Codex-translated events
These come from the codex AI engine, translated server-side into a stable vocabulary so you don't need to know codex internals.
type | Fields | Notes |
|---|---|---|
text | delta | Assistant text delta — concat into a buffer, render markdown |
user-message | text | Past user turn — only inside history (not live) |
turn-started | — | Show spinner |
turn-completed | status, error? | Hide spinner, refresh app data. Status: 'completed'|'failed'|'interrupted' |
tool-start | name, id | Built-in tool call |
tool-end | id | |
command-start | command, id | Shell command |
command-output | delta | Stdout/stderr |
command-end | id, exitCode? | |
file-start | path, id | File change |
file-output | delta | Diff delta |
file-end | id | |
mcp-tool-start | id, server, tool, argsPreview? | MCP tool — emoji-map by tool name |
mcp-tool-end | id, durationMs?, error? | |
mcp-status | name, status | MCP server liveness |
tool-progress | message | MCP tool progress |
thinking-start | id | Reasoning began |
thinking | delta | Reasoning delta |
thinking-end | id | |
web-search | id, query, action? | Agent searched the web. Action: 'search'|'openPage'|'findInPage'|'other' |
plan-updated | steps:[{step, status}], explanation? | Full plan snapshot, NOT a delta — replaces any prior plan. Status: 'pending'|'inProgress'|'completed' |
compacted | — | Context auto-compacted — optional banner so users understand earlier turns appear forgotten |
history | items: ClientMessage[] | Replay batch on thread-resume — items use the same vocab as live events; render through the same dispatcher |
error | message, willRetry? | Show error to user |
tool-call | tool, params, reqId? | Only fires if you configured uiTools (see end of doc). |
Markdown rendering for text
Codex text deltas are GitHub-flavored markdown — concat into a buffer per assistant message and re-render the full buffer on each update. Never render deltas in isolation (a code fence or link can span multiple deltas). Treat the agent's output as untrusted (it can embed user-supplied content) — sanitize before injecting as HTML.
Expect: headings, bold/italic, inline code, fenced code blocks (with language tags), bullet/numbered lists, tables, links, blockquotes. Use a markdown library that tolerates incomplete fences during streaming.
History on thread-resume
When the user picks a past thread, the server fires one {type:'history', items:[…]} before any new turn. Each item is a ClientMessage from the table above (including turn-started/turn-completed boundaries). Run them through your live dispatcher — same render code.
Client: ACK system
Web PubSub can drop a subscription mid-flight (container restart, etc.) where the transport succeeded but Anton never received the message. The ACK pattern detects that and recovers by re-calling anton-connect.
function send() {
const id = crypto.randomUUID();
sendToGroup({ type: 'message', text, id });
setWaitingAck(true);
setGenerating(true);
pendingAckRef.current = {
id,
timer: setTimeout(() => {
// No ACK after 10s — recover by calling anton-connect
// again with the pending message in the body.
setWaitingAck(false);
fetch('/api/anton-connect', {
method: 'POST',
credentials: 'include',
body: JSON.stringify({ connectionId, message: text }),
});
}, 10000),
};
}
// In handleMessage:
if (type === 'ack' && pendingAck?.id === data.id) clearPendingAck();
if (type === 'turn-started') clearPendingAck();
Without ACK recovery the chat still works — the user just refreshes manually if Anton stops responding.
Client: data refresh
When turn-completed fires, refresh the host app's data — the agent may have modified collections, created files, etc.
if (type === 'turn-completed') {
// ...cleanup
onDataChanged?.();
}
Optional: UI tools
Skip this section unless you have a specific reason to push UI side-channel events from codex (e.g., highlight a row, open a modal, ask the user to confirm a destructive action). For normal chat apps, you do NOT need this — the assistant's text replies plus turn-completed → onDataChanged() covers almost every use case.
If you do need it: pass uiTools on connect with each tool's description + params (and returns for round-trip tools). Codex sees them as MCP tools and may call them. Tool calls arrive on the frontend as {type:'tool-call', tool, params, reqId?}; if reqId is set, reply with {type:'tool-result', reqId, result} within 30s.
uiTools shape
Each entry is { description, params?, required?, returns? }. params accepts either form — both are normalized server-side:
uiTools: {
// No-params tool. Fire-and-forget (no `returns`) → frontend doesn't reply.
'refresh-list': {
description: 'Tell the UI to reload the current list. Call after you mutate data.',
params: {},
},
// Tool with params + round-trip (`returns` is set, so the frontend MUST
// send `{type:'tool-result', reqId, result}` within 30s).
'highlight-row': {
description: 'Visually highlight a row by id and return whether it was found.',
params: {
id: { type: 'string', description: 'Row id to highlight.' },
color: { type: 'string', enum: ['red', 'yellow', 'green'] },
},
required: ['id'],
returns: {
type: 'object',
properties: { found: { type: 'boolean' } },
required: ['found'],
},
},
// Same tool, written as a full JSON schema — also accepted. Use whichever
// form your LLM-generated code reaches for.
'highlight-row-alt': {
description: 'Same as above.',
params: {
type: 'object',
properties: {
id: { type: 'string' },
color: { type: 'string', enum: ['red', 'yellow', 'green'] },
},
required: ['id'],
},
returns: { /* same as above */ },
},
}
Codex picks them up via -c MCP server flags when the container boots. Adding or changing uiTools on a later connect call does NOT propagate to a running container. To apply changes, call ctx.anton.close({ identityId }) (kills the container), then reconnect.
Optional: API tools — cost tuning
By default the agent reaches your app's backend REST API through three meta-tools and looks endpoints up on demand. With apiTools: 'all' every endpoint (user/role CRUD, auth, your own collections) becomes a tool of its own; those tool definitions are re-sent to the model uncached on every turn, so a large API is a real per-turn input-token cost even when the agent never touches most of it. For a UI-only or game agent that acts purely through uiTools, any API access is pure overhead.
These tools are generated straight from your OpenAPI, so a function is only drivable by the agent if its inputs are declared in the spec — see Functions the agent calls must declare their inputs.
Control it with apiTools on connect:
ctx.anton.connect({
identityId,
channelName,
connectionId,
uiTools: { /* your gameplay/UI tools */ },
apiTools: 'none', // worm-game agent plays via uiTools only — skip the API
});
apiTools | Agent sees | Use when |
|---|---|---|
'all' | every endpoint as a tool | the agent works directly with your API/data |
'dynamic' (default) | 3 meta-tools (list-api-endpoints, get-api-endpoint-schema, invoke-api-endpoint) | you want the API available but cheap — the agent discovers endpoints on demand (adds round-trips; best with a capable model) |
'explicit' | only the operations in apiToolsAllow (e.g. ['GET-user', 'POST-order']) | the agent needs a known handful of endpoints |
'none' | nothing from the backend API | UI-only / game agents that work purely through uiTools |
apiTools is applied via -c MCP flags at container boot. Changing it on a later connect does NOT affect a running container — ctx.anton.close({ identityId }) then reconnect to apply.
Optional: web search — on or off
The agent has a built-in web search tool as well as your API. Asked "has anything happened to the goats lately?", it may look for news before it looks in your data. Pass webSearch: false on connect to remove that tool:
ctx.anton.connect({
identityId,
channelName,
connectionId,
webSearch: false, // no built-in web search
});
webSearch | Built-in web search tool |
|---|---|
false | removed |
true, or left out | the platform default, which today is on |
Send it on every connect. A value you leave out is remembered only while the session lives; ctx.anton.close or an idle timeout ends the session, and the next connect starts from the default again.
It removes one tool and is not network isolation. The agent still has its shell, your API and UI tools, and anything you add with configFlags (which are applied last, so a web_search setting there wins). The search itself is run by whoever serves the model, not by the agent's container, so on a model endpoint of your own it is that server's web search you are turning off.
webSearch is read when the agent's container starts. Changing it on a later connect does NOT affect a running container — ctx.anton.close({ identityId }), then connect again with the new value.