默认只开主世界层,无转述时直接把原文当终稿呈现。

避免再经转述改写一轮,并让主世界层在关转述时走可见正文协议。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-09 03:06:38 +08:00
co-authored by Cursor
parent 5574b0d08e
commit b871578d59
10 changed files with 96 additions and 38 deletions
+11 -2
View File
@@ -120,7 +120,7 @@ import {
SLOT_ASKED_QUESTIONS, SLOT_ASKED_QUESTIONS,
} from "../skills/question-protocol.js"; } from "../skills/question-protocol.js";
import { parseWorkerSetYaml } from "../skills/worker-set-parse.js"; import { parseWorkerSetYaml } from "../skills/worker-set-parse.js";
import { defaultPlaySlots } from "../skills/play-slots.js"; import { defaultPlaySlots, isNarratorEnabled } from "../skills/play-slots.js";
import { import {
mergeOpeningTablePatch, mergeOpeningTablePatch,
OPENING_CURRENT_VARS_TAG, OPENING_CURRENT_VARS_TAG,
@@ -1252,7 +1252,16 @@ export class PhaseRuntime {
): import("../worker/executor.js").WorkerRunResult { ): import("../worker/executor.js").WorkerRunResult {
if (!isPlayLayerActive(this.session.slots)) return result; if (!isPlayLayerActive(this.session.slots)) return result;
const outputs = { ...result.outputs }; const outputs = { ...result.outputs };
if (Object.keys(outputs).length === 0 && isPlayPresentWorker(workerId)) { if (
Object.keys(outputs).length === 0 &&
isPlayPresentWorker(workerId, {
narratorEnabled: isNarratorEnabled(
parseWorkerSetYaml(
this.blackboard.getContentByTag("设计.worker集") ?? "",
)?.play_slots,
),
})
) {
const shell = const shell =
parseShellAdaptationFromReplyFormat( parseShellAdaptationFromReplyFormat(
this.blackboard.getContentByTag("设计.正文组成") ?? this.blackboard.getContentByTag("设计.正文组成") ??
+2
View File
@@ -512,6 +512,8 @@ const SYNTH_FALLBACK_CONTEXT: Record<
"设计.变量设计与更新规则", "设计.变量设计与更新规则",
"设计.生成规则", "设计.生成规则",
"设计.叙事指南与故事推进", "设计.叙事指南与故事推进",
"设计.正文组成",
"设计.监控栏",
], ],
dynamic: [ dynamic: [
"变量.当前", "变量.当前",
+4 -1
View File
@@ -32,6 +32,7 @@ import {
} from "./context-order.js"; } from "./context-order.js";
import type { ContextSegmentDef } from "./context-segments.js"; import type { ContextSegmentDef } from "./context-segments.js";
import { PLAY_VISIBLE_BODY_INSTRUCTION } from "./present-packet.js"; import { PLAY_VISIBLE_BODY_INSTRUCTION } from "./present-packet.js";
import { isNarratorEnabled } from "./play-slots.js";
type WorkerTemplateDoc = { type WorkerTemplateDoc = {
id?: string; id?: string;
@@ -220,7 +221,9 @@ export function buildDeclaredWorkerSkill(params: {
`执行 ${id}`; `执行 ${id}`;
const excerpt = params.template?.prompt_excerpt?.trim() || ""; const excerpt = params.template?.prompt_excerpt?.trim() || "";
const visibleBodyRule = const visibleBodyRule =
id === "narrator" || id === "round-present" id === "narrator" ||
id === "round-present" ||
(id === "world-simulator" && !isNarratorEnabled(params.workerSet?.play_slots))
? PLAY_VISIBLE_BODY_INSTRUCTION ? PLAY_VISIBLE_BODY_INSTRUCTION
: ""; : "";
const excerptWithRule = [excerpt, visibleBodyRule].filter(Boolean).join("\n\n"); const excerptWithRule = [excerpt, visibleBodyRule].filter(Boolean).join("\n\n");
+30 -18
View File
@@ -11,13 +11,13 @@ export type OnDemandSlotId = "chance";
export type PlaySlotsConfig = { export type PlaySlotsConfig = {
/** /**
* 旁观维护(副 LLM:表/规则检查,默认空操作);默认 true。 * 旁观维护(副 LLM:表/规则检查,默认空操作);默认 false。
* 每轮可上场,但不等于每轮改表。缺省键视为开启。 * 每轮可上场,但不等于每轮改表。缺省键视为关闭。
*/ */
auditor?: boolean; auditor?: boolean;
/** 主世界层(裁决);默认 true */ /** 主世界层(裁决 / 无转述时兼写可见正文);默认 true */
gm: boolean; gm: boolean;
/** 叙事转述;默认 true */ /** 叙事转述;默认 false(主世界层直接交用户可见原文) */
narrator: boolean; narrator: boolean;
/** 单角色知密视角;默认 false */ /** 单角色知密视角;默认 false */
perspective: boolean; perspective: boolean;
@@ -53,12 +53,13 @@ export const PLAY_SLOT_META: Record<
}, },
gm: { gm: {
label: "主世界层", label: "主世界层",
purpose: "读真值与 Progressive 投影,输出结构化裁决包;可提议变量变更。", purpose:
defaultAcceptance: "continue", "读真值与 Progressive 投影并推进世界;无独立转述时直接写用户可见正文,有转述时出裁决包。",
defaultAcceptance: "review",
}, },
narrator: { narrator: {
label: "叙事转述", label: "叙事转述",
purpose: "只读裁决包 + 文风常驻,输出用户可见正文。", purpose: "只读裁决包 + 文风常驻,输出用户可见正文(可选;默认关)。",
defaultAcceptance: "review", defaultAcceptance: "review",
}, },
perspective: { perspective: {
@@ -102,14 +103,19 @@ export function playSlotIdForRef(ref: string): PlaySlotId | undefined {
export function defaultPlaySlots(): PlaySlotsConfig { export function defaultPlaySlots(): PlaySlotsConfig {
return { return {
auditor: true, auditor: false,
gm: true, gm: true,
narrator: true, narrator: false,
perspective: false, perspective: false,
chance: false, chance: false,
}; };
} }
/** 是否启用独立转述层(未显式 true 视为关) */
export function isNarratorEnabled(slots: PlaySlotsConfig | undefined | null): boolean {
return slots?.narrator === true;
}
export function parsePlaySlots(raw: unknown): PlaySlotsConfig | undefined { export function parsePlaySlots(raw: unknown): PlaySlotsConfig | undefined {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined; if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
const row = raw as Record<string, unknown>; const row = raw as Record<string, unknown>;
@@ -154,10 +160,7 @@ export function refForSlot(
} }
export function enabledPlaySlotIds(slots: PlaySlotsConfig): PlaySlotId[] { export function enabledPlaySlotIds(slots: PlaySlotsConfig): PlaySlotId[] {
return PLAY_SLOT_ORDER.filter((id) => { return PLAY_SLOT_ORDER.filter((id) => Boolean(slots[id]));
if (id === "auditor") return slots.auditor !== false;
return Boolean(slots[id]);
});
} }
export function enabledOnDemandSlotIds(slots: PlaySlotsConfig): OnDemandSlotId[] { export function enabledOnDemandSlotIds(slots: PlaySlotsConfig): OnDemandSlotId[] {
@@ -188,14 +191,19 @@ export function workerEntryForSlot(
): WorkerSetEntry { ): WorkerSetEntry {
const meta = PLAY_SLOT_META[id]; const meta = PLAY_SLOT_META[id];
const ref = refForSlot(slots, id); const ref = refForSlot(slots, id);
const soloGm = id === "gm" && !isNarratorEnabled(slots);
return { return {
ref, ref,
name: meta.label, name: meta.label,
role: id, role: id,
duty: meta.purpose, duty: soloGm
? "读真值与投影,推进世界,并直接写出用户可见正文(无独立转述层,原文即终稿)。"
: meta.purpose,
when: when:
id === "gm" id === "gm"
? "每轮用户输入后首先上场(主 LLM / 裁决权威)" ? soloGm
? "每轮用户输入后上场;直接交用户可见正文"
: "每轮用户输入后首先上场(主 LLM / 裁决权威)"
: id === "perspective" : id === "perspective"
? "强信息隔离且本轮需要该角色独立反应时(主世界层之后)" ? "强信息隔离且本轮需要该角色独立反应时(主世界层之后)"
: id === "narrator" : id === "narrator"
@@ -203,13 +211,17 @@ export function workerEntryForSlot(
: "转述完成后;表维护合并进变量.当前,供下一轮主世界层读取", : "转述完成后;表维护合并进变量.当前,供下一轮主世界层读取",
rationale: rationale:
id === "gm" id === "gm"
? "等同 SillyTavern 主 LLM:读设定与状态并产出裁决" ? soloGm
? "默认少槽:主世界层兼呈现,避免转述再改写一遍"
: "有独立转述时:主世界层出裁决,正文交给转述"
: id === "perspective" : id === "perspective"
? "知密内容不能进主世界层上下文" ? "知密内容不能进主世界层上下文"
: id === "narrator" : id === "narrator"
? "用户可见正文与裁决分离,避免文风与规则互相挤压" ? "用户可见正文与裁决分离,避免文风与规则互相挤压"
: "表与规则补充延后执行,改表结果作用于下一轮而非本轮重裁", : "表与规则补充延后执行,改表结果作用于下一轮而非本轮重裁",
acceptance: meta.defaultAcceptance, acceptance:
id === "gm" && isNarratorEnabled(slots) ? "continue" : meta.defaultAcceptance,
outputs: soloGm ? ["输出.用户展示"] : undefined,
invocation: "turn", invocation: "turn",
}; };
} }
@@ -356,7 +368,7 @@ export function formatPlayAgentRosterForPrompt(): string {
]; ];
for (const id of PLAY_SLOT_ORDER) { for (const id of PLAY_SLOT_ORDER) {
const meta = PLAY_SLOT_META[id]; const meta = PLAY_SLOT_META[id];
const def = id === "perspective" ? "默认关" : "默认开"; const def = id === "gm" ? "默认开" : "默认关";
lines.push( lines.push(
`- \`${id}\` → ref=\`${DEFAULT_PLAY_SLOT_REFS[id]}\` **${meta.label}**(${def}):${meta.purpose}`, `- \`${id}\` → ref=\`${DEFAULT_PLAY_SLOT_REFS[id]}\` **${meta.label}**(${def}):${meta.purpose}`,
); );
+8 -3
View File
@@ -198,10 +198,15 @@ export const PLAY_VISIBLE_BODY_INSTRUCTION = [
"禁止把 tag 名(如「输出.用户展示」)、压缩摘要、过程日志写进正文。", "禁止把 tag 名(如「输出.用户展示」)、压缩摘要、过程日志写进正文。",
].join("\n"); ].join("\n");
/** 游玩期把裁决写成用户可见终稿的执行单元 */ /** 游玩期把用户可见终稿写出的执行单元(有转述用转述;无则主世界层直接交原文) */
export function isPlayPresentWorker(workerId: string): boolean { export function isPlayPresentWorker(
workerId: string,
opts?: { narratorEnabled?: boolean },
): boolean {
const id = workerId.trim(); const id = workerId.trim();
return id === "narrator" || id === "round-present"; if (id === "narrator" || id === "round-present") return true;
if (id === "world-simulator" && opts?.narratorEnabled === false) return true;
return false;
} }
/** 按能力探测走 json_schema / forced_tool 时用的 present.v1 形状(全 required,可 strict) */ /** 按能力探测走 json_schema / forced_tool 时用的 present.v1 形状(全 required,可 strict) */
+5 -5
View File
@@ -236,6 +236,9 @@ const DEFAULT_WORKER_CONTRACTS: Record<
"世界.蓝图.确认稿", "世界.蓝图.确认稿",
"世界.拓扑.*", "世界.拓扑.*",
"设计.变量设计与更新规则", "设计.变量设计与更新规则",
"设计.叙事指南与故事推进",
"设计.正文组成",
"设计.监控栏",
], ],
dynamic: [ dynamic: [
"变量.当前", "变量.当前",
@@ -246,7 +249,7 @@ const DEFAULT_WORKER_CONTRACTS: Record<
"运行.本轮.旁观", "运行.本轮.旁观",
], ],
}, },
outputs: ["运行.本轮.裁决", "运行.事件流", "运行.本轮.变量变更"], outputs: ["输出.用户展示", "运行.本轮.裁决", "运行.事件流", "运行.本轮.变量变更"],
}, },
"variable-update": { "variable-update": {
context: { context: {
@@ -767,10 +770,7 @@ export function formatWorkerSetForUser(
? PLAY_SLOT_ORDER.map((id: PlaySlotId) => ({ ? PLAY_SLOT_ORDER.map((id: PlaySlotId) => ({
id, id,
label: PLAY_SLOT_META[id].label, label: PLAY_SLOT_META[id].label,
enabled: enabled: Boolean(parsed.play_slots![id]),
id === "auditor"
? parsed.play_slots!.auditor !== false
: Boolean(parsed.play_slots![id]),
ref: refForSlot(parsed.play_slots!, id), ref: refForSlot(parsed.play_slots!, id),
})) }))
: undefined, : undefined,
+10 -6
View File
@@ -46,6 +46,7 @@ import {
GM_CHANCE_HARNESS_INSTRUCTION, GM_CHANCE_HARNESS_INSTRUCTION,
} from "../skills/gm-tools.js"; } from "../skills/gm-tools.js";
import { runGmHarness } from "./gm-harness.js"; import { runGmHarness } from "./gm-harness.js";
import { isNarratorEnabled } from "../skills/play-slots.js";
import { import {
mergeQuestionsPreferFragment, mergeQuestionsPreferFragment,
normalizeQuestions, normalizeQuestions,
@@ -772,17 +773,20 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
inputs[CONTEXT_BRIEF_TAG] = priorBrief; inputs[CONTEXT_BRIEF_TAG] = priorBrief;
} }
const playSlots = parseWorkerSetYaml(
params.blackboard.getContentByTag("设计.worker集") ?? "",
)?.play_slots;
const useGmHarness = const useGmHarness =
isPlayLayerActive(params.slots) && isPlayLayerActive(params.slots) &&
worker.id === "world-simulator" && worker.id === "world-simulator" &&
gmChanceToolsEnabled( gmChanceToolsEnabled(playSlots);
parseWorkerSetYaml(
params.blackboard.getContentByTag("设计.worker集") ?? "",
)?.play_slots,
);
const playPresent = const playPresent =
isPlayLayerActive(params.slots) && isPlayPresentWorker(worker.id); isPlayLayerActive(params.slots) &&
isPlayPresentWorker(worker.id, {
narratorEnabled: isNarratorEnabled(playSlots),
});
const playShell: PresentShellId | undefined = playPresent const playShell: PresentShellId | undefined = playPresent
? parseShellAdaptationFromReplyFormat( ? parseShellAdaptationFromReplyFormat(
params.blackboard.getContentByTag("设计.正文组成") ?? params.blackboard.getContentByTag("设计.正文组成") ??
+18 -1
View File
@@ -34,8 +34,25 @@ import { synthesizeContextOrderFromWorkers } from "../src/skills/context-order.j
import { DIALOGUE_HISTORY_TAG } from "../src/skills/dialogue-history.js"; import { DIALOGUE_HISTORY_TAG } from "../src/skills/dialogue-history.js";
describe("play_slots", () => { describe("play_slots", () => {
it("expands default auditor+gm+narrator", () => { it("defaults to gm only", () => {
const slots = parsePlaySlots({})!;
expect(slots).toEqual({
auditor: false,
gm: true,
narrator: false,
perspective: false,
chance: false,
});
expect(refsFromPlaySlots(slots)).toEqual(["world-simulator"]);
const workers = expandPlaySlotsToWorkers(slots);
expect(workers.map((w) => w.ref)).toEqual(["world-simulator"]);
expect(workers[0].acceptance).toBe("review");
expect(workers[0].outputs).toEqual(["输出.用户展示"]);
});
it("expands auditor+gm+narrator when all enabled", () => {
const slots = parsePlaySlots({ const slots = parsePlaySlots({
auditor: true,
gm: true, gm: true,
narrator: true, narrator: true,
perspective: false, perspective: false,
+7 -1
View File
@@ -118,10 +118,16 @@ describe("shell adaptation from reply-format", () => {
}); });
describe("play present worker", () => { describe("play present worker", () => {
it("recognizes narrator and round-present", () => { it("recognizes narrator, round-present, and solo gm", () => {
expect(isPlayPresentWorker("narrator")).toBe(true); expect(isPlayPresentWorker("narrator")).toBe(true);
expect(isPlayPresentWorker("round-present")).toBe(true); expect(isPlayPresentWorker("round-present")).toBe(true);
expect(isPlayPresentWorker("world-simulator")).toBe(false); expect(isPlayPresentWorker("world-simulator")).toBe(false);
expect(
isPlayPresentWorker("world-simulator", { narratorEnabled: false }),
).toBe(true);
expect(
isPlayPresentWorker("world-simulator", { narratorEnabled: true }),
).toBe(false);
}); });
it("fallback packet uses the given shell", () => { it("fallback packet uses the given shell", () => {
+1 -1
View File
@@ -629,7 +629,7 @@ function shouldRenderPlayPresent(msg, view) {
if (isPlayHousekeepingMessage(msg)) return false; if (isPlayHousekeepingMessage(msg)) return false;
const actor = String(msg.actor ?? ""); const actor = String(msg.actor ?? "");
const title = String(msg.title ?? ""); const title = String(msg.title ?? "");
return actor === "narrator" || actor === "round-present" || /用户展示|开场白/.test(title); return actor === "narrator" || actor === "round-present" || actor === "world-simulator" || /用户展示|开场白/.test(title);
} }
function isPlayFinalReply(msg, view) { function isPlayFinalReply(msg, view) {