Files
writing-agent/src/runtime/orchestrator.ts
2026-07-10 08:31:27 +08:00

292 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @deprecated 请使用 PhaseRuntime已含 Skill 流程 + 总管 LLM 循环)。
* 本文件保留供对照测试,新入口见 phase-demo / web / run.ts。
*/
import { randomUUID } from "node:crypto";
import { Blackboard } from "../blackboard/blackboard.js";
import type { LlmProvider } from "../llm/client.js";
import {
DEFAULT_WORKERS,
MainAgent,
} from "../main-agent/main-agent.js";
import {
applyEvent,
createArtifact,
createSession,
} from "../runtime/phase-machine.js";
import type {
AcceptanceMode,
MainAgentDecision,
PhaseEffect,
RuntimeEvent,
RuntimeSession,
} from "../types/runtime.js";
export type RuntimeOrchestratorOptions = {
presetId?: string;
flowId?: string;
llm: LlmProvider;
blackboard?: Blackboard;
acceptanceMode?: AcceptanceMode;
onMessage?: (message: string) => void;
};
export class RuntimeOrchestrator {
private session: RuntimeSession;
private readonly blackboard: Blackboard;
private readonly mainAgent: MainAgent;
private readonly acceptanceMode: AcceptanceMode;
private readonly onMessage: (message: string) => void;
constructor(options: RuntimeOrchestratorOptions) {
this.session = createSession(options.presetId ?? "default", options.flowId);
this.blackboard = options.blackboard ?? new Blackboard();
this.mainAgent = new MainAgent(options.llm);
this.acceptanceMode = options.acceptanceMode ?? "user_confirmed";
this.onMessage = options.onMessage ?? (() => {});
}
getSession(): RuntimeSession {
return this.session;
}
getBlackboard(): Blackboard {
return this.blackboard;
}
async start(): Promise<RuntimeSession> {
await this.dispatch({
type: "session_started",
payload: {
presetId: this.session.presetId,
flowId: this.session.flowId,
availableSkills: [{ name: "basic", description: "fallback", category: "novel" }],
},
});
return this.session;
}
async submitUserInput(text: string): Promise<RuntimeSession> {
await this.dispatch({
type: "user_submitted_input",
payload: { text },
});
return this.session;
}
async approveNextStep(): Promise<RuntimeSession> {
const decisionId = this.session.pendingDecision?.id;
if (!decisionId) {
throw new Error("No pending decision to approve");
}
await this.dispatch({
type: "user_approved_next_step",
payload: { decisionId },
});
return this.session;
}
async rejectNextStep(reason?: string): Promise<RuntimeSession> {
const decisionId = this.session.pendingDecision?.id;
if (!decisionId) {
throw new Error("No pending decision to reject");
}
await this.dispatch({
type: "user_rejected_next_step",
payload: { decisionId, reason },
});
return this.session;
}
async acceptArtifact(artifactId?: string): Promise<RuntimeSession> {
const id = artifactId ?? this.session.pendingArtifactId;
if (!id) {
throw new Error("No pending artifact to accept");
}
await this.dispatch({
type: "user_accepted_artifact",
payload: { artifactId: id },
});
return this.session;
}
async rejectArtifact(reason?: string, artifactId?: string): Promise<RuntimeSession> {
const id = artifactId ?? this.session.pendingArtifactId;
if (!id) {
throw new Error("No pending artifact to reject");
}
await this.dispatch({
type: "user_rejected_artifact",
payload: { artifactId: id, reason },
});
return this.session;
}
async completeFlow(): Promise<RuntimeSession> {
await this.dispatch({ type: "flow_completed", payload: {} });
return this.session;
}
private async dispatch(event: RuntimeEvent): Promise<void> {
let result = applyEvent(this.session, event);
this.session = result.session;
if (result.error && event.type !== "runtime_failed") {
this.onMessage(result.error);
}
await this.processEffects(result.effects);
if (result.effects.some((e) => e.type === "invoke_main_agent")) {
await this.runMainAgent();
}
}
private async processEffects(effects: PhaseEffect[]): Promise<void> {
for (const effect of effects) {
switch (effect.type) {
case "emit_message":
this.onMessage(effect.message);
break;
case "run_worker":
await this.runStubWorker(effect);
break;
case "resume_worker":
await this.resumeStubWorker();
break;
case "run_programmatic_review":
await this.runProgrammaticReview(effect.artifactId);
break;
case "invoke_main_agent":
break;
}
}
}
private async runMainAgent(): Promise<void> {
if (this.session.phase !== "running") {
return;
}
const decision = await this.mainAgent.decide({
session: this.session,
blackboardIndex: this.blackboard.listTagIndex(),
availableWorkers: [...DEFAULT_WORKERS],
});
this.onMessage(`[总管] ${decision.action}: ${decision.reason}`);
const result = applyEvent(this.session, {
type: "main_agent_decision_created",
payload: { decision },
});
this.session = result.session;
await this.processEffects(result.effects);
if (result.effects.some((e) => e.type === "invoke_main_agent")) {
await this.runMainAgent();
}
}
private async runStubWorker(
effect: Extract<PhaseEffect, { type: "run_worker" }>,
): Promise<void> {
await this.executeStubWorker(effect.workerId);
}
private async resumeStubWorker(): Promise<void> {
const ctx = this.session.resumeContext;
if (!ctx) {
throw new Error("No resume context");
}
await this.executeStubWorker(ctx.workerId);
}
private async executeStubWorker(workerId: string): Promise<void> {
const stepId = this.session.currentStepId;
this.session = applyEvent(this.session, {
type: "worker_started",
payload: {
workerId,
stepId,
acceptanceMode: this.session.resumeContext?.acceptanceMode ?? this.acceptanceMode,
},
}).session;
this.onMessage(`[Worker 占位] 正在执行 ${workerId} ...`);
const lastInput = String(this.session.slots.lastUserInput ?? "");
const summary = `${workerId} 针对用户输入生成的占位产物`;
const value = `[${workerId} 占位输出]\n用户输入: ${lastInput}\n说明: 这是第一版 stub worker后续会替换为真实 LLM worker。`;
const outputTags = ["output.草稿"];
for (const tag of outputTags) {
this.blackboard.write({
tag,
content: value,
source: workerId,
});
}
const artifact = createArtifact({
workerId,
stepId,
outputTags,
summary,
});
this.session = {
...this.session,
artifacts: [...this.session.artifacts, artifact],
};
const completed = applyEvent(this.session, {
type: "worker_completed",
payload: { artifactId: artifact.id },
});
this.session = completed.session;
await this.processEffects(completed.effects);
if (completed.effects.some((e) => e.type === "invoke_main_agent")) {
await this.runMainAgent();
}
}
private async runProgrammaticReview(artifactId: string): Promise<void> {
this.session = applyEvent(this.session, {
type: "programmatic_review_started",
payload: { artifactId },
}).session;
const artifact = this.session.artifacts.find((a) => a.id === artifactId);
const passed = Boolean(artifact?.summary && artifact.summary.length > 0);
const reviewEvent: RuntimeEvent = passed
? { type: "programmatic_review_passed", payload: { artifactId } }
: {
type: "programmatic_review_failed",
payload: { artifactId, reason: "产物 summary 为空" },
};
const result = applyEvent(this.session, reviewEvent);
this.session = result.session;
await this.processEffects(result.effects);
if (result.effects.some((e) => e.type === "invoke_main_agent")) {
await this.runMainAgent();
}
}
}
export function createDecision(
partial: Omit<MainAgentDecision, "id" | "statePatchAllowed"> &
Partial<Pick<MainAgentDecision, "id">>,
): MainAgentDecision {
return {
id: partial.id ?? randomUUID(),
statePatchAllowed: false,
...partial,
};
}