Play rounds end in present.v1 body instead of creation HITL.
Narrator uses capability-probed structured delivery and falls back to shell body on failure; play UI hides worker questions and keeps thinking on process rows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -130,7 +130,14 @@ import {
|
||||
import {
|
||||
readPlayTurnQueue,
|
||||
withPlayTurnQueue,
|
||||
isPlayLayerActive,
|
||||
} from "../skills/play-turn.js";
|
||||
import {
|
||||
PRESENT_TAG,
|
||||
isPlayPresentWorker,
|
||||
parseShellAdaptationFromReplyFormat,
|
||||
presentOutputFromFallback,
|
||||
} from "../skills/present-packet.js";
|
||||
import { debugLog, labelAction, logStateChange, sessionSnap } from "../log.js";
|
||||
import {
|
||||
executeChance,
|
||||
@@ -1001,6 +1008,37 @@ export class PhaseRuntime {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 游玩层不走创作 HITL:转述必须交出用户可见正文,其它执行单元空产物也继续。
|
||||
*/
|
||||
private settlePlayWorkerResult(
|
||||
workerId: string,
|
||||
result: import("../worker/executor.js").WorkerRunResult,
|
||||
): import("../worker/executor.js").WorkerRunResult {
|
||||
if (!isPlayLayerActive(this.session.slots)) return result;
|
||||
const outputs = { ...result.outputs };
|
||||
if (Object.keys(outputs).length === 0 && isPlayPresentWorker(workerId)) {
|
||||
const shell =
|
||||
parseShellAdaptationFromReplyFormat(
|
||||
this.blackboard.getContentByTag("设计.正文组成") ??
|
||||
this.blackboard.getContentByTag("设计.回复格式"),
|
||||
)?.shell_id ?? "prose";
|
||||
const preview = result.preview?.trim() ?? "";
|
||||
const looksAsk =
|
||||
/可验收|追问|请再补|askUser/.test(preview) || /^\s*-\s/.test(preview);
|
||||
outputs[PRESENT_TAG] = presentOutputFromFallback(
|
||||
looksAsk ? "" : preview,
|
||||
shell,
|
||||
);
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
outputs,
|
||||
askUser: undefined,
|
||||
askAssessment: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** worker 调用 ask_user 能力时,由外层触发此事件(仅无产物时阻塞) */
|
||||
async workerAsk(
|
||||
questions: import("../types/questions.js").QuestionItem[] | string[],
|
||||
@@ -1802,20 +1840,22 @@ export class PhaseRuntime {
|
||||
},
|
||||
});
|
||||
|
||||
if (result.askUser?.length && Object.keys(result.outputs).length === 0) {
|
||||
const settled = this.settlePlayWorkerResult(workerId, result);
|
||||
|
||||
if (settled.askUser?.length && Object.keys(settled.outputs).length === 0) {
|
||||
// 无产物:阻塞追问(信息不足,必须补)
|
||||
if (await this.restoreRevisionTargetForReview(workerId, result.askUser)) {
|
||||
if (await this.restoreRevisionTargetForReview(workerId, settled.askUser)) {
|
||||
return;
|
||||
}
|
||||
await this.workerAsk(result.askUser);
|
||||
await this.workerAsk(settled.askUser);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
workerId === "design-step" &&
|
||||
Object.keys(result.outputs).length === 0
|
||||
Object.keys(settled.outputs).length === 0
|
||||
) {
|
||||
const retryQuestions = result.askUser?.length
|
||||
? result.askUser
|
||||
const retryQuestions = settled.askUser?.length
|
||||
? settled.askUser
|
||||
: [
|
||||
{
|
||||
id: "design-step-retry",
|
||||
@@ -1834,7 +1874,7 @@ export class PhaseRuntime {
|
||||
|
||||
slots = { ...this.session.slots };
|
||||
const writtenTags: string[] = [];
|
||||
for (const [tag, content] of Object.entries(result.outputs)) {
|
||||
for (const [tag, content] of Object.entries(settled.outputs)) {
|
||||
if (isProgressPointerTag(tag)) continue;
|
||||
const written = this.writeWorkerTagContent(tag, content, workerId);
|
||||
slots[tag] = written;
|
||||
@@ -1848,16 +1888,16 @@ export class PhaseRuntime {
|
||||
outputTags: (writtenTags.length > 0 ? writtenTags : worker.outputTags).filter(
|
||||
(t) => !isProgressPointerTag(t),
|
||||
),
|
||||
summary: result.summary,
|
||||
summary: settled.summary,
|
||||
});
|
||||
this.session = { ...this.session, artifacts: [...this.session.artifacts, artifact] };
|
||||
|
||||
const hasPlayVisible = Object.keys(result.outputs).some(
|
||||
const hasPlayVisible = Object.keys(settled.outputs).some(
|
||||
(t) => t === "输出.用户展示" || t === "输出.开场白",
|
||||
);
|
||||
this.onMessage(
|
||||
`[Worker] ${workerId} 已完成\n\n${result.preview}${
|
||||
!hasPlayVisible && result.preview.length >= 4000 ? "\n\n…" : ""
|
||||
`[Worker] ${workerId} 已完成\n\n${settled.preview}${
|
||||
!hasPlayVisible && settled.preview.length >= 4000 ? "\n\n…" : ""
|
||||
}`,
|
||||
);
|
||||
|
||||
@@ -1866,8 +1906,8 @@ export class PhaseRuntime {
|
||||
payload: {
|
||||
artifactId: artifact.id,
|
||||
// 有产物时 askUser 挂到验收态,不阻断 Accept
|
||||
questions: this.freshSidecarQuestions(result.askUser),
|
||||
assessment: result.askAssessment?.trim() || undefined,
|
||||
questions: this.freshSidecarQuestions(settled.askUser),
|
||||
assessment: settled.askAssessment?.trim() || undefined,
|
||||
},
|
||||
});
|
||||
this.maybeCompressAcceptedArtifact(artifact.id);
|
||||
|
||||
@@ -851,7 +851,8 @@ export class SessionManager {
|
||||
kind !== "approve_step" &&
|
||||
kind !== "revision" &&
|
||||
kind !== "next_intent" &&
|
||||
kind !== "pick_creation_step"
|
||||
kind !== "pick_creation_step" &&
|
||||
kind !== "worker_questions"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -1875,12 +1876,23 @@ export class SessionManager {
|
||||
this.captureCheckpoint(managed),
|
||||
);
|
||||
const usage = managed.trackingRef.current.pendingUsage;
|
||||
const thinking = managed.trackingRef.current.pendingReasoning;
|
||||
const sessionId = managed.trackingRef.current.sessionId;
|
||||
const liveThinking = sessionId
|
||||
? this.workerLive.get(sessionId)?.thinking?.trim()
|
||||
: "";
|
||||
managed.trackingRef.current.pendingUsage = undefined;
|
||||
const pendingReasoning = managed.trackingRef.current.pendingReasoning;
|
||||
managed.trackingRef.current.pendingReasoning = undefined;
|
||||
|
||||
const classified =
|
||||
text.trim().length > 0 ? classifyAgentMessage(text) : null;
|
||||
const thinking =
|
||||
pendingReasoning ||
|
||||
(classified?.kind === "worker_output" ||
|
||||
classified?.kind === "worker_questions"
|
||||
? liveThinking
|
||||
: "") ||
|
||||
undefined;
|
||||
const pendingTrace = managed.trackingRef.current.pendingContextTrace;
|
||||
let contextTrace: LlmContextTrace | undefined;
|
||||
if (
|
||||
|
||||
@@ -197,6 +197,75 @@ export const PLAY_VISIBLE_BODY_INSTRUCTION = [
|
||||
"禁止把 tag 名(如「输出.用户展示」)、压缩摘要、过程日志写进正文。",
|
||||
].join("\n");
|
||||
|
||||
/** 游玩期把裁决写成用户可见终稿的执行单元 */
|
||||
export function isPlayPresentWorker(workerId: string): boolean {
|
||||
const id = workerId.trim();
|
||||
return id === "narrator" || id === "round-present";
|
||||
}
|
||||
|
||||
/** 按能力探测走 json_schema / forced_tool 时用的 present.v1 形状(全 required,可 strict) */
|
||||
export const PRESENT_JSON_SCHEMA: Record<string, unknown> = {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: ["schema", "shell", "blocks", "meta"],
|
||||
properties: {
|
||||
schema: { type: "string" },
|
||||
shell: { type: "string" },
|
||||
blocks: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: ["body", "monitor", "header", "footer", "aside"],
|
||||
properties: {
|
||||
body: { type: "string" },
|
||||
monitor: { type: "string" },
|
||||
header: { type: "string" },
|
||||
footer: { type: "string" },
|
||||
aside: { type: "string" },
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: ["suggested_actions"],
|
||||
properties: {
|
||||
suggested_actions: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/** 游玩转述:按探测到的 structured 投递写出 present.v1,不要创作追问 */
|
||||
export function playPresentOutputInstruction(shell: PresentShellId): string {
|
||||
return `
|
||||
|
||||
---
|
||||
|
||||
## 运行时输出协议(游玩正文)
|
||||
|
||||
本步产物是给用户看的终稿。程序按已探测的模型能力用 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、自评或「可验收产物」。`;
|
||||
}
|
||||
|
||||
export function stringifyPresentPacket(packet: PresentPacket): string {
|
||||
return JSON.stringify(packet);
|
||||
}
|
||||
|
||||
export function presentOutputFromFallback(
|
||||
text: string,
|
||||
shell: PresentShellId = "prose",
|
||||
): string {
|
||||
const body = stripPresentSourceFences(text).trim() || "(本轮场面未写完)";
|
||||
return stringifyPresentPacket(presentFromPlainText(body, shell));
|
||||
}
|
||||
|
||||
export function parsePresentPacket(
|
||||
raw: string | undefined | null,
|
||||
fallbackShell: PresentShellId = "prose",
|
||||
|
||||
+229
-51
@@ -25,7 +25,18 @@ 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 } from "../skills/present-packet.js";
|
||||
import {
|
||||
PRESENT_TAG,
|
||||
PRESENT_JSON_SCHEMA,
|
||||
isPlayPresentWorker,
|
||||
isPresentLikeObject,
|
||||
parsePresentPacket,
|
||||
parseShellAdaptationFromReplyFormat,
|
||||
playPresentOutputInstruction,
|
||||
presentOutputFromFallback,
|
||||
stringifyPresentPacket,
|
||||
type PresentShellId,
|
||||
} from "../skills/present-packet.js";
|
||||
import {
|
||||
coerceToFlatArtifact,
|
||||
flatArtifactToFragmentDoc,
|
||||
@@ -44,6 +55,7 @@ import {
|
||||
extractFragmentAskSidecar,
|
||||
isUsableContextFragment,
|
||||
looksLikeFragmentDoc,
|
||||
parseContextFragment,
|
||||
} from "../skills/context-fragment.js";
|
||||
import {
|
||||
isProgressPointerTag,
|
||||
@@ -200,13 +212,66 @@ function existingOutputIsProduct(text: string | undefined): boolean {
|
||||
if (!text?.trim()) return false;
|
||||
if (isUsableContextFragment(text)) return true;
|
||||
const parsed = tryParseJsonDoc(text);
|
||||
return looksLikeCreationFlowDoc(parsed) || looksLikeFragmentDoc(parsed);
|
||||
return (
|
||||
looksLikeCreationFlowDoc(parsed) ||
|
||||
looksLikeFragmentDoc(parsed) ||
|
||||
isPresentLikeObject(parsed)
|
||||
);
|
||||
}
|
||||
|
||||
function usablePlayFallbackText(raw?: string): string {
|
||||
if (!raw?.trim()) return "";
|
||||
if (isQuestionOnlyText(raw)) return "";
|
||||
const parsed = tryParseJsonDoc(raw);
|
||||
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
||||
return "";
|
||||
}
|
||||
return raw.trim();
|
||||
}
|
||||
|
||||
function playVisibleTarget(outputTags: string[]): string | undefined {
|
||||
return productOutputTags(outputTags).find((t) => PLAY_FEED_OUTPUT_TAGS.has(t));
|
||||
}
|
||||
|
||||
function canonicalizePresentOutput(
|
||||
value: unknown,
|
||||
fallbackShell: PresentShellId,
|
||||
): string | undefined {
|
||||
if (value == null) return undefined;
|
||||
if (typeof value === "string") {
|
||||
const view = parsePresentPacket(value, fallbackShell);
|
||||
if (!view.packet.blocks.body && view.fallbackPlain && !value.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
return stringifyPresentPacket(view.packet);
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
if (isPresentLikeObject(value)) {
|
||||
const view = parsePresentPacket(JSON.stringify(value), fallbackShell);
|
||||
return stringifyPresentPacket(view.packet);
|
||||
}
|
||||
const view = parsePresentPacket(JSON.stringify(value), fallbackShell);
|
||||
if (!view.fallbackPlain) return stringifyPresentPacket(view.packet);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function fragmentSceneText(doc: unknown): string | undefined {
|
||||
const frag =
|
||||
typeof doc === "string"
|
||||
? parseContextFragment(doc)
|
||||
: parseContextFragment(JSON.stringify(doc));
|
||||
if (!frag) return undefined;
|
||||
if (typeof frag.正文 === "string" && frag.正文.trim()) return frag.正文.trim();
|
||||
if (typeof frag.brief === "string" && frag.brief.trim()) return frag.brief.trim();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function recoverFragmentOutputs(
|
||||
obj: Record<string, unknown>,
|
||||
outputs: Record<string, string>,
|
||||
outputTags: string[],
|
||||
fallbackShell: PresentShellId = "prose",
|
||||
): void {
|
||||
const target = productOutputTags(outputTags)[0];
|
||||
if (!target) return;
|
||||
@@ -225,7 +290,8 @@ function recoverFragmentOutputs(
|
||||
if (
|
||||
isUsableContextFragment(text) ||
|
||||
looksLikeFragmentDoc(val) ||
|
||||
looksLikeCreationFlowDoc(val)
|
||||
looksLikeCreationFlowDoc(val) ||
|
||||
isPresentLikeObject(val)
|
||||
) {
|
||||
outputs[target] = text;
|
||||
return;
|
||||
@@ -238,6 +304,14 @@ function recoverFragmentOutputs(
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
if (!protocolKeys.has(k)) rest[k] = v;
|
||||
}
|
||||
if (isPresentLikeObject(obj) || isPresentLikeObject(rest)) {
|
||||
const source = isPresentLikeObject(obj) ? obj : rest;
|
||||
const present = canonicalizePresentOutput(source, fallbackShell);
|
||||
if (present) {
|
||||
outputs[target] = present;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (looksLikeFragmentDoc(obj) || looksLikeFragmentDoc(rest)) {
|
||||
const source = looksLikeFragmentDoc(obj) ? obj : rest;
|
||||
outputs[target] = JSON.stringify(source);
|
||||
@@ -267,6 +341,7 @@ function looksLikeStructuredDraft(text: string): boolean {
|
||||
if (t.startsWith("{") || t.startsWith("[")) return true;
|
||||
if (/```(?:json)?/i.test(t) && t.includes("{")) return true;
|
||||
if (/"schema"\s*:/.test(t)) return true;
|
||||
if (/"blocks"\s*:/.test(t) && /"body"\s*:/.test(t)) return true;
|
||||
if (/"技能"\s*:/.test(t) || /"正文"\s*:/.test(t)) return true;
|
||||
if (/"inserts"\s*:/.test(t) || /"agents"\s*:/.test(t) || /"play_slots"\s*:/.test(t)) {
|
||||
return true;
|
||||
@@ -321,10 +396,26 @@ function lookupOutputValue(
|
||||
function parseWorkerResponse(
|
||||
raw: string,
|
||||
outputTags: string[],
|
||||
opts?: { fallbackShell?: PresentShellId },
|
||||
): WorkerRunResult {
|
||||
const fallbackShell = opts?.fallbackShell ?? "prose";
|
||||
const playTarget = playVisibleTarget(outputTags);
|
||||
|
||||
const parsed = tryParseJsonDoc(raw);
|
||||
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
const target = productOutputTags(outputTags)[0] ?? outputTags[0];
|
||||
const target = playTarget ?? productOutputTags(outputTags)[0] ?? outputTags[0];
|
||||
if (playTarget) {
|
||||
const body =
|
||||
raw.trim() && !isQuestionOnlyText(raw)
|
||||
? raw
|
||||
: "";
|
||||
return finalizeParsedOutputs(
|
||||
{ [playTarget]: presentOutputFromFallback(body, fallbackShell) },
|
||||
{ summary: body.slice(0, 80) || "游玩正文" },
|
||||
outputTags,
|
||||
{ playVisible: true, fallbackShell },
|
||||
);
|
||||
}
|
||||
if (target && looksLikeStructuredDraft(raw)) {
|
||||
return finalizeParsedOutputs(
|
||||
{ [target]: raw },
|
||||
@@ -357,6 +448,18 @@ function parseWorkerResponse(
|
||||
|
||||
const obj = parsed as Record<string, unknown>;
|
||||
|
||||
if (playTarget && isPresentLikeObject(obj)) {
|
||||
const present = canonicalizePresentOutput(obj, fallbackShell);
|
||||
if (present) {
|
||||
return finalizeParsedOutputs(
|
||||
{ [playTarget]: present },
|
||||
obj,
|
||||
outputTags,
|
||||
{ playVisible: true, fallbackShell },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 创作流程 DAG:根对象就是产物,不能先收成扁形(brief 会被当成稿、steps 丢掉)
|
||||
if (looksLikeCreationFlowDoc(obj)) {
|
||||
const target = productOutputTags(outputTags)[0] ?? outputTags[0];
|
||||
@@ -369,8 +472,21 @@ function parseWorkerResponse(
|
||||
}
|
||||
}
|
||||
|
||||
// 游玩正文:片段里的场面字收成 present,不要扁形成创作稿
|
||||
if (playTarget && !isPresentLikeObject(obj)) {
|
||||
const scene = fragmentSceneText(obj);
|
||||
if (scene) {
|
||||
return finalizeParsedOutputs(
|
||||
{ [playTarget]: presentOutputFromFallback(scene, fallbackShell) },
|
||||
obj,
|
||||
outputTags,
|
||||
{ playVisible: true, fallbackShell },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 扁形产物 → 旧 fragment 外壳。已是 context-fragment 深树则原样收下(保留自评与嵌套正文)。
|
||||
if (!looksLikeFragmentDoc(obj)) {
|
||||
if (!looksLikeFragmentDoc(obj) && !isPresentLikeObject(obj) && !playTarget) {
|
||||
const asFlat = coerceToFlatArtifact(obj);
|
||||
if (asFlat) {
|
||||
const frag = flatArtifactToFragmentDoc(asFlat);
|
||||
@@ -391,69 +507,116 @@ function parseWorkerResponse(
|
||||
if (outputsRaw && typeof outputsRaw === "object" && !Array.isArray(outputsRaw)) {
|
||||
const bag = outputsRaw as Record<string, unknown>;
|
||||
for (const tag of productOutputTags(outputTags)) {
|
||||
assignOutput(outputs, tag, lookupOutputValue(bag, tag));
|
||||
const val = lookupOutputValue(bag, tag);
|
||||
if (playTarget && tag === playTarget) {
|
||||
const present = canonicalizePresentOutput(val, fallbackShell);
|
||||
if (present) {
|
||||
outputs[tag] = present;
|
||||
continue;
|
||||
}
|
||||
const scene = fragmentSceneText(val);
|
||||
if (scene) {
|
||||
outputs[tag] = presentOutputFromFallback(scene, fallbackShell);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
assignOutput(outputs, tag, val);
|
||||
}
|
||||
}
|
||||
recoverFragmentOutputs(obj, outputs, outputTags);
|
||||
recoverFragmentOutputs(obj, outputs, outputTags, fallbackShell);
|
||||
|
||||
return finalizeParsedOutputs(outputs, obj, outputTags);
|
||||
if (playTarget && outputs[playTarget]) {
|
||||
const present = canonicalizePresentOutput(outputs[playTarget], fallbackShell);
|
||||
if (present) outputs[playTarget] = present;
|
||||
}
|
||||
|
||||
return finalizeParsedOutputs(outputs, obj, outputTags, {
|
||||
playVisible: Boolean(playTarget),
|
||||
fallbackShell,
|
||||
rawFallback: raw,
|
||||
});
|
||||
}
|
||||
|
||||
function finalizeParsedOutputs(
|
||||
outputs: Record<string, string>,
|
||||
obj: Record<string, unknown>,
|
||||
_outputTags: string[],
|
||||
outputTags: string[],
|
||||
opts?: {
|
||||
playVisible?: boolean;
|
||||
fallbackShell?: PresentShellId;
|
||||
rawFallback?: string;
|
||||
},
|
||||
): WorkerRunResult {
|
||||
const isMetaAskToken = (s: string) => /^ask[_-]?user$/i.test(s.trim());
|
||||
const playTarget = playVisibleTarget(outputTags);
|
||||
const playVisible = Boolean(opts?.playVisible || playTarget);
|
||||
const fallbackShell = opts?.fallbackShell ?? "prose";
|
||||
|
||||
let askUser: QuestionItem[] | undefined;
|
||||
if (Array.isArray(obj.askUser)) {
|
||||
askUser = normalizeQuestions(
|
||||
obj.askUser.filter((q) => {
|
||||
if (typeof q === "string") return q.trim() && !isMetaAskToken(q);
|
||||
return true;
|
||||
}),
|
||||
);
|
||||
} else if (typeof obj.askUser === "string" && obj.askUser.trim()) {
|
||||
const q = obj.askUser.trim();
|
||||
askUser = isMetaAskToken(q) ? undefined : normalizeQuestions([q]);
|
||||
}
|
||||
if ((!askUser || askUser.length === 0) && Object.keys(outputs).length === 0) {
|
||||
const summaryText =
|
||||
typeof obj.summary === "string" ? obj.summary.trim() : "";
|
||||
if (
|
||||
summaryText &&
|
||||
!isMetaAskToken(summaryText) &&
|
||||
summaryText.length > 8 &&
|
||||
/[??]/.test(summaryText)
|
||||
) {
|
||||
askUser = normalizeQuestions([summaryText]);
|
||||
} else if (isMetaAskToken(summaryText)) {
|
||||
askUser = normalizeQuestions([
|
||||
"请补充当前步骤所需的信息(情境、参数或你的具体设想)。",
|
||||
]);
|
||||
if (!playVisible) {
|
||||
if (Array.isArray(obj.askUser)) {
|
||||
askUser = normalizeQuestions(
|
||||
obj.askUser.filter((q) => {
|
||||
if (typeof q === "string") return q.trim() && !isMetaAskToken(q);
|
||||
return true;
|
||||
}),
|
||||
);
|
||||
} else if (typeof obj.askUser === "string" && obj.askUser.trim()) {
|
||||
const q = obj.askUser.trim();
|
||||
askUser = isMetaAskToken(q) ? undefined : normalizeQuestions([q]);
|
||||
}
|
||||
if ((!askUser || askUser.length === 0) && Object.keys(outputs).length === 0) {
|
||||
const summaryText =
|
||||
typeof obj.summary === "string" ? obj.summary.trim() : "";
|
||||
if (
|
||||
summaryText &&
|
||||
!isMetaAskToken(summaryText) &&
|
||||
summaryText.length > 8 &&
|
||||
/[??]/.test(summaryText)
|
||||
) {
|
||||
askUser = normalizeQuestions([summaryText]);
|
||||
} else if (isMetaAskToken(summaryText)) {
|
||||
askUser = normalizeQuestions([
|
||||
"请补充当前步骤所需的信息(情境、参数或你的具体设想)。",
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 先抽追问;半残稿仍留在 outputs,追问挂验收卡(不因缺字段丢掉正文)
|
||||
let askAssessment: string | undefined;
|
||||
const fragQuestions: QuestionItem[] = [];
|
||||
for (const content of Object.values(outputs)) {
|
||||
const side = extractFragmentAskSidecar(content);
|
||||
if (side.assessment && !askAssessment) askAssessment = side.assessment;
|
||||
for (const q of side.questions) {
|
||||
if (!fragQuestions.some((x) => x.prompt === q.prompt)) {
|
||||
fragQuestions.push(q);
|
||||
if (!playVisible) {
|
||||
for (const content of Object.values(outputs)) {
|
||||
const side = extractFragmentAskSidecar(content);
|
||||
if (side.assessment && !askAssessment) askAssessment = side.assessment;
|
||||
for (const q of side.questions) {
|
||||
if (!fragQuestions.some((x) => x.prompt === q.prompt)) {
|
||||
fragQuestions.push(q);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fragQuestions.length) {
|
||||
askUser = mergeQuestionsPreferFragment(askUser, fragQuestions);
|
||||
if (fragQuestions.length) {
|
||||
askUser = mergeQuestionsPreferFragment(askUser, fragQuestions);
|
||||
}
|
||||
}
|
||||
|
||||
dropProgressPointerOutputs(outputs);
|
||||
canonicalizeKnownOutputs(outputs);
|
||||
|
||||
if (playVisible && playTarget && !outputs[playTarget]?.trim()) {
|
||||
const scene = fragmentSceneText(obj);
|
||||
const summaryText =
|
||||
typeof obj.summary === "string" && !/[??]/.test(obj.summary)
|
||||
? obj.summary.trim()
|
||||
: "";
|
||||
const fallback =
|
||||
scene ||
|
||||
summaryText ||
|
||||
usablePlayFallbackText(opts?.rawFallback);
|
||||
outputs[playTarget] = presentOutputFromFallback(fallback, fallbackShell);
|
||||
}
|
||||
|
||||
if (Object.keys(outputs).length === 0 && (!askUser || askUser.length === 0)) {
|
||||
askUser = normalizeQuestions(INCOMPLETE_FRAGMENT_ASK);
|
||||
}
|
||||
@@ -470,8 +633,8 @@ function finalizeParsedOutputs(
|
||||
outputs,
|
||||
summary,
|
||||
preview,
|
||||
askUser: askUser?.length ? askUser : undefined,
|
||||
askAssessment,
|
||||
askUser: playVisible ? undefined : askUser?.length ? askUser : undefined,
|
||||
askAssessment: playVisible ? undefined : askAssessment,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -618,9 +781,20 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
||||
)?.play_slots,
|
||||
);
|
||||
|
||||
const playPresent =
|
||||
isPlayLayerActive(params.slots) && isPlayPresentWorker(worker.id);
|
||||
const playShell: PresentShellId | undefined = playPresent
|
||||
? parseShellAdaptationFromReplyFormat(
|
||||
params.blackboard.getContentByTag("设计.正文组成") ??
|
||||
params.blackboard.getContentByTag("设计.回复格式"),
|
||||
)?.shell_id ?? "prose"
|
||||
: undefined;
|
||||
|
||||
const systemContent =
|
||||
promptBody +
|
||||
WORKER_OUTPUT_INSTRUCTION +
|
||||
(playPresent && playShell
|
||||
? playPresentOutputInstruction(playShell)
|
||||
: WORKER_OUTPUT_INSTRUCTION) +
|
||||
(useGmHarness ? GM_CHANCE_HARNESS_INSTRUCTION : "");
|
||||
const playPreset = isPlayLayerActive(params.slots)
|
||||
? resolveActivePreset(loadAppSettings().activePresetId)
|
||||
@@ -663,7 +837,9 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
||||
stream: params.stream,
|
||||
caller: `worker:${worker.id}`,
|
||||
});
|
||||
return parseWorkerResponse(selectJsonPayload(harness.content), worker.outputTags);
|
||||
return parseWorkerResponse(selectJsonPayload(harness.content), worker.outputTags, {
|
||||
fallbackShell: playShell,
|
||||
});
|
||||
}
|
||||
|
||||
const streamCallbacks: StreamCallbacks | undefined = params.stream
|
||||
@@ -674,12 +850,12 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
||||
: undefined;
|
||||
|
||||
const result = await completeStructured(params.llm, messages, {
|
||||
schema: {
|
||||
schema: playPresent ? PRESENT_JSON_SCHEMA : {
|
||||
type: "object",
|
||||
additionalProperties: true,
|
||||
},
|
||||
name: "submit_worker_result",
|
||||
schemaIsLoose: true,
|
||||
name: playPresent ? "submit_present_packet" : "submit_worker_result",
|
||||
schemaIsLoose: !playPresent,
|
||||
capabilities: resolveActiveProfile()?.capabilities,
|
||||
caller: `worker:${worker.id}`,
|
||||
stream: streamCallbacks,
|
||||
@@ -692,6 +868,7 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
||||
return parseWorkerResponse(
|
||||
selectJsonPayload(raw, result.reasoning) || raw,
|
||||
worker.outputTags,
|
||||
{ fallbackShell: playShell },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -699,8 +876,9 @@ export async function runWorkerSkill(params: WorkerRunParams): Promise<WorkerRun
|
||||
export function parseWorkerResponseForTest(
|
||||
raw: string,
|
||||
outputTags: string[],
|
||||
opts?: { fallbackShell?: PresentShellId },
|
||||
): WorkerRunResult {
|
||||
return parseWorkerResponse(raw, outputTags);
|
||||
return parseWorkerResponse(raw, outputTags, opts);
|
||||
}
|
||||
|
||||
/** @internal 供单测验证 inputTags → inputs 拼接 */
|
||||
|
||||
Reference in New Issue
Block a user