UNPKG

@stll/folio-agents

Version:

Framework-neutral LLM tool layer over folio's ai-edits engine: function-calling tools so an agent can read and mutate .docx documents through @stll/folio-core.

189 lines (165 loc) 10.6 kB
--- name: folio-agents description: >- Integrate and use @stll/folio-agents, the framework-neutral LLM tool layer over @stll/folio-core: function-calling tools that read and mutate a .docx document, with every mutation landing as a tracked change or comment pending human review. Load this skill when wiring folio's tools into an agent's tool-use loop (TanStack AI, Vercel AI SDK, raw Anthropic/OpenAI SDKs, or a custom loop), choosing between the headless reviewer bridge and the live-editor-ref bridge, or summarizing what changed between two document versions. metadata: type: core library: "@stll/folio-agents" library_version: "0.0.0" --- # @stll/folio-agents Framework-neutral LLM tool layer over `@stll/folio-core`'s ai-edits engine. Gives a model function-calling tools to read and mutate a `.docx` document; every mutation (`add_comment`, `suggest_changes`) lands as a tracked change or comment pending human review — nothing is silently finalized. ## Integration recipe 1. `getFolioToolDefinitions(options?)` returns `FolioAgentToolDefinition[]` — plain `{ name, description, inputSchema }` objects with a conservative JSON Schema subset (`type: "object"`, `properties`, `required`, `enum`, `additionalProperties: false`, plain arrays — safe for providers that reject uncommon keywords, e.g. Gemini's OpenAPI-3.0 subset). Pass `{ suggestChanges }` options to shape `suggest_changes` for the surface (see below) and pass the same object to `executeFolioToolCall`. 2. Register them with your framework: - TanStack AI: pass `inputSchema` directly as `toolDefinition({ inputSchema })` — no wrapper needed. - Raw Anthropic/OpenAI SDKs: `toAnthropicTools(defs)` / `toOpenAITools(defs)`. - Vercel AI SDK: wrap each `inputSchema` in `jsonSchema()` from `ai`. 3. In your tool-use loop, for every `tool_use` / `tool_call` the model emits: call `executeFolioToolCallUntyped(name, args, bridge, { suggestChanges })` with the same options object as step 1 → returns `{ ok: true, result } | { ok: false, error }`. 4. Feed that result straight back to the model as the tool result message — both branches are meant to reach the model, not just the `ok: true` one: error strings are plain language ("blockId not found; re-read...") that the model can act on directly. ## Bridge selection A `FolioAgentBridge` is the structural seam `executeFolioToolCall` drives. Two are shipped: - `createReviewerBridge(FolioDocxReviewer.fromBuffer(...))` — server/headless, full capability. Use for a document that isn't open in a live editor (a backend job, a batch review pass). Defaults to `mode: "tracked-changes"` (pass `{ mode: "direct" }` to edit in place instead — rarely what you want for an unreviewed agent). - `createEditorRefBridge({ ref, author, getComments, setComments })` — drives a live `DocxEditorRef` from `@stll/folio-react` (or anything structurally matching `FolioAgentEditorRefLike`). Comments live in host React state, not on the ref, hence the `getComments` / `setComments` pair (the same ones the host already passes to `DocxEditor`). The editor-ref bridge's capability now depends on what the underlying `DocxEditorRef` implements. Against a ref with the read surface (`getTrackedChanges`, `getCommentAnchors`, `getSelectionText`, `getPageText`), it has full parity with the headless one: `read_changes` returns real tracked changes, comment entries carry a resolved `blockId` / `quote`, and `read_page` / `read_selection` work against the live view. Against an older ref that predates those methods, each falls back independently: `read_changes` returns `[]`, comment entries keep `blockId: null` / `quote: ""`, and `read_page` / `read_selection` report an unsupported-capability error (see `src/bridges/editor-ref.ts` for the exact per-method fallback). A bridge omits an optional capability member entirely rather than implementing it as a no-op, and `executeFolioToolCall` reports the corresponding tool call as unsupported rather than throwing — this is how the headless reviewer bridge signals `read_page` / `read_selection` / `scroll_to_block` as unsupported too, since a headless document has no live page/selection/scroll surface at all. ## Configuring `suggest_changes` `suggest_changes` is configured per host surface, never re-derived. Pass one `FolioSuggestChangesOptions` object to both `getFolioToolDefinitions({ suggestChanges })` and `executeFolioToolCall(name, args, bridge, { suggestChanges })`: - `operationTypes`: any subset of the contract's operation types (default: everything except `commentOnBlock`, covered by `add_comment`, and `insertSignatureTable`, which is direct-only). The schema advertises only the properties those types accept. - `reviewMeta: "required"`: every operation must carry `severity` and `area` (a review queue that sorts and groups needs them). Default `"optional"`. - `maxOperations`: 1 to 200 per call, default 50. - `documentVersion: { current }`: the model must echo the token; the executor compares it to `bridge.getDocumentVersion()` and skips the whole batch with `documentVersionMismatch` when the document moved on. `describeSuggestChangesCapabilities(options)` returns the capability text the tool description carries; paste it into a system prompt instead of hand-writing a "what this tool can do" paragraph that drifts. The parser decodes leniently (a `kind` key, an operation given as a JSON string, a property that does not apply to the type) and reports each step in the result's `normalizations`; it then enforces the caps, mints ids unique across calls, and delegates every per-operation rule to `parseFolioDocumentOperationBatch` in `@stll/folio-core`. ## Host-managed review queue A host that already has its own review-queue UX — its own place to store a model's proposed edits pending approval, separate from folio's tracked-changes redlines — writes a `FolioAgentBridge` whose `applyDocumentOperations` enqueues the batch and returns `status: "queued"` with the operations listed in `queued` (build `receipts` with `getFolioDocumentOperationReceipts` from `@stll/folio-core/server`). The executor then produces the standard envelope for the model (`queued`, `skipped` with plain-language reasons, `receipts`, `normalizations`) and keeps the automatic `precondition` stamping and the document-version check. A surface with nothing editable reports every operation as skipped with `documentNotEditable`. `parseSuggestChangesInput` / `parseAddCommentInput` stay exported for validation-only paths. ## Ground rules for the model - Block ids (`blockId`) and comment ids (`commentId`) only ever come from a prior tool call in the same conversation — `read_document`, `find_text`, or `read_comments`. Never invent or reuse one from outside the conversation; ids are not guessable and change whenever the document's structure changes. - `suggest_changes` operations that get skipped (`skipped: [{ id, reason }]`) return a plain-language reason, not a machine code (e.g. "the block changed since your snapshot; re-read the document and retry with fresh ids"). Treat a skip as a retry signal: re-read (`read_document` or `find_text`) and reissue with fresh ids, not a dead end. - Successful mutations return input-ordered `receipts`. Use their typed `affected` targets to identify updated blocks, stable ranges, insertions, and created comments without inferring effects from document text. - `suggest_changes` defaults to tracked-changes mode on both shipped bridges — it proposes redlines for a human to accept or reject, never edits the visible text directly. - The operation kinds a surface accepts are exactly the ones in its `suggest_changes` schema's `type` enum (by default `replaceInBlock`, `replaceRange`, `commentOnRange`, `formatRange`, `insertAfterBlock`, `insertBeforeBlock`, `replaceBlock`, `deleteBlock`, `insertTableRow`, `deleteTableRow`, `insertTableColumn`, `deleteTableColumn`, `mergeTableCells`, `splitTableCell`) plus comment/reply/resolve. Do not invent an operation kind or a directive marker; if a document needs a structural operation the contract lacks, extend `@stll/folio-core`'s ai-edits engine and the contract, then the schema follows. - A `queued` list in a `suggest_changes` result means the host parked those operations in its own review queue; treat it like `applied` for the purpose of "the edit has been proposed", never as a failure. - **Untrusted documents:** `read_document`, `read_comments`, `read_changes`, and `find_text` return document content verbatim, so a `.docx` from an untrusted party can inject prompt instructions straight into the model's context via its text — hosts should treat any document-derived tool result as untrusted model input. Mutations stay safe regardless: `suggest_changes` and `add_comment` only ever produce tracked changes or comments pending human review, so an injected instruction can propose an edit, never apply one. ## Summarizing what changed Two different questions, two different tools: - **Pending redlines in one document** — what a reviewer would see as tracked changes right now: use the `read_changes` tool on a capable bridge or call `reviewer.getChanges()` directly. - **Between two saved `.docx` versions** — `compareDocxVersions(previousBuffer, currentBuffer)` + `formatVersionDiffForLLM(diff)`. Both are plain async functions, not tool definitions: a model can't attach two buffers to one tool call (they aren't JSON-serializable arguments). Wrap them in a host-side tool keyed by version identifiers instead — the model calls it with two version ids, your backend resolves the buffers and returns the formatted diff text. - **Reviewable output between two saved versions** — `generateRedlineDocx(previousBuffer, currentBuffer, options)` returns the generated package, operation results, unmatched stories, and a structured package-metadata privacy report. `compareDocxVersions` compares the AS-ACCEPTED view of each buffer: any tracked changes already pending in either one count as already applied before the diff runs, so diffing two versions that each carry their own uncommitted redlines still produces a clean diff instead of raw markup noise. ## Reference Tool names, full JSON Schemas, and the headless/live-editor quickstarts are in this package's README.md. Read `src/tools.ts` for the exact input schema of every tool and `src/execute.ts` for the skip-reason vocabulary.