skip to content
$worker

pdf

v0.1.1-experimental

Read PDFs locally — classify text-based vs scanned, convert to markdown, extract positioned text and tables, and report which pages still need OCR.

Experimental

Published as experimental by its release pipeline. It installs and resolves like any other worker, but its interface may change without notice.

iiiverified
4 installs1 in 7d0 today
install
$iii worker add pdf@0.1.1-experimental
  • macOS: arm64 · x64
  • Linux: arm64 · armv7 · x64
  • Windows: arm64 · x64 · x86

exact versions are immutable; binary and bundle artifacts are digest-pinned.

agent-ready brief for v0.1.1-experimental
install + config + dependencies + readme + api reference, all in one place. fetch as agent-context.md for an llm to consume.
the same content rendered as discrete blocks below is exposed as a single markdown document at /workers/pdf.md?version=0.1.1-experimental. paste it into an llm prompt or pipe it through curl from a worker.

install

install
$iii worker add pdf@0.1.1-experimental

dependencies

dependencies1

readme

README.md

pdf

Read PDFs on the machine, with no OCR service and no API key. This worker classifies a document in about twenty milliseconds — is this real text, or a photograph of a page? — converts text-based documents to markdown that keeps their headings, lists, links and tables, and reports exactly which pages still need OCR and why. It also exposes the layout underneath: where every run of characters sits, and what the text is inside a given box on a page. 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.

It ships a console page too. Drop a PDF in and see exactly what the agent sees: the verdict, the per-page OCR decision, and the extracted markdown.

Install

iii worker add pdf

Quickstart

Classify first. It is cheap, and it decides whether anything else is worth doing: extraction on a scan returns nothing, and without the verdict an empty result is indistinguishable from an empty document.

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 verdict = iii.trigger(TriggerRequest {
        function_id: "pdf::classify".into(),
        payload: json!({ "path": "/tmp/report.pdf" }),
        action: None,
        timeout_ms: Some(30_000),
    }).await?;
    // { "document_type": "text_based", "confidence": 1.0, "page_count": 40,
    //   "pages_needing_ocr": [], "ocr_reasons": [], "elapsed_ms": 18, … }

    let markdown = iii.trigger(TriggerRequest {
        function_id: "pdf::to-markdown".into(),
        payload: json!({ "path": "/tmp/report.pdf", "pages": [1, 2, 3] }),
        action: None,
        timeout_ms: Some(60_000),
    }).await?;
    // { "body": { "text": "# Quarterly Report\n…", "chars": 5693,
    //             "total_chars": 5693, "truncated": false }, … }

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

A document with no path goes in as bytes_base64 instead. An encrypted one takes a password on pdf::classify and pdf::to-markdown.

Reading the verdict

document_type is text_based, scanned, image_based or mixed. The document-level answer is not the whole story: a two-hundred-page report with a scanned cover is not a scanned document, and treating it as one sends the whole thing to an OCR service for the sake of one page. pages_needing_ocr and ocr_reasons carry the per-page decision:

Reason What it means
scanned A raster page. It needs a vision model.
no_text Nothing extractable and nothing to OCR. Often a blank page.
vector_text Characters drawn as outlines rather than text. Unreadable as characters.
suspected_garbled_text A text layer that decodes to nonsense. Do not trust it, whatever the document type says.

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.

The cheap fix is pages, not a bigger cap: conversion cost scales with the document, so narrowing to the pages you need is faster as well as smaller. A four-hundred-page report takes tens of seconds to convert whole and milliseconds a page at a time.

max_chars: 0 lifts the cap entirely. That belongs in a pipeline moving a document to storage, not in a call whose result lands in a conversation.

Reading a box on a page

When a vision model has located a region and you want the real characters rather than its transcription:

{
  "path": "/tmp/invoice.pdf",
  "regions": [{ "page": 1, "boxes": [[320.0, 640.0, 560.0, 700.0]] }],
  "mode": "text"
}

mode: "table" runs table detection over the same box and returns a markdown table instead.

Two conventions worth knowing

Page numbers are 1-indexed everywhere on this surface, in requests and responses.

Coordinates are not uniform, and each response states which it used. pdf::extract-items reports PDF points from the bottom left, the PDF convention. pdf::extract-regions takes boxes in PDF points from the top left, which is what a layout model produces. Getting this wrong is silent: text comes back, just from the wrong end of the page.

