mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-05 23:42:03 +00:00
feat: add MCP tool progress update support in TUI and SDK mode
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
36931e1eab
commit
5ebbceea65
14 changed files with 724 additions and 25 deletions
|
|
@ -1086,6 +1086,26 @@ describe('BaseJsonOutputAdapter', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('emitToolProgress', () => {
|
||||
it('should be a no-op in base class (does not emit any message)', () => {
|
||||
const request: ToolCallRequestInfo = {
|
||||
callId: 'tool-call-1',
|
||||
name: 'mcp__echo-test__echo',
|
||||
args: {},
|
||||
isClientInitiated: false,
|
||||
prompt_id: '',
|
||||
};
|
||||
adapter.emitToolProgress(request, {
|
||||
type: 'mcp_tool_progress',
|
||||
progress: 1,
|
||||
total: 10,
|
||||
message: 'Echo: 1',
|
||||
});
|
||||
|
||||
expect(adapter.emittedMessages).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildResultMessage', () => {
|
||||
beforeEach(() => {
|
||||
adapter.startAssistantMessage();
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import type {
|
|||
SessionMetrics,
|
||||
ServerGeminiStreamEvent,
|
||||
TaskResultDisplay,
|
||||
McpToolProgressData,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import {
|
||||
GeminiEventType,
|
||||
|
|
@ -82,6 +83,18 @@ export interface MessageEmitter {
|
|||
parentToolUseId?: string | null,
|
||||
): void;
|
||||
emitSystemMessage(subtype: string, data?: unknown): void;
|
||||
/**
|
||||
* Emits a tool progress stream event.
|
||||
* Only emits when the adapter supports partial messages (stream mode).
|
||||
* In non-streaming mode, this is a no-op.
|
||||
*
|
||||
* @param request - Tool call request info
|
||||
* @param progress - Structured MCP progress data
|
||||
*/
|
||||
emitToolProgress(
|
||||
request: ToolCallRequestInfo,
|
||||
progress: McpToolProgressData,
|
||||
): void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1051,6 +1064,22 @@ export abstract class BaseJsonOutputAdapter {
|
|||
this.emitMessageImpl(systemMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits a tool progress stream event.
|
||||
* Default implementation is a no-op. StreamJsonOutputAdapter overrides this
|
||||
* to emit stream events when includePartialMessages is enabled.
|
||||
*
|
||||
* @param _request - Tool call request info
|
||||
* @param _progress - Structured MCP progress data
|
||||
*/
|
||||
emitToolProgress(
|
||||
_request: ToolCallRequestInfo,
|
||||
_progress: McpToolProgressData,
|
||||
): void {
|
||||
// No-op in base class. Only StreamJsonOutputAdapter emits tool progress
|
||||
// as stream events when includePartialMessages is enabled.
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a result message from options.
|
||||
* Helper method used by both emitResult implementations.
|
||||
|
|
|
|||
|
|
@ -882,6 +882,115 @@ describe('StreamJsonOutputAdapter', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('emitToolProgress', () => {
|
||||
const mockRequest = {
|
||||
callId: 'tool-call-1',
|
||||
name: 'mcp__echo-test__echo',
|
||||
args: {},
|
||||
isClientInitiated: false,
|
||||
prompt_id: '',
|
||||
};
|
||||
|
||||
it('should emit tool_progress stream event when includePartialMessages is true', () => {
|
||||
adapter = new StreamJsonOutputAdapter(mockConfig, true);
|
||||
stdoutWriteSpy.mockClear();
|
||||
|
||||
adapter.emitToolProgress(mockRequest, {
|
||||
type: 'mcp_tool_progress',
|
||||
progress: 1,
|
||||
total: 10,
|
||||
message: 'Echo: 1',
|
||||
});
|
||||
|
||||
expect(stdoutWriteSpy).toHaveBeenCalledTimes(1);
|
||||
const output = stdoutWriteSpy.mock.calls[0][0] as string;
|
||||
const parsed = JSON.parse(output);
|
||||
|
||||
expect(parsed.type).toBe('stream_event');
|
||||
expect(parsed.parent_tool_use_id).toBeNull();
|
||||
expect(parsed.session_id).toBe('test-session-id');
|
||||
expect(parsed.uuid).toBeDefined();
|
||||
expect(parsed.event).toEqual({
|
||||
type: 'tool_progress',
|
||||
tool_use_id: 'tool-call-1',
|
||||
content: {
|
||||
type: 'mcp_tool_progress',
|
||||
progress: 1,
|
||||
total: 10,
|
||||
message: 'Echo: 1',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not emit tool_progress when includePartialMessages is false', () => {
|
||||
adapter = new StreamJsonOutputAdapter(mockConfig, false);
|
||||
stdoutWriteSpy.mockClear();
|
||||
|
||||
adapter.emitToolProgress(mockRequest, {
|
||||
type: 'mcp_tool_progress',
|
||||
progress: 1,
|
||||
total: 10,
|
||||
message: 'Echo: 1',
|
||||
});
|
||||
|
||||
expect(stdoutWriteSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit multiple tool_progress events for sequential progress updates', () => {
|
||||
adapter = new StreamJsonOutputAdapter(mockConfig, true);
|
||||
stdoutWriteSpy.mockClear();
|
||||
|
||||
adapter.emitToolProgress(mockRequest, {
|
||||
type: 'mcp_tool_progress',
|
||||
progress: 1,
|
||||
total: 3,
|
||||
message: 'Echo: 1',
|
||||
});
|
||||
adapter.emitToolProgress(mockRequest, {
|
||||
type: 'mcp_tool_progress',
|
||||
progress: 2,
|
||||
total: 3,
|
||||
message: 'Echo: 1, 2',
|
||||
});
|
||||
adapter.emitToolProgress(mockRequest, {
|
||||
type: 'mcp_tool_progress',
|
||||
progress: 3,
|
||||
total: 3,
|
||||
message: 'Echo: 1, 2, 3',
|
||||
});
|
||||
|
||||
expect(stdoutWriteSpy).toHaveBeenCalledTimes(3);
|
||||
|
||||
const events = stdoutWriteSpy.mock.calls.map(
|
||||
(call: unknown[]) => JSON.parse(call[0] as string).event,
|
||||
);
|
||||
expect(events[0].content).toEqual({
|
||||
type: 'mcp_tool_progress',
|
||||
progress: 1,
|
||||
total: 3,
|
||||
message: 'Echo: 1',
|
||||
});
|
||||
expect(events[1].content).toEqual({
|
||||
type: 'mcp_tool_progress',
|
||||
progress: 2,
|
||||
total: 3,
|
||||
message: 'Echo: 1, 2',
|
||||
});
|
||||
expect(events[2].content).toEqual({
|
||||
type: 'mcp_tool_progress',
|
||||
progress: 3,
|
||||
total: 3,
|
||||
message: 'Echo: 1, 2, 3',
|
||||
});
|
||||
|
||||
// All events should share the same tool_use_id
|
||||
for (const event of events) {
|
||||
expect(event.type).toBe('tool_progress');
|
||||
expect(event.tool_use_id).toBe('tool-call-1');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSessionId and getModel', () => {
|
||||
beforeEach(() => {
|
||||
adapter = new StreamJsonOutputAdapter(mockConfig, false);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,11 @@
|
|||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { Config } from '@qwen-code/qwen-code-core';
|
||||
import type {
|
||||
Config,
|
||||
ToolCallRequestInfo,
|
||||
McpToolProgressData,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import type {
|
||||
CLIAssistantMessage,
|
||||
CLIMessage,
|
||||
|
|
@ -267,6 +271,32 @@ export class StreamJsonOutputAdapter
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits a tool progress stream event when partial messages are enabled.
|
||||
* This overrides the no-op in BaseJsonOutputAdapter.
|
||||
*/
|
||||
override emitToolProgress(
|
||||
request: ToolCallRequestInfo,
|
||||
progress: McpToolProgressData,
|
||||
): void {
|
||||
if (!this.includePartialMessages) {
|
||||
return;
|
||||
}
|
||||
|
||||
const partial: CLIPartialAssistantMessage = {
|
||||
type: 'stream_event',
|
||||
uuid: randomUUID(),
|
||||
session_id: this.getSessionId(),
|
||||
parent_tool_use_id: null,
|
||||
event: {
|
||||
type: 'tool_progress',
|
||||
tool_use_id: request.callId,
|
||||
content: progress,
|
||||
},
|
||||
};
|
||||
this.emitMessageImpl(partial);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits stream events when partial messages are enabled.
|
||||
* This is a private method specific to StreamJsonOutputAdapter.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue