# Learn

POST /v1/agents/learn. Turn recorded turns into Simulang code, or repair that code (research preview).

Turns what Sai did in one or more [turns](/documentation/api-reference/turns) into a Simulang function you own, `async function main(params = { ... })`, which your program then runs on a computer through [direct control](/documentation/api-reference/control). Given code you already have, the same call repairs it. [Learned code](/documentation/concepts/learned-code) walks through the whole loop.

Research preview: the request and response may change.

## Request

```
POST /v1/agents/learn
```

```json
{
  "turns": ["turn_vzjz62w7j7jvOilZ6zBL"],
  "template": "Open {{site}} and read the title of the top story"
}
```

| Field             | Type   | Required | Notes                                                                                                                                                      |
| ----------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `turns`           | array  | yes¹     | Turn ids from one session, in order, the first one stating the task. Each must have ended and been sent with `options.recordTrajectory: true`. At most 20. |
| `template`        | string | no       | The task with `{{name}}` placeholders; each one becomes a parameter. Default: the first turn's template, else one inferred from the first turn's message.  |
| `initialCode`     | string | no       | Code to repair. With it the call heals the code; without it, it converts `turns`. At most 200 KB.                                                          |
| `evaluations`     | array  | no       | How `initialCode` did on runs (below). Needs `initialCode`. At most 20.                                                                                    |
| `feedback`        | string | no       | Your note for the repair, e.g. "the Save dialog changed". Needs `initialCode`.                                                                             |
| `model`           | string | no       | An id from [`/models`](/documentation/api-reference/models). Default `anthropic/claude-opus-5-5`. An unknown id is a `400`.                                |
| `idempotency_key` | string | no       | The same key again returns the earlier result and runs nothing.                                                                                            |

¹ Optional when repairing: send `initialCode` with `evaluations`, `feedback` or new `turns`. New turns are the usual input: the code failed on a case, you sent the same template to [`POST /message`](/documentation/api-reference/message) with `recordTrajectory`, Sai solved it, and that turn shows what the code is missing.

An evaluation:

| Field       | Type    | Notes                                                                                                                                          |
| ----------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `inputs`    | object  | The values the code ran with.                                                                                                                  |
| `result`    | object  | The run's [exec result](/documentation/api-reference/control): its `result`, `error` and last `logs`. At most 20 KB; leave the screenshot out. |
| `judgement` | string  | A `judgementId` from [`POST /v1/agents/judge`](/documentation/api-reference/judge); the repair sees its verdict and evidence.                  |
| `passed`    | boolean | Your own verdict.                                                                                                                              |
| `note`      | string  | What went wrong, in your words.                                                                                                                |

The call does the work before it answers, about two minutes for a conversion. Set your client's timeout to at least 10 minutes. If your side times out, send the same request with the same `idempotency_key`, or read the result with `GET /v1/agents/learn/{learnId}`.

## Response

`200`:

```json
{
  "learnId": "learn_3Fq9cT1kZ0aJ",
  "mode": "convert",
  "status": "succeeded",
  "title": "Read the top story title",
  "template": "Open {{site}} and read the title of the top story",
  "parameters": [{ "name": "site", "defaultValue": "https://news.ycombinator.com" }],
  "code": "async function main(params = { site: \"https://news.ycombinator.com\" }) {\n  ...\n}",
  "validation": { "errors": [], "warnings": [] },
  "sourceTurns": ["turn_vzjz62w7j7jvOilZ6zBL"],
  "usage": { "inputTokens": 61240, "outputTokens": 3120 },
  "researchPreview": true
}
```

| Field          | Notes                                                                                                                                                                                                       |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mode`         | `convert` or `heal`.                                                                                                                                                                                        |
| `title`        | A short name for the code, a few words long, written by the model.                                                                                                                                          |
| `status`       | `succeeded`, or `failed` when `validation.errors` is not empty.                                                                                                                                             |
| `template`     | The template used or inferred. Send it to `POST /message` with `options.inputs` when the code fails and you want Sai to take over.                                                                          |
| `parameters`   | The keys of `main`'s `params`, with the values from the source turn as defaults.                                                                                                                            |
| `code`         | The function. It uses the bound `machine`, throws `needs_sign_in: <site or app>` when a sign-in is needed and `precondition_failed: ...` when it starts on the wrong screen, and returns the task's answer. |
| `outputSchema` | When the source turn had an `output_schema`: what `main` returns.                                                                                                                                           |
| `validation`   | Static checks on the code: `errors` (an unused or unknown parameter, code that does not parse, a call direct control does not allow, a ref id or coordinate copied from the trajectory) and `warnings`.     |
| `changes`      | For `heal`: what changed and why.                                                                                                                                                                           |
| `usage`        | Tokens of the model calls this request made, billed to your account.                                                                                                                                        |

A repeated `idempotency_key` returns the earlier result plus `"idempotent_replay": true`.

## Run the code

Run it in a [control lease](/documentation/api-reference/control) with the call to `main` as the block's last line; the block's result is what `main` returns. Pass every input: an argument to `main` replaces its whole default object, so merge your inputs over `parameters`' defaults.

```json
POST /v1/machines/{machineId}/control/exec
{ "code": "<code>\nawait main({\"site\": \"https://lobste.rs\"})", "timeoutMs": 300000 }
```

The SDKs do this for you: `lease.run(learned, inputs)`.

## Read a result later

```
GET /v1/agents/learn/{learnId}
```

Returns the same object. A call that stopped mid-way reads as `failed` with `error: "interrupted"`.

## Errors

| Status | Body                                                                                                               |
| ------ | ------------------------------------------------------------------------------------------------------------------ |
| `400`  | Validation failed: no `turns` without `initialCode`, `evaluations` without `initialCode`, an unknown key or model. |
| `400`  | `{ "error": "mixed_sessions" }`: the turns come from more than one session.                                        |
| `400`  | `{ "error": "template_mismatch" }`: the template does not match the first turn's message.                          |
| `400`  | `{ "error": "no_steps" }`: the turns have no agent steps to convert.                                               |
| `402`  | `{ "error": "model_refused", "message": "Out of credits. ..." }`                                                   |
| `403`  | `{ "error": "model_refused" }`: your plan does not allow the model; pass another `model`.                          |
| `404`  | `{ "error": "turn_not_found" }`, `{ "error": "judgement_not_found" }`, or `learn_not_found` on `GET`.              |
| `409`  | `{ "error": "turn_not_finished" }` or `{ "error": "trajectory_not_recorded" }`.                                    |
| `429`  | More than 30 learn calls in an hour, or the model is rate limited.                                                 |
| `502`  | `{ "error": "model_output_invalid" }` or `learn_failed`: try again.                                                |
