Add capability-probed structured outputs and a prototype/instance creation graph.

Probe API profiles for json_schema/tools, flatten worker artifacts onto a strict schema, and let users spawn or revise graph nodes instead of a one-shot DAG.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-31 01:57:22 +08:00
co-authored by Cursor
parent 872f742d3e
commit bd5d88abfa
65 changed files with 7625 additions and 942 deletions
+134
View File
@@ -0,0 +1,134 @@
import { describe, expect, it } from "vitest";
import {
CAPABILITY_PROBE_REASONING_EFFORT,
resolveStructuredDeliveryMode,
type ProfileCapabilities,
} from "../src/llm/capabilities.js";
import { normalizeProfileReasoningEffort } from "../src/config/api-profiles.js";
import {
coerceToFlatArtifact,
flatArtifactToFragmentDoc,
FLAT_ARTIFACT_SCHEMA_ID,
} from "../src/skills/flat-artifact.js";
describe("capability probe defaults", () => {
it("always probes with low reasoning effort", () => {
expect(CAPABILITY_PROBE_REASONING_EFFORT).toBe("low");
});
});
describe("normalizeProfileReasoningEffort", () => {
it("keeps valid effort and drops auto/empty", () => {
expect(normalizeProfileReasoningEffort("low")).toBe("low");
expect(normalizeProfileReasoningEffort("MAX")).toBe("max");
expect(normalizeProfileReasoningEffort("auto")).toBeUndefined();
expect(normalizeProfileReasoningEffort("")).toBeUndefined();
expect(normalizeProfileReasoningEffort("nope")).toBeUndefined();
});
});
describe("resolveStructuredDeliveryMode", () => {
it("prefers json_schema then forced_tool then json_object", () => {
const base: ProfileCapabilities = {
chat: "ok",
jsonObject: "ok",
jsonSchema: "fail",
tools: "ok",
forcedTool: "ok",
strictTool: "fail",
};
expect(resolveStructuredDeliveryMode(base)).toBe("forced_tool");
expect(
resolveStructuredDeliveryMode({ ...base, jsonSchema: "ok" }),
).toBe("json_schema");
expect(
resolveStructuredDeliveryMode({
...base,
jsonSchema: "fail",
forcedTool: "fail",
}),
).toBe("json_object");
});
});
describe("flat artifact", () => {
it("round-trips deep fragment body into sections", () => {
const flat = coerceToFlatArtifact({
schema: "context-fragment.v1",
技能: "美学纲领与交互范式",
brief: "点题",
mount: ["narrator"],
稳变: "stable",
正文: {
设定逻辑: { 变造: "丧尸" },
美学纲领: "出租屋",
},
开放问题: ["后宫"],
追问: {
题目: [{ 问: "身份?", 建议选项: ["幸存者", "其他"] }],
},
});
expect(flat?.sections.map((s) => s.id)).toEqual(["设定逻辑", "美学纲领"]);
const frag = flatArtifactToFragmentDoc(flat!);
expect(frag.schema).toBe("context-fragment.v1");
expect(frag.brief).toBe("点题");
expect((frag.追问 as { 题目: unknown[] }).题目[0]).toMatchObject({
问: "身份?",
});
});
it("does not coerce a creation-flow DAG (brief + steps) into a flat artifact", () => {
const flow = {
brief: "单角代入",
status: "open",
steps: [{ name: "美学纲领与交互范式", depends_on: [] }],
};
expect(coerceToFlatArtifact(flow)).toBeNull();
});
it("does not coerce a brief-only object into an empty flat artifact", () => {
expect(coerceToFlatArtifact({ brief: "点题" })).toBeNull();
});
it("accepts native flat wire format", () => {
const flat = coerceToFlatArtifact({
schema: FLAT_ARTIFACT_SCHEMA_ID,
skill: "x",
brief: "b",
mount: [],
stability: "semi",
sections: [{ id: "a", title: "A", text: "hello" }],
open_questions: [],
questions: [],
summary: "s",
});
expect(flat?.sections[0]?.text).toBe("hello");
});
it("round-trips nested body objects through flat stringify + fragment parse", () => {
const nested = {
schema: "context-fragment.v1",
技能: "美学纲领与交互范式",
brief: "点题",
mount: [],
正文: {
交互范式: { 前置配置: { 用户角色: "唯一免疫者" } },
美学纲领: { 体验内核: "报复爽", 呈现要点: "内外对比" },
设定逻辑: { 变造: { 原型: "丧尸" } },
},
开放问题: [],
追问: { 题目: [] },
};
const flat = coerceToFlatArtifact(nested);
expect(flat).not.toBeNull();
const frag = flatArtifactToFragmentDoc(flat!);
expect(frag.技能).toBe("美学纲领与交互范式");
expect((frag.正文 as Record<string, unknown>).交互范式).toEqual({
前置配置: { 用户角色: "唯一免疫者" },
});
expect(
((frag.正文 as Record<string, unknown>).美学纲领 as Record<string, unknown>)
.体验内核,
).toBe("报复爽");
});
});
+23
View File
@@ -48,6 +48,29 @@ describe("pruneContextTraces", () => {
expect(pruned.map((m) => m.id)).toEqual(["a", "b"]);
});
it("pins design-flow traces while pruning others", () => {
const messages = [
{
id: "flow",
contextTrace: buildContextTrace({
caller: "worker:design-flow",
messages: [{ role: "user", content: "plan" }],
}),
},
msg("1", true),
msg("2", true),
msg("3", true),
];
const pruned = pruneContextTraces(messages, 1);
expect(pruned[0]?.contextTrace?.caller).toBe("worker:design-flow");
expect(pruned.map((m) => Boolean(m.contextTrace))).toEqual([
true,
false,
false,
true,
]);
});
it("is a no-op when under the limit", () => {
const messages = [msg("1", true), msg("2", true)];
const pruned = pruneContextTraces(messages, 5);
+393 -34
View File
@@ -6,6 +6,8 @@ import {
artifactTagForStep,
extractModuleOpening,
formatCreationFlowForUser,
deriveFlowStepTitle,
titleFromSuggestion,
formatFlowProgressForAgent,
formatModuleCatalogForAgent,
formatRecipeCatalogForAgent,
@@ -18,6 +20,13 @@ import {
mergeCreationFlowPreservingAccepted,
needsFlowExpansion,
nextPendingStep,
listReadySteps,
computeStepLayers,
spawnRepeatableCreationStep,
spawnInstanceFromPrototype,
hasSelectableCreationWork,
removeUnstartedInstance,
looksLikeCreationFlowDoc,
parseCreationFlow,
pickRecordedStepId,
parseModuleCatalog,
@@ -29,6 +38,7 @@ import {
shouldSkipModuleOpening,
isProgressPointerTag,
stringifyCreationFlow,
stripReviseSteps,
creationFlowFromRecipeSeed,
validateCreationFlow,
validateStepReadyToRun,
@@ -51,6 +61,10 @@ modules:
declaration: 锚定实例
artifact: 设计.具体实例
repeatable: true
- name: 开场白与开场变量
declaration: 收口
artifact: 设计.开场白与开场变量
closer: true
`)!;
describe("creation-flow", () => {
@@ -145,7 +159,44 @@ describe("creation-flow", () => {
acceptedStepIds: ["美学纲领与交互范式"],
});
expect(merged.restored).toEqual([]);
expect(merged.raw).toBe(rewritten);
expect(parseCreationFlow(merged.raw)!.steps.map((s) => s.id)).toEqual([
"美学纲领与交互范式",
"实现机制",
]);
});
it("drops planner revise steps when merging flow", () => {
const prev = `{
"status": "open",
"steps": [
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] }
]
}`;
const rewritten = `{
"status": "open",
"steps": [
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] },
{
"id": "美学纲领与交互范式·改",
"name": "美学纲领与交互范式",
"mode": "revise",
"revises": "美学纲领与交互范式",
"depends_on": ["美学纲领与交互范式"]
},
{ "id": "实现机制", "name": "实现机制", "depends_on": ["美学纲领与交互范式·改"] }
]
}`;
const merged = mergeCreationFlowPreservingAccepted({
prevRaw: prev,
nextRaw: rewritten,
acceptedStepIds: ["美学纲领与交互范式"],
});
const flow = parseCreationFlow(merged.raw)!;
expect(flow.steps.map((s) => s.id)).toEqual([
"美学纲领与交互范式",
"实现机制",
]);
expect(flow.steps[1]?.depends_on).toEqual(["美学纲领与交互范式"]);
});
it("allows duplicate capability names with distinct ids", () => {
@@ -185,6 +236,24 @@ describe("creation-flow", () => {
expect(flow?.steps[0]?.name).toBe("美学纲领与交互范式");
});
it("extracts JSON from a code fence", () => {
const flow = parseCreationFlow(
"```json\n{\"brief\":\"单角代入\",\"steps\":[{\"name\":\"美学纲领与交互范式\",\"depends_on\":[]}]}\n```",
);
expect(flow?.brief).toBe("单角代入");
expect(flow?.steps[0]?.name).toBe("美学纲领与交互范式");
});
it("recognizes a DAG object and ignores brief-only objects", () => {
expect(
looksLikeCreationFlowDoc({
brief: "单角代入",
steps: [{ name: "美学纲领与交互范式", depends_on: [] }],
}),
).toBe(true);
expect(looksLikeCreationFlowDoc({ brief: "单角代入" })).toBe(false);
});
it("validates deps must appear earlier", () => {
const bad = parseCreationFlow(`{
"steps": [
@@ -250,13 +319,93 @@ describe("creation-flow", () => {
depends_on: [],
declaration: "站位与体验契约",
});
expect(view.steps[2]?.occurrence).toBe(2);
expect(view.steps[2]?.role).toBe("instance");
expect(view.steps[2]?.occurrence).toBe(1);
expect(view.steps[1]?.role).toBe("prototype");
expect(view.steps[2]?.repeatable).toBe(true);
expect(view.steps[0]?.runState).toBe("current");
expect(view.steps[0]?.runState).toBe("pending");
expect(view.steps[0]?.layer).toBe(0);
expect(view.steps[0]?.ready).toBe(true);
expect(view.steps[0]?.selectable).toBe(true);
expect(view.steps[1]?.runState).toBe("pending");
expect(view.steps[1]?.layer).toBe(1);
expect(view.steps[1]?.ready).toBe(false);
expect(view.steps[2]?.runState).toBe("pending");
});
it("shows a short title for 生成规则 / 具体实例 prototypes and instances", () => {
const flow = parseCreationFlow(`{
"steps": [
{
"id": "生成规则",
"name": "生成规则",
"role": "prototype",
"suggestion": "幸存者生成规则:锚定出租屋内的女性租客",
"depends_on": []
},
{
"id": "生成规则#1",
"name": "生成规则",
"role": "instance",
"from": "生成规则",
"depends_on": []
},
{
"id": "具体实例",
"name": "具体实例",
"role": "prototype",
"suggestion": "按幸存者规则生成开局女租客",
"depends_on": ["生成规则"]
},
{
"id": "具体实例#1",
"name": "具体实例",
"role": "instance",
"from": "具体实例",
"params": { "batch_goal": "开局女租客", "rule_id": "survivors" },
"depends_on": ["生成规则#1"]
}
]
}`)!;
const untitled = formatCreationFlowForUser(flow, sampleCatalog);
expect(untitled.steps.find((s) => s.id === "生成规则")?.title).toBe(
"幸存者生成规则",
);
expect(untitled.steps.find((s) => s.id === "生成规则#1")?.title).toBe(
"幸存者生成规则",
);
expect(untitled.steps.find((s) => s.id === "具体实例")?.title).toBe(
"按幸存者规则生成开局女租客",
);
expect(untitled.steps.find((s) => s.id === "具体实例#1")?.title).toBe(
"开局女租客",
);
const accepted = formatCreationFlowForUser(flow, sampleCatalog, [], [], {
"生成规则#1": { summary: "生成规则 · 幸存者 · 预生成并动态" },
"具体实例#1": { summary: "具体实例 · 女租客 · 2条" },
});
expect(accepted.steps.find((s) => s.id === "生成规则#1")?.title).toBe(
"幸存者",
);
expect(accepted.steps.find((s) => s.id === "具体实例#1")?.title).toBe(
"女租客",
);
expect(
deriveFlowStepTitle({
name: "生成规则",
hint: {
content: {
正文: { 本步参数: { target: "女租客" } },
},
},
}),
).toBe("女租客");
expect(titleFromSuggestion("怪物生成规则:按族群抽样")).toBe(
"怪物生成规则",
);
});
it("marks accepted steps done and the next pending current", () => {
const flow = parseCreationFlow(`{
"brief": "网恋回合",
@@ -272,9 +421,10 @@ describe("creation-flow", () => {
]);
expect(view.steps.map((s) => s.runState)).toEqual([
"done",
"current",
"pending",
"pending",
]);
expect(view.steps[1]?.selectable).toBe(true);
});
it("loads world-simulator catalog and formats for agent", async () => {
@@ -317,7 +467,7 @@ describe("creation-flow", () => {
expect(block).toContain("上下文投影排序〔程序步〕");
expect(block).toContain("何时用");
expect(block).toContain("何时不用");
expect(block).toContain("规划产物字段");
expect(block).toContain("role=prototype");
expect(block).toContain("编排参数");
expect(block).not.toContain("设计.美学纲领与交互范式");
});
@@ -403,6 +553,22 @@ steps:
).toBeNull();
});
it("creationFlowFromRecipeSeed follows the recipe steps, not a fixed first node", () => {
const detail = parseRecipeYaml(
`
when: 其它剧本
steps:
- name: 舞台骨架
depends_on: []
- name: 实现机制
depends_on: ["舞台骨架"]
`,
{ id: "other", name: "其它", declaration: "测" },
);
const seeded = creationFlowFromRecipeSeed(detail);
expect(seeded?.steps.map((s) => s.name)).toEqual(["舞台骨架", "实现机制"]);
});
it("falls back to legacy hint when methodology absent", () => {
const detail = parseRecipeYaml(
`
@@ -478,7 +644,7 @@ recipes:
expect(loaded.promptBody).toContain("美学纲领与交互范式");
expect(loaded.promptBody).toContain("已验收");
expect(loaded.promptBody).toContain("禁止再排新建");
expect(loaded.promptBody).toContain("mode=revise");
expect(loaded.promptBody).toContain("禁止排 mode=revise");
expect(loaded.promptBody).toContain("生成规则");
});
@@ -496,7 +662,36 @@ recipes:
expect(text).toContain("草案已有、尚未验收");
expect(text).toContain("美学纲领与交互范式");
expect(text).toContain("不要当作新规划再写一遍");
expect(text).toContain("可反复追加:生成规则、具体实例");
expect(text).toContain("目录〔可反复〕:生成规则、具体实例");
});
it("tells design-flow to insert nodes before an unaccepted closer", () => {
const catalog = parseModuleCatalog(`
modules:
- name: 美学纲领与交互范式
declaration: 站位
artifact: 设计.美学纲领与交互范式
- name: 开场白与开场变量
declaration: 收口
artifact: 设计.开场白与开场变量
closer: true
`)!;
const flow = parseCreationFlow(`{
"status": "closed",
"steps": [
{ "name": "美学纲领与交互范式", "depends_on": [] },
{ "name": "开场白与开场变量", "depends_on": ["美学纲领与交互范式"] }
]
}`)!;
const text = formatFlowProgressForAgent({
flow,
acceptedStepIds: ["美学纲领与交互范式"],
catalog,
});
expect(text).toContain("〔收口〕");
expect(text).toContain("开场白与开场变量");
expect(text).toContain("插在收口之前");
expect(text).toContain("status 改回 open");
});
it("injects aesthetics-interaction module prompt into design-step", async () => {
@@ -637,7 +832,7 @@ recipes:
expect(missingView.steps[0]?.paramsMissing).toBeUndefined();
const block = formatModuleCatalogForAgent(catalog!);
expect(block).toContain("规划产物字段");
expect(block).toContain("role=prototype");
expect(block).toContain("target");
});
@@ -662,8 +857,7 @@ recipes:
acceptedStepNames: [],
},
);
expect(loaded.promptBody).toContain("【规划产物】");
expect(loaded.promptBody).not.toContain("勿再问「生成什么");
expect(loaded.promptBody).toContain("【本步对象】");
expect(loaded.promptBody).toContain("target: 怪物");
expect(loaded.promptBody).toContain("rule_id: monsters");
expect(loaded.worker.outputTags).toContain("设计.生成规则");
@@ -693,14 +887,22 @@ modules:
undefined,
{
flowRaw: JSON.stringify({
steps: [{ id: "生成规则", name: "生成规则", depends_on: [] }],
steps: [
{
id: "生成规则#1",
name: "生成规则",
role: "instance",
from: "生成规则",
depends_on: [],
},
],
}),
currentStepName: "生成规则",
currentStepName: "生成规则#1",
acceptedStepNames: [],
},
);
expect(loaded.promptBody).toContain("【规划产物】");
expect(loaded.promptBody).toContain("尚未规划本步具体写什么");
expect(loaded.promptBody).toContain("【本步对象】");
expect(loaded.promptBody).toContain("尚未钉本步具体写什么");
});
it("nextPendingStep respects deps and accepted by id", () => {
@@ -780,7 +982,7 @@ modules:
expect(isCreationFlowComplete(legacy, ["美学纲领与交互范式"])).toBe(true);
});
it("allows revising a non-repeatable skill and infers revises + id", () => {
it("parses legacy revise steps then strips them from the user graph", () => {
const flow = parseCreationFlow(`{
"status": "open",
"steps": [
@@ -795,10 +997,35 @@ modules:
revises: "实现机制",
});
expect(validateCreationFlow(flow, sampleCatalog).ok).toBe(true);
const stripped = stripReviseSteps(flow);
expect(stripped.steps.map((s) => s.id)).toEqual(["实现机制"]);
const view = formatCreationFlowForUser(flow, sampleCatalog, ["实现机制"]);
expect(view.steps[1]?.mode).toBe("revise");
expect(view.steps[1]?.occurrence).toBeUndefined();
expect(view.steps[1]?.runState).toBe("current");
expect(view.steps).toHaveLength(1);
expect(view.steps[0]?.id).toBe("实现机制");
expect(view.steps[0]?.runState).toBe("done");
expect(view.steps[0]?.mode).toBeUndefined();
});
it("stripReviseSteps remaps dependents back to the origin step", () => {
const flow = parseCreationFlow(`{
"steps": [
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] },
{
"id": "美学纲领与交互范式·改",
"name": "美学纲领与交互范式",
"mode": "revise",
"revises": "美学纲领与交互范式",
"depends_on": ["美学纲领与交互范式"]
},
{ "id": "正文组成", "name": "实现机制", "depends_on": ["美学纲领与交互范式·改"] }
]
}`)!;
const stripped = stripReviseSteps(flow);
expect(stripped.steps.map((s) => s.id)).toEqual([
"美学纲领与交互范式",
"正文组成",
]);
expect(stripped.steps[1]?.depends_on).toEqual(["美学纲领与交互范式"]);
});
it("still rejects two fresh copies of a non-repeatable skill", () => {
@@ -849,7 +1076,7 @@ modules:
expect(validateCreationFlow(flow, sampleCatalog).ok).toBe(true);
});
it("nextPendingStep prefers a ready revise over a later fresh step", () => {
it("nextPendingStep ignores orchestrated revise steps", () => {
const flow = parseCreationFlow(`{
"status": "open",
"steps": [
@@ -863,31 +1090,26 @@ modules:
}
]
}`)!;
expect(nextPendingStep(flow, ["美学纲领与交互范式"])?.mode).toBe("revise");
expect(nextPendingStep(flow, ["美学纲领与交互范式"])?.id).toBe(
"美学纲领与交互范式·改",
);
expect(nextPendingStep(flow, ["美学纲领与交互范式"])?.mode).toBeUndefined();
expect(nextPendingStep(flow, ["美学纲领与交互范式"])?.id).toBe("生成规则");
expect(
listReadySteps(flow, ["美学纲领与交互范式"]).map((s) => s.id),
).toEqual(["生成规则"]);
});
it("injects inherit draft and skips opening on a revise design-step", async () => {
it("inherits existing artifact when reentering an accepted step", async () => {
const flowRaw = JSON.stringify({
status: "open",
steps: [
{ id: "美学纲领与交互范式", name: "美学纲领与交互范式", depends_on: [] },
{
id: "美学纲领与交互范式·改",
name: "美学纲领与交互范式",
mode: "revise",
revises: "美学纲领与交互范式",
depends_on: ["美学纲领与交互范式"],
},
],
});
const binding = await resolveDesignStepBinding({
skillPackRoot: "dialogue/world-simulator",
flowRaw,
currentStepName: "美学纲领与交互范式·改",
currentStepName: "美学纲领与交互范式",
acceptedStepNames: ["美学纲领与交互范式"],
inheritExisting: true,
});
expect(binding?.opening).toBeNull();
expect(binding?.inheritTag).toBe("设计.美学纲领与交互范式");
@@ -898,16 +1120,153 @@ modules:
undefined,
{
flowRaw,
currentStepName: "美学纲领与交互范式·改",
currentStepName: "美学纲领与交互范式",
acceptedStepNames: ["美学纲领与交互范式"],
inheritExisting: true,
},
);
expect(loaded.promptBody).toContain("【回头修改】");
expect(loaded.promptBody).not.toContain("程序开场");
expect(loaded.worker.name).toContain("回头修改");
expect(loaded.worker.inputTags).toContain("设计.美学纲领与交互范式");
});
it("layers follow longest dependency path", () => {
const flow = parseCreationFlow(`{
"steps": [
{ "name": "美学纲领与交互范式", "depends_on": [] },
{ "name": "生成规则", "depends_on": ["美学纲领与交互范式"] },
{ "name": "具体实例", "depends_on": ["生成规则"] },
{ "name": "开场白与开场变量", "depends_on": ["美学纲领与交互范式", "具体实例"] }
]
}`)!;
const layers = computeStepLayers(flow);
expect(layers.get("美学纲领与交互范式")).toBe(0);
expect(layers.get("生成规则")).toBe(1);
expect(layers.get("具体实例")).toBe(2);
expect(layers.get("开场白与开场变量")).toBe(3);
});
it("listReadySteps returns every unblocked pending step", () => {
const flow = parseCreationFlow(`{
"steps": [
{ "name": "美学纲领与交互范式", "depends_on": [] },
{ "name": "生成规则", "depends_on": ["美学纲领与交互范式"] },
{ "id": "实现机制", "name": "实现机制", "depends_on": ["美学纲领与交互范式"] }
]
}`)!;
expect(listReadySteps(flow, []).map((s) => s.id)).toEqual([
"美学纲领与交互范式",
]);
expect(
loaded.worker.contextSegments?.some((s) => s.id === "inherit-existing"),
listReadySteps(flow, ["美学纲领与交互范式"]).map((s) => s.id),
).toEqual(["生成规则", "实现机制"]);
});
it("spawns instance from prototype before closer and hangs instance on closer deps", () => {
const flow = parseCreationFlow(`{
"status": "open",
"steps": [
{ "name": "美学纲领与交互范式", "depends_on": [] },
{ "name": "开场白与开场变量", "depends_on": ["美学纲领与交互范式"] }
]
}`)!;
expect(
hasSelectableCreationWork(flow, sampleCatalog, ["美学纲领与交互范式"]),
).toBe(true);
const spawned = spawnRepeatableCreationStep({
flow,
catalog: sampleCatalog,
moduleName: "生成规则",
acceptedStepIds: ["美学纲领与交互范式"],
});
expect("error" in spawned).toBe(false);
if ("error" in spawned) return;
expect(spawned.step.id).toBe("生成规则#1");
expect(spawned.step.role).toBe("instance");
expect(spawned.step.depends_on).toEqual(["美学纲领与交互范式"]);
expect(spawned.flow.steps.map((s) => s.id)).toEqual([
"美学纲领与交互范式",
"生成规则",
"生成规则#1",
"开场白与开场变量",
]);
expect(
spawned.flow.steps.find((s) => s.name === "开场白与开场变量")?.depends_on,
).toEqual(["美学纲领与交互范式", "生成规则#1"]);
const second = spawnRepeatableCreationStep({
flow: spawned.flow,
catalog: sampleCatalog,
moduleName: "生成规则",
acceptedStepIds: ["美学纲领与交互范式"],
});
expect("error" in second).toBe(false);
if ("error" in second) return;
expect(second.step.id).toBe("生成规则#2");
});
it("removeUnstartedInstance drops empty spawn and closer dep", () => {
const flow = parseCreationFlow(`{
"status": "open",
"steps": [
{ "name": "美学纲领与交互范式", "depends_on": [] },
{ "id": "生成规则", "name": "生成规则", "role": "prototype", "depends_on": ["美学纲领与交互范式"] },
{ "id": "生成规则#1", "name": "生成规则", "role": "instance", "from": "生成规则", "depends_on": ["美学纲领与交互范式"] },
{ "name": "开场白与开场变量", "depends_on": ["美学纲领与交互范式", "生成规则#1"] }
]
}`)!;
const next = removeUnstartedInstance(flow, "生成规则#1", []);
expect(next.steps.map((s) => s.id)).toEqual([
"美学纲领与交互范式",
"生成规则",
"开场白与开场变量",
]);
expect(
next.steps.find((s) => s.name === "开场白与开场变量")?.depends_on,
).toEqual(["美学纲领与交互范式"]);
const kept = removeUnstartedInstance(next, "美学纲领与交互范式", []);
expect(kept.steps.map((s) => s.id)).toEqual(next.steps.map((s) => s.id));
const accepted = removeUnstartedInstance(flow, "生成规则#1", ["生成规则#1"]);
expect(accepted.steps.some((s) => s.id === "生成规则#1")).toBe(true);
});
it("blocks 具体实例 spawn until a 生成规则 instance is accepted", () => {
const flow = parseCreationFlow(`{
"steps": [
{ "name": "美学纲领与交互范式", "depends_on": [] },
{
"id": "生成规则",
"name": "生成规则",
"role": "prototype",
"depends_on": ["美学纲领与交互范式"]
}
]
}`)!;
const blocked = spawnRepeatableCreationStep({
flow,
catalog: sampleCatalog,
moduleName: "具体实例",
acceptedStepIds: ["美学纲领与交互范式"],
});
expect(blocked).toMatchObject({ error: "先完成一条生成规则" });
const ruleSpawn = spawnInstanceFromPrototype({
flow,
catalog: sampleCatalog,
prototypeId: "生成规则",
acceptedStepIds: ["美学纲领与交互范式"],
});
expect("error" in ruleSpawn).toBe(false);
if ("error" in ruleSpawn) return;
const withRule = ruleSpawn.flow;
const ok = spawnRepeatableCreationStep({
flow: withRule,
catalog: sampleCatalog,
moduleName: "具体实例",
acceptedStepIds: ["美学纲领与交互范式", "生成规则#1"],
});
expect("error" in ok).toBe(false);
if ("error" in ok) return;
expect(ok.step.depends_on).toEqual(["生成规则#1"]);
});
});
+10 -1
View File
@@ -8,12 +8,21 @@ import {
import { parseWorkerSetYaml } from "../src/skills/worker-set-parse.js";
describe("resolveAcceptanceModeForWorker", () => {
it("always confirms design-flow", () => {
it("auto-continues design-flow without a confirm step", () => {
const mode = resolveAcceptanceModeForWorker({
session: createSession(),
blackboard: new Blackboard(),
workerId: "design-flow",
});
expect(mode).toBe("no_confirmation");
});
it("still confirms design-step", () => {
const mode = resolveAcceptanceModeForWorker({
session: createSession(),
blackboard: new Blackboard(),
workerId: "design-step",
});
expect(mode).toBe("user_confirmed");
});
+22
View File
@@ -3,6 +3,7 @@ import {
displayStageLabel,
displayWorkerLabel,
formatWorkerDisplayTitle,
isFlowPlanReview,
reviewComposerCopy,
} from "../src/server/display-labels.js";
@@ -52,4 +53,25 @@ describe("display-labels", () => {
expect(setup.acceptLabel).toBe("选定此开场");
expect(setup.kind).toBe("opening");
});
it("treats design-flow review as the pick-node surface", () => {
expect(
isFlowPlanReview({
waitingReason: { kind: "review_artifact" },
reviewArtifact: { workerId: "design-flow" },
}),
).toBe(true);
expect(
isFlowPlanReview({
waitingReason: { kind: "review_artifact" },
reviewArtifact: { workerId: "design-step" },
}),
).toBe(false);
expect(
isFlowPlanReview({
waitingReason: { kind: "pick_creation_step" },
reviewArtifact: { workerId: "design-flow" },
}),
).toBe(false);
});
});
+249
View File
@@ -0,0 +1,249 @@
import { describe, expect, it } from "vitest";
import {
applyOpenAiGeneration,
applyRejectedGenerationField,
classifyChatModel,
fetchWithGenerationCompat,
parseRejectedGenerationField,
sanitizeReasoningEffort,
sanitizeVerbosity,
} from "../src/llm/generation-compat.js";
import { buildRequestBody } from "../src/llm/client.js";
import type { LlmConfig } from "../src/config/env.js";
const stGeneration = {
temperature: 1,
topP: 0.95,
topK: 40,
minP: 0.05,
frequencyPenalty: 0,
presencePenalty: 0,
repetitionPenalty: 1.05,
maxOutputTokens: 60000,
seed: -1,
reasoningEffort: "auto",
verbosity: "auto",
};
describe("sanitizeReasoningEffort", () => {
it("omits SillyTavern auto and empty values", () => {
expect(sanitizeReasoningEffort("auto")).toBeUndefined();
expect(sanitizeReasoningEffort("AUTO")).toBeUndefined();
expect(sanitizeReasoningEffort("")).toBeUndefined();
expect(sanitizeReasoningEffort(" ")).toBeUndefined();
expect(sanitizeReasoningEffort(undefined)).toBeUndefined();
});
it("keeps values the provider enum accepts", () => {
expect(sanitizeReasoningEffort("none")).toBe("none");
expect(sanitizeReasoningEffort("minimal")).toBe("minimal");
expect(sanitizeReasoningEffort("low")).toBe("low");
expect(sanitizeReasoningEffort("medium")).toBe("medium");
expect(sanitizeReasoningEffort("high")).toBe("high");
expect(sanitizeReasoningEffort("xhigh")).toBe("xhigh");
expect(sanitizeReasoningEffort("max")).toBe("max");
});
it("maps SillyTavern min to minimal and ignores unknown values", () => {
expect(sanitizeReasoningEffort("min")).toBe("minimal");
expect(sanitizeReasoningEffort("MIN")).toBe("minimal");
expect(sanitizeReasoningEffort("unknown")).toBeUndefined();
});
});
describe("sanitizeVerbosity", () => {
it("omits auto and unknown values", () => {
expect(sanitizeVerbosity("auto")).toBeUndefined();
expect(sanitizeVerbosity("loud")).toBeUndefined();
expect(sanitizeVerbosity("medium")).toBe("medium");
});
});
describe("classifyChatModel", () => {
it("treats o-series, gpt-5, kimi-k2.5 and deepseek-reasoner as no-sampling", () => {
expect(classifyChatModel("o3-mini").omitSampling).toBe(true);
expect(classifyChatModel("openai/o4-mini").omitSampling).toBe(true);
expect(classifyChatModel("gpt-5").omitSampling).toBe(true);
expect(classifyChatModel("deepseek-reasoner").omitSampling).toBe(true);
expect(classifyChatModel("kimi-k2.5").omitSampling).toBe(true);
});
it("keeps sampling for ordinary chat models", () => {
expect(classifyChatModel("gpt-4o").omitSampling).toBe(false);
expect(classifyChatModel("deepseek-v4-pro").omitSampling).toBe(false);
expect(classifyChatModel("gpt-5-chat-latest").omitSampling).toBe(false);
});
});
describe("applyOpenAiGeneration", () => {
it("does not send ST extras or auto sentinels on a chat model", () => {
const body: Record<string, unknown> = { model: "deepseek-v4-pro" };
applyOpenAiGeneration(body, stGeneration, "deepseek-v4-pro");
expect(body.temperature).toBe(1);
expect(body.top_p).toBe(0.95);
expect(body.max_tokens).toBe(60000);
expect(body.top_k).toBeUndefined();
expect(body.min_p).toBeUndefined();
expect(body.repetition_penalty).toBeUndefined();
expect(body.reasoning_effort).toBeUndefined();
expect(body.verbosity).toBeUndefined();
expect(body.seed).toBeUndefined();
});
it("drops sampling params for reasoning models and uses max_completion_tokens", () => {
const body: Record<string, unknown> = { model: "o3-mini" };
applyOpenAiGeneration(body, stGeneration, "o3-mini");
expect(body.temperature).toBeUndefined();
expect(body.top_p).toBeUndefined();
expect(body.frequency_penalty).toBeUndefined();
expect(body.presence_penalty).toBeUndefined();
expect(body.max_tokens).toBeUndefined();
expect(body.max_completion_tokens).toBe(60000);
});
});
describe("parseRejectedGenerationField", () => {
it("reads OpenAI unsupported parameter errors", () => {
expect(
parseRejectedGenerationField(
JSON.stringify({
error: {
message: "Unsupported parameter: 'temperature' is not supported with this model.",
param: "temperature",
},
}),
),
).toBe("temperature");
});
it("reads strict JSON deserializer unknown variant errors", () => {
expect(
parseRejectedGenerationField(
JSON.stringify({
error: {
message:
"Failed to deserialize the JSON body into the target type: reasoning_effort: unknown variant `auto`, expected one of `none`, `minimal`",
},
}),
),
).toBe("reasoning_effort");
});
it("drops top_p when temperature and top_p are mutually exclusive", () => {
expect(
parseRejectedGenerationField(
"temperature and top_p cannot both be specified for this model",
),
).toBe("top_p");
});
});
describe("applyRejectedGenerationField", () => {
it("renames max_completion_tokens to max_tokens once", () => {
const body: Record<string, unknown> = { max_completion_tokens: 100 };
const tried = new Set<string>();
expect(applyRejectedGenerationField(body, "max_completion_tokens", tried)).toBe(
true,
);
expect(body.max_tokens).toBe(100);
expect(body.max_completion_tokens).toBeUndefined();
expect(applyRejectedGenerationField(body, "max_tokens", tried)).toBe(true);
expect(body.max_completion_tokens).toBeUndefined();
expect(body.max_tokens).toBeUndefined();
});
});
describe("fetchWithGenerationCompat", () => {
it("retries a 400 after stripping the rejected field", async () => {
const sent: Array<Record<string, unknown>> = [];
const response = await fetchWithGenerationCompat(async (body) => {
sent.push({ ...body });
if ("temperature" in body) {
return new Response(
JSON.stringify({
error: {
message: "Unsupported parameter: 'temperature'",
param: "temperature",
},
}),
{ status: 400 },
);
}
return new Response(JSON.stringify({ ok: true }), { status: 200 });
}, { model: "o3", temperature: 1, messages: [] });
expect(response.ok).toBe(true);
expect(sent).toHaveLength(2);
expect(sent[0].temperature).toBe(1);
expect(sent[1].temperature).toBeUndefined();
});
});
describe("buildRequestBody", () => {
const messages = [{ role: "user" as const, content: "hi" }];
it("does not send reasoning_effort when the preset says auto", () => {
const config: LlmConfig = {
baseUrl: "https://example.test/v1",
apiKey: "k",
model: "deepseek-v4-pro",
};
const body = buildRequestBody(config, messages, {
generation: { reasoningEffort: "auto", temperature: 0.5 },
});
expect(body.reasoning_effort).toBeUndefined();
expect(body.temperature).toBe(0.5);
});
it("sends a supported effort value", () => {
const config: LlmConfig = {
baseUrl: "https://example.test/v1",
apiKey: "k",
model: "gpt-5",
};
const body = buildRequestBody(config, messages, {
generation: { reasoningEffort: "High" },
});
expect(body.reasoning_effort).toBe("high");
expect(body.temperature).toBeUndefined();
});
it("uses profile default reasoningEffort when generation omits it", () => {
const config: LlmConfig = {
baseUrl: "https://example.test/v1",
apiKey: "k",
model: "glm-5.3",
reasoningEffort: "high",
};
const body = buildRequestBody(config, messages, {
generation: { temperature: 1 },
});
expect(body.reasoning_effort).toBe("high");
});
it("lets generation reasoningEffort override the profile default", () => {
const config: LlmConfig = {
baseUrl: "https://example.test/v1",
apiKey: "k",
model: "glm-5.3",
reasoningEffort: "max",
};
const body = buildRequestBody(config, messages, {
generation: { reasoningEffort: "low" },
});
expect(body.reasoning_effort).toBe("low");
});
it("does not forward top_k from an imported ST preset", () => {
const config: LlmConfig = {
baseUrl: "https://example.test/v1",
apiKey: "k",
model: "deepseek-v4-pro",
};
const body = buildRequestBody(config, messages, {
generation: stGeneration,
});
expect(body.top_k).toBeUndefined();
expect(body.min_p).toBeUndefined();
});
});
+39
View File
@@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import {
convertStructuralSmartQuotes,
tryParseJsonDoc,
} from "../web/json-doc.js";
describe("tryParseJsonDoc", () => {
it("keeps interior Chinese quotes in an otherwise valid fragment", () => {
const raw = JSON.stringify({
schema: "context-fragment.v1",
技能: "美学纲领与交互范式",
正文: {
设定逻辑: {
参与方式: {
用户与user关系: {
依据: "用户明确“只有我不会被感染”,且为爽文主角",
},
},
},
},
});
const parsed = tryParseJsonDoc(`## 设计.美学纲领与交互范式\n\n${raw}`);
expect(parsed?.技能).toBe("美学纲领与交互范式");
expect(parsed?.正文?.设定逻辑?.参与方式?.用户与user关系?.依据).toContain(
"只有我不会被感染",
);
});
it("parses JSON that uses smart quotes as delimiters", () => {
const raw = "{“schema”:“context-fragment.v1”,“brief”:“点题”}";
expect(convertStructuralSmartQuotes(raw)).toBe(
'{"schema":"context-fragment.v1","brief":"点题"}',
);
expect(tryParseJsonDoc(raw)).toEqual({
schema: "context-fragment.v1",
brief: "点题",
});
});
});
+11
View File
@@ -4,6 +4,7 @@ import {
isOpeningSealArtifact,
parseOpeningSealPayload,
openingVariablesToTableDoc,
reopenCreationFlowRaw,
} from "../src/skills/opening-seal.js";
const fragment = JSON.stringify({
@@ -71,4 +72,14 @@ describe("opening-seal", () => {
const closed = JSON.parse(closeCreationFlowRaw(raw)!);
expect(closed.status).toBe("closed");
});
it("reopens a closed creation flow", () => {
const raw = JSON.stringify({
version: 1,
status: "closed",
steps: [{ id: "开场白与开场变量", name: "开场白与开场变量", depends_on: [] }],
});
const opened = JSON.parse(reopenCreationFlowRaw(raw)!);
expect(opened.status).toBe("open");
});
});
+31
View File
@@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";
import {
extractLastProductJson,
selectJsonPayload,
tryParseJsonDoc,
} from "../src/parse/json-doc.js";
describe("selectJsonPayload", () => {
it("keeps content when it is already the product", () => {
const json = '{"schema":"context-fragment.v1","brief":"a","正文":{}}';
expect(selectJsonPayload(json, "thinking...")).toBe(json);
});
it("falls back to reasoning when content is not JSON", () => {
const json = '{"schema":"context-fragment.v1","brief":"a","正文":{}}';
expect(selectJsonPayload("Done.", json)).toBe(json);
});
});
describe("extractLastProductJson", () => {
it("skips an earlier example object and takes the fragment", () => {
const raw = `example { "foo": 1 } then ${JSON.stringify({
schema: "context-fragment.v1",
brief: "点题",
正文: { x: 1 },
})}`;
const parsed = extractLastProductJson(raw) as { brief?: string };
expect(parsed?.brief).toBe("点题");
expect(tryParseJsonDoc(raw)).toMatchObject({ brief: "点题" });
});
});
+156
View File
@@ -541,4 +541,160 @@ describe("phase machine", () => {
false,
);
});
it("rejecting the next step goes back to design-flow instead of the main agent", () => {
let session = createSession("default");
const decision = {
id: "d1",
action: "run_worker" as const,
reason: "下一步开场白",
workerId: "design-step",
requiresApproval: true,
statePatchAllowed: false as const,
};
session = {
...session,
phase: "waiting_user",
waitingReason: { kind: "approve_step", decisionId: decision.id },
pendingDecision: decision,
};
const rejected = applyEvent(session, {
type: "user_rejected_next_step",
payload: { decisionId: decision.id, reason: "还没做 NPC" },
});
expect(rejected.session.phase).toBe("running");
expect(rejected.session.slots["用户.下一步意向"]).toBe("还没做 NPC");
expect(rejected.session.pendingDecision).toBeUndefined();
expect(rejected.effects).toEqual([
{ type: "unseal_creation_opening" },
{ type: "run_worker", workerId: "design-flow" },
]);
expect(rejected.effects.some((e) => e.type === "invoke_main_agent")).toBe(
false,
);
});
it("replan from opening review runs design-flow without revising the opening", () => {
let session = createSession("default");
const artifact = createArtifact({
workerId: "design-step",
stepId: "开场白与开场变量",
outputTags: ["设计.开场白与开场变量"],
});
session = {
...session,
artifacts: [artifact],
pendingArtifactId: artifact.id,
phase: "waiting_user",
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
slots: { creationSealedByOpening: true },
};
const replanned = applyEvent(session, {
type: "user_requested_flow_replan",
payload: { reason: "漏了开局 NPC" },
});
expect(replanned.session.phase).toBe("running");
expect(replanned.session.pendingArtifactId).toBeUndefined();
expect(replanned.session.slots.creationSealedByOpening).toBeUndefined();
expect(replanned.session.artifacts[0]?.status).toBe("rejected");
expect(replanned.session.slots["用户.修订说明"]).toBe("漏了开局 NPC");
expect(replanned.effects).toEqual([
{ type: "unseal_creation_opening" },
{ type: "run_worker", workerId: "design-flow" },
]);
});
it("awaits pick on the layered graph and starts the clicked step", () => {
let session = createSession("default");
session = {
...session,
phase: "running",
};
const awaited = applyEvent(session, {
type: "creation_step_pick_awaited",
payload: {},
});
expect(awaited.session.phase).toBe("waiting_user");
expect(awaited.session.waitingReason?.kind).toBe("pick_creation_step");
expect(awaited.session.currentWorkerId).toBeUndefined();
const picked = applyEvent(awaited.session, {
type: "user_picked_creation_step",
payload: { stepId: "生成规则" },
});
expect(picked.session.phase).toBe("running");
expect(picked.session.currentWorkerId).toBe("design-step");
expect(picked.effects).toEqual([
{ type: "run_worker", workerId: "design-step" },
]);
});
it("leaving a creation step returns to the pick graph", () => {
let session = createSession("default");
session = {
...session,
phase: "waiting_user",
waitingReason: {
kind: "worker_questions",
workerId: "design-step",
questions: [{ id: "module-opening", prompt: "引导" }],
},
currentWorkerId: "design-step",
currentStepId: "生成规则#1",
};
const left = applyEvent(session, {
type: "user_left_creation_step",
payload: {},
});
expect(left.session.phase).toBe("waiting_user");
expect(left.session.waitingReason?.kind).toBe("pick_creation_step");
expect(left.session.currentWorkerId).toBeUndefined();
expect(left.session.currentStepId).toBeUndefined();
expect(left.session.resumeContext).toBeUndefined();
expect(left.effects).toEqual([]);
});
it("replan from the pick graph runs design-flow", () => {
let session = createSession("default");
session = {
...session,
phase: "waiting_user",
waitingReason: { kind: "pick_creation_step" },
};
const replanned = applyEvent(session, {
type: "user_requested_flow_replan",
payload: { reason: "补舞台骨架" },
});
expect(replanned.session.phase).toBe("running");
expect(replanned.session.slots["用户.下一步意向"]).toBe("补舞台骨架");
expect(replanned.effects).toEqual([
{ type: "unseal_creation_opening" },
{ type: "run_worker", workerId: "design-flow" },
]);
});
it("design-flow without confirmation proposes the pick graph", () => {
let session = createSession("default");
session = {
...session,
phase: "running",
acceptanceMode: "no_confirmation",
};
const artifact = createArtifact({
workerId: "design-flow",
outputTags: ["设计.创作流程"],
});
session = { ...session, artifacts: [artifact] };
const completed = applyEvent(session, {
type: "worker_completed",
payload: { artifactId: artifact.id },
});
expect(completed.session.artifacts[0]?.status).toBe("accepted");
expect(completed.session.pendingArtifactId).toBeUndefined();
expect(completed.effects).toEqual([
{ type: "propose_next_creation_step" },
]);
});
});
+164
View File
@@ -7,6 +7,7 @@ import {
import { PhaseRuntime, createDecision } from "../src/runtime/phase-runtime.js";
import { createSession } from "../src/runtime/phase-machine.js";
import { Blackboard } from "../src/blackboard/blackboard.js";
import { parseCreationFlow } from "../src/skills/creation-flow.js";
describe("phase runtime", () => {
it("auto-loads default orchestrator and awaits first user input", async () => {
@@ -129,4 +130,167 @@ describe("phase runtime", () => {
await runtime.start();
await expect(runtime.retryStuckRun()).rejects.toThrow("当前没有可重试的执行");
});
it("first input with seeded DAG waits for node pick instead of calling main agent", async () => {
const llm = new MockLlmProvider(["Let me analyze the DAG in English prose"]);
const runtime = new PhaseRuntime({ llm });
await runtime.start();
runtime.getBlackboard().write({
tag: "设计.创作流程",
content: JSON.stringify({
version: 1,
status: "open",
steps: [
{
id: "美学纲领与交互范式",
name: "美学纲领与交互范式",
depends_on: [],
},
],
}),
source: "runtime",
});
await runtime.submitInput("丧尸世界,但只有我不会被感染");
expect(runtime.getSession().phase).toBe("waiting_user");
expect(runtime.getSession().waitingReason?.kind).toBe("pick_creation_step");
});
it("enterSeededCreationPick skips first demand and waits on the seeded graph", async () => {
const runtime = new PhaseRuntime();
await runtime.start();
runtime.getBlackboard().write({
tag: "设计.创作流程",
content: JSON.stringify({
version: 1,
status: "open",
steps: [
{
id: "舞台骨架",
name: "舞台骨架",
depends_on: [],
},
],
}),
source: "runtime",
});
await runtime.enterSeededCreationPick();
expect(runtime.getSession().slots.startupCompleted).toBe(true);
expect(runtime.getSession().waitingReason?.kind).toBe("pick_creation_step");
});
it("unusable main agent output without DAG starts design-flow", async () => {
const llm = new MockLlmProvider(["Let me think about routing"]);
const runtime = new PhaseRuntime({ llm, autoStubWorker: true });
await runtime.start();
await runtime.submitInput("网恋对象对话");
expect(runtime.getSession().phase).not.toBe("running");
expect(
runtime.getSession().artifacts.some((a) => a.workerId === "design-flow"),
).toBe(true);
});
it("failRun leaves error phase instead of running", async () => {
const runtime = new PhaseRuntime();
await runtime.start();
await runtime.submitInput("随便写点");
expect(runtime.getSession().phase).toBe("running");
await runtime.failRun("fetch failed");
expect(runtime.getSession().phase).toBe("error");
expect(runtime.getSession().waitingReason).toBeUndefined();
});
it("recoverOrphanedRun turns stuck running into node pick when DAG exists", async () => {
const runtime = new PhaseRuntime();
await runtime.start();
runtime.getBlackboard().write({
tag: "设计.创作流程",
content: JSON.stringify({
version: 1,
status: "open",
steps: [
{
id: "美学纲领与交互范式",
name: "美学纲领与交互范式",
depends_on: [],
},
],
}),
source: "runtime",
});
const session = runtime.getSession();
session.phase = "running";
session.waitingReason = undefined;
expect(runtime.recoverOrphanedRun()).toBe(true);
expect(runtime.getSession().phase).toBe("waiting_user");
expect(runtime.getSession().waitingReason?.kind).toBe("pick_creation_step");
});
it("leaveCreationStep returns to pick and unsawns an empty instance", async () => {
const session = createSession("default");
session.phase = "waiting_user";
session.waitingReason = {
kind: "worker_questions",
workerId: "design-step",
questions: [{ id: "module-opening", prompt: "生成规则引导" }],
};
session.currentWorkerId = "design-step";
session.slots = { startupCompleted: true };
const blackboard = new Blackboard();
blackboard.write({
tag: "设计.创作流程",
content: JSON.stringify({
version: 1,
status: "open",
steps: [
{
id: "美学纲领与交互范式",
name: "美学纲领与交互范式",
depends_on: [],
},
{
id: "生成规则",
name: "生成规则",
role: "prototype",
depends_on: ["美学纲领与交互范式"],
},
{
id: "生成规则#1",
name: "生成规则",
role: "instance",
from: "生成规则",
depends_on: ["美学纲领与交互范式"],
},
{
id: "开场白与开场变量",
name: "开场白与开场变量",
depends_on: ["美学纲领与交互范式", "生成规则#1"],
},
],
}),
source: "runtime",
});
blackboard.write({
tag: "创作.当前步骤",
content: "生成规则#1",
source: "runtime",
});
const runtime = new PhaseRuntime({
initialSession: session,
initialBlackboardItems: blackboard.exportItems(),
});
await runtime.leaveCreationStep();
expect(runtime.getSession().waitingReason?.kind).toBe("pick_creation_step");
expect(runtime.getSession().currentWorkerId).toBeUndefined();
const flow = parseCreationFlow(
runtime.getBlackboard().getContentByTag("设计.创作流程"),
);
expect(flow?.steps.map((s) => s.id)).toEqual([
"美学纲领与交互范式",
"生成规则",
"开场白与开场变量",
]);
expect(
flow?.steps.find((s) => s.name === "开场白与开场变量")?.depends_on,
).toEqual(["美学纲领与交互范式"]);
});
});
+28
View File
@@ -127,4 +127,32 @@ describe("session persistence", () => {
deleteBook(book.id);
});
it("createForBook with recipe lands on that recipe's seeded pick graph", async () => {
const { createBook, deleteBook } = await import("../src/book/store.js");
const { SessionManager } = await import("../src/server/session-manager.js");
const book = createBook({ title: "配方开局" });
const mgr = new SessionManager();
const view = await mgr.createForBook(
book.id,
"world-simulator",
"world-simulator",
);
expect(view.waitingReason?.kind).toBe("pick_creation_step");
expect(view.startupCompleted).toBe(true);
expect(view.uiPrompt).toBeUndefined();
expect(view.openingGuide).toBeNull();
const { loadAllRecipeDetails } = await import(
"../src/skills/creation-flow.js"
);
const details = await loadAllRecipeDetails("dialogue/world-simulator");
const seedNames = details
.find((d) => d.id === "world-simulator")
?.seed?.steps.map((s) => s.name);
expect(seedNames?.length).toBeGreaterThan(0);
expect(view.creationFlowView?.steps.map((s) => s.name)).toEqual(seedNames);
deleteBook(book.id);
});
});
+10
View File
@@ -4,6 +4,7 @@ import {
createMockToolCall,
MockLlmProvider,
} from "../src/llm/client.js";
import { isUnusableMainAgentOutput } from "../src/main-agent/main-agent.js";
import { runMainAgentToolLoop } from "../src/main-agent/tool-loop.js";
import { MAIN_AGENT_TOOL_DEFINITIONS } from "../src/main-agent/tools.js";
import { toolCallToDecision } from "../src/runtime/tool-registry.js";
@@ -29,6 +30,15 @@ describe("main agent tool loop", () => {
).toEqual(["read_blackboard", "run_worker"]);
});
it("detects unusable main agent output", () => {
expect(
isUnusableMainAgentOutput(
new Error("Main Agent returned invalid JSON: Let me analyze"),
),
).toBe(true);
expect(isUnusableMainAgentOutput(new Error("fetch failed"))).toBe(false);
});
it("runs loop tools then returns terminal decision", async () => {
const llm = new MockLlmProvider([
createMockToolCall("read_blackboard", { tags: ["book.brief"] }),
+120
View File
@@ -9,6 +9,8 @@ import {
parseWorkerResponseForTest,
sanitizeWorkerSetOutputs,
} from "../src/worker/executor.js";
import { selectJsonPayload } from "../src/parse/json-doc.js";
import { parseCreationFlow } from "../src/skills/creation-flow.js";
describe("worker-set prose rejection", () => {
it("does not YAML-crash on Chinese prose; returns clear parseError", () => {
@@ -155,6 +157,39 @@ describe("sanitizeWorkerSetOutputs", () => {
expect(result.askUser?.[0]?.prompt).not.toContain("为贴近你要的质感");
});
it("keeps nested aesthetics 正文 objects for specialty cards", () => {
const frag = {
schema: "context-fragment.v1",
技能: "美学纲领与交互范式",
brief: "孤立免疫",
正文: {
设定逻辑: {
参与方式: {
用户与user关系: {
结论: "完全代入",
完备度: "80%",
依据: "用户明确只有我不会被感染",
},
},
},
交互范式: { 前置配置: { 人称: "你" } },
美学纲领: { 体验内核: "特权与惊惶" },
},
自评: {
维度: [{ 名: "美学纲领", 分数: 8, 说明: "点题" }],
},
};
const result = parseWorkerResponseForTest(JSON.stringify(frag), [
"设计.美学纲领与交互范式",
]);
const parsed = JSON.parse(result.outputs["设计.美学纲领与交互范式"]!);
const body = parsed.正文;
expect(typeof body.设定逻辑).toBe("object");
expect(body.设定逻辑.参与方式.用户与user关系.结论).toBe("完全代入");
expect(body.美学纲领.体验内核).toBe("特权与惊惶");
expect(parsed.自评.维度[0].分数).toBe(8);
});
it("recovers a root-level context-fragment into the artifact tag", () => {
const frag = {
schema: "context-fragment.v1",
@@ -173,6 +208,46 @@ describe("sanitizeWorkerSetOutputs", () => {
expect(result.askUser).toBeUndefined();
});
it("accepts unprefixed outputs keys and JSON sitting after thinking prose", () => {
const frag = {
schema: "context-fragment.v1",
技能: "美学纲领与交互范式",
brief: "孤立免疫",
正文: { 美学纲领: { 体验内核: "人人录我" } },
};
const prefixed = parseWorkerResponseForTest(
JSON.stringify({
outputs: { "美学纲领与交互范式": frag },
summary: "ok",
}),
["设计.美学纲领与交互范式"],
);
expect(prefixed.outputs["设计.美学纲领与交互范式"]).toContain("人人录我");
const mixed = parseWorkerResponseForTest(
`We must output JSON. Example { "foo": 1 }.\n${JSON.stringify(frag)}`,
["设计.美学纲领与交互范式"],
);
expect(mixed.outputs["设计.美学纲领与交互范式"]).toContain("人人录我");
});
it("uses product JSON in reasoning when content is a closing sentence", () => {
const frag = {
schema: "context-fragment.v1",
技能: "美学纲领与交互范式",
brief: "点题",
正文: { 美学纲领: { 体验内核: "内核" } },
};
const payload = selectJsonPayload(
"The artifact is ready.",
JSON.stringify(frag),
);
const result = parseWorkerResponseForTest(payload, [
"设计.美学纲领与交互范式",
]);
expect(result.outputs["设计.美学纲领与交互范式"]).toContain("内核");
});
it("moves a fragment written to 创作.当前步骤 onto the artifact tag", () => {
const frag = {
schema: "context-fragment.v1",
@@ -257,6 +332,51 @@ describe("sanitizeWorkerSetOutputs", () => {
expect(doc.play_slots.gm).toBe(true);
});
it("keeps a root-level creation flow DAG on 设计.创作流程", () => {
const dag = {
brief: "单角代入",
status: "open",
steps: [
{ id: "美学纲领与交互范式", name: "美学纲领与交互范式", depends_on: [] },
{
id: "生成规则",
name: "生成规则",
role: "prototype",
depends_on: ["美学纲领与交互范式"],
},
],
};
const result = parseWorkerResponseForTest(JSON.stringify(dag), [
"设计.创作流程",
]);
const parsed = parseCreationFlow(result.outputs["设计.创作流程"]);
expect(parsed?.brief).toBe("单角代入");
expect(parsed?.steps.map((s) => s.name)).toEqual([
"美学纲领与交互范式",
"生成规则",
]);
expect(result.outputs["设计.创作流程"]).not.toContain("context-fragment");
expect(result.askUser).toBeUndefined();
});
it("keeps creation flow written under outputs bag", () => {
const result = parseWorkerResponseForTest(
JSON.stringify({
outputs: {
"设计.创作流程": {
status: "open",
steps: [{ name: "美学纲领与交互范式", depends_on: [] }],
},
},
summary: "流程 · 1 步 · open",
}),
["设计.创作流程"],
);
expect(parseCreationFlow(result.outputs["设计.创作流程"])?.steps[0]?.name).toBe(
"美学纲领与交互范式",
);
});
it("keeps a whole-response truncated JSON as the artifact draft", () => {
const raw =
'{"outputs":{"设计.上下文投影排序":{"schema":"context-order.v1","brief":"旁观+转述","agents":[';