跳转至

会话是一棵树:每轮挂一份场景快照,重新生成是同级分支,废弃分支的账留在原地,从存档可以分出新世界线

重来和回档:会话是一棵树

网页版 ChatGPT 的重新生成只是换一条回复,它没有别的状态要管。梨园有了场景快照、前情提要和跨局记忆,重来一次就得回答:废掉的那版已经记过的账、压过的摘要,算不算数?梨园的回答是一条原则——世界是当前分支的函数。下面每一步给出梨园源码,和同一段代码真跑出来的效果。

重新生成是同级分支

问题:重新生成时,旧回复要留着随时能切回去,新回复不能覆盖它。

梨园的会话存在 pi 的会话树里,树只追加不改写。重新生成(swipe)时,把树的当前位置挪回最后一条用户消息,再生成一条回复,新回复就成了旧回复的同级分支;左右切换变体,就是把当前位置跳到另一条分支的末端(server/main.ts:1108)。

实跑第一轮同一句用户原话生成了两次,两条分支共用开场白和这句原话,各自往下长:

效果 · 分支 A 的回复
她正往账本上记什么,闻言笔尖一顿,墨在纸上洇开一点。

她没有立刻应声,只把笔轻轻搁下,起身走到柜台后那面墙前。墙上挂着一把青灰色的旧油纸伞,伞骨断了一根,伞面挨近边缘处有一小块焦痕。她踮脚把伞取下来,用袖口沿着伞面慢慢地拂,动作很轻,像在擦一道早该擦掉、却总也擦不掉的旧痕。

「记得。」她背对着你,声音照旧不高,混在檐外的雨声里,「这镇子小,一条街,一个渡口。走一个人,十年也散不干净。」

她把伞重新挂回去,把系绳结得仔细,才转回柜台前,替你续了半盏热茶。茶烟腾起来,隔在你们中间。

「客官想打听他的什么?」
效果 · 分支 B 的回复
*青梧擦着茶碗的手停了。只停了一瞬,像雨点恰好砸在檐下某片松动的瓦上。*

*她把抹布搭回柜台,走到墙边,踮脚取下那把旧油纸伞。伞面上蒙着薄薄的灰,她用袖口一点一点地擦,动作很慢,慢得能听见指节擦过伞骨的轻响。伞骨断了一根,那处纸面微微塌下去,像一只合不拢的眼睛。*

「记得。」*她说,声音还是不高,目光落在伞上。*「沈家旧宅的独子。十年前一个雨夜,从镇北渡口走的,此后再没回来过。」

*她终于抬起眼,看向你,眼底平静,像深夜里不动的河水。*

「镇上记得他的人不少。但肯为一个名字,在这种天气赶来住三天的,客官是头一个。」

账本跟着分支走

问题:有了账本以后,废掉的那版已经记过账,状态里带着废稿里发生的事。

场景快照、前情提要、面板数据都作为条目挂在树上。每轮从当前分支的末端往回找最近一份,现算出来:

模版 · src/stage/assemble.ts:245-255
/** 世界状态 = f(分支):最近一条 rp-state 快照;无快照 = 初始状态(R4 读侧) */
export function stateFromBranch(branch: BranchEntryLike[]): WorldState {
    branch = storyBranch(branch);
    for (let i = branch.length - 1; i >= 0; i--) {
        const e = branch[i];
        if (e.type === "custom" && e.customType === "rp-state" && e.data && typeof e.data === "object") {
            return { ...defaultState(), ...(e.data as Partial<WorldState>) };
        }
    }
    return defaultState();
}

设计文档 docs/PLAN-RP-HARNESS.md:92 把这条定成原则:

R4 世界 = f(分支)。账本从当前分支的 rp-state 快照重建,磁盘文件降级为缓存。swipe/rewind/世界线切换天然看到正确账本——泄漏整类消失,不再需要恢复桥。

拿上面两条分支各跑一次 stateFromBranch:

效果 · 分支 A 的账本
时间:傍晚(雨中)
地点:青梧的客栈(檐外下雨)
青梧:好感 5;状态:客栈掌柜;在:青梧的客栈;备注:对怀瑾公子一事知情,态度谨慎回避,似乎怀有旧情;取下墙上断骨的旧油纸伞仔细拂拭后挂回
旅人:好感 0;在:青梧的客栈
旅人入住天数:三天
旅人打听对象:怀瑾公子(十年前从镇北渡口离开)
青梧对怀瑾的态度:知情但不轻易吐露
剧情线:「旅人查访十年前从镇北渡口离开的怀瑾公子」 「墙上断骨带焦痕的旧油纸伞与怀瑾疑似有关,青梧对其有难言旧情」
效果 · 分支 B 的账本
时间:雨夜(旅人抵达客栈当日,夜)
地点:镇北渡口附近的客栈
旅人:好感 5;在场;备注:为打听怀瑾公子来到镇上,计划住三天
青梧:好感 8;在场;备注:客栈掌柜。记得十年前雨夜从镇北渡口离开的沈家独子怀瑾公子,对旅人主动提起此事抱有留意
物品:旧油纸伞(伞骨断了一根,青梧持有)、旅人的湿外衣(旅人持有)
青梧已知旅人来意:旅人询问十年前从镇北渡口离开的怀瑾公子(沈家旧宅独子)的下落
剧情线:「旅人在客栈住三天,打听十年前雨夜从镇北渡口离开的怀瑾公子」 「青梧收着那把伞骨断裂的旧油纸伞,与怀瑾公子或十年前之事有关」

两份账本各记各的:A 的青梧是「客栈掌柜」,B 里她已经说出了「沈家旧宅的独子」,B 的账本记下了这件事,A 的账本里没有。切回 A,B 记过的账不在 A 的路径上,自然看不到——重新生成不需要任何「恢复账本」的代码。

用户在界面上手改账本,也是在当前位置追加一条新快照,切到别的分支就看不到这次修改。前情提要同理:压缩写下的 rp-summary 挂在哪条分支上,就只对哪条分支生效。

叶守卫:迟到的账整份丢掉

问题:场记和压缩是收笔后的旁路调用,要跑好几秒;这期间用户可能已经切了分支,迟到的结果会记到别人的分支上。

旁路调用前后各读一次树的当前位置(叶),两次不一致就整份丢弃:

模版 · src/stage/scribe-run.ts:88-92
    // R9 叶守卫:调用期间树动过(swipe/rewind/切线)→ 整体丢弃
    if (deps.getLeafId() !== leafBefore) {
        deps.onActivity?.("记账已丢弃(本拍期间切换了分支)");
        return { kind: "stale" };
    }

压缩落摘要前有同样一道(src/stage/compact.ts:274)。另外,台上正在生成时,界面直接拒绝重新生成、切换和压缩。

向量记忆也按分支过滤

问题:向量库不在树上,重新生成废掉的那几轮照样能被召回。src/memory/service.ts:55 记着一次实测(原文的「拍」就是一轮):

13 次重roll 中第 5、10 拍入库,第 11 拍起开始写「承接上一拍」

模型把被废掉的版本当成了上一轮。现在入库时带上树节点的 id,检索、召回、列举都只取当前分支上的节点(server/main.ts:800)。

存档和世界线

问题:想停在某一刻,过几天回来换个走向接着玩。

  • 存档是树上的一条 rp-save 条目,排在场记之后写,保证存档紧跟这一轮的账本
  • 回档就是把当前位置跳回那个节点,账本、摘要随之回到当时
  • 世界线:新存档时看一眼,上一个存档之后有没有不在当前分支上的存档——有,说明从那里走出过另一条后续,这次就分出一条新世界线;没有,就是延长当前这条
模版 · src/worldline.ts:193-237
    const prev = opts.prevOnBranch;

    if (!prev) {
        return {
            id,
            name,
            worldlineId: newId(),
            worldlineName: "主线",
            createdAt: now,
        };
    }

    // prev 之后是否存在「不在当前分支上」的存档 → 曾走出过另一条后续;本次若是分叉后首存则开新线。
    // 调用方应传入 latestSaveOnBranch 作为 prev(当前线上最近钉),故不会再有「prev 之后的同线存档」。
    const divergent = opts.allSaves.filter((s) => {
        if (s.id === prev.id) return false;
        if (opts.branchEntryIds.has(s.entryId)) return false;
        return opts.ancestorsOf(s.entryId).has(prev.entryId);
    });

    if (divergent.length > 0) {
        const forkCount = new Set(
            opts.allSaves.filter((s) => s.forkFromSaveId === prev.id).map((s) => s.worldlineId),
        ).size;
        return {
            id,
            name,
            worldlineId: newId(),
            worldlineName: `从「${prev.name}」分出 · 线${forkCount + 2}`,
            parentSaveId: prev.id,
            forkFromSaveId: prev.id,
            createdAt: now,
        };
    }

    // 同线延长
    return {
        id,
        name,
        worldlineId: prev.worldlineId,
        worldlineName: prev.worldlineName,
        parentSaveId: prev.id,
        ...(prev.forkFromSaveId ? { forkFromSaveId: prev.forkFromSaveId } : {}),
        createdAt: now,
    };

