跳到主要内容

故事状态与记忆规范

本页说明角色与游戏主持人如何记住故事,以及如何把对话控制在模型可接受的大小。除每个对话的 settings.json 外,以下内容都存入会话的 session.sqlite。

记录内容​

记录层级从回合到事件,再到感知和信念。玩家消息及其所有角色轮次组成一个回合。游戏主持人判断故事进入新阶段后,记录员会把尚未处理的轮次写成:

  • 事件是客观发生的事,使用简短过去时,带 1 到 5 的重要度、标签、涉及角色和地点。记录员验证时事件处于草稿状态;提交完成后封存,不能修改。
  • 感知记录每个角色如何见证、听到、被告知或推断事件。它包含角色自己的表述、淡忘后保留的概要和准确、部分正确或错误的标签。准确度只用于检查,不告诉角色。感知可在之后通过 perception_edits 修正,也可用 perceptions_added 让角色后来获知旧事件,事件本身不变。
  • 信念保存角色超越单个事件形成的认识。它们可修改,带 doubtful、likely 或 sure 的确信程度,并明确关联支持或反驳它的角色感知。
  • 故事线索保存未解决的承诺、谜团和目标。
  • 角色状态保存公开状态、内在状态、活动和情绪。

轮次先进入 pending_commits。记录员失败时工作仍在队列中,不会把技术错误写进故事。对话设置会显示等待数量。

角色如何回忆​

角色是一次性智能体。每次激活前,@dsh-agentic-roleplay/state 的 session.recallRanked() 通过核心的 Recaller 边界选择当前情境会唤起的内容:

  1. 以场景线索、FTS5 词语和可选的语义向量检索角色自己的感知与信念。线索包括名称和别名、关系强度、共有词语和标签、无空格文字的双字组合以及当前地点。sqlite-vec 使用余弦相似度,约低于 0.3 的结果不计入。
  2. 融合实际运行的搜索,线索与词语权重为 1,语义权重为 1.5。没有运行的搜索不参与平均。
  3. 先保留 memory.overRetrieve * memory.recall 个候选,默认 4 x 12,最多 60,并结合相关性、重要性和新近程度。完全淡忘的内容不保留。
  4. 配置重排序服务时,让它按当前情境给候选项打 0 到 1 分。最终相关度由 70% 重排序分和 30% 融合检索分组成。服务缺失或失败时只用检索分。
  5. 按相关性、重要性、新近程度和保留率选择 memory.recall 条。最大边际相关性用 memory.diversity 降低近似重复项,最后按发生顺序提供。
  6. 最多再关联 memory.associate 条与已命中记忆共享人物、标签或地点且重要度至少为 3 的内容。没有直接命中时不做关联。

回忆始终是该角色的版本,而不是客观事实。即使情境没有命中线索,近期的重要记忆仍可能出现。

默认开启 memory.flaky。保留率按故事时间计算:0.5 ^ (经过的故事天数 / 半衰期)。低重要度事件从六个故事日的半衰期开始,每提高一级重要度翻倍;信念为四倍,重复回忆会延长,记录 ID 提供确定性的轻微差异。重要度 5 至少保留模糊概要。线索可带回一部分已经淡化的内容。关闭 flaky 后,回忆和感知保持精确。

作为某条信念支持证据的感知,其半衰期会乘以 memory.foldedFade,默认 0.5,并且不会与该信念同时选入。原始感知仍保存为证据。存在反驳证据的信念在回忆时标为有争议。

年龄只看故事时钟,不看回合数或现实时间。事件时间取该轮次开始时刻;信念时间取形成它的回合中第一条记录时刻。未启动故事时钟的会话不会发生淡忘。

游戏主持人读取的上下文​

对话在达到 context.limitTokens 前完整发送。该值为 0 时,默认以模型上下文窗口的 context.limitRatio,即 60%,作为限制,但不超过 context.workingTokens(默认 0,即不设上限),最低 4000 token。窗口取自提供方模型信息;既无明确限制又无已知窗口时直接报错。

达到限制后,预设中的 RoleplayCompaction 只覆盖 DSH 基础折叠引擎的摘要器,把最旧的完整回合移出,同时至少原样保留限制值的 context.retainRatio。表面替换、日志标记和工具调用配对由 DSH 处理。

