打通 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
+124
View File
@@ -0,0 +1,124 @@
import { describe, expect, it } from "vitest";
import { Blackboard } from "../src/blackboard/blackboard.js";
import {
buildDictateContextOrder,
bindDictateProductsToPlaySpec,
collectDictateBindProducts,
isDictateStyleBindTag,
} from "../src/dictate/play-bind.js";
import { DICTATE_ORDER_META_KEY } from "../src/dictate/types.js";
import { DIALOGUE_HISTORY_TAG, WORKER_PERSONA_REF } from "../src/skills/context-order.js";
import { defaultPlaySlots } from "../src/skills/play-slots.js";
describe("dictate play-bind", () => {
it("classifies style tags", () => {
expect(isDictateStyleBindTag("设计.美学纲领")).toBe(true);
expect(isDictateStyleBindTag("设计.正文组成")).toBe(true);
expect(isDictateStyleBindTag("用户.需求")).toBe(false);
});
it("builds gm context_order: persona → products → history → input", () => {
const order = buildDictateContextOrder({
products: [
{ tag: "用户.需求", content: "雨夜", order: -40 },
{ tag: "设计.美学纲领", content: "冷色", order: -30 },
{ tag: "设计.正文组成", content: "{}", order: 0 },
],
playSlots: defaultPlaySlots(),
});
const gm = order.slots.find((s) => s.ref === "world-simulator");
expect(gm).toBeTruthy();
const refs = gm!.inserts.map((i) => i.ref);
expect(refs[0]).toBe(WORKER_PERSONA_REF);
expect(refs).toContain("用户.需求");
expect(refs).toContain("设计.美学纲领");
expect(refs).toContain("设计.正文组成");
expect(refs).toContain(DIALOGUE_HISTORY_TAG);
expect(refs[refs.length - 1]).toBe("用户.最新输入");
const histIdx = refs.indexOf(DIALOGUE_HISTORY_TAG);
expect(refs.indexOf("用户.需求")).toBeLessThan(histIdx);
expect(refs.indexOf("设计.正文组成")).toBeLessThan(histIdx);
});
it("skips opening and worker-set tags; narrator only gets style", () => {
const board = new Blackboard();
board.write({
tag: "用户.需求",
content: "要压抑",
source: "dictate",
metadata: { [DICTATE_ORDER_META_KEY]: -40 },
});
board.write({
tag: "设计.美学纲领",
content: "冷色短句",
source: "dictate",
metadata: { [DICTATE_ORDER_META_KEY]: -30 },
});
board.write({
tag: "设计.开场白",
content: "雨落在檐上。",
source: "dictate",
});
board.write({
tag: "设计.worker集",
content: JSON.stringify({
play_slots: { gm: true, narrator: true, auditor: false, perspective: false },
}),
source: "test",
});
const products = collectDictateBindProducts(board);
expect(products.map((p) => p.tag)).toEqual(["用户.需求", "设计.美学纲领"]);
expect(products.every((p) => p.tag !== "设计.开场白")).toBe(true);
const result = bindDictateProductsToPlaySpec(board);
const narr = result.contextOrder.slots.find((s) => s.ref === "narrator");
expect(narr).toBeTruthy();
const narrRefs = narr!.inserts.map((i) => i.ref);
expect(narrRefs).toContain("设计.美学纲领");
expect(narrRefs).not.toContain("用户.需求");
expect(narrRefs).toContain("运行.本轮.裁决");
const gm = result.contextOrder.slots.find((s) => s.ref === "world-simulator");
expect(gm!.inserts.map((i) => i.ref)).toContain("用户.需求");
expect(result.workerSetJson).toContain("context_order");
});
it("does not invent variable mappings; binds declared map slots and catalog", () => {
const board = new Blackboard();
board.write({ tag: "用户.需求", content: "x", source: "dictate" });
const without = bindDictateProductsToPlaySpec(board);
const refsWithout = without.contextOrder.slots[0]!.inserts.map((i) => i.ref);
expect(refsWithout).not.toContain("变量.当前");
expect(refsWithout).not.toContain("上下文.角色态度");
board.write({
tag: "设计.变量目录",
content: JSON.stringify({
schema: "variable-catalog.v1",
fields: [{ key: "好感", type: "number", initial: 10, user_visible: true }],
}),
source: "test",
});
board.write({
tag: "设计.变量映射",
content: JSON.stringify({
schema: "value-map.v1",
maps: [
{
id: "aff",
field: "好感",
target_tag: "上下文.角色态度",
bands: [{ min: 0, content: "冷" }],
},
],
}),
source: "test",
});
const withMap = bindDictateProductsToPlaySpec(board);
const refs = withMap.contextOrder.slots[0]!.inserts.map((i) => i.ref);
expect(refs).toContain("变量.当前");
expect(refs).toContain("上下文.角色态度");
expect(refs).not.toContain("设计.变量映射");
});
});
+2 -1
View File
@@ -71,9 +71,10 @@ describe("dictate context", () => {
expect(prompt).toContain("设计.开场白");
expect(prompt).toContain("输出.开场白");
expect(prompt).toContain("insert");
expect(prompt).toContain("declare_variable");
expect(prompt).toContain("declare_map");
expect(prompt).toMatch(/order/);
expect(prompt).toContain("仅允许 toolcall");
expect(prompt).toContain("变化越频繁");
});
it("sorts products by relative order: neg then 0 then pos", () => {
+111
View File
@@ -4,6 +4,8 @@ import {
applyRejectedGenerationField,
classifyChatModel,
fetchWithGenerationCompat,
forceReasoningEffortNoneWhenTools,
isToolsReasoningEffortConflictError,
parseRejectedGenerationField,
sanitizeReasoningEffort,
sanitizeVerbosity,
@@ -102,6 +104,58 @@ describe("applyOpenAiGeneration", () => {
});
});
describe("forceReasoningEffortNoneWhenTools", () => {
it("forces none when tools are present", () => {
const body: Record<string, unknown> = {
tools: [{ type: "function", function: { name: "x" } }],
reasoning_effort: "high",
};
forceReasoningEffortNoneWhenTools(body);
expect(body.reasoning_effort).toBe("none");
});
it("sets none even when effort was omitted", () => {
const body: Record<string, unknown> = {
tools: [{ type: "function", function: { name: "x" } }],
};
forceReasoningEffortNoneWhenTools(body);
expect(body.reasoning_effort).toBe("none");
});
it("leaves body alone without tools", () => {
const body: Record<string, unknown> = { reasoning_effort: "high" };
forceReasoningEffortNoneWhenTools(body);
expect(body.reasoning_effort).toBe("high");
});
});
describe("isToolsReasoningEffortConflictError", () => {
it("detects gpt-5.6-luna style conflict", () => {
const text = JSON.stringify({
error: {
message:
"Provider API error: Function tools with reasoning_effort are not supported for gpt-5.6-luna in /v1/chat/completions. To use function tools, use /v1/responses or set reasoning_effort to 'none'.",
type: "invalid_request_error",
},
});
expect(isToolsReasoningEffortConflictError(text, 400)).toBe(true);
});
it("ignores unrelated 400s", () => {
expect(
isToolsReasoningEffortConflictError(
JSON.stringify({
error: {
message: "Unsupported parameter: temperature",
param: "temperature",
},
}),
400,
),
).toBe(false);
});
});
describe("parseRejectedGenerationField", () => {
it("reads OpenAI unsupported parameter errors", () => {
expect(
@@ -151,6 +205,16 @@ describe("applyRejectedGenerationField", () => {
expect(body.max_completion_tokens).toBeUndefined();
expect(body.max_tokens).toBeUndefined();
});
it("sets reasoning_effort to none when tools are present", () => {
const body: Record<string, unknown> = {
tools: [{ type: "function" }],
reasoning_effort: "high",
};
const tried = new Set<string>();
expect(applyRejectedGenerationField(body, "reasoning_effort", tried)).toBe(true);
expect(body.reasoning_effort).toBe("none");
});
});
describe("fetchWithGenerationCompat", () => {
@@ -177,6 +241,34 @@ describe("fetchWithGenerationCompat", () => {
expect(sent[0].temperature).toBe(1);
expect(sent[1].temperature).toBeUndefined();
});
it("retries tools+reasoning conflict by setting none", async () => {
const sent: Array<Record<string, unknown>> = [];
const response = await fetchWithGenerationCompat(async (body) => {
sent.push({ ...body });
if (body.reasoning_effort !== "none") {
return new Response(
JSON.stringify({
error: {
message:
"Function tools with reasoning_effort are not supported for gpt-5.6-luna. Set reasoning_effort to 'none'.",
},
}),
{ status: 400 },
);
}
return new Response(JSON.stringify({ ok: true }), { status: 200 });
}, {
model: "gpt-5.6-luna",
tools: [{ type: "function", function: { name: "run" } }],
messages: [],
});
expect(response.ok).toBe(true);
expect(sent).toHaveLength(2);
expect(sent[0].reasoning_effort).toBeUndefined();
expect(sent[1].reasoning_effort).toBe("none");
});
});
describe("buildRequestBody", () => {
@@ -234,6 +326,25 @@ describe("buildRequestBody", () => {
expect(body.reasoning_effort).toBe("low");
});
it("forces reasoning_effort none when tools are attached", () => {
const config: LlmConfig = {
baseUrl: "https://example.test/v1",
apiKey: "k",
model: "gpt-5.6-luna",
reasoningEffort: "high",
};
const body = buildRequestBody(config, messages, {
tools: [
{
type: "function",
function: { name: "run_worker", parameters: { type: "object" } },
},
],
});
expect(body.reasoning_effort).toBe("none");
expect(body.tools).toHaveLength(1);
});
it("does not forward top_k from an imported ST preset", () => {
const config: LlmConfig = {
baseUrl: "https://example.test/v1",
+152
View File
@@ -0,0 +1,152 @@
import { describe, expect, it } from "vitest";
import { Blackboard } from "../src/blackboard/blackboard.js";
import {
lookupValueMapContent,
parseValueMapDoc,
reprojectValueMaps,
serializeValueMapDoc,
upsertValueMapEntry,
VALUE_MAP_TAG,
} from "../src/skills/value-map.js";
import {
parseVariableCatalog,
seedVariablesFromCatalog,
serializeVariableCatalog,
upsertVariableField,
VARIABLE_CATALOG_TAG,
} from "../src/skills/variable-catalog.js";
import { stringifyTableDoc, createTableFromValues, parseTableDoc } from "../src/blackboard/table-cells.js";
describe("variable-catalog", () => {
it("upserts fields and seeds empty 变量.当前", () => {
let doc = null as ReturnType<typeof parseVariableCatalog>;
const a = upsertVariableField(doc, {
key: "好感",
type: "number",
initial: 10,
user_visible: true,
});
expect(a.error).toBeUndefined();
const b = upsertVariableField(a.doc, {
key: "地点",
type: "string",
initial: "旧港",
user_visible: false,
});
const raw = serializeVariableCatalog(b.doc);
const parsed = parseVariableCatalog(raw);
expect(parsed?.fields).toHaveLength(2);
expect(parsed?.fields.find((f) => f.key === "地点")?.user_visible).toBe(false);
const seeded = seedVariablesFromCatalog({
catalogRaw: raw,
currentRaw: null,
initialRaw: null,
});
expect(seeded.seededKeys).toEqual(expect.arrayContaining(["好感", "地点"]));
expect(seeded.current).toContain("好感");
expect(seeded.initial).toContain("旧港");
});
});
describe("value-map", () => {
it("looks up range bands (min inclusive, max exclusive)", () => {
const { doc } = upsertValueMapEntry(null, {
id: "aff",
field: "好感",
target_tag: "上下文.角色态度",
bands: [
{ min: 0, max: 30, content: "冷淡" },
{ min: 30, max: 60, content: "熟悉" },
{ min: 60, content: "恋爱" },
],
});
const entry = doc.maps[0]!;
expect(lookupValueMapContent(entry, 0)).toBe("冷淡");
expect(lookupValueMapContent(entry, 29)).toBe("冷淡");
expect(lookupValueMapContent(entry, 30)).toBe("熟悉");
expect(lookupValueMapContent(entry, 60)).toBe("恋爱");
});
it("reprojects target tag when 变量.当前 changes", () => {
const board = new Blackboard();
const { doc } = upsertValueMapEntry(null, {
id: "aff",
field: "好感",
target_tag: "上下文.角色态度",
bands: [
{ min: 0, max: 50, content: "档A" },
{ min: 50, content: "档B" },
],
});
board.write({
tag: VALUE_MAP_TAG,
content: serializeValueMapDoc(doc),
source: "test",
});
const tableRaw = stringifyTableDoc(
createTableFromValues({ ["好感"]: 20 }, "test"),
);
board.write({
tag: "变量.当前",
content: tableRaw,
source: "test",
});
expect(parseTableDoc(board.getContentByTag("变量.当前"))?.rows.map((r) => r.key)).toEqual([
"好感",
]);
const r1 = reprojectValueMaps({ blackboard: board, currentRaw: tableRaw });
expect(r1.skipped, JSON.stringify(r1)).toEqual([]);
expect(r1.written[0]?.tag).toBe("上下文.角色态度");
expect(board.getContentByTag("上下文.角色态度")).toBe("档A");
const tableRaw2 = stringifyTableDoc(
createTableFromValues({ ["好感"]: 80 }, "test"),
);
board.write({
tag: "变量.当前",
content: tableRaw2,
source: "test",
});
reprojectValueMaps({ blackboard: board, currentRaw: tableRaw2 });
expect(board.getContentByTag("上下文.角色态度")).toBe("档B");
});
it("rejects bad target_tag", () => {
const { error } = upsertValueMapEntry(null, {
id: "x",
field: "好感",
target_tag: "设计.美学纲领",
bands: [{ content: "no" }],
});
expect(error).toMatch(/上下文/);
});
it("round-trips parse", () => {
const raw = serializeValueMapDoc({
schema: "value-map.v1",
maps: [
{
id: "ch",
field: "章节",
target_tag: "大纲.当前章",
bands: [
{ when: "eq", value: 1, content: "第一章" },
{ when: "default", content: "未定" },
],
},
],
});
const parsed = parseValueMapDoc(raw);
expect(parsed?.maps[0]?.bands).toHaveLength(2);
expect(lookupValueMapContent(parsed!.maps[0]!, 1)).toBe("第一章");
expect(lookupValueMapContent(parsed!.maps[0]!, 9)).toBe("未定");
});
});
describe("catalog tag constants", () => {
it("uses stable design tags", () => {
expect(VARIABLE_CATALOG_TAG).toBe("设计.变量目录");
expect(VALUE_MAP_TAG).toBe("设计.变量映射");
});
});