diff --git a/docs/play-dm-auditor.md b/docs/play-dm-auditor.md index aacd4f3..9af42a0 100644 --- a/docs/play-dm-auditor.md +++ b/docs/play-dm-auditor.md @@ -1,7 +1,7 @@ # 主世界层 与 旁观维护:怎么配合 > 用白话说明「DM」和「副 LLM(旁观)」谁先谁后、谁干什么。 -> 实现上当前管线是:`旁观 →(可选角色视角)→ 主世界层 → 转述`。 +> 实现上当前管线是:`主世界层 →(可选角色视角)→ 转述 → 旁观维护`。 ## 1. 先说清楚两个角色 @@ -34,26 +34,27 @@ DM 出裁决包(含变量变更)→ 旁观只动表/触发生成 → 程序 ## 3. 本仓库采用的(推荐) -**既不是甲也不是乙的「整段返工」,而是:旁观轻扫 → DM 拍板 →(程序合并旁观的改表)→ 转述。** +**采用做法乙:主世界层先拍板 → 转述 → 旁观在回合末改表(合并结果供下一轮主世界层读取)。** ```text 每轮: - 1. 旁观:看规则合同 + 当前表 + 玩家输入 - → 多数轮:什么都不改(空 maintain) - → 少数轮:写 table_ops / need_generate(补字段或点名 rule_id) - 2. 主世界层:出 settlement 裁决包(可含变量变更) - 3. 程序:合并旁观改表 + DM 变量变更(边沿副作用照常) - 4. 转述:只根据裁决包写正文 + 1. 主世界层:读上一轮合并后的 变量.当前 + 设定 + 历史 + 用户输入 + → 出 settlement 裁决包(可含 variable_changes;可经 harness 调机遇工具) + 2. 程序:合并裁决中的 variable_changes → 变量.当前(边沿副作用照常) + 3. 叙事转述(若启用):只读裁决包写用户可见正文 + 4. 旁观维护:读本轮裁决 + 合同 + 表 + → 多数轮:空 maintain + → 少数轮:table_ops / need_generate + 5. 程序:合并旁观 table_ops → 变量.当前(**下一轮**主世界层才读到) ``` 要点: -1. **不要**默认「DM 写完再让旁观润色全文」(贵,且旁观会抢笔)。 -2. **不要**默认「旁观先写一大段设定再交给 DM」(旁观变作者)。 -3. **生成规则给旁观**:只要**合同**(有哪些 rule、格式、何时该生成),不要整本描写范文。 -4. 真要「按规则生成实例」:旁观点 `need_generate.rule_id`,由**程序/既定生成通路**落表,而不是旁观写小说。 - -若以后要改成「乙」:把旁观挪到 **DM 之后**、只保留工具改表、禁止旁观输出正文——可以再开一刀改调度序;当前以已落地的「旁观在前、轻维护」为准。 +1. **不要**默认「旁观先扫再让 DM 裁决」(旧顺序已废弃)。 +2. **不要**默认「DM 写完再让旁观润色全文」(贵,且旁观会抢笔)。 +3. **不要**默认「旁观先写一大段设定再交给 DM」(旁观变作者)。 +4. 生成规则给旁观:只要**合同**(有哪些 rule、格式、何时该生成),不要整本描写范文。 +5. 真要「按规则生成实例」:旁观点 `need_generate.rule_id`,由**程序/既定生成通路**落表,而不是旁观写小说。 ## 4. 和「生成规则要不要给副 LLM」的关系 @@ -64,5 +65,5 @@ DM 出裁决包(含变量变更)→ 旁观只动表/触发生成 → 程序 ## 5. 一句话 -**旁观看门与补表;DM 断事;转述说话。** -交互上优先「轻维护 + 一次裁决」,不优先「来回多轮 LLM 传话」。 +**主世界层断事;转述说话;旁观在回合末对表,改表给下一轮。** +交互上优先「一次裁决 + 延后轻维护」,不优先「来回多轮 LLM 传话」。 diff --git a/docs/progressive-data-design.md b/docs/progressive-data-design.md index f0759a6..a6a82aa 100644 --- a/docs/progressive-data-design.md +++ b/docs/progressive-data-design.md @@ -94,11 +94,10 @@ Progressive 投影 本轮主世界层只看见「当前档」;未 ```text 用户输入 - → 旁观维护(副 LLM):合同+表+本轮工单;默认空操作(maintain.v1) - →(可选)角色视角:强信息隔离时出反应建议 - → 主世界层(世界推演):读变量 + 已投影 data + 旁观包,交事件/改真值(按规则) + → 主世界层(世界推演):读变量 + 已投影 data,交事件/改真值(按规则) →(可选)叙事转述:把裁决写成用户可见正文 - → 用户看到的终稿通常来自「呈现/叙事」执行单元,或主世界层兼呈现(规格决定) + → 旁观维护(副 LLM):合同+表+本轮裁决;默认空操作(maintain.v1) + →(可选)角色视角:强信息隔离时出反应建议(默认关,插在主世界层之后) ``` | 内容 | 给谁 | 不要做成 | diff --git a/src/runtime/phase-runtime.ts b/src/runtime/phase-runtime.ts index 2efd64c..d2df57b 100644 --- a/src/runtime/phase-runtime.ts +++ b/src/runtime/phase-runtime.ts @@ -154,6 +154,14 @@ import type { } from "../types/runtime.js"; import type { BlackboardItem } from "../types/blackboard.js"; +/** 每轮开始前清掉的 transient tag(避免主世界层读到上一轮裁决/旁观包) */ +const PLAY_ROUND_TRANSIENT_TAGS = [ + SETTLEMENT_TAG, + MAINTAIN_TAG, + "运行.本轮.机遇", + "运行.本轮.变量变更", +] as const; + export type PhaseRuntimeOptions = { presetId?: string; flowId?: string; @@ -2072,6 +2080,7 @@ export class PhaseRuntime { /** 游玩:按 playWorkerIds 开一轮 */ private async startPlayTurn(): Promise { this.syncSlotsToBlackboard(this.session); + this.clearPlayRoundTransientTags(); const ids = this.getInstanceWorkerDeclaration().playWorkerIds; if (!ids.length) { this.onMessage( @@ -2118,6 +2127,29 @@ export class PhaseRuntime { }; } + /** 新回合开始前归档上一轮 transient 产物(表合并结果保留在 变量.当前) */ + private clearPlayRoundTransientTags(): void { + const slots = { ...this.session.slots }; + const archivedAt = new Date().toISOString(); + for (const tag of PLAY_ROUND_TRANSIENT_TAGS) { + const hadBoard = this.blackboard.getLatestByTag(tag) != null; + const hadSlot = tag in slots; + if (!hadBoard && !hadSlot) continue; + this.blackboard.write({ + tag, + content: "", + source: "runtime", + metadata: { + role: "archived", + archivedAt, + reason: "play_round_reset", + }, + }); + delete slots[tag]; + } + this.session = { ...this.session, slots }; + } + /** invoke_main_agent 副作用:running 时调用总管 LLM,链式推进直到需用户介入 */ private async maybeRunMainAgent(effects: PhaseEffect[]): Promise { if (inferLifecycleStage(this.session) === "play") return; diff --git a/src/skills/play-slots.ts b/src/skills/play-slots.ts index 215e81b..8d87773 100644 --- a/src/skills/play-slots.ts +++ b/src/skills/play-slots.ts @@ -6,7 +6,7 @@ import type { WorkerAcceptance, WorkerSetEntry } from "./worker-set-parse.js"; export type PlaySlotId = "auditor" | "gm" | "narrator" | "perspective"; -/** 按需槽(不进入 auditor→perspective→gm→narrator 管线) */ +/** 按需槽(不进入 gm→perspective?→narrator→auditor 管线) */ export type OnDemandSlotId = "chance"; export type PlaySlotsConfig = { @@ -82,10 +82,10 @@ export const ON_DEMAND_SLOT_META: Record< /** 推荐回合顺序(启用的槽按此排序;不含按需槽) */ export const PLAY_SLOT_ORDER: PlaySlotId[] = [ - "auditor", - "perspective", "gm", + "perspective", "narrator", + "auditor", ]; /** 这些槽的默认上下文不注入「对话.历史」长聊 */ @@ -194,21 +194,21 @@ export function workerEntryForSlot( role: id, duty: meta.purpose, when: - id === "auditor" - ? "每轮用户输入后、主世界层之前" + id === "gm" + ? "每轮用户输入后首先上场(主 LLM / 裁决权威)" : id === "perspective" - ? "强信息隔离且本轮需要该角色独立反应时" - : id === "gm" - ? "每轮用户输入后(旁观维护与可选视角之后)" - : "主世界层裁决包就绪后", + ? "强信息隔离且本轮需要该角色独立反应时(主世界层之后)" + : id === "narrator" + ? "主世界层裁决包就绪后" + : "转述完成后;表维护合并进变量.当前,供下一轮主世界层读取", rationale: - id === "auditor" - ? "表与规则补充与主裁决分槽,避免挤占对话历史上下文" + id === "gm" + ? "等同 SillyTavern 主 LLM:读设定与状态并产出裁决" : id === "perspective" ? "知密内容不能进主世界层上下文" - : id === "gm" - ? "裁决与真值变更需要独立推理槽" - : "用户可见正文与裁决分离,避免文风与规则互相挤压", + : id === "narrator" + ? "用户可见正文与裁决分离,避免文风与规则互相挤压" + : "表与规则补充延后执行,改表结果作用于下一轮而非本轮重裁", acceptance: meta.defaultAcceptance, invocation: "turn", }; diff --git a/src/skills/play-turn.ts b/src/skills/play-turn.ts index 4f79666..89b6e01 100644 --- a/src/skills/play-turn.ts +++ b/src/skills/play-turn.ts @@ -1,5 +1,6 @@ /** - * 游玩回合:按运行规格 playWorkerIds 连跑(旁观 → 视角? → 主世界层 → 转述)。 + * 游玩回合:按运行规格 playWorkerIds 连跑(主世界层 → 视角? → 转述 → 旁观维护)。 + * 旁观维护在本轮末执行,表变更供下一轮主世界层读取。 * 与创作总管无关;收口后的输入走这里。 */ export const SLOT_PLAY_TURN_QUEUE = "playTurnQueue"; diff --git a/tests/play-slots.test.ts b/tests/play-slots.test.ts index 0b302cf..9707178 100644 --- a/tests/play-slots.test.ts +++ b/tests/play-slots.test.ts @@ -41,22 +41,22 @@ describe("play_slots", () => { })!; expect(slots.auditor).toBe(true); expect(refsFromPlaySlots(slots)).toEqual([ - "auditor", "world-simulator", "narrator", + "auditor", ]); const workers = expandPlaySlotsToWorkers(slots); expect(workers.map((w) => w.ref)).toEqual([ - "auditor", "world-simulator", "narrator", + "auditor", ]); expect(workers[0].acceptance).toBe("continue"); - expect(workers[1].acceptance).toBe("continue"); - expect(workers[2].acceptance).toBe("review"); + expect(workers[1].acceptance).toBe("review"); + expect(workers[2].acceptance).toBe("continue"); }); - it("orders auditor then perspective before gm", () => { + it("orders gm first then perspective before narrator and auditor", () => { const slots = parsePlaySlots({ auditor: true, gm: true, @@ -64,10 +64,10 @@ describe("play_slots", () => { perspective: true, })!; expect(refsFromPlaySlots(slots)).toEqual([ - "auditor", - "role-decide", "world-simulator", + "role-decide", "narrator", + "auditor", ]); }); @@ -97,9 +97,9 @@ describe("play_slots", () => { expect(parsed?.play_slots?.auditor).toBe(true); expect(parsed?.workers).toHaveLength(3); expect(deriveRunWorkerScope(parsed)).toEqual([ - "auditor", "world-simulator", "narrator", + "auditor", ]); });