重构世界模拟器为模块化配方架构,完善创作编排、会话运行时与 Web UI,并清理过时技能。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-30 00:39:32 +08:00
parent 2b74c30d36
commit e670a5129c
167 changed files with 22955 additions and 5659 deletions

View File

@@ -2,50 +2,39 @@ import { describe, expect, it } from "vitest";
import { createDecision } from "../src/runtime/orchestrator.js";
import {
applyEvent,
canApplyEvent,
createArtifact,
createSession,
} from "../src/runtime/phase-machine.js";
import { loadSkill } from "../src/skills/loader.js";
import { toActiveSkillSnapshot } from "../src/skills/snapshot.js";
const mockSkills = [{ name: "basic", description: "基础", category: "novel" }];
const mockSkills = [
{ name: "world-simulator", description: "默认", category: "dialogue" },
];
describe("phase machine", () => {
it("runs worker loop after skill and startup input", async () => {
const { loadSkill } = await import("../src/skills/loader.js");
const { toActiveSkillSnapshot } = await import("../src/skills/snapshot.js");
const snap = toActiveSkillSnapshot(await loadSkill("basic"));
it("runs design-core approve loop on agent-first pack", async () => {
const snap = toActiveSkillSnapshot(await loadSkill("world-simulator"));
let session = createSession("default");
session = applyEvent(session, {
let session = applyEvent(createSession("default"), {
type: "session_started",
payload: { presetId: "default", availableSkills: mockSkills },
payload: {
presetId: "default",
availableSkills: mockSkills,
initialSkill: snap,
},
}).session;
session = applyEvent(session, {
type: "skill_selected",
payload: { skill: snap },
}).session;
const values: Record<string, string> = {};
for (const f of snap.intakeFields) {
values[f.id] = "科幻短篇,第一人称";
}
session = applyEvent(session, {
type: "user_submitted_input",
payload: { text: "科幻短篇,第一人称", intakeValues: values },
}).session;
expect(session.waitingReason?.kind).toBe("intake");
session = applyEvent(session, {
type: "user_confirmed_intake",
payload: {},
payload: { text: "1v1 网恋对话" },
}).session;
expect(session.phase).toBe("running");
expect(session.slots["book.brief"]).toBeTruthy();
const decision = createDecision({
action: "run_worker",
reason: "生成大纲",
workerId: "outline-worker",
reason: "产出 Worker 集",
workerId: "design-core",
requiresApproval: true,
});
session = applyEvent(session, {
@@ -60,12 +49,12 @@ describe("phase machine", () => {
}).session;
session = applyEvent(session, {
type: "worker_started",
payload: { workerId: "outline-worker", acceptanceMode: "user_confirmed" },
payload: { workerId: "design-core", acceptanceMode: "user_confirmed" },
}).session;
const artifact = createArtifact({
workerId: "outline-worker",
outputTags: ["outline.draft"],
workerId: "design-core",
outputTags: ["设计.worker集"],
});
session = { ...session, artifacts: [artifact] };
session = applyEvent(session, {
@@ -78,95 +67,96 @@ describe("phase machine", () => {
type: "user_accepted_artifact",
payload: { artifactId: artifact.id },
}).session;
expect(session.slots.designInstanceReady).toBe(true);
expect(session.phase).toBe("running");
session = applyEvent(session, { type: "flow_completed", payload: {} }).session;
expect(session.phase).toBe("done");
});
it("rejects illegal events from idle", () => {
const session = createSession("default");
expect(canApplyEvent(session, { type: "user_submitted_input", payload: { text: "x" } })).toBe(
false,
);
const result = applyEvent(session, {
type: "user_submitted_input",
payload: { text: "x" },
});
expect(result.session.phase).toBe("error");
});
it("merges follow-up input after intake confirm via ask_user", async () => {
const { createDecision } = await import("../src/runtime/orchestrator.js");
const snap = toActiveSkillSnapshot(await loadSkill("roleplay-game-theory"));
it("hangs optional questions under review_artifact without blocking accept", () => {
let session = createSession("default");
session = {
...session,
phase: "running" as const,
acceptanceMode: "user_confirmed" as const,
};
const artifact = createArtifact({
workerId: "design-core",
outputTags: ["设计.worker集.草稿"],
});
session = { ...session, artifacts: [artifact] };
session = applyEvent(session, {
type: "session_started",
payload: { presetId: "default", availableSkills: mockSkills },
}).session;
session = applyEvent(session, {
type: "skill_selected",
payload: { skill: snap },
type: "worker_completed",
payload: {
artifactId: artifact.id,
questions: [
{
id: "q1",
prompt: "更偏哪种节奏?",
options: [{ id: "A", label: "慢热拉扯" }],
},
],
},
}).session;
const fields = snap.intakeFields;
const values: Record<string, string> = {};
for (const f of fields.filter((x) => x.required)) {
values[f.id] = "已填";
}
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);
// 不答追问,直接接受产物
session = applyEvent(session, {
type: "user_submitted_input",
payload: { text: "首次", intakeValues: values },
type: "user_accepted_artifact",
payload: { artifactId: artifact.id },
}).session;
expect(session.phase).toBe("running");
});
it("sidecar skip clears questions but stays in review", () => {
let session = createSession("default");
session = {
...session,
phase: "running" as const,
acceptanceMode: "user_confirmed" as const,
};
const artifact = createArtifact({
workerId: "design-core",
outputTags: ["设计.worker集.草稿"],
});
session = { ...session, artifacts: [artifact] };
session = applyEvent(session, {
type: "user_confirmed_intake",
type: "worker_completed",
payload: {
artifactId: artifact.id,
questions: ["还想补一点感官细节吗?"],
},
}).session;
session = applyEvent(session, {
type: "user_resolved_sidecar_questions",
payload: {},
}).session;
expect(session.slots.startupCompleted).toBe(true);
session = applyEvent(session, {
type: "main_agent_decision_created",
payload: {
decision: createDecision({
action: "ask_user",
reason: "输出偏好?",
}),
},
}).session;
expect(session.waitingReason?.kind).toBe("input");
session = applyEvent(session, {
type: "user_submitted_input",
payload: { text: "需要思考标签,带场景描写" },
}).session;
const demand = String(session.slots["用户.博弈需求"]);
expect(demand).toContain("需要思考标签");
expect(session.waitingReason?.kind).toBe("review_artifact");
if (session.waitingReason?.kind !== "review_artifact") return;
expect(session.waitingReason.questions).toBeUndefined();
expect(session.pendingArtifactId).toBe(artifact.id);
});
it("emits one follow-up when roleplay intake required fields incomplete", async () => {
const snap = toActiveSkillSnapshot(await loadSkill("roleplay-game-theory"));
let session = createSession("default");
session = applyEvent(session, {
type: "session_started",
payload: { presetId: "default", availableSkills: mockSkills },
}).session;
session = applyEvent(session, {
type: "skill_selected",
payload: { skill: snap },
}).session;
const scenarioField = snap.intakeFields.find((f) => f.label.includes("情境"));
expect(scenarioField).toBeDefined();
const result = applyEvent(session, {
type: "user_submitted_input",
payload: {
text: "德州扑克",
intakeValues: { [scenarioField!.id]: "德州扑克" },
},
it("accepting design-core with only draft does not set designInstanceReady", () => {
let session = createSession();
const artifact = createArtifact({
workerId: "design-core",
outputTags: ["设计.worker集.草稿", "创作.当前单位"],
});
expect(result.effects.some((e) => e.type === "emit_message")).toBe(true);
const msg = result.effects.find((e) => e.type === "emit_message");
expect(msg && "message" in msg && msg.message).toContain("还缺以下必要项");
expect(result.session.slots.intakeFollowUpSent).toBe(true);
session = {
...session,
artifacts: [artifact],
pendingArtifactId: artifact.id,
phase: "waiting_user",
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
};
session = applyEvent(session, {
type: "user_accepted_artifact",
payload: { artifactId: artifact.id },
}).session;
expect(session.slots.designInstanceReady).toBeUndefined();
});
});