# Roleplay：完整产品与原子能力

文档基线：2026-09-21。按固定源码核对实现与接线；生产启用情况需以部署有效配置为准。



<a id="product-agent-v2"></a>

# Agent V2｜持续角色对话与后台连续性维护

Agent V2 用于持续的文字角色互动：用户说一句，角色先接住当前对话；后台同时判断人物关系、知识边界、剧情、承诺和场景是否需要整理，把有必要延续的信息保存下来，供后续回复使用。它可以支撑角色陪伴、角色扮演和开放式互动故事；具体题材、人物及表达方式由角色资料和模型提示词决定。

这里的完整产品是仓库已经接线的普通 Agentic V2 应用：**角色回复 + 每轮后台判断 + 按需咨询专家 + 按需更新持续状态**。六位专家不是六个默认都要运行的步骤；图片、世界书、日记记忆和剧情 Story 也不因启用 V2 就全部自动开启。

本章核对的是 `roleplay-harness` 主干固定源码 `fa41d220d48327e58d994f936517b23f2f15f658`。已确认应用实现和配置入口；未取得目标生产环境的有效配置与运行记录，因此不把某个模型名、图片能力或可选扩展写成所有线上 V2 的默认配置。普通 V2 与 Story 在入口使用互斥选择：场景启用 `storyV1` 时进入 Story 接线，不再同时启动本文的普通 V2 专家后台。[应用装配与分流](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L1160)

## 先看一次真实机制下的两轮演示

下面是用于解释输入、输出和保存关系的业务示例，文字由文档拟写，不是线上模型实测结果。工具是否被模型选中也属于示例分支；程序强制触发的条件在后文单独列出。

角色卡描述“守卫莉娅谨慎，负责旧城城门”；已保存的历史记录说明“用户借了一把铜钥匙，答应天黑前归还”。目前仍在城门前。

| 时机 | 系统实际处理什么 | 本例可能得到的结果 | 谁接着使用 |
| --- | --- | --- | --- |
| 用户发送第 N 句 | `我把铜钥匙递给莉娅，告诉她明早再来。`；程序同时取得角色资料、聊天历史、现有持续状态和本场景的提示词 | 得到本轮回复所需的输入，以及另一份供后台判断的输入 | 回复模型和后台分别读取各自的材料 |
| 当前回复开始 | Actor，即负责写正文的回复模型，读取已经准备好的角色、历史和旧状态 | `莉娅接过钥匙，指尖抚平铜环上的细痕。“明早城门初开时，我还在这里。”` | 通过流式输出交给聊天业务，由业务完成展示和消息保存 |
| 后台判断同时开始 | Orchestrator，即负责决定请谁分析的后台模型，看本轮用户输入和此前材料 | 可能请求 `manage_callbacks`，理由是“钥匙归还及新的会面约定影响后续连续性” | 程序解析工具名称和参数，启动对应专家模型 |
| 专家完成 | 承诺与伏笔专家根据已有材料分析，而不是直接改数据库 | “用户已递出钥匙；此前归还承诺应核对；用户表达了明早再来，莉娅是否同意尚未在本轮初始材料中出现。” | 回给 Orchestrator 第二阶段，也可成为 Director 的参考 |
| 后台决定需要保存 | Orchestrator 选择 `update_director_state`；程序启动 Director，即整理持续笔记的模型 | 生成一整份新的状态草稿，保留此前事实，补充“用户递出钥匙、表示明早再来”等已被材料支持的信息 | 程序检查格式、长度与是否发生变化，再向存储服务提交 |
| 保存成功 | 存储服务接受带旧版本号的更新，返回新版本号 | 状态从版本 12 变成 13，并记录来源轮次、关联回复 ID、调用过的工具及专家分析账目 | 之后开始准备的对话请求可以读到版本 13 |
| 用户发送第 N+1 句 | `第二天，我又来到城门。`；业务传回已保存的前轮正文，程序重新读取当前持续状态 | Actor 同时看到“莉娅上一轮说她还在这里”的聊天原文，以及已保存的约定信息 | 可自然接着写莉娅认出用户，而不重新演一次初次相遇 |

