# @tansr/sdk

Tansr 对外 TypeScript SDK:在自己的程序里以 **headless** 方式驱动 Tansr 智能体——
prompt 进、事件流出,零 UI 依赖。查询环、工具调度、权限引擎、上下文压缩与模型接入
全部来自 tansr 内核(已编译内联进发布产物),你拿到的是与 tansr CLI 同一套 harness,
以库的形态嵌进你的应用。

```bash
npm install @tansr/sdk
```

**运行时要求**:Node ≥ 22.19(Electron ≥ 39,SDK 跑主进程);ESM only;TypeScript
target ≥ ES2022。发布产物 = 单文件 ESM bundle + 单文件 d.ts(未压缩可读);第三方
运行时依赖仅 `undici` / `zod` / `zod-to-json-schema`(+可选 `@vscode/ripgrep` 检索
加速,装不上自动降级)。

## 一分钟上手(平台令牌档,终端分发形态)

```ts
import { createSession } from '@tansr/sdk';

// 令牌从你的服务端换发(appkey 恒在服务端;终端只持短期令牌)
const session = await createSession({ token, baseUrl: 'https://api.example.com' });
session.send('帮我总结这份合同');
for await (const event of session.events) {
  if (event.type === 'msg.text.delta') process.stdout.write(event.text);
}
```

模型目录与授权能力由平台 App 配置下发。系统媒体工具须在 `tools.builtin` 中显式选择；仅开启平台授权不会让缺省 SDK 会话自动装入收费媒体工具。

## 应用系统提示词（AP-SP）

平台管理 API 的 `systemPrompt` 是应用业务角色，`systemPromptPolicy` 控制与 SDK `system` 的关系：

| 平台策略 | 未传 system | system 有内容 | system: [] |
| --- | --- | --- | --- |
| fallback（默认） | 平台 P | SDK S | 无业务段 |
| prepend（保留） | 平台 P | P → S | 平台 P |

平台正文未配置时，仅使用 SDK 段。`systemAppend` 在业务段选定后追加宿主使用指南，不触发覆盖；工具、Skills、MCP 指南仍按已装配能力追加。不要把平台正文再复制进 SDK 参数。`session.applicationPrompt` 提供实际装配的只读 `source`／`policy`，不包含正文。

`PUT /v1/apps/{appId}/config` 写入 `{ systemPrompt, systemPromptPolicy }`，`GET /v1/apps/{appId}` 的 `config` 回读；组织管理另传 `x-tansr-org`。正文最多 16000 Unicode 码点，null／空白清空；策略不接受 null，字段缺席均保留。

P 和 S 同为模型 system 输入，拼接次序不保证冲突指令谁胜出。平台适合公共角色集中维护，SDK 适合动态或版本化场景；有矛盾应修改正文或选择 fallback。权限／裁决／额度不靠提示词强制，正文不能存密钥。

已有令牌档会话在下一新轮前自动核验平台 P／策略，不需重建。每轮 ETag 条件请求绕过装配缓存，工具多步保持同轮快照；先应用新段再计算上下文预算。失败发 application_prompt_refresh_failed 并中止本轮，保留历史、可原会话重试；预检30秒上限且可取消。宿主 S/A、模型和工具权限仍沿原装配生命周期。BYOK／注入 client 不读平台默认，runAgent 需宿主显式调用 resolveApplicationSystem。

