完善创作节点循环与等待态交互,并大幅打磨 Web 壳与会话运行时。

- 节点循环:配方开局直坐 DAG 第一步;验收后先问下一步意向,再确认开干并可钉编排参数;「按意见修改」只重跑当前节点。
- 询问分流:能力 opening 走说话面引导,可跳过追问按题干去重;追问与产物同轮挂载,避免拆成两段历史。
- 运行时:补强 revision 重跑、创作流程合并/进度指针、工具循环与 worker 执行;新增运行日志与 web:watch。
- 前端:统一等待态文案与底栏主按钮,完善询问卡/产物验收/呈现壳样式与交互。
- 同步世界模拟器模块提示、编排文档与相关测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-17 01:28:12 +08:00
parent 20938b04d7
commit ec474cb946
56 changed files with 7460 additions and 1372 deletions

View File

@@ -79,6 +79,38 @@ describe("contextSegments assembly", () => {
expect(text).not.toMatch(/^\s*\{/);
});
it("revision mode asks the worker to apply sidecar answers and keep the draft", () => {
const bb = new Blackboard();
bb.write({
tag: "设计.美学纲领与交互范式",
content: '{"tone":"warm"}',
source: "worker",
});
const text = assembleWorkerContext({
inputs: {
"用户.修订说明": "再冷一点",
"用户.worker答复": "【追问作答】\n问偏好\n答偏冷",
"设计.美学纲领与交互范式": '{"tone":"warm"}',
},
segments: [
{
id: "user",
tier: "dynamic",
tags: ["用户.worker答复", "用户.修订说明"],
label: "## 用户表述",
},
],
blackboard: bb,
workerId: "design-step",
workerName: "创作 · 美学纲领与交互范式",
outputTags: ["设计.美学纲领与交互范式"],
});
expect(text).toContain("待改底稿");
expect(text).toContain("用户.worker答复");
expect(text).toContain("偏冷");
expect(text).toContain("再冷一点");
});
it("falls back to JSON inputs without segments", () => {
const bb = new Blackboard();
const text = assembleWorkerContext({

View File

@@ -4,6 +4,8 @@ import {
projectFragmentContent,
contextFragmentToView,
extractFragmentAskSidecar,
isUsableContextFragment,
expectsContextFragmentTag,
} from "../src/skills/context-fragment.js";
import {
parseContextOrder,
@@ -74,6 +76,26 @@ describe("context-fragment.v1", () => {
"其它",
]);
});
it("isUsableContextFragment requires non-empty 正文", () => {
expect(
isUsableContextFragment({
schema: "context-fragment.v1",
: "美学纲领与交互范式",
brief: "孤立免疫",
: { : { : "特权" } },
}),
).toBe(true);
expect(
isUsableContextFragment({
schema: "context-fragment.v1",
: "美学纲领与交互范式",
brief: "孤立免疫",
}),
).toBe(false);
expect(expectsContextFragmentTag("设计.美学纲领与交互范式")).toBe(true);
expect(expectsContextFragmentTag("设计.创作流程")).toBe(false);
});
});
describe("context-order.v1", () => {

View File

@@ -6,6 +6,7 @@ import {
artifactTagForStep,
extractModuleOpening,
formatCreationFlowForUser,
formatFlowProgressForAgent,
formatModuleCatalogForAgent,
formatRecipeCatalogForAgent,
formatSelectedRecipeForAgent,
@@ -14,16 +15,23 @@ import {
loadModuleCatalog,
loadModulePrompt,
loadRecipeCatalog,
mergeCreationFlowPreservingAccepted,
needsFlowExpansion,
nextPendingStep,
parseCreationFlow,
pickRecordedStepId,
parseModuleCatalog,
parseModuleOpeningState,
parseRecipeCatalog,
parseRecipeYaml,
parseSelectedRecipeRef,
resolveDesignStepBinding,
shouldSkipModuleOpening,
isProgressPointerTag,
stringifyCreationFlow,
creationFlowFromRecipeSeed,
validateCreationFlow,
validateStepReadyToRun,
} from "../src/skills/creation-flow.js";
import { loadWorkerSkillWithContext } from "../src/skills/loader.js";
@@ -67,6 +75,79 @@ describe("creation-flow", () => {
});
});
it("keeps accepted steps intact when the planner rewrites the flow", () => {
const prev = `{
"status": "open",
"steps": [
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] }
]
}`;
const rewritten = `{
"status": "open",
"steps": [
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": ["实现机制"] },
{ "id": "实现机制", "name": "实现机制", "depends_on": [] }
]
}`;
const merged = mergeCreationFlowPreservingAccepted({
prevRaw: prev,
nextRaw: rewritten,
acceptedStepIds: ["美学纲领与交互范式"],
});
expect(merged.restored).toEqual(["美学纲领与交互范式"]);
const flow = parseCreationFlow(merged.raw)!;
expect(flow.steps[0]?.depends_on).toEqual([]);
expect(flow.steps.map((s) => s.id)).toEqual([
"美学纲领与交互范式",
"实现机制",
]);
});
it("re-inserts an accepted step the planner dropped", () => {
const prev = `{
"status": "open",
"steps": [
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] }
]
}`;
const rewritten = `{
"status": "open",
"steps": [{ "id": "实现机制", "name": "实现机制", "depends_on": [] }]
}`;
const merged = mergeCreationFlowPreservingAccepted({
prevRaw: prev,
nextRaw: rewritten,
acceptedStepIds: ["美学纲领与交互范式"],
});
expect(parseCreationFlow(merged.raw)!.steps.map((s) => s.id)).toEqual([
"美学纲领与交互范式",
"实现机制",
]);
});
it("leaves an untouched rewrite alone", () => {
const prev = `{
"status": "open",
"steps": [
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] }
]
}`;
const rewritten = `{
"status": "open",
"steps": [
{ "id": "美学纲领与交互范式", "name": "美学纲领与交互范式", "depends_on": [] },
{ "id": "实现机制", "name": "实现机制", "depends_on": ["美学纲领与交互范式"] }
]
}`;
const merged = mergeCreationFlowPreservingAccepted({
prevRaw: prev,
nextRaw: rewritten,
acceptedStepIds: ["美学纲领与交互范式"],
});
expect(merged.restored).toEqual([]);
expect(merged.raw).toBe(rewritten);
});
it("allows duplicate capability names with distinct ids", () => {
const flow = parseCreationFlow(`{
"status": "open",
@@ -171,6 +252,29 @@ describe("creation-flow", () => {
});
expect(view.steps[2]?.occurrence).toBe(2);
expect(view.steps[2]?.repeatable).toBe(true);
expect(view.steps[0]?.runState).toBe("current");
expect(view.steps[1]?.runState).toBe("pending");
expect(view.steps[2]?.runState).toBe("pending");
});
it("marks accepted steps done and the next pending current", () => {
const flow = parseCreationFlow(`{
"brief": "网恋回合",
"status": "open",
"steps": [
{ "name": "美学纲领与交互范式", "depends_on": [] },
{ "name": "生成规则", "depends_on": ["美学纲领与交互范式"] },
{ "id": "生成规则#2", "name": "生成规则", "depends_on": ["生成规则"] }
]
}`)!;
const view = formatCreationFlowForUser(flow, sampleCatalog, [
"美学纲领与交互范式",
]);
expect(view.steps.map((s) => s.runState)).toEqual([
"done",
"current",
"pending",
]);
});
it("loads world-simulator catalog and formats for agent", async () => {
@@ -273,6 +377,18 @@ steps:
expect(formatted).toContain("用户已选配方");
expect(formatted).toContain("核心思路");
expect(formatted).toContain("增量");
const seeded = creationFlowFromRecipeSeed(detail);
expect(seeded?.steps[0]?.name).toBe("美学纲领与交互范式");
expect(seeded?.status).toBe("open");
const raw = stringifyCreationFlow(seeded!);
expect(parseCreationFlow(raw)?.steps[0]?.id).toBe("美学纲领与交互范式");
expect(creationFlowFromRecipeSeed({ seed: null })).toBeNull();
expect(
creationFlowFromRecipeSeed({
seed: { version: 1, status: "open", steps: [] },
}),
).toBeNull();
});
it("falls back to legacy hint when methodology absent", () => {
@@ -326,9 +442,50 @@ recipes:
expect(selected.promptBody).toContain("核心思路");
expect(selected.promptBody).toContain("何时用");
expect(selected.promptBody).toContain("增量");
expect(selected.promptBody).toContain("【流程进度】");
expect(selected.promptBody).not.toContain("【导演】用户尚未手动选择");
});
it("tells design-flow which steps are done and not to reschedule them", async () => {
const flowRaw = JSON.stringify({
steps: [{ name: "美学纲领与交互范式", depends_on: [] }],
});
const loaded = await loadWorkerSkillWithContext(
"world-simulator",
"design-flow",
undefined,
{
selectedRecipeRef: "world-simulator",
flowRaw,
acceptedStepNames: ["美学纲领与交互范式"],
filledArtifactTags: ["设计.美学纲领与交互范式"],
},
);
expect(loaded.promptBody).toContain("【流程进度】");
expect(loaded.promptBody).toContain("已完成:");
expect(loaded.promptBody).toContain("美学纲领与交互范式");
expect(loaded.promptBody).toContain("已验收");
expect(loaded.promptBody).toContain("禁止再排一次执行");
expect(loaded.promptBody).toContain("生成规则");
});
it("formatFlowProgressForAgent keeps seed steps as draft not new work", () => {
const flow = parseCreationFlow(`{
"steps": [
{ "name": "美学纲领与交互范式", "depends_on": [] }
]
}`)!;
const text = formatFlowProgressForAgent({
flow,
acceptedStepIds: [],
catalog: sampleCatalog,
});
expect(text).toContain("草案已有、尚未验收");
expect(text).toContain("美学纲领与交互范式");
expect(text).toContain("不要当作新规划再写一遍");
expect(text).toContain("可反复追加:生成规则、具体实例");
});
it("injects aesthetics-interaction module prompt into design-step", async () => {
const flowRaw = JSON.stringify({
steps: [{ name: "美学纲领与交互范式", depends_on: [] }],
@@ -344,6 +501,8 @@ recipes:
},
);
expect(loaded.worker.outputTags).toContain("设计.美学纲领与交互范式");
expect(loaded.worker.outputTags).not.toContain("创作.当前步骤");
expect(loaded.worker.inputTags).toContain("创作.当前步骤");
expect(loaded.promptBody).toContain("美学纲领与交互范式");
expect(loaded.promptBody).toContain("体验契约");
expect(loaded.promptBody).toContain("程序开场");
@@ -386,6 +545,35 @@ recipes:
: "shown",
});
expect(
shouldSkipModuleOpening({
demand: "丧尸世界只有我不会被感染",
dependsOn: [],
}),
).toBe(true);
expect(
shouldSkipModuleOpening({
demand: "丧尸世界只有我不会被感染",
dependsOn: ["美学纲领与交互范式"],
}),
).toBe(false);
expect(
shouldSkipModuleOpening({
demand: "",
dependsOn: [],
}),
).toBe(false);
expect(
shouldSkipModuleOpening({
demand: "已有需求",
dependsOn: [],
phase: "shown",
}),
).toBe(false);
expect(isProgressPointerTag("创作.当前步骤")).toBe(true);
expect(isProgressPointerTag("设计.美学纲领与交互范式")).toBe(false);
expect(isProgressPointerTag("设计.创作流程")).toBe(false);
const binding = await resolveDesignStepBinding({
skillPackRoot: "dialogue/world-simulator",
flowRaw: JSON.stringify({
@@ -428,9 +616,11 @@ recipes:
const missing = parseCreationFlow(`{
"steps": [{ "name": "生成规则", "depends_on": [] }]
}`)!;
const bad = validateCreationFlow(missing, catalog);
expect(bad.ok).toBe(false);
expect(bad.errors.some((e) => e.includes("target"))).toBe(true);
// 编排期允许空壳;必填 params 在确认开干时钉齐
expect(validateCreationFlow(missing, catalog).ok).toBe(true);
const stepReady = validateStepReadyToRun(missing.steps[0]!, catalog);
expect(stepReady.ok).toBe(false);
expect(stepReady.errors.some((e) => e.includes("target"))).toBe(true);
const block = formatModuleCatalogForAgent(catalog!);
expect(block).toContain("编排参数");
@@ -483,6 +673,43 @@ recipes:
).toBeNull();
});
it("pickRecordedStepId ignores LLM junk on 创作.当前步骤", () => {
const flow = parseCreationFlow(`{
"status": "open",
"steps": [
{ "name": "美学纲领与交互范式", "depends_on": [] },
{ "name": "生成规则", "depends_on": ["美学纲领与交互范式"] }
]
}`)!;
expect(
pickRecordedStepId({
flow,
alreadyAccepted: [],
pinnedUnitId: "美学纲领与交互范式",
writtenCurrentStep: JSON.stringify({
schema: "context-fragment.v1",
: { : {}, : {} },
}),
}),
).toBe("美学纲领与交互范式");
expect(
pickRecordedStepId({
flow,
alreadyAccepted: [],
pinnedUnitId: "flow",
writtenCurrentStep: "交互范式",
}),
).toBe("美学纲领与交互范式");
expect(
pickRecordedStepId({
flow,
alreadyAccepted: ["美学纲领与交互范式"],
pinnedUnitId: "",
writtenCurrentStep: "",
}),
).toBe("生成规则");
});
it("open flow needs expansion after listed steps accepted", () => {
const flow = parseCreationFlow(`{
"status": "open",

View File

@@ -3,6 +3,7 @@ import {
displayStageLabel,
displayWorkerLabel,
formatWorkerDisplayTitle,
reviewComposerCopy,
} from "../src/server/display-labels.js";
describe("display-labels", () => {
@@ -27,4 +28,22 @@ describe("display-labels", () => {
);
expect(displayWorkerLabel("worker:narrator")).toBe("执行单元 · 叙事转述");
});
it("maps review composer copy by worker", () => {
const flow = reviewComposerCopy("design-flow");
expect(flow.acceptLabel).toBe("确认编排");
expect(flow.submitLabel).toBe("按意见改编排");
expect(flow.kicker).toBe("核对编排");
expect(flow.tone).toBe("plan");
const step = reviewComposerCopy("design-step");
expect(step.acceptLabel).toBe("接受");
expect(step.submitLabel).toBe("按意见修改");
expect(step.kicker).toBe("待验收");
expect(step.tone).toBe("accept");
const opening = reviewComposerCopy("opening-generator");
expect(opening.acceptLabel).toBe("选定此开场");
expect(opening.kicker).toBe("待选定");
});
});

View File

@@ -83,7 +83,7 @@ describe("phase machine", () => {
outputTags: ["设计.worker集.草稿"],
});
session = { ...session, artifacts: [artifact] };
session = applyEvent(session, {
const completed = applyEvent(session, {
type: "worker_completed",
payload: {
artifactId: artifact.id,
@@ -95,12 +95,22 @@ describe("phase machine", () => {
},
],
},
}).session;
});
session = completed.session;
expect(session.waitingReason?.kind).toBe("review_artifact");
if (session.waitingReason?.kind !== "review_artifact") return;
expect(session.waitingReason.questions?.length).toBe(1);
expect(session.waitingReason.questions?.[0]?.required).toBe(false);
// 追问只挂 waitingReason不另发「可选追问」消息与产物同面展示
expect(
completed.effects.some(
(e) =>
e.type === "emit_message" &&
typeof e.message === "string" &&
/可选追问/.test(e.message),
),
).toBe(false);
// 不答追问,直接接受产物
session = applyEvent(session, {
@@ -159,4 +169,152 @@ describe("phase machine", () => {
}).session;
expect(session.slots.designInstanceReady).toBeUndefined();
});
it("rejecting artifact with feedback immediately reruns the same worker", () => {
let session = createSession("default");
const artifact = createArtifact({
workerId: "design-step",
stepId: "step-aesthetics",
outputTags: ["设计.美学纲领"],
});
session = {
...session,
artifacts: [artifact],
pendingArtifactId: artifact.id,
currentStepId: "step-aesthetics",
phase: "waiting_user",
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
slots: {
"用户.worker答复": "【追问作答】\n问偏好\n答偏冷",
},
};
const result = applyEvent(session, {
type: "user_rejected_artifact",
payload: { artifactId: artifact.id, reason: "语气再冷一点" },
});
expect(result.session.phase).toBe("running");
expect(result.session.waitingReason).toBeUndefined();
expect(result.session.slots["用户.修订说明"]).toBe("语气再冷一点");
// 追问答复不得被修订说明冲掉
expect(result.session.slots["用户.worker答复"]).toContain("偏冷");
expect(result.session.artifacts[0]?.status).toBe("revision_requested");
// 只有这一份底稿可在重跑写不出新版时退回
expect(result.session.slots.revisionTargetArtifactId).toBe(artifact.id);
expect(result.session.currentWorkerId).toBe("design-step");
expect(result.session.currentStepId).toBe("step-aesthetics");
expect(result.effects).toEqual([
{ type: "run_worker", workerId: "design-step" },
]);
});
it("failed revision rerun falls back to reviewing the previous artifact", () => {
let session = createSession("default");
const artifact = {
...createArtifact({
workerId: "design-step",
stepId: "step-aesthetics",
outputTags: ["设计.美学纲领"],
}),
status: "revision_requested" as const,
};
session = {
...session,
artifacts: [artifact],
currentWorkerId: "design-step",
currentStepId: "step-aesthetics",
phase: "running",
slots: { revisionTargetArtifactId: artifact.id },
};
const result = applyEvent(session, {
type: "worker_revision_produced_nothing",
payload: {
artifactId: artifact.id,
questions: ["这一步还没写出可验收的产物。请再补一点你最在意的体验。"],
},
});
expect(result.session.phase).toBe("waiting_user");
expect(result.session.waitingReason?.kind).toBe("review_artifact");
expect(result.session.pendingArtifactId).toBe(artifact.id);
expect(result.session.artifacts[0]?.status).toBe("under_review");
// 重试题降级为可选追问,用户仍可直接接受上一版
const reason = result.session.waitingReason;
expect(reason?.kind === "review_artifact" && reason.questions?.[0]?.required).toBe(
false,
);
// 退回后不再是重跑目标,避免下一步无产物时又被挂回来
expect(result.session.slots.revisionTargetArtifactId).toBeUndefined();
});
it("rejecting artifact without feedback waits for revision instruction", () => {
let session = createSession("default");
const artifact = createArtifact({
workerId: "design-step",
outputTags: ["设计.美学纲领"],
});
session = {
...session,
artifacts: [artifact],
pendingArtifactId: artifact.id,
phase: "waiting_user",
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
};
const rejected = applyEvent(session, {
type: "user_rejected_artifact",
payload: { artifactId: artifact.id },
});
expect(rejected.session.waitingReason?.kind).toBe("revision");
expect(rejected.session.artifacts[0]?.status).toBe("rejected");
const resumed = applyEvent(rejected.session, {
type: "user_submitted_input",
payload: { text: "加一点冲突感" },
});
expect(resumed.session.phase).toBe("running");
expect(resumed.session.slots["用户.修订说明"]).toBe("加一点冲突感");
expect(resumed.effects).toEqual([
{ type: "run_worker", workerId: "design-step" },
]);
});
it("accepting design-step enters next_intent instead of invoking main agent", () => {
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 },
};
const accepted = applyEvent(session, {
type: "user_accepted_artifact",
payload: { artifactId: artifact.id },
});
expect(accepted.session.waitingReason?.kind).toBe("next_intent");
expect(accepted.effects.some((e) => e.type === "invoke_main_agent")).toBe(
false,
);
const continued = applyEvent(accepted.session, {
type: "user_submitted_input",
payload: { text: "接下来写怪物生成规则" },
});
expect(continued.session.phase).toBe("running");
expect(continued.session.slots["用户.下一步意向"]).toBe(
"接下来写怪物生成规则",
);
expect(continued.effects).toEqual([
{ type: "propose_next_creation_step" },
]);
});
});

View File

@@ -1,8 +1,14 @@
import { describe, expect, it } from "vitest";
import {
appendAskedQuestions,
dropAlreadyAskedQuestions,
formatQuestionAnswersForAi,
formatQuestionAnswersForDisplay,
isModuleOpeningQuestions,
MODULE_OPENING_QUESTION_ID,
mergeQuestionsPreferFragment,
normalizeQuestions,
parseAskedQuestions,
} from "../src/skills/question-protocol.js";
describe("normalizeQuestions", () => {
@@ -28,6 +34,131 @@ describe("normalizeQuestions", () => {
});
});
describe("isModuleOpeningQuestions", () => {
it("recognizes the single module-opening question", () => {
const qs = normalizeQuestions([
{
id: MODULE_OPENING_QUESTION_ID,
prompt: "本轮生成对象已钉死。请直接谈…",
allowOther: true,
required: true,
},
]);
expect(isModuleOpeningQuestions(qs)).toBe(true);
});
it("rejects ordinary worker questions", () => {
const qs = normalizeQuestions([
{ id: "q1", prompt: "要不要池?" },
]);
expect(isModuleOpeningQuestions(qs)).toBe(false);
expect(isModuleOpeningQuestions([])).toBe(false);
expect(isModuleOpeningQuestions(null)).toBe(false);
});
});
describe("mergeQuestionsPreferFragment", () => {
it("keeps the 示例 version when askUser repeats the same options", () => {
const ask = normalizeQuestions([
{
prompt: "为贴近你要的质感:你最想反复感受到的是哪一种?",
options: [
"隐于都市的日常从容:所有人都只是凡人,而你心里清楚世界是你的",
"身份揭示的瞬间翻转:前一刻还被人平视甚至冒犯,后一刻万人噤声",
],
},
{
prompt: "在你亮明身份之前,你希望普通世界如何对待你?",
options: [
"基本礼貌但无特别敬畏,偶尔会有微小冒犯",
"因你气质不凡而隐隐客气,但没人知道原因",
],
},
]);
const frag = normalizeQuestions([
{
prompt:
"你最想反复感受到的是哪一种?\n示例比如走在现代街头有人撞了你一下。",
options: [
"隐于都市的日常从容:所有人都只是凡人,而你心里清楚世界是你的",
"身份揭示的瞬间翻转:前一刻还被人平视甚至冒犯,后一刻万人噤声",
],
},
{
prompt:
"在你亮明身份之前,你希望普通世界如何对待你?\n示例这样我才好确定叙事里普通人对你的默认距离。",
options: [
"基本礼貌但无特别敬畏,偶尔会有微小冒犯",
"因你气质不凡而隐隐客气,但没人知道原因",
],
},
]);
const merged = mergeQuestionsPreferFragment(ask, frag);
expect(merged).toHaveLength(2);
expect(merged[0]?.prompt).toContain("示例:");
expect(merged[1]?.prompt).toContain("示例:");
});
it("keeps askUser-only questions that are not in the fragment", () => {
const ask = normalizeQuestions([
{ prompt: "人称?", options: ["第二人称", "第一人称"] },
]);
const frag = normalizeQuestions([
{
prompt: "暴力尺度?\n示例锈迹即可",
options: ["少而锋利", "常态压抑"],
},
]);
const merged = mergeQuestionsPreferFragment(ask, frag);
expect(merged).toHaveLength(2);
expect(merged[0]?.prompt).toContain("暴力尺度");
expect(merged[1]?.prompt).toBe("人称?");
});
});
describe("已问追问去重", () => {
const aesthetics = normalizeQuestions([
{
id: "focus",
prompt: "「永远有 1 万元余额」这个设定,你希望体验更偏向哪种?",
options: [
"用这 1 万元在城市里精打细算地活下去",
"靠这 1 万元撬动关系、机会与影响力",
],
},
]);
it("编排层重抛上一步问过的题时整题丢掉", () => {
const history = appendAskedQuestions([], aesthetics);
const replanned = normalizeQuestions([
{
id: "focus",
prompt: "「永远有 1 万元余额」这个设定,你希望体验更偏向哪种?",
options: [
"用这 1 万元在城市里精打细算地活下去",
"靠这 1 万元撬动关系、机会与影响力",
],
},
]);
expect(dropAlreadyAskedQuestions(replanned, history)).toHaveLength(0);
});
it("新问题照常保留", () => {
const history = appendAskedQuestions([], aesthetics);
const fresh = normalizeQuestions([
{ prompt: "下一步先排实现机制还是叙事指南?", options: ["实现机制"] },
]);
expect(dropAlreadyAskedQuestions(fresh, history)).toHaveLength(1);
});
it("留痕可经 slots 往返,且同题不重复堆积", () => {
const once = appendAskedQuestions([], aesthetics);
const twice = appendAskedQuestions(once, aesthetics);
expect(twice).toHaveLength(1);
const roundTripped = parseAskedQuestions(JSON.stringify(twice));
expect(dropAlreadyAskedQuestions(aesthetics, roundTripped)).toHaveLength(0);
});
});
describe("formatQuestionAnswers", () => {
it("AI payload keeps Q+A; display omits prompts", () => {
const questions = normalizeQuestions([

View File

@@ -5,6 +5,7 @@ import {
MockLlmProvider,
} from "../src/llm/client.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";
import { createSession } from "../src/runtime/phase-machine.js";
@@ -22,6 +23,12 @@ describe("main agent tool loop", () => {
listArtifacts: () => [],
};
it("exposes only DAG routing tools", () => {
expect(
MAIN_AGENT_TOOL_DEFINITIONS.map((tool) => tool.function.name),
).toEqual(["read_blackboard", "run_worker"]);
});
it("runs loop tools then returns terminal decision", async () => {
const llm = new MockLlmProvider([
createMockToolCall("read_blackboard", { tags: ["book.brief"] }),

View File

@@ -119,4 +119,114 @@ describe("sanitizeWorkerSetOutputs", () => {
expect(result.askAssessment).toContain("美学纲领 6/10");
expect(result.askAssessment).toContain("还需确认");
});
it("drops askUser copies of fragment 追问 and keeps the 示例 prompt", () => {
const result = parseWorkerResponseForTest(
JSON.stringify({
outputs: {
"设计.美学纲领与交互范式": {
schema: "context-fragment.v1",
brief: "孤立免疫",
: { : { : "特权与惊惶" } },
: {
: "还需确认:",
: [
{
: "你最想反复感受到的是哪一种?",
: ["日常从容", "揭示翻转"],
: "袖口唐纹",
},
],
},
},
},
summary: "美学纲领 · 孤立免疫",
askUser: [
{
prompt: "为贴近你要的质感:你最想反复感受到的是哪一种?",
options: ["日常从容", "揭示翻转"],
},
],
}),
["设计.美学纲领与交互范式"],
);
expect(result.askUser).toHaveLength(1);
expect(result.askUser?.[0]?.prompt).toContain("示例:袖口唐纹");
expect(result.askUser?.[0]?.prompt).not.toContain("为贴近你要的质感");
});
it("recovers a root-level context-fragment into the artifact tag", () => {
const frag = {
schema: "context-fragment.v1",
: "美学纲领与交互范式",
brief: "孤立免疫",
: {
: { : { : "丧尸", : "唯我免疫" } },
: { : "完全" },
: { : "人人录我" },
},
};
const result = parseWorkerResponseForTest(JSON.stringify(frag), [
"设计.美学纲领与交互范式",
]);
expect(result.outputs["设计.美学纲领与交互范式"]).toContain("人人录我");
expect(result.askUser).toBeUndefined();
});
it("moves a fragment written to 创作.当前步骤 onto the artifact tag", () => {
const frag = {
schema: "context-fragment.v1",
: "美学纲领与交互范式",
brief: "孤立免疫",
: { : { : "特权与惊惶" } },
};
const result = parseWorkerResponseForTest(
JSON.stringify({
outputs: { "创作.当前步骤": frag },
summary: "误写入指针",
}),
["设计.美学纲领与交互范式", "创作.当前步骤"],
);
expect(result.outputs["创作.当前步骤"]).toBeUndefined();
expect(result.outputs["设计.美学纲领与交互范式"]).toContain("特权与惊惶");
});
it("does not accept a fragment without 正文; keeps 追问 for the LLM loop", () => {
const result = parseWorkerResponseForTest(
JSON.stringify({
outputs: {
"设计.美学纲领与交互范式": {
schema: "context-fragment.v1",
: "美学纲领与交互范式",
brief: "在唯一免疫的末日联接中体验绝望掌控感",
mount: "world-simulator",
: {
: "还差站位",
: [{ : "你代入吗?", : ["完全代入", "旁观"] }],
},
},
},
summary: "半残",
}),
["设计.美学纲领与交互范式"],
);
expect(result.outputs["设计.美学纲领与交互范式"]).toBeUndefined();
expect(result.askUser?.length).toBeGreaterThan(0);
expect(result.askUser?.[0]?.prompt).toContain("你代入吗");
});
it("asks to continue the loop when fragment JSON is unusable", () => {
const result = parseWorkerResponseForTest(
JSON.stringify({
outputs: {
"设计.美学纲领与交互范式":
'{"schema":"context-fragment.v1","brief":"孤立免疫","mount":"world-simulator"',
},
summary: "截断",
}),
["设计.美学纲领与交互范式"],
);
expect(result.outputs["设计.美学纲领与交互范式"]).toBeUndefined();
expect(result.askUser?.[0]?.prompt).toContain("还没写出可验收的产物");
});
});