# 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)
