Skip to main content

Agents API

This page documents every agent endpoint on agent-engine-api: the full request and response shapes, auth gates, status codes, and the exact error messages the API returns. Set $CAI_API first — see the API overview.

Functions share this surface: deploying with framework=function creates a function, and every other route here works on it. See Functions overview.

Conventions

  • Errors use the standard envelope {"error": "<message>", "request_id": "<id>"}.
  • Project pinning: every route accepts an optional ?project=<slug|short|id> query. Agents are addressed by name, and the same name may exist in several projects.
  • Agent names must match ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$ (a lowercase DNS label, max 63 characters). A bad name is 400 — invalid agent name (must be a lowercase DNS label).
  • Session IDs in paths must match ^[A-Za-z0-9_.-]{1,128}$ (400 — invalid session id). User IDs must match ^[A-Za-z0-9_.:@-]{1,128}$.
  • Auth gates used below:
    • owner — the automation token, a project admin, or the user who deployed the agent. No grant on the project → 404 not found. Someone else's agent → 404 unknown agent: <name> (existence is never confirmed). An agent with no recorded owner → 403 — this agent has no recorded owner, so only a project admin can manage it (a project admin can adopt it by redeploying it).
    • open — no credential needed by default; the operator can require one with INVOKE_AUTH_REQUIRED=true.

Route table

MethodPathAuthPurpose
POST/v1/agentsany credentialDeploy (create or redeploy) from a tarball
GET/v1/agentsany credentialList agents and functions
GET/v1/agents/{name}ownerGet one agent
DELETE/v1/agents/{name}ownerDelete an agent
POST/v1/agents/{name}/redeployownerRebuild from stored source
GET/v1/agents/{name}/logsownerLive pod logs (text/plain)
GET/v1/agents/{name}/logs/historyownerPersisted log history
GET/v1/agents/{name}/secretsownerList secret key names
PUT/v1/agents/{name}/secretsownerReplace all secrets
PATCH/v1/agents/{name}/secretsownerMerge/remove secrets
GET/v1/agents/{name}/envownerRead env vars (names and values)
PATCH/v1/agents/{name}/envownerMerge/remove env vars
GET/v1/agents/{name}/filesownerList stored source files
GET/v1/agents/{name}/files/{path...}ownerRead one file
PUT/v1/agents/{name}/files/{path...}ownerWrite one file
DELETE/v1/agents/{name}/files/{path...}ownerDelete one file
GET/v1/agents/{name}/revisionsownerList revisions with traffic share
POST/v1/agents/{name}/set-trafficownerReplace the traffic split
GET/v1/agents/{name}/configownerRead compute config
PATCH/v1/agents/{name}/configownerUpdate compute config (new revision)
GET/v1/agents/{name}/specownerSanitized live YAML
GET/v1/agents/{name}/metricsownerLive instance/readiness counts
GET/v1/agents/{name}/usersownerList callers (users) of this agent
GET/v1/agents/{name}/sessionsownerList one user's sessions
GET/v1/agents/{name}/sessions/{id}ownerFull session transcript
DELETE/v1/agents/{name}/sessions/{id}ownerDelete a session
POST/v1/agents/{name}/invokeopenTalk to the agent (data plane)
POST/v1/agents/{name}/invoke/streamopenStreaming invoke (NDJSON)
POST/v1/agents/{name}/sessions/{id}/memorizealways authenticatedCommit a session to the memory bank
GET/v1/agents/{name}/embedownerRead chat-widget config
PUT/v1/agents/{name}/embedownerUpdate chat-widget config
DELETE/v1/agents/{name}/embedownerRemove chat-widget config
POST/v1/agents/{name}/embed/rotate-keyownerRotate the public embed key
GET/v1/embed/{key}/configanonymous, Origin-allowlistedPublic widget config
POST/v1/embed/{key}/invokeanonymous, Origin-allowlistedPublic widget invoke

Deploy and lifecycle

POST /v1/agents

Deploys an agent (or function): creates it, or redeploys it if the name already exists and you own it. The body is multipart/form-data, not JSON.

