主世界层直接写正文,转述与旁观改读用户展示输出。
去掉裁决包主路径,让无转述时可直出终稿、有转述时再改写镶壳。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -152,6 +152,7 @@ import {
|
||||
} from "../skills/play-turn.js";
|
||||
import {
|
||||
PRESENT_TAG,
|
||||
isPlayFinalVisibleWorker,
|
||||
isPlayPresentWorker,
|
||||
parseShellAdaptationFromReplyFormat,
|
||||
presentOutputFromFallback,
|
||||
@@ -1253,7 +1254,7 @@ export class PhaseRuntime {
|
||||
}
|
||||
|
||||
/**
|
||||
* 游玩层不走创作 HITL:转述必须交出用户可见正文,其它执行单元空产物也继续。
|
||||
* 游玩层不走创作 HITL:终稿槽必须交出用户可见正文,其它执行单元空产物也继续。
|
||||
*/
|
||||
private settlePlayWorkerResult(
|
||||
workerId: string,
|
||||
@@ -1261,15 +1262,14 @@ export class PhaseRuntime {
|
||||
): import("../worker/executor.js").WorkerRunResult {
|
||||
if (!isPlayLayerActive(this.session.slots)) return result;
|
||||
const outputs = { ...result.outputs };
|
||||
const narratorEnabled = isNarratorEnabled(
|
||||
parseWorkerSetYaml(
|
||||
this.blackboard.getContentByTag("设计.worker集") ?? "",
|
||||
)?.play_slots,
|
||||
);
|
||||
if (
|
||||
Object.keys(outputs).length === 0 &&
|
||||
isPlayPresentWorker(workerId, {
|
||||
narratorEnabled: isNarratorEnabled(
|
||||
parseWorkerSetYaml(
|
||||
this.blackboard.getContentByTag("设计.worker集") ?? "",
|
||||
)?.play_slots,
|
||||
),
|
||||
})
|
||||
isPlayFinalVisibleWorker(workerId, { narratorEnabled })
|
||||
) {
|
||||
const shell =
|
||||
parseShellAdaptationFromReplyFormat(
|
||||
@@ -1279,10 +1279,12 @@ export class PhaseRuntime {
|
||||
const preview = result.preview?.trim() ?? "";
|
||||
const looksAsk =
|
||||
/可验收|追问|请再补|askUser/.test(preview) || /^\s*-\s/.test(preview);
|
||||
outputs[PRESENT_TAG] = presentOutputFromFallback(
|
||||
looksAsk ? "" : preview,
|
||||
shell,
|
||||
);
|
||||
// 主世界终稿用纯正文;转述仍可落 present 包
|
||||
outputs[PRESENT_TAG] = isPlayPresentWorker(workerId)
|
||||
? presentOutputFromFallback(looksAsk ? "" : preview, shell)
|
||||
: looksAsk
|
||||
? "(本轮场面未写完)"
|
||||
: preview || "(本轮场面未写完)";
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
@@ -2578,7 +2580,18 @@ export class PhaseRuntime {
|
||||
inferLifecycleStage(this.session) === "play" &&
|
||||
(tag === "输出.用户展示" || tag === "输出.开场白")
|
||||
) {
|
||||
this.appendDialogueHistory("助手", toWrite);
|
||||
const narratorEnabled = isNarratorEnabled(
|
||||
parseWorkerSetYaml(
|
||||
this.blackboard.getContentByTag("设计.worker集") ?? "",
|
||||
)?.play_slots,
|
||||
);
|
||||
// 有转述时:主世界草稿不进对话史,只记终稿(转述);开场白始终记
|
||||
if (
|
||||
tag === "输出.开场白" ||
|
||||
isPlayFinalVisibleWorker(workerId, { narratorEnabled })
|
||||
) {
|
||||
this.appendDialogueHistory("助手", toWrite);
|
||||
}
|
||||
}
|
||||
if (nextDoc) {
|
||||
this.applyTableSideEffects(prevDoc, nextDoc, workerId);
|
||||
@@ -2586,7 +2599,7 @@ export class PhaseRuntime {
|
||||
if (tag === OPENING_CURRENT_VARS_TAG || tag === "变量.当前") {
|
||||
this.applyValueMapProjections(workerId);
|
||||
}
|
||||
// 裁决包 / 旁观维护包 → 合并进变量.当前(可触发 side_effects)
|
||||
// 遗留裁决包 / 旁观维护包 → 合并进变量.当前(可触发 side_effects)
|
||||
if (tag === SETTLEMENT_TAG) {
|
||||
this.applySettlementVariableChanges(toWrite, workerId);
|
||||
} else if (tag === MAINTAIN_TAG) {
|
||||
|
||||
@@ -498,7 +498,7 @@ const SYNTH_FALLBACK_CONTEXT: Record<
|
||||
"设计.变量控制上下文",
|
||||
],
|
||||
dynamic: [
|
||||
"运行.本轮.裁决",
|
||||
"输出.用户展示",
|
||||
"用户.最新输入",
|
||||
"变量.当前",
|
||||
"上下文.旁观.状态摘要",
|
||||
@@ -528,12 +528,12 @@ const SYNTH_FALLBACK_CONTEXT: Record<
|
||||
"设计.正文组成",
|
||||
"设计.监控栏",
|
||||
],
|
||||
dynamic: ["运行.本轮.裁决"],
|
||||
dynamic: ["输出.用户展示"],
|
||||
skipHistory: true,
|
||||
},
|
||||
"role-decide": {
|
||||
static: [],
|
||||
dynamic: ["用户.最新输入", "运行.本轮.裁决"],
|
||||
dynamic: ["用户.最新输入", "输出.用户展示"],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
} from "./context-order.js";
|
||||
import type { ContextSegmentDef } from "./context-segments.js";
|
||||
import { PLAY_VISIBLE_BODY_INSTRUCTION } from "./present-packet.js";
|
||||
import { isNarratorEnabled } from "./play-slots.js";
|
||||
|
||||
type WorkerTemplateDoc = {
|
||||
id?: string;
|
||||
@@ -223,7 +222,7 @@ export function buildDeclaredWorkerSkill(params: {
|
||||
const visibleBodyRule =
|
||||
id === "narrator" ||
|
||||
id === "round-present" ||
|
||||
(id === "world-simulator" && !isNarratorEnabled(params.workerSet?.play_slots))
|
||||
id === "world-simulator"
|
||||
? PLAY_VISIBLE_BODY_INSTRUCTION
|
||||
: "";
|
||||
const excerptWithRule = [excerpt, visibleBodyRule].filter(Boolean).join("\n\n");
|
||||
|
||||
@@ -114,6 +114,6 @@ export const GM_CHANCE_HARNESS_INSTRUCTION = `
|
||||
|
||||
## 机遇工具(游玩期)
|
||||
需要真随机(掷骰/比点/抽签/加权抽取)时:调用工具 \`chance\`,在 \`requests\` 数组中**一次性**列出本步全部随机需求(每项必须有唯一 \`id\`)。
|
||||
禁止口算或编造随机结果。收到工具结果后,再在 settlement 的 \`resolved\` 等处引用对应 \`id\` 写出后果。
|
||||
无随机需求时:直接输出 settlement.v1 JSON,不要调用工具。
|
||||
禁止口算或编造随机结果。收到工具结果后,在本轮 **Markdown 正文**里演绎对应 \`id\` 的后果(写进场面,不要交 JSON 裁决包)。
|
||||
无随机需求时:直接输出故事正文,不要调用工具。
|
||||
`;
|
||||
|
||||
+18
-15
@@ -54,12 +54,13 @@ export const PLAY_SLOT_META: Record<
|
||||
gm: {
|
||||
label: "主世界层",
|
||||
purpose:
|
||||
"读真值与 Progressive 投影并推进世界;无独立转述时直接写用户可见正文,有转述时出裁决包。",
|
||||
"读真值与 Progressive 投影并推进世界;直接写本轮故事正文(Markdown);无转述时原文即终稿。",
|
||||
defaultAcceptance: "review",
|
||||
},
|
||||
narrator: {
|
||||
label: "叙事转述",
|
||||
purpose: "只读裁决包 + 文风常驻,输出用户可见正文(可选;默认关)。",
|
||||
purpose:
|
||||
"读主世界正文,提取/改写/镶壳为用户终稿;可补残稿,不改剧情(可选;默认关)。",
|
||||
defaultAcceptance: "review",
|
||||
},
|
||||
perspective: {
|
||||
@@ -191,37 +192,39 @@ export function workerEntryForSlot(
|
||||
): WorkerSetEntry {
|
||||
const meta = PLAY_SLOT_META[id];
|
||||
const ref = refForSlot(slots, id);
|
||||
const soloGm = id === "gm" && !isNarratorEnabled(slots);
|
||||
const withNarrator = isNarratorEnabled(slots);
|
||||
const soloGm = id === "gm" && !withNarrator;
|
||||
return {
|
||||
ref,
|
||||
name: meta.label,
|
||||
role: id,
|
||||
duty: soloGm
|
||||
? "读真值与投影,推进世界,并直接写出用户可见正文(无独立转述层,原文即终稿)。"
|
||||
: meta.purpose,
|
||||
when:
|
||||
duty:
|
||||
id === "gm"
|
||||
? soloGm
|
||||
? "每轮用户输入后上场;直接交用户可见正文"
|
||||
: "每轮用户输入后首先上场(主 LLM / 裁决权威)"
|
||||
? "读真值与投影,推进世界,并直接写出用户可见正文(无独立转述层,原文即终稿)。"
|
||||
: "读真值与投影,推进世界,写出本轮故事正文(Markdown);文风润色与壳交给转述。"
|
||||
: meta.purpose,
|
||||
when:
|
||||
id === "gm"
|
||||
? "每轮用户输入后上场;直接交故事正文"
|
||||
: id === "perspective"
|
||||
? "强信息隔离且本轮需要该角色独立反应时(主世界层之后)"
|
||||
: id === "narrator"
|
||||
? "主世界层裁决包就绪后"
|
||||
: "转述完成后;表维护合并进变量.当前,供下一轮主世界层读取",
|
||||
? "主世界层本轮正文就绪后"
|
||||
: "终稿写入后;表维护合并进变量.当前,供下一轮主世界层读取",
|
||||
rationale:
|
||||
id === "gm"
|
||||
? soloGm
|
||||
? "默认少槽:主世界层兼呈现,避免转述再改写一遍"
|
||||
: "有独立转述时:主世界层出裁决,正文交给转述"
|
||||
: "有独立转述时:主世界专注剧情与扮演;转述再提取/改写/镶壳"
|
||||
: id === "perspective"
|
||||
? "知密内容不能进主世界层上下文"
|
||||
: id === "narrator"
|
||||
? "用户可见正文与裁决分离,避免文风与规则互相挤压"
|
||||
? "文风与前端壳与世界推演分离,避免互相挤压"
|
||||
: "表与规则补充延后执行,改表结果作用于下一轮而非本轮重裁",
|
||||
acceptance:
|
||||
id === "gm" && isNarratorEnabled(slots) ? "continue" : meta.defaultAcceptance,
|
||||
outputs: soloGm ? ["输出.用户展示"] : undefined,
|
||||
id === "gm" && withNarrator ? "continue" : meta.defaultAcceptance,
|
||||
outputs: id === "gm" ? ["输出.用户展示"] : undefined,
|
||||
invocation: "turn",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -192,23 +192,40 @@ export function stripPresentSourceFences(raw: string): string {
|
||||
}
|
||||
|
||||
export const PLAY_VISIBLE_BODY_INSTRUCTION = [
|
||||
"用户可见正文(present.v1 的 blocks.body,或纯 Markdown 主读)默认 **1000~2000 字**(按汉字计);以「设计.监控栏/回复呈现」已钉字数为准。",
|
||||
"用户可见正文(主世界层:纯 Markdown;转述:present.v1 的 blocks.body,或纯 Markdown 主读)默认 **1000~2000 字**(按汉字计);以「设计.监控栏/回复呈现」已钉字数为准。",
|
||||
"采用完整长自然段叙述,不要单句成段。",
|
||||
"不要写成几句气泡短信就结束,除非用户明确要求极短。",
|
||||
"禁止把 tag 名(如「输出.用户展示」)、压缩摘要、过程日志写进正文。",
|
||||
].join("\n");
|
||||
|
||||
/** 游玩期把用户可见终稿写出的执行单元(有转述用转述;无则主世界层直接交原文) */
|
||||
export function isPlayPresentWorker(
|
||||
/**
|
||||
* 游玩期走 present.v1 结构化投递的执行单元(转述 / 回合陈述)。
|
||||
* 主世界层不在此列:始终自由写 Markdown 正文。
|
||||
*/
|
||||
export function isPlayPresentWorker(workerId: string): boolean {
|
||||
const id = workerId.trim();
|
||||
return id === "narrator" || id === "round-present";
|
||||
}
|
||||
|
||||
/**
|
||||
* 本轮最终用户可见正文的执行单元(写入对话.历史 / 空产物兜底)。
|
||||
* 无转述:主世界层;有转述:转述(主世界草稿可被覆盖,不进历史)。
|
||||
*/
|
||||
export function isPlayFinalVisibleWorker(
|
||||
workerId: string,
|
||||
opts?: { narratorEnabled?: boolean },
|
||||
): boolean {
|
||||
const id = workerId.trim();
|
||||
if (id === "narrator" || id === "round-present") return true;
|
||||
if (id === "world-simulator" && opts?.narratorEnabled === false) return true;
|
||||
if (id === "world-simulator" && opts?.narratorEnabled !== true) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 游玩主世界层:自由 Markdown 正文(程序可事后落入 present 壳,模型不交 JSON) */
|
||||
export function isPlayGmBodyWorker(workerId: string): boolean {
|
||||
return workerId.trim() === "world-simulator";
|
||||
}
|
||||
|
||||
/** 按能力探测走 json_schema / forced_tool 时用的 present.v1 形状(全 required,可 strict) */
|
||||
export const PRESENT_JSON_SCHEMA: Record<string, unknown> = {
|
||||
type: "object",
|
||||
@@ -243,21 +260,35 @@ export const PRESENT_JSON_SCHEMA: Record<string, unknown> = {
|
||||
},
|
||||
};
|
||||
|
||||
/** 游玩主世界层:直接写 Markdown,禁止 JSON 裁决包 */
|
||||
export function playGmBodyOutputInstruction(): string {
|
||||
return `
|
||||
|
||||
---
|
||||
|
||||
## 运行时输出协议(主世界正文)
|
||||
|
||||
直接输出本轮 **Markdown / 自然语言故事正文**(写入「输出.用户展示」)。
|
||||
不要输出 JSON、不要交 settlement/裁决包、不要套 present.v1 外壳。
|
||||
重心在场面推进与人物扮演;残稿也可以交,有转述时会再处理。
|
||||
不要写追问、askUser、自评或「可验收产物」。`;
|
||||
}
|
||||
|
||||
/** 游玩转述:按探测到的 structured 投递写出 present.v1,不要创作追问 */
|
||||
export function playPresentOutputInstruction(shell: PresentShellId): string {
|
||||
return `
|
||||
|
||||
---
|
||||
|
||||
## 运行时输出协议(游玩正文)
|
||||
## 运行时输出协议(游玩终稿)
|
||||
|
||||
本步产物是给用户看的终稿。程序按已探测的模型能力用 schema / tool / json_object 投递,你输出 **一个 present.v1 对象**:
|
||||
本步读主世界层已写正文,产出给用户看的终稿。程序按已探测的模型能力用 schema / tool / json_object 投递,你输出 **一个 present.v1 对象**:
|
||||
|
||||
{"schema":"present.v1","shell":"${shell}","blocks":{"body":"用户可读正文","monitor":"","header":"","footer":"","aside":""},"meta":{"suggested_actions":[]}}
|
||||
|
||||
\`shell\` 必须是 \`${shell}\`。只填该壳已有区域,没有的键留空字符串。
|
||||
prose 壳也可直接输出 Markdown,程序会落入 body。
|
||||
材料不够也先写一版场面。不要写追问、askUser、自评或「可验收产物」。`;
|
||||
可补全残缺叙述与版式,但不得改剧情事实。不要写追问、askUser、自评或「可验收产物」。`;
|
||||
}
|
||||
|
||||
export function stringifyPresentPacket(packet: PresentPacket): string {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* 主世界层 → 叙事转述的裁决包(settlement / direction packet)。
|
||||
* 存黑板 tag:运行.本轮.裁决
|
||||
* 遗留:settlement.v1 裁决包(运行.本轮.裁决)。
|
||||
* 现行游玩管线:主世界层直接写 Markdown 正文;转述读正文;旁观维护读正文出 maintain.v1。
|
||||
* 仍保留解析/合并,兼容旧存档或手工写入的 variable_changes。
|
||||
*/
|
||||
import {
|
||||
createTableFromValues,
|
||||
|
||||
@@ -221,6 +221,7 @@ const DEFAULT_WORKER_CONTRACTS: Record<
|
||||
"设计.变量控制上下文",
|
||||
],
|
||||
dynamic: [
|
||||
"输出.用户展示",
|
||||
"用户.最新输入",
|
||||
"变量.当前",
|
||||
"上下文.旁观.状态摘要",
|
||||
@@ -249,12 +250,12 @@ const DEFAULT_WORKER_CONTRACTS: Record<
|
||||
"运行.本轮.旁观",
|
||||
],
|
||||
},
|
||||
outputs: ["输出.用户展示", "运行.本轮.裁决", "运行.事件流", "运行.本轮.变量变更"],
|
||||
outputs: ["输出.用户展示", "运行.事件流"],
|
||||
},
|
||||
"variable-update": {
|
||||
context: {
|
||||
static: ["设计.变量设计与更新规则"],
|
||||
dynamic: ["运行.本轮.裁决", "变量.当前"],
|
||||
dynamic: ["输出.用户展示", "变量.当前"],
|
||||
},
|
||||
outputs: ["运行.本轮.变量变更", "变量.当前"],
|
||||
},
|
||||
@@ -267,7 +268,7 @@ const DEFAULT_WORKER_CONTRACTS: Record<
|
||||
"设计.正文组成",
|
||||
"设计.监控栏",
|
||||
],
|
||||
dynamic: ["运行.本轮.裁决", "用户.最新输入", "变量.当前"],
|
||||
dynamic: ["输出.用户展示", "用户.最新输入", "变量.当前"],
|
||||
},
|
||||
outputs: ["输出.用户展示"],
|
||||
},
|
||||
|
||||
+97
-58
@@ -21,17 +21,15 @@ import {
|
||||
} from "../parse/json-doc.js";
|
||||
import { assembleWorkerContext } from "../skills/context-segments.js";
|
||||
import { isPlayLayerActive } from "../skills/play-turn.js";
|
||||
import { loadAppSettings } from "../config/settings.js";
|
||||
import { resolveActivePreset } from "../preset/store.js";
|
||||
import { assemblePlayWorkerMessages } from "../preset/play-frame.js";
|
||||
import { worldInfoPackFromSegments } from "../preset/world-info-pack.js";
|
||||
import {
|
||||
PRESENT_TAG,
|
||||
PRESENT_JSON_SCHEMA,
|
||||
isPlayGmBodyWorker,
|
||||
isPlayPresentWorker,
|
||||
isPresentLikeObject,
|
||||
parsePresentPacket,
|
||||
parseShellAdaptationFromReplyFormat,
|
||||
playGmBodyOutputInstruction,
|
||||
playPresentOutputInstruction,
|
||||
presentOutputFromFallback,
|
||||
stringifyPresentPacket,
|
||||
@@ -46,7 +44,6 @@ import {
|
||||
GM_CHANCE_HARNESS_INSTRUCTION,
|
||||
} from "../skills/gm-tools.js";
|
||||
import { runGmHarness } from "./gm-harness.js";
|
||||
import { isNarratorEnabled } from "../skills/play-slots.js";
|
||||
import {
|
||||
mergeQuestionsPreferFragment,
|
||||
normalizeQuestions,
|
||||
@@ -397,10 +394,15 @@ function lookupOutputValue(
|
||||
function parseWorkerResponse(
|
||||
raw: string,
|
||||
outputTags: string[],
|
||||
opts?: { fallbackShell?: PresentShellId },
|
||||
opts?: { fallbackShell?: PresentShellId; preferPlainBody?: boolean },
|
||||
): WorkerRunResult {
|
||||
const fallbackShell = opts?.fallbackShell ?? "prose";
|
||||
const preferPlainBody = opts?.preferPlainBody === true;
|
||||
const playTarget = playVisibleTarget(outputTags);
|
||||
const packBody = (body: string) =>
|
||||
preferPlainBody
|
||||
? body.trim() || "(本轮场面未写完)"
|
||||
: presentOutputFromFallback(body, fallbackShell);
|
||||
|
||||
const parsed = tryParseJsonDoc(raw);
|
||||
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
@@ -411,10 +413,10 @@ function parseWorkerResponse(
|
||||
? raw
|
||||
: "";
|
||||
return finalizeParsedOutputs(
|
||||
{ [playTarget]: presentOutputFromFallback(body, fallbackShell) },
|
||||
{ [playTarget]: packBody(body) },
|
||||
{ summary: body.slice(0, 80) || "游玩正文" },
|
||||
outputTags,
|
||||
{ playVisible: true, fallbackShell },
|
||||
{ playVisible: true, fallbackShell, preferPlainBody },
|
||||
);
|
||||
}
|
||||
if (target && looksLikeStructuredDraft(raw)) {
|
||||
@@ -449,7 +451,7 @@ function parseWorkerResponse(
|
||||
|
||||
const obj = parsed as Record<string, unknown>;
|
||||
|
||||
if (playTarget && isPresentLikeObject(obj)) {
|
||||
if (playTarget && isPresentLikeObject(obj) && !preferPlainBody) {
|
||||
const present = canonicalizePresentOutput(obj, fallbackShell);
|
||||
if (present) {
|
||||
return finalizeParsedOutputs(
|
||||
@@ -473,15 +475,32 @@ function parseWorkerResponse(
|
||||
}
|
||||
}
|
||||
|
||||
// 游玩正文:片段里的场面字收成 present,不要扁形成创作稿
|
||||
// 游玩正文:片段里的场面字收成 present(或主世界纯正文)
|
||||
if (playTarget && !isPresentLikeObject(obj)) {
|
||||
const scene = fragmentSceneText(obj);
|
||||
if (scene) {
|
||||
return finalizeParsedOutputs(
|
||||
{ [playTarget]: presentOutputFromFallback(scene, fallbackShell) },
|
||||
{ [playTarget]: packBody(scene) },
|
||||
obj,
|
||||
outputTags,
|
||||
{ playVisible: true, fallbackShell },
|
||||
{ playVisible: true, fallbackShell, preferPlainBody },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 主世界偶发 JSON:尽量抽出可读字符串当正文
|
||||
if (playTarget && preferPlainBody) {
|
||||
const scene =
|
||||
fragmentSceneText(obj) ||
|
||||
(typeof obj.body === "string" ? obj.body.trim() : "") ||
|
||||
(typeof obj.visible_now === "string" ? obj.visible_now.trim() : "") ||
|
||||
raw.trim();
|
||||
if (scene) {
|
||||
return finalizeParsedOutputs(
|
||||
{ [playTarget]: packBody(scene) },
|
||||
obj,
|
||||
outputTags,
|
||||
{ playVisible: true, fallbackShell, preferPlainBody },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -510,6 +529,16 @@ function parseWorkerResponse(
|
||||
for (const tag of productOutputTags(outputTags)) {
|
||||
const val = lookupOutputValue(bag, tag);
|
||||
if (playTarget && tag === playTarget) {
|
||||
if (preferPlainBody) {
|
||||
const scene =
|
||||
(typeof val === "string" ? val.trim() : "") ||
|
||||
fragmentSceneText(val) ||
|
||||
"";
|
||||
if (scene) {
|
||||
outputs[tag] = packBody(scene);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const present = canonicalizePresentOutput(val, fallbackShell);
|
||||
if (present) {
|
||||
outputs[tag] = present;
|
||||
@@ -526,7 +555,7 @@ function parseWorkerResponse(
|
||||
}
|
||||
recoverFragmentOutputs(obj, outputs, outputTags, fallbackShell);
|
||||
|
||||
if (playTarget && outputs[playTarget]) {
|
||||
if (playTarget && outputs[playTarget] && !preferPlainBody) {
|
||||
const present = canonicalizePresentOutput(outputs[playTarget], fallbackShell);
|
||||
if (present) outputs[playTarget] = present;
|
||||
}
|
||||
@@ -534,6 +563,7 @@ function parseWorkerResponse(
|
||||
return finalizeParsedOutputs(outputs, obj, outputTags, {
|
||||
playVisible: Boolean(playTarget),
|
||||
fallbackShell,
|
||||
preferPlainBody,
|
||||
rawFallback: raw,
|
||||
});
|
||||
}
|
||||
@@ -545,6 +575,7 @@ function finalizeParsedOutputs(
|
||||
opts?: {
|
||||
playVisible?: boolean;
|
||||
fallbackShell?: PresentShellId;
|
||||
preferPlainBody?: boolean;
|
||||
rawFallback?: string;
|
||||
},
|
||||
): WorkerRunResult {
|
||||
@@ -552,6 +583,11 @@ function finalizeParsedOutputs(
|
||||
const playTarget = playVisibleTarget(outputTags);
|
||||
const playVisible = Boolean(opts?.playVisible || playTarget);
|
||||
const fallbackShell = opts?.fallbackShell ?? "prose";
|
||||
const preferPlainBody = opts?.preferPlainBody === true;
|
||||
const packBody = (body: string) =>
|
||||
preferPlainBody
|
||||
? body.trim() || "(本轮场面未写完)"
|
||||
: presentOutputFromFallback(body, fallbackShell);
|
||||
|
||||
let askUser: QuestionItem[] | undefined;
|
||||
if (!playVisible) {
|
||||
@@ -615,7 +651,7 @@ function finalizeParsedOutputs(
|
||||
scene ||
|
||||
summaryText ||
|
||||
usablePlayFallbackText(opts?.rawFallback);
|
||||
outputs[playTarget] = presentOutputFromFallback(fallback, fallbackShell);
|
||||
outputs[playTarget] = packBody(fallback);
|
||||
}
|
||||
|
||||
if (Object.keys(outputs).length === 0 && (!askUser || askUser.length === 0)) {
|
||||
@@ -782,54 +818,43 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
||||
worker.id === "world-simulator" &&
|
||||
gmChanceToolsEnabled(playSlots);
|
||||
|
||||
const playGmBody =
|
||||
isPlayLayerActive(params.slots) && isPlayGmBodyWorker(worker.id);
|
||||
const playPresent =
|
||||
isPlayLayerActive(params.slots) &&
|
||||
isPlayPresentWorker(worker.id, {
|
||||
narratorEnabled: isNarratorEnabled(playSlots),
|
||||
});
|
||||
const playShell: PresentShellId | undefined = playPresent
|
||||
? parseShellAdaptationFromReplyFormat(
|
||||
params.blackboard.getContentByTag("设计.正文组成") ??
|
||||
params.blackboard.getContentByTag("设计.回复格式"),
|
||||
)?.shell_id ?? "prose"
|
||||
: undefined;
|
||||
isPlayLayerActive(params.slots) && isPlayPresentWorker(worker.id);
|
||||
const playShell: PresentShellId | undefined =
|
||||
playPresent || playGmBody
|
||||
? parseShellAdaptationFromReplyFormat(
|
||||
params.blackboard.getContentByTag("设计.正文组成") ??
|
||||
params.blackboard.getContentByTag("设计.回复格式"),
|
||||
)?.shell_id ?? "prose"
|
||||
: undefined;
|
||||
|
||||
const systemContent =
|
||||
promptBody +
|
||||
(playPresent && playShell
|
||||
? playPresentOutputInstruction(playShell)
|
||||
: WORKER_OUTPUT_INSTRUCTION) +
|
||||
(playGmBody
|
||||
? playGmBodyOutputInstruction()
|
||||
: playPresent && playShell
|
||||
? playPresentOutputInstruction(playShell)
|
||||
: WORKER_OUTPUT_INSTRUCTION) +
|
||||
(useGmHarness ? GM_CHANCE_HARNESS_INSTRUCTION : "");
|
||||
const playPreset = isPlayLayerActive(params.slots)
|
||||
? resolveActivePreset(loadAppSettings().activePresetId)
|
||||
: null;
|
||||
|
||||
const messages = playPreset
|
||||
? assemblePlayWorkerMessages({
|
||||
systemPrompt: systemContent,
|
||||
preset: playPreset,
|
||||
pack: worldInfoPackFromSegments({
|
||||
segments: worker.contextSegments ?? [],
|
||||
inputs,
|
||||
blackboard: params.blackboard,
|
||||
inputMerge,
|
||||
}),
|
||||
})
|
||||
: [
|
||||
{ role: "system" as const, content: systemContent },
|
||||
{
|
||||
role: "user" as const,
|
||||
content: assembleWorkerContext({
|
||||
inputs,
|
||||
segments: worker.contextSegments,
|
||||
blackboard: params.blackboard,
|
||||
inputMerge,
|
||||
workerId: worker.id,
|
||||
workerName: worker.name,
|
||||
outputTags: worker.outputTags,
|
||||
}),
|
||||
},
|
||||
];
|
||||
// 任务 messages 即可;preset 夹心由 wrapLlmForSession / PresetLlmProvider 统一装配
|
||||
const messages: ChatMessage[] = [
|
||||
{ role: "system", content: systemContent },
|
||||
{
|
||||
role: "user",
|
||||
content: assembleWorkerContext({
|
||||
inputs,
|
||||
segments: worker.contextSegments,
|
||||
blackboard: params.blackboard,
|
||||
inputMerge,
|
||||
workerId: worker.id,
|
||||
workerName: worker.name,
|
||||
outputTags: worker.outputTags,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
if (useGmHarness) {
|
||||
const systemMsg = messages.find((m) => m.role === "system");
|
||||
@@ -841,8 +866,10 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
||||
stream: params.stream,
|
||||
caller: `worker:${worker.id}`,
|
||||
});
|
||||
return parseWorkerResponse(selectJsonPayload(harness.content), worker.outputTags, {
|
||||
// 主世界 harness 终稿是 Markdown;勿强行抽 JSON
|
||||
return parseWorkerResponse(harness.content, worker.outputTags, {
|
||||
fallbackShell: playShell,
|
||||
preferPlainBody: playGmBody,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -853,6 +880,18 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
||||
}
|
||||
: undefined;
|
||||
|
||||
if (playGmBody) {
|
||||
const completeOpts = { caller: `worker:${worker.id}`, responseFormat: "text" as const };
|
||||
const result =
|
||||
streamCallbacks && params.llm.completeStream
|
||||
? await params.llm.completeStream(messages, completeOpts, streamCallbacks)
|
||||
: await params.llm.complete(messages, completeOpts);
|
||||
return parseWorkerResponse(result.content, worker.outputTags, {
|
||||
fallbackShell: playShell,
|
||||
preferPlainBody: true,
|
||||
});
|
||||
}
|
||||
|
||||
const result = await completeStructured(params.llm, messages, {
|
||||
schema: playPresent ? PRESENT_JSON_SCHEMA : {
|
||||
type: "object",
|
||||
@@ -880,7 +919,7 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
||||
export function parseWorkerResponseForTest(
|
||||
raw: string,
|
||||
outputTags: string[],
|
||||
opts?: { fallbackShell?: PresentShellId },
|
||||
opts?: { fallbackShell?: PresentShellId; preferPlainBody?: boolean },
|
||||
): WorkerRunResult {
|
||||
return parseWorkerResponse(raw, outputTags, opts);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 主世界层(world-simulator)游玩期 tool harness:
|
||||
* 反复 completeWithTools 直到不再调用工具,最后一次正文视为裁决 JSON。
|
||||
* 反复 completeWithTools 直到不再调用工具,最后一次正文视为故事 Markdown。
|
||||
*/
|
||||
import type { ChatMessage, LlmProvider } from "../llm/client.js";
|
||||
import {
|
||||
|
||||
Reference in New Issue
Block a user