无工具的一次性编年史作者根据事件而非旧摘要写内容:刚移出回合形成短摘要;短摘要超过 context.shortTermTokens 时,最旧部分和其权威事件压缩成章节;章节超过 context.longTermTokens 时再次合并。结果存于 summaries,每段只写一次。对话头部先放长期记录,再放短期记录。两次折叠之间只在尾部增长,有利于提供方前缀缓存。

每个回合尾部还带上场景状态、开放故事线索,以及玩家行为唤起但已经被折叠出对话的旧事件。

设置​

会话目录的 settings.json 中所有字段均可省略。开始对话前可在折叠的高级设置中指定,之后可在 Chat settings 修改。系统在每次使用前重新读取,文件未变化时不重复解析。无效编辑只记录一次日志,并继续使用最近的有效设置。

设置默认值含义
maxRounds3每条玩家消息最多角色轮次
maxCharacters4一轮最多激活角色数
memory.flakytrue不完美记忆与感知
memory.recall12每次行动提醒的最多记录数
memory.lookuptrue角色决策时可继续查询自己的记忆
memory.overRetrieve4最终选择前的候选倍率
memory.diversity0.3近似重复内容的折扣,范围 0 到 1
memory.associate2由强关联额外唤起的最多记录数,0 为关闭
memory.foldedFade0.5感知被信念归纳后,其半衰期的倍率
memory.verifytrue记录员完成前必须读回并核对
context.limitTokens0开始折叠的大小,0 表示跟随模型
context.limitRatio0.6无明确限制时使用的模型窗口比例
context.workingTokens0无明确限制时对话长度的上限,无论模型窗口多大;0 表示不设上限
context.retainRatio0.5折叠后原样保留的比例
context.shortTermTokens1500短期记录目标大小
context.longTermTokens2500长期记录目标大小
embedding.options无嵌入提供方为本对话定义的请求选项

搜索索引与向量​

每条感知、信念和事件都有对应的 recall_items 行,由仓库同步维护。词语索引位于 recall_fts;生成向量后,语义索引位于按角色分区的 sqlite-vec 表 recall_vec,因此一个角色绝不会搜索到另一角色的私有记忆。

可选的 ctx.embeddings 在每次记录员运行后和搜索发现缺失向量时于后台生成向量。文本按文档发送,当前情境按查询发送,并带上对话的 embedding.options。第一批向量固定会话的模型与维数。其他模型的向量会被拒绝,文字搜索继续工作,直到索引重置。文本变化会删除旧向量。提供方失败或缺少 sqlite-vec 时,线索与词语检索仍可用。

角色回合开始时自动注入最佳记忆。开启 memory.lookup 时,角色还可用 recall_my_memories 按关键词或描述查询自己的记忆。游戏主持人和记录员的 search_events 也使用该索引。

地点​

会话保存 locations、带方向的 exits 和 placements。出口状态为 open、locked 或 blocked。地点、特征、通路和初始位置在开始会话时从世界复制。

  • 游戏主持人在 resolve_round 结果中用 moves_to 决定移动。引擎会先验证存在由开放出口组成的路径,失败意图不会移动。移动与本轮其他状态在同一事务中写入,等待记录员的轮次包含原位置 at、目标 moved 及场景地点。
  • 事件带有 location。记录员应填写地点,并只给确实在场或能从附近看到、听到的角色建立感知。地点名也进入搜索索引。
  • 当前地点本身就是强回忆线索,search_events 可按地点限制。
  • 角色只看到自身地点的描述、最多 12 个特征和 12 个出口,以及同处的人。游戏主持人知道玩家所在处的内容、同处角色及其他角色的位置。
  • 游玩中的变化通过记录编辑写入:places、place_edits、exit_changes 和 moves。模型工具包括 list_locations、get_location 和写入时的 move_character。

关系​

关系是每个方向各一份的 relationships 描述,包含起点、终点、标签、摘要、熟悉度、信任、亲近与紧张。世界文件提供初值;只有真正改变看法的事件才应更新。角色只收到自己指向同场角色的描述。关系与信念分开,也承担角色认知图的作用:没有外向关系就表示仍不认识对方。亲眼见面会创建中性的 acquaintance;被告知某人存在时,会从获知者指向该人建立关系。

实体知识​

