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

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-30 00:39:32 +08:00
co-authored by Cursor
parent 2b74c30d36
commit e670a5129c
167 changed files with 22955 additions and 5659 deletions
+235
View File
@@ -0,0 +1,235 @@
import type { Blackboard } from "../blackboard/blackboard.js";
/** 下一 worker / 总管可读的定稿摘要 tag */
export const CONTEXT_BRIEF_TAG = "上下文.定稿摘要";
export type CompressWorkerResult = {
archivedTags: string[];
finals: Array<{ tag: string; chars: number; preview: string }>;
briefText: string;
};
const ALWAYS_ARCHIVE_AFTER_ACCEPT = ["用户.worker答复", "用户.修订说明"];
/**
* Worker 产物经用户验收(或自动完工)后:
* - 终产物打 final
* - 过程/草稿 tag 归档(不再进入后续 worker 取数)
* - 写入定稿摘要,供下一阶段「指点」用
*/
export function compressAfterWorkerAccept(params: {
blackboard: Blackboard;
workerId: string;
outputTags: string[];
summary?: string;
/**
* final = 终稿验收(归档草稿、写定稿摘要)
* unit = 创作单位验收(只归档过程答复,保留 设计.worker集.草稿)
*/
mode?: "final" | "unit";
}): CompressWorkerResult {
const { blackboard, workerId, outputTags, summary } = params;
const mode = params.mode ?? "final";
const finals: CompressWorkerResult["finals"] = [];
const archivedTags: string[] = [];
const uniqueOutputs = [...new Set(outputTags.map((t) => t.trim()).filter(Boolean))];
if (mode === "final") {
for (const tag of uniqueOutputs) {
const content = blackboard.getContentByTag(tag);
if (content == null) continue;
blackboard.write({
tag,
content,
source: workerId,
metadata: {
role: "final",
acceptedAt: new Date().toISOString(),
workerId,
},
});
finals.push({
tag,
chars: content.length,
preview: previewText(content, 120),
});
}
} else {
// 单位验收:草稿保持活跃,终产物列表仅作面板提示
for (const tag of uniqueOutputs) {
if (tag === "用户.需求" || tag === "创作.当前单位") continue;
const content = blackboard.getContentByTag(tag);
if (content == null) continue;
finals.push({
tag,
chars: content.length,
preview: previewText(content, 120),
});
}
}
const toArchive = new Set<string>(ALWAYS_ARCHIVE_AFTER_ACCEPT);
if (mode === "final") {
if (blackboard.getContentByTag("设计.worker集")?.trim()) {
toArchive.add("设计.worker集.草稿");
}
for (const tag of uniqueOutputs) {
if (tag.endsWith(".草稿")) continue;
const draft = `${tag}.草稿`;
if (blackboard.getContentByTag(draft) != null) toArchive.add(draft);
}
}
for (const tag of toArchive) {
if (uniqueOutputs.includes(tag) && mode === "final") continue;
if (mode === "unit" && tag.endsWith(".草稿")) continue;
const content = blackboard.getContentByTag(tag);
if (content == null) continue;
blackboard.write({
tag,
content: `(已压缩归档)原过程内容已折叠。定稿见:${
finals.map((f) => f.tag).join("、") || "(无)"
}\n\n---\n${previewText(content, 400)}`,
source: "compress",
metadata: {
role: "archived",
archivedAt: new Date().toISOString(),
fromWorker: workerId,
compressMode: mode,
},
});
archivedTags.push(tag);
}
const briefText =
mode === "unit"
? buildUnitBriefText({ workerId, summary, finals, archivedTags })
: buildBriefText({
workerId,
summary,
finals,
archivedTags,
});
if (mode === "final") {
blackboard.write({
tag: CONTEXT_BRIEF_TAG,
content: briefText,
source: "compress",
metadata: { role: "final", workerId },
});
}
return { archivedTags, finals, briefText };
}
function buildUnitBriefText(params: {
workerId: string;
summary?: string;
finals: CompressWorkerResult["finals"];
archivedTags: string[];
}): string {
return [
`## 创作单位已验收(${params.workerId})`,
"",
params.summary?.trim() ? `摘要:${params.summary.trim()}` : "摘要:(无)",
"",
"草稿仍保留在 `设计.worker集.草稿`;下一单位继续增量,勿依赖已删的过程对话。",
].join("\n");
}
function buildBriefText(params: {
workerId: string;
summary?: string;
finals: CompressWorkerResult["finals"];
archivedTags: string[];
}): string {
const lines = [
`## 上一阶段定稿(${params.workerId})`,
"",
params.summary?.trim()
? `摘要:${params.summary.trim()}`
: "摘要:(无)",
"",
"### 保留的终产物 tag",
];
if (params.finals.length === 0) {
lines.push("- (无)");
} else {
for (const f of params.finals) {
lines.push(`- \`${f.tag}\`(${f.chars} 字)`);
lines.push(` ${f.preview}`);
}
}
if (params.archivedTags.length) {
lines.push("", "### 已压缩的过程 tag");
for (const t of params.archivedTags) {
lines.push(`- \`${t}\`(归档,后续 worker 默认不读)`);
}
}
lines.push(
"",
"下一 worker 应以以上终产物为准继续;勿依赖已归档的过程讨论。",
);
return lines.join("\n");
}
export function previewText(content: string, max: number): string {
const t = content.replace(/\s+/g, " ").trim();
if (t.length <= max) return t;
return `${t.slice(0, max)}…`;
}
/** 供 UI:黑板可读条目(默认隐藏 archived 全文,只给索引) */
export function buildBoardPanel(blackboard: Blackboard): {
finals: Array<{ tag: string; source: string; preview: string; updatedAt: string }>;
active: Array<{ tag: string; source: string; preview: string; updatedAt: string }>;
archivedCount: number;
brief?: string;
} {
const items = latestItemsByTag(blackboard.exportItems());
const finals: Array<{
tag: string;
source: string;
preview: string;
updatedAt: string;
}> = [];
const active: typeof finals = [];
let archivedCount = 0;
let brief: string | undefined;
for (const item of items.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))) {
const role = String(item.metadata?.role ?? "");
if (item.tag === CONTEXT_BRIEF_TAG) {
brief = item.content;
continue;
}
if (role === "archived") {
archivedCount += 1;
continue;
}
const row = {
tag: item.tag,
source: item.source,
preview: previewText(item.content, 160),
updatedAt: item.updatedAt,
};
if (role === "final") finals.push(row);
else active.push(row);
}
return { finals, active, archivedCount, brief };
}
function latestItemsByTag(
items: import("../types/blackboard.js").BlackboardItem[],
): import("../types/blackboard.js").BlackboardItem[] {
const byTag = new Map<string, (typeof items)[0]>();
for (const item of items) {
const prev = byTag.get(item.tag);
if (!prev || item.updatedAt > prev.updatedAt) byTag.set(item.tag, item);
}
return [...byTag.values()];
}
+1 -1
View File
@@ -60,7 +60,7 @@ export class RuntimeOrchestrator {
payload: {
presetId: this.session.presetId,
flowId: this.session.flowId,
availableSkills: [{ name: "basic", description: "fallback", category: "novel" }],
availableSkills: [{ name: "world-simulator", description: "fallback", category: "dialogue" }],
},
});
return this.session;
+211 -43
View File
@@ -19,22 +19,56 @@ import type {
WaitingReason,
} from "../types/runtime.js";
import type { ActiveSkillSnapshot } from "../types/runtime.js";
import { effectiveStartupMode } from "../config/default-orchestrator.js";
import {
buildIntakeProgress,
buildIntakeFollowUpMessage,
readIntakeValues,
synthesizeDemandText,
} from "../intake/intake.js";
import { normalizeQuestions } from "../skills/question-protocol.js";
import type { QuestionItem } from "../types/questions.js";
function nowIso(): string {
return new Date().toISOString();
}
/** 挂在产物下的追问一律可选(required=false),用户可直接 Accept */
function asOptionalSidecarQuestions(
raw: QuestionItem[] | string[] | undefined,
): QuestionItem[] | undefined {
const normalized = normalizeQuestions(raw ?? []);
if (!normalized.length) return undefined;
return normalized.map((q) => ({ ...q, required: false }));
}
/** 更新 updatedAt 时间戳 */
function touch(session: RuntimeSession): RuntimeSession {
return { ...session, updatedAt: nowIso() };
}
/** 用户每条输入写入 用户.最新输入;首句同时写入需求 tag */
function applyUserTextToSlots(
slots: Record<string, unknown>,
text: string,
demandKey: string,
opts: { initial?: boolean },
): void {
if (!text) return;
slots["用户.最新输入"] = text;
if (opts.initial) {
slots[demandKey] = text;
slots.startupCompleted = true;
return;
}
if (!slots.startupCompleted) {
slots[demandKey] = text;
slots.startupCompleted = true;
return;
}
mergeSlotText(slots, demandKey, text);
}
/** 将用户补充文本追加到 slot(用于合并多轮 ask_user / worker 答复到需求 tag) */
function mergeSlotText(
slots: Record<string, unknown>,
@@ -136,6 +170,7 @@ export function getAllowedEvents(
"user_accepted_artifact",
"user_rejected_artifact",
"user_requested_revision",
"user_resolved_sidecar_questions",
"main_agent_decision_created",
"runtime_failed",
];
@@ -179,6 +214,11 @@ export function canApplyEvent(
case "user_accepted_artifact":
case "user_rejected_artifact":
return reason?.kind === "review_artifact";
case "user_resolved_sidecar_questions":
return (
reason?.kind === "review_artifact" &&
Boolean(reason.questions?.length)
);
case "user_requested_revision":
return (
reason?.kind === "review_artifact" || reason?.kind === "approve_step"
@@ -272,7 +312,7 @@ export function applyEvent(
}
switch (event.type) {
// ── 启动:选 skill ──
// ── 启动:默认 orchestrator 或 legacy 选包 ──
case "session_started": {
const next = appendHistory(
{
@@ -283,40 +323,22 @@ export function applyEvent(
event,
);
const skills = event.payload.availableSkills;
const initialSkill = event.payload.initialSkill;
if (initialSkill) {
return applySkillBinding(next, initialSkill, event);
}
return {
session: waiting(next, {
kind: "skill_selection",
availableSkills: skills,
}),
effects: [
{
type: "emit_message",
message: formatSkillSelectionPrompt(skills),
},
],
effects: [{ type: "emit_message", message: formatSkillSelectionPrompt(skills) }],
};
}
case "skill_selected": {
const { skill } = event.payload;
const next = appendHistory(
{
...session,
flowId: skill.defaultFlowId ?? session.flowId,
slots: {
...session.slots,
activeSkill: skill,
},
},
event,
);
return {
session: waiting(next, {
kind: "intake",
prompt: skill.startupPrompt,
}),
effects: [{ type: "emit_message", message: skill.startupPrompt }],
};
return applySkillBinding(session, skill, event);
}
case "user_confirmed_intake": {
@@ -366,6 +388,21 @@ export function applyEvent(
if (session.waitingReason?.kind === "intake") {
const skill = activeSkill as ActiveSkillSnapshot | undefined;
if (skill && effectiveStartupMode(skill) === "agent-first" && text) {
const key = skill.startupTargetKey || "用户.需求";
applyUserTextToSlots(slots, text, key, { initial: true });
slots.lastUserInput = text;
const next = appendHistory(
{ ...session, slots, resumeContext: undefined },
event,
);
return {
session: touch(running(next)),
effects: [{ type: "invoke_main_agent" }],
};
}
const intakeValues =
event.payload.intakeValues ??
readIntakeValues(session.slots);
@@ -419,16 +456,17 @@ export function applyEvent(
if (session.waitingReason?.kind === "worker_questions") {
slots["用户.worker答复"] = text;
if (text) slots["用户.最新输入"] = text;
if (demandKey && text) {
mergeSlotText(slots, demandKey, text);
}
} else if (demandKey && session.waitingReason?.kind === "input" && text) {
if (!session.slots.startupCompleted) {
slots[demandKey] = text;
slots.startupCompleted = true;
} else {
mergeSlotText(slots, demandKey, text);
}
} else if (session.waitingReason?.kind === "input" && text) {
const key = demandKey || "用户.需求";
applyUserTextToSlots(slots, text, key, {
initial: !session.slots.startupCompleted,
});
} else if (text) {
slots["用户.最新输入"] = text;
}
const next = appendHistory(
{
@@ -465,14 +503,44 @@ export function applyEvent(
switch (decision.action) {
case "ask_user":
case "review_blackboard":
case "review_blackboard": {
const questions = decision.questions?.length
? decision.questions.map((q) => ({ ...q, required: false }))
: undefined;
const assessment =
decision.action === "ask_user"
? decision.assessment?.trim() || undefined
: undefined;
const effects: PhaseEffect[] = [];
if (assessment) {
effects.push({
type: "emit_message",
message: `[Agent] 内容评价:\n${assessment}`,
});
}
if (questions?.length) {
effects.push({
type: "emit_message",
message: `[Agent] 可选追问(可跳过):\n${questions.map((q) => `- ${q.prompt}`).join("\n")}`,
});
}
const inputMessage = assessment
? assessment
: decision.action === "review_blackboard" || !questions?.length
? decision.reason
: undefined;
return {
session: waiting(
{ ...next, pendingDecision: undefined },
{ kind: "input", message: decision.reason },
{
kind: "input",
message: inputMessage,
questions,
},
),
effects: [],
effects,
};
}
case "finish":
return {
session: touch({
@@ -580,13 +648,12 @@ export function applyEvent(
}
case "worker_needs_input": {
const questions = event.payload.questions
.map((q) => q.trim())
.filter(Boolean);
const normalized =
questions.length > 0
? questions
: ["请补充当前步骤所需的信息(情境、参数或你的具体设想)。"];
let normalized = normalizeQuestions(event.payload.questions);
if (normalized.length === 0) {
normalized = normalizeQuestions([
"请补充当前步骤所需的信息(情境、参数或你的具体设想)。",
]);
}
const ctx: ResumeContext = {
workerId: event.payload.workerId,
stepId: event.payload.stepId ?? session.currentStepId,
@@ -608,7 +675,7 @@ export function applyEvent(
effects: [
{
type: "emit_message",
message: `[Worker] ${event.payload.workerId} 提问:\n${normalized.map((q) => `- ${q}`).join("\n")}`,
message: `[Worker] ${event.payload.workerId} 提问:\n${normalized.map((q) => `- ${q.prompt}`).join("\n")}`,
},
],
};
@@ -618,23 +685,64 @@ export function applyEvent(
const result = handleWorkerCompleted(session, event);
const mode = session.acceptanceMode ?? "user_confirmed";
if (mode === "user_confirmed" && result.session.pendingArtifactId) {
const sidecar = asOptionalSidecarQuestions(event.payload.questions);
const effects = [...result.effects];
if (sidecar?.length) {
effects.push({
type: "emit_message",
message: `[Worker] 可选追问(可跳过,直接接受目前产物):\n${sidecar.map((q) => `- ${q.prompt}`).join("\n")}`,
});
}
return {
...result,
effects,
session: waiting(result.session, {
kind: "review_artifact",
artifactId: result.session.pendingArtifactId,
questions: sidecar,
}),
};
}
return result;
}
case "user_resolved_sidecar_questions": {
const reason = session.waitingReason;
if (reason?.kind !== "review_artifact") {
return fail(session, "sidecar questions require review_artifact");
}
const answersText = event.payload.answersText?.trim();
const slots: Record<string, unknown> = { ...session.slots };
const effects: PhaseEffect[] = [];
if (answersText) {
slots["用户.worker答复"] = answersText;
slots["用户.最新输入"] = answersText;
effects.push({
type: "emit_message",
message: `[用户] 已补充可选追问(产物仍待验收)`,
});
}
const next = appendHistory({ ...session, slots }, event);
return {
session: waiting(touch(next), {
kind: "review_artifact",
artifactId: reason.artifactId,
}),
effects,
};
}
// ── 产物验收 ──
case "user_accepted_artifact": {
const artifact = findArtifact(session, event.payload.artifactId);
if (!artifact) {
return fail(session, `Artifact not found: ${event.payload.artifactId}`);
}
const slots: Record<string, unknown> = { ...session.slots };
// 仅终稿 tag「设计.worker集」表示完整 Worker 集验收;草稿单位验收不得开 play
if (artifact.outputTags.some((tag) => tag === "设计.worker集")) {
slots.designInstanceReady = true;
}
const next = appendHistory(
updateArtifact(session, artifact.id, { status: "accepted" }),
event,
@@ -643,6 +751,7 @@ export function applyEvent(
session: touch(
running({
...next,
slots,
pendingArtifactId: undefined,
pendingDecision: undefined,
}),
@@ -791,3 +900,62 @@ function formatSkillSelectionPrompt(
const lines = skills.map((s, i) => ` ${i + 1}. ${s.name} — ${s.description}`);
return ["请选择创作 skill(输入 name 或编号):", ...lines].join("\n");
}
/** agent-first:仅 UI 引导,等用户首句后再 invoke 总管 */
function enterAwaitFirstInput(
session: RuntimeSession,
skill: ActiveSkillSnapshot,
event: RuntimeEvent,
): ApplyEventResult {
const next = appendHistory(
{
...session,
flowId: skill.defaultFlowId ?? session.flowId,
slots: {
...session.slots,
activeSkill: skill,
},
},
event,
);
return {
session: waiting(next, { kind: "input" }),
effects: [],
};
}
function enterIntakeWaiting(
session: RuntimeSession,
skill: ActiveSkillSnapshot,
event: RuntimeEvent,
): ApplyEventResult {
const next = appendHistory(
{
...session,
flowId: skill.defaultFlowId ?? session.flowId,
slots: {
...session.slots,
activeSkill: skill,
},
},
event,
);
return {
session: waiting(next, {
kind: "intake",
prompt: skill.startupPrompt,
}),
effects: [{ type: "emit_message", message: skill.startupPrompt }],
};
}
function applySkillBinding(
session: RuntimeSession,
skill: ActiveSkillSnapshot,
event: RuntimeEvent,
): ApplyEventResult {
if (effectiveStartupMode(skill) === "agent-first") {
return enterAwaitFirstInput(session, skill, event);
}
return enterIntakeWaiting(session, skill, event);
}
File diff suppressed because it is too large Load Diff
+7 -2
View File
@@ -1,6 +1,7 @@
import { randomUUID } from "node:crypto";
import type { ParsedToolCall } from "../llm/client.js";
import type { MainAgentDecision } from "../types/runtime.js";
import { normalizeQuestions } from "../skills/question-protocol.js";
import {
isMainAgentLoopTool,
isMainAgentTerminalTool,
@@ -60,11 +61,15 @@ export function toolCallToDecision(call: ParsedToolCall): MainAgentDecision {
switch (name as MainAgentToolName) {
case "ask_user": {
const reason = requireString(args, "reason");
const message = optionalString(args, "message");
const assessment =
optionalString(args, "assessment") ?? optionalString(args, "message");
const questions = normalizeQuestions(args.questions);
return {
id: randomUUID(),
action: "ask_user",
reason: message ? `${reason}\n${message}` : reason,
reason,
assessment: assessment || undefined,
questions: questions.length ? questions : undefined,
requiresApproval: false,
statePatchAllowed: false,
};