Initial commit

This commit is contained in:
2026-07-10 08:31:27 +08:00
commit 2b74c30d36
134 changed files with 21801 additions and 0 deletions
+191
View File
@@ -0,0 +1,191 @@
import { randomUUID } from "node:crypto";
import type { LlmProvider } from "../llm/client.js";
import type { BlackboardTagIndex } from "../types/blackboard.js";
import type { MainAgentDecision, RuntimeSession } from "../types/runtime.js";
import { runMainAgentToolLoop, type ToolLoopHandlers } from "./tool-loop.js";
export type MainAgentContext = {
session: RuntimeSession;
blackboardIndex: BlackboardTagIndex[];
availableWorkers: Array<{ id: string; description: string }>;
};
export type MainAgentRunOptions = {
handlers: ToolLoopHandlers;
};
function buildMainAgentSystemPrompt(
workers: Array<{ id: string; description: string }>,
): string {
const workerLines =
workers.length > 0
? workers.map((w) => `- ${w.id}:${w.description}`).join("\n")
: "- (当前 skill 未加载 worker 列表)";
return `你是写作系统的总管(Main Agent)。你的职责是调度 worker,而不是直接创作正文。
规则:
1. 你不能直接生成小说/文章正文。
2. 你不能修改运行状态;statePatchAllowed 必须始终为 false。
3. 你只能建议下一步动作:ask_user、run_worker、create_temp_worker、review_blackboard、finish。
4. 当信息不足时,使用 ask_user 向用户提问。
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。总管只在调度层提问。
10. blackboardIndex 只有 tag 索引,不含正文 content。
当前 skill 可用 worker(workerId 必须与下列 id 完全一致):
${workerLines}
输出必须是 JSON 对象,字段:
{
"action": "ask_user" | "run_worker" | "create_temp_worker" | "review_blackboard" | "finish",
"reason": "string",
"workerId": "string | null",
"requiresApproval": boolean,
"workerContext": { "roleId": "string" } | null
}`;
}
export class MainAgent {
constructor(private readonly llm: LlmProvider) {}
/** @deprecated 单次 JSON 决策;请用 runToolLoop */
async decide(context: MainAgentContext): Promise<MainAgentDecision> {
const userPrompt = buildMainAgentUserPrompt(context);
const systemPrompt = buildMainAgentSystemPrompt(context.availableWorkers);
const result = await this.llm.complete(
[
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },
],
{ responseFormat: "json_object", caller: "main_agent" },
);
return parseMainAgentDecision(result.content);
}
/** running 相位:tool loop 直到终止 tool */
async runToolLoop(
context: MainAgentContext,
options: MainAgentRunOptions,
): Promise<MainAgentDecision> {
const { decision } = await runMainAgentToolLoop(
this.llm,
context,
options.handlers,
);
return decision;
}
}
export function buildMainAgentUserPrompt(context: MainAgentContext): string {
const { session, blackboardIndex, availableWorkers } = context;
return JSON.stringify(
{
runtimePhase: session.phase,
waitingReason: session.waitingReason,
flowId: session.flowId,
currentStepId: session.currentStepId,
currentWorkerId: session.currentWorkerId,
acceptanceMode: session.acceptanceMode,
slots: session.slots,
pendingDecision: session.pendingDecision
? {
id: session.pendingDecision.id,
action: session.pendingDecision.action,
reason: session.pendingDecision.reason,
}
: null,
pendingArtifactId: session.pendingArtifactId,
artifacts: session.artifacts.map((a) => ({
id: a.id,
workerId: a.workerId,
status: a.status,
summary: a.summary,
outputTags: a.outputTags,
})),
blackboardIndex,
availableWorkers,
instruction:
"根据当前状态决定下一步。若 collecting_input 或 revision_requested,优先理解用户最新输入。若 planning 且已有足够信息,建议 run_worker(仅 workerId)。",
},
null,
2,
);
}
export function parseMainAgentDecision(raw: string): MainAgentDecision {
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
throw new Error(`Main Agent returned invalid JSON: ${raw.slice(0, 200)}`);
}
if (!parsed || typeof parsed !== "object") {
throw new Error("Main Agent decision must be an object");
}
const obj = parsed as Record<string, unknown>;
const action = obj.action;
if (
action !== "ask_user" &&
action !== "run_worker" &&
action !== "create_temp_worker" &&
action !== "review_blackboard" &&
action !== "finish"
) {
throw new Error(`Invalid action: ${String(action)}`);
}
const reason = typeof obj.reason === "string" ? obj.reason : "";
if (!reason) {
throw new Error("Main Agent decision requires reason");
}
const workerId =
typeof obj.workerId === "string"
? obj.workerId
: obj.workerId === null || obj.workerId === undefined
? undefined
: undefined;
let workerContext: MainAgentDecision["workerContext"];
const ctxRaw = obj.workerContext;
if (ctxRaw && typeof ctxRaw === "object" && !Array.isArray(ctxRaw)) {
const roleIdRaw = (ctxRaw as Record<string, unknown>).roleId;
const roleId = typeof roleIdRaw === "string" ? roleIdRaw.trim() : undefined;
if (roleId) workerContext = { roleId };
}
return {
id: randomUUID(),
action,
reason,
workerId,
workerContext,
requiresApproval: Boolean(obj.requiresApproval),
statePatchAllowed: false,
};
}
export const DEFAULT_WORKERS = [
{
id: "write-rules",
description: "规则怪谈:内部 core.danger + 护命规则 rules.draft + 解析",
},
{
id: "outline",
description: "根据 book.brief 生成 outline.draft",
},
{
id: "review-infer",
description: "读者视角验收 rules(不含 core.danger)",
},
{
id: "review-author",
description: "作者视角验收 rules 与 core 一致性",
},
] as const;
+173
View File
@@ -0,0 +1,173 @@
import type { ChatMessage, LlmProvider, ParsedToolCall } from "../llm/client.js";
import { toolCallToDecision, validateLoopToolCall } from "../runtime/tool-registry.js";
import type { MainAgentDecision } from "../types/runtime.js";
import { isMainAgentTerminalTool } from "../types/tools.js";
import { MAIN_AGENT_TOOL_DEFINITIONS } from "./tools.js";
import { buildMainAgentUserPrompt, parseMainAgentDecision } from "./main-agent.js";
import type { MainAgentContext } from "./main-agent.js";
export type ToolLoopHandlers = {
readBlackboard: (tags: string[]) => Record<string, string>;
listWorkers: () => Array<{ id: string; description: string }>;
listArtifacts: () => Array<{
id: string;
workerId: string;
status: string;
summary?: string;
outputTags: string[];
}>;
onToolCall?: (name: string, detail: string) => void;
};
export type ToolLoopResult = {
decision: MainAgentDecision;
iterations: number;
toolTrace: string[];
};
const MAX_TOOL_LOOP_ITERATIONS = 12;
function buildToolLoopSystemPrompt(
workers: Array<{ id: string; description: string }>,
): string {
const workerLines =
workers.length > 0
? workers.map((w) => `- ${w.id}:${w.description}`).join("\n")
: "- (当前 skill 未加载 worker 列表)";
return `你是写作系统的总管(Main Agent)。你在 running 相位通过 **tool call** 推进流程。
规则:
1. 你不能直接生成小说/文章正文。
2. 你不能修改运行状态。
3. 先用 read_blackboard / list_workers / list_artifacts 收集信息,再决定下一步。
4. 终止动作只能用 tool:ask_user、run_worker、review_blackboard、finish。
5. run_worker 只传 workerId;inputTags/outputTags 由 Runtime 从 Worker Skill 读取。
6. requiresApproval=true 时 run_worker 需用户确认后再执行。
7. run_worker 可选 roleId,用于 role-decide 等指定当前决策角色。
8. 不能把未验收产物当作已定事实。
当前 skill 可用 worker:
${workerLines}
在信息足够前可多次调用 read_blackboard 等;一旦调用终止 tool,本轮循环结束。`;
}
function executeLoopTool(
call: ParsedToolCall,
handlers: ToolLoopHandlers,
): string {
const name = validateLoopToolCall(call);
const args = JSON.parse(call.arguments || "{}") as Record<string, unknown>;
switch (name) {
case "read_blackboard": {
const tags = Array.isArray(args.tags)
? args.tags.filter((t): t is string => typeof t === "string")
: [];
if (tags.length === 0) {
return JSON.stringify({ error: "tags must be a non-empty string array" });
}
return JSON.stringify(handlers.readBlackboard(tags));
}
case "list_workers":
return JSON.stringify(handlers.listWorkers());
case "list_artifacts":
return JSON.stringify(handlers.listArtifacts());
default:
return JSON.stringify({ error: `Unhandled loop tool: ${name}` });
}
}
function assistantMessageFromToolCalls(
content: string | null,
toolCalls: ParsedToolCall[],
): ChatMessage {
return {
role: "assistant",
content,
tool_calls: toolCalls.map((tc) => ({
id: tc.id,
type: "function" as const,
function: { name: tc.name, arguments: tc.arguments },
})),
};
}
/**
* 总管 tool loop:在 running 相位内可多轮调用 read_blackboard 等,
* 直到调用终止 tool 并返回 MainAgentDecision。
*/
export async function runMainAgentToolLoop(
llm: LlmProvider,
context: MainAgentContext,
handlers: ToolLoopHandlers,
): Promise<ToolLoopResult> {
const messages: ChatMessage[] = [
{
role: "system",
content: buildToolLoopSystemPrompt(context.availableWorkers),
},
{ role: "user", content: buildMainAgentUserPrompt(context) },
];
const toolTrace: string[] = [];
for (let iteration = 1; iteration <= MAX_TOOL_LOOP_ITERATIONS; iteration++) {
const result = await llm.completeWithTools(messages, {
tools: MAIN_AGENT_TOOL_DEFINITIONS,
caller: "main_agent",
});
if (result.toolCalls.length === 0) {
if (result.content?.trim()) {
const decision = parseMainAgentDecision(result.content);
return { decision, iterations: iteration, toolTrace };
}
throw new Error("Main Agent returned no tool calls and no content");
}
const terminalCalls = result.toolCalls.filter((tc) =>
isMainAgentTerminalTool(tc.name),
);
const loopCalls = result.toolCalls.filter(
(tc) => !isMainAgentTerminalTool(tc.name),
);
if (terminalCalls.length > 1) {
throw new Error(
`Main Agent returned multiple terminal tools: ${terminalCalls.map((t) => t.name).join(", ")}`,
);
}
messages.push(
assistantMessageFromToolCalls(result.content, result.toolCalls),
);
if (terminalCalls.length === 1) {
const terminal = terminalCalls[0];
handlers.onToolCall?.(terminal.name, terminal.arguments);
toolTrace.push(`${terminal.name} (terminal)`);
return {
decision: toolCallToDecision(terminal),
iterations: iteration,
toolTrace,
};
}
for (const call of loopCalls) {
const output = executeLoopTool(call, handlers);
handlers.onToolCall?.(call.name, output.slice(0, 200));
toolTrace.push(call.name);
messages.push({
role: "tool",
content: output,
tool_call_id: call.id,
});
}
}
throw new Error(
`Main Agent tool loop exceeded ${MAX_TOOL_LOOP_ITERATIONS} iterations`,
);
}
+121
View File
@@ -0,0 +1,121 @@
import type { ToolDefinition } from "../llm/client.js";
/** 总管可用 tool 的 OpenAI function 定义 */
export const MAIN_AGENT_TOOL_DEFINITIONS: ToolDefinition[] = [
{
type: "function",
function: {
name: "read_blackboard",
description:
"读取黑板 tag 正文。tags 可为精确 tag 或带 * 前缀模式。调度前用此了解已有内容。",
parameters: {
type: "object",
properties: {
tags: {
type: "array",
items: { type: "string" },
description: "要读取的 tag 或模式列表",
},
},
required: ["tags"],
additionalProperties: false,
},
},
},
{
type: "function",
function: {
name: "list_workers",
description: "列出当前 skill 可调度的 worker id 与说明。",
parameters: {
type: "object",
properties: {},
additionalProperties: false,
},
},
},
{
type: "function",
function: {
name: "list_artifacts",
description: "列出当前会话 worker 产物(id、workerId、status、summary)。",
parameters: {
type: "object",
properties: {},
additionalProperties: false,
},
},
},
{
type: "function",
function: {
name: "ask_user",
description: "信息不足时向用户提问,将暂停 agent 循环等待用户输入。",
parameters: {
type: "object",
properties: {
reason: { type: "string", description: "为何需要用户输入" },
message: { type: "string", description: "展示给用户的问题或说明" },
},
required: ["reason"],
additionalProperties: false,
},
},
},
{
type: "function",
function: {
name: "run_worker",
description:
"调度 worker 执行任务。只传 workerId,inputTags/outputTags 由 Runtime 从 Worker Skill 读取。",
parameters: {
type: "object",
properties: {
workerId: { type: "string" },
reason: { type: "string" },
requiresApproval: {
type: "boolean",
description: "true 时需用户确认后才执行",
},
roleId: {
type: "string",
description: "role-decide 等 worker 的当前角色 id",
},
},
required: ["workerId", "reason", "requiresApproval"],
additionalProperties: false,
},
},
},
{
type: "function",
function: {
name: "review_blackboard",
description: "向用户说明当前黑板与进度概况,并暂停等待用户回复。",
parameters: {
type: "object",
properties: {
reason: { type: "string" },
summary: { type: "string", description: "给用户看的概况说明" },
},
required: ["reason", "summary"],
additionalProperties: false,
},
},
},
{
type: "function",
function: {
name: "finish",
description: "正常结束当前创作流程。",
parameters: {
type: "object",
properties: {
reason: { type: "string" },
},
required: ["reason"],
additionalProperties: false,
},
},
},
];