import { produce } from "immer"; import { IAgentScopeRuntimeResponse, AgentScopeRuntimeRunStatus, IAgentScopeRuntimeMessage, IContent, AgentScopeRuntimeContentType, ITextContent, IImageContent, IDataContent, AgentScopeRuntimeMessageType } from "../types"; import { uuid } from "@agentscope-ai/chat"; class AgentScopeRuntimeResponseBuilder { static mergeToolMessages(messages: IAgentScopeRuntimeMessage[]) { const bufferMessagesMap = new Map(); let resMessages: IAgentScopeRuntimeMessage[] = []; for (const message of messages) { if (AgentScopeRuntimeResponseBuilder.maybeToolInput(message) && message.content?.length) { const content = message.content[0] as IDataContent<{ name: string; call_id?: string; }>; const key = content.data.call_id || content.data.name; bufferMessagesMap.set(key, content); resMessages.push(message); } else if (AgentScopeRuntimeResponseBuilder.maybeToolOutput(message) && message.content?.length) { const content = message.content[0] as IDataContent<{ name: string; call_id?: string; }>; const key = content.data.call_id || content.data.name; const bufferContent = bufferMessagesMap.get(key); if (bufferContent) { resMessages = resMessages.map(i => { if (!AgentScopeRuntimeResponseBuilder.maybeToolInput(i)) return i; const preContent = i.content[0] as IDataContent<{ name: string; call_id?: string; }>; const preKey = preContent.data.call_id || preContent.data.name; if (preKey === key) { return { ...message, content: [...i.content, content] }; } else { return i; } }); } } else { resMessages.push(message); } } return resMessages; } static maybeToolOutput(message: IAgentScopeRuntimeMessage) { return [ AgentScopeRuntimeMessageType.FUNCTION_CALL_OUTPUT, AgentScopeRuntimeMessageType.PLUGIN_CALL_OUTPUT, AgentScopeRuntimeMessageType.TOOL_CALL_OUTPUT, AgentScopeRuntimeMessageType.COMPONENT_CALL_OUTPUT, AgentScopeRuntimeMessageType.MCP_CALL_OUTPUT, ].includes(message.type); } static maybeToolInput(message: IAgentScopeRuntimeMessage) { return [ AgentScopeRuntimeMessageType.FUNCTION_CALL, AgentScopeRuntimeMessageType.PLUGIN_CALL, AgentScopeRuntimeMessageType.TOOL_CALL, AgentScopeRuntimeMessageType.COMPONENT_CALL, AgentScopeRuntimeMessageType.MCP_CALL, ].includes(message.type); } static maybeGenerating(data: { status: AgentScopeRuntimeRunStatus }) { return [ AgentScopeRuntimeRunStatus.InProgress, AgentScopeRuntimeRunStatus.Created, ].includes(data.status); } static maybeDone(data: { status: AgentScopeRuntimeRunStatus }) { return [ AgentScopeRuntimeRunStatus.Completed, AgentScopeRuntimeRunStatus.Canceled, AgentScopeRuntimeRunStatus.Failed, ].includes(data.status); } data: IAgentScopeRuntimeResponse; constructor({ id, status, created_at }: Pick) { this.data = { id: id, output: [], object: 'response', status: status || AgentScopeRuntimeRunStatus.Created, created_at: created_at || Date.now(), }; } handleResponse(data: IAgentScopeRuntimeResponse) { this.data = produce(this.data, (draft) => { const existingOutput = draft.output || []; const incomingOutput = data.output; Object.assign(draft, data); // If incoming response has no output or empty output, preserve the // accumulated output from streaming to avoid losing intermediate // tool-call messages that were already collected. if (!incomingOutput || incomingOutput.length === 0) { draft.output = existingOutput; } else if (existingOutput.length > 0) { // Merge by id: prefer the version with non-empty content to avoid // a partial-update response wiping out previously accumulated // tool-call data (Bug 2 of issue #4644). const existingMap = new Map(existingOutput.map(m => [m.id, m])); const incomingIds = new Set(incomingOutput.map(m => m.id)); const merged = incomingOutput.map(incoming => { const existing = existingMap.get(incoming.id); if (!existing) return incoming; // Prefer the message with content already populated. const incomingHasContent = incoming.content?.length > 0; const existingHasContent = existing.content?.length > 0; if (existingHasContent && !incomingHasContent) { return { ...incoming, content: existing.content }; } return incoming; }); // Append existing-only messages (not present in incoming). for (const existing of existingOutput) { if (!incomingIds.has(existing.id)) { merged.push(existing); } } draft.output = merged; } }); } handleMessage(data: IAgentScopeRuntimeMessage) { this.data = produce(this.data, (draft) => { if (!draft.output) { draft.output = []; } const existingIndex = draft.output.findIndex(msg => msg.id === data.id); if (existingIndex >= 0) { const existingContent = draft.output[existingIndex].content; Object.assign(draft.output[existingIndex], data); if (!data.content || data.content.length === 0) { draft.output[existingIndex].content = existingContent; } } else { draft.output.push(data); } }); } handleContent(data: IContent) { this.data = produce(this.data, (draft) => { const msg = draft.output.find(m => m.id === data.msg_id); if (!msg) { console.warn('Message not found for content:', data.msg_id); return; } if (!msg.content) { msg.content = []; } if (data.delta) { const lastContent = msg.content[msg.content.length - 1]; if (lastContent && lastContent.delta) { if (data.type === AgentScopeRuntimeContentType.TEXT && lastContent.type === AgentScopeRuntimeContentType.TEXT) { (lastContent as ITextContent).text += (data as ITextContent).text; } else if (data.type === AgentScopeRuntimeContentType.IMAGE) { (lastContent as IImageContent).image_url = (data as IImageContent).image_url; } else if (data.type === AgentScopeRuntimeContentType.DATA) { const isStreamingToolInput = [ AgentScopeRuntimeMessageType.PLUGIN_CALL, AgentScopeRuntimeMessageType.TOOL_CALL, AgentScopeRuntimeMessageType.MCP_CALL, ].includes(msg.type as AgentScopeRuntimeMessageType); if (isStreamingToolInput) { const oldData = (lastContent as IDataContent).data || {}; const newData = (data as IDataContent).data || {}; const merged: Record = { ...oldData }; for (const [key, value] of Object.entries(newData)) { if (typeof value === 'string' && typeof merged[key] === 'string') { merged[key] = merged[key] + value; } else { merged[key] = value; } } (lastContent as IDataContent).data = merged; } else { (lastContent as IDataContent).data = (data as IDataContent).data; } } } else { msg.content.push(data); } } else { if (msg.content.length > 0) { Object.assign(msg.content[msg.content.length - 1], data); } else { msg.content.push(data); } } }); } handleError(data: IAgentScopeRuntimeMessage) { this.data = produce(this.data, (draft) => { draft.status = AgentScopeRuntimeRunStatus.Failed; draft.output.push({ status: AgentScopeRuntimeRunStatus.Failed, type: AgentScopeRuntimeMessageType.ERROR, content: [], id: uuid(), role: 'assistant', code: data.code, message: typeof data.message === 'string' ? data.message : JSON.stringify(data.message), }) }); } handle(data: IAgentScopeRuntimeResponse | IAgentScopeRuntimeMessage | IContent) { if (data.object === 'response') { this.handleResponse(data); } else if (data.object === 'message') { if (data.type === AgentScopeRuntimeMessageType.HEARTBEAT) return this.data; this.handleMessage(data); } else if (data.object === 'content') { this.handleContent(data); } else { this.handleError(data); } return this.data; } cancel() { this.data = produce(this.data, (draft) => { if (AgentScopeRuntimeResponseBuilder.maybeGenerating(draft)) { draft.status = AgentScopeRuntimeRunStatus.Canceled; } draft.output.forEach(msg => { if (AgentScopeRuntimeResponseBuilder.maybeGenerating(msg)) { msg.status = AgentScopeRuntimeRunStatus.Canceled; msg.content.forEach(content => { if (AgentScopeRuntimeResponseBuilder.maybeGenerating(content)) { content.status = AgentScopeRuntimeRunStatus.Canceled; } }); } }); }); return this.data; } } export default AgentScopeRuntimeResponseBuilder;