Steer generation rules toward direction pools and readable fills.

Direction-first pools, recipes, and grade manifestation replace enum-tag stuffing; spawned products can be deleted and in-flight runs aborted.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-08 01:02:32 +08:00
co-authored by Cursor
parent 3c751988de
commit e2446b0d97
23 changed files with 2023 additions and 292 deletions
+468 -1
View File
@@ -3,12 +3,36 @@ import {
createMockMainAgentResponse,
createMockToolCall,
MockLlmProvider,
type LlmProvider,
} from "../src/llm/client.js";
import { PhaseRuntime, createDecision } from "../src/runtime/phase-runtime.js";
import { createSession } from "../src/runtime/phase-machine.js";
import { createArtifact, createSession } from "../src/runtime/phase-machine.js";
import { Blackboard } from "../src/blackboard/blackboard.js";
import { parseCreationFlow } from "../src/skills/creation-flow.js";
function countLlmCalls(llm: MockLlmProvider): { calls: () => number } {
let n = 0;
const bump = () => {
n += 1;
};
const origComplete = llm.complete.bind(llm);
llm.complete = (async (messages, options) => {
bump();
return origComplete(messages, options);
}) as LlmProvider["complete"];
const origStream = llm.completeStream.bind(llm);
llm.completeStream = (async (messages, options, callbacks) => {
bump();
return origStream(messages, options, callbacks);
}) as LlmProvider["completeStream"];
const origTools = llm.completeWithTools.bind(llm);
llm.completeWithTools = (async (messages, options) => {
bump();
return origTools(messages, options);
}) as LlmProvider["completeWithTools"];
return { calls: () => n };
}
describe("phase runtime", () => {
it("auto-loads default orchestrator and awaits first user input", async () => {
const runtime = new PhaseRuntime();
@@ -178,6 +202,120 @@ describe("phase runtime", () => {
expect(runtime.getSession().waitingReason?.kind).toBe("pick_creation_step");
});
it("picking a node shows module opening instead of calling LLM", async () => {
const llm = new MockLlmProvider([
JSON.stringify({
outputs: { "设计.舞台骨架": { 范围: "不该到这一步" } },
summary: "不应生成",
askUser: null,
}),
]);
const llmCalls = countLlmCalls(llm);
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.enterSeededCreationPick();
await runtime.pickCreationStep("舞台骨架");
const session = runtime.getSession();
expect(session.phase).toBe("waiting_user");
expect(session.waitingReason?.kind).toBe("worker_questions");
if (session.waitingReason?.kind === "worker_questions") {
expect(session.waitingReason.questions[0]?.id).toBe("module-opening");
expect(session.waitingReason.questions[0]?.prompt).toContain(
"玩家实际会待着玩的地方",
);
}
expect(llmCalls.calls()).toBe(0);
});
it("picking a root node still shows opening even if 用户.需求 already has text", async () => {
const llm = new MockLlmProvider([
JSON.stringify({
outputs: { "设计.舞台骨架": { 范围: "不该到这一步" } },
summary: "不应生成",
askUser: null,
}),
]);
const llmCalls = countLlmCalls(llm);
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",
});
runtime.getBlackboard().write({
tag: "用户.需求",
content: "丧尸世界只有我不会被感染",
source: "user",
});
runtime.getSession().slots["用户.需求"] = "丧尸世界只有我不会被感染";
await runtime.enterSeededCreationPick();
await runtime.pickCreationStep("舞台骨架");
expect(runtime.getSession().waitingReason?.kind).toBe("worker_questions");
expect(llmCalls.calls()).toBe(0);
});
it("leaving during opening and picking again shows opening instead of calling LLM", async () => {
const llm = new MockLlmProvider([
JSON.stringify({
outputs: { "设计.舞台骨架": { 范围: "不该到这一步" } },
summary: "不应生成",
askUser: null,
}),
]);
const llmCalls = countLlmCalls(llm);
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.enterSeededCreationPick();
await runtime.pickCreationStep("舞台骨架");
expect(runtime.getSession().waitingReason?.kind).toBe("worker_questions");
await runtime.leaveCreationStep();
expect(runtime.getSession().waitingReason?.kind).toBe("pick_creation_step");
await runtime.pickCreationStep("舞台骨架");
expect(runtime.getSession().waitingReason?.kind).toBe("worker_questions");
expect(llmCalls.calls()).toBe(0);
});
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 });
@@ -294,6 +432,335 @@ describe("phase runtime", () => {
).toEqual(["美学纲领与交互范式"]);
});
it("leaveCreationStep from review_artifact returns to pick without unsawning a filled instance", async () => {
const artifact = createArtifact({
workerId: "design-step",
stepId: "生成规则#1",
outputTags: ["设计.生成规则"],
});
const session = createSession("default");
session.phase = "waiting_user";
session.waitingReason = { kind: "review_artifact", artifactId: artifact.id };
session.pendingArtifactId = artifact.id;
session.currentWorkerId = "design-step";
session.currentStepId = "生成规则#1";
session.artifacts = [artifact];
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: ["美学纲领与交互范式"],
},
],
}),
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().pendingArtifactId).toBeUndefined();
const flow = parseCreationFlow(
runtime.getBlackboard().getContentByTag("设计.创作流程"),
);
expect(flow?.steps.map((s) => s.id)).toContain("生成规则#1");
});
it("abortCurrentRun restores the snapshot and waits instead of rerunning", async () => {
const runtime = new PhaseRuntime({ autoStubWorker: true });
await runtime.start();
await runtime.submitInput("网恋对象对话");
await runtime.submitDecision(
createDecision({
action: "run_worker",
reason: "编排创作流程",
workerId: "design-flow",
requiresApproval: false,
}),
);
const snap = runtime.getLastWorkerRunSnapshot();
expect(snap?.workerId).toBe("design-flow");
const artifactsAtSnapshot = snap!.runtimeSession.artifacts.length;
const session = runtime.getSession();
session.phase = "running";
session.waitingReason = undefined;
session.currentWorkerId = "design-flow";
runtime.abortCurrentRun();
expect(runtime.getSession().phase).toBe("waiting_user");
expect(runtime.getSession().currentWorkerId).toBeUndefined();
expect(runtime.getSession().artifacts.length).toBe(artifactsAtSnapshot);
});
it("reentering an accepted instance reopens its artifact for revision", async () => {
const artifact = {
...createArtifact({
workerId: "design-step",
stepId: "生成规则#1",
outputTags: ["设计.生成规则"],
}),
status: "accepted",
};
const session = createSession("default");
session.phase = "waiting_user";
session.waitingReason = { kind: "pick_creation_step" };
session.slots = {
startupCompleted: true,
creationAcceptedUnits: ["生成规则#1"],
};
session.artifacts = [artifact];
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: ["美学纲领与交互范式"],
params: { target: "4位核心女性" },
},
],
}),
source: "runtime",
});
blackboard.write({
tag: "创作.已验收单位",
content: JSON.stringify(["生成规则#1"]),
source: "runtime",
});
blackboard.write({
tag: "设计.生成规则",
content: "已有规则正文",
source: "runtime",
});
const runtime = new PhaseRuntime({
initialSession: session,
initialBlackboardItems: blackboard.exportItems(),
});
await runtime.pickCreationStep("生成规则#1", undefined, { reenter: true });
const next = runtime.getSession();
expect(next.waitingReason?.kind).toBe("review_artifact");
if (next.waitingReason?.kind === "review_artifact") {
expect(next.waitingReason.artifactId).toBe(artifact.id);
}
expect(next.pendingArtifactId).toBe(artifact.id);
expect(next.currentStepId).toBe("生成规则#1");
expect(next.phase).toBe("waiting_user");
expect(runtime.getBlackboard().getContentByTag("创作.继承修改")).toBe("1");
expect(next.slots.revisionTargetArtifactId).toBe(artifact.id);
expect(
next.artifacts.find((a) => a.id === artifact.id)?.status,
).toBe("under_review");
});
it("revising a reentered accepted instance does not bounce to the node graph", async () => {
const artifact = {
...createArtifact({
workerId: "design-step",
stepId: "生成规则#1",
outputTags: ["设计.生成规则"],
}),
status: "accepted",
};
const session = createSession("default");
session.phase = "waiting_user";
session.waitingReason = { kind: "pick_creation_step" };
session.slots = {
startupCompleted: true,
creationAcceptedUnits: ["生成规则#1"],
};
session.artifacts = [artifact];
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: ["美学纲领与交互范式"],
params: { target: "4位核心女性" },
},
],
}),
source: "runtime",
});
blackboard.write({
tag: "创作.已验收单位",
content: JSON.stringify(["生成规则#1"]),
source: "runtime",
});
blackboard.write({
tag: "设计.生成规则",
content: "已有规则正文",
source: "runtime",
});
const runtime = new PhaseRuntime({
initialSession: session,
initialBlackboardItems: blackboard.exportItems(),
});
await runtime.pickCreationStep("生成规则#1", undefined, { reenter: true });
await runtime.rejectArtifact("把对象改成四位男性");
const next = runtime.getSession();
expect(next.waitingReason?.kind).not.toBe("pick_creation_step");
expect(next.phase).toBe("running");
expect(next.currentWorkerId).toBe("design-step");
expect(next.slots.revisionTargetArtifactId).toBe(artifact.id);
});
it("deleteCreationStep removes an accepted repeatable instance", async () => {
const artifact = {
...createArtifact({
workerId: "design-step",
stepId: "生成规则#1",
outputTags: ["设计.生成规则"],
}),
status: "accepted",
};
const session = createSession("default");
session.phase = "waiting_user";
session.waitingReason = { kind: "pick_creation_step" };
session.slots = {
startupCompleted: true,
creationAcceptedUnits: ["美学纲领与交互范式", "生成规则#1"],
};
session.artifacts = [artifact];
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: ["美学纲领与交互范式"],
params: { target: "4位核心女性", rule_id: "core-female-roles" },
},
{
id: "开场白与开场变量",
name: "开场白与开场变量",
depends_on: ["美学纲领与交互范式", "生成规则#1"],
},
],
}),
source: "runtime",
});
blackboard.write({
tag: "创作.已验收单位",
content: JSON.stringify(["美学纲领与交互范式", "生成规则#1"]),
source: "runtime",
});
blackboard.write({
tag: "设计.生成规则",
content: JSON.stringify({
schema: "context-fragment.v1",
技能: "生成规则",
brief: "女臣",
正文: { rules: [{ rule_id: "core-female-roles", 对象: "女臣" }] },
}),
source: "runtime",
});
const runtime = new PhaseRuntime({
initialSession: session,
initialBlackboardItems: blackboard.exportItems(),
});
await runtime.deleteCreationStep("生成规则#1");
const flow = parseCreationFlow(
runtime.getBlackboard().getContentByTag("设计.创作流程"),
);
expect(flow?.steps.map((s) => s.id)).toEqual([
"美学纲领与交互范式",
"生成规则",
"开场白与开场变量",
]);
expect(
flow?.steps.find((s) => s.name === "开场白与开场变量")?.depends_on,
).toEqual(["美学纲领与交互范式"]);
expect(runtime.getSession().slots.creationAcceptedUnits).toEqual([
"美学纲领与交互范式",
]);
expect(
runtime.getSession().artifacts.find((a) => a.id === artifact.id)?.status,
).toBe("rejected");
await expect(
runtime.deleteCreationStep("美学纲领与交互范式"),
).rejects.toThrow(/可增殖/);
});
it("prepareEnterPlay seals when closer is already accepted", () => {
const session = createSession("default");
const blackboard = new Blackboard();