Skip to content

官方集成层

TavernHeadless 提供了一套官方维护的第一方接入层,用来统一前端、桌面客户端、脚本和其他消费方的接入方式。

当前只有两个包:

  • @tavern/sdk —— 基础层,负责和后端打交道
  • @tavern/client-helpers —— 语义层,负责把数据整理成前端好用的形态

合在一起就是 TavernHeadless Official Integration Kit。

为什么要有这层

后端 API 可以直接调,但接入方通常会重复写这些东西:

  • 请求封装和账号头注入
  • SSE 解析
  • 错误处理
  • 时间线整理
  • usage 归一化
  • 流式生成的中间状态累积
  • Tool Calling 和 MCP 的接入包装
  • Client Data 的 owner / key-path / collection 分组语义

如果这些逻辑散落在每个前端里,接入方式会越来越分散,行为也会不一致。

官方集成层把这些已经稳定、已经重复出现、属于接入层的问题收拢住,统一提供。

两个包的边界

@tavern/sdk

基础层。

负责不负责
HTTP API 调用时间线视图整理
默认请求头注入active page 选择
统一错误对象store 状态管理
SSE 读取与解析hooks、composables、组件
资源方法Vue / React / Pinia 绑定
保留底层请求能力

@tavern/client-helpers

语义层。

负责不负责
usage 归一化发请求
时间线构建依赖 fetch
流式状态 reducer依赖 Vue / React / Pinia
active page 选择
API 错误到界面状态的映射
client-data owner / map / 路径读取辅助

@tavern/shared

@tavern/shared 是内部包,仓库内部可以复用,但不属于公开接入面。

建议的使用顺序

  1. @tavern/sdk 读取或写入资源
  2. @tavern/client-helpers 整理数据
  3. 在应用层接入 store、组件、页面

基本示例

创建客户端

ts
import { createTavernClient } from "@tavern/sdk";

const client = createTavernClient({
  baseUrl: "http://localhost:3000",
});

如果后端开启了认证,可以通过 getHeaders 注入:

ts
const client = createTavernClient({
  baseUrl: "http://localhost:3000",
  getHeaders: () => ({
    authorization: "Bearer <token>",
  }),
});

如果服务端启用了多账号:

  • AUTH_MODE=jwt 时,应当使用已经带有目标账号 claim 的 JWT;默认 claim 字段名是 account_id,可由服务端通过 AUTH_JWT_ACCOUNT_CLAIM 改名
  • AUTH_MODE=api_key 时,应当由服务端通过 AUTH_API_KEY_ACCOUNTS 把 API Key 绑定到账号
  • SDK 各资源方法里的 accountId 参数,以及 buildAccountHeaders() 生成的 x-account-id,都只是兼容头提示,不能替代服务端认证,也不会直接切换账号

创建会话和 Project 兼容字段

client.sessions.create(...) 对应 POST /sessions。普通客户端不需要传 Workspace 或 Project。

ts
const session = await client.sessions.create({
  accountId: "account-1",
  title: "Campfire",
  characterId: "char-1",
  userId: "user-1",
  presetId: "preset-1",
});

如果调用方已经知道目标 Project,可以传 projectId。SDK 会把它写成 REST 请求体里的 project_id

ts
const sessionInProject = await client.sessions.create({
  accountId: "account-1",
  projectId: "proj-1",
  title: "Project Session",
});

省略 projectId 时,服务端会使用当前账号默认 Workspace,并为新 Session 创建 session_default Project。

Client Data 第二期接入

如果接入方要启用插件级 caller owner 隔离,优先建议在单次 domain-scoped 调用上显式传 callerOwner

ts
const pluginOwner = { ownerType: "plugin", ownerId: "chat-annotator" } as const;

await client.clientData.items.getByKey({
  accountId: "account-1",
  domainId: "domain-1",
  callerOwner: pluginOwner,
  collectionName: "settings",
  itemKey: "theme.dark",
});