Form fieldTypeRequiredDefaultNotes
namestringyesLowercase DNS label, max 63 chars. Immutable.
frameworkstringnoadkOne of adk, langgraph, crewai, function.
runtimestringnopythonFunctions only. One of python, nodejs, go, ruby.
codefileyesA .tar.gz of the agent directory.
configstring (JSON)noSame shape as PATCH /config below; applied to the first revision.

Upload cap: 100 MiB by default (operator knob MAX_UPLOAD_BYTES), plus 1 MiB of slack for the form fields. The uploaded source is also stored as editable files (see Source files).

Success: 202

{"agent": "<name>", "build_id": "<uuid>"}

The build runs asynchronously. Poll GET /v1/agents/{name} for status: buildingdeployingready, or failed. There is no separate build-log endpoint — on failure, the first 4000 characters of the build output (with ...(truncated) appended) land in the agent's message field. Note that this is the beginning of the build log, so a failure reported at the very end of a long build may be cut off.

Errors:

  • 400 — missing or invalid 'name' (must be a lowercase DNS label)
  • 400 — unsupported 'framework': one of "adk", "langgraph", "crewai", "function"
  • 400 — unsupported 'runtime': one of "python", "nodejs", "go", "ruby"
  • 400 — 'runtime' only applies to framework=function
  • 400 — missing 'code' file field: ... / invalid multipart form: ... / invalid 'config' field: ...
  • 409 — this project is at its service limit (N / M): deploying needs at least one more service and cannot proceed. Delete an agent or function, or ask an admin to raise the project's service quota, then deploy. (N and M are the live used/limit numbers)
  • 409 — a build is already in progress for agent <name> (concurrent deploys are arbitrated in the database; a stale claim becomes reclaimable after 30 minutes)
  • 403 — an agent named <name> already exists and has no recorded owner, so only a project admin can redeploy it
  • 404 — unknown agent: <name> (the name is owned by someone else — deliberately indistinguishable from not existing)
Deploys can also wedge at "deploying"

If the project hits its service quota after the build, status stays deploying and message explains: This project is at its service limit (N / M), so the new revision cannot get a network route yet - .... See Platform limits.

GET /v1/agents

Lists agents and functions. Plain project members see only their own agents; project admins and the automation token see everything in visible projects.

Query: ?project=, ?page_size (default 50, max 200 — over-max is rejected), ?page_token.

Response: 200

{"agents": [ ...agentView ], "next_page_token": ""}

next_page_token is always present and empty on the last page.

The agentView object:

FieldTypeMeaning
namestringThe agent's immutable name.
statusstringbuilding | deploying | ready | failed.
kindstringagent | function.
frameworkstringadk | langgraph | crewai | function | function-nodejs | function-go | function-ruby.
runtimestringFunctions only: python | nodejs | go | ruby.
imagestringBuilt image reference (digest-pinned at runtime).
urlstringInternal cluster URL.
external_urlstringLive public HTTPS URL — present only when published and the certificate is valid.
public_urlstringCanonical public address: https://<name>-<project-short>.apps.codyhill.dev.
latest_revisionstringName of the newest revision.
messagestringLast error or build output.
ownerstringEmail of the deployer; empty for tokenless deploys.

GET /v1/agents/{name}

Returns one agentView (same shape as above). 404 — unknown agent: <name>.

DELETE /v1/agents/{name}

Deletes the runtime service, the per-agent Secret, the env ConfigMap, the compute ConfigMap, and the database row.

Response: 200

{"agent": "<name>", "deleted": true}

POST /v1/agents/{name}/redeploy

Rebuilds the agent from its stored source files — no tarball needed. This is the browser editor's deploy button. It works for functions too: the rebuild uses the framework recorded on the deployment, so a function is rebuilt as a function. (The console's Functions page has no redeploy button — it re-uploads through POST /v1/agents — but the endpoint itself is not restricted.)

Response: 202

{"agent": "<name>", "build_id": "<uuid>", "files": 3, "note": "rebuilding from the stored source"}

Errors: 400 — no source is stored for this agent - deploy it once from the CLI or upload files first

Logs

GET /v1/agents/{name}/logs

Streams text/plain logs from the newest live pod. Add ?follow=true to keep tailing.

