Skip to content

Commit bce6029

Browse files
Handle merging multiple tool results into a single assistant message (#1829)
<!-- Ensure the title clearly reflects what was changed. Provide a clear and concise description of the changes made. The PR should only contain the changes related to the issue, and no other unrelated changes. --> Fixes OPS-3386
1 parent 3eca5cf commit bce6029

File tree

2 files changed

+80
-21
lines changed

2 files changed

+80
-21
lines changed

packages/server/api/src/app/ai/chat/utils.ts

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,11 @@ export function mergeToolResultsIntoMessages(
1414
},
1515
): Array<Omit<UIMessage, 'id'>> {
1616
const uiMessages: Array<Omit<UIMessage, 'id'>> = [];
17-
const toolResultsToMerge: Array<{
18-
toolResult: ToolResultPart;
19-
messageIndex: number;
20-
}> = [];
21-
22-
for (let i = 0; i < messages.length; i++) {
23-
const msg = messages[i];
17+
const toolResultsToMerge: ToolResultPart[] = [];
2418

19+
for (const msg of messages) {
2520
if (isToolMessage(msg)) {
26-
if (Array.isArray(msg.content) && msg.content.length > 0) {
27-
const toolResult = msg.content[0];
28-
if (
29-
toolResult &&
30-
typeof toolResult === 'object' &&
31-
'toolCallId' in toolResult
32-
) {
33-
toolResultsToMerge.push({
34-
toolResult: toolResult as ToolResultPart,
35-
messageIndex: i,
36-
});
37-
}
38-
}
21+
toolResultsToMerge.push(...extractToolResults(msg));
3922
continue;
4023
}
4124

@@ -45,13 +28,29 @@ export function mergeToolResultsIntoMessages(
4528
}
4629
}
4730

48-
for (const { toolResult } of toolResultsToMerge) {
31+
for (const toolResult of toolResultsToMerge) {
4932
mergeToolResultIntoUIMessage(toolResult, uiMessages);
5033
}
5134

5235
return uiMessages;
5336
}
5437

38+
function extractToolResults(msg: ModelMessage): ToolResultPart[] {
39+
const toolResults: ToolResultPart[] = [];
40+
if (Array.isArray(msg.content)) {
41+
for (const part of msg.content) {
42+
if (isToolResultPart(part)) {
43+
toolResults.push(part);
44+
}
45+
}
46+
}
47+
return toolResults;
48+
}
49+
50+
function isToolResultPart(part: any): part is ToolResultPart {
51+
return part && typeof part === 'object' && 'toolCallId' in part;
52+
}
53+
5554
/**
5655
* Sanitize chat history for secondary tasks like naming/summarization.
5756
* - keeps only 'user' and 'assistant' roles

packages/server/api/test/unit/ai/utils.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,66 @@ describe('mergeToolResultsIntoMessages', () => {
802802
input: { userId: '123' },
803803
});
804804
});
805+
806+
it('should handle multiple tool results in a single tool message', () => {
807+
const messages: ModelMessage[] = [
808+
{
809+
role: 'assistant',
810+
content: [
811+
{
812+
type: 'tool-call',
813+
toolCallId: 'call_1',
814+
toolName: 'tool_1',
815+
input: { arg: 1 },
816+
},
817+
{
818+
type: 'tool-call',
819+
toolCallId: 'call_2',
820+
toolName: 'tool_2',
821+
input: { arg: 2 },
822+
},
823+
],
824+
},
825+
{
826+
role: 'tool',
827+
content: [
828+
{
829+
type: 'tool-result',
830+
toolCallId: 'call_1',
831+
toolName: 'tool_1',
832+
output: {
833+
type: 'text',
834+
value: 'result_1',
835+
} as any,
836+
},
837+
{
838+
type: 'tool-result',
839+
toolCallId: 'call_2',
840+
toolName: 'tool_2',
841+
output: {
842+
type: 'text',
843+
value: 'result_2',
844+
} as any,
845+
},
846+
],
847+
},
848+
];
849+
850+
const result = mergeToolResultsIntoMessages(messages);
851+
expect(result).toHaveLength(1);
852+
expect(result[0].parts).toHaveLength(2);
853+
854+
expect(result[0].parts[0]).toMatchObject({
855+
toolCallId: 'call_1',
856+
state: 'output-available',
857+
output: 'result_1',
858+
});
859+
expect(result[0].parts[1]).toMatchObject({
860+
toolCallId: 'call_2',
861+
state: 'output-available',
862+
output: 'result_2',
863+
});
864+
});
805865
});
806866
});
807867

0 commit comments

Comments
 (0)