回合引擎规范
角色扮演会话运行在使用 roleplay 智能体预设的 DSH 对话中。对话智能体是游戏主持人;所有非玩家控制角色由游戏主持人工具启动的一次性角色子智能体扮演。预设挂载 @dsh-agentic-roleplay/core/orchestration,其工具、钩子和指令只作用于角色扮演对话。
回合循环
一个回合包括一条玩家消息、一个或多个角色行动轮次,以及最终输出。
- 玩家输入后,游戏主持人获得场景状态,
InputValidator检查输入。 activate_characters开始一轮。主持人客观描述时刻,并从场景中选择确实能接触到该时刻的角色,最多maxCharacters个。异地角色不会自动感知;世界有地点时,没有位置的角色会被拒绝。角色各自并发返回结构化intent、可选的逐字utterance,以及low、medium或high的commitment。- 主持人综合意图并调用
resolve_round,为每个角色提供succeeded、partial或failed的result、实际发生内容summary、他人可观察的public_state,以及角色无法说出发言时的spoken: false。 - 失败且 commitment 为 high 的角色可在同一轮再行动一次。它知道其他人的行动和自己的失败结果。重试后不再重试。
- 本轮结束时,引擎立即应用活动和公开状态,并把轮次可靠地排入记录员队列。只有主持人调用
record_memory时才启动记录员,主持人无需等待。 - 主持人判断故事是否自然延续。直接回应、自然反应或角色会继续处理的悬而未决事项可以开启下一轮,而且只激活当前时刻需要的角色。否则把控制交还玩家。提示词要求在第一个自然停顿点结束,硬性轮次上限提供兜底。
- 主持人通过
narrate(在格式自身的检查之后,decisions.agency、decisions.refusal与decisions.secrets的可选输出检查可能会把它退回,见可选能力集成)提交有序的 narration 与 utterance 段。所有轮次中被接受的发言必须逐字、各一次、按说话顺序出现;重试发言取代第一次尝试。同一轮内部可自由排列。检查失败时会列出全部问题并要求重交;两次仍失败后,检查器会恢复原话、删除虚构或重复发言,并把遗漏内容放回对应轮次,标记repaired。成功交付会结束回合,更新turns和lastPlayedAt。结果编码在<roleplay-output>及包含segments、repaired的 JSON 中,由转录界面渲染。
关系只影响优先级、风险判断、语气和记忆线索,不形成第二套行动系统。场景给主持人带理由的激活建议,考虑直接点名、是否在场、紧张、不信任和亲近,但不能越过可达性。角色只收到自己指向相关人物的单向关系,并被提醒这是一种可能出错的态度,不是对方动机的证据。记录员只修改具体事件支持的方向和字段,不自动镜像另一方向。阵营目标和关系也只施加压力,不命令成员。
节奏与剧情
可选的 plots.json 保存带前置条件、风险、结局、指引及禁止建立内容的钩子和发展图。会话进度保存在带命名空间的元数据中,与世界原文件和事件分开。推荐只更新冷却;只有已解决游玩让发展可感知、玩家拒绝它或整个剧情已解决后,才通过 record_plot_progress 推进。
每回合开始时,确定性导演先检查近期事件和立即开放的线索。只有超过安静阈值、回合冷却和故事时间冷却后,才向主持人提供一个符合条件的机会。它只是建议,仍受可达性、角色自主、知识和结果规则约束。被拒绝的钩子不会重复推荐,前置条件阻止内容过早出现,剧情无需强制完成。没有脚手架的安静会话可调用 author_plot,让一次性子智能体基于当前状态建立分支脚手架;其中内容尚未发生,要等之后的节奏建议才能使用。
世界编辑器可编辑世界和实时会话中的同一套定义。世界保存原子替换 plots.json;会话保存替换元数据定义,并保留相同剧情 ID 的进度。所有参与实体 ID 在开始会话和编辑器保存时验证。
agent/turn-stopping 钩子阻止尚未交付输出的回合结束,并在同一缺失事项上连续最多三次把缺失步骤告知主持人(回合有进展后重新计数),避免顽固模型困住对话。模型请求触及输出上限后,DSH 只要没有引导就会立即结束回合,所以之后的每一步都需要被引导;sampling.masterMaxTokens 防止循环的推理耗尽整个上限。角色子智能体不受此门控制,非角色扮演对话不受影响。
记录员
记录员是拥有专属工具的游戏主持人子智能体。它把轮次写入故事数据库,主持人和角色都拿不到这些工具。
- 主持人在决策、揭示、承诺、伤势、关系改变、场景或时间改变等阶段转换时调用
record_memory,可用focus指出重点。小谈话和常规移动无需立即记录。一次运行处理所有等待轮次,并与主持人后续工作并行。memory.backlogRounds默认 6,达到后会在回合结束时自动启动。 - 先有朴素事件。轮次被记录的那一刻,不需要任何模型,引擎就在写入队列的同一个事务里写下它的朴素事件(
host/orchestration/skeleton.ts):每个角色做了什么写一条(主持人对结果的概括),说了什么再写一条,一字不差。身处同一地点的角色会感知到它们(话语是“听到”);对旁观者来说,他们对一个行为所能感知的,只有主持人说过看得见的那部分。重要度为 2,有人或物移动时为 3,没有标签。记录员不再重写这些:它用event_edits充实它们(重要度、标签、涉及的角色、谁感知到了以及感知得如何),只为没有任何条目涵盖的内容添加新事件。因此运行失败或从未被请求时,留下的是较朴素的记录,而不是缺失的记录;这些草稿事件会随完成该轮的运行一并封存。 - 标签取自已在使用的词汇。回忆按标签精确匹配,所以记录员的简报会列出故事已用的标签及次数,它的修改在应用前会经过
canonicalTags(host/memory/tags.ts):大小写、分隔符、复数词尾,或较长单词里的一个字母之差,都会归并到已有标签。完全不匹配的标签保留;不再设别的出口。 - 失败不会进入玩家输出,轮次继续排队。下一次运行会看到已有事件并继续而非重复。主持人主动请求的失败任务在下一回合第一步再试一次。连续三次失败后,相关轮次标为已处理但无记忆,活动、公开状态、地点和物品变化仍保留,避免队列无限增长。
- 事件在验证期间可通过
event_edits和event_retractions修正,完成后封存。感知和信念仍可修改,perceptions_added可记录角色后来获知旧事件。 - 每次请求前缀是固定 persona,变化内容仅包括设置、待处理轮次、角色状态、近期事件、已写事件和开放线索。工具包括
edit_records、read_records、search_events、list_threads与submit_result {complete, notes}。edit_records原子写入或返回全部问题,也会拒绝未提供轮次的引用。 - 默认的
memory.verify是提交门槛。最后一次修改后,记录员必须用read_records读回,并检查无依据事件、遗漏、重复、错误感知者、缺少感知、状态泄露思想、无依据信念和失衡重要度。关闭后可在写入后直接提交。complete: false表示仍有问题并使运行失败,最多允许 60 次调用。
可通过 Committer 边界替换记录员。未安装记录员时默认 writesOutcomes。主持人决定的活动与公开状态由引擎在记录轮次时应用,不交给记录员。
角色
每个角色使用相同子智能体结构,固定 persona 包含角色定义和规则,变化提示包含情境、在场者、回忆和已发生内容。开启 memory.lookup 时,专属的 recall_my_memories 只注册到该角色作用域,并发角色只能访问各自记忆。最终调用 submit_result {intent, utterance, commitment}。
提供方缓存
每次子智能体请求都先放固定 persona 和固定工具列表,把变化内容放在末尾。测试会断言固定前缀不变。游戏主持人对话在折叠之间也只在尾部增长,方便本地提供方复用前缀缓存。
每个对话的轮次限制
同一玩家消息中,以忽略大小写和标点后的同一情境重复询问同一角色会被拒绝。主持人必须说明变化、改问其他角色或交付输出。没有任何角色产出的空轮不计数。
maxRounds 范围 1 到 10,默认 3,保存在 settings.json。每轮重新读取,因此修改立即生效。达到上限后,activate_characters 会被拒绝并要求交付;每个已记录轮次也会告知剩余额度。设置通过 Chat settings 及 GET、PUT /roleplay-api/sessions/settings?id= 编辑。
扩展点
TurnEngine 位于 src/host/orchestration/turn-engine.ts,不依赖 DSH。可替换行为由 boundaries.ts 定义,defaults.ts 提供默认实现。roleplayOrchestration 服务保存当前实现;插件可调用 ctx.roleplayOrchestration.replace({ ... }),从下一次调用开始生效,并取得恢复函数。
| 边界 | 决定内容 | 默认实现 |
|---|---|---|
ActivationPlanner | 激活哪些角色 | 所有非玩家角色,或主持人点名者 |
CharacterContextProvider | 角色的 persona 与变化提示 | 共用场景加角色自身定义 |
CharacterOutputValidator | 角色回答是否可用 | intent 非空 |
RetryPolicy | 谁在首轮后重试 | 失败且 commitment 为 high |
InputValidator | 玩家输入能否开始轮次 | 接受全部输 入 |
Author | 必要时创建和整合实体 | 世界作者;未安装时全部拒绝 |
Committer | 写入等待轮次 | 记录员;未安装时为 writesOutcomes |
Narrator | 谁写最终输出 | 默认由主持人写 |
NarrationChecker | 发言是否逐字且有序,以及如何修复 | 忽略包裹引号和空白后逐字比较 |
SceneContextProvider | 回合开始时主持人看到什么 | 角色公开状态和活动 |
输入被拒绝时跳过轮次,主持人收到原因并可直接结束。单个角色失败或输出无效只报告给主持人,不阻止其他角色。
代码位置
src/orchestration.ts 是预设入口,构建为 lib/orchestration.js。它由所有使用该预设的对话共享,所以状态必须按对话保存。tools.ts 定义工具,hooks.ts 定义回合门和场景注入,character-runner.ts 启动角色,store.ts 映射角色扮演会话并写记录,prompt.ts 保存主持人指令。tests/host/orchestration/ 同时使用假实现测试引擎,并使用脚本模型测试真实 DSH 循环。
渲染
src/client/transcript/ 从 narrate 工具结果读取输出;Narrator 边界写入时则读取 resolve_round 结果。内容作为会话级节点显示在普通 Chat 中,不折入回合过程 。旁白显示为段落,发言显示为带角色名、首字母头像和稳定角色颜色的气泡。修复过的输出带小提示。
本插件不另写自定义日志事件,因为 DSH 会拒绝重新加载包含未知事件类型的会话,追加 API 也不能把事件标为可忽略。
开场
世界的 opening_message 可以是纯文本,也可以是旁白和世界角色发言列表。新会话在玩家输入前直接显示开场,不调用模型,渲染方式与普通回合相同。系统在智能体创建前,以完成的第一回合和 narrate 结果写入日志,再按 ID 接管对话。智能体因此读取正确的回合计数,玩家第一条消息成为第 2 回合。没有开场的对话不变。由于初始已有消息,DSH 会立即按会话名称列出,而不是显示为新会话。
地点、世界扩展与物品
resolve_round.moves_to 可包含 location、position、feature 和 ignore_exits。TurnStore.checkMove 检查不可达、锁定或阻塞通路及失败意图,finishRound 在同一事务中应用移动。排队的 RoundRecord 保存场景 place 及各角色的 at 和 moved。角色只看到自己的地点和同处者,主持人通过 castSummary 看到全体位置。
expand_world {need, what} 只供主持人使用,并调用 TurnEngine.expand。激活与结算之间、原因不属 于 scene_change、arrival、introduction、世界关闭或超过每回合上限时拒绝。Author 可创建或拒绝;新角色立即进入角色表。交付回合后,consolidateIfDue 在后台逐对话运行一次,失败只记录并保留队列。世界作者使用读取工具、world_lint、edit_world,整合时还使用 list_consolidation 与 resolve_consolidation。
item_changes 先在始终回滚的事务中连续试运行,后一步能看到前一步结果,失败意图不操作物品。正式提交顺序是先处理物品,再移动角色,因此操作发生在角色原位置。结果进入主持人的 things 和记录员的 items。角色简报含携带物和地点物品,castSummary 也得到包含隐藏信息的 SceneItems。
时间与角色日程
回合开始时更新现实模式时钟并处理日程补进度。每轮结算后调用 endRound,模拟模式增加 elapsed_seconds 或默认时长,同时受路径 travel_seconds 下限约束。advance_time 只能在轮次之间调用。时间变化会触发日程移动、到期事项和触发器检查。
角色简报中的 agenda 合并日程窗、约定和目标。离场角色到期事项在回合交付后后台处理。开启 offstage.joint 时,同一约定的角色依次回答,后答者会知道谁已经决定前往,避免各自独立猜测。