Configuration

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

max_input_bytes: 268435456   # largest document accepted, before parsing
max_chars: 40000             # default cap on returned text or markdown
preview_chars: 600           # leading characters shown alongside a capped body
max_items: 5000              # default cap on positioned items in one response
classify_sample_pages: 8     # pages sampled to classify; 0 scans everything
min_text_ops_per_page: 3     # text operators before a page counts as text
text_page_ratio_threshold: 0.6  # share of text pages to call a document text-based

The three detection fields are the ones worth understanding. Sampling is what keeps classification at tens of milliseconds on a four-hundred-page file; it also means the verdict comes from part of the document, which is why every response reports pages_sampled. Raise classify_sample_pages, or set it to 0, when a borderline mixed document needs settling.

Defaults live in 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; a person finds it through the console page.

What this worker does not do

It does not rasterize pages, so it cannot OCR anything. Scanned and image-based documents get classified and routed, not read. Image content is reported as a placeholder with a real bounding box and no pixels.

Routed where, in practice: document::ocr renders those pages through the browser worker and reads them with a vision model. It costs money per page, which is exactly why pdf::classify exists — pass it the pages_needing_ocr named here rather than the whole document.

It is a parser, not a renderer: it walks the document's content streams and reconstructs the geometry, which is why it is fast and why it needs no service behind it.

api reference (json)