A scaled-to-zero agent still answers 200, with this body:

agent <name> has no live pods: it is scaled to zero (no running replicas), which is normal for an idle serverless agent - it cold-starts on the next invoke. For logs from earlier runs, use GET /v1/agents/<name>/logs/history.

A genuinely missing agent is 404.

GET /v1/agents/{name}/logs/history

Persisted logs that survive scale-to-zero and revision rollouts. Retention defaults to 14 days (operator knob LOG_RETENTION).

Query: ?limit (default 500), ?since (RFC3339 timestamp).

Response: 200 (lines oldest-first)

{"agent": "<name>",
"lines": [{"ts": "...", "revision": "...", "pod": "...", "stream": "...", "message": "..."}],
"count": 42,
"note": "persisted history - survives scale-to-zero and revision rollouts"}

See also Agent logs.

Secrets and env

Secrets are write-only: key names are readable, values are never returned. Env vars are readable configuration. Every change to either rolls a new revision — a running revision never changes. Keys must match ^[A-Za-z_][A-Za-z0-9_]*$. Secret request bodies are capped at 1 MiB. See Secrets and env.

GET /v1/agents/{name}/secrets

Response: 200 — {"agent": "<name>", "keys": ["MODEL_API_KEY", ...]}

PUT /v1/agents/{name}/secrets

Full replace. Body is a flat map:

{"MODEL_API_KEY": "sk-...", "OTHER_KEY": "value"}

Empty values are dropped ("blank means inherit").

Response: 200 — {"agent": "<name>", "secrets_updated": true}

PATCH /v1/agents/{name}/secrets

Merge. Body:

{"set": {"KEY": "value"}, "remove": ["OLD_KEY"]}

An empty value inside set becomes a removal.

Response: 200 — {"agent": "<name>", "secrets_updated": true, "keys": [...]}

Errors:

  • 400 — nothing to do: provide 'set' and/or 'remove'
  • 400 — invalid secret key (must be a valid environment variable name): <key>
tip

Setting MODEL_API_KEY as a per-agent secret overrides the platform-default model key for that agent.

GET /v1/agents/{name}/env

Response: 200 — {"agent": "<name>", "env": {"CHAT_MODEL": "...", ...}} (names and values — env vars are not secret).

PATCH /v1/agents/{name}/env

Same {"set": {...}, "remove": [...]} shape as secrets.

Response: 200 — {"agent": "<name>", "env_updated": true, "env": {...}}

Source files

The platform stores the latest version of your uploaded source so you can edit it in the browser. Latest version only — there is no history and no git. Limits: 1 MiB per file, 200 files per agent, and an uploaded archive may expand to at most 64 MiB. Symlinks and path-traversal entries are rejected. Binary content only survives via the tar.gz upload path. See Files and the editor.

GET /v1/agents/{name}/files

Response: 200

{"agent": "<name>", "agent_id": "<uuid>",
"files": [{"path": "agent.py", "size": 512, "updated_at": "..."}],
"editable": true,
"note": "latest version only - the platform keeps no history, so copy anything you want to keep"}

GET /v1/agents/{name}/files/{path...}

Response: 200 — {"path": "...", "content": "...", "updated_at": "..."}

PUT /v1/agents/{name}/files/{path...}

Body: {"content": "<text>"}

Response: 200 — {"path": "...", "bytes": 512, "note": "saved - redeploy the agent for this to take effect"}

File edits do not take effect until you call POST /v1/agents/{name}/redeploy.

DELETE /v1/agents/{name}/files/{path...}

Response: 200 — {"deleted": "<path>"}

Revisions, traffic, and compute config

Every code, secret, env, or config change creates an immutable revision. See Traffic and revisions.

GET /v1/agents/{name}/revisions

Response: 200 — {"agent": "<name>", "revisions": [...]}, newest first. Each revision carries name, generation, traffic_percent, tag, created_at, readiness, and replica count, annotated with the live traffic share.

POST /v1/agents/{name}/set-traffic

Replaces the complete traffic split. Percentages must sum to exactly 100.

{"traffic": [{"revision_name": "research-buddy-00002", "percent": 100}]}

Response: 202 — {"agent": "<name>", "traffic": [...]}

