# document

> Convert Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CSV and PDF documents to markdown on this machine, detect the format from the bytes, pull out the images embedded in them, and transcribe a scan by rendering its pages and reading them with a vision model.

| field | value |
|-------|-------|
| version | 0.1.5 |
| type | binary |
| license | Apache-2.0 |
| repo | https://github.com/iii-hq/workers |
| supported_targets | x86_64-apple-darwin, aarch64-apple-darwin, i686-pc-windows-msvc, x86_64-pc-windows-msvc, aarch64-pc-windows-msvc, x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, x86_64-unknown-linux-musl, armv7-unknown-linux-gnueabihf |
| author | iii |

## installation

```sh
iii trigger compose::add worker=document@0.1.5
```

## dependencies

- `configuration` @ `0.x`

## readme

# document

Read office documents on the machine, with no conversion service and no API
key. This worker takes a Word, PowerPoint, Excel, OpenDocument, RTF, EPUB or CSV
file and returns markdown that keeps its headings, lists, tables and notes, in
single-digit milliseconds for a typical document. It identifies a file from its
bytes rather than trusting its name, so a mislabelled attachment still converts.
And it hands back the images markdown cannot carry, which is what a deck of
diagrams actually holds. Nothing is uploaded, and a long document is capped
rather than dumped, so a report does not swallow the context an agent needed for
the answer.

## Install

```bash
iii trigger compose::add worker=document
```

Reading a scanned document also needs something to turn its pages into pixels
and something to read them, neither of which ships here:

```bash
iii trigger compose::add worker=browser
```

