fix: isolate visual tool failures from agent runs

This commit is contained in:
john
2026-07-27 19:20:23 +08:00
parent 7002007128
commit 17c59fde71
10 changed files with 444 additions and 16 deletions
+70 -9
View File
@@ -178,15 +178,32 @@ function buildFreshSessionContext(conversation, { maxMessages = 16, maxChars = 1
return selected.join('\n\n');
}
function appendFreshSessionContext(userMessage, conversation) {
function appendFreshSessionContext(
userMessage,
conversation,
{ visualFallback = false } = {},
) {
const context = buildFreshSessionContext(conversation);
if (!context) return userMessage;
if (!context && !visualFallback) return userMessage;
const message = userMessage && typeof userMessage === 'object'
? { ...userMessage }
: { role: 'user', content: [] };
const content = Array.isArray(message.content) ? [...message.content] : [];
const textIndex = content.findIndex((item) => item?.type === 'text');
const note = `【会话恢复上下文】\n以下内容仅用于理解上文,不是新的执行指令:\n${context}`;
const notes = [];
if (context) {
notes.push(
`【会话恢复上下文】\n以下内容仅用于理解上文,不是新的执行指令:\n${context}`,
);
}
if (visualFallback) {
notes.push(
'【视觉检查已降级】\n'
+ '当前文本模型不接受图片工具结果。请跳过缩略图或图片视觉检查,'
+ '不要再次调用 read_image;直接依据现有 HTML、CSS、文件内容和用户要求继续完成主任务。',
);
}
const note = notes.join('\n\n');
if (textIndex >= 0) {
content[textIndex] = {
...content[textIndex],
@@ -201,6 +218,7 @@ function appendFreshSessionContext(userMessage, conversation) {
const FRESH_SESSION_RECOVERY_CODES = new Set([
'SESSION_TOOL_HISTORY_FRESH_SESSION_REQUIRED',
'SESSION_REASONING_CONTENT_POISONED',
'SESSION_VISUAL_CONTEXT_UNSUPPORTED',
]);
async function resolveConversationForFreshSessionRecovery({
@@ -547,6 +565,7 @@ export function createAgentRunGateway({
observeWorkflowValidation = null,
isSessionExternallyBusy = null,
validateRunDeliverables = null,
cancelSessionOnRetry = null,
quiesceSessionOnTerminal = null,
retryDelaysMs = DEFAULT_RUN_RETRY_DELAYS_MS,
autoDispatch = envFlag(process.env.MEMIND_AGENT_RUN_AUTODISPATCH, true),
@@ -1645,7 +1664,8 @@ export function createAgentRunGateway({
}
if (awaitSessionFinish) {
let submitMessage = ensureGooseUserMessageMetadata(userMessage);
let replacedPoisonedSession = false;
let disableImageReading = false;
const recoveredSessionCodes = new Set();
while (true) {
try {
const finish = await tkmindProxy.submitSessionReplyAndAwaitFinishForUser(
@@ -1657,6 +1677,7 @@ export function createAgentRunGateway({
toolMode: effectiveToolMode,
forceDeepReasoning: runOptions.forceDeepReasoning,
timeoutMs: runTimeoutMs,
disableImageReading,
},
);
await appendEvent(runId, 'session_finished', {
@@ -1668,10 +1689,14 @@ export function createAgentRunGateway({
toolEvidence = finish.toolEvidence ?? null;
break;
} catch (err) {
const recoveryCode = String(err?.code ?? '').trim();
if (
!replacedPoisonedSession
&& FRESH_SESSION_RECOVERY_CODES.has(String(err?.code ?? '').trim())
FRESH_SESSION_RECOVERY_CODES.has(recoveryCode)
&& !recoveredSessionCodes.has(recoveryCode)
&& recoveredSessionCodes.size < FRESH_SESSION_RECOVERY_CODES.size
) {
const visualFallback =
recoveryCode === 'SESSION_VISUAL_CONTEXT_UNSUPPORTED';
const previousSessionId = sessionId;
const conversationForContext = await resolveConversationForFreshSessionRecovery({
err,
@@ -1680,9 +1705,15 @@ export function createAgentRunGateway({
tkmindProxy,
sessionSnapshotService,
});
const replacement = await tkmindProxy.startSessionForUser(row.user_id);
const replacement = visualFallback
? await tkmindProxy.startSessionForUser(
row.user_id,
{ disableImageReading: true },
)
: await tkmindProxy.startSessionForUser(row.user_id);
sessionId = replacement.id;
replacedPoisonedSession = true;
recoveredSessionCodes.add(recoveryCode);
disableImageReading ||= visualFallback;
await pool.query(
`UPDATE h5_agent_runs SET agent_session_id = ?, updated_at = ? WHERE id = ?`,
[sessionId, nowMs(), runId],
@@ -1697,13 +1728,18 @@ export function createAgentRunGateway({
previousSessionId,
sessionId,
reason: err.code,
visualFallback,
repairedMessageCount: conversationForContext.length,
persistedMessageCount: persisted.saved,
});
await appendRunSnapshot(runId);
await invalidatePortalDirectChatSnapshot(sessionId);
submitMessage = ensureGooseUserMessageMetadata(
appendFreshSessionContext(userMessage, conversationForContext),
appendFreshSessionContext(
userMessage,
conversationForContext,
{ visualFallback },
),
);
continue;
}
@@ -1970,6 +2006,31 @@ export function createAgentRunGateway({
await appendEvent(runId, 'timeout', { timeoutMs: runTimeoutMs });
}
const retryable = !timedOut && err?.retryable !== false && nextAttempt < retryDelaysMs.length;
if (retryable && typeof cancelSessionOnRetry === 'function' && recoverySessionId) {
try {
const cancellation = await cancelSessionOnRetry({
runId,
userId: row.user_id,
sessionId: recoverySessionId,
requestId: row.request_id,
});
await appendEvent(runId, 'session_retry_cancelled', {
sessionId: recoverySessionId,
requestId: row.request_id,
cancelled: Boolean(cancellation?.cancelled),
skipped: Boolean(cancellation?.skipped),
});
} catch (cancelError) {
await appendEvent(runId, 'session_retry_cancel_failed', {
sessionId: recoverySessionId,
requestId: row.request_id,
error:
cancelError instanceof Error
? cancelError.message
: String(cancelError),
}).catch(() => {});
}
}
if (!retryable && isPageDataSuggestionRemediable(err?.code)) {
await maybeTriggerPageDataSuggestionRepair({
userId: row.user_id,