entity_knowledge 是从角色指向地点、物品、有方向出口或设定资料的显式闭世界图。没有边就表示不知道。对物理实体的知识只表示能识别,不透露当前位置、所有权、内容、在场者或状态,也不做传递扩展。

character.json 可用 knowledge.locations、knowledge.items、knowledge.exits 和 knowledge.lore 提供初值。代码会记录普通观察到的当前地点、可见出口和目的地、可见或携带物品及在场角色。记录员在角色被告知、阅读或推断实体存在时增加知识;只有错误记录才撤回。角色简报列出已知子图,recall_my_known_entities 不泄露当前事实。主动规划移动使用 planKnownMove,只经过已知目的地和出口;故事强制移动使用权威规划器。

recall_my_lore 只返回角色知道的设定正文——即它有知识边的条目,以及 scope 为 everyone 的条目(迁移 9 增加 lore.scope,默认为 listed);不搜索未知设定。角色简报只列出这些设定的标题,不给正文;有此类设定的角色在首次提交回答时会被问一次:此刻是否涉及自己的背景,若涉及则自行查阅条目。游戏主持人的 search_lore 可搜索完整规范设定库。 进行中的故事可以添加或移除世界书(GET/POST/DELETE /sessions/lorebooks):条目以 <书名>:<id> 为编号,在同一个事务中复制或删除,条目的知识边随之变化,元数据 roleplay.lorebooks 同步更新;回合进行中拒绝操作,移除时若其他设定或剧情仍引用这些条目也拒绝。

扩展世界​

只有故事确实需要时,游戏主持人才通过 expand_world {need, what} 请求世界作者。need 可为 scene_change、arrival 或 introduction。气氛、路人、群体、顺带提及的内容以及已有实体能承担的用途都不满足门槛。作者必须先搜索并复用,拒绝创建也是正常结果。

authoring.enabled 可关闭扩展。每回合最多创建 authoring.perTurn 个实体,工具和作者编辑都会检查。重名或别名冲突会被拒绝。新地点必须连接,新角色必须放置,作者不能在 world_lint 仍报告其创建内容有错误时结束。

只有世界作者的原子 edit_world 能创建内容:由智能体控制且不可扮演的角色、角色状态和位置、地点与特征、通路及关系。其他智能体无权创建实体。新内容进入 consolidation_items。达到 authoring.consolidateAfter 或等待四回合后,后台再次运行作者,补全通路和关系,并结合角色见闻、邻近地点和 world_lint 处理所有项目。该阶段不能再创建新内容,失败时队列保留。

world_lint 会检查不可进入或离开的地点、从玩家处不可达的地点、无位置角色、重名角色或地点、新角色缺少关系、单向通路和单边关系。

物品​

所有实体物品、装备和容器都使用一种 item 模型。它包含数量、唯一性、状况、notable、hidden、穿戴槽位、open、closed 或 locked 容器状态、开启物及所有者。物品恰好由一个位置持有:地点、角色携带或穿戴、或另一个容器物品。实际位置通过持有链推导,所有权与占有分开。槽位是会话数据,带 DEFAULT_SLOTS,世界和故事都可添加。

resolve_round 的 item_changes 可依次执行 take、drop、give、put、wear、remove、open、close、lock、unlock、reveal。引擎先对所有结果做连续试运行,后一步能看到前一步结果,并一次返回全部失败原因。检查包括不在场、隐藏、位于关闭或锁定容器中、由别人持有、未携带、槽位已满、钥匙错误或不在手中、数量不足。forced 只用于故事强行破锁,出口锁使用同一规则。拆分数量会生成部分堆,并与落点的同类堆合并。

隐藏物品及关闭或锁定容器中的物品对角色不可见、不可操作;穿戴物和开放容器内容可见。游戏主持人和工具看得到全部。每次最多列出 12 件,显著物品优先。事件可关联物品并按物品名索引,search_events 支持物品筛选。记录员记录发生的物品操作,但不自行移动物品。

故事确实建立但尚不存在的重要物品可由世界作者通过 expand_world 的 object 原因创建,并受同样门槛和 authoring.perTurn 限制。新物品起初必须位于一个位置并进入整合队列;必要时作者也可增加穿戴槽位。world_lint 还检查没有已知开启物的锁及同处同名的重复物品。

时间​