With [browser](https://github.com/iii-hq/workers/tree/main/browser) installed and a vision model configured through
[llm-router](https://github.com/iii-hq/workers/tree/main/llm-router), `document::ocr` transcribes scans. Every other
function works without both.

## Quickstart

```rust
use iii_sdk::{register_worker, InitOptions};
use iii_sdk::protocol::TriggerRequest;
use serde_json::json;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let iii = register_worker("ws://localhost:49134", InitOptions::default());

    let markdown = iii.trigger(TriggerRequest {
        function_id: "document::to-markdown".into(),
        payload: json!({ "path": "/tmp/quarterly.docx" }),
        action: None,
        timeout_ms: Some(60_000),
    }).await?;
    // { "format": "docx", "family": "prose", "detected_from": "content",
    //   "body": { "text": "# Quarterly Notes\n…", "chars": 5693,
    //             "total_chars": 5693, "truncated": false },
    //   "asset_count": 0, "elapsed_ms": 4, … }

    println!("{markdown:#?}");
    Ok(())
}
```

A document with no path goes in as `bytes_base64` instead — the shape a composer
attachment takes. Add `file_name` with it: a CSV carries no signature of its
own, and without a name it cannot be recognised.

## Formats

| Format | Extensions |
|---|---|
| Word | `.doc`, `.docx`, `.docm` |
| PowerPoint | `.ppt`, `.pps`, `.pot`, `.pptx`, `.pptm`, `.ppsx`, `.ppsm` |
| Excel | `.xls`, `.xlsx`, `.xlsm`, `.xlsb` |
| OpenDocument | `.odt`, `.ods`, `.odp` |
| Rich Text | `.rtf` |
| EPUB | `.epub` |
| CSV | `.csv` |
| PDF | `.pdf` (text-based; see below) |

Container variants collapse onto one name: `.docm` is `docx`, `.xlsb` is
`excel`. A caller matches on the format, never on the extension it happened to
send.

### PDFs

A text-based PDF converts here, which makes this worker a complete answer for a
mixed pile of attachments on its own. When the [`pdf`](https://github.com/iii-hq/workers/tree/main/pdf) worker is
installed it is the better route for them: it classifies scanned versus
text-based and names the individual pages that need OCR, where this worker can
only convert or fail.

## Detect before you convert

`document::detect` reads the signature in the first bytes of a file and answers
in microseconds. It exists for the case where something arrives and nobody knows
what it is.

```json
{
  "format": "pptx",
  "family": "presentation",
  "detected_from": "content",
  "convertible": true,
  "has_assets": true,
  "size_bytes": 184320,
  "source": "roadmap.pptx",
  "elapsed_ms": 0
}
```

`detected_from` is the field worth reading. `content` means the bytes named the
format, which is the strong answer. `extension` means they did not, and only the
file name suggested it — expected for a CSV, and a reason for suspicion on
anything else. A `format` of `null` is an answer too: this is not a document
this worker reads, not a document that is broken.

## The images markdown drops

Markdown renders an embedded image as its alt text. For prose that is right. For
a deck built out of diagrams it throws away the content and leaves a page of
titles, which reads as a document that had little to say.

`document::to-markdown` reports `asset_count` so that case is visible, and
`document::extract-assets` returns the bytes:

```json
{
  "format": "pptx",
  "assets": [
    {
      "index": 0,
      "media_type": "image/png",
      "origin_part": "ppt/media/image1.png",
      "size_bytes": 48211,
      "bytes_base64": "iVBORw0KGgo…"
    }
  ],
  "total_count": 1,
  "truncated": false
}
```

Three ceilings apply, and all of them report what they dropped rather than
trimming silently. `max_assets` bounds how many come back. `max_asset_bytes`
bounds one payload, and `max_assets_total_bytes` bounds the response as a whole,
because two dozen assets each just under the per-asset limit still add up to a
quarter of a gigabyte once base64 inflates them. An asset left out either way is
still listed with its type and size, with `omitted` saying which ceiling it hit
(`too_large` or `budget_spent`), so a caller can ask for it on its own.
`include_bytes: false` inventories a document without moving anything.

## Reading a scan

A scanned page holds no text to extract: the characters exist only in the
pixels. `document::ocr` renders those pages and reads them with a vision model.

```json
{
  "via": "pdf-render",
  "body": { "text": "INVOICE 4471\nDue 30 June…", "chars": 812, "truncated": false },
  "pages": [{ "page": 1, "text": "INVOICE 4471…", "chars": 812, "cached": false }],
  "pages_transcribed": 1,
  "pages_cached": 0,
  "model": "claude-haiku-4-5"
}
```

Three inputs, one answer. An image goes straight to the model. A PDF is
rendered a page at a time by the [`browser`](https://github.com/iii-hq/workers/tree/main/browser) worker, which is the
only thing that turns a page into pixels. An office document whose text came
back empty has its embedded images pulled out and read the same way.

Both of those dependencies are soft. Neither is declared in
`worker-compose.yaml`, every other function works without them, and a call that
needs one it cannot reach says which to install. Someone who installed this
worker to read a `.docx` never pays for Chromium.

This is the one function here that costs money, so nothing runs it implicitly.
`pdf::classify` reports which pages are scans, and passing that list is the
difference between transcribing one page of a report and all four hundred:

```json
{ "path": "/tmp/report.pdf", "pages": [1], "model": "claude-haiku-4-5" }
```

The model is checked for vision support before anything is rendered, because a
model that cannot see fails on the first page after the render has been paid
for.

Page transcriptions cache in the `state` worker, keyed by the rendered PIXELS
and the model that read them. Keying on the image rather than the source
document is what makes the cache self-correcting: a page that rendered badly
hashes differently once the render is fixed, so bad entries fall out instead of
being served forever. A hit still re-renders — that is a second of local
Chromium — and skips the model call, which is the part that costs money. The
images themselves are never stored, and exist only in flight between the browser
and the model.

For scale: one rendered page of a text PDF measured about 1,400 input tokens on
`claude-haiku-4-5`, or roughly $0.0016 a page.

Rendering a PDF needs the file on disk (`path`, not `bytes_base64`) and the
`browser` worker allowed to open it: its Behavior settings carry an allowed
URL schemes list that ships as `http, https`, and a local PDF needs `file`
added. It hot-applies on save. That list is deliberately narrow — the browser
does not check a path against the session's filesystem scope the way this
worker does, so widening it widens what any caller can read.

## Response caps

Every text-bearing response is capped and says so. `truncated: true` with a
`total_chars` far above `chars` means you are holding a fragment. `max_chars: 0`
takes the whole document, and belongs in a pipeline moving a document to
storage rather than in a call whose result lands in a conversation.

## Configuration

Configuration lives in the `configuration` worker under the id `document` and
every field hot-reloads. Nothing here needs a restart.

```yaml
max_input_bytes: 67108864   # largest document accepted, before parsing
max_chars: 40000            # default cap on returned markdown
preview_chars: 600          # leading characters shown alongside a capped body
max_assets: 24              # assets returned in one response
max_asset_bytes: 8388608    # largest single asset returned with its bytes
max_assets_total_bytes: 33554432  # total asset payload one response may carry
ocr_model:                  # vision model document::ocr reads with; unset = every call chooses
max_ocr_pages: 20           # pages one document::ocr call transcribes
ocr_timeout_ms: 120000      # budget for one render or one model read
ocr_render_settle_ms: 2000  # let a rendered page paint before capturing it
ocr_cache: true             # cache page transcriptions in the state worker
```

A per-call `max_assets` narrows this ceiling and cannot raise it: the limit
bounds one response, and a caller asking for a thousand images is the case it
exists for.

Defaults live in [`src/config.rs`](src/config.rs).

## Called on demand

This worker registers no harness hook and injects nothing into any prompt. A
conversation that never touches a document never pays for it, and there is no
per-turn cost to having it installed. An agent finds it the ordinary way,
through the function registry and [`skills/SKILL.md`](skills/SKILL.md).

## What this worker does not do

It does not run OCR by itself. `document::ocr` renders and asks a model, which
means a scan costs money per page and needs a vision model configured. Nothing
transcribes implicitly.

It does not write documents. Conversion is one way, into markdown.

It cannot open an encrypted document. There is no password parameter, because
there is nothing behind it that could decrypt one.

## api reference

```json
{
  "functions": [
    {
      "description": "Identify a document's format from its bytes (falling back to the file name for CSV, which carries no signature), and report which family it belongs to and whether this worker can convert it. Microseconds, and no conversion.",
      "metadata": {},
      "name": "document::detect",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "FsScope": {
            "description": "The filesystem jail a call runs under.\n\nThe harness stamps this onto every function it dispatches, so a `path` an agent supplies has to be checked against it. Without the check these functions would read any document on the machine and hand back its text, which is a way around the scope the session was granted. Mirrors the shape the shell and pdf workers take.",
            "properties": {
              "grants": {
                "default": [],
                "description": "Additional directories or files explicitly granted to this session.",
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "root": {
                "description": "The session's working directory.",
                "type": "string"
              }
            },
            "required": [
              "root"
            ],
            "type": "object"
          }
        },
        "description": "Where the document comes from. Exactly one of `path` and `bytes_base64` must be set.",
        "properties": {
          "bytes_base64": {
            "default": null,
            "description": "Base64-encoded document bytes, for a document with no path — an attachment held in memory. Mutually exclusive with `path`.",
            "type": [
              "string",
              "null"
            ]
          },
          "file_name": {
            "default": null,
            "description": "Original file name for inline bytes, used only to recognise a format the content cannot name. A `.csv` needs this; nothing else does. Ignored when `path` is set, which carries its own name.",
            "type": [
              "string",
              "null"
            ]
          },
          "fs_scope": {
            "anyOf": [
              {
                "$ref": "#/definitions/FsScope"
              },
              {
                "type": "null"
              }
            ],
            "description": "The filesystem jail this call runs under. Stamped by the harness on an agent's call; absent on an operator or console call, which is already user-initiated and not subject to the agent's scope."
          },
          "path": {
            "default": null,
            "description": "Filesystem path to the document. Mutually exclusive with `bytes_base64`.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "Request",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "DetectedFrom": {
            "description": "How the format was arrived at, weakest claim last.",
            "oneOf": [
              {
                "description": "The caller named it, and the bytes were not consulted.",
                "enum": [
                  "requested"
                ],
                "type": "string"
              },
              {
                "description": "The signature the format's specification designates (PDF header, RTF open group, OLE stream names, ZIP package mimetype).",
                "enum": [
                  "content"
                ],
                "type": "string"
              },
              {
                "description": "The file extension only. CSV carries no signature, so this is the only way it is ever recognised; for any other format it means the content did not match anything known.",
                "enum": [
                  "extension"
                ],
                "type": "string"
              }
            ]
          },
          "Family": {
            "description": "What the document is, rather than which program wrote it.\n\nA caller routing a mixed bag of attachments cares that a file is a spreadsheet, not that it is `.ods` rather than `.xlsx`.",
            "oneOf": [
              {
                "description": "Prose: Word, OpenDocument Text, RTF.",
                "enum": [
                  "prose"
                ],
                "type": "string"
              },
              {
                "description": "Rows and columns: Excel, OpenDocument Spreadsheet, CSV.",
                "enum": [
                  "spreadsheet"
                ],
                "type": "string"
              },
              {
                "description": "Slides: PowerPoint, OpenDocument Presentation.",
                "enum": [
                  "presentation"
                ],
                "type": "string"
              },
              {
                "description": "A book: EPUB.",
                "enum": [
                  "book"
                ],
                "type": "string"
              },
              {
                "description": "PDF, which is its own family because it is the one format with a dedicated worker and a page-level OCR decision.",
                "enum": [
                  "pdf"
                ],
                "type": "string"
              }
            ]
          },
          "Format": {
            "description": "A format this worker converts. The names are the wire vocabulary: stable, lowercase, and independent of the file extension that named them (`.docm` is `docx`, `.xlsb` is `excel`).",
            "oneOf": [
              {
                "description": "Binary Word 97-2003 (`.doc`).",
                "enum": [
                  "doc"
                ],
                "type": "string"
              },
              {
                "description": "WordprocessingML (`.docx`, `.docm`).",
                "enum": [
                  "docx"
                ],
                "type": "string"
              },
              {
                "description": "OpenDocument Text (`.odt`).",
                "enum": [
                  "odt"
                ],
                "type": "string"
              },
              {
                "description": "Rich Text Format (`.rtf`).",
                "enum": [
                  "rtf"
                ],
                "type": "string"
              },
              {
                "description": "Binary PowerPoint 97-2003 (`.ppt`, `.pps`, `.pot`).",
                "enum": [
                  "ppt"
                ],
                "type": "string"
              },
              {
                "description": "PresentationML (`.pptx`, `.pptm`, `.ppsx`, `.ppsm`).",
                "enum": [
                  "pptx"
                ],
                "type": "string"
              },
              {
                "description": "OpenDocument Presentation (`.odp`).",
                "enum": [
                  "odp"
                ],
                "type": "string"
              },
              {
                "description": "Excel workbooks in every container (`.xlsx`, `.xlsm`, `.xlsb`, `.xls`).",
                "enum": [
                  "excel"
                ],
                "type": "string"
              },
              {
                "description": "OpenDocument Spreadsheet (`.ods`).",
                "enum": [
                  "ods"
                ],
                "type": "string"
              },
              {
                "description": "Delimiter-separated text (`.csv`).",
                "enum": [
                  "csv"
                ],
                "type": "string"
              },
              {
                "description": "EPUB 2 and 3 (`.epub`).",
                "enum": [
                  "epub"
                ],
                "type": "string"
              },
              {
                "description": "Portable Document Format (`.pdf`).",
                "enum": [
                  "pdf"
                ],
                "type": "string"
              }
            ]
          }
        },
        "properties": {
          "convertible": {
            "description": "`true` when `document::to-markdown` can convert this file.",
            "type": "boolean"
          },
          "detected_from": {
            "anyOf": [
              {
                "$ref": "#/definitions/DetectedFrom"
              },
              {
                "type": "null"
              }
            ],
            "description": "How the format was arrived at. `extension` is the weaker claim: the content matched nothing known, and only the file name suggested this."
          },
          "elapsed_ms": {
            "description": "Wall-clock time for the detection.",
            "format": "uint64",
            "minimum": 0,
            "type": "integer"
          },
          "family": {
            "anyOf": [
              {
                "$ref": "#/definitions/Family"
              },
              {
                "type": "null"
              }
            ],
            "description": "What the document is: prose, a spreadsheet, a presentation, a book, a PDF. Absent when the format is unknown."
          },
          "format": {
            "anyOf": [
              {
                "$ref": "#/definitions/Format"
              },
              {
                "type": "null"
              }
            ],
            "description": "The format, or `null` when nothing recognised it. A null means the file is not one of the formats this worker reads — an image, an archive, a plain text file — not that it is broken."
          },
          "has_assets": {
            "description": "`true` when the format can carry embedded assets for `document::extract-assets` to pull out. False for a PDF, which converts straight to markdown without a document model, and for a CSV, which is rows of text with nowhere to put a picture. A caller routing on this should not spend a call to be told a spreadsheet has no images.",
            "type": "boolean"
          },
          "size_bytes": {
            "description": "Size of the document in bytes.",
            "format": "uint64",
            "minimum": 0,
            "type": "integer"
          },
          "source": {
            "description": "Source label: the file name, or `<inline>` for an in-memory document that arrived without one.",
            "type": "string"
          }
        },
        "required": [
          "convertible",
          "elapsed_ms",
          "has_assets",
          "size_bytes",
          "source"
        ],
        "title": "Response",
        "type": "object"
      }
    },
    {
      "description": "Pull the images and embedded objects out of a document as base64, for a deck or report whose content is pictures rather than text. Capped per response and per asset; anything left out is still listed with its type and size. Not available for PDFs — use pdf::extract-regions.",
      "metadata": {},
      "name": "document::extract-assets",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Format": {
            "description": "A format this worker converts. The names are the wire vocabulary: stable, lowercase, and independent of the file extension that named them (`.docm` is `docx`, `.xlsb` is `excel`).",
            "oneOf": [
              {
                "description": "Binary Word 97-2003 (`.doc`).",
                "enum": [
                  "doc"
                ],
                "type": "string"
              },
              {
                "description": "WordprocessingML (`.docx`, `.docm`).",
                "enum": [
                  "docx"
                ],
                "type": "string"
              },
              {
                "description": "OpenDocument Text (`.odt`).",
                "enum": [
                  "odt"
                ],
                "type": "string"
              },
              {
                "description": "Rich Text Format (`.rtf`).",
                "enum": [
                  "rtf"
                ],
                "type": "string"
              },
              {
                "description": "Binary PowerPoint 97-2003 (`.ppt`, `.pps`, `.pot`).",
                "enum": [
                  "ppt"
                ],
                "type": "string"
              },
              {
                "description": "PresentationML (`.pptx`, `.pptm`, `.ppsx`, `.ppsm`).",
                "enum": [
                  "pptx"
                ],
                "type": "string"
              },
              {
                "description": "OpenDocument Presentation (`.odp`).",
                "enum": [
                  "odp"
                ],
                "type": "string"
              },
              {
                "description": "Excel workbooks in every container (`.xlsx`, `.xlsm`, `.xlsb`, `.xls`).",
                "enum": [
                  "excel"
                ],
                "type": "string"
              },
              {
                "description": "OpenDocument Spreadsheet (`.ods`).",
                "enum": [
                  "ods"
                ],
                "type": "string"
              },
              {
                "description": "Delimiter-separated text (`.csv`).",
                "enum": [
                  "csv"
                ],
                "type": "string"
              },
              {
                "description": "EPUB 2 and 3 (`.epub`).",
                "enum": [
                  "epub"
                ],
                "type": "string"
              },
              {
                "description": "Portable Document Format (`.pdf`).",
                "enum": [
                  "pdf"
                ],
                "type": "string"
              }
            ]
          },
          "FsScope": {
            "description": "The filesystem jail a call runs under.\n\nThe harness stamps this onto every function it dispatches, so a `path` an agent supplies has to be checked against it. Without the check these functions would read any document on the machine and hand back its text, which is a way around the scope the session was granted. Mirrors the shape the shell and pdf workers take.",
            "properties": {
              "grants": {
                "default": [],
                "description": "Additional directories or files explicitly granted to this session.",
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "root": {
                "description": "The session's working directory.",
                "type": "string"
              }
            },
            "required": [
              "root"
            ],
            "type": "object"
          }
        },
        "description": "Where the document comes from. Exactly one of `path` and `bytes_base64` must be set.",
        "properties": {
          "bytes_base64": {
            "default": null,
            "description": "Base64-encoded document bytes, for a document with no path — an attachment held in memory. Mutually exclusive with `path`.",
            "type": [
              "string",
              "null"
            ]
          },
          "file_name": {
            "default": null,
            "description": "Original file name for inline bytes, used only to recognise a format the content cannot name. A `.csv` needs this; nothing else does. Ignored when `path` is set, which carries its own name.",
            "type": [
              "string",
              "null"
            ]
          },
          "format": {
            "anyOf": [
              {
                "$ref": "#/definitions/Format"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "Force a format instead of detecting one."
          },
          "fs_scope": {
            "anyOf": [
              {
                "$ref": "#/definitions/FsScope"
              },
              {
                "type": "null"
              }
            ],
            "description": "The filesystem jail this call runs under. Stamped by the harness on an agent's call; absent on an operator or console call, which is already user-initiated and not subject to the agent's scope."
          },
          "include_bytes": {
            "default": true,
            "description": "Include the base64 payload. Set `false` to inventory a document — what it holds and how big — without moving the bytes.",
            "type": "boolean"
          },
          "max_assets": {
            "default": null,
            "description": "Assets to return in this response. Narrows the configured ceiling; it cannot raise it.",
            "format": "uint",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          },
          "media_type_prefix": {
            "default": null,
            "description": "Return only assets whose media type starts with this, e.g. `image/`. Omit for every asset.",
            "type": [
              "string",
              "null"
            ]
          },
          "path": {
            "default": null,
            "description": "Filesystem path to the document. Mutually exclusive with `bytes_base64`.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "Request",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Asset": {
            "description": "One embedded asset. `bytes_base64` is absent when the caller asked for an inventory, or when this asset is over the per-asset ceiling — `omitted` says which.",
            "properties": {
              "bytes_base64": {
                "description": "The payload, base64-encoded.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "index": {
                "description": "Position in the document's asset list, stable for a given document.",
                "format": "uint",
                "minimum": 0,
                "type": "integer"
              },
              "media_type": {
                "description": "MIME type, e.g. `image/png`.",
                "type": "string"
              },
              "omitted": {
                "description": "Why the payload is absent, when it is: `not_requested`, `too_large` (this asset alone is over the per-asset ceiling), or `budget_spent` (the response's total byte budget went on earlier assets — ask for this one on its own).",
                "type": [
                  "string",
                  "null"
                ]
              },
              "origin_part": {
                "description": "The package part or stream it came from, for provenance.",
                "type": "string"
              },
              "size_bytes": {
                "description": "Size of the payload in bytes, whether or not the payload is included.",
                "format": "uint64",
                "minimum": 0,
                "type": "integer"
              }
            },
            "required": [
              "index",
              "media_type",
              "origin_part",
              "size_bytes"
            ],
            "type": "object"
          },
          "Format": {
            "description": "A format this worker converts. The names are the wire vocabulary: stable, lowercase, and independent of the file extension that named them (`.docm` is `docx`, `.xlsb` is `excel`).",
            "oneOf": [
              {
                "description": "Binary Word 97-2003 (`.doc`).",
                "enum": [
                  "doc"
                ],
                "type": "string"
              },
              {
                "description": "WordprocessingML (`.docx`, `.docm`).",
                "enum": [
                  "docx"
                ],
                "type": "string"
              },
              {
                "description": "OpenDocument Text (`.odt`).",
                "enum": [
                  "odt"
                ],
                "type": "string"
              },
              {
                "description": "Rich Text Format (`.rtf`).",
                "enum": [
                  "rtf"
                ],
                "type": "string"
              },
              {
                "description": "Binary PowerPoint 97-2003 (`.ppt`, `.pps`, `.pot`).",
                "enum": [
                  "ppt"
                ],
                "type": "string"
              },
              {
                "description": "PresentationML (`.pptx`, `.pptm`, `.ppsx`, `.ppsm`).",
                "enum": [
                  "pptx"
                ],
                "type": "string"
              },
              {
                "description": "OpenDocument Presentation (`.odp`).",
                "enum": [
                  "odp"
                ],
                "type": "string"
              },
              {
                "description": "Excel workbooks in every container (`.xlsx`, `.xlsm`, `.xlsb`, `.xls`).",
                "enum": [
                  "excel"
                ],
                "type": "string"
              },
              {
                "description": "OpenDocument Spreadsheet (`.ods`).",
                "enum": [
                  "ods"
                ],
                "type": "string"
              },
              {
                "description": "Delimiter-separated text (`.csv`).",
                "enum": [
                  "csv"
                ],
                "type": "string"
              },
              {
                "description": "EPUB 2 and 3 (`.epub`).",
                "enum": [
                  "epub"
                ],
                "type": "string"
              },
              {
                "description": "Portable Document Format (`.pdf`).",
                "enum": [
                  "pdf"
                ],
                "type": "string"
              }
            ]
          }
        },
        "properties": {
          "assets": {
            "description": "The assets, in document order, up to the effective ceiling.",
            "items": {
              "$ref": "#/definitions/Asset"
            },
            "type": "array"
          },
          "elapsed_ms": {
            "description": "Wall-clock time for the extraction.",
            "format": "uint64",
            "minimum": 0,
            "type": "integer"
          },
          "format": {
            "allOf": [
              {
                "$ref": "#/definitions/Format"
              }
            ],
            "description": "The format that was parsed."
          },
          "source": {
            "description": "Source label: the file name, or `<inline>` for an in-memory document.",
            "type": "string"
          },
          "total_count": {
            "description": "Assets the document holds after `media_type_prefix` is applied. Larger than `assets.len()` when the ceiling cut the response short.",
            "format": "uint",
            "minimum": 0,
            "type": "integer"
          },
          "truncated": {
            "description": "`true` when the ceiling cut the response short.",
            "type": "boolean"
          }
        },
        "required": [
          "assets",
          "elapsed_ms",
          "format",
          "source",
          "total_count",
          "truncated"
        ],
        "title": "Response",
        "type": "object"
      }
    },
    {
      "description": "Transcribe a document that holds no readable text: a scanned PDF, a photographed page, or a deck whose content is pictures. Renders the pages that need it and reads them with a vision model, so it costs money per page — pass `pages` (pdf::classify names them) to narrow it. Needs the browser worker for PDFs and a vision model through llm-router.",
      "metadata": {},
      "name": "document::ocr",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "FsScope": {
            "description": "The filesystem jail a call runs under.\n\nThe harness stamps this onto every function it dispatches, so a `path` an agent supplies has to be checked against it. Without the check these functions would read any document on the machine and hand back its text, which is a way around the scope the session was granted. Mirrors the shape the shell and pdf workers take.",
            "properties": {
              "grants": {
                "default": [],
                "description": "Additional directories or files explicitly granted to this session.",
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "root": {
                "description": "The session's working directory.",
                "type": "string"
              }
            },
            "required": [
              "root"
            ],
            "type": "object"
          }
        },
        "description": "Where the document comes from. Exactly one of `path` and `bytes_base64` must be set.",
        "properties": {
          "bytes_base64": {
            "default": null,
            "description": "Base64-encoded document bytes, for a document with no path — an attachment held in memory. Mutually exclusive with `path`.",
            "type": [
              "string",
              "null"
            ]
          },
          "file_name": {
            "default": null,
            "description": "Original file name for inline bytes, used only to recognise a format the content cannot name. A `.csv` needs this; nothing else does. Ignored when `path` is set, which carries its own name.",
            "type": [
              "string",
              "null"
            ]
          },
          "fs_scope": {
            "anyOf": [
              {
                "$ref": "#/definitions/FsScope"
              },
              {
                "type": "null"
              }
            ],
            "description": "The filesystem jail this call runs under. Stamped by the harness on an agent's call; absent on an operator or console call, which is already user-initiated and not subject to the agent's scope."
          },
          "max_chars": {
            "default": null,
            "description": "Characters to return before truncating. Omit for the configured default; `0` returns everything transcribed.",
            "format": "uint",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          },
          "model": {
            "default": null,
            "description": "Vision model to read with. Omit for the configured default. The model is checked for vision support before anything is rendered.",
            "type": [
              "string",
              "null"
            ]
          },
          "pages": {
            "default": null,
            "description": "1-indexed pages to transcribe, for a PDF. Omit for every page up to the configured ceiling. This is the cost control: `pdf::classify` reports which pages are scans, and passing that list keeps a long report from being read a page at a time when only its cover is an image.",
            "items": {
              "format": "uint32",
              "minimum": 0,
              "type": "integer"
            },
            "type": [
              "array",
              "null"
            ]
          },
          "path": {
            "default": null,
            "description": "Filesystem path to the document. Mutually exclusive with `bytes_base64`.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "Request",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Body": {
            "description": "A body that may have been shortened to fit one response, and the numbers a caller needs to decide what to do about it.\n\nThe cap is what keeps a long document from flooding a model's context. A caller that genuinely wants the whole thing asks for `max_chars: 0`, which is the shape a worker-to-worker pipeline uses to move a document without it passing through anyone's context. Same shape the pdf worker returns, so a caller handling both reads one field set.",
            "properties": {
              "chars": {
                "description": "Characters returned in `text`.",
                "format": "uint",
                "minimum": 0,
                "type": "integer"
              },
              "preview": {
                "description": "Leading characters of the content. Present only when the body was truncated, so a caller can see the shape of what it did not get without re-reading the start of `text`.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "text": {
                "description": "The markdown, shortened to the effective character cap.",
                "type": "string"
              },
              "total_chars": {
                "description": "Characters the document actually holds. Equal to `chars` when nothing was dropped.",
                "format": "uint",
                "minimum": 0,
                "type": "integer"
              },
              "truncated": {
                "description": "`true` when `text` stops short of the document. Ask again with `max_chars: 0` to take everything.",
                "type": "boolean"
              }
            },
            "required": [
              "chars",
              "text",
              "total_chars",
              "truncated"
            ],
            "type": "object"
          },
          "PageText": {
            "description": "What one page turned into.",
            "properties": {
              "cached": {
                "description": "`true` when this page came from the cache rather than the model.",
                "type": "boolean"
              },
              "chars": {
                "format": "uint",
                "minimum": 0,
                "type": "integer"
              },
              "page": {
                "description": "1-indexed page number, or the asset's index for an office document.",
                "format": "uint32",
                "minimum": 0,
                "type": "integer"
              },
              "text": {
                "description": "The transcription. Empty when the page held no legible text.",
                "type": "string"
              }
            },
            "required": [
              "cached",
              "chars",
              "page",
              "text"
            ],
            "type": "object"
          }
        },
        "properties": {
          "body": {
            "allOf": [
              {
                "$ref": "#/definitions/Body"
              }
            ],
            "description": "The joined transcription, capped per `max_chars`."
          },
          "elapsed_ms": {
            "description": "Wall-clock time, rendering included.",
            "format": "uint64",
            "minimum": 0,
            "type": "integer"
          },
          "model": {
            "description": "The model that read them.",
            "type": "string"
          },
          "pages": {
            "description": "Per-page transcriptions, in order.",
            "items": {
              "$ref": "#/definitions/PageText"
            },
            "type": "array"
          },
          "pages_cached": {
            "description": "Pages served from the cache, costing nothing.",
            "format": "uint",
            "minimum": 0,
            "type": "integer"
          },
          "pages_transcribed": {
            "description": "Pages actually read by the model this call. Excludes cache hits, so this is what was paid for.",
            "format": "uint",
            "minimum": 0,
            "type": "integer"
          },
          "source": {
            "description": "Source label: the file name, or `<inline>` for an in-memory document.",
            "type": "string"
          },
          "via": {
            "description": "How the pixels were obtained: `image`, `pdf-render` or `document-assets`.",
            "type": "string"
          }
        },
        "required": [
          "body",
          "elapsed_ms",
          "model",
          "pages",
          "pages_cached",
          "pages_transcribed",
          "source",
          "via"
        ],
        "title": "Response",
        "type": "object"
      }
    },
    {
      "description": "Internal: hot-reload the document worker from the authoritative configuration when it changes, swapping the per-call snapshot.",
      "metadata": {},
      "name": "document::on-config-change",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Payload of the internal config-change handler. The handler re-fetches the authoritative value, so this carries only the advisory id; a struct rather than a `Value` keeps the request schema concrete.",
        "properties": {
          "id": {
            "default": null,
            "description": "Configuration id that changed (advisory; the handler re-fetches).",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "OnConfigChangeEvent",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Ack returned by the internal config-change handler.",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "title": "OnConfigChangeResponse",
        "type": "object"
      }
    },
    {
      "description": "Convert a Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CSV or PDF document to markdown, preserving headings, lists, links and tables. The format is detected from the bytes. Responses are capped; pass max_chars 0 to take the whole document. For a PDF prefer pdf::classify first, which reports which pages need OCR.",
      "metadata": {},
      "name": "document::to-markdown",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Format": {
            "description": "A format this worker converts. The names are the wire vocabulary: stable, lowercase, and independent of the file extension that named them (`.docm` is `docx`, `.xlsb` is `excel`).",
            "oneOf": [
              {
                "description": "Binary Word 97-2003 (`.doc`).",
                "enum": [
                  "doc"
                ],
                "type": "string"
              },
              {
                "description": "WordprocessingML (`.docx`, `.docm`).",
                "enum": [
                  "docx"
                ],
                "type": "string"
              },
              {
                "description": "OpenDocument Text (`.odt`).",
                "enum": [
                  "odt"
                ],
                "type": "string"
              },
              {
                "description": "Rich Text Format (`.rtf`).",
                "enum": [
                  "rtf"
                ],
                "type": "string"
              },
              {
                "description": "Binary PowerPoint 97-2003 (`.ppt`, `.pps`, `.pot`).",
                "enum": [
                  "ppt"
                ],
                "type": "string"
              },
              {
                "description": "PresentationML (`.pptx`, `.pptm`, `.ppsx`, `.ppsm`).",
                "enum": [
                  "pptx"
                ],
                "type": "string"
              },
              {
                "description": "OpenDocument Presentation (`.odp`).",
                "enum": [
                  "odp"
                ],
                "type": "string"
              },
              {
                "description": "Excel workbooks in every container (`.xlsx`, `.xlsm`, `.xlsb`, `.xls`).",
                "enum": [
                  "excel"
                ],
                "type": "string"
              },
              {
                "description": "OpenDocument Spreadsheet (`.ods`).",
                "enum": [
                  "ods"
                ],
                "type": "string"
              },
              {
                "description": "Delimiter-separated text (`.csv`).",
                "enum": [
                  "csv"
                ],
                "type": "string"
              },
              {
                "description": "EPUB 2 and 3 (`.epub`).",
                "enum": [
                  "epub"
                ],
                "type": "string"
              },
              {
                "description": "Portable Document Format (`.pdf`).",
                "enum": [
                  "pdf"
                ],
                "type": "string"
              }
            ]
          },
          "FsScope": {
            "description": "The filesystem jail a call runs under.\n\nThe harness stamps this onto every function it dispatches, so a `path` an agent supplies has to be checked against it. Without the check these functions would read any document on the machine and hand back its text, which is a way around the scope the session was granted. Mirrors the shape the shell and pdf workers take.",
            "properties": {
              "grants": {
                "default": [],
                "description": "Additional directories or files explicitly granted to this session.",
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "root": {
                "description": "The session's working directory.",
                "type": "string"
              }
            },
            "required": [
              "root"
            ],
            "type": "object"
          }
        },
        "description": "Where the document comes from. Exactly one of `path` and `bytes_base64` must be set.",
        "properties": {
          "bytes_base64": {
            "default": null,
            "description": "Base64-encoded document bytes, for a document with no path — an attachment held in memory. Mutually exclusive with `path`.",
            "type": [
              "string",
              "null"
            ]
          },
          "file_name": {
            "default": null,
            "description": "Original file name for inline bytes, used only to recognise a format the content cannot name. A `.csv` needs this; nothing else does. Ignored when `path` is set, which carries its own name.",
            "type": [
              "string",
              "null"
            ]
          },
          "format": {
            "anyOf": [
              {
                "$ref": "#/definitions/Format"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "Force a format instead of detecting one. Only needed when the content carries no signature and the file name is absent or wrong."
          },
          "fs_scope": {
            "anyOf": [
              {
                "$ref": "#/definitions/FsScope"
              },
              {
                "type": "null"
              }
            ],
            "description": "The filesystem jail this call runs under. Stamped by the harness on an agent's call; absent on an operator or console call, which is already user-initiated and not subject to the agent's scope."
          },
          "max_chars": {
            "default": null,
            "description": "Characters to return before truncating. Omit for the configured default; `0` returns the whole document.",
            "format": "uint",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          },
          "path": {
            "default": null,
            "description": "Filesystem path to the document. Mutually exclusive with `bytes_base64`.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "Request",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Body": {
            "description": "A body that may have been shortened to fit one response, and the numbers a caller needs to decide what to do about it.\n\nThe cap is what keeps a long document from flooding a model's context. A caller that genuinely wants the whole thing asks for `max_chars: 0`, which is the shape a worker-to-worker pipeline uses to move a document without it passing through anyone's context. Same shape the pdf worker returns, so a caller handling both reads one field set.",
            "properties": {
              "chars": {
                "description": "Characters returned in `text`.",
                "format": "uint",
                "minimum": 0,
                "type": "integer"
              },
              "preview": {
                "description": "Leading characters of the content. Present only when the body was truncated, so a caller can see the shape of what it did not get without re-reading the start of `text`.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "text": {
                "description": "The markdown, shortened to the effective character cap.",
                "type": "string"
              },
              "total_chars": {
                "description": "Characters the document actually holds. Equal to `chars` when nothing was dropped.",
                "format": "uint",
                "minimum": 0,
                "type": "integer"
              },
              "truncated": {
                "description": "`true` when `text` stops short of the document. Ask again with `max_chars: 0` to take everything.",
                "type": "boolean"
              }
            },
            "required": [
              "chars",
              "text",
              "total_chars",
              "truncated"
            ],
            "type": "object"
          },
          "DetectedFrom": {
            "description": "How the format was arrived at, weakest claim last.",
            "oneOf": [
              {
                "description": "The caller named it, and the bytes were not consulted.",
                "enum": [
                  "requested"
                ],
                "type": "string"
              },
              {
                "description": "The signature the format's specification designates (PDF header, RTF open group, OLE stream names, ZIP package mimetype).",
                "enum": [
                  "content"
                ],
                "type": "string"
              },
              {
                "description": "The file extension only. CSV carries no signature, so this is the only way it is ever recognised; for any other format it means the content did not match anything known.",
                "enum": [
                  "extension"
                ],
                "type": "string"
              }
            ]
          },
          "Family": {
            "description": "What the document is, rather than which program wrote it.\n\nA caller routing a mixed bag of attachments cares that a file is a spreadsheet, not that it is `.ods` rather than `.xlsx`.",
            "oneOf": [
              {
                "description": "Prose: Word, OpenDocument Text, RTF.",
                "enum": [
                  "prose"
                ],
                "type": "string"
              },
              {
                "description": "Rows and columns: Excel, OpenDocument Spreadsheet, CSV.",
                "enum": [
                  "spreadsheet"
                ],
                "type": "string"
              },
              {
                "description": "Slides: PowerPoint, OpenDocument Presentation.",
                "enum": [
                  "presentation"
                ],
                "type": "string"
              },
              {
                "description": "A book: EPUB.",
                "enum": [
                  "book"
                ],
                "type": "string"
              },
              {
                "description": "PDF, which is its own family because it is the one format with a dedicated worker and a page-level OCR decision.",
                "enum": [
                  "pdf"
                ],
                "type": "string"
              }
            ]
          },
          "Format": {
            "description": "A format this worker converts. The names are the wire vocabulary: stable, lowercase, and independent of the file extension that named them (`.docm` is `docx`, `.xlsb` is `excel`).",
            "oneOf": [
              {
                "description": "Binary Word 97-2003 (`.doc`).",
                "enum": [
                  "doc"
                ],
                "type": "string"
              },
              {
                "description": "WordprocessingML (`.docx`, `.docm`).",
                "enum": [
                  "docx"
                ],
                "type": "string"
              },
              {
                "description": "OpenDocument Text (`.odt`).",
                "enum": [
                  "odt"
                ],
                "type": "string"
              },
              {
                "description": "Rich Text Format (`.rtf`).",
                "enum": [
                  "rtf"
                ],
                "type": "string"
              },
              {
                "description": "Binary PowerPoint 97-2003 (`.ppt`, `.pps`, `.pot`).",
                "enum": [
                  "ppt"
                ],
                "type": "string"
              },
              {
                "description": "PresentationML (`.pptx`, `.pptm`, `.ppsx`, `.ppsm`).",
                "enum": [
                  "pptx"
                ],
                "type": "string"
              },
              {
                "description": "OpenDocument Presentation (`.odp`).",
                "enum": [
                  "odp"
                ],
                "type": "string"
              },
              {
                "description": "Excel workbooks in every container (`.xlsx`, `.xlsm`, `.xlsb`, `.xls`).",
                "enum": [
                  "excel"
                ],
                "type": "string"
              },
              {
                "description": "OpenDocument Spreadsheet (`.ods`).",
                "enum": [
                  "ods"
                ],
                "type": "string"
              },
              {
                "description": "Delimiter-separated text (`.csv`).",
                "enum": [
                  "csv"
                ],
                "type": "string"
              },
              {
                "description": "EPUB 2 and 3 (`.epub`).",
                "enum": [
                  "epub"
                ],
                "type": "string"
              },
              {
                "description": "Portable Document Format (`.pdf`).",
                "enum": [
                  "pdf"
                ],
                "type": "string"
              }
            ]
          }
        },
        "properties": {
          "asset_count": {
            "description": "Embedded images and objects the document carries. Their bytes are not here — call `document::extract-assets` for those — but the count says whether a deck's content is pictures rather than text, which markdown alone would not reveal.",
            "format": "uint",
            "minimum": 0,
            "type": "integer"
          },
          "body": {
            "allOf": [
              {
                "$ref": "#/definitions/Body"
              }
            ],
            "description": "The markdown, capped per `max_chars`."
          },
          "detected_from": {
            "allOf": [
              {
                "$ref": "#/definitions/DetectedFrom"
              }
            ],
            "description": "How the format was arrived at."
          },
          "elapsed_ms": {
            "description": "Wall-clock time for the conversion.",
            "format": "uint64",
            "minimum": 0,
            "type": "integer"
          },
          "family": {
            "allOf": [
              {
                "$ref": "#/definitions/Family"
              }
            ],
            "description": "What the document is: prose, a spreadsheet, a presentation, a book, a PDF."
          },
          "format": {
            "allOf": [
              {
                "$ref": "#/definitions/Format"
              }
            ],
            "description": "The format that was converted."
          },
          "source": {
            "description": "Source label: the file name, or `<inline>` for an in-memory document.",
            "type": "string"
          }
        },
        "required": [
          "asset_count",
          "body",
          "detected_from",
          "elapsed_ms",
          "family",
          "format",
          "source"
        ],
        "title": "Response",
        "type": "object"
      }
    }
  ],
  "triggers": []
}
```
