Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,68 @@ describe('a hosted turn the guard stops, or that fails', () => {
expect(posted()).toEqual([HOSTED_RUN_FAILED_LINE]);
});

// 10-25: the recovery round was stopped though every kept result passed the
// check on its own. The rest are set aside for one last round.
test('a stop in the extra round sets aside the web results left and answers in one last round', async () => {
chatReplies = [
toolCalls(fetchCall('c1', CLEAN_URL), fetchCall('c2', LATER_URL)),
toolCalls(fetchCall('c3', STOPPED_URL)),
guardStop,
guardStop,
reply('Here is what I can say without those pages.'),
];

const result = await runAgent(hostedInstall(), mention);

expect(result.status).toBe('succeeded');
expect(chatBodies).toHaveLength(5);
const last = JSON.stringify(chatBodies[4].messages);
expect(last).not.toContain(STOPPED_TEXT);
expect(last).not.toContain(CLEAN_TEXT);
expect(last).not.toContain(LATER_TEXT);
expect(offeredIn(chatBodies[4])).toEqual(['commonly_post_message']);
expect(savedRun.webSetAside).toBe(3);
expect(posted()).toEqual(['Here is what I can say without those pages.']);
});

test('text the model wrote after a web read is cleared too, its tool calls kept', async () => {
const quoting = toolCalls(fetchCall('c3', STOPPED_URL));
quoting.data.choices[0].message.content = `Noting: ${CLEAN_TEXT}`;
chatReplies = [
toolCalls(fetchCall('c1', CLEAN_URL), fetchCall('c2', LATER_URL)),
quoting,
guardStop,
guardStop,
reply('Answered without the pages.'),
];

const result = await runAgent(hostedInstall(), mention);

expect(result.status).toBe('succeeded');
const last = chatBodies[4].messages;
expect(JSON.stringify(last)).not.toContain(CLEAN_TEXT);
// Every tool result still follows the assistant call that asked for it.
const calls = last.filter((m) => m.role === 'assistant').flatMap((m) => (m.tool_calls || []).map((c) => c.id));
last.filter((m) => m.role === 'tool').forEach((m) => expect(calls).toContain(m.tool_call_id));
});

test('a third stop ends the run with the fixed line', async () => {
chatReplies = [
toolCalls(fetchCall('c1', CLEAN_URL)),
toolCalls(fetchCall('c2', STOPPED_URL)),
guardStop,
guardStop,
guardStop,
reply('Never asked.'),
];

const result = await runAgent(hostedInstall(), mention);

expect(result).toEqual(expect.objectContaining({ status: 'failed', errorKind: 'guardrail_blocked' }));
expect(chatBodies).toHaveLength(5);
expect(posted()).toEqual([HOSTED_RUN_FAILED_LINE]);
});

test('a stop with no web read checks nothing and ends with the fixed line', async () => {
chatReplies = [guardStop];

Expand Down
22 changes: 22 additions & 0 deletions backend/services/nativeRuntimeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2072,6 +2072,9 @@ export async function runAgent(
const webResults: Array<{ round: number; index: number }> = [];
const roundEnds: number[] = [];
let webSetAside = false;
// Set when the recovery round was stopped too, and every web result left
// was set aside for one last round.
let webSetAsideAll = false;
// Returns how many results were set aside, and how many web results the
// turn lost in all (those plus the ones in the dropped rounds).
const setAsideStoppedWebResult = async (): Promise<{ setAside: number; removed: number }> => {
Expand Down Expand Up @@ -2187,6 +2190,25 @@ export async function runAgent(
run.webSetAside = removed;
continue;
}
} else if (webSetAside && !webSetAsideAll && !postedViaTool && webResults.length > 0) {
// The recovery round was stopped too, though every result it
// kept passed the check on its own: the guard stops on them
// together. One outside user's research turns failed this way 6
// times in a day (10-25). Set aside the rest of the web results
// and give the same single round once more.
webResults.forEach(({ index }) => {
messages[index] = { ...messages[index], content: JSON.stringify({ ok: false, error: WEB_SET_ASIDE_NOTICE }) };
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
// The model's own text after the first web result may quote it.
// Keep its tool calls, so every tool result still has its call.
const firstWeb = Math.min(...webResults.map(({ index }) => index));
for (let i = firstWeb + 1; i < messages.length; i += 1) {
if (messages[i]?.role === 'assistant' && messages[i].content) messages[i] = { ...messages[i], content: null };
}
run.webSetAside = Number(run.webSetAside || 0) + webResults.length;
webResults.splice(0, webResults.length);
webSetAsideAll = true;
continue;
}
run.status = 'failed';
run.errorKind = 'guardrail_blocked';
Expand Down
Loading