实跑搭一棵小树过 planNewSave:开场 → 第一轮回复(存档「雨夜问名」)→ 第二轮回复(存档「直问生死」);再回到第一轮的存档,换个走向演一轮,在新的末端存档「坦言来意」。另外在一棵没有存档的树上存第一个档「开场」:

效果 · planNewSave 的三次规划
{
 "sameLine": {
  "id": "61183e42a558bf23",
  "name": "直问生死",
  "worldlineId": "w-main",
  "worldlineName": "主线",
  "parentSaveId": "s1",
  "createdAt": 3
 },
 "fork": {
  "id": "d357f6456693e6fc",
  "name": "坦言来意",
  "worldlineId": "f304cb5043ffb4fd",
  "worldlineName": "从「雨夜问名」分出 · 线2",
  "parentSaveId": "s1",
  "forkFromSaveId": "s1",
  "createdAt": 3
 },
 "first": {
  "id": "90a5c5097ea1ed13",
  "name": "开场",
  "worldlineId": "32401a96fc97b5b5",
  "worldlineName": "主线",
  "createdAt": 0
 }
}
  • first:分支上没有更早的存档,开一条新的「主线」
  • sameLine:在原路径的末端再存,「雨夜问名」之后的存档都在当前分支上,延长主线
  • fork:回档后换了走向,「直问生死」在「雨夜问名」之后、却不在当前分支上,于是分出「从「雨夜问名」分出 · 线2」

世界线的改名、软删除记在旁路文件 世界线.json 里,不往树里写。

agent 模式不用树导航

问题:写小说时要回到的是「某一次改稿之前」,不是对话树上的某个节点。

agent 模式不靠会话树回退,靠每轮末尾的文件检查点,可以只恢复正文文件,也可以文件和对话一起回去,见正文怎么产出。

谁管什么

  • 程序:分支怎么挂、按分支现算账本和摘要、叶守卫、向量按分支过滤、分不分新世界线
  • 模型:不参与,回退和分支全由程序处理
  • 用户:重新生成、左右切换、存档、回档、给世界线改名

实跑原件

