完善创作节点循环与等待态交互,并大幅打磨 Web 壳与会话运行时。
- 节点循环:配方开局直坐 DAG 第一步;验收后先问下一步意向,再确认开干并可钉编排参数;「按意见修改」只重跑当前节点。 - 询问分流:能力 opening 走说话面引导,可跳过追问按题干去重;追问与产物同轮挂载,避免拆成两段历史。 - 运行时:补强 revision 重跑、创作流程合并/进度指针、工具循环与 worker 执行;新增运行日志与 web:watch。 - 前端:统一等待态文案与底栏主按钮,完善询问卡/产物验收/呈现壳样式与交互。 - 同步世界模拟器模块提示、编排文档与相关测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+231
-37
@@ -26,7 +26,10 @@ import {
|
||||
readIntakeValues,
|
||||
synthesizeDemandText,
|
||||
} from "../intake/intake.js";
|
||||
import { normalizeQuestions } from "../skills/question-protocol.js";
|
||||
import {
|
||||
isModuleOpeningQuestions,
|
||||
normalizeQuestions,
|
||||
} from "../skills/question-protocol.js";
|
||||
import type { QuestionItem } from "../types/questions.js";
|
||||
|
||||
function nowIso(): string {
|
||||
@@ -119,6 +122,67 @@ function updateArtifact(
|
||||
};
|
||||
}
|
||||
|
||||
/** 最近一次被拒 / 待修订的产物(用于 revision 等待态补交意见后重跑) */
|
||||
function findRevisionTargetArtifact(
|
||||
session: RuntimeSession,
|
||||
): ArtifactRecord | undefined {
|
||||
if (session.pendingArtifactId) {
|
||||
const pending = findArtifact(session, session.pendingArtifactId);
|
||||
if (pending) return pending;
|
||||
}
|
||||
for (let i = session.artifacts.length - 1; i >= 0; i -= 1) {
|
||||
const a = session.artifacts[i];
|
||||
if (a.status === "revision_requested" || a.status === "rejected") {
|
||||
return a;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 已有修改意见 → 写入「用户.修订说明」并立刻重跑同一 worker。
|
||||
* 语义:在已有产物上按意见改(上下文会注入待改底稿),不是推倒重做。
|
||||
* 对应验收底栏:看产物 → 输入意见 →「按意见修改」。
|
||||
*/
|
||||
function beginRevisionRerun(
|
||||
session: RuntimeSession,
|
||||
event: RuntimeEvent,
|
||||
artifact: ArtifactRecord,
|
||||
instruction: string,
|
||||
): ApplyEventResult {
|
||||
const trimmed = instruction.trim();
|
||||
const slots: Record<string, unknown> = {
|
||||
...session.slots,
|
||||
"用户.修订说明": trimmed,
|
||||
"用户.最新输入": trimmed,
|
||||
revisionInstruction: trimmed,
|
||||
// 本次重跑的底稿;重跑写不出新版时只许退回这一份
|
||||
revisionTargetArtifactId: artifact.id,
|
||||
};
|
||||
const next = appendHistory(
|
||||
updateArtifact(session, artifact.id, { status: "revision_requested" }),
|
||||
event,
|
||||
);
|
||||
return {
|
||||
session: touch(
|
||||
running({
|
||||
...next,
|
||||
slots,
|
||||
pendingArtifactId: undefined,
|
||||
pendingDecision: undefined,
|
||||
currentWorkerId: artifact.workerId,
|
||||
currentStepId: artifact.stepId ?? session.currentStepId,
|
||||
}),
|
||||
),
|
||||
effects: [
|
||||
{
|
||||
type: "run_worker",
|
||||
workerId: artifact.workerId,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/** 创建 idle 状态的新会话 */
|
||||
export function createSession(
|
||||
presetId: string,
|
||||
@@ -154,6 +218,7 @@ export function getAllowedEvents(
|
||||
"worker_started",
|
||||
"worker_completed",
|
||||
"worker_needs_input",
|
||||
"worker_revision_produced_nothing",
|
||||
"programmatic_review_started",
|
||||
"programmatic_review_passed",
|
||||
"programmatic_review_failed",
|
||||
@@ -204,7 +269,8 @@ export function canApplyEvent(
|
||||
reason?.kind === "intake" ||
|
||||
reason?.kind === "input" ||
|
||||
reason?.kind === "worker_questions" ||
|
||||
reason?.kind === "revision"
|
||||
reason?.kind === "revision" ||
|
||||
reason?.kind === "next_intent"
|
||||
);
|
||||
case "user_confirmed_intake":
|
||||
return reason?.kind === "intake";
|
||||
@@ -256,6 +322,14 @@ function handleWorkerCompleted(
|
||||
const mode = session.acceptanceMode ?? "user_confirmed";
|
||||
let next = appendHistory(session, event);
|
||||
|
||||
if (next.slots.revisionTargetArtifactId) {
|
||||
// 已写出新版,旧底稿不再是退回目标
|
||||
next = {
|
||||
...next,
|
||||
slots: { ...next.slots, revisionTargetArtifactId: undefined },
|
||||
};
|
||||
}
|
||||
|
||||
if (mode === "user_confirmed") {
|
||||
next = updateArtifact(next, artifact.id, { status: "under_review" });
|
||||
return {
|
||||
@@ -454,6 +528,62 @@ export function applyEvent(
|
||||
};
|
||||
}
|
||||
|
||||
if (session.waitingReason?.kind === "next_intent") {
|
||||
if (text) {
|
||||
slots["用户.下一步意向"] = text;
|
||||
slots["用户.最新输入"] = text;
|
||||
}
|
||||
const next = appendHistory(
|
||||
{
|
||||
...session,
|
||||
slots,
|
||||
resumeContext: undefined,
|
||||
},
|
||||
event,
|
||||
);
|
||||
return {
|
||||
session: touch(running(next)),
|
||||
effects: [{ type: "propose_next_creation_step" }],
|
||||
};
|
||||
}
|
||||
|
||||
if (session.waitingReason?.kind === "revision") {
|
||||
const instruction =
|
||||
text.trim() || session.waitingReason.instruction?.trim() || "";
|
||||
if (!instruction) {
|
||||
return {
|
||||
session: waiting(touch(appendHistory({ ...session, slots }, event)), {
|
||||
kind: "revision",
|
||||
instruction: session.waitingReason.instruction,
|
||||
}),
|
||||
effects: [
|
||||
{
|
||||
type: "emit_message",
|
||||
message: "请说明要改哪里,再发送。",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
const target = findRevisionTargetArtifact(session);
|
||||
if (target) {
|
||||
return beginRevisionRerun(session, event, target, instruction);
|
||||
}
|
||||
slots["用户.修订说明"] = instruction;
|
||||
slots["用户.最新输入"] = instruction;
|
||||
slots.revisionInstruction = instruction;
|
||||
return {
|
||||
session: touch(
|
||||
running(
|
||||
appendHistory(
|
||||
{ ...session, slots, pendingArtifactId: undefined },
|
||||
event,
|
||||
),
|
||||
),
|
||||
),
|
||||
effects: [{ type: "invoke_main_agent" }],
|
||||
};
|
||||
}
|
||||
|
||||
if (session.waitingReason?.kind === "worker_questions") {
|
||||
slots["用户.worker答复"] = text;
|
||||
if (text) slots["用户.最新输入"] = text;
|
||||
@@ -660,6 +790,7 @@ export function applyEvent(
|
||||
acceptanceMode: session.acceptanceMode ?? "user_confirmed",
|
||||
questions: normalized,
|
||||
};
|
||||
const openingAsk = isModuleOpeningQuestions(normalized);
|
||||
return {
|
||||
session: waiting(
|
||||
appendHistory(
|
||||
@@ -675,7 +806,10 @@ export function applyEvent(
|
||||
effects: [
|
||||
{
|
||||
type: "emit_message",
|
||||
message: `[Worker] ${event.payload.workerId} 提问:\n${normalized.map((q) => `- ${q.prompt}`).join("\n")}`,
|
||||
// 默认问题正文由说话面 openingGuide 展示,勿再整段塞进调度消息
|
||||
message: openingAsk
|
||||
? `[Worker] ${event.payload.workerId} · 默认问题(请在主栏按引导作答)`
|
||||
: `[Worker] ${event.payload.workerId} 提问:\n${normalized.map((q) => `- ${q.prompt}`).join("\n")}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -690,16 +824,10 @@ export function applyEvent(
|
||||
typeof event.payload.assessment === "string"
|
||||
? event.payload.assessment.trim()
|
||||
: "";
|
||||
const effects = [...result.effects];
|
||||
if (sidecar?.length) {
|
||||
effects.push({
|
||||
type: "emit_message",
|
||||
message: `[Worker] 可选追问(可跳过,直接接受目前产物):\n${sidecar.map((q) => `- ${q.prompt}`).join("\n")}`,
|
||||
});
|
||||
}
|
||||
// 追问与产物同一次产出:只挂 review_artifact,勿再 emit 独立「可选追问」消息
|
||||
//(否则历史里会拆成 worker_output + worker_questions 两段)
|
||||
return {
|
||||
...result,
|
||||
effects,
|
||||
session: waiting(result.session, {
|
||||
kind: "review_artifact",
|
||||
artifactId: result.session.pendingArtifactId,
|
||||
@@ -711,6 +839,42 @@ export function applyEvent(
|
||||
return result;
|
||||
}
|
||||
|
||||
case "worker_revision_produced_nothing": {
|
||||
const artifact = findArtifact(session, event.payload.artifactId);
|
||||
if (!artifact) {
|
||||
return fail(session, `Artifact not found: ${event.payload.artifactId}`);
|
||||
}
|
||||
const retryQuestions = normalizeQuestions(event.payload.questions).map(
|
||||
(q) => ({ ...q, required: false }),
|
||||
);
|
||||
const next = appendHistory(
|
||||
updateArtifact(session, artifact.id, { status: "under_review" }),
|
||||
event,
|
||||
);
|
||||
return {
|
||||
session: waiting(
|
||||
{
|
||||
...next,
|
||||
slots: { ...next.slots, revisionTargetArtifactId: undefined },
|
||||
pendingArtifactId: artifact.id,
|
||||
currentWorkerId: undefined,
|
||||
},
|
||||
{
|
||||
kind: "review_artifact",
|
||||
artifactId: artifact.id,
|
||||
questions: retryQuestions.length ? retryQuestions : undefined,
|
||||
},
|
||||
),
|
||||
effects: [
|
||||
{
|
||||
type: "emit_message",
|
||||
message:
|
||||
"[系统] 这次按意见重写没能产出可验收的新版本,已退回上一版产物:可补充说明再改,或直接接受上一版。",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
case "user_resolved_sidecar_questions": {
|
||||
const reason = session.waitingReason;
|
||||
if (reason?.kind !== "review_artifact") {
|
||||
@@ -745,22 +909,43 @@ export function applyEvent(
|
||||
}
|
||||
const slots: Record<string, unknown> = { ...session.slots };
|
||||
// 仅终稿 tag「设计.worker集」表示完整 Worker 集验收;草稿单位验收不得开 play
|
||||
if (artifact.outputTags.some((tag) => tag === "设计.worker集")) {
|
||||
const isFinalSet = artifact.outputTags.some((tag) => tag === "设计.worker集");
|
||||
if (isFinalSet) {
|
||||
slots.designInstanceReady = true;
|
||||
}
|
||||
const next = appendHistory(
|
||||
updateArtifact(session, artifact.id, { status: "accepted" }),
|
||||
event,
|
||||
);
|
||||
return {
|
||||
session: touch(
|
||||
running({
|
||||
...next,
|
||||
slots,
|
||||
pendingArtifactId: undefined,
|
||||
pendingDecision: undefined,
|
||||
const cleared = {
|
||||
...next,
|
||||
slots,
|
||||
pendingArtifactId: undefined,
|
||||
pendingDecision: undefined,
|
||||
currentWorkerId: undefined,
|
||||
};
|
||||
// 创作步验收后:先问下一步意向,再展示下一节点确认开干(勿直接连跑)
|
||||
const gateNextIntent =
|
||||
!isFinalSet &&
|
||||
(artifact.workerId === "design-step" ||
|
||||
artifact.workerId === "design-flow");
|
||||
if (gateNextIntent) {
|
||||
return {
|
||||
session: waiting(cleared, {
|
||||
kind: "next_intent",
|
||||
afterWorkerId: artifact.workerId,
|
||||
}),
|
||||
),
|
||||
effects: [
|
||||
{
|
||||
type: "emit_message",
|
||||
message:
|
||||
"这一步已收下。下一步想写什么?可留空,直接发送后看下一节点并确认开干。",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
session: touch(running(cleared)),
|
||||
effects: [{ type: "invoke_main_agent" }],
|
||||
};
|
||||
}
|
||||
@@ -770,19 +955,21 @@ export function applyEvent(
|
||||
if (!artifact) {
|
||||
return fail(session, `Artifact not found: ${event.payload.artifactId}`);
|
||||
}
|
||||
const reason = event.payload.reason?.trim() ?? "";
|
||||
// 验收态已写意见 → 在现有产物上按意见改;无意见才进入 revision 等待补交
|
||||
if (reason) {
|
||||
return beginRevisionRerun(session, event, artifact, reason);
|
||||
}
|
||||
const next = appendHistory(
|
||||
updateArtifact(session, artifact.id, { status: "rejected" }),
|
||||
event,
|
||||
);
|
||||
return {
|
||||
session: waiting(next, {
|
||||
kind: "revision",
|
||||
instruction: event.payload.reason,
|
||||
}),
|
||||
session: waiting(next, { kind: "revision" }),
|
||||
effects: [
|
||||
{
|
||||
type: "emit_message",
|
||||
message: event.payload.reason ?? "产物已被拒绝,等待修改指示。",
|
||||
message: "已保留当前产物,请说明要改哪里。",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -800,15 +987,26 @@ export function applyEvent(
|
||||
effects: [{ type: "invoke_main_agent" }],
|
||||
};
|
||||
}
|
||||
const instruction = event.payload.instruction?.trim() ?? "";
|
||||
const artifactId =
|
||||
event.payload.artifactId ?? session.pendingArtifactId;
|
||||
const artifact = artifactId
|
||||
? findArtifact(session, artifactId)
|
||||
: findRevisionTargetArtifact(session);
|
||||
if (instruction && artifact) {
|
||||
return beginRevisionRerun(session, event, artifact, instruction);
|
||||
}
|
||||
return {
|
||||
session: waiting(
|
||||
appendHistory({ ...session, pendingArtifactId: undefined }, event),
|
||||
{ kind: "revision", instruction: event.payload.instruction },
|
||||
appendHistory(session, event),
|
||||
{ kind: "revision", instruction: instruction || undefined },
|
||||
),
|
||||
effects: [
|
||||
{
|
||||
type: "emit_message",
|
||||
message: `返工请求:${event.payload.instruction}`,
|
||||
message: instruction
|
||||
? `修改意见:${instruction}`
|
||||
: "请说明要改哪里(在现有产物上改)。",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -840,20 +1038,16 @@ export function applyEvent(
|
||||
if (!artifact) {
|
||||
return fail(session, `Artifact not found: ${event.payload.artifactId}`);
|
||||
}
|
||||
const next = appendHistory(
|
||||
updateArtifact(session, artifact.id, { status: "revision_requested" }),
|
||||
event,
|
||||
);
|
||||
const reason = event.payload.reason?.trim() || "程序验收失败";
|
||||
const result = beginRevisionRerun(session, event, artifact, reason);
|
||||
return {
|
||||
session: waiting(next, {
|
||||
kind: "revision",
|
||||
instruction: event.payload.reason,
|
||||
}),
|
||||
...result,
|
||||
effects: [
|
||||
{
|
||||
type: "emit_message",
|
||||
message: `程序验收失败:${event.payload.reason}`,
|
||||
message: `程序验收未过,按意见在现有产物上改:${reason}`,
|
||||
},
|
||||
...result.effects,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
+430
-25
@@ -57,18 +57,36 @@ import {
|
||||
import {
|
||||
CREATION_FLOW_TAG,
|
||||
CREATION_CURRENT_STEP_TAG,
|
||||
CREATION_PROPOSED_STEP_TAG,
|
||||
CREATION_MODULE_OPENING_TAG,
|
||||
CREATION_MODULE_OPENING_STATE_TAG,
|
||||
SLOT_CREATION_MODULE_OPENING_STATE,
|
||||
loadModuleCatalog,
|
||||
mergeCreationFlowPreservingAccepted,
|
||||
nextPendingStep,
|
||||
parseCreationFlow,
|
||||
parseModuleOpeningState,
|
||||
patchCreationFlowStepParams,
|
||||
pickRecordedStepId,
|
||||
resolveDesignStepBinding,
|
||||
shouldSkipModuleOpening,
|
||||
isProgressPointerTag,
|
||||
stepUnitId,
|
||||
stringifyCreationFlow,
|
||||
stringifyModuleOpeningState,
|
||||
type CreationFlowStepParams,
|
||||
type ModuleParamSpec,
|
||||
} from "../skills/creation-flow.js";
|
||||
import { normalizeQuestions } from "../skills/question-protocol.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 { debugLog, labelAction, logStateChange, sessionSnap } from "../log.js";
|
||||
import {
|
||||
executeChance,
|
||||
resolveChanceRequest,
|
||||
@@ -363,10 +381,18 @@ export class PhaseRuntime {
|
||||
return this.session;
|
||||
}
|
||||
|
||||
/** 统一事件入口:applyEvent + 更新 session + 处理 effects + 可选总管 LLM */
|
||||
async dispatch(event: RuntimeEvent): Promise<ApplyEventResult> {
|
||||
/** 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" ||
|
||||
@@ -416,14 +442,26 @@ export class PhaseRuntime {
|
||||
source: "user",
|
||||
});
|
||||
}
|
||||
const revision = session.slots.revisionInstruction;
|
||||
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: "用户.修改说明",
|
||||
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",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -490,10 +528,15 @@ export class PhaseRuntime {
|
||||
return this.session;
|
||||
}
|
||||
|
||||
/** 用户确认 pendingDecision(approve_step) */
|
||||
async approve(): Promise<RuntimeSession> {
|
||||
/** 用户确认 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;
|
||||
}
|
||||
@@ -577,7 +620,7 @@ export class PhaseRuntime {
|
||||
return this.session;
|
||||
}
|
||||
|
||||
/** 用户拒绝 pendingArtifact,进入 revision */
|
||||
/** 用户拒绝 pendingArtifact:有意见则立刻按「用户.修订说明」在现有产物上改 */
|
||||
async rejectArtifact(reason?: string, artifactId?: string): Promise<RuntimeSession> {
|
||||
const id = artifactId ?? this.session.pendingArtifactId;
|
||||
if (!id) throw new Error("当前没有待拒绝的产物");
|
||||
@@ -588,12 +631,39 @@ export class PhaseRuntime {
|
||||
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;
|
||||
}
|
||||
|
||||
/** 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 },
|
||||
@@ -601,6 +671,35 @@ export class PhaseRuntime {
|
||||
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 缺省 = 跳过。
|
||||
@@ -662,6 +761,13 @@ export class PhaseRuntime {
|
||||
/** 消费 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);
|
||||
@@ -705,10 +811,209 @@ export class PhaseRuntime {
|
||||
case "run_programmatic_review":
|
||||
await this.runProgrammaticReview(effect.artifactId);
|
||||
break;
|
||||
case "propose_next_creation_step":
|
||||
await this.proposeNextCreationStep();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户填完「下一步想写什么」后:有待执行步则提案并进入确认;否则交总管扩步/收口。
|
||||
*/
|
||||
private async proposeNextCreationStep(): Promise<void> {
|
||||
this.syncSlotsToBlackboard(this.session);
|
||||
const flow = parseCreationFlow(
|
||||
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
|
||||
);
|
||||
const accepted = parseAcceptedUnits(
|
||||
this.session.slots[SLOT_CREATION_ACCEPTED_UNITS] ??
|
||||
this.blackboard.getContentByTag(CREATION_ACCEPTED_UNITS_TAG),
|
||||
);
|
||||
const pending = nextPendingStep(flow, accepted);
|
||||
if (!pending || !flow) {
|
||||
debugLog("step", "暂无下一步,交给总管");
|
||||
this.session = {
|
||||
...this.session,
|
||||
slots: {
|
||||
...this.session.slots,
|
||||
[CREATION_PROPOSED_STEP_TAG]: undefined,
|
||||
},
|
||||
};
|
||||
if (this.mainAgent) {
|
||||
await this.runMainAgent();
|
||||
} else {
|
||||
this.onMessage(
|
||||
"[阶段机] 暂无下一节点,等待总管编排或补充意向。",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const active = this.getActiveSkill();
|
||||
let paramSpecs: ModuleParamSpec[] = [];
|
||||
let declaration = "";
|
||||
if (active?.name) {
|
||||
try {
|
||||
const skill = await loadSkill(active.name);
|
||||
if (skill.skillPackRoot) {
|
||||
const catalog = await loadModuleCatalog(skill.skillPackRoot);
|
||||
const mod = catalog?.modules.find((m) => m.name === pending.name);
|
||||
paramSpecs = mod?.params ?? [];
|
||||
declaration = mod?.declaration?.trim() || "";
|
||||
}
|
||||
} catch {
|
||||
/* catalog optional */
|
||||
}
|
||||
}
|
||||
|
||||
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 intent =
|
||||
typeof this.session.slots["用户.下一步意向"] === "string"
|
||||
? String(this.session.slots["用户.下一步意向"]).trim()
|
||||
: "";
|
||||
const paramHint =
|
||||
Object.keys(params).length > 0
|
||||
? `;参数 ${Object.entries(params)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join(" · ")}`
|
||||
: paramsMissing.length
|
||||
? `;待钉参数:${paramsMissing.join("、")}`
|
||||
: "";
|
||||
const reason = [
|
||||
`下一步:${pending.name}`,
|
||||
declaration ? `—— ${declaration}` : "",
|
||||
paramHint,
|
||||
intent ? `(你的意向:${intent})` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("");
|
||||
|
||||
const snapshot = {
|
||||
stepId: pending.id,
|
||||
name: pending.name,
|
||||
declaration: declaration || undefined,
|
||||
params,
|
||||
paramsMissing,
|
||||
paramSpecs: paramSpecs.map((p) => ({
|
||||
key: p.key,
|
||||
label: p.label,
|
||||
required: Boolean(p.required),
|
||||
hint: p.hint,
|
||||
})),
|
||||
intent: intent || undefined,
|
||||
};
|
||||
this.blackboard.write({
|
||||
tag: CREATION_PROPOSED_STEP_TAG,
|
||||
content: JSON.stringify(snapshot),
|
||||
source: "runtime",
|
||||
});
|
||||
this.session = {
|
||||
...this.session,
|
||||
slots: {
|
||||
...this.session.slots,
|
||||
[CREATION_PROPOSED_STEP_TAG]: JSON.stringify(snapshot),
|
||||
},
|
||||
};
|
||||
|
||||
const decision = createDecision({
|
||||
action: "run_worker",
|
||||
workerId: "design-step",
|
||||
reason,
|
||||
requiresApproval: true,
|
||||
});
|
||||
await this.dispatch({
|
||||
type: "main_agent_decision_created",
|
||||
payload: { decision },
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
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.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,
|
||||
@@ -840,24 +1145,50 @@ export class PhaseRuntime {
|
||||
|
||||
if (result.askUser?.length && Object.keys(result.outputs).length === 0) {
|
||||
// 无产物:阻塞追问(信息不足,必须补)
|
||||
if (await this.restoreRevisionTargetForReview(workerId, result.askUser)) {
|
||||
return;
|
||||
}
|
||||
await this.workerAsk(result.askUser);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
workerId === "design-step" &&
|
||||
Object.keys(result.outputs).length === 0
|
||||
) {
|
||||
const retryQuestions = result.askUser?.length
|
||||
? result.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(result.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:
|
||||
Object.keys(result.outputs).length > 0
|
||||
? Object.keys(result.outputs)
|
||||
: worker.outputTags,
|
||||
outputTags: (writtenTags.length > 0 ? writtenTags : worker.outputTags).filter(
|
||||
(t) => !isProgressPointerTag(t),
|
||||
),
|
||||
summary: result.summary,
|
||||
});
|
||||
this.session = { ...this.session, artifacts: [...this.session.artifacts, artifact] };
|
||||
@@ -871,7 +1202,7 @@ export class PhaseRuntime {
|
||||
payload: {
|
||||
artifactId: artifact.id,
|
||||
// 有产物时 askUser 挂到验收态,不阻断 Accept
|
||||
questions: result.askUser?.length ? result.askUser : undefined,
|
||||
questions: this.freshSidecarQuestions(result.askUser),
|
||||
assessment: result.askAssessment?.trim() || undefined,
|
||||
},
|
||||
});
|
||||
@@ -1054,6 +1385,22 @@ export class PhaseRuntime {
|
||||
}
|
||||
}
|
||||
}
|
||||
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,
|
||||
@@ -1252,6 +1599,48 @@ export class PhaseRuntime {
|
||||
|
||||
if (phase === "answered") return false;
|
||||
|
||||
// 配方开局模块 + 用户首句已在「用户.需求」→ 默认视为 opening 已做完,直接进 LLM
|
||||
const demand = (
|
||||
this.blackboard.getContentByTag("用户.需求") ??
|
||||
this.session.slots["用户.需求"] ??
|
||||
""
|
||||
)
|
||||
.toString()
|
||||
.trim();
|
||||
if (
|
||||
shouldSkipModuleOpening({
|
||||
demand,
|
||||
dependsOn: binding.step.depends_on,
|
||||
phase,
|
||||
})
|
||||
) {
|
||||
state[stepKey] = "answered";
|
||||
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]:
|
||||
"(已跳过默认问题:用户首句见「用户.需求」)",
|
||||
},
|
||||
};
|
||||
this.onMessage(
|
||||
`[系统] ${binding.module.name} · 已坐在配方开局步,首句见「用户.需求」,跳过默认问题`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (phase === "shown" && this.session.resumeContext) {
|
||||
// 用户刚答完默认问题 → 标记已答,继续走 LLM
|
||||
state[stepKey] = "answered";
|
||||
@@ -1315,15 +1704,13 @@ export class PhaseRuntime {
|
||||
|
||||
const questions = normalizeQuestions([
|
||||
{
|
||||
id: "module-opening",
|
||||
id: MODULE_OPENING_QUESTION_ID,
|
||||
prompt: binding.opening.trim(),
|
||||
allowOther: true,
|
||||
required: true,
|
||||
},
|
||||
]);
|
||||
this.onMessage(
|
||||
`[Worker] ${binding.module.name} · 默认问题(程序):\n${binding.opening.trim()}`,
|
||||
);
|
||||
// 正文由说话面 openingGuide 展示(对齐美学纲领);workerAsk 只发短调度句
|
||||
await this.workerAsk(questions);
|
||||
return true;
|
||||
}
|
||||
@@ -1356,6 +1743,8 @@ export class PhaseRuntime {
|
||||
|
||||
if (!current) current = "flow";
|
||||
|
||||
debugLog("step", `钉住当前步骤 ${workerId} → ${current}`);
|
||||
|
||||
const anchorAt = new Date().toISOString();
|
||||
this.blackboard.write({
|
||||
tag: CREATION_CURRENT_UNIT_TAG,
|
||||
@@ -1386,17 +1775,29 @@ export class PhaseRuntime {
|
||||
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();
|
||||
const unitId =
|
||||
(artifact.workerId === "design-step" ? fromStep : "") ||
|
||||
fromBoard ||
|
||||
fromSlot ||
|
||||
fromSummary ||
|
||||
"";
|
||||
// 接受工作流计划 ≠ 某一步做完;「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({
|
||||
@@ -1518,6 +1919,7 @@ export class PhaseRuntime {
|
||||
return;
|
||||
}
|
||||
|
||||
debugLog("step", "总管开始");
|
||||
const availableWorkers = await this.resolveAvailableWorkers();
|
||||
|
||||
const decision = await this.mainAgent.runToolLoop(
|
||||
@@ -1554,13 +1956,16 @@ export class PhaseRuntime {
|
||||
},
|
||||
);
|
||||
|
||||
debugLog(
|
||||
"step",
|
||||
`总管决策 ${labelAction(decision.action)}${decision.workerId ? ` ${decision.workerId}` : ""}`,
|
||||
);
|
||||
this.onMessage(`[总管] ${decision.action}: ${decision.reason}`);
|
||||
|
||||
const result = applyEvent(this.session, {
|
||||
const result = this.commitEvent({
|
||||
type: "main_agent_decision_created",
|
||||
payload: { decision },
|
||||
});
|
||||
this.session = result.session;
|
||||
|
||||
if (result.error) {
|
||||
this.onMessage(result.error);
|
||||
|
||||
Reference in New Issue
Block a user