Invoke an agent
This page shows you how to talk to a deployed agent: the invoke request and response shapes, streaming, calling the agent's public URL, and what to expect from cold starts.
Before you begin
- You have a deployed agent in
readystate. If not, start with deploy an agent. - For
curlexamples you need the API endpoint in$CAI_API. The platform is in alpha and has no public API hostname yet — ask your administrator for the endpoint, or port-forward:
kubectl -n cai-system port-forward svc/agent-engine-api 8080:8080 &
export CAI_API=http://localhost:8080
Talking to an agent needs no credential by default — the data plane is open (your administrator can close it by setting INVOKE_AUTH_REQUIRED=true on the platform). Managing an agent (deploy, logs, secrets, delete) always requires signing in.
The invoke endpoint
POST /v1/agents/{name}/invoke — send one message, get one reply.
- Auth: none required by default. Optional
Authorization: Bearertoken. - Request body: JSON, at most 1 MiB.
- Add
?project=<slug>if the same agent name exists in more than one project.
Request fields
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
message | string | yes | — | The user's message. Blank or missing returns 400 message is required. |
session_id | string | no | platform-minted UUID | Reuse it to continue a conversation. Anonymous callers who pick their own must use at least 24 characters. |
user_id | string | no | derived from the session id | Must match ^[A-Za-z0-9_.:@-]{1,128}$. Omit it and the platform derives a stable one. |
memorize | boolean | no | false | Also commit this session to the memory bank. Requires authentication. |
Response fields
| Field | Type | Notes |
|---|---|---|
session_id | string | Always filled — save it to continue the conversation. |
user_id | string | Always filled — the caller identity the session belongs to. |
output | string | The agent's answer. |
reasoning | string | The agent's thinking text, when the model produced any. |
tool_calls | array | One entry per tool the agent used: {"name": "run_python", "summary": "called with args={...}"}. |
events | array | The raw framework events for the turn (verbose; useful for debugging). |
Invoke with platformctl
platformctl invoke my-agent "My boat is a Mastercraft Maristar 245. Compute 2**32 in python."
You should see:
2**32 is 4294967296.
(session: 3f2c8a1e-9d41-4c1b-a2f7-0b1c2d3e4f5a)
tool_call: run_python called with args={'code': 'print(2**32)'}
Continue the same conversation by passing the session id back:
platformctl invoke my-agent "What boat do I have?" --session 3f2c8a1e-9d41-4c1b-a2f7-0b1c2d3e4f5a
Flags: --session <id> reuses a conversation, --memorize also writes it to the memory bank (needs platformctl login first). There is no --user flag — the platform derives the user id from the session. With -o json the CLI prints {"session_id", "response", "tool_calls"}. The CLI waits up to 5 minutes for a reply.
Invoke with curl
curl -s -X POST "$CAI_API/v1/agents/my-agent/invoke" \
-H 'content-type: application/json' \
-d '{"message":"Compute 2**32 in python."}'
You should see:
{
"session_id": "3f2c8a1e-9d41-4c1b-a2f7-0b1c2d3e4f5a",
"user_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"output": "2**32 is 4294967296.",
"reasoning": "",
"tool_calls": [
{"name": "run_python", "summary": "called with args={'code': 'print(2**32)'}"}
],
"events": ["..."]
}
Session id rules for anonymous callers
If you omit session_id, the platform mints a UUID and returns it — this is the easy path. If an unauthenticated caller supplies their own session_id, it must be at least 24 characters long, or the call fails with 400:
session_id chosen by an unauthenticated caller must be at least 24 characters of unguessable randomness (or omit it and the platform will generate one). A short, guessable id would let anyone else read this conversation.
Authenticated callers are exempt. If you omit user_id, the platform derives the same one from the session id every time, so multi-turn conversations just work. See sessions for the full story.
The memorize flag
"memorize": true asks the platform to also commit the session to the agent's long-term memory bank. Anonymous callers get 401:
memorize requires authentication: it writes durable memory that later callers read back. Invoke without 'memorize', then call POST /v1/agents/{name}/sessions/{id}/memorize with a session token.
See long-term memory.
Streaming
POST /v1/agents/{name}/invoke/stream takes the exact same request body and validation rules, but responds with application/x-ndjson: one JSON object per line, in turn order.
curl -sN -X POST "$CAI_API/v1/agents/my-agent/invoke/stream" \
-H 'content-type: application/json' \
-d '{"message":"Compute 2**32 in python."}'
You should see (one object per line):
{"type":"thinking", "seq":1, "text":"<delta>"}
{"type":"output", "seq":2, "text":"<delta>"}
{"type":"block_end", "seq":3, "kind":"output"}
{"type":"tool_call", "name":"run_python", "args":{"code":"print(2**32)"}}
{"type":"tool_result","name":"run_python", "result":"4294967296\n"}
{"type":"done", "session_id":"...", "user_id":"..."}
Line types: thinking (reasoning delta), output (answer delta), block_end (a thinking or output block finished), tool_call, tool_result, done (carries the final session_id and user_id), and error with a message field if the turn fails.
Two extras worth knowing:
- Minted ids are also returned as response headers
X-CAI-Session-IdandX-CAI-User-Id, so you can capture them before the stream ends. - You get token-level deltas when the model supports streaming; otherwise the first "delta" is the whole text.
Invoke via the agent's public URL
Agents are private by default: the deployed service is cluster-local, and the control-plane routes above are the only way to reach it. GET /v1/agents/{name} still returns public_url — the address the agent would have, https://<name>-<project-short>.apps.codyhill.dev — but nothing serves that host until you publish the agent by setting CAI_EXPOSE_EXTERNAL=true on its env:
curl -s -X PATCH "$CAI_API/v1/agents/my-agent/env" \
-H "Authorization: Bearer $CAI_TOKEN" -H 'content-type: application/json' \
-d '{"set": {"CAI_EXPOSE_EXTERNAL": "true"}}'
This rolls a new revision. Once the address is live over a valid certificate, external_url is filled in and the agent serves POST /invoke and POST /invoke/stream on that host with the same body:
export AGENT_URL="$(curl -s -H "Authorization: Bearer $CAI_TOKEN" "$CAI_API/v1/agents/my-agent" | jq -r .public_url)"
curl -s -X POST "$AGENT_URL/invoke" \
-H 'content-type: application/json' \
-d '{"message":"Hello!"}'
Use the public URL for plain invocation — for example from a webhook or a frontend. For anything that touches session management or memory (browsing transcripts, deleting sessions, memorize), go through the control-plane routes under $CAI_API/v1/agents/{name}/... instead: those surfaces on the agent's own URL are locked to the platform's internal credential and will refuse you.
The agent also serves two open helper endpoints on its public URL: GET /healthz (returns {"status": "ok"}) and GET /debug/config (non-secret config: the resolved model, framework, and whether a model key is present — handy for troubleshooting).
Cold starts
Idle agents scale to zero — they run no machines at all until the next request arrives, and you pay nothing to keep them around. The first invoke after an idle period triggers a cold start: the platform schedules a pod, starts the harness, and imports your code before answering. Expect that first reply to take noticeably longer than warm requests. The whole invoke, cold start included, must finish inside the invoke timeout (60 seconds by default), so a slow-importing agent can time out on its first call and succeed on the second. See autoscaling and scale-to-zero.
Limits
| Limit | Value |
|---|---|
| Request body | 1 MiB |
| Response body (buffered by the control plane) | 32 MiB — larger replies fail with 502 |
| Invoke timeout | 60 s default (platform-configurable via INVOKE_TIMEOUT_SECONDS) |
Anonymous self-chosen session_id | at least 24 characters |
session_id charset | Not enforced on invoke, but session ids used with the memorize and session-browsing routes must match ^[A-Za-z0-9_.-]{1,128}$ (a path id outside that returns 400 invalid session id) — so stay inside it |
user_id charset | ^[A-Za-z0-9_.:@-]{1,128}$ |
Status codes
All errors use the envelope {"error": "<message>", "request_id": "<id>"}.
| Code | Meaning | Example message |
|---|---|---|
| 400 | Bad request | message is required |
| 401 | Auth needed | the memorize message above, or when the platform runs with INVOKE_AUTH_REQUIRED=true |
| 404 | No such agent (or not visible to you) | unknown agent: my-agent |
| 409 | Ambiguous name | the agent name my-agent exists in more than one project; add ?project=<slug> to say which one, or sign in so it resolves within your project |
| 502 | The agent failed or timed out | invoke agent my-agent: ... |
Next steps
- Sessions — how conversation history works.
- Long-term memory — teach an agent facts it keeps forever.
- Built-in tools — what
run_pythonandsearch_memorydo. - Troubleshooting — real error messages and fixes.