agent-api-reference.json
{
  "functions": [
    {
      "description": "Classify a PDF as text-based, scanned, image-based or mixed, and report which pages need OCR and why. Samples content streams rather than extracting text, so it answers in tens of milliseconds. Call this before any other pdf function.",
      "metadata": {},
      "name": "pdf::classify",
      "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 worker takes.",
            "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 PDF comes from. Exactly one of the two fields must be set.",
        "properties": {
          "bytes_base64": {
            "default": null,
            "description": "Base64-encoded PDF bytes, for a document with no path. Mutually exclusive with `path`.",
            "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."
          },
          "password": {
            "default": null,
            "description": "Password for an encrypted document. Never logged or echoed back.",
            "type": [
              "string",
              "null"
            ]
          },
          "path": {
            "default": null,
            "description": "Filesystem path to the PDF. Mutually exclusive with `bytes_base64`.",
            "type": [
              "string",
              "null"
            ]
          },
          "sample_pages": {
            "default": null,
            "description": "Pages sampled for the verdict, overriding the configured default. `0` scans every page, which is slower but settles a borderline mixed document.",
            "format": "uint",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "title": "Request",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "DocumentType": {
            "description": "What a document is made of.",
            "oneOf": [
              {
                "description": "Real text throughout. Extract locally.",
                "enum": [
                  "text_based"
                ],
                "type": "string"
              },
              {
                "description": "Pictures of pages. Every page needs OCR.",
                "enum": [
                  "scanned"
                ],
                "type": "string"
              },
              {
                "description": "Images with little or no text layer.",
                "enum": [
                  "image_based"
                ],
                "type": "string"
              },
              {
                "description": "Some pages carry text, others do not. Read `pages_needing_ocr`.",
                "enum": [
                  "mixed"
                ],
                "type": "string"
              }
            ]
          },
          "PageOcrReason": {
            "description": "Why one page cannot be read without OCR.",
            "properties": {
              "page": {
                "description": "1-indexed page number.",
                "format": "uint32",
                "minimum": 0,
                "type": "integer"
              },
              "reasons": {
                "description": "Machine-readable reasons: `scanned` (a raster page), `no_text` (nothing extractable and nothing to OCR), `vector_text` (characters drawn as outlines rather than text) or `suspected_garbled_text` (a text layer that decodes to nonsense).",
                "items": {
                  "type": "string"
                },
                "type": "array"
              }
            },
            "required": [
              "page",
              "reasons"
            ],
            "type": "object"
          }
        },
        "properties": {
          "confidence": {
            "description": "How much to trust the verdict, from 0.0 to 1.0.",
            "format": "float",
            "type": "number"
          },
          "document_type": {
            "allOf": [
              {
                "$ref": "#/definitions/DocumentType"
              }
            ],
            "description": "The document-level verdict."
          },
          "elapsed_ms": {
            "description": "Wall-clock time for the classification.",
            "format": "uint64",
            "minimum": 0,
            "type": "integer"
          },
          "has_encoding_issues": {
            "description": "`true` when font encodings decoded badly. Only known on the encrypted path, which extracts far enough to notice; absent otherwise, where `suspected_garbled_text` in `ocr_reasons` carries the same signal.",
            "type": [
              "boolean",
              "null"
            ]
          },
          "ocr_reasons": {
            "description": "Per-page explanation for `pages_needing_ocr`.",
            "items": {
              "$ref": "#/definitions/PageOcrReason"
            },
            "type": "array"
          },
          "ocr_recommended": {
            "description": "`true` when the images carry meaning the text layer does not, so OCR adds something even on a text-based document. Absent for an encrypted document, for the same reason as `pages_sampled`.",
            "type": [
              "boolean",
              "null"
            ]
          },
          "page_count": {
            "description": "Pages in the document.",
            "format": "uint32",
            "minimum": 0,
            "type": "integer"
          },
          "pages_needing_ocr": {
            "description": "1-indexed pages that cannot be read without OCR. Empty for a clean text-based document.",
            "items": {
              "format": "uint32",
              "minimum": 0,
              "type": "integer"
            },
            "type": "array"
          },
          "pages_sampled": {
            "description": "Pages actually inspected. Lower than `page_count` when sampling, so a verdict from a sample can be told apart from one that read everything. Absent for an encrypted document, which takes a decryption path that does not report the counters.",
            "format": "uint32",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          },
          "pages_with_text": {
            "description": "Inspected pages that carry text operators. Absent for an encrypted document, for the same reason as `pages_sampled`.",
            "format": "uint32",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          },
          "source": {
            "description": "Source label: the file name, or `<inline>` for an in-memory document.",
            "type": "string"
          },
          "title": {
            "description": "Document title from the PDF metadata, when it has one.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "confidence",
          "document_type",
          "elapsed_ms",
          "ocr_reasons",
          "page_count",
          "pages_needing_ocr",
          "source"
        ],
        "title": "Response",
        "type": "object"
      }
    },
    {
      "description": "Extract positioned text items: the box, font, size and styling of every run of characters on a page. Coordinates are PDF points with a bottom-left origin. Use this for layout-aware reading; use pdf::to-markdown to just read the document.",
      "metadata": {},
      "name": "pdf::extract-items",
      "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 worker takes.",
            "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 PDF comes from. Exactly one of the two fields must be set.",
        "properties": {
          "bytes_base64": {
            "default": null,
            "description": "Base64-encoded PDF bytes, for a document with no path. Mutually exclusive with `path`.",
            "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_items": {
            "default": null,
            "description": "Items to return before truncating. Omit for the configured default; `0` returns every item, which on a dense document is a very large response.",
            "format": "uint",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          },
          "pages": {
            "default": null,
            "description": "1-indexed pages to read. Omit for the whole document.",
            "items": {
              "format": "uint32",
              "minimum": 0,
              "type": "integer"
            },
            "type": [
              "array",
              "null"
            ]
          },
          "path": {
            "default": null,
            "description": "Filesystem path to the PDF. Mutually exclusive with `bytes_base64`.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "Request",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Item": {
            "description": "One positioned run of characters.",
            "properties": {
              "bold": {
                "type": "boolean"
              },
              "font": {
                "description": "Font name as the document names it.",
                "type": "string"
              },
              "font_size": {
                "description": "Font size in points.",
                "format": "float",
                "type": "number"
              },
              "height": {
                "description": "Height in PDF points, approximated from the font size.",
                "format": "float",
                "type": "number"
              },
              "italic": {
                "type": "boolean"
              },
              "kind": {
                "$ref": "#/definitions/ItemKind"
              },
              "link": {
                "description": "Link target, for a `link` item.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "mcid": {
                "description": "Marked-content id tying this item to the document's tagged structure tree, when the document has one.",
                "format": "int64",
                "type": [
                  "integer",
                  "null"
                ]
              },
              "page": {
                "description": "1-indexed page number.",
                "format": "uint32",
                "minimum": 0,
                "type": "integer"
              },
              "strikeout": {
                "description": "Recovered from vector lines through the text, not from a flag.",
                "type": "boolean"
              },
              "text": {
                "description": "The characters.",
                "type": "string"
              },
              "underline": {
                "description": "Recovered from vector lines near the baseline, not from a flag.",
                "type": "boolean"
              },
              "width": {
                "description": "Width in PDF points.",
                "format": "float",
                "type": "number"
              },
              "x": {
                "description": "Left edge, PDF points from the left of the page.",
                "format": "float",
                "type": "number"
              },
              "y": {
                "description": "Baseline, PDF points from the **bottom** of the page.",
                "format": "float",
                "type": "number"
              }
            },
            "required": [
              "bold",
              "font",
              "font_size",
              "height",
              "italic",
              "kind",
              "page",
              "strikeout",
              "text",
              "underline",
              "width",
              "x",
              "y"
            ],
            "type": "object"
          },
          "ItemKind": {
            "description": "What one item is.",
            "oneOf": [
              {
                "description": "Ordinary text.",
                "enum": [
                  "text"
                ],
                "type": "string"
              },
              {
                "description": "An image placeholder. The box is real; no pixels are decoded.",
                "enum": [
                  "image"
                ],
                "type": "string"
              },
              {
                "description": "Text carrying a hyperlink; the target is in `link`.",
                "enum": [
                  "link"
                ],
                "type": "string"
              },
              {
                "description": "A filled-in form field value.",
                "enum": [
                  "form_field"
                ],
                "type": "string"
              }
            ]
          }
        },
        "properties": {
          "coordinate_origin": {
            "description": "Origin convention for `x` and `y`, always `pdf-points, bottom-left`. Stated on every response because a caller that assumed the other convention reads the wrong end of the page with no error. Note `pdf::extract-regions` takes boxes with a top-left origin instead.",
            "type": "string"
          },
          "count": {
            "description": "Items returned.",
            "format": "uint",
            "minimum": 0,
            "type": "integer"
          },
          "elapsed_ms": {
            "description": "Wall-clock time for the extraction.",
            "format": "uint64",
            "minimum": 0,
            "type": "integer"
          },
          "items": {
            "description": "The items, in document order, capped per `max_items`.",
            "items": {
              "$ref": "#/definitions/Item"
            },
            "type": "array"
          },
          "source": {
            "description": "Source label: the file name, or `<inline>` for an in-memory document.",
            "type": "string"
          },
          "total_count": {
            "description": "Items the document holds for the requested pages. Equal to `count` when nothing was dropped.",
            "format": "uint",
            "minimum": 0,
            "type": "integer"
          },
          "truncated": {
            "description": "`true` when `items` stops short. Narrow `pages`, or pass `max_items: 0`.",
            "type": "boolean"
          }
        },
        "required": [
          "coordinate_origin",
          "count",
          "elapsed_ms",
          "items",
          "source",
          "total_count",
          "truncated"
        ],
        "title": "Response",
        "type": "object"
      }
    },
    {
      "description": "Extract the real text, or a markdown table, from inside bounding boxes on given pages. Built for the hybrid path where a vision model locates a region and the exact characters come from the document rather than from a transcription. Coordinates are PDF points with a top-left origin.",
      "metadata": {},
      "name": "pdf::extract-regions",
      "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 worker takes.",
            "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"
          },
          "Mode": {
            "description": "What to pull out of each box.",
            "oneOf": [
              {
                "description": "The characters inside the box, as flat text.",
                "enum": [
                  "text"
                ],
                "type": "string"
              },
              {
                "description": "A markdown table, when the items inside the box form one.",
                "enum": [
                  "table"
                ],
                "type": "string"
              }
            ]
          },
          "PageRegions": {
            "description": "Boxes to read on one page.",
            "properties": {
              "boxes": {
                "description": "Boxes as `[x1, y1, x2, y2]` in PDF points, origin at the top left.",
                "items": {
                  "items": {
                    "format": "float",
                    "type": "number"
                  },
                  "maxItems": 4,
                  "minItems": 4,
                  "type": "array"
                },
                "type": "array"
              },
              "page": {
                "description": "1-indexed page number.",
                "format": "uint32",
                "minimum": 0,
                "type": "integer"
              }
            },
            "required": [
              "boxes",
              "page"
            ],
            "type": "object"
          }
        },
        "description": "Where the PDF comes from. Exactly one of the two fields must be set.",
        "properties": {
          "bytes_base64": {
            "default": null,
            "description": "Base64-encoded PDF bytes, for a document with no path. Mutually exclusive with `path`.",
            "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."
          },
          "mode": {
            "allOf": [
              {
                "$ref": "#/definitions/Mode"
              }
            ],
            "default": "text",
            "description": "Flat text, or a markdown table."
          },
          "path": {
            "default": null,
            "description": "Filesystem path to the PDF. Mutually exclusive with `bytes_base64`.",
            "type": [
              "string",
              "null"
            ]
          },
          "regions": {
            "description": "One entry per page, each carrying the boxes to read on it.",
            "items": {
              "$ref": "#/definitions/PageRegions"
            },
            "type": "array"
          }
        },
        "required": [
          "regions"
        ],
        "title": "Request",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "PageResult": {
            "description": "Results for one page, parallel to that page's requested boxes.",
            "properties": {
              "page": {
                "description": "1-indexed page number.",
                "format": "uint32",
                "minimum": 0,
                "type": "integer"
              },
              "regions": {
                "description": "One result per requested box, in the order they were given.",
                "items": {
                  "$ref": "#/definitions/RegionResult"
                },
                "type": "array"
              }
            },
            "required": [
              "page",
              "regions"
            ],
            "type": "object"
          },
          "RegionResult": {
            "description": "What one box held.",
            "properties": {
              "needs_ocr": {
                "description": "`true` when the extraction is not trustworthy: an empty box, a font the parser cannot decode, or text that decodes to nonsense. In `table` mode it also means no table structure was found.",
                "type": "boolean"
              },
              "ocr_reason": {
                "description": "Machine-readable reason, when the cause is known.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "text": {
                "description": "The text, or the markdown table in `table` mode.",
                "type": "string"
              }
            },
            "required": [
              "needs_ocr",
              "text"
            ],
            "type": "object"
          }
        },
        "properties": {
          "coordinate_origin": {
            "description": "Origin convention the requested boxes were read under, always `pdf-points, top-left`. Stated on every response because a caller that assumed the other convention gets text from the wrong end of the page with no error. Note `pdf::extract-items` reports bottom-left instead.",
            "type": "string"
          },
          "elapsed_ms": {
            "description": "Wall-clock time for the extraction.",
            "format": "uint64",
            "minimum": 0,
            "type": "integer"
          },
          "pages": {
            "description": "One entry per requested page, in the order they were given.",
            "items": {
              "$ref": "#/definitions/PageResult"
            },
            "type": "array"
          },
          "region_count": {
            "description": "Boxes read across every page.",
            "format": "uint",
            "minimum": 0,
            "type": "integer"
          },
          "regions_needing_ocr": {
            "description": "Boxes whose result should not be trusted.",
            "format": "uint",
            "minimum": 0,
            "type": "integer"
          },
          "source": {
            "description": "Source label: the file name, or `<inline>` for an in-memory document.",
            "type": "string"
          }
        },
        "required": [
          "coordinate_origin",
          "elapsed_ms",
          "pages",
          "region_count",
          "regions_needing_ocr",
          "source"
        ],
        "title": "Response",
        "type": "object"
      }
    },
    {
      "description": "Extract a PDF as plain text, with no structure recovery. Cheaper than pdf::to-markdown and the right call when the text will be searched or embedded rather than read.",
      "metadata": {},
      "name": "pdf::extract-text",
      "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 worker takes.",
            "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 PDF comes from. Exactly one of the two fields must be set.",
        "properties": {
          "bytes_base64": {
            "default": null,
            "description": "Base64-encoded PDF bytes, for a document with no path. Mutually exclusive with `path`.",
            "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 the whole document.",
            "format": "uint",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          },
          "path": {
            "default": null,
            "description": "Filesystem path to the PDF. 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.",
            "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 content, 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, or on the functions that accept one, narrow with a `pages` filter.",
                "type": "boolean"
              }
            },
            "required": [
              "chars",
              "text",
              "total_chars",
              "truncated"
            ],
            "type": "object"
          }
        },
        "properties": {
          "body": {
            "allOf": [
              {
                "$ref": "#/definitions/Body"
              }
            ],
            "description": "The text, capped per `max_chars`."
          },
          "elapsed_ms": {
            "description": "Wall-clock time for the extraction.",
            "format": "uint64",
            "minimum": 0,
            "type": "integer"
          },
          "source": {
            "description": "Source label: the file name, or `<inline>` for an in-memory document.",
            "type": "string"
          }
        },
        "required": [
          "body",
          "elapsed_ms",
          "source"
        ],
        "title": "Response",
        "type": "object"
      }
    },
    {
      "description": "Internal: hot-reload the pdf worker from the authoritative configuration when it changes, swapping the per-call snapshot.",
      "metadata": {},
      "name": "pdf::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 text-based PDF to markdown, preserving headings, lists, links and tables. Returns nothing for a scanned document — call pdf::classify first. Responses are capped; pass max_chars 0 to take the whole document, or pages to take a slice of it.",
      "metadata": {},
      "name": "pdf::to-markdown",
      "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 worker takes.",
            "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"
          },
          "Profile": {
            "description": "How faithful the markdown should be to the source characters.",
            "oneOf": [
              {
                "description": "Preserve the source text as written.",
                "enum": [
                  "fidelity"
                ],
                "type": "string"
              },
              {
                "description": "Prefer shorter output, collapsing runs like the dot leaders in a table of contents. Not character-faithful to the source.",
                "enum": [
                  "compact"
                ],
                "type": "string"
              }
            ]
          }
        },
        "description": "Where the PDF comes from. Exactly one of the two fields must be set.",
        "properties": {
          "bytes_base64": {
            "default": null,
            "description": "Base64-encoded PDF bytes, for a document with no path. Mutually exclusive with `path`.",
            "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."
          },
          "include_images": {
            "default": false,
            "description": "Include `[Image: …]` placeholders. Off by default: nothing here decodes pixels, so a placeholder adds noise without adding information.",
            "type": "boolean"
          },
          "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"
            ]
          },
          "pages": {
            "default": null,
            "description": "1-indexed pages to convert. Omit for the whole document. A page filter is the cheap way to read a long report: take the pages you need rather than the whole thing truncated.",
            "items": {
              "format": "uint32",
              "minimum": 0,
              "type": "integer"
            },
            "type": [
              "array",
              "null"
            ]
          },
          "password": {
            "default": null,
            "description": "Password for an encrypted document. Never logged or echoed back.",
            "type": [
              "string",
              "null"
            ]
          },
          "path": {
            "default": null,
            "description": "Filesystem path to the PDF. Mutually exclusive with `bytes_base64`.",
            "type": [
              "string",
              "null"
            ]
          },
          "per_page": {
            "default": false,
            "description": "Return markdown per page as well as the joined document. Useful when a caller wants to route some pages to OCR and keep the rest.",
            "type": "boolean"
          },
          "profile": {
            "allOf": [
              {
                "$ref": "#/definitions/Profile"
              }
            ],
            "default": "fidelity",
            "description": "Source fidelity versus token efficiency."
          },
          "strip_headers_footers": {
            "default": true,
            "description": "Strip repeated running headers and footers.",
            "type": "boolean"
          }
        },
        "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.",
            "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 content, 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, or on the functions that accept one, narrow with a `pages` filter.",
                "type": "boolean"
              }
            },
            "required": [
              "chars",
              "text",
              "total_chars",
              "truncated"
            ],
            "type": "object"
          },
          "DocumentType": {
            "description": "What a document is made of.",
            "oneOf": [
              {
                "description": "Real text throughout. Extract locally.",
                "enum": [
                  "text_based"
                ],
                "type": "string"
              },
              {
                "description": "Pictures of pages. Every page needs OCR.",
                "enum": [
                  "scanned"
                ],
                "type": "string"
              },
              {
                "description": "Images with little or no text layer.",
                "enum": [
                  "image_based"
                ],
                "type": "string"
              },
              {
                "description": "Some pages carry text, others do not. Read `pages_needing_ocr`.",
                "enum": [
                  "mixed"
                ],
                "type": "string"
              }
            ]
          },
          "PageOcrReason": {
            "description": "Why one page cannot be read without OCR.",
            "properties": {
              "page": {
                "description": "1-indexed page number.",
                "format": "uint32",
                "minimum": 0,
                "type": "integer"
              },
              "reasons": {
                "description": "Machine-readable reasons: `scanned` (a raster page), `no_text` (nothing extractable and nothing to OCR), `vector_text` (characters drawn as outlines rather than text) or `suspected_garbled_text` (a text layer that decodes to nonsense).",
                "items": {
                  "type": "string"
                },
                "type": "array"
              }
            },
            "required": [
              "page",
              "reasons"
            ],
            "type": "object"
          },
          "PageResult": {
            "description": "One page of markdown, with its own OCR verdict.",
            "properties": {
              "markdown": {
                "description": "Markdown for this page.",
                "type": "string"
              },
              "needs_ocr": {
                "description": "`true` when this page's text is not trustworthy and OCR would do better.",
                "type": "boolean"
              },
              "ocr_reason": {
                "description": "Machine-readable reason, when the cause is known.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "page": {
                "description": "1-indexed page number.",
                "format": "uint32",
                "minimum": 0,
                "type": "integer"
              }
            },
            "required": [
              "markdown",
              "needs_ocr",
              "page"
            ],
            "type": "object"
          }
        },
        "properties": {
          "body": {
            "allOf": [
              {
                "$ref": "#/definitions/Body"
              }
            ],
            "description": "The markdown, capped per `max_chars`."
          },
          "document_type": {
            "allOf": [
              {
                "$ref": "#/definitions/DocumentType"
              }
            ],
            "description": "The document-level verdict, so a caller that skipped `pdf::classify` still learns it got nothing because the document is a scan."
          },
          "elapsed_ms": {
            "description": "Wall-clock time for the conversion.",
            "format": "uint64",
            "minimum": 0,
            "type": "integer"
          },
          "has_encoding_issues": {
            "description": "`true` when font encodings decoded badly. The markdown, if any, is not to be trusted.",
            "type": "boolean"
          },
          "ocr_reasons": {
            "description": "Per-page explanation for `pages_needing_ocr`.",
            "items": {
              "$ref": "#/definitions/PageOcrReason"
            },
            "type": "array"
          },
          "page_count": {
            "description": "Pages in the document.",
            "format": "uint32",
            "minimum": 0,
            "type": "integer"
          },
          "pages": {
            "description": "Per-page markdown, when `per_page` was requested.",
            "items": {
              "$ref": "#/definitions/PageResult"
            },
            "type": [
              "array",
              "null"
            ]
          },
          "pages_converted": {
            "description": "Pages actually converted. Equal to `page_count` unless `pages` was set.",
            "format": "uint32",
            "minimum": 0,
            "type": "integer"
          },
          "pages_needing_ocr": {
            "description": "1-indexed pages that need OCR.",
            "items": {
              "format": "uint32",
              "minimum": 0,
              "type": "integer"
            },
            "type": "array"
          },
          "pages_with_columns": {
            "description": "1-indexed pages laid out in multiple columns.",
            "items": {
              "format": "uint32",
              "minimum": 0,
              "type": "integer"
            },
            "type": "array"
          },
          "pages_with_tables": {
            "description": "1-indexed pages holding a detected table.",
            "items": {
              "format": "uint32",
              "minimum": 0,
              "type": "integer"
            },
            "type": "array"
          },
          "source": {
            "description": "Source label: the file name, or `<inline>` for an in-memory document.",
            "type": "string"
          }
        },
        "required": [
          "body",
          "document_type",
          "elapsed_ms",
          "has_encoding_issues",
          "ocr_reasons",
          "page_count",
          "pages_converted",
          "pages_needing_ocr",
          "pages_with_columns",
          "pages_with_tables",
          "source"
        ],
        "title": "Response",
        "type": "object"
      }
    },
    {
      "description": "Serve the pdf worker's injected console UI assets (content function for its console:script / console:style triggers).",
      "metadata": {
        "internal": true
      },
      "name": "pdf::ui-content",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Input of the content function: the console asks for one asset by path.",
        "properties": {
          "path": {
            "description": "The asset path from the trigger config (e.g. `state/page.js`).",
            "type": "string"
          }
        },
        "required": [
          "path"
        ],
        "title": "UiContentInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Output of the content function.",
        "properties": {
          "content": {
            "description": "The asset source, verbatim.",
            "type": "string"
          },
          "content_type": {
            "description": "MIME type the console should serve the asset with.",
            "type": "string"
          }
        },
        "required": [
          "content",
          "content_type"
        ],
        "title": "UiContentResult",
        "type": "object"
      }
    }
  ],
  "triggers": []
}