Errors:

  • 400 — traffic is required - send the complete split, e.g. [{"revision_name":"research-buddy-00002","percent":100}]
  • 400 — revision <r> does not exist for this agent
Rollback is traffic-only

set-traffic moves serving traffic only. The stored source is unchanged, so a later redeploy builds forward and supersedes the pin.

GET /v1/agents/{name}/config

Response: 200 — {"agent": "<name>", "config": {...}}config is null when the agent runs on platform defaults.

PATCH /v1/agents/{name}/config

Sparse update; omitted fields keep their current values. Rolls a new revision.

{"scaling": {"min_scale": 0, "max_scale": 5, "container_concurrency": 10},
"resources": {"requests": {"cpu": "100m", "memory": "256Mi"},
"limits": {"cpu": "1", "memory": "1Gi"}},
"timeout_seconds": 300}
FieldTypeMeaning
scaling.min_scaleintInstance floor. min_scale: 1 avoids cold starts.
scaling.max_scaleintInstance ceiling. On this agent surface, 0 means unbounded.
scaling.container_concurrencyintSimultaneous requests per instance.
resources.requests / resources.limitsobjectCPU/memory, Kubernetes quantity strings.
timeout_secondsintRequest timeout.

Response: 200 — {"agent": "<name>", "config_updated": true, "config": {...}}

Errors: negative values are 400 with explanatory messages, for example min_scale -1 is negative; ... and min_scale N is greater than max_scale M; ....

Spec and metrics

GET /v1/agents/{name}/spec

Returns the live Knative Service as sanitized YAML: managedFields and last-applied annotations are stripped, and any env var whose name matches (?i)(KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|PRIVATE) has its value replaced with ***REDACTED***.

Response: 200 — {"agent": "<name>", "yaml": "...", "note": "..."}

GET /v1/agents/{name}/metrics

Live counts only — there are no historical charts (no time-series database is deployed), and the response note says so plainly.

Response: 200 — {"agent", "instances", "revisions", "readiness", "latest_ready_revision", "target_concurrency", "note"}

Users and sessions browser

These management routes let an agent's owner browse who has talked to it and read transcripts. They proxy to the agent's internal read surface using a platform-internal shared secret. When the platform's CAI_INTERNAL_TOKEN is not configured, every one of them answers 503 — session browsing is not configured: the control plane has no CAI_INTERNAL_TOKEN, so it cannot authenticate to the agent's read surface.

Harness-side page size: default 50, hard max 200. See Sessions.

GET /v1/agents/{name}/users

Query: ?page_size, ?page_token.

Response: 200

{"users": [{"user_id": "...", "first_seen": "...", "last_seen": "...", "session_count": 3}],
"next_page_token": ""}

GET /v1/agents/{name}/sessions

Query: ?user_id=<id> (required — 400 the user_id query parameter is required), plus pagination.

Response: 200

{"sessions": [{"session_id": "...", "created": "...", "last_update": "...", "event_count": 12, "preview": "..."}],
"next_page_token": ""}

GET /v1/agents/{name}/sessions/{id}

Returns the full transcript: an ordered events list where each event carries author ("user" or the agent name), timestamp, invocation_id, and content.parts (text parts, function_call, function_response; reasoning parts are flagged thought: true).

Errors: 404 — session <id> not found

DELETE /v1/agents/{name}/sessions/{id}

Response: 204 on success; 404 if missing.

Invoke (the data plane)

POST /v1/agents/{name}/invoke

Talks to the agent. Open to anonymous callers by default (INVOKE_AUTH_REQUIRED=false). Request body max 1 MiB; the buffered upstream response is capped at 32 MiB (over-cap → 502). Timeout: 60 seconds by default (operator knob INVOKE_TIMEOUT_SECONDS).

FieldTypeRequiredDefaultNotes
messagestringyesThe user's message.
session_idstringnoplatform-minted UUIDAnonymous callers who choose their own must use ≥ 24 characters.
user_idstringnoderived from the session ID (UUIDv5)Stable across repeat invokes on the same session — this is what makes multi-turn continuity work.
memorizeboolnofalseAnonymous callers may not set it (401, below).