会话的故事时钟以 UTC 毫秒保存,并按对话时区显示。clock.mode 有 simulated 和 real。现实模式可切换到模拟模式,并从较晚的故事时间或墙上时间继续;一旦故事使用自己的模拟日期,就不能切回现实模式。

  • 模拟模式只随故事推进。每轮使用 resolve_round.elapsed_seconds,最多一天;未提供时用 clock.roundSeconds,默认 30。轮次之间可用 advance_time 的 by_minutes 或 until_time 加 days_later 跳时,单次最多 clock.maxSkipDays,默认 30 天,并保存原因。起点取世界的 starting_time,否则取会话创建时的分钟。
  • 现实模式跟随墙上时间,不回退、不能跳时,每轮用真实耗时。玩家离开至少五分钟后回来时,游戏主持人会得到离开时长,并负责说明这段时间中的变化。

每一步写入 time_log,类型为 start、elapsed、real、skip 或 resync,并带回合和轮次。事件时间取 session.clock.at(turn, round)。时区只影响显示。游戏主持人在回合开始和每轮后看到完整日期时间,角色看到 Now: ...,记录员得到每轮开始时刻和时长。

状态包提供 inWindow、catchUpPlan、nextTimeOfDay、parseInstant、describeInstant、describeDuration 和 dayPart。日期采用公历。它还能确定性解析月份、日期、日期区间、每月日期、月内第几个星期、按半球计算的季节、世界节日和排除项。可选 calendar.json 在会话开始时复制到元数据。

回合中的玩家角色​

系统从不要求玩家角色自行行动,但当玩家行为移动角色或操作物品时,resolve_round 可按角色键包含其结果。普通移动使用 moves_to 并检查开放通路;故事搬运或强制移动可用 ignore_exits。物品使用 item_changes。它与其他角色接受相同验证并写入轮次。没有其他人反应时,可不调用 activate_characters,直接由该结果形成一轮,仍计入回合上限。模拟模式下,轮次时长不能短于路径中全部出口给出的 travel_seconds 总和。

日程、约定与目标​

状态包的 agendaAt 合并三层信息:

  • routine_windows 是按顺序匹配的日程。它按故事时区、可选的 days 星期和 on 日历标记判断,第一个匹配项生效。
  • engagements 是带日期的约定,状态为 pending、kept、missed 或 cancelled。记录员通过 engagements 与 engagement_edits 写入。代码不自行判断是否履约;过时的 pending 项会显示为逾期,等待记录员依据轮次判断。
  • intents 是角色目标,包含类型、优先度、紧迫度、截止时间和计划。开始时由世界提供,之后由记录员通过 intents_added 和 intent_edits 修改。活动目标按优先度、紧迫度和距截止时间排序。

角色简报显示当前与下个日程、应做事项和前三个目标,并明确它们是计划而非命令。游戏主持人看到日程移动、同场角色本应去往别处的提示,以及半小时内到期或已经逾期的最多六项约定。记录员看到活动目标和 pending 约定。get_agenda 读取角色整日安排。

时间移动时,代码执行补进度,不调用模型。玩家不在场且日程窗发生变化的角色会忽略出口前往日程地点并更新活动。游标保存在 agenda.cursor。没有日程、并非背景或群体且在跳时期间离场的角色,会进入整合队列,最多同时三个,由世界作者在角色生活确实需要时补充日程。

离场行动​

交付回合后,后台会处理远离玩家且到期的约定和逾期目标。每回合最多 offstage.perTurn 个角色,默认 2,0 为关闭;按最旧事项优先,同一对话一次运行一个。offstage.perCharacter 默认限制每个角色一次。处理标记存在元数据 offscene.handled,无人能回答的事项稍后重试。

角色收到自己的简报、日程、记忆和目标,并在无人观察的前提下决策。其活动改为所选意图;中高承诺且事项有地点时,会忽略出口前往。结果写成从 100 开始编号的离场轮次,避免与游戏主持人轮次冲突,只有参与者形成感知。记录员立即写入结果并更新约定或目标。

玩家离开对话期间没有计时器,也不会消耗 token。超出每回合限制的事项继续保持到期,留给后续玩家消息逐步处理。

默认开启 offstage.summary。同一请求还必须给出最多 300 字符、第三人称过去时、无思想或秘密的 account。这些说明和本回合的日程移动合并到最多八行的 away note,在界面显示为 Meanwhile 卡片,下个回合开始时清除。卡片不进入对话历史,实际行动仍由记录保存。

