fix: isolate visual tool failures from agent runs
This commit is contained in:
@@ -1246,6 +1246,103 @@ test('agent run replaces reasoning-poisoned Goose session and retries with visib
|
||||
assert.equal(replacedData?.reason, 'SESSION_REASONING_CONTENT_POISONED');
|
||||
});
|
||||
|
||||
test('agent run degrades visual inspection after an earlier session-history recovery', async () => {
|
||||
const pool = createFakePool();
|
||||
const submitted = [];
|
||||
const started = [];
|
||||
const replacementIds = ['session-tool-clean', 'session-visual-fallback'];
|
||||
const priorConversation = [
|
||||
{
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '帮我写一首诗,做成页面' }],
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: '页面已经生成。' }],
|
||||
},
|
||||
];
|
||||
const gateway = createAgentRunGateway({
|
||||
pool,
|
||||
userAuth: {},
|
||||
tkmindProxy: {
|
||||
async startSessionForUser(_userId, options) {
|
||||
started.push(options ?? null);
|
||||
return { id: replacementIds.shift() };
|
||||
},
|
||||
async fetchSessionConversationForUser() {
|
||||
return priorConversation;
|
||||
},
|
||||
async submitSessionReplyAndAwaitFinishForUser(
|
||||
_userId,
|
||||
sessionId,
|
||||
_requestId,
|
||||
userMessage,
|
||||
options,
|
||||
) {
|
||||
submitted.push({ sessionId, userMessage, options });
|
||||
if (sessionId === 'session-poisoned') {
|
||||
const error = new Error('tool history requires fresh session');
|
||||
error.code = 'SESSION_TOOL_HISTORY_FRESH_SESSION_REQUIRED';
|
||||
error.repairedConversation = priorConversation;
|
||||
throw error;
|
||||
}
|
||||
if (sessionId === 'session-tool-clean') {
|
||||
const error = new Error(
|
||||
'messages[8]: unknown variant `image_url`, expected `text`',
|
||||
);
|
||||
error.code = 'SESSION_VISUAL_CONTEXT_UNSUPPORTED';
|
||||
error.retryable = false;
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
finishEvent: { type: 'Finish' },
|
||||
toolEvidence: { calls: ['sandbox-fs__edit_file'] },
|
||||
};
|
||||
},
|
||||
},
|
||||
retryDelaysMs: [],
|
||||
});
|
||||
|
||||
const run = await gateway.createRun('user-1', {
|
||||
sessionId: 'session-poisoned',
|
||||
requestId: 'req-visual-fallback',
|
||||
userMessage: {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '页面再精美一点' }],
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => pool.runs.get(run.id)?.status === 'succeeded');
|
||||
assert.deepEqual(submitted.map((item) => item.sessionId), [
|
||||
'session-poisoned',
|
||||
'session-tool-clean',
|
||||
'session-visual-fallback',
|
||||
]);
|
||||
assert.deepEqual(started, [
|
||||
null,
|
||||
{ disableImageReading: true },
|
||||
]);
|
||||
assert.equal(
|
||||
submitted.at(-1).options.disableImageReading,
|
||||
true,
|
||||
);
|
||||
assert.match(
|
||||
submitted.at(-1).userMessage.content[0].text,
|
||||
/视觉检查已降级/,
|
||||
);
|
||||
assert.match(
|
||||
submitted.at(-1).userMessage.content[0].text,
|
||||
/不要再次调用 read_image/,
|
||||
);
|
||||
assert.equal(
|
||||
pool.events.filter(
|
||||
(event) => event.eventType === 'poisoned_session_replaced',
|
||||
).length,
|
||||
2,
|
||||
);
|
||||
});
|
||||
|
||||
test('Page Data run fails closed when Finish arrives without a generated page', async () => {
|
||||
const pool = createFakePool();
|
||||
const repairSubmits = [];
|
||||
@@ -2756,6 +2853,50 @@ test('agent run retries transient failures and then becomes terminal', async ()
|
||||
assert.match(pool.runs.get(run.id).error_message, /upstream unavailable/);
|
||||
});
|
||||
|
||||
test('agent run cancels an active upstream request before retrying', async () => {
|
||||
const pool = createFakePool();
|
||||
const calls = [];
|
||||
let submissions = 0;
|
||||
const gateway = createAgentRunGateway({
|
||||
pool,
|
||||
userAuth: {},
|
||||
tkmindProxy: {
|
||||
async startSessionForUser() {
|
||||
return { id: 'session-retry-cancel' };
|
||||
},
|
||||
async submitSessionReplyForUser() {
|
||||
submissions += 1;
|
||||
calls.push(`submit-${submissions}`);
|
||||
if (submissions === 1) throw new Error('upstream unavailable');
|
||||
},
|
||||
},
|
||||
async cancelSessionOnRetry(input) {
|
||||
calls.push('cancel');
|
||||
assert.equal(input.sessionId, 'session-retry-cancel');
|
||||
assert.equal(input.requestId, 'req-retry-cancel');
|
||||
return { cancelled: true, skipped: false };
|
||||
},
|
||||
retryDelaysMs: [0, 0],
|
||||
});
|
||||
|
||||
const run = await gateway.createRun('user-1', {
|
||||
requestId: 'req-retry-cancel',
|
||||
userMessage: {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '继续任务' }],
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => pool.runs.get(run.id)?.status === 'succeeded');
|
||||
assert.deepEqual(calls, ['submit-1', 'cancel', 'submit-2']);
|
||||
assert.equal(
|
||||
pool.events.some(
|
||||
(event) => event.eventType === 'session_retry_cancelled',
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('agent run queue limits concurrent execution', async () => {
|
||||
const pool = createFakePool();
|
||||
let active = 0;
|
||||
|
||||
Reference in New Issue
Block a user