如果整个客户端都固定代表同一个 owner,也可以在默认请求头中加入:

ts
const client = createTavernClient({
  baseUrl: "http://localhost:3000",
  getHeaders: () => ({
    authorization: "Bearer <token>",
    "x-client-owner-type": "plugin",
    "x-client-owner-id": "chat-annotator",
  }),
});

说明:

  • callerOwner 参数和默认头最终都会落到 X-Client-Owner-TypeX-Client-Owner-Id
  • 未提供 caller owner 头时,服务端保持第一期兼容模式
  • 提供非法 caller owner 头时,服务端返回 400 client_data_caller_owner_invalid
  • grant / audit 管理路由要求 caller owner 必须是 domain owner
  • 如果服务端把某个 domain 标记为 managed domain,raw /client-data 写路径会返回 403 client_data_managed_domain_raw_access_forbidden

调用 Client Data 资源

ts
const domainOwner = { ownerType: "application", ownerId: "my-app" } as const;
const pluginOwner = { ownerType: "plugin", ownerId: "chat-annotator" } as const;

const domain = await client.clientData.domains.create({
  accountId: "account-1",
  ...domainOwner,
  domainName: "preferences",
});

await client.clientData.domains.updateQuota({
  accountId: "account-1",
  domainId: domain.id,
  quotaMaxEntries: 20000,
  quotaMaxBytes: 20971520,
});

const grant = await client.clientData.grants.create({
  accountId: "account-1",
  callerOwner: domainOwner,
  domainId: domain.id,
  granteeOwnerType: pluginOwner.ownerType,
  granteeOwnerId: pluginOwner.ownerId,
  canRead: true,
  canWrite: false,
  canDelete: false,
  canList: true,
});

const imported = await client.clientData.domains.importAsNew({
  accountId: "account-1",
  conflictPolicy: "fail",
  payload: {
    domain: {
      ownerType: "application",
      ownerId: "my-app",
      domainName: "preferences-copy",
    },
    collections: [],
  },
});

const item = await client.clientData.items.getByKey({
  accountId: "account-1",
  callerOwner: pluginOwner,
  domainId: imported.domain.id,
  collectionName: "settings",
  itemKey: "theme.dark",
});

const auditLogs = await client.clientData.auditLogs.list({
  accountId: "account-1",
  callerOwner: domainOwner,
  domainId: imported.domain.id,
  limit: 20,
});

console.log(grant.canRead);
console.log(item?.valueJson);
console.log(auditLogs.data[0]?.action);

这里的 clientData 资源仍然对应 raw /client-data。如果后端把某个底层 domain 标记为 managed domain,接入方需要改走对应的受治理服务,而不是继续直接写 raw clientData

Client Data helper 用法

ts
import {
  buildPluginOwner,
  groupItemsByCollection,
  resolveItemByPath,
  toClientDataMap,
} from "@tavern/client-helpers";

const owner = buildPluginOwner("chat-annotator");
const grouped = groupItemsByCollection(items);
const nested = toClientDataMap(items, collections);
const resolved = await resolveItemByPath(client, domainId, "settings", "theme.dark", {
  accountId: "account-1",
});

Variables 的接入建议

变量相关的官方接入建议分成三层:

  1. client.variables.resolveContext(...) 读取 durable truth
  2. client.variables.getPageStagedWrites(...) 读取页级候选写入
  3. client.variables.getPagePromotions(...) 读取 durable promotion 轨迹

如果你需要把这些数据整理成 inspector 视图,再交给 @tavern/client-helpers

ts
import {
  flattenPageStagedVariableWrites,
  flattenVariableSnapshot,
  groupVariablePromotionTrace,
} from "@tavern/client-helpers";

const resolvedRows = flattenVariableSnapshot(resolvedSnapshot);
const stagedRows = flattenPageStagedVariableWrites(stagedSnapshot);
const promotionGroups = groupVariablePromotionTrace(promotionSnapshot);

