From cf385be8c3a0f3656a97d532166f2f108035b739 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 08:29:36 +0000 Subject: [PATCH 1/3] Initial plan From 1ec6d8e4be061146fb37277382432dc7cd4d7f54 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 08:34:54 +0000 Subject: [PATCH 2/3] fix(service-ai): yield tool-result events in streamChatWithTools SSE stream Previously, streamChatWithTools only yielded tool-call events but not tool-result events. Tool results were pushed to the conversation but never emitted via SSE, making it impossible for the frontend to track tool output in real-time. Now each tool execution result is yielded as a tool-result TextStreamPart before being appended to the conversation, which the encoder maps to tool-output-available SSE chunks. Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/6af7123e-c579-4a8f-84f6-a58107c88797 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../__tests__/auth-and-toolcalling.test.ts | 50 ++++++++++++++++++- .../services/service-ai/src/ai-service.ts | 7 +++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/packages/services/service-ai/src/__tests__/auth-and-toolcalling.test.ts b/packages/services/service-ai/src/__tests__/auth-and-toolcalling.test.ts index c39e9e489..4cab052b1 100644 --- a/packages/services/service-ai/src/__tests__/auth-and-toolcalling.test.ts +++ b/packages/services/service-ai/src/__tests__/auth-and-toolcalling.test.ts @@ -521,16 +521,64 @@ describe('streamChatWithTools', () => { events.push(event); } - // Should have tool-call event followed by text-delta + finish (no double call) + // Should have tool-call + tool-result events followed by text-delta + finish const toolCallEvents = events.filter(e => e.type === 'tool-call'); expect(toolCallEvents).toHaveLength(1); expect((toolCallEvents[0] as any).toolName).toBe('get_weather'); + const toolResultEvents = events.filter(e => e.type === 'tool-result'); + expect(toolResultEvents).toHaveLength(1); + expect((toolResultEvents[0] as any).toolCallId).toBe('call_1'); + expect((toolResultEvents[0] as any).toolName).toBe('get_weather'); + const finishEvent = events.find(e => e.type === 'finish'); expect(finishEvent).toBeDefined(); expect(adapter.chat).toHaveBeenCalledTimes(2); }); + it('should yield tool-result events with tool output', async () => { + const toolCall: ToolCallPart = { + type: 'tool-call', + toolCallId: 'call_weather', + toolName: 'get_weather', + input: { city: 'Paris' }, + }; + + let chatCallIndex = 0; + const adapter: LLMAdapter = { + name: 'mock-stream', + chat: vi.fn(async () => { + chatCallIndex++; + if (chatCallIndex === 1) { + return { content: '', toolCalls: [toolCall] }; + } + return { content: 'Paris is 22°C' }; + }), + complete: vi.fn(async () => ({ content: '' })), + }; + + const service = new AIService({ adapter, logger: silentLogger, toolRegistry: registry }); + const events: TextStreamPart[] = []; + for await (const event of service.streamChatWithTools( + [{ role: 'user', content: 'Weather in Paris?' }], + )) { + events.push(event); + } + + // Verify the tool-result contains actual tool output + const toolResultEvents = events.filter(e => e.type === 'tool-result'); + expect(toolResultEvents).toHaveLength(1); + const toolResult = toolResultEvents[0] as any; + expect(toolResult.toolCallId).toBe('call_weather'); + expect(toolResult.toolName).toBe('get_weather'); + expect(toolResult.output).toEqual({ type: 'text', value: JSON.stringify({ temp: 22, city: 'Paris' }) }); + + // Verify order: tool-call comes before tool-result + const toolCallIdx = events.findIndex(e => e.type === 'tool-call'); + const toolResultIdx = events.findIndex(e => e.type === 'tool-result'); + expect(toolCallIdx).toBeLessThan(toolResultIdx); + }); + it('should fall back to non-streaming when adapter has no streamChat', async () => { const adapter: LLMAdapter = { name: 'no-stream', diff --git a/packages/services/service-ai/src/ai-service.ts b/packages/services/service-ai/src/ai-service.ts index f4a664609..67e979e36 100644 --- a/packages/services/service-ai/src/ai-service.ts +++ b/packages/services/service-ai/src/ai-service.ts @@ -332,6 +332,13 @@ export class AIService implements IAIService { } } } + // Emit tool-result so the client can see tool output via SSE + yield { + type: 'tool-result', + toolCallId: tr.toolCallId, + toolName: tr.toolName, + output: tr.output, + } as TextStreamPart; conversation.push({ role: 'tool', content: [tr], From 9a93633f349c34403f0363296c6074fc4ab2eb70 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:21:21 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20auth-and-toolcalling.t?= =?UTF-8?q?est.ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../service-ai/src/__tests__/auth-and-toolcalling.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/services/service-ai/src/__tests__/auth-and-toolcalling.test.ts b/packages/services/service-ai/src/__tests__/auth-and-toolcalling.test.ts index 4cab052b1..f0433ba5b 100644 --- a/packages/services/service-ai/src/__tests__/auth-and-toolcalling.test.ts +++ b/packages/services/service-ai/src/__tests__/auth-and-toolcalling.test.ts @@ -576,6 +576,8 @@ describe('streamChatWithTools', () => { // Verify order: tool-call comes before tool-result const toolCallIdx = events.findIndex(e => e.type === 'tool-call'); const toolResultIdx = events.findIndex(e => e.type === 'tool-result'); + expect(toolCallIdx).toBeGreaterThanOrEqual(0); + expect(toolResultIdx).toBeGreaterThanOrEqual(0); expect(toolCallIdx).toBeLessThan(toolResultIdx); });