完整三档 SDK、Windows Electron、serve、Android／iOS 示例见[提示词指南](https://docs.tansr.com/sdk/system-prompts/)与仓内 doc/90 §4.12。此能力仍须发布包含 AP-SP 的 API 与 SDK／serve，旧版本不识别保留策略。

## 模型、上下文和图文输入

开发中的新接口须使用包含它们的 SDK 构建；旧 npm 包没有这些接口时，宿主应提示升级。
`session.contextState()` 和 `session.subscribeContext(listener)` 提供可序列化快照：
`selected` 是下一轮模型，`active` 是运行轮冻结的模型；`lastObserved` 仅在收到模型用量证据后存在。
`currentModel({ active: true })` 查询执行轮模型，空闲时返回当前选择。fallback 只报告已观测到的提供方变化。
`budget` 包含物理与有效窗口、输入估算、输出/思考预留、剩余量、压缩阈值及设置来源。
未知值为 `null`；本地输入估算与平台累计计费用量分别展示。

大窗口降到小窗口时，在空闲状态 `await session.switchModel(next)`。
SDK 保留迁移前原文备份，必要时用源模型分段摘要，核验目标预算并落盘后再提交切换；
图片、工具配对及其前后顺序保留，无法容纳即拒绝。失败或取消不静默截断历史。
`modelTransitionBackup()` 可取回最近一次迁移备份；需要跨进程恢复时配置 checkpoint/store。
外部压缩管理器需要通过 `switchModel(next, { compaction })` 显式提供与目标匹配的配置。
摘要调用可能产生用量；备份不代表摘要语义必然完整，业务关键事实仍应核对。

`sendBlocks([{ t: 'image', mime: 'image/png', data: base64 }, { t: 'text', text: '描述图片' }])`
提交同一轮图文，返回 `started` 或带原因的 `rejected`。忙时由宿主保留整份草稿，不能自动去图重发。
图片需满足格式魔数、单图 3.75 MiB、最多 20 张、总图片 30 MiB 和上下文预算限制；
模型与传输通道都需支持图片。Read 产生的像素随工具结果发送到支持视觉的模型，元数据读取成功不等于模型已看见图片。

`planSpeechInput(text, { maxCharacters, segment })` 使用授权模型 `maxChars` 规划朗读；
汉字按 2、其他 Unicode 码点按 1 估算。超出单次限制须显式允许分段，总上限 32000、最多 32 段。
宿主负责逐段执行、取消和保留产物，已成功或结果不明的收费请求不自动重放。
应用提示词刷新失败提供 `reason`、`stage`、`retry`，继续中止该轮；由宿主换发开发者凭据并决定重试。

The new interfaces above require a build that includes them. `contextState()` separates the next selected
model, the active turn's frozen model, observed usage, and local context estimates. Unknown values are
`null`. Await `switchModel()` while idle to prepare and validate a smaller context before committing;
failure keeps the original history. `sendBlocks()` submits image and text together and rejects busy
submissions without removing images. Speech planning requires the authorized model's character limit
and explicit segmentation; completed or uncertain paid requests must not be replayed automatically.

## 三档 API × 三档模型来源

| API | 形态 | 适用 |
|---|---|---|
| `query(options)` | `AsyncGenerator<KernelEvent, QueryResult>` | 单轮一问一答,`return` 值携终值(finalText/usage/历史) |
| `createSession(options)` | `Promise<AgentSession>` | 多轮会话:`send` / `events` / `interrupt` / `messages` / `setModel` / `close` |
| `runAgent(options)` | `QueryHandle` | 低阶直通:自带 client/executor/tools,零装配 |

| 模型来源 | 传什么 | 适用 |
|---|---|---|
| **令牌档** | `{ token, baseUrl }` | 终端分发(Electron/桌面);能力位由平台治理 |
| **托管档(BYOK)** | `model: '别名'` + 本地 `.tansr/settings.json` | 自己的服务器/脚本,自带模型 key |
| **注入档** | `{ client, model }` 对象成对 | 测试(scripted client)/自定义接入 |

## 能力面一览

- **系统工具与业务工具统一入口**:`tools: { builtin: [...], custom: [...] }`。
  系统工具包括文件、命令、网络及多媒体；`imageGen`、`videoGen`、`speechToText`、
  `textToSpeech` 与其他系统工具一起从 `tools.builtin` 选择，执行骨架统一在内核。
  SDK 媒体后端由平台提供，受应用授权、模型范围和计量约束。`defineTool` 注册业务函数，
  接受 JSON 参数表或 zod schema。旧 `tools.platform` 仅为四媒体的弃用兼容别名，
  会并入同一内置工具集合并去重，不代表另一层工具;
- **联网搜索双位双门**:webSearch 工具本体是内置 `WebSearch`(写在 `tools.builtin`,
  旧写法 `tools.platform` 报迁移错),后端恒平台通道(按次计费归 App,SDK 恒无 BYO
  径)——装配需双位齐开(`tools.webSearch` 工具位 + `platform.webSearch` 通道位)且
  令牌档在场;缺席选择时门不备静默不装,显式选择即 fail-fast 分因可读错(详见手册
  §5.3);
- **skills**:`defineSkill` 内联 + `dirs` 目录装载(`<name>/SKILL.md`),按需装载零
  发现(恒不扫用户目录);
- **MCP 外接**:`createMcpHost({ servers })` 应用级共享 / `mcp: { servers }` 会话级,
  配置与业界 `mcpServers` 同形;
- **渲染管道**:`createSessionView`(事件流 → 不可变视图快照,structuredClone-safe,
  Electron IPC 直传)+ `createNarrator`(人类可读日志行);恒不用手拼事件;呈现档
  开发者可选:`{ delivery: { text: 'stream'|'final', thinking: 'stream'|'final'|'off' } }`
  ——文本/思考各自选流式或整段一次性,思考可整体关显(状态派生与工具卡恒不受影响,
  缺省全流式零漂移);且可**动态切换**:`view.setDelivery(delivery)` 块粒度即时生效,
  切 off 自动追溯剔除既有思考;生成面思考(产不产/预算)走
  `createSession({ thinking: { budget } })` + `session.setThinking(…)`(下一轮生效),
  与呈现档正交(详见手册 §9.1);
- **权限**:`permission: { mode?, rules?, askUser? }`——ask 裁决桥到你的 UI,缺席
  fail-closed 降级 deny;令牌档缺省装配控制台任命的**裁决人**(`adjudication?: { posture?, callBudget?,
  enabled?, endUser? }`,模式由 kernel 按姿态 × 资格档推导):显式 `permission.mode` 即视为自管模式——
  裁决人不装并通报 `adjudicator_skipped_by_mode`(0.12 兼容径);**例外**:显式 `adjudication: { … }`
  与 `permission.mode` 同现即 throw `invalid_options`(真矛盾,fail-fast),静默自管请传
  `adjudication: { enabled: false }`;变更登记见 `CHANGELOG.md`;
- **计量**:`cost.usage.updated` 逐请求;终端自查 `/v1/my-usage` 恒无金额字段,
  开发者对账 `/v1/app-usage/by-end-user` 含金额(恒不代理给终端)。

工具与扩展授权以及平台服务授权均在控制台按 App 配置；字段分别为 `capabilities.tools`
与 `capabilities.platform`，键集合由 `CAPABILITY_TOOL_KEYS` / `CAPABILITY_PLATFORM_KEYS`
提供。授权合同为 17 工具位 + 11 平台位。原五位为 `imageGen`、`videoGen`、`webSearch`、
`speechToText`、`textToSpeech`，控制平台搜索及四媒体服务的使用权。新增六位 `embedding`、
`rerank`、`document`、`music`、`voiceCreate`、`realtimeAudio` 仅授权独立非聊天 API，
通过 `nonchat-operations-v1` 特性协商；缺省不授权（等效 false），不会自动注册聊天工具。
平台授权不是工具归属分类。位关而显式选择得到
`capability_disabled`，不静默缺工具。

### 临时文档与显式非聊天操作

`createPlatformNonchatClient({ baseUrl, token })` 提供独立 HTTP 消费。`uploadDocument({ fileName, mime, dataBase64 }, options?)` 暂存 1–20 MiB 的 PDF、PNG、JPEG、BMP 或 TIFF，返回 `{ uploadRef, expiresAt }`；文件名、MIME、扩展名、文件头和规范 Base64 均受校验。该步骤不调用收费模型、不要求持久幂等键、不自动重传，令牌明确失效时仅按原规则刷新一次。

临时文档按 owner/app/endUser 隔离，在当前 API 实例内存最多保留十分钟；不提供持久性或跨实例保证。宿主另行显式 `document({ operation: 'extract', model, document: { uploadRef }, fields }, { idempotencyKey })` 提交收费任务，仍须模型授权与费用验证，缺页数费用证据继续禁止外呼。原任务可能仍在使用文件时不要删除；无需使用后可 `deleteDocumentUpload(uploadRef, options?)` 幂等删除。示例及未知结果处理见 [非聊天操作](../../examples/nonchat-operations/README.md)。

### 系统媒体工具示例 / System media tools

```ts
const session = await createSession({
  token,
  baseUrl: 'https://api.example.com',
  tools: { builtin: ['imageGen', 'videoGen', 'speechToText', 'textToSpeech'] },
});
```

先在应用配置中授权所需媒体服务与模型。示例只选择四个媒体工具；需要文件、网络等工具时，
也应显式加入同一个 `builtin` 列表。权限确认、预算与平台计费继续适用。用户主动录音转写或朗读
可以调用 `session.platform.transcribe()` / `.speak()`；这些是直连操作，不会伪造模型工具事件或自动写入聊天历史。

All four media tools are built-in system tools selected through `tools.builtin`. The platform supplies
their backend and enforces app permissions, model access, quotas, and billing. The retained
`capabilities.platform` fields describe service authorization, not a separate tool category.
Media tools require explicit selection; the default SDK tool set does not enable them automatically.
The deprecated `tools.platform` selection remains an equivalent alias. User-triggered transcription
and speech through `session.platform` are direct operations, separate from model tool calls.

### 子代理与远端 serve / Subagents and remote serve

Electron／Node 进程内集成继续使用本包原会话与 Task 装配、依赖及权限回调。SDK2 远端档的子代理则由 serve/kernel 运行，终端继续消费原会话事件和执行合同，不另建子模型循环。前台 Task 受应用 `tools.agent` 与可信策略收窄；常驻及后台由开发者服务端的 `execution.subagentsFor` 显式启用。

完整宿主配置见 [serve 子代理示例](../server/README.md#子代理与可信宿主配置--subagents-and-trusted-host-configuration)。后台须闭合 `outputFile`、`writeFileImpl`、`onNotification`：报告地址对应真实受控存储，通知由宿主负责同域交付及所承诺的持久化。子树共享父预算和审批边界；旧子代理不会随父会话换设备而自动迁移。本节为配套开发候选，不表示当前 npm 版本已发布这些远端配置。

English: Electron and Node retain the integrated SDK and its existing dependencies, session API and permissions. Remote SDK2 subagents run in serve/kernel; clients keep the existing session and execution protocol. Foreground Task requires app and policy grants. Trusted server code opts into resident/background agents and supplies real scoped report storage plus notification delivery. Notifications are not automatically durable or injected into the parent. See the linked serve example and use matching candidate builds.

## 文档与示例

### 可等待生命周期（ARC-07，当前源码能力，待配套发布）

`idle()`、`close()`、`session.ended` 维持原有业务语义；它们不证明异步存储、生成器的 `finally` 或 MCP 传输已经完成。离开页面或退出应用时，检查新的回执：

```ts
const result = await session.closeAsync({ timeoutMs: 30_000, flushStore: true });
if (result.status !== 'completed') {
  // 保留 session；显示状态/数量即可，不把原始 cause 直接写日志或发给 renderer。
  console.warn({ status: result.status, pending: result.pending, failureCount: result.failureCount });
  // 稍后继续观察：await session.drain({ timeoutMs: 30_000 });
}
```

`drain()` 不关闭会话；`closeAsync()` 先逻辑关闭再等待。超时或 `signal` 取消的是本次观察，底层清理继续，其他等待者不受影响。默认观察上限 30 秒，`0` 读取当前状态，`Infinity` 只用于宿主明确管理的真实依赖链。`flushStore` 缺省关闭；借用的 store 永不由 SDK 自动 `close()`。`retryPersistence: true` 显式重试待修复历史提交，不重放业务回调；不要在自己的 `onHistoryCommit` 内等待本会话的 `drain()`。

会话内 `mcp: { servers }` 由 SDK 所有；传入应用级 `McpHost` 是借用。共享 host 必须等全部使用它的会话完成后，再由应用 `await host.dispose()`。`cleanupEvidence` 中 `transport-completion` 指本地传输完成层，第三方连接没有 `settleClose` 时只提供 `connector-close-promise`，不是任意操作系统子进程或 HTTP 远端退出证明。

`query()` 提前 `break` 仍发送中止，并等待真实收尾到 `cleanupTimeoutMs`（默认 30 秒）。用 `onLifecycleError(error)` 保留 `SdkLifecycleError.cleanup`，随后可继续 `error.cleanup?.drain()`；消费者自己抛错时，JavaScript 可能优先保留消费者异常，因此第二条回调通道不可省略。托管／令牌档 `query` 现可显式传 `skills`、`mcp`，能力位及所有权规则与 `createSession` 相同；注入档仍不隐式装配工具或读取技能目录。

完整规则见 `doc/90-SDK技术手册.md` §4.13。Electron 真正使用 `closeAsync`，外层 30 秒观察超时可再次等待；Android／iOS 继续现有 serve 会话协议，手机断线不等于删除会话，真实资源收尾由 Node 宿主负责。

- **技术手册(完整)**:仓内 `doc/90-SDK技术手册.md`——安装、三方闭环令牌接入、三档
  API、能力位、defineTool/skills/MCP、渲染管道、权限、计量、imageGen、错误码排障、
  安全须知;
- **官方示例**:`examples/token-server`(开发者服务端令牌换发样板)、
  `examples/electron-chat`(Electron 全形态样板:权限弹框桥 + SessionView 投影 +
  imageGen 产图渲染);
- **平台契约**:仓内 `doc/89-SDK平台契约v1.md`(wire 形态事实源)。

## 安全一句话

appkey 恒不出你的服务端;终端只持短期令牌(TTL + 平台定向吊销 = 止损面);Electron
里 SDK 与令牌恒在主进程,renderer 只见投影状态。完整安全须知见手册第十四章。

## License

MIT(发布产物内联了 tansr 内部 workspace 包的编译产物,同以 MIT 随包分发;见
dist 文件头 NOTICE)。


## MEDIA-03 历史媒体恢复

媒体产物随工具回执保存，恢复后的 result 可复用现有媒体组件；artifactUnavailable 单独表示预览不可恢复，不代表工具失败。首次写产物会启用存储版本 2，请统一升级写者并先备份。配置、TS 示例及英文说明见[媒体历史恢复 / Media history recovery](../../examples/serve-demo/MEDIA-HISTORY.md)。本能力已随 @tansr/sdk@0.15.0 发布，须同时升级消费端。

## SDK2 归档宿主：`@tansr/sdk/extensions`（开发中，尚未发布）

新入口显式启用归档交接与恢复；SDK1 根入口、原会话循环和默认持久化行为保持不变。以下示例对应当前源码，不能据此假定 npm 已发布版本包含该入口。

当前开发候选还在原 FileSessionStore 的历史提交中保存核心附件准入状态，同一可信域恢复可延续引用及累计预算；换史、cwd或授权变化清引用而保累计。旧自定义Store保持原五方法；可选 `attachmentCheckpoints: true` 仅供能将旁带与历史同次保存并原样读回的可信实现。公开旁带为 `unknown`，只保管回送，不解析、构造或改写；实际修复历史才设置可选 `attachmentReferencesInvalidated: true`。未知窗口可恢复历史，超帽时可舍引用索引；不承诺跨设备或供应商缓存收益。详见[会话存储与冷恢复](../../doc/90-SDK技术手册.md#46-会话管理sessionstore-与-resumedoc91-实施批2026-08-31)。

English: The candidate's existing FileSessionStore persists trusted attachment admission state with history. Same-scope recovery keeps references and cumulative accounting; rewritten history or changed scope invalidates references, preserving the budget. Custom Stores remain compatible. Opt into `attachmentCheckpoints: true` only with atomic history/checkpoint persistence; client metadata is not trusted. Preserve the opaque `unknown` value without parsing, constructing or modifying it; set the optional `attachmentReferencesInvalidated: true` only after actually repairing history. This does not establish cross-device or provider-cache savings.

```ts
import { createFileSessionStore } from '@tansr/sdk';
import {
  createNodeArchiveHost,
  type NodeArchiveHostOptions,
  type NodeArchiveFreshSessionOptions,
  type NodeArchiveSubject,
} from '@tansr/sdk/extensions';

async function openArchivedSession(
  config: Omit<NodeArchiveHostOptions, 'store'>,
  sessionOptions: NodeArchiveFreshSessionOptions,
  subject: NodeArchiveSubject,
) {
  const store = createFileSessionStore({ dir: './sessions' });
  const host = createNodeArchiveHost({
    ...config,
    store,
    archive: { ...config.archive, contextMaterials: true },
  });
  const session = await host.open({ mode: 'fresh', subject, session: sessionOptions });
  // 返回原 AgentSession，继续使用原 send、事件和工具接口。
  return { host, session };
}
```

`config` 由应用按 `NodeArchiveHostOptions` 配置：稳定应用/用户身份、归档目录、有限额度及同步 `authorize`。额度结构为 `ArchiveHostQuota`，协议上限为 `ArchiveHostLimits`；授权必须检查当前权限，拒绝时抛错。创建宿主本身不创建数据库，`open` 才执行安装或恢复。Store 必须来自**同一次安装的同包根入口**，使用未配置冷层的原文件 Store；不要复制、包装或混用另一份 SDK 的 Store。归档目录独立于原会话目录，Node 宿主在本地管理捕获与来源两座库。

拿到会话后调用 `session.send(...)`。需要等待归档交接时调用 `host.flushArchive(subject)`；`pending: true` 表示还有后续有界批次，不能当作已完成 ACK。恢复使用 `open({ mode: 'resume', subject, session })` 和 `NodeArchiveResumeSessionOptions`，保持原目录、身份及配置；恢复选项不再注入 `initialMessages`。完成原未决工作还须显式 `finishPending: true`，并提供独立的 `authorizeCompletion`（参数类型 `ArchiveCompletionAuthorization`）。

示例中的 `archive.contextMaterials: true` 是显式选择：它协商 `context-materials-v1`，并沿原额度安装有界材料队列（队列上限8）。省略或设为 `false` 时只做归档交接，不额外建立材料队列；恢复须沿原安装配置，不能借重开扩大授权。返回的仍是完整 `AgentSession`，Electron 的主进程、SDK 依赖、原 Store、审批回调与工具调用均保持原方式。

启用材料后，宿主使用 `readArchiveBinding(subject)` 读取当前绑定及操作代际，`readArchiveRecords(subject, { afterSequence, limit, maxBytes })` 分页读取已保存记录，`readArchiveArtifact(subject, ref)` 读取原记录登记的字节。记录页最多128条、1 MiB元数据，单对象仍受原32 MiB帽；这些接口复核当前读取授权，不能用任意路径或URL代替引用。`requestMaterials` 只提出点名记录的请求；上传及 `respondMaterials` 得到 `received` 不等于模型已消费。宿主再以原 `materialRequestId` 和 `leaseId` 调用 `enqueueMaterials`，由下一查询屏障交给同一个 ContextManager 检查代际、记录关系和预算；结果以 `materialStatus` 为准。冷重开不自动把全部档案或被摘要覆盖的原文塞回模型。

需要向应用状态栏或诊断页展示当前存储口径时，可调用 `readArchiveStorageStrategy(subject)`。当前返回 `single-authorized-source`、`source-ack-with-durable-spool`，并明确 `replication: 'none'` 与 `synchronization: 'none'`；这只是已接线来源的事实视图，不是跨设备同步、备份或供应商耐久证明。只有后续显式接入相应策略后，才能对外提供那些能力。

外置附件另由顶层 `trustedAttachmentSourceFor(subject)` 显式选择 `ArchiveAttachmentSource`，缺省不开。来源的 `describe(request)` 必须从可信登记表返回读取前已经固定的 `{ mediaType, bytes, sha256 }`；异步 `read(request, signal)` 仅供给这个对象，并遵守 `maxBytes`、取消与当前权限。`reference` 只是登记表查找键，SDK 不自动下载其中的URL或读取路径。正文和经核验的附件沿原归档提交；缺失、撤权、换体或超帽不能伪装成完整交接。当前外置来源范围是原媒体合同中的图片工件，不代表已支持任意文件、音视频导入或图片自动进入模型上下文。真实摘要发布、冷材料回供及组合入模行为仍以配套候选的实际验收为准，此使用说明不代验收或发布回执。

English: The example explicitly sets `archive.contextMaterials: true` to negotiate `context-materials-v1` and install a bounded material queue, capped at eight entries. Omitted or false keeps archive transfer only. Reopen with the original installation settings. The host still returns the full AgentSession; Electron keeps its integrated SDK, dependencies, Store, approval callbacks and tool APIs.

Use `readArchiveBinding`, bounded `readArchiveRecords` pages (at most 128 records / 1 MiB of metadata), and `readArchiveArtifact` (the existing 32 MiB object cap) under current read authorization. Request named records through `requestMaterials`; upload and `respondMaterials` establish receipt only. `enqueueMaterials` hands the original request and lease to the existing ContextManager at the next query boundary, where generations, record relationships and budgets still apply. Observe `materialStatus`; reopening does not inject every archived record or revive source text covered by a summary.

当前轮必须取得该材料时，显式使用 `enqueueMaterials(subject, { materialRequestId, leaseId, required: true })`。供给未就绪时 Node 抛 `material_required`，Serve 以 503 拒绝受理本轮；保留原会话与输入草稿，恢复原 source 后重试。省略 `required` 仍是原可选材料行为，最终是否采用由核心预算及准入决定。

For material required by this turn, use `enqueueMaterials(subject, { materialRequestId, leaseId, required: true })`. An unavailable supply causes Node to throw `material_required` or Serve to reject admission with 503. Keep the same session and input draft, restore the original source and retry. Omitting `required` preserves optional material behavior; the core retains final budget and admission control.

Use `readArchiveStorageStrategy(subject)` when a UI or diagnostic view needs the current storage posture. It reports the installed `single-authorized-source` and `source-ack-with-durable-spool` contract, with `replication: 'none'` and `synchronization: 'none'`. This is a fact view of the connected source, not a promise of cross-device sync, backup, or provider durability; those claims require a separately installed strategy and evidence.

`trustedAttachmentSourceFor(subject)` separately opts into a trusted attachment source. Its synchronous `describe` supplies a pre-registered MIME type, length and SHA256; asynchronous `read` supplies only those bytes under the byte limit, cancellation and current authorization. A reference is a registry key, never permission for arbitrary fetch or filesystem access. Missing, revoked, changed or oversized objects cannot complete a full archive transfer. This candidate captures external image artifacts from the existing media contract; complete archival storage is not proof that an image reached a vision model. Real summary publication, cold material recall and their combined model-input behavior remain subject to the matching candidate's acceptance results; these instructions are not a release or acceptance claim.

`await host.closeOverlay(subject)` 排空并关闭归档叠加能力，**保留原 Store 和业务会话**；同一 `session` 仍可继续发送。它不删除历史、不退还固定额度。`dispose()` 只关闭宿主介质：先关闭已返回会话并通过原 `idle()` / `drain()` 等待排空，再调用它；不能用它代替 ACK 或归档关闭。未知写入结果须沿原请求与身份恢复，不通过改会话 ID 或删除目录绕过。

English: `await host.closeOverlay(subject)` drains and closes the archive overlay while preserving the original Store and business session; the same session can continue sending. It neither deletes history nor refunds reserved capacity. Before disposing the host storage, close its returned sessions and await their original `idle()` / `drain()` lifecycle. `dispose()` does not replace durable acknowledgement or overlay closure. Reconcile unknown writes with the original request and identity; changing the session ID or deleting its directory is not recovery. Closing an overlay does not convert an offload session into an SDK1 session or reconstruct missing original history.


### 轮末有效上下文引用 / Committed effective context

在业务轮及原持久化完成后，可读取当前有效历史的关联信息：

```ts
await session.idle();
await session.drain();
const reference = await host.readEffectiveContext(subject);
// storeCommitId、target 和 completion 来自原 Store 与原完成事实。
// referenceDigest 可用于比较；不传给模型，也不能用来授权。
```

该异步只读方法使用原会话排他队列；运行中或未决写入拒绝，不补交Store、不重放模型或工具。它重读实际有效历史，包含最后一次模型发送后提交的assistant回复。同一当前读取授权版本、原提交和目标在冷重建后产生相同引用；授权或提交变化会改变引用。未完成文件轮换、截断和损坏按原恢复链处理，不能靠读取触发修复。

Call `host.readEffectiveContext(subject)` after the original turn and persistence settle. It returns a frozen reference to committed effective history, including the final assistant reply. A cold reconstruction of the same commit produces the same reference under the same current read authorization revision. Pending work, revocation, or unconfirmed storage rejects the read without repairing files or replaying work. This is correlation data, not executable authority or proof of a provider cache hit. Closing invalidates a late result while the original read remains owned until it settles.

原 Store 不逐次落盘登记该提交 ID：此引用核对原完成事实与当前内容，不能识别外部同内容重写，也不能作为防回滚凭证。 The original Store does not persist this commit ID per write; this reference cannot detect external rewrites with identical content and is not an anti-rollback proof.

## SDK2 缓存宿主与后端接续 / Cache host and backend continuity

当前开发候选通过 `@tansr/sdk/extensions` 的 `createNodeCacheHost` 显式开启平台令牌档缓存关系。返回的仍是原完整 `AgentSession`；Electron 继续在主进程集成原 SDK、依赖、Store、工具和权限回调。此配置不启用远端薄终端模式，也不承诺供应商缓存命中或费用收益；须使用包含此出口及 bridge API 的配套版本。

```ts
import { createFileSessionStore } from '@tansr/sdk';
import { createNodeCacheHost } from '@tansr/sdk/extensions';

// 下列路径、身份、令牌和密钥由应用配置提供；不从聊天内容推导。
const store = createFileSessionStore({ dir: historyDirectory });
const cacheHost = createNodeCacheHost({
  store, applicationScopeId, endUserId, baseUrl,
  cache: {
    directory: cacheDirectory, mode: 'create', storeId: 'app-cache-v1',
    key: { id: localVaultKeyId, bytes: localVaultKey },
  },
  authorize: ({ sessionId, operation }) => currentAuthorization(sessionId, operation),
});
const session = await cacheHost.open({
  mode: 'fresh', sessionId,
  session: { token, baseUrl, tools: { builtin: [] } },
});
// 继续原 session.send / events / 工具与审批接口。
const closed = await session.closeAsync({ timeoutMs: 30_000, flushStore: true });
if (closed.status === 'completed') await cacheHost.dispose();
// 其他状态保留 session，后续 await session.drain(...) 并确认 completed 后再 dispose。
```

`historyDirectory` 和 `cacheDirectory` 分别保存原会话与本地加密 vault；缓存目录必须是绝对路径。`localVaultKey` 是应用安全保存的 32 字节本地介质密钥，不是平台 issuer 签名密钥。冷重开沿用原 Store 目录、cache 目录、`storeId`、key ID/字节及应用/用户/API 身份，将 `cache.mode` 改为 `'recover'`，用同一 `sessionId` 调用 `open({ mode: 'resume', ... })`。不要丢弃 vault 或改身份绕过未知结果。新扩展当前使用原 `createFileSessionStore` 的受保护删除能力；旧自定义五方法 Store 的普通会话兼容性不因此改变，也不代表可直接接入此扩展。

`authorize` 是应用的同步当前权限检查：拒绝时抛错，允许时返回 `{ authorizationRevision }`（非负十进制整数字符串），权限变化时更新版本。`open`、`resume`、`cold-new`、`run`、`delete` 各操作都要核验。`session` 使用 `NodeCacheSessionOptions`，只传原令牌档会话选项，不能再注入 `store`、`sessionId`、`resume`；`baseUrl` 必须与宿主相同。`mode: 'fresh'` 可沿原 SDK 二选一传 `fork` 或 `initialMessages`；恢复模式不接受这两项。分叉只读取同一原 Store 中获授权的源会话，异步装配后、写入目标前再次核验源权限。新目标获得独立 runtime、逻辑关系和请求身份，导入正文不会继承 Core 材料品牌或源未决执行。

```ts
// checkpoint 来自原 sourceSession.checkpoint()；源和目标均须当前授权。
const forked = await cacheHost.open({ mode: 'fresh', sessionId: 'forked-session',
  session: { token, baseUrl, fork: { sessionId: 'source-session', checkpointId: checkpoint.checkpointId } } });
// 或使用原 SDK 导入选项；不要同时传 fork。
const imported = await cacheHost.open({ mode: 'fresh', sessionId: 'imported-session',
  session: { token, baseUrl, initialMessages: authorizedHistory } });
```

旧档案没有缓存证明、或新节点尚无缓存库存时，仍沿原 Store 读历史并建立独立冷关系；不能把无法读取、损坏或仍含 unknown 的既有库存当成空库。旧会话归档后只有显式 `archived: 'cold-new'` 才获准冷建，源未决请求不转移到新身份。

恢复存储分四种原有路径：内置热 FileStore、FileStore 加自建 `cold` 介质、普通五方法自建 Store、旧档案缺少 `platformSessionId`。前两种可使用本扩展；后两种继续原 SDK 会话，缓存按冷路径处理，不接受自报缓存证明。自建 Store 应原样保存并返回 `meta.platformSessionId`，支持 `create` 的幂等更新；旧字段缺失时 SDK 生成新 runtime 并先经原 `create` 保存，保存失败则不开模型请求。恢复与继续持久化需同时配置顶层 `store` 与 `resume.store`，后者单独使用只指定读取来源：

```ts
const restored = await createSession({ token, baseUrl, store: customStore,
  resume: { store: customStore, sessionId } });
```

缓存库存仅缺 `relation` 二级索引时，宿主从同一加密库存的唯一原 session/lineage 归属及已验接续回执重建索引；原 runtime、来源和未决 requestId 均不改。主 session 行缺失而原 runtime/relation 尚在则拒绝 `mapping_unavailable`，包括删除，保护唯一历史和未决责任。回滚旧库存也不能使已退役运行复活；应找回匹配的原库存后恢复，不能擦库求成功。

English: the four storage paths are the built-in hot FileStore, FileStore with a custom `cold` medium, an ordinary custom five-method Store, and legacy history without `platformSessionId`. The first two can use this extension. The last two retain the original SDK session path and cold-cache behavior without client-asserted cache proof. Preserve `meta.platformSessionId` and support idempotent `create` updates. For legacy records the SDK stores a new runtime through `create` before a model request; failed writes stop initialization. Configure both top-level `store` and `resume.store` as above to resume and persist; `resume.store` alone only chooses the read source. A missing secondary `relation` index is rebuilt from the unique original session/lineage and an accepted continuity receipt in the same encrypted inventory. Missing primary ownership with retained runtime/relation evidence rejects opening and deletion. Keep the original history, inventory and unresolved request identity; restoring an old inventory never authorizes revival of a retired runtime.

在 `open` 完成后、`dispose` 之前，可以读取本地连续性观察：

```ts
const cacheView = cacheHost.readCacheContinuity(sessionId);
const projection = cacheView.projection;
console.log({
  state: cacheView.state,
  logicalGroup: cacheView.logicalGroup?.id ?? null,
  projection: projection ? {
    revision: projection.revision, status: projection.status, reason: projection.reason,
  } : null,
  provider: cacheView.provider,
});
```

该同步入口先检查当前 `run` 权限，只返回新对象，不读会话正文、不出网、不推进未决操作。`projection` 是最近接纳并保存在本地的网关投影版本、状态及原因，不含 `projectionRef`、绑定 ID、票据或供应商 cache key；`current` 仅是该次网关回执的状态，不证明实时上游状态或缓存命中。当前 runtime 未加载、宿主非 ready 或绑定控制操作未决时返回 `null`；字段本身为可选，以兼容较早候选。冷构造时的 `absent` 仅说明该宿主尚未加载记录，不能据此判断磁盘或远端没有映射。`provider.hit`、`usage`、`billing` 保持 `unknown`；真实命中和费用须另核供应商实报与账单。修改返回对象不会修改宿主缓存事实。

English example: after `await cacheHost.open(...)` and before disposal, call `cacheHost.readCacheContinuity(sessionId)` as above. The optional projection contains only `revision`, `status`, and `reason`; use `cacheView.projection ?? null` when consuming older candidates. This synchronous read checks current `run` authorization and returns independent objects. It does not read conversation bodies, make HTTP requests, or resolve pending operations. The projection describes the last locally accepted gateway receipt; `current` is not proof of current upstream state or a cache hit. It is `null` when the runtime is not loaded, the host record is not ready, or a binding control operation is pending. An `absent` result on a newly constructed cold host does not prove there is no mapping on disk or remotely. No projection reference, binding ID, ticket, or provider cache key is exposed. Provider hit, usage, and billing remain `unknown`; verify provider reports and bills separately. Mutating the returned view cannot change the host's stored facts.

新 Core 缓存链属于配套可信宿主的加法能力：共同 kernel 提供实际系统/工具输入指纹和已准入材料来源，平台用原 continuity 权威、原 C2 事务和原用量生成有界诊断。当前只有受信宿主显式 `coreProjection: true` 且持原 issuer 时启用；公开 `NodeCacheContinuityBridge` 不自动取得 Core 签发能力。旧 SDK1、原 Electron 完整集成和上述纯本地观察接口保持原行为。启用会在无未决交换时将缓存库存迁为 `sdk2-cache-client-core-v1`；旧二进制不能读取该库存，必须保留原文件和同请求意图，不能擦库或退旧路由重发。具体入口、限值及迁移规则见[加法合同](../../doc/rfc/sdk2-cache-core-v1.md)。

English: the additive Core cache path uses kernel-owned input/material facts, the existing trusted host issuer and the original C2/usage transaction. Only an explicitly configured trusted host with `coreProjection: true` enables it; the public continuity bridge does not become a Core signer. SDK1, embedded Electron and the local observation API retain their behavior. Enabling it migrates a quiescent cache inventory to `sdk2-cache-client-core-v1`; older binaries must refuse and preserve it. Never delete the inventory or replay unresolved work through the old route. See the linked additive contract for diagnostic endpoints and rollback constraints.

默认恢复保留同一个平台 runtime（same-runtime）。需跨运行接续时，显式在构造选项中传入 `continuity: NodeCacheContinuityBridge`：

```ts
import type { NodeCacheContinuityBridge } from '@tansr/sdk/extensions';

const continuity: NodeCacheContinuityBridge = {
  issuerId, sourceNamespace,
  runtimePolicy: 'renew-on-restore',
  credential: grantFromOriginalStoreBackend,
};
// 构造时传 createNodeCacheHost({ ...hostOptions, continuity })。
```

`runtimePolicy` 省略或为 `'preserve'` 时继续同 runtime；`'renew-on-restore'` 才请求受信来源接续。`credential({ sessionId, intentBytes, signal, assertCurrent })` 返回短期 grant，必须由持有**同一真实 Store 来源权威**的后端核验用户、应用、来源映射、原历史与删除状态后签发。异步等待须尊重取消并在返回前调用 `assertCurrent()`；SDK 已保存的 `intentBytes` 不能改写，凭据过期只刷新同一意图，不换 `requestId`。未知提交先经 bridge `/query` 恢复原意图；`result_unknown` 不是创建新关系的许可。宿主沿 `/t1/runtime-continuity-bridge/v1/execute` 和 `/query` 传递 grant，发行者密钥始终留在受信后端。示例见[同 Store 后端接续装配](../../examples/token-server/cache-continuity.mjs)；该闭包不能改成接受任意终端历史或自报摘要的盲签入口。

平台 `410` 是归档终态，缺省拒绝恢复。仅 `open({ mode: 'resume', sessionId, session, archived: 'cold-new' })` 且当前 `cold-new` 授权允许时建立独立新关系；不复用已归档的 logical/group，也不重发旧未决请求。存在未决控制或执行时先解决原意图。退出时逐一关闭并检查**全部**已返回会话的 `closeAsync` / `drain` 完成回执，再 `await cacheHost.dispose()`；dispose 不删历史、不确认未知提交，也不关闭应用借给它的 Store。

需要删除历史时，全部宿主会话也须先关闭并排空，在 dispose **之前**调用显式删除入口：

```ts
// NodeCacheDeleteRequest：使用当前平台令牌（或原令牌 getter）及同一 baseUrl。
await cacheHost.delete({ sessionId, authorization: { token, baseUrl } });
// 自定义传输可在 authorization 中显式传 fetchImpl。
await cacheHost.dispose();
```

`delete` 返回 `Promise<void>`，沿原来源删除和 Store 清理链确认；冷构造后可直接用此入口，不创建 AgentSession、不发 heartbeat 或模型请求。删除失回包时保留原介质和历史，再以当前认证在恢复宿主上调用同一 `delete`，沿耐久记录查询/完成**原 requestId**，不能换来源或请求。删除进行中拒绝 open/dispose。仅构造冷宿主后直接 `store.delete(sessionId)` 仍会明确拒绝 `mapping_unavailable`：这不表示磁盘没有来源，也不能据此静默删史；用上述认证删除入口恢复原意图。

English: `createNodeCacheHost` is an explicit, matching-candidate extension for the platform-token profile. It returns the full existing AgentSession. Electron keeps the integrated SDK, dependencies, main process, tools, Store and approval callbacks. Supply the original `createFileSessionStore`, an absolute private cache directory, stable application/user/API identity, `storeId`, and a securely retained 32-byte local vault key. This key protects local storage and is not an issuer signing key. Reopen with the same values, `cache.mode: 'recover'`, and `open({ mode: 'resume', sessionId, session })`. Existing custom five-method Stores remain valid for ordinary SDK sessions; the new host requires the original FileStore's guarded deletion support.

`authorize` synchronously checks current rights for each operation and returns a nonnegative decimal-string `authorizationRevision`, changing it when rights change; throw to deny. `session` takes `NodeCacheSessionOptions` for the original token profile and matching `baseUrl`, without `store`, `sessionId` or `resume`. Fresh sessions can use the original `fork` or `initialMessages` options, exclusively; resume cannot. Fork reads an authorized source in the same Store and rechecks its authorization after asynchronous assembly, immediately before writing the target. Each target gets a new runtime, logical relation and request identity. Imported text does not inherit trusted material provenance or unresolved source executions. Old history without cache proof and a new node with no vault may build an independent cold relation; an unreadable, corrupt or unresolved existing vault is not an empty vault.

Recovery preserves the same runtime by default. A `continuity` bridge with `runtimePolicy: 'renew-on-restore'` requests trusted continuity; omitted policy or `'preserve'` keeps the runtime. Its `credential` callback receives the original persisted intent bytes, session ID, cancellation signal and `assertCurrent`. Only a backend authoritative for the same real Store may issue the short-lived grant after checking scope, source mapping, history and deletion. Honor cancellation and check `assertCurrent()` after waiting. Never sign arbitrary terminal history or self-reported digests, or embed an issuer key in Electron. The linked backend example stays within that Store's trusted process.

Grant refresh preserves the original intent and request ID. An unknown commit is recovered through the bridge's `/query`; it does not authorize a new request or relationship. HTTP 410 is final for the archived runtime. Explicit `archived: 'cold-new'` plus current authorization creates an independent relationship without reusing the archived logical/group or replaying pending work. Close every returned session, confirm completed `closeAsync` / `drain` receipts, then dispose the host; disposal neither deletes history nor settles unknown results or closes the borrowed Store. This documents candidate configuration, not a published package or provider-cache savings claim.

To delete, first close and drain every host session, then call `cacheHost.delete({ sessionId, authorization: { token, baseUrl } })` before disposal. The public `NodeCacheDeleteRequest` also accepts optional `authorization.fetchImpl`. This authenticated `Promise<void>` operation follows the original source-deletion and Store cleanup chain, including from a cold host without creating an AgentSession, heartbeat or model request. After a lost deletion reply, keep the original vault and history, recover the host, and call the same delete operation with current authentication to resolve its durable original request ID. Opening or disposing during deletion is rejected. Calling `store.delete` directly on a cold host that has not opened still rejects with `mapping_unavailable`; use the explicit authenticated host deletion entry instead of treating an unavailable mapping as absent history.

Core 响应丢失时，配套可信宿主可关闭并排空原会话，保留原库存及 issuer 配置，从冷宿主查询原请求。`requestId` 必须是原未决请求的已保存身份，不能另铸；普通终端 bridge 不因此获得签发能力。

```ts
// host 已用原 key/storeId/身份恢复；全部会话 close + drain 后再调用。
const observed = await cacheHost.lookup({ sessionId: 'source-session', requestId: originalRequestId,
  authorization: { token, baseUrl }, signal: abortController.signal });
// observed.state: not-found | accepted | result-unknown | completed | receipt-expired
// completed.result 可能为 null；响应正文失效不等于未执行。
```

查询只读当前受理事实和仍保存的原响应，任何状态均不清除 pending，不创建模型会话、不重发、不换 runtime，也不把原响应当作下一条新输入的结果。未部署查询接口的服务返回错误时仍保留原 unknown。真正续办原执行仍须重走原材料 supply/reader，以新 lease 核验当前来源并重建真实 kernel 品牌，保持原请求键、正文和 intent；仅持久化 intent 不能自授执行。`send` 和 `sendBlocks` 对显式 required 材料会在空闲起轮前拒绝尚未准入的供给；最终 kernel 屏障仍复验。

English: the matching trusted Core host can use `lookup()` after closing and draining its sessions, reopening the same vault and retaining the original issuer configuration. Pass the saved unresolved request ID and current app-user token. This lookup only reads the original admission or retained response; every state preserves pending work. `not-found` and `completed` with a null result are not evidence that execution never occurred. There is no first dispatch, new runtime or fallback to the old exchange route, including on an older server's lookup error. To resume the original execution, reread its materials through the original authorized supply and a new lease, then let the kernel rebuild its Core evidence for the same request key, body and intent. The stored intent alone cannot authorize execution. Explicitly required material queues block idle `send`/`sendBlocks` before starting a turn and are checked again at final kernel admission.
