Files
writing-agent/src/dictate/turn.ts
T

1083 lines
40 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { LlmProvider } from "../llm/client.js";
import {
DriverIterationLimitError,
type AgentDriver,
} from "../runtime/driver.js";
import { createPlannedDictateDriver } from "./round.js";
import { buildDictateMessages } from "./context.js";
import { buildDictateSystemPrompt } from "./prompt.js";
import { DICTATE_TOOL_DEFINITIONS } from "./tools.js";
import {
isAllowedProductTag,
parseDictateOrder,
type DictateSelfScore,
type DictateChatTurn,
type DictateProduct,
} from "./types.js";
import {
DICTATE_MULTI_MODULE_REPLY_HINT,
resolveModuleForInsertTag,
} from "./insert-feedback.js";
import { dictateProductFamily, resolveRepeatableInsertTag, splitConcreteInstanceContents } from "./repeatable-tags.js";
import type { ModuleCatalog } from "../skills/creation-flow.js";
import {
CREATION_PLAN_TAG,
formatCreationPlanCapabilityCatalog,
formatCreationPlanProgress,
parseCreationPlan,
requiredCreationPlanGaps,
validateCreationPlan,
} from "./creation-plan.js";
import {
DICTATE_MODULE_READ_STATE_TAG,
parseDictateModuleReadState,
readLibraryEntries,
serializeDictateModuleReadState,
type DictateModuleReadPayload,
} from "./read-module.js";
import {
VARIABLE_CATALOG_TAG,
parseVariableCatalog,
serializeVariableCatalog,
upsertVariableField,
removeVariableField,
seedVariablesFromCatalog,
type UpsertVariableInput,
} from "../skills/variable-catalog.js";
import {
VARIABLE_DESIGN_TAG,
syncVariableDesignToRuntime,
writeVariableDesignSync,
} from "../skills/variable-design-compile.js";
import {
VALUE_MAP_TAG,
parseValueMapDoc,
serializeValueMapDoc,
upsertValueMapEntry,
removeValueMapEntry,
type UpsertMapInput,
} from "../skills/value-map.js";
import {
parseTableDoc,
stringifyTableDoc,
} from "../blackboard/table-cells.js";
import { isDictateOpeningTag } from "./experience-anchor.js";
import { canonicalizeInsertedContent } from "../parse/json-doc.js";
import {
openingContractError,
parseReplyDataContract,
replyContractSummary,
type ReplyDataContract,
} from "../skills/reply-contract.js";
function seedCurrentFromCatalog(handlers: DictateTurnHandlers): string[] {
const seeded = seedVariablesFromCatalog({
catalogRaw: handlers.readTag(VARIABLE_CATALOG_TAG),
currentRaw: handlers.readTag("变量.当前"),
initialRaw: handlers.readTag("运行.初始变量"),
actor: "system:declare_variable",
});
const wrote: string[] = [];
if (seeded.initial) {
handlers.writeTag("运行.初始变量", seeded.initial);
wrote.push("运行.初始变量");
}
if (seeded.current) {
handlers.writeTag("变量.当前", seeded.current);
wrote.push("变量.当前");
}
return wrote;
}
function syncDesignProductToTable(
handlers: DictateTurnHandlers,
designContent?: string,
): string[] {
const sync = syncVariableDesignToRuntime({
designRaw: designContent ?? handlers.readTag(VARIABLE_DESIGN_TAG),
catalogRaw: handlers.readTag(VARIABLE_CATALOG_TAG),
currentRaw: handlers.readTag("变量.当前"),
initialRaw: handlers.readTag("运行.初始变量"),
});
return writeVariableDesignSync(sync, (tag, content) =>
handlers.writeTag(tag, content),
);
}
export type DictateTurnHandlers = {
listProducts: () => DictateProduct[];
/** order 省略时由实现沿用原序或默认 */
writeProduct: (
tag: string,
content: string,
order?: number,
metadata?: { layer?: "intermediate" | "final"; selfScore?: DictateSelfScore },
) => void;
/** 删除固定产物 tag;返回是否曾存在 */
deleteProduct: (tag: string) => boolean;
/** 读/写变量目录与映射(程序校验后的 JSON) */
readTag: (tag: string) => string | undefined;
writeTag: (tag: string, content: string) => void;
/** 删除任意黑板 tag(投影 / 变量.当前 等) */
deleteTag: (tag: string) => boolean;
/** 写入开场白前运行确定性收口工具,保证运行配置先于开场落盘。 */
prepareOpening?: () => void;
clearDialogue: () => void;
onToolCall?: (name: string, detail: string) => void;
onThinkingDelta?: (delta: string) => void;
onThinkingDone?: (text: string) => void;
/**
* insert 成功后:按 tag 反查能力,返回独立「回复模块」markdown。
* 程序写入 tool result 的 reply_module;同轮多个须在末尾回复中全部附带。
*/
lookupInsertFeedback?: (tag: string) => string | undefined;
};
export type DictateTurnResult = {
reply: string;
iterations: number;
wroteTags: string[];
deletedTags: string[];
clearedDialogue: boolean;
};
/**
* 一轮对话落盘的工具上限。
* 串行模型往往一轮只调一个工具:读能力再写入,方案里十余项,再加上收尾回复。
*/
export const DICTATE_DRIVER_MAX_ITERATIONS = 36;
/** 接近上限时写进工具结果,让模型停下来回复用户。 */
export const DICTATE_TURN_LIMIT_HINT =
"本轮工具次数已接近上限。停止调用工具,直接回复用户。已写入的保留;未写的能力下一轮继续。";
/**
* 跑一轮对话落盘整理:任务 messages 由本模块拼;preset 夹心由 LLM 包装层统一装配。
*/
export async function runDictateTurn(params: {
llm: LlmProvider;
dialogue: DictateChatTurn[];
handlers: DictateTurnHandlers;
systemPrompt?: string;
recipeName?: string;
recipeBrief?: string;
/** modules catalog 的何时落盘块(formatModuleCatalogForDictate) */
moduleGuide?: string;
/** 尚无本局方案时显示的旧配方案例目录 */
recipeExamples?: string;
/** 用于本局方案覆盖校验及方案态能力状态 */
moduleCatalog?: ModuleCatalog | null;
/** 当前能力提示的可读内容与版本;存在时启用写前读取门禁 */
moduleReadIndex?: ReadonlyMap<string, DictateModuleReadPayload>;
/** catalog 中 repeatable 能力的 artifact 基名(设计.生成规则 等) */
repeatableFamilies?: ReadonlySet<string> | readonly string[];
/** 更早对话保留轮数;默认 2,可用 DICTATE_HISTORY_TURNS 覆盖 */
historyTurns?: number;
driver?: AgentDriver;
}): Promise<DictateTurnResult> {
const driver =
params.driver ??
createPlannedDictateDriver({
llm: params.llm,
dialogue: params.dialogue,
handlers: {
listProducts: params.handlers.listProducts,
readTag: params.handlers.readTag,
writeTag: params.handlers.writeTag,
deleteTag: params.handlers.deleteTag,
prepareOpening: params.handlers.prepareOpening,
lookupInsertFeedback: params.handlers.lookupInsertFeedback,
onThinkingDelta: params.handlers.onThinkingDelta,
onThinkingDone: params.handlers.onThinkingDone,
},
moduleCatalog: params.moduleCatalog,
moduleReadIndex: params.moduleReadIndex,
moduleGuide: params.moduleGuide,
recipeExamples: params.recipeExamples,
recipeName: params.recipeName,
recipeBrief: params.recipeBrief,
systemPrompt: params.systemPrompt,
historyTurns: params.historyTurns,
});
const wroteTags: string[] = [];
const deletedTags: string[] = [];
let clearedDialogue = false;
let openingPrepared = false;
let dialogue = [...params.dialogue];
const repeatableFamilies = new Set(
[...(params.repeatableFamilies ?? [])].map((t) => String(t).trim()).filter(Boolean),
);
const systemPrompt =
params.systemPrompt ??
(() => {
const products = params.handlers.listProducts();
const plan = parseCreationPlan(
products.find((product) => product.tag === CREATION_PLAN_TAG)?.content,
);
return buildDictateSystemPrompt({
...(plan
? {
creationPlanState: formatCreationPlanProgress({
plan,
catalog: params.moduleCatalog,
products,
}),
}
: {
capabilityCatalog:
formatCreationPlanCapabilityCatalog(params.moduleCatalog) ||
params.moduleGuide,
recipeExamples: params.recipeExamples,
recipeName: params.recipeName,
recipeBrief: params.recipeBrief,
}),
});
})();
const assembled = buildDictateMessages({
systemPrompt,
products: params.handlers.listProducts(),
dialogue,
});
const userBlock = assembled.filter((m) => m.role !== "system");
let toolSteps = 0;
let run;
try {
run = await driver.run({
system: systemPrompt,
messages: userBlock,
tools: DICTATE_TOOL_DEFINITIONS,
caller: "dictate_agent",
label: "对话落盘",
maxIterations: DICTATE_DRIVER_MAX_ITERATIONS,
onThinkingDelta: params.handlers.onThinkingDelta,
onThinkingDone: params.handlers.onThinkingDone,
handleStep: (calls) => {
toolSteps += 1;
const moduleNamesThisStep: string[] = [];
const results = calls.map((call) => {
const args = safeParseArgs(call.arguments);
if (
call.name === "declare_variable" ||
call.name === "declare_map" ||
call.name === "delete" ||
call.name === "undeclare_variable" ||
call.name === "remove_map"
) {
openingPrepared = false;
}
if (call.name === "read_module") {
const ref = String(args.module_id ?? "").trim();
const module = params.moduleReadIndex?.get(ref);
if (!module) {
const err = `未找到能力:${ref || "空"}`;
params.handlers.onToolCall?.(call.name, err);
return { callId: call.id, content: JSON.stringify({ error: err }) };
}
const state = parseDictateModuleReadState(
params.handlers.readTag(DICTATE_MODULE_READ_STATE_TAG),
);
state.modules[module.module_id] = module.version;
state.active_module_id = module.module_id;
params.handlers.writeTag(
DICTATE_MODULE_READ_STATE_TAG,
serializeDictateModuleReadState(state),
);
params.handlers.onToolCall?.(
call.name,
`${module.module_id}@${module.version}`,
);
const replyContract =
module.module_id === "opening-setup" ||
module.module_id === "zero-layer-opening-setup"
? contractForOpeningTag(
module.module_id === "zero-layer-opening-setup"
? "设计.0层开场白与初态"
: "设计.开场白",
params.handlers.listProducts(),
)
: undefined;
return {
callId: call.id,
content: JSON.stringify({
ok: true,
...module,
...(replyContract
? { reply_contract: replyContractSummary(replyContract) }
: module.module_id === "opening-setup" ||
module.module_id === "zero-layer-opening-setup"
? {
reply_contract_missing:
"还没有可执行的正文组成数据契约。先写正文组成,再写开场。",
}
: {}),
}),
};
}
if (call.name === "read_library_entry") {
const state = parseDictateModuleReadState(
params.handlers.readTag(DICTATE_MODULE_READ_STATE_TAG),
);
const module = state.active_module_id
? params.moduleReadIndex?.get(state.active_module_id)
: undefined;
if (!module) {
const err = "请先 read_module 读取要使用该库的能力";
params.handlers.onToolCall?.(call.name, err);
return { callId: call.id, content: JSON.stringify({ error: err }) };
}
const result = readLibraryEntries({
module,
libraryId: String(args.library_id ?? ""),
entryIds: Array.isArray(args.entry_ids)
? args.entry_ids.map(String)
: [],
reason: typeof args.reason === "string" ? args.reason : undefined,
});
params.handlers.onToolCall?.(
call.name,
result.error ?? `${args.library_id}: ${result.entries?.length ?? 0}`,
);
return {
callId: call.id,
content: JSON.stringify(
result.error
? { error: result.error }
: {
ok: true,
module_id: module.module_id,
library_id: String(args.library_id ?? ""),
entries: result.entries,
},
),
};
}
if (call.name === "prepare_opening") {
params.handlers.prepareOpening?.();
openingPrepared = true;
params.handlers.onToolCall?.(
call.name,
"信息可见范围 → 游玩执行方式 → 上下文排序 → 运行配置组装",
);
return {
callId: call.id,
content: JSON.stringify({
ok: true,
runtime_prepared: true,
next: "现在生成并 insert 开场白",
}),
};
}
if (call.name === "insert" || call.name === "write_product") {
let tag = String(args.position ?? args.tag ?? "").trim();
const content = canonicalizeInsertedContent(String(args.content ?? ""));
if (tag === VARIABLE_CATALOG_TAG || tag === VALUE_MAP_TAG) {
const err = `请用 declare_variable / declare_map,不要 insert ${tag}`;
params.handlers.onToolCall?.(call.name, err);
return { callId: call.id, content: JSON.stringify({ error: err }) };
}
if (!isAllowedProductTag(tag)) {
const err = `拒绝写入:position 必须以「用户.」或「设计.」开头(收到:${tag || "空"})`;
params.handlers.onToolCall?.(call.name, err);
return { callId: call.id, content: JSON.stringify({ error: err }) };
}
if (!content.trim()) {
const err = "content 不能为空";
params.handlers.onToolCall?.(call.name, err);
return { callId: call.id, content: JSON.stringify({ error: err }) };
}
const targetModule = resolveModuleForInsertTag(
tag,
params.moduleCatalog,
);
if (
targetModule &&
params.moduleReadIndex &&
requiresModuleRead(targetModule.id, targetModule.auto)
) {
const current = params.moduleReadIndex.get(targetModule.id);
const state = parseDictateModuleReadState(
params.handlers.readTag(DICTATE_MODULE_READ_STATE_TAG),
);
if (!current || state.modules[targetModule.id] !== current.version) {
const err = `写入 ${tag} 前必须先 read_module("${targetModule.id}") 读取当前提示版本`;
params.handlers.onToolCall?.(call.name, err);
return {
callId: call.id,
content: JSON.stringify({
error: err,
required_module_id: targetModule.id,
required_version: current?.version,
}),
};
}
}
const currentModule = targetModule
? params.moduleReadIndex?.get(targetModule.id)
: undefined;
const selfScore = parseAndValidateSelfScore(
args.self_score,
currentModule?.score_dimensions ?? [],
);
if ("error" in selfScore) {
params.handlers.onToolCall?.(call.name, selfScore.error);
return {
callId: call.id,
content: JSON.stringify({ error: selfScore.error }),
};
}
if (tag === CREATION_PLAN_TAG) {
const validation = validateCreationPlan(content, params.moduleCatalog);
if (!validation.ok) {
const err = validation.error ?? "本局创作方案校验失败";
params.handlers.onToolCall?.(call.name, err);
return {
callId: call.id,
content: JSON.stringify({
error: err,
missing_ids: validation.missingIds,
duplicate_ids: validation.duplicateIds,
unknown_ids: validation.unknownIds,
}),
};
}
}
if (repeatableFamilies.size) {
const family = dictateProductFamily(tag);
const pieces =
targetModule?.id === "concrete-instances" ||
family === "设计.具体实例"
? splitConcreteInstanceContents(content)
: [content];
const baseTag = pieces.length > 1 ? "设计.具体实例" : tag;
const positions: string[] = [];
const resolvedPieces: Array<{ tag: string; content: string }> = [];
for (const piece of pieces) {
const resolved = resolveRepeatableInsertTag({
tag: baseTag,
content: piece,
existingTags: [
...params.handlers.listProducts().map((p) => p.tag),
...resolvedPieces.map((item) => item.tag),
],
repeatableFamilies,
});
if ("error" in resolved) {
params.handlers.onToolCall?.(call.name, resolved.error);
return {
callId: call.id,
content: JSON.stringify({ error: resolved.error }),
};
}
resolvedPieces.push({ tag: resolved.tag, content: piece });
}
let replyModule: string | undefined;
for (const [index, piece] of resolvedPieces.entries()) {
const pieceTag = piece.tag;
const order = parseDictateOrder(args.order);
const productsBeforeWrite = params.handlers.listProducts();
const isOpening = isDictateOpeningTag(pieceTag);
const contractMiss = openingContractMiss(
pieceTag,
piece.content,
params.handlers.listProducts(),
);
if (contractMiss) {
params.handlers.onToolCall?.(call.name, contractMiss.error);
return {
callId: call.id,
content: JSON.stringify(contractMiss),
};
}
const explicitlyPrepared = openingPrepared;
if (isOpening && !explicitlyPrepared) {
params.handlers.prepareOpening?.();
}
params.handlers.writeProduct(pieceTag, piece.content, order, {
...(targetModule
? {
layer: resolveInsertLayer({
moduleId: targetModule.id,
catalogLayer: targetModule.layer,
content: piece.content,
}),
}
: {}),
...(selfScore.value ? { selfScore: selfScore.value } : {}),
});
if (!isOpening) openingPrepared = false;
wroteTags.push(pieceTag);
positions.push(pieceTag);
const detail =
order === undefined ? pieceTag : `${pieceTag} order=${order}`;
params.handlers.onToolCall?.(call.name, detail);
replyModule =
params.handlers.lookupInsertFeedback?.(pieceTag)?.trim() ||
replyModule;
const requiredGaps = isDictateOpeningTag(pieceTag)
? requiredCreationPlanGaps({
planRaw: productsBeforeWrite.find(
(product) => product.tag === CREATION_PLAN_TAG,
)?.content,
catalog: params.moduleCatalog,
products: [
...productsBeforeWrite.filter((product) => product.tag !== pieceTag),
{ tag: pieceTag, content: piece.content, order },
],
})
: [];
if (index === resolvedPieces.length - 1) {
if (replyModule) {
const title =
replyModule.match(/^#\s*(?:模块|本轮关注)\s*·\s*(.+)$/m)?.[1]?.trim() ||
pieceTag;
moduleNamesThisStep.push(title);
}
return {
callId: call.id,
content: JSON.stringify({
ok: true,
position: positions[0],
...(positions.length > 1 ? { positions, split: positions.length } : {}),
...(isOpening
? {
runtime_prepared_before_opening: true,
preparation_mode: explicitlyPrepared
? "prepare_opening"
: "implicit_fallback",
}
: {}),
...(order !== undefined ? { order } : {}),
...(requiredGaps.length
? {
warning: `仍有“必须”能力未落:${requiredGaps.join("、")}。开场白已写入,请在准备开玩前提醒用户补齐或调整方案。`,
missing_required_module_ids: requiredGaps,
}
: {}),
...(replyModule
? {
reply_module: replyModule,
reply_hint: DICTATE_MULTI_MODULE_REPLY_HINT,
}
: {}),
}),
};
}
}
}
const order = parseDictateOrder(args.order);
const productsBeforeWrite = params.handlers.listProducts();
const isOpening = isDictateOpeningTag(tag);
const contractMiss = openingContractMiss(
tag,
content,
params.handlers.listProducts(),
);
if (contractMiss) {
params.handlers.onToolCall?.(call.name, contractMiss.error);
return {
callId: call.id,
content: JSON.stringify(contractMiss),
};
}
const explicitlyPrepared = openingPrepared;
if (isOpening && !explicitlyPrepared) {
params.handlers.prepareOpening?.();
}
params.handlers.writeProduct(tag, content, order, {
...(targetModule
? {
layer: resolveInsertLayer({
moduleId: targetModule.id,
catalogLayer: targetModule.layer,
content,
}),
}
: {}),
...(selfScore.value ? { selfScore: selfScore.value } : {}),
});
if (tag === VARIABLE_DESIGN_TAG || tag.startsWith(`${VARIABLE_DESIGN_TAG}#`)) {
const synced = syncDesignProductToTable(params.handlers, content);
wroteTags.push(...synced);
}
if (!isOpening) openingPrepared = false;
wroteTags.push(tag);
const detail =
order === undefined ? tag : `${tag} order=${order}`;
params.handlers.onToolCall?.(call.name, detail);
const replyModule =
params.handlers.lookupInsertFeedback?.(tag)?.trim() || undefined;
if (replyModule) {
const title =
replyModule.match(/^#\s*(?:模块|本轮关注)\s*·\s*(.+)$/m)?.[1]?.trim() || tag;
moduleNamesThisStep.push(title);
}
const requiredGaps = isDictateOpeningTag(tag)
? requiredCreationPlanGaps({
planRaw: productsBeforeWrite.find(
(product) => product.tag === CREATION_PLAN_TAG,
)?.content,
catalog: params.moduleCatalog,
products: [
...productsBeforeWrite.filter((product) => product.tag !== tag),
{ tag, content, order },
],
})
: [];
return {
callId: call.id,
content: JSON.stringify({
ok: true,
position: tag,
...(isOpening
? {
runtime_prepared_before_opening: true,
preparation_mode: explicitlyPrepared
? "prepare_opening"
: "implicit_fallback",
}
: {}),
...(order !== undefined ? { order } : {}),
...(requiredGaps.length
? {
warning: `仍有“必须”能力未落:${requiredGaps.join("、")}。开场白已写入,请在准备开玩前提醒用户补齐或调整方案。`,
missing_required_module_ids: requiredGaps,
}
: {}),
...(replyModule
? {
reply_module: replyModule,
reply_hint: DICTATE_MULTI_MODULE_REPLY_HINT,
}
: {}),
}),
};
}
if (call.name === "declare_variable") {
const input: UpsertVariableInput = {
key: String(args.key ?? "").trim(),
type: args.type != null ? String(args.type) : undefined,
initial: args.initial,
user_visible:
args.user_visible === undefined
? undefined
: Boolean(args.user_visible),
note: args.note != null ? String(args.note) : undefined,
};
const current = parseVariableCatalog(
params.handlers.readTag(VARIABLE_CATALOG_TAG),
);
const { doc, error } = upsertVariableField(current, input);
if (error) {
params.handlers.onToolCall?.(call.name, error);
return { callId: call.id, content: JSON.stringify({ error }) };
}
params.handlers.writeTag(
VARIABLE_CATALOG_TAG,
serializeVariableCatalog(doc),
);
wroteTags.push(VARIABLE_CATALOG_TAG);
const seeded = seedCurrentFromCatalog(params.handlers);
wroteTags.push(...seeded);
const detail = `${input.key} visible=${doc.fields.find((f) => f.key === input.key)?.user_visible !== false}`;
params.handlers.onToolCall?.(call.name, detail);
return {
callId: call.id,
content: JSON.stringify({ ok: true, key: input.key, fields: doc.fields.length }),
};
}
if (call.name === "declare_map") {
const input: UpsertMapInput = {
id: String(args.id ?? "").trim(),
field: String(args.field ?? "").trim(),
target_tag: String(args.target_tag ?? "").trim(),
bands: args.bands,
note: args.note != null ? String(args.note) : undefined,
};
const current = parseValueMapDoc(
params.handlers.readTag(VALUE_MAP_TAG),
);
const { doc, error } = upsertValueMapEntry(current, input);
if (error) {
params.handlers.onToolCall?.(call.name, error);
return { callId: call.id, content: JSON.stringify({ error }) };
}
params.handlers.writeTag(VALUE_MAP_TAG, serializeValueMapDoc(doc));
wroteTags.push(VALUE_MAP_TAG);
const detail = `${input.id} ${input.field}→${input.target_tag}`;
params.handlers.onToolCall?.(call.name, detail);
return {
callId: call.id,
content: JSON.stringify({
ok: true,
id: input.id,
target_tag: input.target_tag,
maps: doc.maps.length,
}),
};
}
if (call.name === "delete") {
const tag = String(args.position ?? args.tag ?? "").trim();
const reason = String(args.reason ?? "").trim();
if (!isAllowedProductTag(tag)) {
const err = `拒绝删除:position 必须以「用户.」或「设计.」开头(收到:${tag || "空"})`;
params.handlers.onToolCall?.(call.name, err);
return { callId: call.id, content: JSON.stringify({ error: err }) };
}
const existed = params.handlers.deleteProduct(tag);
if (!existed) {
const err = `无产物 ${tag}`;
params.handlers.onToolCall?.(call.name, err);
return { callId: call.id, content: JSON.stringify({ error: err }) };
}
deletedTags.push(tag);
const detail = reason ? `${tag}(${reason})` : tag;
params.handlers.onToolCall?.(call.name, detail);
return {
callId: call.id,
content: JSON.stringify({ ok: true, deleted: tag }),
};
}
if (call.name === "undeclare_variable") {
const key = String(args.key ?? "").trim();
const current = parseVariableCatalog(
params.handlers.readTag(VARIABLE_CATALOG_TAG),
);
const { doc, error } = removeVariableField(current, key);
if (error) {
params.handlers.onToolCall?.(call.name, error);
return { callId: call.id, content: JSON.stringify({ error }) };
}
if (doc.fields.length === 0) {
params.handlers.deleteTag(VARIABLE_CATALOG_TAG);
} else {
params.handlers.writeTag(
VARIABLE_CATALOG_TAG,
serializeVariableCatalog(doc),
);
}
// 同步从 变量.当前 / 运行.初始变量 去掉该格
for (const tableTag of ["变量.当前", "运行.初始变量"] as const) {
const raw = params.handlers.readTag(tableTag);
const table = parseTableDoc(raw);
if (!table?.rows.some((r) => r.key === key)) continue;
const next = {
rows: table.rows.filter((r) => r.key !== key),
};
if (next.rows.length === 0) {
params.handlers.deleteTag(tableTag);
} else {
params.handlers.writeTag(tableTag, stringifyTableDoc(next));
}
}
deletedTags.push(`${VARIABLE_CATALOG_TAG}#${key}`);
params.handlers.onToolCall?.(call.name, key);
return {
callId: call.id,
content: JSON.stringify({
ok: true,
key,
fields: doc.fields.length,
}),
};
}
if (call.name === "remove_map") {
const id = String(args.id ?? "").trim();
const current = parseValueMapDoc(
params.handlers.readTag(VALUE_MAP_TAG),
);
const { doc, error, removed } = removeValueMapEntry(current, id);
if (error || !removed) {
const err = error ?? `映射无 id ${id}`;
params.handlers.onToolCall?.(call.name, err);
return { callId: call.id, content: JSON.stringify({ error: err }) };
}
if (doc.maps.length === 0) {
params.handlers.deleteTag(VALUE_MAP_TAG);
} else {
params.handlers.writeTag(VALUE_MAP_TAG, serializeValueMapDoc(doc));
}
const stillUsed = doc.maps.some(
(m) => m.target_tag === removed.target_tag,
);
if (!stillUsed) {
params.handlers.deleteTag(removed.target_tag);
}
deletedTags.push(`${VALUE_MAP_TAG}#${id}`);
params.handlers.onToolCall?.(
call.name,
`${id} → ${removed.target_tag}`,
);
return {
callId: call.id,
content: JSON.stringify({
ok: true,
id,
cleared_target: stillUsed ? null : removed.target_tag,
maps: doc.maps.length,
}),
};
}
if (call.name === "clear_dialogue") {
const reason = String(args.reason ?? "").trim() || "上下文过长";
params.handlers.clearDialogue();
clearedDialogue = true;
dialogue = [];
params.handlers.onToolCall?.(call.name, reason);
return {
callId: call.id,
content: JSON.stringify({
ok: true,
cleared: true,
note: "对话已清空;产物与变量/映射仍在。",
}),
};
}
const err = `未知工具:${call.name}`;
params.handlers.onToolCall?.(call.name, err);
return { callId: call.id, content: JSON.stringify({ error: err }) };
});
if (moduleNamesThisStep.length > 1) {
const roster = moduleNamesThisStep.join("、");
for (const r of results) {
try {
const parsed = JSON.parse(r.content) as Record<string, unknown>;
if (typeof parsed.reply_module === "string") {
parsed.reply_hint = `${DICTATE_MULTI_MODULE_REPLY_HINT} 本步已带回模块:${roster}。`;
r.content = JSON.stringify(parsed);
}
} catch {
/* ignore */
}
}
}
if (toolSteps >= DICTATE_DRIVER_MAX_ITERATIONS - 2) {
noteTurnLimit(results);
}
return { kind: "continue" as const, results };
},
});
} catch (err) {
if (!(err instanceof DriverIterationLimitError)) throw err;
return {
reply: composeDictateReply({
wroteTags,
deletedTags,
clearedDialogue,
capped: true,
}),
iterations: err.iterations,
wroteTags: [...new Set(wroteTags)],
deletedTags: [...new Set(deletedTags)],
clearedDialogue,
};
}
return {
reply: composeDictateReply({
stopText: run.stop.kind === "text" ? run.stop.content : "",
wroteTags,
deletedTags,
clearedDialogue,
}),
iterations: run.iterations,
wroteTags: [...new Set(wroteTags)],
deletedTags: [...new Set(deletedTags)],
clearedDialogue,
};
}
function noteTurnLimit(
results: Array<{ callId: string; content: string }>,
): void {
const last = results.at(-1);
if (!last) return;
try {
const parsed = JSON.parse(last.content) as Record<string, unknown>;
parsed.turn_limit = DICTATE_TURN_LIMIT_HINT;
last.content = JSON.stringify(parsed);
} catch {
last.content = `${last.content}\n${DICTATE_TURN_LIMIT_HINT}`;
}
}
function composeDictateReply(params: {
stopText?: string;
wroteTags: string[];
deletedTags: string[];
clearedDialogue: boolean;
capped?: boolean;
}): string {
let reply = params.stopText?.trim() ?? "";
if (!reply) {
if (params.wroteTags.length || params.deletedTags.length) {
const parts: string[] = [];
if (params.wroteTags.length) {
parts.push(`已写入:${[...new Set(params.wroteTags)].join("、")}`);
}
if (params.deletedTags.length) {
parts.push(`已删除:${[...new Set(params.deletedTags)].join("、")}`);
}
reply = `${parts.join(";")}。还有要补充的吗?`;
} else if (params.clearedDialogue) {
reply = "对话已清空,产物都还在。可以继续说。";
} else if (params.capped) {
reply = "这轮工具次数已用完,还没有新的产物。可以说要先补哪一块。";
} else {
reply = "我在听,请继续说你想要的体验或设定。";
}
}
if (params.capped && !reply.includes("工具次数已用完")) {
reply = `${reply} 这轮工具次数已用完,其余下一轮继续。`;
}
return reply;
}
function safeParseArgs(raw: string): Record<string, unknown> {
try {
const parsed = JSON.parse(raw || "{}");
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return {};
}
return parsed as Record<string, unknown>;
} catch {
return {};
}
}
function contractForOpeningTag(
tag: string,
products: DictateProduct[],
): ReplyDataContract | undefined {
const want = tag.includes("0层") ? "设计.0层正文组成" : "设计.正文组成";
const raw =
products.find((product) => product.tag === want)?.content ??
products.find((product) => product.tag === "设计.回复格式")?.content;
return parseReplyDataContract(raw);
}
function openingContractMiss(
tag: string,
content: string,
products: DictateProduct[],
): { error: string; reply_contract?: ReturnType<typeof replyContractSummary> } | undefined {
if (!isDictateOpeningTag(tag)) return undefined;
const contract = contractForOpeningTag(tag, products);
if (!contract) return undefined;
const error = openingContractError(content, contract);
if (!error) return undefined;
return { error, reply_contract: replyContractSummary(contract) };
}
function requiresModuleRead(moduleId: string, auto?: boolean): boolean {
if (auto) return false;
return !new Set([
"narrative",
"variable-context",
"context-order",
"worker-spec",
"refine",
]).has(moduleId);
}
function resolveInsertLayer(params: {
moduleId: string;
catalogLayer?: "intermediate" | "final";
content: string;
}): "intermediate" | "final" {
if (params.moduleId === "generation-rules") {
const lifecycle = normalizeGenerationRuleLifecycle(
extractGenerationRuleLifecycle(params.content),
);
if (lifecycle === "seed_only") return "intermediate";
if (lifecycle === "runtime_only" || lifecycle === "seed_and_runtime") {
return "final";
}
}
return params.catalogLayer ?? "final";
}
function extractGenerationRuleLifecycle(
content: string,
): string | undefined {
try {
const parsed = JSON.parse(content) as {
正文?: { 生命周期?: unknown };
生命周期?: unknown;
};
const raw = parsed.正文?.生命周期 ?? parsed.生命周期;
return typeof raw === "string" ? raw.trim() : undefined;
} catch {
return undefined;
}
}
function normalizeGenerationRuleLifecycle(
raw: string | undefined,
): string | undefined {
if (!raw) return undefined;
if (raw === "只预生成" || raw === "seed_only") return "seed_only";
if (raw === "仅游玩期" || raw === "runtime_only") return "runtime_only";
if (raw === "预生成并游玩期" || raw === "seed_and_runtime") {
return "seed_and_runtime";
}
return raw;
}
function parseAndValidateSelfScore(
raw: unknown,
expectedNames: readonly string[],
): { value?: DictateSelfScore } | { error: string } {
if (expectedNames.length === 0) return {};
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
return { error: `self_score 必填;维度:${expectedNames.join("、")}` };
}
const dims = (raw as { dims?: unknown }).dims;
if (!Array.isArray(dims)) {
return { error: "self_score.dims 必须是数组" };
}
const parsed: DictateSelfScore["dims"] = [];
for (const item of dims) {
if (!item || typeof item !== "object" || Array.isArray(item)) {
return { error: "self_score.dims 含无效条目" };
}
const row = item as Record<string, unknown>;
const name = typeof row.name === "string" ? row.name.trim() : "";
const score = Number(row.score);
const gap = typeof row.gap === "string" ? row.gap.trim() : "";
const options = Array.isArray(row.options)
? row.options.map(String).map((option) => option.trim()).filter(Boolean)
: [];
if (!name || !Number.isInteger(score) || score < 1 || score > 5) {
return { error: "self_score 每项须含合法 name 与 1~5 整数 score" };
}
if (score <= 3 && !gap) {
return { error: `self_score「${name}」不高于 3 分时必须写 gap` };
}
if (options.length > 0 && (options.length < 2 || options.length > 3)) {
return { error: `self_score「${name}」的 options 必须为 2~3 项` };
}
parsed.push({
name,
score,
...(gap ? { gap } : {}),
...(options.length ? { options } : {}),
});
}
const actual = parsed.map((dim) => dim.name);
if (
actual.length !== expectedNames.length ||
expectedNames.some((name) => !actual.includes(name)) ||
actual.some((name) => !expectedNames.includes(name))
) {
return {
error: `self_score 维度必须与能力 score 一致:${expectedNames.join("、")}`,
};
}
return { value: { dims: parsed } };
}