Skip to content

Commit 4ec345b

Browse files
committed
agentHost: bound pending BYOK continuations
1 parent 59ae436 commit 4ec345b

2 files changed

Lines changed: 60 additions & 0 deletions

File tree

src/vs/platform/agentHost/node/copilot/byokLmProxyService.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export interface IByokLmProxyService {
7272
const PROXY_USER_FACING_NAME = 'ByokLmProxyService';
7373
const VENDOR_PATH_PREFIX = '/v/';
7474
const RESPONSES_SUFFIX = '/responses';
75+
const MAX_PENDING_TOOL_CONTINUATIONS = 256;
7576

7677
type PendingToolCallKind = 'function_call' | 'custom_tool_call';
7778

@@ -347,7 +348,16 @@ export class ByokLmProxyService extends LoopbackProxyServer<ByokLmProxyState> im
347348
}
348349
}
349350
if (calls.size) {
351+
// A session can disappear after receiving a tool call, so keep abandoned
352+
// continuations from growing for the lifetime of the shared proxy.
353+
state.delete(key);
350354
state.set(key, { responseId: result.responseId, calls });
355+
if (state.size > MAX_PENDING_TOOL_CONTINUATIONS) {
356+
const oldestKey = state.keys().next().value;
357+
if (oldestKey !== undefined) {
358+
state.delete(oldestKey);
359+
}
360+
}
351361
} else {
352362
state.delete(key);
353363
}

src/vs/platform/agentHost/test/node/byokLmProxyService.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,56 @@ suite('ByokLmProxyService', () => {
380380
});
381381
});
382382

383+
test('bounds abandoned tool continuations while preserving recent state', async () => {
384+
const captured: IByokLmChatRequest[] = [];
385+
const maximumPendingContinuations = 256;
386+
387+
await withProxy(
388+
async request => {
389+
const index = captured.length;
390+
captured.push(request);
391+
if (index <= maximumPendingContinuations + 1) {
392+
return {
393+
responseId: `resp_${index}`,
394+
output: [{ type: 'function_call', callId: `call_${index}`, name: 'tool', argumentsJson: '{}' }],
395+
};
396+
}
397+
return { output: [{ type: 'message', content: [{ type: 'text', text: 'done' }] }] };
398+
},
399+
async handle => {
400+
const post = async (index: number, input: unknown) => {
401+
const response = await fetch(responsesUrl(handle, 'acme'), {
402+
method: 'POST',
403+
headers: authHeaders(handle, `sess-${index}`),
404+
body: JSON.stringify({ model: 'm', input }),
405+
});
406+
assert.strictEqual(response.status, 200);
407+
await response.text();
408+
};
409+
410+
// Sessions can disappear after receiving a tool call. Fill the proxy,
411+
// refresh its oldest entry, then overflow it with one abandoned session.
412+
for (let index = 0; index < maximumPendingContinuations; index++) {
413+
await post(index, []);
414+
}
415+
await post(0, []);
416+
await post(maximumPendingContinuations, []);
417+
418+
for (const [session, call] of [[1, 1], [0, maximumPendingContinuations]]) {
419+
await post(session, [
420+
{ type: 'function_call', call_id: `call_${call}`, name: 'tool', arguments: '{}' },
421+
{ type: 'function_call_output', call_id: `call_${call}`, output: 'done' },
422+
]);
423+
}
424+
},
425+
);
426+
427+
assert.deepStrictEqual(captured.slice(-2).map(request => request.previousResponseId), [
428+
undefined,
429+
`resp_${maximumPendingContinuations}`,
430+
]);
431+
});
432+
383433
test('decodes a url-encoded vendor path segment', async () => {
384434
let captured: IByokLmChatRequest | undefined;
385435
await withProxy(

0 commit comments

Comments
 (0)