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

# Learning

> How an agent learns. The observe call, file upload, how to follow the learning, and every error with what to do.

This page explains how an agent learns. After you read it, you can send observations, upload a document, follow the learning until it is done, and handle every error.

An agent learns from **observations**. An observation is one thing that happened: a message, a note, or a piece of a document. You send observations to a thread. The agent turns them into memories after it has answered your request.

## One observation

The example below is one observation.

```json theme={null}
{
  "content": [{"type": "text", "text": "The customer runs version 4.2 on Ubuntu 22.04."}],
  "timestamp": "2026-09-17T10:06:00Z",
  "speaker": "Priya",
  "tags": ["email", "support"]
}
```

The table below lists every field.

| Field | Required | Meaning |
| - | - | - |
| `content` | yes | 1 to 20 content blocks. Today every block is `{"type": "text", "text": "..."}`. Each text can be up to 100,000 characters. |
| `timestamp` | no | When the thing happened. The agent uses it as the time of the memory. If you leave it out, the agent uses the time of the request. |
| `speaker` | no | **Who wrote or said this**, for example a person's name. Up to 200 characters. Send it whenever you know it. Use a readable name, not an id: the agent reads it when it reasons about the conversation, and answers "who said..." questions from it. In a shared thread, it is what keeps each person's messages apart. |
| `tags` | no | Free-form labels for the source, channel, or topic, for example `email` or `whatsapp`. With `speaker`, the agent keeps every tag with the message and sees them with it. Up to 20 entries, 1 to 64 characters each. |
| `source_ids` | no | Ids of uploaded files this observation came from. Memories keep these ids, so an answer can point to the file. |
| `img_url` | no | One image for this observation. See the next section. |
| `blip_caption` | no | Your own description of that image. See the next section. |

Send a `timestamp` when you send old messages. Without it, every message looks like it happened now.

<Note>
  **Clients written before `speaker` existed.** Without `speaker`, Priors reads the first tag as the speaker, does not keep the tags, and records `user` when there are no tags either. This still works, but send `speaker` in new code: it keeps your tags as labels.
</Note>

### Images in an observation

The observe calls do not accept image bytes. There are two ways to include an image:

* `img_url`: a URL that the Priors servers can download. Priors downloads the image, writes a description of it, and learns the description together with your text. The URL can be up to 2,048 characters. The download must finish within 10 seconds. The image must be JPEG, PNG, GIF, or WebP, and at most 10 MB. A URL on `localhost`, on a private network, or behind a login cannot be downloaded.
* `blip_caption`: your own description of the image. The agent learns it together with the downloaded description, or instead of it when the download fails.

Priors does not keep the image bytes on these calls. If you want the image kept as a file, attach it to a chat message instead; see [Sources](/docs/concepts/sources).

If the download fails, the observation is still learned from your text and caption, and the status does not report the failure. Always send a `blip_caption` when you have one.

## The `observe` call: learn only

Use `observe` when you want the agent to remember something and you do not need an answer. One call can carry up to 100 observations.

```bash theme={null}
curl -X POST https://app.sapientpriors.com/api/v1/agents/agent_01J.../threads/thread_01J.../observe \
  -H "Authorization: Bearer $SP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "observations": [
      {"content": [{"type": "text", "text": "The customer runs version 4.2 on Ubuntu 22.04."}], "timestamp": "2026-09-17T10:06:00Z"},
      {"content": [{"type": "text", "text": "Their last outage was caused by a full disk."}], "timestamp": "2026-09-17T10:07:00Z"}
    ]
  }'
```

### The success response

The status is `202`. Nothing has been learned yet. Save the `observation_id`.

```json theme={null}
{
  "agent_id": "agent_01J...",
  "thread_id": "thread_01J...",
  "observation_id": "obs_01J9X6M8N0P2Q4R6S8T0V2W4X6",
  "accepted": true
}
```

### Errors

The first table lists errors where your request is wrong. Correct the request before you send it again.

| Status | `code` | Cause | What to do |
| - | - | - | - |
| `400` or `422` | `invalid_request` | `observations` is empty or has more than 100 items, a block is not text, a text is empty or longer than 100,000 characters, or `img_url` is longer than 2,048 characters. | Correct the request body. |
| `402` | `payment_required` | Your organization has no credits. | Add credits. |
| `404` | `thread_not_found` | The thread or the agent does not exist, or you cannot open it. | Check the ids. |

The second table lists errors where your request is correct but Priors cannot accept it now. Wait, then send the same request again.

| Status | `code` | Cause | What to do |
| - | - | - | - |
| `409` | `agent_not_ready` | The agent is still `initializing`, or it `failed`. | Poll the agent until its status is `ready`. |
| `429` | `rate_limit_exceeded` | More than 60 calls in one minute, or this agent is still learning an earlier observation request, in any of its threads (`Retry-After: 1`). An agent learns one request at a time, until it is `committed`. | Poll the earlier request until `committed`, then send this one. Send calls one after another, not at the same time. |
| `503` | `service_unavailable` | Priors could not store the observations, or the agent is restarting. Nothing was accepted. | Wait a few seconds, then send the request again. |

