默认只开主世界层,无转述时直接把原文当终稿呈现。
避免再经转述改写一轮,并让主世界层在关转述时走可见正文协议。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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("设计.正文组成") ??
|
||||||
|
|||||||
@@ -512,6 +512,8 @@ const SYNTH_FALLBACK_CONTEXT: Record<
|
|||||||
"设计.变量设计与更新规则",
|
"设计.变量设计与更新规则",
|
||||||
"设计.生成规则",
|
"设计.生成规则",
|
||||||
"设计.叙事指南与故事推进",
|
"设计.叙事指南与故事推进",
|
||||||
|
"设计.正文组成",
|
||||||
|
"设计.监控栏",
|
||||||
],
|
],
|
||||||
dynamic: [
|
dynamic: [
|
||||||
"变量.当前",
|
"变量.当前",
|
||||||
|
|||||||
@@ -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
@@ -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}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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) */
|
||||||
|
|||||||
@@ -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
@@ -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("设计.正文组成") ??
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user