两条分支过 rebuildHistory 和 stateFromBranch 的输出
{
 "branches": [
  {
   "history": [
    {
     "role": "assistant",
     "text": "【开场 · 青梧】\n*雨下到第三天,青石板路上的水洼连成了片。你推开听雨轩的门,檐角的铜铃轻响了一声。*\n\n*柜台后的女子抬起头来,目光在你被雨水浸透的肩头停了一瞬,随即移开,像是什么都没看见。她取过一只干净的粗瓷杯,倒了盏冒着热气的茶,推到柜台外沿。*\n\n「这样的天气还赶路,多半是有不得不赶的缘由。」*她的声音不高,混在雨声里却听得很清楚。*「茶是热的,房也有空的。客官打算住几天?」"
    },
    {
     "role": "user",
     "text": "我把湿透的外衣搭在椅背上,接过茶喝了一口:「住三天。顺便打听个人——十年前从镇北渡口走的那位怀瑾公子,掌柜可还记得?」"
    },
    {
     "role": "assistant",
     "text": "她正往账本上记什么,闻言笔尖一顿,墨在纸上洇开一点。\n\n她没有立刻应声,只把笔轻轻搁下,起身走到柜台后那面墙前。墙上挂着一把青灰色的旧油纸伞,伞骨断了一根,伞面挨近边缘处有一小块焦痕。她踮脚把伞取下来,用袖口沿着伞面慢慢地拂,动作很轻,像在擦一道早该擦掉、却总也擦不掉的旧痕。\n\n「记得。」她背对着你,声音照旧不高,混在檐外的雨声里,「这镇子小,一条街,一个渡口。走一个人,十年也散不干净。」\n\n她把伞重新挂回去,把系绳结得仔细,才转回柜台前,替你续了半盏热茶。茶烟腾起来,隔在你们中间。\n\n「客官想打听他的什么?」"
    }
   ],
   "state": "时间:傍晚(雨中)\n地点:青梧的客栈(檐外下雨)\n青梧:好感 5;状态:客栈掌柜;在:青梧的客栈;备注:对怀瑾公子一事知情,态度谨慎回避,似乎怀有旧情;取下墙上断骨的旧油纸伞仔细拂拭后挂回\n旅人:好感 0;在:青梧的客栈\n旅人入住天数:三天\n旅人打听对象:怀瑾公子(十年前从镇北渡口离开)\n青梧对怀瑾的态度:知情但不轻易吐露\n剧情线:「旅人查访十年前从镇北渡口离开的怀瑾公子」 「墙上断骨带焦痕的旧油纸伞与怀瑾疑似有关,青梧对其有难言旧情」"
  },
  {
   "history": [
    {
     "role": "assistant",
     "text": "【开场 · 青梧】\n*雨下到第三天,青石板路上的水洼连成了片。你推开听雨轩的门,檐角的铜铃轻响了一声。*\n\n*柜台后的女子抬起头来,目光在你被雨水浸透的肩头停了一瞬,随即移开,像是什么都没看见。她取过一只干净的粗瓷杯,倒了盏冒着热气的茶,推到柜台外沿。*\n\n「这样的天气还赶路,多半是有不得不赶的缘由。」*她的声音不高,混在雨声里却听得很清楚。*「茶是热的,房也有空的。客官打算住几天?」"
    },
    {
     "role": "user",
     "text": "我把湿透的外衣搭在椅背上,接过茶喝了一口:「住三天。顺便打听个人——十年前从镇北渡口走的那位怀瑾公子,掌柜可还记得?」"
    },
    {
     "role": "assistant",
     "text": "*青梧擦着茶碗的手停了。只停了一瞬,像雨点恰好砸在檐下某片松动的瓦上。*\n\n*她把抹布搭回柜台,走到墙边,踮脚取下那把旧油纸伞。伞面上蒙着薄薄的灰,她用袖口一点一点地擦,动作很慢,慢得能听见指节擦过伞骨的轻响。伞骨断了一根,那处纸面微微塌下去,像一只合不拢的眼睛。*\n\n「记得。」*她说,声音还是不高,目光落在伞上。*「沈家旧宅的独子。十年前一个雨夜,从镇北渡口走的,此后再没回来过。」\n\n*她终于抬起眼,看向你,眼底平静,像深夜里不动的河水。*\n\n「镇上记得他的人不少。但肯为一个名字,在这种天气赶来住三天的,客官是头一个。」"
    }
   ],
   "state": "时间:雨夜(旅人抵达客栈当日,夜)\n地点:镇北渡口附近的客栈\n旅人:好感 5;在场;备注:为打听怀瑾公子来到镇上,计划住三天\n青梧:好感 8;在场;备注:客栈掌柜。记得十年前雨夜从镇北渡口离开的沈家独子怀瑾公子,对旅人主动提起此事抱有留意\n物品:旧油纸伞(伞骨断了一根,青梧持有)、旅人的湿外衣(旅人持有)\n青梧已知旅人来意:旅人询问十年前从镇北渡口离开的怀瑾公子(沈家旧宅独子)的下落\n剧情线:「旅人在客栈住三天,打听十年前雨夜从镇北渡口离开的怀瑾公子」 「青梧收着那把伞骨断裂的旧油纸伞,与怀瑾公子或十年前之事有关」"
  }
 ]
}
分支 B 那一轮的完整记录:注入、思考、工具调用、定稿、场记
{
 "user": "我把湿透的外衣搭在椅背上,接过茶喝了一口:「住三天。顺便打听个人——十年前从镇北渡口走的那位怀瑾公子,掌柜可还记得?」",
 "injection": "【世界状态】\n(尚无记录)\n\n【登场名录】(尚无登记)\n\n【相关设定】本拍命中 3 条:听雨轩、怀瑾、镇北渡口\n\n【设定集索引】共 5 条:听雨轩、旧油纸伞、怀瑾、镇北渡口、更夫\n\n【语言】以中文写叙事与对白(专有名词可保留原文)。",
 "activated": [
  "听雨轩",
  "怀瑾",
  "镇北渡口"
 ],
 "messages_sent": [
  {
   "role": "system",
   "content": "(system,见 system.json)"
  },
  {
   "role": "assistant",
   "content": "【开场 · 青梧】\n*雨下到第三天,青石板路上的水洼连成了片。你推开听雨轩的门,檐角的铜铃轻响了一声。*\n\n*柜台后的女子抬起头来,目光在你被雨水浸透的肩头停了一瞬,随即移开,像是什么都没看见。她取过一只干净的粗瓷杯,倒了盏冒着热气的茶,推到柜台外沿。*\n\n「这样的天气还赶路,多半是有不得不赶的缘由。」*她的声音不高,混在雨声里却听得很清楚。*「茶是热的,房也有空的。客官打算住几天?」"
  },
  {
   "role": "user",
   "content": "【世界状态】\n(尚无记录)\n\n【登场名录】(尚无登记)\n\n【相关设定】本拍命中 3 条:听雨轩、怀瑾、镇北渡口\n\n【设定集索引】共 5 条:听雨轩、旧油纸伞、怀瑾、镇北渡口、更夫\n\n【语言】以中文写叙事与对白(专有名词可保留原文)。\n\n我把湿透的外衣搭在椅背上,接过茶喝了一口:「住三天。顺便打听个人——十年前从镇北渡口走的那位怀瑾公子,掌柜可还记得?」"
  }
 ],
 "steps": [
  {
   "round": 1,
   "reasoning": "用户扮演旅人,接开场。他提到「怀瑾公子」「十年前从镇北渡口走」。这涉及设定条目:听雨轩、怀瑾、镇北渡口。我需要先查设定再写,不能自己发明。\n\n让我查 lorebook 里「怀瑾」和「镇北渡口」的正文。同时查听雨轩。\n\n我先并行搜索/读取。lorebook_search 给 ref 或内容。我先用 lorebook_search 查「怀瑾」「镇北渡口」。\n\n实际上相关设定已经给了标题,但正文要用 lorebook_search 取。我并行查这几个。",
   "content": "",
   "calls": [
    {
     "name": "lorebook_search",
     "args": {
      "query": "怀瑾"
     },
     "result": [
      {
       "title": "怀瑾",
       "score": 9,
       "content": "沈怀瑾,镇北沈家旧宅的独子,十年前一个雨夜从镇北渡口搭船离开,此后音讯全无。他走的那晚,把一把青灰色的油纸伞留在了听雨轩。青梧从不提他的名字,有人提起时,她会先去擦那把伞。"
      }
     ]
    },
    {
     "name": "lorebook_search",
     "args": {
      "query": "镇北渡口"
     },
     "result": [
      {
       "title": "镇北渡口",
       "score": 9,
       "content": "镇北渡口每日只有清晨一班船出镇。船夫老周摆渡三十年,认得每一个离镇的人。"
      },
      {
       "title": "怀瑾",
       "score": 1,
       "content": "沈怀瑾,镇北沈家旧宅的独子,十年前一个雨夜从镇北渡口搭船离开,此后音讯全无。他走的那晚,把一把青灰色的油纸伞留在了听雨轩。青梧从不提他的名字,有人提起时,她会先去擦那把伞。"
      }
     ]
    },
    {
     "name": "lorebook_search",
     "args": {
      "query": "听雨轩"
     },
     "result": [
      {
       "title": "听雨轩",
       "score": 10,
       "content": "听雨轩是栖水镇上开了近百年的老客栈,前后两进,后院有一株老梨树。店里的规矩是祖上传下来的:不问来处,不问去处,账清即可。镇上人都说,在听雨轩说过的话,不会传到第二个人耳朵里。"
      },
      {
       "title": "栖水镇",
       "score": 1,
       "content": "栖水镇是江南水网间的小镇,一条青石街贯穿南北,镇北有渡口,镇南是茶田。镇上除了听雨轩没有别的旅店,夜里亥时之后由更夫巡街。"
      },
      {
       "title": "怀瑾",
       "score": 1,
       "content": "沈怀瑾,镇北沈家旧宅的独子,十年前一个雨夜从镇北渡口搭船离开,此后音讯全无。他走的那晚,把一把青灰色的油纸伞留在了听雨轩。青梧从不提他的名字,有人提起时,她会先去擦那把伞。"
      }
     ]
    }
   ],
   "usage": {
    "prompt_tokens": 7037,
    "completion_tokens": 247,
    "total_tokens": 7284,
    "prompt_tokens_details": {
     "cached_tokens": 6912
    },
    "completion_tokens_details": {
     "reasoning_tokens": 131
    },
    "prompt_cache_hit_tokens": 6912,
    "prompt_cache_miss_tokens": 125
   }
  },
  {
   "round": 2,
   "reasoning": "查到了关键设定:\n\n- 沈怀瑾,镇北沈家旧宅独子,十年前雨夜从镇北渡口搭船离开,音讯全无。走那晚把青灰色油纸伞留在听雨轩。青梧从不提他的名字,有人提起时,她会先去擦那把伞。\n- 镇北渡口:每日清晨一班船出镇。船夫老周摆渡三十年,认得每一个离镇的人。\n- 听雨轩规矩:不问来处,不问去处,账清即可。说过的话不传第二人耳。\n\n所以旅人问「怀瑾公子」,正好戳中青梧的忌讳/心事。青梧会先去擦那把伞(或至少手上有这个动作的倾向)。这是卡里明确写的反应:「有人提起时,她会先去擦那把伞。」\n\n现在演这一拍。青梧的反应:温和沉静,言语简省。不主动打探。旅人开口就打听人——这其实违背了听雨轩「不问来处」的规矩,但那是店对自己的约束,不是客人的约束。青梧不会生硬拒绝,但怀瑾是她的心事,她不会直接答。\n\n设定明确:有人提起怀瑾,青梧会先去擦那把伞。伞在柜台后的墙上挂着,旧油纸伞,断了一根伞骨。我应该写这个动作。\n\n正文走向:青梧听到「怀瑾」二字,动作微微一顿。然后起身去擦那把伞。言语上不直接回答,可能反问或轻轻带过。言语简省,每句落在点上。\n\n这一拍要交付正文。旅人问了一个问题,青梧需要回应。我不应该替旅人决定,但这一拍是我演青梧的回应。走向上有一个岔口:青梧是承认记得,还是岔开。这是人物性格决定的,不是需要用户拍板的重大变量——青梧的反应有明确设定支撑(擦伞、从不提他名字)。所以我可以顺着演。\n\n我该怎么写青梧的回应?她不会否认「不记得」——她记性极好,且设定说她不主动提,问急了只是笑。旅人直接问「可还记得」,这不算逼问,是正常打听。\n\n青梧的反应:擦伞,然后可能说一句点到为止的话。比如「记得」或「客官打听他,是为了什么」——但听雨轩规矩是不问来处去处,所以她也不会反问太多。\n\n我倾向于:青梧沉默一瞬,起身去擦墙上的伞。动作多于言语。然后简短回应,比如「记得。十年前雨夜,从镇北渡口走的。」但关于他的下落,她不会多说,或说她也不知道(音讯全无,设定上确实无人知道)。\n\n要不要给旅人留钩子?可以。青梧可以反问:「客官寻他,是故人?」这样把球交回,让旅人交代一点来意。但不要替用户决定。\n\n让我写正文。这一拍不长,但要有质感。\n\n写法:\n- 旅人问出口后,青梧倒茶的手/擦杯子的动作停了一瞬。\n- 她没立刻答,走到墙边,摘下那把旧油纸伞,用袖口轻轻擦去伞面上的浮尘。\n- 然后说:「记得。」语气平,然后看向旅人:「沈家旧宅的独子,十年前一个雨夜从渡口走的,此后再没回来过。」\n- 她可以补一句:「客官是他什么人?」——这算不主动打探吗?问来意不算打探客人的来历隐私,是正常的。但设定说「不主动打探客人的来历」。问「是他什么人」有点像打探。也许更克制:她只陈述记得的事实,把「为什么打听」的空白留给旅人自己填,或者用一句「镇上的人,大多记得他。」来轻轻带过。\n\n我觉得青梧的核心反应是擦伞这个动作,以及承认记得但不主动展开。然后可以停顿,等旅人进一步说明。\n\n让我写。直接 draft_write。\n\n不需要 beat_plan,这是简单回应。\n\n正文要精炼。写个三四段。",
   "content": "",
   "calls": [
    {
     "name": "draft_write",
     "args": {
      "version": 0,
      "content": "*青梧擦着茶碗的手停了。只停了一瞬,像雨点恰好砸在檐下某片松动的瓦上。*\n\n*她把抹布搭回柜台,走到墙边,踮脚取下那把旧油纸伞。伞面上蒙着薄薄的灰,她用袖口一点一点地擦,动作很慢,慢得能听见指节擦过伞骨的轻响。伞骨断了一根,那处纸面微微塌下去,像一只合不拢的眼睛。*\n\n「记得。」*她说,声音还是不高,目光落在伞上。*「沈家旧宅的独子。十年前一个雨夜,从镇北渡口走的,此后再没回来过。」\n\n*她终于抬起眼,看向你,眼底平静,像深夜里不动的河水。*\n\n「镇上记得他的人不少。但肯为一个名字,在这种天气赶来住三天的,客官是头一个。」\n"
     },
     "result": {
      "ok": true,
      "version": 1,
      "chars": 265
     }
    }
   ],
   "usage": {
    "prompt_tokens": 7762,
    "completion_tokens": 1127,
    "total_tokens": 8889,
    "prompt_tokens_details": {
     "cached_tokens": 7168
    },
    "completion_tokens_details": {
     "reasoning_tokens": 872
    },
    "prompt_cache_hit_tokens": 7168,
    "prompt_cache_miss_tokens": 594
   }
  },
  {
   "round": 3,
   "reasoning": "",
   "content": "",
   "calls": [
    {
     "name": "draft_seal",
     "args": {
      "version": 1
     },
     "result": {
      "ok": true,
      "sealed": true,
      "version": 1
     }
    }
   ],
   "usage": {
    "prompt_tokens": 8917,
    "completion_tokens": 46,
    "total_tokens": 8963,
    "prompt_tokens_details": {
     "cached_tokens": 8832
    },
    "completion_tokens_details": {
     "reasoning_tokens": 0
    },
    "prompt_cache_hit_tokens": 8832,
    "prompt_cache_miss_tokens": 85
   }
  }
 ],
 "final": "*青梧擦着茶碗的手停了。只停了一瞬,像雨点恰好砸在檐下某片松动的瓦上。*\n\n*她把抹布搭回柜台,走到墙边,踮脚取下那把旧油纸伞。伞面上蒙着薄薄的灰,她用袖口一点一点地擦,动作很慢,慢得能听见指节擦过伞骨的轻响。伞骨断了一根,那处纸面微微塌下去,像一只合不拢的眼睛。*\n\n「记得。」*她说,声音还是不高,目光落在伞上。*「沈家旧宅的独子。十年前一个雨夜,从镇北渡口走的,此后再没回来过。」\n\n*她终于抬起眼,看向你,眼底平静,像深夜里不动的河水。*\n\n「镇上记得他的人不少。但肯为一个名字,在这种天气赶来住三天的,客官是头一个。」\n",
 "sealed": true,
 "scribe": {
  "systemPrompt": "你是一场角色扮演的场记。阅读【当前账本】与【本轮对话】,只做一件事:输出 JSON,更新需要记账的持久变化。\n\n输出唯一字段:\n\"patch\":从本轮对话中提取需要记账的持久变化。字段语义:\n- \"time\" / \"location\":字符串,整体替换。剧内时间推移(入夜、次日清晨、数日后)必须更新 time。\n- \"characters\":{ \"名字\": { \"affinity\"?, \"status\"?, \"notes\"?, \"at\"? } },按字段合并。affinity 为 -100..100 的对旅人态度值,基于账本当前值小步调整(通常 ±1~10)。\"at\" 是该角色此刻所在地,与 location 同一写法;离开当前场景的人写他去了哪。用户角色写作「旅人」;只有全新出场的人物才建新条目,键用正文中的人名——不要把作品/剧本标题(如「青梧」这类非人名)当作角色。\n- \"inventory\":字符串数组,整体替换——只在物品归属变化时给出变化后的完整清单,条目注明归属(如「黄铜怀表(旅人持有)」)。\n- \"flags\":键值对,按键合并(值为字符串)。\n- \"plot_threads\":字符串数组,整体替换——新增或了结剧情线时给出完整清单。\n要点:否定性事件也要记账(赠礼被拒→物品仍在原主处;承诺被收回→记入 flags);新的承诺、约定、伏笔进 plot_threads;没有变化的字段不要出现在 patch 中;完全无变化则 \"patch\" 为 {}。\n\n只输出 JSON 对象,例如 {\"patch\":{...}} 或 {\"patch\":{}}。不要输出 warnings、不要输出其他文字。",
  "userText": "【当前账本】\n{\n  \"time\": \"\",\n  \"location\": \"\",\n  \"characters\": {},\n  \"inventory\": [],\n  \"flags\": {},\n  \"plot_threads\": []\n}\n\n【本轮对话】\n旅人:我把湿透的外衣搭在椅背上,接过茶喝了一口:「住三天。顺便打听个人——十年前从镇北渡口走的那位怀瑾公子,掌柜可还记得?」\n\n青梧:*青梧擦着茶碗的手停了。只停了一瞬,像雨点恰好砸在檐下某片松动的瓦上。*\n\n*她把抹布搭回柜台,走到墙边,踮脚取下那把旧油纸伞。伞面上蒙着薄薄的灰,她用袖口一点一点地擦,动作很慢,慢得能听见指节擦过伞骨的轻响。伞骨断了一根,那处纸面微微塌下去,像一只合不拢的眼睛。*\n\n「记得。」*她说,声音还是不高,目光落在伞上。*「沈家旧宅的独子。十年前一个雨夜,从镇北渡口走的,此后再没回来过。」\n\n*她终于抬起眼,看向你,眼底平静,像深夜里不动的河水。*\n\n「镇上记得他的人不少。但肯为一个名字,在这种天气赶来住三天的,客官是头一个。」\n",
  "raw": "{\"patch\":{\"time\":\"雨夜(旅人抵达客栈当日,夜)\",\"location\":\"镇北渡口附近的客栈\",\"characters\":{\"旅人\":{\"affinity\":5,\"notes\":\"为打听怀瑾公子来到镇上,计划住三天\",\"at\":\"镇北渡口附近的客栈\"},\"青梧\":{\"affinity\":8,\"notes\":\"客栈掌柜。记得十年前雨夜从镇北渡口离开的沈家独子怀瑾公子,对旅人主动提起此事抱有留意\",\"at\":\"镇北渡口附近的客栈\"}},\"inventory\":[\"旧油纸伞(伞骨断了一根,青梧持有)\",\"旅人的湿外衣(旅人持有)\"],\"flags\":{\"青梧已知旅人来意\":\"旅人询问十年前从镇北渡口离开的怀瑾公子(沈家旧宅独子)的下落\"},\"plot_threads\":[\"旅人在客栈住三天,打听十年前雨夜从镇北渡口离开的怀瑾公子\",\"青梧收着那把伞骨断裂的旧油纸伞,与怀瑾公子或十年前之事有关\"]}}",
  "applied": [
   "time → 雨夜(旅人抵达客栈当日,夜)",
   "location → 镇北渡口附近的客栈",
   "characters.旅人 已更新",
   "characters.青梧 已更新",
   "inventory → [旧油纸伞(伞骨断了一根,青梧持有)、旅人的湿外衣(旅人持有)]",
   "flags.青梧已知旅人来意 → 旅人询问十年前从镇北渡口离开的怀瑾公子(沈家旧宅独子)的下落",
   "plot_threads → [旅人在客栈住三天,打听十年前雨夜从镇北渡口离开的怀瑾公子、青梧收着那把伞骨断裂的旧油纸伞,与怀瑾公子或十年前之事有关]"
  ],
  "warnings": [],
  "formatted": "时间:雨夜(旅人抵达客栈当日,夜)\n地点:镇北渡口附近的客栈\n旅人:好感 5;在场;备注:为打听怀瑾公子来到镇上,计划住三天\n青梧:好感 8;在场;备注:客栈掌柜。记得十年前雨夜从镇北渡口离开的沈家独子怀瑾公子,对旅人主动提起此事抱有留意\n物品:旧油纸伞(伞骨断了一根,青梧持有)、旅人的湿外衣(旅人持有)\n青梧已知旅人来意:旅人询问十年前从镇北渡口离开的怀瑾公子(沈家旧宅独子)的下落\n剧情线:「旅人在客栈住三天,打听十年前雨夜从镇北渡口离开的怀瑾公子」 「青梧收着那把伞骨断裂的旧油纸伞,与怀瑾公子或十年前之事有关」",
  "roster": "人物:旅人(雨夜(旅人抵达客栈当日,夜))、青梧(雨夜(旅人抵达客栈当日,夜));地点:镇北渡口附近的客栈(雨夜(旅人抵达客栈当日,夜));物品:旧油纸伞(伞骨断了一根,青梧持有)(雨夜(旅人抵达客栈当日,夜))、旅人的湿外衣(旅人持有)(雨夜(旅人抵达客栈当日,夜));剧情线:旅人在客栈住三天,打听十年前雨夜从镇北渡口离开的怀瑾公子(雨夜(旅人抵达客栈当日,夜))、青梧收着那把伞骨断裂的旧油纸伞,与怀瑾公子或十年前之事有关(雨夜(旅人抵达客栈当日,夜))"
 },
 "note": "梨园在 draft_seal / previous_draft_edit 成功后结束本轮(src/stage/engine.ts:1883),之后的模型调用已从记录里去掉"
}