@@ -201,6 +201,208 @@ describe('Message Converters', () => {
201201 expect ( result [ 0 ] ?. thinking ) . toEqual ( [ { content : 'Let me think...' } ] )
202202 } )
203203
204+ it ( 'should preserve thinking order around provider-executed tool calls' , ( ) => {
205+ const providerToolMetadata = {
206+ providerExecuted : true ,
207+ anthropic : {
208+ serverToolType : 'web_search' ,
209+ resultBlockType : 'web_search_tool_result' ,
210+ result : [
211+ {
212+ type : 'web_search_result' ,
213+ title : 'Example result' ,
214+ url : 'https://example.com' ,
215+ encrypted_content : 'opaque-provider-payload' ,
216+ } ,
217+ ] ,
218+ } ,
219+ }
220+ const uiMessage : UIMessage = {
221+ id : 'assistant-message' ,
222+ role : 'assistant' ,
223+ parts : [
224+ {
225+ type : 'thinking' ,
226+ content : 'First signed thinking block' ,
227+ signature : 'signature-1' ,
228+ } ,
229+ {
230+ type : 'tool-call' ,
231+ id : 'srvtoolu_search' ,
232+ name : 'web_search' ,
233+ arguments : '{"query":"top AI companies"}' ,
234+ state : 'input-complete' ,
235+ metadata : providerToolMetadata ,
236+ } ,
237+ {
238+ type : 'thinking' ,
239+ content : 'Second signed thinking block' ,
240+ signature : 'signature-2' ,
241+ } ,
242+ {
243+ type : 'tool-call' ,
244+ id : 'toolu_create_block' ,
245+ name : 'createBlock' ,
246+ arguments : '{"block":{"type":"entity-grid"}}' ,
247+ state : 'complete' ,
248+ output : { ok : true } ,
249+ } ,
250+ {
251+ type : 'tool-result' ,
252+ toolCallId : 'toolu_create_block' ,
253+ content : '{"ok":true}' ,
254+ state : 'complete' ,
255+ } ,
256+ ] ,
257+ }
258+
259+ const result = convertMessagesToModelMessages ( [ uiMessage ] )
260+
261+ expect ( result ) . toEqual ( [
262+ {
263+ role : 'assistant' ,
264+ content : null ,
265+ toolCalls : [
266+ {
267+ id : 'srvtoolu_search' ,
268+ type : 'function' ,
269+ function : {
270+ name : 'web_search' ,
271+ arguments : '{"query":"top AI companies"}' ,
272+ } ,
273+ metadata : providerToolMetadata ,
274+ } ,
275+ ] ,
276+ thinking : [
277+ {
278+ content : 'First signed thinking block' ,
279+ signature : 'signature-1' ,
280+ } ,
281+ ] ,
282+ } ,
283+ {
284+ role : 'assistant' ,
285+ content : null ,
286+ toolCalls : [
287+ {
288+ id : 'toolu_create_block' ,
289+ type : 'function' ,
290+ function : {
291+ name : 'createBlock' ,
292+ arguments : '{"block":{"type":"entity-grid"}}' ,
293+ } ,
294+ } ,
295+ ] ,
296+ thinking : [
297+ {
298+ content : 'Second signed thinking block' ,
299+ signature : 'signature-2' ,
300+ } ,
301+ ] ,
302+ } ,
303+ {
304+ role : 'tool' ,
305+ content : '{"ok":true}' ,
306+ toolCallId : 'toolu_create_block' ,
307+ } ,
308+ ] )
309+ } )
310+
311+ it ( 'should keep local tool calls and results in the same assistant turn' , ( ) => {
312+ const uiMessage : UIMessage = {
313+ id : 'assistant-message' ,
314+ role : 'assistant' ,
315+ parts : [
316+ {
317+ type : 'tool-call' ,
318+ id : 'tool-call-a' ,
319+ name : 'toolA' ,
320+ arguments : '{"value":"a"}' ,
321+ state : 'input-complete' ,
322+ } ,
323+ {
324+ type : 'thinking' ,
325+ content : 'Thinking between local tool calls' ,
326+ } ,
327+ {
328+ type : 'tool-call' ,
329+ id : 'tool-call-b' ,
330+ name : 'toolB' ,
331+ arguments : '{"value":"b"}' ,
332+ state : 'input-complete' ,
333+ } ,
334+ {
335+ type : 'tool-result' ,
336+ toolCallId : 'tool-call-a' ,
337+ content : '{"result":"a"}' ,
338+ state : 'complete' ,
339+ } ,
340+ {
341+ type : 'tool-result' ,
342+ toolCallId : 'tool-call-b' ,
343+ content : '{"result":"b"}' ,
344+ state : 'complete' ,
345+ } ,
346+ ] ,
347+ }
348+
349+ const modelMessages = uiMessageToModelMessages ( uiMessage )
350+
351+ expect ( modelMessages ) . toEqual ( [
352+ {
353+ role : 'assistant' ,
354+ content : null ,
355+ toolCalls : [
356+ {
357+ id : 'tool-call-a' ,
358+ type : 'function' ,
359+ function : {
360+ name : 'toolA' ,
361+ arguments : '{"value":"a"}' ,
362+ } ,
363+ } ,
364+ {
365+ id : 'tool-call-b' ,
366+ type : 'function' ,
367+ function : {
368+ name : 'toolB' ,
369+ arguments : '{"value":"b"}' ,
370+ } ,
371+ } ,
372+ ] ,
373+ thinking : [ { content : 'Thinking between local tool calls' } ] ,
374+ } ,
375+ {
376+ role : 'tool' ,
377+ content : '{"result":"a"}' ,
378+ toolCallId : 'tool-call-a' ,
379+ } ,
380+ {
381+ role : 'tool' ,
382+ content : '{"result":"b"}' ,
383+ toolCallId : 'tool-call-b' ,
384+ } ,
385+ ] )
386+
387+ const roundTripped = modelMessagesToUIMessages ( modelMessages )
388+
389+ expect ( roundTripped ) . toHaveLength ( 1 )
390+ expect ( roundTripped [ 0 ] ?. parts ) . toEqual (
391+ expect . arrayContaining ( [
392+ expect . objectContaining ( { type : 'tool-call' , id : 'tool-call-a' } ) ,
393+ expect . objectContaining ( { type : 'tool-call' , id : 'tool-call-b' } ) ,
394+ expect . objectContaining ( {
395+ type : 'tool-result' ,
396+ toolCallId : 'tool-call-a' ,
397+ } ) ,
398+ expect . objectContaining ( {
399+ type : 'tool-result' ,
400+ toolCallId : 'tool-call-b' ,
401+ } ) ,
402+ ] ) ,
403+ )
404+ } )
405+
204406 it ( 'should skip system messages' , ( ) => {
205407 const uiMessage : UIMessage = {
206408 id : 'msg-1' ,
0 commit comments