重构世界模拟器为模块化配方架构,完善创作编排、会话运行时与 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
+34 -4
View File
@@ -1,5 +1,6 @@
import { randomUUID } from "node:crypto";
import type { LlmProvider } from "../llm/client.js";
import { normalizeQuestions } from "../skills/question-protocol.js";
import type { BlackboardTagIndex } from "../types/blackboard.js";
import type { MainAgentDecision, RuntimeSession } from "../types/runtime.js";
import { runMainAgentToolLoop, type ToolLoopHandlers } from "./tool-loop.js";
@@ -22,27 +23,46 @@ function buildMainAgentSystemPrompt(
? workers.map((w) => `- ${w.id}:${w.description}`).join("\n")
: "- (当前 skill 未加载 worker 列表)";
return `你是写作系统的总管(Main Agent)。你的职责是调度 worker,而不是直接创作正文。
return `你是写作系统的导演(Main Agent)。你的职责是调度演员(worker),而不是直接创作正文。
规则:
1. 你不能直接生成小说/文章正文。
2. 你不能修改运行状态;statePatchAllowed 必须始终为 false。
3. 你只能建议下一步动作:ask_user、run_worker、create_temp_worker、review_blackboard、finish。
4. 当信息不足时,使用 ask_user 向用户提问。
4. 当信息不足时,使用 ask_user:assessment 是主内容(完备度评价);questions 挂在其下且用户可跳过;一次 1~2 题。
5. 当需要执行任务时,使用 run_worker,只指定 workerId。不要指定 inputTags 或 outputTags——Runtime 从 Worker Skill 读取。
6. requiresApproval 表示运行 worker 前是否需要用户确认。代笔模式通常为 true。
7. run_worker 可选 workerContext:{ "roleId": "A" },用于 role-decide 等指定当前决策角色(Runtime 写入 世界.当前角色.id)。
8. 你不能把未验收内容当作事实。
9. 向用户提问是 worker 的能力(ask_user tool),不是独立 worker。总管只在调度层提问。
9. 向用户提问是 worker 的能力(ask_user tool),不是独立 worker。导演只在调度层提问。
10. blackboardIndex 只有 tag 索引,不含正文 content。
## 调度思维
用需求正推:「用户需要 [具体体验/能力] → 调用 [worker/skill] 来 [生成/补充/调整] [什么],以便更好满足用户。」
禁止否定式路由:「某模式 / 背景形态 → 不需要某步骤 / 跳过某 worker」。
反例(禁止):「背景为单一角色,无需世界构建」「不是规则怪谈,跳过 write-rules」「默认上世界模拟套件」
正例:「用户已选世界模拟器导演且要网恋对话 → design-flow 排出近期增量步骤(可调味、可后补)→ 用户认可后反复 design-step;需要再补规则/实例 → 再 design-flow 追加同能力 → 收成后若需开局 → opening-generator;用户手动进 play」
当前 skill 可用 worker(workerId 必须与下列 id 完全一致):
${workerLines}
## design 优先顺序
- 尚无「设计.创作流程」→ **design-flow**(近期 horizon;status=open;未选导演则先请用户选)
- 流程已验收且还有未完成步骤 → **design-step**(一次一步;能力由程序注入)
- 已列步骤都验收但流程仍 **status=open** → 再 **design-flow**(追加反复步或 closed)
- 禁止调度已废弃的 design-core / design-fixed / design-worker / design-refine
- 终稿已 accept 且可用 opening-generator、尚无开场产物 → opening-generator
- 进 play 由用户手动决定
- **禁止**替用户猜测或改选导演
- **禁止**一次编排排死全程固定 DAG
输出必须是 JSON 对象,字段:
{
"action": "ask_user" | "run_worker" | "create_temp_worker" | "review_blackboard" | "finish",
"reason": "string",
"assessment": "string | null",
"questions": [{ "id": "q1", "prompt": "…", "options": [{ "label": "建议示范…" }] }] | null,
"workerId": "string | null",
"requiresApproval": boolean,
"workerContext": { "roleId": "string" } | null
@@ -109,7 +129,7 @@ export function buildMainAgentUserPrompt(context: MainAgentContext): string {
blackboardIndex,
availableWorkers,
instruction:
"根据当前状态决定下一步。若 collecting_input 或 revision_requested,优先理解用户最新输入。若 planning 且已有足够信息,建议 run_worker(仅 workerId)。",
"根据当前状态决定下一步。尚无设计.创作流程 → design-flow;有未完成步骤 → design-step;steps 做完但 status=open → 再 design-flow;禁止旧 design-core/fixed/worker/refine。已 accept 且需开局 → opening-generator。进 play 由用户手动。",
},
null,
2,
@@ -160,10 +180,20 @@ export function parseMainAgentDecision(raw: string): MainAgentDecision {
if (roleId) workerContext = { roleId };
}
const assessmentRaw =
typeof obj.assessment === "string"
? obj.assessment.trim()
: typeof obj.message === "string"
? obj.message.trim()
: "";
const questions = normalizeQuestions(obj.questions);
return {
id: randomUUID(),
action,
reason,
assessment: assessmentRaw || undefined,
questions: questions.length ? questions : undefined,
workerId,
workerContext,
requiresApproval: Boolean(obj.requiresApproval),
+90 -9
View File
@@ -1,4 +1,5 @@
import type { ChatMessage, LlmProvider, ParsedToolCall } from "../llm/client.js";
import type { ChatMessage, LlmProvider, ParsedToolCall, StreamCallbacks } from "../llm/client.js";
import { supportsToolStream } from "../llm/stream-complete.js";
import { toolCallToDecision, validateLoopToolCall } from "../runtime/tool-registry.js";
import type { MainAgentDecision } from "../types/runtime.js";
import { isMainAgentTerminalTool } from "../types/tools.js";
@@ -17,6 +18,10 @@ export type ToolLoopHandlers = {
outputTags: string[];
}>;
onToolCall?: (name: string, detail: string) => void;
/** 思维链 / reasoning 流式增量(供 UI 实时展示) */
onThinkingDelta?: (delta: string) => void;
/** 一轮 LLM 调用结束后的完整思考文本 */
onThinkingDone?: (text: string) => void;
};
export type ToolLoopResult = {
@@ -37,20 +42,84 @@ function buildToolLoopSystemPrompt(
return `你是写作系统的总管(Main Agent)。你在 running 相位通过 **tool call** 推进流程。
规则:
## 能力边界
1. 你不能直接生成小说/文章正文。
2. 你不能修改运行状态。
3. 先用 read_blackboard / list_workers / list_artifacts 收集信息,再决定下一步。
4. 终止动作只能用 tool:ask_user、run_worker、review_blackboard、finish。
- ask_user:**主内容是 assessment(内容完备度评价)**;questions 挂在其下且可被用户跳过。
· assessment(必填):给用户看的评价正文。以【核心体验】为首;按需列维度(内容/人物关系/感官/背景规则/意义主题…),不必凑齐。每维写完备度%、已知、待探;待探用【】标互斥或可组合方向。
· questions(必填数组,语义可选):只针对 assessment「待探」里想确认的点;一次 1~2 题。用户可不答、直接让你基于现有信息继续。prompt=明确题干;options=2~4 条**建议示范**;required 默认 false。
· reason:调度短句,正推,勿塞长文。
· 能推断就别问;已写在 用户.需求 里的不要重复问。
5. run_worker 只传 workerId;inputTags/outputTags 由 Runtime 从 Worker Skill 读取。
6. requiresApproval=true 时 run_worker 需用户确认后再执行。
7. run_worker 可选 roleId,用于 role-decide 等指定当前决策角色。
8. 不能把未验收产物当作已定事实。
7. 不能把未验收产物当作已定事实。
## 调度思维(必须遵守)
用 **需求正推**,禁止 **模式否定式** 表述。
**要这样思考(正推):**
「用户需要 [具体体验/能力/产物] → 因此调用 [worker/skill] 来 [生成/补充/调整] [什么],以便更好满足用户。」
**不要这样思考(反例):**
「这是 xxx 模式 / 形态 → 不需要 xxx 步骤 / 跳过 xxx。」
「背景为单一角色,无需世界构建。」
「不是规则怪谈,跳过 write-rules。」
示例(好):
- 「用户已选导演且要 1v1 网恋 → design-flow 排出近期增量步骤(可调味)→ 认可后反复 design-step;不够再追加。」
- 「尚无设计.创作流程 → run_worker(design-flow);有未完成步骤 → design-step;steps 完但 status=open → 再 design-flow。」
- 「Worker 集已 accept 且声明需要开局 → 调用 opening-generator 写开场白。」
示例(坏):
- 「调度 design-core / design-fixed(已废弃)。」
- 「一次 design-flow 排死全程固定 DAG。」
- 「跳过流程编排直接写满 Worker 集。」
- 「默认先上世界模拟全套再问用户要什么。」
- 「替用户改选 / 猜测导演。」
在 reasoning / 可见 content 中请用 **正推句式** 写出调度理由;终止 tool 的 reason 字段同样用正推表述。
## design 阶段提示
- 尚无「设计.创作流程」→ **优先 design-flow**(近期 horizon;status=open;未选则先请用户选)。
- 流程已验收、还有未完成步骤 → **design-step**(一次一步)。
- 已列步骤全验收但 **status=open** → 再 **design-flow**(追加生成规则/具体实例等,或设 closed)。
- 禁止 run_worker(design-core|design-fixed|design-worker|design-refine)——已废弃。
- 不要重复问用户「想做什么」——意图已在 用户.需求。
- Worker 集已 accept,且可用 worker 含 opening-generator,尚无开场产物 → opening-generator。
- 进 play 由用户手动决定。
- 未声明开局、用户也未要求开场时,不要硬调 opening-generator。
- **禁止**替用户猜测或改选导演。
- **禁止**一次编排排死全程固定长链。
当前 skill 可用 worker:
${workerLines}
在信息足够前可多次调用 read_blackboard 等;一旦调用终止 tool,本轮循环结束。`;
信息不足时可多次 read_blackboard;一旦调用终止 tool,本轮循环结束。`;
}
async function completeToolsPreferStream(
llm: LlmProvider,
messages: ChatMessage[],
callbacks: StreamCallbacks,
): Promise<Awaited<ReturnType<LlmProvider["completeWithTools"]>>> {
if (supportsToolStream(llm)) {
return llm.completeWithToolsStream!(messages, {
tools: MAIN_AGENT_TOOL_DEFINITIONS,
caller: "main_agent",
}, callbacks);
}
const result = await llm.completeWithTools(messages, {
tools: MAIN_AGENT_TOOL_DEFINITIONS,
caller: "main_agent",
});
if (result.reasoning) {
callbacks.onReasoningDelta?.(result.reasoning);
}
if (result.content) {
callbacks.onContentDelta?.(result.content);
}
return result;
}
function executeLoopTool(
@@ -94,6 +163,16 @@ function assistantMessageFromToolCalls(
};
}
function emitThinkingDone(handlers: ToolLoopHandlers, parts: {
reasoning?: string;
content?: string | null;
}): void {
const reasoning = parts.reasoning?.trim() ?? "";
const content = parts.content?.trim() ?? "";
const combined = [reasoning, content].filter(Boolean).join("\n\n").trim();
if (combined) handlers.onThinkingDone?.(combined);
}
/**
* 总管 tool loop:在 running 相位内可多轮调用 read_blackboard 等,
* 直到调用终止 tool 并返回 MainAgentDecision。
@@ -112,12 +191,14 @@ export async function runMainAgentToolLoop(
];
const toolTrace: string[] = [];
const streamCallbacks: StreamCallbacks = {
onReasoningDelta: (delta) => handlers.onThinkingDelta?.(delta),
onContentDelta: (delta) => handlers.onThinkingDelta?.(delta),
};
for (let iteration = 1; iteration <= MAX_TOOL_LOOP_ITERATIONS; iteration++) {
const result = await llm.completeWithTools(messages, {
tools: MAIN_AGENT_TOOL_DEFINITIONS,
caller: "main_agent",
});
const result = await completeToolsPreferStream(llm, messages, streamCallbacks);
emitThinkingDone(handlers, result);
if (result.toolCalls.length === 0) {
if (result.content?.trim()) {
+70 -4
View File
@@ -50,14 +50,80 @@ export const MAIN_AGENT_TOOL_DEFINITIONS: ToolDefinition[] = [
type: "function",
function: {
name: "ask_user",
description: "信息不足时向用户提问,将暂停 agent 循环等待用户输入。",
description:
"向用户追问。assessment 是给用户看的主内容(完备度评价);questions 挂在其下,用户可跳过并请你基于现有信息继续。将暂停等待用户。",
parameters: {
type: "object",
properties: {
reason: { type: "string", description: "为何需要用户输入" },
message: { type: "string", description: "展示给用户的问题或说明" },
reason: {
type: "string",
description:
"调度层短理由(正推:用户需要澄清 X → 因此追问)。勿把长文评价写这里。",
},
assessment: {
type: "string",
description:
"给用户看的内容完备度评价(Markdown)。以【核心体验】为首要;按需列维度(不必凑齐),每维:完备度%、已知、待探。待探用【】标出互斥/可组合方向。评价只服务「用户想要什么」。",
},
message: {
type: "string",
description:
"兼容旧字段:等同 assessment。优先传 assessment。",
},
questions: {
type: "array",
description:
"挂在 assessment 下的可选追问(用户可不答)。基于「待探」出题,一次 1~2 题。prompt=明确选择题干;options=建议示范(可改写后采用)。",
items: {
type: "object",
properties: {
id: {
type: "string",
description: "稳定 id,如 q1 / stance",
},
prompt: {
type: "string",
description:
"题干:引导用户选定一种详细体验倾向(例:「你更倾向于哪种皇帝的享受?」)。",
},
options: {
type: "array",
description:
"2~4 个建议示范。label 写完整可采纳文案(可先一句场景钩子再点题),不是标签词。",
items: {
type: "object",
properties: {
id: {
type: "string",
description: "A/B/C…",
},
label: {
type: "string",
description:
"建议正文:用户点选后可直接当答案,也可改写。例:「冰冷的、主宰一切的权力感——九重宫阙一言定生死…」",
},
editable: {
type: "boolean",
description: "默认 true:点文案可改写,点字母才选中",
},
},
required: ["label"],
},
},
allowOther: {
type: "boolean",
description: "默认 true:允许 Other 自拟",
},
required: {
type: "boolean",
description: "默认 false(可选追问);仅关键阻塞题才 true",
},
},
required: ["prompt", "options"],
},
},
},
required: ["reason"],
required: ["reason", "assessment", "questions"],
additionalProperties: false,
},
},