Initial commit
This commit is contained in:
@@ -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;
|
||||
@@ -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`,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user