去掉裁决包主路径,让无转述时可直出终稿、有转述时再改写镶壳。 Co-authored-by: Cursor <cursoragent@cursor.com>
3348 lines
110 KiB
TypeScript
3348 lines
110 KiB
TypeScript
/**
|
||
* 阶段机运行层(有副作用的执行器)。
|
||
*
|
||
* 职责:
|
||
* - 持有可变 session,统一 dispatch(event) 入口
|
||
* - 调用纯函数 applyEvent,再 processEffects 执行 IO(消息、worker、验收)
|
||
* - 启动时从 skills/ 加载 registry,驱动 skill 选择与 SKILL.md 启动询问
|
||
*
|
||
* 与 phase-machine.ts 的分工:前者「算状态」,本文件「跑状态」。
|
||
*/
|
||
import { randomUUID } from "node:crypto";
|
||
import {
|
||
applyEvent,
|
||
createArtifact,
|
||
createSession,
|
||
getAllowedEvents,
|
||
} from "./phase-machine.js";
|
||
import { Blackboard } from "../blackboard/blackboard.js";
|
||
import type { LlmProvider } from "../llm/client.js";
|
||
import {
|
||
DEFAULT_WORKERS,
|
||
isUnusableMainAgentOutput,
|
||
MainAgent,
|
||
} from "../main-agent/main-agent.js";
|
||
import { listSkills, listWorkerSkills, loadSkill, resolveSkillId } from "../skills/loader.js";
|
||
import {
|
||
buildInstanceWorkerDeclaration,
|
||
formatUndeclaredWorkerError,
|
||
inferLifecycleStage,
|
||
mergeDeclaredWorkersForAgent,
|
||
readWorkerSetYamlForDeclaration,
|
||
shouldEnforceWorkerDeclaration,
|
||
} from "../skills/worker-declaration.js";
|
||
import { resolveDefaultOrchestratorId } from "../config/default-orchestrator.js";
|
||
import { toActiveSkillSnapshot } from "../skills/snapshot.js";
|
||
import { runWorkerSkill } from "../worker/executor.js";
|
||
import { resolveWorkerId } from "../worker/resolve-id.js";
|
||
import { resolveWorkerLlmProvider } from "../skills/worker-llm.js";
|
||
import {
|
||
DIALOGUE_HISTORY_TAG,
|
||
appendDialogueHistoryTurn,
|
||
} from "../skills/dialogue-history.js";
|
||
import { extractIntakeFromMessage } from "../intake/extract.js";
|
||
import { readIntakeValues } from "../intake/intake.js";
|
||
import { compressAfterWorkerAccept } from "./compress-after-worker.js";
|
||
import {
|
||
CREATION_ACCEPTED_UNITS_TAG,
|
||
CREATION_CURRENT_UNIT_TAG,
|
||
CREATION_ACCEPTED_CONTENT_TAG,
|
||
WORKER_SET_DRAFT_TAG,
|
||
WORKER_SET_FINAL_TAG,
|
||
SLOT_CREATION_ACCEPTED_UNITS,
|
||
SLOT_CREATION_CURRENT_UNIT,
|
||
SLOT_CREATION_UNIT_ANCHOR_AT,
|
||
extractUnitContentFromDraft,
|
||
isDesignDiskWorker,
|
||
isDesignUnitArtifact,
|
||
normalizeCreationUnitId,
|
||
parseAcceptedContentStore,
|
||
parseAcceptedUnits,
|
||
dropAcceptedUnitContent,
|
||
upsertAcceptedUnitContent,
|
||
} from "../skills/creation-units.js";
|
||
import {
|
||
CREATION_FLOW_TAG,
|
||
CREATION_CURRENT_STEP_TAG,
|
||
CREATION_PROPOSED_STEP_TAG,
|
||
CREATION_INHERIT_EXISTING_TAG,
|
||
CREATION_MODULE_OPENING_TAG,
|
||
CREATION_MODULE_OPENING_STATE_TAG,
|
||
SLOT_CREATION_MODULE_OPENING_STATE,
|
||
loadModuleCatalog,
|
||
isReviseStep,
|
||
isStepAccepted,
|
||
isInheritExistingFlag,
|
||
mergeCreationFlowPreservingAccepted,
|
||
nextPendingStep,
|
||
listReadySteps,
|
||
listSelectablePrototypes,
|
||
listRepeatableSpawns,
|
||
isStepReady,
|
||
hasSelectableCreationWork,
|
||
spawnRepeatableCreationStep,
|
||
spawnInstanceFromPrototype,
|
||
removeUnstartedInstance,
|
||
removeRepeatableInstance,
|
||
pruneRepeatableArtifactContent,
|
||
isPrototypeStep,
|
||
isPrototypeSelectable,
|
||
isInstanceStep,
|
||
findStepByRef,
|
||
findModuleByName,
|
||
hasAcceptedCloserStep,
|
||
parseCreationFlow,
|
||
DESIGN_FLOW_WORKER_ID,
|
||
DESIGN_STEP_WORKER_ID,
|
||
parseModuleOpeningState,
|
||
patchCreationFlowStepParams,
|
||
pickRecordedStepId,
|
||
resolveDesignStepBinding,
|
||
isProgressPointerTag,
|
||
missingRequiredStepParams,
|
||
stepUnitId,
|
||
stringifyCreationFlow,
|
||
stringifyModuleOpeningState,
|
||
finalizeCreationFlow,
|
||
type CreationFlow,
|
||
type CreationFlowStep,
|
||
type CreationFlowStepParams,
|
||
type ModuleCatalog,
|
||
type ModuleCatalogEntry,
|
||
type ModuleParamSpec,
|
||
} from "../skills/creation-flow.js";
|
||
import {
|
||
appendAskedQuestions,
|
||
dropAlreadyAskedQuestions,
|
||
MODULE_OPENING_QUESTION_ID,
|
||
normalizeQuestions,
|
||
parseAskedQuestions,
|
||
SLOT_ASKED_QUESTIONS,
|
||
} from "../skills/question-protocol.js";
|
||
import { parseWorkerSetYaml } from "../skills/worker-set-parse.js";
|
||
import { defaultPlaySlots, isNarratorEnabled } from "../skills/play-slots.js";
|
||
import {
|
||
mergeOpeningTablePatch,
|
||
OPENING_CURRENT_VARS_TAG,
|
||
OPENING_INITIAL_VARS_TAG,
|
||
OPENING_OUTPUT_TAG,
|
||
OPENING_SETUP_ARTIFACT_TAG,
|
||
parseOpeningSealPayload,
|
||
reopenCreationFlowRaw,
|
||
SLOT_CREATION_SEALED_BY_OPENING,
|
||
SLOT_OPENING_SELECTED_INDEX,
|
||
} from "../skills/opening-seal.js";
|
||
import {
|
||
CREATION_INTAKE_MODE_TAG,
|
||
isDictateModeValue,
|
||
} from "../dictate/types.js";
|
||
import { applyDictatePlayBind } from "../dictate/play-bind.js";
|
||
import {
|
||
VARIABLE_CATALOG_TAG,
|
||
seedVariablesFromCatalog,
|
||
} from "../skills/variable-catalog.js";
|
||
import {
|
||
VALUE_MAP_TAG,
|
||
reprojectValueMaps,
|
||
} from "../skills/value-map.js";
|
||
import {
|
||
readPlayTurnQueue,
|
||
withPlayTurnQueue,
|
||
isPlayLayerActive,
|
||
} from "../skills/play-turn.js";
|
||
import {
|
||
PRESENT_TAG,
|
||
isPlayFinalVisibleWorker,
|
||
isPlayPresentWorker,
|
||
parseShellAdaptationFromReplyFormat,
|
||
presentOutputFromFallback,
|
||
} from "../skills/present-packet.js";
|
||
import { debugLog, labelAction, logStateChange, sessionSnap } from "../log.js";
|
||
import {
|
||
executeChance,
|
||
executeChanceBatch,
|
||
resolveChanceBatchRequest,
|
||
resolveChanceRequest,
|
||
} from "../skills/chance-tools.js";
|
||
import {
|
||
resolveAcceptanceModeForWorker,
|
||
resolveRunnableWorker,
|
||
} from "../skills/declared-worker.js";
|
||
import {
|
||
mergeTableCells,
|
||
parseTableDoc,
|
||
stringifyTableDoc,
|
||
type TableDoc,
|
||
} from "../blackboard/table-cells.js";
|
||
import {
|
||
SIDE_EFFECT_FIRED_TAG,
|
||
applySideEffectTagActions,
|
||
evaluateSideEffects,
|
||
parseFiredRegistry,
|
||
parseSideEffectRules,
|
||
stringifyFiredRegistry,
|
||
} from "../blackboard/table-side-effects.js";
|
||
import {
|
||
SETTLEMENT_TAG,
|
||
parseSettlementPacket,
|
||
settlementChangesToTablePatch,
|
||
} from "../skills/settlement-packet.js";
|
||
import {
|
||
MAINTAIN_TAG,
|
||
maintainOpsToTablePatch,
|
||
parseMaintainPacket,
|
||
} from "../skills/maintain-packet.js";
|
||
import {
|
||
MAINTAIN_GENERATE_TAG,
|
||
runMaintainNeedGenerateSampling,
|
||
} from "../skills/maintain-need-generate.js";
|
||
import {
|
||
mountResidentContextForWorker as writeResidentContextTags,
|
||
parseResidentContext,
|
||
} from "../skills/resident-context.js";
|
||
import type {
|
||
AcceptanceMode,
|
||
ActiveSkillSnapshot,
|
||
ApplyEventResult,
|
||
ArtifactRecord,
|
||
MainAgentDecision,
|
||
PhaseEffect,
|
||
RuntimeEvent,
|
||
RuntimeSession,
|
||
SkillIndexEntry,
|
||
} from "../types/runtime.js";
|
||
import type { BlackboardItem } from "../types/blackboard.js";
|
||
|
||
/** 每轮开始前清掉的 transient tag(避免主世界层读到上一轮裁决/旁观包) */
|
||
const PLAY_ROUND_TRANSIENT_TAGS = [
|
||
SETTLEMENT_TAG,
|
||
MAINTAIN_TAG,
|
||
MAINTAIN_GENERATE_TAG,
|
||
"运行.本轮.机遇",
|
||
"运行.本轮.变量变更",
|
||
] as const;
|
||
|
||
export type PhaseRuntimeOptions = {
|
||
presetId?: string;
|
||
flowId?: string;
|
||
/** worker 完成后的默认验收模式,会写入 worker_started 事件 */
|
||
acceptanceMode?: AcceptanceMode;
|
||
/** true 时 run_worker 自动走占位 worker(演示 / 测试用) */
|
||
autoStubWorker?: boolean;
|
||
/** 传入后 invoke_main_agent 会自动调用总管 LLM */
|
||
llm?: LlmProvider;
|
||
/** 运行时黑板;worker 产出写入此处 */
|
||
blackboard?: Blackboard;
|
||
/** 副作用 emit_message 与用户提示的回调(Web / CLI 接入点) */
|
||
onMessage?: (message: string) => void;
|
||
/** 总管 reasoning 流式增量 */
|
||
onAgentThinkingDelta?: (delta: string) => void;
|
||
/** 一轮总管 LLM 思考完成 */
|
||
onAgentThinkingDone?: (text: string) => void;
|
||
/** worker 开始流式输出 */
|
||
onWorkerStreamStart?: (workerId: string) => void;
|
||
/** worker 思维链流式增量 */
|
||
onWorkerThinkingDelta?: (workerId: string, delta: string) => void;
|
||
/** worker 正文/JSON 流式增量 */
|
||
onWorkerOutputDelta?: (workerId: string, delta: string) => void;
|
||
/** worker 流式结束 */
|
||
onWorkerStreamDone?: (workerId: string) => void;
|
||
/** 从磁盘恢复时使用,跳过 createSession */
|
||
initialSession?: RuntimeSession;
|
||
/** 与 initialSession 一并恢复黑板 */
|
||
initialBlackboardItems?: BlackboardItem[];
|
||
};
|
||
|
||
/**
|
||
* 阶段机运行层:包装纯函数 phase-machine,处理副作用。
|
||
* 启动时加载 skills/,先选 skill,再读 SKILL.md 启动询问。
|
||
*/
|
||
export class PhaseRuntime {
|
||
private session: RuntimeSession;
|
||
private readonly acceptanceMode: AcceptanceMode;
|
||
private autoStubWorker: boolean;
|
||
private readonly onMessage: (message: string) => void;
|
||
private readonly onAgentThinkingDelta?: (delta: string) => void;
|
||
private readonly onAgentThinkingDone?: (text: string) => void;
|
||
private readonly onWorkerStreamStart?: (workerId: string) => void;
|
||
private readonly onWorkerThinkingDelta?: (workerId: string, delta: string) => void;
|
||
private readonly onWorkerOutputDelta?: (workerId: string, delta: string) => void;
|
||
private readonly onWorkerStreamDone?: (workerId: string) => void;
|
||
private blackboard: Blackboard;
|
||
private llm?: LlmProvider;
|
||
private mainAgent?: MainAgent;
|
||
/** autoStubWorker=false 时,run_worker effect 暂存于此,等 startPendingWorker() */
|
||
private pendingWorkerEffect: Extract<PhaseEffect, { type: "run_worker" }> | null =
|
||
null;
|
||
/** 最近一次 worker 启动前的快照,供 UI 重 roll 使用 */
|
||
private lastWorkerRunSnapshot: {
|
||
workerId: string;
|
||
runtimeSession: RuntimeSession;
|
||
blackboardItems: BlackboardItem[];
|
||
} | null = null;
|
||
/** start() 时从 registry 加载;legacy skill_selection 会话仍用编号解析 */
|
||
private availableSkills: SkillIndexEntry[] = [];
|
||
|
||
constructor(options: PhaseRuntimeOptions = {}) {
|
||
this.session =
|
||
options.initialSession ??
|
||
createSession(options.presetId ?? "default", options.flowId);
|
||
this.acceptanceMode = options.acceptanceMode ?? "user_confirmed";
|
||
this.autoStubWorker = options.autoStubWorker ?? false;
|
||
this.onMessage = options.onMessage ?? (() => {});
|
||
this.onAgentThinkingDelta = options.onAgentThinkingDelta;
|
||
this.onAgentThinkingDone = options.onAgentThinkingDone;
|
||
this.onWorkerStreamStart = options.onWorkerStreamStart;
|
||
this.onWorkerThinkingDelta = options.onWorkerThinkingDelta;
|
||
this.onWorkerOutputDelta = options.onWorkerOutputDelta;
|
||
this.onWorkerStreamDone = options.onWorkerStreamDone;
|
||
this.blackboard = options.blackboard ?? new Blackboard();
|
||
if (options.initialBlackboardItems?.length) {
|
||
this.blackboard.seed(options.initialBlackboardItems);
|
||
}
|
||
if (options.llm) {
|
||
this.llm = options.llm;
|
||
this.mainAgent = new MainAgent(options.llm);
|
||
}
|
||
}
|
||
|
||
getBlackboard(): Blackboard {
|
||
return this.blackboard;
|
||
}
|
||
|
||
hasMainAgent(): boolean {
|
||
return Boolean(this.mainAgent);
|
||
}
|
||
|
||
/** 设置变更后热更新 LLM(API profile / preset) */
|
||
reloadLlm(llm: LlmProvider, autoStubWorker?: boolean): void {
|
||
this.llm = llm;
|
||
this.mainAgent = new MainAgent(llm);
|
||
if (autoStubWorker !== undefined) {
|
||
this.autoStubWorker = autoStubWorker;
|
||
}
|
||
}
|
||
|
||
getLlm(): LlmProvider | undefined {
|
||
return this.llm;
|
||
}
|
||
|
||
getSession(): RuntimeSession {
|
||
return this.session;
|
||
}
|
||
|
||
getLastWorkerRunSnapshot(): {
|
||
workerId: string;
|
||
runtimeSession: RuntimeSession;
|
||
blackboardItems: BlackboardItem[];
|
||
} | null {
|
||
return this.lastWorkerRunSnapshot
|
||
? {
|
||
workerId: this.lastWorkerRunSnapshot.workerId,
|
||
runtimeSession: structuredClone(this.lastWorkerRunSnapshot.runtimeSession),
|
||
blackboardItems: structuredClone(this.lastWorkerRunSnapshot.blackboardItems),
|
||
}
|
||
: null;
|
||
}
|
||
|
||
/** 从 checkpoint 恢复 runtime + 黑板(编辑 / 切换分支) */
|
||
restoreFromCheckpoint(
|
||
session: RuntimeSession,
|
||
blackboardItems: BlackboardItem[],
|
||
): void {
|
||
this.session = structuredClone(session);
|
||
this.blackboard = new Blackboard();
|
||
if (blackboardItems.length) {
|
||
this.blackboard.seed(structuredClone(blackboardItems));
|
||
}
|
||
this.pendingWorkerEffect = null;
|
||
this.lastWorkerRunSnapshot = null;
|
||
}
|
||
|
||
/**
|
||
* 提前停止当前生成:还原开跑快照,回到生成前的等待面(或分层图)。
|
||
* 不重跑。
|
||
*/
|
||
abortCurrentRun(): RuntimeSession {
|
||
const snap = this.lastWorkerRunSnapshot;
|
||
if (snap) {
|
||
this.restoreFromCheckpoint(snap.runtimeSession, snap.blackboardItems);
|
||
}
|
||
return this.settleAfterAbort();
|
||
}
|
||
|
||
/** 停止后落到可操作等待态,避免停在 running 空转。 */
|
||
private settleAfterAbort(): RuntimeSession {
|
||
const s = this.session;
|
||
if (s.waitingReason) return s;
|
||
const pendingId = s.pendingArtifactId?.trim();
|
||
const revisionTarget =
|
||
typeof s.slots.revisionTargetArtifactId === "string"
|
||
? s.slots.revisionTargetArtifactId.trim()
|
||
: "";
|
||
const reviewId =
|
||
pendingId && s.artifacts.some((a) => a.id === pendingId)
|
||
? pendingId
|
||
: revisionTarget && s.artifacts.some((a) => a.id === revisionTarget)
|
||
? revisionTarget
|
||
: "";
|
||
if (reviewId) {
|
||
this.session = {
|
||
...s,
|
||
phase: "waiting_user",
|
||
waitingReason: { kind: "review_artifact", artifactId: reviewId },
|
||
pendingArtifactId: reviewId,
|
||
currentWorkerId: undefined,
|
||
pendingDecision: undefined,
|
||
updatedAt: new Date().toISOString(),
|
||
};
|
||
return this.session;
|
||
}
|
||
if (this.hasCreationFlow()) {
|
||
this.clearProposedStep();
|
||
this.session = {
|
||
...s,
|
||
phase: "waiting_user",
|
||
waitingReason: { kind: "pick_creation_step" },
|
||
pendingDecision: undefined,
|
||
currentWorkerId: undefined,
|
||
updatedAt: new Date().toISOString(),
|
||
};
|
||
return this.session;
|
||
}
|
||
if (this.recoverOrphanedRun()) return this.session;
|
||
if (s.phase === "running") {
|
||
this.session = {
|
||
...s,
|
||
phase: "waiting_user",
|
||
waitingReason: { kind: "input", message: "已停止生成,可继续说明。" },
|
||
currentWorkerId: undefined,
|
||
pendingDecision: undefined,
|
||
updatedAt: new Date().toISOString(),
|
||
};
|
||
}
|
||
return this.session;
|
||
}
|
||
|
||
/**
|
||
* 执行中卡住:有进行中的 worker 则按开跑快照重跑;否则重跑总管。
|
||
*/
|
||
async retryStuckRun(): Promise<RuntimeSession> {
|
||
const workerId = this.session.currentWorkerId;
|
||
const snap = this.lastWorkerRunSnapshot;
|
||
if (workerId && snap && snap.workerId === workerId) {
|
||
this.restoreFromCheckpoint(
|
||
snap.runtimeSession,
|
||
snap.blackboardItems,
|
||
);
|
||
return this.rerunWorker(workerId);
|
||
}
|
||
if (this.session.phase === "running" && !this.session.waitingReason) {
|
||
await this.runMainAgent();
|
||
return this.session;
|
||
}
|
||
throw new Error("当前没有可重试的执行");
|
||
}
|
||
|
||
/** LLM/解析失败后离开 running,避免界面一直转圈。 */
|
||
async failRun(reason: string): Promise<RuntimeSession> {
|
||
if (this.session.phase !== "running") return this.session;
|
||
const result = this.commitEvent({
|
||
type: "runtime_failed",
|
||
payload: { reason },
|
||
});
|
||
await this.processEffects(result.effects);
|
||
return this.session;
|
||
}
|
||
|
||
/**
|
||
* 进程里留下的「执行中、没 worker」:配方已预置 DAG 时改回点选等待。
|
||
* 读档 / 轮询时调用,避免界面一直转圈。
|
||
*/
|
||
recoverOrphanedRun(): boolean {
|
||
if (this.session.phase !== "running" || this.session.waitingReason) {
|
||
return false;
|
||
}
|
||
if (this.session.currentWorkerId) return false;
|
||
if (!this.hasCreationFlow()) return false;
|
||
debugLog("step", "恢复中断的调度,等待点选节点");
|
||
this.clearProposedStep();
|
||
this.session = {
|
||
...this.session,
|
||
phase: "waiting_user",
|
||
waitingReason: { kind: "pick_creation_step" },
|
||
pendingDecision: undefined,
|
||
currentWorkerId: undefined,
|
||
updatedAt: new Date().toISOString(),
|
||
};
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 配方已预置 DAG:跳过「先描述需求」,直接进入点选图。
|
||
* 图上节点来自当前剧本 seed,换配方即换开局。
|
||
*/
|
||
async enterSeededCreationPick(): Promise<void> {
|
||
if (!this.hasCreationFlow()) return;
|
||
this.session = {
|
||
...this.session,
|
||
slots: {
|
||
...this.session.slots,
|
||
startupCompleted: true,
|
||
},
|
||
};
|
||
await this.awaitCreationStepPick();
|
||
}
|
||
|
||
/**
|
||
* 转述整理式进料:跳过配方 DAG / 点选节点,直接等用户说话。
|
||
* 总管不跑 design-flow;由 session-manager 的 dictate 对话环处理输入。
|
||
*/
|
||
enterDictateIntake(message?: string): void {
|
||
this.session = {
|
||
...this.session,
|
||
phase: "waiting_user",
|
||
waitingReason: {
|
||
kind: "input",
|
||
message:
|
||
message?.trim() ||
|
||
"转述整理:直接说你想要的体验与设定;我会整理进产物。缺什么会问你。",
|
||
},
|
||
pendingDecision: undefined,
|
||
currentWorkerId: undefined,
|
||
pendingArtifactId: undefined,
|
||
resumeContext: undefined,
|
||
slots: {
|
||
...this.session.slots,
|
||
startupCompleted: true,
|
||
},
|
||
updatedAt: new Date().toISOString(),
|
||
};
|
||
}
|
||
|
||
/** 重 roll 指定 worker(刷新 Skill 回复) */
|
||
async rerunWorker(workerId: string): Promise<RuntimeSession> {
|
||
const effect: Extract<PhaseEffect, { type: "run_worker" }> = {
|
||
type: "run_worker",
|
||
workerId,
|
||
};
|
||
if (this.autoStubWorker) {
|
||
await this.runStubWorker(effect);
|
||
} else if (this.llm) {
|
||
await this.runRealWorker(effect);
|
||
} else {
|
||
this.pendingWorkerEffect = effect;
|
||
this.onMessage(`[阶段机] 重 roll worker: ${workerId}。输入 /worker-start`);
|
||
}
|
||
return this.session;
|
||
}
|
||
|
||
setLifecycleStage(stage: "design" | "play"): void {
|
||
this.session = {
|
||
...this.session,
|
||
slots: { ...this.session.slots, uiLifecycleStage: stage },
|
||
updatedAt: new Date().toISOString(),
|
||
};
|
||
}
|
||
|
||
/** 收口已验收但未落印时补封印,并保证有可上场的运行规格 */
|
||
prepareEnterPlay(): void {
|
||
if (
|
||
this.hasAcceptedCloser() &&
|
||
!this.session.slots[SLOT_CREATION_SEALED_BY_OPENING]
|
||
) {
|
||
this.sealCreationOpening();
|
||
return;
|
||
}
|
||
if (
|
||
this.session.slots[SLOT_CREATION_SEALED_BY_OPENING] ||
|
||
this.session.slots.designInstanceReady
|
||
) {
|
||
this.ensurePlaySpecReady();
|
||
return;
|
||
}
|
||
// 转述:无节点收口,落档/开玩时直接封印并补默认运行规格
|
||
if (this.isDictateIntakeMode()) {
|
||
this.ensurePlaySpecReady();
|
||
}
|
||
}
|
||
|
||
private isDictateIntakeMode(): boolean {
|
||
return isDictateModeValue(
|
||
this.blackboard.getContentByTag(CREATION_INTAKE_MODE_TAG),
|
||
);
|
||
}
|
||
|
||
getAvailableSkills(): SkillIndexEntry[] {
|
||
return this.availableSkills;
|
||
}
|
||
|
||
/** 恢复会话或首次 start 前加载 skill 列表(供 UI 展示) */
|
||
async ensureAvailableSkills(): Promise<void> {
|
||
if (this.availableSkills.length === 0) {
|
||
this.availableSkills = await listSkills();
|
||
}
|
||
}
|
||
|
||
getActiveSkill(): ActiveSkillSnapshot | undefined {
|
||
return this.session.slots.activeSkill as ActiveSkillSnapshot | undefined;
|
||
}
|
||
|
||
getAllowedEventTypes(): RuntimeEvent["type"][] {
|
||
return getAllowedEvents(this.session);
|
||
}
|
||
|
||
/**
|
||
* 是否处于「running 且无事可做,等总管决策」状态。
|
||
* Web 层据此显示「生成大纲」等按钮。
|
||
*/
|
||
needsMainAgentDecision(): boolean {
|
||
return (
|
||
this.session.phase === "running" &&
|
||
!this.session.currentWorkerId &&
|
||
!this.pendingWorkerEffect
|
||
);
|
||
}
|
||
|
||
/** 加载 registry → 自动进入默认 orchestrator → intake(无选包阶段) */
|
||
async start(): Promise<RuntimeSession> {
|
||
this.availableSkills = await listSkills();
|
||
if (this.availableSkills.length === 0) {
|
||
throw new Error("skills/registry 中没有注册任何 orchestrator");
|
||
}
|
||
const defaultId = resolveDefaultOrchestratorId(this.availableSkills);
|
||
const initialSkill = toActiveSkillSnapshot(await loadSkill(defaultId));
|
||
await this.dispatch({
|
||
type: "session_started",
|
||
payload: {
|
||
presetId: this.session.presetId,
|
||
flowId: this.session.flowId,
|
||
availableSkills: this.availableSkills,
|
||
initialSkill,
|
||
},
|
||
});
|
||
return this.session;
|
||
}
|
||
|
||
/** 启动并加载指定 orchestrator(legacy 包 / 测试用) */
|
||
async startWithOrchestrator(orchestratorId: string): Promise<RuntimeSession> {
|
||
this.availableSkills = await listSkills();
|
||
if (this.availableSkills.length === 0) {
|
||
throw new Error("skills/registry 中没有注册任何 orchestrator");
|
||
}
|
||
const initialSkill = toActiveSkillSnapshot(await loadSkill(orchestratorId));
|
||
await this.dispatch({
|
||
type: "session_started",
|
||
payload: {
|
||
presetId: this.session.presetId,
|
||
flowId: this.session.flowId,
|
||
availableSkills: this.availableSkills,
|
||
initialSkill,
|
||
},
|
||
});
|
||
return this.session;
|
||
}
|
||
|
||
/** 解析并加载 SKILL.md → 触发 skill_selected → waiting_user(input) */
|
||
async selectSkill(skillIdOrName: string): Promise<RuntimeSession> {
|
||
const dir = await resolveSkillId(skillIdOrName);
|
||
if (!dir) {
|
||
throw new Error(`未找到 skill: ${skillIdOrName}`);
|
||
}
|
||
const parsed = await loadSkill(dir);
|
||
await this.dispatch({
|
||
type: "skill_selected",
|
||
payload: { skill: toActiveSkillSnapshot(parsed) },
|
||
});
|
||
return this.session;
|
||
}
|
||
|
||
/** applyEvent 并打状态日志(dispatch / 总管决策共用) */
|
||
private commitEvent(event: RuntimeEvent): ApplyEventResult {
|
||
const before = sessionSnap(this.session);
|
||
const result = applyEvent(this.session, event);
|
||
this.session = result.session;
|
||
logStateChange(event, before, result);
|
||
return result;
|
||
}
|
||
|
||
/** 统一事件入口:applyEvent + 更新 session + 处理 effects + 可选总管 LLM */
|
||
async dispatch(event: RuntimeEvent): Promise<ApplyEventResult> {
|
||
const result = this.commitEvent(event);
|
||
|
||
if (
|
||
event.type === "user_submitted_input" ||
|
||
event.type === "user_confirmed_intake"
|
||
) {
|
||
this.syncSlotsToBlackboard(this.session);
|
||
}
|
||
|
||
if (result.error && event.type !== "runtime_failed") {
|
||
this.onMessage(result.error);
|
||
}
|
||
|
||
await this.processEffects(result.effects);
|
||
await this.maybeRunMainAgent(result.effects);
|
||
return result;
|
||
}
|
||
|
||
/** 将 slots 中的启动目标等同步为黑板 tag(迁移期 tag 名可与旧 key 相同) */
|
||
private syncSlotsToBlackboard(session: RuntimeSession): void {
|
||
const activeSkill = session.slots.activeSkill as ActiveSkillSnapshot | undefined;
|
||
const demandTag = activeSkill?.startupTargetKey || "用户.需求";
|
||
const demandContent = session.slots[demandTag];
|
||
if (typeof demandContent === "string" && demandContent.trim()) {
|
||
this.blackboard.write({
|
||
tag: demandTag,
|
||
content: demandContent.trim(),
|
||
source: "user",
|
||
});
|
||
}
|
||
const latestInput = session.slots["用户.最新输入"];
|
||
if (typeof latestInput === "string" && latestInput.trim()) {
|
||
this.blackboard.write({
|
||
tag: "用户.最新输入",
|
||
content: latestInput.trim(),
|
||
source: "user",
|
||
});
|
||
// 游玩期把用户话追加进可投影的「对话.历史」标签
|
||
if (inferLifecycleStage(session) === "play") {
|
||
this.appendDialogueHistory("用户", latestInput.trim());
|
||
}
|
||
}
|
||
const workerReply = session.slots["用户.worker答复"];
|
||
if (typeof workerReply === "string" && workerReply.trim()) {
|
||
this.blackboard.write({
|
||
tag: "用户.worker答复",
|
||
content: workerReply.trim(),
|
||
source: "user",
|
||
});
|
||
}
|
||
const revision =
|
||
(typeof session.slots["用户.修订说明"] === "string" &&
|
||
session.slots["用户.修订说明"]) ||
|
||
(typeof session.slots.revisionInstruction === "string" &&
|
||
session.slots.revisionInstruction);
|
||
if (typeof revision === "string" && revision.trim()) {
|
||
this.blackboard.write({
|
||
tag: "用户.修订说明",
|
||
content: revision.trim(),
|
||
source: "user",
|
||
});
|
||
}
|
||
const nextIntent = session.slots["用户.下一步意向"];
|
||
if (typeof nextIntent === "string" && nextIntent.trim()) {
|
||
this.blackboard.write({
|
||
tag: "用户.下一步意向",
|
||
content: nextIntent.trim(),
|
||
source: "user",
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 用户文本输入的统一入口。
|
||
* legacy skill_selection 阶段会先解析为 selectSkill,否则走 user_submitted_input。
|
||
*/
|
||
async submitInput(text: string): Promise<RuntimeSession> {
|
||
if (this.session.waitingReason?.kind === "skill_selection") {
|
||
const picked = await this.resolveSkillFromUserInput(text);
|
||
if (!picked) {
|
||
throw new Error(
|
||
`无法识别 skill: ${text}。旧版会话请用 skill id(如 world-simulator)或列表编号。`,
|
||
);
|
||
}
|
||
return this.selectSkill(picked);
|
||
}
|
||
if (this.session.waitingReason?.kind === "intake") {
|
||
const skill = this.getActiveSkill();
|
||
const fields = skill?.intakeFields ?? [];
|
||
const current = readIntakeValues(this.session.slots);
|
||
const intakeValues = await extractIntakeFromMessage(
|
||
text,
|
||
fields,
|
||
current,
|
||
this.llm,
|
||
);
|
||
await this.dispatch({
|
||
type: "user_submitted_input",
|
||
payload: { text, intakeValues },
|
||
});
|
||
return this.session;
|
||
}
|
||
await this.dispatch({ type: "user_submitted_input", payload: { text } });
|
||
return this.session;
|
||
}
|
||
|
||
/** 必要项已填完:确认进入实例化 / 下一阶段 */
|
||
async confirmIntake(): Promise<RuntimeSession> {
|
||
if (this.session.waitingReason?.kind !== "intake") {
|
||
throw new Error("当前不在填空收集阶段");
|
||
}
|
||
await this.dispatch({ type: "user_confirmed_intake", payload: {} });
|
||
this.syncSlotsToBlackboard(this.session);
|
||
return this.session;
|
||
}
|
||
|
||
/** 支持按编号(1-based)或 skill name 解析 */
|
||
private async resolveSkillFromUserInput(text: string): Promise<string | null> {
|
||
const trimmed = text.trim();
|
||
const num = Number(trimmed);
|
||
if (Number.isInteger(num) && num >= 1 && num <= this.availableSkills.length) {
|
||
const entry = this.availableSkills[num - 1];
|
||
return resolveSkillId(entry.name);
|
||
}
|
||
return resolveSkillId(trimmed);
|
||
}
|
||
|
||
/** 提交总管决策(通常来自 Main Agent / 手动按钮) */
|
||
async submitDecision(decision: MainAgentDecision): Promise<RuntimeSession> {
|
||
await this.dispatch({
|
||
type: "main_agent_decision_created",
|
||
payload: { decision },
|
||
});
|
||
return this.session;
|
||
}
|
||
|
||
/** 用户确认 pendingDecision(approve_step);可顺带钉死下一步 params */
|
||
async approve(stepParams?: CreationFlowStepParams): Promise<RuntimeSession> {
|
||
const id = this.session.pendingDecision?.id;
|
||
if (!id) throw new Error("当前没有待确认的决策");
|
||
if (stepParams && Object.keys(stepParams).length > 0) {
|
||
this.applyProposedStepParams(stepParams);
|
||
}
|
||
const readyErr = this.assertProposedStepReady();
|
||
if (readyErr) throw new Error(readyErr);
|
||
await this.dispatch({ type: "user_approved_next_step", payload: { decisionId: id } });
|
||
return this.session;
|
||
}
|
||
|
||
/** 用户拒绝 pendingDecision:回到流程编排,可追加节点 */
|
||
async rejectStep(reason?: string): Promise<RuntimeSession> {
|
||
const id = this.session.pendingDecision?.id;
|
||
if (!id) throw new Error("当前没有待拒绝的决策");
|
||
await this.dispatch({
|
||
type: "user_rejected_next_step",
|
||
payload: { decisionId: id, reason },
|
||
});
|
||
return this.session;
|
||
}
|
||
|
||
/**
|
||
* 离开当前技能步,回到分层图。
|
||
* 可增殖节点刚增殖出、尚未验收的空实例从图上撤掉。
|
||
*/
|
||
async leaveCreationStep(): Promise<RuntimeSession> {
|
||
if (this.session.phase === "running" && !this.session.waitingReason) {
|
||
this.abortCurrentRun();
|
||
}
|
||
if (this.session.waitingReason?.kind === "pick_creation_step") {
|
||
return this.session;
|
||
}
|
||
const waiting = this.session.waitingReason?.kind;
|
||
const allowed =
|
||
waiting === "worker_questions" ||
|
||
waiting === "input" ||
|
||
waiting === "revision" ||
|
||
waiting === "approve_step" ||
|
||
waiting === "review_artifact";
|
||
if (!allowed) {
|
||
throw new Error("当前不能返回节点选择");
|
||
}
|
||
if (inferLifecycleStage(this.session) === "play") {
|
||
throw new Error("游玩中不能返回创作节点");
|
||
}
|
||
const stepId = this.readPinnedStepId();
|
||
this.forgetUnansweredModuleOpening(stepId);
|
||
const removed = this.unspawnCurrentInstanceIfEmpty();
|
||
this.clearProposedStep();
|
||
this.clearCurrentStepPointers();
|
||
const result = await this.dispatch({
|
||
type: "user_left_creation_step",
|
||
payload: {},
|
||
});
|
||
if (result.error) {
|
||
throw new Error(result.error);
|
||
}
|
||
const stepName = removed?.name?.trim();
|
||
this.onMessage(
|
||
removed
|
||
? `已取消刚增殖的「${stepName || stepId}」,回到节点选择。`
|
||
: "已回到节点选择。",
|
||
);
|
||
return this.session;
|
||
}
|
||
|
||
private unspawnCurrentInstanceIfEmpty(): { id: string; name: string } | null {
|
||
const stepId = this.readPinnedStepId();
|
||
if (!stepId) return null;
|
||
const flow = parseCreationFlow(
|
||
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||
);
|
||
if (!flow) return null;
|
||
const step = findStepByRef(flow, stepId);
|
||
if (!step || !isInstanceStep(step)) return null;
|
||
const accepted = this.readAcceptedStepIds();
|
||
if (isStepAccepted(step, accepted)) return null;
|
||
if (
|
||
this.session.artifacts.some(
|
||
(a) => a.stepId === step.id && a.status !== "rejected",
|
||
)
|
||
) {
|
||
return null;
|
||
}
|
||
const next = removeUnstartedInstance(flow, step.id, accepted);
|
||
if (next.steps.length === flow.steps.length) return null;
|
||
this.writeCreationFlow(next);
|
||
debugLog("step", `取消未完成实例 ${step.id}`);
|
||
return { id: step.id, name: step.name };
|
||
}
|
||
|
||
/** 离开节点时若默认问题只展示过、还没答:清状态,下次点进再出引导。 */
|
||
private forgetUnansweredModuleOpening(stepId: string): void {
|
||
const id = stepId.trim();
|
||
if (!id) return;
|
||
const flow = parseCreationFlow(
|
||
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||
);
|
||
const step = flow ? findStepByRef(flow, id) : null;
|
||
const key = step ? stepUnitId(step) : id;
|
||
const state = parseModuleOpeningState(
|
||
this.session.slots[SLOT_CREATION_MODULE_OPENING_STATE] ??
|
||
this.blackboard.getContentByTag(CREATION_MODULE_OPENING_STATE_TAG),
|
||
);
|
||
if (state[key] !== "shown") return;
|
||
delete state[key];
|
||
const raw = stringifyModuleOpeningState(state);
|
||
this.blackboard.write({
|
||
tag: CREATION_MODULE_OPENING_STATE_TAG,
|
||
content: raw,
|
||
source: "runtime",
|
||
});
|
||
this.blackboard.write({
|
||
tag: CREATION_MODULE_OPENING_TAG,
|
||
content: "",
|
||
source: "runtime",
|
||
});
|
||
this.session = {
|
||
...this.session,
|
||
slots: {
|
||
...this.session.slots,
|
||
[SLOT_CREATION_MODULE_OPENING_STATE]: raw,
|
||
[CREATION_MODULE_OPENING_TAG]: undefined,
|
||
},
|
||
};
|
||
}
|
||
|
||
private clearCurrentStepPointers(): void {
|
||
this.blackboard.write({
|
||
tag: CREATION_CURRENT_STEP_TAG,
|
||
content: "",
|
||
source: "runtime",
|
||
});
|
||
this.blackboard.write({
|
||
tag: CREATION_INHERIT_EXISTING_TAG,
|
||
content: "",
|
||
source: "runtime",
|
||
});
|
||
this.session = {
|
||
...this.session,
|
||
slots: {
|
||
...this.session.slots,
|
||
[CREATION_CURRENT_STEP_TAG]: undefined,
|
||
[CREATION_INHERIT_EXISTING_TAG]: undefined,
|
||
[SLOT_CREATION_CURRENT_UNIT]: undefined,
|
||
},
|
||
};
|
||
}
|
||
|
||
/** 开场白等步骤上发现漏了前序节点:再进流程编排,不整局重开 */
|
||
async replanCreationFlow(reason?: string): Promise<RuntimeSession> {
|
||
const waiting = this.session.waitingReason?.kind;
|
||
if (waiting === "approve_step") {
|
||
return this.rejectStep(reason);
|
||
}
|
||
await this.dispatch({
|
||
type: "user_requested_flow_replan",
|
||
payload: { reason },
|
||
});
|
||
return this.session;
|
||
}
|
||
|
||
/** 图上点选:普通节点开干;原型槽位先增殖再进去;已完成节点可 reenter 重进 */
|
||
async pickCreationStep(
|
||
stepId: string,
|
||
stepParams?: CreationFlowStepParams,
|
||
opts?: { reenter?: boolean },
|
||
): Promise<RuntimeSession> {
|
||
const id = stepId.trim();
|
||
this.syncSlotsToBlackboard(this.session);
|
||
const catalog = await this.loadActiveModuleCatalog();
|
||
const flow = this.readNormalizedFlow(catalog);
|
||
const step = flow ? findStepByRef(flow, id) : null;
|
||
if (flow && step && isPrototypeStep(step)) {
|
||
if (opts?.reenter) {
|
||
throw new Error("原型槽位不能回头修改,请点已完成的实例");
|
||
}
|
||
const spawned = spawnInstanceFromPrototype({
|
||
flow,
|
||
catalog,
|
||
prototypeId: step.id,
|
||
acceptedStepIds: this.readAcceptedStepIds(),
|
||
});
|
||
if ("error" in spawned) throw new Error(spawned.error);
|
||
this.writeCreationFlow(spawned.flow);
|
||
debugLog("step", `点原型 ${step.id} → ${spawned.step.id}`);
|
||
await this.startCreationStep(spawned.step.id, { stepParams });
|
||
return this.session;
|
||
}
|
||
await this.startCreationStep(id, {
|
||
stepParams,
|
||
reenter: opts?.reenter === true,
|
||
});
|
||
return this.session;
|
||
}
|
||
|
||
/** 可反复能力:新编一条再进去 */
|
||
async spawnCreationStep(moduleName: string): Promise<RuntimeSession> {
|
||
this.syncSlotsToBlackboard(this.session);
|
||
const catalog = await this.loadActiveModuleCatalog();
|
||
const flow = this.readNormalizedFlow(catalog);
|
||
if (!flow) throw new Error("还没有工作流计划,无法追加节点");
|
||
const spawned = spawnRepeatableCreationStep({
|
||
flow,
|
||
catalog,
|
||
moduleName,
|
||
acceptedStepIds: this.readAcceptedStepIds(),
|
||
});
|
||
if ("error" in spawned) throw new Error(spawned.error);
|
||
this.writeCreationFlow(spawned.flow);
|
||
debugLog("step", `新开 ${spawned.step.id}(${spawned.step.name})`);
|
||
await this.startCreationStep(spawned.step.id);
|
||
return this.session;
|
||
}
|
||
|
||
/** 图上删除可增殖产物(已验收也可)。 */
|
||
async deleteCreationStep(stepId: string): Promise<RuntimeSession> {
|
||
const id = stepId.trim();
|
||
if (!id) throw new Error("请指定要删除的产物");
|
||
this.syncSlotsToBlackboard(this.session);
|
||
const catalog = await this.loadActiveModuleCatalog();
|
||
const flow = this.readNormalizedFlow(catalog);
|
||
if (!flow) throw new Error("还没有工作流计划");
|
||
const result = removeRepeatableInstance({ flow, stepId: id, catalog });
|
||
if ("error" in result) throw new Error(result.error);
|
||
this.writeCreationFlow(result.flow);
|
||
this.forgetDeletedCreationStep(result.removed, result.flow, catalog);
|
||
this.onMessage(`已删除「${result.removed.name}」产物 ${result.removed.id}。`);
|
||
debugLog("step", `删除产物 ${result.removed.id}`);
|
||
return this.session;
|
||
}
|
||
|
||
private forgetDeletedCreationStep(
|
||
step: CreationFlowStep,
|
||
flow: CreationFlow,
|
||
catalog: ModuleCatalog | null,
|
||
): void {
|
||
const unitId = step.id;
|
||
const accepted = this.readAcceptedStepIds().filter((item) => item !== unitId);
|
||
const acceptedRaw = JSON.stringify(accepted);
|
||
this.blackboard.write({
|
||
tag: CREATION_ACCEPTED_UNITS_TAG,
|
||
content: acceptedRaw,
|
||
source: "runtime",
|
||
});
|
||
const contentRaw = dropAcceptedUnitContent(
|
||
this.blackboard.getContentByTag(CREATION_ACCEPTED_CONTENT_TAG),
|
||
unitId,
|
||
);
|
||
this.blackboard.write({
|
||
tag: CREATION_ACCEPTED_CONTENT_TAG,
|
||
content: contentRaw,
|
||
source: "runtime",
|
||
});
|
||
const now = new Date().toISOString();
|
||
const artifacts = this.session.artifacts.map((a) =>
|
||
a.stepId === unitId && a.status !== "rejected"
|
||
? { ...a, status: "rejected" as const, updatedAt: now }
|
||
: a,
|
||
);
|
||
const remainingSameModule = accepted.filter((item) => {
|
||
const other = findStepByRef(flow, item);
|
||
return other?.name === step.name;
|
||
}).length;
|
||
const tag = findModuleByName(catalog, step.name)?.artifact?.trim();
|
||
if (tag) {
|
||
const pruned = pruneRepeatableArtifactContent({
|
||
raw: this.blackboard.getContentByTag(tag) ?? "",
|
||
step,
|
||
remainingSameModule,
|
||
});
|
||
this.blackboard.write({
|
||
tag,
|
||
content: pruned,
|
||
source: "runtime",
|
||
});
|
||
}
|
||
const pinned = this.readPinnedStepId();
|
||
this.session = {
|
||
...this.session,
|
||
artifacts,
|
||
currentStepId: pinned === unitId ? undefined : this.session.currentStepId,
|
||
slots: {
|
||
...this.session.slots,
|
||
[SLOT_CREATION_ACCEPTED_UNITS]: accepted,
|
||
[CREATION_ACCEPTED_UNITS_TAG]: acceptedRaw,
|
||
[CREATION_ACCEPTED_CONTENT_TAG]: contentRaw,
|
||
...(tag ? { [tag]: this.blackboard.getContentByTag(tag) ?? "" } : {}),
|
||
},
|
||
updatedAt: now,
|
||
};
|
||
if (pinned === unitId) this.clearCurrentStepPointers();
|
||
}
|
||
|
||
private writeCreationFlow(flow: CreationFlow): void {
|
||
const content = stringifyCreationFlow(flow);
|
||
this.blackboard.write({
|
||
tag: CREATION_FLOW_TAG,
|
||
content,
|
||
source: "user",
|
||
});
|
||
this.session = {
|
||
...this.session,
|
||
slots: { ...this.session.slots, [CREATION_FLOW_TAG]: content },
|
||
};
|
||
}
|
||
|
||
private readNormalizedFlow(catalog: ModuleCatalog | null): CreationFlow | null {
|
||
const flow = parseCreationFlow(
|
||
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||
);
|
||
if (!flow) return null;
|
||
const next = finalizeCreationFlow(flow, catalog);
|
||
const changed = JSON.stringify(next.steps) !== JSON.stringify(flow.steps);
|
||
if (changed) this.writeCreationFlow(next);
|
||
return next;
|
||
}
|
||
|
||
/** 将实例 Worker 声明写入 slots,便于 session 持久化与调试 */
|
||
private persistInstanceWorkerDeclaration(): void {
|
||
const active = this.getActiveSkill();
|
||
if (!shouldEnforceWorkerDeclaration(active?.name)) return;
|
||
|
||
const declaration = this.getInstanceWorkerDeclaration();
|
||
this.session = {
|
||
...this.session,
|
||
slots: {
|
||
...this.session.slots,
|
||
instanceWorkerDeclaration: {
|
||
sourceTag: declaration.sourceTag,
|
||
accepted: declaration.accepted,
|
||
playWorkerIds: declaration.playWorkerIds,
|
||
onDemandWorkerIds: declaration.onDemandWorkerIds,
|
||
designEndWorkerIds: declaration.designEndWorkerIds,
|
||
activeWorkerIds: declaration.activeWorkerIds,
|
||
},
|
||
},
|
||
};
|
||
}
|
||
|
||
/** 用户接受 pendingArtifact;创作单位验收 vs 终稿验收分流 */
|
||
async acceptArtifact(
|
||
artifactId?: string,
|
||
opts?: { openingIndex?: number },
|
||
): Promise<RuntimeSession> {
|
||
const id = artifactId ?? this.session.pendingArtifactId;
|
||
if (!id) throw new Error("当前没有待验收的产物");
|
||
const artifact = this.session.artifacts.find((a) => a.id === id);
|
||
if (artifact && opts?.openingIndex != null && Number.isFinite(opts.openingIndex)) {
|
||
this.session = {
|
||
...this.session,
|
||
slots: {
|
||
...this.session.slots,
|
||
[SLOT_OPENING_SELECTED_INDEX]: Math.trunc(opts.openingIndex),
|
||
},
|
||
};
|
||
}
|
||
const unitAccept = artifact ? isDesignUnitArtifact(artifact) : false;
|
||
// 细化终稿写「设计.worker集」,不是单位草稿,但仍须记入已验收步骤,
|
||
// 否则编排器会当成没做完再跑一遍。须在 dispatch 之前写入。
|
||
const shouldRecordStep = Boolean(
|
||
artifact && (unitAccept || artifact.workerId === "design-step"),
|
||
);
|
||
if (artifact && shouldRecordStep) {
|
||
const slots = { ...this.session.slots };
|
||
this.recordCreationUnitAccepted(slots, artifact);
|
||
this.session = { ...this.session, slots };
|
||
}
|
||
|
||
await this.dispatch({ type: "user_accepted_artifact", payload: { artifactId: id } });
|
||
this.persistInstanceWorkerDeclaration();
|
||
|
||
if (artifact) {
|
||
const result = compressAfterWorkerAccept({
|
||
blackboard: this.blackboard,
|
||
workerId: artifact.workerId,
|
||
outputTags: artifact.outputTags,
|
||
summary: artifact.summary,
|
||
mode: unitAccept ? "unit" : "final",
|
||
});
|
||
|
||
const slots: Record<string, unknown> = {
|
||
...this.session.slots,
|
||
lastCompressBrief: result.briefText,
|
||
lastCompressedWorkerId: artifact.workerId,
|
||
};
|
||
|
||
if (shouldRecordStep) {
|
||
const unitId = String(slots[SLOT_CREATION_CURRENT_UNIT] ?? "").trim();
|
||
this.onMessage(
|
||
`[创作单位已验收] ${unitId || artifact.workerId}:${
|
||
artifact.summary?.trim() || "草稿单位"
|
||
};过程对话将折叠,草稿保留。`,
|
||
);
|
||
slots[SLOT_CREATION_CURRENT_UNIT] = undefined;
|
||
slots[SLOT_CREATION_UNIT_ANCHOR_AT] = undefined;
|
||
}
|
||
if (!unitAccept) {
|
||
this.onMessage(
|
||
`[上下文已压缩] ${artifact.workerId}:保留 ${result.finals.map((f) => f.tag).join("、") || "(无终产物)"};归档 ${result.archivedTags.length} 个过程 tag。下一阶段以定稿为准。`,
|
||
);
|
||
}
|
||
|
||
this.session = { ...this.session, slots };
|
||
}
|
||
|
||
return this.session;
|
||
}
|
||
|
||
/** 用户拒绝 pendingArtifact:有意见则立刻按「用户.修订说明」在现有产物上改 */
|
||
async rejectArtifact(reason?: string, artifactId?: string): Promise<RuntimeSession> {
|
||
const id = artifactId ?? this.session.pendingArtifactId;
|
||
if (!id) throw new Error("当前没有待拒绝的产物");
|
||
await this.dispatch({
|
||
type: "user_rejected_artifact",
|
||
payload: { artifactId: id, reason },
|
||
});
|
||
return this.session;
|
||
}
|
||
|
||
/**
|
||
* 按意见重跑却没写出新产物:退回**本次修订的那一份**产物验收,重试追问改挂为可选。
|
||
* 只认 beginRevisionRerun 钉下的底稿;否则会把无关的旧产物(如上一轮流程编排)
|
||
* 错误地挂回验收面。首跑无产物仍走阻塞追问。
|
||
* @returns true = 已退回验收,调用方应 return
|
||
*/
|
||
private async restoreRevisionTargetForReview(
|
||
workerId: string,
|
||
questions: import("../types/questions.js").QuestionItem[] | string[],
|
||
): Promise<boolean> {
|
||
const targetId = this.session.slots.revisionTargetArtifactId;
|
||
if (typeof targetId !== "string" || !targetId) return false;
|
||
const target = this.session.artifacts.find(
|
||
(a) =>
|
||
a.id === targetId &&
|
||
a.workerId === workerId &&
|
||
a.status === "revision_requested",
|
||
);
|
||
if (!target) return false;
|
||
await this.dispatch({
|
||
type: "worker_revision_produced_nothing",
|
||
payload: { artifactId: target.id, questions },
|
||
});
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 游玩层不走创作 HITL:终稿槽必须交出用户可见正文,其它执行单元空产物也继续。
|
||
*/
|
||
private settlePlayWorkerResult(
|
||
workerId: string,
|
||
result: import("../worker/executor.js").WorkerRunResult,
|
||
): import("../worker/executor.js").WorkerRunResult {
|
||
if (!isPlayLayerActive(this.session.slots)) return result;
|
||
const outputs = { ...result.outputs };
|
||
const narratorEnabled = isNarratorEnabled(
|
||
parseWorkerSetYaml(
|
||
this.blackboard.getContentByTag("设计.worker集") ?? "",
|
||
)?.play_slots,
|
||
);
|
||
if (
|
||
Object.keys(outputs).length === 0 &&
|
||
isPlayFinalVisibleWorker(workerId, { narratorEnabled })
|
||
) {
|
||
const shell =
|
||
parseShellAdaptationFromReplyFormat(
|
||
this.blackboard.getContentByTag("设计.正文组成") ??
|
||
this.blackboard.getContentByTag("设计.回复格式"),
|
||
)?.shell_id ?? "prose";
|
||
const preview = result.preview?.trim() ?? "";
|
||
const looksAsk =
|
||
/可验收|追问|请再补|askUser/.test(preview) || /^\s*-\s/.test(preview);
|
||
// 主世界终稿用纯正文;转述仍可落 present 包
|
||
outputs[PRESENT_TAG] = isPlayPresentWorker(workerId)
|
||
? presentOutputFromFallback(looksAsk ? "" : preview, shell)
|
||
: looksAsk
|
||
? "(本轮场面未写完)"
|
||
: preview || "(本轮场面未写完)";
|
||
}
|
||
return {
|
||
...result,
|
||
outputs,
|
||
askUser: undefined,
|
||
askAssessment: undefined,
|
||
};
|
||
}
|
||
|
||
/** worker 调用 ask_user 能力时,由外层触发此事件(仅无产物时阻塞) */
|
||
async workerAsk(
|
||
questions: import("../types/questions.js").QuestionItem[] | string[],
|
||
): Promise<RuntimeSession> {
|
||
const workerId = this.session.currentWorkerId;
|
||
if (!workerId) throw new Error("当前没有运行中的 worker");
|
||
this.rememberAskedQuestions(normalizeQuestions(questions));
|
||
await this.dispatch({
|
||
type: "worker_needs_input",
|
||
payload: { workerId, stepId: this.session.currentStepId, questions },
|
||
});
|
||
return this.session;
|
||
}
|
||
|
||
private askedQuestionHistory() {
|
||
return parseAskedQuestions(this.session.slots[SLOT_ASKED_QUESTIONS]);
|
||
}
|
||
|
||
private rememberAskedQuestions(
|
||
questions: readonly import("../types/questions.js").QuestionItem[],
|
||
): void {
|
||
if (!questions.length) return;
|
||
const next = appendAskedQuestions(this.askedQuestionHistory(), questions);
|
||
this.session = {
|
||
...this.session,
|
||
slots: { ...this.session.slots, [SLOT_ASKED_QUESTIONS]: next },
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 挂在验收态的可跳过追问:本局问过的不再问一次。
|
||
* 编排层常把上一步已问过的体验题再抛一遍,用户会读成「又要重做那一步」。
|
||
*/
|
||
private freshSidecarQuestions(
|
||
questions: readonly import("../types/questions.js").QuestionItem[] | undefined,
|
||
): import("../types/questions.js").QuestionItem[] | undefined {
|
||
if (!questions?.length) return undefined;
|
||
const fresh = dropAlreadyAskedQuestions(questions, this.askedQuestionHistory());
|
||
if (!fresh.length) return undefined;
|
||
this.rememberAskedQuestions(fresh);
|
||
return fresh;
|
||
}
|
||
|
||
/**
|
||
* 验收态下作答或跳过挂载追问:不离开 review_artifact。
|
||
* answersText 缺省 = 跳过。
|
||
*/
|
||
async resolveSidecarQuestions(answersText?: string): Promise<RuntimeSession> {
|
||
await this.dispatch({
|
||
type: "user_resolved_sidecar_questions",
|
||
payload: { answersText },
|
||
});
|
||
return this.session;
|
||
}
|
||
|
||
/** 写入创作过程对话摘要(design worker contextSegments 用) */
|
||
writeCreationDialogue(transcript: string): void {
|
||
const tag = "创作.对话";
|
||
this.blackboard.write({
|
||
tag,
|
||
content: transcript,
|
||
source: "system",
|
||
});
|
||
this.session = {
|
||
...this.session,
|
||
slots: { ...this.session.slots, [tag]: transcript },
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 手动标记 worker 完成:先创建 artifact 写入 session,再 dispatch worker_completed。
|
||
* 真实 worker 集成时,产物内容应在此之前写入 slots / blackboard。
|
||
*/
|
||
async workerComplete(summary?: string): Promise<RuntimeSession> {
|
||
const workerId = this.session.currentWorkerId;
|
||
if (!workerId) throw new Error("当前没有运行中的 worker");
|
||
|
||
const artifact = createArtifact({
|
||
workerId,
|
||
stepId: this.session.currentStepId,
|
||
outputTags: ["output.草稿"],
|
||
summary: summary ?? `${workerId} 产物`,
|
||
});
|
||
this.session = { ...this.session, artifacts: [...this.session.artifacts, artifact] };
|
||
|
||
await this.dispatch({
|
||
type: "worker_completed",
|
||
payload: { artifactId: artifact.id },
|
||
});
|
||
return this.session;
|
||
}
|
||
|
||
/** 非 autoStub 模式下,手动启动 pendingWorkerEffect */
|
||
async startPendingWorker(): Promise<RuntimeSession> {
|
||
const effect = this.pendingWorkerEffect;
|
||
if (!effect) throw new Error("没有待启动的 worker");
|
||
this.pendingWorkerEffect = null;
|
||
await this.runStubWorker(effect, false);
|
||
return this.session;
|
||
}
|
||
|
||
/** 消费 applyEvent 返回的 PhaseEffect 列表 */
|
||
private async processEffects(effects: PhaseEffect[]): Promise<void> {
|
||
for (const effect of effects) {
|
||
if (effect.type === "run_worker") {
|
||
debugLog("step", `启动执行单元 ${effect.workerId}`);
|
||
} else if (effect.type === "propose_next_creation_step") {
|
||
debugLog("step", "提案下一步");
|
||
} else if (effect.type === "resume_worker") {
|
||
debugLog("step", `恢复执行单元 ${this.session.resumeContext?.workerId ?? "未知"}`);
|
||
}
|
||
switch (effect.type) {
|
||
case "emit_message":
|
||
this.onMessage(effect.message);
|
||
break;
|
||
case "invoke_main_agent":
|
||
if (!this.mainAgent) {
|
||
this.onMessage(
|
||
"[阶段机] running:等待总管决策。请用 /decide 提交 run_worker 或 finish。",
|
||
);
|
||
}
|
||
break;
|
||
case "run_worker":
|
||
if (
|
||
effect.workerId === DESIGN_STEP_WORKER_ID &&
|
||
!this.canRunPinnedDesignStep()
|
||
) {
|
||
await this.awaitCreationStepPick();
|
||
break;
|
||
}
|
||
if (this.autoStubWorker) {
|
||
await this.runStubWorker(effect);
|
||
} else if (this.llm) {
|
||
await this.runRealWorker(effect);
|
||
} else {
|
||
this.pendingWorkerEffect = effect;
|
||
this.onMessage(
|
||
`[阶段机] 待启动 worker: ${effect.workerId}。输入 /worker-start`,
|
||
);
|
||
}
|
||
break;
|
||
case "resume_worker": {
|
||
const ctx = this.session.resumeContext;
|
||
if (!ctx) break;
|
||
const workerEffect: Extract<PhaseEffect, { type: "run_worker" }> = {
|
||
type: "run_worker",
|
||
workerId: ctx.workerId,
|
||
};
|
||
if (this.autoStubWorker) {
|
||
await this.runStubWorker(workerEffect);
|
||
} else if (this.llm) {
|
||
await this.runRealWorker(workerEffect);
|
||
} else {
|
||
this.pendingWorkerEffect = workerEffect;
|
||
this.onMessage(`[阶段机] 恢复 worker: ${ctx.workerId}。输入 /worker-start`);
|
||
}
|
||
break;
|
||
}
|
||
case "run_programmatic_review":
|
||
await this.runProgrammaticReview(effect.artifactId);
|
||
break;
|
||
case "propose_next_creation_step":
|
||
await this.proposeNextCreationStep();
|
||
break;
|
||
case "seal_creation_opening":
|
||
this.sealCreationOpening();
|
||
break;
|
||
case "unseal_creation_opening":
|
||
this.unsealCreationOpening();
|
||
break;
|
||
case "run_play_turn":
|
||
await this.startPlayTurn();
|
||
break;
|
||
case "continue_play_turn":
|
||
await this.continuePlayTurn();
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 用户填完「下一步想写什么」后:有可点节点则停在分层图;程序步自动开干;否则交总管扩步。
|
||
*/
|
||
private async proposeNextCreationStep(): Promise<void> {
|
||
await this.awaitCreationStepPick();
|
||
}
|
||
|
||
private readAcceptedStepIds(): string[] {
|
||
return parseAcceptedUnits(
|
||
this.session.slots[SLOT_CREATION_ACCEPTED_UNITS] ??
|
||
this.blackboard.getContentByTag(CREATION_ACCEPTED_UNITS_TAG),
|
||
);
|
||
}
|
||
|
||
private async loadActiveModuleCatalog(): Promise<ModuleCatalog | null> {
|
||
const active = this.getActiveSkill();
|
||
if (!active?.name) return null;
|
||
try {
|
||
const skill = await loadSkill(active.name);
|
||
if (!skill.skillPackRoot) return null;
|
||
return await loadModuleCatalog(skill.skillPackRoot);
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private readPinnedStepId(): string {
|
||
const fromCurrent = this.blackboard
|
||
.getContentByTag(CREATION_CURRENT_STEP_TAG)
|
||
?.trim();
|
||
if (fromCurrent) return fromCurrent;
|
||
const raw =
|
||
(typeof this.session.slots[CREATION_PROPOSED_STEP_TAG] === "string"
|
||
? String(this.session.slots[CREATION_PROPOSED_STEP_TAG])
|
||
: null) || this.blackboard.getContentByTag(CREATION_PROPOSED_STEP_TAG);
|
||
if (!raw?.trim()) return "";
|
||
try {
|
||
const doc = JSON.parse(raw) as { stepId?: unknown };
|
||
return typeof doc.stepId === "string" ? doc.stepId.trim() : "";
|
||
} catch {
|
||
return "";
|
||
}
|
||
}
|
||
|
||
private hasPinnedReadyStep(): boolean {
|
||
const flow = parseCreationFlow(
|
||
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||
);
|
||
if (!flow) return false;
|
||
const step = findStepByRef(flow, this.readPinnedStepId());
|
||
if (!step) return false;
|
||
return isStepReady(flow, step, this.readAcceptedStepIds());
|
||
}
|
||
|
||
/** 已验收节点回头改:isStepReady 为 false,但仍须跑 worker,不能弹回分层图。 */
|
||
private canRunPinnedDesignStep(): boolean {
|
||
if (this.hasPinnedReadyStep()) return true;
|
||
if (typeof this.session.slots.revisionTargetArtifactId === "string") {
|
||
return Boolean(this.session.slots.revisionTargetArtifactId.trim());
|
||
}
|
||
if (
|
||
isInheritExistingFlag(
|
||
this.blackboard.getContentByTag(CREATION_INHERIT_EXISTING_TAG) ??
|
||
this.session.slots[CREATION_INHERIT_EXISTING_TAG],
|
||
)
|
||
) {
|
||
return true;
|
||
}
|
||
const flow = parseCreationFlow(
|
||
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||
);
|
||
const step = flow
|
||
? findStepByRef(flow, this.readPinnedStepId())
|
||
: null;
|
||
return Boolean(step && isReviseStep(step));
|
||
}
|
||
|
||
private pinCreationStepSnapshot(params: {
|
||
stepId: string;
|
||
name: string;
|
||
declaration: string;
|
||
params: Record<string, unknown>;
|
||
paramsMissing: string[];
|
||
paramSpecs: ModuleParamSpec[];
|
||
kind?: ModuleCatalogEntry["kind"];
|
||
closer?: boolean;
|
||
revise: boolean;
|
||
revises?: string;
|
||
}): void {
|
||
const intent =
|
||
typeof this.session.slots["用户.下一步意向"] === "string"
|
||
? String(this.session.slots["用户.下一步意向"]).trim()
|
||
: "";
|
||
const snapshot = {
|
||
stepId: params.stepId,
|
||
name: params.name,
|
||
declaration: params.declaration || undefined,
|
||
params: params.params,
|
||
paramsMissing: params.paramsMissing,
|
||
paramSpecs: params.paramSpecs.map((p) => ({
|
||
key: p.key,
|
||
label: p.label,
|
||
required: Boolean(p.required),
|
||
hint: p.hint,
|
||
})),
|
||
intent: intent || undefined,
|
||
...(params.kind ? { kind: params.kind } : {}),
|
||
...(params.closer ? { closer: true } : {}),
|
||
...(params.revise ? { mode: "revise" as const } : {}),
|
||
...(params.revises ? { revises: params.revises } : {}),
|
||
};
|
||
this.blackboard.write({
|
||
tag: CREATION_PROPOSED_STEP_TAG,
|
||
content: JSON.stringify(snapshot),
|
||
source: "runtime",
|
||
});
|
||
this.blackboard.write({
|
||
tag: CREATION_CURRENT_STEP_TAG,
|
||
content: params.stepId,
|
||
source: "runtime",
|
||
});
|
||
this.blackboard.write({
|
||
tag: CREATION_INHERIT_EXISTING_TAG,
|
||
content: params.revise ? "1" : "",
|
||
source: "runtime",
|
||
});
|
||
this.session = {
|
||
...this.session,
|
||
slots: {
|
||
...this.session.slots,
|
||
[CREATION_PROPOSED_STEP_TAG]: JSON.stringify(snapshot),
|
||
[CREATION_CURRENT_STEP_TAG]: params.stepId,
|
||
[CREATION_INHERIT_EXISTING_TAG]: params.revise ? "1" : "",
|
||
},
|
||
};
|
||
}
|
||
|
||
private async startCreationStep(
|
||
stepId: string,
|
||
opts?: { stepParams?: CreationFlowStepParams; reenter?: boolean },
|
||
): Promise<void> {
|
||
this.syncSlotsToBlackboard(this.session);
|
||
let flow = parseCreationFlow(
|
||
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||
);
|
||
if (!flow) throw new Error("还没有工作流计划");
|
||
let step = findStepByRef(flow, stepId);
|
||
if (!step) throw new Error(`找不到节点:${stepId}`);
|
||
const accepted = this.readAcceptedStepIds();
|
||
const catalog = await this.loadActiveModuleCatalog();
|
||
const reenter = opts?.reenter === true;
|
||
|
||
if (isPrototypeStep(step)) {
|
||
if (!isPrototypeSelectable(flow, step, accepted)) {
|
||
throw new Error(`「${step.name}」的前置还没齐,不能增殖`);
|
||
}
|
||
const spawned = spawnInstanceFromPrototype({
|
||
flow,
|
||
catalog,
|
||
prototypeId: step.id,
|
||
acceptedStepIds: accepted,
|
||
});
|
||
if ("error" in spawned) throw new Error(spawned.error);
|
||
const content = stringifyCreationFlow(spawned.flow);
|
||
this.blackboard.write({
|
||
tag: CREATION_FLOW_TAG,
|
||
content,
|
||
source: "user",
|
||
});
|
||
this.session = {
|
||
...this.session,
|
||
slots: { ...this.session.slots, [CREATION_FLOW_TAG]: content },
|
||
};
|
||
flow = spawned.flow;
|
||
step = spawned.step;
|
||
} else if (!reenter && !isStepReady(flow, step, accepted)) {
|
||
throw new Error(`「${step.name}」的前置节点还没完成,不能开始`);
|
||
}
|
||
|
||
const pendingModule = findModuleByName(catalog, step.name);
|
||
if (reenter) {
|
||
const artTag = pendingModule?.artifact?.trim();
|
||
const hasArt = Boolean(
|
||
artTag && this.blackboard.getContentByTag(artTag)?.trim(),
|
||
);
|
||
if (
|
||
!isStepAccepted(step, accepted) &&
|
||
!hasArt &&
|
||
!this.findReenterArtifact(step.id, artTag)
|
||
) {
|
||
throw new Error(`「${step.name}」还没完成,请直接进入而不是回头修改`);
|
||
}
|
||
}
|
||
const paramSpecs = pendingModule?.params ?? [];
|
||
const auto = pendingModule?.auto === true;
|
||
if (opts?.stepParams && Object.keys(opts.stepParams).length > 0) {
|
||
const next = patchCreationFlowStepParams(flow, step.id, opts.stepParams);
|
||
const content = stringifyCreationFlow(next);
|
||
this.blackboard.write({
|
||
tag: CREATION_FLOW_TAG,
|
||
content,
|
||
source: "user",
|
||
});
|
||
this.session = {
|
||
...this.session,
|
||
slots: { ...this.session.slots, [CREATION_FLOW_TAG]: content },
|
||
};
|
||
}
|
||
const latest = parseCreationFlow(
|
||
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||
);
|
||
const pinned = latest ? findStepByRef(latest, step.id) ?? step : step;
|
||
const params = pinned.params ? { ...pinned.params } : {};
|
||
const paramsMissing = missingRequiredStepParams(pinned, pendingModule);
|
||
this.pinCreationStepSnapshot({
|
||
stepId: pinned.id,
|
||
name: pinned.name,
|
||
declaration: pendingModule?.declaration?.trim() || "",
|
||
params,
|
||
paramsMissing,
|
||
paramSpecs,
|
||
kind: pendingModule?.kind,
|
||
closer: pendingModule?.closer === true,
|
||
revise: reenter || isReviseStep(pinned),
|
||
revises: pinned.revises,
|
||
});
|
||
|
||
const waitingPick =
|
||
this.session.waitingReason?.kind === "pick_creation_step";
|
||
const needsParamForm = paramsMissing.length > 0 && !auto;
|
||
|
||
if (reenter) {
|
||
const existing = this.ensureReenterArtifact(
|
||
pinned.id,
|
||
pendingModule?.artifact,
|
||
);
|
||
if (existing) {
|
||
this.reopenArtifactForRevision(existing, pinned);
|
||
return;
|
||
}
|
||
throw new Error(
|
||
`找不到「${pinned.name}」的已有产物,无法回头修改。要新开一条请点原型上的「增殖」。`,
|
||
);
|
||
}
|
||
|
||
if (needsParamForm) {
|
||
const decision = createDecision({
|
||
action: "run_worker",
|
||
workerId: DESIGN_STEP_WORKER_ID,
|
||
reason: `「${pinned.name}」还缺参数:${paramsMissing.join("、")}`,
|
||
requiresApproval: true,
|
||
});
|
||
await this.dispatch({
|
||
type: "main_agent_decision_created",
|
||
payload: { decision },
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (waitingPick) {
|
||
await this.dispatch({
|
||
type: "user_picked_creation_step",
|
||
payload: { stepId: pinned.id },
|
||
});
|
||
return;
|
||
}
|
||
|
||
const decision = createDecision({
|
||
action: "run_worker",
|
||
workerId: DESIGN_STEP_WORKER_ID,
|
||
reason: reenter || isReviseStep(pinned)
|
||
? `回头修改 · ${pinned.name}`
|
||
: `开始:${pinned.name}`,
|
||
requiresApproval: false,
|
||
});
|
||
await this.dispatch({
|
||
type: "main_agent_decision_created",
|
||
payload: { decision },
|
||
});
|
||
}
|
||
|
||
/** 回头修改:打开该步已有产物,让用户像未验收时一样写意见再改。 */
|
||
private findReenterArtifact(
|
||
stepId: string,
|
||
artifactTag?: string,
|
||
): ArtifactRecord | undefined {
|
||
const id = stepId.trim();
|
||
const tag = artifactTag?.trim();
|
||
if (!id && !tag) return undefined;
|
||
const usable = this.session.artifacts.filter(
|
||
(a) => a.workerId === DESIGN_STEP_WORKER_ID && a.status !== "rejected",
|
||
);
|
||
const byStep = id ? usable.filter((a) => a.stepId === id) : [];
|
||
const byTag = tag
|
||
? usable.filter((a) => a.outputTags.includes(tag))
|
||
: [];
|
||
const matches = byStep.length ? byStep : byTag;
|
||
const accepted = matches.filter((a) => a.status === "accepted");
|
||
const pool = accepted.length ? accepted : matches;
|
||
return pool.at(-1);
|
||
}
|
||
|
||
private ensureReenterArtifact(
|
||
stepId: string,
|
||
artifactTag?: string,
|
||
): ArtifactRecord | undefined {
|
||
const found = this.findReenterArtifact(stepId, artifactTag);
|
||
if (found) return found;
|
||
const tag = artifactTag?.trim();
|
||
let content = tag
|
||
? this.blackboard.getContentByTag(tag)?.trim() || ""
|
||
: "";
|
||
if (!content) {
|
||
content = this.readAcceptedUnitSlice(stepId);
|
||
if (content && tag) {
|
||
this.blackboard.write({
|
||
tag,
|
||
content,
|
||
source: "runtime",
|
||
});
|
||
}
|
||
}
|
||
if (!tag || !content) return undefined;
|
||
const artifact = createArtifact({
|
||
workerId: DESIGN_STEP_WORKER_ID,
|
||
stepId,
|
||
outputTags: [tag],
|
||
summary: "既有产物",
|
||
});
|
||
this.session = {
|
||
...this.session,
|
||
artifacts: [...this.session.artifacts, artifact],
|
||
};
|
||
return artifact;
|
||
}
|
||
|
||
private readAcceptedUnitSlice(stepId: string): string {
|
||
const store = parseAcceptedContentStore(
|
||
this.blackboard.getContentByTag(CREATION_ACCEPTED_CONTENT_TAG),
|
||
);
|
||
const entry = store.units[normalizeCreationUnitId(stepId)];
|
||
if (!entry || entry.content == null) return "";
|
||
return typeof entry.content === "string"
|
||
? entry.content.trim()
|
||
: JSON.stringify(entry.content, null, 2).trim();
|
||
}
|
||
|
||
private reopenArtifactForRevision(
|
||
artifact: ArtifactRecord,
|
||
step: { id: string; name: string },
|
||
): void {
|
||
const now = new Date().toISOString();
|
||
this.session = {
|
||
...this.session,
|
||
phase: "waiting_user",
|
||
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
|
||
pendingArtifactId: artifact.id,
|
||
currentWorkerId: DESIGN_STEP_WORKER_ID,
|
||
currentStepId: step.id,
|
||
pendingDecision: undefined,
|
||
artifacts: this.session.artifacts.map((a) =>
|
||
a.id === artifact.id
|
||
? { ...a, status: "under_review", updatedAt: now }
|
||
: a,
|
||
),
|
||
slots: {
|
||
...this.session.slots,
|
||
revisionTargetArtifactId: artifact.id,
|
||
},
|
||
updatedAt: now,
|
||
};
|
||
this.onMessage(
|
||
`已回到「${step.name}」,可按意见修改现有产物(继承原文),或返回节点再选别的步骤。`,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 验收后回到分层图:点节点即确认并进入,底栏写意见再发即改编排。
|
||
*/
|
||
private async awaitCreationStepPick(): Promise<void> {
|
||
this.syncSlotsToBlackboard(this.session);
|
||
const flow = parseCreationFlow(
|
||
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||
);
|
||
const accepted = this.readAcceptedStepIds();
|
||
if (!flow) {
|
||
debugLog("step", "暂无可用流程,等待点选或改编排");
|
||
await this.enterCreationStepPickWait();
|
||
return;
|
||
}
|
||
|
||
const catalog = await this.loadActiveModuleCatalog();
|
||
const ready = listReadySteps(flow, accepted);
|
||
const autoReady = ready.filter((step) => {
|
||
const mod = findModuleByName(catalog, step.name);
|
||
return (
|
||
mod?.auto === true &&
|
||
missingRequiredStepParams(step, mod).length === 0
|
||
);
|
||
});
|
||
if (autoReady.length > 0 && autoReady.length === ready.length) {
|
||
const protoReady = listSelectablePrototypes(flow, accepted).length > 0;
|
||
const spawnReady = catalog
|
||
? listRepeatableSpawns(flow, catalog, accepted).some((s) => s.ready)
|
||
: false;
|
||
if (!protoReady && !spawnReady) {
|
||
debugLog("step", `程序步直接开始 ${autoReady[0]!.id}`);
|
||
await this.startCreationStep(autoReady[0]!.id);
|
||
return;
|
||
}
|
||
}
|
||
debugLog(
|
||
"step",
|
||
hasSelectableCreationWork(flow, catalog, accepted)
|
||
? "等待点选创作节点"
|
||
: "图上暂无已就绪节点,点节点进入或在底栏写意见改编排",
|
||
);
|
||
await this.enterCreationStepPickWait();
|
||
}
|
||
|
||
private async enterCreationStepPickWait(): Promise<void> {
|
||
this.clearProposedStep();
|
||
if (this.session.waitingReason?.kind === "pick_creation_step") return;
|
||
if (this.session.phase === "running") {
|
||
await this.dispatch({ type: "creation_step_pick_awaited", payload: {} });
|
||
return;
|
||
}
|
||
this.session = {
|
||
...this.session,
|
||
phase: "waiting_user",
|
||
waitingReason: { kind: "pick_creation_step" },
|
||
pendingDecision: undefined,
|
||
currentWorkerId: undefined,
|
||
updatedAt: new Date().toISOString(),
|
||
};
|
||
}
|
||
|
||
private clearProposedStep(): void {
|
||
this.session = {
|
||
...this.session,
|
||
slots: {
|
||
...this.session.slots,
|
||
[CREATION_PROPOSED_STEP_TAG]: undefined,
|
||
},
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 选定开场白后:把候选落成 输出.开场白 / 初值表。不关 DAG,创作流程继续。
|
||
*/
|
||
private sealCreationOpening(): void {
|
||
const selectedRaw = this.session.slots[SLOT_OPENING_SELECTED_INDEX];
|
||
const selectedIndex =
|
||
typeof selectedRaw === "number"
|
||
? selectedRaw
|
||
: typeof selectedRaw === "string" && selectedRaw.trim()
|
||
? Number(selectedRaw)
|
||
: 0;
|
||
|
||
const fragmentRaw =
|
||
this.blackboard.getContentByTag(OPENING_SETUP_ARTIFACT_TAG) ?? "";
|
||
const payload = parseOpeningSealPayload(fragmentRaw, selectedIndex);
|
||
const existingOpening = this.blackboard.getContentByTag(OPENING_OUTPUT_TAG)?.trim();
|
||
const openingText = payload?.selectedText?.trim() || existingOpening || "";
|
||
|
||
if (openingText) {
|
||
this.writeWorkerTagContent(OPENING_OUTPUT_TAG, openingText, "opening-setup");
|
||
}
|
||
|
||
if (payload?.variables.length) {
|
||
const source = "worker:opening-setup";
|
||
const initialPatch = mergeOpeningTablePatch(
|
||
this.blackboard.getContentByTag(OPENING_INITIAL_VARS_TAG),
|
||
payload.variables,
|
||
source,
|
||
);
|
||
if (initialPatch) {
|
||
this.writeWorkerTagContent(OPENING_INITIAL_VARS_TAG, initialPatch, "opening-setup");
|
||
}
|
||
const currentPatch = mergeOpeningTablePatch(
|
||
this.blackboard.getContentByTag(OPENING_CURRENT_VARS_TAG),
|
||
payload.variables,
|
||
source,
|
||
);
|
||
if (currentPatch) {
|
||
this.writeWorkerTagContent(OPENING_CURRENT_VARS_TAG, currentPatch, "opening-setup");
|
||
}
|
||
}
|
||
|
||
const n = payload?.candidates.length ?? (openingText ? 1 : 0);
|
||
const which =
|
||
n > 1 && payload
|
||
? `第 ${payload.selectedIndex + 1}/${n} 条`
|
||
: n
|
||
? "开场白"
|
||
: "开场";
|
||
this.onMessage(
|
||
`[创作] 已选定${which}。可保存为产物后开玩,或继续补节点。`,
|
||
);
|
||
|
||
this.session = {
|
||
...this.session,
|
||
slots: {
|
||
...this.session.slots,
|
||
[SLOT_CREATION_SEALED_BY_OPENING]: true,
|
||
[OPENING_OUTPUT_TAG]: openingText || this.session.slots[OPENING_OUTPUT_TAG],
|
||
},
|
||
};
|
||
this.ensurePlaySpecReady();
|
||
}
|
||
|
||
private hasAcceptedCloser(): boolean {
|
||
const flow = parseCreationFlow(
|
||
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||
);
|
||
return hasAcceptedCloserStep(flow, this.readAcceptedStepIds());
|
||
}
|
||
|
||
/** 收口后若还没有运行规格,用草稿或默认槽位补一份,才能进游玩 */
|
||
private ensurePlaySpecReady(): void {
|
||
// Boss 直聘:落档/开玩时自动挂载产物;种变量初值;按映射重投影
|
||
if (this.isDictateIntakeMode()) {
|
||
const bound = applyDictatePlayBind(this.blackboard);
|
||
if (bound.boundTags.length) {
|
||
this.onMessage(
|
||
`[Boss直聘] 已自动挂载 ${bound.boundTags.length} 个产物到游玩上下文:${bound.boundTags.join("、")}`,
|
||
);
|
||
} else {
|
||
this.onMessage("[Boss直聘] 已生成默认游玩规格(尚无产物可挂载)");
|
||
}
|
||
this.seedVariablesAndReproject("opening-setup");
|
||
} else {
|
||
const finalTag = this.blackboard.getContentByTag(WORKER_SET_FINAL_TAG)?.trim();
|
||
const draft = this.blackboard.getContentByTag(WORKER_SET_DRAFT_TAG)?.trim();
|
||
if (!finalTag) {
|
||
const spec = draft?.trim()
|
||
? draft
|
||
: JSON.stringify({ play_slots: defaultPlaySlots() });
|
||
this.writeWorkerTagContent(WORKER_SET_FINAL_TAG, spec, "opening-setup");
|
||
}
|
||
}
|
||
if (this.session.slots.designInstanceReady) return;
|
||
this.session = {
|
||
...this.session,
|
||
slots: {
|
||
...this.session.slots,
|
||
designInstanceReady: true,
|
||
},
|
||
};
|
||
}
|
||
|
||
/** 目录初值 → 变量.当前;再按 设计.变量映射 重写投影 tag */
|
||
private seedVariablesAndReproject(actor: string): void {
|
||
const seeded = seedVariablesFromCatalog({
|
||
catalogRaw: this.blackboard.getContentByTag(VARIABLE_CATALOG_TAG),
|
||
currentRaw: this.blackboard.getContentByTag(OPENING_CURRENT_VARS_TAG),
|
||
initialRaw: this.blackboard.getContentByTag(OPENING_INITIAL_VARS_TAG),
|
||
actor: `system:${actor}`,
|
||
});
|
||
if (seeded.initial) {
|
||
this.blackboard.write({
|
||
tag: OPENING_INITIAL_VARS_TAG,
|
||
content: seeded.initial,
|
||
source: actor,
|
||
});
|
||
}
|
||
if (seeded.current) {
|
||
this.blackboard.write({
|
||
tag: OPENING_CURRENT_VARS_TAG,
|
||
content: seeded.current,
|
||
source: actor,
|
||
});
|
||
if (seeded.seededKeys.length) {
|
||
this.onMessage(
|
||
`[变量] 已种初值:${seeded.seededKeys.join("、")}`,
|
||
);
|
||
}
|
||
}
|
||
this.applyValueMapProjections(actor);
|
||
}
|
||
|
||
/** 按当前真值持续投影映射 tag(非 once) */
|
||
private applyValueMapProjections(workerId: string): void {
|
||
if (!this.blackboard.getContentByTag(VALUE_MAP_TAG)?.trim()) return;
|
||
const result = reprojectValueMaps({
|
||
blackboard: this.blackboard,
|
||
source: `value-map:${workerId}`,
|
||
});
|
||
if (result.written.length) {
|
||
this.onMessage(
|
||
`[变量映射] 投影 ${result.written
|
||
.map((w) => `${w.field}=${String(w.value)}→${w.tag}`)
|
||
.join("、")}`,
|
||
);
|
||
}
|
||
}
|
||
|
||
/** 再编排:解开开场收口,让 design-flow 能在收口前插入节点 */
|
||
private unsealCreationOpening(): void {
|
||
const reopened = reopenCreationFlowRaw(
|
||
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||
);
|
||
if (reopened) {
|
||
this.blackboard.write({
|
||
tag: CREATION_FLOW_TAG,
|
||
content: reopened,
|
||
source: "runtime",
|
||
});
|
||
}
|
||
const slots = { ...this.session.slots };
|
||
delete slots[SLOT_CREATION_SEALED_BY_OPENING];
|
||
if (reopened) slots[CREATION_FLOW_TAG] = reopened;
|
||
this.session = { ...this.session, slots };
|
||
}
|
||
|
||
private applyProposedStepParams(params: CreationFlowStepParams): void {
|
||
const raw =
|
||
(typeof this.session.slots[CREATION_PROPOSED_STEP_TAG] === "string"
|
||
? String(this.session.slots[CREATION_PROPOSED_STEP_TAG])
|
||
: null) ||
|
||
this.blackboard.getContentByTag(CREATION_PROPOSED_STEP_TAG);
|
||
if (!raw?.trim()) return;
|
||
let snap: {
|
||
stepId?: string;
|
||
kind?: string;
|
||
paramSpecs?: Array<{ key: string; required?: boolean }>;
|
||
};
|
||
try {
|
||
snap = JSON.parse(raw) as typeof snap;
|
||
} catch {
|
||
return;
|
||
}
|
||
const stepId = typeof snap.stepId === "string" ? snap.stepId.trim() : "";
|
||
if (!stepId) return;
|
||
const flow = parseCreationFlow(
|
||
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||
);
|
||
if (!flow) return;
|
||
const next = patchCreationFlowStepParams(flow, stepId, params);
|
||
const content = stringifyCreationFlow(next);
|
||
this.blackboard.write({
|
||
tag: CREATION_FLOW_TAG,
|
||
content,
|
||
source: "user",
|
||
});
|
||
const paramsMissing =
|
||
snap.kind === "prior-artifact"
|
||
? []
|
||
: (snap.paramSpecs ?? [])
|
||
.filter((p) => p.required)
|
||
.map((p) => p.key)
|
||
.filter((key) => {
|
||
const v = params[key];
|
||
return v == null || (typeof v === "string" && !v.trim());
|
||
});
|
||
const snapshot = {
|
||
...snap,
|
||
params,
|
||
paramsMissing,
|
||
};
|
||
this.blackboard.write({
|
||
tag: CREATION_PROPOSED_STEP_TAG,
|
||
content: JSON.stringify(snapshot),
|
||
source: "runtime",
|
||
});
|
||
this.session = {
|
||
...this.session,
|
||
slots: {
|
||
...this.session.slots,
|
||
[CREATION_FLOW_TAG]: content,
|
||
[CREATION_PROPOSED_STEP_TAG]: JSON.stringify(snapshot),
|
||
},
|
||
};
|
||
}
|
||
|
||
private assertProposedStepReady(): string | null {
|
||
const decision = this.session.pendingDecision;
|
||
if (decision?.workerId !== "design-step") return null;
|
||
const raw =
|
||
(typeof this.session.slots[CREATION_PROPOSED_STEP_TAG] === "string"
|
||
? String(this.session.slots[CREATION_PROPOSED_STEP_TAG])
|
||
: null) ||
|
||
this.blackboard.getContentByTag(CREATION_PROPOSED_STEP_TAG);
|
||
if (!raw?.trim()) return null;
|
||
try {
|
||
const snap = JSON.parse(raw) as { paramsMissing?: string[] };
|
||
if (snap.paramsMissing?.length) {
|
||
return `请先补齐必填参数:${snap.paramsMissing.join("、")}`;
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private getInstanceWorkerDeclaration() {
|
||
return buildInstanceWorkerDeclaration(
|
||
this.session,
|
||
this.blackboard,
|
||
inferLifecycleStage(this.session),
|
||
);
|
||
}
|
||
|
||
/** world-simulator:run_worker 必须在 设计.worker集 声明的 activeWorkerIds 内 */
|
||
private assertWorkerDeclared(workerId: string): void {
|
||
const active = this.getActiveSkill();
|
||
if (!shouldEnforceWorkerDeclaration(active?.name)) return;
|
||
|
||
const declaration = this.getInstanceWorkerDeclaration();
|
||
const resolved = resolveWorkerId(workerId);
|
||
if (!declaration.activeWorkerIds.includes(resolved)) {
|
||
throw new Error(formatUndeclaredWorkerError(resolved, declaration));
|
||
}
|
||
}
|
||
|
||
private async runRealWorker(
|
||
effect: Extract<PhaseEffect, { type: "run_worker" }>,
|
||
): Promise<void> {
|
||
this.assertWorkerDeclared(effect.workerId);
|
||
|
||
const activeSkill = this.getActiveSkill();
|
||
if (!activeSkill?.name) {
|
||
throw new Error("当前没有 active skill,无法运行 worker");
|
||
}
|
||
|
||
const workerId = resolveWorkerId(effect.workerId);
|
||
|
||
// 机遇裁定:纯程序,不走 LLM
|
||
if (workerId === "chance") {
|
||
await this.runChanceWorker(effect);
|
||
return;
|
||
}
|
||
|
||
if (!this.llm) {
|
||
throw new Error("未配置 LLM,无法运行 worker");
|
||
}
|
||
this.lastWorkerRunSnapshot = {
|
||
workerId,
|
||
runtimeSession: structuredClone(this.session),
|
||
blackboardItems: this.blackboard.exportItems(),
|
||
};
|
||
|
||
let slots = { ...this.session.slots };
|
||
|
||
this.syncSlotsToBlackboard(this.session);
|
||
|
||
if (effect.workerContext?.roleId?.trim()) {
|
||
const roleId = effect.workerContext.roleId.trim();
|
||
slots["世界.当前角色.id"] = roleId;
|
||
this.blackboard.write({
|
||
tag: "世界.当前角色.id",
|
||
content: roleId,
|
||
source: "runtime",
|
||
});
|
||
this.session = { ...this.session, slots };
|
||
}
|
||
|
||
const acceptanceMode = resolveAcceptanceModeForWorker({
|
||
session: this.session,
|
||
blackboard: this.blackboard,
|
||
workerId: effect.workerId,
|
||
});
|
||
|
||
this.session = (
|
||
await this.dispatch({
|
||
type: "worker_started",
|
||
payload: {
|
||
workerId: effect.workerId,
|
||
stepId: this.session.currentStepId,
|
||
acceptanceMode:
|
||
this.session.resumeContext?.acceptanceMode ?? acceptanceMode,
|
||
},
|
||
})
|
||
).session;
|
||
|
||
if (isDesignDiskWorker(workerId)) {
|
||
this.beginCreationUnitRun(workerId);
|
||
}
|
||
|
||
// design-step:能力「默认问题」由程序先发出,等用户首答后再调 LLM
|
||
if (workerId === "design-step") {
|
||
const delivered = await this.maybeDeliverModuleOpening(activeSkill.name);
|
||
if (delivered) return;
|
||
}
|
||
|
||
const resolved = await resolveRunnableWorker({
|
||
skillPackName: activeSkill.name,
|
||
workerId: effect.workerId,
|
||
session: this.session,
|
||
blackboard: this.blackboard,
|
||
});
|
||
const worker = resolved.worker;
|
||
this.ensureResidentContextMounted(workerId);
|
||
const workerLlm = resolveWorkerLlmProvider({
|
||
worker,
|
||
bindings: (await loadSkill(activeSkill.name)).workerLlmBindings,
|
||
slots,
|
||
fallbackLlm: this.llm,
|
||
});
|
||
|
||
this.onMessage(
|
||
`[Worker] ${workerId} 执行中…${resolved.source === "declaration" ? "(声明驱动)" : ""}`,
|
||
);
|
||
debugLog("step", `调用模型 ${workerId}`);
|
||
this.onWorkerStreamStart?.(workerId);
|
||
|
||
try {
|
||
const result = await runWorkerSkill({
|
||
skillName: activeSkill.name,
|
||
workerId: effect.workerId,
|
||
slots,
|
||
blackboard: this.blackboard,
|
||
llm: workerLlm,
|
||
declared: { worker: resolved.worker, promptBody: resolved.promptBody },
|
||
stream: {
|
||
onThinkingDelta: (delta) =>
|
||
this.onWorkerThinkingDelta?.(workerId, delta),
|
||
onOutputDelta: (delta) =>
|
||
this.onWorkerOutputDelta?.(workerId, delta),
|
||
},
|
||
});
|
||
|
||
const settled = this.settlePlayWorkerResult(workerId, result);
|
||
|
||
if (settled.askUser?.length && Object.keys(settled.outputs).length === 0) {
|
||
// 无产物:阻塞追问(信息不足,必须补)
|
||
if (await this.restoreRevisionTargetForReview(workerId, settled.askUser)) {
|
||
return;
|
||
}
|
||
await this.workerAsk(settled.askUser);
|
||
return;
|
||
}
|
||
if (
|
||
workerId === "design-step" &&
|
||
Object.keys(settled.outputs).length === 0
|
||
) {
|
||
const retryQuestions = settled.askUser?.length
|
||
? settled.askUser
|
||
: [
|
||
{
|
||
id: "design-step-retry",
|
||
prompt:
|
||
"这一步还没写出可验收的产物。请再补一点你最在意的体验或参与方式;也可以说「按已有描述先出一版」。",
|
||
allowOther: true,
|
||
required: true,
|
||
},
|
||
];
|
||
if (await this.restoreRevisionTargetForReview(workerId, retryQuestions)) {
|
||
return;
|
||
}
|
||
await this.workerAsk(retryQuestions);
|
||
return;
|
||
}
|
||
|
||
slots = { ...this.session.slots };
|
||
const writtenTags: string[] = [];
|
||
for (const [tag, content] of Object.entries(settled.outputs)) {
|
||
if (isProgressPointerTag(tag)) continue;
|
||
const written = this.writeWorkerTagContent(tag, content, workerId);
|
||
slots[tag] = written;
|
||
writtenTags.push(tag);
|
||
}
|
||
this.session = { ...this.session, slots };
|
||
|
||
const artifact = createArtifact({
|
||
workerId: effect.workerId,
|
||
stepId: this.session.currentStepId,
|
||
outputTags: (writtenTags.length > 0 ? writtenTags : worker.outputTags).filter(
|
||
(t) => !isProgressPointerTag(t),
|
||
),
|
||
summary: settled.summary,
|
||
});
|
||
this.session = { ...this.session, artifacts: [...this.session.artifacts, artifact] };
|
||
|
||
const hasPlayVisible = Object.keys(settled.outputs).some(
|
||
(t) => t === "输出.用户展示" || t === "输出.开场白",
|
||
);
|
||
this.onMessage(
|
||
`[Worker] ${workerId} 已完成\n\n${settled.preview}${
|
||
!hasPlayVisible && settled.preview.length >= 4000 ? "\n\n…" : ""
|
||
}`,
|
||
);
|
||
|
||
await this.dispatch({
|
||
type: "worker_completed",
|
||
payload: {
|
||
artifactId: artifact.id,
|
||
// 有产物时 askUser 挂到验收态,不阻断 Accept
|
||
questions: this.freshSidecarQuestions(settled.askUser),
|
||
assessment: settled.askAssessment?.trim() || undefined,
|
||
},
|
||
});
|
||
this.maybeCompressAcceptedArtifact(artifact.id);
|
||
} finally {
|
||
this.onWorkerStreamDone?.(workerId);
|
||
}
|
||
}
|
||
|
||
/** 机遇裁定:程序掷骰/比点/抽签,写入 运行.本轮.机遇 */
|
||
private async runChanceWorker(
|
||
effect: Extract<PhaseEffect, { type: "run_worker" }>,
|
||
): Promise<void> {
|
||
const workerId = "chance";
|
||
this.lastWorkerRunSnapshot = {
|
||
workerId,
|
||
runtimeSession: structuredClone(this.session),
|
||
blackboardItems: this.blackboard.exportItems(),
|
||
};
|
||
|
||
const acceptanceMode = resolveAcceptanceModeForWorker({
|
||
session: this.session,
|
||
blackboard: this.blackboard,
|
||
workerId: effect.workerId,
|
||
});
|
||
|
||
this.session = (
|
||
await this.dispatch({
|
||
type: "worker_started",
|
||
payload: {
|
||
workerId: effect.workerId,
|
||
stepId: this.session.currentStepId,
|
||
acceptanceMode:
|
||
this.session.resumeContext?.acceptanceMode ?? acceptanceMode,
|
||
},
|
||
})
|
||
).session;
|
||
|
||
const boardRaw = this.blackboard.getContentByTag("运行.机会请求");
|
||
const batch = resolveChanceBatchRequest({
|
||
workerContext: effect.workerContext ?? null,
|
||
blackboardRequestJson: boardRaw,
|
||
});
|
||
|
||
const result = batch?.length
|
||
? batch.length === 1
|
||
? executeChance(batch[0]!)
|
||
: executeChanceBatch(batch)
|
||
: (() => {
|
||
const single = resolveChanceRequest({
|
||
workerContext: effect.workerContext ?? null,
|
||
blackboardRequestJson: boardRaw,
|
||
});
|
||
if (single) return executeChance(single);
|
||
return {
|
||
schema: "chance.v1" as const,
|
||
op: "roll" as const,
|
||
ok: false,
|
||
summary:
|
||
"机遇失败:缺少请求(workerContext.chance.requests 或 运行.机会请求)",
|
||
detail: {},
|
||
error: "missing_request",
|
||
};
|
||
})();
|
||
|
||
const content = JSON.stringify(result, null, 2);
|
||
const written = this.writeWorkerTagContent(
|
||
"运行.本轮.机遇",
|
||
content,
|
||
workerId,
|
||
);
|
||
this.session = {
|
||
...this.session,
|
||
slots: { ...this.session.slots, "运行.本轮.机遇": written },
|
||
};
|
||
|
||
const artifact = createArtifact({
|
||
workerId: effect.workerId,
|
||
stepId: this.session.currentStepId,
|
||
outputTags: ["运行.本轮.机遇"],
|
||
summary: result.summary,
|
||
});
|
||
this.session = {
|
||
...this.session,
|
||
artifacts: [...this.session.artifacts, artifact],
|
||
};
|
||
|
||
this.onMessage(`[机遇裁定] ${result.summary}\n\n${content}`);
|
||
|
||
await this.dispatch({
|
||
type: "worker_completed",
|
||
payload: { artifactId: artifact.id },
|
||
});
|
||
this.maybeCompressAcceptedArtifact(artifact.id);
|
||
}
|
||
|
||
/**
|
||
* 占位 worker:worker_started → 可选自动 worker_completed。
|
||
* 演示与单测用,真实环境应替换为真实 worker 调度。
|
||
*/
|
||
private async runStubWorker(
|
||
effect: Extract<PhaseEffect, { type: "run_worker" }>,
|
||
autoComplete = true,
|
||
): Promise<void> {
|
||
this.assertWorkerDeclared(effect.workerId);
|
||
|
||
const workerId = resolveWorkerId(effect.workerId);
|
||
this.lastWorkerRunSnapshot = {
|
||
workerId,
|
||
runtimeSession: structuredClone(this.session),
|
||
blackboardItems: this.blackboard.exportItems(),
|
||
};
|
||
|
||
const acceptanceMode = resolveAcceptanceModeForWorker({
|
||
session: this.session,
|
||
blackboard: this.blackboard,
|
||
workerId: effect.workerId,
|
||
});
|
||
|
||
this.session = (
|
||
await this.dispatch({
|
||
type: "worker_started",
|
||
payload: {
|
||
workerId: effect.workerId,
|
||
stepId: this.session.currentStepId,
|
||
acceptanceMode:
|
||
this.session.resumeContext?.acceptanceMode ?? acceptanceMode,
|
||
},
|
||
})
|
||
).session;
|
||
|
||
this.onMessage(`[Worker 占位] ${effect.workerId} 已开始。`);
|
||
|
||
if (!autoComplete) return;
|
||
|
||
const lastInput = String(this.session.slots.lastUserInput ?? "");
|
||
const value = `[${effect.workerId} 占位输出]\n${lastInput ? `输入: ${lastInput}\n` : ""}说明: stub worker,后续替换为真实 LLM worker。`;
|
||
|
||
const outputTags = await this.resolveWorkerOutputTags(effect.workerId);
|
||
const tagsToWrite = outputTags.length > 0 ? outputTags : ["output.草稿"];
|
||
|
||
for (const tag of tagsToWrite) {
|
||
this.blackboard.write({
|
||
tag,
|
||
content: value,
|
||
source: effect.workerId,
|
||
});
|
||
}
|
||
|
||
const artifact = createArtifact({
|
||
workerId: effect.workerId,
|
||
stepId: this.session.currentStepId,
|
||
outputTags: tagsToWrite,
|
||
summary: `${effect.workerId} 占位产物`,
|
||
});
|
||
this.session = { ...this.session, artifacts: [...this.session.artifacts, artifact] };
|
||
|
||
await this.dispatch({
|
||
type: "worker_completed",
|
||
payload: { artifactId: artifact.id },
|
||
});
|
||
this.maybeCompressAcceptedArtifact(artifact.id);
|
||
}
|
||
|
||
/** 表类 tag 走字段格合并,尊重 user 手改与 rev;合并后算边沿副作用 */
|
||
private writeWorkerTagContent(
|
||
tag: string,
|
||
content: string,
|
||
workerId: string,
|
||
): string {
|
||
const tableTags = new Set(["运行.初始变量", "变量.当前"]);
|
||
let toWrite = content;
|
||
let prevDoc: TableDoc | null = null;
|
||
let nextDoc: TableDoc | null = null;
|
||
if (tableTags.has(tag)) {
|
||
const patch = parseTableDoc(content);
|
||
if (patch) {
|
||
prevDoc = parseTableDoc(this.blackboard.getContentByTag(tag));
|
||
const { doc, skipped } = mergeTableCells({
|
||
current: prevDoc,
|
||
patch,
|
||
actor: `worker:${workerId}`,
|
||
});
|
||
nextDoc = doc;
|
||
toWrite = stringifyTableDoc(doc);
|
||
if (skipped.length) {
|
||
this.onMessage(
|
||
`[表合并] ${tag}:跳过 ${skipped.map((s) => `${s.key}(${s.reason})`).join("、")}`,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
if (tag === CREATION_FLOW_TAG) {
|
||
const merged = mergeCreationFlowPreservingAccepted({
|
||
prevRaw: this.blackboard.getContentByTag(tag),
|
||
nextRaw: toWrite,
|
||
acceptedStepIds: parseAcceptedUnits(
|
||
this.session.slots[SLOT_CREATION_ACCEPTED_UNITS] ??
|
||
this.blackboard.getContentByTag(CREATION_ACCEPTED_UNITS_TAG),
|
||
),
|
||
});
|
||
toWrite = merged.raw;
|
||
if (merged.restored.length) {
|
||
this.onMessage(
|
||
`[流程编排] 已验收步骤按原样保留:${merged.restored.join("、")}`,
|
||
);
|
||
}
|
||
}
|
||
this.blackboard.write({
|
||
tag,
|
||
content: toWrite,
|
||
source: workerId,
|
||
});
|
||
if (
|
||
inferLifecycleStage(this.session) === "play" &&
|
||
(tag === "输出.用户展示" || tag === "输出.开场白")
|
||
) {
|
||
const narratorEnabled = isNarratorEnabled(
|
||
parseWorkerSetYaml(
|
||
this.blackboard.getContentByTag("设计.worker集") ?? "",
|
||
)?.play_slots,
|
||
);
|
||
// 有转述时:主世界草稿不进对话史,只记终稿(转述);开场白始终记
|
||
if (
|
||
tag === "输出.开场白" ||
|
||
isPlayFinalVisibleWorker(workerId, { narratorEnabled })
|
||
) {
|
||
this.appendDialogueHistory("助手", toWrite);
|
||
}
|
||
}
|
||
if (nextDoc) {
|
||
this.applyTableSideEffects(prevDoc, nextDoc, workerId);
|
||
}
|
||
if (tag === OPENING_CURRENT_VARS_TAG || tag === "变量.当前") {
|
||
this.applyValueMapProjections(workerId);
|
||
}
|
||
// 遗留裁决包 / 旁观维护包 → 合并进变量.当前(可触发 side_effects)
|
||
if (tag === SETTLEMENT_TAG) {
|
||
this.applySettlementVariableChanges(toWrite, workerId);
|
||
} else if (tag === MAINTAIN_TAG) {
|
||
this.applyMaintainTableOps(toWrite, workerId);
|
||
}
|
||
return toWrite;
|
||
}
|
||
|
||
/** settlement.v1.variable_changes → 变量.当前 */
|
||
private applySettlementVariableChanges(
|
||
settlementRaw: string,
|
||
workerId: string,
|
||
): void {
|
||
const view = parseSettlementPacket(settlementRaw);
|
||
const changes = view.packet?.variable_changes;
|
||
if (!view.ok || !changes?.length) return;
|
||
const current = parseTableDoc(
|
||
this.blackboard.getContentByTag("变量.当前"),
|
||
);
|
||
const patch = settlementChangesToTablePatch(changes, current);
|
||
if (!patch) return;
|
||
this.writeWorkerTagContent(
|
||
"变量.当前",
|
||
stringifyTableDoc(patch),
|
||
workerId,
|
||
);
|
||
this.onMessage(
|
||
`[裁决合并] 变量.当前 ← ${changes.map((c) => c.key).join("、")}`,
|
||
);
|
||
}
|
||
|
||
/** maintain.v1.table_ops → 变量.当前(及其他已支持表) */
|
||
private applyMaintainTableOps(maintainRaw: string, workerId: string): void {
|
||
const view = parseMaintainPacket(maintainRaw);
|
||
const ops = view.packet?.table_ops;
|
||
if (!view.ok || !ops?.length) {
|
||
if (view.ok && view.packet?.need_generate) {
|
||
this.applyMaintainNeedGenerateSampling(view.packet.need_generate, workerId);
|
||
}
|
||
return;
|
||
}
|
||
const current = parseTableDoc(
|
||
this.blackboard.getContentByTag("变量.当前"),
|
||
);
|
||
const patch = maintainOpsToTablePatch(ops, current, "变量.当前");
|
||
if (!patch) return;
|
||
this.writeWorkerTagContent(
|
||
"变量.当前",
|
||
stringifyTableDoc(patch),
|
||
workerId,
|
||
);
|
||
const keys = ops
|
||
.filter((o) => (o.tag?.trim() || "变量.当前") === "变量.当前")
|
||
.map((o) => o.key);
|
||
if (keys.length) {
|
||
this.onMessage(`[旁观维护] 变量.当前 ← ${keys.join("、")}`);
|
||
}
|
||
if (view.packet?.need_generate) {
|
||
this.applyMaintainNeedGenerateSampling(view.packet.need_generate, workerId);
|
||
}
|
||
}
|
||
|
||
/** need_generate:按生成规则合同程序批量抽样,写入 运行.本轮.旁观.生成抽样 */
|
||
private applyMaintainNeedGenerateSampling(
|
||
need: { rule_id: string; reason: string },
|
||
workerId: string,
|
||
): void {
|
||
const rulesRaw = this.blackboard.getContentByTag("设计.生成规则");
|
||
const result = runMaintainNeedGenerateSampling({
|
||
need,
|
||
generationRulesRaw: rulesRaw,
|
||
});
|
||
if (!result) {
|
||
this.onMessage(
|
||
`[旁观维护] need_generate=${need.rule_id}:无可程序抽样的元素池(方向池由填写方按规则自创)`,
|
||
);
|
||
return;
|
||
}
|
||
const content = JSON.stringify(
|
||
{ rule_id: need.rule_id, reason: need.reason, chance: result },
|
||
null,
|
||
2,
|
||
);
|
||
this.writeWorkerTagContent(MAINTAIN_GENERATE_TAG, content, workerId);
|
||
this.onMessage(`[旁观维护] 生成抽样 ${need.rule_id}:${result.summary}`);
|
||
}
|
||
|
||
/** 避免 syncSlots 重复把同一句用户输入追加进历史 */
|
||
private lastAppendedUserHistory = "";
|
||
|
||
/** 追加一轮到黑板「对话.历史」(排序表可投影裁剪) */
|
||
private appendDialogueHistory(role: "用户" | "助手" | "系统", text: string): void {
|
||
const body = text.trim();
|
||
if (!body) return;
|
||
if (role === "用户") {
|
||
if (this.lastAppendedUserHistory === body) return;
|
||
this.lastAppendedUserHistory = body;
|
||
}
|
||
const prev = this.blackboard.getContentByTag(DIALOGUE_HISTORY_TAG) ?? "";
|
||
const next = appendDialogueHistoryTurn(prev, { role, text: body });
|
||
if (next === prev.trim()) return;
|
||
this.blackboard.write({
|
||
tag: DIALOGUE_HISTORY_TAG,
|
||
content: next,
|
||
source: "runtime",
|
||
});
|
||
this.session = {
|
||
...this.session,
|
||
slots: { ...this.session.slots, [DIALOGUE_HISTORY_TAG]: next },
|
||
};
|
||
}
|
||
|
||
/** 从 Worker 集 tables.side_effects 算边沿触发并写 tag / 记 fired */
|
||
private applyTableSideEffects(
|
||
prev: TableDoc | null,
|
||
next: TableDoc,
|
||
workerId: string,
|
||
): void {
|
||
const raw = readWorkerSetYamlForDeclaration(this.blackboard, this.session);
|
||
const parsed = raw ? parseWorkerSetYaml(raw.yaml) : null;
|
||
const rules = parseSideEffectRules(parsed?.tables);
|
||
if (rules.length === 0) return;
|
||
|
||
const fired = parseFiredRegistry(
|
||
this.blackboard.getContentByTag(SIDE_EFFECT_FIRED_TAG),
|
||
);
|
||
const { triggers, nextFired, queuedWorkers } = evaluateSideEffects({
|
||
prev,
|
||
next,
|
||
rules,
|
||
fired,
|
||
});
|
||
if (triggers.length === 0) return;
|
||
|
||
const { writtenTags } = applySideEffectTagActions({
|
||
blackboard: this.blackboard,
|
||
triggers,
|
||
source: `side-effect:${workerId}`,
|
||
});
|
||
this.blackboard.write({
|
||
tag: SIDE_EFFECT_FIRED_TAG,
|
||
content: stringifyFiredRegistry(nextFired),
|
||
source: "system:table-side-effect",
|
||
});
|
||
|
||
const ruleIds = triggers.map((t) => t.rule.id).join("、");
|
||
this.onMessage(
|
||
`[表副作用] 边沿触发 ${ruleIds}` +
|
||
(writtenTags.length ? ` → 写入 ${writtenTags.join("、")}` : "") +
|
||
(queuedWorkers.length
|
||
? `;建议调度 ${queuedWorkers.map((q) => q.workerId).join("、")}`
|
||
: ""),
|
||
);
|
||
|
||
if (queuedWorkers.length) {
|
||
this.session = {
|
||
...this.session,
|
||
slots: {
|
||
...this.session.slots,
|
||
pendingSideEffectWorkers: JSON.stringify(queuedWorkers),
|
||
},
|
||
};
|
||
}
|
||
}
|
||
|
||
/** 按规格把常驻上下文写入黑板,供本 worker inputTags 读取 */
|
||
private ensureResidentContextMounted(workerId: string): void {
|
||
const raw = readWorkerSetYamlForDeclaration(this.blackboard, this.session);
|
||
const parsed = raw ? parseWorkerSetYaml(raw.yaml) : null;
|
||
const entries = parseResidentContext(parsed?.resident_context);
|
||
if (entries.length === 0) return;
|
||
writeResidentContextTags({
|
||
blackboard: this.blackboard,
|
||
entries,
|
||
workerId,
|
||
source: "system:resident-context",
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 若本步能力声明了默认问题且尚未收过首答:写开场白、workerAsk,不调 LLM。
|
||
* @returns true = 已进入等待用户,调用方应 return
|
||
*/
|
||
private async maybeDeliverModuleOpening(
|
||
skillPackName: string,
|
||
): Promise<boolean> {
|
||
const skill = await loadSkill(skillPackName);
|
||
const packRoot = skill.skillPackRoot;
|
||
if (!packRoot) return false;
|
||
|
||
const flowRaw = this.blackboard.getContentByTag(CREATION_FLOW_TAG);
|
||
const currentStepName =
|
||
this.blackboard.getContentByTag(CREATION_CURRENT_STEP_TAG)?.trim() ||
|
||
String(this.session.slots[SLOT_CREATION_CURRENT_UNIT] ?? "").trim();
|
||
const accepted = parseAcceptedUnits(
|
||
this.session.slots[SLOT_CREATION_ACCEPTED_UNITS] ??
|
||
this.blackboard.getContentByTag(CREATION_ACCEPTED_UNITS_TAG),
|
||
);
|
||
|
||
const binding = await resolveDesignStepBinding({
|
||
skillPackRoot: packRoot,
|
||
flowRaw,
|
||
currentStepName,
|
||
acceptedStepNames: accepted,
|
||
inheritExisting: isInheritExistingFlag(
|
||
this.blackboard.getContentByTag(CREATION_INHERIT_EXISTING_TAG) ??
|
||
this.session.slots[CREATION_INHERIT_EXISTING_TAG],
|
||
),
|
||
});
|
||
if (!binding?.opening?.trim()) return false;
|
||
|
||
const stepKey = stepUnitId(binding.step);
|
||
const state = parseModuleOpeningState(
|
||
this.session.slots[SLOT_CREATION_MODULE_OPENING_STATE] ??
|
||
this.blackboard.getContentByTag(CREATION_MODULE_OPENING_STATE_TAG),
|
||
);
|
||
const phase = state[stepKey];
|
||
|
||
if (phase === "answered") return false;
|
||
|
||
if (phase === "shown" && this.session.resumeContext) {
|
||
// 用户刚答完默认问题 → 标记已答,继续走 LLM
|
||
state[stepKey] = "answered";
|
||
const raw = stringifyModuleOpeningState(state);
|
||
this.blackboard.write({
|
||
tag: CREATION_MODULE_OPENING_STATE_TAG,
|
||
content: raw,
|
||
source: "runtime",
|
||
});
|
||
this.session = {
|
||
...this.session,
|
||
slots: {
|
||
...this.session.slots,
|
||
[SLOT_CREATION_MODULE_OPENING_STATE]: raw,
|
||
},
|
||
resumeContext: undefined,
|
||
};
|
||
return false;
|
||
}
|
||
|
||
// 首次,或开场已发出但用户还没答(离开节点 / 恢复丢了 resume):展示引导,等用户先说一轮
|
||
state[stepKey] = "shown";
|
||
const raw = stringifyModuleOpeningState(state);
|
||
this.blackboard.write({
|
||
tag: CREATION_MODULE_OPENING_TAG,
|
||
content: binding.opening.trim(),
|
||
source: "runtime",
|
||
});
|
||
this.blackboard.write({
|
||
tag: CREATION_MODULE_OPENING_STATE_TAG,
|
||
content: raw,
|
||
source: "runtime",
|
||
});
|
||
this.session = {
|
||
...this.session,
|
||
slots: {
|
||
...this.session.slots,
|
||
[CREATION_MODULE_OPENING_TAG]: binding.opening.trim(),
|
||
[SLOT_CREATION_MODULE_OPENING_STATE]: raw,
|
||
},
|
||
};
|
||
|
||
const questions = normalizeQuestions([
|
||
{
|
||
id: MODULE_OPENING_QUESTION_ID,
|
||
prompt: binding.opening.trim(),
|
||
allowOther: true,
|
||
required: true,
|
||
},
|
||
]);
|
||
// 正文由说话面 openingGuide 展示(对齐美学纲领);workerAsk 只发短调度句
|
||
await this.workerAsk(questions);
|
||
return true;
|
||
}
|
||
|
||
/** design-flow / design-step:钉当前步骤 */
|
||
private beginCreationUnitRun(workerId: string): void {
|
||
const accepted = parseAcceptedUnits(
|
||
this.session.slots[SLOT_CREATION_ACCEPTED_UNITS] ??
|
||
this.blackboard.getContentByTag(CREATION_ACCEPTED_UNITS_TAG),
|
||
);
|
||
|
||
let current =
|
||
this.blackboard.getContentByTag(CREATION_CURRENT_UNIT_TAG)?.trim() ||
|
||
String(this.session.slots[SLOT_CREATION_CURRENT_UNIT] ?? "").trim();
|
||
|
||
if (workerId === "design-flow") {
|
||
current = "flow";
|
||
} else if (workerId === "design-step") {
|
||
const flow = parseCreationFlow(
|
||
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||
);
|
||
const pinnedId = this.readPinnedStepId();
|
||
const pinned = flow && pinnedId ? findStepByRef(flow, pinnedId) : null;
|
||
const pending = pinned ?? nextPendingStep(flow, accepted);
|
||
current = pending ? stepUnitId(pending) : current || "(无待执行步骤)";
|
||
this.blackboard.write({
|
||
tag: CREATION_CURRENT_STEP_TAG,
|
||
content: current,
|
||
source: "runtime",
|
||
});
|
||
}
|
||
|
||
if (!current) current = "flow";
|
||
|
||
debugLog("step", `钉住当前步骤 ${workerId} → ${current}`);
|
||
|
||
const anchorAt = new Date().toISOString();
|
||
this.blackboard.write({
|
||
tag: CREATION_CURRENT_UNIT_TAG,
|
||
content: current,
|
||
source: "runtime",
|
||
});
|
||
this.blackboard.write({
|
||
tag: CREATION_ACCEPTED_UNITS_TAG,
|
||
content: JSON.stringify(accepted),
|
||
source: "runtime",
|
||
});
|
||
this.session = {
|
||
...this.session,
|
||
slots: {
|
||
...this.session.slots,
|
||
[SLOT_CREATION_CURRENT_UNIT]: current,
|
||
[SLOT_CREATION_ACCEPTED_UNITS]: accepted,
|
||
[SLOT_CREATION_UNIT_ANCHOR_AT]: anchorAt,
|
||
},
|
||
};
|
||
}
|
||
|
||
private recordCreationUnitAccepted(
|
||
slots: Record<string, unknown>,
|
||
artifact: { summary?: string; outputTags: string[]; workerId?: string },
|
||
): void {
|
||
const fromBoard = this.blackboard.getContentByTag(CREATION_CURRENT_UNIT_TAG)?.trim();
|
||
const fromSlot = String(slots[SLOT_CREATION_CURRENT_UNIT] ?? "").trim();
|
||
const fromStep = this.blackboard.getContentByTag(CREATION_CURRENT_STEP_TAG)?.trim();
|
||
const fromSummary = artifact.summary?.match(/单位\s+([^\s·]+)/)?.[1]?.trim();
|
||
// 接受工作流计划 ≠ 某一步做完;「flow」不得写入已验收步骤
|
||
if (artifact.workerId === "design-flow") return;
|
||
const prev = parseAcceptedUnits(
|
||
slots[SLOT_CREATION_ACCEPTED_UNITS] ??
|
||
this.blackboard.getContentByTag(CREATION_ACCEPTED_UNITS_TAG),
|
||
);
|
||
const unitId =
|
||
artifact.workerId === "design-step"
|
||
? pickRecordedStepId({
|
||
flow: parseCreationFlow(
|
||
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||
),
|
||
alreadyAccepted: prev,
|
||
pinnedUnitId: fromSlot || fromBoard,
|
||
writtenCurrentStep: fromStep,
|
||
summaryHint: fromSummary,
|
||
}) ?? ""
|
||
: fromBoard || fromSlot || fromSummary || "";
|
||
if (!unitId || unitId === "flow") return;
|
||
const next = unitId && !prev.includes(unitId) ? [...prev, unitId] : prev;
|
||
if (next !== prev) {
|
||
debugLog("step", `验收步骤 ${unitId} 已验收 ${next.join("、")}`);
|
||
}
|
||
slots[SLOT_CREATION_ACCEPTED_UNITS] = next;
|
||
slots[SLOT_CREATION_CURRENT_UNIT] = unitId || undefined;
|
||
this.blackboard.write({
|
||
tag: CREATION_ACCEPTED_UNITS_TAG,
|
||
content: JSON.stringify(next),
|
||
source: "runtime",
|
||
});
|
||
|
||
// 前情提要:写入该单位「最后一次验收」的内容切片
|
||
if (unitId) {
|
||
let slice: unknown = null;
|
||
if (artifact.workerId === "design-step" || artifact.workerId === "design-flow") {
|
||
const pack = this.getActiveSkill()?.skillPackRoot;
|
||
// skillPackRoot may not be on snapshot — resolve via artifact tags
|
||
const contentTag =
|
||
artifact.outputTags.find(
|
||
(t) => t !== CREATION_CURRENT_STEP_TAG && t !== CREATION_CURRENT_UNIT_TAG,
|
||
) || artifact.outputTags[0];
|
||
if (contentTag) {
|
||
const raw = this.blackboard.getContentByTag(contentTag)?.trim();
|
||
if (raw) {
|
||
try {
|
||
slice = JSON.parse(raw);
|
||
} catch {
|
||
slice = raw;
|
||
}
|
||
}
|
||
}
|
||
void pack;
|
||
} else {
|
||
const draftRaw =
|
||
this.blackboard.getContentByTag(WORKER_SET_DRAFT_TAG)?.trim() ||
|
||
this.blackboard.getContentByTag(WORKER_SET_FINAL_TAG)?.trim() ||
|
||
"";
|
||
const parsed = draftRaw ? parseWorkerSetYaml(draftRaw) : null;
|
||
slice = extractUnitContentFromDraft(parsed, unitId);
|
||
}
|
||
if (slice != null) {
|
||
const existing = this.blackboard.getContentByTag(CREATION_ACCEPTED_CONTENT_TAG);
|
||
const updated = upsertAcceptedUnitContent(existing, {
|
||
unitId,
|
||
content: slice,
|
||
summary: artifact.summary,
|
||
});
|
||
this.blackboard.write({
|
||
tag: CREATION_ACCEPTED_CONTENT_TAG,
|
||
content: updated,
|
||
source: "runtime",
|
||
});
|
||
slots[CREATION_ACCEPTED_CONTENT_TAG] = updated;
|
||
}
|
||
}
|
||
}
|
||
|
||
private maybeCompressAcceptedArtifact(artifactId: string): void {
|
||
const artifact = this.session.artifacts.find((a) => a.id === artifactId);
|
||
if (!artifact || artifact.status !== "accepted") return;
|
||
const result = compressAfterWorkerAccept({
|
||
blackboard: this.blackboard,
|
||
workerId: artifact.workerId,
|
||
outputTags: artifact.outputTags,
|
||
summary: artifact.summary,
|
||
});
|
||
this.session = {
|
||
...this.session,
|
||
slots: {
|
||
...this.session.slots,
|
||
lastCompressBrief: result.briefText,
|
||
lastCompressedWorkerId: artifact.workerId,
|
||
},
|
||
};
|
||
this.onMessage(
|
||
`[上下文已压缩] ${artifact.workerId}:保留 ${result.finals.map((f) => f.tag).join("、") || "(无终产物)"};归档 ${result.archivedTags.length} 个过程 tag。下一阶段以定稿为准。`,
|
||
);
|
||
}
|
||
|
||
private async resolveWorkerOutputTags(workerId: string): Promise<string[]> {
|
||
const active = this.getActiveSkill();
|
||
if (!active?.name) return [];
|
||
try {
|
||
const resolved = await resolveRunnableWorker({
|
||
skillPackName: active.name,
|
||
workerId,
|
||
session: this.session,
|
||
blackboard: this.blackboard,
|
||
});
|
||
return resolved.worker.outputTags;
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/** 简易程序验收:summary 非空则通过,否则 revision */
|
||
private async runProgrammaticReview(artifactId: string): Promise<void> {
|
||
await this.dispatch({ type: "programmatic_review_started", payload: { artifactId } });
|
||
|
||
const artifact = this.session.artifacts.find((a) => a.id === artifactId);
|
||
const passed = Boolean(artifact?.summary && artifact.summary.length > 0);
|
||
|
||
await this.dispatch(
|
||
passed
|
||
? { type: "programmatic_review_passed", payload: { artifactId } }
|
||
: {
|
||
type: "programmatic_review_failed",
|
||
payload: { artifactId, reason: "产物 summary 为空" },
|
||
},
|
||
);
|
||
}
|
||
|
||
/** 游玩:按 playWorkerIds 开一轮 */
|
||
private async startPlayTurn(): Promise<void> {
|
||
this.syncSlotsToBlackboard(this.session);
|
||
this.clearPlayRoundTransientTags();
|
||
const ids = this.getInstanceWorkerDeclaration().playWorkerIds;
|
||
if (!ids.length) {
|
||
this.onMessage(
|
||
"[游玩] 运行规格里没有可上场的执行单元,无法推进回合。",
|
||
);
|
||
this.waitForPlayInput();
|
||
return;
|
||
}
|
||
const [first, ...rest] = ids;
|
||
this.session = {
|
||
...this.session,
|
||
slots: withPlayTurnQueue(this.session.slots, rest),
|
||
};
|
||
await this.processEffects([{ type: "run_worker", workerId: first }]);
|
||
}
|
||
|
||
/** 游玩:跑管线下一个;队空则等用户 */
|
||
private async continuePlayTurn(): Promise<void> {
|
||
if (this.session.phase === "waiting_user") return;
|
||
const queue = readPlayTurnQueue(this.session.slots);
|
||
if (!queue.length) {
|
||
this.waitForPlayInput();
|
||
return;
|
||
}
|
||
const [next, ...rest] = queue;
|
||
this.session = {
|
||
...this.session,
|
||
slots: withPlayTurnQueue(this.session.slots, rest),
|
||
};
|
||
await this.processEffects([{ type: "run_worker", workerId: next }]);
|
||
}
|
||
|
||
private waitForPlayInput(): void {
|
||
this.session = {
|
||
...this.session,
|
||
phase: "waiting_user",
|
||
waitingReason: { kind: "input" },
|
||
currentWorkerId: undefined,
|
||
pendingArtifactId: undefined,
|
||
pendingDecision: undefined,
|
||
resumeContext: undefined,
|
||
slots: withPlayTurnQueue(this.session.slots, undefined),
|
||
updatedAt: new Date().toISOString(),
|
||
};
|
||
}
|
||
|
||
/** 新回合开始前归档上一轮 transient 产物(表合并结果保留在 变量.当前) */
|
||
private clearPlayRoundTransientTags(): void {
|
||
const slots = { ...this.session.slots };
|
||
const archivedAt = new Date().toISOString();
|
||
for (const tag of PLAY_ROUND_TRANSIENT_TAGS) {
|
||
const hadBoard = this.blackboard.getLatestByTag(tag) != null;
|
||
const hadSlot = tag in slots;
|
||
if (!hadBoard && !hadSlot) continue;
|
||
this.blackboard.write({
|
||
tag,
|
||
content: "",
|
||
source: "runtime",
|
||
metadata: {
|
||
role: "archived",
|
||
archivedAt,
|
||
reason: "play_round_reset",
|
||
},
|
||
});
|
||
delete slots[tag];
|
||
}
|
||
this.session = { ...this.session, slots };
|
||
}
|
||
|
||
/** invoke_main_agent 副作用:running 时调用总管 LLM,链式推进直到需用户介入 */
|
||
private async maybeRunMainAgent(effects: PhaseEffect[]): Promise<void> {
|
||
if (inferLifecycleStage(this.session) === "play") return;
|
||
if (this.mainAgent && effects.some((e) => e.type === "invoke_main_agent")) {
|
||
await this.runMainAgent();
|
||
}
|
||
}
|
||
|
||
private hasCreationFlow(): boolean {
|
||
return Boolean(
|
||
parseCreationFlow(this.blackboard.getContentByTag(CREATION_FLOW_TAG)),
|
||
);
|
||
}
|
||
|
||
private async commitMainAgentDecision(
|
||
decision: MainAgentDecision,
|
||
): Promise<void> {
|
||
debugLog(
|
||
"step",
|
||
`总管决策 ${labelAction(decision.action)}${decision.workerId ? ` ${decision.workerId}` : ""}`,
|
||
);
|
||
this.onMessage(`[总管] ${decision.action}: ${decision.reason}`);
|
||
const result = this.commitEvent({
|
||
type: "main_agent_decision_created",
|
||
payload: { decision },
|
||
});
|
||
if (result.error) this.onMessage(result.error);
|
||
await this.processEffects(result.effects);
|
||
await this.maybeRunMainAgent(result.effects);
|
||
}
|
||
|
||
private async runMainAgent(): Promise<void> {
|
||
if (!this.mainAgent || this.session.phase !== "running") {
|
||
return;
|
||
}
|
||
|
||
if (this.hasCreationFlow()) {
|
||
debugLog("step", "已有创作流程,等待点选节点");
|
||
await this.awaitCreationStepPick();
|
||
return;
|
||
}
|
||
|
||
debugLog("step", "总管开始");
|
||
const availableWorkers = await this.resolveAvailableWorkers();
|
||
|
||
try {
|
||
const decision = await this.mainAgent.runToolLoop(
|
||
{
|
||
session: this.session,
|
||
blackboardIndex: this.blackboard.listTagIndex(),
|
||
availableWorkers,
|
||
},
|
||
{
|
||
handlers: {
|
||
readBlackboard: (tags) => this.readBlackboardForAgent(tags),
|
||
listWorkers: () => availableWorkers,
|
||
listArtifacts: () =>
|
||
this.session.artifacts.map((a) => ({
|
||
id: a.id,
|
||
workerId: a.workerId,
|
||
status: a.status,
|
||
summary: a.summary,
|
||
outputTags: a.outputTags,
|
||
})),
|
||
onToolCall: (name, detail) => {
|
||
const preview =
|
||
detail.length > 120 ? `${detail.slice(0, 120)}…` : detail;
|
||
this.onMessage(`[总管 tool] ${name}${preview ? `: ${preview}` : ""}`);
|
||
},
|
||
onThinkingDelta: (delta) => {
|
||
this.onAgentThinkingDelta?.(delta);
|
||
},
|
||
onThinkingDone: (text) => {
|
||
this.onAgentThinkingDone?.(text);
|
||
this.onMessage(`[总管 思考]\n\n${text}`);
|
||
},
|
||
},
|
||
},
|
||
);
|
||
await this.commitMainAgentDecision(decision);
|
||
} catch (err) {
|
||
const reason = err instanceof Error ? err.message : String(err);
|
||
if (this.hasCreationFlow()) {
|
||
debugLog("step", "总管无可用决策,改为等待点选");
|
||
await this.awaitCreationStepPick();
|
||
return;
|
||
}
|
||
if (isUnusableMainAgentOutput(err)) {
|
||
debugLog("step", "总管未给出工具调用,改为启动 design-flow");
|
||
await this.commitMainAgentDecision(
|
||
createDecision({
|
||
action: "run_worker",
|
||
workerId: DESIGN_FLOW_WORKER_ID,
|
||
reason: "尚无创作流程,开始编排近期起点",
|
||
requiresApproval: false,
|
||
}),
|
||
);
|
||
return;
|
||
}
|
||
debugLog("step", `总管失败 ${reason.slice(0, 120)}`);
|
||
await this.failRun(reason);
|
||
}
|
||
}
|
||
|
||
/** 总管 read_blackboard tool:按 tag 或模式读取正文 */
|
||
private readBlackboardForAgent(tags: string[]): Record<string, string> {
|
||
const out: Record<string, string> = {};
|
||
for (const pattern of tags) {
|
||
const items = this.blackboard.queryByPatterns([pattern], "latest");
|
||
if (items.length === 0) {
|
||
const slotVal = this.session.slots[pattern];
|
||
if (typeof slotVal === "string" && slotVal.trim()) {
|
||
out[pattern] = slotVal.trim();
|
||
} else {
|
||
out[pattern] = "";
|
||
}
|
||
continue;
|
||
}
|
||
for (const item of items) {
|
||
out[item.tag] = item.content;
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
private async resolveAvailableWorkers(): Promise<
|
||
Array<{ id: string; description: string }>
|
||
> {
|
||
const active = this.getActiveSkill();
|
||
if (!active?.name) return [...DEFAULT_WORKERS];
|
||
try {
|
||
const workers = await listWorkerSkills(active.name);
|
||
const installed = workers.map((w) => ({ id: w.id, description: w.description }));
|
||
|
||
if (!shouldEnforceWorkerDeclaration(active.name)) {
|
||
return installed.length > 0 ? installed : [...DEFAULT_WORKERS];
|
||
}
|
||
|
||
const declaration = this.getInstanceWorkerDeclaration();
|
||
const merged = mergeDeclaredWorkersForAgent(installed, declaration);
|
||
return merged.length > 0 ? merged : installed;
|
||
} catch {
|
||
return [...DEFAULT_WORKERS];
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 构造总管决策,强制 statePatchAllowed=false */
|
||
export function createDecision(
|
||
partial: Omit<MainAgentDecision, "id" | "statePatchAllowed"> &
|
||
Partial<Pick<MainAgentDecision, "id">>,
|
||
): MainAgentDecision {
|
||
return {
|
||
id: partial.id ?? randomUUID(),
|
||
statePatchAllowed: false,
|
||
...partial,
|
||
};
|
||
}
|
||
|
||
/** CLI 调试:单行摘要当前会话状态 */
|
||
export function formatSession(session: RuntimeSession): string {
|
||
const active = session.slots.activeSkill as ActiveSkillSnapshot | undefined;
|
||
const lines = [
|
||
`phase: ${session.phase}`,
|
||
session.waitingReason ? `waitingReason: ${session.waitingReason.kind}` : null,
|
||
active ? `skill: ${active.name}` : null,
|
||
session.slots.startupCompleted ? "startup: done" : null,
|
||
session.slots["book.brief"] ? "book.brief: yes" : null,
|
||
session.currentWorkerId ? `worker: ${session.currentWorkerId}` : null,
|
||
`artifacts: ${session.artifacts.length}`,
|
||
`events: ${session.history.length}`,
|
||
];
|
||
return lines.filter(Boolean).join("\n");
|
||
}
|
||
|
||
/** 集成测试用:从 start 到 finish 的最小闭环脚本 */
|
||
export async function runMinimalClosedLoop(
|
||
runtime: PhaseRuntime,
|
||
): Promise<RuntimeSession> {
|
||
await runtime.startWithOrchestrator("world-simulator");
|
||
await runtime.submitInput("科幻长篇,带世界运转,约 20 万字交互");
|
||
const decision = createDecision({
|
||
action: "run_worker",
|
||
reason: "根据用户需求产出 Worker 集",
|
||
workerId: "design-flow",
|
||
requiresApproval: false,
|
||
});
|
||
await runtime.submitDecision(decision);
|
||
|
||
if (runtime.getSession().currentWorkerId) {
|
||
await runtime.workerComplete();
|
||
}
|
||
|
||
if (runtime.getSession().pendingArtifactId) {
|
||
await runtime.acceptArtifact();
|
||
}
|
||
|
||
await runtime.submitDecision(
|
||
createDecision({ action: "finish", reason: "演示结束", requiresApproval: false }),
|
||
);
|
||
|
||
return runtime.getSession();
|
||
}
|