引入游玩回合与开场收口,抽出可替换 agent 司机并支持运行中止;完善预设洞位、世界书拼装与试跑,并打磨会话运行时与 Web 交互。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user