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