feat: strengthen agent runtime boundaries
This commit is contained in:
parent
ea561418c4
commit
9ecd91376d
@ -123,11 +123,26 @@ import type {
|
||||
} from "@/lib/ai/context-memory/types";
|
||||
import { ChatSDKError } from "@/lib/errors";
|
||||
import { extractImageUrlsFromText } from "@/lib/image-url";
|
||||
import { isExplicitPromptOptimizationRequest } from "@/lib/ai/smart-analyzer";
|
||||
import {
|
||||
resolveClarificationBypass,
|
||||
resolveClarificationMessage,
|
||||
} from "@/lib/ai/runtime/clarification-interruption";
|
||||
import {
|
||||
pruneTaskBoundaryContext,
|
||||
pruneTaskBoundaryHistoryMessages,
|
||||
} from "@/lib/ai/runtime/task-boundary-context-pruner";
|
||||
import {
|
||||
resolveAgentModelSlotDescriptor,
|
||||
upsertAgentModelSlot,
|
||||
type AgentModelSlots,
|
||||
} from "@/lib/ai/runtime/agent-model-slots";
|
||||
import { isPureGreetingTurnRequest } from "@/lib/ai/runtime/task-boundary-classifier";
|
||||
import {
|
||||
getProviderHealthSnapshot,
|
||||
recordProviderHealthFailure,
|
||||
recordProviderHealthSuccess,
|
||||
shouldAvoidProvider,
|
||||
} from "@/lib/ai/runtime/provider-health";
|
||||
import { uploadFile } from "@/lib/storage/service";
|
||||
import { getVideoStoryboardConfig } from "@/lib/video-agent/config";
|
||||
import { runRuxaNineGridPipeline } from "@/lib/video-agent/pipeline/ruxa-nine-grid-pipeline";
|
||||
@ -1234,15 +1249,6 @@ function normalizeAssistantPartsForPersistence(parts: Array<Record<string, any>>
|
||||
});
|
||||
}
|
||||
|
||||
function isPureGreetingTurnRequest(text: string): boolean {
|
||||
const normalized = String(text || "").toLowerCase().replace(/\s+/g, "").trim();
|
||||
if (!normalized) return false;
|
||||
|
||||
return /^(你好|您好|hello|hi|嗨|哈喽|在吗|早上好|中午好|下午好|晚上好|深夜好|谢谢|感谢|收到|好的|ok|okay|明白了|了解了)$/.test(
|
||||
normalized,
|
||||
);
|
||||
}
|
||||
|
||||
function buildAgentToolImageMarkdown(params: {
|
||||
originalUrl: string;
|
||||
thumbnailUrl?: string | null;
|
||||
@ -3044,8 +3050,10 @@ export async function POST(request: Request) {
|
||||
let reusablePromptMemory = null;
|
||||
let confirmedPromptForConversationState: string | undefined;
|
||||
let smartAutoHistoryMessagesFromDb: DBMessage[] = [];
|
||||
let smartAutoMediaHistoryMessagesFromDb: DBMessage[] = [];
|
||||
let preferredAgentModelId: string | undefined;
|
||||
let configuredAgentConversationModelId: string | undefined;
|
||||
let agentModelSlots: AgentModelSlots = {};
|
||||
let pendingGridConfirmation:
|
||||
| PendingGridConfirmation
|
||||
| null = null;
|
||||
@ -3153,6 +3161,24 @@ export async function POST(request: Request) {
|
||||
if (configuredAgentModel?.model?.id) {
|
||||
preferredAgentModelId = configuredAgentModel.model.id;
|
||||
configuredAgentConversationModelId = configuredAgentModel.model.id;
|
||||
agentModelSlots = upsertAgentModelSlot(agentModelSlots, "planner", {
|
||||
modelId: configuredAgentModel.model.id,
|
||||
displayName: configuredAgentModel.model.displayName || configuredAgentModel.model.modelId,
|
||||
providerId: configuredAgentModel.model.providerId,
|
||||
providerType: configuredAgentModel.provider.type,
|
||||
providerKey: configuredAgentModel.model.providerId,
|
||||
source: "configured_agent_default",
|
||||
reason: "agent_default_model",
|
||||
});
|
||||
agentModelSlots = upsertAgentModelSlot(agentModelSlots, "analyzer", {
|
||||
modelId: configuredAgentModel.model.id,
|
||||
displayName: configuredAgentModel.model.displayName || configuredAgentModel.model.modelId,
|
||||
providerId: configuredAgentModel.model.providerId,
|
||||
providerType: configuredAgentModel.provider.type,
|
||||
providerKey: configuredAgentModel.model.providerId,
|
||||
source: "configured_agent_default",
|
||||
reason: "agent_default_model",
|
||||
});
|
||||
console.log(
|
||||
`[Smart Auto Mode] 🧠 Loaded configured agent conversation model: ${configuredAgentModel.model.displayName}`,
|
||||
);
|
||||
@ -3239,9 +3265,12 @@ export async function POST(request: Request) {
|
||||
workingState: requestWorkingState,
|
||||
activeStoryboardTask: activeStoryboardTaskForChat,
|
||||
hasImageInputForPromptCarryForward: hasImageInputForConversationAction,
|
||||
hasCanvasSelection: Boolean(contextPayload?.selectedNodeIds?.length),
|
||||
});
|
||||
console.log("[ConversationStateProtocol] Bootstrap:", {
|
||||
action: conversationStateBootstrap.actionResolution.kind,
|
||||
taskType: conversationStateBootstrap.taskBoundaryDecision.taskType,
|
||||
taskMode: conversationStateBootstrap.taskBoundaryDecision.taskMode,
|
||||
hasPromptMemory: Boolean(conversationStateBootstrap.promptMemory?.optimizedPrompt),
|
||||
hasImageWorkflowMemory: Boolean(conversationStateBootstrap.imageWorkflowMemory),
|
||||
hasStoryboardPreferenceMemory: Boolean(conversationStateBootstrap.storyboardPreferenceMemory),
|
||||
@ -3252,6 +3281,7 @@ export async function POST(request: Request) {
|
||||
phase: "bootstrap",
|
||||
payload: {
|
||||
action: conversationStateBootstrap.actionResolution.kind,
|
||||
taskBoundary: conversationStateBootstrap.taskBoundaryDecision,
|
||||
pendingGridConfirmation: conversationStateBootstrap.pendingGridConfirmation,
|
||||
hasPromptMemory: Boolean(conversationStateBootstrap.promptMemory?.optimizedPrompt),
|
||||
hasImageWorkflowMemory: Boolean(conversationStateBootstrap.imageWorkflowMemory),
|
||||
@ -3265,6 +3295,17 @@ export async function POST(request: Request) {
|
||||
pendingGridConfirmation = conversationStateBootstrap.pendingGridConfirmation;
|
||||
confirmedPromptForConversationState = conversationStateBootstrap.confirmedPrompt;
|
||||
const resolvedConversationAction = conversationStateBootstrap.actionResolution;
|
||||
const taskBoundaryDecision = conversationStateBootstrap.taskBoundaryDecision;
|
||||
const prunedTaskBoundaryContext = pruneTaskBoundaryContext({
|
||||
decision: taskBoundaryDecision,
|
||||
contextPayload: contextPayload || undefined,
|
||||
autoContextText: autoContextText || undefined,
|
||||
workingState: requestWorkingState || undefined,
|
||||
hasCurrentImageInput: hasImageInputForConversationAction,
|
||||
});
|
||||
const effectiveContextPayload = prunedTaskBoundaryContext.contextPayload;
|
||||
const effectiveAutoContextText = prunedTaskBoundaryContext.autoContextText || "";
|
||||
const effectiveWorkingState = prunedTaskBoundaryContext.workingState;
|
||||
|
||||
if (resolvedConversationAction.forceAgentConversation && !shouldRunAgentConversation) {
|
||||
shouldRunAgentConversation = true;
|
||||
@ -3288,13 +3329,13 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
const shouldFastTrackPromptOnly =
|
||||
shouldRunAgentConversation && isExplicitPromptOptimizationRequest(requestTextContent);
|
||||
shouldRunAgentConversation && taskBoundaryDecision.taskType === "prompt_only";
|
||||
if (shouldFastTrackPromptOnly) {
|
||||
if (!requestResolvedMediaInput) {
|
||||
requestResolvedMediaInput = resolveChatMediaInput({
|
||||
messageParts: message.parts as any[],
|
||||
historyMessages: [],
|
||||
contextPayload: contextPayload || undefined,
|
||||
contextPayload: effectiveContextPayload,
|
||||
requestOrigin,
|
||||
maxImages: 4,
|
||||
});
|
||||
@ -3304,8 +3345,8 @@ export async function POST(request: Request) {
|
||||
requestTargetResolution = resolveTargetAssets({
|
||||
text: requestTextContent,
|
||||
mediaInput: requestResolvedMediaInput,
|
||||
contextPayload: contextPayload || undefined,
|
||||
workingState: requestWorkingState || undefined,
|
||||
contextPayload: effectiveContextPayload,
|
||||
workingState: effectiveWorkingState || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@ -3329,8 +3370,26 @@ export async function POST(request: Request) {
|
||||
|
||||
try {
|
||||
// 🆕 首先获取历史消息(在 Smart Auto Mode 内部单独获取,避免依赖后面的 messagesFromDb)
|
||||
smartAutoHistoryMessagesFromDb = await getMessagesByChatId({ id });
|
||||
console.log("[Smart Auto Mode] Loaded", smartAutoHistoryMessagesFromDb.length, "messages from DB");
|
||||
const loadedHistoryMessages = await getMessagesByChatId({ id });
|
||||
smartAutoHistoryMessagesFromDb = pruneTaskBoundaryHistoryMessages({
|
||||
decision: taskBoundaryDecision,
|
||||
messages: loadedHistoryMessages,
|
||||
purpose: "model",
|
||||
hasCurrentImageInput: hasImageInputForConversationAction,
|
||||
});
|
||||
smartAutoMediaHistoryMessagesFromDb = pruneTaskBoundaryHistoryMessages({
|
||||
decision: taskBoundaryDecision,
|
||||
messages: loadedHistoryMessages,
|
||||
purpose: "media",
|
||||
hasCurrentImageInput: hasImageInputForConversationAction,
|
||||
});
|
||||
console.log("[Smart Auto Mode] Loaded", loadedHistoryMessages.length, "messages from DB");
|
||||
console.log("[Smart Auto Mode] Pruned history:", {
|
||||
modelMessages: smartAutoHistoryMessagesFromDb.length,
|
||||
mediaMessages: smartAutoMediaHistoryMessagesFromDb.length,
|
||||
taskType: taskBoundaryDecision.taskType,
|
||||
taskMode: taskBoundaryDecision.taskMode,
|
||||
});
|
||||
|
||||
autoTextForFallback = smartAutoInputText;
|
||||
if (!requestWorkingState) {
|
||||
@ -3347,8 +3406,8 @@ export async function POST(request: Request) {
|
||||
if (!requestResolvedMediaInput) {
|
||||
requestResolvedMediaInput = resolveChatMediaInput({
|
||||
messageParts: message.parts as any[],
|
||||
historyMessages: smartAutoHistoryMessagesFromDb,
|
||||
contextPayload: contextPayload || undefined,
|
||||
historyMessages: smartAutoMediaHistoryMessagesFromDb,
|
||||
contextPayload: effectiveContextPayload,
|
||||
requestOrigin,
|
||||
maxImages: 4,
|
||||
});
|
||||
@ -3357,14 +3416,14 @@ export async function POST(request: Request) {
|
||||
requestTargetResolution = resolveTargetAssets({
|
||||
text: requestTextContent,
|
||||
mediaInput: requestResolvedMediaInput,
|
||||
contextPayload: contextPayload || undefined,
|
||||
workingState: requestWorkingState || undefined,
|
||||
contextPayload: effectiveContextPayload,
|
||||
workingState: effectiveWorkingState || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
const hasSelectedImageHint =
|
||||
Boolean(contextPayload?.selectedNodeIds?.length) ||
|
||||
/用户已选中\s*\d+\s*张图片/.test(autoContextText);
|
||||
Boolean(effectiveContextPayload?.selectedNodeIds?.length) ||
|
||||
/用户已选中\s*\d+\s*张图片/.test(effectiveAutoContextText);
|
||||
const hasExplicitTargetHint =
|
||||
requestTargetResolution.resolutionMode === "text_reference" ||
|
||||
/第[一二三四五六七八九十\d]+张|左边|左侧|左上|左下|右边|右侧|右上|右下|上面|上方|下面|下方|中间|居中|中央|选中/.test(requestTextContent);
|
||||
@ -3438,7 +3497,7 @@ export async function POST(request: Request) {
|
||||
}
|
||||
const composedConversationState = composeConversationStateProtocol({
|
||||
bootstrap: conversationStateBootstrap,
|
||||
autoContextText: autoContextText || undefined,
|
||||
autoContextText: effectiveAutoContextText || undefined,
|
||||
resolvedMediaInput: requestResolvedMediaInput,
|
||||
targetResolution: requestTargetResolution,
|
||||
});
|
||||
@ -3462,16 +3521,16 @@ export async function POST(request: Request) {
|
||||
hasImage,
|
||||
imageUrls,
|
||||
assetIds: Array.from(
|
||||
new Set([
|
||||
...extractMessageImageAssetIds(message.parts as any[]),
|
||||
...((contextPayload?.selectedAssetIds || []).filter(Boolean) as string[]),
|
||||
]),
|
||||
),
|
||||
new Set([
|
||||
...extractMessageImageAssetIds(message.parts as any[]),
|
||||
...((effectiveContextPayload?.selectedAssetIds || []).filter(Boolean) as string[]),
|
||||
]),
|
||||
),
|
||||
imageFromHistory: smartAutoHistoryImages.length > 0,
|
||||
targetResolutionMode: requestTargetResolution.resolutionMode,
|
||||
mediaSource: requestResolvedMediaInput.source,
|
||||
hasCanvasSelection:
|
||||
Boolean(contextPayload?.selectedNodeIds?.length) ||
|
||||
Boolean(effectiveContextPayload?.selectedNodeIds?.length) ||
|
||||
requestResolvedMediaInput.selectedImageUrls.length > 0,
|
||||
selectedImageCount: requestResolvedMediaInput.selectedImageUrls.length,
|
||||
historyImageCount:
|
||||
@ -3481,7 +3540,7 @@ export async function POST(request: Request) {
|
||||
historyMessages,
|
||||
context:
|
||||
composedConversationState.composedConversationMemory.contextSummary ||
|
||||
autoContextText ||
|
||||
effectiveAutoContextText ||
|
||||
undefined,
|
||||
preferredModelId: preferredAgentModelId,
|
||||
threadState: composedConversationState.threadState,
|
||||
@ -3838,7 +3897,7 @@ export async function POST(request: Request) {
|
||||
const rescueAssetIds = Array.from(
|
||||
new Set([
|
||||
...extractMessageImageAssetIds(message.parts as any[]),
|
||||
...((contextPayload?.selectedAssetIds || []).filter(Boolean) as string[]),
|
||||
...((effectiveContextPayload?.selectedAssetIds || []).filter(Boolean) as string[]),
|
||||
]),
|
||||
);
|
||||
const rescueTimeoutMs = getBudgetedTimeout({
|
||||
@ -4043,7 +4102,7 @@ export async function POST(request: Request) {
|
||||
const summaryAssetIds = Array.from(
|
||||
new Set([
|
||||
...extractMessageImageAssetIds(message.parts as any[]),
|
||||
...((contextPayload?.selectedAssetIds || []).filter(Boolean) as string[]),
|
||||
...((effectiveContextPayload?.selectedAssetIds || []).filter(Boolean) as string[]),
|
||||
]),
|
||||
);
|
||||
const fallbackSummaryTimeoutMs = getBudgetedTimeout({
|
||||
@ -4056,7 +4115,7 @@ export async function POST(request: Request) {
|
||||
summarizeImageForExecutionReport({
|
||||
text: autoTextForFallback,
|
||||
imageUrls: summaryImageUrls,
|
||||
context: typeof autoContext === "string" ? autoContext : undefined,
|
||||
context: effectiveAutoContextText || undefined,
|
||||
preferredModelId: preferredAgentModelId,
|
||||
assetIds: summaryAssetIds,
|
||||
}),
|
||||
@ -4335,11 +4394,11 @@ export async function POST(request: Request) {
|
||||
.map((part) => part.text)
|
||||
.join(" ");
|
||||
const summaryAssetIds = Array.from(
|
||||
new Set([
|
||||
...extractMessageImageAssetIds(message.parts as any[]),
|
||||
...((contextPayload?.selectedAssetIds || []).filter(Boolean) as string[]),
|
||||
]),
|
||||
);
|
||||
new Set([
|
||||
...extractMessageImageAssetIds(message.parts as any[]),
|
||||
...((effectiveContextPayload?.selectedAssetIds || []).filter(Boolean) as string[]),
|
||||
]),
|
||||
);
|
||||
|
||||
let summary: string | null = null;
|
||||
const summaryTimeoutMs = getBudgetedTimeout({
|
||||
@ -4352,7 +4411,7 @@ export async function POST(request: Request) {
|
||||
summarizeImageForExecutionReport({
|
||||
text: latestUserText,
|
||||
imageUrls: summaryImageUrls,
|
||||
context: typeof autoContext === "string" ? autoContext : undefined,
|
||||
context: effectiveAutoContextText || undefined,
|
||||
preferredModelId: preferredAgentModelId,
|
||||
assetIds: summaryAssetIds,
|
||||
}),
|
||||
@ -4421,6 +4480,86 @@ export async function POST(request: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRunAgentConversation && smartAutoExecution?.shouldStayConversational) {
|
||||
const candidateReplyDescriptor = await resolveAgentModelSlotDescriptor(finalModelId).catch(
|
||||
() => null,
|
||||
);
|
||||
if (candidateReplyDescriptor) {
|
||||
agentModelSlots = upsertAgentModelSlot(agentModelSlots, "reply", {
|
||||
...candidateReplyDescriptor,
|
||||
source: "candidate_reply_model",
|
||||
reason: "post_agent_routing",
|
||||
locked: shouldKeepSelectedModel,
|
||||
});
|
||||
|
||||
const providerHealth = getProviderHealthSnapshot(candidateReplyDescriptor.providerKey);
|
||||
if (
|
||||
!shouldKeepSelectedModel &&
|
||||
shouldAvoidProvider(providerHealth)
|
||||
) {
|
||||
let fallbackReplyModelId: string | undefined;
|
||||
|
||||
if (
|
||||
preferredAgentModelId &&
|
||||
preferredAgentModelId !== finalModelId
|
||||
) {
|
||||
const preferredDescriptor = await resolveAgentModelSlotDescriptor(
|
||||
preferredAgentModelId,
|
||||
).catch(() => null);
|
||||
if (
|
||||
preferredDescriptor &&
|
||||
!shouldAvoidProvider(getProviderHealthSnapshot(preferredDescriptor.providerKey))
|
||||
) {
|
||||
fallbackReplyModelId = preferredAgentModelId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fallbackReplyModelId) {
|
||||
try {
|
||||
const { routeToModel } = await import("@/lib/ai/model-router");
|
||||
const replyFallbackRoute = await routeToModel({
|
||||
intent: "chat",
|
||||
subIntent: undefined,
|
||||
hasImage: false,
|
||||
});
|
||||
const fallbackDescriptor = await resolveAgentModelSlotDescriptor(
|
||||
replyFallbackRoute.id,
|
||||
).catch(() => null);
|
||||
if (
|
||||
fallbackDescriptor &&
|
||||
!shouldAvoidProvider(getProviderHealthSnapshot(fallbackDescriptor.providerKey))
|
||||
) {
|
||||
fallbackReplyModelId = replyFallbackRoute.id;
|
||||
}
|
||||
} catch (replyRouteError) {
|
||||
console.warn(
|
||||
"[Smart Auto Mode] Failed to resolve reply fallback route after provider degradation:",
|
||||
replyRouteError,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (fallbackReplyModelId && fallbackReplyModelId !== finalModelId) {
|
||||
finalModelId = fallbackReplyModelId;
|
||||
const fallbackDescriptor = await resolveAgentModelSlotDescriptor(finalModelId).catch(
|
||||
() => null,
|
||||
);
|
||||
if (fallbackDescriptor) {
|
||||
agentModelSlots = upsertAgentModelSlot(agentModelSlots, "reply", {
|
||||
...fallbackDescriptor,
|
||||
source: "provider_health_fallback",
|
||||
reason: providerHealth?.lastEventType || "degraded_provider",
|
||||
locked: false,
|
||||
});
|
||||
}
|
||||
console.warn(
|
||||
`[Smart Auto Mode] ♻️ Switched reply model due provider health: ${candidateReplyDescriptor.displayName || candidateReplyDescriptor.modelId} -> ${fallbackDescriptor?.displayName || fallbackReplyModelId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
@ -4474,6 +4613,7 @@ export async function POST(request: Request) {
|
||||
|
||||
let chat = await getChatById({ id });
|
||||
let messagesFromDb: DBMessage[] = [];
|
||||
let messagesForMediaInput: DBMessage[] = [];
|
||||
|
||||
if (chat) {
|
||||
if (chat.userId !== session.user.id) {
|
||||
@ -4522,6 +4662,20 @@ export async function POST(request: Request) {
|
||||
messagesFromDb = await getMessagesByChatId({ id });
|
||||
}
|
||||
|
||||
const loadedMessagesForBoundary = messagesFromDb;
|
||||
messagesFromDb = pruneTaskBoundaryHistoryMessages({
|
||||
decision: taskBoundaryDecision,
|
||||
messages: loadedMessagesForBoundary,
|
||||
purpose: "model",
|
||||
hasCurrentImageInput: hasImageInputForConversationAction,
|
||||
});
|
||||
messagesForMediaInput = pruneTaskBoundaryHistoryMessages({
|
||||
decision: taskBoundaryDecision,
|
||||
messages: loadedMessagesForBoundary,
|
||||
purpose: "media",
|
||||
hasCurrentImageInput: hasImageInputForConversationAction,
|
||||
});
|
||||
|
||||
if (!requestWorkingState) {
|
||||
requestWorkingState = await getConversationWorkingStateByChatId({
|
||||
chatId: id,
|
||||
@ -4530,8 +4684,8 @@ export async function POST(request: Request) {
|
||||
if (!requestResolvedMediaInput) {
|
||||
requestResolvedMediaInput = resolveChatMediaInput({
|
||||
messageParts: message.parts as any[],
|
||||
historyMessages: messagesFromDb,
|
||||
contextPayload: contextPayload || undefined,
|
||||
historyMessages: messagesForMediaInput,
|
||||
contextPayload: effectiveContextPayload,
|
||||
requestOrigin,
|
||||
maxImages: 4,
|
||||
});
|
||||
@ -4540,8 +4694,8 @@ export async function POST(request: Request) {
|
||||
requestTargetResolution = resolveTargetAssets({
|
||||
text: requestTextContent,
|
||||
mediaInput: requestResolvedMediaInput,
|
||||
contextPayload: contextPayload || undefined,
|
||||
workingState: requestWorkingState || undefined,
|
||||
contextPayload: effectiveContextPayload,
|
||||
workingState: effectiveWorkingState || undefined,
|
||||
});
|
||||
}
|
||||
if (
|
||||
@ -4688,7 +4842,7 @@ export async function POST(request: Request) {
|
||||
shouldRunAgentConversation &&
|
||||
!smartAutoNeedsClarification &&
|
||||
promptOnlySourceImageUrls.length > 0 &&
|
||||
isExplicitPromptOptimizationRequest(requestTextContent);
|
||||
taskBoundaryDecision.taskType === "prompt_only";
|
||||
|
||||
if (wantsPromptOnlyFromImageContext) {
|
||||
let promptOnlyImageSummary = "";
|
||||
@ -4701,16 +4855,16 @@ export async function POST(request: Request) {
|
||||
});
|
||||
if (summaryTimeoutMs) {
|
||||
const summaryAssetIds = Array.from(
|
||||
new Set([
|
||||
...extractMessageImageAssetIds(message.parts as any[]),
|
||||
...((contextPayload?.selectedAssetIds || []).filter(Boolean) as string[]),
|
||||
]),
|
||||
);
|
||||
new Set([
|
||||
...extractMessageImageAssetIds(message.parts as any[]),
|
||||
...((effectiveContextPayload?.selectedAssetIds || []).filter(Boolean) as string[]),
|
||||
]),
|
||||
);
|
||||
const summary = await withOperationTimeout(
|
||||
summarizeImageForExecutionReport({
|
||||
text: requestTextContent,
|
||||
imageUrls: promptOnlySourceImageUrls,
|
||||
context: typeof autoContext === "string" ? autoContext : undefined,
|
||||
context: effectiveAutoContextText || undefined,
|
||||
preferredModelId: preferredAgentModelId,
|
||||
assetIds:
|
||||
promptOnlySourceAssetIds.length > 0 ? promptOnlySourceAssetIds : summaryAssetIds,
|
||||
@ -4752,7 +4906,7 @@ export async function POST(request: Request) {
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as Array<{ role: "user" | "assistant"; content: string }>,
|
||||
context: typeof autoContext === "string" ? autoContext : undefined,
|
||||
context: effectiveAutoContextText || undefined,
|
||||
}).catch(() =>
|
||||
optimizePrompt({
|
||||
originalPrompt: promptSeed,
|
||||
@ -4832,7 +4986,7 @@ export async function POST(request: Request) {
|
||||
part?.type === "file" || part?.type === "image" || part?.type === "image_url"
|
||||
),
|
||||
hasHistoryImageInput: smartAutoHistoryImages.length > 0,
|
||||
hasCanvasSelection: Boolean(contextPayload?.selectedNodeIds?.length),
|
||||
hasCanvasSelection: Boolean(effectiveContextPayload?.selectedNodeIds?.length),
|
||||
targetResolutionMode: requestTargetResolution?.resolutionMode,
|
||||
executionMode: smartAutoExecution?.mode,
|
||||
friendlyMessage: smartAutoFriendlyMessage,
|
||||
@ -6495,6 +6649,7 @@ export async function POST(request: Request) {
|
||||
let actualModelId: string;
|
||||
let resolvedModelSupportsImageGen = false;
|
||||
let resolvedProviderType: string | undefined;
|
||||
let resolvedRuntimeModelLookupId = finalModelId;
|
||||
|
||||
if (isUUID) {
|
||||
// 从数据库获取动态配置的模型
|
||||
@ -6542,6 +6697,7 @@ export async function POST(request: Request) {
|
||||
).toResponse();
|
||||
}
|
||||
languageModel = dynamicModel;
|
||||
resolvedRuntimeModelLookupId = targetModelId;
|
||||
// 对于动态模型,我们需要从数据库获取 modelId
|
||||
const finalModelData = await getAiModelById({ id: targetModelId });
|
||||
actualModelId = finalModelData?.modelId ?? targetModelId;
|
||||
@ -6556,6 +6712,38 @@ export async function POST(request: Request) {
|
||||
languageModel = myProvider.languageModel(finalModelId);
|
||||
actualModelId = languageModel.modelId;
|
||||
resolvedProviderType = (languageModel as any)?.provider;
|
||||
resolvedRuntimeModelLookupId = actualModelId;
|
||||
}
|
||||
|
||||
const activeRuntimeModelDescriptor = await resolveAgentModelSlotDescriptor(
|
||||
resolvedRuntimeModelLookupId,
|
||||
).catch(() => null);
|
||||
if (shouldRunAgentConversation && activeRuntimeModelDescriptor) {
|
||||
const slotName =
|
||||
smartAutoExecution?.shouldApplyImagePipeline ||
|
||||
smartAutoExecution?.shouldApplyVideoPipeline
|
||||
? "execution"
|
||||
: "reply";
|
||||
agentModelSlots = upsertAgentModelSlot(agentModelSlots, slotName, {
|
||||
...activeRuntimeModelDescriptor,
|
||||
source: "runtime_selected_model",
|
||||
reason:
|
||||
slotName === "reply"
|
||||
? smartAutoExecution?.shouldStayConversational
|
||||
? "conversational_reply_runtime"
|
||||
: "default_reply_runtime"
|
||||
: "execution_runtime_model",
|
||||
locked: slotName === "reply" ? shouldKeepSelectedModel : false,
|
||||
});
|
||||
pendingProtocolDebugEvents.push({
|
||||
phase: "modelSlots",
|
||||
payload: {
|
||||
modelSlots: agentModelSlots,
|
||||
providerHealth: activeRuntimeModelDescriptor.providerKey
|
||||
? getProviderHealthSnapshot(activeRuntimeModelDescriptor.providerKey)
|
||||
: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ========== 图片生成配额检查 ==========
|
||||
@ -9301,6 +9489,12 @@ export async function POST(request: Request) {
|
||||
functionId: "stream-text",
|
||||
},
|
||||
onFinish: async ({ usage }) => {
|
||||
if (activeRuntimeModelDescriptor?.providerKey) {
|
||||
recordProviderHealthSuccess({
|
||||
providerKey: activeRuntimeModelDescriptor.providerKey,
|
||||
providerType: activeRuntimeModelDescriptor.providerType,
|
||||
});
|
||||
}
|
||||
try {
|
||||
const providers = await getTokenlensCatalog();
|
||||
if (!actualModelId) {
|
||||
@ -9343,6 +9537,13 @@ export async function POST(request: Request) {
|
||||
result.consumeStream({
|
||||
onError: (error) => {
|
||||
console.error("[Chat API] streamText consume error:", error);
|
||||
if (activeRuntimeModelDescriptor?.providerKey) {
|
||||
recordProviderHealthFailure({
|
||||
providerKey: activeRuntimeModelDescriptor.providerKey,
|
||||
providerType: activeRuntimeModelDescriptor.providerType,
|
||||
error,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -9351,6 +9552,13 @@ export async function POST(request: Request) {
|
||||
sendReasoning: true,
|
||||
onError: (error) => {
|
||||
console.error("[Chat API] streamText ui stream error:", error);
|
||||
if (activeRuntimeModelDescriptor?.providerKey) {
|
||||
recordProviderHealthFailure({
|
||||
providerKey: activeRuntimeModelDescriptor.providerKey,
|
||||
providerType: activeRuntimeModelDescriptor.providerType,
|
||||
error,
|
||||
});
|
||||
}
|
||||
return getChatStreamErrorMessage(error);
|
||||
},
|
||||
})
|
||||
|
||||
667
docs/Agent真正变聪明-系统级整改清单-2026-03-16.md
Normal file
667
docs/Agent真正变聪明-系统级整改清单-2026-03-16.md
Normal file
@ -0,0 +1,667 @@
|
||||
# Agent 真正变聪明:系统级整改清单(2026-03-16)
|
||||
|
||||
> 目标:不是继续给 Agent 打零散补丁,而是把它从“很多聪明 patch 的组合体”,升级成“统一、稳定、可解释、可扩展的对话智能系统”。
|
||||
>
|
||||
> 适用范围:
|
||||
> - `app/(chat)/api/chat/route.ts`
|
||||
> - `lib/ai/auto-agent-graph.ts`
|
||||
> - `lib/ai/smart-analyzer.ts`
|
||||
> - `lib/ai/conversation-state-protocol.ts`
|
||||
> - `lib/ai/conversation-*`
|
||||
> - `lib/ai/runtime/*`
|
||||
> - `lib/video-agent/*`
|
||||
> - 画布上下文、图片工作流、视频工作流
|
||||
|
||||
---
|
||||
|
||||
## 0. 先说结论
|
||||
|
||||
### 0.1 现在 Agent 为什么“看起来不聪明”
|
||||
|
||||
不是单纯模型不够强,而是系统层面还有 5 个根问题:
|
||||
|
||||
1. **任务边界不稳定**
|
||||
- 新任务、继续任务、只聊天、只看图、只要提示词、直接执行,经常互相串。
|
||||
|
||||
2. **上下文污染严重**
|
||||
- 当前输入、历史图片、画布图片、上一轮 workflow、长期记忆,经常一起灌给模型。
|
||||
- 结果就是用户说一句“你好”,系统也可能把它理解成“继续上一轮修图”。
|
||||
|
||||
3. **理解层和执行层没完全解耦**
|
||||
- 一部分逻辑已经进 protocol / subagent / runtime。
|
||||
- 但还有不少判断仍然散在 `chat/route.ts`,导致行为不稳定、修改成本高。
|
||||
|
||||
4. **模型链路不够统一**
|
||||
- planner、analyzer、最终回复、图片执行、视频执行并不是同一套稳定规则。
|
||||
- 之前就出现过:Agent 默认模型是豆包,但最终对话回复走了别的供应商。
|
||||
|
||||
5. **超时退化不够“保正确”**
|
||||
- 一旦 `runAutoAgentGraph`、图像总结、Provider 流式返回超时,系统容易退化成“还能跑,但不对题”。
|
||||
|
||||
### 0.2 我们这次整改的目标
|
||||
|
||||
这次不是“继续修 10 个 bug”,而是把 Agent 升级成:
|
||||
|
||||
- **先判断任务,再选择上下文**
|
||||
- **先选正确路线,再决定模型和工具**
|
||||
- **宁可简单,也不跑偏**
|
||||
- **即使失败,也要明确告诉用户为什么失败**
|
||||
|
||||
---
|
||||
|
||||
## 1. 这次整改的总原则
|
||||
|
||||
### 1.1 一个原则:先判任务,再喂上下文
|
||||
|
||||
以后不能再用“把所有上下文先塞给模型,再让模型自己判断”的方式。
|
||||
|
||||
要改成:
|
||||
|
||||
1. 先判断这轮是:
|
||||
- 新任务
|
||||
- 连续任务
|
||||
- 只分析
|
||||
- 只要提示词
|
||||
- 直接执行
|
||||
- 聊天寒暄
|
||||
2. 再按这个任务类型,裁剪要喂给模型的上下文
|
||||
|
||||
### 1.2 一个原则:执行型和对话型必须分开
|
||||
|
||||
以后要明确分两类:
|
||||
|
||||
- **对话型回合**
|
||||
- 聊天
|
||||
- 看图分析
|
||||
- 提示词整理
|
||||
- 澄清确认
|
||||
|
||||
- **执行型回合**
|
||||
- 生图
|
||||
- 修图
|
||||
- 生视频
|
||||
- 分镜确认执行
|
||||
|
||||
对话型回合不能轻易被旧工作流拖进执行型链路。
|
||||
|
||||
### 1.3 一个原则:失败也要“可解释”
|
||||
|
||||
以后不允许再出现这几种体验:
|
||||
|
||||
- 用户发了消息,看起来“没反应”
|
||||
- 数据库里只存了 keepalive
|
||||
- 模型报错了,但前端没显示
|
||||
- 回退了,但用户不知道系统为什么回退
|
||||
|
||||
---
|
||||
|
||||
## 2. 总体架构目标
|
||||
|
||||
目标架构:
|
||||
|
||||
1. **Task Boundary Layer**
|
||||
2. **Context Pruning Layer**
|
||||
3. **Conversation State Protocol**
|
||||
4. **Planner / Subagent Layer**
|
||||
5. **Execution Gate Layer**
|
||||
6. **Persistence / Event Layer**
|
||||
7. **Frontend Projection Layer**
|
||||
|
||||
一句话解释:
|
||||
|
||||
- **边界层**决定“你这轮到底要干嘛”
|
||||
- **裁剪层**决定“该给模型看哪些上下文”
|
||||
- **协议层**负责统一状态读写
|
||||
- **子代理层**负责专业领域判断
|
||||
- **执行门控层**负责确认、取消、修改、超时回退
|
||||
- **事件层**负责可追踪
|
||||
- **前端层**负责把系统理解结果明确展示给用户
|
||||
|
||||
---
|
||||
|
||||
## 3. 五大整改块
|
||||
|
||||
---
|
||||
|
||||
## 3.1 任务理解层
|
||||
|
||||
### 3.1.1 目标
|
||||
|
||||
让 Agent 先稳定判断:
|
||||
|
||||
- 这是新任务还是继续任务
|
||||
- 这是对话型还是执行型
|
||||
- 这是只分析、只要提示词、还是直接执行
|
||||
|
||||
### 3.1.2 当前问题
|
||||
|
||||
当前系统虽然已经有:
|
||||
|
||||
- `conversation-action-resolver`
|
||||
- `smart-analyzer`
|
||||
- `skill-router`
|
||||
- `auto-agent-graph`
|
||||
|
||||
但这几层对“任务边界”的定义还不完全统一。
|
||||
|
||||
典型问题:
|
||||
|
||||
1. “你好”被旧图片 workflow 拖成连续修图
|
||||
2. “帮我写这张图的提示词”被拖成生图/修图流程
|
||||
3. 用户只是问“这个图怎么写提示词”,系统却提前进入执行链
|
||||
|
||||
### 3.1.3 必做改造
|
||||
|
||||
#### A. 新增统一任务边界分类器
|
||||
|
||||
建议新增:
|
||||
|
||||
- `lib/ai/runtime/task-boundary-classifier.ts`
|
||||
|
||||
输出统一结构:
|
||||
|
||||
```ts
|
||||
type TaskBoundaryDecision = {
|
||||
taskType:
|
||||
| "greeting"
|
||||
| "chat"
|
||||
| "analysis_only"
|
||||
| "prompt_only"
|
||||
| "execution_image"
|
||||
| "execution_video"
|
||||
| "workflow_followup";
|
||||
taskMode: "new" | "continue" | "revise" | "confirm" | "cancel";
|
||||
shouldUseCanvasContext: boolean;
|
||||
shouldUseHistoryImages: boolean;
|
||||
shouldUseWorkflowMemory: boolean;
|
||||
shouldAllowExecution: boolean;
|
||||
confidence: number;
|
||||
reason: string;
|
||||
};
|
||||
```
|
||||
|
||||
#### B. 明确任务优先级规则
|
||||
|
||||
优先级必须写死在系统里,不能全靠模型猜:
|
||||
|
||||
1. `prompt_only` > `workflow_followup`
|
||||
2. `analysis_only` > `execution_image`
|
||||
3. `greeting` > 任意旧 workflow
|
||||
4. `confirm/cancel/revise` > 普通分析
|
||||
5. 当前上传图 > 历史图 > 画布背景图
|
||||
|
||||
#### C. 给“非执行任务”开快路径
|
||||
|
||||
以下场景应该直接走快路径:
|
||||
|
||||
- 纯打招呼
|
||||
- 只要提示词
|
||||
- 只看图分析
|
||||
- 只问建议
|
||||
|
||||
这些回合不应该再先跑完整 `runAutoAgentGraph`。
|
||||
|
||||
### 3.1.4 落点文件
|
||||
|
||||
- `app/(chat)/api/chat/route.ts`
|
||||
- `lib/ai/conversation-action-resolver.ts`
|
||||
- `lib/ai/smart-analyzer.ts`
|
||||
- `lib/ai/runtime/task-boundary-classifier.ts`(新增)
|
||||
|
||||
### 3.1.5 验收标准
|
||||
|
||||
- “你好”永远不被识别为连续修图/连续生图
|
||||
- “帮我写这张图的提示词”永远不直接触发执行
|
||||
- “按照刚才那个提示词,再生成 4 张”会稳定识别为连续生图执行
|
||||
|
||||
---
|
||||
|
||||
## 3.2 记忆层
|
||||
|
||||
### 3.2.1 目标
|
||||
|
||||
让 Agent 的记忆从“混在一起”变成“分层可控”。
|
||||
|
||||
### 3.2.2 当前问题
|
||||
|
||||
现在虽然已经有:
|
||||
|
||||
- `promptMemory`
|
||||
- `imageWorkflowMemory`
|
||||
- `storyboardPreferenceMemory`
|
||||
- `threadState`
|
||||
- `workingState`
|
||||
|
||||
但问题是:
|
||||
|
||||
1. 历史 workflow 容易污染当前回合
|
||||
2. 画布上下文太大,容易把简单任务拖重
|
||||
3. “当前相关记忆”和“长期偏好记忆”还没完全分层
|
||||
|
||||
### 3.2.3 必做改造
|
||||
|
||||
#### A. 拆成 4 层记忆
|
||||
|
||||
1. **Turn Memory**
|
||||
- 当前回合输入
|
||||
- 当前上传图片
|
||||
- 当前画布选中
|
||||
|
||||
2. **Task Memory**
|
||||
- 当前任务工作流状态
|
||||
- 当前任务目标图
|
||||
- 当前任务优化提示词
|
||||
|
||||
3. **Session Memory**
|
||||
- 当前会话最近 3~5 个高相关任务摘要
|
||||
|
||||
4. **Preference Memory**
|
||||
- 用户长期偏好
|
||||
- 常用比例
|
||||
- 常用宫格
|
||||
- 常用确认习惯
|
||||
|
||||
#### B. 增加“记忆衰减/降权”机制
|
||||
|
||||
如果旧 workflow 满足任一条件,应自动降权:
|
||||
|
||||
- 超过 N 轮未继续
|
||||
- 当前上传了新图
|
||||
- 当前任务类型和旧任务类型冲突
|
||||
- 用户显式说“重新来 / 新开一个 / 别按刚才那个”
|
||||
|
||||
#### C. 建立 relevance score
|
||||
|
||||
建议新增:
|
||||
|
||||
- `lib/ai/runtime/memory-relevance.ts`
|
||||
|
||||
对候选记忆打分:
|
||||
|
||||
- 当前上传图命中:+100
|
||||
- 当前选中图命中:+90
|
||||
- 显式引用“上一张 / 第三张 / 这个提示词”:+80
|
||||
- 同类任务:+40
|
||||
- 同会话最近 3 轮:+20
|
||||
- 超过 10 轮未使用:-50
|
||||
|
||||
以后只把 top-k 记忆喂给 Planner。
|
||||
|
||||
### 3.2.4 落点文件
|
||||
|
||||
- `lib/ai/conversation-memory-composer.ts`
|
||||
- `lib/ai/conversation-thread-state-builder.ts`
|
||||
- `lib/ai/runtime/agent-thread-state.ts`
|
||||
- `lib/ai/runtime/memory-relevance.ts`(新增)
|
||||
|
||||
### 3.2.5 验收标准
|
||||
|
||||
- 同一会话里,旧图不再轻易干扰新上传图任务
|
||||
- 简单聊天不再带上大段无关画布上下文
|
||||
- “继续上一个提示词”仍然能稳定命中
|
||||
|
||||
---
|
||||
|
||||
## 3.3 路由层
|
||||
|
||||
### 3.3.1 目标
|
||||
|
||||
让 Planner、Analyzer、最终回复模型、执行模型之间的职责彻底清晰。
|
||||
|
||||
### 3.3.2 当前问题
|
||||
|
||||
现在的问题不是“没有路由”,而是“路由规则不够统一”:
|
||||
|
||||
1. planner 模型和 reply 模型曾经不一致
|
||||
2. analyzer 有时会被别的 provider 抢走
|
||||
3. 生图/修图/视频执行模型与对话模型混杂
|
||||
4. provider 报错或额度不足时,回退不够统一
|
||||
|
||||
### 3.3.3 必做改造
|
||||
|
||||
#### A. 明确 4 类模型槽位
|
||||
|
||||
统一定义:
|
||||
|
||||
1. `plannerModel`
|
||||
2. `analyzerModel`
|
||||
3. `replyModel`
|
||||
4. `executionModel`
|
||||
|
||||
而不是所有地方都混着叫“当前模型”。
|
||||
|
||||
#### B. Agent 模式默认规则
|
||||
|
||||
在 `selectedChatModel = auto` 或进入 Agent 模式时:
|
||||
|
||||
- `plannerModel` = Agent 默认模型
|
||||
- `analyzerModel` = Agent 默认模型优先,多模态能力不足时再回退
|
||||
- `replyModel` = Agent 默认模型
|
||||
- `executionModel` = 按任务类型路由
|
||||
|
||||
#### C. Provider 健康与额度状态缓存
|
||||
|
||||
建议新增:
|
||||
|
||||
- `lib/ai/runtime/provider-health.ts`
|
||||
|
||||
缓存内容:
|
||||
|
||||
- 最近错误类型
|
||||
- 最近额度不足
|
||||
- 最近超时率
|
||||
- 最近 5 分钟成功率
|
||||
|
||||
如果某 provider 连续报:
|
||||
|
||||
- `quota not enough`
|
||||
- `403`
|
||||
- `timeout`
|
||||
|
||||
则在短时间内直接降级,避免用户连续踩坑。
|
||||
|
||||
### 3.3.4 落点文件
|
||||
|
||||
- `app/(chat)/api/chat/route.ts`
|
||||
- `lib/ai/model-router.ts`
|
||||
- `lib/ai/smart-analyzer.ts`
|
||||
- `lib/ai/auto-agent-graph.ts`
|
||||
- `lib/ai/runtime/provider-health.ts`(新增)
|
||||
|
||||
### 3.3.5 验收标准
|
||||
|
||||
- Agent 模式不会再出现“规划走豆包、回复走别家”的错位
|
||||
- 上游 provider 额度不足时,能快速回退或明确报错
|
||||
- 对话模型、执行模型职责清晰
|
||||
|
||||
---
|
||||
|
||||
## 3.4 执行层
|
||||
|
||||
### 3.4.1 目标
|
||||
|
||||
让执行系统遵守:
|
||||
|
||||
- 该确认时必须确认
|
||||
- 该取消时必须停
|
||||
- 该提示时只提示,不执行
|
||||
- 该失败时明确失败
|
||||
|
||||
### 3.4.2 当前问题
|
||||
|
||||
虽然我们已经做了很多 protocol / workflow 事件:
|
||||
|
||||
- clarification
|
||||
- storyboard confirm
|
||||
- revision
|
||||
- cancel
|
||||
- execution started/completed
|
||||
- video generation started/completed/failed
|
||||
|
||||
但执行层还存在 3 个结构问题:
|
||||
|
||||
1. 快路径不够多
|
||||
2. timeout fallback 仍偏重
|
||||
3. “对话型输出”和“执行型输出”没有完全剥离
|
||||
|
||||
### 3.4.3 必做改造
|
||||
|
||||
#### A. 建立 execution policy gate
|
||||
|
||||
建议新增:
|
||||
|
||||
- `lib/ai/runtime/execution-policy-gate.ts`
|
||||
|
||||
统一做这些判定:
|
||||
|
||||
- 当前回合是否允许执行
|
||||
- 当前回合是否必须先确认
|
||||
- 当前回合是否应该只输出提示词
|
||||
- 当前回合是否应该只对话
|
||||
|
||||
#### B. 扩展 fast-path 列表
|
||||
|
||||
必须内建以下快路径:
|
||||
|
||||
1. greeting fast-path
|
||||
2. prompt-only fast-path
|
||||
3. analysis-only fast-path
|
||||
4. confirmation fast-path
|
||||
5. cancel fast-path
|
||||
|
||||
#### C. timeout 退化策略升级
|
||||
|
||||
以后 timeout 退化规则应改成:
|
||||
|
||||
- `execute` 超时 → 不自动换任务类型
|
||||
- `planner` 超时 → 退到简单规则,但不跨任务边界
|
||||
- `provider` 超时 → 直接明确告诉用户该 provider 超时
|
||||
|
||||
一句话:
|
||||
|
||||
**退化后可以更笨,但不能更错。**
|
||||
|
||||
### 3.4.4 落点文件
|
||||
|
||||
- `lib/ai/runtime/middlewares/human-confirmation-gate.ts`
|
||||
- `lib/ai/runtime/execution-policy-gate.ts`(新增)
|
||||
- `app/(chat)/api/chat/route.ts`
|
||||
- `lib/video-agent/storyboard-chat-followup.ts`
|
||||
|
||||
### 3.4.5 验收标准
|
||||
|
||||
- “只要提示词”绝不执行生图
|
||||
- “确认开始生成”绝不只回复聊天
|
||||
- provider 报错时前端总能看到明确原因
|
||||
|
||||
---
|
||||
|
||||
## 3.5 前端交互层
|
||||
|
||||
### 3.5.1 目标
|
||||
|
||||
让用户知道 Agent 当前理解成了什么,而不是靠猜。
|
||||
|
||||
### 3.5.2 当前问题
|
||||
|
||||
现在前端可见性还不够:
|
||||
|
||||
1. 用户不知道系统把这轮理解成聊天、分析还是执行
|
||||
2. 用户不知道系统到底引用了哪张图
|
||||
3. 用户不知道系统为什么突然继续上一轮任务
|
||||
4. 用户不知道系统为什么没执行
|
||||
|
||||
### 3.5.3 必做改造
|
||||
|
||||
#### A. 增加“本轮理解摘要”
|
||||
|
||||
建议在输入框附近或 assistant 第一段里展示:
|
||||
|
||||
- 本轮类型:`聊天 / 分析 / 提示词 / 生图 / 修图 / 视频`
|
||||
- 目标来源:`当前上传 / 画布选中 / 历史结果 / 上一轮提示词`
|
||||
- 是否会执行:`仅分析 / 待确认 / 直接执行`
|
||||
|
||||
#### B. 增加“任务切换提示”
|
||||
|
||||
当系统从旧 workflow 切到新任务时,前端明确显示:
|
||||
|
||||
- “已识别为新任务,不继续沿用上一轮修图流程”
|
||||
|
||||
当系统继续旧任务时,显示:
|
||||
|
||||
- “已继续上一轮修图流程”
|
||||
|
||||
#### C. 协议调试面板继续升级
|
||||
|
||||
当前已有 `ProtocolDebugPanel`,建议继续加:
|
||||
|
||||
- 当前任务边界摘要
|
||||
- 当前记忆裁剪摘要
|
||||
- 当前模型槽位摘要(planner / reply / execution)
|
||||
- 当前 provider 健康状态
|
||||
|
||||
### 3.5.4 落点文件
|
||||
|
||||
- `components/chat.tsx`
|
||||
- `components/protocol-debug-panel.tsx`
|
||||
- `components/canvas/CanvasChatPanel.tsx`
|
||||
|
||||
### 3.5.5 验收标准
|
||||
|
||||
- 用户能看见“系统理解成什么”
|
||||
- 用户能看见“为什么这轮不执行”
|
||||
- debug 时能看见模型和 workflow 生命周期
|
||||
|
||||
---
|
||||
|
||||
## 4. 实施顺序(必须按这个顺序)
|
||||
|
||||
### P0:先止血
|
||||
|
||||
1. greeting fast-path
|
||||
2. prompt-only fast-path
|
||||
3. stream provider 错误明确回显
|
||||
4. Agent 模式 replyModel 跟随 Agent 默认模型
|
||||
|
||||
> 这几项已经部分落地,但要继续收边并补测试。
|
||||
|
||||
### P1:统一任务边界
|
||||
|
||||
1. `task-boundary-classifier`
|
||||
2. 统一 taskType / taskMode / allowExecution
|
||||
3. 对 route 中旧 heuristics 收口
|
||||
|
||||
### P2:统一记忆裁剪
|
||||
|
||||
1. relevance scoring
|
||||
2. session/task/preference 记忆分层
|
||||
3. 旧 workflow 自动降权
|
||||
|
||||
### P3:统一模型链路
|
||||
|
||||
1. planner / analyzer / reply / execution 四槽位
|
||||
2. provider health cache
|
||||
3. 额度不足/超时自动降级
|
||||
|
||||
### P4:统一前端解释性
|
||||
|
||||
1. 本轮理解摘要
|
||||
2. 任务切换提示
|
||||
3. 调试面板生命周期摘要
|
||||
|
||||
---
|
||||
|
||||
## 5. 开发清单(可直接排期)
|
||||
|
||||
### 5.1 任务理解层
|
||||
|
||||
- [ ] 新增 `lib/ai/runtime/task-boundary-classifier.ts`
|
||||
- [ ] 把 `conversation-action-resolver` 和 `smart-analyzer` 结果统一映射到 `taskType`
|
||||
- [ ] 增加 `taskMode`:`new/continue/revise/confirm/cancel`
|
||||
- [ ] 增加 fast-path 优先级表
|
||||
|
||||
### 5.2 记忆层
|
||||
|
||||
- [ ] 新增 `memory-relevance.ts`
|
||||
- [ ] `composeConversationMemory` 支持 top-k 裁剪
|
||||
- [ ] 为 workflow memory 增加过期/降权规则
|
||||
- [ ] 为 prompt-only / analysis-only 场景建立独立上下文模板
|
||||
|
||||
### 5.3 路由层
|
||||
|
||||
- [ ] 统一 planner / analyzer / reply / execution 槽位
|
||||
- [ ] Agent replyModel 跟随 Agent 默认模型
|
||||
- [ ] provider health 状态缓存
|
||||
- [ ] 上游 quota / timeout 短时熔断
|
||||
|
||||
### 5.4 执行层
|
||||
|
||||
- [ ] 新增 `execution-policy-gate.ts`
|
||||
- [ ] clarification / prompt-only / analysis-only 全部走快路径
|
||||
- [ ] timeout fallback 改成“保正确优先”
|
||||
- [ ] 错误统一映射为用户可理解消息
|
||||
|
||||
### 5.5 前端层
|
||||
|
||||
- [ ] 增加“本轮理解摘要”
|
||||
- [ ] 增加“任务切换提示”
|
||||
- [ ] Protocol Debug 增加模型槽位摘要
|
||||
- [ ] Protocol Debug 增加上下文裁剪摘要
|
||||
|
||||
---
|
||||
|
||||
## 6. 这次整改完成后,用户体验应该变成什么样
|
||||
|
||||
### 场景 1:用户说“你好”
|
||||
|
||||
系统行为:
|
||||
|
||||
- 识别为 `greeting`
|
||||
- 不读取旧修图 workflow
|
||||
- 不做大规模图像分析
|
||||
- 1 秒内稳定回复
|
||||
|
||||
### 场景 2:用户上传图片,说“帮我写这张图的提示词”
|
||||
|
||||
系统行为:
|
||||
|
||||
- 识别为 `prompt_only`
|
||||
- 只分析当前图片
|
||||
- 不进入生图/修图执行链
|
||||
- 输出提示词
|
||||
- 把提示词写入 prompt memory
|
||||
|
||||
### 场景 3:用户说“按刚才那个提示词再来 4 张”
|
||||
|
||||
系统行为:
|
||||
|
||||
- 识别为 `image_generation_followup`
|
||||
- 复用上一轮提示词和参数
|
||||
- 明确出图数量为 4
|
||||
- 进入执行链
|
||||
|
||||
### 场景 4:用户说“修改第 3 镜头,再确认”
|
||||
|
||||
系统行为:
|
||||
|
||||
- 识别为 `workflow_followup + revise`
|
||||
- 只进入 storyboard revision 子链路
|
||||
- 不误切普通生图
|
||||
|
||||
---
|
||||
|
||||
## 7. 最后一句话
|
||||
|
||||
我们现在已经有:
|
||||
|
||||
- protocol
|
||||
- subagent
|
||||
- workflow event
|
||||
- image/video memory
|
||||
- debug stream
|
||||
|
||||
所以真正差的不是“没有 Agent”,而是:
|
||||
|
||||
> **还没有把任务边界、记忆裁剪、模型链路、执行门控做成统一系统。**
|
||||
|
||||
这份整改清单的目标,就是把 Agent 模式从:
|
||||
|
||||
**很多聪明 patch**
|
||||
|
||||
升级成:
|
||||
|
||||
**真正稳定的统一 Agent Runtime**
|
||||
|
||||
---
|
||||
|
||||
## 8. 建议的下一步执行顺序
|
||||
|
||||
如果马上开工,建议按下面顺序推进:
|
||||
|
||||
1. 先做 **`task-boundary-classifier`**
|
||||
2. 再做 **memory relevance / context pruning**
|
||||
3. 再做 **provider health + model slot unification**
|
||||
4. 最后做 **前端理解摘要**
|
||||
|
||||
因为只有先把任务边界做对,后面的“更聪明”才不会继续建立在错误理解之上。
|
||||
|
||||
@ -3,6 +3,11 @@ import "server-only";
|
||||
import type { AgentThreadState } from "@/lib/ai/runtime/agent-thread-state";
|
||||
import type { ImageWorkflowMemory } from "@/lib/ai/image-workflow-memory";
|
||||
import type { PromptMemoryEntry } from "@/lib/ai/prompt-memory";
|
||||
import {
|
||||
applyContextLineLimit,
|
||||
applyMemoryHintLimit,
|
||||
type MemoryRelevancePolicy,
|
||||
} from "@/lib/ai/runtime/memory-relevance";
|
||||
import type { StoryboardPreferenceMemory } from "@/lib/video-agent/storyboard-preference-memory";
|
||||
import type {
|
||||
ResolvedChatMediaInput,
|
||||
@ -164,6 +169,7 @@ export function composeConversationMemory(params: {
|
||||
imageWorkflowMemory?: ImageWorkflowMemory | null;
|
||||
storyboardPreferenceMemory?: StoryboardPreferenceMemory | null;
|
||||
threadState?: AgentThreadState | null;
|
||||
memoryRelevancePolicy?: MemoryRelevancePolicy;
|
||||
}) {
|
||||
const targetSummary = summarizeTargetResolution(params.targetResolution);
|
||||
const mediaSummary = summarizeResolvedMedia(params.resolvedMediaInput);
|
||||
@ -197,8 +203,12 @@ export function composeConversationMemory(params: {
|
||||
};
|
||||
|
||||
return {
|
||||
contextSummary,
|
||||
memoryHints,
|
||||
contextSummary: params.memoryRelevancePolicy
|
||||
? applyContextLineLimit(contextSummary, params.memoryRelevancePolicy.maxContextLines)
|
||||
: contextSummary,
|
||||
memoryHints: params.memoryRelevancePolicy
|
||||
? applyMemoryHintLimit(memoryHints, params.memoryRelevancePolicy.maxMemoryHints)
|
||||
: memoryHints,
|
||||
metadata,
|
||||
} satisfies ComposedConversationMemory;
|
||||
}
|
||||
|
||||
@ -22,6 +22,11 @@ import {
|
||||
resolveConversationAction,
|
||||
extractPendingGridConfirmationFromWorkingState,
|
||||
} from "@/lib/ai/conversation-action-resolver";
|
||||
import {
|
||||
applyContextLineLimit,
|
||||
applyMemoryHintLimit,
|
||||
resolveMemoryRelevancePolicy,
|
||||
} from "@/lib/ai/runtime/memory-relevance";
|
||||
import {
|
||||
composeConversationMemory,
|
||||
type ComposedConversationMemory,
|
||||
@ -37,6 +42,10 @@ import {
|
||||
buildPromptMemoryUpdate,
|
||||
} from "@/lib/ai/prompt-memory";
|
||||
import type { AgentThreadState } from "@/lib/ai/runtime/agent-thread-state";
|
||||
import {
|
||||
classifyTaskBoundary,
|
||||
type TaskBoundaryDecision,
|
||||
} from "@/lib/ai/runtime/task-boundary-classifier";
|
||||
import type { StoryboardPreferenceMemory } from "@/lib/video-agent/storyboard-preference-memory";
|
||||
import {
|
||||
extractStoryboardPreferenceMemory,
|
||||
@ -56,6 +65,7 @@ export interface ConversationStateProtocolBootstrap {
|
||||
pendingGridConfirmation: PendingGridConfirmation | null;
|
||||
confirmedPrompt: string | undefined;
|
||||
actionResolution: ReturnType<typeof resolveConversationAction>;
|
||||
taskBoundaryDecision: TaskBoundaryDecision;
|
||||
}
|
||||
|
||||
export interface ConversationStateProtocolComposed {
|
||||
@ -181,6 +191,7 @@ export function bootstrapConversationStateProtocol(params: {
|
||||
} | null;
|
||||
activeStoryboardTask?: StoryboardTaskDetail | null;
|
||||
hasImageInputForPromptCarryForward: boolean;
|
||||
hasCanvasSelection?: boolean;
|
||||
}) {
|
||||
const storyboardPreferenceMemory = extractStoryboardPreferenceMemory(params.workingState);
|
||||
const imageWorkflowMemory = extractImageWorkflowMemory(params.workingState);
|
||||
@ -197,6 +208,14 @@ export function bootstrapConversationStateProtocol(params: {
|
||||
confirmedPrompt,
|
||||
hasImageInputForPromptCarryForward: params.hasImageInputForPromptCarryForward,
|
||||
});
|
||||
const taskBoundaryDecision = classifyTaskBoundary({
|
||||
text: params.text,
|
||||
actionResolution,
|
||||
hasCurrentImageInput: params.hasImageInputForPromptCarryForward,
|
||||
hasCanvasSelection: params.hasCanvasSelection === true,
|
||||
pendingGridConfirmation,
|
||||
hasActiveStoryboardTask: Boolean(params.activeStoryboardTask?.task?.id),
|
||||
});
|
||||
|
||||
return {
|
||||
workingState: params.workingState || null,
|
||||
@ -207,6 +226,7 @@ export function bootstrapConversationStateProtocol(params: {
|
||||
pendingGridConfirmation,
|
||||
confirmedPrompt,
|
||||
actionResolution,
|
||||
taskBoundaryDecision,
|
||||
} satisfies ConversationStateProtocolBootstrap;
|
||||
}
|
||||
|
||||
@ -216,33 +236,62 @@ export function composeConversationStateProtocol(params: {
|
||||
resolvedMediaInput: ResolvedChatMediaInput;
|
||||
targetResolution: ResolvedTargetAssets;
|
||||
}) {
|
||||
const memoryRelevancePolicy = resolveMemoryRelevancePolicy({
|
||||
taskBoundaryDecision: params.bootstrap.taskBoundaryDecision,
|
||||
actionResolution: params.bootstrap.actionResolution,
|
||||
hasActiveStoryboardTask: Boolean(params.bootstrap.activeStoryboardTask?.task?.id),
|
||||
hasPendingGridConfirmation: Boolean(params.bootstrap.pendingGridConfirmation),
|
||||
hasCurrentImageInput: params.resolvedMediaInput.currentMessageImageUrls.length > 0,
|
||||
hasCanvasSelection: params.resolvedMediaInput.selectedImageUrls.length > 0,
|
||||
});
|
||||
|
||||
const baseThreadState = buildConversationThreadState({
|
||||
activeStoryboardTask: params.bootstrap.activeStoryboardTask,
|
||||
storyboardPreferenceMemory: params.bootstrap.storyboardPreferenceMemory,
|
||||
promptMemory: params.bootstrap.promptMemory,
|
||||
imageWorkflowMemory: params.bootstrap.imageWorkflowMemory,
|
||||
activeStoryboardTask: memoryRelevancePolicy.includeWorkflowState
|
||||
? params.bootstrap.activeStoryboardTask
|
||||
: null,
|
||||
storyboardPreferenceMemory: memoryRelevancePolicy.includeStoryboardPreferenceMemory
|
||||
? params.bootstrap.storyboardPreferenceMemory
|
||||
: null,
|
||||
promptMemory: memoryRelevancePolicy.includePromptMemory
|
||||
? params.bootstrap.promptMemory
|
||||
: null,
|
||||
imageWorkflowMemory: memoryRelevancePolicy.includeImageWorkflowMemory
|
||||
? params.bootstrap.imageWorkflowMemory
|
||||
: null,
|
||||
});
|
||||
|
||||
const composedConversationMemory = composeConversationMemory({
|
||||
autoContextText: params.autoContextText || undefined,
|
||||
resolvedMediaInput: params.resolvedMediaInput,
|
||||
targetResolution: params.targetResolution,
|
||||
promptMemory: params.bootstrap.promptMemory,
|
||||
imageWorkflowMemory: params.bootstrap.imageWorkflowMemory,
|
||||
storyboardPreferenceMemory: params.bootstrap.storyboardPreferenceMemory,
|
||||
promptMemory: memoryRelevancePolicy.includePromptMemory
|
||||
? params.bootstrap.promptMemory
|
||||
: null,
|
||||
imageWorkflowMemory: memoryRelevancePolicy.includeImageWorkflowMemory
|
||||
? params.bootstrap.imageWorkflowMemory
|
||||
: null,
|
||||
storyboardPreferenceMemory: memoryRelevancePolicy.includeStoryboardPreferenceMemory
|
||||
? params.bootstrap.storyboardPreferenceMemory
|
||||
: null,
|
||||
threadState: baseThreadState,
|
||||
memoryRelevancePolicy,
|
||||
});
|
||||
|
||||
const threadState: AgentThreadState = {
|
||||
workflowState: baseThreadState?.workflowState || null,
|
||||
memoryHints: Array.from(
|
||||
memoryHints: applyMemoryHintLimit(
|
||||
Array.from(
|
||||
new Set([
|
||||
...(baseThreadState?.memoryHints || []),
|
||||
...composedConversationMemory.memoryHints,
|
||||
])
|
||||
),
|
||||
contextSummary:
|
||||
memoryRelevancePolicy.maxMemoryHints,
|
||||
),
|
||||
contextSummary: applyContextLineLimit(
|
||||
composedConversationMemory.contextSummary || baseThreadState?.contextSummary,
|
||||
memoryRelevancePolicy.maxContextLines,
|
||||
),
|
||||
metadata: {
|
||||
...(baseThreadState?.metadata || {}),
|
||||
...composedConversationMemory.metadata,
|
||||
|
||||
108
lib/ai/runtime/agent-model-slots.ts
Normal file
108
lib/ai/runtime/agent-model-slots.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import "server-only";
|
||||
|
||||
import {
|
||||
getAiModelById,
|
||||
getAiProviderById,
|
||||
getEnabledAiModelsWithProvider,
|
||||
} from "@/lib/db/queries";
|
||||
|
||||
export type AgentModelSlotName = "planner" | "analyzer" | "reply" | "execution";
|
||||
|
||||
export interface AgentModelSlotDescriptor {
|
||||
modelId: string;
|
||||
displayName?: string;
|
||||
providerId?: string;
|
||||
providerType?: string;
|
||||
providerKey?: string;
|
||||
}
|
||||
|
||||
export interface AgentModelSlot extends AgentModelSlotDescriptor {
|
||||
slot: AgentModelSlotName;
|
||||
source: string;
|
||||
reason?: string;
|
||||
locked?: boolean;
|
||||
}
|
||||
|
||||
export interface AgentModelSlots {
|
||||
planner?: AgentModelSlot;
|
||||
analyzer?: AgentModelSlot;
|
||||
reply?: AgentModelSlot;
|
||||
execution?: AgentModelSlot;
|
||||
}
|
||||
|
||||
function buildProviderKey(params: {
|
||||
providerId?: string;
|
||||
providerType?: string;
|
||||
modelId: string;
|
||||
}) {
|
||||
if (params.providerId) return params.providerId;
|
||||
if (params.providerType) return `${params.providerType}:${params.modelId}`;
|
||||
return `model:${params.modelId}`;
|
||||
}
|
||||
|
||||
export async function resolveAgentModelSlotDescriptor(modelId: string) {
|
||||
const normalizedModelId = String(modelId || "").trim();
|
||||
if (!normalizedModelId) return null;
|
||||
|
||||
const isUuid =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(normalizedModelId);
|
||||
|
||||
if (isUuid) {
|
||||
const model = await getAiModelById({ id: normalizedModelId }).catch(() => null);
|
||||
if (!model) {
|
||||
return {
|
||||
modelId: normalizedModelId,
|
||||
providerKey: `model:${normalizedModelId}`,
|
||||
} satisfies AgentModelSlotDescriptor;
|
||||
}
|
||||
const provider = await getAiProviderById({ id: model.providerId }).catch(() => null);
|
||||
return {
|
||||
modelId: model.id,
|
||||
displayName: model.displayName || model.modelId,
|
||||
providerId: model.providerId,
|
||||
providerType: provider?.type || undefined,
|
||||
providerKey: buildProviderKey({
|
||||
providerId: model.providerId,
|
||||
providerType: provider?.type || undefined,
|
||||
modelId: model.id,
|
||||
}),
|
||||
} satisfies AgentModelSlotDescriptor;
|
||||
}
|
||||
|
||||
const enabledModels = await getEnabledAiModelsWithProvider(false).catch(() => []);
|
||||
const matched = enabledModels.find(
|
||||
({ model }) => model.id === normalizedModelId || model.modelId === normalizedModelId,
|
||||
);
|
||||
if (!matched) {
|
||||
return {
|
||||
modelId: normalizedModelId,
|
||||
providerKey: `model:${normalizedModelId}`,
|
||||
} satisfies AgentModelSlotDescriptor;
|
||||
}
|
||||
|
||||
return {
|
||||
modelId: matched.model.id,
|
||||
displayName: matched.model.displayName || matched.model.modelId,
|
||||
providerId: matched.model.providerId,
|
||||
providerType: matched.provider?.type || undefined,
|
||||
providerKey: buildProviderKey({
|
||||
providerId: matched.model.providerId,
|
||||
providerType: matched.provider?.type || undefined,
|
||||
modelId: matched.model.id,
|
||||
}),
|
||||
} satisfies AgentModelSlotDescriptor;
|
||||
}
|
||||
|
||||
export function upsertAgentModelSlot(
|
||||
slots: AgentModelSlots,
|
||||
slot: AgentModelSlotName,
|
||||
value: Omit<AgentModelSlot, "slot">,
|
||||
) {
|
||||
return {
|
||||
...slots,
|
||||
[slot]: {
|
||||
slot,
|
||||
...value,
|
||||
},
|
||||
} satisfies AgentModelSlots;
|
||||
}
|
||||
134
lib/ai/runtime/memory-relevance.ts
Normal file
134
lib/ai/runtime/memory-relevance.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import "server-only";
|
||||
|
||||
import type { ConversationActionResolution } from "@/lib/ai/conversation-action-resolver";
|
||||
import type { TaskBoundaryDecision } from "@/lib/ai/runtime/task-boundary-classifier";
|
||||
|
||||
export interface MemoryRelevancePolicy {
|
||||
includePromptMemory: boolean;
|
||||
includeImageWorkflowMemory: boolean;
|
||||
includeStoryboardPreferenceMemory: boolean;
|
||||
includeWorkflowState: boolean;
|
||||
maxMemoryHints: number;
|
||||
maxContextLines: number;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export function resolveMemoryRelevancePolicy(params: {
|
||||
taskBoundaryDecision: TaskBoundaryDecision;
|
||||
actionResolution: ConversationActionResolution;
|
||||
hasActiveStoryboardTask: boolean;
|
||||
hasPendingGridConfirmation: boolean;
|
||||
hasCurrentImageInput: boolean;
|
||||
hasCanvasSelection: boolean;
|
||||
}) {
|
||||
const { taskBoundaryDecision, actionResolution } = params;
|
||||
|
||||
switch (taskBoundaryDecision.taskType) {
|
||||
case "greeting":
|
||||
return {
|
||||
includePromptMemory: false,
|
||||
includeImageWorkflowMemory: false,
|
||||
includeStoryboardPreferenceMemory: false,
|
||||
includeWorkflowState: false,
|
||||
maxMemoryHints: 0,
|
||||
maxContextLines: 2,
|
||||
reason: "greeting_turn_ignores_task_memory",
|
||||
} satisfies MemoryRelevancePolicy;
|
||||
case "chat":
|
||||
return {
|
||||
includePromptMemory: false,
|
||||
includeImageWorkflowMemory: false,
|
||||
includeStoryboardPreferenceMemory: false,
|
||||
includeWorkflowState: false,
|
||||
maxMemoryHints: 1,
|
||||
maxContextLines: 3,
|
||||
reason: "chat_turn_prefers_clean_context",
|
||||
} satisfies MemoryRelevancePolicy;
|
||||
case "prompt_only":
|
||||
return {
|
||||
includePromptMemory: false,
|
||||
includeImageWorkflowMemory: false,
|
||||
includeStoryboardPreferenceMemory: false,
|
||||
includeWorkflowState: false,
|
||||
maxMemoryHints: 2,
|
||||
maxContextLines: 4,
|
||||
reason: "prompt_only_turn_ignores_execution_memory",
|
||||
} satisfies MemoryRelevancePolicy;
|
||||
case "analysis_only":
|
||||
return {
|
||||
includePromptMemory: false,
|
||||
includeImageWorkflowMemory: false,
|
||||
includeStoryboardPreferenceMemory: false,
|
||||
includeWorkflowState: false,
|
||||
maxMemoryHints: 2,
|
||||
maxContextLines: 4,
|
||||
reason: "analysis_only_turn_ignores_workflow_memory",
|
||||
} satisfies MemoryRelevancePolicy;
|
||||
case "execution_video":
|
||||
return {
|
||||
includePromptMemory: false,
|
||||
includeImageWorkflowMemory: false,
|
||||
includeStoryboardPreferenceMemory: true,
|
||||
includeWorkflowState:
|
||||
params.hasActiveStoryboardTask || params.hasPendingGridConfirmation,
|
||||
maxMemoryHints: 4,
|
||||
maxContextLines: 6,
|
||||
reason: "video_execution_prefers_storyboard_preferences",
|
||||
} satisfies MemoryRelevancePolicy;
|
||||
case "workflow_followup":
|
||||
return {
|
||||
includePromptMemory:
|
||||
actionResolution.kind === "image_generation_followup" ||
|
||||
actionResolution.kind === "prompt_carry_forward",
|
||||
includeImageWorkflowMemory:
|
||||
actionResolution.kind === "image_edit_followup" ||
|
||||
actionResolution.kind === "image_generation_followup" ||
|
||||
actionResolution.kind === "prompt_carry_forward",
|
||||
includeStoryboardPreferenceMemory:
|
||||
actionResolution.kind === "grid_confirmation_followup" ||
|
||||
actionResolution.kind === "active_storyboard_followup",
|
||||
includeWorkflowState: true,
|
||||
maxMemoryHints: 6,
|
||||
maxContextLines: 8,
|
||||
reason: "workflow_followup_requires_workflow_memory",
|
||||
} satisfies MemoryRelevancePolicy;
|
||||
case "execution_image":
|
||||
default:
|
||||
return {
|
||||
includePromptMemory:
|
||||
actionResolution.usePromptCarryForward ||
|
||||
taskBoundaryDecision.taskMode === "continue",
|
||||
includeImageWorkflowMemory:
|
||||
taskBoundaryDecision.taskMode === "continue" ||
|
||||
(!params.hasCurrentImageInput && !params.hasCanvasSelection),
|
||||
includeStoryboardPreferenceMemory: false,
|
||||
includeWorkflowState:
|
||||
taskBoundaryDecision.taskMode === "continue" &&
|
||||
(actionResolution.kind === "image_edit_followup" ||
|
||||
actionResolution.kind === "image_generation_followup" ||
|
||||
actionResolution.kind === "prompt_carry_forward"),
|
||||
maxMemoryHints: taskBoundaryDecision.taskMode === "continue" ? 5 : 3,
|
||||
maxContextLines: taskBoundaryDecision.taskMode === "continue" ? 7 : 5,
|
||||
reason:
|
||||
taskBoundaryDecision.taskMode === "continue"
|
||||
? "image_execution_continue_reuses_relevant_memory"
|
||||
: "image_execution_new_task_limits_old_memory",
|
||||
} satisfies MemoryRelevancePolicy;
|
||||
}
|
||||
}
|
||||
|
||||
export function applyMemoryHintLimit(hints: string[], maxHints: number) {
|
||||
if (!Array.isArray(hints) || maxHints <= 0) return [] as string[];
|
||||
return hints.slice(0, maxHints);
|
||||
}
|
||||
|
||||
export function applyContextLineLimit(summary: string | undefined, maxLines: number) {
|
||||
const normalized = String(summary || "").trim();
|
||||
if (!normalized || maxLines <= 0) return "";
|
||||
return normalized
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, maxLines)
|
||||
.join("\n");
|
||||
}
|
||||
109
lib/ai/runtime/provider-health.ts
Normal file
109
lib/ai/runtime/provider-health.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import "server-only";
|
||||
|
||||
export type ProviderHealthEventType = "success" | "timeout" | "quota" | "error";
|
||||
|
||||
export interface ProviderHealthSnapshot {
|
||||
providerKey: string;
|
||||
providerType?: string;
|
||||
lastEventType: ProviderHealthEventType;
|
||||
lastError?: string;
|
||||
consecutiveFailures: number;
|
||||
lastSuccessAt?: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
const providerHealthMap = new Map<string, ProviderHealthSnapshot>();
|
||||
|
||||
function normalizeErrorMessage(error: unknown) {
|
||||
return error instanceof Error ? String(error.message || "").trim() : String(error || "").trim();
|
||||
}
|
||||
|
||||
export function classifyProviderHealthEvent(error: unknown): ProviderHealthEventType {
|
||||
const normalized = normalizeErrorMessage(error).toLowerCase();
|
||||
|
||||
if (
|
||||
normalized.includes("token quota is not enough") ||
|
||||
normalized.includes("pre_consume_token_quota_failed") ||
|
||||
normalized.includes("quota is not enough")
|
||||
) {
|
||||
return "quota";
|
||||
}
|
||||
|
||||
if (
|
||||
normalized.includes("timeout") ||
|
||||
normalized.includes("timed out") ||
|
||||
normalized.includes("body timeout") ||
|
||||
normalized.includes("headers timeout")
|
||||
) {
|
||||
return "timeout";
|
||||
}
|
||||
|
||||
return "error";
|
||||
}
|
||||
|
||||
export function recordProviderHealthSuccess(params: {
|
||||
providerKey?: string;
|
||||
providerType?: string;
|
||||
}) {
|
||||
const providerKey = String(params.providerKey || "").trim();
|
||||
if (!providerKey) return;
|
||||
|
||||
const existing = providerHealthMap.get(providerKey);
|
||||
providerHealthMap.set(providerKey, {
|
||||
providerKey,
|
||||
providerType: params.providerType || existing?.providerType,
|
||||
lastEventType: "success",
|
||||
consecutiveFailures: 0,
|
||||
lastError: undefined,
|
||||
lastSuccessAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
export function recordProviderHealthFailure(params: {
|
||||
providerKey?: string;
|
||||
providerType?: string;
|
||||
error: unknown;
|
||||
}) {
|
||||
const providerKey = String(params.providerKey || "").trim();
|
||||
if (!providerKey) return;
|
||||
|
||||
const existing = providerHealthMap.get(providerKey);
|
||||
providerHealthMap.set(providerKey, {
|
||||
providerKey,
|
||||
providerType: params.providerType || existing?.providerType,
|
||||
lastEventType: classifyProviderHealthEvent(params.error),
|
||||
lastError: normalizeErrorMessage(params.error).slice(0, 400),
|
||||
consecutiveFailures:
|
||||
existing?.lastEventType === "success"
|
||||
? 1
|
||||
: Math.max(1, (existing?.consecutiveFailures || 0) + 1),
|
||||
lastSuccessAt: existing?.lastSuccessAt,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
export function getProviderHealthSnapshot(providerKey?: string) {
|
||||
const normalized = String(providerKey || "").trim();
|
||||
if (!normalized) return null;
|
||||
return providerHealthMap.get(normalized) || null;
|
||||
}
|
||||
|
||||
export function shouldAvoidProvider(snapshot?: ProviderHealthSnapshot | null) {
|
||||
if (!snapshot) return false;
|
||||
|
||||
const ageMs = Date.now() - snapshot.updatedAt;
|
||||
if (snapshot.lastEventType === "quota") {
|
||||
return ageMs <= 15 * 60 * 1000;
|
||||
}
|
||||
|
||||
if (snapshot.lastEventType === "timeout") {
|
||||
return snapshot.consecutiveFailures >= 2 && ageMs <= 5 * 60 * 1000;
|
||||
}
|
||||
|
||||
if (snapshot.lastEventType === "error") {
|
||||
return snapshot.consecutiveFailures >= 3 && ageMs <= 5 * 60 * 1000;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
226
lib/ai/runtime/task-boundary-classifier.ts
Normal file
226
lib/ai/runtime/task-boundary-classifier.ts
Normal file
@ -0,0 +1,226 @@
|
||||
import "server-only";
|
||||
|
||||
import type {
|
||||
ConversationActionResolution,
|
||||
PendingGridConfirmation,
|
||||
} from "@/lib/ai/conversation-action-resolver";
|
||||
import { isExplicitPromptOptimizationRequest } from "@/lib/ai/smart-analyzer";
|
||||
import { detectStoryboardCommandIntent } from "@/lib/video-agent/storyboard-command-interpreter";
|
||||
|
||||
export type TaskBoundaryType =
|
||||
| "greeting"
|
||||
| "chat"
|
||||
| "analysis_only"
|
||||
| "prompt_only"
|
||||
| "execution_image"
|
||||
| "execution_video"
|
||||
| "workflow_followup";
|
||||
|
||||
export type TaskBoundaryMode = "new" | "continue" | "revise" | "confirm" | "cancel";
|
||||
|
||||
export interface TaskBoundaryDecision {
|
||||
taskType: TaskBoundaryType;
|
||||
taskMode: TaskBoundaryMode;
|
||||
shouldUseCanvasContext: boolean;
|
||||
shouldUseHistoryImages: boolean;
|
||||
shouldUseWorkflowMemory: boolean;
|
||||
shouldAllowExecution: boolean;
|
||||
confidence: number;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
function normalizeCompactText(text: string) {
|
||||
return String(text || "").toLowerCase().replace(/\s+/g, "").trim();
|
||||
}
|
||||
|
||||
export function isPureGreetingTurnRequest(text: string): boolean {
|
||||
const normalized = normalizeCompactText(text);
|
||||
if (!normalized) return false;
|
||||
|
||||
return /^(你好|您好|hello|hi|嗨|哈喽|在吗|早上好|中午好|下午好|晚上好|深夜好|谢谢|感谢|收到|好的|ok|okay|明白了|了解了)$/.test(
|
||||
normalized,
|
||||
);
|
||||
}
|
||||
|
||||
function isLikelyVideoExecutionRequest(text: string): boolean {
|
||||
const normalized = normalizeCompactText(text);
|
||||
if (!normalized) return false;
|
||||
|
||||
return /图生视频|文生视频|视频生成|生成视频|做视频|做成视频|开始生成视频|让.*动起来|变成视频|视频/.test(
|
||||
normalized,
|
||||
);
|
||||
}
|
||||
|
||||
function isLikelyImageExecutionRequest(text: string): boolean {
|
||||
const normalized = normalizeCompactText(text);
|
||||
if (!normalized) return false;
|
||||
|
||||
return /生图|生成图片|出图|画一张|帮我画|做图|做一张图|重新生成|再来一张|执行生成|开始生成|生成出来/.test(
|
||||
normalized,
|
||||
);
|
||||
}
|
||||
|
||||
function isLikelyAnalysisOnlyRequest(text: string, hasImageContext: boolean): boolean {
|
||||
if (!hasImageContext) return false;
|
||||
const normalized = normalizeCompactText(text);
|
||||
if (!normalized) return false;
|
||||
|
||||
return /分析|看图|看看|识别|拆解|提炼|整理|归纳|总结|描述|解释|判断|帮我看看|帮我分析|帮我判断|是什么|适合吗|怎么样|评价/.test(
|
||||
normalized,
|
||||
);
|
||||
}
|
||||
|
||||
function resolveStoryboardTaskMode(params: {
|
||||
text: string;
|
||||
hasPendingGridConfirmation: boolean;
|
||||
hasActiveStoryboardTask: boolean;
|
||||
}): TaskBoundaryMode {
|
||||
if (!params.hasPendingGridConfirmation && !params.hasActiveStoryboardTask) {
|
||||
return "continue";
|
||||
}
|
||||
|
||||
const intent = detectStoryboardCommandIntent(params.text);
|
||||
switch (intent) {
|
||||
case "confirm":
|
||||
return "confirm";
|
||||
case "revise":
|
||||
return "revise";
|
||||
case "cancel":
|
||||
return "cancel";
|
||||
case "unclear":
|
||||
default:
|
||||
return "continue";
|
||||
}
|
||||
}
|
||||
|
||||
export function classifyTaskBoundary(params: {
|
||||
text: string;
|
||||
actionResolution: ConversationActionResolution;
|
||||
hasCurrentImageInput: boolean;
|
||||
hasCanvasSelection: boolean;
|
||||
pendingGridConfirmation?: PendingGridConfirmation | null;
|
||||
hasActiveStoryboardTask: boolean;
|
||||
}) {
|
||||
const hasImageContext = params.hasCurrentImageInput || params.hasCanvasSelection;
|
||||
|
||||
if (isPureGreetingTurnRequest(params.text) && !params.hasCurrentImageInput) {
|
||||
return {
|
||||
taskType: "greeting",
|
||||
taskMode: "new",
|
||||
shouldUseCanvasContext: false,
|
||||
shouldUseHistoryImages: false,
|
||||
shouldUseWorkflowMemory: false,
|
||||
shouldAllowExecution: false,
|
||||
confidence: 0.99,
|
||||
reason: "pure_greeting_turn",
|
||||
} satisfies TaskBoundaryDecision;
|
||||
}
|
||||
|
||||
if (isExplicitPromptOptimizationRequest(params.text)) {
|
||||
return {
|
||||
taskType: "prompt_only",
|
||||
taskMode: "new",
|
||||
shouldUseCanvasContext: !params.hasCurrentImageInput && params.hasCanvasSelection,
|
||||
shouldUseHistoryImages: !params.hasCurrentImageInput && !params.hasCanvasSelection,
|
||||
shouldUseWorkflowMemory: false,
|
||||
shouldAllowExecution: false,
|
||||
confidence: 0.97,
|
||||
reason: "explicit_prompt_only_request",
|
||||
} satisfies TaskBoundaryDecision;
|
||||
}
|
||||
|
||||
switch (params.actionResolution.kind) {
|
||||
case "grid_confirmation_followup":
|
||||
case "active_storyboard_followup":
|
||||
return {
|
||||
taskType: "workflow_followup",
|
||||
taskMode: resolveStoryboardTaskMode({
|
||||
text: params.text,
|
||||
hasPendingGridConfirmation: Boolean(params.pendingGridConfirmation),
|
||||
hasActiveStoryboardTask: params.hasActiveStoryboardTask,
|
||||
}),
|
||||
shouldUseCanvasContext: params.hasCanvasSelection,
|
||||
shouldUseHistoryImages: !params.hasCurrentImageInput,
|
||||
shouldUseWorkflowMemory: true,
|
||||
shouldAllowExecution: true,
|
||||
confidence: 0.95,
|
||||
reason: params.actionResolution.kind,
|
||||
} satisfies TaskBoundaryDecision;
|
||||
case "image_edit_followup":
|
||||
case "image_generation_followup":
|
||||
return {
|
||||
taskType: "workflow_followup",
|
||||
taskMode: "continue",
|
||||
shouldUseCanvasContext: params.hasCanvasSelection,
|
||||
shouldUseHistoryImages: !params.hasCurrentImageInput,
|
||||
shouldUseWorkflowMemory: true,
|
||||
shouldAllowExecution: true,
|
||||
confidence: 0.92,
|
||||
reason: params.actionResolution.kind,
|
||||
} satisfies TaskBoundaryDecision;
|
||||
case "prompt_carry_forward":
|
||||
return {
|
||||
taskType: "execution_image",
|
||||
taskMode: "continue",
|
||||
shouldUseCanvasContext: params.hasCanvasSelection,
|
||||
shouldUseHistoryImages: !params.hasCurrentImageInput,
|
||||
shouldUseWorkflowMemory: true,
|
||||
shouldAllowExecution: true,
|
||||
confidence: 0.93,
|
||||
reason: "prompt_carry_forward_execution",
|
||||
} satisfies TaskBoundaryDecision;
|
||||
case "default":
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (isLikelyVideoExecutionRequest(params.text)) {
|
||||
return {
|
||||
taskType: "execution_video",
|
||||
taskMode: "new",
|
||||
shouldUseCanvasContext: params.hasCanvasSelection,
|
||||
shouldUseHistoryImages: false,
|
||||
shouldUseWorkflowMemory: false,
|
||||
shouldAllowExecution: true,
|
||||
confidence: 0.88,
|
||||
reason: "explicit_video_execution_request",
|
||||
} satisfies TaskBoundaryDecision;
|
||||
}
|
||||
|
||||
if (isLikelyImageExecutionRequest(params.text)) {
|
||||
return {
|
||||
taskType: "execution_image",
|
||||
taskMode: "new",
|
||||
shouldUseCanvasContext: params.hasCanvasSelection,
|
||||
shouldUseHistoryImages: false,
|
||||
shouldUseWorkflowMemory: false,
|
||||
shouldAllowExecution: true,
|
||||
confidence: 0.87,
|
||||
reason: "explicit_image_execution_request",
|
||||
} satisfies TaskBoundaryDecision;
|
||||
}
|
||||
|
||||
if (isLikelyAnalysisOnlyRequest(params.text, hasImageContext)) {
|
||||
return {
|
||||
taskType: "analysis_only",
|
||||
taskMode: "new",
|
||||
shouldUseCanvasContext: params.hasCanvasSelection,
|
||||
shouldUseHistoryImages: !params.hasCurrentImageInput && !params.hasCanvasSelection,
|
||||
shouldUseWorkflowMemory: false,
|
||||
shouldAllowExecution: false,
|
||||
confidence: 0.9,
|
||||
reason: "analysis_only_request",
|
||||
} satisfies TaskBoundaryDecision;
|
||||
}
|
||||
|
||||
return {
|
||||
taskType: "chat",
|
||||
taskMode: "new",
|
||||
shouldUseCanvasContext: false,
|
||||
shouldUseHistoryImages: false,
|
||||
shouldUseWorkflowMemory: false,
|
||||
shouldAllowExecution: false,
|
||||
confidence: 0.7,
|
||||
reason: "default_chat_turn",
|
||||
} satisfies TaskBoundaryDecision;
|
||||
}
|
||||
169
lib/ai/runtime/task-boundary-context-pruner.ts
Normal file
169
lib/ai/runtime/task-boundary-context-pruner.ts
Normal file
@ -0,0 +1,169 @@
|
||||
import "server-only";
|
||||
|
||||
import type { DBMessage } from "@/lib/db/schema";
|
||||
import type {
|
||||
CanvasImageNodeContext,
|
||||
ContextPayload,
|
||||
WorkingStateImageMemory,
|
||||
} from "@/lib/ai/context-memory/types";
|
||||
import type { TaskBoundaryDecision } from "@/lib/ai/runtime/task-boundary-classifier";
|
||||
|
||||
function buildSelectedCanvasSubset(contextPayload?: ContextPayload) {
|
||||
const selectedNodeIds = new Set(contextPayload?.selectedNodeIds || []);
|
||||
const selectedAssetIds = new Set(contextPayload?.selectedAssetIds || []);
|
||||
const selectedNodes = (contextPayload?.canvasImageNodes || []).filter((node) => {
|
||||
if (!node) return false;
|
||||
if (node.isSelected) return true;
|
||||
if (node.nodeId && selectedNodeIds.has(node.nodeId)) return true;
|
||||
if (node.assetId && selectedAssetIds.has(node.assetId)) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
return {
|
||||
selectedNodes,
|
||||
selectedNodeIds: selectedNodes.map((node) => node.nodeId).filter(Boolean),
|
||||
selectedAssetIds: selectedNodes.map((node) => node.assetId).filter(Boolean) as string[],
|
||||
};
|
||||
}
|
||||
|
||||
function buildSelectedCanvasSummary(selectedNodes: CanvasImageNodeContext[]) {
|
||||
if (selectedNodes.length === 0) return undefined;
|
||||
const previewLines = selectedNodes.slice(0, 3).map((node, index) => {
|
||||
const title = String(node.title || node.prompt || node.nodeId || `图片${index + 1}`).trim();
|
||||
return `选中图片${index + 1}: ${title}`;
|
||||
});
|
||||
return [`用户已选中 ${selectedNodes.length} 张图片,优先处理这些`, ...previewLines].join("\n");
|
||||
}
|
||||
|
||||
export function pruneTaskBoundaryContext(params: {
|
||||
decision: TaskBoundaryDecision;
|
||||
contextPayload?: ContextPayload;
|
||||
autoContextText?: string;
|
||||
workingState?: WorkingStateImageMemory | null;
|
||||
hasCurrentImageInput: boolean;
|
||||
}) {
|
||||
if (!params.contextPayload && !params.autoContextText) {
|
||||
return {
|
||||
contextPayload: params.contextPayload,
|
||||
autoContextText: params.autoContextText,
|
||||
workingState: params.decision.shouldUseWorkflowMemory ? params.workingState || null : null,
|
||||
};
|
||||
}
|
||||
|
||||
const selectedSubset = buildSelectedCanvasSubset(params.contextPayload);
|
||||
const shouldPreferSelectedOnly =
|
||||
selectedSubset.selectedNodes.length > 0 &&
|
||||
(params.decision.taskType === "prompt_only" ||
|
||||
params.decision.taskType === "analysis_only" ||
|
||||
params.decision.taskType === "execution_image" ||
|
||||
params.decision.taskType === "execution_video" ||
|
||||
params.hasCurrentImageInput);
|
||||
|
||||
let nextContextPayload = params.contextPayload;
|
||||
let nextAutoContextText = params.autoContextText;
|
||||
|
||||
if (!params.decision.shouldUseCanvasContext) {
|
||||
nextContextPayload = params.contextPayload
|
||||
? {
|
||||
source: params.contextPayload.source,
|
||||
projectId: params.contextPayload.projectId,
|
||||
focusPolicy: params.contextPayload.focusPolicy,
|
||||
selectedNodeIds: [],
|
||||
selectedAssetIds: [],
|
||||
canvasImageNodes: [],
|
||||
}
|
||||
: undefined;
|
||||
nextAutoContextText = undefined;
|
||||
} else if (shouldPreferSelectedOnly) {
|
||||
nextContextPayload = params.contextPayload
|
||||
? {
|
||||
...params.contextPayload,
|
||||
focusPolicy: "selected",
|
||||
selectedNodeIds: selectedSubset.selectedNodeIds,
|
||||
selectedAssetIds: selectedSubset.selectedAssetIds,
|
||||
canvasImageNodes: selectedSubset.selectedNodes,
|
||||
}
|
||||
: undefined;
|
||||
nextAutoContextText = buildSelectedCanvasSummary(selectedSubset.selectedNodes);
|
||||
}
|
||||
|
||||
return {
|
||||
contextPayload: nextContextPayload,
|
||||
autoContextText: nextAutoContextText,
|
||||
workingState: params.decision.shouldUseWorkflowMemory ? params.workingState || null : null,
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeHistoryText(text: string) {
|
||||
return String(text || "")
|
||||
.replace(/!\[[^\]]*]\([^)]+\)/g, "")
|
||||
.replace(/<video[^>]*>[\s\S]*?<\/video>/gi, "")
|
||||
.replace(/<video[^>]+src="[^"]+"[^>]*>/gi, "")
|
||||
.replace(/<!--[\s\S]*?-->/g, "")
|
||||
.replace(/◈\s*[^\n]+\n*/g, "")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function toTextOnlyHistoryMessages(messages: DBMessage[], maxMessages: number): DBMessage[] {
|
||||
const sliced = messages.slice(-Math.max(1, maxMessages));
|
||||
const result: DBMessage[] = [];
|
||||
|
||||
for (const message of sliced) {
|
||||
const parts = Array.isArray(message.parts) ? (message.parts as Array<Record<string, unknown>>) : [];
|
||||
const textParts = parts
|
||||
.filter((part) => part?.type === "text" && typeof part?.text === "string")
|
||||
.map((part) => ({
|
||||
type: "text",
|
||||
text: sanitizeHistoryText(String(part.text || "")),
|
||||
}))
|
||||
.filter((part) => part.text);
|
||||
|
||||
if (textParts.length === 0) continue;
|
||||
|
||||
result.push({
|
||||
...message,
|
||||
parts: textParts,
|
||||
attachments: [],
|
||||
} satisfies DBMessage);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function pruneTaskBoundaryHistoryMessages(params: {
|
||||
decision: TaskBoundaryDecision;
|
||||
messages: DBMessage[];
|
||||
purpose: "media" | "model";
|
||||
hasCurrentImageInput: boolean;
|
||||
}): DBMessage[] {
|
||||
const messages = Array.isArray(params.messages) ? params.messages : [];
|
||||
if (messages.length === 0) return [] as DBMessage[];
|
||||
|
||||
switch (params.decision.taskType) {
|
||||
case "greeting":
|
||||
return [];
|
||||
case "prompt_only":
|
||||
return params.purpose === "media"
|
||||
? []
|
||||
: toTextOnlyHistoryMessages(messages, params.hasCurrentImageInput ? 2 : 4);
|
||||
case "analysis_only":
|
||||
return params.purpose === "media"
|
||||
? []
|
||||
: toTextOnlyHistoryMessages(messages, 4);
|
||||
case "chat":
|
||||
return params.purpose === "media"
|
||||
? []
|
||||
: toTextOnlyHistoryMessages(messages, 6);
|
||||
case "execution_image":
|
||||
case "execution_video":
|
||||
if (params.decision.shouldUseHistoryImages) {
|
||||
return messages.slice(-8);
|
||||
}
|
||||
return params.purpose === "media" ? [] : toTextOnlyHistoryMessages(messages, 4);
|
||||
case "workflow_followup":
|
||||
return messages.slice(-(params.purpose === "media" ? 8 : 12));
|
||||
default:
|
||||
return params.purpose === "media" ? [] : toTextOnlyHistoryMessages(messages, 4);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user