# page-agent-sdk

> **[English](https://github.com/whyymj/page-agent-sdk/blob/master/README.md)** · **[中文](https://github.com/whyymj/page-agent-sdk/blob/master/README.zh-CN.md)**

> Give your web page an **AI assistant that edits the page itself**. Mount a chat dialog in one line; the AI reads/writes page data safely via schema-validated tools — "conversational" building/editing/ops. **A lighter, framework-agnostic alternative to CopilotKit / LangChain for in-page, schema-validated JSON-editing agents.**

> **AI agent integration**: see [Agent Integration Cheat Sheet](#agent-integration-cheat-sheet-for-ai-agents) below (exports / options / extension points / built-in tools / file structure). Architecture & gotchas in [`CLAUDE.md`](https://github.com/whyymj/page-agent-sdk/blob/master/CLAUDE.md).

[![npm](https://img.shields.io/npm/v/page-agent-sdk.svg)](https://www.npmjs.com/package/page-agent-sdk)
[![license](https://img.shields.io/badge/license-ISC-blue.svg)](https://github.com/whyymj/page-agent-sdk/blob/master/LICENSE)
[![tests](https://img.shields.io/badge/self%20tests-3793%20asserts-brightgreen.svg)](#self-tests)

---

> 🚀 **Quick start?** → [30-second quickstart](#30-second-quickstart) · [Examples](#examples) · [Options cheat sheet](#createchatsdk-options-cheat-sheet) · [Usage map](#usage-map-task--where)

## Usage map (task → where)

Starting point for both humans and AI agents (Claude Code / Cursor): find the feature you need, follow the link. Details live in sub-docs — [doc/](https://github.com/whyymj/page-agent-sdk/blob/master/doc/README.en.md) index.

| I want to… | See |
|---|---|
| Just add an AI chat box (no data ops) | [30-second quickstart](#30-second-quickstart) · `examples/minimal-demo` |
| AI edits my page data (schema + bind) | [Design: three-layer split](#design-the-schema--systemprompt--skill-three-layer-split) · [usage-guide §6.1](https://github.com/whyymj/page-agent-sdk/blob/master/doc/usage-guide.en.md#61-data-ops-single-main-object--let-the-agent-edit-your-json) |
| Custom tools / skills / memory / middleware | [Extension points](#extension-points) · [usage-guide §6.2–6.4, §7](https://github.com/whyymj/page-agent-sdk/blob/master/doc/usage-guide.en.md) |
| Connect LLM (DeepSeek/OpenAI-compatible/Claude/proxy, no apiKey leak) | [Configuration](#configuration) · [usage-guide §8.6 Proxy](https://github.com/whyymj/page-agent-sdk/blob/master/doc/usage-guide.en.md) · `examples/proxy-demo` |
| Headless / Node.js (own UI, no dialog) | [Bundle size & tree-shaking](#bundle-size--tree-shaking) (headless subpath) · `examples/headless-demo`, `examples/customize-demo` |
| Old bundler (webpack ≤4 / vue-cli 2-3) | [Bundle size & tree-shaking](#bundle-size--tree-shaking) (legacy subpath, es2017 all-inlined) |
| HTML/code components (AI writes page blocks) | [Capability packs](#createchatsdk-options-cheat-sheet) (`createHtmlSubagent`, auto-registered 3.9+) · `examples/html-page-demo`, `examples/complex-demo` |
| RAG / MCP tools | [Capability packs](#createchatsdk-options-cheat-sheet) (`createRagSubagent`, `mcp`) · `examples/rag-demo` |
| Let the AI see images (paste/drop/pick) | [options cheat sheet](#createchatsdk-options-cheat-sheet) (`images` / `llm.vision`) · [usage-guide §6.17](https://github.com/whyymj/page-agent-sdk/blob/master/doc/usage-guide.en.md#617-image-input-multimodal-direct--captioning-bypass) · `examples/images-demo` |
| Docs-site QA (quote a selection + read the current page) | [options cheat sheet](#createchatsdk-options-cheat-sheet) (`dialog.autoQuote` / `capabilities.domInspect` + `pageContext`) · [usage-guide §6.20](https://github.com/whyymj/page-agent-sdk/blob/master/doc/usage-guide.en.md#620-text-selection-quoting--page-qa-page-quote--read_page--pagecontext) · `examples/docs-demo` |
| Host-page QA reliability (SPA doc-switch invalidation / no-guessing gate / quote DOM anchor) | `sdk.notifyHostChange({reason})` on route change · page-assertion gate auto-mounted with `domInspect` · quote anchor captured with selections (`setQuote` 3rd arg) · [usage-guide §6.20](https://github.com/whyymj/page-agent-sdk/blob/master/doc/usage-guide.en.md#620-text-selection-quoting--page-qa-page-quote--read_page--pagecontext) |
| 📡 Auto host-change reporting (4.21+) | `hostWatch: true` — URL change (hashchange/popstate) auto-fires the full notifyHostChange chain, turning "the defense relies on the host remembering to call" into "the defense is automatic"; pushState patch / title watching are opt-in; debounce merging + `ignore` hook; server/headless degrades to a silent no-op (`inspect().hostWatch` reflection). Also: dom_edit/dom_restore landing auto-invalidates prior page reads (default-on) | `hostWatch` |
| 📝 Content proposals — review & apply (4.22+) | `proposals: { read, onProposal }`: controlled editing for content outside the data slot — `read_content` (base + hash) → `propose_content` (**incremental ops**: literal unique anchors, atomic; tokens scale with the change; base-hash drift rejected) → host diff panel → write-back only on explicit user apply; `sdk.resolveProposal` closes the loop (events + next-turn outcome notice); model has zero write permission, unconfigured = zero registration; `lineDiff/applyProposalOps/hashContent` exported | `proposals` + `sdk.resolveProposal` |
| Customize UI (theme / icons / i18n / button labels) | [`DialogConfig` fields](#dialogconfig-fields) · [usage-guide §6.15](https://github.com/whyymj/page-agent-sdk/blob/master/doc/usage-guide.en.md#615-ui-customization--i18n-icons--theme--language--message-overrides-317321) · `examples/i18n-demo` |
| Sessions / persistence (IndexedDB) | [options cheat sheet](#createchatsdk-options-cheat-sheet) (`storage`/`session`) · `examples/page-demo` (`storage:'indexed'` + built-in history dropdown) |
| Long conversations / big JSON (context & compression) | [usage-guide §6.8](https://github.com/whyymj/page-agent-sdk/blob/master/doc/usage-guide.en.md) · [context-management doc](https://github.com/whyymj/page-agent-sdk/blob/master/doc/context-management.md) |
| Events / audit / token usage | [Configuration](#configuration) (`onEvent`/`onAudit`/`sdk.usage`) · [usage-guide §6.9](https://github.com/whyymj/page-agent-sdk/blob/master/doc/usage-guide.en.md) |
| Unattended automation / batch / budget | [options cheat sheet](#createchatsdk-options-cheat-sheet) (`capabilities.automation`, `sdk.batch`) · [usage-guide automation section](https://github.com/whyymj/page-agent-sdk/blob/master/doc/usage-guide.en.md) |
| Debug (prompts / tool IO / context) | `debug: true` + built-in DebugDrawer · `sdk.inspect()` / `sdk.debugLogs` / `sdk.inspectContext()` / `sdk.exportDiagnostics()` (one-click diagnostics report, copy the full text to the maintainer) |
| Everything, full API | [Agent Integration Cheat Sheet](#agent-integration-cheat-sheet-for-ai-agents) · [usage-guide](https://github.com/whyymj/page-agent-sdk/blob/master/doc/usage-guide.en.md) · [doc index](https://github.com/whyymj/page-agent-sdk/blob/master/doc/README.en.md) |

## Who is it for

**Low-code / visual builders, form & page designers, CMS, ops consoles** — anywhere "page data is structured, and you want natural language to drive it".

One-line gist: **declare the page data structure (schema) to the Agent; it reads/writes via tools, validated by schema** — "editing the page" goes from drag/fill to a single sentence.

### What it is: a standardized JSON-operation Agent

At its core, it gives the AI a **standardized, safe JSON-operation channel**. AI editing JSON is no longer "generate a blob of text and stuff it back" (uncontrolled), but a structured operation under four constraints:

| Constraint | Mechanism | Effect |
|---|---|---|
| **Scope control** | Declared schema fields (`data`) — only declared keys are writable; schema shape auto-whitelist (top-level + sub-path recursively projected by sub-schema; undeclared fields hidden/denied; whole-set becomes merge to prevent accidental deletion) | AI touching undeclared fields → `PATH_DENIED` |
| **Validity check** | zod schema — `write`/`set`/`edit` validated against schema | Invalid type/enum/structure → structured error, no write |
| **Incremental op** | `write` with `patch` (set/remove/merge/append/move) or `patches` (batch, atomic rollback) by `jsonPath` | Avoid re-sending the whole large JSON; precise local edits; use `patches` to edit many at once |
| **Large-object retrieval** | `read` supports `fields` (projection) + `depth` (truncation) to shrink payload; `query_data` (JSONPath, 4.6+ batch `queries` 2-10 in one call)/`search_data` (text)/`eval_script` (sandboxed JS) | Efficient retrieval + pinpoint location in large JSON |
| **Rollbackable** | per-path snapshots (auto-stacked) + session checkpoint | Bad edit → one-click restore to the last good state |
| **Optimistic lock** | `conflictWatchFields` opt-in optimistic lock + conflict human-in-the-loop (3.29+ `conflictPolicy` declares auto-adjudication: overwrite / keep_external) | Concurrent external edits detected → suspend, user picks keep/overwrite/restore |

"Editing JSON" moves from free-form LLM text generation to **structured, validatable, auditable, rollbackable** tool operations. This is the fundamental difference from "let the AI output a JSON string directly".

## Use cases

| Scenario | User says | AI does |
|---|---|---|
| 🏗 **Low-code builder** | "Top banner → dark, bold the title, add a new-product card" | Incremental patch the component tree via jsonPath; canvas refreshes live |
| 📝 **Form designer** | "Add phone format validation, address → 3-level cascade" | Incremental field-definition edits, schema-validated |
| 📰 **CMS ops** | "Prefix these products with 'Limited', mark under ¥100 red" | JSONPath filter + sandbox script batch edit |
| 🖥 **Ops console** | "Raise A's threshold to 30%, turn off switch B" | Whitelist + human-confirm to edit config, read-back verify |
| 🤖 **AI-native assistant** | "Change this chart's legend to bars" | Conversational ops on product data, no UI needed |
| 🔬 **Research agent** | "Compare 3 solutions and recommend one" | Parallel subagents investigate each, return only conclusions |
| 🧩 **Headless / server-side** | "Run the agent in Node.js" | `ui:false` + `storage:'memory'`, drive via `sdk.send` |

> `examples/nested-demo` is a full low-code example: nested block tree + human confirm + one-click rollback.

**Full end-to-end scenarios with copy-paste code** (9 cases: low-code builder / form designer / CMS batch / ops console / AI-native / research / server-side / multi-agent / MCP) live in the bundled Agent Skill at `skills/page-agent-sdk-integrate/references/use-cases.md` (also shipped in the npm package). See [Skills for AI tools](#skills-for-ai-tools-for-integrators) below to install the skill.

## When to use / When not

**Use it if you** want an AI assistant embedded in your web page that edits structured page data (config / component tree / form definitions / CMS content) — safely, rollbackably, via tools — and you don't want to hand-roll an agent harness, schema validation, optimistic lock, or snapshot system.

**Don't use it if you** only need a stateless chat widget (use any chat UI lib), or you want the AI to drive a browser / automate arbitrary DOM across sites (use Playwright / browser-use), or your data has no schema you can declare.

### FAQ

- **Q: I want an AI assistant embedded in my web page that can edit the page data.** → `page-agent-sdk`: declare a zod schema + `bind`, mount the dialog, done. See [30-second quickstart](#30-second-quickstart).
- **Q: Alternative to CopilotKit / LangChain for an in-page agent?** → `page-agent-sdk` is framework-agnostic (Vue bundled, host can be React / vanilla), schema-validated, ships optimistic lock + snapshot rollback + MCP, and needs no LangGraph. See [Comparison](#comparison).
- **Q: How to let AI safely edit a large JSON on my page?** → `data` + zod schema + `write` with `patch` / `patches` + `conflictWatchFields` optimistic lock. Invalid edits are rejected pre-write; bad edits rollback in one click.
- **Q: Does it work with DeepSeek / OpenAI / any OpenAI-compatible endpoint / Anthropic Claude?** → Yes. `llm: { apiKey, baseUrl, model }` defaults to DeepSeek (OpenAI protocol); `llm: { provider: 'anthropic', apiKey, model: 'claude-...' }` uses Claude native protocol (dynamic-loaded `@langchain/anthropic`, optional peer); any LangChain `BaseChatModel` also accepted.
- **Q: Can I run it headless / in Node.js?** → Yes. `ui:false` + `storage:'memory'`, drive via `sdk.send`. See [headless-demo](#examples).
- **Q: Does it support MCP?** → Yes. `mcp: [{ transport, url }]` connects remote MCP servers and injects tools dynamically.

### Comparison

| | page-agent-sdk | CopilotKit | LangChain (chat models) | LangGraph | raw LLM tool-calling |
|---|---|---|---|---|---|
| Framework-agnostic, UI bundled | ✅ Vue bundled, host-agnostic | ❌ React-only | ✅ (no UI) | ✅ (no UI) | ✅ (no UI) |
| Schema-validated JSON ops | ✅ zod, whitelist + merge-safe | ⚠️ partial (tool args) | ⚠️ tool args only | ⚠️ tool args only | ❌ |
| Incremental patch (jsonPath) | ✅ `write` patch / `patches` | ❌ | ❌ | ❌ | ❌ |
| Optimistic lock + conflict HITL | ✅ `conflictWatchFields` | ❌ | ❌ | ❌ | ❌ |
| Snapshot rollback + checkpoint | ✅ per-path + session | ❌ | ❌ | ❌ | ❌ |
| Proactive human-confirm | ✅ built-in | ⚠️ manual | ❌ | ❌ | ❌ |
| MCP | ✅ | ✅ | ✅ | ✅ | manual |
| Subagents | ✅ | ❌ | ✅ (manual) | ✅ | manual |
| Context compression | ✅ 4-layer built-in | ❌ | ❌ | ✅ checkpointer | ❌ |
| In-browser persistence | ✅ IndexedDB | ❌ | ❌ | ❌ | ❌ |
| Bundle | ~963 KB ESM / 2.0 MB IIFE | React dep | large | large | none |

> Nuance: CopilotKit is a great choice if you're already on React and want a polished AI-chat UI with backend actions; LangChain / LangGraph are general-purpose agent orchestration (server-side strong). `page-agent-sdk` specifically targets **in-page, schema-validated, rollbackable JSON editing** — that niche is its differentiation.

## 30-second quickstart

```bash
npm install page-agent-sdk zod @langchain/openai @langchain/core
```

> Note: `@langchain/openai` is a **de-facto required dependency** (statically imported by the SDK — needed even for Anthropic-only setups; optional-peer conversion was evaluated and rejected). `@langchain/anthropic` / `@modelcontextprotocol/sdk` are truly optional (dynamic import).

```ts
import { createChatSdk } from 'page-agent-sdk'
import { z } from 'zod'

const page = { title: 'New Products', theme: 'light' }
window.page = page  // optional: mount to window for your page to read; SDK tools operate on `bind` directly

createChatSdk({
  container: '#chat',
  llm: { apiKey: 'sk-...', baseUrl: 'https://api.deepseek.com', model: 'deepseek-v4-flash' },
  systemPrompt: 'You are a page-builder assistant; read/write the main data via tools.',
  data: {
    schema: z.object({
      title: z.string().describe('Page title'),
      theme: z.enum(['light', 'dark']).describe('Theme'),
    }),
    bind: page,
    description: 'Page config',
  },
  approval: { tools: ['write'] }, // confirm writes
  checkpoint: true, // one-click rollback on mistake
}).mount()
```

User says "title → 'Summer New', theme → dark" → AI calls `write` with `patch` (incremental) → schema validation → pre-write confirm → reactive refresh. Said wrong? Click "↩ Undo".

CDN zero-config: `<script src="https://unpkg.com/page-agent-sdk"></script>` → `ChatSdk.createChatSdk({...})`.

## Capabilities

| Capability | Description | Option |
|---|---|---|
| 🛠 window ops | Read/write registered props, schema validation + incremental patch + snapshot rollback | `data` |
| 🧠 ReAct harness | Pluggable middleware (8 hooks), in-house (no LangGraph) | `middleware` |
| 📋 planning/skills/memory | `write_todos` / `define_skill` / AGENTS.md directives | `capabilities.*` |
| 🗄 virtual workspace | In-memory file system; large results offloaded (won't blow context) | `capabilities.vfs` |
| ↩️ rollback | per-path snapshots (small fixes) + session checkpoint (big fixes) | `checkpoint` |
| ✋ human confirm | Pre-write dialog + AI proactive inquiry (uncertain/multi-plan/high-risk) | `approval` |
| ✅ self-verify | Run `check` before return; on fail, feedback re-injects to self-correct | `capabilities.verify` |
| 🤖 subagents | Delegate subtasks; process stays out of main context | `subagent` |
| 🔌 MCP | Connect remote MCP servers, inject tools dynamically | `mcp` |
| 📦 context compression | 4-layer adaptive compression, presets + LLM summary | `contextPreset` |
| 🧪 complex-task tuned | `complex` context preset (larger window + later compress + more recall, for multi-step / large-JSON / long-workflow tasks); vfs JSON-aware tools (`vfs_json_read` / `vfs_json_patch`) for structured big-JSON ops inside vfs; vfs three-pool LRU (large_results / drafts / userFiles isolated, no mutual eviction) | `contextPreset:'complex'`, `capabilities.vfs` |
| 🛡️ compression-safe | Live data snapshot + preserved tool results in summary; write returns hint available paths; `systemPromptHelpers.reliableWriteRules` | built-in |
| 💰 Context economy (3.10/3.11+) | Compression cost cap `promptSoftCapTokens` (defaults to 160K when window ≥320K — huge-window models no longer burn hundreds of thousands of tokens before compressing; reflected via `inspect().compression`) + agent budget self-awareness (70%-rounds / half-cap token hint, consecutive write-failure reminder, per-invocation `roundTokenBudget` friendly wrap-up) + tool-description slimming (-40% prompt) | `contextOptions.promptSoftCapTokens`, `roundTokenBudget` |
| 💾 persistence | IndexedDB multi-session + quota eviction + switch | `storage` |
| ⚡ quick actions / session transfer (4.10+) | `dialog.quickActions` chip row above the input sends high-frequency prompts in one click (no pre-fill; queueing/gate semantics inherited); `sdk.exportSession`/`importSession` fully restorable session JSON (copy semantics, never overwrites; `dialog.sessionTransfer:true` adds history-panel entries); `dialog.onDropElement` callback when a host element is dragged into the input (focus entry, mapping stays host-side); `approval.preview:true` renders the write-approval bar as a structured old→new diff (validation failures visible before approving) | `dialog.{quickActions,sessionTransfer,onDropElement}` + `approval.preview` |
| 🖥 server-side (node) execution (smoke-backed 4.11+) | the same SDK runs in node: `page-agent-sdk/headless` subpath + `ui:false`, backed by a real-LLM dual-protocol smoke test (full read→write→restore_data tool loop); browser-domain APIs explicitly unavailable (get_dom returns a friendly pointer to data tools; canvas render checks auto-degrade); unattended combo (approval auto-reject + `conflictPolicy` + `batch`) for scheduled/webhook long tasks | `ui:false` + headless subpath (`npm run test:node-real` is the in-repo smoke) |
| 🧪 regression toolkit eval-toolkit | the SDK's own real-LLM regression core, exported for integrators to run **their own** pre-upgrade scenario regressions: `createEvalHarness({sdk})` (waitForIdle dual-condition idle detection + collectReport snapshots) + `diffReport` (token ±15% AND ±2000 / toolCount ±3 thresholds, variance-proof); report shape matches the SDK's own — bug reports parse for free; shipped from both main and headless entries | `import { createEvalHarness, diffReport } from 'page-agent-sdk'` |
| 👁 DOM inspect (2.20+) | `get_dom` structure read + `dom_search` (selector/text) + `dom_info` (content/computed styles/event bindings from inline/Vue props/listener recorder) — lazy-injected via the `dom-inspect` skill so they don't occupy standing tool context | `capabilities.domInspect` |
| 📊 Context inspector | Snapshot actual-LLM-message composition (total / occupancy / category ratio); DebugDrawer `📊 上下文` tab + `inspectContext()`; zero LLM cost, default on | `capabilities.contextInspector` |
| 🤖 Agent-driven compression (2.33+) | `capabilities.agentCompression` (opt-in) lets the summary LLM decide per-trigger compression strategy via an `inspect_context` tool loop (keepRounds / windowRatio / summary mode / recall / preserve); `shouldTriggerCompression` gate avoids per-message LLM cost; decide failure/timeout degrades to static; `decisionTimeoutMs` / `decisionMaxTokens` configurable | `capabilities.agentCompression` + `summaryLlm` |
| 🧭 Instruction adherence (3.35+) | **Completion gate**: if the agent tries to close with plain text while todos still have unfinished items, a "two-exit" nudge is injected (mark done via update_todo, or keep executing; ≤2 retries) — fixes "planned 3 tasks, did 1, then stopped" premature interruption. **Question-intent guard**: a 3-tier regex heuristic classifies each user message as a question; on hit a "answer first, don't act" pin segment is injected (survives compression) — fixes long-chat questions being dragged into actions by history (e.g. asking "what is this component" but ending up generating code). Both default on, zero config, prefer-miss-over-false-positive | built-in |
| 🎨 Subagent model/thinking tiering | `createHtmlSubagent({ llm, thinkingMode })`: code-gen subagents get their own stronger model (main stays light for orchestration) + thinking-depth lock (`'deep'` injects thinking params for quality / `'simple'` strips them to save tokens; top-level `subagent.thinkingMode` as global default). LLMConfig construction path only (pre-built instance → warn + no-op); requires a thinking-capable model (deepseek-thinking / claude); `inspect().subagent.subagents` reflects the effective state | `createHtmlSubagent({ llm, thinkingMode })` |
| ⚡ host actions (2.20+) | Register save/publish/preview etc; SDK auto-generates named tools, agent triggers page ops directly (no `trigger_action` indirection). Semantic flags (4.20+): `readsHostState` (action reads host state → past results become stale placeholders on `notifyHostChange`), `deferredWrite` (proposal-style, effect awaits user confirmation → fact sheet annotates "awaiting confirmation", preventing false "changes complete" claims) | `actions` |
| | `hostWatch` | `boolean \| { url?, pushState?, title?, debounceMs?, ignore? }` | **(4.21+) Auto host-change reporting**: `true` = listen to hashchange/popstate, URL changes auto-trigger notifyHostChange; `pushState` patch / `title` watching opt-in; `debounceMs` default 300 (burst merging); `ignore` filters your own anchor-only jumps; server no-op (confirm via `inspect().hostWatch`) |
| | `proposals` | `{ read, onProposal, toolName?, readToolName?, contentKind?, maxPending? }` | **(4.22+) content proposal channel**: `read` returns current content, `onProposal` receives the full proposal (with diff) to render; tool names / content kind / pending cap configurable; unconfigured = zero registration |
| 🧩 schema tiered disclosure (2.20+) | Large schema → systemPrompt injects top-level overview only (no constraints/no recursion); deep constraints via `schema_data` on demand; small schema unaffected (full) | `schemaHint` |
| 🖼 image input | Built-in three entry points (📎 pick / drag / paste screenshot) → compression gate (long edge ≤1568px, ≤4 per round, >20MB rejected); multimodal main model (gpt-4o/claude/qwen-vl table hit, or `llm.vision:true`) → images sent directly as content parts, zero config; text-only main model (deepseek etc.) → `images.describe` captions each image into the user context (image never sent); neither → honest rejection, never silently dropped; `images.upload` swaps the original for an https URL (integrator OSS); persistence keeps only a thumbnail + vfs reference | `images: { upload?, describe? }` + `llm.vision` |
| 📌 cross-compress working memory (2.20+) | Pin recent read/query paths + hashes across compression; no re-fetch, correct optimistic-lock hash | `capabilities.workingMemory` |
| 🤖 unattended automation (2.20+) | Resource budget guard (`tokenBudget`/`timeBudgetMs`) + fatal-error auto-recovery (`maxAutoRetries`: restore checkpoint + retry) + cross-refresh resume + `sdk.batch(tasks)` batch processing | `capabilities.automation` |
| 📐 context resilience (2.30+) | Hard floor `contextWindow ≥200K` (rejects <200K models like legacy `deepseek`/`gpt-4o`/`glm-4.5` at startup); three gates (compress/trim/offload) thresholds follow the live window after `setLlm`; reactive retry on `context_length_exceeded` (aggressive trim → single retry, never fails raw); vfs large-result refs protected from LRU eviction + OOM 1.5× fallback; system-prompt budget (25% window, drops non-pinned segments, keeps base/mission/workingMemory) | built-in |
| 🎯 focus auto-switch (2.31+) | AI auto-judges task scope → `set_focus` (local task) / `clear_focus` (global/done); focus persists across refresh/session-switch (restore validates path via `getSchemaAtPath`, drops if invalid); spawned subagents inherit parent focus (three-layer convergence; parent unfocused → child no focus middleware, zero regression) | `capabilities.focus` |
| 🔒 precise-value protection (2.32+) | `data.resources: [{path, mode}]` protects exact-value fields: `freeze` (read-only, value hidden via `⟦frozen:path⟧` placeholder, FROZEN_FIELD on write) / `verbatim` (preserved verbatim, `⟦res:handle⟧`, modify via `resource_update` else VERBATIM_MISMATCH); write-side enforcement across commitSetToBind/applyPatches/eval + resource tools (`resource_get/update/list/delete`, advanced) + cross-compression pin | `data.resources` + `capabilities.vfs` |
| 🌍 UI customization & i18n (3.17+–3.22+) | Dialog UI is fully customizable without forking: `dialog.icons` (per-icon override; plain text or sanitized HTML fragment) + built-in `dialog.theme: 'dark'`, and the top-level `i18n` group (3.22+): `locale: 'en-US'` switches the built-in message pack (chat surface + Debug drawer + Skill panel + code preview; `formatTime`/autoTitle follow, **default systemPrompt switches to English** so agent replies match the UI language), `messages` per-key overrides (e.g. `statusDone: '<b style="color:#10b981">Done ✓</b>'` — rich-text render spots accept inline HTML fragments sanitized via a text allowlist) — switch language and tweak individual strings in one group; `DialogMessages` (~226 keys) + `MESSAGES_ZH_CN`/`MESSAGES_EN_US`/`resolveDialogMessages` exported for custom UIs | `dialog.{icons,theme}` + `i18n.{locale,messages}` |

Capabilities default on (`verify`/`approval`/`checkpoint` default off; **proactive `humanConfirm` default on** — AI asks when uncertain/multi-plan instead of guessing). Turn off unneeded ones via `capabilities` to save tokens.

## Design: the schema / systemPrompt / skill three-layer split

The core of letting AI safely edit JSON is a **three-layer decoupled split** — each layer has its own job, changing one never forces changes to the others:

| Layer | Carrier | Real intent | Loaded when |
|---|---|---|---|
| **Mechanical (structure + validation)** | `data.schema` (zod) | Defines field names/types/shapes; write-time validation guardrail (invalid → structured error, no write); `ZodObject` top-level keys auto-whitelist (hides undeclared fields, prevents accidental delete/edit) | Fixed at construction; field `.describe()` text auto-extracted into systemPrompt |
| **Generic rules (identity + write methodology)** | `systemPrompt` | Agent identity; `reliableWriteRules` (read before write, fields per `describe`, retry on validation error, prefer incremental patch) | Every round (persistent) |
| **Deep business (semantics + edit recipes)** | `skills` (`defineSkill`) | Component-library specs, detailed field business semantics, scenario-specific edit strategies, glossaries | On-demand (agent sees name+description index, calls `load_skill` to pull full text — saves tokens) |

**How they cooperate**

- **Structure** → schema defines it (integrator writes); the agent never sees the zod itself, but `.describe()` text auto-enters the systemPrompt "operable data" section so the agent knows field names + purpose
- **Semantics** → shallow via schema `.describe()` (one line per field, persistent); deep via skills (full business spec, on-demand)
- **Edit judgment** → generic strategy via `systemPrompt` `reliableWriteRules` (persistent); business-specific strategy via skills (on-demand); fallback via schema validation feedback (write errors return structured errors the agent retries from)

**Design intent**: schema governs "what can be changed / whether a change is valid" (mechanical safety); systemPrompt + skills govern "how to change / why to change it this way" (semantic guidance). Three layers decoupled — change schema and validation follows automatically; change skills without touching the prompt; change the prompt without touching the schema.

**Example (low-code page builder)**

- schema: `z.object({ components: z.array(...) }).describe('component tree')` → agent knows there's a `components` field, an array of components
- systemPrompt: built-in "JSON operation assistant" + `reliableWriteRules` (default `appendReliableWriteRules:true` auto-appends with a `---` separator distinguishing user content from SDK-appended rules) → agent knows to `read` before write, prefer `write` patch incremental
- skill: `page-builder` skill details each component's props field meanings + edit recipes (e.g. "to change Banner bg use `write({patch:{op:'set', jsonPath:'components.0.props.bg'}})`") → agent loads on demand, edits precisely

> `appendReliableWriteRules` defaults to `true`: when a custom `systemPrompt` is set, auto-appends `reliableWriteRules` with a `---` separator (avoids forgetting the write methodology); set `false` to disable; no effect when `systemPrompt` is omitted (default prompt already includes them).

## Agent Integration Cheat Sheet (for AI agents)

> Dense integration reference for AI agents: exports / options / extension points / built-in tools / file structure. Deep dive in `doc/` and `CLAUDE.md`.

### Exports (`import { ... } from 'page-agent-sdk'`)

```ts
// entry & tool construction
createChatSdk, defineTool, defineSkill, presets, z
// proxy connection (prevent apiKey leakage: proxy mode / direct mode)
createProxyLlm
// harness & middleware (custom orchestration)
createAgent, createSubagentMiddleware, createSubagentsMiddleware,
createVerifyMiddleware, createWriteBackCheck, createApprovalMiddleware,
createHumanConfirmMiddleware, createHumanConfirmTool, createCheckpointMiddleware, createCheckpointManager,
createUsageHintsMiddleware, createDataOps, createVfs, connectMcp
// context & model
resolveContextOptions, CONTEXT_PRESETS, resolveModelCaps, estimateTokens, isContextLengthError, MIN_CONTEXT_WINDOW
// storage
createSessionStore, createMemoryBackend, createWebStorageBackend, isQuotaError
// UI (reuse when headless)
ChatDialog, MessageContent, CodePreview, SkillPanel, DebugDrawer, useChat
// types (omitted): ChatSdkOptions, Middleware, SubagentConfig, SkillSpec, DataConfig, AgentMessage, StreamEvent …
```

### `createChatSdk` options cheat sheet

| Group | Option | Type / Default | Description |
|---|---|---|---|
| **Basics** | `container` | `string \| HTMLElement` | Mount point (`ui:true` required) |
| | `ui` | `boolean \| 'default'` · default `true` | `false` = headless (build UI with `agent.messages`) |
| | `llm` | `LLMConfig \| BaseChatModel` · **required** | `LLMConfig={provider?,apiKey,baseUrl?,model?,temperature?,maxTokens?}`; `provider` defaults to `'openai'` (OpenAI/DeepSeek-compatible, default DeepSeek); `'anthropic'` dynamic-loads `@langchain/anthropic` for Claude native protocol |
| | `id` | `string` | Stable id (multi-agent isolation + persistence resume; random+warn if omitted) |
| | `systemPrompt` | `string` | Agent identity (no hardcoded business; inject via this). Optional — built-in default is capability-aware (4.16): with data declared = JSON operation assistant + `reliableWriteRules`; with dataOps:false + domInspect = "page content assistant" (write rules not appended); passing your own fully overrides it. `appendReliableWriteRules` defaults to `true`: auto-appends `reliableWriteRules` with a `---` separator (not appended when dataOps:false); set `false` to disable |
| | `augmentSystem` | `(ctx:{state,data?}) => string \| undefined` | Dynamic system prompt injection hook: called each turn, returns a string injected as a segment based on runtime state/data; return undefined to skip; callback errors degrade to skip (no crash). `ctx.data` is taken from liveData() each turn (auto-syncs after setData), enabling dynamic component descriptions / partial schema hints. Not set = current behavior |
| **Page data** | `data` | `{schema,bind,description?}` | Single main object: declare zod schema (validation + field descriptions auto-injected into prompt) + bind (reactive/plain object, tools read/write directly, no `window`) + description |
| | `tools` / `skills` / `memory` | `Tool[]` / `SkillSpec[]` / `string` | Custom tools / skills / AGENTS.md-style directives |
| | `images` | `{upload?,describe?,describeTimeoutMs?}` | **Image input (image-input-vision)**: built-in three entry points (📎 pick / drag / paste) → compression gate (long edge ≤1568px, ≤4 per round, >20MB rejected). Multimodal main model (table hit or `llm.vision:true`) → images sent directly as content parts, zero config; text-only main model → configure `describe` to caption each image into the context (image never sent); neither → honest rejection, never silently dropped; `upload` swaps the original for an https URL (integrator OSS). See [usage-guide §6.17](doc/usage-guide.en.md#617-image-input-multimodal-direct--captioning-bypass) |
| | `dialog.autoQuote` | `boolean` | **Selection quoting · silent capture (page-quote, default false)**: when true, opening the drawer or clicking the input area lazily captures the host page's current text selection (outside the dialog) into a removable quote chip, sent with the next message as question context. Privacy opt-in; `sdk.setQuote/clearQuote` work regardless of this toggle. See [usage-guide §6.20](doc/usage-guide.en.md#620-text-selection-quoting--page-qa-page-quote--read_page--pagecontext) |
| | `dialog.selectionMenu` | `boolean` | **Selection floating menu (page-quote explicit confirm, default false)**: selecting text floats a "❝ Quote to AI assistant" toolbar above the selection; click = attach the quote chip + open the dialog + focus the input; dismiss on outside click / scroll / Esc; composable with autoQuote. See [usage-guide §6.20](doc/usage-guide.en.md#620-text-selection-quoting--page-qa-page-quote--read_page--pagecontext) |
| | `capabilities.pageContext` | `boolean` | **Page anchor (default false)**: injects the current page title + URL as a pinned system segment each round (pair with `domInspect`'s read_page for page QA). See [usage-guide §6.20](doc/usage-guide.en.md#620-text-selection-quoting--page-qa-page-quote--read_page--pagecontext) |
| | `screenshot` | `{renderer?}` | **Screenshot config (take_screenshot)**: assembled when domInspect is on AND (multimodal main model \|\| images.describe); default renderer is html-to-image, pass `renderer` when CSP restricts. See [usage-guide §6.21](doc/usage-guide.en.md#621-screenshot-viewing--page-content-analysis-take_screenshot--page-analysis) |
| | `capabilities.domEdit` | `boolean` | **DOM editing (default false; requires domInspect)**: assembles `dom_edit` (atomic batch ops on the host page — set_text/set_html/set_attr/add_class/set_style/insert/remove/move/highlight) + `dom_restore` (snapshot rollback, stack of 20). Unique-selector discipline, dangerous-content gate (script / on\* / javascript:), SDK-own-DOM protection; edits are session-ephemeral — data-driven pages should edit data (`write`) instead. See [usage-guide §6.22](doc/usage-guide.en.md#622-dom-editing-dom_edit--dom_restore) |
| **Capability toggles** | `capabilities` | `{planning?,dataOps?,fetch?,skills?,vfs?,summarization?,memory?,subagent?,verify?,focus?}` | Default all on (`verify` default off, opt-in; `focus` = context focus for refining one component, default on); `false` to turn off |
| | `permissions` | `PermissionRule[]` | Scope whitelist (first-match-wins, default off) |
| | `humanConfirm` | `boolean` · default `true` | Proactive inquiry (AI asks when uncertain/multi-plan) |
| | `approval` | `{tools?,confirm?,timeoutMs?,humanConfirmTool?}` · default off | Passive confirm whitelist (pre-write allow/deny) |
| | `checkpoint` | `boolean \| {maxCheckpoints?,auto?}` · default off | Session-level rollback (`auto` default `true`) |
| | `verify` | `{check?,maxAttempts?,adversarial?}` | Needs `capabilities.verify:true`; `check` omitted → `createWriteBackCheck` (read-back root auto-bound to `data.bind`, adapts to `sdk.setData` runtime swap) |
| **Subagents** | `subagent` | `{allowedTools?,systemPrompt?,temperature?,llm?,maxDepth?·1,maxParallel?·4}` | Runtime ad-hoc delegation (`spawn_agent`/`spawn_agents`) |
| | `subagents` | `SubagentConfig[]` | Pre-declared named subagents → each generates `use_<id>` tool |
| **Capability packs** (2.37+) | `subagents` | `createRagSubagent({retriever?,loader?,useVfs?})` / `createHtmlSubagent({writablePaths?,codeVfsPrefix?,codeField?,orchestratorPrompt?,formatCheck?,craftNotes?,design?})` (3.9+ usually no need to declare one — createChatSdk auto-registers a default HTML subagent at assembly; declare explicitly only to customize codeField/formatCheck etc.; open schemas / nested containers / dotted codeField need an explicit value) | Specialized subagent factories — **RAG**: multi-source retrieval (semantic `search_docs` / async `load_doc` / vfs / fetch), read-only, independent context; **HTML**: code-component generation — **code as a data asset** (code lives in `data.<writablePath>[i].code`, persisted with the data JSON; vfs is an edit working copy). The framework auto-checks-out (data.code→vfs by `__pgId`) before the subagent runs and auto-commits (vfs→data.code, direct bind mutation — no snapshot stack) after; the main agent is transparent (main-scope read sees a `<code Nkb>` summary). New components via `write`; edits via `vfs_edit` on the working copy. `codeField` (default `'code'`, nested jsonPath like `'props.html_code'` for open-schema platforms; + assembly-time hit-check warns on wrong path); main-agent orchestration **auto-injected** at assembly (3.9+ zero-config: a default HTML subagent is **auto-registered** when no explicit one exists and the schema has a code array — no switch needed, info logged; opt out prompt-only via `orchestratorPrompt:false`); model advice: prefer strong instruction-following models (deepseek-v4/claude/gpt-4o) for html codegen — flash-class amplifies over-thinking; **craft notes `craftNotes`** (on by default): the html subagent's final reply `[note]` lines are persisted to the component's `__pgNotes` (travels with the data JSON), and injected via the file map on the next delegation to that component ("handoff from the previous maintainer": design decisions / user feedback / pitfalls) — design intent persists across delegations; opt out via `craftNotes:false`; **built-in design-taste skill `design`** (4.7+ mounted by default): vendors ConardLi garden-skills `web-design-engineer` (MIT) — declare-the-design-system-first / anti-AI-cliché rules / oklch palettes / 25 named style recipes (linear/apple-hig/muji…), progressive disclosure (33K main doc + 29 references enter context only on load_skill); complements the html-fragment skill (taste vs. mechanics); `design:false` disables / pass a SkillSpec to replace; `formatCheck` on by default = `validate_code` self-check + verify beforeReturn gate with feedback self-correction; `validateHtmlFormat` exported. **Breaking (3.0)**: removed `onComplete`/`codeRef`/`codeSnapshots` — migrate `codeRef`→`code` field, drop `onComplete`/mirror. Composable/splitable, opt-in, ship with `rag-search`/`html-builder` skills. Plus `sdk.vfsWrite(path,content)` for async doc injection. See [doc/usage-guide.md](doc/usage-guide.md#capability-packs) |
| **Subagent observability** (2.38+) | — | `inspect().subagent.{active,history}` / `sdk.{getActiveSubagents,subagentHistory}` | active/history runtime state + DebugDrawer "🤖 subagent" tab (follows `subagent` capability, session-level, not persisted) |
| **Context** | `contextPreset` | `'auto' \| 'conservative' \| 'aggressive' \| 'complex'` · default `auto` | Compression preset (`complex` for multi-step / large-JSON / long-workflow tasks) |
| | `contextOptions` | `Partial<ContextManagerOptions> \| false` | Fine params (`false` disables compression). Includes `promptSoftCapTokens` (3.11+ compression cost cap — 160K default when window ≥320K, explicit `0` disables) and `preserveLastToolResults` (default `['schema_data','read']` — keep field descriptions in compressed summary) |
| | `summaryLlm` | `BaseChatModel \| LLMConfig` | Summary-dedicated LLM (defaults to main `llm`) |
| | `maxMemoryRounds` | `number` · default `30` | Dialog history memory round cap (`0` disables trim) |
| | `staleReadInvalidation` | `boolean` · default `true` | 3.42+ write-driven stale read invalidation: within one invoke's ReAct window, old `read`/`query_data`/`search_data` results hit by a later successful write are replaced with an invalidation placeholder (keeps the model from answering state from stale snapshots; anti-thrash copy references the write's own new value/hash). `false` = off for main + subagent stacks |
| | `vfs` | `{initialFiles?,maxBytes?,poolBytes?}` · default 4MB | In-memory workspace cap (LRU evict on overflow) |
| **Persistence** | `storage` | `'indexed' \| 'session' \| 'local' \| 'memory' \| config (incl. custom backend instance) \| false` · default off | Assign to enable; multi-agent isolated by `id`; pass a `StorageBackend` instance as `backend` for server-side persistence (5 KV methods) |
| | `session` | `{id?,autoResume?,title?}` | Session control |
| | `shareContext` | `boolean` · default `false` | Same `id` instances share one agent |
| **Robustness/other** | `maxRetries` / `maxParallelTools` / `maxToolRounds` | `number` · 2 / 1 / 30 | Model retries / per-round tool concurrency (>1 enables same-round parallel delegation with failure isolation + per-component mutex locks) / max rounds |
| | `roundTokenBudget` | `number` · default `0` (off) | Per-invocation cumulative token cap (3.11+; exceed → friendly wrap-up, partial work preserved; orthogonal to automation's `tokenBudget`, no automation capability needed) |
| | `mcp` | `McpServerConfig[]` | Remote MCP servers (http/sse/websocket) |
| | `middleware` | `Middleware[]` | Custom middleware (appended to built-in stack) |
| | `streaming` / `debug` | — | UI/debug |
| | `dialog` | `DialogConfig` | Grouped dialog UI config; see `DialogConfig` fields below |

#### `DialogConfig` fields

| Field | Type · default | Purpose |
|---|---|---|
| `title` / `placeholder` | `string` | Dialog title / input placeholder (cosmetic) |
| `drawer` | `boolean` · default `false` | Drawer mode: ChatDialog slides in from right + mask + close button (replaces collapse arrow); clicking mask/close defaults to `hide` (keeps agent/history/in-flight generation; `mount`/`show` resumes). Pass `onClose` to customize |
| `drawerWidth` | `number \| string` · default `420` | Drawer mode width (pixels or CSS string, e.g. `500` / `'500px'` / `'40vw'`); only effective when `drawer: true`; inline mode width determined by `container` |
| `drawerResizable` | `boolean` · default `true` | Drawer left-edge drag handle: drag / arrow keys to resize (clamped 320 ~ min(960, 90% viewport)); width remembered in `localStorage`; `false` = fixed width |
| `drawerHidden` | `boolean` · default `false` | Drawer mode hidden by default (not shown after `mount`; requires `sdk.show()` to display): for "click button to show chatbox" scenarios; only effective when `drawer: true` |
| `inputRows` | `number` · default `2` | Input box rows (visible height); `1` = single row; `2` = 2-row initial height, auto-expands up to max-height:100px; `>2` = taller initial height |
| `onClose` | `() => void` | Drawer mode close callback (default `hide`; pass to override and sync external mount state) |
| `theme` | `'light' \| 'dark'` · default `'dark'` | Built-in theme (dark = Ark design palette); fully customizable via `--cs-*` on an ancestor |
| `i18n` | `I18nOptions` | **Top-level i18n group (3.22+; replaces `dialog.locale`/`dialog.messages`)**: `locale` switches the built-in message pack — chat surface + Debug drawer + Skill panel + CodePreview; `formatTime` (12h/24h), autoTitle, and the **default systemPrompt** follow (`en-US` → English `DEFAULT_SYSTEM_PROMPT_EN` with a "Respond in English" anchor, so agent replies match the UI language; a custom `systemPrompt` is untouched, but the auto-appended `reliableWriteRules` segment switches to English). `messages` = per-key overrides (priority over the locale pack, e.g. `statusDone: '<b style="color:#10b981">Done ✓</b>'` — values may be inline HTML fragments on rich-text render spots, sanitized via a text allowlist). Full key list (~226 keys) in `DialogMessages` |
| `icons` | `Partial<DialogIcons>` | **Icon customization**: partial override of default emojis (`header` 🤖 / `subagent` 🤖 / `subagentProgress` 🧬 / `empty` 💬 / `focus` 🎯 / `queued` 📋 / `queuedEdit` ✏️ / `recommend` 💡 / `conflict` ⚠️; `assistantAvatar`/`userAvatar`/`send` and the header-button keys `newSession`/`history`/`more`/`close` default to built-in SVG — pass emoji/char/HTML fragment to replace; history delete button `sessionDelete` defaults to ✕ text). Values: plain text (emoji/char) **or an HTML fragment** (starting with `<`, e.g. inline `<svg>`/`<img>` — sanitized via a DOMPurify icon allowlist; event attributes/dangerous protocols stripped); empty string hides the icon (button keys treat it as unset, prevents an empty button); unset keys keep defaults |
| `headerLabels` | `boolean` · default `true` | **Adaptive header-button text labels**: when wide enough (header content ≥440px ≈ dialog ≥472px with default padding), "New chat / History / More" show text+icon; narrower widths fall back to icon-only (close stays icon-only; pure CSS container queries, old browsers gracefully degrade to icon-only); `false` = always icon-only. Button text = i18n `newSession`/`history`/`more` keys (overridable via `i18n.messages`), icons = the four same-named `dialog.icons` keys |
| `toolStepView` | `ToolStepViewFn` | **Tool-step display mapper (pure display-layer interceptor)**: replaces raw tool names (read/write/use_html …) on the tool-call step rows with business-friendly names/content. Called on each tool-call render with `name`/`args`/`status`/`result` (map dynamically by args); return `{ title?, detail? }` or `undefined` (fall back to the raw name); mapper exceptions are safe (fall back to the raw name). Example: `toolStepView: (s) => s.name === 'write' ? { title: 'Edit page', detail: (s.args as any)?.jsonPath } : undefined`. Never affects the tool names/protocol/validation sent to the LLM; subagent steps map too; the expanded args/result detail panel stays raw |

### Extension points

```ts
// ① Custom tool
const myTool = defineTool({ name: 'do_x', description: '...', schema: z.object({...}), handler: (args) => 'result' })
createChatSdk({ tools: [myTool], /*...*/ })

// ② Custom skill (progressive disclosure: load_skill fetches details on demand)
const mySkill = defineSkill({ name: 'style_guide', description: 'Brand color spec', body: 'Primary #1f4d3a…' })
//    Dynamic skill (skill-external-scripts): exec runs a script on load → inject live data; tools attaches callable tools
//    defineSkill({ name: 'orders', getContent: () => 'spec…', exec: { code: '...', context: 'sandbox' }, tools: [() => orderQueryTool] })
createChatSdk({ skills: [mySkill], /*...*/ })

// ③ Custom middleware (8 hooks: beforeAgent/wrapModelCall/beforeModel/afterModel/wrapToolCall/afterAgent/beforeReturn + augmentPrompt/compressInput/tools)
const mw: Middleware = { name: 'telemetry', afterModel: async (ctx, next) => { await next(ctx); console.log('round done') } }
createChatSdk({ middleware: [mw], /*...*/ })

// ④ Pre-declared subagents (planner-reflector-executor fixed roles)
createChatSdk({ subagents: [
  { id: 'planner', description: 'Creative planner', temperature: 0.9, systemPrompt: '…' },
  { id: 'reflector', description: 'Reflective reviewer', temperature: 0.3, systemPrompt: '…' },
], /*...*/ })
```

### Built-in tools (Agent-callable)

- **data ops** (all 9 tools always exposed): `read` (list/get/describe merged; omitting jsonPath returns overall description + format) / `write` (set/edit/delete merged + auto optimistic lock + auto snapshot) — recommended; `restore_data` / `history_data` (snapshot rollback/history); `schema_data` / `diff_data` / focus tools (low-level CRUD `get_data`/`set_data`/`edit_data`/`delete_data` removed in 4.0; `describe_data` removed in 4.9 — fully equivalent to `read` without jsonPath, 0 calls across three consecutive real-LLM baselines)
- **window query**: `query_data` (JSONPath) / `search_data` (fuzzy) / `eval_script` (sandboxed)
- **fetch**: `fetch_document`
- **vfs**: `vfs_read` / `vfs_write` / `vfs_edit` / `vfs_ls` / `vfs_glob` / `vfs_grep`
- **planning/skills**: `write_todos` / `define_skill` / `load_skill` (skill can carry `exec` to run a script on load injecting live data + `tools` for repeatedly-callable tools; `exec` always runs in the Worker sandbox — `context:'host'` was removed in 4.1.0)
- **human confirm**: `request_human_confirmation` (proactive inquiry, default on)
- **subagents**: `spawn_agent` / `spawn_agents` / `use_<id>` (pre-declared)
- **checkpoint**: `restore_last_checkpoint` / `list_checkpoints`

### File structure

```
src/core/
├── sdk/createChatSdk.ts        # imperative entry (assembles harness + tools + middleware)
│   sdk/defineTool.ts  presets.ts  contextPreset.ts
├── harness/                    # in-house ReAct harness (middleware-driven)
│   createAgent.ts  middleware.ts  state.ts
│   todos.ts  skills.ts  memory.ts  summarization.ts  retry.ts
│   subagent.ts  verify.ts  approval.ts  humanConfirm.ts  checkpoint.ts
│   permissions.ts  usageHints.ts
├── tools/                      # dataOps (schema validation + incremental edit + snapshot + whitelist) / dataSlotQuery / fetchDoc
├── backends/                   # vfs (memory) / storage (IndexedDB + multi-backend + quota eviction)
├── mcp/client.ts              # remote MCP tool integration
├── composables/               # useChat / useContextManager / useMarkdown
├── components/                 # ChatDialog / MessageContent / CodePreview / DebugDrawer
└── types/index.ts  index.ts    # types / sole library entry
examples/                       # page-demo / nested-demo / dynamic-demo / human-confirm-demo / planner-demo / subagent-demo / toolsets-demo / proxy-demo
doc/                            # usage-guide / architecture / context-management / architecture-files
CLAUDE.md                       # architecture + gotchas + coding conventions (agent must-read)
```

## Skills for AI tools (for integrators)

A ready-to-use Agent Skill is bundled for integrators using Claude Code / Cursor (or any agent harness that loads `.claude/skills/` / `~/.claude/skills/`). It teaches the AI how to use **this SDK** in your project:

| Skill | When it triggers |
|---|---|
| `page-agent-sdk-integrate` | Embedding the SDK — choose install method, declare `data` + zod schemas, configure the LLM, mount, subscribe to events (`onEvent` / `sdk.hook`), run headless, troubleshoot common pitfalls |

**Install** (pick one):

```bash
# Option A — copy from the installed npm package
npm i page-agent-sdk
cp -R node_modules/page-agent-sdk/skills/page-agent-sdk-integrate ~/.claude/skills/

# Option B — download from the repo (no install needed)
curl -L https://github.com/whyymj/page-agent-sdk/tarball/master | tar xz --strip-components=1 --wildcards '*/skills/page-agent-sdk-integrate'
mv skills/page-agent-sdk-integrate ~/.claude/skills/
```

After install, restart your AI tool; the skill auto-triggers when you ask things like "add page-agent-sdk to my page".

> **Don't want to install the skill?** Copy the bundled generic integration prompt template to the target project's AI: see `node_modules/page-agent-sdk/skills/page-agent-sdk-integrate/references/integration-prompt.md` (fill in `[...]` per your scenario). For a specific scenario example, see the repo's `doc/集成提示词-Vue2-低代码页面-抽屉.md`.

> A second skill `page-agent-sdk-release` (release workflow for maintainers) is kept in the repo's `.claude/skills/` for project maintainers only and is **not** distributed via the npm package.

## Architecture

```mermaid
flowchart TD
    APP[Host page] -->|createChatSdk| SDK[createChatSdk<br/>assembles harness + tools + middleware]
    SDK --> CORE[AgentCore<br/>messages / vfs / store / checkpoint]
    CORE --> AGENT[createAgent<br/>ReAct loop + middleware stack]
    AGENT --> MW[Middleware stack<br/>usageHints→todos→skills→vfs→summarization<br/>→memory→permissions→checkpoint→approval<br/>→humanConfirm→verify→subagent→user]
    AGENT --> TOOLS[Tools<br/>dataOps / fetchDoc / vfs / MCP / user]
    TOOLS -->|direct read/write| DATA[Main data bind<br/>reactive/plain object<br/>schema validation + whitelist]
    AGENT --> LLM[LLM<br/>OpenAI-compatible / any ChatModel]
    SDK --> UI[ChatDialog UI<br/>Vue bundled in / or headless]
```

- **Framework-agnostic**: Vue bundled in the lib (not a peer); host can be React/vanilla. Also supports `ui:false` headless — and runs in **Node.js** as a backend Agent (custom tools / subagents / verify; disable `fetch`+`eval_script` (dataOps body works in Node with any `bind`), use `storage:'memory'`)
- **Provider-agnostic**: `llm` accepts any LangChain `BaseChatModel`, or `LLMConfig` (`provider:'openai'` default builds `ChatOpenAI`, OpenAI-compatible default DeepSeek; `provider:'anthropic'` dynamic `import('@langchain/anthropic')` builds `ChatAnthropic` for Claude native protocol; `createProxyLlm` proxy stays OpenAI-only)
- **In-house harness**: no LangGraph/langchain full bundle; avoids browser bundling blockers

## Configuration

```bash
# .env (VITE_ prefix)
VITE_AI_API_KEY=sk-...
VITE_AI_BASE_URL=https://api.deepseek.com
VITE_AI_MODEL=deepseek-v4-flash
VITE_AI_TEMPERATURE=0.3        # low temp recommended for structured ops
# VITE_AI_MAX_TOKENS=           # omit → model default
```

> ⚠️ **Minimum context window 200K (2.30+)**: the SDK rejects models with `contextWindow < 200000` at startup (`setLlm`/subagent too) — excludes legacy `deepseek`/`deepseek-reasoner`/`glm-4.5`/`gpt-4o`/`qwen-max` etc. Use a ≥200K model (`deepseek-v4`/`glm-5.2`/`claude-3-*`/`kimi-k3`/`qwen-1m`) or declare `llm: { contextWindow: 500000 }` to override the table lookup.

```ts
createChatSdk({
  container: '#root',
  llm: { apiKey, baseUrl, model },
  id: 'my-agent',              // stable id (multi-agent isolation + persistence resume)
  systemPrompt: '...',
  data: { schema, bind, description? },  // single main object: bind directly connects reactive/plain object (tools read/write bind, not auto-mounted to window); schema field .describe() auto-injected into systemPrompt「可操作数据」section
  storage: 'indexed',          // persistence (default off)
  streaming: true, ui: 'default',
  capabilities: { verify: true },        // capability toggles
  humanConfirm: true,           // proactive inquiry (default on)
  approval: { tools: ['write'] }, // passive confirm whitelist (default off)
  checkpoint: true,
  contextPreset: 'auto',       // auto/conservative/aggressive/complex
  summaryLlm: { ... },         // summary-dedicated LLM (defaults to main llm)
  maxRetries: 2, maxParallelTools: 1,
  subagent: { allowedTools: [...] },
  middleware: [/* custom middleware */],
  onEvent(e) {                 // SDK event callback: data change / message update / tool call / usage / session_restored / error, replaces polling
    if (e.type === 'data_change') refreshUI()
    if (e.type === 'usage') console.log('round tokens', e.usage, 'cumulative', e.cumulative)
    if (e.type === 'session_restored') toast(`restored ${e.rounds} rounds`)
  },
  // onAudit: (entry) => logAudit(entry),  // structured audit of data writes (independent of debug)
}).mount()

// Convenience API
// sdk.exportData()              // deep copy of main data bind (backup/migrate)
// sdk.importData(json)          // replace bind in-place (preserves reactive ref; schema-validated by default)
// sdk.setSkills(skills)         // runtime swap the entire skill list (same-name overwrites; clears cache, index re-renders next round)
// sdk.invalidateSkillCache(name?)  // invalidate skill full-text cache (proactive; omit name to clear all)
// sdk.addSkill(skill)          // user-created skill (independent SkillStore, default indexedDB, separate from storage; same-name overwrites; ChatDialog has a built-in Skill panel)
// sdk.removeSkill(name)        // remove a user-created skill (only user-created, not integrator initialSkills)
// sdk.listUserSkills()         // list user-created skill names
// sdk.getUserSkill(name)       // read a user-created skill's detail (for SkillPanel editing)
// skillStorage: { id: 'shared' }  // manually specify the same id to share the same skill set across pages/agents
// sdk.usage                     // cumulative token usage {prompt_tokens, completion_tokens, total_tokens}
// sdk.hide() / sdk.show()      // drawer mode hide/show (keeps agent/history/in-flight generation; mount after hide resumes via show, no rebuild)
// Runtime dynamic reconfiguration (zero-breakage; not calling = current behavior):
// sdk.setTools(tools)           // replace user tools at runtime (built-ins untouched; internal rebind to LLM; next round uses new set)
// sdk.addTool(tool)             // append user tool at runtime (dedup by name)
// sdk.removeTool(name)          // remove user tool at runtime (built-ins untouched); returns whether removed
// sdk.setLlm(llm)               // switch LLM at runtime (quota-exhausted→cheaper model / complex task→stronger model / switch provider; param BaseChatModel or LLMConfig; rebind + re-resolve model caps)
// sdk.setMemory(source)         // update memory at runtime; supports string and sync/async function (async fn evaluated in background, fits RAG doc loading)
// sdk.refreshMemory()           // re-evaluate current memory function source (force refresh after RAG doc update); returns latest text
// sdk.setSubagents(configs)     // replace pre-declared subagents at runtime (regenerates use_<id> delegation tools + rebind; requires subagents:[] at creation)
// sdk.addSubagent(config)        // append pre-declared subagent at runtime
// sdk.removeSubagent(id)        // remove pre-declared subagent at runtime; returns whether removed
```

## Examples

After `npm run dev`, visit the corresponding page:

| Example | Entry | Demonstrates |
|---|---|---|
| minimal-demo | `/examples/minimal-demo/` | Minimal: 5-line chat dialog, no data ops |
| rag-demo | `/examples/rag-demo/` | RAG/MCP, 4 modes: A `memory` async fn load/switch KB · B `createRagSubagent` mock retriever · C subagent + real MCP (`VITE_RAG_MCP_URL`) · D MCP direct inject (`npm run mcp:mock` fallback) |
| headless-demo | `/examples/headless-demo/` | Headless: `ui:false` + self-built UI via `sdk.messages`/`sdk.send` |
| page-demo | `/` | Self-bootstrapping demo: left JSON reactive page + right chat |
| nested-demo | `/examples/nested-demo/` | Nested block tree + human confirm + checkpoint |
| dynamic-demo | `/examples/dynamic-demo/` | Lazy-loaded components with dynamic schemas (`sdk.setData`/``) |
| human-confirm-demo | `/examples/human-confirm-demo/` | AI proactive inquiry (multi-plan pick) + pre-write confirm |
| planner-demo | `/examples/planner-demo/` | Plan-reflect-execute (high-temp creative planner + low-temp reflector) |
| subagent-demo | `/examples/subagent-demo/` | Subagent parallel orchestration |
| animation-demo | `/examples/animation-demo/` | ChatDialog enter/collapse/unmount animations + inline/drawer + hide/show |
| multi-agent-demo | `/examples/multi-agent-demo/` | Multi-agent parallel + exclusive switch (3 independent agents, drawer hide/show keeps each history) |
| proxy-demo | `/examples/proxy-demo/` | LLM connection config: proxy to prevent apiKey leakage (browser holds only userToken, proxy injects real key; auto-refresh on expired token; needs `npm run proxy:mock`) + Provider switch (`provider:'anthropic'` for Claude native protocol, streaming + extended thinking) |
| images-demo | `/examples/images-demo/` | Image input: text-only main model + `images.describe` captioning bypass (captions injected, image never sent; auto direct-send when the main model is multimodal) |
| docs-demo | `/examples/docs-demo/` | Docs-site integration template: selection-quote questioning (floating menu + autoQuote lazy capture) + paginated `read_page` page QA + `pageContext` page anchor + `?shot=1` screenshot visual verification + dom_edit highlight/rollback; copy into your own docs site |

Framework-agnostic integration: `demo/plain.html` (importmap + esm.sh).

### Multi-agent parallel + exclusive switch

A single page can host multiple independent agents (each `createChatSdk` + distinct `id` for isolation), each managing its own `data`/history/tools, running their own generation tasks **in parallel**; **exclusive** chatbox switching uses `drawer` + `hide()`/`show()` — `hide` the old one (keeps agent/history/in-flight generation), `show` the new one (history resumes), no unmount, no lost conversation:

```ts
const agents = [agentA, agentB, agentC]  // each createChatSdk({ id, drawer: true, data, ... })
await Promise.all(agents.map(a => a.mount()))  // ready in parallel
agents.slice(1).forEach(a => a.hide())         // show only the first initially

let active = 0
function switchTo(i: number) {
  agents[active].hide(); active = i; agents[i].show()  // exclusive switch, each history preserved
}
```

> Multiple agents operating on the same `data` need coordination (optimistic lock `conflictWatchFields` or `jsonPath` partitioning); each managing its own `data` object has no conflict (recommended). Full example: `examples/multi-agent-demo/`.

## Documentation

| Doc | Contents |
|---|---|
| [Doc Index](https://github.com/whyymj/page-agent-sdk/blob/master/doc/README.en.md) | Navigation + other info sources (specs/changes/tests) |
| [Usage Guide](https://github.com/whyymj/page-agent-sdk/blob/master/doc/usage-guide.en.md) | Install / options / capability deep-dive / custom middleware / FAQ |
| [Architecture](https://github.com/whyymj/page-agent-sdk/blob/master/doc/architecture.md) *(Chinese)* | Layering / control flow / window-op safety flow |
| [Context & Compression](https://github.com/whyymj/page-agent-sdk/blob/master/doc/context-management.md) *(Chinese)* | Context composition / 4-layer compression / flow diagrams |
| [File Overview](https://github.com/whyymj/page-agent-sdk/blob/master/doc/architecture-files.md) *(Chinese)* | Per-file responsibilities / deps / data flow |
| [CLAUDE.md](https://github.com/whyymj/page-agent-sdk/blob/master/CLAUDE.md) | **agent must-read** · architecture / gotchas / coding conventions |

## Self-tests

```bash
npm test            # 3283 assertions (tsx, source-level; no LLM dependency)
npm run test:e2e    # 1048 integration assertions (node, built dist; covers APIs/options/modules/simple&complex scenes: default systemPrompt(capability overview) / dynamic register + inspect sync / inspect(tools/middleware/subagent/verify/mcp/todos/lastCompression/checkpoints reflect config) / custom tools/middleware/skills/memory injection / runtime dynamic reconfiguration(setTools/addTool/removeTool/setLlm/setMemory/setSubagents reflect) / switchSession(on/off) / shareContext on/off sharing/independent / storage backends + object config / presets(3) / checkpoint / exports complete(39+ fns/components) / util fns usable(isQuotaError/estimateTokens/jpEval/searchJson) / source=builtin / mount boundary / hook multi-listener / llm config / hide/show / error scenes)
```

## Local npm package test

Verify the **published npm package** actually works (distinct from `src/` local code and `dist/*.iife.js` local build): set up a standalone vite app in an isolated directory, install `page-agent-sdk` from the npm registry, and run it.

**Scenario**: after publishing a new version, confirm the package from `npm install page-agent-sdk` imports + mounts + calls tools correctly; or reproduce an integrator's issue in a clean environment (ruling out local `node_modules` cache / stale `dist` artifacts).

**Minimal steps**:

```bash
mkdir npm-pkg-test && cd npm-pkg-test
npm init -y
npm install page-agent-sdk zod @langchain/openai @langchain/core
npm install -D vite typescript
```

`index.html` (mount point) + `main.ts`:

```ts
import { createChatSdk, z } from 'page-agent-sdk'
import 'page-agent-sdk/style.css'

const app = { title: 'Demo', theme: 'light' }
window.app = app  // optional: mount to window for your page; tools operate on `bind` directly

createChatSdk({
  container: '#root',
  llm: { apiKey: 'sk-...', baseUrl: 'https://api.deepseek.com/v1', model: 'deepseek-v4-flash' },
  systemPrompt: 'You are a page assistant; read/write the main data via tools.',
  data: {
    schema: z.object({
      title: z.string().describe('Title'),
      theme: z.enum(['light', 'dark']).describe('Theme'),
    }),
    bind: app,
    description: 'App config',
  },
}).mount()
```

`npx vite` → type "change app.theme to dark" in the dialog → AI calls `write({ value:{ theme:'dark' }, patch:{ op:'merge' } })` → `app.theme` becomes `dark` → verified.

> Add this test dir to `.gitignore` (local only, not in repo) to avoid committing `.env` with real keys to remotes.

## Bundle size & tree-shaking

The package ships three builds — pick by integration scenario:

| Build | File | When to use | Approx. size |
|---|---|---|---|
| ESM (bundled, peer external) | `dist/page-agent-sdk.js` | `import` via npm or esm.sh — recommended for module hosts | ~963 KB |
| UMD | `dist/page-agent-sdk.umd.cjs` | `require()` in Node/legacy bundlers | ~762 KB |
| IIFE (all-inlined, single file) | `dist/page-agent-sdk.iife.js` | `<script src>` CDN direct include, zero config | ~2.0 MB |
| **headless ESM** (no UI layer) | `dist/page-agent-sdk.headless.js` | `page-agent-sdk/headless` — pure core for `ui:false` custom UI | **~446 KB** |
| **legacy ESM** (es2017, all-inlined) | `dist/page-agent-sdk.legacy.js` | `page-agent-sdk/legacy` — **webpack ≤4 / vue-cli 2-3 hosts**: `await import('page-agent-sdk/legacy')` lazy chunk, zero transpile/peers | **~3.0 MB** |

### Import only what you need (subpath exports)

Besides the top-level `import { createChatSdk } from 'page-agent-sdk'`, four subpath entries scope your import to a single capability:

| subpath | key exports | use case |
|---|---|---|
| `page-agent-sdk/storage` | `createSessionStore` / `createMemoryBackend` / `createWebStorageBackend` / `isQuotaError` | persistence layer only, no Agent |
| `page-agent-sdk/query` | `jpEval` / `searchJson` / `runSandboxedScript` + all jsonUtils/schemaUtils pure fns | JSON query / sandbox / path helpers |
| `page-agent-sdk/llm` | `createProxyLlm` + `ProxyLlmMode` / `ProxyLlmOptions` | proxy connection to avoid leaking apiKey |
| `page-agent-sdk/headless` | `createChatSdk` + full core API — **without** ChatDialog/marked/highlight.js/dompurify | `ui:false` custom UI, leanest bundle |

```js
import { createSessionStore, createMemoryBackend } from 'page-agent-sdk/storage'
import { jpEval, searchJson } from 'page-agent-sdk/query'
```

> `storage` / `query` / `llm` resolve to the same dist + types (clear semantics and per-entry CDN fetch); when a multi-entry build lands, your import paths won't change. `headless` is a **separately-built lean bundle** (own dist + types) — see below.

`sideEffects` is set to `["**/*.css"]` only, so bundlers can tree-shake the JS when you import named symbols. Tips to keep your bundle lean:

- **Headless (`ui:false`)**: skip the built-in dialog and render `agent.messages` yourself. For the leanest bundle, import from the **headless subpath** — `import { createChatSdk } from 'page-agent-sdk/headless'` (~446 KB ESM vs ~963 KB main; drops marked/highlight.js/dompurify/ChatDialog you never use at runtime). Same `createChatSdk(options): ChatSdk` signature; pair with `ui:false`. From the main package you can also avoid importing `ChatDialog`/`CodePreview` and drop the CSS (`import 'page-agent-sdk'` without `'page-agent-sdk/style.css'`). **Persistence pitfall**: `sdk.stream` does NOT auto-persist (built-in `useChat` calls `afterRound` via `onPersist`); in a self-built dialog call `sdk.afterRound()` after each turn, otherwise `switchSession` won't restore messages. **Reuse the built-in DebugDrawer** (main package only): `import { DebugDrawer }` — pure-props (`logs=sdk.debugLogs`, `getInfo=()=>sdk.inspect()`, `infoTick=sdk.infoTick`), mount it in your own UI without needing ChatDialog.
- **Disable unused capabilities**: `capabilities:{ dataOps:false, fetch:false, planning:false, skills:false, vfs:false, summarization:false, memory:false, subagent:false }` — removes the corresponding tool schemas and middleware from the agent prompt (saves tokens, not bytes).
- **CDN via esm.sh**: `import { createChatSdk } from 'https://esm.sh/page-agent-sdk'` — peer deps (`zod`, `@langchain/*`) are resolved and deduped by esm.sh automatically; smallest for module scenarios.
- **IIFE only for zero-config**: the all-inlined single file is convenient but heaviest; prefer ESM when the host supports modules.
- **MCP is an optional peer**: `@modelcontextprotocol/sdk` is dynamically imported only when `options.mcp` is passed — omit it to avoid loading the MCP runtime entirely.

## Development

```bash
npm install
npm run dev      # port 3000 (3001 if occupied)
npm run build    # ESM + UMD + IIFE + CSS
npm test
```

## Relationship to Deep Agents

Borrows the harness idea from [Deep Agents](https://github.com/langchain-ai/deepagents) (ReAct + middleware + planning + skills + memory + context management), but implemented in-house: no LangGraph/langchain full bundle; browser-oriented (persistence via IndexedDB, not server-side DB); context via input compression + memory trim + large-result offload, rather than per-step checkpointer archival. See [Context & Compression - Differences from Deep Agents](https://github.com/whyymj/page-agent-sdk/blob/master/doc/context-management.md#七与-deep-agents-的差异).

## Acknowledgments

The built-in design-taste skill (`web-design-engineer`) is vendored from
[ConardLi/garden-skills](https://github.com/ConardLi/garden-skills/tree/main/skills/web-design-engineer)
(skill `web-design-engineer` v1.2.2, © ConardLi), used and redistributed under the upstream MIT License:

```
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
```

Adaptation note: the SDK's built-in copy adapts the main doc in three places for the delegated
sub-agent context (fact verification without network → trust the delegation task / no mid-run user
questions → conservative defaults + stated assumptions / output form → self-contained component);
the 29 reference files are kept verbatim from upstream.

## License

[ISC](https://github.com/whyymj/page-agent-sdk/blob/master/LICENSE)