## Follow the learning

Learning finishes after the `202`. To know when it is done, poll the status: call `GET .../observe/{observation_id}/status` every one or two seconds until `status` is `committed`.

```bash theme={null}
curl https://app.sapientpriors.com/api/v1/agents/agent_01J.../threads/thread_01J.../observe/obs_01J.../status \
  -H "Authorization: Bearer $SP_API_KEY"
```

```json theme={null}
{
  "observation_id": "obs_01J...",
  "status": "processing",
  "turns_total": 2,
  "turns_processed": 1,
  "queue_depth": 0
}
```

The table below lists every status value.

| `status` | Meaning | What to do |
| - | - | - |
| `queued` | Earlier observations in this thread are still being learned. `queue_depth` says how many are ahead. | Poll again. |
| `converting` | A document is being converted and split. | Poll again. |
| `processing` | Memories are being written. | Poll again. |
| `committed` | Done. The memories are in the agent. | Ask the agent. |
| `failed` | Learning stopped. The `error` field says why, and `learning` says which pieces were not learned. | Read `error`. Correct the input and send the pieces in `learning` again. |
| `cancelled` | You cancelled it. | Send it again if you still need it. |
| `interrupted` | A Priors restart paused the learning. It resumes by itself where it stopped, and nothing is charged again. | Keep polling. Nothing to do. |

`committed`, `failed`, and `cancelled` are final. The other values change.

`turns_processed` counts the pieces that have been read. The memory for the last piece is written a moment after the read. So `turns_processed` can equal `turns_total` before the status is `committed`. Wait for `committed`.

Three more fields can appear on the status:

| Field | Meaning |
| - | - |
| `queued` | `true` while an uploaded file waits its turn behind another file this agent is reading. The status is `converting` meanwhile. It starts by itself. |
| `resumed_at` | When a Priors restart paused this request and it resumed where it stopped. Null when it ran without a break. Show "resumed after an update, nothing to do", not an error. |
| `learning` | Set when a `failed` or `cancelled` job has settled. `not_learned_turns` lists the pieces that were not learned, as ranges (`{"first": 3, "last": 5}`, where piece 1 is the first observation you sent). `refunded_credits` says what was refunded for them. `message` is a sentence to show a person. They are never learned later: send them again. |

## Cancel an observation

`POST .../observe/{observation_id}/cancel` stops learning that has not finished. The response is `200` with the status after the cancel. Pieces that were already learned stay learned. If the learning had already finished, the call returns the final status and changes nothing.

## The `observe-file` call: learn from a document

Upload a file, and the agent reads it for you. Priors converts the file to text, splits the text into pieces, and learns each piece as one observation.

```bash theme={null}
curl -X POST https://app.sapientpriors.com/api/v1/agents/agent_01J.../threads/thread_01J.../observe-file \
  -H "Authorization: Bearer $SP_API_KEY" \
  -F "file=@acme-runbook.pdf" \
  -F "strategy=structure"
```

The request is `multipart/form-data`. The table below lists every field.

| Field | Required | Meaning |
| - | - | - |
| `allow_duplicate` | no | `true` reads a file this agent already read in another thread. The default is `false`, which refuses it with `409 document_already_ingested`. |
| `file` | yes | One file. PDF, Word (`.doc`, `.docx`), PowerPoint (`.ppt`, `.pptx`, `.pps`, `.ppsx`), Excel (`.xls`, `.xlsx`, `.xlsb`), OpenDocument (`.odt`, `.ods`, `.odp`), CSV, RTF, EPUB, plain text and source files (Markdown, JSON, YAML, HTML, and the common code extensions), or an image (PNG, JPEG, GIF, WebP). An image is read as a one-page document. Priors reads the format from the bytes, not from the file name. The name is used only for formats with no signature, such as CSV and plain text. SVG is not accepted. Up to 10 MB. |
| `strategy` | no | How to split the text: `auto` (the default), `length`, `sentence`, `code`, or `structure` (by headings). |
| `max_chars` | no | The largest piece, from 200 to 4,000 characters. The default is 2,000. |
| `overlap_chars` | no | How many characters of the previous piece are repeated at the start of the next piece, so that a fact on the boundary is not lost. The default is 200. It must be less than half of `max_chars`. |

One call carries one file. To upload several files, send several calls, one after another.

### The success response

The status is `202`. The file has been converted and split. Learning starts now.

```json theme={null}
{
  "agent_id": "agent_01J...",
  "thread_id": "thread_01J...",
  "observation_id": "obs_01J...",
  "accepted": true,
  "turns_total": 38,
  "deferred": false,
  "figures": 4,
  "figures_dropped": 0,
  "words_total": 9120,
  "duplicate": false,
  "filename": "acme-runbook.pdf",
  "doc_format": "pdf",
  "source_id": "src_01J..."
}
```