这里要注意两点:

  • resolveContext(...) 仍然是 durable-only
  • staged / promotion inspect 是额外观察面,不是 resolveContext(...) 的扩展字段

SDK 资源覆盖范围

目前 @tavern/sdk 已覆盖这些资源:

分类资源
会话与内容结构healthsessionsmessagesfloorspagesbranches
提示词运行时高级资源promptRuntime
角色、资料与配置charactersuserspresetspresetEntriesworldbooksworldbookEntriesregexProfiles
导入、导出、备份与模型importsexportsbackupbackupJobschatTransferJobsllmProfilesllmInstances
账号、变量与记忆accountsvariablesmemoriesmemoryEdgesmemoryJobsmemoryScopes
工具与运行集成toolsmcp

Backup 资源支持核心资产导出和恢复作业。导出时可以通过 includeVcTags 保留 VC Tag,并通过 includeOperationLogs: "referenced" | "selected_scope" 选择是否导出相关 Operation Log。

| 高级客户端数据系统 | clientDatasessionState |

其中 backupbackupJobs 对应核心资产备份 v1:

  • backup.createExportJob(...)
  • backup.previewRestore(...)
  • backup.createRestoreJob(...)
  • backupJobs.list(...) / getDetail(...) / cancel(...) / retry(...) / downloadFile(...)

.thbackup 文件本身继续使用 snake_case 契约。SDK 返回值仍按既有约定映射为 camelCase

记忆 scope 约定

memoriesmemoryJobsmemoryScopes 现在都接受 globalchatbranchfloor 四种记忆作用域。

其中:

  • 主聊天链默认使用 branch scope。
  • chat scope 只表示显式的 session 级共享记忆。
  • branch scope 的 scopeId 需要使用 JSON.stringify([sessionId, branchId]) 构造。

Prompt Runtime preview 示例

ts
const preview = await client.promptRuntime.previewText({
  accountId: "account-1",
  sessionId: "session-1",
  branchId: "main",
  text: "{{setvar::资产.金币::3}}{{getvar::资产}}",
  budget: {
    maxInputTokens: 4096,
    reservedCompletionTokens: 1024,
  },
  sourceSelection: {
    history: { mode: "windowed", maxMessages: 24 },
    examples: { enabled: false },
  },
  visibility: {
    mode: "allow_all_except_hidden",
    hiddenFloorRanges: [{ startFloorNo: 1, endFloorNo: 2 }],
  },
});

console.log(preview.policy.budget);
console.log(preview.text);
console.log(preview.runtimeTrace.sourceSelection?.excludedSources);
console.log(preview.memory?.runtimeMode);
console.log(preview.memory?.scopeResolution);
console.log(preview.runtimeTrace.visibility?.filteredFloorNos);
console.log(preview.runtimeTrace.macro?.mutationPreview);
console.log(preview.runtimeTrace.macro?.stagedMutations); // []

这个方法正式契约是 macro_text_preview,对应 capabilities.observability.preview.mode === "macro_text_preview"returnsAssemblyTruth === false。它不会调用 LLM,不会创建 floor,也不会写 promptSnapshot,同时也不会执行 prompt assembly、budget allocation 或 delivery materialization。当前 request 级 structure / delivery / budget / sourceSelection / visibility 覆盖会进入返回结果里的 policysourceMap,但返回的 runtimeTrace 固定只投影 capabilities.observability.preview.traceSubset,即 macrosourceSelectionvisibility。宏诊断继续统一走 runtimeTrace.macro;响应的 limitations 会额外说明 preview 只是 macro_text_preview 子视图,便于 UI 上与 live / dry-run 区分。

Prompt Runtime inspect 示例

