> ## Documentation Index
> Fetch the complete documentation index at: https://docs.upsonic.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent contract

> The two HTTP endpoints every agent serves, what each run sends, and how to answer.

An AgentOS agent is any container that serves two endpoints on the `port` from [agentos.yaml](https://docs.upsonic.ai/agentos/build/agentos-yaml.md):

* `GET /health` — answers any `2xx` once the agent is ready.
* `POST /invoke` — runs one capability and answers with the result.

That is the whole contract. Any language that can serve HTTP works; the [SDKs](https://docs.upsonic.ai/agentos/build/sdks.md) implement it for you in Python and TypeScript, and the [conformance kit](https://docs.upsonic.ai/agentos/build/conformance.md) checks an agent written in anything else.

## GET /health

After a container starts, AgentOS polls `GET /health` every 0.5 seconds (2 seconds per request, only the status line is read) for up to 60 seconds. Any `2xx` means healthy, and only then does a new version go live. If the container exits first, the deploy fails at once with its exit code and last output.

## POST /invoke

One endpoint serves every capability; the body names which one:

```json theme={null}
{
  "capability": "echo",
  "run_id": "r_501",
  "session_id": null,
  "user": null,
  "input": {"text": "hi"},
  "invocation": {"id": "r_501:1", "seq": 1, "attempt": 1, "kind": "start"},
  "resume": null,
  "context": {
    "token": "tok_...",
    "api_url": "http://web:8000",
    "config": {"models": {"reader": "anthropic/claude-sonnet-4-5"}, "prompts": {}, "params": {}},
    "agent_id": "...",
    "version": "v3",
    "llm": {"base_url": "http://litellm:4000/v1", "api_key": "anything (ignored)"},
    "secrets": {"CRM_API_KEY": "..."},
    "eval": null
  }
}
```

**capability** `string`

The capability to run, one of those in `agentos.yaml`.

**run\_id** `string`

The run this invocation belongs to.

**session\_id** `string | null`

The run's [session](https://docs.upsonic.ai/agentos/build/state-and-sessions.md), if it has one.

**user** `string | null`

The end user the run is for (from its session; a child run keeps its parent's), or `null`.

**input** `object`

The run's input, already checked against the capability's `input` schema.

**invocation** `object`

`id`, `seq`, `attempt` and `kind`. `kind` is `start` for the first invocation and `resume` after a person decided a [hold](https://docs.upsonic.ai/agentos/build/human-approvals.md). `seq` counts invocations of the run; `attempt` counts retries of one invocation.

**resume** `object | null`

On a `resume` invocation, the decision. `null` on `start`.

**context** `object`

Everything the agent needs to use the platform during this invocation:
`token` (the [run token](#the-run-token)), `api_url` (the platform's address as seen from inside your container; platform calls go to `{api_url}/v0/...`), `config` (models, prompts, params of this version), `agent_id`, `version`, `llm` (the [model gateway](https://docs.upsonic.ai/agentos/build/models.md)), `secrets` (see [Secrets](https://docs.upsonic.ai/agentos/build/secrets.md)), and `eval` (`null` except in [experiment runs](#experiment-runs)).

> [!NOTE]
>
> New fields may be added to the invoke body over time. Ignore what you don't know.

## Answering

Answer `200` with JSON in one of three shapes:

```json completed theme={null}
{"status": "completed", "output": {"labels": ["billing"]}}
```

```json failed theme={null}
{"status": "failed", "error": {"code": "crm_unreachable", "message": "The CRM didn't answer in 10 s."}}
```

```json suspended theme={null}
{"status": "suspended", "hold_id": "h_7"}
```

* **`completed`** — `output` must match the capability's `output` schema, otherwise the run fails with `invalid_output` (the output is kept on the run so you can see what was wrong).
* **`failed`** — `error.code` is your own code. Use lowercase letters, digits and `_`, starting with a letter, at most 63 characters, and a message that isn't empty; anything else is recorded as `agent_failed`.
* **`suspended`** — after you [asked a person](https://docs.upsonic.ai/agentos/build/human-approvals.md) with `POST /v0/holds`. `hold_id` must name the open hold this invocation created, otherwise the run fails with `invalid_response`.

You can also answer with a stream instead of JSON. See [Streaming answers](https://docs.upsonic.ai/agentos/build/streaming.md).

## How runs are invoked

For each run, AgentOS calls `POST /invoke` once, on the version the run was started on (an older version's stopped container is started again for it), and waits up to the capability's `timeout` for the answer.

* **Before your agent gets the request** (connection refused, container not ready yet), the call is retried for 30 seconds.
* **Once your agent got the request**, nothing is retried. A timeout, a crash, or any non-`2xx` status fails the run with `agent_interrupted`. Start a new run to try again.

After a person decides a hold, the same run is invoked again with `invocation.kind: "resume"`, the next `seq` and a new token. A run has at most 100 invocations (its start and one resume per decision); after that it fails with `too_many_resumes`.

### Make side effects happen once

Code before an `ask` runs again on the resume, and `invocation.id` changes on every resume. So key anything that must happen once per run — a payment, an email, a [child run](https://docs.upsonic.ai/agentos/build/child-runs.md) — by `run_id` plus a stable step name, never by `invocation.id`:

```python theme={null}
key = f"{body['run_id']}:pay:{invoice_id}"   # e.g. "r_501:pay:INV-1"
```

Keep progress in [run state](https://docs.upsonic.ai/agentos/build/state-and-sessions.md) before you suspend, so the resume knows what was already done.

## The run token

`context.token` belongs to this invocation only. It works while your agent is answering, and stops working as soon as it has answered (or the run is cancelled): at once on the platform, within 5 seconds at the model gateway.

Send it as the `x-agentos-run-token` header on every platform call:

| Call | Endpoint | Page |
| - | - | - |
| Call a model | `{context.llm.base_url}/chat/completions` and other gateway routes | [Calling models](https://docs.upsonic.ai/agentos/build/models.md) |
| Ask a person | `POST {api_url}/v0/holds` | [Human approvals](https://docs.upsonic.ai/agentos/build/human-approvals.md) |
| Keep state | `GET/PUT/DELETE {api_url}/v0/state/{scope}/{key}` | [State and sessions](https://docs.upsonic.ai/agentos/build/state-and-sessions.md) |
| Memory | `{api_url}/v0/memory/...` | [Memory](https://docs.upsonic.ai/agentos/build/memory.md) |
| Files | `GET/POST {api_url}/v0/files` | [Files and events](https://docs.upsonic.ai/agentos/build/files-and-events.md) |
| Timeline events | `POST {api_url}/v0/runs/{run_id}/events` | [Files and events](https://docs.upsonic.ai/agentos/build/files-and-events.md) |
| Child runs | `POST {api_url}/v0/capabilities/{capability}/runs` | [Child runs](https://docs.upsonic.ai/agentos/build/child-runs.md) |
| Report a streamed result | `POST {api_url}/v0/runs/{run_id}/result` | [Streaming answers](https://docs.upsonic.ai/agentos/build/streaming.md) |

> [!WARNING]
>
> Never store or log the run token, and never send it anywhere but `api_url` and the gateway. The SDKs enforce this for you.

## Experiment runs

When an [eval](https://docs.upsonic.ai/agentos/platform/evals.md) runs your agent on a dataset, each item is a normal invocation with two differences:

* `context.config` is the version's config with the experiment's temporary override merged in (for example another model for `models.reader`).
* `context.eval` is `{"recorded_holds": [...]}` instead of `null`: the decisions people made on the run the item was copied from, each `{intent, actions, items, details, decision}` (`decision` is `null` for a hold that expired).

Nobody reviews holds in an experiment. A run that asks anyway ends as failed with `asked_for_human`, and the item counts as "asked for a human". Use the recorded decisions or skip the human step, and check `context.eval` to keep side effects (payments, emails) out of experiment runs. Memory in an experiment run is a temporary, empty one that is deleted when the item's run ends.

## Next steps

> **[Calling models](https://docs.upsonic.ai/agentos/build/models.md)**
>
> Use the environment's models through the gateway.

> **[Human approvals](https://docs.upsonic.ai/agentos/build/human-approvals.md)**
>
> Ask a person and continue with their decision.

> **[SDKs](https://docs.upsonic.ai/agentos/build/sdks.md)**
>
> Let the SDK speak the contract for you.

> **[Conformance kit](https://docs.upsonic.ai/agentos/build/conformance.md)**
>
> Check an agent in any language against the contract.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.