Initial commit
This commit is contained in:
291
src/runtime/orchestrator.ts
Normal file
291
src/runtime/orchestrator.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* @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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user