ts
const inspect = await client.promptRuntime.inspect({
  accountId: "account-1",
  sessionId: "session-1",
  message: "Please continue the campfire scene.",
  branchId: "alt-branch",
  sourceFloorId: "floor-12",
  promptIntent: "continue",
  generationParams: {
    maxOutputTokens: 256,
    temperature: 0.7,
  },
  sessionStateWrites: [
    {
      namespace: "quest_flags",
      slot: "companion",
      value: { mood: "ally" },
    },
  ],
});

console.log(inspect.preparedTurn.messages);
console.log(inspect.preparedTurn.promptSnapshot?.promptDigest);
console.log(inspect.preparedTurn.memorySummary);
console.log(inspect.preparedTurn.memory);
console.log(inspect.preparedTurn.runtimeTrace?.memory);
console.log(inspect.preparedTurn.runtimeTrace?.budgets?.byGroup);
console.log(inspect.preparedTurn.sessionStateWrites);
console.log(inspect.governance.entries);
console.log(inspect.governance.mismatches);

inspect(...) 是只读 prepared-turn 检查接口。它会准备完整 prompt turn,并返回 preparedTurnpolicysourceMapgovernancediagnosticstrimReasonsexcludedSourcessectionStats,以及与兼容 memorySummary 并存的结构化 preparedTurn.memory。但它不会调用模型,也不会创建 floor、写 promptSnapshot、写 explain snapshot,或者提交任何副作用。对应能力位是 capabilities.observability.inspect.mode === "prepared_turn"

Prompt Runtime governance / explain / compare 示例

ts
const policy = await client.promptRuntime.patchPolicy({
  accountId: "account-1",
  sessionId: "session-1",
  budget: {
    maxInputTokens: 4096,
    reservedCompletionTokens: 1024,
  },
  sourceSelection: {
    history: { mode: "windowed", maxMessages: 24 },
    examples: { enabled: false },
  },
});

const explain = await client.promptRuntime.getFloorExplain({
  accountId: "account-1",
  floorId: "floor-12",
});

const inspect = await client.promptRuntime.inspect({
  accountId: "account-1",
  sessionId: "session-1",
  message: "Please continue the campfire scene.",
});

const diff = await client.promptRuntime.compare({
  accountId: "account-1",
  sessionId: "session-1",
  leftFloorId: "floor-11",
  rightFloorId: "floor-12",
});

console.log(policy.persistentPolicyEnvelope?.version);
console.log(policy.persistentPolicyEnvelope?.updatedAt);
console.log(policy.persistentPolicyEnvelope?.updatedBy);
console.log(policy.persistentPolicyEnvelope?.value.budget);

console.log(explain.snapshotAvailable);
console.log(explain.assets);
console.log(explain.sectionStats);
console.log(explain.memory); // 旧 snapshot 或更早 explain 行可能为 null
console.log(explain.governance); // 旧 snapshot 可能为 null
console.log(explain.resolvedPolicy);

console.log(inspect.preparedTurn.messages);
console.log(inspect.governance.entries);
console.log(diff.left.snapshotAvailable, diff.right.snapshotAvailable);
console.log(diff.policyChanges);
console.log(diff.governanceChanges);
  • patchPolicy(...)patchBranchPolicy(...) 现在都支持 structuredeliverybudgetsourceSelection
  • 写入后的持久化策略会带 envelope 元数据:versionupdatedAtupdatedByvalue
  • getFloorExplain(...) 只读取 committed floor 的持久化真相,对应 capabilities.observability.explain.persistedTruthOnly === true。snapshot-backed 路径会附带"只读持久化真相"声明;snapshotAvailable = true 表示响应来自 committed explain snapshot,false 表示旧楼层 fallback,此时 limitations 会在通用只读声明基础上额外追加"老 floor 字段可能为 null"的 fallback 条目,assetsresolvedPolicysectionStats 等也可能为 null
  • previewText(...) 会在顶层 memory 字段里返回结构化记忆真相;它不会把这部分真相塞进 runtimeTrace,因为 preview trace 仍固定只保留 macrosourceSelectionvisibility 三个子字段。
  • inspect(...).preparedTurn.memorySummary 继续保留兼容摘要字符串;inspect(...).preparedTurn.memorygetFloorExplain(...).memory 是新的结构化真相对象。较旧的 explain snapshot 行可能返回 null
  • compare(...) 只支持同一 session 内的两个 committed floor,且只返回结构化 path/value diff;不会做 explain recompute。

