打通 Boss 直聘落档挂载与真值→投影映射,并修正带 tools 时的推理强度。

落档自动把产物挂进 context_order;创作期用 declare_variable/declare_map 钉目录与分档;真值变更持续重投影。顺带强制 tools 请求 reasoning_effort=none,并支持消息 restart。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
moran
2026-09-09 17:50:07 +08:00
co-authored by Cursor
parent bf0b218f7c
commit beb4c332e6
23 changed files with 1780 additions and 95 deletions
+19
View File
@@ -19,3 +19,22 @@ export {
} from "./context.js";
export { DICTATE_TOOL_DEFINITIONS } from "./tools.js";
export { runDictateTurn, type DictateTurnResult } from "./turn.js";
export {
applyDictatePlayBind,
bindDictateProductsToPlaySpec,
buildDictateContextOrder,
collectDictateBindProducts,
type DictatePlayBindResult,
} from "./play-bind.js";
export {
VARIABLE_CATALOG_TAG,
parseVariableCatalog,
type VariableCatalogDoc,
} from "../skills/variable-catalog.js";
export {
VALUE_MAP_TAG,
parseValueMapDoc,
lookupValueMapContent,
reprojectValueMaps,
type ValueMapDoc,
} from "../skills/value-map.js";
+351
View File
@@ -0,0 +1,351 @@
/**
* Boss 直聘式 · 落档/开玩时自动把产物 tag 挂到游玩 worker。
* 只做「固定上下文绑定」;变量清单、可见性、分档映射仍靠对话产出,此处不发明。
*/
import type { Blackboard } from "../blackboard/blackboard.js";
import {
CONTEXT_ORDER_SCHEMA,
CONTEXT_ORDER_TAG,
DIALOGUE_HISTORY_TAG,
WORKER_PERSONA_REF,
mergeContextOrderIntoWorkerSetJson,
renumberContextOrder,
type ContextOrderDoc,
type ContextOrderInsert,
type ContextOrderProjection,
type ContextOrderSlot,
} from "../skills/context-order.js";
import {
WORKER_SET_DRAFT_TAG,
WORKER_SET_FINAL_TAG,
} from "../skills/creation-units.js";
import {
defaultPlaySlots,
enabledPlaySlotIds,
parsePlaySlots,
refForSlot,
type PlaySlotsConfig,
} from "../skills/play-slots.js";
import { parseWorkerSetYaml } from "../skills/worker-set-parse.js";
import {
VARIABLE_CATALOG_TAG,
} from "../skills/variable-catalog.js";
import {
VALUE_MAP_TAG,
listValueMapTargetTags,
} from "../skills/value-map.js";
import {
DICTATE_ORDER_META_KEY,
effectiveDictateOrder,
sortDictateProducts,
type DictateProduct,
} from "./types.js";
/** 开场白进对话历史,不重复挂进每轮投影 */
const SKIP_BIND_TAGS = new Set([
"设计.开场白",
"设计.worker集",
"设计.worker集.草稿",
CONTEXT_ORDER_TAG,
"创作.进料模式",
VARIABLE_CATALOG_TAG,
VALUE_MAP_TAG,
]);
/** 偏文风/呈现:有叙事转述时挂转述;否则仍挂主世界层 */
const STYLE_TAG_RE = /叙事指南|文风|美学|纲领|禁忌|示例|正文组成|回复格式|监控栏|篇幅|结构/;
export type DictatePlayBindResult = {
workerSetJson: string;
contextOrder: ContextOrderDoc;
boundTags: string[];
changed: boolean;
};
function isStyleProductTag(tag: string): boolean {
return STYLE_TAG_RE.test(tag);
}
function shouldSkipBindTag(tag: string): boolean {
const t = tag.trim();
if (!t) return true;
if (SKIP_BIND_TAGS.has(t)) return true;
if (t.startsWith("输出.")) return true;
if (t.startsWith("运行.")) return true;
if (t.startsWith("创作.")) return true;
return false;
}
/** 从黑板收集 Boss 直聘产物(有正文的 用户.* / 设计.*) */
export function collectDictateBindProducts(board: Blackboard): DictateProduct[] {
const products: DictateProduct[] = [];
for (const e of board.listTagIndex()) {
if (!e.tag.startsWith("用户.") && !e.tag.startsWith("设计.")) continue;
if (shouldSkipBindTag(e.tag)) continue;
const item = board.getLatestByTag(e.tag);
const content = item?.content?.trim() ?? "";
if (!content) continue;
const rawOrder = item?.metadata?.[DICTATE_ORDER_META_KEY];
const order =
typeof rawOrder === "number" && Number.isFinite(rawOrder)
? rawOrder
: undefined;
products.push({ tag: e.tag, content, order });
}
return sortDictateProducts(products);
}
/**
* 真值 + 映射投影固定槽。
* - 有目录或已有表 → 挂 变量.当前
* - 映射声明的 target_tag 一律挂上(槽位固定;内容由重投影填充)
* - 已有正文的其它 上下文.* 也挂
*/
function collectRuntimeStateTags(board: Blackboard): string[] {
const out: string[] = [];
const push = (tag: string) => {
if (!tag || out.includes(tag)) return;
out.push(tag);
};
const hasCatalog = Boolean(board.getContentByTag(VARIABLE_CATALOG_TAG)?.trim());
const hasCurrent = Boolean(board.getContentByTag("变量.当前")?.trim());
if (hasCatalog || hasCurrent) push("变量.当前");
for (const tag of listValueMapTargetTags(board.getContentByTag(VALUE_MAP_TAG))) {
push(tag);
}
for (const tag of ["上下文.角色态度", "大纲.当前章", "上下文.旁观.状态摘要"]) {
if (board.getContentByTag(tag)?.trim()) push(tag);
}
for (const e of board.listTagIndex()) {
if (!e.tag.startsWith("上下文.")) continue;
if (e.tag === "上下文.定稿摘要") continue;
if (!board.getContentByTag(e.tag)?.trim()) continue;
push(e.tag);
}
return out;
}
function insert(
order: number,
ref: string,
projection: ContextOrderProjection,
note?: string,
): ContextOrderInsert {
return { order, ref, projection, ...(note ? { note } : {}) };
}
function buildGmInserts(params: {
products: DictateProduct[];
runtimeTags: string[];
includeStyle: boolean;
}): ContextOrderInsert[] {
const inserts: ContextOrderInsert[] = [
insert(0, WORKER_PERSONA_REF, "fixed", "槽位人设"),
];
let o = 1;
for (const p of params.products) {
if (!params.includeStyle && isStyleProductTag(p.tag)) continue;
inserts.push(
insert(
o++,
p.tag,
"full",
`Boss直聘固定产物〔order=${effectiveDictateOrder(p)}〕`,
),
);
}
inserts.push(
insert(o++, DIALOGUE_HISTORY_TAG, "summary", "对话历史(按投影裁剪)"),
);
for (const tag of params.runtimeTags) {
inserts.push(
insert(
o++,
tag,
tag === "变量.当前" ? "fields" : "full",
tag.startsWith("上下文.") || tag.startsWith("大纲.")
? "映射投影固定槽(内容随真值)"
: undefined,
),
);
}
inserts.push(insert(o++, "用户.最新输入", "full"));
return inserts;
}
function buildNarratorInserts(products: DictateProduct[]): ContextOrderInsert[] {
const inserts: ContextOrderInsert[] = [
insert(0, WORKER_PERSONA_REF, "fixed", "槽位人设"),
];
let o = 1;
for (const p of products) {
if (!isStyleProductTag(p.tag)) continue;
inserts.push(insert(o++, p.tag, "full", "文风/呈现参考"));
}
inserts.push(insert(o++, "运行.本轮.裁决", "full"));
return inserts;
}
function buildAuditorInserts(params: {
products: DictateProduct[];
runtimeTags: string[];
}): ContextOrderInsert[] {
const inserts: ContextOrderInsert[] = [
insert(0, WORKER_PERSONA_REF, "fixed", "槽位人设"),
];
let o = 1;
for (const p of params.products) {
if (!/变量|生成规则/.test(p.tag)) continue;
inserts.push(insert(o++, p.tag, "full"));
}
for (const tag of params.runtimeTags) {
inserts.push(insert(o++, tag, tag === "变量.当前" ? "fields" : "full"));
}
inserts.push(insert(o++, "运行.本轮.裁决", "full"));
inserts.push(insert(o++, "用户.最新输入", "full"));
return inserts;
}
/**
* 按当前 play_slots 与产物,合成 context_order(主世界拿最多;转述只拿文风/呈现)。
*/
export function buildDictateContextOrder(params: {
products: DictateProduct[];
playSlots: PlaySlotsConfig;
runtimeTags?: string[];
}): ContextOrderDoc {
const runtimeTags = params.runtimeTags ?? [];
const slots: ContextOrderSlot[] = [];
for (const slotId of enabledPlaySlotIds(params.playSlots)) {
const ref = refForSlot(params.playSlots, slotId);
if (slotId === "gm") {
slots.push({
ref,
label: "主世界层",
inserts: buildGmInserts({
products: params.products,
runtimeTags,
// 主世界推进剧情:固定产物全挂(含文风);有转述时转述另挂文风切片
includeStyle: true,
}),
});
continue;
}
if (slotId === "narrator") {
slots.push({
ref,
label: "叙事转述",
inserts: buildNarratorInserts(params.products),
});
continue;
}
if (slotId === "auditor") {
slots.push({
ref,
label: "旁观维护",
inserts: buildAuditorInserts({
products: params.products,
runtimeTags,
}),
});
continue;
}
if (slotId === "perspective") {
slots.push({
ref,
label: "角色视角",
inserts: [
insert(0, WORKER_PERSONA_REF, "fixed", "槽位人设"),
insert(1, "用户.最新输入", "full"),
insert(2, "运行.本轮.裁决", "full"),
],
});
}
}
return renumberContextOrder({
schema: CONTEXT_ORDER_SCHEMA,
brief: "Boss直聘落档自动挂载:固定产物 → 对话.历史 → 已有真值/投影 → 本轮输入",
play_slots: params.playSlots,
slots,
});
}
function resolvePlaySlots(existingRaw: string | undefined): PlaySlotsConfig {
const parsed = existingRaw?.trim()
? parseWorkerSetYaml(existingRaw)
: null;
return parsed?.play_slots ?? defaultPlaySlots();
}
/**
* 生成/更新 设计.worker集:保留已有 play_slots,重写 context_order 为产物挂载表。
*/
export function bindDictateProductsToPlaySpec(board: Blackboard): DictatePlayBindResult {
const products = collectDictateBindProducts(board);
const existing =
board.getContentByTag(WORKER_SET_FINAL_TAG)?.trim() ||
board.getContentByTag(WORKER_SET_DRAFT_TAG)?.trim() ||
"";
const playSlots = resolvePlaySlots(existing || undefined);
const runtimeTags = collectRuntimeStateTags(board);
const contextOrder = buildDictateContextOrder({
products,
playSlots,
runtimeTags,
});
const baseJson = existing.trim()
? existing
: JSON.stringify(
{
play_slots: playSlots,
brief: "Boss直聘落档自动生成",
},
null,
2,
);
let row: Record<string, unknown>;
try {
const parsed = JSON.parse(baseJson) as unknown;
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("bad");
}
row = parsed as Record<string, unknown>;
} catch {
row = { play_slots: playSlots, brief: "Boss直聘落档自动生成" };
}
if (!row.play_slots) row.play_slots = playSlots;
const withSlots = JSON.stringify(row, null, 2);
const workerSetJson = mergeContextOrderIntoWorkerSetJson(withSlots, contextOrder);
const boundTags = products.map((p) => p.tag);
const changed = workerSetJson.trim() !== existing.trim();
return { workerSetJson, contextOrder, boundTags, changed };
}
/** 写入黑板:设计.worker集 + 设计.上下文投影排序 */
export function applyDictatePlayBind(board: Blackboard): DictatePlayBindResult {
const result = bindDictateProductsToPlaySpec(board);
board.write({
tag: WORKER_SET_FINAL_TAG,
content: result.workerSetJson,
source: "runtime",
});
board.write({
tag: CONTEXT_ORDER_TAG,
content: JSON.stringify(result.contextOrder, null, 2),
source: "runtime",
});
return result;
}
/** @internal 测试用:判断 tag 是否文风类 */
export function isDictateStyleBindTag(tag: string): boolean {
return isStyleProductTag(tag);
}
+34 -34
View File
@@ -1,10 +1,8 @@
/**
* 转述整理式 · 创作进料专用系统提示
* Boss 直聘式 · 创作进料专用系统提示
*
* 拼装:本提示(system)+ 当前产物(按相对序)+ 全量对话。
* 仅用于创作层;开玩后由游玩管线接棒。
*
* 产物只许用 insert toolcall;聊天只做确认与建议。
*/
export function buildDictateSystemPrompt(opts?: {
@@ -21,55 +19,57 @@ ${opts.recipeBrief ? `要点:${opts.recipeBrief}` : ""}
`
: "";
return `你是转述整理助手:在**创作进料**阶段,把用户原话整理成黑板产物。作用对象是本局创作上下文(对话可清空,产物保留)。
return `你是 Boss 直聘式创作助手:把用户原话整理成可落盘规格。作用对象是本局创作上下文(对话可清空,产物保留)。
**本提示只用于创作。** 开玩后的叙事由程序另调游玩 worker;不要假装在跑游玩回合。
**本提示只用于创作。** 开玩后叙事由游玩 worker 执行;不要假装在跑游玩回合。
## 落盘(仅允许 toolcall)
产物**只能**用工具 **insert** 写入,禁止把设定/格式/开场全文只写在聊天里冒充已落盘。
- \`position\`:插入位置(tag),如 \`用户.需求\`、\`设计.正文组成\`、\`设计.开场白\`
- \`content\`:该位置正文
- \`order\`(可选):相对顺序,数字可负、可为 0、可正;**越小越靠前**
禁止把设定/变量/映射全文只写在聊天里冒充已落盘。
**order 怎么取:**
1. 变化越频繁 → 数字越大(越靠后)。例:开场白常改 > 正文组成 > 需求/美学。
2. 改动频率差不多 → 越重要数字越小(越靠前)。例:用户.需求 通常最前。
3. 同轮可多次 insert;程序按 order 升序拼进上下文。省略 order 时沿用该 position 原序,或按 tag 默认。
### insert — 不变型固定产物
- position:\`用户.*\` / \`设计.*\`(如需求、美学、正文组成、开场白)
- content:正文;order 可选(越小越靠前;越常改越大)
- 勿用 insert 写变量目录或映射(用下面两个工具)
**自然语言回复**只写:短确认、是否这个、建议补充 A/B/C、提问。不要复述 insert 全文。
### declare_variable — 跨轮真值
- key / type / initial / user_visible(用户是否可见)
- 只立必须跨轮记住的状态;派生文案不要立成可写变量
**分清两件事:**
- **正文组成(格式)**:insert → \`设计.正文组成\`(壳与分区,非故事)。
- **开场白(内容)**:insert → \`设计.开场白\`(按格式写的首屏);落档/开玩同步 \`输出.开场白\`。
### declare_map — 真值→投影上下文
- id / field(真值名)/ target_tag(\`上下文.*\` 或 \`大纲.*\`)/ bands[]
- bands:\`{min,max,content}\` 区间(min 含 max 不含),或 \`{value,content}\`,或 \`{when:"default",content}\`
- 槽位固定、内容随真值换档;旧档离开上下文
**自然语言回复**只写:短确认、是否这个、建议补充 A/B/C、提问。不要复述 tool 全文。
**分清:**
- 正文组成(格式)→ insert \`设计.正文组成\`
- 开场白(内容)→ insert \`设计.开场白\`;落档/开玩同步 \`输出.开场白\`
- 好感分档性格等 → declare_variable + declare_map,不要 insert 一整张表进设定散文
${recipeBlock}
## 按用户本轮意图择一为主
### A · 钉设定
1. insert 设定类 position(保留原意原词);order 偏小(靠前)。
2. 回复:确认体验(2~4 句 + 问是否这个)+ 建议补充(1~2 缺口,各 A/B/C)。
insert 设定类;order 偏小。回复:确认体验 + 建议补充。
### B · 钉正文格式
1. 必须 insert \`设计.正文组成\`(见规格);order 居中(约 0)。
2. 回复短说明已钉壳与主块。
必须 insert \`设计.正文组成\`。
### C · 生成开场白
1. 先有 \`设计.正文组成\`(否则先 B;缺设定先 A)。
2. insert \`设计.开场白\`:Markdown 主阅读;岔路用 \`## 岔路\`;勿写 【body】/【footer】。order 偏大(靠后)。
3. 回复只短说已写入;全文在 content。
先有正文组成;insert \`设计.开场白\`(Markdown;岔路用 \`## 岔路\`)。
## 常用 position 与默认序感
- 用户.需求、设计.美学纲领等设定:靠前(负)
- 设计.正文组成:中间(0 附近)
- 设计.开场白:靠后(正)
### D · 钉变量与映射
用户提到跨轮状态、分档态度、章大纲切换时:declare_variable(含是否对用户可见)→ declare_map(分档正文)。可同轮多次调用。
## 设计.正文组成 · 规格(格式)
JSON 即可。核心:\`正文.呈现壳.shell_id\`、\`正文.可见块[]\`(区域 body/footer 等)。
文本局默认 shell_id=\`prose\`;至少一块 body。要选项再声明 footer。
## 常用序感
- 需求/美学等不变设定:靠前(负)
- 正文组成:中间(0)
- 开场白:靠后(正)
## 设计.开场白 · 规格(内容)
按正文组成职责写满主阅读;Markdown;确认与建议不进 content。
## 设计.正文组成
JSON:\`正文.呈现壳.shell_id\`、\`正文.可见块[]\`。文本局默认 prose;至少一块 body。
## 其它
对话明显过长时 clear_dialogue(产物保留);否则不要清。
对话明显过长时 clear_dialogue(产物与变量/映射保留)。
`;
}
+84 -8
View File
@@ -1,8 +1,7 @@
import type { ToolDefinition } from "../llm/client.js";
/**
* 转述创作期工具:只允许用 toolcall 写产物。
* insert(position, content, order?) —— 打标记落盘并带相对序。
* Boss 直聘创作期工具:产物 / 变量 / 映射均须 toolcall;聊天只做确认与建议。
*/
export const DICTATE_TOOL_DEFINITIONS: ToolDefinition[] = [
{
@@ -10,24 +9,24 @@ export const DICTATE_TOOL_DEFINITIONS: ToolDefinition[] = [
function: {
name: "insert",
description:
"唯一写产物入口:把正文插入黑板。确认/建议/思考只写在普通回复,禁止放进 content。用 order 标相对先后以便拼装。",
"写入不变型固定产物(设定/格式/开场等)。确认与建议只写在普通回复。用 order 标相对先后。",
parameters: {
type: "object",
properties: {
position: {
type: "string",
description:
"插入位置(产物 tag)。须以「用户.」或「设计.」开头。例:用户.需求、设计.正文组成、设计.开场白",
"产物 tag,须以「用户.」或「设计.」开头。例:用户.需求、设计.正文组成、设计.开场白。勿用本工具写变量目录/映射。",
},
content: {
type: "string",
description:
"落入该位置的正文。设计.正文组成=格式 JSON;设计.开场白=可读开场(Markdown,用 ## 分段,勿写【body】、勿夹确认/建议)",
"落入该位置的正文。设计.正文组成=格式 JSON;设计.开场白=可读开场(Markdown)",
},
order: {
type: "number",
description:
"相对顺序(可负、0、正)。越小越靠前。原则:越常改越大(靠后);改动频率差不多时,越重要越小(靠前)。省略则沿用原序或按 tag 默认。",
"相对顺序(可负)。越小越靠前。越常改越大。省略则沿用原序或默认。",
},
},
required: ["position", "content"],
@@ -38,9 +37,86 @@ export const DICTATE_TOOL_DEFINITIONS: ToolDefinition[] = [
{
type: "function",
function: {
name: "clear_dialogue",
name: "declare_variable",
description:
"对话过长时清空对话记录。产物保留。清空后继续用自然语言回复用户。",
"声明或更新一个跨轮真值变量(写入设计.变量目录)。可见性、初值在此钉死;映射用 declare_map。",
parameters: {
type: "object",
properties: {
key: {
type: "string",
description: "字段名,如 好、章节、地点",
},
type: {
type: "string",
description: "number | string | boolean | enum",
},
initial: {
description: "初值(数字/字符串/布尔)",
},
user_visible: {
type: "boolean",
description: "是否对用户可见(监控栏等);默认 true",
},
note: {
type: "string",
description: "备注(可选)",
},
},
required: ["key"],
additionalProperties: false,
},
},
},
{
type: "function",
function: {
name: "declare_map",
description:
"声明真值→投影上下文的映射(写入设计.变量映射)。投影 tag 槽位固定,内容随真值换档。",
parameters: {
type: "object",
properties: {
id: {
type: "string",
description: "映射 id,如 affinity-attitude",
},
field: {
type: "string",
description: "依赖的真值字段名(须已 declare_variable)",
},
target_tag: {
type: "string",
description: "投影 tag,须以「上下文.」或「大纲.」开头,如 上下文.角色态度",
},
bands: {
type: "array",
description:
"分档:区间用 min(含)/max(不含)+content;精确用 value+content;兜底用 when=default+content",
items: {
type: "object",
properties: {
when: { type: "string", description: "range | eq | default;可省略,有 min/max 即区间" },
min: { type: "number" },
max: { type: "number" },
value: {},
content: { type: "string" },
},
required: ["content"],
},
},
note: { type: "string" },
},
required: ["id", "field", "target_tag", "bands"],
additionalProperties: false,
},
},
},
{
type: "function",
function: {
name: "clear_dialogue",
description: "对话过长时清空对话。产物与变量/映射保留。",
parameters: {
type: "object",
properties: {
+91 -14
View File
@@ -12,11 +12,28 @@ import {
type DictateChatTurn,
type DictateProduct,
} from "./types.js";
import {
VARIABLE_CATALOG_TAG,
parseVariableCatalog,
serializeVariableCatalog,
upsertVariableField,
type UpsertVariableInput,
} from "../skills/variable-catalog.js";
import {
VALUE_MAP_TAG,
parseValueMapDoc,
serializeValueMapDoc,
upsertValueMapEntry,
type UpsertMapInput,
} from "../skills/value-map.js";
export type DictateTurnHandlers = {
listProducts: () => DictateProduct[];
/** order 省略时由实现沿用原序或默认 */
writeProduct: (tag: string, content: string, order?: number) => void;
/** 读/写变量目录与映射(程序校验后的 JSON) */
readTag: (tag: string) => string | undefined;
writeTag: (tag: string, content: string) => void;
clearDialogue: () => void;
onToolCall?: (name: string, detail: string) => void;
onThinkingDelta?: (delta: string) => void;
@@ -31,8 +48,7 @@ export type DictateTurnResult = {
};
/**
* 跑一轮转述整理:上下文 = 预设提示 + 按相对序的产物 + 全量对话。
* 产物只许 insert(position, content, order?);最终文本即对用户回复。
* 跑一轮 Boss 直聘整理:上下文 = 预设提示 + 按相对序的产物 + 全量对话。
*/
export async function runDictateTurn(params: {
llm: LlmProvider;
@@ -59,7 +75,6 @@ export async function runDictateTurn(params: {
products: params.handlers.listProducts(),
dialogue,
});
// driver 会再前置 system;此处只传 user 拼装块,避免双重 system
const userBlock = assembled.filter((m) => m.role !== "system");
const run = await driver.run({
@@ -67,17 +82,20 @@ export async function runDictateTurn(params: {
messages: userBlock,
tools: DICTATE_TOOL_DEFINITIONS,
caller: "dictate_agent",
label: "转述整理",
label: "Boss直聘",
onThinkingDelta: params.handlers.onThinkingDelta,
onThinkingDone: params.handlers.onThinkingDone,
handleStep: (calls) => {
const results = calls.map((call) => {
const args = safeParseArgs(call.arguments);
if (call.name === "insert" || call.name === "write_product") {
const tag = String(
args.position ?? args.tag ?? "",
).trim();
const tag = String(args.position ?? args.tag ?? "").trim();
const content = 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);
@@ -103,6 +121,67 @@ export async function runDictateTurn(params: {
}),
};
}
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 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 === "clear_dialogue") {
const reason = String(args.reason ?? "").trim() || "上下文过长";
params.handlers.clearDialogue();
@@ -114,7 +193,7 @@ export async function runDictateTurn(params: {
content: JSON.stringify({
ok: true,
cleared: true,
note: "对话已清空;产物仍在。后续上下文以产物为准。",
note: "对话已清空;产物与变量/映射仍在。",
}),
};
}
@@ -126,8 +205,6 @@ export async function runDictateTurn(params: {
},
});
// driver 的 messages 在首轮已固定;clear 后若还要续写依赖模型已知工具结果。
// 若模型只调工具不说话,给一个兜底回复。
let reply = "";
if (run.stop.kind === "text") {
reply = run.stop.content.trim();
@@ -153,11 +230,11 @@ export async function runDictateTurn(params: {
function safeParseArgs(raw: string): Record<string, unknown> {
try {
const parsed = JSON.parse(raw || "{}");
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
return parsed as Record<string, unknown>;
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return {};
}
return parsed as Record<string, unknown>;
} catch {
/* ignore */
return {};
}
return {};
}
+1
View File
@@ -37,6 +37,7 @@ export function isAllowedProductTag(tag: string): boolean {
export function defaultDictateOrder(tag: string): number {
const t = tag.trim();
if (t === "用户.需求") return -40;
if (t === "设计.变量目录" || t === "设计.变量映射") return -25;
if (/美学|纲领|禁忌|示例/.test(t)) return -30;
if (/文风|叙事指南|篇幅|结构/.test(t)) return -20;
if (/正文组成|回复格式/.test(t)) return 0;