From b81e3d78ee7d574a4f328b50c72ee01b6de157e0 Mon Sep 17 00:00:00 2001 From: "superdoc-bot[bot]" <235763992+superdoc-bot[bot]@users.noreply.github.com> Date: Sun, 5 Jul 2026 19:36:51 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=94=84=20Sync=20stable=20=E2=86=92=20?= =?UTF-8?q?main?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Merges latest stable patches into main with a real merge commit so stable release tags remain reachable from main. This keeps `@next` prerelease version calculation ahead of the latest published stable release. ## Merge requirement This PR must be merged with GitHub's merge-commit option. Squash or rebase merging will discard stable ancestry and break `@next` version calculation again. ## Conflict handling Merge status: `clean`. Version-only release artifact conflicts are auto-resolved to keep stable's already-published versions. Non-version conflicts fail this workflow instead of committing conflict markers. --- _Auto-created by sync-patches workflow._ Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com> Co-authored-by: semantic-release-bot Co-authored-by: Caio Pizzol Source-PR: https://github.com/superdoc-dev/superdoc/pull/3800 Closes superdoc-dev/superdoc#3800 Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 7302c2f935324898524bfd2edaaef349d3e1cd4b Ported-Public-Prefix: superdoc/public --- apps/cli/package.json | 2 +- apps/cli/platforms/cli-darwin-arm64/package.json | 2 +- apps/cli/platforms/cli-darwin-x64/package.json | 2 +- apps/cli/platforms/cli-linux-arm64/package.json | 2 +- apps/cli/platforms/cli-linux-x64/package.json | 2 +- apps/cli/platforms/cli-windows-x64/package.json | 2 +- apps/mcp/package.json | 2 +- apps/vscode-ext/package.json | 2 +- packages/fonts/package.json | 2 +- packages/layout-engine/measuring/dom/src/index.ts | 2 +- .../painters/dom/src/table/border-utils.test.ts | 2 +- packages/react/package.json | 2 +- packages/sdk/langs/node/package.json | 2 +- .../node/platforms/sdk-darwin-arm64/package.json | 2 +- .../langs/node/platforms/sdk-darwin-x64/package.json | 2 +- .../node/platforms/sdk-linux-arm64/package.json | 2 +- .../langs/node/platforms/sdk-linux-x64/package.json | 2 +- .../node/platforms/sdk-windows-x64/package.json | 2 +- .../superdoc-sdk-cli-darwin-arm64/pyproject.toml | 2 +- .../superdoc-sdk-cli-darwin-x64/pyproject.toml | 2 +- .../superdoc-sdk-cli-linux-arm64/pyproject.toml | 2 +- .../superdoc-sdk-cli-linux-x64/pyproject.toml | 2 +- .../superdoc-sdk-cli-windows-x64/pyproject.toml | 2 +- packages/sdk/langs/python/pyproject.toml | 12 ++++++------ packages/sdk/package.json | 2 +- packages/sdk/version.json | 2 +- packages/superdoc/package.json | 2 +- 27 files changed, 32 insertions(+), 32 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index ac7c62e524..894d8eb77c 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "@superdoc-dev/cli", - "version": "0.20.1", + "version": "0.21.0", "type": "module", "bin": { "superdoc": "./dist/index.js" diff --git a/apps/cli/platforms/cli-darwin-arm64/package.json b/apps/cli/platforms/cli-darwin-arm64/package.json index 685b84d0d9..28861c1e23 100644 --- a/apps/cli/platforms/cli-darwin-arm64/package.json +++ b/apps/cli/platforms/cli-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@superdoc-dev/cli-darwin-arm64", - "version": "0.20.1", + "version": "0.21.0", "os": [ "darwin" ], diff --git a/apps/cli/platforms/cli-darwin-x64/package.json b/apps/cli/platforms/cli-darwin-x64/package.json index 808a891a41..bbe1463dc4 100644 --- a/apps/cli/platforms/cli-darwin-x64/package.json +++ b/apps/cli/platforms/cli-darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@superdoc-dev/cli-darwin-x64", - "version": "0.20.1", + "version": "0.21.0", "os": [ "darwin" ], diff --git a/apps/cli/platforms/cli-linux-arm64/package.json b/apps/cli/platforms/cli-linux-arm64/package.json index 7626b8b640..9efcfa5b66 100644 --- a/apps/cli/platforms/cli-linux-arm64/package.json +++ b/apps/cli/platforms/cli-linux-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@superdoc-dev/cli-linux-arm64", - "version": "0.20.1", + "version": "0.21.0", "os": [ "linux" ], diff --git a/apps/cli/platforms/cli-linux-x64/package.json b/apps/cli/platforms/cli-linux-x64/package.json index 02151a8e82..4abff90020 100644 --- a/apps/cli/platforms/cli-linux-x64/package.json +++ b/apps/cli/platforms/cli-linux-x64/package.json @@ -1,6 +1,6 @@ { "name": "@superdoc-dev/cli-linux-x64", - "version": "0.20.1", + "version": "0.21.0", "os": [ "linux" ], diff --git a/apps/cli/platforms/cli-windows-x64/package.json b/apps/cli/platforms/cli-windows-x64/package.json index 2e53b75c03..c38d1a389a 100644 --- a/apps/cli/platforms/cli-windows-x64/package.json +++ b/apps/cli/platforms/cli-windows-x64/package.json @@ -1,6 +1,6 @@ { "name": "@superdoc-dev/cli-windows-x64", - "version": "0.20.1", + "version": "0.21.0", "os": [ "win32" ], diff --git a/apps/mcp/package.json b/apps/mcp/package.json index 7f2363da29..95d68fb919 100644 --- a/apps/mcp/package.json +++ b/apps/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@superdoc-dev/mcp", - "version": "0.15.1", + "version": "0.16.0", "type": "module", "engines": { "node": ">=20" diff --git a/apps/vscode-ext/package.json b/apps/vscode-ext/package.json index b0fc7cb645..f5e12c121c 100644 --- a/apps/vscode-ext/package.json +++ b/apps/vscode-ext/package.json @@ -2,7 +2,7 @@ "name": "superdoc-vscode-ext", "displayName": "SuperDoc - Word Document Editor", "description": "Open and edit Word documents (.docx) directly in VS Code using SuperDoc", - "version": "2.15.1", + "version": "2.16.0", "publisher": "superdoc-dev", "icon": "logo.png", "homepage": "https://superdoc.dev", diff --git a/packages/fonts/package.json b/packages/fonts/package.json index d2e88342b5..fd8cea261b 100644 --- a/packages/fonts/package.json +++ b/packages/fonts/package.json @@ -1,6 +1,6 @@ { "name": "@superdoc-dev/fonts", - "version": "0.1.2", + "version": "0.1.3", "description": "Optional bundled metric-compatible font substitutes for SuperDoc, shipped as bundler-resolvable assets.", "type": "module", "license": "SEE LICENSE IN assets/LICENSES.md", diff --git a/packages/layout-engine/measuring/dom/src/index.ts b/packages/layout-engine/measuring/dom/src/index.ts index 92315b2de8..86e182f72e 100644 --- a/packages/layout-engine/measuring/dom/src/index.ts +++ b/packages/layout-engine/measuring/dom/src/index.ts @@ -197,7 +197,7 @@ import { getCellSpacingPx, getBorderBandWidthPx } from '@superdoc/contracts'; /** * Returns the border band width in pixels for a table border value. * Delegates to the shared contracts helper so this always matches the painter's - * rendered width (thick = 2x min 3px, double = 3x per-rule width min 3px). Used for + * rendered width (thick = authored width min 1px, double = 3x per-rule width min 3px). Used for * outer table dimensions and per-row band reservation. */ function getTableBorderWidthPx(value: TableBorderValue | null | undefined): number { diff --git a/packages/layout-engine/painters/dom/src/table/border-utils.test.ts b/packages/layout-engine/painters/dom/src/table/border-utils.test.ts index e04f9caba5..c74cf1603b 100644 --- a/packages/layout-engine/painters/dom/src/table/border-utils.test.ts +++ b/packages/layout-engine/painters/dom/src/table/border-utils.test.ts @@ -105,7 +105,7 @@ describe('applyBorder', () => { expect(element.style.borderTop).toMatch(/1px solid (#000000|rgb\(0,\s*0,\s*0\))/i); }); - it('should handle thick border with larger width', () => { + it('paints a wider thick border at the authored width', () => { const border: BorderSpec = { style: 'thick', width: 3, color: '#000000' }; applyBorder(element, 'Top', border); expect(element.style.borderTop).toMatch(/3px solid (#000000|rgb\(0,\s*0,\s*0\))/i); diff --git a/packages/react/package.json b/packages/react/package.json index 71b5174a0b..8313d87d5e 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@superdoc-dev/react", - "version": "1.14.1", + "version": "1.15.0", "description": "Official React wrapper for the SuperDoc document editor", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/sdk/langs/node/package.json b/packages/sdk/langs/node/package.json index 89d3e522e2..74814431fa 100644 --- a/packages/sdk/langs/node/package.json +++ b/packages/sdk/langs/node/package.json @@ -1,6 +1,6 @@ { "name": "@superdoc-dev/sdk", - "version": "1.19.1", + "version": "1.20.0", "private": false, "type": "module", "main": "./dist/index.cjs", diff --git a/packages/sdk/langs/node/platforms/sdk-darwin-arm64/package.json b/packages/sdk/langs/node/platforms/sdk-darwin-arm64/package.json index 6d2b9d0799..d12409dfda 100644 --- a/packages/sdk/langs/node/platforms/sdk-darwin-arm64/package.json +++ b/packages/sdk/langs/node/platforms/sdk-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@superdoc-dev/sdk-darwin-arm64", - "version": "1.19.1", + "version": "1.20.0", "os": [ "darwin" ], diff --git a/packages/sdk/langs/node/platforms/sdk-darwin-x64/package.json b/packages/sdk/langs/node/platforms/sdk-darwin-x64/package.json index f420d61fb6..5bc4b691bb 100644 --- a/packages/sdk/langs/node/platforms/sdk-darwin-x64/package.json +++ b/packages/sdk/langs/node/platforms/sdk-darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@superdoc-dev/sdk-darwin-x64", - "version": "1.19.1", + "version": "1.20.0", "os": [ "darwin" ], diff --git a/packages/sdk/langs/node/platforms/sdk-linux-arm64/package.json b/packages/sdk/langs/node/platforms/sdk-linux-arm64/package.json index 94bb26ddf7..b3be5c6a44 100644 --- a/packages/sdk/langs/node/platforms/sdk-linux-arm64/package.json +++ b/packages/sdk/langs/node/platforms/sdk-linux-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@superdoc-dev/sdk-linux-arm64", - "version": "1.19.1", + "version": "1.20.0", "os": [ "linux" ], diff --git a/packages/sdk/langs/node/platforms/sdk-linux-x64/package.json b/packages/sdk/langs/node/platforms/sdk-linux-x64/package.json index d299dfab70..6bd1a804ef 100644 --- a/packages/sdk/langs/node/platforms/sdk-linux-x64/package.json +++ b/packages/sdk/langs/node/platforms/sdk-linux-x64/package.json @@ -1,6 +1,6 @@ { "name": "@superdoc-dev/sdk-linux-x64", - "version": "1.19.1", + "version": "1.20.0", "os": [ "linux" ], diff --git a/packages/sdk/langs/node/platforms/sdk-windows-x64/package.json b/packages/sdk/langs/node/platforms/sdk-windows-x64/package.json index 779e0b4511..e4c46f62e1 100644 --- a/packages/sdk/langs/node/platforms/sdk-windows-x64/package.json +++ b/packages/sdk/langs/node/platforms/sdk-windows-x64/package.json @@ -1,6 +1,6 @@ { "name": "@superdoc-dev/sdk-windows-x64", - "version": "1.19.1", + "version": "1.20.0", "os": [ "win32" ], diff --git a/packages/sdk/langs/python/platforms/superdoc-sdk-cli-darwin-arm64/pyproject.toml b/packages/sdk/langs/python/platforms/superdoc-sdk-cli-darwin-arm64/pyproject.toml index 537be3b7bd..1e7b0bf8fb 100644 --- a/packages/sdk/langs/python/platforms/superdoc-sdk-cli-darwin-arm64/pyproject.toml +++ b/packages/sdk/langs/python/platforms/superdoc-sdk-cli-darwin-arm64/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "superdoc-sdk-cli-darwin-arm64" -version = "1.19.1" +version = "1.20.0" description = "SuperDoc CLI binary for macOS ARM64 (Apple Silicon)" readme = "README.md" requires-python = ">=3.9" diff --git a/packages/sdk/langs/python/platforms/superdoc-sdk-cli-darwin-x64/pyproject.toml b/packages/sdk/langs/python/platforms/superdoc-sdk-cli-darwin-x64/pyproject.toml index fd5bb047b0..cb00c4fb39 100644 --- a/packages/sdk/langs/python/platforms/superdoc-sdk-cli-darwin-x64/pyproject.toml +++ b/packages/sdk/langs/python/platforms/superdoc-sdk-cli-darwin-x64/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "superdoc-sdk-cli-darwin-x64" -version = "1.19.1" +version = "1.20.0" description = "SuperDoc CLI binary for macOS x64 (Intel)" readme = "README.md" requires-python = ">=3.9" diff --git a/packages/sdk/langs/python/platforms/superdoc-sdk-cli-linux-arm64/pyproject.toml b/packages/sdk/langs/python/platforms/superdoc-sdk-cli-linux-arm64/pyproject.toml index f8eb9370b7..0823f388ce 100644 --- a/packages/sdk/langs/python/platforms/superdoc-sdk-cli-linux-arm64/pyproject.toml +++ b/packages/sdk/langs/python/platforms/superdoc-sdk-cli-linux-arm64/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "superdoc-sdk-cli-linux-arm64" -version = "1.19.1" +version = "1.20.0" description = "SuperDoc CLI binary for Linux ARM64" readme = "README.md" requires-python = ">=3.9" diff --git a/packages/sdk/langs/python/platforms/superdoc-sdk-cli-linux-x64/pyproject.toml b/packages/sdk/langs/python/platforms/superdoc-sdk-cli-linux-x64/pyproject.toml index 9ce66fce70..bf2f09d0fe 100644 --- a/packages/sdk/langs/python/platforms/superdoc-sdk-cli-linux-x64/pyproject.toml +++ b/packages/sdk/langs/python/platforms/superdoc-sdk-cli-linux-x64/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "superdoc-sdk-cli-linux-x64" -version = "1.19.1" +version = "1.20.0" description = "SuperDoc CLI binary for Linux x64" readme = "README.md" requires-python = ">=3.9" diff --git a/packages/sdk/langs/python/platforms/superdoc-sdk-cli-windows-x64/pyproject.toml b/packages/sdk/langs/python/platforms/superdoc-sdk-cli-windows-x64/pyproject.toml index 1e9ff98628..cede8815e7 100644 --- a/packages/sdk/langs/python/platforms/superdoc-sdk-cli-windows-x64/pyproject.toml +++ b/packages/sdk/langs/python/platforms/superdoc-sdk-cli-windows-x64/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "superdoc-sdk-cli-windows-x64" -version = "1.19.1" +version = "1.20.0" description = "SuperDoc CLI binary for Windows x64" readme = "README.md" requires-python = ">=3.9" diff --git a/packages/sdk/langs/python/pyproject.toml b/packages/sdk/langs/python/pyproject.toml index 2c245c7423..0e82a55912 100644 --- a/packages/sdk/langs/python/pyproject.toml +++ b/packages/sdk/langs/python/pyproject.toml @@ -4,18 +4,18 @@ build-backend = "setuptools.build_meta" [project] name = "superdoc-sdk" -version = "1.19.1" +version = "1.20.0" description = "SuperDoc SDK (CLI-backed)" readme = "README.md" requires-python = ">=3.9" license = "AGPL-3.0" authors = [{ name = "SuperDoc" }] dependencies = [ - "superdoc-sdk-cli-darwin-arm64==1.19.1; platform_system == 'Darwin' and (platform_machine == 'arm64' or platform_machine == 'aarch64' or platform_machine == 'ARM64')", - "superdoc-sdk-cli-darwin-x64==1.19.1; platform_system == 'Darwin' and (platform_machine == 'x86_64' or platform_machine == 'AMD64' or platform_machine == 'amd64')", - "superdoc-sdk-cli-linux-x64==1.19.1; platform_system == 'Linux' and (platform_machine == 'x86_64' or platform_machine == 'AMD64' or platform_machine == 'amd64')", - "superdoc-sdk-cli-linux-arm64==1.19.1; platform_system == 'Linux' and (platform_machine == 'arm64' or platform_machine == 'aarch64' or platform_machine == 'ARM64')", - "superdoc-sdk-cli-windows-x64==1.19.1; platform_system == 'Windows' and (platform_machine == 'x86_64' or platform_machine == 'AMD64' or platform_machine == 'amd64')", + "superdoc-sdk-cli-darwin-arm64==1.20.0; platform_system == 'Darwin' and (platform_machine == 'arm64' or platform_machine == 'aarch64' or platform_machine == 'ARM64')", + "superdoc-sdk-cli-darwin-x64==1.20.0; platform_system == 'Darwin' and (platform_machine == 'x86_64' or platform_machine == 'AMD64' or platform_machine == 'amd64')", + "superdoc-sdk-cli-linux-x64==1.20.0; platform_system == 'Linux' and (platform_machine == 'x86_64' or platform_machine == 'AMD64' or platform_machine == 'amd64')", + "superdoc-sdk-cli-linux-arm64==1.20.0; platform_system == 'Linux' and (platform_machine == 'arm64' or platform_machine == 'aarch64' or platform_machine == 'ARM64')", + "superdoc-sdk-cli-windows-x64==1.20.0; platform_system == 'Windows' and (platform_machine == 'x86_64' or platform_machine == 'AMD64' or platform_machine == 'amd64')", ] [tool.setuptools] diff --git a/packages/sdk/package.json b/packages/sdk/package.json index b61ab6c105..651097bb6f 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@superdoc-dev/sdk-workspace", - "version": "1.19.1", + "version": "1.20.0", "private": true, "type": "module", "scripts": { diff --git a/packages/sdk/version.json b/packages/sdk/version.json index 5475785bef..d2020727e0 100644 --- a/packages/sdk/version.json +++ b/packages/sdk/version.json @@ -1,3 +1,3 @@ { - "sdkVersion": "1.19.1" + "sdkVersion": "1.20.0" } diff --git a/packages/superdoc/package.json b/packages/superdoc/package.json index 0d76a75dfd..293fae98f9 100644 --- a/packages/superdoc/package.json +++ b/packages/superdoc/package.json @@ -1,7 +1,7 @@ { "name": "superdoc", "type": "module", - "version": "1.43.1", + "version": "1.44.0", "license": "AGPL-3.0", "repository": { "type": "git", From 7480828280d302dc4003e519a51757ec5d3d24c6 Mon Sep 17 00:00:00 2001 From: Andrii Orlov <120495135+andrii-harbour@users.noreply.github.com> Date: Mon, 6 Jul 2026 23:52:46 +0200 Subject: [PATCH 2/2] LLM tools updated docs (#348) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(ai): core preset action-surface reference + LLM tools overview rewrite (WIP) SD-3553 work in progress: architecture diagram, two-preset guide, 40-action reference, receipts/redlining, excludeActions, provider loops (OpenAI + Anthropic, Node + Python), token budget, error codes, troubleshooting, streaming guidance. Co-Authored-By: Claude Fable 5 * docs(examples): core-actions-agent runnable example (Node + Python) SD-3553: minimal headless agent on the core preset — 2 tools, receipts printed as streamable status lines, --tracked flag for redlining. Verified end-to-end: tracked insert lands as in the saved docx. Registered in examples manifest/README; docs nav gains the core-preset reference page; lockfile gains only the example's importer. Co-Authored-By: Claude Fable 5 * docs(ai): cover createAgentToolkit and MCP core preset Bring the pages up to the final 3316 surface: the one-call toolkit as the recommended setup (coherent tools/prompt/dispatch, Node + Python), its row in the SDK functions table, the same-list rule for standalone functions, and the MCP_PRESET=core serving path on both pages. Co-Authored-By: Claude Fable 5 * docs(ai): move_range is direct-only after PR #346 Tracked mode on move_range now fails pre-mutation (block-range deletion cannot be tracked); the reference row and the always-direct list say so and point tracked moves at move_text. Also documents the reversed-range refusal. Co-Authored-By: Claude Fable 5 * docs(ai): dedicated architecture + legacy preset pages; toolkit-first everywhere - New 'How it works' page: the pieces-fit diagram at full size plus the SDK ↔ CLI ↔ LLM mechanics — spawn/handshake/sessions/revisions and a complete tool-call round trip as sequence diagrams (was cramped into one overview section) - New legacy preset page: catalog, search/ref-expiry/mutations behaviors, migration mapping — with an explicit not-recommended-for- new-integrations callout (legacy stays the default for compatibility) - Overview slimmed: compact flow diagram linking to the full page, recommendation callout, quick starts rebuilt on createAgentToolkit; the old 'Legacy tool catalog' heading remains as an anchor-stable pointer (no page links broken, nav only gains entries) - Core preset: quick start via createAgentToolkit; 'Create custom actions' section marked coming soon (defineAction / SD-3317) Co-Authored-By: Claude Fable 5 * docs(ai): nest preset pages under Agents > Presets Nav-only change — page URLs unchanged. Co-Authored-By: Claude Fable 5 * docs(ai): sidebar reads 'Core preset (recommended)' Co-Authored-By: Claude Fable 5 * docs(mcp): real architecture diagrams + core preset coverage on the overview Replaces the ASCII block with a proper flowchart (client ↔ stdio ↔ server internals ↔ disk) and a session-lifecycle sequence diagram, matching the agents architecture page style. Documents the two tool surfaces with the MCP_PRESET=core config snippet (recommended) and keeps the legacy tables as the default reference. Co-Authored-By: Claude Fable 5 * docs(ai): preset-aware guidance across the Agents section Follows the pattern the major API vendors converged on for a recommended-vs-legacy surface split (OpenAI Responses/Chat Completions, Stripe PaymentIntents/Charges): the recommended surface is the default in every guide, legacy is contained with explicit status callouts and a migration path, and no per-page toggles. - best-practices: restructured into shared practices + two main preset sections (core playbook written around receipts/inspect-windows/ selector batching; legacy's search-then-edit guidance preserved intact) - integrations: core-first via createAgentToolkit end to end (steps, all three full examples, Bedrock); legacy delta as one callout - debugging: new 'read the receipt first' section; failure-mode tables split shared / core / legacy - eval-results: core revision-fidelity suite documented with current results (24/28, July 2026) alongside the legacy tool-quality suites All page URLs unchanged. Co-Authored-By: Claude Fable 5 * docs(mcp): note that the intent tools table is the legacy surface Points MCP_PRESET=core users at the core preset reference for its tool set instead. Co-Authored-By: Claude Fable 5 * docs(ai): custom tools guide moves to the legacy page; overview keeps a pointer stub The step-by-step custom-tools walkthrough (provider schemas, dispatcher, merging) is legacy-flavored — it now lives on the legacy preset page. The overview's #creating-custom-tools anchor survives as a stub pointing at both presets: legacy's custom tools and core's 'Creating custom actions' (defineAction — coming soon), which is renamed for parity. Co-Authored-By: Claude Fable 5 * docs(ai): restore eval-results to its published state Internal eval data stays internal — the page is byte-identical to main again. Co-Authored-By: Claude Fable 5 * docs(ai): snippet gate — every agents code block validated against the real SDK New check:ai-snippets script verifies all 43 code blocks across the four agents pages: jsonc action/selector/placement/inspect examples against the LIVE core-preset catalog schemas (action enum + declared arg keys), TypeScript snippets compiled with tsc against the SDK dist types (with conditional ambient context for intentional fragments), Python snippets parsed with imports checked against the package exports, and bash CLI lines replayed against the built CLI. Three snippets were genuinely broken for strict-TS copy-pasters and are fixed: untyped messages arrays (OpenAI + Anthropic loops) and untyped dispatch results now use the exported types / honest result shapes. Co-Authored-By: Claude Fable 5 * docs(ai): address PR #348 review — Bedrock parity, example hardening, gate coverage CodeRabbit findings, all seven fixed: - Bedrock Python migrated to create_agent_toolkit + core preset (was silently teaching legacy); both Bedrock loops now send the toolkit's bundled system prompt instead of a hardcoded string - Vercel conversion snippets fixed for the FLAT vercel dialect — t.function does not exist on those tools and would throw at runtime (pre-existing bug the widened gate caught) - example agent.mjs: --out without a value fails at parse time; the loop and save run inside try/finally so API errors or malformed tool calls can't leak the session/CLI process; JSON.parse feeds back as an error receipt - example agent.py: full parity with the Node twin (same guards, same structured {code, message} error shape, verification-failed suffix) - example README: preset wording matches the actual call shapes - snippet gate now also covers integrations.mdx and best-practices.mdx (65 blocks total) with legacy-action-aware JSON validation Example re-verified end-to-end: tracked insert lands as w:ins in the saved docx; --out guard confirmed. Co-Authored-By: Claude Fable 5 * docs(ai): snippet-gate hardening from PR #348 round 2 - explicit exit when the core catalog is missing the expected tools - aliased named imports (x as y) resolve to the exported name - fence regex accepts info strings (```lang Title) so titled blocks are validated instead of silently skipped Co-Authored-By: Claude Fable 5 * docs(examples): valid manifest metadata for core-actions-agent Codex caught the examples validator rejecting the entry: 'agent-example' is not an allowed kind and subsection was missing. Now minimal-example / agents like its siblings; validate-examples-demos passes. Co-Authored-By: Claude Fable 5 * docs(ai): Codex round-3 fixes — CLI flag casing, cache markers, gate mask Both verified against the built artifacts before fixing: - the preset commands accept only the camelCase --excludeActions flag (kebab --exclude-actions is rejected with Unknown option) — CLI tab updated to what actually parses - cache_control markers only appear with cache: true — the Anthropic provider snippet now passes it and the comment states the condition - the snippet gate's bash check ran with --help appended, which short-circuits before flag validation and masked exactly this class; it now runs the real invocation and fails on unknown-option / INVALID_ARGUMENT while tolerating session preconditions Note for follow-up: --excludeActions is the CLI's only camelCase flag (everything else is kebab-case) — worth accepting both spellings in the CLI parser eventually. Co-Authored-By: Claude Fable 5 * docs(ai): document the real cacheStrategy union values Codex verified against the SDK contract: the values are disabled | explicit | automatic | unsupported (confirmed live: openai/no-cache → disabled, anthropic + cache:true → explicit) — not the invented none/anthropic-ephemeral, which no branch could ever match. Co-Authored-By: Claude Fable 5 * docs(ai): mark the core preset as beta * docs(ai): address final review round — 10-tool catalog, snippet gate in CI, provider prompt caching --------- Co-authored-by: Claude Fable 5 Note: this ports only the public subtree changes from a mixed source commit (19 public paths, 1 non-public path ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: b9165a37070487f320735f13ed98894f2ce8533d Ported-Public-Prefix: superdoc/public --- .github/workflows/ci-docs.yml | 9 + apps/docs/ai/agents/architecture.mdx | 137 ++++++ apps/docs/ai/agents/best-practices.mdx | 230 ++++------ apps/docs/ai/agents/core-preset.mdx | 369 ++++++++++++++++ apps/docs/ai/agents/debugging.mdx | 59 ++- apps/docs/ai/agents/integrations.mdx | 153 +++---- apps/docs/ai/agents/legacy-preset.mdx | 226 ++++++++++ apps/docs/ai/agents/llm-tools.mdx | 466 ++++++++++++-------- apps/docs/ai/mcp/overview.mdx | 94 +++- apps/docs/docs.json | 7 +- apps/docs/package.json | 3 +- apps/docs/scripts/validate-ai-snippets.mjs | 334 ++++++++++++++ examples/README.md | 1 + examples/ai/core-actions-agent/README.md | 44 ++ examples/ai/core-actions-agent/agent.mjs | 113 +++++ examples/ai/core-actions-agent/agent.py | 108 +++++ examples/ai/core-actions-agent/package.json | 13 + examples/manifest.json | 15 + pnpm-lock.yaml | 14 +- 19 files changed, 1962 insertions(+), 433 deletions(-) create mode 100644 apps/docs/ai/agents/architecture.mdx create mode 100644 apps/docs/ai/agents/core-preset.mdx create mode 100644 apps/docs/ai/agents/legacy-preset.mdx create mode 100644 apps/docs/scripts/validate-ai-snippets.mjs create mode 100644 examples/ai/core-actions-agent/README.md create mode 100644 examples/ai/core-actions-agent/agent.mjs create mode 100644 examples/ai/core-actions-agent/agent.py create mode 100644 examples/ai/core-actions-agent/package.json diff --git a/.github/workflows/ci-docs.yml b/.github/workflows/ci-docs.yml index cc676fba6d..39a19f5316 100644 --- a/.github/workflows/ci-docs.yml +++ b/.github/workflows/ci-docs.yml @@ -51,3 +51,12 @@ jobs: - name: Test Code Examples run: pnpm --filter @superdoc/docs test:examples + + - name: Build SDK (Node) + run: pnpm --prefix packages/sdk/langs/node run build + + - name: Build CLI + run: pnpm --prefix apps/cli run build + + - name: Check AI Snippets + run: pnpm --filter @superdoc/docs check:ai-snippets diff --git a/apps/docs/ai/agents/architecture.mdx b/apps/docs/ai/agents/architecture.mdx new file mode 100644 index 0000000000..93b135eeed --- /dev/null +++ b/apps/docs/ai/agents/architecture.mdx @@ -0,0 +1,137 @@ +--- +title: How it works +sidebarTitle: How it works +description: "The moving parts behind SuperDoc's LLM tools: your agent loop, the SDK, the CLI host, and the model — and exactly what crosses each boundary" +keywords: "superdoc architecture, sdk cli host, agent loop, llm tool dispatch, document sessions, json-rpc" +--- + +Three layers cooperate to let a model edit Word documents. Nothing here is required reading to get started — the [overview quick start](/ai/agents/llm-tools#quick-start) works without it — but when you're debugging, budgeting tokens, or deciding where code should run, this is the map. + +## The big picture + +```mermaid +flowchart TB + subgraph browser["🖥 Browser (optional)"] + UI["Your chat UI"] + ED["SuperDoc editor
superdoc / @superdoc-dev/react
renders the same .docx for the user"] + end + + subgraph backend["⚙️ Your backend"] + direction TB + APP["Your agent loop
the broker — owns messages, calls the model,
forwards tool calls, streams status
"] + subgraph sdkbox["@superdoc-dev/sdk (Node) · superdoc-sdk (Python)"] + KIT["createAgentToolkit()
tools · systemPrompt · dispatch"] + CLIENT["Document client
open / save / close sessions"] + end + subgraph clibox["SuperDoc CLI host (bundled binary)"] + HOST["JSON-RPC host process"] + ENGINE["Headless document engine
the same engine the browser editor uses"] + SESS["Sessions
live documents + revision counters"] + end + end + + subgraph provider["☁️ LLM provider"] + LLM["OpenAI · Anthropic · Vercel AI"] + end + + UI -- "user request" --> APP + APP -- "1 · tools + system prompt" --> KIT + APP -- "2 · messages + tools" --> LLM + LLM -- "3 · tool calls" --> APP + APP -- "4 · dispatch(doc, name, args)" --> KIT + KIT -- "typed operations" --> HOST + CLIENT -- "spawn + sessions" --> HOST + HOST --- ENGINE + ENGINE --- SESS + HOST -- "5 · receipts" --> APP + APP -- "status + final summary" --> UI +``` + +The three rules this diagram encodes: + +1. **The SDK is server-side.** `dispatchSuperDocTool` (and the toolkit's `dispatch`) need a session-bound document handle from `createSuperDocClient().open(...)`. Never import the SDK — or the `superdoc` editor package — into browser bundles or Next.js API-route bundling without marking it external. +2. **The model never touches a document.** It only ever sees tool definitions, a system prompt, and tool results. Your loop is the broker for everything. +3. **The engine is the same everywhere.** The CLI host embeds the identical document engine the browser editor uses — edits made headless render exactly the same in the editor. + +## SDK ↔ CLI: where documents actually live + +The SDK packages are deliberately thin: typed clients, the tool surfaces, and prompts. The document engine ships inside the **CLI host binary** (`@superdoc-dev/cli-` for Node, an embedded companion in the Python wheels). + +```mermaid +sequenceDiagram + autonumber + participant SDK as SDK client + participant CLI as CLI host process + participant ENG as Document engine + + SDK->>CLI: spawn (bundled binary), JSON-RPC over stdio + CLI-->>SDK: host.capabilities (handshake) + SDK->>CLI: open({doc: "contract.docx"}) + CLI->>ENG: load .docx into a live session + CLI-->>SDK: session handle (revision 0) + Note over SDK,CLI: every operation now targets this session + SDK->>CLI: blocks.list / comments.create / mutations.apply ... + CLI->>ENG: execute against the live document + CLI-->>SDK: result + revision bump when mutated + SDK->>CLI: save({out: "reviewed.docx"}) + CLI-->>SDK: bytes written + SDK->>CLI: close() +``` + +What crosses this boundary, precisely: + +- **Transport**: newline-delimited JSON-RPC on stdio. One host process serves many sequential requests; sessions keep documents live between calls, which is what makes multi-step agent edits fast. +- **Validation**: every operation's input is validated against the generated contract *inside the host* before it touches the document — a malformed tool call fails loudly with a coded error, never half-applies. +- **Revisions**: the host keeps a per-session revision counter (starts at 0, +1 per mutation). `--expected-revision` / optimistic-concurrency guards compare against this counter. Receipts additionally carry the engine's own revision string for before/after evidence. +- **Tracked changes**: `changeMode: "tracked"` rides the operation into the engine, which records real OOXML revisions (`w:ins`, `w:del`, `w:pPrChange`) — the same marks Word shows. + +If the host can't start, everything surfaces as `Host process disconnected` — the [troubleshooting checklist](/ai/agents/llm-tools#troubleshooting-host-process-disconnected) walks the causes (Gatekeeper, Node version, bundler). + +## LLM ↔ SDK: one tool call, end to end + +The model's entire world is `tools` + `systemPrompt` + tool results. Here's a complete round trip for "rewrite the termination clause as a tracked change" on the core preset: + +```mermaid +sequenceDiagram + autonumber + participant App as Your agent loop + participant LLM as Model + participant SDK as Toolkit dispatch + participant CLI as CLI host + + App->>SDK: createAgentToolkit({provider, preset: "core"}) + SDK-->>App: tools (2) + systemPrompt + dispatch + App->>LLM: system + user message + tools + LLM-->>App: tool call · superdoc_inspect {includeDomains: ["blocks"]} + App->>SDK: dispatch(doc, "superdoc_inspect", args) + SDK->>CLI: read operations (session) + CLI-->>SDK: snapshot (blocks, 1-based ordinals, nodeIds) + SDK-->>App: snapshot → tool result + App->>LLM: tool result + LLM-->>App: tool call · superdoc_perform_action
{action: "rewrite_block", selector, text, changeMode: "tracked"} + App->>SDK: dispatch(doc, "superdoc_perform_action", args) + Note over SDK,CLI: resolve target → validate args → mutate → re-inspect → verify + CLI-->>SDK: receipt {status: "ok", verification, pre/post evidence} + SDK-->>App: receipt → tool result + App->>LLM: receipt + LLM-->>App: final text: "Rewrote clause 8.2 as a tracked change." + App->>CLI: doc.save({out: "reviewed.docx"}) +``` + +Three properties worth internalizing: + +- **The static prefix repeats every turn.** Tools + system prompt are re-sent on each model call and every tool result stays in history — this is why the [token budget](/ai/agents/llm-tools#token-budget) section exists, and why Anthropic callers should enable prompt caching. +- **Receipts are the feedback loop.** The model self-corrects from `status`, `verification`, and `errors[].message` — which is why core-preset receipts carry evidence rather than a bare "ok", and why streaming them to your UI gives users meaningful progress for free. +- **Dispatch is the security boundary you control.** The toolkit's pre-bound `dispatch` enforces the preset and `excludeActions` no matter what the model asks for. + +## Where Python and MCP fit + +- **Python** is the same architecture with the same binary: `superdoc-sdk` talks to the CLI host embedded in its platform wheel (or `SUPERDOC_CLI_BIN`). The core preset's tools, prompt, and dispatch are proxied through the host, so both languages expose byte-identical surfaces. +- **MCP** is an alternative front door for MCP clients (Claude Desktop, IDEs): the `superdoc-mcp` server embeds the document engine in-process and registers either preset's tools (`MCP_PRESET=core`) plus session lifecycle tools. Same engine, same actions — no agent loop of your own required. + +## Related + +- [Overview & agent loops](/ai/agents/llm-tools) +- [Core preset reference](/ai/agents/core-preset) +- [Legacy preset reference](/ai/agents/legacy-preset) +- [Document API](/document-api/overview) — the operation contract the SDK speaks diff --git a/apps/docs/ai/agents/best-practices.mdx b/apps/docs/ai/agents/best-practices.mdx index 13cbd278a3..ac67f0cbce 100644 --- a/apps/docs/ai/agents/best-practices.mdx +++ b/apps/docs/ai/agents/best-practices.mdx @@ -1,206 +1,162 @@ --- title: Best practices sidebarTitle: Best practices -description: "Get better results from LLM document editing: prompting, tool call patterns, and workflow tips" +description: "Get better results from LLM document editing: prompting, tool call patterns, and workflow tips for both presets" keywords: "llm best practices, ai document editing, prompt engineering, superdoc tools, tool calling, document automation" --- -These patterns help your LLM agent produce reliable, efficient document edits. +These patterns help your LLM agent produce reliable, efficient document edits. The first group applies to every integration; the rest is split by preset — [core](/ai/agents/core-preset) (recommended) and [legacy](/ai/agents/legacy-preset) work differently enough that their playbooks are separate. -## Use the bundled system prompt +## Practices for every integration -`getSystemPrompt()` returns a tested prompt that teaches the model how to use SuperDoc tools: targeting, workflow order, and multi-action tools. Load it once and pass it as the system message. +### Use the bundled system prompt — from the same preset as your tools + +Each preset ships the prompt it was designed and evaluated with. Load both through the toolkit so they can never mismatch: ```typescript -import { getSystemPrompt } from '@superdoc-dev/sdk'; +import { createAgentToolkit } from '@superdoc-dev/sdk'; -const systemPrompt = await getSystemPrompt(); -// Pass as the system message in your LLM call +const { tools, systemPrompt, dispatch } = await createAgentToolkit({ + provider: 'openai', + preset: 'core', +}); ``` -You can extend it with task-specific instructions. Append your own rules after the bundled prompt: +Extend it with task-specific rules rather than replacing it: ```typescript -const systemPrompt = await getSystemPrompt(); -const fullPrompt = `${systemPrompt}\n\n## Additional rules\n- Use tracked changes for all edits.\n- Always search before editing.`; +const fullPrompt = `${systemPrompt}\n\n## Additional rules\n- Use tracked changes for all edits.\n- End with a one-sentence summary of what changed.`; ``` -Or start from scratch with something like this: - -````markdown -You edit `.docx` files using SuperDoc intent tools. Be efficient and minimize tool calls. - -## Workflow - -1. **Read**: Use `superdoc_get_content` to understand the document. -2. **Search**: Use `superdoc_search` to find stable handles or block addresses. -3. **Edit**: Use the focused tool that matches the job: - - `superdoc_edit` for insert, replace, delete, undo, redo - - `superdoc_format` for inline or paragraph formatting - - `superdoc_create` for paragraphs and headings - - `superdoc_comment` for comment threads - - `superdoc_track_changes` for review decisions -4. **Batch only when useful**: Use `superdoc_mutations` for preview/apply or atomic multi-step edits. +### Feed errors back -## Rules +Dispatch failures are written for the model to act on. Pass them back as tool results — most models self-correct on the next turn: -- Search before mutating so targets come from fresh results. -- Use focused intent tools for normal edits. -- Use `superdoc_mutations` when you need an atomic batch or preview/apply flow. -- Set `changeMode: "tracked"` when edits need human review. -- Feed tool errors back so you can recover. -```` +```typescript +try { + const result = await dispatch(doc, call.function.name, JSON.parse(call.function.arguments)); + messages.push({ role: 'tool', tool_call_id: call.id, content: JSON.stringify(result) }); +} catch (err: any) { + // Return the error as a tool result: the model will see it and adjust + messages.push({ role: 'tool', tool_call_id: call.id, content: JSON.stringify({ error: err.message }) }); +} +``` -## Read first, search, then edit +### Cache tools and prompts -A typical edit takes 3-5 tool calls: +Tools and the system prompt don't change between requests. Build the toolkit once at startup and reuse it across conversations. On Anthropic, also pass `cache: true` so the tool array carries prompt-caching markers (see [token budget](/ai/agents/llm-tools#token-budget)). -1. `superdoc_get_content`: understand what's in the document -2. `superdoc_search`: find the exact location (returns stable handles/addresses) -3. Edit tool (`superdoc_edit`, `superdoc_format`, etc.): apply the change using targets from search +```typescript +let kit: Awaited> | null = null; -This matters because handles from search results point to the exact right location. If the model guesses a block address instead of searching for it, edits land in the wrong place. +async function ensureToolkit() { + kit ??= await createAgentToolkit({ provider: 'openai', preset: 'core' }); + return kit; +} +``` -## Minimize tool calls +### Use tracked changes for review workflows -Instruct the LLM to plan all edits before calling tools. A well-structured prompt like "Find the termination clause and rewrite it to allow 30-day notice" should take 3-5 calls, not 15. +Set `changeMode: "tracked"` (or instruct it in your appended prompt rules): every AI edit lands as a tracked change users can accept or reject in SuperDoc or Microsoft Word. A "suggest vs. apply directly" toggle in your UI maps 1:1 to this flag. -Batch multiple changes only when atomic execution is genuinely helpful: use `superdoc_mutations` for that. +### Add examples for repeatable workflows -## Prefer markdown insert for multi-block creation +If the same kind of edit runs across many documents, include a concrete tool call example in your system prompt. Models that see a working example of the exact invocation produce correct calls more reliably than models that only see the schema. -When you need to create multiple headings and paragraphs in one operation, use `superdoc_edit` with `type: "markdown"` instead of calling `superdoc_create` once per block. A single markdown insert produces the entire structure in one call. +### Pin your model version -```json -{ - "action": "insert", - "type": "markdown", - "value": "## Executive Summary\n\nThis agreement governs the terms of service.\n\n## Key Provisions\n\nThe following provisions apply to all parties." -} -``` +Use a specific model ID rather than an alias. Aliases can change behavior between releases and break working tool call patterns. -After inserting, apply formatting in a single `superdoc_mutations` batch using `format.apply` steps: one step per block or range. This reduces a workflow that might otherwise take 40+ calls down to 4: read, search, insert, format. +## Core preset (recommended) -## Use focused tools: `superdoc_mutations` is an escape hatch +The core surface is two tools — `superdoc_inspect` and `superdoc_perform_action` — with 40 named actions that resolve their own targets and return receipts. The playbook follows from that. -For straightforward edits, use the focused intent tools (`superdoc_edit`, `superdoc_format`, `superdoc_create`, `superdoc_list`, `superdoc_comment`). They validate arguments, give clear errors, and are easier for models to call correctly. +### Inspect narrowly, then act -Reach for `superdoc_mutations` only when you need: -- Preview/apply semantics (show what will change before committing) -- Atomic multi-step edits (all-or-nothing batch) -- A workflow that would otherwise require refreshing targets between steps +A typical edit is 2–3 calls: one narrow inspect, one action, and (only when the receipt says so) a follow-up. Steer the model away from full-document reads: -## Feed errors back +- `{countsOnly: true}` for orientation; `includeDomains` to fetch only what the task needs. +- On large documents, window with `blockOffset`/`blockLimit` — ordinals are absolute, so windows line up. -`dispatchSuperDocTool` returns structured errors. Pass them back as tool results: most models self-correct on the next turn. +### Trust receipts, not vibes -```typescript -try { - const result = await dispatchSuperDocTool(doc, toolCall.function.name, JSON.parse(toolCall.function.arguments)); - messages.push({ role: 'tool', tool_call_id: toolCall.id, content: JSON.stringify(result) }); -} catch (err: any) { - // Return the error as a tool result: the model will see it and adjust - messages.push({ role: 'tool', tool_call_id: toolCall.id, content: JSON.stringify({ error: err.message }) }); -} -``` +Every action returns real pre/post evidence. Teach your loop (and your users) to read it: -## Choose formatting values from the document +- `status: "ok"` with `verificationPassed: true` — done; don't re-inspect "to be sure." +- `status: "partial"` — some of the work landed; the receipt says which part. Re-inspect, then fix forward. +- `status: "failed"` — nothing changed unless the receipt explicitly says otherwise. `errors[].message` usually contains the exact recovery step, and `recovery`/`revertHint` are machine-usable. -Don't hardcode formatting values. Read them from the document's existing content and match what's already there. +### Batch with selectors, not with repeated calls -**Body text:** Read `fontFamily`, `fontSize`, and `color` from non-empty paragraphs with `alignment: "justify"` or `alignment: "left"`. Set `bold: false` for body paragraphs. +Actions absorb batching: `add_comments` takes `selectors[]` for many blocks in one call; `replace_text` takes `edits[]`; `format_text` takes `targetTexts[]`. One action call with a batch argument beats N calls every time — cheaper, atomic, one receipt. -Many DOCX documents report `underline: true` on all blocks due to style inheritance. This is a DOCX artifact: not intentional formatting. Do not carry it forward when inserting new paragraphs. +### Narrow the surface for your product -**Headings:** Read from existing heading blocks in the document. Scale `fontSize` up relative to body text. Headings are typically bold and sometimes centered: confirm against what's already in the document rather than assuming. +If your product should never delete tables or fill placeholders, exclude those actions — [`excludeActions`](/ai/agents/core-preset#narrowing-the-surface-excludeactions) removes them from the enum, the prompt, and dispatch in one move. A smaller surface is also cheaper per turn. -```typescript -// Get content first, find a representative body paragraph -const content = await superdoc.getContent(); -const bodyParagraph = content.blocks.find( - (b) => b.type === 'paragraph' && b.text?.trim().length > 0 -); -const { fontFamily, fontSize, color } = bodyParagraph?.formatting ?? {}; - -// Use those values when formatting inserted content -``` +### Prompt examples -## Add examples for repeatable workflows +Tested against the core action surface — use as inspiration or few-shot examples: -If the same kind of edit runs across many documents (e.g., always rewriting a specific clause, always adding a comment to a section), include a concrete tool call example in your system prompt. Models that see a working example of the exact tool invocation produce correct calls more reliably than models that only see the schema. +- "Find the termination clause and rewrite it to require 30-day written notice. Use tracked changes." +- "Replace all references to 'Contractor' with 'Service Provider' as tracked changes." +- "Add a comment to every paragraph that mentions personally identifiable information: 'Verify PII handling.'" +- "Number the unnumbered obligation at the end of section 2 like its siblings." +- "Accept all formatting revisions but leave text edits pending review." +- "Move the PREAMBLE section after SCHEDULE A." +- "Add a 2×3 table under the second heading with headers Owner and Stage, then style it." -## Use tracked changes for review workflows +## Legacy preset -Add `changeMode: "tracked"` to edit tool calls, or instruct the model via the system prompt: +The legacy surface is low-level: the model searches for handles, then edits by address. If you're on it, these patterns matter — and [migrating to core](/ai/agents/legacy-preset#migrating-to-core) removes most of them. -``` -Use tracked changes for all edits so a human can review them. -``` +### Read first, search, then edit -This way every AI edit appears as a tracked change that users can accept or reject in SuperDoc or Microsoft Word. +A typical edit takes 3-5 tool calls: -## Pin your model version +1. `superdoc_get_content`: understand what's in the document +2. `superdoc_search`: find the exact location (returns stable handles/addresses) +3. Edit tool (`superdoc_edit`, `superdoc_format`, etc.): apply the change using targets from search -Use a specific model ID (e.g., `gpt-4.1` or `claude-sonnet-4-6`) rather than an alias like `gpt-4o`. Aliases can change behavior between releases and break working tool call patterns. +Handles from search results point to the exact right location. If the model guesses a block address instead of searching, edits land in the wrong place. **Search again after every mutation** — refs expire when the revision bumps. -## Cache tools and prompts +### Prefer markdown insert for multi-block creation -Tools and the system prompt don't change between requests. Load them once at startup and reuse across all conversations. +When creating multiple headings and paragraphs, use `superdoc_edit` with `type: "markdown"` instead of one `superdoc_create` per block: -```typescript -let cachedTools: any[] | null = null; -let cachedSystemPrompt: string | null = null; - -async function ensureToolsLoaded() { - if (!cachedTools) { - const result = await chooseTools({ provider: 'openai' }); - cachedTools = result.tools; - } - if (!cachedSystemPrompt) { - cachedSystemPrompt = await getSystemPrompt(); - } - return { tools: cachedTools, systemPrompt: cachedSystemPrompt }; +```json +{ + "action": "insert", + "type": "markdown", + "value": "## Executive Summary\n\nThis agreement governs the terms of service.\n\n## Key Provisions\n\nThe following provisions apply to all parties." } ``` -## Prompt examples +After inserting, apply formatting in a single `superdoc_mutations` batch using `format.apply` steps — one step per block or range. This reduces a workflow that might otherwise take 40+ calls down to 4: read, search, insert, format. -These prompts have been tested against the SuperDoc tool set. Use them as inspiration for your own workflows, or include them as few-shot examples in your system prompt. +### Use focused tools; `superdoc_mutations` is an escape hatch -### Document review +For straightforward edits, use the focused intent tools — they validate arguments and give clear errors. Reach for `superdoc_mutations` only when you need preview/apply semantics, an atomic multi-step batch, or a workflow that would otherwise require refreshing targets between steps (it resolves all targets before any step executes). -- "Find the termination clause and rewrite it to require 30-day written notice. Use tracked changes." -- "Apply yellow highlight to every sentence that contains an indemnification obligation." -- "Replace all references to 'Contractor' with 'Service Provider' and make each replacement italic with tracked changes enabled." -- "Underline every sentence that references payment terms or late fees." -- "Insert CONFIDENTIAL: DO NOT DISTRIBUTE at the very top of the document and make it bold, red, 14pt." -- "Scan the document for inconsistent capitalization of defined terms and fix them with tracked changes enabled." +### Choose formatting values from the document -### Formatting and structure +Don't hardcode formatting values — read them from existing content and match: -- "Format the entire document in Times New Roman, 12-point." -- "Make all Heading 2 paragraphs bold and set them to 14-point font." -- "Keep each section heading with the paragraph that follows it so they don't split across pages." -- "Remove all extra blank paragraphs and convert all double spaces after periods to single spaces." -- "Right-align all section headings." +- **Body text**: read `fontFamily`, `fontSize`, `color` from non-empty paragraphs; set `bold: false` for body. +- Many DOCX documents report `underline: true` on all blocks due to style inheritance — a DOCX artifact, not intentional formatting. Don't carry it forward. +- **Headings**: read from existing heading blocks; confirm bold/centering against the document rather than assuming. -### Content generation and editing +### Prompt examples -- "Add a new heading 'Learning Objectives' at the top, followed by a bullet list with 3 key takeaways from the document content." -- "Read the document and add a heading 'Executive Summary' at the end, followed by a one-paragraph summary and a bullet list of the 5 key provisions." -- "Find the governing law section and insert a new paragraph after it: 'Any disputes arising under this Agreement shall be resolved through binding arbitration.'" -- "Find all paragraphs that mention 'personally identifiable information' and add a comment: 'Verify PII handling complies with current data retention policy.'" -- "Convert the list of references at the end into a numbered list and restart numbering at 1." - -### Search and replace - -- "Rewrite all dates in this document in the format January 1, 2026." +- "Format the entire document in Times New Roman, 12-point." +- "Make all Heading 2 paragraphs bold and set them to 14-point font." - "Replace every occurrence of 'FY2024' with 'FY2025' throughout the document." -- "Add the § symbol before every section number reference." +- "Insert CONFIDENTIAL: DO NOT DISTRIBUTE at the very top, bold, red, 14pt." +- "Convert the list of references at the end into a numbered list and restart numbering at 1." ## Related -- [LLM tools](/ai/agents/llm-tools): tool catalog and SDK functions +- [Core preset reference](/ai/agents/core-preset) · [Legacy preset reference](/ai/agents/legacy-preset) - [How to use](/ai/agents/integrations): step-by-step integration guide - [Debugging](/ai/agents/debugging): troubleshoot tool call failures -- [Document API](/document-api/overview): the operation set behind the tools +- [How it works](/ai/agents/architecture): SDK ↔ CLI ↔ LLM mechanics diff --git a/apps/docs/ai/agents/core-preset.mdx b/apps/docs/ai/agents/core-preset.mdx new file mode 100644 index 0000000000..d695396ed4 --- /dev/null +++ b/apps/docs/ai/agents/core-preset.mdx @@ -0,0 +1,369 @@ +--- +title: Core preset — the action surface +sidebarTitle: Core preset (recommended) +description: "Two tools, forty deterministic actions: inspect a document, then edit it with named, verifiable operations — including tracked-changes redlining." +keywords: "superdoc core preset, superdoc_perform_action, superdoc_inspect, llm document actions, tracked changes ai, redlining, deterministic document editing" +--- + +The `core` preset is an **actions-only LLM surface**: instead of many low-level tools, the model gets exactly two — + +| Tool | Role | +| --- | --- | +| `superdoc_inspect` | Read: a deterministic snapshot of the document (blocks, lists, tables, comments, tracked changes, …) | +| `superdoc_perform_action` | Write: one of **40 named actions** with flat, validated arguments and a verifiable receipt | + +Every action wraps the underlying [Document API](/document-api/overview) operations with product semantics: it resolves targets deterministically, applies the edit, re-inspects the document, and returns a **receipt** with real pre/post evidence — so your agent loop (and your users) can trust what actually happened. + +The default preset is still `legacy` (the grouped intent tools documented in the [overview](/ai/agents/llm-tools)). The core preset is opt-in: pass `preset: 'core'` everywhere. + +## Quick start + + + + ```typescript + import { createSuperDocClient, createAgentToolkit, type AgentReceipt } from '@superdoc-dev/sdk'; + + const client = createSuperDocClient(); + await client.connect(); + const doc = await client.open({ doc: './contract.docx' }); + + // One call — tools, system prompt, and a pre-bound dispatcher that are + // guaranteed to agree on preset and exclusions. + const { tools, systemPrompt, dispatch } = await createAgentToolkit({ + provider: 'openai', + preset: 'core', + }); + + // ... run your agent loop (see the overview page) ... + const receipt = (await dispatch(doc, 'superdoc_perform_action', { + action: 'replace_text', + edits: [{ find: 'thirty (30) days', replace: 'sixty (60) days' }], + changeMode: 'tracked', + })) as AgentReceipt; + + console.log(receipt.status, receipt.verificationPassed); + ``` + + + ```python + from superdoc import SuperDocClient, create_agent_toolkit + + client = SuperDocClient() + client.connect() + doc = client.open({"doc": "./contract.docx"}) + + # One call — tools, system prompt, and pre-bound dispatchers that are + # guaranteed to agree on preset and exclusions. + kit = create_agent_toolkit({"provider": "openai", "preset": "core"}) + tools, system_prompt = kit["tools"], kit["system_prompt"] + + receipt = kit["dispatch"]( + doc, + "superdoc_perform_action", + { + "action": "replace_text", + "edits": [{"find": "thirty (30) days", "replace": "sixty (60) days"}], + "changeMode": "tracked", + }, + ) + print(receipt["status"], receipt.get("verificationPassed")) + ``` + + + + +Tools, system prompt, and dispatch must all come from the **same preset** — the legacy dispatcher does not know `superdoc_perform_action` and fails with `Unknown tool`. `createAgentToolkit` guarantees this; if you use the standalone functions (`chooseTools`, `getSystemPrompt`, `dispatchSuperDocTool`) instead, pass the same `preset` (and `excludeActions`) to every call. + + +## Reading: `superdoc_inspect` + +`superdoc_inspect` returns a stable snapshot the model can target edits against. Prefer the **narrowest** inspect that answers the question: + +```jsonc +// counts only — cheapest possible orientation call +{ "countsOnly": true } + +// just the lists and tables +{ "includeDomains": ["lists", "tables"] } + +// only headings +{ "includeDomains": ["blocks"], "blockNodeTypes": ["heading"] } +``` + +Available domains: `blocks`, `lists`, `tables`, `comments`, `trackedChanges`, `sections`, `headerFooters`, `styles`, `contentControls`, `fields`, `hyperlinks`, `bookmarks`, `permissionRanges`, `images`. + +### Large documents: windowed reads + +For long documents, read blocks in contiguous windows instead of one giant snapshot. Ordinals are **absolute**, so windows line up across calls: + +```jsonc +{ "includeDomains": ["blocks"], "blockOffset": 0, "blockLimit": 200 } +{ "includeDomains": ["blocks"], "blockOffset": 200, "blockLimit": 200, "omitEmptyBlocks": true, "dropTextPreview": true } +``` + +`omitEmptyBlocks` and `dropTextPreview` trim the payload for a pure reading pass; `blockTextLimit` caps per-block text length. This keeps a single inspect call from dominating your context window (every tool result lives in conversation history and is re-billed as prompt tokens on every later turn). + + +All ordinals shown by `superdoc_inspect` (`blockOrdinal`, `paragraphOrdinal`, `headingOrdinal`, `tableOrdinal`, …) are **1-based**, and selectors accept the same 1-based values. + + +## Editing: `superdoc_perform_action` + +One tool, one `action` argument, flat parameters. The dispatcher statically validates arguments against the action's declared schema (unknown keys are rejected with a descriptive error) before anything touches the document. + +### Targeting: selectors + +Actions that operate on a specific block accept a `selector`: + +| Selector | Shape | Use when | +| --- | --- | --- | +| Node id | `{kind:"nodeId", nodeId}` | You have a `nodeId` from `superdoc_inspect` (most precise) | +| Ordinal | `{kind:"ordinal", ordinalKind:"paragraphOrdinal"\|"headingOrdinal"\|"tableOrdinal"\|"listOrdinal"\|"sectionOrdinal"\|"bodyParagraphOrdinal"\|"blockOrdinal", value:N}` | "the 3rd paragraph", "the 2nd table" (1-based) | +| Text search | `{kind:"textSearch", terms:[...], match?:"all"\|"any", occurrence?:N, nodeTypes?:[...]}` | You know the text but not the position | +| Table cell | `{kind:"tableCell", tableOrdinal, rowIndex, columnIndex}` | Cell-scoped edits | +| Relative | `{kind:"relative", position:"after"\|"before", target:selector}` | "the paragraph after the heading X" | + +### Placement + +Insert-style actions accept a `placement`: + +```jsonc +{ "at": "document_end" } +{ "at": "document_start" } +{ "at": "after", "selector": { "kind": "textSearch", "terms": ["Definitions"] } } +{ "at": "before", "selector": { "kind": "nodeId", "nodeId": "p42" } } +``` + +### Tracked changes: `changeMode` + +Most mutating actions accept `changeMode: "tracked"`. In tracked mode the edit is recorded as a **redline suggestion** — a tracked insert/delete/format change the user (or the model) can accept or reject later — instead of being applied directly. This is the backbone of review workflows: + + + + ```jsonc + { + "action": "rewrite_block", + "selector": { "kind": "ordinal", "ordinalKind": "paragraphOrdinal", "value": 4 }, + "text": "Either party may terminate this Agreement on sixty (60) days' written notice.", + "changeMode": "tracked" + } + ``` + + + ```jsonc + { + "action": "rewrite_block", + "selector": { "kind": "ordinal", "ordinalKind": "paragraphOrdinal", "value": 4 }, + "text": "Either party may terminate this Agreement on sixty (60) days' written notice." + } + ``` + + + +A "suggest vs. apply directly" toggle in your chat UI maps 1:1 to setting `changeMode` on every mutating call. Review then happens with the same action surface: + +- `accept_tracked_changes` / `reject_tracked_changes` — optionally filtered by `author` or `changeType` (`insert` | `delete` | `replacement` | `format`) +- `undo_changes` / `redo_changes` — deterministic history recovery (`untilMarker` restores until a rendered clause marker like `"2.1."` reappears) + +A few actions are **always direct** (not tracked) and say so in their reference entry: `move_range`, `split_list`, `set_paragraph_spacing`, `insert_page_break`, `add_hyperlink`, `style_table`. Requesting `changeMode:"tracked"` on `move_range` fails with nothing changed — use `move_text` for tracked text-span moves. + +### Receipts + +Every action returns a receipt — not just "ok", but evidence: + +```jsonc +{ + "status": "ok", // "ok" | "partial" | "failed" + "intent": "replace_text", + "preSnapshot": { "revision": "41", "counts": { "paragraphs": 58, "trackedChanges": 0 } }, + "postSnapshot": { "revision": "42", "counts": { "paragraphs": 58, "trackedChanges": 2 } }, + "verification": [ + { "check": { "kind": "text-replaced", "find": "thirty (30) days" }, "passed": true } + ], + "verificationPassed": true, + "editsApplied": 2 +} +``` + +What to rely on: + +- **`status`** — `partial` means some of the requested work landed (the receipt says which part); `failed` means nothing changed unless the receipt explicitly says otherwise. +- **`verification`** — post-edit checks the action ran against a fresh snapshot (counts deltas, placement adjacency, text presence). `verificationPassed` is the roll-up. +- **`errors[]`** — failures carry a `code`, a human-readable `message` written for the model to act on, and often a structured `recovery` (`{kind: "reinspect" | "retry" | "revert", call?}`) plus a paste-ready `revertHint`. +- **`formattingMatched`** — insert actions that blend new content into its surroundings (e.g. `add_list_items`) report the font/size/style they copied from neighbors, so the model knows not to re-format. +- **List caps** — long per-item lists (`executedOperations`, `selectedTargets`) are capped at 8 entries with a `*Count` field preserving the true total, to keep receipts from bloating your conversation history. + +### Action reference + +Forty actions, grouped. Arguments marked `?` are optional; most mutating actions also accept `changeMode`. + +#### Text & structure + +| Action | Arguments | Notes | +| --- | --- | --- | +| `insert_paragraphs` | `texts[]` (or `text`), `placement?`, `headingLevel?` | First item can become a heading (1–6) | +| `insert_heading` | `text`, `level`, `placement?` | | +| `replace_text` | `edits[{find, replace}]`, `selector?`, `caseSensitive?` | Selector scopes all edits to one block | +| `delete_text` | `finds[]`, `selector?`, `caseSensitive?` | Scope with `selector` to delete stray whitespace safely | +| `append_list` | `items[]`, `kind?: ordered\|bullet`, `headingText?`, `placement?` | With `placement`, builds the list at that block instead of document end | +| `create_table` | `rows`, `columns`, `cellTexts?`, `placement?` | Tracked mode makes the whole insertion one tracked change | +| `rewrite_block` | `selector`, `text` | Replaces a block's text; tracked mode produces a redline | +| `fill_placeholders` | `values[]` and/or `fields[{label?, value}]` | Fills template placeholders | +| `move_range` | `fromText`, `toText?`, `afterText` or `beforeText` | Moves a block range or visual "section" identified by text. Auto-extends to the section end when `toText` is omitted. Direct-only: `changeMode:"tracked"` is refused with nothing changed (use `move_text` for tracked moves). Also refuses reversed ranges and ranges containing tables/lists/images — move those with `move_table` or narrow the range | + +#### Lists & numbering + +| Action | Arguments | Notes | +| --- | --- | --- | +| `convert_list` | `kind`, one of: `listOrdinal?`/`anchorText?`, `fromMarker`+`toMarker`, `fromText`+`toText` | Converts lists, rendered clause ranges ("2.1."–"2.3."), or plain paragraphs to a list **in place** | +| `attach_numbering` | `anchorText` or `nodeId`, `likeMarker` | Numbers a block at the same scheme/level as the sibling rendering `likeMarker` (e.g. `"10."`); tracked mode records the former state | +| `add_list_items` | `anchorText` + `entries[{text, level?}]` | **The** way to add items into an existing list. `level` is relative to the anchor (0 same, positive nests, negative promotes — e.g. `-1` from item "12(e)" creates top-level item 13). Inherits the anchor's look automatically | +| `split_list` | `anchorText`, `restartNumbering?` (default true) | Splits one list into two at an item. Direct edit | + +#### History + +| Action | Arguments | Notes | +| --- | --- | --- | +| `undo_changes` | `untilMarker?`, `steps?` (1–25) | Steps history back until the marker reappears — deterministic revert | +| `redo_changes` | `steps?` (default 1) | Re-applies edits after an undo overshot. Only valid before any new edit | + +#### Moving text + +| Action | Arguments | Notes | +| --- | --- | --- | +| `move_text` | `text`, `afterText?` | Direct by default (requires `afterText`). `changeMode:"tracked"` records the move as a redline: tracked delete at the source + tracked insert at the destination — accept keeps the move, reject restores the original order | + +#### Comments + +| Action | Arguments | Notes | +| --- | --- | --- | +| `comment_paragraphs` | `commentText`, `scope?: all\|body`, `excludeBlockQuotes?` | One comment per paragraph | +| `add_comments` | `commentText`, `selector` or `selectors[]` | Batch many targets into `selectors[]` in ONE call | +| `resolve_comments` | `anchorText?`, `reopen?` | Omit `anchorText` to resolve all open comments | +| `reply_to_comment` | `commentText`, `anchorText` or `commentId` | Adds a **threaded** reply, not a new top-level comment | + +#### Tracked-change review + +| Action | Arguments | Notes | +| --- | --- | --- | +| `accept_tracked_changes` | `author?`, `changeType?` | e.g. `changeType:"format"` accepts only formatting revisions | +| `reject_tracked_changes` | `author?`, `changeType?` | | + +#### Formatting + +| Action | Arguments | Notes | +| --- | --- | --- | +| `format_text` | `bold?`/`italic?`/`underline?`/`strike?`, `highlight?`, `color?`, `fontSize?`, `targetText`/`targetTexts[]`/`selector`, `caseSensitive?` | Applies to every occurrence; tracked-safe | +| `apply_style` | `selector`, one of `styleId`, `headingLevel`, `likeText` | `likeText` copies another block's style **and** effective look | +| `format_paragraph` | `selector`, `alignment` | Tracked mode records the former alignment (`w:pPrChange`) | +| `set_paragraph_spacing` | `selector`, `lineSpacing?`, `spaceBefore?`, `spaceAfter?` | Never insert blank paragraphs for spacing. Direct edit | +| `normalize_body_font_size` | `fontSize` | Whole-body font size | +| `set_font_family` | `fontFamily`, `selector?` or `targetText`/`targetTexts[]?` | Omit both to set the whole body typeface | +| `apply_letter_spacing` | `selector`, `letterSpacing` | | + +#### Layout, links, media + +| Action | Arguments | Notes | +| --- | --- | --- | +| `insert_page_break` | `selector` | Sets `pageBreakBefore` on the block — never pushes content with empty paragraphs. Direct edit | +| `add_hyperlink` | `text`, `url`, `tooltip?` | Turns existing text into a link. Direct edit | +| `insert_toc` | `title?`, `placement?` | Table of contents | + +#### Tables + +| Action | Arguments | Notes | +| --- | --- | --- | +| `style_table` | `tableOrdinal?`, `accentColor?` | Professional look in one call: accent header, bold labels, banded rows. Direct edit | +| `move_table` | `tableOrdinal?`, `placement` | Moves the whole table with all content in one call | +| `delete_table` | `tableOrdinal?` | Deletes an entire table | +| `insert_table_row` | `tableOrdinal?`, `rowIndex?`, `position?`, `cellTexts?`, `dryRun?` | | +| `insert_table_column` | `tableOrdinal?`, `columnIndex?`, `position?`, `headerText?` | | +| `delete_table_row` | `tableOrdinal?`, `rowIndex` | | +| `delete_table_column` | `tableOrdinal?`, `columnIndex` | | +| `split_table` | `tableOrdinal?`, `rowIndex`, `separatorText?` | | + +## Narrowing the surface: `excludeActions` + +Hide actions you don't want the model to see. The exclusion narrows **everything coherently**: the `action` enum, the argument schema, the per-action documentation lines in the system prompt, and the dispatch guard (an excluded action is refused even if the model guesses its name). + +The safest way to use it is `createAgentToolkit` — one options object, all three surfaces guaranteed to agree: + +```typescript +const { tools, systemPrompt, dispatch } = await createAgentToolkit({ + provider: 'anthropic', + preset: 'core', + excludeActions: ['delete_table', 'fill_placeholders'], +}); +``` + +With the standalone functions, pass the same list everywhere: + + + + ```typescript + const { tools } = await chooseTools({ + provider: 'anthropic', + preset: 'core', + excludeActions: ['delete_table', 'fill_placeholders'], + }); + const prompt = await getSystemPrompt('core', { excludeActions: ['delete_table', 'fill_placeholders'] }); + + // Defense in depth: pass the same list at dispatch time. + await dispatchSuperDocTool(doc, name, args, { + preset: 'core', + excludeActions: ['delete_table', 'fill_placeholders'], + }); + ``` + + + ```python + tools = choose_tools({ + "provider": "anthropic", + "preset": "core", + "excludeActions": ["delete_table", "fill_placeholders"], + })["tools"] + prompt = get_system_prompt("core", exclude_actions=["delete_table", "fill_placeholders"]) + + dispatch_superdoc_tool(doc, name, args, preset="core", + exclude_actions=["delete_table", "fill_placeholders"]) + ``` + + + ```bash + superdoc preset get-tools --preset core --provider anthropic \ + --excludeActions delete_table,fill_placeholders + superdoc preset get-system-prompt --preset core \ + --excludeActions delete_table,fill_placeholders + ``` + + + +Unknown action names in the list throw immediately (typo protection). The legacy preset ignores exclusion options entirely. + +## The system prompt + +`getSystemPrompt('core')` returns the prompt the action surface was **evaluated with**: document-model vocabulary (visual sections, rendered markers, effective formatting), the full per-action argument documentation, targeting conventions, tracked-changes rules, and receipt-reading discipline (trust the receipt, re-inspect on `partial`, use `revertHint` on failures). + +- **Use it as-is** for the best out-of-the-box behavior — the eval suite scores this exact prompt. +- **Extend it** by appending your domain instructions (tone, house style, what to never touch) at the end. +- **Replacing it entirely is not recommended**: the per-action lines teach argument shapes the schema alone can't convey. If you do, keep the action documentation block. + +## Creating custom actions + + +**Coming soon.** A first-class authoring kit (`defineAction`) is in the works: define your own action with a typed schema and handler, register it alongside the built-in forty, and it appears in the tool enum, the system prompt's action list, and the dispatch surface automatically — same receipts, same exclusion support. Until it ships, custom capabilities can be added as [custom tools](/ai/agents/legacy-preset#creating-custom-tools) registered next to the preset's own tools in your loop. + + +## Over MCP + +The core preset is also available through the SuperDoc MCP server: start it with `MCP_PRESET=core` and clients get the session lifecycle tools (`superdoc_open` / `superdoc_save` / `superdoc_close`) plus `superdoc_inspect` and `superdoc_perform_action`, registered from the same catalog `chooseTools()` serves — the MCP surface cannot drift from the SDK surface. Server instructions use the core MCP prompt automatically. + +## Experimental: `superdoc_execute_code` + +The SDK can dispatch a third tool, `superdoc_execute_code` (model-authored JavaScript against a synchronous in-process Document API). It is **work-in-progress and deliberately not advertised**: it is absent from `chooseTools()` results and from the served system prompt, and its behavior may change. It will ship behind an explicit safety flag in a future release. Don't build on it yet. + +## Related + +- [Overview & agent loops](/ai/agents/llm-tools): providers, token budget, error codes, troubleshooting +- [Best practices](/ai/agents/best-practices) +- [Document API](/document-api/overview): the operations the actions wrap diff --git a/apps/docs/ai/agents/debugging.mdx b/apps/docs/ai/agents/debugging.mdx index e811346c94..2f9fa8fb4a 100644 --- a/apps/docs/ai/agents/debugging.mdx +++ b/apps/docs/ai/agents/debugging.mdx @@ -9,7 +9,7 @@ When tool calls fail or produce unexpected results, use these patterns to diagno ## LLM tools wrap the Document API -Every LLM tool call maps to a [Document API](/document-api/overview) operation under the hood. `superdoc_edit` with `action: "replace"` calls the same function as `doc.replace()`. +Every LLM tool call maps to a [Document API](/document-api/overview) operation under the hood. On the core preset, `superdoc_perform_action` with `action: "replace_text"` resolves its targets and then runs the same operations as `doc.replace()`; on legacy, `superdoc_edit` with `action: "replace"` calls it directly. This gives you a clear debugging strategy: @@ -20,8 +20,8 @@ This gives you a clear debugging strategy: ```typescript // Instead of going through the LLM, test the operation directly: const result = await doc.replace({ - target: { handle: 'some-handle' }, - content: 'New text', + ref: 'ref-from-a-recent-search', // refs/handles come from search results + text: 'New text', }); console.log(result); // Does this work? ``` @@ -32,6 +32,10 @@ This narrows every issue to one of two layers: the operation or the prompt. Add logging around `dispatchSuperDocTool` to see exactly what the model is requesting and what comes back. + +`dispatchSuperDocTool(doc, name, args)` without options dispatches against the **legacy default**. Pass `{ preset: 'core' }` when your tools came from the core preset — or use the toolkit's pre-bound `dispatch`, which can't mismatch. The examples below assume core. + + ```typescript for (const toolCall of choice.message.tool_calls) { const args = JSON.parse(toolCall.function.arguments); @@ -40,7 +44,7 @@ for (const toolCall of choice.message.tool_calls) { console.log(`[agent] tool: ${toolCall.function.name}`, JSON.stringify(args, null, 2)); try { - const result = await dispatchSuperDocTool(doc, toolCall.function.name, args); + const result = await dispatchSuperDocTool(doc, toolCall.function.name, args, { preset: 'core' }); // Log the result (truncate large responses) const resultStr = JSON.stringify(result); @@ -60,6 +64,17 @@ What to look for in logs: - **Targets**: are handles/addresses from a recent search, or did the model guess? - **Result**: did the operation return data or an error? +## Read the receipt first (core preset) + +On the core preset, most "failures" aren't thrown errors — they're receipts doing their job. Before reaching for logs: + +- `status: "failed"` + `errors[].code: "MATCH_NOT_FOUND"` — the target text/element wasn't found and **nothing was changed**. The message names what to fix; `recovery` is machine-usable (`reinspect` / `retry` / `revert` with a paste-ready call). +- `status: "partial"` — part of the batch landed; the receipt reports which part (`editsApplied` / `editsSkipped`, capped lists with `*Count` totals). Re-inspect, then fix forward. +- `verificationPassed: false` on an `ok` receipt — the edit applied but a post-check (placement adjacency, count delta) disagreed; the `verification` array shows which check. +- `INVALID_ARGUMENT` with `excluded: true` — the action is excluded by your `excludeActions` configuration, not broken. + +Feeding the whole receipt back as the tool result is usually all the "debugging" the model needs. + ## Error shapes `dispatchSuperDocTool` throws errors in two categories: @@ -67,7 +82,7 @@ What to look for in logs: **Validation errors**: bad arguments before the operation runs: ```json { "error": "Missing required parameter: action" } -{ "error": "Unknown action 'bold' for tool superdoc_format. Valid actions: inline, set_style, set_alignment, set_indentation, set_spacing" } +{ "error": "Unknown action 'bold' for tool superdoc_format. Valid actions: inline, set_alignment, set_direction, set_flow_options, set_indentation, set_spacing, set_style" } { "error": "Parameter 'target' is required for action 'replace'" } ``` @@ -81,17 +96,35 @@ Both types are returned as strings in `err.message`. Pass them back as tool resu ## Common failure modes +Shared (either preset): + | Symptom | Cause | Fix | | --- | --- | --- | -| Model calls the wrong tool | System prompt missing or too vague | Use `getSystemPrompt()` or add workflow instructions | -| "Target not found" errors | Model uses stale or guessed handles | Instruct model to always search before editing | -| Edits land in the wrong place | Model invented a block address | Use `superdoc_search` to get fresh handles | +| Model calls the wrong tool | System prompt missing, or paired with the wrong preset's tools | Use `createAgentToolkit` so prompt and tools always match | +| `Unknown tool` on dispatch | Tools from one preset dispatched through another | Same fix — one preset for tools, prompt, and dispatch | | Infinite tool call loop | Model never reaches a stopping point | Add a max iterations guard (see below) | -| Model doesn't use tools at all | Tools not passed to the API call | Verify `chooseTools()` result is in the `tools` param | +| Model doesn't use tools at all | Tools not passed to the API call | Verify the toolkit's `tools` is in the `tools` param | | "Missing required parameter" | Model forgot `action` or another field | Check the tool schema: add examples to the prompt | | Collaboration edits not appearing | SDK not in the same collab room | Verify the collaboration URL and documentId match | | Operation works via API but fails via tool | Model passes wrong argument types/names | Log the parsed arguments and compare to the API signature | +Core preset: + +| Symptom | Cause | Fix | +| --- | --- | --- | +| `MATCH_NOT_FOUND` receipts | Target text drifted after earlier edits | Re-inspect, target current text (nothing was changed — safe to retry) | +| `partial` receipts on batches | Some edits' targets matched, others didn't | Read `editsApplied`/`editsSkipped`; retry only the skipped ones | +| Action refused (`excluded: true`) | Your `excludeActions` config | Expected — the guard is doing its job | +| Tracked edit landed direct | `changeMode` missing on that call | Set `changeMode: "tracked"` per call or via prompt rules | + +Legacy preset: + +| Symptom | Cause | Fix | +| --- | --- | --- | +| "Target not found" errors | Stale or guessed handles | Always search before editing; search again after every mutation | +| `REVISION_MISMATCH` | Ref fetched before a mutation, used after | Use `superdoc_mutations` for multi-block edits, or re-search between edits | +| Edits land in the wrong place | Model invented a block address | Use `superdoc_search` to get fresh handles | + ## Inspect tools directly Dump the tool schemas to verify the SDK loaded correctly: @@ -99,12 +132,12 @@ Dump the tool schemas to verify the SDK loaded correctly: ```typescript import { listTools, getToolCatalog } from '@superdoc-dev/sdk'; -// See all tools for a provider -const tools = await listTools('openai'); +// See all tools for a provider — omitting the preset argument selects legacy +const tools = await listTools('openai', 'core'); console.log(JSON.stringify(tools, null, 2)); // Get the full catalog with metadata -const catalog = await getToolCatalog(); +const catalog = await getToolCatalog('core'); console.log(`Loaded ${catalog.tools.length} tools`); ``` @@ -124,7 +157,7 @@ while (iterations++ < MAX_ITERATIONS) { if (!message.tool_calls?.length) break; for (const call of message.tool_calls) { - const result = await dispatchSuperDocTool(doc, call.function.name, JSON.parse(call.function.arguments)); + const result = await dispatchSuperDocTool(doc, call.function.name, JSON.parse(call.function.arguments), { preset: 'core' }); messages.push({ role: 'tool', tool_call_id: call.id, content: JSON.stringify(result) }); } } diff --git a/apps/docs/ai/agents/integrations.mdx b/apps/docs/ai/agents/integrations.mdx index d90092d63c..6e9dfa9e01 100644 --- a/apps/docs/ai/agents/integrations.mdx +++ b/apps/docs/ai/agents/integrations.mdx @@ -50,37 +50,47 @@ await client.connect(); const doc = await client.open({ doc: './contract.docx' }); ``` -## Step 3: Load tools and system prompt +## Step 3: Load the toolkit -Load the tool definitions for your provider and the default system prompt. Both can be cached: they don't change between requests. +One call returns the tool definitions for your provider, the matching system prompt, and a pre-bound dispatcher. This guide uses the [core preset](/ai/agents/core-preset) (recommended); everything can be cached — it doesn't change between requests. ```typescript - import { chooseTools, getSystemPrompt } from '@superdoc-dev/sdk'; + import { createAgentToolkit } from '@superdoc-dev/sdk'; - const { tools } = await chooseTools({ provider: 'openai' }); - const systemPrompt = await getSystemPrompt(); + const { tools, systemPrompt, dispatch } = await createAgentToolkit({ + provider: 'openai', + preset: 'core', + }); ``` ```typescript - import { chooseTools, getSystemPrompt } from '@superdoc-dev/sdk'; + import { createAgentToolkit } from '@superdoc-dev/sdk'; - const { tools } = await chooseTools({ provider: 'anthropic' }); - const systemPrompt = await getSystemPrompt(); + const { tools, systemPrompt, dispatch } = await createAgentToolkit({ + provider: 'anthropic', + preset: 'core', + }); ``` ```typescript - import { chooseTools, getSystemPrompt } from '@superdoc-dev/sdk'; + import { createAgentToolkit } from '@superdoc-dev/sdk'; - const { tools: sdkTools } = await chooseTools({ provider: 'vercel' }); - const systemPrompt = await getSystemPrompt(); + const { tools: sdkTools, systemPrompt, dispatch } = await createAgentToolkit({ + provider: 'vercel', + preset: 'core', + }); ``` + +On the legacy preset instead? Pass `preset: 'legacy'` (or omit it — legacy is the default for backwards compatibility). The loop below is identical; what changes is the tool surface the model sees and that results are raw operation results rather than receipts. See the [legacy preset page](/ai/agents/legacy-preset) for its behaviors. + + ## Step 4: Run the agent loop The agent loop sends messages to the LLM, dispatches tool calls, feeds results back, and repeats until the model is done. @@ -89,8 +99,7 @@ The agent loop sends messages to the LLM, dispatches tool calls, feeds results b ```typescript import OpenAI from 'openai'; - import { dispatchSuperDocTool } from '@superdoc-dev/sdk'; - + const openai = new OpenAI(); // uses OPENAI_API_KEY env var const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [ @@ -119,8 +128,7 @@ The agent loop sends messages to the LLM, dispatches tool calls, feeds results b if (toolCall.type !== 'function') continue; try { - const result = await dispatchSuperDocTool( - doc, + const result = await dispatch(doc, toolCall.function.name, JSON.parse(toolCall.function.arguments), ); @@ -144,15 +152,14 @@ The agent loop sends messages to the LLM, dispatches tool calls, feeds results b **What's happening:** 1. The system prompt teaches the model how to use SuperDoc tools. - 2. The `while(true)` loop calls OpenAI, checks for tool calls, dispatches them via `dispatchSuperDocTool`, and feeds results back. + 2. The `while(true)` loop calls OpenAI, checks for tool calls, dispatches them via the toolkit's `dispatch`, and feeds results back. 3. When the model returns `finish_reason: 'stop'` (no more tool calls), the loop ends. 4. Errors are caught and returned as tool results so the model can see what went wrong and retry. ```typescript import Anthropic from '@anthropic-ai/sdk'; - import { dispatchSuperDocTool } from '@superdoc-dev/sdk'; - + const anthropic = new Anthropic(); // uses ANTHROPIC_API_KEY env var const messages: Anthropic.MessageParam[] = [ @@ -161,7 +168,7 @@ The agent loop sends messages to the LLM, dispatches tool calls, feeds results b while (true) { const response = await anthropic.messages.create({ - model: 'claude-sonnet-4-6', + model: 'claude-sonnet-5', max_tokens: 4096, system: systemPrompt, messages, @@ -183,8 +190,7 @@ The agent loop sends messages to the LLM, dispatches tool calls, feeds results b if (block.type !== 'tool_use') continue; try { - const result = await dispatchSuperDocTool( - doc, + const result = await dispatch(doc, block.name, block.input as Record, ); @@ -220,18 +226,17 @@ The agent loop sends messages to the LLM, dispatches tool calls, feeds results b ```typescript import { generateText, jsonSchema, stepCountIs } from 'ai'; import { openai } from '@ai-sdk/openai'; - import { dispatchSuperDocTool } from '@superdoc-dev/sdk'; - + // Convert SDK tool definitions into Vercel AI tool objects with execute functions + // The vercel dialect is flat: { name, description, inputSchema }. const tools: Record = {}; for (const t of sdkTools as any[]) { - const fn = t.function; - tools[fn.name] = { - description: fn.description, - inputSchema: jsonSchema>(fn.parameters), + tools[t.name] = { + description: t.description, + inputSchema: jsonSchema>(t.inputSchema), execute: async (args: Record) => { try { - return await dispatchSuperDocTool(doc, fn.name, args); + return await dispatch(doc, t.name, args); } catch (err: any) { return { error: err.message }; } @@ -255,7 +260,7 @@ The agent loop sends messages to the LLM, dispatches tool calls, feeds results b **What's happening:** - 1. SDK tool definitions are converted into Vercel AI tool objects: each with an `execute` function that calls `dispatchSuperDocTool`. + 1. SDK tool definitions are converted into Vercel AI tool objects: each with an `execute` function that calls the toolkit's `dispatch`. 2. `generateText` handles the agent loop internally: it calls the model, executes tools, feeds results back, and repeats. 3. `stopWhen: stepCountIs(10)` sets a max iteration guard. 4. No manual `while(true)` loop needed: Vercel AI manages it for you. @@ -278,21 +283,18 @@ A complete, copy-pasteable script that opens a document, runs an agent, saves, a ```typescript import OpenAI from 'openai'; - import { - createSuperDocClient, - chooseTools, - dispatchSuperDocTool, - getSystemPrompt, - } from '@superdoc-dev/sdk'; + import { createSuperDocClient, createAgentToolkit } from '@superdoc-dev/sdk'; // 1. Open the document const client = createSuperDocClient(); await client.connect(); const doc = await client.open({ doc: './contract.docx' }); - // 2. Load tools and system prompt - const { tools } = await chooseTools({ provider: 'openai' }); - const systemPrompt = await getSystemPrompt(); + // 2. Load the toolkit (tools + system prompt + dispatch, core preset) + const { tools, systemPrompt, dispatch } = await createAgentToolkit({ + provider: 'openai', + preset: 'core', + }); // 3. Build the conversation const openai = new OpenAI(); @@ -321,8 +323,7 @@ A complete, copy-pasteable script that opens a document, runs an agent, saves, a if (toolCall.type !== 'function') continue; try { - const result = await dispatchSuperDocTool( - doc, + const result = await dispatch(doc, toolCall.function.name, JSON.parse(toolCall.function.arguments), ); @@ -350,21 +351,18 @@ A complete, copy-pasteable script that opens a document, runs an agent, saves, a ```typescript import Anthropic from '@anthropic-ai/sdk'; - import { - createSuperDocClient, - chooseTools, - dispatchSuperDocTool, - getSystemPrompt, - } from '@superdoc-dev/sdk'; + import { createSuperDocClient, createAgentToolkit } from '@superdoc-dev/sdk'; // 1. Open the document const client = createSuperDocClient(); await client.connect(); const doc = await client.open({ doc: './contract.docx' }); - // 2. Load tools and system prompt - const { tools } = await chooseTools({ provider: 'anthropic' }); - const systemPrompt = await getSystemPrompt(); + // 2. Load the toolkit (tools + system prompt + dispatch, core preset) + const { tools, systemPrompt, dispatch } = await createAgentToolkit({ + provider: 'anthropic', + preset: 'core', + }); // 3. Build the conversation const anthropic = new Anthropic(); @@ -375,7 +373,7 @@ A complete, copy-pasteable script that opens a document, runs an agent, saves, a // 4. Agent loop while (true) { const response = await anthropic.messages.create({ - model: 'claude-sonnet-4-6', + model: 'claude-sonnet-5', max_tokens: 4096, system: systemPrompt, messages, @@ -395,8 +393,7 @@ A complete, copy-pasteable script that opens a document, runs an agent, saves, a if (block.type !== 'tool_use') continue; try { - const result = await dispatchSuperDocTool( - doc, + const result = await dispatch(doc, block.name, block.input as Record, ); @@ -428,12 +425,7 @@ A complete, copy-pasteable script that opens a document, runs an agent, saves, a ```typescript import { generateText, jsonSchema, stepCountIs } from 'ai'; import { openai } from '@ai-sdk/openai'; - import { - createSuperDocClient, - chooseTools, - dispatchSuperDocTool, - getSystemPrompt, - } from '@superdoc-dev/sdk'; + import { createSuperDocClient, createAgentToolkit } from '@superdoc-dev/sdk'; // 1. Open the document const client = createSuperDocClient(); @@ -441,19 +433,21 @@ A complete, copy-pasteable script that opens a document, runs an agent, saves, a const doc = await client.open({ doc: './contract.docx' }); // 2. Load tools and system prompt - const { tools: sdkTools } = await chooseTools({ provider: 'vercel' }); - const systemPrompt = await getSystemPrompt(); + const { tools: sdkTools, systemPrompt, dispatch } = await createAgentToolkit({ + provider: 'vercel', + preset: 'core', + }); // 3. Convert SDK tools into Vercel AI tool objects + // The vercel dialect is flat: { name, description, inputSchema }. const tools: Record = {}; for (const t of sdkTools as any[]) { - const fn = t.function; - tools[fn.name] = { - description: fn.description, - inputSchema: jsonSchema>(fn.parameters), + tools[t.name] = { + description: t.description, + inputSchema: jsonSchema>(t.inputSchema), execute: async (args: Record) => { try { - return await dispatchSuperDocTool(doc, fn.name, args); + return await dispatch(doc, t.name, args); } catch (err: any) { return { error: err.message }; } @@ -486,22 +480,25 @@ A complete, copy-pasteable script that opens a document, runs an agent, saves, a ### AWS Bedrock -Use `chooseTools({ provider: 'anthropic' })` and convert to Bedrock's `toolSpec` shape: +Use the toolkit with `provider: 'anthropic'` and convert to Bedrock's `toolSpec` shape: ```typescript import { BedrockRuntimeClient, ConverseCommand } from '@aws-sdk/client-bedrock-runtime'; - import { createSuperDocClient, chooseTools, dispatchSuperDocTool } from '@superdoc-dev/sdk'; + import { createSuperDocClient, createAgentToolkit } from '@superdoc-dev/sdk'; const client = createSuperDocClient(); await client.connect(); const doc = await client.open({ doc: './contract.docx' }); // Get tools in Anthropic format, convert to Bedrock toolSpec shape - const { tools } = await chooseTools({ provider: 'anthropic' }); + const { tools, systemPrompt, dispatch } = await createAgentToolkit({ + provider: 'anthropic', + preset: 'core', + }); const toolConfig = { - tools: tools.map((t) => ({ + tools: tools.map((t: any) => ({ toolSpec: { name: t.name, description: t.description, @@ -517,9 +514,11 @@ Use `chooseTools({ provider: 'anthropic' })` and convert to Bedrock's `toolSpec` while (true) { const res = await bedrock.send(new ConverseCommand({ + // Bedrock uses regional inference-profile IDs, not Anthropic model + // names — copy the exact ID from your Bedrock console's model catalog. modelId: 'us.anthropic.claude-sonnet-4-6', messages, - system: [{ text: 'You edit .docx files using SuperDoc tools. Use tracked changes for all edits.' }], + system: [{ text: systemPrompt }], toolConfig, })); @@ -533,7 +532,7 @@ Use `chooseTools({ provider: 'anthropic' })` and convert to Bedrock's `toolSpec` const results = []; for (const block of toolUses) { const { name, input, toolUseId } = block.toolUse; - const result = await dispatchSuperDocTool(doc, name, input ?? {}); + const result = await dispatch(doc, name, input ?? {}); const json = typeof result === 'object' && result !== null ? result : { result }; results.push({ toolResult: { toolUseId, content: [{ json }] } }); } @@ -548,14 +547,14 @@ Use `chooseTools({ provider: 'anthropic' })` and convert to Bedrock's `toolSpec` ```python import boto3 - from superdoc import SuperDocClient, choose_tools, dispatch_superdoc_tool + from superdoc import SuperDocClient, create_agent_toolkit client = SuperDocClient() client.connect() doc = client.open({"doc": "./contract.docx"}) # Get tools in Anthropic format, convert to Bedrock toolSpec shape - sd_tools = choose_tools({"provider": "anthropic"}) + kit = create_agent_toolkit({"provider": "anthropic", "preset": "core"}) tool_config = { "tools": [ { @@ -565,7 +564,7 @@ Use `chooseTools({ provider: 'anthropic' })` and convert to Bedrock's `toolSpec` "inputSchema": {"json": t.get("input_schema", {})}, } } - for t in sd_tools["tools"] + for t in kit["tools"] ] } @@ -574,9 +573,11 @@ Use `chooseTools({ provider: 'anthropic' })` and convert to Bedrock's `toolSpec` while True: response = bedrock.converse( + # Bedrock uses regional inference-profile IDs, not Anthropic model + # names — copy the exact ID from your Bedrock console's model catalog. modelId="us.anthropic.claude-sonnet-4-6", messages=messages, - system=[{"text": "You edit .docx files using SuperDoc tools. Use tracked changes for all edits."}], + system=[{"text": kit["system_prompt"]}], toolConfig=tool_config, ) @@ -590,7 +591,7 @@ Use `chooseTools({ provider: 'anthropic' })` and convert to Bedrock's `toolSpec` tool_results = [] for block in tool_uses: tu = block["toolUse"] - result = dispatch_superdoc_tool(doc, tu["name"], tu.get("input", {})) + result = kit["dispatch"](doc, tu["name"], tu.get("input", {})) json_result = result if isinstance(result, dict) else {"result": result} tool_results.append( {"toolResult": {"toolUseId": tu["toolUseId"], "content": [{"json": json_result}]}} diff --git a/apps/docs/ai/agents/legacy-preset.mdx b/apps/docs/ai/agents/legacy-preset.mdx new file mode 100644 index 0000000000..625816ef27 --- /dev/null +++ b/apps/docs/ai/agents/legacy-preset.mdx @@ -0,0 +1,226 @@ +--- +title: Legacy preset — grouped intent tools +sidebarTitle: Legacy preset +description: "The original 10-tool surface: grouped intent tools with search-then-edit targeting. Maintained for existing integrations; new integrations should use the core preset." +keywords: "superdoc legacy preset, superdoc_edit, superdoc_search, superdoc_mutations, intent tools" +--- + + +**Not recommended for new integrations.** The legacy preset remains the **default** (omitting `preset` selects it) purely for backwards compatibility — existing integrations keep working unchanged. For new work, use the [core preset](/ai/agents/core-preset): it scores measurably higher on our revision-fidelity evals, returns verifiable receipts instead of raw operation results, and treats tracked-changes redlining as a first-class workflow. + + +The legacy preset gives the model 10 grouped **intent tools** that map closely to [Document API](/document-api/overview) operations. The model works low-level: search for text to obtain handles/addresses, then edit by address. + +## Quick start + + + + ```typescript + import { createAgentToolkit } from '@superdoc-dev/sdk'; + + // 'legacy' is also what you get if you omit `preset`. + const { tools, systemPrompt, dispatch } = await createAgentToolkit({ + provider: 'openai', + preset: 'legacy', + }); + ``` + + + ```python + from superdoc import create_agent_toolkit + + kit = create_agent_toolkit({"provider": "openai", "preset": "legacy"}) + tools, system_prompt = kit["tools"], kit["system_prompt"] + ``` + + + +The legacy preset ignores `excludeActions` everywhere (it has no action surface to narrow) — passing it is a harmless no-op. + +## Tool catalog + +Most tools use an `action` argument to select the underlying operation. Single-action tools like `superdoc_search` do not require `action`. + +| Tool | Actions | What it does | +| --- | --- | --- | +| `superdoc_get_content` | `text`, `markdown`, `html`, `blocks`, `extract`, `info` | Read document content in different formats | +| `superdoc_search` | _(single action)_ | Find text or nodes and return handles or addresses for later edits | +| `superdoc_edit` | `insert`, `replace`, `delete`, `undo`, `redo` | Perform text edits and history actions | +| `superdoc_format` | `inline`, `set_style`, `set_alignment`, `set_indentation`, `set_spacing`, `set_direction`, `set_flow_options` | Apply inline or paragraph formatting | +| `superdoc_create` | `paragraph`, `heading`, `table` | Create structural block elements | +| `superdoc_list` | `insert`, `create`, `attach`, `detach`, `delete`, `merge`, `split`, `indent`, `outdent`, `set_level`, `set_type`, `set_value`, `continue_previous` | Create and manipulate lists | +| `superdoc_comment` | `create`, `update`, `delete`, `get`, `list` | Manage comment threads | +| `superdoc_track_changes` | `list`, `decide` | Review and resolve tracked changes | +| `superdoc_mutations` | `preview`, `apply` | Execute multi-step atomic edits as a batch | +| `superdoc_table` | `delete`, `delete_column`, `delete_row`, `insert_column`, `insert_row`, `merge_cells`, `set_borders`, `set_cell`, `set_cell_text`, `set_column`, `set_layout`, `set_options`, `set_row`, `set_row_options`, `set_shading`, `set_style_options`, `unmerge_cells` | Modify table structure, content, and styling (find table/row/cell nodeIds via `superdoc_get_content` or `superdoc_search`) | + +## Behaviors you must know + +**`superdoc_search` `require` values** — `first` returns the first match (error on zero), `all` returns every match, `any` returns matches without failing on zero, `exactlyOne` errors unless exactly one matched (`AMBIGUOUS_MATCH` on several, `MATCH_NOT_FOUND` on none). + +**Refs expire on mutation.** Every successful edit bumps the document revision and invalidates previously fetched refs — using a stale ref raises `REVISION_MISMATCH`. This is the sharpest edge of the legacy surface: pre-fetching refs for two blocks and editing them sequentially **always fails on the second edit**. For multi-block edits use `superdoc_mutations` (which resolves all targets before any step executes and applies everything in one revision bump), or re-search between edits. + +**`get_content action:"text"` returns the whole document** with no pagination — in an agent loop that result lives in conversation history forever and amplifies every later turn's token cost. Prefer scoped reads. + +## System prompt + +`getSystemPrompt()` / `getSystemPrompt('legacy')` returns the prompt this surface was designed with: the search-before-edit workflow, targeting rules, and batching guidance. Use it as-is or extend it — and pair prompt and tools from the same preset. + +## Creating custom tools + +The built-in intent tools cover core editing operations. For anything else, create custom tools that call `doc.*` methods directly and merge them with the SDK tools. + +| Step | What you do | +| --- | --- | +| 1. Pick operations | Browse `doc.*` to find the methods you need | +| 2. Define the schema | Write a function tool definition for your provider | +| 3. Write a dispatcher | Map tool actions to `doc.*` calls | +| 4. Merge and use | Combine with SDK tools in your agentic loop | + +### Step 1: Pick your operations + +Every `doc.*` namespace maps to a group of [Document API](/document-api/overview) operations: + +```text +doc.hyperlinks → list, get, wrap, insert, patch, remove +doc.tables → get, insertRow, deleteRow, mergeCells, ... +doc.images → list, get, setSize, rotate, crop, ... +doc.footnotes → list, get, create, delete, update, ... +doc.bookmarks → list, get, create, delete, ... +``` + +### Step 2: Define the tool schema + +Group related operations under a single tool using an `action` enum. This matches the pattern the built-in tools use. + +```typescript +import type { ChatCompletionTool } from 'openai/resources/chat/completions'; + +const hyperlinkTool: ChatCompletionTool = { + type: 'function', + function: { + name: 'superdoc_hyperlink', + description: + 'Create, read, update, or remove hyperlinks in the document.', + parameters: { + type: 'object', + properties: { + action: { + type: 'string', + enum: ['list', 'get', 'wrap', 'insert', 'patch', 'remove'], + }, + target: { + description: 'Target address from superdoc_search results.', + }, + text: { type: 'string', description: 'Display text (insert only).' }, + href: { type: 'string', description: 'URL destination.' }, + tooltip: { type: 'string', description: 'Hover tooltip text.' }, + }, + required: ['action'], + additionalProperties: false, + }, + }, +}; +``` + + +- Keep descriptions short. The model reads every tool definition on each turn. +- Use `additionalProperties: false` to prevent hallucinated parameters. +- Reference `superdoc_search` in descriptions so the model knows how to get targets. + + +### Step 3: Write a dispatcher + +Map each action to the corresponding `doc.*` call: + +```typescript +import { dispatchSuperDocTool } from '@superdoc-dev/sdk'; + +async function dispatchToolCall(doc, toolName, args) { + // Built-in tools: delegate to the SDK + if (toolName !== 'superdoc_hyperlink') { + return dispatchSuperDocTool(doc, toolName, args); + } + + // Custom tool: call doc.* directly + const { action, target, text, href, tooltip } = args; + + switch (action) { + case 'list': + return doc.hyperlinks.list({}); + case 'get': + return doc.hyperlinks.get({ target }); + case 'wrap': + return doc.hyperlinks.wrap({ + target, + link: { destination: { href }, ...(tooltip && { tooltip }) }, + }); + case 'insert': + return doc.hyperlinks.insert({ + text, + link: { destination: { href }, ...(tooltip && { tooltip }) }, + ...(target && { target }), + }); + case 'patch': + return doc.hyperlinks.patch({ + target, + patch: { ...(href && { href }), ...(tooltip && { tooltip }) }, + }); + case 'remove': + return doc.hyperlinks.remove({ target }); + default: + throw new Error(`Unknown action: "${action}"`); + } +} +``` + +### Step 4: Merge and use + +Combine your custom tool with the SDK tools and use your dispatcher in the agentic loop: + +```typescript +const { tools: sdkTools } = await chooseTools({ provider: 'openai' }); +const allTools = [...sdkTools, hyperlinkTool]; + +// In your agentic loop, use your dispatcher instead of dispatchSuperDocTool: +// OpenAI chat completions store the tool name on function.name. +const toolName = toolCall.function.name; +const result = await dispatchToolCall(doc, toolName, args); +``` + +### Extending the system prompt + +For custom tools, append usage instructions to the SDK system prompt so the model knows how to use them: + +```typescript +const systemPrompt = await getSystemPrompt(); + +const customInstructions = ` +## superdoc_hyperlink + +Use this tool to manage hyperlinks. First use superdoc_search to find +text you want to link, then pass the handle as target to the wrap action. +`; + +const fullPrompt = systemPrompt + '\n' + customInstructions; +``` + +## Migrating to core + +The mapping is conceptual, not mechanical — core actions absorb multi-call legacy patterns into single verbs: + +| Legacy pattern | Core equivalent | +| --- | --- | +| `superdoc_search` → `superdoc_edit` (replace by ref) | `replace_text` (finds and edits in one call) | +| `superdoc_search` → `superdoc_mutations` (multi-block batch) | one action call — each action resolves its own targets atomically | +| `superdoc_create` + `superdoc_format` chains | `insert_paragraphs` / `insert_heading` / `create_table` with `placement` | +| `superdoc_track_changes {action:"decide"}` | `accept_tracked_changes` / `reject_tracked_changes` (with `author`/`changeType` filters) | +| `superdoc_comment` | `add_comments` / `reply_to_comment` / `resolve_comments` | + +Switching is a one-line change per call site (`preset: 'core'` on the toolkit) plus adopting the receipt contract — see the [core preset reference](/ai/agents/core-preset). + +## Related + +- [Core preset reference](/ai/agents/core-preset) — the recommended surface +- [How it works](/ai/agents/architecture) — SDK ↔ CLI ↔ LLM mechanics +- [Overview](/ai/agents/llm-tools) — providers, token budget, errors, troubleshooting diff --git a/apps/docs/ai/agents/llm-tools.mdx b/apps/docs/ai/agents/llm-tools.mdx index 1e2cd17a8b..444b0968cd 100644 --- a/apps/docs/ai/agents/llm-tools.mdx +++ b/apps/docs/ai/agents/llm-tools.mdx @@ -1,11 +1,49 @@ --- title: AI Agents sidebarTitle: Overview -description: "Document tools that plug into any LLM provider: what they do and how they work" +description: "Document tools that plug into any LLM provider: what they do, how the pieces connect, and how to run a reliable agent loop" keywords: "llm tools, ai document editing, openai tools, anthropic tools, tool use, function calling, superdoc sdk, document automation" --- -The SuperDoc SDK ships tool definitions that give LLMs structured access to document operations. They cover reading, searching, editing, formatting, lists, comments, tracked changes, and batched mutations. Pick a provider format, pass the tools to your model, dispatch the calls, and the SDK handles schema formatting, argument validation, and execution. +The SuperDoc SDK ships tool definitions that give LLMs structured access to document operations: reading, searching, editing, formatting, lists, tables, comments, and tracked changes. Pick a provider format, pass the tools to your model, dispatch the calls, and the SDK handles schema formatting, argument validation, and execution. + +## How the pieces fit + +```mermaid +flowchart LR + UI["Your chat UI +
SuperDoc editor"] -->|"user request"| APP["Your agent loop"] + APP <-->|"messages + tools
tool calls + results"| LLM["LLM provider"] + APP <-->|"toolkit: tools · prompt · dispatch"| SDK["@superdoc-dev/sdk"] + SDK <-->|"sessions + operations"| CLI["SuperDoc CLI host
the document engine"] +``` + +Your loop is the broker: the model only ever sees tools, a system prompt, and tool results; documents live in sessions inside the CLI host the SDK spawns; the browser editor renders the same file for the user. Three rules prevent most first-hour confusion: + +1. **The SDK is server-side** — `dispatch` needs a session-bound handle from `createSuperDocClient().open(...)`; it does not run in the browser. +2. **The editor is browser-side** — never import `superdoc` / `@superdoc-dev/react` in backend code or API routes. +3. **Pair everything from one preset** — tools, system prompt, and dispatch must come from the same preset (the toolkit guarantees this). + + +The full mechanics — what crosses the SDK ↔ CLI boundary, sessions and revisions, a complete tool-call round trip with sequence diagrams, and where Python and MCP fit — have their own page: **[How it works](/ai/agents/architecture)**. + + +## Two presets + +The SDK ships two tool surfaces. Pass the same `preset` to `chooseTools`, `getSystemPrompt`, and `dispatchSuperDocTool`. + +| | `legacy` (default) | `core` | +| --- | --- | --- | +| Surface | 10 grouped intent tools (`superdoc_edit`, `superdoc_search`, …) | 2 tools: `superdoc_inspect` + `superdoc_perform_action` (40 named actions) | +| Style | Low-level: search for handles, then edit by address | High-level: named product verbs with deterministic targeting | +| Results | Operation results | Receipts with pre/post evidence and verification | +| Tracked changes | Via `changeMode` on individual ops | First-class: every mutating action is redline-aware, plus accept/reject/undo/redo actions | +| Best for | Fine-grained control, existing integrations | Agent loops, review/redlining workflows, fastest correct results | + + +**Use the core preset for new integrations.** Legacy remains the default only for backwards compatibility — existing integrations keep working unchanged. Core scores measurably higher on our revision-fidelity evals and returns verifiable receipts. Each preset has its own reference page: [core](/ai/agents/core-preset) · [legacy](/ai/agents/legacy-preset). + + +Both presets are also served over **MCP**: the SuperDoc MCP server registers the legacy intent tools by default, or the core action surface with `MCP_PRESET=core` (two tools plus session lifecycle, with the core MCP instructions). ## Quick start @@ -18,19 +56,23 @@ Install the SDK, create a client, open a document, and wire up an agentic loop. ``` ```typescript - import { createSuperDocClient, chooseTools, dispatchSuperDocTool } from '@superdoc-dev/sdk'; + import { createSuperDocClient, createAgentToolkit } from '@superdoc-dev/sdk'; import OpenAI from 'openai'; const client = createSuperDocClient(); await client.connect(); const doc = await client.open({ doc: './contract.docx' }); - const { tools } = await chooseTools({ provider: 'openai' }); + // One call — tools, system prompt, and dispatch, guaranteed coherent. + const { tools, systemPrompt, dispatch } = await createAgentToolkit({ + provider: 'openai', + preset: 'core', + }); const openai = new OpenAI(); - const messages = [ - { role: 'system', content: 'You edit documents using the provided tools.' }, - { role: 'user', content: 'Find the termination clause and rewrite it to allow 30-day notice.' }, + const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: 'Find the termination clause and rewrite it to allow 30-day notice. Use tracked changes.' }, ]; while (true) { @@ -46,11 +88,7 @@ Install the SDK, create a client, open a document, and wire up an agentic loop. if (!message.tool_calls?.length) break; for (const call of message.tool_calls) { - const result = await dispatchSuperDocTool( - doc, - call.function.name, - JSON.parse(call.function.arguments), - ); + const result = await dispatch(doc, call.function.name, JSON.parse(call.function.arguments)); messages.push({ role: 'tool', tool_call_id: call.id, @@ -69,25 +107,29 @@ Install the SDK, create a client, open a document, and wire up an agentic loop. pip install superdoc-sdk openai ``` + The PyPI package is `superdoc-sdk`, but the import is `from superdoc import …` — `import superdoc_sdk` will raise `ModuleNotFoundError`. + ```python import json - import openai - from superdoc import SuperDocClient, choose_tools, dispatch_superdoc_tool + from openai import OpenAI + from superdoc import SuperDocClient, create_agent_toolkit + client_llm = OpenAI() # uses OPENAI_API_KEY env var client = SuperDocClient() client.connect() doc = client.open({"doc": "./contract.docx"}) - result = choose_tools({"provider": "openai"}) - tools = result["tools"] + # One call — tools, system prompt, and dispatch, guaranteed coherent. + kit = create_agent_toolkit({"provider": "openai", "preset": "core"}) + tools, system_prompt = kit["tools"], kit["system_prompt"] messages = [ - {"role": "system", "content": "You edit documents using the provided tools."}, - {"role": "user", "content": "Find the termination clause and rewrite it to allow 30-day notice."}, + {"role": "system", "content": system_prompt}, + {"role": "user", "content": "Find the termination clause and rewrite it to allow 30-day notice. Use tracked changes."}, ] while True: - response = openai.chat.completions.create( + response = client_llm.chat.completions.create( model="gpt-5.4", messages=messages, tools=tools ) message = response.choices[0].message @@ -97,13 +139,12 @@ Install the SDK, create a client, open a document, and wire up an agentic loop. break for call in message.tool_calls: - result = dispatch_superdoc_tool( - doc, call.function.name, json.loads(call.function.arguments) - ) + result = kit["dispatch"](doc, call.function.name, json.loads(call.function.arguments)) messages.append({ "role": "tool", "tool_call_id": call.id, - "content": json.dumps(result), + # Receipts may contain non-JSON-serializable values; default=str is safe. + "content": json.dumps(result, default=str), }) doc.save({"inPlace": True}) @@ -115,7 +156,40 @@ Install the SDK, create a client, open a document, and wire up an agentic loop. ## Tool selection -`chooseTools()` returns provider-formatted tool definitions ready to pass to your LLM. +The one-call setup — tools, system prompt, and a pre-bound dispatcher that always agree on preset and exclusions: + + + + ```typescript + import { createAgentToolkit } from '@superdoc-dev/sdk'; + + const { tools, systemPrompt, dispatch, meta } = await createAgentToolkit({ + provider: 'openai', + preset: 'core', + excludeActions: ['delete_table'], // applied to tools, prompt, AND dispatch + }); + + // in the loop: + const receipt = await dispatch(doc, call.function.name, JSON.parse(call.function.arguments)); + ``` + + + ```python + from superdoc import create_agent_toolkit + + kit = create_agent_toolkit({"provider": "openai", "preset": "core", + "excludeActions": ["delete_table"]}) + tools, system_prompt = kit["tools"], kit["system_prompt"] + + receipt = kit["dispatch"](doc, call.function.name, json.loads(call.function.arguments)) + # async loops: kit["dispatch_async"](...) + ``` + + + +The toolkit makes preset/exclusion mismatches impossible by construction — an excluded action is simultaneously out of the tool enum, out of the system prompt, and refused at dispatch. The standalone functions below remain available when you need the pieces individually; if you use them with `excludeActions`, pass the **same list to all three**. + +`chooseTools()` returns provider-formatted tool definitions plus metadata about the selection. @@ -124,40 +198,28 @@ Install the SDK, create a client, open a document, and wire up an agentic loop. const { tools, meta } = await chooseTools({ provider: 'openai', // 'openai' | 'anthropic' | 'vercel' | 'generic' + preset: 'core', // omit for the default 'legacy' surface }); + + // meta = { preset: 'core', provider: 'openai', toolCount: 2, cacheStrategy: 'disabled' } + // cacheStrategy: 'disabled' | 'explicit' | 'automatic' | 'unsupported' — + // this call returns 'disabled'; anthropic with cache: true returns 'explicit'. ``` ```python from superdoc import choose_tools - result = choose_tools({"provider": "openai"}) + result = choose_tools({"provider": "openai", "preset": "core"}) tools = result["tools"] + meta = result["meta"] # preset, provider, toolCount, cacheStrategy ``` -The current SDK returns the full grouped intent tool set for the selected provider. Group filtering and meta-discovery are not part of the shipped public API here. - -## Tool catalog +## Legacy tool catalog -The generated catalog currently contains 9 grouped intent tools. Most tools use an `action` argument to select the underlying operation. Single-action tools like `superdoc_search` do not require `action`. - -| Tool | Actions | What it does | -| --- | --- | --- | -| `superdoc_get_content` | `text`, `markdown`, `html`, `info` | Read document content in different formats | -| `superdoc_search` | _(single action)_ | Find text or nodes and return handles or addresses for later edits | -| `superdoc_edit` | `insert`, `replace`, `delete`, `undo`, `redo` | Perform text edits and history actions | -| `superdoc_format` | `inline`, `set_style`, `set_alignment`, `set_indentation`, `set_spacing`, `set_direction`, `set_flow_options` | Apply inline or paragraph formatting | -| `superdoc_create` | `paragraph`, `heading`, `table` | Create structural block elements | -| `superdoc_list` | `insert`, `create`, `detach`, `indent`, `outdent`, `set_level`, `set_type` | Create and manipulate lists | -| `superdoc_comment` | `create`, `update`, `delete`, `get`, `list` | Manage comment threads | -| `superdoc_track_changes` | `list`, `decide` | Review and resolve tracked changes | -| `superdoc_mutations` | `preview`, `apply` | Execute multi-step atomic edits as a batch | - - -Built-in tools cover the core operations. For tables, images, hyperlinks, and anything else, [create custom tools](#creating-custom-tools) that call any `doc.*` operation today. - +The legacy preset's 10 grouped intent tools, their behaviors (`superdoc_search` `require` semantics, ref expiry, `superdoc_mutations` batching), and the migration mapping to core actions now live on the [legacy preset page](/ai/agents/legacy-preset). ## Dispatching tool calls @@ -168,219 +230,261 @@ Built-in tools cover the core operations. For tables, images, hyperlinks, and an ```typescript import { dispatchSuperDocTool } from '@superdoc-dev/sdk'; - const result = await dispatchSuperDocTool(doc, toolName, args); + const result = await dispatchSuperDocTool(doc, toolName, args, { preset: 'core' }); ```
```python from superdoc import dispatch_superdoc_tool - result = dispatch_superdoc_tool(doc, tool_name, args) + result = dispatch_superdoc_tool(doc, tool_name, args, preset="core") ``` ```python from superdoc import dispatch_superdoc_tool_async - result = await dispatch_superdoc_tool_async(doc, tool_name, args) + result = await dispatch_superdoc_tool_async(doc, tool_name, args, preset="core") ```
-The dispatcher validates required parameters, checks that arguments are compatible, and throws descriptive errors the LLM can act on. +The dispatcher validates required parameters, rejects unknown arguments, and throws descriptive errors the LLM can act on. `doc` must be the session-bound handle from `client.open(...)` — a plain object or a browser editor instance will not work. ## System prompt -`getSystemPrompt()` returns a default prompt that teaches the model the tool workflow: targeting, search-before-edit, and common patterns. It's optional. You can use it as-is, extend it with your own instructions, or write a completely custom prompt. +`getSystemPrompt(preset?)` returns the prompt each tool surface was designed — and evaluated — with. It teaches the model the document vocabulary the tools use (blocks, ordinals, markers, visual sections), when to inspect before editing, how to read results, and the tracked-changes rules. -```typescript -import { getSystemPrompt } from '@superdoc-dev/sdk'; + + + ```typescript + import { getSystemPrompt } from '@superdoc-dev/sdk'; -const systemPrompt = await getSystemPrompt(); -``` + const legacyPrompt = await getSystemPrompt(); // legacy surface + const corePrompt = await getSystemPrompt('core'); // action surface + ``` + + + ```python + from superdoc import get_system_prompt + + core_prompt = get_system_prompt("core") + ``` + + + +Guidance: + +- **Use it as-is** as your system message, or as the first section of one. +- **Extend, don't replace**: append your product's instructions (tone, guardrails, domain language) after it. The prompt's tool-usage sections encode behavior the schemas alone can't teach; dropping it measurably degrades edit quality. +- Pair prompt and tools from the **same preset** — the prompt documents exactly the surface the model was given. ## Provider formats -Each provider gets tool definitions in its native format. +Each provider gets tool definitions in its native format: ```typescript - const { tools } = await chooseTools({ provider: 'openai' }); + const { tools } = await chooseTools({ provider: 'openai', preset: 'core' }); // [{ type: 'function', function: { name, description, parameters } }] ``` ```typescript - const { tools } = await chooseTools({ provider: 'anthropic' }); + const { tools } = await chooseTools({ provider: 'anthropic', preset: 'core', cache: true }); // [{ name, description, input_schema }] + // With cache: true, the last tool carries cache_control for prompt caching + // (see Token budget). Without it, no cache markers are added. ``` ```typescript - const { tools } = await chooseTools({ provider: 'vercel' }); - // [{ type: 'function', function: { name, description, parameters } }] + const { tools } = await chooseTools({ provider: 'vercel', preset: 'core' }); + // [{ name, description, inputSchema }] — AI SDK dialect ``` ```typescript - const { tools } = await chooseTools({ provider: 'generic' }); - // [{ name, description, parameters, returns, metadata }] + const { tools } = await chooseTools({ provider: 'generic', preset: 'core' }); + // [{ name, description, parameters }] ``` -## Creating custom tools + +**Agent loops are not provider-interchangeable.** The tool *definitions* adapt automatically, but the message protocol does not: OpenAI uses `message.tool_calls` + `role: "tool"` replies; Anthropic uses `tool_use` content blocks + `role: "user"` messages containing `tool_result` blocks. Budget for a per-provider loop — the Anthropic variant is below. + -The built-in tools cover core editing operations. For advanced features like tables, images, hyperlinks, footnotes, and citations, create custom tools that call `doc.*` methods directly. +### Anthropic loop -| Step | What you do | -| --- | --- | -| 1. Pick operations | Browse `doc.*` to find the methods you need | -| 2. Define the schema | Write a function tool definition for your provider | -| 3. Write a dispatcher | Map tool actions to `doc.*` calls | -| 4. Merge and use | Combine with SDK tools in your agentic loop | + + + ```typescript + import Anthropic from '@anthropic-ai/sdk'; + import { chooseTools, getSystemPromptForProvider, dispatchSuperDocTool } from '@superdoc-dev/sdk'; -### Step 1: Pick your operations + const anthropic = new Anthropic(); + // cache: true on both halves of the static prefix — the tool array and the + // system prompt — so Anthropic caches them across turns (see Token budget). + const { tools } = await chooseTools({ provider: 'anthropic', preset: 'core', cache: true }); + const sys = await getSystemPromptForProvider({ provider: 'anthropic', preset: 'core', cache: true }); -Every `doc.*` namespace maps to a group of [Document API](/document-api/overview) operations: + const messages: Anthropic.MessageParam[] = [ + { role: 'user', content: 'Rewrite the termination clause for 30-day notice, tracked.' }, + ]; -```text -doc.hyperlinks → list, get, wrap, insert, patch, remove -doc.tables → get, insertRow, deleteRow, mergeCells, ... -doc.images → list, get, setSize, rotate, crop, ... -doc.footnotes → list, get, create, delete, update, ... -doc.bookmarks → list, get, create, delete, ... -``` + while (true) { + const response = await anthropic.messages.create({ + model: 'claude-sonnet-5', + max_tokens: 4096, + system: sys.content, // text blocks carrying cache_control markers + tools, + messages, + }); + messages.push({ role: 'assistant', content: response.content }); -### Step 2: Define the tool schema + const toolUses = response.content.filter((block) => block.type === 'tool_use'); + if (toolUses.length === 0) break; -Group related operations under a single tool using an `action` enum. This matches the pattern the built-in tools use. + const results = []; + for (const use of toolUses) { + const result = await dispatchSuperDocTool(doc, use.name, use.input, { preset: 'core' }); + results.push({ type: 'tool_result', tool_use_id: use.id, content: JSON.stringify(result) }); + } + messages.push({ role: 'user', content: results }); + } + ``` + + + ```python + import json + import anthropic + from superdoc import choose_tools, get_system_prompt, dispatch_superdoc_tool -```typescript -import type { ChatCompletionTool } from 'openai/resources/chat/completions'; - -const hyperlinkTool: ChatCompletionTool = { - type: 'function', - function: { - name: 'superdoc_hyperlink', - description: - 'Create, read, update, or remove hyperlinks in the document.', - parameters: { - type: 'object', - properties: { - action: { - type: 'string', - enum: ['list', 'get', 'wrap', 'insert', 'patch', 'remove'], - }, - target: { - description: 'Target address from superdoc_search results.', - }, - text: { type: 'string', description: 'Display text (insert only).' }, - href: { type: 'string', description: 'URL destination.' }, - tooltip: { type: 'string', description: 'Hover tooltip text.' }, - }, - required: ['action'], - additionalProperties: false, - }, - }, -}; -``` + client_llm = anthropic.Anthropic() + tools = choose_tools({"provider": "anthropic", "preset": "core", "cache": True})["tools"] + # The Node SDK wraps this in getSystemPromptForProvider; in Python, build + # the cacheable system block directly: + system = [{ + "type": "text", + "text": get_system_prompt("core"), + "cache_control": {"type": "ephemeral"}, + }] - -- Keep descriptions short. The model reads every tool definition on each turn. -- Use `additionalProperties: false` to prevent hallucinated parameters. -- Reference `superdoc_search` in descriptions so the model knows how to get targets. - + messages = [{"role": "user", "content": "Rewrite the termination clause for 30-day notice, tracked."}] -### Step 3: Write a dispatcher + while True: + response = client_llm.messages.create( + model="claude-sonnet-5", max_tokens=4096, + system=system, tools=tools, messages=messages, + ) + messages.append({"role": "assistant", "content": response.content}) -Map each action to the corresponding `doc.*` call: + tool_uses = [b for b in response.content if b.type == "tool_use"] + if not tool_uses: + break -```typescript -import { dispatchSuperDocTool } from '@superdoc-dev/sdk'; - -async function dispatchToolCall(doc, toolName, args) { - // Built-in tools: delegate to the SDK - if (toolName !== 'superdoc_hyperlink') { - return dispatchSuperDocTool(doc, toolName, args); - } - - // Custom tool: call doc.* directly - const { action, target, text, href, tooltip } = args; - - switch (action) { - case 'list': - return doc.hyperlinks.list({}); - case 'get': - return doc.hyperlinks.get({ target }); - case 'wrap': - return doc.hyperlinks.wrap({ - target, - link: { destination: { href }, ...(tooltip && { tooltip }) }, - }); - case 'insert': - return doc.hyperlinks.insert({ - text, - link: { destination: { href }, ...(tooltip && { tooltip }) }, - ...(target && { target }), - }); - case 'patch': - return doc.hyperlinks.patch({ - target, - patch: { ...(href && { href }), ...(tooltip && { tooltip }) }, - }); - case 'remove': - return doc.hyperlinks.remove({ target }); - default: - throw new Error(`Unknown action: "${action}"`); - } -} -``` + results = [] + for use in tool_uses: + result = dispatch_superdoc_tool(doc, use.name, use.input, preset="core") + results.append({ + "type": "tool_result", + "tool_use_id": use.id, + "content": json.dumps(result, default=str), + }) + messages.append({"role": "user", "content": results}) + ``` + + -### Step 4: Merge and use +## Token budget -Combine your custom tool with the SDK tools and use your dispatcher in the agentic loop: +Tool schemas and the system prompt are re-sent on **every** turn, and every tool result lives in conversation history forever. Untended, a typical loop crosses low-tier per-minute token ceilings within a few turns. What the SDK gives you and what to do yourself: -```typescript -const { tools: sdkTools } = await chooseTools({ provider: 'openai' }); -const allTools = [...sdkTools, hyperlinkTool]; +- **Prompt caching (Anthropic)** — pass `cache: true` to `chooseTools({ provider: 'anthropic', cache: true, ... })`: the SDK marks the tool array with `cache_control: {type: 'ephemeral'}` so the static prefix is cached across turns (~90% cost reduction on the cached portion). For the other half of the prefix, `getSystemPromptForProvider({ provider: 'anthropic', cache: true })` returns the system prompt as cacheable system blocks — pass its `content` as the `system` parameter (the [Anthropic loop](#anthropic-loop) shows both together). +- **Narrow the surface** — `excludeActions` (core preset) removes actions from the schema *and* the prompt in one move. +- **Windowed reads** — on large documents, inspect in block windows (`blockOffset`/`blockLimit`) instead of pulling the whole document into history; with legacy `superdoc_get_content action:"text"`, be aware the full text lands in history on every use. +- **Receipts are pre-capped** — core-preset receipts cap long per-item lists at 8 entries with count fields, specifically to keep history lean. +- **Plan for 429s** — tier-1 accounts should implement exponential backoff and history truncation from day one. -// In your agentic loop, use your dispatcher instead of dispatchSuperDocTool: -// OpenAI chat completions store the tool name on function.name. -const toolName = toolCall.function.name; -const result = await dispatchToolCall(doc, toolName, args); -``` +## Error codes + +Runtime errors carry a stable `code` your loop (and your model) can branch on: -### Extending the system prompt +| Code | Meaning | Recoverable? | +| --- | --- | --- | +| `REVISION_MISMATCH` | A ref/handle from before a mutation was used after it (legacy) or the session revision guard failed | Yes — re-search / re-inspect and retry | +| `AMBIGUOUS_MATCH` | `exactlyOne` matched several occurrences | Yes — narrow the pattern or use `all` | +| `MATCH_NOT_FOUND` | Target text/element not found; **nothing was changed** | Yes — re-inspect, fix the target | +| `INVALID_ARGUMENT` / `INVALID_INPUT` | Bad or unknown arguments (includes actions excluded by configuration) | Fix the call | +| `TOOL_DISPATCH_NOT_FOUND` | Tool name unknown to the selected preset | Fix preset/tool pairing | +| `TOOLS_ASSET_NOT_FOUND` / `TOOLS_ASSET_UNREADABLE` | Bundled prompt asset missing vs. unreadable (IO/permissions — details carry the cause) | Environment issue | +| `HOST_HANDSHAKE_FAILED` | CLI host binary could not start | No — fix the environment (see below) | -For custom tools, append usage instructions to the SDK system prompt so the model knows how to use them: +Core-preset action failures additionally return structured `recovery` hints (`reinspect` / `retry` / `revert` with a paste-ready call) inside the receipt. -```typescript -const systemPrompt = await getSystemPrompt(); +## Troubleshooting: `Host process disconnected` -const customInstructions = ` -## superdoc_hyperlink +This one error has several distinct causes — check in order: -Use this tool to manage hyperlinks. First use superdoc_search to find -text you want to link, then pass the handle as target to the wrap action. -`; +1. **macOS Gatekeeper killed the unsigned binary** (SIGKILL at launch). Check `xattr -d com.apple.quarantine ` / your MDM policy. +2. **Unsupported Node version** — the SDK supports current LTS versions, but doesn't declare `engines`, so npm won't warn you at install time. Check `node --version` first. +3. **The host crashed mid-call** — enable transport debug logs (`DEBUG=superdoc.transport`) to see the host's stderr and exit code. +4. **Next.js bundling** — mark the SDK as external (`serverExternalPackages: ['@superdoc-dev/sdk']`) so the native binary isn't bundled away. -const fullPrompt = systemPrompt + '\n' + customInstructions; +## Streaming status to your UI + +The agent loop is the natural place to emit progress events — each tool call is a meaningful step. Server-sent events sketch: + +```typescript +// Express/Node SSE endpoint around the agent loop +for (const call of message.tool_calls) { + const args = JSON.parse(call.function.arguments); + send({ type: 'tool_start', tool: call.function.name, action: args.action ?? null }); + + const receipt = (await dispatchSuperDocTool(doc, call.function.name, args, { + preset: 'core', + })) as { status?: string; verificationPassed?: boolean }; + + send({ + type: 'tool_done', + tool: call.function.name, + action: args.action ?? null, + status: receipt.status ?? 'ok', // core receipts: ok | partial | failed + verified: receipt.verificationPassed ?? null, + }); + messages.push({ role: 'tool', tool_call_id: call.id, content: JSON.stringify(receipt) }); +} +send({ type: 'assistant_message', text: finalText }); ``` +Core-preset receipts make the events meaningful for users: `action` names read like product verbs ("replace_text", "add_comments"), and `status`/`verified` let you render success/warning states without parsing prose. For the final message, instruct the model (in your appended system-prompt section) to end with a short user-facing summary of what changed — receipts give it the evidence to be specific. + +## Creating custom tools + +Custom capabilities are documented per preset: + +- **Core preset** — [Creating custom actions](/ai/agents/core-preset#creating-custom-actions): the `defineAction` authoring kit (coming soon). +- **Legacy preset** — [Creating custom tools](/ai/agents/legacy-preset#creating-custom-tools): define provider tools that call `doc.*` operations and merge them with the SDK's. + ## SDK functions | Function | Description | | --- | --- | -| `chooseTools(input)` | Load grouped tool definitions for a provider | -| `dispatchSuperDocTool(doc, name, args)` | Execute a tool call against a bound document handle | -| `listTools(provider)` | List all tool definitions for a provider | -| `getToolCatalog()` | Load the full tool catalog with metadata | -| `getSystemPrompt()` | Read the bundled system prompt for intent tools | +| `createAgentToolkit(input)` | One call: coherent `{tools, systemPrompt, dispatch, meta}` for a preset (recommended) | +| `chooseTools(input)` | Load tool definitions for a provider (`preset`, `excludeActions`, `cache` options) | +| `dispatchSuperDocTool(doc, name, args, options?)` | Execute a tool call against a bound document handle | +| `listTools(provider, preset?)` | List all tool definitions for a provider | +| `getToolCatalog(preset?)` | Load the full tool catalog with metadata | +| `getSystemPrompt(preset?, options?)` | Read the bundled system prompt for a tool surface | +| `getSystemPromptForProvider(input)` | System prompt shaped for a provider — for `anthropic` with `cache: true`, returns system blocks carrying `cache_control` (Node only; in Python build the block from `get_system_prompt`) | ## Related +- [How it works](/ai/agents/architecture): SDK ↔ CLI ↔ LLM mechanics with sequence diagrams +- [Core preset reference](/ai/agents/core-preset): the two-tool action surface, all 40 actions, receipts, redlining +- [Legacy preset reference](/ai/agents/legacy-preset): the previous 10-tool surface (not recommended for new work) - [How to use](/ai/agents/integrations): step-by-step integration guide with copy-pasteable code - [Best practices](/ai/agents/best-practices): prompting, workflow tips, and tested prompt examples - [Debugging](/ai/agents/debugging): troubleshoot tool call failures diff --git a/apps/docs/ai/mcp/overview.mdx b/apps/docs/ai/mcp/overview.mdx index 83b82628be..4d1d9c729a 100644 --- a/apps/docs/ai/mcp/overview.mdx +++ b/apps/docs/ai/mcp/overview.mdx @@ -9,22 +9,50 @@ The SuperDoc MCP server lets AI agents open, read, edit, and save `.docx` files. ## How it works -The MCP server runs as a local subprocess, communicating over stdio. It manages document sessions in memory: each `superdoc_open` creates an Editor instance, and all subsequent operations run against that in-memory state until you `superdoc_save`. - +Your MCP client spawns the server as a local subprocess and talks to it over stdio. The server embeds the SuperDoc document engine — the same engine the browser editor uses — and manages documents as in-memory **sessions**: `superdoc_open` loads a file and returns a `session_id`, every tool call targets that session, and nothing touches disk until `superdoc_save`. + +```mermaid +flowchart LR + subgraph client["Your MCP client"] + AGENT["AI agent
Claude Code · Claude Desktop
Cursor · Windsurf
"] + end + subgraph server["@superdoc-dev/mcp — local subprocess"] + direction TB + TOOLS["Registered tools
lifecycle + preset surface"] + SM["Session manager
session_id → live document"] + ENGINE["Document engine (in-memory)
same engine as the SuperDoc editor"] + TOOLS --> SM --> ENGINE + end + DISK[(".docx files
on disk")] + + AGENT <-->|"MCP protocol (stdio)
tools/list · tools/call"| TOOLS + DISK -->|"superdoc_open"| ENGINE + ENGINE -->|"superdoc_save"| DISK ``` -AI Agent (Claude, Cursor, Windsurf) - │ MCP protocol (stdio) - ▼ -@superdoc-dev/mcp - │ Document API - ▼ -SuperDoc Editor (in-memory) - │ export - ▼ -.docx file on disk + +A typical conversation, end to end: + +```mermaid +sequenceDiagram + autonumber + participant A as AI agent + participant S as MCP server + participant D as .docx on disk + + A->>S: superdoc_open {path: "contract.docx"} + S->>D: read file into an in-memory session + S-->>A: session_id + A->>S: superdoc_inspect / read tools {session_id} + S-->>A: document structure + A->>S: edit tools {session_id, ...} + Note over S: edits apply to the live session —
tracked changes, comments, formatting + S-->>A: results / receipts + A->>S: superdoc_save {session_id} + S->>D: write the updated .docx + A->>S: superdoc_close {session_id} ``` -The MCP server runs locally: SuperDoc never uploads your files. The AI agent you connect still sends content to its own provider. +Everything runs locally — SuperDoc never uploads your files. The AI agent you connect still sends document content to its own model provider as tool results. ## Setup @@ -82,12 +110,27 @@ Install once. Your MCP client spawns the server automatically on each conversati ## Tools -The MCP server exposes 12 tools total: +The server registers one of two tool surfaces, selected by the `MCP_PRESET` environment variable. All tools except `superdoc_open` take a `session_id` from `superdoc_open`. -- 3 lifecycle tools: `superdoc_open`, `superdoc_save`, `superdoc_close` -- 9 grouped intent tools generated from the SDK catalog - -All tools except `superdoc_open` take a `session_id` from `superdoc_open`. +| | Default (`legacy`) | `MCP_PRESET=core` — recommended | +| --- | --- | --- | +| Tools | 13: lifecycle + 10 grouped intent tools | 5: lifecycle + `superdoc_inspect` + `superdoc_perform_action` (40 named actions) | +| Style | Low-level: search for handles, edit by address | High-level verbs with deterministic targeting and verifiable receipts | +| Reference | tables below | [core preset reference](/ai/agents/core-preset) | + +To use the core surface, add the env to your client config: + +```json +{ + "mcpServers": { + "superdoc": { + "command": "npx", + "args": ["@superdoc-dev/mcp"], + "env": { "MCP_PRESET": "core" } + } + } +} +``` ### Lifecycle @@ -99,17 +142,22 @@ All tools except `superdoc_open` take a `session_id` from `superdoc_open`. ### Intent tools + +These are the **legacy preset's** tools (the default surface). With `MCP_PRESET=core` the server registers `superdoc_inspect` and `superdoc_perform_action` instead — see the [core preset reference](/ai/agents/core-preset) for its 40 actions, selectors, and receipts. + + | Tool | Actions | Description | | --- | --- | --- | -| `superdoc_get_content` | `text`, `markdown`, `html`, `info` | Read document content in different formats | -| `superdoc_search` | `match` | Find text or nodes and return handles or addresses for later edits | +| `superdoc_get_content` | `text`, `markdown`, `html`, `blocks`, `extract`, `info` | Read document content in different formats | +| `superdoc_search` | _(single action)_ | Find text or nodes and return handles or addresses for later edits | | `superdoc_edit` | `insert`, `replace`, `delete`, `undo`, `redo` | Perform text edits and history actions | -| `superdoc_format` | `inline`, `set_style`, `set_alignment`, `set_indentation`, `set_spacing` | Apply inline or paragraph formatting | -| `superdoc_create` | `paragraph`, `heading` | Create structural block elements | -| `superdoc_list` | `insert`, `create`, `detach`, `indent`, `outdent`, `set_level`, `set_type` | Create and manipulate lists | +| `superdoc_format` | `inline`, `set_style`, `set_alignment`, `set_indentation`, `set_spacing`, `set_direction`, `set_flow_options` | Apply inline or paragraph formatting | +| `superdoc_create` | `paragraph`, `heading`, `table` | Create structural block elements | +| `superdoc_list` | `insert`, `create`, `attach`, `detach`, `delete`, `merge`, `split`, `indent`, `outdent`, `set_level`, `set_type`, `set_value`, `continue_previous` | Create and manipulate lists | | `superdoc_comment` | `create`, `update`, `delete`, `get`, `list` | Manage comment threads | | `superdoc_track_changes` | `list`, `decide` | Review and resolve tracked changes | | `superdoc_mutations` | `preview`, `apply` | Execute multi-step atomic edits as a batch | +| `superdoc_table` | `delete`, `delete_column`, `delete_row`, `insert_column`, `insert_row`, `merge_cells`, `set_borders`, `set_cell`, `set_cell_text`, `set_column`, `set_layout`, `set_options`, `set_row`, `set_row_options`, `set_shading`, `set_style_options`, `unmerge_cells` | Modify table structure, content, and styling | Multi-action tools use an `action` argument to select the underlying operation. `superdoc_search` is a single-action tool and does not require `action`. diff --git a/apps/docs/docs.json b/apps/docs/docs.json index c81cdc87ee..2b61a86d70 100644 --- a/apps/docs/docs.json +++ b/apps/docs/docs.json @@ -196,6 +196,11 @@ "group": "Agents", "pages": [ "ai/agents/llm-tools", + "ai/agents/architecture", + { + "group": "Presets", + "pages": ["ai/agents/core-preset", "ai/agents/legacy-preset"] + }, "ai/agents/integrations", "ai/agents/best-practices", "ai/agents/debugging", @@ -295,7 +300,7 @@ ] }, "banner": { - "content": "**Bring your own editor UI.** Use SuperDoc as the document engine underneath. [Read the announcement →](https://www.superdoc.dev/changelog/2026-05-01-bring-your-own-editor)", + "content": "**Bring your own editor UI.** Use SuperDoc as the document engine underneath. [Read the announcement \u2192](https://www.superdoc.dev/changelog/2026-05-01-bring-your-own-editor)", "dismissible": true }, "redirects": [ diff --git a/apps/docs/package.json b/apps/docs/package.json index eb79518505..59b09fb2f7 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -12,7 +12,8 @@ "check:icons": "bun scripts/validate-icons.ts", "check:em-dashes": "bun scripts/validate-em-dashes.ts", "check:types": "tsx __tests__/doctest-types.ts", - "test:examples": "bun test __tests__/doctest.test.ts" + "test:examples": "bun test __tests__/doctest.test.ts", + "check:ai-snippets": "node scripts/validate-ai-snippets.mjs" }, "devDependencies": { "documentation": "^14.0.3", diff --git a/apps/docs/scripts/validate-ai-snippets.mjs b/apps/docs/scripts/validate-ai-snippets.mjs new file mode 100644 index 0000000000..4ee87a0142 --- /dev/null +++ b/apps/docs/scripts/validate-ai-snippets.mjs @@ -0,0 +1,334 @@ +#!/usr/bin/env node +/** + * AI-docs snippet gate: every code block on the core-preset / agents pages + * must work against the REAL SDK surface. + * + * What it checks, per fence language: + * - jsonc/json — parsed (comments stripped). Blocks with an "action" key + * are validated against the live core-preset catalog: the + * action must exist in the superdoc_perform_action enum and + * every top-level arg key must be a declared schema + * property. Inspect-shaped blocks validate against the + * superdoc_inspect schema. Selector/placement fragments + * validate their `kind`/`at` vocabulary. + * - typescript — every `import {...} from '@superdoc-dev/sdk'` symbol must + * exist on the built SDK; snippets are also compiled with + * tsc against the dist types (fragments get ambient decls). + * - python — compiled with ast.parse; every `from superdoc import ...` + * symbol must exist in the python package's __all__. + * - bash — CLI invocations are replayed against the real CLI. Any + * non-zero exit fails the block, except session/document + * preconditions a bare checkout can't satisfy. + * + * Requires built SDK dist (pnpm --prefix packages/sdk/langs/node build) + * and built CLI dist (pnpm --prefix apps/cli build). + * Run: node scripts/validate-ai-snippets.mjs + */ +import { readFileSync, writeFileSync, mkdtempSync, rmSync, existsSync } from 'node:fs'; +import { execFileSync } from 'node:child_process'; +import { tmpdir } from 'node:os'; +import { join, resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const DOCS = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const REPO = resolve(DOCS, '../..'); +const SDK_DIST = join(REPO, 'packages/sdk/langs/node/dist/index.js'); +const PAGES = [ + 'ai/agents/core-preset.mdx', + 'ai/agents/llm-tools.mdx', + 'ai/agents/legacy-preset.mdx', + 'ai/agents/architecture.mdx', + 'ai/agents/integrations.mdx', + 'ai/agents/best-practices.mdx', + 'ai/agents/debugging.mdx', +]; + +if (!existsSync(SDK_DIST)) { + console.error('validate-ai-snippets: SDK dist missing — run: pnpm --prefix packages/sdk/langs/node build'); + process.exit(2); +} +if (!existsSync(join(REPO, 'apps/cli/dist/index.js'))) { + console.error('validate-ai-snippets: CLI dist missing — run: pnpm --prefix apps/cli build'); + process.exit(2); +} + +const sdk = await import(SDK_DIST); +const catalog = await sdk.getPreset('core').getCatalog(); +const perform = catalog.tools.find((t) => t.toolName === 'superdoc_perform_action'); +const inspect = catalog.tools.find((t) => t.toolName === 'superdoc_inspect'); +if (!perform || !inspect) { + console.error('validate-ai-snippets: core catalog is missing superdoc_perform_action/superdoc_inspect — rebuild the SDK dist.'); + process.exit(2); +} +const ACTION_ENUM = new Set(perform.inputSchema.properties.action.enum); +const legacyCatalog = await sdk.getPreset('legacy').getCatalog(); +const LEGACY_ACTIONS = new Set( + legacyCatalog.tools.flatMap((t) => t.inputSchema?.properties?.action?.enum ?? []), +); +const PERFORM_KEYS = new Set(Object.keys(perform.inputSchema.properties)); +const INSPECT_KEYS = new Set(Object.keys(inspect.inputSchema.properties)); +const SELECTOR_KINDS = new Set(['nodeId', 'ordinal', 'textSearch', 'tableCell', 'placement', 'relative']); +const PLACEMENT_AT = new Set(['document_end', 'document_start', 'after', 'before']); + +let failures = 0; +const fail = (page, idx, lang, msg) => { + failures += 1; + console.error(` ✗ ${page} block[${idx}] (${lang}): ${msg}`); +}; + +function stripJsonc(text) { + const lines = text.split('\n').map((line) => { + let inString = false; + for (let i = 0; i < line.length; i += 1) { + if (line[i] === '"' && line[i - 1] !== '\\') inString = !inString; + if (!inString && line[i] === '/' && line[i + 1] === '/') return line.slice(0, i).trimEnd(); + } + return line; + }); + return lines.join('\n').replace(/,\s*([}\]])/g, '$1'); +} + +function checkJsonBlock(page, idx, code) { + // A jsonc block may contain several standalone objects (one per line-group). + const objects = []; + const chunks = code.split(/\n(?=\{)/).map((c) => stripJsonc(c).trim()).filter(Boolean); + for (const chunk of chunks) { + try { + objects.push(JSON.parse(chunk)); + } catch (e) { + fail(page, idx, 'jsonc', `does not parse: ${e.message} :: ${chunk.slice(0, 60)}`); + return; + } + } + for (const obj of objects) { + if (typeof obj.action === 'string') { + if (ACTION_ENUM.has(obj.action)) { + for (const key of Object.keys(obj)) { + if (!PERFORM_KEYS.has(key)) fail(page, idx, 'jsonc', `arg "${key}" not in superdoc_perform_action schema`); + } + } else if (!LEGACY_ACTIONS.has(obj.action)) { + fail(page, idx, 'jsonc', `action "${obj.action}" exists in neither the core enum nor any legacy tool`); + } + } else if (typeof obj.kind === 'string') { + if (!SELECTOR_KINDS.has(obj.kind)) fail(page, idx, 'jsonc', `unknown selector kind "${obj.kind}"`); + } else if (typeof obj.at === 'string') { + if (!PLACEMENT_AT.has(obj.at)) fail(page, idx, 'jsonc', `unknown placement at "${obj.at}"`); + } else if (Object.keys(obj).some((k) => INSPECT_KEYS.has(k))) { + for (const key of Object.keys(obj)) { + if (!INSPECT_KEYS.has(key)) fail(page, idx, 'jsonc', `arg "${key}" not in superdoc_inspect schema`); + } + } + } +} + +function checkTsImports(page, idx, code) { + const m = code.match(/import\s*\{([^}]+)\}\s*from\s*'@superdoc-dev\/sdk'/g) ?? []; + for (const imp of m) { + const names = imp + .replace(/import\s*\{|\}\s*from.*/g, '') + .split(',') + .map((n) => n.trim()) + .filter(Boolean); + for (const name of names) { + if (name.startsWith('type ')) continue; // type-only: validated by the tsc pass + const exported = name.split(/\s+as\s+/)[0].trim(); + if (!(exported in sdk)) fail(page, idx, 'ts', `'${exported}' is not exported by @superdoc-dev/sdk`); + } + } +} + +function checkPython(page, idx, code, pyInfo) { + const { execFileSync } = pyInfo; + const tmp = join(pyInfo.dir, `snippet_${idx}.py`); + writeFileSync(tmp, code); + try { + execFileSync('python3', ['-c', `import ast,sys; ast.parse(open(${JSON.stringify(tmp)}).read())`]); + } catch (e) { + fail(page, idx, 'python', `syntax error: ${String(e.stderr ?? e.message).slice(0, 120)}`); + return; + } + const m = code.match(/from superdoc import ([^\n]+)/g) ?? []; + for (const imp of m) { + const names = imp.replace('from superdoc import ', '').split(',').map((n) => n.trim().split(' ')[0]); + for (const name of names) { + if (!pyInfo.exports.has(name)) fail(page, idx, 'python', `'${name}' not exported by the superdoc package`); + } + } +} + +const CLI_DIST = join(REPO, 'apps/cli/dist/index.js'); +// Snippet preconditions a bare checkout can't satisfy (no live session, no +// sample document). Everything else that exits non-zero is a real failure — +// pattern-matching only known-bad output proved too easy to slip past. +const TOLERATED_PRECONDITIONS = /SESSION_NOT_FOUND|session .{0,40}not found|DOC(UMENT)?_NOT_FOUND|no such file|ENOENT/i; + +function checkBash(page, idx, code) { + for (const rawLine of code.split(/\\\n/).join(' ').split('\n')) { + const line = rawLine.trim(); + if (!line.startsWith('superdoc ')) continue; + const args = line.replace(/^superdoc\s+/, '').match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? []; + try { + // Run the real invocation — appending --help would short-circuit before + // flag validation and mask unknown-option errors (it did once). + execFileSync('node', [CLI_DIST, ...args.map((a) => a.replace(/^['"]|['"]$/g, ''))], { + stdio: 'pipe', + timeout: 30_000, + }); + } catch (e) { + const out = `${e.stdout ?? ''}${e.stderr ?? ''}`; + const flagError = /unknown option|unknown flag|INVALID_ARGUMENT|MISSING_REQUIRED/i.test(out); + // Fail on any non-zero exit unless it's ONLY a missing-session/document + // precondition. Flag/validation errors fail even alongside one. + if (flagError || !TOLERATED_PRECONDITIONS.test(out)) { + fail(page, idx, 'bash', `CLI rejected (exit ${e.status ?? '?'}): ${line.slice(0, 80)} :: ${out.slice(0, 160) || e.message.slice(0, 160)}`); + } + } + } +} + +// python package exports +const pyDir = mkdtempSync(join(tmpdir(), 'ai-snippets-')); +const pyExportsRaw = execFileSync('python3', ['-c', ` +import sys; sys.path.insert(0, ${JSON.stringify(join(REPO, 'packages/sdk/langs/python'))}) +import superdoc; print(','.join(superdoc.__all__)) +`]).toString(); +const pyExports = new Set(pyExportsRaw.trim().split(',')); +const pyInfo = { dir: pyDir, exports: pyExports, execFileSync }; + +// TS compile pass setup — ambient names are injected per snippet, and only +// when the snippet USES the name without declaring or importing it (docs +// fragments legitimately assume surrounding context like `doc` or `messages`). +const tsDir = mkdtempSync(join(tmpdir(), 'ai-snippets-ts-')); +const AMBIENT_DECLS = { + doc: "declare const doc: import('@superdoc-dev/sdk').BoundDocApi;", + call: 'declare const call: { id: string; function: { name: string; arguments: string } };', + toolCall: 'declare const toolCall: any;', + message: 'declare const message: any;', + response: 'declare const response: any;', + choice: 'declare const choice: any;', + messages: 'declare const messages: any[];', + args: 'declare const args: Record;', + name: 'declare const name: string;', + toolName: 'declare const toolName: string;', + openai: 'declare const openai: any;', + anthropic: 'declare const anthropic: any;', + model: 'declare const model: string;', + finalText: 'declare const finalText: string;', + receipt: 'declare const receipt: any;', + kit: 'declare const kit: any;', + tools: 'declare const tools: any[];', + sdkTools: 'declare const sdkTools: any[];', + systemPrompt: 'declare const systemPrompt: string;', + dispatch: 'declare const dispatch: (...a: any[]) => Promise;', + client: 'declare const client: any;', + send: 'declare function send(event: unknown): void;', + hyperlinkTool: 'declare const hyperlinkTool: any;', + dispatchToolCall: 'declare function dispatchToolCall(doc: any, toolName: string, args: any): Promise;', + streamFromServer: 'declare function streamFromServer(prompt: string, opts: any): AsyncIterable;', + signal: 'declare const signal: AbortSignal;', + buffer: 'declare let buffer: string;', + pendingFlush: 'declare let pendingFlush: any;', + editor: 'declare const editor: any;', + activeEditor: 'declare const activeEditor: any;', + superdoc: 'declare const superdoc: any;', + prompt: 'declare const prompt: string;', + res: 'declare const res: any;', + chooseTools: "declare const chooseTools: typeof import('@superdoc-dev/sdk').chooseTools;", + getSystemPrompt: "declare const getSystemPrompt: typeof import('@superdoc-dev/sdk').getSystemPrompt;", + dispatchSuperDocTool: "declare const dispatchSuperDocTool: typeof import('@superdoc-dev/sdk').dispatchSuperDocTool;", + createAgentToolkit: "declare const createAgentToolkit: typeof import('@superdoc-dev/sdk').createAgentToolkit;", + listTools: "declare const listTools: typeof import('@superdoc-dev/sdk').listTools;", + getToolCatalog: "declare const getToolCatalog: typeof import('@superdoc-dev/sdk').getToolCatalog;", +}; +function ambientFor(code) { + const out = []; + for (const [name, decl] of Object.entries(AMBIENT_DECLS)) { + const used = new RegExp(`\\b${name}\\b`).test(code); + if (!used) continue; + const declared = new RegExp(`\\b(const|let|var|function|class)\\s+(\\{[^}]*\\b${name}\\b[^}]*\\}|${name}\\b)`).test(code) + || new RegExp(`\\{[^}]*\\b${name}\\b[^}]*\\}\\s*=`).test(code) + || new RegExp(`import[^;]*\\b${name}\\b[^;]*from`).test(code); + if (!declared) out.push(decl); + } + return out.join('\n'); +} +const tsFiles = []; + +function dedent(code) { + const lines = code.split('\n'); + const indents = lines.filter((l) => l.trim()).map((l) => l.match(/^\s*/)[0].length); + const cut = indents.length ? Math.min(...indents) : 0; + return lines.map((l) => l.slice(cut)).join('\n'); +} + +let blockTotal = 0; +for (const page of PAGES) { + const text = readFileSync(join(DOCS, page), 'utf8'); + const blocks = [...text.matchAll(/```(\w+)[^\n]*\n(.*?)```/gs)].map((m) => ({ lang: m[1], code: dedent(m[2]) })); + blocks.forEach(({ lang, code }, idx) => { + blockTotal += 1; + if (lang === 'jsonc' || lang === 'json') checkJsonBlock(page, idx, code); + else if (lang === 'typescript' || lang === 'ts') { + checkTsImports(page, idx, code); + const file = join(tsDir, `${page.replace(/[^a-z0-9]+/gi, '_')}_${idx}.ts`); + writeFileSync(file, `export {};\n${ambientFor(code)}\n${code}`); + tsFiles.push(file); + } else if (lang === 'python') checkPython(page, idx, code, pyInfo); + else if (lang === 'bash') checkBash(page, idx, code); + }); +} + +// Single tsc pass over all TS snippets against the dist types. +writeFileSync(join(tsDir, 'tsconfig.json'), JSON.stringify({ + compilerOptions: { + strict: false, + noEmit: true, + skipLibCheck: true, + module: 'esnext', + target: 'es2022', + moduleResolution: 'bundler', + paths: { + '@superdoc-dev/sdk': [join(REPO, 'packages/sdk/langs/node/dist/index.d.ts')], + 'openai': [join(tsDir, 'openai-stub.d.ts')], + 'openai/resources/chat/completions': [join(tsDir, 'openai-cc-stub.d.ts')], + '@anthropic-ai/sdk': [join(tsDir, 'anthropic-stub.d.ts')], + 'ai': [join(tsDir, 'ai-stub.d.ts')], + '@ai-sdk/openai': [join(tsDir, 'ai-sdk-openai-stub.d.ts')], + '@aws-sdk/client-bedrock-runtime': [join(tsDir, 'bedrock-stub.d.ts')], + }, + }, + include: ['*.ts'], +}, null, 2)); +writeFileSync(join(tsDir, 'anthropic-stub.d.ts'), ` +declare class Anthropic { constructor(...a: any[]); messages: any; } +declare namespace Anthropic { type MessageParam = any; type ToolResultBlockParam = any; } +export default Anthropic;`); +writeFileSync(join(tsDir, 'openai-stub.d.ts'), ` +declare class OpenAI { constructor(...a: any[]); chat: any; } +declare namespace OpenAI { namespace Chat { namespace Completions { type ChatCompletionMessageParam = any; } type ChatCompletionMessageParam = any; } } +export default OpenAI;`); +writeFileSync(join(tsDir, 'openai-cc-stub.d.ts'), 'export type ChatCompletionTool = any;'); +writeFileSync(join(tsDir, 'ai-stub.d.ts'), ` +export declare function generateText(o: any): Promise; +export declare function jsonSchema(s: any): any; +export declare function stepCountIs(n: number): any; +export declare function tool(d: any): any;`); +writeFileSync(join(tsDir, 'ai-sdk-openai-stub.d.ts'), 'export declare const openai: ((model: string) => any) & { chat(model: string): any };'); +writeFileSync(join(tsDir, 'bedrock-stub.d.ts'), ` +export declare class BedrockRuntimeClient { constructor(...a: any[]); send(c: any): Promise; } +export declare class ConverseCommand { constructor(...a: any[]); }`); +try { + execFileSync('npx', ['tsc', '-p', tsDir], { cwd: join(REPO, 'apps/docs'), stdio: 'pipe', timeout: 180_000 }); + console.log(` ✓ ${tsFiles.length} TypeScript snippets compile against the SDK dist types`); +} catch (e) { + const out = `${e.stdout ?? ''}`; + for (const line of out.split('\n')) { + if (line.includes('error TS')) { fail('(tsc)', '-', 'ts', line.trim().replace(/^.*ai-snippets-ts-[^/]+\//, '').slice(0, 220)); } + } +} + +rmSync(pyDir, { recursive: true, force: true }); +rmSync(tsDir, { recursive: true, force: true }); + +console.log(`\nai-snippets: ${blockTotal} blocks checked across ${PAGES.length} pages, ${failures} failure(s)`); +process.exit(failures > 0 ? 1 : 0); diff --git a/examples/README.md b/examples/README.md index 2eb56ad15e..22d333a05f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -121,6 +121,7 @@ Document editing through models and agents. | [streaming](./ai/streaming) | Stream model output into a visible editor | | [redlining](./ai/redlining) | LLM-driven tracked-change review (browser) | | [footnote-tool-agent](./ai/footnote-tool-agent) | Real LLM tool-use loop: model picks `addFootnoteCitation`, browser executes against `editor.doc` | +| [core-actions-agent](./ai/core-actions-agent) | Headless agent on the `core` preset: 2 tools, 40 actions, receipts, tracked-changes redlining (Node + Python) | ## Advanced diff --git a/examples/ai/core-actions-agent/README.md b/examples/ai/core-actions-agent/README.md new file mode 100644 index 0000000000..787daf56d9 --- /dev/null +++ b/examples/ai/core-actions-agent/README.md @@ -0,0 +1,44 @@ +# Core actions agent + +A minimal, headless agent that edits a `.docx` from a natural-language instruction using the SuperDoc SDK's **`core` LLM-tools preset** — two tools (`superdoc_inspect`, `superdoc_perform_action`), forty deterministic actions, receipts with verification. + +Docs: [Core preset reference](https://docs.superdoc.dev/ai/agents/core-preset) · [AI agents overview](https://docs.superdoc.dev/ai/agents/llm-tools) + +## Setup + +```bash +pnpm install # from the repo root (workspace) +export OPENAI_API_KEY=sk-... +``` + +Or standalone outside the monorepo: replace the `workspace:*` dependency with the published `@superdoc-dev/sdk`. + +In a **dev checkout** (no published platform binary installed), point the SDK at the locally built CLI: + +```bash +export SUPERDOC_CLI_BIN=/apps/cli/dist/index.js # pnpm --prefix apps/cli run build +``` + +## Run (Node) + +```bash +node agent.mjs ./contract.docx "Rewrite the termination clause to allow 30-day notice." + +# Redlining: every edit becomes a tracked change a reviewer can accept/reject +node agent.mjs ./contract.docx "Tighten the confidentiality clause." --tracked --out reviewed.docx +``` + +## Run (Python) + +```bash +pip install superdoc-sdk openai # import name is `superdoc` +python agent.py ./contract.docx "Add a short summary paragraph at the top." --tracked +``` + +## What to look at + +- **One preset everywhere** — both scripts load tools, the system prompt, and the dispatcher through a single `createAgentToolkit({ preset: 'core', ... })` (`create_agent_toolkit` in Python) call, so they can never disagree on preset or exclusions. Mixing presets between hand-assembled calls is the most common integration mistake — the toolkit makes it impossible. +- **Receipts as status lines** — each tool call prints `→ … ok|partial|failed`, exactly the events you would stream to a chat UI over SSE. +- **`--tracked`** — appends a tracked-changes instruction; the model sets `changeMode: "tracked"` on every mutating action, producing redline suggestions instead of direct edits. + +Both scripts cap the loop at 16 turns and save to `out.docx` by default. diff --git a/examples/ai/core-actions-agent/agent.mjs b/examples/ai/core-actions-agent/agent.mjs new file mode 100644 index 0000000000..334c9980b6 --- /dev/null +++ b/examples/ai/core-actions-agent/agent.mjs @@ -0,0 +1,113 @@ +/** + * Core-preset agent: edit a .docx from a natural-language instruction. + * + * node agent.mjs "" [--tracked] [--out ] + * + * Demonstrates the `core` LLM-tools preset end to end via createAgentToolkit: + * - tools → 2 tools (superdoc_inspect, superdoc_perform_action) + * - systemPrompt → the prompt the action surface is evaluated with + * - dispatch → pre-bound to the preset; returns receipts with verification + * + * Each tool call is printed as a status line — the same shape you would + * stream to a chat UI (see the "Streaming status to your UI" docs section). + */ +import 'dotenv/config'; +import OpenAI from 'openai'; +import { createSuperDocClient, createAgentToolkit } from '@superdoc-dev/sdk'; + +const MODEL = process.env.OPENAI_MODEL ?? 'gpt-5.4'; +const MAX_TURNS = 16; + +// --------------------------------------------------------------------------- +// CLI arguments +// --------------------------------------------------------------------------- +const args = process.argv.slice(2); +const tracked = args.includes('--tracked'); +const outFlag = args.indexOf('--out'); +if (outFlag >= 0 && (args[outFlag + 1] == null || args[outFlag + 1].startsWith('--'))) { + console.error('--out requires a path argument'); + process.exit(1); +} +const outPath = outFlag >= 0 ? args[outFlag + 1] : 'out.docx'; +const positional = args.filter((a, i) => !a.startsWith('--') && args[i - 1] !== '--out'); +const [inputPath, instruction] = positional; + +if (!inputPath || !instruction) { + console.error('Usage: node agent.mjs "" [--tracked] [--out ]'); + process.exit(1); +} + +// --------------------------------------------------------------------------- +// SuperDoc session + core-preset tool surface +// --------------------------------------------------------------------------- +const client = createSuperDocClient(); +await client.connect(); +const doc = await client.open({ doc: inputPath }); + +// Everything after open() runs inside try/finally so an API error or a +// malformed response never leaks the session or (in dev checkouts) the +// locally spawned CLI process. +try { + + // One call — tools, system prompt, and a pre-bound dispatcher that are + // guaranteed to agree on preset (and excludeActions, if you narrow). + const { tools, systemPrompt, dispatch } = await createAgentToolkit({ provider: 'openai', preset: 'core' }); + + const userInstruction = tracked + ? `${instruction}\n\nMake every edit as a tracked change (changeMode: "tracked") so a reviewer can accept or reject it.` + : instruction; + + const messages = [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: userInstruction }, + ]; + + // --------------------------------------------------------------------------- + // Agent loop + // --------------------------------------------------------------------------- + const openai = new OpenAI(); + + for (let turn = 0; turn < MAX_TURNS; turn += 1) { + const response = await openai.chat.completions.create({ model: MODEL, messages, tools }); + const message = response.choices[0].message; + messages.push(message); + + if (!message.tool_calls?.length) { + console.log(`\n${message.content ?? '(no final message)'}`); + break; + } + + for (const call of message.tool_calls) { + let receipt; + try { + // Malformed tool-call arguments become an error receipt the model can + // read and correct, instead of crashing the run. + const callArgs = JSON.parse(call.function.arguments); + process.stdout.write(` → ${callArgs.action ?? call.function.name} … `); + receipt = await dispatch(doc, call.function.name, callArgs); + const status = receipt?.status ?? 'ok'; + const verified = receipt?.verificationPassed; + console.log(status + (verified === false ? ' (verification failed)' : '')); + } catch (error) { + receipt = { status: 'failed', error: { code: error.code, message: error.message } }; + console.log(`error: ${error.code ?? error.message}`); + } + + messages.push({ + role: 'tool', + tool_call_id: call.id, + content: JSON.stringify(receipt), + }); + } + } + + // --------------------------------------------------------------------------- + // Save + // --------------------------------------------------------------------------- + await doc.save({ out: outPath, force: true }); + console.log(`\nSaved: ${outPath}`); + +} finally { + await doc.close({ discard: true }).catch(() => {}); + await client.dispose(); +} diff --git a/examples/ai/core-actions-agent/agent.py b/examples/ai/core-actions-agent/agent.py new file mode 100644 index 0000000000..bdca4199f7 --- /dev/null +++ b/examples/ai/core-actions-agent/agent.py @@ -0,0 +1,108 @@ +"""Core-preset agent (Python twin of agent.mjs). + + python agent.py "" [--tracked] [--out ] + +Demonstrates the `core` LLM-tools preset from the Python SDK via +create_agent_toolkit — one call returning tools, the evaluated system prompt, +and a dispatcher pre-bound to the preset (receipts with verification). + +Requires: pip install superdoc-sdk openai (import name is `superdoc`). +""" + +from __future__ import annotations + +import json +import os +import sys + +from openai import OpenAI +from superdoc import SuperDocClient, create_agent_toolkit + +MODEL = os.environ.get("OPENAI_MODEL", "gpt-5.4") +MAX_TURNS = 16 + + +def parse_args(argv: list[str]) -> tuple[str, str, bool, str]: + tracked = "--tracked" in argv + out_path = "out.docx" + if "--out" in argv: + out_path = argv[argv.index("--out") + 1] + positional = [ + a for i, a in enumerate(argv) + if not a.startswith("--") and (i == 0 or argv[i - 1] != "--out") + ] + if len(positional) < 2: + print('Usage: python agent.py "" [--tracked] [--out ]') + sys.exit(1) + return positional[0], positional[1], tracked, out_path + + +def main() -> None: + input_path, instruction, tracked, out_path = parse_args(sys.argv[1:]) + + # One call — tools, system prompt, and a pre-bound dispatcher that are + # guaranteed to agree on preset (and excludeActions, if you narrow). + kit = create_agent_toolkit({"provider": "openai", "preset": "core"}) + tools, system_prompt, dispatch = kit["tools"], kit["system_prompt"], kit["dispatch"] + + if tracked: + instruction += ( + '\n\nMake every edit as a tracked change (changeMode: "tracked") ' + "so a reviewer can accept or reject it." + ) + + llm = OpenAI() + + with SuperDocClient() as client: + doc = client.open({"doc": input_path}) + # Everything after open() runs inside try/finally so an API error or a + # malformed tool call never leaks the session (mirrors agent.mjs). + try: + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": instruction}, + ] + + for _turn in range(MAX_TURNS): + response = llm.chat.completions.create(model=MODEL, messages=messages, tools=tools) + message = response.choices[0].message + messages.append(message) + + if not message.tool_calls: + print(f"\n{message.content or '(no final message)'}") + break + + for call in message.tool_calls: + try: + # Malformed tool-call arguments become an error receipt + # the model can read and correct, instead of a crash. + call_args = json.loads(call.function.arguments) + print(f" -> {call_args.get('action', call.function.name)} ... ", end="", flush=True) + receipt = dispatch(doc, call.function.name, call_args) + status = receipt.get("status", "ok") if isinstance(receipt, dict) else "ok" + verified = receipt.get("verificationPassed") if isinstance(receipt, dict) else None + print(status + (" (verification failed)" if verified is False else "")) + except Exception as error: + # SuperDocError carries a structured .code — keep the + # receipt shape identical to agent.mjs. + receipt = { + "status": "failed", + "error": {"code": getattr(error, "code", None), "message": str(error)}, + } + print(f"error: {getattr(error, 'code', None) or error}") + + messages.append({ + "role": "tool", + "tool_call_id": call.id, + # Receipts can contain non-JSON-serializable values. + "content": json.dumps(receipt, default=str), + }) + + doc.save({"out": out_path, "force": True}) + print(f"\nSaved: {out_path}") + finally: + doc.close({"discard": True}) + + +if __name__ == "__main__": + main() diff --git a/examples/ai/core-actions-agent/package.json b/examples/ai/core-actions-agent/package.json new file mode 100644 index 0000000000..2e06ec089c --- /dev/null +++ b/examples/ai/core-actions-agent/package.json @@ -0,0 +1,13 @@ +{ + "name": "@superdoc-examples/ai-core-actions-agent", + "private": true, + "type": "module", + "scripts": { + "start": "node agent.mjs" + }, + "dependencies": { + "@superdoc-dev/sdk": "workspace:*", + "dotenv": "^17.4.2", + "openai": "^6.33.0" + } +} diff --git a/examples/manifest.json b/examples/manifest.json index e99c8e2fd6..9a770b0124 100644 --- a/examples/manifest.json +++ b/examples/manifest.json @@ -449,6 +449,21 @@ "docs": "https://docs.superdoc.dev/ai/overview", "ci": true }, + { + "id": "ai-core-actions-agent", + "section": "ai", + "subsection": "agents", + "kind": "minimal-example", + "status": "active", + "sourceKind": "local", + "title": "Core actions agent", + "category": "AI", + "surface": "SDK (Node + Python)", + "sourceRepo": "superdoc-dev/superdoc", + "sourcePath": "examples/ai/core-actions-agent", + "docs": "https://docs.superdoc.dev/ai/agents/core-preset", + "ci": false + }, { "id": "document-engine-ai-redlining", "section": "document-engine", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 200a0a08e7..5cb8da1114 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1667,6 +1667,18 @@ importers: specifier: ^4.21.0 version: 4.21.0 + examples/ai/core-actions-agent: + dependencies: + '@superdoc-dev/sdk': + specifier: workspace:* + version: link:../../../packages/sdk/langs/node + dotenv: + specifier: ^17.4.2 + version: 17.4.2 + openai: + specifier: ^6.33.0 + version: 6.35.0(ws@8.20.0)(zod@4.3.6) + examples/ai/footnote-tool-agent: dependencies: dotenv: @@ -46757,7 +46769,7 @@ snapshots: jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.46.1 - webpack: 5.105.4(esbuild@0.27.7)(webpack-cli@5.1.4) + webpack: 5.105.4(esbuild@0.27.7)(webpack-cli@6.0.1) optionalDependencies: esbuild: 0.27.7