# Run-uri asincrone (/ro/docs/agents/async-runs)

Creezi un run durabil, îl interoghezi, îi citești pașii, îl urmărești prin SSE, îl anulezi.

Un run asincron este persistat în momentul în care îl creezi. Cererea HTTP se întoarce imediat; bucla se execută pe gateway și supraviețuiește dispariției clientului tău. Apoi faci poll, stream sau anulezi după id.

Run-urile asincrone au nevoie de o cheie care aparține unui cont codai. Cheile compat legacy primesc `403 forbidden` — *Async agent runs require an authenticated codai account key.* — și pot folosi doar endpoint-ul sincron.

## Ciclul de viață [#ciclul-de-viață]

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

`status` este una dintre valorile `queued`, `running`, `completed`, `failed`, `cancelled`. Un run care se oprește înainte să termine poartă `error`; unul care termină poartă `result`.

<Steps>
<Step>

### Creezi [#creezi]

`POST /v1/agents/runs` primește același corp ca un [run sincron](./index.mdx#corpul-cererii) plus două câmpuri:

<TypeTable
  type={{
    budget_seconds: { type: 'integer', description: 'Indiciu de timp real (wall-clock) pentru run, 10 – 1800. Stocat pe run; bucla aplică momentan propriul buget.', default: '300' },
    client_request_id: { type: 'string', description: 'Cheie de idempotență, până la 200 de caractere, la nivel de cont. O reîncercare cu aceeași valoare returnează run-ul existent în loc să creeze un al doilea.' },
  }}
/>

<Tabs items={['curl', 'TypeScript', 'Python']}>
<Tab value="curl">

```bash
curl https://ai.codai.ro/v1/agents/runs \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "task": "Auditează API-ul public al github.com/codai-ro/codai-protocol și listează fiecare breaking change de la v0.9.",
    "budget_seconds": 600,
    "client_request_id": "audit-protocol-2026-09-23"
  }'
```

</Tab>
<Tab value="TypeScript">

```ts
const res = await fetch('https://ai.codai.ro/v1/agents/runs', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CODAI_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    task: 'Auditează API-ul public al github.com/codai-ro/codai-protocol și listează fiecare breaking change de la v0.9.',
    budget_seconds: 600,
    client_request_id: 'audit-protocol-2026-09-23',
  }),
});
const { id, status, poll } = (await res.json()) as { id: string; status: string; poll: string };
```

</Tab>
<Tab value="Python">

```python
import os, requests

r = requests.post(
    "https://ai.codai.ro/v1/agents/runs",
    headers={"Authorization": f"Bearer {os.environ['CODAI_API_KEY']}"},
    json={
        "task": "Auditează API-ul public al github.com/codai-ro/codai-protocol și listează fiecare breaking change de la v0.9.",
        "budget_seconds": 600,
        "client_request_id": "audit-protocol-2026-09-23",
    },
    timeout=30,
)
run = r.json()  # {"id": "...", "status": "queued", "poll": "/v1/agents/runs/..."}
```

</Tab>
</Tabs>

Răspunsul este `202 Accepted`:

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

Un `client_request_id` duplicat răspunde tot `202`, cu `id`-ul original.

</Step>
<Step>

### Faci poll [#faci-poll]

`GET /v1/agents/runs/:id` — limitat la proprietar; id-ul altcuiva este `404 not_found`.

```json
{
  "id": "3f9a1c2e-…",
  "status": "completed",
  "task": "Auditează API-ul public al …",
  "model": "codai",
  "result": "Două breaking changes de la 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"
}
```

Faci poll la câteva secunde; bucla termină de obicei în zeci de secunde până la câteva minute. Stările terminale sunt `completed`, `failed`, `cancelled`.

</Step>
<Step>

### Citești pașii [#citești-pașii]

`GET /v1/agents/runs/:id/steps` returnează `{ id, steps: [...] }`, cei mai noi primii. Fiecare pas:

<TypeTable
  type={{
    seq: { type: 'integer', description: 'Poziția în run, de la 0.' },
    kind: { type: "'plan' | 'tool_call' | 'observation' | 'answer'", description: 'Ce înregistrează pasul.' },
    summary: { type: 'string', description: 'Text scurt — pentru un pas answer, primele 2 000 de caractere ale rezultatului.' },
    detail: { type: 'object | null', description: 'Payload structurat, când pasul are unul.' },
    latencyMs: { type: 'integer | null', description: 'Timpul petrecut pe pas.' },
    createdAt: { type: 'string', description: 'Timestamp ISO.' },
  }}
/>

<Callout type="info">
Momentan gateway-ul înregistrează un singur pas `answer` când run-ul se încheie. Pașii intermediari `plan` / `tool_call` / `observation` fac parte din schemă și vor apărea pe măsură ce bucla începe să îi persiste — citește `kind` în loc să presupui o secvență fixă.
</Callout>

</Step>
<Step>

### Stream [#stream]

`GET /v1/agents/runs/:id/stream` este Server-Sent Events. Trimiți `Accept: text/event-stream` și citești până la `done`:

```bash
curl -N https://ai.codai.ro/v1/agents/runs/3f9a1c2e-…/stream \
  -H "Authorization: Bearer $CODAI_API_KEY"
```

```text
event: step
data: {"seq":0,"kind":"answer","summary":"Două breaking changes de la v0.9: …"}

event: done
data: {"status":"completed","result":"Două breaking changes de la v0.9: …","error":null,"step_count":0}
```

| Eveniment | Payload | Semnificație |
| --- | --- | --- |
| `step` | `{ seq, kind, summary }` | Un pas, în ordine. Fiecare pas înregistrat până atunci este redat din nou când te conectezi. |
| `done` | `{ status, result, error, step_count }` | Run-ul a ajuns într-o stare terminală. Stream-ul se încheie. |
| `timeout` | `{}` | Ești conectat de 30 de minute. Stream-ul se încheie; reconectează-te dacă run-ul încă rulează. |

**Reconectarea.** Cadrele nu poartă o linie `id:` și gateway-ul nu citește `Last-Event-ID`. Pentru a relua, pur și simplu te conectezi din nou: toți pașii sunt redați de la `seq 0`, așa că păstrezi cel mai mare `seq` pe care l-ai văzut și sari peste orice e la sau sub el. Stream-ul interoghează run-ul o dată pe secundă, deci o reconectare costă cel mult o secundă de latență.

Stream-ul este `text/event-stream; charset=utf-8` cu `Cache-Control: no-cache` și `x-accel-buffering: no`, așa că trece nebufferizat prin proxy-uri de tip nginx. Folosește `curl -N` sau un client fără buffering.

</Step>
<Step>

### Anulezi [#anulezi]

`POST /v1/agents/runs/:id/cancel` — doar un run `queued` sau `running` poate fi anulat:

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

Orice altceva — deja terminat, deja anulat, nu e al tău — este `404 not_found` (*Agent run not found or not cancellable.*). Bucla în desfășurare nu e întreruptă în mijlocul unui tool, dar rezultatul ei este aruncat când încearcă să se încheie, iar run-ul rămâne `cancelled`.

</Step>
</Steps>

## Statistici [#statistici]

`GET /v1/agents/runs/stats?days=7` — run-urile tale din ultimele 1 – 90 de zile (implicit 7):

```json
{
  "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
}
```

## Durabilitate [#durabilitate]

Un run a cărui instanță de gateway repornește în timpul execuției rămâne `running` cu un lease scurt. Un sweeper recuperează run-urile al căror lease a expirat și le execută din nou din sarcina stocată, astfel încât run-ul ajunge întotdeauna într-o stare terminală fără să retrimiți nimic. Pentru că re-execuția pornește de la zero, nu reia de la un checkpoint, asigură-te că sarcina e sigură de rulat de două ori — citirile sunt; efectele secundare neprotejate nu sunt.

## Erori [#erori]

| Status | `code` | Când |
| --- | --- | --- |
| `400` | `bad_request` | Corp invalid; `error.details` listează câmpurile. |
| `403` | `forbidden` | Plan fără sub-agenți/unelte, sau o cheie legacy pe endpoint-urile asincrone. |
| `404` | `not_found` | Id necunoscut, nu e run-ul tău, sau cancel pe un run terminat. |