**最关键的时间关系：本轮 Actor 不等后台才开始写，后台也不自动拿到它同时生成的新正文。** 因此，后台可以根据“用户递出钥匙”整理信息，却不能提前把 Actor 稍后才写出的“莉娅接过钥匙、答应明早见面”当成已经看到的事实。到了下一次请求，只有业务已经保存并传回这些文字，后台才会在新的可见历史中得到它们。运行时对 Actor 的完成记录用于观测，没有把该正文追加进本次已启动的后台输入。[轮初材料](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/runtime.ts#L462) · [前后台同时启动](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/runtime.ts#L570) · [Actor 完成后的处理](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/runtime.ts#L701)

如果用户很快发出下一句，版本 13 尚未保存，下一轮就先使用实际读到的旧版本 12，加上当时能取得的聊天历史。普通 V2 不会为了等上一轮后台而把下一句整体排队。

## 一轮内部怎样运转

### 1. 程序先确认这次请求应进入普通 V2

接入方提交本轮消息、会话、用户、角色和场景标识；服务从已允许的场景表找配置。只有选中的场景配置了 `agenticV2`，并走服务准备上下文的闭环路径，才有本文的后台处理。`roleplay-harness/v2` 是请求协议版本，`agenticV2` 是产品的后台配置，二者不是同一个开关。

服务创建三类对象：读取与保存状态的适配器、后台 Planner、安排前后台的 Coordinator，再包装普通的回复运行时。实际代码分别位于 `dispatch.ts`、`planning/runtime.ts`、`planning/agentic-v2.ts`，不是通过一个叫“Agent V2”的文件自动包含整个仓库功能。[场景校验](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L298) · [普通 V2 装配](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L1177)

### 2. 分别准备回复模型和后台的输入

不是“把同一个完整 Prompt 发给两个模型”。两边共用轮初读取的状态，但其他材料各自组装。

| 材料 | Actor：本轮写正文 | Orchestrator／专家／Director：后台 |
| --- | --- | --- |
| 角色是谁 | 按 Actor 的 ModelConfig／PromptManager 展开角色变量和模板 | 角色名、简介、定义；专家与 Director 还会收到角色开场白 |
| 用户这次说什么 | 本轮输入作为回复材料 | 独立列出的 `LATEST USER QUERY` |
| 聊过什么 | 按 Actor 模板中的历史插入规则与上下文预算保留消息 | 编译器只抽取最近 **9 条**非空 user／assistant 消息；不是 9 轮 |
| 已保存的连续性信息 | 把轮初状态及有限的账目内容加入系统消息 | 同一份轮初状态被编译成后台材料；常规上限为 6,500 字符 |
| 用户 Persona | 如果接入和模板提供，则参与回复材料 | 从组装结果取得有效 Persona，截取至 4,000 字符；明确告诉模型这是用户身份，不是角色或 NPC 身份 |
| 本轮选中的世界书 | 场景开启世界书后，由世界书流程选出并按位置注入 Actor 消息 | 当前后台材料构造没有复制本轮世界书条目；专家名叫“知识审计”也不代表自动有世界书检索权限 |
| 日记摘要、记忆表、外部 Hook 补充内容 | 取决于所选记忆来源、Hook 和模板是否接入 | 不会自动复制 Actor 的全部模板变量；不能仅凭 Actor 看到它，就认为后台也看到了 |
| Compact 压缩摘要 | 开启 Compact 后参与 Actor 的预算与上下文组装 | `contextGovernance.mode=enforce` 时使用压缩后保留历史和摘要；`shadow` 模式仅作对照，后台仍取原请求历史 |
| 本轮专家结果 | 不会自动追加入已经发出的本轮 Actor 请求 | 第二阶段 Orchestrator 和 Director 可读取 |
| 本轮新生成正文 | Actor 自己生成 | 本次后台初始快照不含该正文，之后也不会自动补入 |

上述字符数是应用主动截取的材料长度，不等于模型的 token 窗口。[后台历史与输入](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/agentic-v2.ts#L315) · [专家输入](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/memory/advisory.ts#L218) · [前台模板和预算来源](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/integrations/roleplay-axon-adapter.ts#L425)

### 3. Actor 写回复时，后台先判断要不要调用工具

普通 V2 在每次成功进入 `generate()` 的逻辑轮安排一次后台，协调器防止同一轮重复安排。这里**没有“累计 X 轮才启动后台”的总开关**。前置准备失败、尚未进入生成时，自然不会启动该次后台；自动回复建议也有自己的入口，不应混算成普通 RP 生成。

后台第一阶段会给 Orchestrator：它自己的系统提示词、上表中的业务材料、当前允许使用的工具定义。请求设置 `toolChoice: auto` 和允许并行工具调用。它可以返回工具调用，也可以不调用工具。工具调用形如：

```json
{
  "name": "manage_callbacks",
  "arguments": {
    "reason": "用户递还借用物品并提出新的会面约定",
    "focus": "区分已经发生的递出动作、待确认的接收动作和新的承诺"
  }
}
```

这是工具名与参数的业务展示；实际模型消息使用 `tool_calls` 包装，`function.arguments` 是 JSON 字符串。程序按工具名到已注册能力中查找，解析参数，调用该专家的文本模型。参数不是直接执行的脚本，也不是模型自己访问数据库。

普通 V2 注册表固定装配六位专家与 `update_director_state`；场景 `enabledTools` 从这七个名字中选择。它没有在这条后台默认装上生图、世界书查找、独立 NPC 卡保存、任意 HTTP 请求等工具。公共 SDK 能扩展 Registry，不等于普通 V2 的场景 JSON 可以直接填任意新工具名。[工具注册](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/agentic-v2.ts#L464) · [模型请求](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/agentic-v2.ts#L1083) · [参数解析与执行](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/registry.ts#L19)

### 4. 程序有少量强制分析规则

| 情况 | 程序做什么 | 生效条件与限制 |
| --- | --- | --- |
| 首轮 | 补上人物审查 `judge_npc` 和场面指导 `direct_scene` | 对应工具必须存在于 `enabledTools`；目的是逐个识别人和建立场面尺度 |
| 近期出现旧状态未记载的名字 | 补上 `judge_npc` | 从最近 4 条可见消息和本轮问题，用多语言规则识别候选名字，再检查旧状态；这不是能识别所有新人物的语义模型 |
| 上述强制专家被安排过 | 第二阶段补上 `update_director_state` | 状态工具必须启用，仍需通过后续模型调用、内容校验和保存检查 |
| 其他普通继续聊天 | 由 Orchestrator 决定是否需要专家或状态更新 | 不调用专家、不更新状态都是正常分支 |

因此“每轮后台都会跑”不等于“每轮六位专家都会跑”，也不等于“每轮都保存一个新状态”。[强制规则与名字识别](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/agentic-v2.ts#L655) · [第二阶段补状态调用](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/agentic-v2.ts#L933)

### 5. 六位专家究竟分别做什么

专家是“带岗位提示词、用各自 ModelConfig 执行一次分析的文本模型工具”。以下输出是解释用途的示例，不是固定 JSON 字段，也不代表自动写库。

| 工具与岗位 | 解决什么问题 | 本例或相近场景的建议结果 | 不能把它理解成什么 |
| --- | --- | --- | --- |
| `analyze_plot` 剧情分析 | 现在是否需要改变可选主线、当前情节或故事方向 | “用户只想归还钥匙，本轮结束借物情节；明早可以留一个低压力的相遇机会。” | 不是强制执行十几轮完整剧本，也不是 Story 产品的世界事件调度器 |
| `judge_npc` 人物审查 | 谁是谁、几个人、动机、知道什么、在哪、如何进入或离开 | “新出现的商人艾登是一名独立人物；与莉娅的关系尚未建立，不应当作旧友。” | 不是一个直接创建独立角色卡并保存的 NPC 服务 |
| `audit_knowledge` 知识审计 | 事实、猜测、谎言、秘密与证据分别被谁知道 | “用户只说钥匙归还，莉娅没有依据知道用户昨晚去过地下室。” | 不会因名称含知识而自动搜索世界书或联网 |
| `simulate_world` 世界时间与场外生活分析 | 经过了多少时间、位置是否合理、日程和期限如何变化 | “用户明确说第二天，时间可推进；莉娅是否仍值守需要结合已建立班次。” | 不是自动把世界书设定或世界数据库全面修改 |
| `manage_callbacks` 承诺、物品与伏笔管理 | 哪些约定、线索、物品、伤势、义务和后果尚未处理 | “核对铜钥匙归还状态；保留用户说明早再来的意向。” | 不意味着伏笔必须立即兑现，也不直接更新物品表 |
| `direct_scene` 场面指导 | 视角与描写重心怎样分配，角色允许主动做多少事 | “以莉娅反应和城门环境为主；她可以提出一个话题，不替用户决定留下。” | 不是后面写完整持续状态的 Director；这是六位专家之一 |

所有成功专家先返回 `{ ok: true, tool, result, usage? }`；`result` 是分析文本。常规模式最多保留 5,000 字符。开启严格上下文治理后，结果超出配置长度会成为失败结果，不静默截成一份貌似完整的建议。[工具职责与 schema](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/memory/advisory.ts#L18) · [模型执行与结果限制](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/memory/advisory.ts#L244)

### 6. 第二阶段决定是否把建议变成持续状态

已选中的专家并行执行。程序把工具调用与结果重新交给 Orchestrator；这一阶段只提供 `update_director_state` 一个工具，模型选择更新或结束。这是看完分析后的再次决策，并没有无限的“再叫专家、再分析”循环。

如果第一阶段没有咨询专家，但直接请求了状态更新，程序可以直接运行 Director，省略第二阶段；如果什么都不需要，保留旧状态。如果第一阶段同时请求专家和 Director，Director 被延后，不能在还没得到专家建议时与专家一起写状态。[第一阶段执行和第二阶段工具范围](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/agentic-v2.ts#L843) · [延后 Director 的消息](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/agentic-v2.ts#L607)

### 7. Director 生成整份笔记，程序校验后保存

Director 接收角色与开场白、用户 Persona、旧状态、近期历史、本轮用户输入、更新理由，以及已取得的专家结果。它输出一整份 **Agentic RP State 文本**，不是补丁，也不是用户看的回复。

这份持续笔记包含固定栏目：当前场景、角色生活与近期目标、柔性主线、未来发展备选、人物关系和代词归属、知识边界、位置与进入限制、新人物安排、时间与场外生活、待兑现承诺与物品、场面重心、角色主动程度、必须保持的事实、用户拒绝过的方向等。它允许提出后续方向，但这些方向不是已经发生的故事事实，也不是必须照做的工作流。

程序检查候选是否为空、是否至少 500 字符、是否超过配置上限（最多 8,000 字符）、规定栏目是否存在且顺序正确、是否混入内部推理标签，并比较新旧文本是否真的变化。**这是结构和写入检查，不是另一个模型逐条核实新状态真实性的独立复审。** Director 的输入提示要求保留未变事实、只采用材料支持的变化，但不能把这条提示当成事实一致性已经被程序证明。[Director 输入与输出要求](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/agentic-v2.ts#L380) · [程序校验](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/agentic-v2.ts#L514)

## 保存什么、何时保存、下次取回什么

| 数据 | 什么时候产生或保存 | 保存／读取通道 | 实际用途 |
| --- | --- | --- | --- |
| 用户与角色聊天原文 | 由聊天业务在自己的消息生命周期中保存 | Chat Service 的消息系统；后续请求传入历史 | 让下一轮知道实际说过什么，包括 Actor 本轮新写的内容 |
| Agentic RP State | Director 给出有效、与旧状态不同的候选，且存储检查通过时 | Axon `roleplay.state.compare_and_set` | 后续轮初通过 `roleplay.state.get` 取回，加入 Actor 与后台输入 |
| 专家分析账目 `ledger` | 与成功状态提交一起写入；不是专家一完成就单独保存 | 同一状态提交携带 `ledgerEntries` | 保留该轮分析、来源工具和用户问题；之后只选有限的活动账目投影到上下文 |
| 已调用工具与来源 | 状态提交时 | `toolsCalled`、`sourceTurn`、`sourceMessageId`、`historyRevision` | 识别状态来自哪个轮次和回复，协助版本检查和追踪 |
| 调试节点与完成状态 | 各阶段异步、尽力记录 | `markPlanning`、`markDebugNode`、`markProcessed` | 观察走到哪、为什么没有更新；不是持久业务状态的替代品 |

持续状态按 **用户 + 会话 + 场景** 读取和提交。正常提交携带“我基于版本 12 生成”这样的预期版本。存储服务若认为已经过时，返回冲突；本次 Planner 记为 `stale_write_rejected`，不会换成另一个版本再自行拼接重写。该仓库调用 Axon 接口，实际底层表、事务及跨实例仲裁由 Axon 实现，不能从客户端代码推断出未读过的数据库保证。[状态读取与提交适配器](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/runtime.ts#L84) · [RPC 字段](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/axon/client.ts#L697)

下次读取返回的是“版本信息 + 状态文本 + 账目”，不是模型的一段隐藏思考。以下展示应用内部转换后的数据形态，`state` 内容只节选了两行用于阅读，不能拿这个节选当作满足 Director 校验的完整状态：

```json
{
  "found": true,
  "revision": 13,
  "sourceTurn": 8,
  "sourceMessageId": "assistant-turn-8",
  "historyRevision": 16,
  "state": "[Agentic RP State]\nCurrent scene: 旧城城门；用户已递出铜钥匙，表示明早再来。\nOpen callbacks, promises, items, injuries, and deadlines: 核对钥匙交接；用户表达了明早再来的意向。",
  "toolsCalled": ["manage_callbacks", "update_director_state"],
  "ledger": [
    {
      "type": "callback",
      "key": "manage_callbacks:turn:8",
      "status": "active",
      "payload": {
        "analysis": "区分用户递出、对方接收与双方同意会面，不能把未看到的回应补成事实。",
        "sourceTool": "manage_callbacks",
        "sourceTurn": 8,
        "userQuery": "我把铜钥匙递给莉娅，告诉她明早再来。"
      },
      "sourceTool": "manage_callbacks",
      "sourceTurn": 8
    }
  ]
}
```

程序会把 `state` 和选中的账目拼成供模型读取的文字，再放进相应消息；并非把整个数据库对象直接给模型。Actor 这段状态材料的编译上限为 `maxStateChars + 2000` 字符，后台常规调用使用 6,500 字符；最终仍分别受各自模型输入预算约束。

账目按 `event / npc / knowledge / world / callback / camera / agency` 分类。下一次编译上下文时，不会把所有历史账目无限堆给模型：代码选最近 60 个活动项，每类最多再取 6 条、每条内容最多 900 字符，最后受总字符上限约束。专家成功但未执行 Director、候选没变化、候选无效或提交冲突，都不能直接等同于这些专家结果已经成为下一轮的持续资料。[账目形成](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/agentic-v2.ts#L758) · [读回时的账目选择](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/memory/advisory.ts#L197)

首次没有状态时，程序给一份保守默认笔记：从角色开场和可见对话自然继续、不凭空确定未出现的人物、秘密和场外事件、保留用户自主决定。提供默认笔记不代表已写库；仍需后续完成有效状态提交。[初始状态](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/agentic-v2.ts#L470)

## 模型开始工作前，系统到底给了什么提示词

普通 V2 **没有一条写死在 Planner 文件内、覆盖所有角色的“主 Agent 完整 system prompt”**。当前服务从 Axon 获取各 ModelConfig 关联的 PromptManager，再按岗位名读取模板：

| 模型岗位 | 提示词来源 | 程序另加的业务材料 |
| --- | --- | --- |
| Actor | 本轮有效 Actor ModelConfig 的 PromptManager／Preset；按各 prompt 的角色和顺序展开 | 角色、用户、历史、接入的记忆／世界书、V2 持续状态 |
| Orchestrator | `orchestrator` 命名模板；升级模型可以有自己的版本 | 轮次、旧状态版本、角色、Persona、近期对话、本轮输入；结尾明确允许普通继续聊天不选工具 |
| 六位专家 | `specialist_analyze_plot` 等六个命名模板，各自从配置的 ModelConfig 读取 | 岗位共有材料加 `reason`／`focus` |
| Director | `director` 命名模板；加载时必须含明确的状态开头与栏目 | 旧状态、角色、历史、问题、更新理由、专家结果；要求生成完整替换文本 |

模型调用形式是“系统岗位提示词 + 用户角色的材料文本 + 本阶段工具定义”。运行时会展开语言和角色等模板变量。源码可以证实材料与工具是如何组织的；**具体环境里正在生效的整段岗位提示词，要从该环境 ModelConfig／PromptManager 读取**。本文没有把测试里的假提示词、某份旧 Demo 的模板或工具介绍冒充成生产 prompt。[模板加载与岗位名称](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/prompt-manager.ts#L21) · [后台首条业务材料](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/agentic-v2.ts#L538)

## 可配置什么，默认值意味着什么

这些是服务端场景配置，不是让最终用户随请求任意控制的参数。下表默认值来自构造函数和配置解析器，表示“未覆盖时的程序行为”，不是宣称生产每个场景都采用这些值。

| 配置 | 默认／范围 | 开启或修改以后 |
| --- | --- | --- |
| `agenticV2` | 缺省不启用；内部 `enabled:false` 也不启用 | 装配本文的后台；普通模式需不启用 `storyV1` |
| `modelConfigs.actor` | 必填，并必须等于该场景 `modelConfigRef` | 选择写正文的模型配置；同时需要有效 PromptManager／Preset |
| `modelConfigs.orchestrator`、`specialists`、`director` | 均需提供引用；六位专家分别配置 | 可以替换不同岗位模型及其 prompt；即使限制 enabledTools，当前加载仍要求六专家配置与模板齐全 |
| `enabledTools` | 默认六专家 + `update_director_state` | 限制 Orchestrator 可选工具；只能填当前七个名字。删掉 Director 后，专家咨询结果不会沿本文流程写成新状态 |
| `routingMode` | `manual` | 指固定编排模型，不是人工手动点选工具；`auto` 在达到阈值时可换升级模型重新作选择 |
| `escalationToolCallThreshold` | 3；可配 1–20 | 自动路由时模型请求工具数达到阈值会触发升级；需同时设置 `escalationModelConfigId` |
| `maxToolCalls` | 7；可配 1–20 | 参与第一阶段的选取预算；第一阶段实际执行上限同时受并行上限控制，Director 在后续独立处理。不能把 7 理解成整轮最多只有 7 次模型请求 |
| `maxParallelTools` | 6；可配 1–20 | 限制第一阶段并行专家选择；模型重试、编排判断和 Director 不等于这六个并行名额 |
| `maxStateChars` | 8,000；可配 800–8,000 | 限制完整持续状态候选长度；不是聊天历史长度或模型窗口大小 |
| `modelCallTimeoutMs` | 180,000 ms；可配 10,000–300,000 | 后台单次模型调用超时；不是用户一定要等待三分钟，也不是整个后台统一三分钟截止 |
| `contextGovernance` | 缺省不启用 | `shadow` 记录预算对照；`enforce` 严格约束并要求配置 Compact。V2 与 Compact 同时配置时必须显式指定该项 |
| `contextGovernance.safetyMarginTokens` | 开启治理时默认 256；可配 1–8,192 | 在模型可用预算内预留空间 |
| `contextGovernance.maxToolResultChars` | 开启治理时默认 5,000；可配 512–20,000 | 控制专家结果大小；严格模式过长则按失败结果处理 |
| `jevOrchestration.argumentModelConfigId` | 缺省不用 Jev | 可把两阶段工具选择交给 Jev 决策端点；专家和 Director 仍用文本模型，工具自由文本参数由指定文本模型补全 |
| `recovery.transientRetryModelConfigId` | 可选 | 提供暂时故障时的恢复模型；具体恢复策略由模型适配器处理 |
| `recovery.tolerateMissingStreamMetadata` | 仅显式 `true` 时启用 | 容许特定缺失流元信息情形；不是忽略一切模型错误 |
| `modelConfigs.autoReply`、`autoReplyWaitMs` | AutoReply 模型可选；等待默认 30,000 ms，范围 0–120,000 | 供自动回复建议的独立请求使用，不是控制普通后台每隔多少轮触发 |

配置解析还要求自动路由有升级模型；Jev 的参数模型不能填写 Orchestrator／升级 Orchestrator 的配置；Actor 引用要与场景一致。这些不匹配会在配置解析或依赖就绪检查时失败。[默认值](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/agentic-v2.ts#L220) · [配置范围与校验](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/server.ts#L114) · [V2 与 Compact 组合条件](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/server.ts#L808)

另外，仓库一些 JSON 留有 `actorControlEnabled`，但本基线的 `AgenticV2Config` 和解析返回值没有采用这个字段；它不能作为“关闭后台控制”的有效开关。前台模型选 Actor／图片／DIO 的工具循环由另一个配置 `preActorDirector` 控制，需在对应原子能力中说明，不能混进后台专家开关。[实际解析返回值](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/server.ts#L235) · [独立前台循环接线](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L957)

### 上下文窗口能不能改

可以改，但不在 `maxStateChars` 上改。Actor 的窗口上限来自有效 ModelConfig 的 `context_limit`，或服务接受的 `runtime_decision.context_limit`；模型的输出预算来自生成参数。历史实际保留多少还受 PromptManager 的历史插入规则、Compact 和最终预算计算影响。旧路径会保留系统消息，按剩余空间从较近消息向前选择；严格治理路径在最终请求上计数和检查。

后台又有自己的截取规则：最近 9 条可见消息、状态材料和专家结果长度限制。**只把 Actor 窗口调大，不会自动取消后台最近 9 条的规则。** 需要更长后台上下文时，应修改／扩展对应编译逻辑，或使用已实现的 Compact 严格接线；不能对客户承诺“换成长上下文模型就自动看完所有历史”。[Actor 窗口来源](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/integrations/roleplay-axon-adapter.ts#L439) · [旧 V2 补状态与滑动历史](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/runtime.ts#L498)

## 怎么接入这个完整产品

普通 V2 当前是 `apps/emochi` 下的应用实现，已经通过 Agent Router 服务接入；不能只安装 SDK 后调用一个未提供的 `createAgentV2()` 就获得上述全部行为。

接完整产品的路径是：**业务聊天服务 → 已配置的 Agent Router 场景 → Harness 应用服务 → Axon 资料／状态与 Kaon 模型 → 流式事件与结果返回业务**。接入方继续负责用户权限、额度、聊天消息保存、最终展示和自身的计费／通知；本应用承担本文明确接管的上下文准备和 V2 状态处理。

### 接入要准备的数据和服务

| 需要准备 | 为什么需要 | 缺少时会怎样 |
| --- | --- | --- |
| 用户 ID、会话 ID、角色／Prompt ID、请求 ID | 找到角色和会话资料，定位本轮 | 请求或资料查询无法正常执行 |
| 当前用户输入、此前可见消息及消息 ID | 当前回复和后台连续性判断的事实来源 | 无法恢复未传回的实际正文；部分校验会直接报错 |
| 预先分配的本轮 `assistant_message_id` | 关联本轮后台来源及状态保存 | 普通 V2 prepare 明确报错，不能省略 |
| 场景 ID 与对应服务配置 | 选择普通 V2、阶段模型和预算 | 未注册场景返回错误，不自动随意选模型 |
| Actor、Orchestrator、六专家、Director 的 ModelConfig 和 PromptManager | 取得实际模型、岗位提示词、生成参数 | 模型配置、岗位模板或 provider 不合要求时失败 |
| Axon | 角色资料、变量／模板展开、状态读取和条件保存等 | 完整应用当前不能脱离这些依赖直接运行 |
| Kaon 模型服务与凭证 | 执行正文和后台模型调用 | 没有可用路由就不能生成 |
| 后台任务生命周期与关停处理 | 后台在当前进程运行，应允许正常完成或处理关停 | 普通进程内 runner 不是可在重启后自动恢复的持久任务队列 |

**服务配置片段**如下，只说明字段之间的关系。占位 ModelConfig 需要由接入环境真正创建；这不是复制即可运行的完整配置，也不是公共 SDK options：

```json
{
  "upstreamModel": "<该场景允许的模型路由>",
  "defaultTimeoutMs": 580000,
  "modelConfigRef": "<actor-model-config>",
  "presetRef": "<actor-preset>",
  "agenticV2": {
    "modelConfigs": {
      "actor": "<actor-model-config>",
      "orchestrator": "<orchestrator-model-config>",
      "specialists": {
        "analyze_plot": "<plot-model-config>",
        "judge_npc": "<npc-model-config>",
        "audit_knowledge": "<knowledge-model-config>",
        "simulate_world": "<world-model-config>",
        "manage_callbacks": "<callback-model-config>",
        "direct_scene": "<scene-model-config>"
      },
      "director": "<director-model-config>"
    }
  }
}
```

**请求扩展片段**如下。它应放进 Agent Router 的完整请求中，外层还需要协议要求的项目、版本、会话、角色、消息及 `generation.n=1` 等字段。这里不把片段冒充为完整 HTTP 请求：

```json
{
  "extensions_version": "roleplay-harness/v2",
  "extensions": {
    "scenario_id": "<服务端已启用的普通-v2-场景>",
    "turn_context": {
      "user_id": "user-example",
      "model_alias": "<业务侧有效模型别名>",
      "assistant_message_id": "assistant-next-message",
      "language": "zh",
      "input_question": "我把铜钥匙递给莉娅，告诉她明早再来。"
    }
  }
}
```

实际服务使用 ConnectRPC AgentService／Agent Router 协议，不应把这些内部字段随意塞到普通 OpenAI `/chat/completions` 就认为能启用该产品。[请求扩展解析](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L482) · [完整服务接入说明](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/docs/integrations/chat-service-agent-router.md)

场景文件存在不等于场景启用：文件方案需要服务的 `HARNESS_ENABLED_SCENARIOS` 选中，旧配置表 `AGENT_ROUTER_SCENARIOS_JSON` 也仍然支持。文件与旧表同名时以旧表的整项定义为准。实际生效配置在服务启动时加载，不是每个请求自动重新读取文件。[场景启用与合并规则](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/docs/scenario-configuration.md)

## 失败、取消和并发时会发生什么

| 情况 | 当前行为 | 对业务接入的含义 |
| --- | --- | --- |
| Orchestrator 决定不用工具 | 本次后台结束，旧状态保留 | 正常的成本控制分支，不是漏跑后台 |
| 某位专家失败 | 返回失败结果，其他并行专家仍可完成；结果交给后续阶段 | 后续模型可能仍决定更新，不能把“有一位专家失败”说成整轮必定回滚 |
| Director 输出不合法 | 有限重试后仍失败则拒绝该候选，旧状态保留 | 文本回复可以已经成功；观察后台 outcome 才知道状态是否更新 |
| Director 输出与原状态相同 | 不创建新版本 | 后台执行过不代表写库过 |
| 状态提交发现旧版本过时 | 本次结果记为 `stale_write_rejected` | 不覆盖新状态，也不在这次后台自动换材料重跑 |
| 保存请求失败但是否写入不明确 | 有限重试；若后续出现冲突，会读回并对版本、来源回复 ID、状态全文核对是否其实已经成功 | 处理不明确结果，不能简单把重试次数当成功次数 |
| 用户取消／Actor 报错 | 已安排的后台没有绑定 Actor 请求取消信号，仍可能完成并尝试保存 | 若业务要求“正文已接受才允许状态生效”，需要额外接入接受机制；普通后台本身没有这个保证 |
| 下一句到来时上一轮后台仍运行 | 可以并发，以各自轮初材料工作 | 下一轮可能先用旧状态；不存在必然在下一句前完成的 SLA |
| 进程退出 | runner 依赖进程内任务与关停信号 | 不承诺重启后自动恢复这次后台 |

后台模型层有有限失败重试（通常原调用加两次），并对中止、上下文治理错误及明确不应重试的错误提前停止。整个后台任务此处未设置统一超时，但各模型调用有自己的超时；界面状态使用的 `timeoutSeconds:900` 是规划状态记录字段，不能当成后台运行器一定在 15 分钟停止的保证。[模型和保存重试](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/agentic-v2.ts#L102) · [保存 outcome](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/agentic-v2.ts#L957) · [后台任务生命周期](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/tasks.ts#L23)

## 二开时哪些可以直接改，哪些需要补接线

- **换模型、岗位提示词和预算**：使用现有配置入口；必须保持岗位模板、状态格式和模型 provider 等要求。
- **启用世界书或记忆来源**：使用对应应用已有配置和依赖接线；Actor 能读取不代表后台自动拥有同样资料，需要检查实际输入。
- **为前台增加“写正文／配图／长期修改”的选择**：使用独立前台工具循环配置及相关服务，不是在后台专家名单里加三个名字。
- **希望新人物成为独立可编辑 NPC 卡**：现有 `judge_npc` 产出分析，普通 V2 保存的是持续状态和分析账目；还需角色卡 schema、持久化工具、身份归属和读取接入，不能直接承诺已经完成。
- **希望后台能检索世界书、根据剧情改写设定**：需为该后台提供检索／写入能力、输入与权限策略，再定义哪些结果可以变成事实；当前六专家并不具有这套自动执行链。
- **希望根据用户是否接受本轮正文来提交状态**：需实现并接通明确的接受边界、来源版本和取消语义；不能只借用“快慢通道”名称便认为已有。
- **希望把这些模型工具用于别的业务**：原子能力可分别使用；普通 V2 的两阶段、强制 NPC 规则和状态格式属于本产品策略，不是公共 SDK 对每个接入方的硬要求。

## 代码入口与当前交付核对点

| 要定位的内容 | 固定源码位置 |
| --- | --- |
| 普通 V2 与 Story 如何分流、应用怎样装配 | [dispatch.ts 1160](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L1160) |
| 前后台材料、逐轮调度、Actor 完成与取消关系 | [planning/runtime.ts 350](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/runtime.ts#L350) |
| 工具选择、首轮强制规则、第二阶段、Director 和提交 | [planning/agentic-v2.ts 792](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/agentic-v2.ts#L792) |
| 实际岗位提示词从哪里加载 | [prompt-manager.ts 75](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/prompt-manager.ts#L75) |
| 六专家共有执行、输入和结果 | [memory/advisory.ts 218](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/memory/advisory.ts#L218) |
| 配置字段是否真的被解析 | [server.ts 114](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/server.ts#L114) |

源码已经具备普通 V2 应用与可选 Jev 编排适配；仓库场景中未默认开启 Jev。`agentic-v2-luna-gemini38-pov-v1` 名字带 V2，但文件启用了 Story，不作为本文普通后台的默认配置。Pioneer 等开发分支也不自动进入本文主干行为。对具体交付环境，还需取得有效场景、运行镜像、各岗位 ModelConfig／PromptManager、真实状态读写与取消／并发结果，才能形成“该产品此环境当前运行什么”的交付记录。[Jev 接线](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L934) · [Story 场景文件](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/config/scenarios/agentic-v2-luna-gemini38-pov-v1.json)



<a id="product-story"></a>

# Story｜Branches 的互动剧情产品

Story 让角色回复随着用户选择推进故事：保留当前场景，安排已经埋下的场外事件，提供这一段适合推进的剧情目标，再根据聊天里真正保存的正文更新世界和剧情进度。用户可以继续当前事件、回应邀请、改变追求；程序会检查改变是否有已有事件和正文证据支持。

业务归属为 **Branches**；实现位于 `roleplay-harness / main`。本章以 `fa41d220d48327e58d994f936517b23f2f15f658` 固定源码为准。已确认应用路由、持久化接线及仓库配置样例；线上正在启用的场景、模型与流量应以部署配置为准。

**产品类型：程序协调的完整剧情运行流程，内部组合六个 Story 工具与正文生成模型。** `storyV1` 启用后使用自己的协调器；沿用 Actor 的角色回复与模型恢复能力，但不会再同时运行普通 Agent V2 的 Orchestrator、六位咨询专家和 Director 后台。这一点直接由路由中的 `if (storyV1) … else if (agenticV2)` 决定。[应用接线](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L1161)

## 1. 用户实际得到什么

以“玩家和守卫莉娅在城门核对铜钥匙”为例：

- 用户说“我把铜钥匙递给莉娅，但先不进城”，正文可以写莉娅核对钥匙、提出下一步，不能代替用户决定进入城内。
- 先前已确立的“巡逻队即将返回”可以在后续产生影响；程序控制它何时只是远处动静，何时进入当前场景。
- 剧情目标可以是“让钥匙上的旧徽记得到辨认”，但目标被放进模型输入不等于已经完成。只有后来保存的正文实际表现了辨认结果，才能记入进度。
- 用户持续改为追查失踪商队时，主线可以调整；已经发生的事实和未完成承诺需要继续承接。

正文仍是用户日常看到的回复。世界快照、剧情目标、计数器和结算证据由程序保存与使用，不要求用户阅读。可选的 Auto Reply 入口提供三条“用户下一句可以怎么说”的建议，建议本身不等于用户已经作出选择。[正文约束](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/story/world.ts#L39) · [建议回复输入与输出](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/auto-reply.ts#L47)

## 2. 一次请求怎样运行

```text
新请求：本轮输入 + 带消息 ID 的历史 + 角色与会话身份
  ↓
读取该用户 / 会话 / Story 场景的已保存状态
  ↓
有上轮等待确认的正文？
  ├─ 有且证据匹配：从聊天服务读取正式保存正文，先结算上轮
  └─ 首次或历史已改变：按真实角色资料与可见历史初始化 / 重建
  ↓
理解本轮行动 → 推进场景规则 → 必要时规划剧情目标
  ↓
选择本轮可推进的目标，组成“场景材料 + 剧情指导”
  ↓
Actor 结合原有角色提示词与聊天材料，生成本轮正文
  ↓
Harness 成功后：CAS 保存准备态和本轮消息 ID，等待正文被业务保存
  ↓
下一次正常请求：读回正式正文，再确认这轮实际发生了什么
```

**本轮正文开始前会等待 Story 准备完成。** 初始化、上轮世界结算、行动理解以及必要的剧情规划都可能发生在这里。它不是“先无等待地回复，然后所有剧情工作都扔到后台”。本轮生成完成后的 `settle()` 主要保存准备态；真正采用正文的世界结算发生在下一次正常请求的准备阶段。[准备顺序](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/coordinator.ts#L156) · [生成后保存](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/coordinator.ts#L390)

## 3. 三轮完整实例：每一步拿什么、产出什么、何时保存

以下是说明机制的示例内容，名字、台词和剧情目标不是固定生成结果。消息 ID 用 `u1 / a1` 等表示；实际由业务系统分配。

### 第一轮：用户把钥匙递给守卫

**用户输入 u1：**“我把铜钥匙递给莉娅，但先不进城。”

1. **读原始材料。** 程序从 Axon 读取角色卡、角色 Prompt、对话长度；从请求取本轮输入、带 ID 的历史。角色材料可能包括“莉娅负责城门登记”“不得泄露内城秘密”；开场消息交代两人站在城门。没有已有 Story 状态时，初始化模型根据这些材料建立世界与叙事快照。
2. **得到内部状态。** 世界前景可以是“莉娅正在核验用户提供的钥匙”；主线可以是“查明钥匙对应的旧城设施”，支线可以是“建立与莉娅的合作”。初始化不会把未来目标写成已经发生的事实。
3. **理解用户行动。** `pov_prepare_turn` 对自由文本调用行动分类模型。这个例子仍留在当前事件，可返回 `normal`；“先不进城”不授权程序切换场景。
4. **安排可推进目标。** 新叙事快照 `needPlan=true`，程序调用 `narrative_plan_cycle` 创建一组目标，再调用无需模型的 `narrative_deal_beat` 选本轮适用目标。例如当前目标为“让旧徽记得到辨认”。
5. **Actor 获得实际材料。** 原有角色提示词、历史、本轮输入之外，再增加系统消息 `[CURRENT STORY STAGE]`，其 JSON 包含 `stage` 和 `narrative`：当前事件、阶段、应承接的用户动作、可见的场外线索、当前剧情目标、主副线方向与情绪指导。完整隐藏世界状态不会原样交给 Actor。
6. **用户看到 a1。** 例如：“莉娅接过钥匙，指尖停在柄端的旧徽记上。‘这是旧档案馆的标记。你可以先在门外等，我去核对登记。’”
7. **此时保存什么。** Harness 成功后，Story 保存世界准备态、叙事快照、`pendingActor.messageId=a1`、u1 的 ID 与内容哈希、当时给 Actor 的场景投影。状态报告为 `waiting_for_acceptance`。这里没有把 `result.output.text` 直接判定为权威剧情事实，也不把整篇正文复制进 Story 状态；正文由业务方保存到聊天消息中。

[角色与请求材料](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/axon-adapter.ts#L30) · [初始化提示词](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/initializer.ts#L39) · [Actor 新增输入](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/coordinator.ts#L247) · [保存内容](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/coordinator.ts#L405)

### 第二轮：先采用上一句，再处理新要求

**用户输入 u2：**“不用去核对了，先把钥匙还我。”

1. **读回上轮正式正文。** 程序检查历史中最近的 assistant 是否对应 a1，通过 `conversationMessage` 按用户、会话、消息 ID 读取业务实际保存的 a1；核对上轮用户输入 u1 和更早已接受正文的锚点。
2. **结算 a1。** `pov_advance_world` 获得保存的 a1 完整正文、u1、当时准备态及截至 a1 的材料。u2 的“还我钥匙”不会被混进对 a1 的结算里。若下游把 a1 改成“莉娅只看了一眼，没有接过钥匙”，结算使用改后的正式正文。
3. **更新世界候选。** 记录已经表现的结果、尚未完成的场外事务、已传达的邀请等，增加世界轮数；把 a1 的消息引用与用户行动引用加入当前剧情事件的证据集合。只有事件结束或被打断时，才调用 `narrative_settle_event` 判断剧情目标和主副线进度，不是每句话都重算完整事件。
4. **处理 u2。** 再用新世界和 u2 准备本轮场景。“用户要求归还钥匙”成为应回应的动作，而不是上一轮已经归还。
5. **用户看到 a2。** 例如：“莉娅停下脚步，把钥匙递回给你。‘好，我们就在这里谈。’”
6. **保存第二轮准备态。** a2 正文生成成功后，一次 CAS 保存“已采用 a1 后的状态 + 为 a2 准备的状态”。`acceptedMessage` 指向 a1，`pendingActor` 改为 a2。若第二轮 Actor 失败，前面算出的 a1 结算仍只在本次内存中，尚未独立持久化；下次可以从旧准备态重新恢复。

[采用与切分证据](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/coordinator.ts#L190) · [世界采用与事件证据](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/coordinator.ts#L365) · [对应回归测试](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/test/story-deferred-review.test.ts#L68)

### 第三轮：结果怎样真正用于后续

**用户输入 u3：**“我留在原地，问她档案馆为什么被封了。”

程序先读正式保存的 a2，采用其中“归还钥匙”等事实，再准备 u3。Actor 获得的当前事件与写作要求已经承接这些事实；它也会读取当前请求提供的聊天历史。若此前的事件在此时结束或被用户改道打断，事件结算模型读取这个事件累计的完整已接受正文和用户行动，判断哪些目标完成、主副线各自推进到哪里、是否需要更换方向。

这里的“承接”不代表系统自动生成一个通用道具数据库。Story 快照没有内置“铜钥匙持有人”标准字段；此类具体事实首先保留在正式聊天正文中，并进入后续可见历史和故事材料。若业务要可查询的物品栏或 NPC 小卡，需要另外接对应数据能力及写入规则。

例如“确认徽记来源”已有正文证据，就可以完成；“进入档案馆”从未发生，就不能因为原规划里有这项目标而被标记完成。新的主副线进度与下一批目标会参与后续发放；这才是准备结果持续影响正文的路径。[事件判定与后续动作](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/story/narrative.ts#L182)

## 4. 内部有哪些工作，分别由谁决定

| 工作 | 谁决定何时执行 | 输入 | 输出及用途 |
| --- | --- | --- | --- |
| 初始化世界与故事方向 | Story 协调器：首次没有状态，或历史变化需要重建 | 角色定义、开场、近期可见历史、本轮输入 | 世界快照与主副线；给后续工具使用 |
| 准备当前场景 `PovPrepareTurnTool` | 每次可正常推进的 Story 请求 | 上轮已结算世界、本轮行动、故事材料 | 世界准备态、行动意图、时钟变化、当前可见场景 |
| 制订一批剧情目标 `NarrativePlanCycleTool` | `cycle.needPlan=true`，且没有未解决事件阻塞 | 主副线、已有进度、欠缺回报、旧目标、设定 | 新增 5–7 个目标，保留未完成目标；不生成正文 |
| 选本轮目标 `NarrativeDealBeatTool` | 每轮准备，且无未解决事件阻塞 | 叙事快照、当前事件 start/develop/end | 一个目标及情绪和方向指导；纯程序计算 |
| 写本轮正文 Actor | 上述准备完成后由 Harness 调用 | 原角色回复材料 + 场景与剧情指导 | 用户可见回复 |
| 采用正文并推进世界 `PovAdvanceWorldTool` | 下一正常请求核对上轮正式保存正文后 | 对应准备态、完整正文、上轮用户动作、当时材料 | 世界结算候选；确认本轮表现与场外后果 |
| 结算一个事件 `NarrativeSettleEventTool` | 当前事件结束或被改道打断；或重试待结算事件 | 同一事件累计的已接受正文、用户行动、叙事快照 | 目标完成情况、主副线进度、情绪观察、下一步指令 |
| 调整剧情线 `NarrativeRebranchTool` | 协调器执行上一步返回的 `rebranch` | 当前主副线、已确认用户新方向或长期未推进支线 | 新方向与承接说明；保留已发生事实和欠账 |
| 持久化 | 本轮 Harness 成功后由 Story 协调器调用 Axon CAS | 完整 Story 状态与消息身份 | 下轮能读回的状态版本 |

Story 的工具调用顺序由程序条件控制，不是先把这六个名字交给通用主 Agent 让它任意循环选择。模型参与行动解释、剧情目标、世界语义和事件判定；计时、容量、去重、合法字段和持久化由程序负责。`NarrativePlanCycleTool` 里的 plan 指“一批剧情目标”，不意味着整段对话采用预写固定剧本。[工具调用位置](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/coordinator.ts#L247) · [剧情规划规则](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/story/narrative-prompts.ts#L1)

## 5. 世界与剧情到底记录什么

### 世界记录：当前发生什么，场外还有什么

| 记录 | 业务含义 | 例子 | 怎样进入正文 |
| --- | --- | --- | --- |
| `foreground` 当前事件 | 用户所在场景中正在处理的具体事情 | 莉娅核对铜钥匙 | 每轮作为当前场景；start/develop/end 约束展开节奏 |
| `backgrounds` 场外事务 | 已确立、之后可能影响当前场景的独立事务 | 巡逻队正在返回城门 | 倒计时到期后按当前场景阶段延后、加入障碍或接过场景 |
| `undercurrents` 隐藏发展 | 尚未完整进入用户视野的变化 | 某个既有敌对组织正在追查钥匙 | 先只表现氛围或迹象，随后影响条件或给出可拒绝邀请；不自动变成角色已知事实 |
| `seedPool / seedQueue` 未来可能事件 | 与已有世界一致的后续素材 | 既有商队可能送来城外消息 | 规则允许时进入隐藏事件；不能作为已发生事实 |
| `pendingSignalDemand` 待传达邀请 | 需要让用户实际收到的线索邀请 | 信使请用户查看某处 | Actor 要实际表现“邀请已到达”，用户仍可拒绝；结算确认传达后才更新记录 |
| `pendingInjections / pendingNextForeground` 后续结果与下一事件 | 已有机制产生的场外后果、已结束事件之后的候选场景 | 巡逻队带回报告；准备转向核对档案 | 进入后续的场景材料，以观察或通信方式呈现 |

Actor 只获得 `stage` 投影：当前事件、应承接动作、选中的场外影响、最多预算内的隐藏线索及待传达邀请。全部暗线、未来种子和计数器不直接作为角色知识。主角色以实际角色名作为受保护 owner，不能被初始化为隐蔽暗流或种子；伴随角色是否跟随用户转场，必要时另由模型判断。[世界结构](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/story/world-types.ts#L18) · [投影选择](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/story/world-mechanics.ts#L246) · [角色保护](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/initializer.ts#L90)

### 剧情记录：为什么推进，进展到了哪里

- `main / sub`：主副线各保存主题、方向、预期体验、尚未兑现的回报、当前进度 `cursor`，以及连续多少个已结算事件没有推进 `starve`。
- `cycle.pending`：当前要尝试推进的目标。每项注明服务主线还是支线、属于人物/关系/冲突/揭示/回报、前置目标、是否完成和优先级。回报类目标必须有明确铺垫依赖。
- `emotion`：目标与已观察的情绪轨迹。三个轴分别描述好坏感受、平静或紧张、视角方掌握主动权的程度；供后续节奏调整，不是修改用户真实情绪。
- `clock`：已结算事件 ID、已完成目标、最近用户方向、本轮已发放内容等，负责恢复与去重。

`narrative_settle_event` 的模型输出必须区分“目标完成”和“剧情线推进”。两者可以不同；每个完成目标和发生移动的进度游标都需要正文证据。[叙事结构](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/story/narrative-state.ts#L1) · [结算输出约束](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/story/narrative-prompts.ts#L17)

## 6. 默认什么时候触发，哪些参数能改

### 产品接入配置 `storyV1`

| 字段 | 默认与范围 | 影响 |
| --- | --- | --- |
| `enabled` | 对象存在且未设 false 时启用；整个 `storyV1` 不提供则关闭 | 是否进入 Story 专属流程 |
| `modelConfigId` | 必填非空字符串 | 行动解释、世界与叙事工具使用的模型配置 |
| `initializerModelConfigId` | 可省略，使用 `modelConfigId` | 可单独指定初始化模型 |
| `modelCallTimeoutMs` | 45,000 ms；允许 1,000–180,000 | Story JSON 调用的时间预算；修复尝试共享这次工具调用的超时信号 |
| `postTurnTimeoutMs` | 90,000 ms；允许 1,000–180,000 | 本轮 Actor 完成后保存准备态的时间预算，不是“下一轮世界结算最多 90 秒”的开关 |
| `maxStateBytes` | 60,000 UTF-8 字节；允许 4,000–60,000 | Story envelope 保存上限；超出会失败，不静默剪掉证据 |
| `seed` | 731；允许 0–2³²−1 | 机械随机过程的初始种子，随快照保存与恢复 |

这些是应用配置实际接受的字段；未知字段会报错。[配置解析](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/config.ts#L11)

### 内部默认规则：不等于都有业务配置开关

| 规则 | 默认行为 | 可修改的位置 |
| --- | --- | --- |
| 前景事件节奏 | start 后进入 develop；`developMax=2` 控制发展阶段；end 后等待合法后继事件 | 独立世界工具通过初始 snapshot 的 `policy`；现有 Story 场景配置未暴露 |
| 场外容量 | background 最多 3；undercurrent 最多 5、目标下限 2；每轮选最多 2 条暗流进舞台 | 同上；缺少已存在种子时不会为了凑下限凭空制造事实 |
| 暗流发展 | 各阶段按确定性骰子和阈值推进；`maxStageRounds=6`；阶段 3 的邀请未送达时暂停进一步推进 | 世界 `policy`；不能解释为每 6 轮必有新剧情 |
| 新剧情目标 | 每次规划新增 5–7 项，至少一项服务副线；保留未完成项 | 独立 narrative 工具构造参数 `options.policy` |
| 发放目标 | 每个正常准备轮都计算；`dealEveryTurns=3` 参与强制轮换判定 | narrative `policy`；不是三轮才启动 Story |
| 周期进阶 | 已完成目标达到当轮 `ceil(总数×2/3)` 门槛后，先给晋级考验；完成或等待 2 个已结算事件后推进周期 | narrative `policy` |
| 主线改向 | 连续 3 个已结算事件出现同一个明确的新追求，才请求主线重定向 | narrative `policy.branchConsecutiveEvents` |
| 支线长期无进展 | 连续 5 个已结算事件未推进，可更换支线；主线 3 个未推进先记录诊断，不擅自改用户追求 | narrative `policy` |

应用协调器当前直接使用工具默认世界和叙事策略，只在初始化覆盖主角色保护与随机种子。要把这些规则做成客户可配置字段，需要在宿主新增配置接线；不能往现有 `storyV1` 对象里直接塞 `dealEveryTurns` 等字段。[世界默认值](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/story/world-state.ts#L16) · [世界规则](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/story/world-mechanics.ts#L153) · [叙事默认值](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/story/narrative-state.ts#L93) · [主副线改向触发](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/story/narrative-state.ts#L257)

## 7. 模型上下文如何组装，窗口在哪里控制

Story 不把一次请求的全部材料原样交给所有模型。

| 使用者 | 实际材料 | 输出 |
| --- | --- | --- |
| 初始化模型 | 实际角色名；定义前 12,000 字符；开场前 3,000；最近 18 条 user/assistant 各前 3,000；当前输入；主角色保护规则 | 世界初始事件、场外事务、暗流、种子、主副线 |
| 行动解释模型 | 当前输入、当前事件、可转向的背景和阶段 2–3 暗流、上轮实际传达的邀请、同伴身份、故事材料 | normal/cue/signal，必要时目标 ID 与同伴邀请 |
| 规划与重定向模型 | 叙事快照 + 同一份裁切后的故事材料；明确任务提示词 | 新目标或主副线方向；不直接写正文 |
| Actor 正文模型 | 原业务的角色与聊天输入 + 不可压缩系统消息中的 stage/narrative | 角色正文 |
| 世界结算模型 | 已恢复且校验的准备态、规则标记、骰子、对应用户动作、已接受完整正文、截至该正文的故事材料 | 世界变化提案；程序检查合法 ID、阶段、容量与可写范围 |
| 事件结算模型 | 叙事快照、整个事件已接受正文拼接、对应用户行动、是否被打断 | 有证据的目标完成、剧情进度和情绪观察 |

12,000 / 3,000 / 18 是当前 `compileStorySource` 的代码裁切值，不是统一模型 token 窗口，也不是当前提供的场景配置项。事件正式正文按消息 ID 另行取回，结算并不使用裁短的历史片段替代完整正文。Actor 的 token 窗口与截断/压缩仍由原 Actor 模型配置和基础上下文准备处理；Story 新增系统材料明确通过 `nonCompressibleSystemMessages` 注入，会占用同一个可用窗口。[材料裁切](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/initializer.ts#L23) · [Actor 注入方式](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/axon-adapter.ts#L87) · [完整事件证据读取](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/coordinator.ts#L286)

**世界书的边界也在这里：** 当前 Scenario 校验不允许 `storyV1` 与 `worldbook` 同开；世界书现成接线只支持专用 V2 场景。StorySource 也没有“本轮命中的世界书原文”专用字段，Story 协调器准备发生在基础 Actor 上下文准备之前。若二开给 Actor 接入世界书，仍需单独把必要条目传给 Story 的规划、事件结算模型，并保留条目与版本依据；只把条目放进 Actor 输入不够。[配置限制](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/server.ts#L793) · [准备调用顺序](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/axon-adapter.ts#L70)

## 8. 接入完整 Story 产品，需要准备什么

### 服务端装配

1. 使用仓库已有闭环 Agent Router；Story 要求扩展协议 `roleplay-harness/v2`。
2. 为 Story 设置独立 `scenario_id`，配置角色 Actor 的 `modelConfigRef`、`presetRef`、显式 `agenticV2` Actor/recovery 配置以及 `storyV1`。
3. 接好 Axon：角色卡、角色 Prompt、对话长度、按消息 ID 读取正式正文、角色扮演状态读取与 CAS 写入。接好模型运行适配器，能解析 Story 及 Actor 的 ModelConfig。
4. 业务方保存正式聊天消息。下一次请求传入的历史身份应指向真正保留的消息；否则 Story 无法沿用上次待确认状态。
5. Actor 的输入预设必须以当前 user 消息结束；Story adapter 会检查。

仓库提供完整配置样例 `config/scenarios/agentic-v2-luna-gemini38-pov-v1.json`。其中保留了 `agenticV2.modelConfigs.orchestrator / specialists / director` 等字段，但路由不会因为字段存在就执行普通 V2 后台。该样例的 580,000 ms 请求超时和具体模型名称是这个样例的配置，不是所有 Story 接入的固定值。[配置样例](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/config/scenarios/agentic-v2-luna-gemini38-pov-v1.json#L1) · [配置互斥检查](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/server.ts#L761)

以下是放在完整场景配置里的 **Story 片段**，不是独立可提交的完整配置：

```json
{
  "storyV1": {
    "enabled": true,
    "modelConfigId": "your-story-model-config",
    "initializerModelConfigId": "your-story-initializer-config",
    "modelCallTimeoutMs": 45000,
    "postTurnTimeoutMs": 90000,
    "maxStateBytes": 60000,
    "seed": 731
  }
}
```

### 每次业务请求中与 Story 相关的身份

| 字段 / 材料 | 谁提供 | 为什么需要 |
| --- | --- | --- |
| `requestId`、`conversationId`、`promptId`、模型配置身份 | 调用方按 Router 协议提供 | 定位本轮、会话、角色和生成配置 |
| `extensions.scenario_id` | 业务选择已装配的 Story 场景 | 确定启用哪套产品流程与状态作用域 |
| `extensions.turn_context.user_id / model_alias` | 经业务认证的上下文 | 用户作用域及模型入口；闭环请求必需 |
| `extensions.turn_context.assistant_message_id` | 业务预分配本轮将保存的回复 ID | Story 把它作为本轮准备态、下轮接受正文的身份 |
| `extensions.turn_context.input_question` | 业务传入的当前输入 | 覆盖缺省的请求输入来源；Continue 也要有实际继续指令 |
| 带消息 ID 的 user/assistant 历史 | 业务会话服务 | 对齐最后保存的正文、原用户动作、历史版本 |
| `generation.n=1` | 调用方 | 当前 Router 一次仅支持一个生成候选 |

字段名称须按所用 Router SDK/协议序列化；上表的 `requestId` 等是 TypeScript 接收字段，扩展对象内使用代码列出的下划线名称。[请求校验](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L300) · [闭环扩展解析](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L482)

### 与其他能力的组合边界

- **不能同时配置 `preActorDirector`。** 当前 Story 产品使用自己的闭环 Actor 路径，不兼容“前台主控自主选择 Actor/图片/DIO”的另一套协调器。
- **不能同时配置 `sumiV1`。** Sumi 由 Router 提前分流到独立图文应用，不能当作 Story 默认内置的一组工具。
- **普通咨询专家不是 Story 默认调用项。** 如果二开需要调用某个专家补充资料，可以复用原子能力，但需要明确调用时机与结果如何进入 Story，现有应用没有自动加这一层。
- **不能直接与 `worldbook` 同开。** 当前配置校验拒绝该组合；需要世界书时须扩展适配，明确哪些条目进入 Actor，哪些进入 Story 模型。
- **Compact、记忆等需按各自实际接线处理。** Story 的主副线不是用户长期记忆；不能以“配置了 Story”替代这些模块的输入、保存与启用条件。

[互斥条件](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/server.ts#L761) · [Sumi 独立入口](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L759)

## 9. 保存、重生成、失败与恢复

Story 状态通过 `AxonAgenticStateRuntime` 读写，作用域为 **userId + conversationId + scenarioId**。状态中有 `promptId`，读取时要求仍属于同一角色。CAS 使用读取时的 `expectedRevision` 与来源位置；旧请求不能覆盖新版本。[读写适配器](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/runtime.ts#L86)

需要分清三种进度：会话来源 `sourceTurn` 在该应用中按消息位置推进；`world.round` 在成功采用正文后推进；`narrative.clock.eventCount` 在一个剧情事件有证据地结算后推进。它们不是同一个“聊天第 X 轮”。

| 情况 | 当前代码行为 | 对产品的影响 |
| --- | --- | --- |
| 正常新消息 | 先采用上轮权威正文，再准备当前输入；当前 Actor 成功后 CAS 保存 | 后续上下文承接实际保存的故事 |
| Continue | 使用会话消息位置推进，并把继续指令作为合成行动 | 不因用户消息条数没增加而卡住；与真人用户动作的证据类型区分 |
| Regenerate / edit_regenerate | 使用继承 Actor 回复；不运行 Story 准备、模型调用或状态写入 | 当前重生成没有 Story 新增舞台指导；下次普通请求再核对保留下来的历史 |
| 上轮消息 ID 对不上、历史被改或截断 | 普通前进请求根据可见历史重新初始化；不是沿用旧事实硬接 | 业务应尽量提供稳定历史与正式消息读取 |
| 历史回退到已存来源水位之前 | Actor 继续回复，Story 只读不写 | 当前存储不支持把旧来源当成新版本替换 |
| 初始化暂时不可用或两次 JSON 均不合格 | 可恢复情况退回原 Actor，本轮不写 Story；记录诊断 | 可有回复，但没有新增 Story 指导与进度；权限/程序错误等不被统一吞掉 |
| 上轮世界结算没有有效结果 | 保留旧 prepared；不继续准备新 Story 轮 | 当前正常请求会失败并等待恢复，不能伪造“无变化”然后推进 |
| 事件结算未知，或旧事件证据丢失/变化 | 保留引用并跨请求重试；未解决期间暂停新增剧情发放 | 世界准备可以继续，但不能假设该事件目标完成 |
| 未解决事件达到 3 个不同来源位置上的尝试，或距首次尝试达到 12 个来源位置 | 生成 `story_event_quarantine`，与队列移除一起 CAS 保存 | 留给人工恢复；没有假装事件已完成。来源位置不是墙钟时长，也不是 world.round |
| Actor 失败或被取消 | 不保存本轮 Story 候选 | 前面模型调用可能已有成本，已保存状态保持旧版本 |
| 保存冲突或状态超限 | report 标记 `stale_write_rejected` 或 `failed` | 已生成正文不因这个报告自动撤销；业务需要观察保存结果，不能把“收到正文”当“Story 保存成功” |

Story 状态保存上限默认 60,000 UTF-8 字节，事件证据采用消息 ID 和哈希，结算时再取完整文本。隔离记录单项上限 64 KiB；代码注明 Axon ledger 有 60 条、512 KiB 的保留界限，因此不是永久审计档案。[保存与报告](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/coordinator.ts#L390) · [恢复分支](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/axon-adapter.ts#L52) · [事件隔离](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/event-recovery.ts#L6)

## 10. 可以拿什么二开

完整 Story 应用已经接入角色、模型、聊天读取和状态服务，适合沿用现有 Router/Actor 服务体系的产品。要在其他宿主里只使用剧情机制，可以从 `@flowgpt/agent-core-tools/story` 导入六个工具、快照创建与解析函数；它们接受输入并返回新快照，没有内置业务数据库，也不会自己把其他工具串起来。

二开方可控制世界策略、叙事策略、Narrative 模型提示词、模型实现、消息接受边界及保存机制。现有应用的“下一请求才采用上一正文”是一种具体宿主策略；自己的产品如果能在同一请求明确取得最终被接受正文，可以在当轮调用推进/结算并保存。两种接法不能混写成同一份流程承诺。[公开导出](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/story.ts#L1) · [独立组合示例](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/examples/sdk/04-story-turn.ts#L7)

## 11. 接入验收看什么

- 连续两次正常请求后，能看到上次 `pendingActor` 被正式保存正文采用，新请求的 `pendingActor` 接替；读到的正文来自正确会话和消息 ID。
- 下游实际保存正文与 Harness 原始结果不同，下一次结算依据保存后的版本；下一次用户的新要求不能反向污染上轮事实。
- 看 `story_turn_completed` 的 outcome、状态版本、工具调用列表、诊断与 prepare/settle 耗时，区分正文成功与状态成功。
- 检查重生成、Continue、删改历史、Actor 失败、世界模型失败及 CAS 冲突时是否符合上表。
- 工具机制测试与真实模型体验分开验收；固定断言可以证明保存顺序与隔离，不能证明实际模型总能合理推进剧情。

本次已阅读源码，并运行固定快照自带编译产物的四组既有测试：`story-deferred-review`、`story-scenario`、`story-scenario-dispatch`、`story-settlement-recovery`，38 项通过。这不是生产验证，也没有重新编译或修改产品源码。[关键测试源](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/test/story-deferred-review.test.ts#L68)



<a id="atoms-index"></a>

# 原子能力：可以拿什么做二次开发

这里按“要完成的事情”找能力。每项都会说明它是独立包、服务接口、应用内模块，还是配置入口。**下载仓库、安装 SDK、接入已经部署的业务服务，是三种不同的接入动作。**安装 SDK 不会自动连到公司的模型、配置中心、世界书或数据库。

## 能力目录

| 要完成什么 | 能力与入口 | 现成提供的形态 | 接入者还要准备什么 |
| --- | --- | --- | --- |
| 写角色正文，或按段落配合图片 | [Actor 与前台工具循环](#atoms-generation) | `apps/emochi` 内的正文运行时与工具接线 | 角色、历史、模型配置、Prompt 与 Axon 等应用依赖；不是 Core Tools 根入口导出的独立 Actor |
| 分析剧情、人物、知识、时间、伏笔、场面 | [六类分析工具](#atoms-specialists) | `@flowgpt/agent-core-tools` 公共导出 | 提供真实材料、系统提示词和模型适配器；自己决定如何采用建议 |
| 让世界事件与剧情计划运转 | [Story 工具](#atoms-story) | `@flowgpt/agent-core-tools/story` 子入口 | 世界/叙事状态、事件、模型与存储接法；完整产品的正文确认流程需要宿主编排 |
| 导入设定、选出本轮需要的内容 | [世界书服务](#atoms-worldbook) | 独立 REST 服务＋仓内 Axon 适配；现成接线限专用 `agentic-v2-worldbook` 场景 | 租户与业务授权、书籍版本、索引、Prompt 关联、模型注入位置；Story、Sumi、前台工具循环当前不能直接同开，需另写适配 |
| 读写 JSON 记忆，或接入日记、记忆表及外部记忆 | [记忆与状态](#atoms-memory) | `/memory` 子入口的 `get_memory`、`update_memory`；另有应用内 Axon / Hinos 适配 | JSON 工具需要绑定身份的存储、原子版本检查和字段校验；应用接法另需对应服务、来源配置及模板引用 |
| 在长对话里压缩历史、控制输入长度 | [历史压缩 Compact](#atoms-compact) | 当前为应用运行时与 Scenario 配置 | 原始历史及消息锚点、计数服务、压缩模型、检查点持久化 |
| 把已取回的资料放到模型输入中 | [上下文组装](#atoms-context) | SDK `placeContextBlocks`；分析工具自定义编译器 | 自己取资料、定义插入位置、控制预算；组装函数不检索也不保存 |
| 生成独立图片，或给聊天正文配图 | [图片与视觉状态](#atoms-images) | `/image` 资产工具；另有应用内 Imagine 与图片 MCP 接线 | 图片后端、任务归属、审核结果、展示与保存；两种生图入口参数不同 |
| 持续改变角色后续行为 | [DIO 长期指令](#atoms-instructions) | 应用内外部服务适配器与前台 `schedule_dio` 动作 | DIO 服务、授权、目标 Prompt、后续有效 Prompt 的读取链 |
| 做自己的 Agent、工具循环、快慢协作，或固定串联工具 | [编排与扩展](#atoms-runtime) | SDK `Agent` / `SlowTurnRuntime` / `FastSlowRuntime` 与 Tool Hook；另有仓内 Profile＋Planner＋Executor 固定流程 | 自己的提示词、工具执行器、输入采用与保存规则；固定流程需仓内组装，当前 Router 的 Scenario JSON 不直接装载任意节点图 |
| 生成用户可以选择的下一句话 | [下一句建议](#atoms-auto-reply) | V2 应用 `auto_reply` 请求入口 | 已保存的助手消息、独立模型配置、业务前端的选择与发送动作 |

## 包、源码与服务怎样对应

| 入口 | 这次固定源码中的版本或位置 | 使用方式 |
| --- | --- | --- |
| SDK | `@flowgpt/roleplay-harness`，包配置版本 `0.3.0` | 在调用方进程执行。提供通用 Agent、可选快慢运行时、注册表、上下文放置、观测等 |
| Core Tools | `@flowgpt/agent-core-tools`，包配置版本 `0.2.1` | 可直接调用分析工具；`/memory`、`/story`、`/image`、`/pi` 按子入口导入 |
| Agent V2 / Story 应用实现 | `apps/emochi`＋`adapters` | 仓内应用接线，需相应业务服务和配置。两个 npm 包没有整体导出这套产品 |
| Worldbook | `worldbook-service` 独立仓库 | 部署服务或接入已授权服务地址，经 REST / 业务适配调用 |

两个包配置的发布目标都是 `https://npm.pkg.github.com`，访问级别 `restricted`，要求 Node.js ≥22。实际安装先取得 registry 读取权限，再确认要用的功能已经进入所安装版本。**主干中新增的 Slow gate 不应仅凭 `package.json` 仍写 0.3.0 就认定 0.3.0 发布包包含它。**本页按固定源码说明能力；交付具体发布包时须对照该包版本与构建身份。

[仓库、包与应用的归属](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/README.md#L3) · [SDK 包入口](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/package.json#L1) · [Core Tools 子入口与安装条件](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/package.json#L1) · [SDK 真实导出](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/index.ts#L1)

## 开关写在它控制的能力下面

`preActorDirector` 控制前台工具编排，`compact` 控制历史压缩，`storyV1` 选择 Story 产品流程。它们不在“模型可以调用的工具名”里。每个能力页都列出对应字段、默认行为、开启条件、互斥限制和生效时机。

固定流程的 `executionGraph` 是仓内 `ScenarioProfile` 的程序契约；现有 Router 的场景配置表不是任意工具图编辑器。需要固定串联业务工具时，按 [编排与扩展](#atoms-runtime) 的源码接法注册执行器、定义节点，并把结果送入实际模型输入。

二开前至少走通一次：提供实际输入 → 调用能力 → 检查返回 → 按业务规则采用或拒绝 → 保存 → 再发一轮读取验证。某项工具 `ok:true`，只证明该次工具返回成功，不自动证明正文被接受、图片已展示或状态已持久化。



<a id="atoms-generation"></a>

# Actor：生成正文与前台工具编排

**业务用途：**让角色根据当前输入和资料说话、行动；需要图文节奏时，把一轮正文拆成合适片段，再安排图片。例如用户说“让守卫接过钥匙，并配一张图”，Actor 负责写守卫的反应，图片能力负责生成相应画面。

**形态与接入位置：**当前 Actor 是 `apps/emochi/actor/roleplay-runtime.ts` 的应用正文运行时。`actor` 是前台工具循环中的函数名；Actor 的 `beforeHooks` 是执行前改写参数的接入点。Actor 本身不是 Hook，也不是在这个循环里选择其他工具的主控模型。

它是已有应用接线，不是 SDK 根入口可直接 import 的公共 `Actor` 类。复用完整能力需要沿用应用的 Router/Axon/模型/聊天交付依赖，或自己实现这些适配；只安装 `@flowgpt/roleplay-harness` 不会自动得到这条业务链路。

## 两种实际运行方式

| 方式 | 谁决定执行 | 拿什么输入 | 返回什么 |
| --- | --- | --- | --- |
| 直接生成 | 程序调用 `generate`；没有 `functionLoop` 时进入 `generateOnce` | 已准备的角色 Prompt、聊天材料、模型参数、本轮输入 | 流式正文、最终文本、模型使用信息 |
| 前台工具循环 | 前台控制模型每次选择一个动作，程序执行并把回执交回 | 控制模型先读工具政策与本轮用户输入；实际 Actor 使用自己准备的正文上下文 | 多个正文片段组成一轮回复；图片/DIO 返回调度回执 |

控制模型的系统提示明确要求：通过函数控制一轮 RP，不直接写用户可见正文；读实际工具结果再选下一步，不按固定顺序机械调用。代码变量叫 `planner`，这里实施的是逐步选工具的循环，不需要先生成一个完整长期计划。它和普通 V2 后台的 Orchestrator 是两个不同接线。

这里有三个层次：**前台控制模型选 `actor` → 程序执行正文运行时 → 正文模型写回复**。另外，Harness 的 `options.actor.beforeHooks` 发生在创建和执行 Actor 节点之前，用来准备或覆盖参数；当前前台循环没有把这些 Hook 当作主模型可以任意选择的菜单。

[是否开启前台循环](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/actor/roleplay-runtime.ts#L305) · [控制提示词与逐步调用](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/actor/roleplay-runtime.ts#L344)

## 模型真正可以调用什么

| 工具 | 参数 | 程序执行与回执 | 结果何时使用 |
| --- | --- | --- | --- |
| `actor` | `responseMode` 可省略，默认 `complete`；也可为 `visual_beat` | 执行原有正文生成器，返回片段与消耗等信息 | 正文本轮流式交付；控制模型据回执决定继续或结束 |
| `generate_image` | `{}` | 使用最新 Actor 片段安排图片，返回 `scheduled` 等调度信息 | 图片完成后回填聊天；调度成功不是图片已生成 |
| `schedule_dio` | `{}` | 宿主将用户原始长期要求交给 DIO，返回 `accepted` | 外部编译完成且后续请求读到有效 Prompt 后才影响回复 |
| `over` | `{}` | 在 Actor 至少执行过一次后结束前台循环 | 不生成正文，不保存角色状态 |

菜单至少包含 Actor 与结束；图片、DIO 只有相应执行能力接好才提供。循环最多 12 步，有 DIO 接线时额外允许 1 步。所有 Actor 片段共享有效 ModelConfig 的整轮输出额度。旧分支 `actor({maxTokens:...})` 的接口不是当前主干参数。

整轮 Actor 额度读取有效参数 `max_tokens`，缺失时使用 2,048。控制模型调用本身另有成本，不能把这个额度理解为所有模型的总 token 上限。某个 Actor 片段没有返回输出 token 计量时，程序按当时剩余额度全部消耗记账。正文是否能逐 token 显示还受整轮 after-hook、正则后处理与 `visual_beat` 缓冲影响；不能承诺所有配置都即时逐 token 透出。

`visual_beat` 是 Actor 参数，不是另一项工具。片段形成合格的完整视觉情节且循环还有下一步时，程序可以自动安排图片；已经配过图的片段不能重复调度。正好用尽步骤时，不能保证还有一次自动排图机会。

[工具定义与参数解析](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/actor/roleplay-runtime.ts#L86) · [执行、步骤与预算](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/actor/roleplay-runtime.ts#L381)

## 怎样接入和配置

应用接入使用 Scenario 中的 `modelConfigRef`、`presetRef` 等选择正文模型与模板；真实角色、会话、消息、历史等由业务请求和 Axon 准备。它们不是模型自己可以随意修改的工具参数。

配置 `preActorDirector` 后，当前 dispatch 装配前台 `functionLoop`。其 `modelConfigId` 是前台控制模型，`skills` 含已授权预设；配置含 `afterActorTools: ["generate_image"]` 的图片预设、`imagePromptProducerPrompt`、图片后端依赖，以及本轮 `assistantMessageId`，才形成当前图片调度接线。DIO 也需要独立服务接线。不能只向模型展示工具名，却不给程序实际执行器。

<details>
<summary>宿主配置字段与 Hook 的准确位置</summary>

| 位置 | 字段或接口 | 改变什么 |
| --- | --- | --- |
| Scenario | `preActorDirector.modelConfigId` | 选择前台控制模型，不替换 Actor 模型 |
| Scenario | `preActorDirector.skills` | 已授权技能预设；当前 dispatch 从其中识别图片接线 |
| Scenario | `preActorDirector.imagePromptProducerPrompt` | 图片描述生产的系统提示；不是 Scenario 根级字段 |
| Actor 宿主参数 | `options.actor.parameters` | 宿主选择的模型、Prompt 与变量覆盖，仍需通过实际允许范围校验 |
| Actor 执行前 | `options.actor.beforeHooks` | 程序依次运行 Hook，将返回字段合入 Actor 参数 |
| 仓内另一可复用实现 | `selectPreActorSkill` / `createPreActorDirectorHook` | 提供 `actor({skill})` 的单次预选方案；不是当前 dispatch 使用的前台循环协议 |

Story 配置明确排斥 `preActorDirector`；Sumi 和专用 Worldbook 场景也有组合限制。需要跨产品组合时应开发和验证对应接线，不能把多个字段堆在同一 Scenario 里。

[图片预设与前台循环装配](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L957) · [预设选择接口](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/tools/pre-actor-director.ts#L7) · [Actor 参数与 beforeHooks](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/harness.ts#L235) · [组合限制](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/server.ts#L761)

</details>

## 保存、失败与下一轮

Actor 生成结果经整轮后处理交给聊天服务，聊天正文是否被采用和落库由业务链路负责。Actor 工具本身不自动创建 NPC 卡片、发布世界书或更新普通 V2 的持久状态。前台控制在没有生成可见正文之前失败时，代码可以回退到直接 Actor 路径；已有正文后的失败需按当前运行时的规则保留或收束已生成片段。

具体而言：控制模型报错、返回非法工具请求或用尽步骤，而没有任何可用 Actor 结果时，进入直接生成回退；已有结果时结束循环并后处理已有片段。后续 Actor 调用发生空回复或超时、且前面已有完整片段时，会把失败回执交给控制模型继续决定。首次 Actor 执行失败、其他不可恢复错误或外部取消会传播；不能统一理解成“所有失败都返回已有正文”。已被外部图片或 DIO 服务接受的任务，也不因这段文字生成后来失败而自动撤销。

**二开验证例子：**输入“守卫接过钥匙，并配一张图”，分别检查正文是否先可用、是否只安排了一张图、图片结果是否绑定到正确消息、图片失败后用户看到什么；再发下一句，确认读取的是业务已保存的正文。不要用“模型调用了 actor”代替上述交付检查。

[控制失败与正文回退](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/actor/roleplay-runtime.ts#L413) · [整轮后处理](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/actor/roleplay-runtime.ts#L1078)



<a id="atoms-specialists"></a>

# 六类分析工具：给出建议，由业务决定采用

**用途：**当你的产品已经有角色、对话或故事材料，需要额外检查“人物有没有突然知道秘密”“承诺有没有忘记”“场面是否该转换”等问题，可以直接调用对应分析工具，不必先接完整 Agent V2。

**交付形态：**`@flowgpt/agent-core-tools` 的公共工具。它们调用宿主提供的模型，产出文本建议和使用信息；全部标记 `advisory: true`，不会自行修改聊天、人物卡、世界书或数据库。

这里的“公共”指包的可复用导出 API。仓库发布配置是 GitHub Packages、`restricted` 访问，接入发布包需要相应读取权限，并不代表公共 npm 可匿名安装。

## 每个工具具体干什么

| 工具与导出类 | 检查什么 | 业务输入例子 | 结果怎样采用 |
| --- | --- | --- | --- |
| `analyze_plot` / `PlotTool` | 主线、当前情节和分支是否需要变化 | “用户拒绝了原定任务，接下来怎么办？” | 返回推进建议；产品决定是否写入剧情计划或下一轮约束 |
| `judge_npc` / `CharacterTool` | 人物身份、数量、动机、知识、位置、出入场与连续性 | “一个新守卫出现，他凭什么认识用户？” | 给人物连续性建议；不自动生成独立 NPC ID 或完整卡片 |
| `audit_knowledge` / `KnowledgeTool` | 事实、秘密、谎言、猜测、证据，以及谁知道什么 | “信封没打开，Mira 能否知道信里内容？” | 明确信息边界，供写作或状态整理采用；不会自动搜索世界书 |
| `simulate_world` / `WorldTool` | 时间经过、位置、期限与镜头外活动 | “用户离开酒馆三天，约定是否过期？” | 给出需要考虑的世界变化建议；不等于 Story 的事件状态引擎 |
| `manage_callbacks` / `CallbackTool` | 承诺、线索、物件、伤势、义务、伏笔和延迟影响 | “借的钥匙还没还，要记住哪件事？” | 提醒持续追踪，不强制马上让伏笔发生 |
| `direct_scene` / `SceneTool` | 视角、镜头、场面与角色自主行动尺度 | “守卫能主动拦路，但能替用户作决定吗？” | 输出场面建议，提示词要求保留用户行动自主权 |

[六工具定义](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/memory/advisory.ts#L18) · [公共导出与工厂](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/memory/index.ts#L1)

## 调用前究竟要准备什么

模型可见参数是 `reason`（为什么咨询）和可选 `focus`（这次重点）。角色资料、历史、已有状态、用户身份等由宿主提供，不能指望专家名称自动带来数据库访问。

默认 RP 上下文包含目标角色、开场白、当前用户输入、用户 Persona、已有状态、压缩摘要与最近可见对话。默认历史投影只取过滤后的最后 9 条 user/assistant 消息；Persona 最多 4,000 字符；状态投影使用 6,500 字符预算。这些是字符投影规则，不是模型完整上下文窗口。

如果你的业务需要完整世界书条目、外部事实或更长历史，先由宿主取回，再通过 `compileContext` 自定义 user message。系统岗位提示词由调用者另行传入。编译器是同步格式化函数，本身不做网络检索。

[上下文与模型适配接口](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/contracts.ts#L79) · [默认输入投影](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/memory/advisory.ts#L169)

## 最小接线片段

下面是**宿主接线片段**，`context`、`model`、`systemPrompt` 必须由业务准备，不是安装包自动提供。

```ts
import { createAgentCoreTool } from '@flowgpt/agent-core-tools';

const tool = createAgentCoreTool({
  name: 'audit_knowledge',
  modelConfigId: 'your-knowledge-model',
  modelCallTimeoutMs: 15_000,
});

const result = await tool.execute({
  context, model, prompt: systemPrompt,
  arguments: { reason: '核查信息边界', focus: 'Mira 是否看过未打开的信' },
  signal,
});
if (!result.ok) throw new Error(result.error);
// result.result 是分析文本。这里交给业务采用，不会自动写数据库。
```

需要让自己的 Agent 选择工具时，用 `/pi` 的 `toPiTool(tool, prepare)` 包装，再加入 Pi 的工具列表。`prepare` 负责绑定真实业务上下文、模型和提示词；包装层校验 `{reason, focus}` 参数并把失败转换为工具异常。直接调用适合固定流程，注册后由模型选择适合动态流程。

参数校验由 `toPiTool` 包装执行，`prepare` 负责准备业务材料；如果绕过包装直接调用 `.execute`，应由宿主检查工具参数，不要把暴露给模型的 JSON Schema 当作该直调入口已自动执行的校验。

[完整假模型直调示例](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/examples/sdk/02-direct-tools.ts#L1) · [模型选择工具的完整示例](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/examples/sdk/03-agent-tools.ts#L1) · [Pi 工具适配](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/pi.ts#L9)

## 配置、保存和失败边界

| 项 | 实际行为 |
| --- | --- |
| 调哪个模型 | `modelConfigId` 由宿主模型适配器解释；SDK 不自动查公司配置中心 |
| 岗位提示词 | 每次调用的 `prompt`；不是六工具包内自带完整产品提示词 |
| 模型重试 | 通常最多初次＋2次重试；取消、内容过滤、已发生恢复或特定上下文错误可提前停止 |
| 结果长度 | 默认整理为最多 5,000 字符。`contextGovernance` 的 `enforce` 下超出结果额度会返回失败，而非照常采用 |
| 返回 | `{ok, tool, result?, error?, usage?}`；Promise 正常 resolve 仍可能 `ok:false`。`contextGovernance: enforce` 的计数不可用／上下文溢出错误会向上抛出，宿主还需要处理 Promise reject |
| 何时保存 | 工具不保存。产品可以把建议给 Director、给下一步工具、给人工，或自行校验后写库 |
| 何时生效 | 取决于宿主把建议放到哪个后续输入或存储里；不会自动影响正在并行生成的回复 |

**具体闭环：**业务提交“未打开的信”及现有记录 → `audit_knowledge` 返回“没有依据得知信中秘密” → 业务把它作为下一次写作约束 → 下一次生成遵守该信息边界。如果只调用工具而丢弃结果，就不会改变产品行为。

[重试与失败](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/memory/advisory.ts#L127) · [输出、长度与保存边界](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/memory/advisory.ts#L244)

<details>
<summary>独立工具配置的必填项与治理边界</summary>

`name`、`modelConfigId`、`modelCallTimeoutMs` 都是 `AgentCoreToolConfig` 的必填项。这里没有独立工具默认 180 秒的构造规则；180 秒是普通 V2 应用配置的默认值，客户直调用多少必须自己提供。`compileContext`、`contextGovernance`、`onDiagnostic`、`shouldStopRetries`、`isContextGovernanceFailure` 可选。

`contextGovernance` 若启用，需给出 `mode: "shadow" | "enforce"`、`safetyMarginTokens`、`maxToolResultChars`。工具把输入窗口预算要求传给宿主 `model.complete`；宿主模型适配器需要实际实现计数与拦截，不能因为传了对象就认定第三方模型端已经受控。工具自己会检查返回文本长度：shadow 仅记录诊断，仍按默认最多 5,000 字符整理；enforce 超配额返回 `tool_result_oversized`，未超限则保留完整结果。

类导出分别为 `PlotTool`、`CharacterTool`、`KnowledgeTool`、`WorldTool`、`CallbackTool`、`SceneTool`，根入口同时导出 `createAgentCoreTool`；`createAgentCoreToolWithContext` 要求提供 `compileContext`。角色型默认 context 的限制是材料投影，并不限制客户自定义编译器可提供的全部业务字段。

[独立工具必填配置](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/memory/advisory.ts#L43) · [enforce和shadow输出处理](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/memory/advisory.ts#L291) · [类与工厂导出](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/memory/index.ts#L1) · [根入口、Pi适配与发布边界](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/package.json#L12)

</details>



<a id="atoms-story"></a>

# Story 原子能力｜六个可单独组合的剧情工具

这组能力适合自行开发互动故事、游戏对话或剧情型角色产品：把“当前场景怎么准备”“剧情目标怎么安排”“正式正文发生了什么”拆开调用。它们已在 `roleplay-harness / main` 导出，入口为 **`@flowgpt/agent-core-tools/story`**；不是六个咨询专家，也不是六个自动注册到所有 Agent 的函数调用菜单。

当前源码包版本为 `0.2.1`，仓库配置的发布目标是受限 GitHub Packages。宿主需获得包与模型访问权限；此处不把“源码有导出”写成“任意客户已能匿名安装”。固定源码基线：`fa41d220d48327e58d994f936517b23f2f15f658`。[包导出与发布配置](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/package.json#L1) · [Story 导出文件](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/story.ts#L1)

## 1. 能拿走什么

| 能力与类型 | 客户可以做什么 | 当前 Story 产品什么时候使用 |
| --- | --- | --- |
| `PovPrepareTurnTool`：有状态输入输出的世界准备工具 | 根据用户行动决定是否转场，运行场景计时与事件规则，准备本轮写作材料 | 每次正常请求，先采用上轮正文，再准备当前行动 |
| `PovAdvanceWorldTool`：有状态输入输出的世界结算工具 | 根据实际被接受的完整正文更新世界、后果和待传达邀请 | 下一正常请求取得上一轮正式保存正文后 |
| `NarrativePlanCycleTool`：调用 LLM 的目标规划工具 | 根据主副线与未完成事项，新增一批有因果前提的目标 | `cycle.needPlan=true` 且未解决事件没有阻塞时 |
| `NarrativeDealBeatTool`：纯程序目标选择工具 | 在已有可用目标中选一个，配上情绪与节奏指导 | 每轮准备时；不是每轮新建整批计划 |
| `NarrativeSettleEventTool`：调用 LLM 的证据结算工具 | 判断一段已完成或被打断的事件到底完成了什么 | 事件结束或转场打断，及必要的重试 |
| `NarrativeRebranchTool`：调用 LLM 的剧情方向调整工具 | 按持续的新追求改主线，或替换长期未发展的支线 | 协调器收到 `rebranch` 下一步指令后 |

所有工具都返回结果；它们不创建数据库、不自己存会话、不决定业务是否接受正文、不递归执行返回的下一步动作。业务自行安排调用，或采用已有 Story 产品协调器。[应用组合入口](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/coordinator.ts#L247)

## 2. 公共调用形状

```ts
import type { AgentCoreModelRuntime } from '@flowgpt/agent-core-tools';
import {
  createStoryWorldSnapshot, parseStoryWorldSnapshot,
  initNarrativeSnapshot, parseNarrativeSnapshot,
  PovPrepareTurnTool, PovAdvanceWorldTool,
  NarrativePlanCycleTool, NarrativeDealBeatTool,
  NarrativeSettleEventTool, NarrativeRebranchTool,
  projectStoryStage, restoreStoryPreparedTurn,
  POV_WRITER_STAGE_INSTRUCTIONS,
  type StoryContext, type StoryModelConfig,
} from '@flowgpt/agent-core-tools/story';
```

每次调用需要 `context`：

| 字段 | 实际要求 |
| --- | --- |
| `requestId` | 当前调用链稳定 ID，用于模型调用标记 |
| `turnId` | 当前准备/采用轮的稳定 ID；恢复同一轮不能换 ID |
| `language` | 输出语言 |
| `signal` | `AbortSignal`，用于取消 |
| `trace` | 可选追踪信息 |

需要模型时注入 `StoryModelConfig`：`model` 是实现 `AgentCoreModelRuntime.complete` 的对象，`modelConfigId` 是宿主可解析的模型配置 ID，`timeoutMs` 可选，`isRecoverableModelError` 可选。凭证、供应商连接、模型配置解析由宿主负责。JSON 结果校验不通过最多尝试一次修复，最多两次模型响应；工具返回诊断和用量。[调用契约](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/story/common.ts#L3)

## 3. 六个工具：准确参数、结果与使用例子

### A. 准备本轮场景 `PovPrepareTurnTool`

**构造：** `new PovPrepareTurnTool(modelConfig?, hooks?)`。

**输入：** `{ context, snapshot, action, worldContext? }`。`snapshot` 是 `StoryWorldSnapshot`；`worldContext` 是宿主提供的设定与事实材料字符串。`action` 三选一：

```ts
{ type: 'free', text: '我把钥匙给她，但先不进城。' }
{ type: 'intent', text: '我留在城门询问。', intent: { kind: 'normal' } }
{ type: 'choice', index: 0 }
```

- `free` + 有 modelConfig：调用模型判断 `normal`（留在当前事件）、`cue`（主动转向已有事件）或 `signal`（回应上轮确实传达的邀请）；需要时判断同伴是否跟随。
- `intent`：宿主已经判断行动，直接提供结果；`targetId` 必须匹配可用事件，不能靠任意字符串创建一个目标。
- `choice`：索引来自快照已保存的 `choices`。
- 不提供模型且用自由输入时，采用 `normal`；工具不会自己理解一个自由文本转场意图。

**输出 `StoryPreparedTurn`：** `snapshot`、`intent`、`userText`、`flags`、`stage`、`surfacing`、`dice`、`events`、`diagnostics`、`usage`。给 Writer 的主要是 `stage`：当前事件与阶段、应承接动作、可见的场外影响、选中的暗流线索、待传达邀请。全快照用于恢复，不宜原样作为角色已知信息。

**保存时机：** 这是准备候选。宿主应保留它，直到对应正文被接受后交给 Advance；同一 `turnId + action` 可恢复准备，不重复推进骰子和时钟。不同轮不能在未解决的 pending 上继续 prepare。[输入与分支](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/story/world.ts#L85) · [数据结构](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/story/world-types.ts#L182)

### B. 根据正式正文推进世界 `PovAdvanceWorldTool`

**构造：** `new PovAdvanceWorldTool(modelConfig?, hooks?)`。

**输入：** `{ context, prepared, writer, worldContext? }`。`context.turnId` 必须匹配 `prepared`。`writer` 至少含完整的已接受 `prose`，也可含 `choices` 和 `nextForeground`；这两个字段是结构化 Writer 宿主的可选能力，现有 Story 应用主要交 `{ prose }`。

```ts
writer: {
  prose: '莉娅把钥匙递还，示意你可以留在门外等消息。',
  // 可选 choices: [{ text: '我留在门外等。', expect: '等待', isSignal: false }]
}
```

**输出：** `{ outcome, snapshot, events, diagnostics, usage }`。

- `committed`：工具算法已经计算新世界快照；**不代表数据库写成功**。
- `duplicate`：同一已完成轮没有再次结算。
- `pending`：没有可靠世界提案，保留原 prepared；宿主保存已接受正文证据并重试同一轮，不能把它当成“无变化成功”。

使用模型时会检查邀请是否实际传达、场外结果、合法后继事件等；模型不能随意改时钟、ID、容量或机械阶段。公开构造允许不传模型，但这时没有模型解释世界内容，只使用已有机械材料和显式 Writer 输出，不能与完整产品的语义结算能力画等号。[执行与 outcome](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/story/world.ts#L153)

### C. 创建剧情目标 `NarrativePlanCycleTool`

**构造：** `new NarrativePlanCycleTool(modelConfig, options?, hooks?)`，`options` 可含 `{ policy, prompt }`。

**输入：** `{ context, snapshot, setting }`。`snapshot` 是 `NarrativeSnapshot`，`setting` 是设定和当前已知故事材料字符串。

**输出：** `{ snapshot, nextActions, diagnostics, usage }`。新增目标写入 `snapshot.cycle.pending`；包含 `id / serves / angle / gist / reward / core / done / priority / prerequisites`。例如“让守卫辨认钥匙徽记”是 `reveal` 目标，“守卫给予通行许可”若作为回报，须引用已经发生或安排中的铺垫目标。

工具只在 `needPlan=true` 且本轮未规划过时调用模型。默认新增 5–7 项、至少一项服务副线；原未完成事项保留。无效输出修复后仍失败，则仍保持 `needPlan`，不能假定已有可用计划。[实现与验证](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/story/narrative.ts#L47)

### D. 选本轮剧情目标 `NarrativeDealBeatTool`

**构造：** `new NarrativeDealBeatTool({ policy }?, hooks?)`。不接受模型配置，不调用 LLM。

**输入：** `{ context, snapshot, phase }`，`phase` 为 `start / develop / end`。

**输出：** 通用结果加 `packet`：

```ts
{
  pending: { id, serves, angle, gist, reward, fresh } /* 或 null */,
  emotionGuide: { enabled, valence, arousal, dominance, label, beat },
  mainDirection,
  subDirection,
  bias: { preferBackgroundKind: 'conflict' /* 或 'progress' / null */ }
}
```

程序按前置目标是否完成、优先级、欠缺回报、情绪匹配与主副线多久没推进来选择。没有满足前置条件的目标就返回 `pending:null`；不会硬塞一个未铺垫回报。将 `packet` 与世界 `stage` 一起交给 Writer，模型才有机会采用它。返回快照也要保存，因为里面记录了本轮选择和轮换计数。[选择机制](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/story/narrative.ts#L100)

### E. 判断一个事件实际完成了什么 `NarrativeSettleEventTool`

**构造：** `new NarrativeSettleEventTool(modelConfig, options?, hooks?)`，支持 `{ policy, prompt }`。

**输入：** `{ context, snapshot, eventId, prose, userActions, interrupted? }`。

- `eventId` 是这次事件的稳定身份；不同发生次数不能复用同一个 ID。
- `prose` 是该事件累计的完整已接受正文，不一定只有一条回复。
- `userActions` 是对应用户行动数组；不要把事件结束后的新输入混进来。
- `interrupted=true` 表示事件被打断，不代表已完成所有目标。

**输出：** 通用结果加 `outcome: 'settled' | 'unknown' | 'duplicate'`。成功快照更新目标完成、主副线进度、情绪观察及事件计数；`nextActions` 可能要求 `plan-cycle` 或 `rebranch`。未知评估不记作“剧情没有推进”，也不增加未推进计数；宿主要保留证据重试。

例如正式正文只是“守卫看见徽记，但没有认出”，不能把“辨认来源”标完成。模型返回的完成目标、移动游标都必须带证据。[事件输入与结果](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/story/narrative.ts#L182)

### F. 调整主副线 `NarrativeRebranchTool`

**构造：** `new NarrativeRebranchTool(modelConfig, options?, hooks?)`，支持 `{ policy, prompt }`。

**输入：** `{ context, snapshot, setting, scope, direction? }`。

- `scope:'main'`：必须提供非空 `direction`。例如用户持续转向“寻找失踪商队”；模型调整主线与相配副线，承接已有进度与承诺。
- `scope:'sub'`：只替换副线，主线保持；无需 `direction`。

**输出：** 新 `snapshot`、`nextActions`、`diagnostics`、`usage`；连续性说明放在 `narrative_rebranched` 诊断中。工具本身不决定“用户是不是该改主线”：现有 Story 协调器依据已结算事件中的连续方向触发，其他宿主也可按自己的显式业务规则调用。`rebranch` 是剧情方向调整，不是创建 Git 分支或自动复制会话。[实现](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/story/narrative.ts#L254)

## 4. 独立组合：哪些东西一定由二开方提供

| 宿主责任 | 必须完成的事 |
| --- | --- |
| 模型 | 提供 `model.complete`、配置 ID 与语言，处理凭证/路由；按实际需要提供模型失败分类 |
| 初始化 | 用 `createStoryWorldSnapshot` 与 `initNarrativeSnapshot` 建立真实初态，或自建初始化模型。完整应用的角色卡读取和初始化器没有因导入工具包而自动出现 |
| Writer | 把 stage + narrative packet + 自己的角色/风格/历史放入写作模型；Story 工具不输出最终 RP 正文 |
| 调度 | 调用 prepare、条件 plan、deal、Writer、accept、advance、事件 settle，以及执行 `nextActions`；工具之间不会自动调用 |
| 接受正文 | 明确哪个完整结果被业务采用，只有它才能用于结算。中间 token、候选草稿与下游尚会变更的正文不等价 |
| 状态存储 | 保存完整世界和叙事快照，保留准备态及正式正文证据；通过事务/CAS 防止旧请求覆盖新进度 |
| 恢复 | `pending / unknown` 保留证据后重试；取消、大小上限、删改历史和重生成规则由宿主定义 |
| 呈现与查询 | 角色只能看到允许表现的内容；需要审计或运营查询时另建界面 |

最小组合顺序：

```text
读已保存 world / narrative
→ PovPrepareTurnTool
→ 如果 needPlan：NarrativePlanCycleTool
→ NarrativeDealBeatTool
→ 把 stage / packet 交给自己的 Writer
→ 业务接受完整正文
→ PovAdvanceWorldTool
→ 事件到达边界：NarrativeSettleEventTool
→ 根据 nextActions 调整主副线或安排下一次规划
→ 校验 outcome 并事务/CAS 保存
```

已有完整应用把“接受完整正文”推迟到下一请求；公开工具不强制这样做。源码示例 `examples/sdk/04-story-turn.ts` 演示同一次调用内接受正文并存储，使用假模型验证控制流，没有调用真实模型或数据库。[可运行组合示例](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/examples/sdk/04-story-turn.ts#L7)

## 5. 配置和扩展边界

| 层次 | 怎么配置 | 当前可改变什么 |
| --- | --- | --- |
| 已接好的 Story 产品 | 场景 `storyV1` | enabled、模型、初始化模型、模型调用超时、生成后保存超时、状态字节上限、随机 seed |
| 独立世界能力 | `createStoryWorldSnapshot({ policy })` | 当前事件发展长度、场外容量、暗流阶段阈值、表露预算、种子注入节奏、保护角色 |
| 独立叙事能力 | 各工具 `options.policy` | 新目标数量、剧情轮换、主副线饥饿阈值、持续新方向阈值、情绪调节、依赖与去重窗口等 |
| 三个叙事模型工具 | `options.prompt` | 替换 PlanCycle / SettleEvent / Rebranch 的任务说明；结果仍须满足相应 schema |
| 工具生命周期 | `hooks.beforeExecute / afterExecute / afterTurn` | 输入前处理、执行结果观察或后处理、整轮处理；`afterTurn` 必须由宿主显式调用 |
| Writer 输入 | `POV_WRITER_STAGE_INSTRUCTIONS` + 自己的角色提示词 | 控制舞台如何被写入正文；导出的说明不替宿主定义角色身份或风格 |

当前 `storyV1` **没有**直接暴露 world/narrative 的所有内部 policy，不能把公共工具构造参数与应用 JSON 开关混为一谈。世界工具也没有 `options.prompt` 参数；它们的语义提示词在源码中，不能照抄叙事工具的构造方式来修改。

`projectStoryStage(prepared, bias?)` 可在 Writer 前改变同等候选间的 `conflict / progress` 偏好；结果应保存。`restoreStoryPreparedTurn(snapshot, bias?)` 是恢复，bias 仅用于断言一致，不重新选择、不再次消耗随机数。新 prepared 会记录独立准备基线，校验时重放机械决策，防止保存的舞台与标记被一起改写后冒充合法结果。[世界投影与恢复](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/story/world.ts#L18) · [生命周期](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/tool.ts#L10) · [应用配置](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/config.ts#L11)

默认关键值为 `dealEveryTurns=3`、每次新增目标 `5–7`、主线持续新方向 `3` 个已结算事件、副线 `5` 个已结算事件未推进触发替换、世界 `surfaceBudget=2`、background 最多 `3`、undercurrent 最多 `5`。它们的单位不同，不能统一解释成“累积 X 轮启动慢系统”。[叙事默认策略](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/story/narrative-state.ts#L93) · [世界默认策略](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/story/world-state.ts#L16)



<a id="atoms-worldbook"></a>

# 世界书：保存设定，并在每次生成前选择需要交给模型的原文

世界书适合保存作者事先写好的地点、人物、规则和背景。例如“灰港没有电力”“铜钥匙只能开侧门”“守卫放行前要核对手令”。接入方把这些资料存进服务；用户说“我把铜钥匙递给守卫”时，服务选择本轮相关条目，把原文和放置位置交回来。接入方再把它放进回复模型的输入，模型才有机会按这条规则写出“手令呢？”这样的回应。

**可复用的交付物是独立 HTTP 服务及源码；它本身不生成 RP 回复，也不是默认挂在主 Agent 工具菜单里的一个 function tool。** 当前 Harness 已有一条专用世界书接线；客户也可以从自己的后端直接调用服务，把结果交给自己的模型或工作流。[服务入口](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/app.py#L216) · [Harness 接线](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L765)

## 能拆出来使用的能力

| 能力 | 交什么进去 | 拿到什么 | 谁继续处理 |
|---|---|---|---|
| **世界书创建与版本管理** | 一本书的名称、条目原文及触发规则 | `book_id`、不可变 `revision`；可读取旧版本 | 内容管理后台保存 ID；发布修改时提交新版本 |
| **角色卡／世界书文件导入** | CCv2/v3 JSON 或 PNG 中的世界书、SillyTavern 世界书 JSON | 归一化条目、支持范围报告；保存后得到书籍 ID | 运营检查被阻止的动态内容，再决定使用或改写 |
| **Prompt 与世界书关联** | 角色／作品 `prompt_id`、书籍 ID、固定或跟随最新的版本策略 | 已保存关联；运行时解析出的具体版本 | 宿主每轮重新解析，再使用这个版本选条目 |
| **词法／向量搜索** | 查询文本、允许查询的书籍范围 | 排序结果、分数、最多 600 字符的原文摘录 | 搜索页面或自己的召回程序；它不是最终模型上下文 |
| **本轮条目选择** | 本轮输入、历史、状态条件、书籍版本、预算 | 原文 `blocks`、聚合 `context`、每条入选／排除理由 | 模型输入组装程序按位置放入条目 |
| **指定版本索引构建** | 书籍 ID、版本、是否强制重建 | 索引状态、分块数量、Embedding 版本 | 部署／运营侧维护；规则选择不需要向量索引 |
| **原文位置装配** | 条目返回的 `position`、`role`、`depth` 等 | 按角色资料、历史或指定区域装好的模型消息 | Harness 的 Actor 接线已实现；自建宿主需实现或复用 SDK 装配函数 |

## 一次使用从准备到生效

1. **运营准备设定**：把独立规则写成条目。为“灰港基本设定”标记常驻；为“铜钥匙与城门守卫”填写关键词“铜钥匙、城门”。原文由作者／运营提供，不是服务自动生成。
2. **保存一本书**：调用原生创建接口。服务把原文、条目和版本写入 PostgreSQL，返回书籍 ID 与版本 1。以后修改是发布版本 2，不覆盖版本 1。
3. **决定谁使用它**：若走现成 Harness 接线，把书关联到角色／作品的 `prompt_id`；若走独立服务，自己的程序在每次请求中直接提供书籍 ID 与版本。
4. **用户发来一句话**：“我把铜钥匙递给守卫。”宿主取得这次允许使用的书籍版本，提交本轮输入与状态。
5. **服务选出本轮原文**：“灰港基本设定”因常驻进入候选，“铜钥匙与城门守卫”因关键词进入候选；通过条件与预算后返回两段原文。返回原因分别是 `constant`、`keyword`。
6. **回复前注入**：宿主把这两段文字放到角色资料前，再与角色、历史、本轮用户输入一起发给回复模型。模型生成的守卫回应受这份资料影响；选择成功不等于模型一定遵守，仍需业务侧验收实际回复。
7. **下一轮再算**：书籍原文继续留在数据库；本轮选中的两段原文不会因此变成永久聊天历史。下一次输入重新选择，已关联的固定版本／最新版本策略决定用哪份书。

原文选择和位置装配发生在生成前，会占用本轮准备时间。它不是等后台累计若干轮再执行的记忆任务。[逐轮请求与选择](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/worldbook/runtime.ts#L59)

## 运营准备什么数据

**推荐从原生 JSON 开始。** 下面整个 JSON 对象可以直接作为 `POST /v1/books` 的请求体。`name` 是书名；`entries` 中每项是一条设定；`content` 就是希望模型读到的原文。不要把书籍 ID、租户或 Prompt ID 写进这个对象，它们属于保存结果或关联关系。

```json
{
  "name": "灰港世界设定",
  "description": "演示：常驻世界规则与按铜钥匙触发的城门规则。",
  "entries": [
    {
      "id": "greyport-background",
      "title": "灰港基本设定",
      "content": "灰港是一座蒸汽城市，城内没有电力，夜晚依靠煤气灯照明。",
      "constant": true,
      "position": "before_character",
      "audience": ["actor"]
    },
    {
      "id": "copper-key",
      "title": "铜钥匙与城门守卫",
      "content": "铜钥匙只能开启灰港城门的侧门。守卫在放行前必须核对来访者的手令，不能仅凭钥匙放人。",
      "keys": ["铜钥匙", "城门"],
      "position": "before_character",
      "audience": ["actor"],
      "priority": 20
    }
  ]
}
```

运营首先需要填好 **条目名称、原文、何时使用**。常驻条目用 `constant: true`；动态条目用 `keys`。每项给一个稳定且不重复的 `id`，方便后续修改与定位。同一条目可以写多条触发词，命中任意主关键词就有机会进入本轮候选。

**原生 JSON 和文件导入是两个入口。** 这个示例应提交到 `/v1/books`，批量原生书则提交 `{"books":[上述对象]}` 到 `/v1/imports`。不要把原生 JSON 当作 ST 文件上传到 `/v1/imports/files`：文件入口会按 ST／角色卡规则重新归一化，字段含义和保存方式不同。[原生模型](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/models.py#L46) · [两个导入入口](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/app.py#L216)

<details>
<summary>原生数据格式、校验与字段默认值</summary>

| 字段 | 含义、默认值与边界 |
|---|---|
| `name` | 必填，1–300 字符；`description` 默认空，最多 10,000 字符 |
| `entries` | 默认空数组，一本最多 5,000 条；原生批量入口一次 1–10 本 |
| `id` | 同一版本内唯一，最多 150 字符；建议明确填写。省略时程序按顺序补 `entry-0` 等 |
| `title`／`content` | 标题默认空，最多 500 字符；原文 `content` 必填，最多 200,000 字符。空原文不进入本轮上下文 |
| `enabled` | 默认 `true`；关闭后仍可保存、查询，默认不参与本轮选择 |
| `constant`／`required` | 均默认 `false`；常驻和必需的实际规则见后文，二者并非同义 |
| `keys`／`secondary_keys` | 主／次关键词数组，分别最多 200 个，每项最多 500 字符；默认空 |
| `selective`／`logic` | 默认 `false`／`and_any`；开启且有次关键词时，在主关键词命中后额外按次关键词条件过滤 |
| `case_sensitive`／`match_whole_words` | 默认均 `false`；可选择区分大小写、匹配完整词；支持有限正则 |
| `probability` | 默认 100，范围 0–100；依赖请求 `seed` 与条目 ID 作确定性抽选 |
| `scan_depth` | 条目自己的扫描深度；默认 `null`，使用请求层的深度；范围 0–100 |
| `conditions` | 最多 20 项，按调用方 `state` 的路径检查；操作为 `eq`、`ne`、`in`、`exists` |
| `requires` | 最多 20 个同一本书同一版本内的条目 ID；发布时拒绝缺失依赖、自依赖及循环依赖 |
| `priority`／`order` | 默认 0，范围 ±1,000,000；前者影响优先选择，后者参与选择排序与最终装配顺序 |
| `conflict_key` | 同一冲突组只能选一个条目；默认 `null` |
| `audience` | 默认 `["actor"]`；请求的 audience 必须在条目允许列表内，它不会主动把内容发给该模型 |
| `kind` | `lore`、`character`、`rule` 可作为静态资料；`template`、`state_update`、`presentation` 被默认选择器排除 |
| `position`／`depth`／`role` | 默认 `before_character`／0／`system`；实际可用位置由宿主声明，见后文 |
| `exclude_recursion`／`prevent_recursion` | 原生条目默认均 `true`：不被递归内容激活，也不把本条内容用于激活别人；ST 导入有其映射默认值 |
| `runtime_requirements` | 默认空；非空表示需要尚未提供的运行环境，本轮不会直接注入 |
| `metadata`／`source` | 书籍附加信息／条目来源信息；不会替代 `content` 自动成为模型上下文 |

数据模型拒绝未知字段。把 `tenant_id`、`book_id`、`prompt_id` 随意混入创建书籍 JSON，会触发校验错误，而不是自动建立绑定。[完整契约](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/models.py#L16)

</details>

## 已有 ST／角色卡资料怎样导入

**ST 指 SillyTavern。当前代码支持它的静态世界书资料导入，不是完整运行 SillyTavern。** 文件可以是独立 world-info JSON，也可以是 CCv2／CCv3 角色卡 JSON 或 PNG 中嵌入的 `character_book`。这里提取的是世界书；角色卡的其他角色字段不会自动变成此服务管理的角色产品。[解析格式与报告](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/importer.py#L208)

| 顺序 | 入口 | 结果与操作 |
|---|---|---|
| 1. 预览 | `POST /v1/imports/preview`，multipart 字段为 `file` | 返回 `book` 和 `import_report`，不落库。先看条目数、被阻止的条目和 warning |
| 2. 保存 | 原文件走 `POST /v1/imports/files`，multipart 字段为 `files`；若已修改归一化 `book`，走原生 `/v1/books` | 一批文件全部解析成功才进入同一保存事务，返回书籍 ID 和 revision |
| 3. 建索引（需要向量时） | `POST /v1/books/{book_id}/index` | 显式构建本次精确版本的向量索引；规则／词法使用不依赖这步 |
| 4. 关联或调用 | 保存 Prompt 关联，或自己的程序直接传书籍版本 | 保存文件不自动关联某个角色，也不自动修改已有聊天 |

预览报告中的 `runtime_blocked_count` 是需要额外运行环境的条目数。EJS 模板、MVU 状态更新、CharInfo 人物生成协议、脚本／前端面板、sticky／cooldown／delay 等动态功能，不会因为导入成功就执行；带这类要求的条目会被默认上下文选择排除。普通 `{{char}}`、`{{user}}` 可以替换，但调用方必须传入对应变量。[动态能力识别](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/importer.py#L117)

<details>
<summary>文件上传请求、重复导入和发布新版本</summary>

以下命令运行在能访问内网服务的开发环境；`tenant_id=local` 是示例租户。`WB_BASE` 应设置为实际服务地址。

```sh
WB_BASE='http://127.0.0.1:8876'
curl --fail-with-body -sS "$WB_BASE/v1/imports/preview?tenant_id=local" \
  -F 'file=@world-info.json'
curl --fail-with-body -sS "$WB_BASE/v1/imports/files?tenant_id=local" \
  -H 'Idempotency-Key: greyport-file-import-v1' \
  -F 'files=@world-info.json'
```

单文件默认上限 16 MiB，批量文件和整个 HTTP body 默认各 48 MiB；一次 1–10 个文件。PNG 只读取支持的内嵌卡片元数据，不执行脚本，也不抓取其中的远程链接。

创建／导入接口支持 `Idempotency-Key`：同租户、同 key、同内容返回同一结果；同 key 换内容返回 409。原生 JSON 不传 key，每次会新建一本书。原文件按文件 SHA-256 去重；重复上传同一个原文件返回最初的 revision 1，而不是后来编辑出的最新版。

编辑已有书要调用 `POST /v1/books/{book_id}/revisions`，提交**新版本全量书籍内容**，并加 `expected_revision`。例如当前版本 1，则提交 `expected_revision: 1`，成功返回版本 2；如果别人先改过，返回 `revision_conflict`，重新读取后再发布。此接口不是单条条目 PATCH；省略的旧条目不会自动继承进新版本。[创建与去重](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/repository.py#L103) · [全量修订](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/repository.py#L340)

</details>

## 怎样独立接入：一次创建与选择的最小请求

这条路径不依赖 Agent V2、Story 或 Axon。你的后端调用世界书 HTTP API，再负责把结果装进自己的模型请求。

**第一步：创建。** 将前面的原生 JSON 保存为 `worldbook-native-example.json`，提交：

```sh
WB_BASE='http://127.0.0.1:8876'
curl --fail-with-body -sS "$WB_BASE/v1/books?tenant_id=local" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: greyport-native-v1' \
  --data-binary @worldbook-native-example.json
```

返回的结构如下。UUID 是示意，实际使用服务返回的值：

```json
{
  "books": [{
    "book_id": "00000000-0000-0000-0000-000000000001",
    "revision": 1,
    "deduplicated": false,
    "import_report": {"format": "native", "entry_count": 2}
  }],
  "replayed": false
}
```

**第二步：选择。** 把真实 `book_id` 替换进下列请求，保存为 `select.json`。书籍可固定 `revision`；为了同轮结果稳定，接入方应先解析出具体版本，再在同一轮中复用。

```json
{
  "books": [{"book_id": "00000000-0000-0000-0000-000000000001", "revision": 1}],
  "query": "我把铜钥匙递给守卫。",
  "history": [],
  "state": {},
  "audience": "actor",
  "strategy": "rules",
  "scan_depth": 4,
  "token_budget": 1500,
  "seed": "example-turn-1",
  "template_vars": {"char": "守卫", "user": "旅人"}
}
```

```sh
curl --fail-with-body -sS "$WB_BASE/v1/context/select?tenant_id=local" \
  -H 'Content-Type: application/json' \
  --data-binary @select.json
```

下面是返回中一条 `blocks` 元素的完整形状。真实响应还包含另一条常驻设定和整体诊断：

```json
{
  "id": "00000000-0000-0000-0000-000000000001:1:copper-key",
  "book_id": "00000000-0000-0000-0000-000000000001",
  "revision": 1,
  "entry_id": "copper-key",
  "title": "铜钥匙与城门守卫",
  "content": "铜钥匙只能开启灰港城门的侧门。守卫在放行前必须核对来访者的手令，不能仅凭钥匙放人。",
  "position": "before_character",
  "depth": 0,
  "role": "system",
  "order": 0,
  "reason": "keyword"
}
```

**第三步：使用。** `content` 是作者的文字，并未经过另一个 LLM 总结。自建宿主按 `position` 放置这些 `blocks`，再加角色资料、历史和本轮输入，最终调用自己的回复模型。简单单位置接法也可使用聚合 `context`，但如果书里使用不同深度或插槽，就必须按 `blocks` 的位置装配，不能把所有内容统一贴在末尾。

| 返回字段 | 接入方怎样使用 |
|---|---|
| `blocks` | 正式装配材料：书籍／版本／条目 ID、原文、角色、位置、原因 |
| `context` | 已转义并用 `<worldbook-entry>` 包装的聚合文本；适合诊断或单一位置注入 |
| `decisions` | 每条是否入选、排除原因；用来回答“为什么这条没被读到” |
| `snapshot` | 本次实际书籍版本与内容哈希；与本轮请求关联，方便复现 |
| `token_count`、`token_budget`、`tokenizer`、`token_scope` | 本服务返回文本的预算，不是整个回复模型请求的 token 数 |
| `degraded` | 混合检索发生了什么降级；空数组表示未记录降级 |
| `policy_version`、`prompt_hash`、`selection_fingerprint` | 选择策略及结果／输入条件的诊断标识 |
| `duration_ms` | 服务处理耗时；不包含后续 RP 模型生成 |

本文样例已用此固定版本的 `BookCreate`、`SelectRequest`、真实选择函数和 OpenAPI 结构验证：选出两条，分别因为关键词与常驻；示例聚合文本为 191 tokens。此验证没有连接数据库或调用模型，不作为线上部署或模型遵循效果的验证。[返回实现](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/selection.py#L337)

## 常驻、动态、必需：谁决定本轮读哪些条目

**决定者分三层：作者决定条目的规则，接入方决定本轮材料与预算，服务程序按规则选择。** 当前这步不需要一个 LLM 阅读全书再挑条目。

| 类型 | 作者怎样设置 | 实际行为 |
|---|---|---|
| **常驻** | `constant: true` | 不要求关键词命中，每轮进入候选；仍受启用、受众、条件、概率、位置、预算约束，并不保证每次一定放入 |
| **动态** | 普通条目填写 `keys`；请求使用 `rules` 或 `hybrid` | 关键词命中，或 hybrid 召回相关条目，再经过统一的过滤和预算；没有命中不代表资料被删除 |
| **必需** | `required: true` | 在通过资格过滤后优先完整装入；依赖、冲突、位置或预算无法满足时可报错。它不是绕过所有过滤的强制开关 |

执行次序：

1. 排除停用、受众不符、状态条件不符、不支持动态运行环境、概率未通过、空内容或无法放置的条目。
2. 激活必需、常驻、关键词命中和 hybrid 召回的条目；按配置允许有限递归激活。
3. 展开同版本内的显式依赖，把“条目＋依赖”作为一组，检查冲突。
4. 按优先级和预算选择：必需优先，其次可选常驻，再处理其他候选；默认常驻可选条目最多使用总预算的 35%。
5. 返回完整原文和实际选择原因。装不下的可选条目整条被排除，不自动缩写成摘要。

例如“守卫手令规则”命中了，但被标记为仅在 `state.quest.started = true` 时允许使用；本轮宿主没有交这项状态，规则仍不会进入。`state` 必须由宿主提供，服务不自行查询剧情数据库。[选择步骤](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/selection.py#L107)

<details>
<summary>扫描、混合召回、预算与递归的具体控制参数</summary>

| 参数 | 服务接口默认值 | 作用 |
|---|---|---|
| `strategy` | `rules` | `rules` 走作者规则；`hybrid` 加入同范围词法＋向量召回，再经相同条件和预算选择 |
| `scan_depth` | 4，范围 0–100 | 扫描“history 的内容＋本轮 query”末尾多少条消息；query 也算一条。4 是消息数，不是四轮对话；0 关闭关键词扫描 |
| `token_budget` | 1500，范围 0–32000 | 返回原文包装后的总预算；不是整个模型上下文窗口 |
| `constant_budget_ratio` | 0.35，范围 0–1 | 可选常驻条目的预算上限占比；必需条目不受此常驻配额限制，但受总预算限制 |
| `max_entries` | 50，范围 1–200 | 最多放入多少条，依赖也计数 |
| `max_recursion_rounds` | 2，范围 0–5 | 首次匹配后额外进行的有限递归轮数；需条目双方允许，不等于默认递归所有内容 |
| `seed` | `"0"`，最多 200 字符 | 概率抽选的稳定种子；相同输入下便于复现。Harness 使用 request ID |
| `audience` | `actor` | 只有条目的 audience 包含它，才有资格参与；不是权限认证 |
| `template_vars` | 空对象 | 内容中 `{{char}}`、`{{user}}` 的替换来源；未提供所需变量则排除该条 |
| `history` | 空，最多 100 条 | 每条必须有 role/content；每条 content 最多 16,000 字符 |
| `query` | 必填，1–8000 字符 | 本轮查询文字；不等于整个系统 Prompt |
| `books` | 必填，1–20 本 | 明确限定书籍范围；单次快照还受条目数量和原文总量上限约束 |

当前选择器先放必需组，再放常驻组，再放其他候选；组内依次看 `required`、高 `priority`、关键词命中、混合得分、高 `order`、稳定 ID。装配输出另按位置、深度及 `order` 排序，不能把“优先入选”理解为“最后放在 Prompt 哪儿”。

次关键词逻辑仅在主关键词已命中、`selective=true` 且有次关键词时参与：`and_any` 至少命中一个；`and_all` 全部命中；`not_any` 一个都不能命中；`not_all` 不能全部命中。

`hybrid` 在同一本书版本范围内结合词法和向量排序；不是 LLM reranker。向量不可用或指定版本索引没就绪时，hybrid 在 `degraded` 标注原因并退回词法候选；纯 `vector` 搜索会直接返回错误。规则选择无需 Embedding。

服务的计数默认 `cl100k_base`，只计算返回的 `context`。接入方仍须计算角色、聊天历史、工具消息和输出预留组成的完整请求。当前 Harness 在世界书装配之后、发模型之前做最终 Actor 请求预算检查；超出时会以 `agent.context_overflow` 失败，世界书 1500／2400 tokens 的小预算不能替代模型总窗口配置。[接口默认值](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/models.py#L168) · [混合降级](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/service.py#L68) · [最终请求预算](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/actor/roleplay-runtime.ts#L845)

</details>

## 原文放进模型输入的哪里

“本轮注入”就是把选中的条目原文装成这次模型请求中的消息。它不表示永久改写角色卡、给模型训练新知识，或把整本世界书塞进历史。

| 条目 `position` | 模型实际看到的位置 | 宿主需要准备什么 |
|---|---|---|
| `before_character`／`after_character` | 角色资料之前／之后 | 角色资料区域；默认可用 |
| `at_depth` | 真实聊天历史按消息深度定位的位置 | 聊天消息位置，默认可用；不把示例和世界书条目当作聊天轮次 |
| `before_examples`／`after_examples` | 对话示例区域前／后 | 明确提供结构化 examples；不能靠猜角色卡内的小标题定位 |
| `author_note_top`／`author_note_bottom` | 作者注释容器内的顶部／底部 | 宿主提供 authorNote；注入内容继承该容器的角色 |
| `outlet` | 宿主指定的命名插槽，例如 `scene_lore` | 条目填 `outlet_name`，宿主声明同名模板与标记 |

服务根据 `placement_capabilities` 先排除宿主不支持的位置。普通条目被排除并记录原因；必需条目找不到位置则报错。服务不声明位置能力时只允许前三种默认位置。当前 Harness 会明确声明本次真实具备的区域，而不是把所有 8 种位置都声称可用。[位置资格](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/placement.py#L6) · [Harness 位置装配](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/worldbook/prompt-layout.ts#L40)

## 接现成 Harness：具体开关、每轮读取与模型边界

**当前已经接好的入口是 `agentic-v2-worldbook` 专用运行配置。** 它要求 Agentic V2、Actor 的模型配置和 preset；不能同时启用 `preActorDirector`、`storyV1` 或 `sumiV1`。这意味着世界书服务能够给客户自己的 Story／生图流程复用，但此固定版本没有“给现成 Story 或 Sumi 加一个 worldbook 开关就自动接好”的组合。[启动配置限制](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/server.ts#L793)

宿主配置片段如下；这是添加到完整 Agentic V2 场景中的字段，不是独立运行所需的全部配置：

```json
{
  "worldbook": {
    "tenantId": "tavern",
    "strategy": "hybrid",
    "tokenBudget": 2400
  }
}
```

`tenantId` 必填，选择资料所在命名空间；`strategy` 在 Harness 默认 `hybrid`，`tokenBudget` 默认 2400。它们不同于独立服务 `SelectRequest` 的 `rules`／1500 默认值。Harness 当前这组配置不直接开放所有服务选择参数，例如 `constant_budget_ratio`、`scan_depth`、`max_entries`；如需逐项控制，要扩展适配层，或从自己的后端直接调用服务。

现成接线每轮做这些事：

| 顺序 | 程序做什么 | 使用的具体材料 |
|---|---|---|
| 1 | `worldbook.conversation.check` 校验身份 | 配置的 tenantId，加可信用户 ID、会话、Prompt、场景；不是固定世界书版本 |
| 2 | `worldbook.bindings.resolve` 读取该 Prompt 关联 | 返回最多 20 本的具体 `book_id + revision`；本轮保留此快照 |
| 3 | 读取已保存的 Agentic V2 状态 | 先尝试解析为 JSON 对象，成功才把字段交给条件选择；普通 V2 保存的栏目文本不能解析为 JSON，条件状态为 `{}`。查询文字仍可从文本的 `Current scene:` 行提取公开场景 |
| 4 | `worldbook.select` 选择一次 | query 是本轮输入＋公开场景文本，最多 8000 字符；传最近最多 20 条 user/assistant 历史，每条最多 16000 字符；服务仍默认只扫描末尾 4 条消息，条目可覆盖深度 |
| 5 | 组装 Actor 输入并检查总预算 | 把 `blocks` 按位置放入角色／历史之间，再生成本轮回复 |
| 6 | 后续新一轮再次解析与选择 | 同一逻辑轮的重试复用已解析版本与选择结果；下一轮重新计算 |

**世界书的条件不会自动读取 Director 笔记里的事实。** 例如，笔记写了“守卫已接过铜钥匙”，不等于选择器收到 `keyHolder:"守卫"` 这个字段。若要按任务进度、人物状态等条件选条目，宿主必须向选择接口提供结构化 `state`；使用当前 Harness 接线则需扩展适配层，把所需字段明确传入。JSON 状态中的 `current_scene.location/time/situation` 可补入查询；普通 V2 文本只有 `Current scene:` 行参与这条查询补充路径。[状态解析与查询生成](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/worldbook/runtime.ts#L59)

**Actor 与后台没有自动共享这批原文。** `WorldbookTurn` 只用于本轮 Actor；慢通道 planner 的输入独立组装，并不会自动收到本轮入选的世界书条目。“知识专家”这个名字也不会额外授予世界书访问能力。若客户希望后台根据同一份规则做校验，需要明确把条目传给后台，或给后台另外接一个带书籍范围的读取工具；这是二开工作。[本轮持有与 Actor 注入](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/worldbook/runtime.ts#L36) · [Actor 状态与查询准备](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/runtime.ts#L350)

## 哪些资料什么时候保存，什么时候生效

| 对象 | 谁触发保存 | 保存到哪里 | 什么时候使用／生效 |
|---|---|---|---|
| 世界书原文、规则、导入报告 | 运营或宿主调用创建／导入／修订 API | PostgreSQL 的书籍、不可变版本、条目记录 | 成功后可用具体版本执行规则选择；不是等用户聊天才保存 |
| 当前版本指针 | 发布新 revision 的同一事务 | 书籍 head | 跟随最新的关联在下次 resolve 读到新版本；固定旧版不变 |
| Prompt 与书籍关联 | 管理端／可信后端调用 PUT | `prompt_worldbook_bindings` | 下一逻辑轮 resolve 生效；本轮已取得的快照不重算 |
| 向量索引 | 明确调用 index 构建 | PostgreSQL 中的向量、索引状态和 Embedding 版本 | 对应精确 revision ready 后可参与向量检索；新原文版本不自动沿用旧索引 |
| 本轮选择结果 | 世界书程序生成 | Harness 当前 `WorldbookTurn` 内存 | 本轮 Actor 和同轮重试使用；不自动写入规范聊天历史 |
| 选择诊断 trace | Demo 的 onTrace 接收 | Demo 可按回复 ID 保存在浏览器缓存 | 用于查看本轮实际装配了哪些条目；不是持久化的世界事实 |
| 用户正文、模型回复、剧情／人物状态 | 聊天宿主和各产品自己的保存流程 | 各自聊天与状态存储 | 世界书服务不代替它们保存，也不自动把剧情变化回写世界书 |

关联保存示例（书籍 ID 替换为实际返回值）：

```json
{
  "bindings": [{
    "book_id": "00000000-0000-0000-0000-000000000001",
    "revision": null,
    "position": 0,
    "enabled": true
  }]
}
```

提交到 `PUT /v1/prompt-bindings?tenant_id=local&prompt_id=guard-role-1`。`prompt_id` 是外部角色／作品 ID，不是 PromptManager 模板 ID。`revision: null` 表示跟随最新版；正整数表示固定版本；`bindings: []` 清空。PUT 原子替换这个 Prompt 的**全部关联**，不是只追加本次列表；最多 20 本，不能重复。

每轮 `GET /v1/prompt-bindings/resolve?tenant_id=local&prompt_id=guard-role-1` 返回：

```json
{
  "tenant_id": "local",
  "prompt_id": "guard-role-1",
  "books": [{"book_id": "00000000-0000-0000-0000-000000000001", "revision": 1}]
}
```

这是**逐轮解析角色关联**，不是“建会话时永久 pin 一版书”。无启用关联返回空 books，Harness 正常聊天且跳过 select；显式关联失效则失败，不悄悄改用别的书。修改书或关联会影响已有会话的下一轮，不会重写已经产生的历史消息。[关联保存与解析](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/repository.py#L173) · [Harness 逐轮解析](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/worldbook/resolve.ts#L4)

<details>
<summary>索引、异常处理、独立部署与身份接入</summary>

**索引。** `POST /v1/books/{book_id}/index?tenant_id=local`，请求 `{"revision":1,"force":false}`。这是同步构建接口；已有相同 Embedding 版本的 ready 索引会复用。正文每 1000 字符取最多 1200 字符块，存在重叠；按批生成 Embedding。生成索引不会改变原文和条目 enabled。失败记录 failed，重试该版本；单版本同时构建冲突返回 `index_busy`。[索引实现](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/service.py#L152)

**部署。** 服务需要 Python 3.12–3.13、PostgreSQL；仓库提供依赖锁文件、Dockerfile、迁移和本地 Compose。仅规则模式 `VECTOR_BACKEND=none` 不调用 Embedding；启用 `pgvector` 后需要对应向量配置和 Embedding 服务／本地后端。原文与向量同在 PostgreSQL，向量可由原文重建。[依赖与启动配置](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/config.py#L8)

**调用身份。** 当前普通 REST 路由要求 query `tenant_id`，服务没有默认租户，也没有实现请求认证。租户是资料命名空间，不是登录凭据。不能把“能够传 tenant_id”理解为“服务会确认这个用户有权访问该租户”。公网接入需要业务后端／网关处理登录、租户授权、管理端权限等，再调用此服务；`/v1/admin/*` 还能跨租户浏览，须放在受控入口。自带 Python client 虽会发送 Bearer Header，当前服务代码不会据此校验用户。[实际租户参数](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/src/worldbook/app.py#L35) · [无认证测试](https://github.com/FlowGPT/worldbook-service/blob/9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b/tests/test_api.py#L60)

**接入 Harness。** 已有调用链是业务请求 → Harness → Axon 的 worldbook RPC → Worldbook Service。Harness 将可信 `X-User-ID` 交给 Axon，用于会话身份命名空间；它不是最终用户可自行宣称的认证凭据。Worldbook Service 本身无需 Axon；直接 HTTP 复用时由自己的后端提供同等权限和版本管理。Axon 属于外部依赖，本文对 RPC 的说明以 Harness 调用端为准，不把外部仓库实现当作本次已扫描代码。

**Demo。** Harness 的世界书 Demo 提供查看关联、聊天和注入诊断；共享导入和关联修改不对 Demo 开放。不能把 Demo 的页面包装直接当成完整对外的世界书管理产品。[Demo 接口边界](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/worldbook/debug-api.ts#L94)

| 情况 | 当前结果 | 接入方应做的事 |
|---|---|---|
| 无 Prompt 关联 | 空书籍列表，现成 Harness 跳过选择继续生成 | 可展示“没有关联资料”，无需伪造选中结果 |
| 无动态条目命中 | 对应条目 `not_triggered`；其他合格条目照常选择 | 检查 query、扫描窗口、关键词、条件与预算 |
| 普通常驻装不下 | `constant_budget_dropped` 或 `budget_dropped` | 缩短／拆分资料，调整常驻配额或总预算 |
| 必需条目无法容纳、依赖失效、冲突 | 明确错误 | 修复资料／预算；不要悄悄删掉必需规则继续 |
| 向量索引未就绪 | hybrid 标注降级；纯 vector 搜索报错 | 检查精确 revision 的索引；明确是否接受词法退化 |
| 修订并发冲突 | 409 `revision_conflict` | 重新读取 head，合并修改后再发布 |
| 同幂等 key 用了不同内容 | 409 `idempotency_conflict` | 新操作使用新 key；同操作重试保留原 key 和原内容 |
| 租户／书籍／版本不匹配 | 404 或明确绑定错误 | 检查资料归属，不能跨租户猜书籍 ID |
| 最终 Actor 输入超模型窗口 | `agent.context_overflow` | 调整完整输入预算或模型配置，不只看世界书 token_count |
| 服务调用失败 | 当前世界书解析／选择接线向上抛错 | 明确产品错误提示与重试；不要描述成现成的“自动无世界书继续” |

</details>

## 二开时能直接复用什么，还需自己补什么

**可以直接复用**：版本化存储、静态资料归一化、Prompt 关联解析、规则／混合选择、精确版本索引、诊断返回；接自己的模型无需先启用整套 Agent V2。调用 `search` 可做资料浏览，调用 `context/select` 可做生成前的资料选择。

**需在宿主实现**：把返回资料正确放进模型窗口、总 token 预算、用户／租户授权、聊天与状态保存、所用产品的接入流程。若让 Agent 自己决定何时查世界书，还需注册一个带明确 scope 与输入返回契约的工具；服务存在不等于它已经挂进每个 Agent 的工具菜单。

**当前没有自动完成**：Story／Sumi 的现成开关组合、后台专家读取同批条目、剧情状态自动修改世界书、从新 NPC 自动生成并持久化独立小卡、完整 ST 动态运行时、独立服务自带的公网用户鉴权。客户如果需要，应把它们明确列为二开范围，而不是世界书默认交付内容。

源码基线：Worldbook `9fd897eafcb23c3e9db6e0a8fdc6bf859fd7753b`；Harness `fa41d220d48327e58d994f936517b23f2f15f658`。本章使用固定源码、类型、路由与样例验证；未把 main 合并状态当作生产开启或客户交付验证。



<a id="atoms-memory"></a>

# 记忆与状态：什么时候写，下一轮拿回什么

**可以二开的能力有两层：**一层是公共包提供的 JSON 读写工具，适合接入自己的数据库；另一层是应用已接好的日记、记忆表和 V2 状态。它们分别保存，改动一处不会自动同步到其他所有地方。

## 独立 JSON 记忆工具

入口：`@flowgpt/agent-core-tools/memory` 的 `createMemoryTools`。宿主把存储绑定到已认证的产品、用户和会话，再交给工具；模型参数里没有任意切换用户或租户的 ID。

| 动作 | 输入 | 直接结果 | 何时保存、谁用 |
| --- | --- | --- | --- |
| `get_memory` | `{}` | `{revision, memory}`；没有记录时 `revision:0, memory:null` | 只读；模型或程序接着使用返回对象，不调用 LLM |
| `update_memory` | `{expectedRevision, memory}` | 写成功后的 `{revision}` | **调用时就执行存储提交**，不是只生成候选；下次 `get_memory` 返回新对象 |

例：先读出版本 7，内容 `{"scene":"城门","keyHolder":"用户"}`；确认守卫接过钥匙后，提交版本 7 和完整新对象 `{"scene":"城门","keyHolder":"守卫"}`；存储成功返回版本 8。之后再读，拿到的是版本 8 的对象。

这里是完整替换，不是字段合并。如果新对象只写 `{"keyHolder":"守卫"}`，原来的 `scene` 会被删除。`expectedRevision` 必须使用产生该修改时依据的版本；冲突后重新读和重新判断，不能拿旧提议配一个新版本强行覆盖。

记忆必须是普通 JSON 对象，不能把数组、`null`、函数或循环对象当成整份记忆。序列化后最多 **64 KiB**，嵌套深度上限 **32**；版本是非负安全整数且小于 `Number.MAX_SAFE_INTEGER`。空记录严格对应 `revision:0, memory:null`，已有记录必须是正版本和对象。`beforeUpdate` 可以做业务字段检查，检查通过后才调用宿主存储；写入后若返回的版本不是旧版本＋1，工具报错，此时要先读回确认，不能当作一定未写入而盲目重试。

[参数、写入与结果](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/memory-tool.ts#L8) · [JSON范围、大小和深度校验](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/memory/json.ts#L1) · [完整读取、冲突和业务接受示例](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/examples/sdk/05-memory.ts#L1)

### 最小宿主接线

```ts
import { createMemoryTools } from '@flowgpt/agent-core-tools/memory';

const [getMemory, updateMemory] = createMemoryTools({
  store: {
    get: signal => scopedStore.get(signal),
    compareAndSet: (commit, signal) => scopedStore.compareAndSet(commit, signal),
  },
  beforeUpdate(memory) {
    if (typeof memory.scene !== 'string') throw new Error('scene 必填');
  },
});
const current = (await getMemory.execute('read-1', {}, signal)).details;
const saved = await updateMemory.execute('write-1', {
  expectedRevision: current.revision,
  memory: { scene: '城门', keyHolder: '守卫' },
}, signal);
```

这是接线片段：`scopedStore` 必须由你实现，数据库中要原子检查版本并写入，返回版本应为原版本＋1。若业务要求用户接受正文后才能改变事实，不要提前让模型直接调用写工具；先产出提议，接受后再由宿主执行写入。工具设置 `replay: "never"`，不应盲目重放结果不确定的写操作。

`/memory` 是工具包的正式导出入口。仓库的发布配置为 GitHub Packages、`restricted` 访问；使用发布包还需要相应包读取权限，不能默认从公共 npm 匿名安装。若按源码构建，也需安装该入口使用的 TypeBox 等依赖。

[memory导出与发布权限配置](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/package.json#L12)

## 产品里已经接好的几种资料

| 资料 | 什么条件下处理 | 什么时候存 | 下次取回什么、怎样用 |
| --- | --- | --- | --- |
| 聊天正文 | 聊天业务接受并保存消息 | 聊天服务自己的保存流程 | 原始消息；业务作为历史提供给生成链路 |
| V2 后台状态 | 普通 V2 后台决定更新并产出有效候选 | 状态校验和版本检查后，通过 Axon CAS 提交 | 文本状态＋分类记录，再投影为 Actor / 后台各自的上下文；详见 [V2 产品](#product-agent-v2) |
| Axon 日记 | 配置 `compassMemory` 来源、相应运行时与日记编排器；轮次、额度等策略允许 | 回复完成事件安排后台；规划允许后生成、解析并 `appendDiary` | 摘要取为 `compassMemory`；需要模板实际引用才会进入正文模型 |
| Axon 记忆表 | 配置 `memoryTable` 来源；回复里有可应用的表格编辑 | 回复完成后提取编辑，确有变化才替换表格；版本冲突至多重读重试一次 | 表格提示文本 `memoryTable`，版本另留供后续写入 |
| Hinos 外部记忆 | 选择 `memoryRuntime:"hinos"` 并配置 `hinosMemory` 来源 | 本轮 Actor 成功产出正文、通过取消检查后，提交该轮用户输入和正文；提交成功要求服务返回 `committed:true` | 下轮向 Hinos 重新 `prepare`，取回服务生成的 `memory` 文本，经模板引用进入 Actor |
| Compact 历史摘要 | 预算或检查点边界触发 | Actor 生成前，摘要和最终输入通过预算及持久条件检查后 | 摘要＋剩余原文；详见 [Compact](#atoms-compact) |
| Story 世界/叙事快照 | Story 自己的准备、正文确认与结算流程 | 按 Story 对消息锚点和存储版本的检查提交 | 世界事实、事件和叙事状态；详见 [Story](#product-story) |

**日记的“累计轮次”不是普通 V2 后台的全局触发规则。**日记节点默认 `min_rounds=20`、免费周期 `free_rounds_per_diary=10`；另一组 memory_v2 配置默认首次 20、后续 10，且是否实际生成、只存摘要还是也存日记，由 `planDiary` 返回及免费/付费策略共同决定。不能简化成“所有记忆每十轮保存一次”。

日记和表格此处收到的是回复完成事件，代码没有在这里额外等待“用户接受这段正文”的确认事件。业务若需要接受之后再保存，应在接入层明确实现该条件，不能用“后台保存”替代产品的接受规则。

[取回摘要和表格](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/axon-memory-runtime.ts#L98) · [何时安排写入](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/axon-memory-runtime.ts#L151) · [日记节点默认参数](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/diary-orchestrator.ts#L91) · [规划、生成和实际保存](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/diary-orchestrator.ts#L269)

### Hinos：外部记忆服务怎样参加一轮

这是已接入应用的另一种记忆运行时，需要部署或接通 Hinos 服务。它不调用 `get_memory` / `update_memory`，也不把 Hinos 返回内容存进 V2 的 Director 状态。

| 时机 | Harness 发送什么 | 服务返回什么、接下来怎么用 |
| --- | --- | --- |
| Actor 回复前 | 调用 `/v1/memory/prepare`，发送已渲染的角色 system 消息、业务提供的 user/assistant 正式聊天（末条必须是当前用户），以及请求、场景、模型配置、Prompt 等来源标识 | 校验协议后取得 `claim_id`、`memory` 文本、`turn`、`state_through_round`。`claim_id` 标识本次准备；它不是 JSON 记忆工具的版本号 |
| 本轮组装 Actor 输入 | `memory` 非空时，把它设为 `hinosMemory`，再执行基础上下文准备 | 模板实际引用 `hinosMemory` 后，Actor 才能读到这段资料；返回空文本则继续使用原有准备结果 |
| Actor 正文生成完成后 | 调用 `/v1/memory/commit`，发送同一 `claim_id`、本轮 `user` 输入和 `assistant` 正文 | 要求响应协议正确且 `committed:true`；这是 Hinos 接收本轮对话的回执，其内部如何提炼、存储不在本仓实现 |
| 下一轮 | 再次调用 `prepare`，提供新的正式聊天 | 取得服务为新一轮准备的 `memory`；不保证上一轮输入每个字都出现在返回记忆中 |

例如，本轮用户说“我答应明天归还铜钥匙”，Actor 回应“守卫点头，记下了约定”。Harness 完成正文后把这两段文本提交给 Hinos。下一轮 Hinos **可能**返回“用户答应明天归还铜钥匙”等记忆文本，实际内容由该服务决定；Harness 负责把返回文本交给本轮模板，不自行把例子中的句子硬编码为记忆。

当前闭环路径会等待 `commit` 调用返回，再发出 `turn.completed`，并未另等用户接受正文。`prepare` 失败会阻止本轮继续生成；`commit` 失败会发出记忆写入失败事件，而当前 Router 的 `memoryIngest:"best_effort"` 允许正文仍完成。已经流出的正文与记忆保存成功是两个结果。取消检查或请求终止可能阻止提交，不能仅凭“看到正文”认定记忆已写入。

启用时，在已有可运行 Scenario 中加入以下字段，并设置服务环境变量 `HINOS_MEMORY_URL`；`HINOS_MEMORY_TIMEOUT_MS` 默认 **10000 ms**。`memorySources` 中必须恰好有一项 `variable:"hinosMemory"`；`id`、`agentRef` 仍是 Scenario 来源配置的必填字段。

```json
{
  "memoryRuntime": "hinos",
  "memorySources": [{
    "id": "external-memory",
    "agentRef": "your-hinos-source",
    "variable": "hinosMemory"
  }]
}
```

服务收到 `x-flow-conversation-id` 与 `x-request-id` 请求头。当前适配器不附带独立认证凭证，接入部署需要自行配置受信服务访问及其权限边界；这里不能把会话 ID 当成鉴权。

[来源检查、prepare协议与记忆返回](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/hinos-memory-runtime.ts#L21) · [commit、请求头与超时](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/hinos-memory-runtime.ts#L112) · [放回Actor输入](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/hinos-memory-runtime.ts#L161) · [生成后提交与完成顺序](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/harness.ts#L317) · [闭环记忆失败策略](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L744) · [服务地址与超时](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/server.ts#L995)

<details>
<summary>应用接入开关与需要接通的依赖</summary>

Scenario 的 `memorySources` 每项包含 `id`、`agentRef`、`variable`，可有 `priority`。Axon Go 接法只支持 `compassMemory` 和 `memoryTable`，没有配置相应来源就不发对应召回请求。当前召回 RPC 按用户、会话和来源取资料，没有把本轮“铜钥匙”这个 query 传去做语义搜索。

`memoryRuntime` 可以选 `personalization`、`axon_shadow`、`axon_go`、`hinos`。`axon_shadow` 比较两套召回，不能理解为已经切换到新的主写入路径；日记编排器在 `axon_go` 下按来源装配。Hinos 还需自己的服务地址和超时。业务接线应跟随这些实际分支，而不是只换文档中的“记忆系统”名称。

不指定 `memoryRuntime` 时，当前 Router 使用 personalization 路径。Axon Go 的日记召回条数还受 `subscriptionCategory` 影响：默认 1、plus 2、ultra 3、max 5；这是召回资料量，不是生成／保存日记的轮数。表格与日记返回的是本次可用的提示文本，日记缓存 `agentRef` 来自所配来源，不能把它误当成模型自动选择的检索工具。

日记依赖额度、锁、轮次规划、模型、解析、保存与可选通知；表格依赖编辑解析、版本替换，以及可选提炼上传。表格替换成功后，后续提炼步骤失败不能证明表格没有写入。模板没有引用返回变量时，“取到了”仍不等于“模型看到了”。

[多种记忆运行时装配](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L811) · [来源与运行时配置校验](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/server.ts#L649) · [表格保存与后续副作用](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/axon-memory-runtime.ts#L177)

</details>

## 二开时的最小验收

连续执行三步：读取旧值 → 触发一次明确修改并检查保存回执 → 新请求读回新值并确认真正放入模型输入。同时测一次版本冲突和一次保存失败。若接日记/表格，还要验证“不满足轮次/没有编辑”时确实不产生相应写入。



<a id="atoms-compact"></a>

# 历史压缩：对话变长以后，模型怎样继续读材料

用户已经与角色聊了很久，但回复模型一次能读取的文本有限。Compact 把较早的一段历史变成摘要，保留最近的原文，再重新计算这次输入是否放得下。它处理的是“这一次把哪些历史交给模型”，不是把所有聊天变成永不丢失、自动检索的长期记忆。

**当前交付形态是应用内的 Compact 运行时、Scenario 配置及检查点存储接线。** 两个公共 npm 包没有把这一整套历史压缩服务直接导出。客户可以使用仓内应用接法，或在源码层移植它并提供模型、计数、历史身份和存储实现。

[配置契约](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L141) · [SDK 公开导出范围](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/index.ts#L1)

## 先看压缩前后真正拿到什么

举例：前面几十条聊天交代了“莉娅借给用户铜钥匙，约定天黑前归还”；最近四条原文仍在讨论城门。用户现在说：“我回来还钥匙了。”

| 材料 | 压缩前 | 压缩后 |
| --- | --- | --- |
| 角色、规则、本轮输入 | 按既有模板组装 | 继续参与本轮；压缩历史不意味着可以删除这些固定材料 |
| 较早历史 | 多条逐字消息，占用较多窗口 | 例如“用户向莉娅借铜钥匙，承诺天黑前归还；归还尚未在历史中确认” |
| 最近原文 | 最新的 user/assistant 消息 | 按配置保留最近若干完整历史组，继续逐字交给模型 |
| 已保存摘要 | 若曾压缩，会读取可继续衔接的检查点 | 与新候选历史一起参与摘要更新，不能重复把已覆盖的历史加入 |
| 提交给 Actor 的东西 | 一组模型消息 | 重新渲染后的模型消息，包含摘要和保留原文；通过 token 计数后才继续 |

这里的摘要是解释性示例。压缩模型会读取当前用户输入以判断重要性，但“用户说要还钥匙”不能仅因此被叙述成“历史中已经归还”。摘要事实质量由压缩提示词、模型和验收决定；运行时重点校验格式、长度、历史覆盖与版本关系。

## 何时执行：按窗口预算，而不是固定聊天轮数

1. 开启 Compact 的请求先取得压缩模型配置，并读取检查点。即使请求只带较短的近期历史，也先检查能否接上之前的摘要。
2. 计算这次可容纳输入的预算：**模型上下文上限 − 最大输出预留 − 安全余量**。
3. 把实际角色提示词、历史、旧摘要、最终注入内容组装起来，计数；不能只估算历史字符数。
4. 超过配置的触发比例时尝试压缩。若旧摘要的覆盖末端即将离开请求携带的历史窗口，也会尝试提前推进检查点。
5. 选择可压缩的已闭合历史组，排除配置要求保留的最近组，调用压缩模型；再将新摘要和保留原文组装成 Actor 输入，重新计数。
6. 只有实际输入放得下，才采用这次压缩结果；具备持久化条件时还会在 Actor 生成之前保存检查点。

例如模型窗口假设为 8,192 tokens，输出预留 2,048，安全余量 512，则输入预算是 5,632；触发比例设为 0.8 时，普通长度触发线为 4,505。这组数字只演示计算方法，**不是所有场景的默认模型窗口**。窗口取自模型配置，输出预算取自本轮实际配置；调大配置不能扩大模型服务自身支持的上限。

[输入预算](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L1326) · [先读取检查点](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L1358) · [触发及保留历史](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L1394) · [压缩后重新计数](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L1455)

## 保存什么、什么时候保存、下一轮拿什么

| 时机 | 产物及保存行为 |
| --- | --- |
| 压缩模型刚完成 | 新摘要暂时只在本次准备过程里，尚未证明最终 Actor 输入放得下 |
| 重新计数通过 | 采用摘要及保留历史。若允许修改检查点、覆盖历史具备持久消息身份、旧版本仍可提交，则 CAS 保存检查点 |
| 检查点内容 | 摘要、覆盖历史的首尾消息 ID 和历史指纹、版本/链路信息，以及模型/模板/计数等审计信息；不是再保存一遍所有聊天正文 |
| 本轮 Actor 写正文 | 使用已经压缩后的输入。检查点在此前已可能保存，不依赖本轮新正文后来是否被用户接受 |
| 下一次请求 | 读取检查点，核对角色/配置投影与历史锚点；可用时读出摘要，再衔接未覆盖的近期原文 |
| 历史删改、重生成或窗口跳跃 | 无法证明旧摘要能与当前历史衔接时，不应继续当成可靠前缀；由代码失效/重建或降级路径处理 |

当前压缩成功而保存失败时，本轮仍可能使用内存中的摘要继续回复；以后请求不保证能读回这次摘要。需要看 `persisted` 或检查点结果，不能把“本轮压缩成功”与“以后一定记住”合成一个状态。

检查点按 **用户＋会话＋Scenario** 读取和保存。应用适配器只在 `chat`、`continue`、`auto_reply` 类型允许修改检查点，其他类型不能借这条路径改写旧摘要；这是组装器收到请求后的权限条件，不表示独立 Auto Reply 建议入口一定会执行 Compact。调用方若直接复用运行时，须明确传入 `checkpointMutationAllowed`。

读回也不是“有摘要就使用”：代码检查格式版本、角色/配置的语义指纹、摘要大小及压缩协议；若覆盖的首尾消息都在当前历史里，还检查覆盖条数与历史内容指纹。至少需要找到旧摘要覆盖的末端消息，才能证明它与后续原文接得上。读失败、配置无法核实或未知格式时，本次不会拿该检查点继续写新版本；已知配置/历史发生变化且允许修改时，先按版本将旧检查点失效，再考虑重建。

[保存前提及覆盖信息](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L1462) · [版本提交](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L1544) · [本轮返回的投影](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L1628)

[请求身份与修改权限](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/integrations/roleplay-axon-adapter.ts#L458) · [读回、校验与失效](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L1150)

## 接入和配置：实际字段放在哪里

以下是仓库 `compact-automemory-v0-native.json` 中 Compact 部分的原样配置。它展示当前解析器支持的字段；`compactorModelConfigId` 需在所接入配置中心真实存在。该场景样例并非普通 V2 的统一默认值，也不是打开整个记忆系统的开关。

```json
{
  "compact": {
    "enabled": true,
    "boundaryMode": "selected_turns",
    "compactorModelConfigId": "RP-Harness-Compact-AutoMemory-v0-Native",
    "triggerRatio": 0.8,
    "keepLastCanonicalGroups": 2,
    "checkpointMutationTimeoutMs": 5000,
    "safetyMarginTokens": 512,
    "maxSummaryTokens": 700,
    "reliability": {
      "version": "v1.1",
      "minGenerationTokens": 256,
      "maxCandidateAttempts": 24,
      "maxModelCalls": 2,
      "maxCompactionMs": 120000
    },
    "profile": "configurable-v2",
    "inputProtocol": "dream_text_v0",
    "outputFormat": "plain_text",
    "lengthPolicy": "accept_bounded",
    "executionPolicy": "bounded_recent"
  }
}
```

| 配置 | 控制什么 | 修改后的实际影响 |
| --- | --- | --- |
| `enabled` | 是否启用整条压缩准备链 | 未配置或关闭不等于模型窗口无限，也不会自动有持久摘要 |
| `boundaryMode` | `selected_turns` 从所选消息推导完整轮次；`explicit_groups` 使用显式历史分组 | 决定哪些历史能作为一个整体压缩；身份/边界不正确会影响安全覆盖与持久化 |
| `triggerRatio` | 长度触发比例，必须大于 0 且小于 1 | 越小越早尝试；不是后台每隔多少轮执行 |
| `keepLastCanonicalGroups` | 至少保留多少近期历史组，0–100 | 不是字符数。设为 0 要用 `configurable-v2` |
| `safetyMarginTokens` | 输入预算中额外留的空间 | 增大会减少给输入的预算；不能扩大模型窗口 |
| `maxSummaryTokens` | 摘要最终长度上限 | 摘要还需和其他材料合起来放得下 |
| `compactorModelConfigId` | 摘要使用的模型和提示词配置 | 与 Actor 模型是两个岗位；内容提炼规则由该配置提供 |
| `profile`、`inputProtocol`、`outputFormat`、`lengthPolicy` | 压缩请求和输出的协议 | 当前属于 **Scenario 的 compact 配置**，不能随意塞到 ModelConfig 当运行开关 |
| `executionPolicy: bounded_recent` | 限定近期候选的处理和降级方式 | 需要显式可靠性配置；可能放弃未覆盖历史，不承诺全历史永久保存 |
| `checkpointMutationTimeoutMs` | 检查点失效/写入操作的超时；未配置时已有 2,000 毫秒默认值，可显式设 1,000–10,000 毫秒 | 显式设置后，写入超时会做一次有界读回确认；重建时失效旧检查点与写新检查点分别使用该时限，避免把不确定写入盲目重放 |

`compact.contentPolicy` 已被明确拒绝；代码要求移除旧字段，通过压缩用 PromptManager/Preset 表达内容提炼策略。本轮问题始终进入压缩材料。普通 V2 同时启用 Compact 时还需要 `contextGovernance`，其中 `enforce` 才让后台采用受控的压缩投影；`shadow` 是观测对照。Sumi 配置当前不允许这个 Compact 开关。

[真实配置样例](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/config/scenarios/compact-automemory-v0-native.json#L9) · [字段校验](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/server.ts#L694) · [协议支持范围](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L93) · [旧字段处理](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L135)

<details>
<summary>协议、可靠性预算与失败行为</summary>

- `generic-json-v1` 与 `configurable-v2` 是当前支持的 profile。只有 `configurable-v2` 接受自选输入/输出协议，且必须显式提供 `reliability`。
- 输入协议支持 `harness_json_v1`、`dream_text_v0`、`automemory_recursive_v1`；输出支持 `structured_json`、`plain_text`。
- 结构化摘要包含 `canonical`（可延续事实）、`interaction`（互动关系）、`open_threads`（未完事项）；纯文本投影为 `{format:"plain_text",text:...}`。不是所有输出格式都返回同一业务对象。
- `lengthPolicy` 默认 `reject`；`accept_bounded` 仅允许纯文本。无论采用哪一种，最终还要通过摘要及 Actor 的总预算检查。
- `maxCandidateAttempts` 限制尝试候选，`maxModelCalls` 限制真正发给压缩模型的调用，`maxCompactionMs` 限制候选压缩阶段的时长。它不是整轮请求时限：最终 Actor 输入计数及检查点保存另按请求/存储时限执行。候选次数不等于付费模型次数。
- 可选递归切片 `recursive.segmentContentTokens` 需要 `configurable-v2` 与 `automemory_recursive_v1`；中间摘要不逐片保存成最终检查点，整个结果通过后再提交。
- 模型出错但原输入仍在窗口内时，可以沿用已有输入；否则逐步裁掉可移除的旧完整组，再计数。`bounded_recent` 裁掉未覆盖历史、破坏旧摘要连接时，也会放弃该次旧摘要。仍超窗则返回上下文溢出错误。
- 配了检查点独立超时时，写超时后最多用 2 秒读回，核对版本、链路及完整提交内容；匹配才认定已保存。版本冲突不通过覆盖来“强行成功”。

[输出投影与格式](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-protocol.ts#L4) · [可靠性策略](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L27) · [保存失败与确认](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L1562) · [历史裁剪降级](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L1654)
</details>



<a id="atoms-context"></a>

# 上下文组装：把资料放到模型真正看得见的位置

**用途：**业务已经取到了角色设定、世界书、历史摘要或工具结果，需要决定它们放在模型输入的哪里、以哪个消息角色出现。

**公共入口：**SDK 的 `placeContextBlocks`、`getPlacementCapabilities`。这是纯消息组装能力，输入消息与资料块，返回新消息数组及放置记录；不调用 LLM，不检索数据库，不保存任何长期资料。

## 一次具体调用

原输入里有角色设定和用户问题。你选择把“进入禁区需要手令”放在角色设定后面：

```ts
import { placeContextBlocks } from '@flowgpt/roleplay-harness';

const result = placeContextBlocks({
  messages: [
    { role: 'system', content: '你扮演城门守卫。' },
    { role: 'user', content: '我递出铜钥匙。' },
  ],
  layout: { character: { start: 0, end: 1 } },
  blocks: [{
    id: 'gate-rule', position: 'after_character', order: 0,
    role: 'system', content: '进入禁区需要手令；铜钥匙不能替代手令。',
  }],
  createMessage: message => message,
});
// result.messages：角色设定 → 手令规则 → 本轮用户输入
// 将这个新数组交给模型，规则才会参与本轮生成。
```

这个例子只展示组装；资料是否可信、是否被授权、是否适合本轮，应由调用前的业务选择完成。组装后也应计数并执行模型预算检查。

[位置、资料块与布局定义](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/context-placement.ts#L1) · [组装函数](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/context-placement.ts#L50) · [完整组装示例](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/examples/sdk/09-context-placement.ts#L1)

## 位置可以改，但需要对应锚点

| 位置 | 放在哪里 | 宿主必须提供什么 |
| --- | --- | --- |
| `before_character` / `after_character` | 角色描述前/后 | `layout.character` 的半开区间 |
| `before_examples` / `after_examples` | 示例对话前/后 | `layout.examples` |
| `at_depth` | 真实聊天历史的指定深度 | `layout.conversation: {messageIndices, emptyIndex}`；前者是原消息数组中真实历史的有序索引，后者是无历史时的插入点。不要把样例、作者备注或 lore 算成聊天历史 |
| `author_note_top` / `author_note_bottom` | 作者备注顶部/底部 | `layout.authorNote` 是原消息数组索引；继承该消息的 role |
| `outlet` | 模板中显式的 `{{outlet::name}}` 插槽 | `layout.outlets: {name: 消息索引}`，block 提供 `outlet_name:name`；每个对应标记恰好一次。内容嵌进该消息，继承该消息的 role |

`depth:0` 在最后一条真实对话之后，`depth:1` 在最后一条真实对话之前；深度超过历史条数时放在最早一条真实对话前。`order` 控制同类放置的顺序，同值再按 ID 排序；相邻区域共享插入点时先遵守区域顺序，不能用一个更小的 order 任意跨越角色/示例区域。没有对应锚点、重复 ID、非法角色或嵌套 outlet 时会报错，不悄悄丢掉已经选择的条目。

`getPlacementCapabilities(layout)` 只按你声明的布局列出可用位置与 outlet 名称；完整的边界、索引和模板标记校验在 `placeContextBlocks` 执行。调用方可以用 `renderBlock` 格式化内容，或用 `renderMessages` 将独立资料块转换成多条消息；作者备注和 outlet 是嵌入宿主消息，不按独立消息的格式转换。

公共组装器支持八种位置，不等于每条业务接线开放了全部位置。世界书服务的选择结果、Harness 当前布局和可用位置应配套，详见 [世界书](#atoms-worldbook)。

## 分析工具的上下文是另一个入口

六类分析工具可以传 `compileContext`，它返回整条 user message 文本；系统岗位说明仍由 `prompt` 提供。先检索资料再编译，不能在同步编译器里期待自动查库。普通 V2 Actor、后台 Orchestrator、专家和 Director 各自组装输入；给 Actor 注入过的世界书不会自动传播到其他模型。

[自定义编译器的职责](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/contracts.ts#L87) · [系统提示和 user 材料的组合](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/memory/advisory.ts#L244)

## 窗口在哪里改，超了怎么办

模型完整窗口由实际模型配置与适配层负责，资料字符截断、世界书 token 预算、Actor 输出预留属于不同限制。`placeContextBlocks` 不替你扩容或自动压缩；组装完成后由宿主计数。普通应用可以使用 [Compact](#atoms-compact) 和相应 `contextGovernance`；自己构建 Agent 时也要明确执行预算策略。

验收时直接检查最终 `messages` 与 `placements`：规则在哪条消息、role 是什么、是否出现一次、给哪个模型；再测缺少锚点和预算超限。不能只检查“世界书接口返回了条目”。



<a id="atoms-images"></a>

# 图片：独立图片资产、聊天配图与视觉连续性

如果客户只想“给一段描述，拿到一张图”，可以复用 **`generate_image_asset` 独立资产工具**。如果客户要“角色先说一段、这段配一张图、图片出现在对应聊天消息中”，还需要正文分段、图片提示词、消息占位、图片服务回填这条应用接线。两者的参数和保存职责不同。

Sumi 是仓库另一个专门的图文应用，拥有自己的生成与保存流程。它不等于给普通 Agent V2 打开一个生图开关；本文先说明可复用能力和已有接法，完整产品入口仍分别见 Agent V2 与 Story。

## 先选要复用的能力

| 需求 | 代码提供什么 | 输入 | 输出与后续动作 |
| --- | --- | --- | --- |
| 为自己的产品生成一张图片资产 | Core Tools `/image` 的 `createImageAssetTool` | 描述、可选参考图、数量与尺寸；接入者绑定图片模型配置 | 任务 ID、图片 URL、逐图审核证据；接入者决定保存在哪、展示给谁 |
| 为当前角色正文配图 | 应用内 `ImagineSkill`、`ImageMcpTool` 与前台 `generate_image` | 用户/会话/角色/助手消息 ID、正文片段、图片位置 | 先保存该消息的 `pending` 图片位置，再调用图片服务；不是同步返回可展示图片的承诺 |
| 后续图片维持人物外貌与场景 | `prepare_visual_world` / `commit_visual_world` 及仓内视觉状态运行时 | 角色、此前消息锚点；本轮完成后再提供正文和旧视觉状态 | 本轮画图的视觉材料，及后续请求可以读取的视觉状态 |
| 使用 Sumi 的整套图文生成 | `apps/sumi` 独立路由与应用 | Sumi 所需角色、历史、状态和模型配置 | 该应用自己的正文、图像描述、状态与图片归档流程；不是本页独立工具的默认效果 |

[独立图片工具](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/image-tool.ts#L1) · [聊天配图模块](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/tools/imagine-skill.ts#L313) · [Sumi 独立入口](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L759)

## 独立图片资产：怎样接入

业务示例：运营输入“灰港城门的铜钥匙，桌面静物特写”，生成一张道具插图，之后由客户自己的后台保存到道具资料里。这里不需要 RP 会话，也不会自动创建 NPC 或世界书条目。

```ts
import { createImageAssetTool } from "@flowgpt/agent-core-tools/image";

// 调用方实现图片后端；工具本身没有默认服务地址或账号。
const imageTool = createImageAssetTool({
  configRef: { source: "image-model", id: "your-image-config" },
  generate: async (input, { toolCallId, signal }) => {
    return yourImageBackend.generate(input, { toolCallId, signal });
  },
});

const result = await imageTool.execute("image-call-1", {
  prompt: "灰港城门的铜钥匙，桌面静物特写，黄铜材质与旧徽记清晰可见。",
  count: 1,
});
// result.details 是后端生成结果与审核证据；检查后再采用和保存。
```

`yourImageBackend` 是客户需要实现的传输适配，不是包内对象。适配必须返回工具规定的结构；仅返回 `{url: ...}` 不符合协议。配置引用由宿主绑定，模型不能通过工具参数自行切换账户或配置。

| 参数 | 真实约束 | 业务含义 |
| --- | --- | --- |
| `prompt` | 必填，非空；执行校验最多 32,768 UTF-8 字节 | 已经准备好的画面描述；工具不会先替你生成描述 |
| `reference_images` | 可选，最多 9 个 HTTP(S) URL，不能带用户名密码 | 顺序要与描述匹配；多参考图是否生效还取决于图片模型 |
| `count` | 可选，1–8；schema 标注默认 1 | 工具不会把缺失值主动补进后端请求，后端也应约定默认数量 |
| `width` / `height` | 可选，正安全整数 | 工具仅校验整数，模型支持哪些分辨率由后端决定 |
| `configRef` | 创建工具时绑定；`source` 为 `multimodal` 或 `image-model`，`id` 非空且不超过 256 字节 | 选择受授权的图片配置，不是用户正文参数 |

<details>
<summary>返回格式、采用条件与失败处理</summary>

返回包含顶层 `moderation_status` 和 1–8 个 `jobs`；每个任务含 `task_id`、`image_urls`、与每张图片一一对应的 `moderation`。审核项的 `asset_url` 必须对应图片 URL。

例如，后端可以提供如下结构（任务、地址和审核字段是业务示例）：

```json
{
  "moderation_status": "COMPLETED",
  "jobs": [{
    "task_id": "image-task-1",
    "image_urls": ["https://assets.example.com/copper-key.png"],
    "moderation": [{
      "asset_url": "https://assets.example.com/copper-key.png",
      "status": "COMPLETED",
      "classification": "SFW",
      "blocked": false
    }]
  }]
}
```

工具会接受结构合法的 `COMPLETED` 或 `ERROR` 审核结果。**结构校验通过不等于允许展示。** 这个工具的校验器只检查任务、URL、逐图证据配对及状态，没有要求或验证 `classification`、`blocked`。因此接入方应像仓库示例一样，另行确认顶层 `moderation_status="COMPLETED"`，并逐张确认 `status="COMPLETED"`、`classification="SFW"`、`blocked=false`，且 `asset_url` 与采用的图片 URL 对应，再保存或展示。

本基线公开示例使用的审核分类字段是 **`classification`**，不是 `label` 或 `decision`。如果客户的图片后端采用其他字段，应在传输适配层转换或实现对应的采用检查；不要只看工具调用没有抛错便放行图片。

这是计费且非幂等的操作，工具声明 `replay: "never"`。调用只提交一次；超时或返回证据不完整时，不能自动重提并假定第一次没发生。任务追踪、去重与已生成资产的恢复由宿主承担。

[结果校验与执行](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/image-tool.ts#L43) · [独立图片接入示例](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/examples/sdk/06-image.ts#L1)
</details>

## 聊天配图：从用户请求到消息里的图片

示例输入：“莉娅抬头看我，给这个瞬间配张图。”当前应用接线按以下顺序执行：

1. **前台控制模型决定下一步。** 开启前台工具循环后，控制模型可以调用 `actor` 生成正文片段，再选择 `generate_image` 给片段配图；另一路是调用 `actor` 时指定 `responseMode="visual_beat"`，该段正文成功后由程序自动安排这张图，不需要模型再为同一段调用一次 `generate_image`。工具名不是用户直接上传的画图指令，具体如何选由控制提示词和模型决定。
2. **程序准备画图材料。** 首次实际画图时，读取或初始化视觉世界；取得角色卡、角色 Prompt、持久角色状态，以及已投影的人物/线索/世界材料。
3. **图片提示词模型写画面描述。** 输入包括原用户要求、这段正文、图片序号、人物永久外貌及当前视觉状态。输出必须是一个带镜头、比例等属性且正文符合规定结构的 `<image>` 块。用户指定的画面重点优先于默认构图。
4. **等待业务把助手消息保存下来。** `ImagineSkill` 按助手消息 ID 读取消息。默认最多等 120 秒，每 200 毫秒检查；消息尚不存在时，无法给它插入图片位置。
5. **先保存图片位置。** 在这条消息的 `narrative.sections[index]` 写入 `type:image`、`status:pending`、空 URL 等；对应描述存入 `narrative.imagePrompts[index]`。失败则停止该次图片提交。
6. **调用图片后端。** `ImageMcpTool` 通过 MCP `generate_image`，或配置了直接视觉运行时后的 Image Agent HTTP 接口，提交对应消息和位置。最终图片生成、审核和消息回填依赖下游服务。
7. **整轮完成后处理视觉连续性。** 如果本轮确实准备过视觉状态，且 Harness 完成成功，程序安排后台 `commit_visual_world`，用正文和本轮旧视觉状态进行更新；不会在每张图片之后都重复结算整轮。

**正文生成完成、图片任务已提交、图片可展示、视觉状态已保存是四个不同时间点。** 普通 V2 的后台导演笔记也不是这个图片占位或视觉状态。

[接线与任务调度](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L1004) · [图片模型材料](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L1060) · [图片提示词生成与校验](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/tools/imagine-skill.ts#L63) · [消息位置保存](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/tools/imagine-skill.ts#L321) · [整轮视觉状态结算](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/tools/imagine-skill.ts#L268)

前台两种配图触发及重复限制见 [前台配图决策与自动安排](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/actor/roleplay-runtime.ts#L327)。前台工具结果返回图片序号或任务受理信息，让控制模型继续当前正文；最终图片 URL 由下游生成和消息回填链路提供。

## 开启需要什么，不能只填一个工具名

| 条件 | 当前应用要求 |
| --- | --- |
| 前台工具控制 | 场景有 `preActorDirector` 和对应控制模型配置 |
| 生图技能 | `skills` 至少一项的 `afterActorTools` 包含 `generate_image` |
| 图片描述生成 | 提供 `imagePromptProducerPrompt`，由控制模型配置执行描述生成 |
| 下游依赖 | 接好 `ImagineSkill`、图片服务、角色资料与消息读写服务 |
| 消息归属 | 请求提供真实的 `assistantMessageId`，业务会保存对应消息 |
| 兼容关系 | 当前 Story、专用 Worldbook 场景与这个前台控制开关互斥；Sumi 走独立路由 |

这些是应用内部接法，`generate_image` 不是 Core Tools `/image` 子入口里 `generate_image_asset` 的别名。如果客户只需要图片资产，直接用独立工具；如果需要沿用这套聊天交付，需要接好上表依赖或自行实现同等职责。

## 异常时读者应该预期什么

- 图片描述模型失败时，应用记录降级原因，改用正文里的现成 `<image>` 块；正文没有图片块时，就把整段文字包成 `<image ratio="16:9">正文</image>`。这一步不再调用另一个描述模型，不应承诺仍有完整镜头结构。
- 多图片任务按顺序交接，已有任务的等待会影响后续图片到达；代码中的后台超时预算不是对客户承诺的图片出图时间。
- `pending` 已保存之后，下游提交仍可能失败。这段 Imagine 接线没有在所有失败路径里自动把该位置改为终态；业务需读取任务及消息状态，不能把 `pending` 当作成功。
- 没有配置图片依赖时，普通 Actor 回复和后台专家分析不自动获得生图能力。

[描述失败的降级](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L1108) · [提交错误处理](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/tools/imagine-skill.ts#L362) · [场景兼容规则](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/server.ts#L761)



<a id="atoms-instructions"></a>

# DIO：把明确的长期要求用于后续角色回复

用户说“从现在起莉娅都用短句回答，少用比喻”，这是一条希望持续生效的角色要求。现有 DIO 接线把这类要求提交给外部指令服务处理；后续 RP 请求再读取服务产出的有效提示词，让回复模型使用。

**可复用形态：仓内的 `DioTool` 服务适配器，以及前台工具循环中的 `schedule_dio` 动作。** DIO 服务本身不在本次两个仓库里；本仓能验证的是提交、查询、有效提示词读取和注入链路，不能据此承诺外部服务内部怎样编辑或审核每条要求。

[服务适配器](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/tools/dio-skill.ts#L52) · [前台提交接线](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L974)

## 从一句要求到后续生效

| 时机 | 谁做什么 | 输入与结果 | 保存与生效 |
| --- | --- | --- | --- |
| 用户提出长期要求 | 前台控制模型按提示词判断是否调用 `schedule_dio` | 本轮原文：“从现在起莉娅都用短句回答，少用比喻。” | 仅模型选择，不等于要求已修改成功 |
| 提交 | 程序调用 DIO `POST /v1/chat` | 用户、RP 会话、目标角色 Prompt、原用户输入；返回已接受任务的身份信息 | 外部任务已受理；当前 RP 回复继续 |
| 后台执行 | 程序每秒查询任务，直到终态或后台超时 | `processing` → `succeeded` / `failed` / `timed_out`；附 `promptChanged` 等 | 指令编译与持久化由 DIO 服务负责 |
| 后续请求准备 | 开启相同接线的 RP 入口查询有效提示词 | 按用户、RP 会话、目标 Prompt 读取 `compiledPrompt`、来源版本及哈希 | 验证内容哈希后写入当前请求上下文 |
| 后续 Actor 回复 | 回复组装器用有效提示词替换指定的角色提示词位置，再展开模型消息 | 控制说明、历史与本轮问题仍按原模板保留；替换位置放入 DIO 编译后的完整角色提示词 | 影响这次新生成；已经输出的旧正文不重写 |

受理结果中的 `appliesFrom: "next_turn"` 表示后续轮次使用这条通路；**不是保证紧接着的一句一定等得到新要求。** 如果任务仍在处理、没有生成新 Prompt，下一句只能读取当时服务实际已有的有效内容。

当前程序把**原始用户输入**发给 DIO，没有让前台模型另外编造一份要提交的文本。控制提示词要求只在用户明确要求长期变化时使用该动作；执行端主要验证工具可用、参数和重复调用，不额外运行一个语义模型重新判断用户意图。

[提交原始输入及后台查询](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L974) · [下一轮读取](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L775) · [有效内容与哈希校验](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/tools/dio-skill.ts#L113)

## 有效提示词怎样真正进入 Actor

下一轮查询得到有效 `compiledPrompt` 后，入口把它放到请求的 `dioCompiledPrompt`。随后 Actor 组装器执行 `applyDioCompiledPrompt`，并把原文放入模板变量 `dioCompiledPrompt`，再由原有模板展开流程生成 Actor 的实际消息。

替换规则是明确的：

1. 如果 PromptManager 有且只有一个 `name="roleplay"` 的提示词，替换它的整个内容，角色和顺序保持不变。
2. 如果没有 `roleplay` 提示词，则要求所有提示词合计只有一个 `{{getvar::systemPrompt}}` 占位，把该占位改为 `{{getvar::dioCompiledPrompt}}`。
3. 重复的 `roleplay`、没有可用位置、或没有 `roleplay` 却有多个 `systemPrompt` 占位，均会抛出错误；程序不随意挑一个位置，也不把长期要求追加到整份上下文末尾。

例如，原 Actor 模板为“控制说明 → 角色提示词 → 历史 → 本轮问题”，新一轮变为“控制说明 → DIO 编译后的完整角色提示词 → 历史 → 本轮问题”。这意味着 DIO 交付的是**可替换该角色位置的完整编译提示词**，不能只交一句“少用比喻”并假定原角色设定仍然保留在这个被替换的位置。

仓库测试检查了最终 `prepared.history` 确实含 DIO 内容，并检查控制说明、历史与当前问题保留；因此这里不是仅把字符串读回日志、却没有交给 Actor 的半成品接线。DIO 服务如何把旧角色资料与新要求编译成这段文本，仍属于外部服务内部职责。

[角色提示词替换规则](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/integrations/roleplay-axon-adapter.ts#L132) · [绑定变量并继续组装](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/integrations/roleplay-axon-adapter.ts#L435) · [最终 Actor 消息与歧义失败测试](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/test/closed-loop-parity.test.ts#L633)

## 如何接入

接入现成 RP 应用时，需要场景开启 `preActorDirector`，服务依赖中提供 DIO 适配器，并带正确的用户、会话、目标 Prompt 身份；Actor 的 PromptManager 还必须满足上述唯一角色提示词位置要求。只填写 `schedule_dio` 字符串不能建立服务连接。

客户若在仓内二开，可以复用适配器。它是应用源码模块，当前不是 Core Tools 的公共子入口：

```ts
// 仓库内代码位置：apps/emochi/tools/dio-skill.ts
const dio = new DioTool(dioServiceUrl, requestTimeoutMs);
const accepted = await dio.schedule({
  actorTurnRequestId: "rp-turn-101",
  userId: "user-42",
  rpConversationId: "conversation-8",
  targetPromptId: "lia-character",
  instruction: "从现在起莉娅都用短句回答，少用比喻。",
  language: "zh",
  userAuthorization: userAuthorizationHeader,
}, signal);
```

示例中服务地址、超时、授权头和 `signal` 由接入者提供。适配器派生 `dio_conversation-8` 作为 DIO 会话，`rp-turn-101:dio` 作为请求 ID，`rp-turn-101:dio:assistant` 作为任务消息 ID；收到的受理身份必须与这次提交一致，否则抛错。

<details>
<summary>接口、返回字段及失败边界</summary>

| 方法 | 接口 | 关键返回 |
| --- | --- | --- |
| `schedule` | `POST /v1/chat` | 验证 `ok`、`accepted`、会话、Prompt、消息、请求 ID；转换为 `{accepted:true,dioConversationId,requestId,taskMessageId,appliesFrom:"next_turn"}` |
| `waitForTerminal` | `GET /v1/director/tasks/by-request/{requestId}` | 任务状态、`promptChanged`、可选 `sourceVersionId` / `compiledPromptSha256` / 错误码与信息 |
| `activePrompt` | `GET /v1/director/active-prompt?userId=…&conversationId=…&targetPromptId=…` | 没找到返回 `undefined`；找到则返回版本、变更集、完整编译提示词及 SHA-256 |

应用后台轮询任务预算为 16 分钟；不是每次 RP 都等 16 分钟，也不是服务处理时效承诺。成功终态仍可能 `promptChanged:false`，不能据“任务成功”就宣布角色要求已改变。

有效提示词**查询或哈希校验失败**时，入口记录降级事件，继续使用正常 RP 材料；显式取消仍向外传播。与此不同，已经读到有效内容，但后续发现 Actor 模板缺少或重复替换位置时，组装器会报错，不走上述查询失败降级。接入验收需要同时验证“服务能返回有效内容”和“Actor 模板能够正确采用”。

长期要求不是普通 V2 的持续笔记，不保存在 `update_director_state` 里，也不自动写世界书或创建独立角色小卡。

[受理请求与字段校验](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/tools/dio-skill.ts#L65) · [终态返回校验](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/tools/dio-skill.ts#L144) · [读取失败的处理](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L794)
</details>



<a id="atoms-runtime"></a>

# 编排与扩展：做自己的 Agent、工具循环和快慢协作

**用途：**业务可以复用公共执行组件，自己决定模型要完成什么、能够调用哪些工具、结果由谁采用。这个入口不自动附带普通 V2 的六专家、Director 或 Story 的剧情策略。

## 三个可以单独使用的公共入口

| 入口 | 你提供什么 | 程序实际做什么 | 得到什么 |
| --- | --- | --- | --- |
| `Agent<TInput, TResult>` | 实现 `prepare`，返回原生 Pi `options`、`prompt` 和 `result` 解释器 | 每次 `run` 新建一个 Pi Agent，执行对话/工具循环，传递取消 | `result` 从 Agent 消息中提取的业务结果；没有默认状态写入 |
| `SlowTurnRuntime` | 一次准备好的上下文、模型、系统提示、可选工具与预算 | 运行受限制的模型—工具循环，可等待完成 | `final`、`toolResults`、调用量、状态结果等 |
| `FastSlowRuntime` | 快路执行器＋慢路配置＋状态仓库 | 先读同一份状态并冻结本轮材料，再启动慢路及快路 | `fast` 和 `slow` 两路结果、起始快照、`cancel` |

`Agent` 入口已经具备通用模型选工具循环；不能写成“Harness 的 agentic 编排都待支持”。同时，普通 V2 应用自己的两阶段规则并不因此自动替换为这个通用循环。

三者都是 SDK 根入口 `@flowgpt/roleplay-harness` 的公开导出。`apps/emochi` 下的 Actor、应用协调器与 profile 节点执行器不在这个包的公开导出范围内；安装 SDK 不会同时装出一套可直接运行的 RP 产品。固定源码的 package.json 标注 SDK `0.3.0`、Node.js ≥22、受限 GitHub Packages；这个版本号本身不能证明某个仓库新增字段已在注册表中的同号包发布。

[通用 Agent 的 prepare/run](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/agent.ts#L3) · [独立慢路实际循环](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/slow-runtime.ts#L64) · [读取状态与启动快慢两路](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/runtime.ts#L89)

[实际公开导出](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/index.ts#L1) · [包范围、运行环境与发布目标](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/package.json#L1)

### 接线片段：调用自己的快慢两路

下面用公共入口展示接入位置。`replyModel`、`analysisModel` 是宿主实现的模型适配器；`scopedStateStore` 绑定业务身份并实现读与 CAS；`registeredTools` 是宿主明确允许的工具执行器列表；`validateStateProposal` 是自己的采用规则。它们都不是安装包后自动出现的服务。

```ts
import { FastSlowRuntime } from '@flowgpt/roleplay-harness';

const runtime = new FastSlowRuntime({
  fast: { model: replyModel, modelConfigId: 'reply-config' },
  slow: {
    model: analysisModel, modelConfigId: 'analysis-config',
    prompt: '检查连续性；把已确认事实与后续建议分开。',
    capabilities: registeredTools,
    state: scopedStateStore,
    prepareCommit: (final, results, context) =>
      validateStateProposal(final, results, context),
  },
});

const turn = await runtime.run({ context: {
  requestId: 'turn-2', sourceTurn: 2, sourceMessageId: 'user-message-2',
  language: 'zh', input: '我递出铜钥匙。',
  history: [{ role: 'assistant', content: '莉娅站在城门旁，等你归还钥匙。' }],
}});
const reply = await turn.fast;    // reply.content 交给自己的聊天业务
const background = await turn.slow; // 检查 status 和 stateOutcome
```

该例使用 `run()`，返回完整快路结果。若前端需要流式正文，改用 `stream()` 并提供真实流式快路执行器。`validateStateProposal` 只能返回符合契约的候选或 `undefined`，不能直接返回任意数据库对象。产品级服务通常复用 runtime；服务关闭时调用 `shutdown()`，并处理尚未完成任务的结果。

模型适配器实现 `complete(request, signal)`，返回 `content`、`toolCalls`、模型身份和所需 usage；状态仓库提供 `get` / `compareAndSet`。仓库的完整示例连同假模型、工具适配与存储实现可以直接查看并运行，示例里的固定文本不代表真实模型质量。

[完整工具接线与快慢调用](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/examples/sdk/07-fast-slow.ts#L18) · [上下文与状态仓库示例](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/examples/sdk/support/runtime.ts#L5) · [通用 Agent 的 prepare 与 result 示例](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/examples/sdk/01-agent.ts#L10)

## 主模型实际收到什么

`SlowTurnRuntime` 给主模型的消息依次是：你配置的系统提示词、已保存状态的 system 消息、传入的历史、本轮 user 输入；提供给模型的工具列表来自你传入的 `capabilities`。没有自动读取业务数据库、世界书或角色卡的步骤。

每次模型返回工具请求，程序解析参数、验证这一批请求，再调用已注册执行器。结果回到模型消息中，模型可以继续选择工具或输出最终内容。调用方必须解释 `final.content` 与 `toolResults`，不能把任意文字直接当作一份合格的业务状态。

`SimpleFastRuntime` 使用同一份起始状态、历史和输入调用快模型。它的默认 `stream()` 先等待 `complete()` 再输出一个完整块；要真正逐 token 交付，需要提供自己的流式快路执行器。

[快照、历史与本轮消息组装](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/runtime-shared.ts#L154) · [主模型输入及工具回传](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/slow-runtime.ts#L82) · [默认快路的流式行为](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/runtime.ts#L12)

## 结果何时保存

`SlowTurnRuntime` 可以只做分析。未配置 `prepareCommit` 时，不自动写状态；配置后由该回调检查模型结果，返回 `{state, toolsCalled?, ledgerEntries?}`，或返回 `undefined` 表示本次不改。

只有回调给出候选，才调用宿主的 `state.compareAndSet`，使用本轮读取的版本、来源消息和轮次做检查。版本冲突返回 `stale_write_rejected`。数据库、权限与原子检查由宿主实现，SDK 没有自带持久数据库。下一轮 `FastSlowRuntime` 才会重新读当时已保存的版本。

**例：**用户要求角色以后说话简洁，快路先承接本轮对话；慢路分析后返回风格偏好提议。宿主只允许白名单字段，通过才写入。下一轮重新取出这份偏好并作为输入，才可能影响说话方式。若没实现 `prepareCommit` 或没把保存值交回下一轮，分析不会自行变成长期能力。快路也不应在保存尚未确认时把“永久记住了”当成已完成事实。

默认 `prepareCommit` 只拿到慢路最终模型结果、工具结果和起始上下文，**没有本轮快路最终正文或“业务已接受正文”的标志**。若你的产品要求正文接受后才能改状态，应采用等待式 `SlowTurnRuntime`，先得到分析结果，再由业务接受边界显式提交；不能假设 `FastSlowRuntime` 已自动等快路完成。

[候选到 CAS 保存](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/slow-runtime.ts#L153) · [状态快照与写入字段](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/contracts.ts#L96)

<details>
<summary>预算、gate、取消和工作流接入参数</summary>

| 配置 | 源码默认或要求 | 影响 |
| --- | --- | --- |
| `limits.maxSteps` | 4 | 慢路模型步骤上限 |
| `limits.maxToolCalls` | 8 | 整轮工具调用上限 |
| `limits.maxParallelTools` | 4 | 一批允许的工具调用数量上限 |
| `limits.timeoutMs` | 180,000 | 慢路期限 |
| `limits.maxTotalTokens` / `maxCostUsd` | 可选 | 调用前检查已累计值，调用后检查本次 usage；缺少所需计量时失败。一次调用仍可能使累计超额，不能理解成供应商绝不超支的硬上限 |
| `gate(context, signal)` | `slow.gate` 可选；每次慢路 run 开始评估一次 | 返回 `{run:false}` 跳过慢模型循环、工具与提交；结果为 completed、保留状态、慢循环调用计数 0 |
| `prepareCommit` | 可选 | 解释结果与决定是否更新状态 |
| `cancel()` / 外部 signal | 显式取消 | 传播到两路与相关任务；快路失败本身不能当作慢路保存已取消的保证 |

gate 如果自身调用模型，该调用和成本要由宿主记录；慢循环计数为 0 不代表整项业务毫无开销。gate 抛错、取消或返回非法对象会进入失败路径。它没有自动接入普通 V2 应用，也不能当作“普通 V2 现在累计 X 轮才运行”的证据。该能力在本次主干中有实现，使用发布包前需核实具体版本是否包含。

`gate` 的合法结果必须有 boolean `run`，另可含 `reason`、0–1 的 `confidence` 与非负 `latencyMs`；跳过时没有 `final`，读取结果前需要判空。`FastSlowRuntime` 仍先读状态、再调度慢任务和快路；它不是让 gate 在快路之前统一决定本轮是否执行。

两路是同一进程中的异步任务，不是各自创建一个系统线程。快路失败、用户停止读取快路迭代器，不会自动等价于 `cancel()`；需要取消整轮时显式触发 `cancel` 或外部 signal。取消会终止等待并传递信号，但不能撤销已经发生的外部工具副作用或数据库提交；后端仍需配合取消和幂等。`drain()` 只等现有后台任务，`shutdown()` 发出取消再等；两者到等待期限都可返回，不能作为“所有外部写入一定停止”的证明。

后台任务是进程内执行，不是可跨进程恢复的持久任务队列。需要可靠工作流时，把 `SlowTurnRuntime.run` 作为可等待的步骤，持久化任务输入、结果与业务接受状态，再由外部工作流决定重试和提交。`status:'failed'` 是返回结果，不一定抛异常，工作流步骤应主动检查。

[预算与 gate 定义](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/runtime-shared.ts#L47) · [默认限制](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/runtime-shared.ts#L144) · [跳过与失败行为](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/slow-runtime.ts#L69) · [等待分析与业务接受后提交的完整示例](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/examples/sdk/08-workflow-step.ts#L24)

[drain、shutdown 与共享取消](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/runtime.ts#L75) · [进程内异步任务](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/tasks.ts#L23) · [按模型响应计量预算](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/slow-runtime.ts#L275)

</details>

## 工具注册和 Hook 怎样接

注册表保存的是工具定义和执行器；不会扫描仓库把所有函数自动挂给模型。`AgenticCapabilityRegistry` 直接构造时参数校验默认关闭，设置 `validateArguments:true` 才按 Schema 强校验；`SlowTurnRuntime` 使用自己的受控注册与调用校验。通用 Pi Agent 则由 `options.initialState.tools` 明确提供工具。

Core Tools 基类的生命周期是 `validate → beforeExecute → 再次 validate → perform → afterExecute`。`afterExecute` 失败时底层副作用可能已经完成，不自动重跑。`afterTurn` 必须由宿主在整轮结束后显式调用，`execute` 不会自动触发它。

已有 Workflow 可以在一个节点直接 await 工具，也可以把 `SlowTurnRuntime.run` 作为一个节点；依赖执行顺序由该 Workflow 决定。仓内另有下面这套 Profile 固定流程，可以按业务规定的顺序串联工具。

[注册与参数校验开关](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/registry.ts#L32) · [工具生命周期](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/tool.ts#L3) · [Hook 接口](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/hooks.ts#L1) · [完整通用 Agent 示例](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/examples/sdk/01-agent.ts#L1) · [完整快慢协作示例](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/examples/sdk/07-fast-slow.ts#L1)

## 固定 Workflow：先查资料，再形成写作要求

**可复用形态是仓内程序组件，当前不是公共 npm SDK 导出。** `StaticPlanner` 读取业务定义的节点图，`CapabilityExecutor` 按依赖调用工具，`RPPromptCompiler` 把明确指定的文本结果整理到输入区域。模型不用先判断要不要查这份资料：只要走到这个流程，程序就按定义执行。

比如用户说“我递出铜钥匙”，你希望每次都先查询城门通行规则，再结合规则形成写作要求，最后交给正文模型。业务可以定义：**查规则 → 准备场面要求 → 组装模型输入 → 调用自己的正文模型**。这套组件负责前三步的节点执行和材料整理；模型调用、正文保存与接受规则仍由接入者或选用的应用运行时负责。

| 组件 | 接入位置 | 负责什么 |
| --- | --- | --- |
| `StaticPlanner.plan(request, profile)` | `apps/emochi/planning/static-planner.ts` | 校验请求与 Profile 对应、节点可见性、重复节点、依赖缺失及环；输出按依赖排序的 `ExecutionPlan` |
| `CapabilityExecutor.execute(plan, request, signal, emit, additionalCapabilities?, parallel?)` | `apps/emochi/capabilities/executor.ts` | 调用已注册的节点执行器，传递本轮请求、节点参数和已完成输出；返回 `{outputs, traces}` |
| `RPPromptCompiler.compile({axon, memory, plan, capabilities})` | `apps/emochi/prompt/compiler.ts` | 将基础材料、记忆与节点的 `promptContent` 组成 `blocks`，并生成 `foundation_context` / `turn_context` 两个文本区域 |
| `McpCapability` | `apps/emochi/capabilities/mcp-capability.ts` | 把宿主提供的 `MCPAdapter.invoke` 包装成一个 Capability；提供工具名、节点参数、本轮 `query` 和已完成的 `dependencies`，再由回调提取提示文本/资产 |

`McpCapability` 没有默认 MCP 服务地址、账号或网络客户端。接入者实现适配器并绑定允许使用的服务；如果只是调用本地函数，直接实现下面示例的 `Capability.execute` 即可。

[固定图的校验与排序](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/static-planner.ts#L4) · [执行入口](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/capabilities/executor.ts#L18) · [编译结果](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/prompt/compiler.ts#L33) · [外部工具包装](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/capabilities/mcp-capability.ts#L4)

### Profile 和节点里分别填什么

| 字段 | 作用与条件 |
| --- | --- |
| `profile.id` | 本次流程标识，必须等于 `request.scenarioId` |
| `modelConfigRef` / `presetRef` | 完整 `ScenarioProfile` 的模型/提示词引用；单独规划及执行节点时不会读取这两个配置，也不会调用模型 |
| `visibleCapabilities` | 此 Profile 允许节点引用的能力名称；还必须为名称注册实际执行器，名字本身不会加载代码 |
| `executionGraph.nodes` | 业务定义的固定节点列表；程序根据 `dependsOn` 排序 |
| 节点 `id` / `capability` | `id` 是本流程内唯一节点 ID，供依赖与结果定位；`capability` 是执行器的注册名称，可以多次调用同一能力但要使用不同节点 ID |
| 节点 `input` | 可选参数对象，原样交给执行器；这条执行器不会自动按 JSON Schema 校验它，应在能力内部校验 |
| 节点 `dependsOn` | 必须先完成的节点 ID 列表；依赖失败时怎样处理见下表 |
| 节点 `required` | 该工具抛错时是否把流程判为必需步骤失败；不能代替输入、权限和存储校验 |
| 节点 `projectTo` | `foundation_context`、`turn_context` 或 `none`；只决定该节点明确返回的 `promptContent` 放在哪个编译区域 |
| `defaultTimeoutMs` / `failurePolicy` | 完整 `TurnHarness.run` 采用的轮次默认超时及记忆读取/写入失败策略；单独调用 Planner/Executor 不会自动读取它们来设定时限或保存记忆 |

`failurePolicy` 的实际字段为 `memoryRecall: "fail_turn" | "continue_without_memory"`、`memoryIngest: "fail_turn" | "best_effort"`。这是记忆步骤的策略；工具节点失败由 `required` 处理。能力元数据中的 `readOnly`、`idempotent` 等描述性质，执行器不会据此自动撤销写入或保证幂等。

[能力、节点与 Profile 字段](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/domain/contracts.ts#L91) · [Capability 调用与返回契约](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/ports.ts#L131) · [完整 Harness 的超时与记忆策略](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/harness.ts#L145)

### 最小可运行示例：看到工具结果进入最终 messages

下面两个工具是**示例里由接入者新定义的工具**，不冒充仓库自带的世界书 API。它们使用内存资料演示接口；换成真实世界书或其他服务时，在各自 `execute` 内完成调用和校验。

在已安装依赖并完成 `npm run build` 的源码仓库根目录，将此段保存为 `workflow-demo.mjs`，运行 `node workflow-demo.mjs`。使用的是仓内构建产物路径，不是公共 SDK 的导入路径：

```js
import { StaticPlanner } from './dist/apps/emochi/planning/static-planner.js';
import { CapabilityExecutor } from './dist/apps/emochi/capabilities/executor.js';
import { RPPromptCompiler } from './dist/apps/emochi/prompt/compiler.js';

const request = {
  requestId: 'workflow-1', scenarioId: 'gate-workflow',
  actorId: 'guard', input: '我递出铜钥匙。', stateVersion: 0,
};
const profile = {
  id: 'gate-workflow', modelConfigRef: 'actor-config', presetRef: 'actor-preset',
  defaultTimeoutMs: 30000,
  failurePolicy: { memoryRecall: 'fail_turn', memoryIngest: 'best_effort' },
  visibleCapabilities: ['read_gate_rule', 'prepare_scene'],
  executionGraph: { nodes: [
    { id: 'rule', capability: 'read_gate_rule', dependsOn: [],
      required: true, projectTo: 'none', input: { location: '城门' } },
    { id: 'scene', capability: 'prepare_scene', dependsOn: ['rule'],
      required: true, projectTo: 'turn_context' },
  ] },
};
const metadata = name => ({ name, readOnly: true, idempotent: true,
  longRunning: false, streaming: false, cancellable: true });
const tools = [
  {
    metadata: metadata('read_gate_rule'),
    async execute({ input }, signal) {
      signal.throwIfAborted();
      if (input.location !== '城门') throw new Error('未知地点');
      return { value: { rule: '进入禁区需要手令；铜钥匙不能替代手令。' } };
    },
  },
  {
    metadata: metadata('prepare_scene'),
    async execute({ dependencyOutputs }, signal) {
      signal.throwIfAborted();
      const rule = dependencyOutputs.get('rule')?.value?.rule;
      if (typeof rule !== 'string') throw new Error('缺少已查到的规则');
      const instruction = `${rule} 守卫应检查手令，不把递出钥匙写成已经获准通行。`;
      return { value: { instruction }, promptContent: instruction };
    },
  },
];
const plan = new StaticPlanner().plan(request, profile);
const capabilities = await new CapabilityExecutor(tools).execute(
  plan, request, AbortSignal.timeout(30000), () => {},
);
const compiled = new RPPromptCompiler().compile({
  axon: { history: [], foundationBlocks: [] },
  memory: { version: '0', items: [] }, plan, capabilities,
});
const messages = [
  { role: 'system', content: '你扮演城门守卫，承接用户动作。' },
  ...compiled.blocks.map(({ role, content }) => ({ role, content })),
  { role: 'user', content: request.input },
];
console.log(JSON.stringify({ outputs: capabilities.outputs, messages }, null, 2));
// 自己的模型适配器接收 messages 后，材料才真正参与生成。
```

执行后，`outputs` 中有原始规则对象与场面要求对象；`messages` 是“角色岗位 → 场面要求（内含手令规则）→ 用户输入”。`rule` 节点设为 `none`，所以它不会被重复直接注入；`scene` 节点显式返回 `promptContent`，才产生模型可读的文本块。这段示例不调用真实模型，也不写数据库。

### 执行结果、失败及进入模型的边界

| 情况 | 实际行为 | 二开时需要做什么 |
| --- | --- | --- |
| 默认执行 | `parallel` 默认为 `false`，按排好序的节点逐个执行 | 有独立慢查询时，可显式传 `true`；声明依赖的节点仍等待前置节点 |
| 节点读取前序结果 | `dependencyOutputs` 是以节点 ID 为键的已完成输出 Map，并不限于自身 `dependsOn` 中的节点 | 只读取声明过的依赖 ID；并行时不要依赖另一个无依赖关系节点碰巧先完成 |
| `required:true` 节点失败 | 串行立即失败，后面尚未开始的节点不运行；并行时阻断它的依赖节点，其他已启动的独立节点仍可能完成，最终执行抛错 | 业务明确哪些步骤不可缺；并行失败不等于所有外部副作用已取消 |
| `required:false` 节点失败 | 记录失败 trace，没有该节点输出；依赖它的节点仍可继续 | 下游明确处理“缺少结果”，不能直接读取不存在字段 |
| 工具没有注册、依赖无效 | 返回/抛出执行或规划错误；未注册工具不会因 `required:false` 自动变成可忽略步骤 | 配对校验 Profile 和执行器注册表 |
| 执行完成 | `outputs` 含 `nodeId/capability/value`，可有 `promptContent/artifacts`；`traces` 含耗时、状态和失败信息 | 单独使用执行器时由业务保存/展示；它不会持久化任务、自动重试或合并长期状态 |
| 取消/超时 | 执行器传递并检查传入的 `signal`；正在运行的外部调用也必须配合信号 | 单独使用时显式设置请求期限；取消不能撤销已经写出的数据 |
| 整理成 Prompt | 仅非 `none` 且有 `promptContent` 的输出会进入编译块，role 为 `system`；`value`、`artifacts` 不自动转成 Prompt | 验证/筛选工具文本后再投影；还要由下游模板或消息适配器真正采用，完成计数再发给模型 |

编译器按区域、priority 和来源等字段排序；它不是直接把所有结果按工具完成先后追加在末尾。`TurnHarness` 负责把 `compiledContext` 交给所选正文运行时，**具体运行时仍必须使用它**。例如 ChatKernel 接法写入 `harnessFoundationContext` / `harnessTurnContext` 模板变量；当前闭环 Actor 主要发送 `axon.history` 中已准备好的消息，不能仅把工具结果放进 `compiledContext` 就假定已经注入了这个 Actor。上面的示例显式构造 `messages`，正是为了让采用动作可见。

当前 Agent Router 的普通入口与闭环入口会构造空的 `visibleCapabilities` 和 `executionGraph.nodes`。所以这项能力的交付方式是**在仓内二开组装 Profile、执行器和模型材料接法**，不是往现有 Scenario JSON 随意加一个 `executionGraph` 就能加载任意工具。通用 SDK 的工具名单、这个固定节点图、普通 V2 的后台专家名单，是三种不同接线。

[依赖、并行及失败](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/capabilities/executor.ts#L42) · [只投影明确的提示文本](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/prompt/compiler.ts#L35) · [模板采用编译区域](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/integrations/chat-kernel-runtime.ts#L111) · [闭环 Actor 的实际消息来源](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/actor/roleplay-runtime.ts#L732) · [闭环入口构造的空节点图](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L731) · [普通入口构造的空节点图](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L1325)

## 观测与接入验证

可以接入 `HarnessTelemetry`、`observeModel` 和 `/langfuse` 子入口，记录一次调用的模型、工具、状态步骤；`createAgentBuildMetadata` 描述构建和配置身份。观测用于核对“实际执行了什么”，不代替业务接受或存储成功的检查。

先用仓库公开示例中的假模型和假存储跑通：模型请求工具 → 执行器收到正确参数 → 结果回到模型 → 宿主校验并采用 → CAS 写入 → 下一轮读回；再替换真实后端。安装包不会自动获得 Axon、Kaon 或其他业务凭证。



<a id="atoms-auto-reply"></a>

# 下一句建议：给用户三个可选择的回应

**用途：**角色回复后，为用户提供三条“我接下来可以说什么／做什么”的候选。例如守卫问“手令呢？”，可以返回“我翻找随身的手令”“我问他去哪里补办”“我先退到旁边等候”。这些是用户可选择的输入建议，不是角色再回复三次，也不会自动替用户发送。

**交付形态：**当前是 Harness 应用里的 Auto Reply 生成流程，已有普通 Agent V2 和 Story 两种接线；不是 `@flowgpt/agent-core-tools` 导出的独立工具，也不是默认交给 Orchestrator 选择的专家。接完整产品时走现成 `AUTO_REPLY` 请求；单独二开需复用应用实现并接通模型、Prompt 与对应的状态／消息读取依赖。

[调用输入与普通V2实现](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/auto-reply.ts#L27) · [Story实现](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/auto-reply.ts#L47) · [产品请求分流和返回](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L613)

## 用户看到什么，业务拿回什么

业务在一条已生成的角色回复下请求建议。程序以这条回复的 `assistantMessageId` 为锚点，组织材料，调用专门模型，解析成三条不同的短文本，交还业务显示。

应用函数返回：

```json
{
  "options": [
    "我翻找随身的手令。",
    "如果没带手令，我该去哪里补办？",
    "我先退到旁边等候。"
  ],
  "usage": {"inputTokens": 800, "outputTokens": 60, "totalTokens": 860}
}
```

这里是结构示例，具体文案与 token 数由模型实际返回。现成 Agent Router 把三个选项转为单个结果中的编号文本：

```text
1: 我翻找随身的手令。
2: 如果没带手令，我该去哪里补办？
3: 我先退到旁边等候。
```

**生成建议时不写用户聊天消息、不更新 V2 状态、不结算 Story 剧情。** 业务方决定把它们做成按钮、填入输入框或保存为建议记录；只有用户实际选择／编辑并发送后，业务才按正常聊天流程保存那一句并发起下一轮。普通 V2 的调试记录可能记录建议文本，但调试记录不是用户已说出口的聊天历史。

[调试记录与options返回](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/auto-reply.ts#L184) · [编号文本、空artifacts与stateVersion0](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L677)

## 普通 Agent V2 怎样生成

| 步骤 | 具体做什么 | 输入／结果 |
|---|---|---|
| 1. 定位那一条回复 | 在请求给出的非空 user/assistant 消息里找到目标 assistant ID | 取该回复及它前面最多 6 条可见消息；之后已经发生的消息不会加入此次建议 |
| 2. 取得对应轮次的指导材料 | 调用 Axon `roleplay.state.get`，在 pending 或 planning_settlements 中查找同一个 source_message_id | 必须有非空 `auto_reply_guidance`；不能随便拿当前最新 state 顶替 |
| 3. 准备模型输入 | 角色名称／描述、认证上下文中的输出语言、最近用户输入、上述指导文字、最近可见对话 | 系统提示词来自单独 Auto Reply ModelConfig 关联的 PromptManager |
| 4. 生成候选 | 调用模型，要求三个不同方向 | 1 可以顺着当前剧情，2 关注情绪／关系／好奇，3 保留其他或中立方向；这是提示词指导，不是三个硬编码答案 |
| 5. 解析和检查 | 清理编号／空白、去重、最多取 3 条、每条最多 160 字符；部分语言有文字检查 | 凑足 3 条且通过检查才返回，否则尝试重新生成 |

### 等待的是什么状态

普通 V2 的后台开始工作时，会把**这一轮开始时读到的状态及其版本**，连同目标回复 ID、轮次和阶段，通过 `markPlanning` 发给 Axon。Auto Reply 随后读取状态接口里与这个回复 ID 匹配的 `auto_reply_guidance`。

所以它等待的是“这条回复所属轮次的指导快照可读”，**不是必须等六位专家都执行完，也不是必须等 Director 把本轮新状态保存成功**。指导记录可以在 pending 中，也可以在 planning_settlements 中。Harness 没有在这里现生成一份摘要；具体指导文字由外部状态服务提供，当前两个仓库不能证明其内部转换算法。

默认最多等待窗口为 **30,000ms**，两次读取之间等 **250ms**；配置范围 0–120,000ms。设为 0 仍会读取一次，不表示完全跳过检查。该数值控制查快照的轮询窗口，不是包括网络请求与模型生成的完整响应时限。

后台阶段记录是旁路写入，失败不阻塞 RP 正文；因此可能出现“角色回复已经成功，但建议因指导快照未就绪而失败”。这种情况不应伪造三条静态建议作为成功结果。

[轮初快照随阶段写入](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/runtime.ts#L593) · [mark_planning传输字段](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/axon/client.ts#L783) · [按回复ID轮询指导](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/auto-reply.ts#L253)

## Story 怎样生成

Story 的建议流程不读取普通 V2 的 pending／planning_settlements，也不等待旧后台快照。

| 步骤 | 具体做什么 | 业务意义 |
|---|---|---|
| 1. 检查回复锚点 | 请求中必须有且只有一条目标 ID 的 assistant 消息，前面还要有用户消息 | 防止把其他轮次的结果混进建议 |
| 2. 读取真正保存的正文 | 用可信 userId、conversationId、assistantMessageId 调 Axon `conversationMessage` | 正文以聊天服务保存的内容为准，不以请求中可能过时或截断的文本为准 |
| 3. 组织上下文 | 使用目标回复前最多 6 条非空可见消息，加入查回的完整正文、角色信息和指定语言 | 目标回复之后的消息不加入；不读取 Story 整个世界／叙事状态快照 |
| 4. 生成三种选择 | 提示词要求三个适合当前时刻的不同策略，允许暂停、澄清或换方向 | 不固定“选项1推进、2情绪、3其他”的分槽，也不替用户宣布行动成功或其他角色反应 |
| 5. 严格检查结果 | JSON 中必须恰好 3 条，非空、不同、每条不超过 160 字符，符合相关语言文字要求 | 失败时可带格式修正要求再次调用；不把超长条目直接截断采用 |

例如请求里缓存的是“守卫让你通过”，但聊天服务保存的正式正文是“守卫伸手索要手令”，Story 会按后者生成建议。如果正式正文尚未保存、查不到、角色不对或为空，流程失败；它不会退回请求里的旧文本继续生成，也没有在此实现等待正文落库的轮询。

源码函数允许额外提供 `pendingStage`，但它只是可选的未完成阶段建议，提示词明确不能把它当成已经发生的事实。**当前统一 Router 调用没有传入这个字段**，不能写成 Story 默认会把剧情计划一起交给 Auto Reply。

[唯一锚点与正式正文读取](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/auto-reply.ts#L53) · [输入与用户自主权要求](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/auto-reply.ts#L74) · [严格结果解析](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/auto-reply.ts#L17)

## 如何在现成产品里开启和调用

### 先配置专用模型和提示词

在已有完整场景配置的 `agenticV2.modelConfigs` 中补 `autoReply`，普通 V2 可同时调整 `autoReplyWaitMs`：

```json
{
  "agenticV2": {
    "modelConfigs": {
      "autoReply": "your-auto-reply-model-config-id"
    },
    "autoReplyWaitMs": 30000
  }
}
```

这是**合入现有配置的字段片段**，不能用它替换整个 agenticV2 配置；原有 actor、orchestrator、specialists、director 配置仍需保留。没有 `autoReply` ModelConfig 时，现成 Router 拒绝 Auto Reply 请求；它不自动借用 Actor 模型。

这个 ModelConfig 关联的 PromptManager 必须有名为 `auto_reply` 的系统提示词，而且必须包含 `{{getvar::language}}`。程序会填入输出语言和 `characterName`；缺失模板或语言变量会失败。不能把一段 `auto_reply` 提示词直接塞进 scenario，就当成替代配置中心接线。

Story 仍通过同一个 `agenticV2.modelConfigs.autoReply` 指定建议模型；程序根据场景是否存在 `storyV1` 选择 Story 实现。`autoReplyWaitMs` 虽会作为通用输入传入，但 Story 当前路径不使用这个等待值。

[autoReply模型字段读取](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/server.ts#L124) · [等待范围校验](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/server.ts#L170) · [auto_reply模板要求](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/prompt-manager.ts#L160) · [普通V2/Story分流](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L651)

### 再由业务发起 AUTO_REPLY 请求

现成产品入口是 Agent Router 的 `ChatRequest`，`chatType` 设为 `AUTO_REPLY`。必要材料包括：

| 字段 | 谁提供、用于什么 |
|---|---|
| `conversationId`／`promptId` | 已有会话和角色／作品身份 |
| `extensions.scenario_id` | 服务器已允许且已经配置 autoReply 的场景 |
| `extensions.turn_context.user_id` | 上游认证后得到的用户身份；不能由不可信前端任意代填 |
| `extensions.turn_context.assistant_message_id` | 为哪条角色回复生成建议，必须与 messages 内目标 ID 一致 |
| `extensions.turn_context.language` | 输出语言，来自可信业务上下文；不是让模型从用户上一句猜语言 |
| `messages` | 带 ID 和角色的消息序列，含目标 assistant 消息及此前用户内容 |
| `modelConfigId`、项目／协议字段 | 沿用已接通的 Agent Router 请求契约，不是直接把建议模型 ID 当作任意上游模型绕过场景 |

下例基于**已有且完整有效的 ChatRequest**修改本次用途； `request`、`runtimeConfig`、`userId` 和 `signal` 由可信宿主准备，其他项目与协议字段不在此重复展开：

```ts
import { ChatType } from '@flowgpt/agent-router-sdk/agent_pb.js';

request.chatType = ChatType.AUTO_REPLY;
request.extensions = {
  ...request.extensions,
  scenario_id: 'your-enabled-v2-or-story-scenario',
  turn_context: {
    user_id: userId,
    model_alias: 'your-model-alias',
    language: 'zh-CN',
    assistant_message_id: 'assistant-7',
    extra_set_vars: {},
    template_set_vars: {},
  },
};
// request.messages 需要包含目标 assistant-7 和它之前的用户消息。
// 通过现有 Agent Router client 发给 Harness；不要再发一条普通聊天来代替。
```

Router 返回编号文本与 usage。此专用途径提前返回，不再运行正常 Actor 正文生成或其后的记忆写入流程。它仍需要上游正常鉴权、消息来源与产品交付处理。Sumi 入口明确拒绝这里的 `auto_reply`，要求走 Chat Service 自己的独立路径；不能由这个实现推导出 Sumi 已复用同一套建议流程。

[真实AUTO_REPLY请求构造示例](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/test/agent-router.test.ts#L1220) · [提前返回分支](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L804) · [Sumi调用边界](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/sumi/runtime.ts#L41)

<details>
<summary>单独二开、返回校验、重试、费用与失败细节</summary>

**源码入口。** 普通 V2 为 `apps/emochi/planning/auto-reply.ts` 的 `generateAgenticAutoReplies`；Story 为 `apps/emochi/story/auto-reply.ts` 的 `generateStoryAutoReplies`。这是应用源码导出，不在两个公共 npm 包的导出表里。若从源码调用，输入要提供 requestId、parentRequestId、userId、conversationId、promptId、assistantMessageId、scenarioId、modelConfigId、waitMs、language、角色名称／描述和带 ID 的 messages；另有可选 temperature、trace、usage 要求。依赖对象是 `{axon, model, observeAutoReply?}`。

模型接口 `model.complete` 和 Axon 不是纯“一个 Prompt”能代替：普通路径依赖状态指导、ModelConfig／Prompt 解析与可选 token 计数；Story 依赖正式消息读取、Prompt 解析与可选 token 计数。若希望发布为客户独立可安装的能力，还需做这一层依赖封装。

| 参数或行为 | 普通 Agent V2 | Story |
|---|---|---|
| 默认温度 | 应用请求未指定时，显式覆盖为 0.7 | 未指定时不覆盖，让 ModelConfig 决定 |
| 每次模型调用超时 | 180,000ms | 180,000ms |
| 本地生成尝试 | 最多 3 次；取消、内容过滤或已尝试模型恢复等情况可能提前结束 | 最多 3 次；无效结构会把先前输出和修正要求带到下一次；同样可能提前结束 |
| 普通输出解析 | 支持数组、`options` 对象及 `{content}` 选项；去重、清理、截为每条最多160字符，再取前三条 | 必须恰好3条；超长、空、重复即无效；不会截断超长内容后当成功 |
| 语言检查 | 中文要求含汉字，日文含假名，韩文含韩文；其他语言当前没有同等文字校验 | 同类检查；不等于完整语义语言质量验证 |
| usage | Router 要求 usage；上游未提供时用 token 计数补算；多次模型结果可累计 | 同样累计；缺失时计数补算，计数失败可向上抛错 |
| 保存 | 可写 best-effort 调试节点与日志；不写聊天／V2事实状态 | 完成观察日志；不写聊天／Story状态 |

这里的“最多 3 次”是 Auto Reply 自己的调用循环；宿主模型适配器若配置了独立模型恢复，还可能在单次调用内尝试恢复。不能把它作为整体所有网络请求数的硬上限。

| 失败 | 用户业务如何理解 | 当前处理 |
|---|---|---|
| 场景未配置 autoReply 模型，或没传目标回复 ID | 接入材料不完整 | Router `agent.invalid_argument` |
| 角色 Prompt 不存在或没有名称 | 没有足够角色材料生成 | `router.unavailable`，可重试 |
| 普通 V2 指导未就绪／对应用户或回复文字缺失 | 暂不能为这条回复提供建议 | `turn_start_snapshot_not_ready`；Router 映射为可重试 `router.unavailable` |
| Story 正式正文未保存或查不到 | 缺少权威依据 | `auto_reply_generation_failed`；不回退请求侧正文，不等旧状态 |
| 三条结果仍无法通过检查 | 本次没有有效建议 | `agentic_auto_reply.failed`；不补固定兜底文案 |
| 用户取消 | 不再继续该请求 | 取消向依赖调用传播；业务不用显示过期建议 |

普通 V2 最多等待指导快照的时间、单次模型超时、Router／Story 请求总超时是不同限制，不能把 `autoReplyWaitMs=30000` 描述成“所有建议必在30秒内返回”。

此外，main 里还保留 Pioneer API 包装：它先查已保存最近历史，支持请求体 `enabled`，返回 `{enabled, options, fallback:false, parentRequestId, usage?}`，有自己的 JWT／来源和模型配置。这个入口的字段不能与统一 Router 混用；统一 Router 没有要求请求体 `enabled:true`。本能力页以 Router 的普通 V2／Story 接法为主。

[普通重试与usage](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/auto-reply.ts#L114) · [普通解析和语言检查](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/planning/auto-reply.ts#L310) · [Story重试和修正](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/story/auto-reply.ts#L98) · [Pioneer独立包装](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/pioneer/api.ts#L129) · [SDK公共导出范围](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/package.json#L9) · [工具包公共导出范围](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/package.json#L12)

</details>

## 二开验收应看到的结果

先保存一条角色回复，再对它请求建议：得到三个文本，并确认聊天里没有因此多出用户消息。接着让用户选择其中一条，验证它才按正常聊天入口成为下一轮输入。

普通 V2 再测同一回复指导尚未就绪、目标回复之后已有新消息这两种情况；Story 再测请求侧旧正文与已保存正文不一致、正式正文未保存这两种情况。检查候选建议依据了正确的一轮，且失败时没有从其他轮次捡一个状态继续。

基线为 Harness `fa41d220d48327e58d994f936517b23f2f15f658`；本章区分调用源码、公共包导出与产品接线。配置样例不是所有线上场景已开启的证明。

