diff --git a/lib/ai/runtime/agent-thread-state.ts b/lib/ai/runtime/agent-thread-state.ts new file mode 100644 index 000000000..0a4f25ddc --- /dev/null +++ b/lib/ai/runtime/agent-thread-state.ts @@ -0,0 +1,41 @@ +import { + normalizeAgentWorkflowState, + type AgentWorkflowState, +} from "@/lib/ai/runtime/agent-workflow-state"; + +export interface AgentThreadState { + workflowState: AgentWorkflowState | null; + memoryHints: string[]; + contextSummary?: string; + metadata?: Record; +} + +function normalizeText(value: unknown, max = 300): string | undefined { + const normalized = String(value || "").trim(); + if (!normalized) return undefined; + return normalized.slice(0, max); +} + +function normalizeMemoryHints(value: unknown): string[] { + if (!Array.isArray(value)) return []; + return value + .map((item) => normalizeText(item, 120)) + .filter((item): item is string => Boolean(item)); +} + +export function normalizeAgentThreadState( + value?: Partial | null, +): AgentThreadState | null { + if (!value || typeof value !== "object") return null; + + return { + workflowState: normalizeAgentWorkflowState(value.workflowState), + memoryHints: normalizeMemoryHints(value.memoryHints), + contextSummary: normalizeText(value.contextSummary, 400), + metadata: + value.metadata && typeof value.metadata === "object" && !Array.isArray(value.metadata) + ? (value.metadata as Record) + : undefined, + }; +} + diff --git a/lib/ai/runtime/agent-workflow-state.ts b/lib/ai/runtime/agent-workflow-state.ts new file mode 100644 index 000000000..4c915cdc2 --- /dev/null +++ b/lib/ai/runtime/agent-workflow-state.ts @@ -0,0 +1,114 @@ +export type AgentWorkflowType = + | "storyboard_video" + | "high_cost_video" + | "image_batch" + | "custom"; + +export type AgentWorkflowStatus = + | "draft" + | "awaiting_user_confirmation" + | "confirmed" + | "rejected" + | "running" + | "completed" + | "failed" + | "cancelled"; + +export interface AgentWorkflowPendingItem { + id?: string; + index?: number; + title?: string; + summary?: string; + status?: string; +} + +export interface AgentWorkflowState { + workflowId: string; + type: AgentWorkflowType; + status: AgentWorkflowStatus; + title?: string; + summary?: string; + confirmPhrases: string[]; + revisionHints: string[]; + pendingItems: AgentWorkflowPendingItem[]; + metadata?: Record; +} + +function normalizeText(value: unknown, max = 200): string | undefined { + const normalized = String(value || "").trim(); + if (!normalized) return undefined; + return normalized.slice(0, max); +} + +function normalizeList(value: unknown, fallback: string[]): string[] { + const raw = Array.isArray(value) ? value : fallback; + const normalized = raw + .map((item) => String(item || "").trim()) + .filter(Boolean); + return normalized.length > 0 ? Array.from(new Set(normalized)) : fallback; +} + +function normalizePendingItems(value: unknown): AgentWorkflowPendingItem[] { + if (!Array.isArray(value)) return []; + return value + .map((item) => { + if (!item || typeof item !== "object") return null; + const record = item as Record; + const normalized: AgentWorkflowPendingItem = { + id: normalizeText(record.id, 80), + title: normalizeText(record.title, 80), + summary: normalizeText(record.summary, 160), + status: normalizeText(record.status, 32), + }; + const rawIndex = Number(record.index); + if (Number.isFinite(rawIndex) && rawIndex > 0) { + normalized.index = Math.round(rawIndex); + } + return normalized; + }) + .filter((item): item is AgentWorkflowPendingItem => Boolean(item)); +} + +export function normalizeAgentWorkflowState( + value?: Partial | null, +): AgentWorkflowState | null { + if (!value || typeof value !== "object") return null; + + const workflowId = normalizeText(value.workflowId, 120); + const type = normalizeText(value.type, 32) as AgentWorkflowType | undefined; + const status = normalizeText(value.status, 40) as AgentWorkflowStatus | undefined; + + if (!workflowId || !type || !status) { + return null; + } + + return { + workflowId, + type, + status, + title: normalizeText(value.title, 120), + summary: normalizeText(value.summary, 240), + confirmPhrases: normalizeList(value.confirmPhrases, [ + "确认开始生成视频", + "开始生成", + "按这个分镜生成", + ]), + revisionHints: normalizeList(value.revisionHints, [ + "修改第3镜头", + "调整第5镜头动作", + "重生第2张高清图", + ]), + pendingItems: normalizePendingItems(value.pendingItems), + metadata: + value.metadata && typeof value.metadata === "object" && !Array.isArray(value.metadata) + ? (value.metadata as Record) + : undefined, + }; +} + +export function isAwaitingUserConfirmationWorkflow( + workflowState?: AgentWorkflowState | null, +): boolean { + return workflowState?.status === "awaiting_user_confirmation"; +} + diff --git a/lib/ai/runtime/middlewares/human-confirmation-gate.ts b/lib/ai/runtime/middlewares/human-confirmation-gate.ts new file mode 100644 index 000000000..e42c77be1 --- /dev/null +++ b/lib/ai/runtime/middlewares/human-confirmation-gate.ts @@ -0,0 +1,195 @@ +import { + isAwaitingUserConfirmationWorkflow, + type AgentWorkflowState, + type AgentWorkflowStatus, + type AgentWorkflowType, +} from "@/lib/ai/runtime/agent-workflow-state"; +import type { AgentThreadState } from "@/lib/ai/runtime/agent-thread-state"; + +export type HumanConfirmationIntent = + | "none" + | "confirm" + | "revise" + | "cancel" + | "unclear"; + +export type HumanConfirmationGateAction = + | "pass" + | "awaiting_confirmation" + | "confirmed" + | "revision_requested" + | "cancel_requested"; + +export interface HumanConfirmationGateDecision { + action: HumanConfirmationGateAction; + confirmationIntent: HumanConfirmationIntent; + workflowId?: string; + workflowType?: AgentWorkflowType; + nextWorkflowStatus?: AgentWorkflowStatus; + allowWorkflowExecution: boolean; + shouldBypassPlanner: boolean; + friendlyMessage?: string; +} + +const CONFIRM_PATTERNS = [ + /确认开始生成视频/, + /确认开始生成/, + /按这个分镜生成/, + /开始生成视频/, + /开始生成$/, + /可以开始了/, + /可以执行了/, + /就按这个/, + /没问题开始/, + /确认无误/, +]; + +const REVISE_PATTERNS = [ + /修改第\d+镜头/, + /第\d+镜头/, + /调整第\d+镜头/, + /重生第\d+/, + /重做第\d+/, + /改一下/, + /修改/, + /调整/, + /不要这样/, + /不对/, + /不行/, + /换成/, +]; + +const CANCEL_PATTERNS = [ + /取消/, + /先不要/, + /暂停/, + /停止/, + /先别生成/, + /不用生成了/, +]; + +function normalizeCompactText(text: string) { + return String(text || "").toLowerCase().replace(/\s+/g, "").trim(); +} + +export function detectHumanConfirmationIntent(text: string): HumanConfirmationIntent { + const normalized = normalizeCompactText(text); + if (!normalized) return "unclear"; + + if (CANCEL_PATTERNS.some((pattern) => pattern.test(normalized))) { + return "cancel"; + } + if (REVISE_PATTERNS.some((pattern) => pattern.test(normalized))) { + return "revise"; + } + if (CONFIRM_PATTERNS.some((pattern) => pattern.test(normalized))) { + return "confirm"; + } + return "unclear"; +} + +function buildAwaitingConfirmationMessage(workflowState: AgentWorkflowState): string { + const title = workflowState.title || "当前工作流"; + const summary = workflowState.summary + ? `\n当前待确认内容:${workflowState.summary}` + : ""; + const pendingItemsPreview = workflowState.pendingItems + .slice(0, 3) + .map((item) => { + const indexText = item.index ? `镜头${item.index}` : item.title || "待确认项"; + const summaryText = item.summary ? `:${item.summary}` : ""; + return `- ${indexText}${summaryText}`; + }) + .join("\n"); + const previewBlock = pendingItemsPreview ? `\n${pendingItemsPreview}` : ""; + const confirmExample = workflowState.confirmPhrases[0] || "确认开始生成视频"; + const reviseExample = workflowState.revisionHints[0] || "修改第3镜头"; + + return [ + `当前有一个待确认流程:${title}。`, + summary ? summary.trim() : "", + previewBlock.trim(), + `如果确认无误,请回复“${confirmExample}”。`, + `如果需要修改,请直接回复类似“${reviseExample}”。`, + ] + .filter(Boolean) + .join("\n"); +} + +function buildConfirmMessage(workflowState: AgentWorkflowState): string { + const title = workflowState.title || "当前流程"; + return `已收到确认:${title} 可以进入下一步执行。我会按已确认内容继续处理。`; +} + +function buildRevisionMessage(workflowState: AgentWorkflowState): string { + const title = workflowState.title || "当前流程"; + return `收到修改意见。我会先更新 ${title} 的草案内容,并在重新确认前暂停执行。`; +} + +function buildCancelMessage(workflowState: AgentWorkflowState): string { + const title = workflowState.title || "当前流程"; + return `已收到取消指令。${title} 会继续停留在人工确认前状态,不会继续自动执行。`; +} + +export function runHumanConfirmationGate(params: { + text: string; + threadState?: AgentThreadState | null; +}): HumanConfirmationGateDecision { + const workflowState = params.threadState?.workflowState; + if (!workflowState || !isAwaitingUserConfirmationWorkflow(workflowState)) { + return { + action: "pass", + confirmationIntent: "none", + allowWorkflowExecution: true, + shouldBypassPlanner: false, + }; + } + + const confirmationIntent = detectHumanConfirmationIntent(params.text); + const baseDecision = { + workflowId: workflowState.workflowId, + workflowType: workflowState.type, + shouldBypassPlanner: true, + }; + + switch (confirmationIntent) { + case "confirm": + return { + ...baseDecision, + action: "confirmed", + confirmationIntent, + nextWorkflowStatus: "confirmed", + allowWorkflowExecution: true, + friendlyMessage: buildConfirmMessage(workflowState), + }; + case "revise": + return { + ...baseDecision, + action: "revision_requested", + confirmationIntent, + nextWorkflowStatus: "draft", + allowWorkflowExecution: false, + friendlyMessage: buildRevisionMessage(workflowState), + }; + case "cancel": + return { + ...baseDecision, + action: "cancel_requested", + confirmationIntent, + nextWorkflowStatus: "cancelled", + allowWorkflowExecution: false, + friendlyMessage: buildCancelMessage(workflowState), + }; + case "unclear": + case "none": + default: + return { + ...baseDecision, + action: "awaiting_confirmation", + confirmationIntent: "unclear", + nextWorkflowStatus: workflowState.status, + allowWorkflowExecution: false, + friendlyMessage: buildAwaitingConfirmationMessage(workflowState), + }; + } +}