引入游玩回合与开场收口,抽出可替换 agent 司机并支持运行中止;完善预设洞位、世界书拼装与试跑,并打磨会话运行时与 Web 交互。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-24 01:13:48 +08:00
co-authored by Cursor
parent ec474cb946
commit b82da4c5a5
95 changed files with 6781 additions and 1344 deletions
+161
View File
@@ -0,0 +1,161 @@
/**
* 可替换的 agent 司机:问模型 → 有 tool-call 则交给调用方处理 → 再问,直到不再欠请求。
*
* 相位机、黑板、规格不走这里。换 dsh / 其它 harness 时只换 {@link AgentDriver} 实现。
*
* @module runtime/driver
*/
import type {
ChatMessage,
CompleteWithToolsResult,
LlmProvider,
StreamCallbacks,
ToolDefinition,
} from "../llm/client.js";
import { supportsToolStream } from "../llm/stream-complete.js";
import { debugLog } from "../log.js";
export type DriverToolCall = {
id: string;
name: string;
arguments: string;
};
/** 一步里所有 tool-call 的处理结果:继续把结果写回,或本步结账停循环。 */
export type DriverStepOutcome =
| { kind: "continue"; results: Array<{ callId: string; content: string }> }
| { kind: "conclude"; call: DriverToolCall };
export type DriverRunInput = {
system: string;
messages: ChatMessage[];
tools: ToolDefinition[];
caller?: string;
/** 日志用短名,如「总管循环」 */
label?: string;
maxIterations?: number;
onThinkingDelta?: (delta: string) => void;
onThinkingDone?: (text: string) => void;
handleStep: (
calls: DriverToolCall[],
) => DriverStepOutcome | Promise<DriverStepOutcome>;
};
export type DriverStop =
| { kind: "text"; content: string }
| { kind: "conclude"; call: DriverToolCall };
export type DriverRunResult = {
iterations: number;
stop: DriverStop;
};
/** 司机:直到模型不再欠一次请求。 */
export type AgentDriver = {
run(input: DriverRunInput): Promise<DriverRunResult>;
};
export const DEFAULT_DRIVER_MAX_ITERATIONS = 12;
function assistantMessageFromToolCalls(
content: string | null,
toolCalls: DriverToolCall[],
): ChatMessage {
return {
role: "assistant",
content,
tool_calls: toolCalls.map((tc) => ({
id: tc.id,
type: "function" as const,
function: { name: tc.name, arguments: tc.arguments },
})),
};
}
function emitThinkingDone(
input: DriverRunInput,
reasoning?: string,
): void {
const text = reasoning?.trim() ?? "";
if (text) input.onThinkingDone?.(text);
}
async function completeToolsPreferStream(
llm: LlmProvider,
messages: ChatMessage[],
tools: ToolDefinition[],
caller: string | undefined,
callbacks: StreamCallbacks,
): Promise<CompleteWithToolsResult> {
const options = { tools, caller };
if (supportsToolStream(llm)) {
return llm.completeWithToolsStream!(messages, options, callbacks);
}
const result = await llm.completeWithTools(messages, options);
if (result.reasoning) callbacks.onReasoningDelta?.(result.reasoning);
if (result.content) callbacks.onContentDelta?.(result.content);
return result;
}
/** 现有 OpenAI 兼容 provider 上的司机,行为对齐旧 tool-loop。 */
export function createLocalLlmDriver(llm: LlmProvider): AgentDriver {
return {
async run(input: DriverRunInput): Promise<DriverRunResult> {
const maxIterations = input.maxIterations ?? DEFAULT_DRIVER_MAX_ITERATIONS;
const messages: ChatMessage[] = [
{ role: "system", content: input.system },
...input.messages,
];
const streamCallbacks: StreamCallbacks = {
onReasoningDelta: (delta) => input.onThinkingDelta?.(delta),
};
const label = input.label ?? "driver";
for (let iteration = 1; iteration <= maxIterations; iteration++) {
debugLog("llm", `${label} 第${iteration}轮`);
const result = await completeToolsPreferStream(
llm,
messages,
input.tools,
input.caller,
streamCallbacks,
);
emitThinkingDone(input, result.reasoning);
if (result.toolCalls.length === 0) {
const content = result.content?.trim() ?? "";
if (content) {
return { iterations: iteration, stop: { kind: "text", content } };
}
throw new Error("Driver returned no tool calls and no content");
}
const calls: DriverToolCall[] = result.toolCalls.map((tc) => ({
id: tc.id,
name: tc.name,
arguments: tc.arguments,
}));
messages.push(assistantMessageFromToolCalls(result.content, calls));
const outcome = await input.handleStep(calls);
if (outcome.kind === "conclude") {
return {
iterations: iteration,
stop: { kind: "conclude", call: outcome.call },
};
}
for (const item of outcome.results) {
messages.push({
role: "tool",
content: item.content,
tool_call_id: item.callId,
});
}
}
throw new Error(`Driver exceeded ${maxIterations} iterations`);
},
};
}
+11 -19
View File
@@ -1,23 +1,21 @@
import { profileToLlmConfig } from "../config/api-profiles.js";
import {
ensureActiveProfileDefault,
loadAppSettings,
resolveActiveProfile,
} from "../config/settings.js";
import { resolveActivePreset } from "../preset/store.js";
import {
createMockMainAgentResponse,
MockLlmProvider,
OpenAiCompatibleProvider,
type LlmProvider,
} from "../llm/client.js";
import { PresetLlmProvider } from "../llm/preset-wrapper.js";
import type { PresetPackage } from "../types/preset.js";
import {
TokenTrackingProvider,
type LlmTrackingContext,
} from "../llm/token-tracker.js";
wrapLlmForSession,
type LlmTrackingRef,
} from "../llm/preset-wrapper.js";
export type LlmTrackingRef = { current: LlmTrackingContext };
export type { LlmTrackingRef };
function buildInnerLlm(): LlmProvider {
ensureActiveProfileDefault();
@@ -42,22 +40,11 @@ function buildInnerLlm(): LlmProvider {
]);
}
function wrapWithPreset(inner: LlmProvider): LlmProvider {
const settings = loadAppSettings();
const preset = resolveActivePreset(settings.activePresetId);
if (!preset) return inner;
return new PresetLlmProvider(inner, () =>
resolveActivePreset(loadAppSettings().activePresetId),
);
}
/** Web/CLI 默认 LLM:本地 profile + 全局 preset + token 统计 */
export function createDefaultMainAgentLlm(
trackingRef?: LlmTrackingRef,
): LlmProvider {
const llm = wrapWithPreset(buildInnerLlm());
if (!trackingRef) return llm;
return new TokenTrackingProvider(llm, () => trackingRef.current);
return wrapLlmForSession(buildInnerLlm(), trackingRef);
}
export function hasRealLlmConfig(): boolean {
@@ -76,3 +63,8 @@ export function reloadDefaultMainAgentLlm(
): LlmProvider {
return createDefaultMainAgentLlm(trackingRef);
}
/** 试跑指定预设:生成参数跟这条预设走,不依赖当前选用。 */
export function createLlmForPreset(preset: PresetPackage): LlmProvider {
return wrapLlmForSession(buildInnerLlm(), undefined, () => preset);
}
+2
View File
@@ -158,6 +158,8 @@ export class RuntimeOrchestrator {
await this.runProgrammaticReview(effect.artifactId);
break;
case "invoke_main_agent":
case "propose_next_creation_step":
case "seal_creation_opening":
break;
}
}
+138 -34
View File
@@ -30,6 +30,12 @@ import {
isModuleOpeningQuestions,
normalizeQuestions,
} from "../skills/question-protocol.js";
import {
CREATION_SEALED_WAITING_MESSAGE,
SLOT_CREATION_SEALED_BY_OPENING,
isOpeningSealArtifact,
} from "../skills/opening-seal.js";
import { inferLifecycleStage } from "../skills/worker-declaration.js";
import type { QuestionItem } from "../types/questions.js";
function nowIso(): string {
@@ -81,7 +87,45 @@ function mergeSlotText(
const trimmed = text.trim();
if (!trimmed) return;
const prev = String(slots[key] ?? "").trim();
slots[key] = prev ? `${prev}\n\n${trimmed}` : trimmed;
if (!prev) {
slots[key] = trimmed;
return;
}
if (
prev === trimmed ||
prev.includes(`\n\n${trimmed}`) ||
prev.startsWith(`${trimmed}\n\n`)
) {
return;
}
slots[key] = `${prev}\n\n${trimmed}`;
}
/** 同一步内多次「按意见修改」叠意见;较新的写在后面 */
function accumulateRevisionInstruction(prev: unknown, next: string): string {
const trimmed = next.trim();
const old = typeof prev === "string" ? prev.trim() : "";
if (!old || old === trimmed) return trimmed;
if (old.includes(trimmed)) return old;
return `${old}\n\n——\n\n${trimmed}`;
}
/** 验收后把本步意见并入「用户.需求」按时间保留,再清掉本步槽位以免下一步误进修订 */
function archiveStepNotesIntoDemand(slots: Record<string, unknown>): void {
const reply =
typeof slots["用户.worker答复"] === "string"
? slots["用户.worker答复"].trim()
: "";
const note =
typeof slots["用户.修订说明"] === "string"
? slots["用户.修订说明"].trim()
: "";
if (reply) mergeSlotText(slots, "用户.需求", reply);
if (note) mergeSlotText(slots, "用户.需求", note);
delete slots["用户.修订说明"];
delete slots.revisionInstruction;
delete slots.revisionTargetArtifactId;
delete slots["用户.worker答复"];
}
/** 追加事件到 history 并 touch */
@@ -102,6 +146,30 @@ function running(session: RuntimeSession): RuntimeSession {
return touch({ ...session, phase: "running", waitingReason: undefined });
}
/** 游玩连跑下一执行单元;创作则交总管 */
function afterPlayOrMainAgent(
session: RuntimeSession,
next: RuntimeSession,
): ApplyEventResult {
const cleared = {
...next,
pendingArtifactId: undefined,
currentWorkerId: undefined,
pendingDecision: undefined,
resumeContext: undefined,
};
if (inferLifecycleStage(session) === "play") {
return {
session: touch(running(cleared)),
effects: [{ type: "continue_play_turn" }],
};
}
return {
session: touch(running(cleared)),
effects: [{ type: "invoke_main_agent" }],
};
}
function findArtifact(
session: RuntimeSession,
artifactId: string,
@@ -150,11 +218,14 @@ function beginRevisionRerun(
artifact: ArtifactRecord,
instruction: string,
): ApplyEventResult {
const trimmed = instruction.trim();
const trimmed = accumulateRevisionInstruction(
session.slots["用户.修订说明"] ?? session.slots.revisionInstruction,
instruction,
);
const slots: Record<string, unknown> = {
...session.slots,
"用户.修订说明": trimmed,
"用户.最新输入": trimmed,
"用户.最新输入": instruction.trim(),
revisionInstruction: trimmed,
// 本次重跑的底稿;重跑写不出新版时只许退回这一份
revisionTargetArtifactId: artifact.id,
@@ -319,7 +390,10 @@ function handleWorkerCompleted(
return fail(session, `Artifact not found: ${event.payload.artifactId}`);
}
const mode = session.acceptanceMode ?? "user_confirmed";
const mode =
inferLifecycleStage(session) === "play"
? "no_confirmation"
: (session.acceptanceMode ?? "user_confirmed");
let next = appendHistory(session, event);
if (next.slots.revisionTargetArtifactId) {
@@ -346,16 +420,13 @@ function handleWorkerCompleted(
if (mode === "no_confirmation") {
next = updateArtifact(next, artifact.id, { status: "accepted" });
return {
session: touch({
...running(next),
pendingArtifactId: undefined,
currentWorkerId: undefined,
pendingDecision: undefined,
resumeContext: undefined,
}),
effects: [{ type: "invoke_main_agent" }],
};
return afterPlayOrMainAgent(session, {
...next,
pendingArtifactId: undefined,
currentWorkerId: undefined,
pendingDecision: undefined,
resumeContext: undefined,
});
}
// programmatic_review
@@ -587,9 +658,8 @@ export function applyEvent(
if (session.waitingReason?.kind === "worker_questions") {
slots["用户.worker答复"] = text;
if (text) slots["用户.最新输入"] = text;
if (demandKey && text) {
mergeSlotText(slots, demandKey, text);
}
// 开场/追问答复只进「用户.worker答复」,不追加进「用户.需求」
// (否则后续步骤的用户表述会把各步答复叠成一份长转录,修订意见被淹没)
} else if (session.waitingReason?.kind === "input" && text) {
const key = demandKey || "用户.需求";
applyUserTextToSlots(slots, text, key, {
@@ -615,6 +685,28 @@ export function applyEvent(
};
}
if (inferLifecycleStage(session) === "play") {
return {
session: touch(running({ ...next, resumeContext: undefined })),
effects: [{ type: "run_play_turn" }],
};
}
if (session.slots[SLOT_CREATION_SEALED_BY_OPENING]) {
return {
session: waiting(touch({ ...next, resumeContext: undefined }), {
kind: "input",
message: CREATION_SEALED_WAITING_MESSAGE,
}),
effects: [
{
type: "emit_message",
message: "创作已收口并保存。请切换到「游玩」开始。",
},
],
};
}
// 常规定稿用户输入 → 等总管下一步
return {
session: touch(running({ ...next, resumeContext: undefined })),
@@ -817,7 +909,10 @@ export function applyEvent(
case "worker_completed": {
const result = handleWorkerCompleted(session, event);
const mode = session.acceptanceMode ?? "user_confirmed";
const mode =
inferLifecycleStage(session) === "play"
? "no_confirmation"
: (session.acceptanceMode ?? "user_confirmed");
if (mode === "user_confirmed" && result.session.pendingArtifactId) {
const sidecar = asOptionalSidecarQuestions(event.payload.questions);
const assessment =
@@ -908,6 +1003,7 @@ export function applyEvent(
return fail(session, `Artifact not found: ${event.payload.artifactId}`);
}
const slots: Record<string, unknown> = { ...session.slots };
archiveStepNotesIntoDemand(slots);
// 仅终稿 tag「设计.worker集」表示完整 Worker 集验收;草稿单位验收不得开 play
const isFinalSet = artifact.outputTags.some((tag) => tag === "设计.worker集");
if (isFinalSet) {
@@ -924,24 +1020,29 @@ export function applyEvent(
pendingDecision: undefined,
currentWorkerId: undefined,
};
// 创作步验收后:先问下一步意向,再展示下一节点确认开干(勿直接连跑)
const gateNextIntent =
!isFinalSet &&
(artifact.workerId === "design-step" ||
artifact.workerId === "design-flow");
if (gateNextIntent) {
// 开场白终节点:选定后落库、关 DAG,不再问「下一步想写什么」。
if (isOpeningSealArtifact(artifact)) {
return {
session: waiting(cleared, {
kind: "next_intent",
afterWorkerId: artifact.workerId,
kind: "input",
message: CREATION_SEALED_WAITING_MESSAGE,
}),
effects: [
{
type: "emit_message",
message:
"这一步已收下。下一步想写什么?可留空,直接发送后看下一节点并确认开干。",
},
],
effects: [{ type: "seal_creation_opening" }],
};
}
if (inferLifecycleStage(session) === "play") {
return afterPlayOrMainAgent(session, cleared);
}
// 创作步验收后直接提案下一步(有待执行步则确认开干;否则交总管扩步)。
// 不再先停在「下一步想写什么」——与「同意并开始」叠成双重确认。
// 细化终稿也走提案,而不是 invoke_main_agent(否则容易把刚收下的终稿再跑一遍)。
const proposeNext =
artifact.workerId === "design-step" ||
artifact.workerId === "design-flow";
if (proposeNext) {
return {
session: touch(running(cleared)),
effects: [{ type: "propose_next_creation_step" }],
};
}
return {
@@ -1029,7 +1130,10 @@ export function applyEvent(
session: touch(
running({ ...next, pendingArtifactId: undefined }),
),
effects: [{ type: "invoke_main_agent" }],
effects:
inferLifecycleStage(session) === "play"
? [{ type: "continue_play_turn" }]
: [{ type: "invoke_main_agent" }],
};
}
+239 -26
View File
@@ -62,6 +62,7 @@ import {
CREATION_MODULE_OPENING_STATE_TAG,
SLOT_CREATION_MODULE_OPENING_STATE,
loadModuleCatalog,
isReviseStep,
mergeCreationFlowPreservingAccepted,
nextPendingStep,
parseCreationFlow,
@@ -71,10 +72,12 @@ import {
resolveDesignStepBinding,
shouldSkipModuleOpening,
isProgressPointerTag,
missingRequiredStepParams,
stepUnitId,
stringifyCreationFlow,
stringifyModuleOpeningState,
type CreationFlowStepParams,
type ModuleCatalogEntry,
type ModuleParamSpec,
} from "../skills/creation-flow.js";
import {
@@ -86,6 +89,22 @@ import {
SLOT_ASKED_QUESTIONS,
} from "../skills/question-protocol.js";
import { parseWorkerSetYaml } from "../skills/worker-set-parse.js";
import {
closeCreationFlowRaw,
CREATION_SEALED_WAITING_MESSAGE,
mergeOpeningTablePatch,
OPENING_CURRENT_VARS_TAG,
OPENING_INITIAL_VARS_TAG,
OPENING_OUTPUT_TAG,
OPENING_SETUP_ARTIFACT_TAG,
parseOpeningSealPayload,
SLOT_CREATION_SEALED_BY_OPENING,
SLOT_OPENING_SELECTED_INDEX,
} from "../skills/opening-seal.js";
import {
readPlayTurnQueue,
withPlayTurnQueue,
} from "../skills/play-turn.js";
import { debugLog, labelAction, logStateChange, sessionSnap } from "../log.js";
import {
executeChance,
@@ -272,6 +291,26 @@ export class PhaseRuntime {
this.lastWorkerRunSnapshot = null;
}
/**
* 执行中卡住:有进行中的 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("当前没有可重试的执行");
}
/** 重 roll 指定 worker(刷新 Skill 回复) */
async rerunWorker(workerId: string): Promise<RuntimeSession> {
const effect: Extract<PhaseEffect, { type: "run_worker" }> = {
@@ -575,15 +614,38 @@ export class PhaseRuntime {
}
/** 用户接受 pendingArtifact;创作单位验收 vs 终稿验收分流 */
async acceptArtifact(artifactId?: string): Promise<RuntimeSession> {
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 unitAccept = isDesignUnitArtifact(artifact);
const result = compressAfterWorkerAccept({
blackboard: this.blackboard,
workerId: artifact.workerId,
@@ -598,8 +660,7 @@ export class PhaseRuntime {
lastCompressedWorkerId: artifact.workerId,
};
if (unitAccept) {
this.recordCreationUnitAccepted(slots, artifact);
if (shouldRecordStep) {
const unitId = String(slots[SLOT_CREATION_CURRENT_UNIT] ?? "").trim();
this.onMessage(
`[创作单位已验收] ${unitId || artifact.workerId}:${
@@ -608,7 +669,8 @@ export class PhaseRuntime {
);
slots[SLOT_CREATION_CURRENT_UNIT] = undefined;
slots[SLOT_CREATION_UNIT_ANCHOR_AT] = undefined;
} else {
}
if (!unitAccept) {
this.onMessage(
`[上下文已压缩] ${artifact.workerId}:保留 ${result.finals.map((f) => f.tag).join("、") || "(无终产物)"};归档 ${result.archivedTags.length} 个过程 tag。下一阶段以定稿为准。`,
);
@@ -814,6 +876,15 @@ export class PhaseRuntime {
case "propose_next_creation_step":
await this.proposeNextCreationStep();
break;
case "seal_creation_opening":
this.sealCreationOpening();
break;
case "run_play_turn":
await this.startPlayTurn();
break;
case "continue_play_turn":
await this.continuePlayTurn();
break;
}
}
}
@@ -822,6 +893,10 @@ export class PhaseRuntime {
* 用户填完「下一步想写什么」后:有待执行步则提案并进入确认;否则交总管扩步/收口。
*/
private async proposeNextCreationStep(): Promise<void> {
if (this.session.slots[SLOT_CREATION_SEALED_BY_OPENING]) {
this.onMessage(CREATION_SEALED_WAITING_MESSAGE);
return;
}
this.syncSlotsToBlackboard(this.session);
const flow = parseCreationFlow(
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
@@ -853,6 +928,9 @@ export class PhaseRuntime {
const active = this.getActiveSkill();
let paramSpecs: ModuleParamSpec[] = [];
let declaration = "";
let auto = false;
let kind: ModuleCatalogEntry["kind"];
let pendingModule: ModuleCatalogEntry | null = null;
if (active?.name) {
try {
const skill = await loadSkill(active.name);
@@ -861,6 +939,9 @@ export class PhaseRuntime {
const mod = catalog?.modules.find((m) => m.name === pending.name);
paramSpecs = mod?.params ?? [];
declaration = mod?.declaration?.trim() || "";
auto = mod?.auto === true;
kind = mod?.kind;
pendingModule = mod ?? null;
}
} catch {
/* catalog optional */
@@ -870,13 +951,7 @@ export class PhaseRuntime {
debugLog("step", `提案下一步 ${pending.id}(${pending.name})`);
const params = pending.params ? { ...pending.params } : {};
const paramsMissing = paramSpecs
.filter((p) => p.required)
.map((p) => p.key)
.filter((key) => {
const v = params[key];
return v == null || (typeof v === "string" && !v.trim());
});
const paramsMissing = missingRequiredStepParams(pending, pendingModule);
const intent =
typeof this.session.slots["用户.下一步意向"] === "string"
@@ -890,8 +965,9 @@ export class PhaseRuntime {
: paramsMissing.length
? `;待钉参数:${paramsMissing.join("、")}`
: "";
const revise = isReviseStep(pending);
const reason = [
`下一步:${pending.name}`,
revise ? `下一步:回头修改 · ${pending.name}` : `下一步:${pending.name}`,
declaration ? `—— ${declaration}` : "",
paramHint,
intent ? `(你的意向:${intent})` : "",
@@ -912,6 +988,9 @@ export class PhaseRuntime {
hint: p.hint,
})),
intent: intent || undefined,
...(kind ? { kind } : {}),
...(revise ? { mode: "revise" as const } : {}),
...(pending.revises ? { revises: pending.revises } : {}),
};
this.blackboard.write({
tag: CREATION_PROPOSED_STEP_TAG,
@@ -930,7 +1009,8 @@ export class PhaseRuntime {
action: "run_worker",
workerId: "design-step",
reason,
requiresApproval: true,
// 程序步(如投影排序)且参数已齐:跳过「同意并开始」,直接执行。
requiresApproval: !(auto && paramsMissing.length === 0),
});
await this.dispatch({
type: "main_agent_decision_created",
@@ -938,6 +1018,81 @@ export class PhaseRuntime {
});
}
/**
* 选定开场白后:把候选落成 输出.开场白 / 初值表,关闭 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 closed = closeCreationFlowRaw(
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
);
if (closed) {
this.blackboard.write({
tag: CREATION_FLOW_TAG,
content: closed,
source: "runtime",
});
}
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,
[CREATION_FLOW_TAG]: closed ?? this.session.slots[CREATION_FLOW_TAG],
[OPENING_OUTPUT_TAG]: openingText || this.session.slots[OPENING_OUTPUT_TAG],
},
};
}
private applyProposedStepParams(params: CreationFlowStepParams): void {
const raw =
(typeof this.session.slots[CREATION_PROPOSED_STEP_TAG] === "string"
@@ -947,6 +1102,7 @@ export class PhaseRuntime {
if (!raw?.trim()) return;
let snap: {
stepId?: string;
kind?: string;
paramSpecs?: Array<{ key: string; required?: boolean }>;
};
try {
@@ -967,13 +1123,16 @@ export class PhaseRuntime {
content,
source: "user",
});
const paramsMissing = (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 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,
@@ -1122,6 +1281,7 @@ export class PhaseRuntime {
this.onMessage(
`[Worker] ${workerId} 执行中…${resolved.source === "declaration" ? "(声明驱动)" : ""}`,
);
debugLog("step", `调用模型 ${workerId}`);
this.onWorkerStreamStart?.(workerId);
try {
@@ -1131,10 +1291,7 @@ export class PhaseRuntime {
slots,
blackboard: this.blackboard,
llm: workerLlm,
declared:
resolved.source === "declaration"
? { worker: resolved.worker, promptBody: resolved.promptBody }
: undefined,
declared: { worker: resolved.worker, promptBody: resolved.promptBody },
stream: {
onThinkingDelta: (delta) =>
this.onWorkerThinkingDelta?.(workerId, delta),
@@ -1193,8 +1350,13 @@ export class PhaseRuntime {
});
this.session = { ...this.session, artifacts: [...this.session.artifacts, artifact] };
const hasPlayVisible = Object.keys(result.outputs).some(
(t) => t === "输出.用户展示" || t === "输出.开场白",
);
this.onMessage(
`[Worker] ${workerId} 已完成\n\n${result.preview}${result.preview.length >= 4000 ? "\n\n…" : ""}`,
`[Worker] ${workerId} 已完成\n\n${result.preview}${
!hasPlayVisible && result.preview.length >= 4000 ? "\n\n…" : ""
}`,
);
await this.dispatch({
@@ -1907,8 +2069,59 @@ export class PhaseRuntime {
);
}
/** 游玩:按 playWorkerIds 开一轮 */
private async startPlayTurn(): Promise<void> {
this.syncSlotsToBlackboard(this.session);
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(),
};
}
/** invoke_main_agent 副作用:running 时调用总管 LLM,链式推进直到需用户介入 */
private async maybeRunMainAgent(effects: PhaseEffect[]): Promise<void> {
if (inferLifecycleStage(this.session) === "play") return;
if (this.session.slots[SLOT_CREATION_SEALED_BY_OPENING]) return;
if (this.mainAgent && effects.some((e) => e.type === "invoke_main_agent")) {
await this.runMainAgent();
}