Response: 200 (the control plane guarantees session_id and user_id are filled)

{"session_id": "...", "user_id": "...", "output": "...", "reasoning": "",
"tool_calls": [{"name": "run_python", "summary": "called with args={...}"}],
"events": [ ... ]}

Errors:

  • 400 — message is required
  • 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.
  • 400 — invalid user_id: must match ^[A-Za-z0-9_.:@-]{1,128}$ (omit it and the platform will derive one from the session)
  • 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.
  • 404 — unknown agent: <name>
  • 409 — the agent name <name> exists in more than one project; add ?project=<slug> to say which one, or sign in so it resolves within your project
  • 502 — invoke agent <name>: ... (upstream failures; upstream FastAPI detail errors are normalized into the standard error envelope)

Example:

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":"<minted>","user_id":"<derived>","output":"2**32 is 4294967296.","reasoning":"","tool_calls":[{"name":"run_python","summary":"called with args={...}"}],"events":[...]}

POST /v1/agents/{name}/invoke/stream

Same request body and validation as invoke. The response is application/x-ndjson — one JSON object per line, in turn order:

{"type":"thinking", "seq":N, "text":"<delta>"}
{"type":"output", "seq":N, "text":"<delta>"}
{"type":"block_end", "seq":N, "kind":"thinking"|"output"}
{"type":"tool_call", "name":..., "args":{...}}
{"type":"tool_result","name":..., "result":...}
{"type":"done", "session_id":..., "user_id":...}
{"type":"error", "message":...}

Minted IDs are also returned as response headers X-CAI-Session-Id and X-CAI-User-Id. You get token-level streaming when the model supports it; otherwise the first "delta" is the whole text. See Invoke an agent.

POST /v1/agents/{name}/sessions/{id}/memorize

Commits a session to the agent's long-term memory bank. Always requires authentication, even when invoke is open:

  • 401 — writing to the memory bank requires authentication, even though invoking this agent does not: stored memories are read back into later callers' context. Sign in (POST /v1/auth/login) and send 'Authorization: Bearer <token>'

Response: 200 — {"status": "ok"} (relayed from the agent). 404 — session <id> not found.

Memorized content is shared

The memory bank has no per-caller partition: anything memorized is retrievable by every later caller of that agent. That is exactly why memorize always requires auth. See Memory.

Embed (chat widget)

The embed surface has two halves: owner-gated configuration on the agent, and an anonymous public plane keyed by a rotatable embed_key with an Origin allowlist. See Embed chat.

GET /v1/agents/{name}/embed

Response: 200

{"enabled": false, "embed_key": "", "allowed_origins": [], "title": "", "subtitle": "",
"greeting": "", "accent_color": "", "launcher_text": "", "position": "right", "updated_at": ""}

An unset config reads {"enabled": false, "allowed_origins": [], "position": "right"}.

PUT /v1/agents/{name}/embed

Partial update; all fields optional, same names as the GET response. The first PUT mints the public embed_key.

DELETE /v1/agents/{name}/embed

Removes the widget configuration.

POST /v1/agents/{name}/embed/rotate-key

Mints a new public embed_key; the old one stops working.

GET /v1/embed/{key}/config

Anonymous, CORS-gated by the Origin allowlist.

Response: 200 — {"agent", "title", "subtitle", "greeting", "accent_color", "launcher_text", "position", "streaming"}

POST /v1/embed/{key}/invoke

Anonymous, CORS-gated. Body: {"message", "session_id?", "user_id?"}. Memorize is not available on this plane. OPTIONS preflights are answered on both public routes.

Served by the agent itself

Every deployed agent also serves, on its own URL:

  • GET /healthz{"status": "ok"}
  • GET /debug/config — unauthenticated and deliberately narrow: agent name, framework, resolved model, model_key_present, and non-secret config env key names. Its note explains that full env and secret names are available only to the owner via GET /v1/agents/{name}/env and /secrets.

The agent's own read/delete/memorize surfaces fail closed without the platform-internal secret header (503 — read surface disabled: CAI_INTERNAL_TOKEN is not configured, 401 — missing or invalid X-CAI-Internal). Always go through the control-plane routes above instead.