当前 @tavern/client-helpers 没有为 historical explain、compare 或 Prompt Runtime 结构化 memory 对象增加专用 helper。原因很简单:这些响应已经是稳定的只读对象,当前没有额外的跨框架语义整理需求。接入方直接使用 SDK 返回值即可。

assembly:提示词组装的运行结果

respondDryRun(...) 返回的 assembly 可以理解为 dry-run 的兼容摘要面。SDK 现在同时导出:

  • PromptAssemblyCompat
  • RespondDryRunAssembly(兼容别名)

如果同一事实已经在 runtimeTrace 中以更结构化的形式出现,建议优先读取 runtimeTraceassembly 继续保留,主要是为了让既有 dry-run 调试面和 preset 兼容说明保持稳定。

常见对应关系如下:

assembly 字段优先读取的 runtimeTrace说明
assistantPrefillApplied / assistantPrefillStrategyruntimeTrace.deliveryassistant prefill 是否真正落到最终发送消息
regexPreRules / regexPostRules / preprocessedUserMessageruntimeTrace.regex正则执行结果与预处理后的用户消息
worldbookHits / worldbookMatchesruntimeTrace.worldbook世界书命中数量与详情
memorySummaryInjectedruntimeTrace.memory.summaryInjected记忆摘要是否注入
selectedPromptOrderCharacterId / ignoredPromptOrderCharacterIds / continueNudgeApplied / continueNudgeText / namesBehaviorAppliedruntimeTrace.preset预设运行事实与降级说明

对于 regex,SDK 现在还会继续解析 runtimeTrace.regex.phasesruntimeTrace.regex.reservedPlacementsruntimeTrace.regex.substitutionMode

  • phases 用来表达每个 regex phase 的真实执行、跳过或 reserved 状态。
  • reservedPlacements 用来表达当前仅保留、不执行的 placement,例如 WORLD_INFO
  • substitutionMode 用来表达当前 regex substitute 的正式语义边界。

同时要区分两类名字:

  • runtimeTrace.budgets.byGroup[].groupruntimeTrace.budgets.trimReasons[].group 是 budget group 标签,可以出现具体 section 标签,例如 section:main
  • capabilities.sourceSelection.supportedSourcesruntimeTrace.sourceSelection.excludedSources[].source 仍只使用公开 source kind:historymemoryworldbookexamples

下面这些字段仍主要留在 assembly

  • mode
  • promptIntent
  • unsupportedPresetFields
  • ignoredPresetFields
  • unresolvedPresetMarkers
  • presetWarnings

@tavern/client-helpers 当前导出

函数用途
resolveUsageusage 归一化
buildTimelineMessages楼层数据 → 时间线消息列表
createInitialRespondStreamState流式状态初始值
reduceRespondStreamSSE 事件 → 流式状态累积
getDisplayPage优先使用运行中的候选输出,否则回退到 active page
groupToolEventsByExecution工具流式事件 → 执行历史分组
getActivePage从楼层取真实持久化 active page
flattenVariableSnapshotresolved variable snapshot → inspector 行
sortVariableInspectorRows变量 inspector 行稳定排序
formatVariablePreview变量值 → 展示预览字符串
mapApiErrorToUiStateAPI 错误 → 界面错误状态
summarizeRuntimeToolCatalog会话级运行时工具目录 → 摘要
buildPluginOwner构造 plugin owner
buildApplicationOwner构造 application owner
groupItemsByCollectionclient-data item 按 collection 分组
organizeCollectionItemscollection 内 item 整理
toClientDataMapitem 列表转嵌套 map
resolveItemByPathcollectionName + itemKey 直接读取单项数据

