codai docs
Agents

Async runs

Create a durable run, poll it, read its steps, follow it over SSE, cancel it.

An asynchronous run is persisted the moment you create it. The HTTP request returns at once; the loop executes on the gateway and survives your client going away. You then poll, stream or cancel by id.

Async runs need a key that belongs to a codai account. Legacy compat keys get 403 forbidden — Async agent runs require an authenticated codai account key. — and can use the synchronous endpoint only.

Lifecycle

queued ──▶ running ──▶ completed
                  ├──▶ failed
                  └──▶ cancelled

status is one of queued, running, completed, failed, cancelled. A run that stops before finishing carries error; one that finishes carries result.

Create

POST /v1/agents/runs takes the same body as a sync run plus two fields:

Prop

Type

curl https://ai.codai.ro/v1/agents/runs \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "task": "Audit the public API of github.com/codai-ro/codai-protocol and list every breaking change since v0.9.",
    "budget_seconds": 600,
    "client_request_id": "audit-protocol-2026-09-23"
  }'

Response is 202 Accepted:

{ "id": "3f9a1c2e-…", "status": "queued", "poll": "/v1/agents/runs/3f9a1c2e-…" }

A duplicate client_request_id also answers 202, with the original id.

Poll

GET /v1/agents/runs/:id — owner-scoped; someone else's id is 404 not_found.

{
  "id": "3f9a1c2e-…",
  "status": "completed",
  "task": "Audit the public API of …",
  "model": "codai",
  "result": "Two breaking changes since v0.9: …",
  "error": null,
  "step_count": 0,
  "usage": { "prompt_tokens": 41230, "completion_tokens": 1874 },
  "created_at": "2026-09-23T09:12:04.118Z",
  "started_at": "2026-09-23T09:12:04.402Z",
  "finished_at": "2026-09-23T09:13:51.977Z"
}

Poll every few seconds; the loop typically finishes in tens of seconds to a few minutes. Terminal states are completed, failed, cancelled.

Read the steps

GET /v1/agents/runs/:id/steps returns { id, steps: [...] }, newest first. Each step:

Prop

Type

Today the gateway records one answer step when the run completes. Intermediate plan / tool_call / observation steps are part of the schema and will appear as the loop starts persisting them — read kind rather than assuming a fixed sequence.

Stream

GET /v1/agents/runs/:id/stream is Server-Sent Events. Send Accept: text/event-stream and read until done:

curl -N https://ai.codai.ro/v1/agents/runs/3f9a1c2e-…/stream \
  -H "Authorization: Bearer $CODAI_API_KEY"
event: step
data: {"seq":0,"kind":"answer","summary":"Two breaking changes since v0.9: …"}

event: done
data: {"status":"completed","result":"Two breaking changes since v0.9: …","error":null,"step_count":0}
EventPayloadMeaning
step{ seq, kind, summary }One step, in order. Every step recorded so far is replayed when you connect.
done{ status, result, error, step_count }The run reached a terminal state. The stream ends.
timeout{}You have been connected for 30 minutes. The stream ends; reconnect if the run is still going.

Reconnecting. Frames carry no id: line and the gateway does not read Last-Event-ID. To resume, simply connect again: all steps are replayed from seq 0, so keep the highest seq you have seen and skip anything at or below it. The stream polls the run once per second, so a reconnect costs at most a second of latency.

The stream is text/event-stream; charset=utf-8 with Cache-Control: no-cache and x-accel-buffering: no, so it passes through nginx-style proxies unbuffered. Use curl -N or a non-buffering client.

Cancel

POST /v1/agents/runs/:id/cancel — only a queued or running run can be cancelled:

{ "id": "3f9a1c2e-…", "status": "cancelled" }

Anything else — already finished, already cancelled, not yours — is 404 not_found (Agent run not found or not cancellable.). The in-flight loop is not interrupted mid-tool, but its result is discarded when it tries to complete, and the run stays cancelled.

Stats

GET /v1/agents/runs/stats?days=7 — your runs over the last 1 – 90 days (default 7):

{
  "window_days": 7,
  "total": 42,
  "completed": 38,
  "failed": 3,
  "cancelled": 1,
  "in_flight": 0,
  "prompt_tokens": 1830412,
  "completion_tokens": 78210,
  "avg_steps": 0,
  "avg_latency_ms": 64211
}

Durability

A run whose gateway instance restarts mid-flight is left running with a short lease. A sweeper reclaims runs whose lease lapsed and executes them again from the stored task, so the run always reaches a terminal state without you resubmitting. Because the re-execution starts over rather than resuming a checkpoint, make sure the task is safe to run twice — reads are; unguarded side effects are not.

Errors

StatuscodeWhen
400bad_requestInvalid body; error.details lists the fields.
403forbiddenPlan without sub-agents/tools, or a legacy key on the async endpoints.
404not_foundUnknown id, not your run, or cancel on a finished run.

On this page