故事变量​

变量统一表示情绪、伤势、债务、标志和声望。variable_defs 声明 number、flag 或 choice,适用对象可为角色、物品、地点或世界,并包含范围或选项、默认值、可见性、数字分段及可选基线。variable_values 只存与默认值不同的值。valence、arousal、dominance 是范围 -1 到 1、对角色私有且有气质基线的内建变量,不再使用独立情绪表。

世界可通过 variables.json 声明变量。故事中只有世界作者可在 expand_world 的 variable 原因满足门槛时通过 edit_world 创建,最多 60 个。游戏主持人的 variable_changes 和记录员的同名编辑可使用 to、受边界限制的 by 或 baseline 修改。未知变量、错误对象或越界值会被逐项拒绝。

角色看到自身公开和私有变量,以及同场者的公开变量;游戏主持人看到全部,场景优先,最多 14 行;记录员看到非默认值并可调用 list_variables。默认只显示与默认值不同或标记 always 的值,每个对象最多八项。

骰子​

骰子检定和掷骰记录是故事状态的一部分(数据库结构版本 8)。dice_checks 保存世界的检定,在会话开始时从世界的 dice.json 复制而来:key、名称、何时使用(use_text)以及一份 JSON 定义,包含骰子、读取方式(total 或 successes)、比较方式(相对目标 absolute、over 或 under)、可选的要加上的角色变量,以及各个区段。dice_rolls 是一份日志:回合、检定(普通掷骰则为空)、表达式、每一个骰子和保留的骰子、修正值、目标、总和、被读取的数值、落入的区段、原因、角色,以及发起者(agent 或 player)。掷骰在掷出时就被记录,与工具作答在同一步,因此重启或回退恢复的恰好是被恢复的状态所拥有的掷骰。检定在记法无效、没有区段、区段缺少标签或结果、或骰子可能给出没有区段包含的数值时会被拒绝(对照目标或带修正值的检定在掷出时检查)。

世界触发器​

角色计划、目标和约定表示角色想做什么;触发器表示世界在记录条件满足时确定执行什么。triggers 保存定义、dormant、fired、failed 或 expired 状态、触发次数、上次触发、上次条件值、失败原因和 world 或 story 来源。trigger_notes 保存触发后留下的 beat 与 cue,状态为 pending、done 或 lapsed。

状态包的 records/trigger-rules.ts 与 records/triggers.ts 只读取记录,确定性判断时间、角色位置、变量、物品持有或地点、事件标签及其他触发器。自由文本的 judged 条件由后台判断器给出真假,触发本身仍由代码读取该判定。条件从假变真时触发,默认一次;支持优先级、每次检查上限、带种子的概率、故事时间冷却和过期。一个触发器的效果在同一事务中执行,edit 效果走记录员相同的校验路径并封存事件;失败时标记 failed。

带 announce 的触发器可预先公开,受众可为所有人、指定角色或地点,并可给出文本、from 和 at。触发前,游戏主持人会看到谁知道什么,符合受众的角色会看到最近三项及日期和剩余时间。没有 announce 的触发器保持秘密。秘密事件发生后的传播必须通过事件的 perceived_by: "everyone"、面向所有人或地点的 cue,或角色在故事中交谈完成。

未公开且未触发的内容不会进入提示词。触发后,游戏主持人在回合开始、resolve_round 或 advance_time 结果中看到世界变化和待处理 beat;角色在简报中一次性收到 cue。游戏主持人通过 narrate.addressed 报告已采用的 beat。beat 在 triggers.beatTurns 后失效,cue 在 triggers.cueTurns 后失效。

世界作者可在游戏主持人以 expand_world 的 trigger 原因请求时,通过 authoring 模式的 edit_world.triggers 创建触发器。它必须是故事已经建立、必然会到来且可由记录证明的时刻,并验证全部引用;同一编辑内新建的触发器可以相互引用。

相关设置为 triggers.enabled、triggers.maxFires、triggers.beatTurns 和 triggers.cueTurns。尚未实现的内容包括按约定状态触发、强制角色行动、触发器之上的幕与分支脚本、由记录员或游戏主持人直接创建触发器、休眠触发器调试页,以及实时模式长时间离开后的聚合补算。长时间离开时,每个触发器只会在下次检查时触发一次。