Skip to content

Commit 5d3b3bb

Browse files
authored
Merge pull request #1087 from objectstack-ai/copilot/fix-sse-tool-result-streaming
2 parents 0ae9b42 + 9a93633 commit 5d3b3bb

File tree

2 files changed

+58
-1
lines changed

2 files changed

+58
-1
lines changed

packages/services/service-ai/src/__tests__/auth-and-toolcalling.test.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -521,16 +521,66 @@ describe('streamChatWithTools', () => {
521521
events.push(event);
522522
}
523523

524-
// Should have tool-call event followed by text-delta + finish (no double call)
524+
// Should have tool-call + tool-result events followed by text-delta + finish
525525
const toolCallEvents = events.filter(e => e.type === 'tool-call');
526526
expect(toolCallEvents).toHaveLength(1);
527527
expect((toolCallEvents[0] as any).toolName).toBe('get_weather');
528528

529+
const toolResultEvents = events.filter(e => e.type === 'tool-result');
530+
expect(toolResultEvents).toHaveLength(1);
531+
expect((toolResultEvents[0] as any).toolCallId).toBe('call_1');
532+
expect((toolResultEvents[0] as any).toolName).toBe('get_weather');
533+
529534
const finishEvent = events.find(e => e.type === 'finish');
530535
expect(finishEvent).toBeDefined();
531536
expect(adapter.chat).toHaveBeenCalledTimes(2);
532537
});
533538

539+
it('should yield tool-result events with tool output', async () => {
540+
const toolCall: ToolCallPart = {
541+
type: 'tool-call',
542+
toolCallId: 'call_weather',
543+
toolName: 'get_weather',
544+
input: { city: 'Paris' },
545+
};
546+
547+
let chatCallIndex = 0;
548+
const adapter: LLMAdapter = {
549+
name: 'mock-stream',
550+
chat: vi.fn(async () => {
551+
chatCallIndex++;
552+
if (chatCallIndex === 1) {
553+
return { content: '', toolCalls: [toolCall] };
554+
}
555+
return { content: 'Paris is 22°C' };
556+
}),
557+
complete: vi.fn(async () => ({ content: '' })),
558+
};
559+
560+
const service = new AIService({ adapter, logger: silentLogger, toolRegistry: registry });
561+
const events: TextStreamPart<ToolSet>[] = [];
562+
for await (const event of service.streamChatWithTools(
563+
[{ role: 'user', content: 'Weather in Paris?' }],
564+
)) {
565+
events.push(event);
566+
}
567+
568+
// Verify the tool-result contains actual tool output
569+
const toolResultEvents = events.filter(e => e.type === 'tool-result');
570+
expect(toolResultEvents).toHaveLength(1);
571+
const toolResult = toolResultEvents[0] as any;
572+
expect(toolResult.toolCallId).toBe('call_weather');
573+
expect(toolResult.toolName).toBe('get_weather');
574+
expect(toolResult.output).toEqual({ type: 'text', value: JSON.stringify({ temp: 22, city: 'Paris' }) });
575+
576+
// Verify order: tool-call comes before tool-result
577+
const toolCallIdx = events.findIndex(e => e.type === 'tool-call');
578+
const toolResultIdx = events.findIndex(e => e.type === 'tool-result');
579+
expect(toolCallIdx).toBeGreaterThanOrEqual(0);
580+
expect(toolResultIdx).toBeGreaterThanOrEqual(0);
581+
expect(toolCallIdx).toBeLessThan(toolResultIdx);
582+
});
583+
534584
it('should fall back to non-streaming when adapter has no streamChat', async () => {
535585
const adapter: LLMAdapter = {
536586
name: 'no-stream',

packages/services/service-ai/src/ai-service.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,13 @@ export class AIService implements IAIService {
332332
}
333333
}
334334
}
335+
// Emit tool-result so the client can see tool output via SSE
336+
yield {
337+
type: 'tool-result',
338+
toolCallId: tr.toolCallId,
339+
toolName: tr.toolName,
340+
output: tr.output,
341+
} as TextStreamPart<ToolSet>;
335342
conversation.push({
336343
role: 'tool',
337344
content: [tr],

0 commit comments

Comments
 (0)