# API reference — instance methods, tool/skill definition, data tools, events

## ChatSdk instance (`createChatSdk(...)` return)

| Method / field | Signature | Purpose |
|---|---|---|
| `mount()` | `() => Promise<void>` | Initialize & render. Await before `send` in headless. |
| `unmount()` | `() => void` | Tear down UI, listeners, flush storage. |
| `messages` | `AgentMessage[]` (reactive) | The conversation. Headless reads this to render. UI shares the same array (single source). |
| `send(message)` | `(msg: string) => Promise<string>` | Send a user message (invoke mode, no stream events). Returns final content. |
| `stream` | `(messages, onEvent, signal?) => Promise<string>` | Low-level stream. UI uses this internally; headless can call directly for streaming. |
| `inspect()` | `() => AgentInfo` | Inspect agent: tools/skills/data/middleware/todos/mcp.servers (each tool's `source`: `builtin`/`mcp:<name>`/`user`). DebugDrawer uses this. |
| `switchSession(id?)` | `(id?: string) => Promise<string>` | Switch session context (load or create by id). Requires `storage` enabled. |
| `hook(handler)` | `(h: SdkEventHandler) => () => void` | Runtime event subscription (multi-listener, returns unsubscribe). Complements `onEvent`. |
| `setData(config)` | `(config: DataConfig) => void` | Runtime swap the main data config (`{ schema, bind, description? }`). Tools pick up new bind/schema immediately, no rebuild. Clears snapshots & resets optimistic-lock hash. |
| `getData()` | `() => DataConfig \| undefined` | Read current main data config (reflects runtime `setData`). `undefined` if `dataOps` disabled. |
| `exportData()` | `() => any` | Deep copy of main data `bind` (backup/migration). `null` if dataOps off / no data. |
| `importData(json, opts?)` | `(json, opts?: { validate?: boolean; emit?: boolean }) => { ok: boolean; error?: string }` | Replace `bind` entirely (in-place, preserves reactive ref). Schema-validated by default; `opts.validate:false` skips; `opts.emit:false` suppresses `data_change`. |
| `setSkills(skills)` | `(skills: SkillSpec[]) => void` | Runtime swap the entire skill list (same-name skill overwrites). Takes effect next round: the skill index section of the system prompt re-renders with the new skills; clears the skill full-text cache & in-round loaded set, so the next `load_skill` re-fetches the latest full text (incl. vfs doc). Requires skills enabled (default on). |
| `invalidateSkillCache(name?)` | `(name?: string) => void` | Invalidate the skill full-text cache (proactive invalidation when a dynamic skill's content changes). Omit `name` to clear all; pass `name` to clear one. The next `load_skill` re-runs `getContent`/`readSkillDoc`. Requires skills enabled (default on). |
| `usage` | `TokenUsage` | Cumulative token usage `{prompt_tokens, completion_tokens, total_tokens}` (accumulated per LLM call). |
| `setTools(tools)` | `(tools: StructuredToolInterface[]) => void` | Runtime swap user tools (built-ins untouched; internal `rebindTools` re-binds to LLM; next round uses new set). Zero-breakage: not calling = current behavior. Supports per-permission/business-stage/A-B-test dynamic tool groups without rebuilding agent. |
| `addTool(tool)` | `(tool: StructuredToolInterface) => void` | Append user tool at runtime (dedup by name; built-ins untouched). |
| `removeTool(name)` | `(name: string) => boolean` | Remove user tool at runtime (built-ins untouched). Returns whether removed. |
| `setLlm(llm)` | `(llm: BaseChatModel \| LLMConfig) => void` | Switch LLM at runtime (quota-exhausted→cheaper model / complex task→stronger model / switch provider). Param `BaseChatModel` or `LLMConfig` (constructs `ChatOpenAI` internally). Rebinds tools + re-resolves model caps (`contextWindow`/`maxOutputTokens`). `summaryLlm` unaffected. If new model lacks `bindTools`, tool-calling degrades (agent stays up). |
| `setMemory(source)` | `(source: string \| (() => string \| Promise<string>)) => void` | Update memory at runtime. Supports `string` and sync/async function (async fn evaluated in background, fits RAG doc loading). `setMemory('')` clears. |
| `refreshMemory()` | `() => Promise<string>` | Re-evaluate current memory function source (force refresh after RAG doc update). String source returns current value. |
| `setSubagents(configs)` | `(configs: SubagentConfig[]) => void` | Runtime swap pre-declared subagents (regenerates `use_<id>` delegation tools + triggers rebind). Requires `subagents:[]` at creation (else controller is null, setter warns, no throw). |
| `addSubagent(config)` | `(config: SubagentConfig) => void` | Append pre-declared subagent at runtime (duplicate id warns & skips). Requires `subagents:[]` at creation. |
| `removeSubagent(id)` | `(id: string) => boolean` | Remove pre-declared subagent at runtime (by id). Returns whether removed. Requires `subagents:[]` at creation. |
| `restoreLastCheckpoint()` | `() => boolean` | Restore last good checkpoint (needs `checkpoint` enabled). |
| `listCheckpoints()` | `() => CheckpointMeta[]` | List available checkpoints. |
| `addSkill(skill)` | `(skill: { name, description, prompt \| getContent \| doc }) => void` | Add a user-created skill at runtime. Auto-merges into the skill list, persists via **independent SkillStore** (default indexedDB, separate from `storage` option), takes effect next round. Same-name overwrites. Requires `capabilities.skills` (default on) + `skillStorage` not `false` for persistence. |
| `removeSkill(name)` | `(name: string) => void` | Remove a user-created skill by name (only user-created, not init-time skills). Removes from SkillStore. No-op if not found. |
| `listUserSkills()` | `() => string[]` | List names of user-created skills (not init-time skills). Useful for UI panels. |
| `getUserSkill(name)` | `(name: string) => { name, description, content } \| undefined` | Read a user-created skill's detail (for SkillPanel edit). Returns `undefined` if not found. |

## defineTool (custom tools)

```ts
import { defineTool, z } from 'page-agent-sdk'

const addTool = defineTool({
  name: 'add',
  description: 'Add two numbers',
  schema: z.object({ a: z.number(), b: z.number() }),
  handler: async ({ a, b }) => `sum: ${a + b}`,
})

createChatSdk({ tools: [addTool], /* llm, ... */ }).mount()
```

`handler` receives validated args; return a string (or structured result stringified). Errors via `toolError({ code, message })`.

## defineSkill (progressive disclosure)

```ts
import { defineSkill } from 'page-agent-sdk'

const apiSkill = defineSkill({
  name: 'api-design',
  description: 'REST API design conventions for this project (load when designing/reviewing APIs)',
  getContent: () => 'Use kebab-case URLs; version under /v1; ...',   // or `doc: 'https://...'` for remote
})

createChatSdk({ skills: [apiSkill], /* ... */ }).mount()
```

Skills are loaded on demand by the agent (not always in context) — saves tokens.

## presets (scenario bundles)

```ts
import { createChatSdk, presets } from 'page-agent-sdk'

createChatSdk({ ...presets.pageBuilder, llm, container }).mount()
// or presets.researcher / presets.minimal
```

Spread into options for common scenarios.

## systemPromptHelpers (best-practice prompt snippets)

```ts
import { createChatSdk, systemPromptHelpers } from 'page-agent-sdk'

createChatSdk({
  systemPrompt: `你是 JSON 操作助手。\n${systemPromptHelpers.reliableWriteRules}`,
  llm, container,
}).mount()
```

`reliableWriteRules` — standardized "reliable write rules": read before write (`read`), fields per `read({jsonPath})` (returns format hint), retry on schema-validation errors, prefer `write` with `patch` incremental edits. Recommended for any scenario involving data writes.

By default (`appendReliableWriteRules: true`), the SDK auto-appends `reliableWriteRules` to your custom `systemPrompt` with a `---` separator (clearly distinguishing your content from the SDK-appended write rules); set `appendReliableWriteRules: false` to disable. The default prompt (when `systemPrompt` omitted) already includes them.

## Built-in data tools (auto-injected when `capabilities.dataOps`)

High-level `read`/`write` are the main entry points; query/snapshot/eval tools are always available (no mode switch). Low-level CRUD `get_data`/`set_data`/`edit_data`/`delete_data` was **removed in 4.0** — `read`/`write` cover everything (`get_data({p})`→`read({jsonPath:p})`, `set_data({value})`→`write({value})`, `edit_data({op,p,value})`→`write({patch:{op,jsonPath:p,value}})`, `delete_data({p})`→`write({patch:{jsonPath:p},del:true})`).

| Tool | Purpose |
|---|---|
| **`read`** / **`write`** (recommended) | High-level entry: `read({jsonPath?, jsonPaths?, fields?, depth?, offset?, limit?})` lists/reads (field projection + depth truncation + array paging); `write({value?, patch?, patches?, del?, dryRun?})` merges set/edit/delete + auto optimistic lock + auto snapshot |
| `describe_data` | Show main data description + format hints |
| `schema_data` / `diff_data` | Inspect schema constraints at a path / diff snapshots or JSON |
| `restore_data` / `history_data` | Restore snapshot (no id = most recent) / list & read snapshots |
| `query_data` / `search_data` | JSONPath query / full-text search |
| `eval_script` | Sandboxed script on data (query/transform; transform supports `{patches:[...]}` incremental mode) |
| `draft_write` / `draft_commit` | Opt-in (`capabilities.draftWrite`) chunked build for very large JSON + atomic commit |

**Key rule**: `write` (all four intents) only affects **schema-declared** fields (ZodObject auto-whitelist; undeclared fields hidden/denied). Sub-path reads are recursively projected by the sub-schema at that location (e.g. `read components.0` hides child undeclared fields). `jsonPath` is segment-by-segment validated against schema. Invalid schema → structured error, no write. `write` writes in-place (preserves Vue reactive refs). `write` auto-tracks hash from `read` for optimistic lock (no manual `expectedHash` needed). Whole-set / `eval` transform become **merge** semantics in whitelist mode (only updates declared fields, undeclared fields preserved — prevents accidental deletion); `interceptors.write`-supplied invisible fields (not in schema) are written back to bind after schema+merge (not stripped).

### write / jsonPath edit operations

`write({ value, patch: { op, jsonPath } })` (or `write({ patch: { jsonPath }, del: true })` to delete; or `write({ patches: [...] })` for batch atomic):
- `set` — set a sub-path (or whole value when no `patch`)
- `remove` — remove a sub-path / array element
- `merge` — shallow-merge an object
- `append` — append to an array

Example: `write({ value: 9.9, patch: { op: 'set', jsonPath: 'items.0.price' } })` — precise local edit, no full re-send. `value` is a JSON object (recommended) or JSON string. `patches: [{op:'set', jsonPath:'a', value:1}, {op:'append', jsonPath:'items', value:newItem}]` — batch atomic (any failure → whole batch rolled back).

## Built-in fetch tools (`capabilities.fetch`)

`fetch_document` — GET a URL, return cleaned text (HTML→markdown, truncated, offloaded to vfs if large).

## Built-in vfs tools (`capabilities.vfs`, default on)

The virtual file workspace holds tool-result offloads (>6000 chars), agent/integrator drafts, and user files. 2.16.0+ partitions it into three independent LRU pools (`large_results/*` / `drafts/*` / `userFiles`), so offloaded results can't evict drafts or user files.

| Tool | Signature | Purpose |
|---|---|---|
| `vfs_read` | `({ path }) => string` | Read a file (path may be returned by a prior offload). |
| `vfs_write` | `({ path, content, jsonString? }) => meta` | Write/overwrite a file. `jsonString:true` (2.16.0+) validates `content` is valid JSON before writing (invalid → `VFS_JSON_INVALID`, not written). |
| `vfs_grep` | `({ pattern, paths? }) => matches` | Grep across workspace files. |
| `vfs_json_read` (2.16.0+) | `({ path, jsonPath? }) => json\|string` | Read a JSON subtree from a vfs file via jsonPath (omit for the whole file). Returns `VFS_JSON_INVALID` if the file isn't valid JSON; `VFS_PATH_NOT_FOUND` if jsonPath doesn't exist. |
| `vfs_json_patch` (2.16.0+) | `({ path, patches: [{op:'set'\|'remove'\|'merge'\|'append', jsonPath, value?}] }) => meta` | Atomic jsonPath patch inside a vfs file. Applied on a clone; any patch failure → `PATCH_FAILED`, original file unchanged. Avoids re-sending large JSON (delta only). |

## Context compression presets (`contextPreset`)

`contextPreset` is a one-line knob over the `summarization` middleware (ratio-based); `contextOptions` overrides individual fields.

| Preset | When | Profile |
|---|---|---|
| `auto` (default) | General chat | window 0.4 / threshold 0.5 / recall Top-3 / LLM summary |
| `conservative` | Big models / cost | window 0.5 / threshold 0.7 / recall Top-2 / index summary (no LLM) |
| `aggressive` | Small models / save context | window 0.3 / threshold 0.3 / recall Top-5 |
| `complex` (2.16.0+) | Multi-step complex tasks / large JSON / long workflows | windowRatio 0.6 / summaryThresholdRatio 0.7 / recall Top-5 / LLM summary; `preserveLastToolResults` defaults to `['describe_data','read','query_data','search_data']` |

```ts
createChatSdk({ contextPreset: 'complex', contextOptions: { recallTopK: 8 } })
```

`inspect().contextPreset` (2.16.0+) exposes the effective preset.

## SdkEvent types (for `onEvent` / `sdk.hook`)

| `type` | Payload | When |
|---|---|---|
| `round_start` | `round` | Each agent round begins |
| `reasoning` | `delta` | Reasoning token (models that emit it) |
| `text` | `delta` | Streamed text delta (stream mode only) |
| `tool_call` | `name, args` | A tool is invoked |
| `tool_result` | `name, result, status` | Tool returns (`status`: `done`/`error`) |
| `subagent` | `taskId, label, kind, name, args?, result?, status?` | Subagent tool progress (forwarded to UI, NOT into main LLM context) |
| `done` | `content` | Agent round completes |
| `usage` | `round, usage, cumulative` | After each LLM call (if provider returns usage); `usage`/`cumulative` are `{prompt_tokens, completion_tokens, total_tokens}` |
| `session_restored` | `sessionId, rounds` | After storage restores a session snapshot (mount auto-resume / `switchSession` to existing session) |
| `data_change` | `operation, value?` | Main data was written via `write` (infers `set`/`edit`/`delete` from args) or low-level `set`/`edit`/`delete`/`restore_data` |
| `message_update` | `count` | The `messages` array changed |
| `error` | `message` | An error occurred (abort excluded) |

`approval_request` is **NOT** forwarded via `onEvent`/`hook` (UI handles it; headless integrators use a custom approval middleware listener).

## `data` config (single main object — schema + bind + auto field-hints)

`data` is the single entry for main-data config — combining schema declaration + object direct-bind + auto field-hint injection:

```ts
import { reactive } from 'vue'  // or any reactivity impl; plain object also works
const PageSchema = z.object({
  title: z.string().describe('页面标题'),
  count: z.number().describe('计数器'),
})
const page = reactive({ title: '首页', count: 0 })  // reactive recommended for UI auto-refresh

createChatSdk({
  data: {
    schema: PageSchema,   // write validation + field .describe() auto-injected into systemPrompt「可操作数据」section + ZodObject top-level keys auto-whitelist
    bind: page,           // reactive/plain object; tools read/write directly (no window)
    description: '页面配置',  // optional; auto-generated if omitted
  },
})
// LLM write page → page reactively updates; integrator changes page → LLM read sees it
```

- **`bind` is required** (any object): reactive → auto-refresh on write (recommended for UI); plain object → write works but no auto-refresh (suitable for headless / backend; integrator uses `onEvent`/`hook` `data_change` to be notified). Tools mutate in-place (`restoreInPlace`), compatible with reactive proxies; plain objects also write fine.
- **Notifying the outside world of changes**: subscribe `data_change` via `onEvent` (constructor) or `sdk.hook` (runtime, multi-listener, cancellable) — fires after `write`/`set`/`edit`/`delete`/`restore`, with `operation`/`value`. For Vue + reactive bind, template/watch auto-react (no manual notify needed); `onEvent` can coexist for audit/analytics.
- **Protected resources (precise-value protection)**: declare `data.resources: [{ path, mode }]` to protect fields needing exact preservation (ids / hashes / tokens / long verbatim / critical config).
  - `mode: 'freeze'` = read-only (exact value hidden from LLM via `⟦frozen:path⟧` placeholder; any write rejected with `FROZEN_FIELD`; need value via `resource_get`).
  - `mode: 'verbatim'` = exact string preserved (returns `⟦res:handle⟧` placeholder, original in resource pool; modify via `resource_update({path,value})` first, then write back handle; direct new value → `VERBATIM_MISMATCH`).
  - opt-in: only when `data.resources` non-empty + vfs enabled (`capabilities.vfs`, default on) → exposes `resource_get`/`resource_update`/`resource_list`/`resource_delete` tools (advanced mode) + injects cross-compression pin. Unconfigured → zero behavior change.
  ```js
  data: { schema, bind, resources: [{ path: 'id', mode: 'freeze' }, { path: 'token', mode: 'verbatim' }] }
  ```
  SDK API: `sdk.createResource/getResource/updateResource/deleteResource/listResources/releaseResources`.
- **Runtime swap**: `sdk.setData({ schema, bind, description? })` replaces the whole config; tools pick up immediately (no rebuild). Snapshots & lock hash reset.
- **Runtime skill swap**: `sdk.setSkills(skills)` replaces the entire skill list (same-name overwrites); the skill index section of the system prompt re-renders next round, and the skill full-text cache is cleared so the next `load_skill` re-fetches the latest content (incl. vfs doc). Use `sdk.invalidateSkillCache(name?)` to proactively invalidate the cache when a dynamic skill's content changes (without swapping the whole list).
- **Runtime dynamic reconfiguration (zero-breakage; not calling = current behavior)**: beyond data/skills, you can also dynamically reconfigure tools / LLM / memory / subagents at runtime without rebuilding the agent:
  - `sdk.setTools(tools)` / `addTool(tool)` / `removeTool(name)` — swap/append/remove user tools (built-ins untouched; internal `rebindTools` re-binds to LLM; next round uses new set). Use cases: per-permission tool groups, business-stage gating, A/B experiments.
  - `sdk.setLlm(llm)` — switch LLM at runtime (quota-exhausted→cheaper model / complex task→stronger model / switch provider). Param `BaseChatModel` or `LLMConfig`. Rebinds tools + re-resolves model caps. `summaryLlm` unaffected.
  - `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-evaluates the current function source (force refresh after RAG doc update).
  - `sdk.setSubagents(configs)` / `addSubagent(config)` / `removeSubagent(id)` — swap/append/remove pre-declared subagents (regenerates `use_<id>` delegation tools + triggers rebind). Requires `subagents:[]` at creation.
  - All setters trigger `infoTick++` → DebugDrawer refreshes; `inspect()` reflects the latest tools/model/memory/subagent.subagents.

## Exported building blocks (for custom UIs)

- `ChatDialog`, `MessageContent`, `CodePreview` — Vue components
- `useChat(opts)` — composable (streaming/retry/stop/regenerate logic)
- `createAgent(options)` — the raw harness (if you bypass `createChatSdk`)
- Middleware factories: `createApprovalMiddleware`, `createVerifyMiddleware`, `createWriteBackCheck`, `createSubagentMiddleware`, `createCheckpointMiddleware`, `createUsageHintsMiddleware`
- Storage: `createSessionStore`, `createMemoryBackend`, `createWebStorageBackend`, `isQuotaError`
- JSON helpers: `jpEval`, `searchJson`, `runSandboxedScript`, `toolError`, `zodError`

## Proxy connection (`createProxyLlm`) — prevent apiKey leakage

Browser-direct LLM calls expose `apiKey` in DevTools. Use `createProxyLlm` to unify dev/prod access:

```ts
import { createChatSdk, createProxyLlm } from 'page-agent-sdk'

// Production: proxy mode (server injects real key)
createChatSdk({
  llm: createProxyLlm({
    mode: 'proxy',
    baseUrl: '/api/llm',        // your proxy (same-origin avoids CORS)
    userToken: getUserToken(),   // session token (server validates)
    model: 'deepseek-v4-flash',
    refreshToken?: async () => ...,  // optional: refresh on 401
    headers?: { 'X-Tenant': 'acme' }, // optional: extra headers
  }),
  ...
})

// Dev: direct mode (browser holds real key; dev only)
createChatSdk({
  llm: createProxyLlm({
    mode: 'direct',
    apiKey: 'sk-xxx',
    baseUrl: 'https://api.deepseek.com/v1',
    model: 'deepseek-v4-flash',
  }),
  ...
})
```

| | `proxy` (prod) | `direct` (dev) |
|---|---|---|
| apiKey | server (invisible) | browser (DevTools visible) |
| browser holds | userToken | real apiKey |
| token refresh | yes (401 retry) | n/a |
| headers | supported | n/a |

Server-side proxy essentials: validate userToken → inject real apiKey → forward to LLM API; handle CORS; stream SSE through; pass tool-calling fields. See `doc/usage-guide*.md` §8.6 for full guide.

