Skip to content

Commit 2dc376c

Browse files
committed
Merge branch 'main'
2 parents 0d1f416 + 72c3aac commit 2dc376c

File tree

4 files changed

+87
-13
lines changed

4 files changed

+87
-13
lines changed

packages/agent/src/agent/gemini-vercel-sdk-adapter/strategies/message.test.ts

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,18 @@ describe('MessageConversionStrategy', () => {
127127
});
128128

129129
// Tool result messages (function responses from user)
130+
// NOTE: Each test includes matching tool call + tool result pairs because
131+
// orphaned tool results (without matching tool_use) are filtered out to
132+
// prevent "unexpected tool_use_id found in tool_result blocks" errors
130133

131134
t(
132135
'tests that function response converts to tool role not user role',
133136
() => {
134137
const contents: Content[] = [
138+
{
139+
role: 'model',
140+
parts: [{ functionCall: { id: 'call_123', name: 'get_weather', args: {} } }],
141+
},
135142
{
136143
role: 'user',
137144
parts: [
@@ -149,14 +156,18 @@ describe('MessageConversionStrategy', () => {
149156
const result = strategy.geminiToVercel(contents);
150157

151158
// CRITICAL: Must be 'tool' role, not 'user'
152-
expect(result[0].role).toBe('tool');
159+
expect(result[1].role).toBe('tool');
153160
},
154161
);
155162

156163
t(
157164
'tests that function response content is array of tool-result parts',
158165
() => {
159166
const contents: Content[] = [
167+
{
168+
role: 'model',
169+
parts: [{ functionCall: { id: 'call_456', name: 'search', args: {} } }],
170+
},
160171
{
161172
role: 'user',
162173
parts: [
@@ -173,8 +184,8 @@ describe('MessageConversionStrategy', () => {
173184

174185
const result = strategy.geminiToVercel(contents);
175186

176-
expect(Array.isArray(result[0].content)).toBe(true);
177-
const content = result[0].content as VercelContentPart[];
187+
expect(Array.isArray(result[1].content)).toBe(true);
188+
const content = result[1].content as VercelContentPart[];
178189
const toolResult = content[0] as VercelToolResultPart;
179190
expect(toolResult.type).toBe('tool-result');
180191
expect(toolResult.toolCallId).toBe('call_456');
@@ -184,6 +195,10 @@ describe('MessageConversionStrategy', () => {
184195

185196
t('tests that function response output contains structured response per v5', () => {
186197
const contents: Content[] = [
198+
{
199+
role: 'model',
200+
parts: [{ functionCall: { id: 'call_789', name: 'get_data', args: {} } }],
201+
},
187202
{
188203
role: 'user',
189204
parts: [
@@ -200,7 +215,7 @@ describe('MessageConversionStrategy', () => {
200215

201216
const result = strategy.geminiToVercel(contents);
202217

203-
const content = result[0].content as VercelContentPart[];
218+
const content = result[1].content as VercelContentPart[];
204219
const toolResult = content[0] as VercelToolResultPart;
205220
// AI SDK v5 uses structured output format
206221
expect(toolResult.output).toEqual({ type: 'json', value: { data: 'test', success: true } });
@@ -210,6 +225,10 @@ describe('MessageConversionStrategy', () => {
210225
'tests that function response with error field uses error output type',
211226
() => {
212227
const contents: Content[] = [
228+
{
229+
role: 'model',
230+
parts: [{ functionCall: { id: 'call_error', name: 'broken_tool', args: {} } }],
231+
},
213232
{
214233
role: 'user',
215234
parts: [
@@ -226,7 +245,7 @@ describe('MessageConversionStrategy', () => {
226245

227246
const result = strategy.geminiToVercel(contents);
228247

229-
const content = result[0].content as VercelContentPart[];
248+
const content = result[1].content as VercelContentPart[];
230249
const toolResult = content[0] as VercelToolResultPart;
231250
// AI SDK v5 uses error-text or error-json for error responses
232251
expect(toolResult.output).toEqual({
@@ -240,6 +259,10 @@ describe('MessageConversionStrategy', () => {
240259
'tests that function response without response field uses empty json output',
241260
() => {
242261
const contents: Content[] = [
262+
{
263+
role: 'model',
264+
parts: [{ functionCall: { id: 'call_no_response', name: 'simple_tool', args: {} } }],
265+
},
243266
{
244267
role: 'user',
245268
parts: [
@@ -255,7 +278,7 @@ describe('MessageConversionStrategy', () => {
255278

256279
const result = strategy.geminiToVercel(contents);
257280

258-
const content = result[0].content as VercelContentPart[];
281+
const content = result[1].content as VercelContentPart[];
259282
const toolResult = content[0] as VercelToolResultPart;
260283
// AI SDK v5 uses structured output format
261284
expect(toolResult.output).toEqual({ type: 'json', value: {} });
@@ -287,6 +310,10 @@ describe('MessageConversionStrategy', () => {
287310

288311
t('tests that function response without name uses unknown', () => {
289312
const contents: Content[] = [
313+
{
314+
role: 'model',
315+
parts: [{ functionCall: { id: 'call_no_name', name: 'some_tool', args: {} } }],
316+
},
290317
{
291318
role: 'user',
292319
parts: [
@@ -302,7 +329,7 @@ describe('MessageConversionStrategy', () => {
302329

303330
const result = strategy.geminiToVercel(contents);
304331

305-
const content = result[0].content as VercelContentPart[];
332+
const content = result[1].content as VercelContentPart[];
306333
const toolResult = content[0] as VercelToolResultPart;
307334
expect(toolResult.toolName).toBe('unknown');
308335
});
@@ -311,6 +338,13 @@ describe('MessageConversionStrategy', () => {
311338
'tests that multiple function responses in one message all convert',
312339
() => {
313340
const contents: Content[] = [
341+
{
342+
role: 'model',
343+
parts: [
344+
{ functionCall: { id: 'call_1', name: 'tool1', args: {} } },
345+
{ functionCall: { id: 'call_2', name: 'tool2', args: {} } },
346+
],
347+
},
314348
{
315349
role: 'user',
316350
parts: [
@@ -334,7 +368,7 @@ describe('MessageConversionStrategy', () => {
334368

335369
const result = strategy.geminiToVercel(contents);
336370

337-
const content = result[0].content as VercelContentPart[];
371+
const content = result[1].content as VercelContentPart[];
338372
expect(content).toHaveLength(2);
339373
const toolResult0 = content[0] as VercelToolResultPart;
340374
const toolResult1 = content[1] as VercelToolResultPart;
@@ -344,6 +378,9 @@ describe('MessageConversionStrategy', () => {
344378
);
345379

346380
// Assistant messages with tool calls
381+
// NOTE: Each test includes matching tool call + tool result pairs because
382+
// orphaned tool calls (without matching tool_result) are filtered out to
383+
// prevent "tool_use ids were found without tool_result blocks" errors
347384

348385
t(
349386
'tests that function call converts to assistant message with tool-call part',
@@ -361,6 +398,10 @@ describe('MessageConversionStrategy', () => {
361398
},
362399
],
363400
},
401+
{
402+
role: 'user',
403+
parts: [{ functionResponse: { id: 'call_abc', name: 'search', response: {} } }],
404+
},
364405
];
365406

366407
const result = strategy.geminiToVercel(contents);
@@ -391,6 +432,10 @@ describe('MessageConversionStrategy', () => {
391432
},
392433
],
393434
},
435+
{
436+
role: 'user',
437+
parts: [{ functionResponse: { id: 'call_def', name: 'get_weather', response: {} } }],
438+
},
394439
];
395440

396441
const result = strategy.geminiToVercel(contents);
@@ -423,6 +468,10 @@ describe('MessageConversionStrategy', () => {
423468
},
424469
],
425470
},
471+
{
472+
role: 'user',
473+
parts: [{ functionResponse: { id: 'call_search', name: 'search', response: {} } }],
474+
},
426475
];
427476

428477
const result = strategy.geminiToVercel(contents);
@@ -473,6 +522,10 @@ describe('MessageConversionStrategy', () => {
473522
},
474523
],
475524
},
525+
{
526+
role: 'user',
527+
parts: [{ functionResponse: { id: 'call_xyz', name: 'unknown', response: {} } }],
528+
},
476529
];
477530

478531
const result = strategy.geminiToVercel(contents);
@@ -495,6 +548,10 @@ describe('MessageConversionStrategy', () => {
495548
},
496549
],
497550
},
551+
{
552+
role: 'user',
553+
parts: [{ functionResponse: { id: 'call_no_args', name: 'simple_tool', response: {} } }],
554+
},
498555
];
499556

500557
const result = strategy.geminiToVercel(contents);
@@ -525,6 +582,13 @@ describe('MessageConversionStrategy', () => {
525582
},
526583
],
527584
},
585+
{
586+
role: 'user',
587+
parts: [
588+
{ functionResponse: { id: 'call_1', name: 'tool1', response: {} } },
589+
{ functionResponse: { id: 'call_2', name: 'tool2', response: {} } },
590+
],
591+
},
528592
];
529593

530594
const result = strategy.geminiToVercel(contents);

packages/agent/src/agent/gemini-vercel-sdk-adapter/strategies/message.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,14 @@ export class MessageConversionStrategy {
3333
const messages: CoreMessage[] = [];
3434
const seenToolResultIds = new Set<string>();
3535

36-
// First pass: collect all tool result IDs to validate tool calls
36+
// First pass: collect all tool call IDs and tool result IDs
37+
const allToolCallIds = new Set<string>();
3738
const allToolResultIds = new Set<string>();
3839
for (const content of contents) {
3940
for (const part of content.parts || []) {
41+
if (isFunctionCallPart(part) && part.functionCall?.id) {
42+
allToolCallIds.add(part.functionCall.id);
43+
}
4044
if (isFunctionResponsePart(part) && part.functionResponse?.id) {
4145
allToolResultIds.add(part.functionResponse.id);
4246
}
@@ -115,12 +119,18 @@ export class MessageConversionStrategy {
115119
// CASE 2: Tool results (user providing tool execution results)
116120
if (functionResponses.length > 0) {
117121

118-
// Filter out duplicate tool results based on ID
122+
// Filter out duplicate tool results AND orphaned tool results (no matching tool_use)
119123
const uniqueResponses = functionResponses.filter((fr) => {
120124
const id = fr.id || '';
125+
// Skip duplicates
121126
if (seenToolResultIds.has(id)) {
122127
return false;
123128
}
129+
// Skip orphaned tool results (no matching tool_use in history)
130+
// This prevents: "unexpected tool_use_id found in tool_result blocks"
131+
if (id && !allToolCallIds.has(id)) {
132+
return false;
133+
}
124134
seenToolResultIds.add(id);
125135
return true;
126136
});

packages/agent/src/http/HttpServer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ export function createHttpServer(config: HttpServerConfig) {
172172
fetch: app.fetch,
173173
port: validatedConfig.port,
174174
hostname: validatedConfig.host,
175-
idleTimeout: 0,
175+
idleTimeout: 0, // Disable idle timeout for long-running LLM streams
176176
});
177177

178178
logger.info('HTTP Agent Server started', {

scripts/build_server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ interface BuildTarget {
2828
const TARGETS: Record<string, BuildTarget> = {
2929
"linux-x64": {
3030
name: "Linux x64",
31-
bunTarget: "bun-linux-x64-modern",
31+
bunTarget: "bun-linux-x64-baseline",
3232
outfile: "dist/server/browseros-server-linux-x64",
3333
},
3434
"linux-arm64": {
@@ -38,7 +38,7 @@ const TARGETS: Record<string, BuildTarget> = {
3838
},
3939
"windows-x64": {
4040
name: "Windows x64",
41-
bunTarget: "bun-windows-x64-modern",
41+
bunTarget: "bun-windows-x64-baseline",
4242
outfile: "dist/server/browseros-server-windows-x64.exe",
4343
},
4444
"darwin-arm64": {

0 commit comments

Comments
 (0)