Session State 的公开接口已进入官方包

/sessions/:id/state/* 是 Session State 的公开受治理接口。@tavern/sdk 现在提供 client.sessionState,当前覆盖:

  • registerNamespace
  • listNamespaces
  • writeValue
  • deleteValue
  • resolve
  • getFloorSnapshots
  • diff

其中:

  • registerNamespace 是 control-plane write,只负责注册 custom namespace
  • writeValue / deleteValue 是 public Session State value write,只针对 registered custom namespace
  • turn API 现在也支持 sessionStateWrites,对应 turn-embedded commit_bound 写入:
    • sessions.respond
    • sessions.respondStream
    • sessions.regenerate
    • floors.retry
    • messages.editAndRegenerate
  • listNamespaces 会同时返回公开稳定的 built-in namespace 与当前 session 下已注册的 custom namespace
  • custom slot 会在首次成功 direct write 或首次成功 turn-bound commit 后 materialize,并进入 discovery / resolve / snapshot / diff
  • 当前公开稳定的 built-in slot 只有 game_state.scenegame_state.world
  • game_state 仍然对客户端只读;public delete 与 turn 内 delete: true 的治理语义都是 present: false
  • registerNamespace 当前遵循已经冻结的 identity contract:
    • namespacelogicalOwnerType 必须使用小写稳定标识,可带点分段
    • logicalOwnerId 必须使用小写稳定 id,允许字符 a-z0-9._:@/-
    • game_stategame_state.* 仍然是保留 built-in namespace / prefix,不能注册为 custom namespace
    • 服务端只会先做 trim(),不会自动转小写
  • listNamespacesresolvegetFloorSnapshotsdiff 当前都不提供 limit / offset 分页;服务端会直接返回当前过滤条件命中的完整结果
  • listNamespaces 返回的 sizeBudgetBytes 就是 slot 当前有效的 payload budget。custom namespace 默认继承当前部署的 Client Data item size limit;built-in slot 使用各自固定预算
  • Session State 的规模限制来自底层 managed storage。常见 error.code 包括:
    • validation_error
    • session_state_namespace_count_limit_exceededsession_state_namespace_item_limit_exceededsession_state_namespace_byte_limit_exceeded
    • session_state_account_item_limit_exceededsession_state_account_byte_limit_exceeded
    • session_state_payload_too_large
  • 当前实现里,如果部署关闭了 enableClientData
    • /sessions/:id/state/* 这组 public route family 默认不会注册,外部通常直接看到 404
    • turn API 如果携带了 sessionStateWrites,会返回 503 feature_unavailable

公开端点定义见 reference/api/session-state.md。 最小联调顺序见 session-state-client-checklist.md

Session-State 观察面仍不在官方包范围内

/sessions/:id/session-state/*/floors/:id/session-state/* 仍然是内部观察面。它们继续只用于排错与运维,不会进入 @tavern/sdk@tavern/client-helpers

当前实现里,如果部署关闭了 enableClientData,这组 observation route family 默认也不会注册,外部通常直接看到 404

如果你的集成方确实需要对接这组内部观察面,请基于 OpenAPI 自行封装,并接受该契约可能变化。完整定义见 reference/api/session-state-observation.md

文档同步规则

如果改动影响以下任意一项,应同步检查官方包与文档:

  • 后端 API 资源契约
  • SSE 事件结构
  • OpenAPI 输出
  • SDK 资源覆盖范围
  • helper 导出范围
  • Client Data 的 owner / grant / audit 语义

至少同步更新:

  • packages/official-integration-kit/sdk/README.md
  • packages/official-integration-kit/client-helpers/README.md
  • vitepress/guide/integration-kit.md
  • vitepress/reference/api.md
  • 对应资源参考页