| Field | Meaning |
| - | - |
| `turns_total` | How many pieces the file became. |
| `deferred` | `true` when the file is a scan with more pages than Priors can read inside one request. The file was accepted, and the conversion continues in the background. Poll the status. |
| `figures` | How many pictures inside the document were kept. Each is described in the piece where it appeared. |
| `duplicate` | `true` when this thread already has a file with exactly the same bytes. Nothing was read again, and nothing was charged. The same bytes in another thread of this agent return `409 document_already_ingested` instead. |
| `document_state` | Present only when `duplicate` is `true`. `completed` means the earlier upload finished, and this call reused it. `ingesting` means the earlier upload is still running, and this call joined it. Poll the returned `observation_id`. |
| `source_id` | The id of the stored file. Answers that use this file point to it with this id. |
| `queued` | `true` when this agent was reading another file. This file is accepted and waits its turn, then starts by itself. Do not send it again. |

Then poll the status in the same way as for `observe`.

An agent reads one file at a time. A second file sent while it reads one is not refused: it is accepted with `queued: true` and starts when the first one finishes.

### Upload the same file again safely

Send an `Idempotency-Key` header with a value that is unique for this upload. If you send the same key and the same file again, Priors returns the original `202` and does not read or charge the file again. If you send the same key with a different file, Priors returns `409`. Priors keeps a key for 24 hours.

### Errors

Priors converts and splits the file before it returns `202`. So every problem with the file itself is an error on this call, not a `failed` status later.

The first table lists errors where the file is the problem. Correct the file, then upload it again.

| Status | `code` | Cause | What to do |
| - | - | - | - |
| `400` | `unsupported_document` | Priors does not convert this file type. | Convert the file to one of the listed formats. |
| `400` | `document_needs_ocr` | The file type is fine, but the pages contain no text (a scan), and text recognition is not available for it. | Send a version that contains text. |
| `400` | `encrypted_document` | The file has a password. | Remove the password. |
| `400` | `malformed_document` | The file is cut off, damaged, or missing a part. | Export the file again. |
| `400` | `empty_document` | The file converted to no text at all. | Check the file. |
| `400` | `ingestion_failed` | The conversion failed for a reason that no other code names. | Read `error`. Export the file again. |
| `413` | `document_too_large` | The file is larger than 10 MB. | Split the file. |
| `413` | `document_too_complex` | The file is under 10 MB, but processing it would pass a limit: the converter's resource limit (a file that expands far beyond its size on disk), a scan with more pages than text recognition accepts, or a scan too large to send for recognition. `error` says which one. | Split the file, or send a version that contains text instead of a scan. |
| `413` | `document_too_many_turns` | The file splits into more than 2,000 pieces. | Split the file, or raise `max_chars`. |
| `413` | `document_too_many_pages`, `document_too_large_for_ocr` | The scan has more pages, or is larger, than text recognition accepts. | Split the file. |

Two `409` codes are neither of those, and waiting does not clear them:

* `document_already_ingested` means this agent already read these exact bytes in another thread. One memory index serves every thread, so what that read learned is already searchable from here. To read the file again anyway, send `allow_duplicate=true`. That stores a second copy of every memory, which costs more and puts duplicates in competition during retrieval, so send it only when you mean it.
* `document_uploaded_elsewhere` means an upload of this file in another thread stopped partway or failed. Finish it there with [`finish-learning`](/docs/concepts/sources#finish-an-upload-that-stopped), which learns and charges only what is left. Or send `allow_duplicate=true` to read the whole file again here, charged in full.

The second table lists errors where the file is fine but Priors cannot accept it now. Wait, then upload the same file again.

| Status | `code` | Cause | What to do |
| - | - | - | - |
| `409` | `duplicate_upload_in_progress` | This agent is already learning **this** file, or has it queued in another thread. | Do nothing. What it learns is searchable from every thread of the agent. |
| `409` | `conflict` | Your `Idempotency-Key` belongs to an upload that is still running, or was used for a different file. | Wait for the first upload, or use a new key. |
| `429` | `rate_limit_exceeded` | More than one upload in 10 seconds, or three uploads already running in your organization. | Wait the number of seconds in `Retry-After`. |
| `502` | `document_service_unavailable` | The document converter did not answer. | Wait, then try again. |
| `502` | `ocr_failed` | Text recognition failed on a scan. | Wait, then try again. If it fails again, send a version that contains text. |
| `502` | `document_pages_unexpected` | Text recognition returned pages that do not belong to this file. Nothing was stored. | Wait, then try again. |
| `503` | `ocr_busy` | Text recognition is busy with other uploads. | Try again in a few minutes. |

### After the `202`

Learning can still stop after the file was accepted. The status then reads `failed`, and `error` says why in one sentence. Pieces that were already learned stay learned, and Priors keeps a copy of the file.

To finish it, call [`finish-learning`](/docs/concepts/sources#finish-an-upload-that-stopped) on its `source_id`. You do not send the file again, and only the pieces that were not learned are read and charged. Uploading the same file again to the same thread also resumes it.

A restart of Priors does not stop an upload. The status reads `interrupted`, then the upload resumes by itself.

## What to send

* Send facts, decisions, and events. Send them soon after they happen, with a `timestamp`.
* Send each document once. Priors detects a repeat upload of the same bytes and skips it.
* Do not send secrets. Everything you send can become a memory that the agent repeats in an answer.


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