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