Reorder play pipeline: gm first, auditor last.

Main world layer runs before narrator and deferred maintenance. Round-scoped settlement/maintain tags are archived at turn start so prior auditor table merges feed the next gm read.

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