optimized ChatBox component.

This commit is contained in:
Xiaohan-Tian
2025-08-16 17:35:40 -07:00
parent 429efaf629
commit 8b0ddae58d
6 changed files with 351 additions and 294 deletions
+1
View File
@@ -18,6 +18,7 @@ DEPLOYMENT.md
# Editor directories and files # Editor directories and files
!.vscode/extensions.json !.vscode/extensions.json
.idea .idea
.claude
.DS_Store .DS_Store
*.suo *.suo
*.ntvs* *.ntvs*
+48 -263
View File
@@ -8,23 +8,18 @@ import { GeminiProvider } from '../agent/llm/GeminiProvider';
import { LLMProvider } from '../agent/llm/LLMProvider'; import { LLMProvider } from '../agent/llm/LLMProvider';
import { ConfigManager } from '../core/config/ConfigManager'; import { ConfigManager } from '../core/config/ConfigManager';
import { useProjectStore } from '../stores/projectStore'; import { useProjectStore } from '../stores/projectStore';
import { XMLToolExecutor } from '../agent/core/XMLToolExecutor';
import { extractXMLFromString } from '../util/xmlUtil';
import { SystemPrompts } from '../agent/core/SystemPrompts'; import { SystemPrompts } from '../agent/core/SystemPrompts';
import { clearChatHistoryAndUI, registerClearChatUICallback } from '../util/chatUtil'; import { clearChatHistoryAndUI, registerClearChatUICallback } from '../util/chatUtil';
import { processUserMessage } from '../util/messageFilter/UserMessageFilter'; import { processUserMessage } from '../util/messageFilter/UserMessageFilter';
import { useStreamProcessor } from '../hooks/useStreamProcessor';
import { createMessage, addWelcomeMessage } from '../utils/chatMessageUtils';
import { extractActionableTools, executeAllTools } from '../utils/toolExecutionUtils';
import type { ChatMessage } from '../types/projectTypes';
// Module-level guard to avoid duplicate welcome in React StrictMode dev remounts // Module-level guard to avoid duplicate welcome in React StrictMode dev remounts
let hasShownWelcomeOnceInRuntime = false; let hasShownWelcomeOnceInRuntime = false;
interface ChatMessage {
id: string;
role: 'user' | 'assistant';
content: string;
isStreaming?: boolean;
tokenCount?: number;
}
/** /**
* Create the appropriate LLM provider based on configuration * Create the appropriate LLM provider based on configuration
*/ */
@@ -55,7 +50,6 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
// Initialize with empty messages // Initialize with empty messages
const [messages, setMessages] = useState<ChatMessage[]>([]); const [messages, setMessages] = useState<ChatMessage[]>([]);
const [isProcessing, setIsProcessing] = useState(false); const [isProcessing, setIsProcessing] = useState(false);
const [abortController, setAbortController] = useState<AbortController | null>(null);
const [lastUserMessage, setLastUserMessage] = useState<string>(''); const [lastUserMessage, setLastUserMessage] = useState<string>('');
// Tool execution state // Tool execution state
@@ -66,9 +60,25 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
// Track if this is the first message (for system prompt logging) // Track if this is the first message (for system prompt logging)
const [isFirstMessage, setIsFirstMessage] = useState(true); const [isFirstMessage, setIsFirstMessage] = useState(true);
const generateMessageId = (): string => { // Message update callbacks for stream processor
return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; const handleMessageUpdate = useCallback((messageId: string, updater: (msg: ChatMessage) => ChatMessage) => {
}; setMessages(prev => prev.map(msg => msg.id === messageId ? updater(msg) : msg));
}, []);
const handleMessageAdd = useCallback((message: ChatMessage) => {
setMessages(prev => [...prev, message]);
}, []);
const handleProcessingChange = useCallback((processing: boolean) => {
setIsProcessing(processing);
}, []);
// Stream processor hook
const streamProcessor = useStreamProcessor({
onMessageUpdate: handleMessageUpdate,
onMessageAdd: handleMessageAdd,
onProcessingChange: handleProcessingChange
});
const clearChatUI = useCallback(async () => { const clearChatUI = useCallback(async () => {
// Clear UI state // Clear UI state
@@ -78,18 +88,9 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
setIsFirstMessage(true); setIsFirstMessage(true);
// Auto-show welcome message after clearing (like on app startup) // Auto-show welcome message after clearing (like on app startup)
try { const welcomeMessage = await addWelcomeMessage();
const result = await processUserMessage('/welcome'); if (welcomeMessage) {
if (result.pseudoAssistantResponse) { setMessages([welcomeMessage]);
const pseudoId = generateMessageId();
setMessages(prev => [...prev, {
id: pseudoId,
role: 'assistant',
content: result.pseudoAssistantResponse!,
}]);
}
} catch {
// ignore errors, just don't show welcome if it fails
} }
}, []); }, []);
@@ -117,29 +118,20 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
// Auto-trigger welcome on first launch (guard against React StrictMode double-invoke only) // Auto-trigger welcome on first launch (guard against React StrictMode double-invoke only)
(async () => { (async () => {
try {
if (hasShownWelcomeOnceInRuntime) return; if (hasShownWelcomeOnceInRuntime) return;
hasShownWelcomeOnceInRuntime = true; hasShownWelcomeOnceInRuntime = true;
const result = await processUserMessage('/welcome'); const welcomeMessage = await addWelcomeMessage();
if (result.pseudoAssistantResponse) { if (welcomeMessage) {
const pseudoId = generateMessageId(); setMessages([welcomeMessage]);
setMessages(prev => [...prev, {
id: pseudoId,
role: 'assistant',
content: result.pseudoAssistantResponse!,
}]);
}
} catch {
// ignore
} }
})(); })();
}, [clearChatUI]); }, [clearChatUI]);
const handleAbort = () => { const handleAbort = () => {
if (abortController) { const controller = streamProcessor.abortController;
abortController.abort(); if (controller) {
setAbortController(null); controller.abort();
// Use AgentCore to clean up the data model and get the user message content // Use AgentCore to clean up the data model and get the user message content
const agentCore = AgentCore.instance(); const agentCore = AgentCore.instance();
@@ -160,28 +152,9 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
clearChatHistoryAndUI(setStatus); clearChatHistoryAndUI(setStatus);
}; };
const addToolResultMessage = (toolName: string, success: boolean, result: string) => {
const toolMsgId = generateMessageId();
const friendlyDisplay = `${success ? '✅' : '❌'} __**${toolName}**__ \n\n └── ${result}`;
setMessages(prev => [...prev, {
id: toolMsgId,
role: 'user',
content: friendlyDisplay
}]);
};
const executeToolsFromResponse = async (response: string): Promise<boolean> => { const executeToolsFromResponse = async (response: string): Promise<boolean> => {
try { try {
// Check if response contains XML tool invocations const actionableBlocks = extractActionableTools(response);
const xmlBlocks = extractXMLFromString(response);
// Consider only actionable tools (exclude think/thinking)
const actionableBlocks = xmlBlocks.filter((block) => {
const match = block.match(/<([a-zA-Z_][a-zA-Z0-9_-]*)/);
const name = match ? match[1].toLowerCase() : '';
return name !== 'think' && name !== 'thinking';
});
if (actionableBlocks.length === 0) { if (actionableBlocks.length === 0) {
// No actionable tools to execute, stop the loop // No actionable tools to execute, stop the loop
@@ -194,46 +167,12 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
setCurrentToolIndex(0); setCurrentToolIndex(0);
const { setStatus } = useProjectStore.getState(); const { setStatus } = useProjectStore.getState();
setStatus(`Executing ${actionableBlocks.length} tool(s)...`);
const executor = XMLToolExecutor.instance(); // Execute all tools and get accumulated results
let accumulatedResults = ''; const accumulatedResults = await executeAllTools(actionableBlocks, {
onMessageAdd: handleMessageAdd,
// Execute tools sequentially with real-time updates onStatusUpdate: setStatus
for (let i = 0; i < actionableBlocks.length; i++) { });
setCurrentToolIndex(i + 1);
setStatus(`Executing tool ${i + 1} of ${actionableBlocks.length}...`);
// Determine tool name from XML block
const toolNameMatch = actionableBlocks[i].match(/<([a-zA-Z_][a-zA-Z0-9_-]*)/);
const toolName = toolNameMatch ? toolNameMatch[1] : 'unknown_tool';
try {
// Execute single XML block
const results = await executor.executeXMLTools(actionableBlocks[i]);
const result = results[0]; // Single block should give single result
if (result) {
// Add friendly display message
addToolResultMessage(toolName, result.success, result.result);
// Accumulate formatted result for LLM (skip thinking tools)
if (toolName !== 'thinking' && toolName !== 'think') {
const formattedResult = `tool: ${toolName}\nsuccess: ${result.success}\nresult:\n${result.result}\n------------\n`;
accumulatedResults += formattedResult;
}
}
} catch (error) {
// Handle individual tool error
addToolResultMessage(toolName, false, `Tool execution failed: ${error}`);
// Accumulate error result for LLM (skip thinking tools)
if (toolName !== 'thinking' && toolName !== 'think') {
const formattedResult = `tool: ${toolName}\nsuccess: false\nresult:\nTool execution failed: ${error}\n------------\n`;
accumulatedResults += formattedResult;
}
}
}
// Store accumulated results // Store accumulated results
setToolResults(accumulatedResults); setToolResults(accumulatedResults);
@@ -264,61 +203,8 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
}; };
const sendToolResultsToLLM = async (toolResultsString: string): Promise<void> => { const sendToolResultsToLLM = async (toolResultsString: string): Promise<void> => {
// Send tool results as hidden user input to LLM // Process tool results through the stream processor
setIsProcessing(true); const assistantResponse = await streamProcessor.processStream(toolResultsString, 'TOOL_RESULTS');
// Create abort controller for this request
const controller = new AbortController();
setAbortController(controller);
// Add streaming assistant message for the response
const assistantMsgId = generateMessageId();
setMessages(prev => [...prev, {
id: assistantMsgId,
role: 'assistant',
content: '<span class="processing-wave">Processing...</span> 0 tokens received. click here to abort.',
isStreaming: true,
tokenCount: 0
}]);
try {
const agentCore = AgentCore.instance();
let assistantResponse = '';
let tokenCount = 0;
// Log the tool results being sent to LLM
console.log('------------ USER ------------');
console.log(toolResultsString);
console.log('------------------------------');
for await (const chunk of agentCore.processUserInput(toolResultsString)) {
// Check if request was aborted
if (controller.signal.aborted) {
return;
}
if (chunk.type === 'text') {
assistantResponse += chunk.content;
tokenCount++;
// Update streaming message with token count and abort link
setMessages(prev => prev.map(msg =>
msg.id === assistantMsgId
? { ...msg, content: `<span class="processing-wave">Processing...</span> ${tokenCount} tokens received. click here to abort.`, tokenCount }
: msg
));
} else if (chunk.type === 'done') {
// Replace with final response
setMessages(prev => prev.map(msg =>
msg.id === assistantMsgId
? { ...msg, content: assistantResponse, isStreaming: false, tokenCount: undefined }
: msg
));
// Log the complete assistant response
console.log('------------ ASSISTANT ------------');
console.log(assistantResponse);
console.log('-----------------------------------');
// Check if the new response contains more tools // Check if the new response contains more tools
const hasMoreTools = await executeToolsFromResponse(assistantResponse); const hasMoreTools = await executeToolsFromResponse(assistantResponse);
@@ -328,27 +214,6 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
const agentCore = AgentCore.instance(); const agentCore = AgentCore.instance();
agentCore.getAgentState().setIsWorkingOnTask(false); agentCore.getAgentState().setIsWorkingOnTask(false);
} }
break;
}
}
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
// Request was aborted, don't show error
return;
}
console.error('Error processing tool results:', error);
// Update with error message
setMessages(prev => prev.map(msg =>
msg.id === assistantMsgId
? { ...msg, content: 'Error: Failed to process tool results', isStreaming: false, tokenCount: undefined }
: msg
));
} finally {
setAbortController(null);
setIsProcessing(false);
}
}; };
const handleSend = async () => { const handleSend = async () => {
@@ -362,22 +227,14 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
// Conditionally show the user message bubble // Conditionally show the user message bubble
if (filterResult.displayUserMessage) { if (filterResult.displayUserMessage) {
const userMsgId = generateMessageId(); const userMsgObject = createMessage('user', userMessage);
setMessages(prev => [...prev, { handleMessageAdd(userMsgObject);
id: userMsgId,
role: 'user',
content: userMessage
}]);
} }
// If we have a pseudo assistant response, show it immediately // If we have a pseudo assistant response, show it immediately
if (filterResult.pseudoAssistantResponse) { if (filterResult.pseudoAssistantResponse) {
const pseudoId = generateMessageId(); const pseudoMessage = createMessage('assistant', filterResult.pseudoAssistantResponse);
setMessages(prev => [...prev, { handleMessageAdd(pseudoMessage);
id: pseudoId,
role: 'assistant',
content: filterResult.pseudoAssistantResponse!,
}]);
} }
// If we shouldn't send anything to LLM, stop here // If we shouldn't send anything to LLM, stop here
@@ -385,28 +242,8 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
return; return;
} }
setIsProcessing(true);
// Create abort controller for this request
const controller = new AbortController();
setAbortController(controller);
// Add streaming assistant message
const assistantMsgId = generateMessageId();
setMessages(prev => [...prev, {
id: assistantMsgId,
role: 'assistant',
content: '<span class="processing-wave">Processing...</span> 0 tokens received. click here to abort.',
isStreaming: true,
tokenCount: 0
}]);
try {
const agentCore = AgentCore.instance();
let assistantResponse = '';
let tokenCount = 0;
// Set working on task flag when user sends a message // Set working on task flag when user sends a message
const agentCore = AgentCore.instance();
agentCore.getAgentState().setIsWorkingOnTask(true); agentCore.getAgentState().setIsWorkingOnTask(true);
// Log system prompt only for first message or first message after clear // Log system prompt only for first message or first message after clear
@@ -423,39 +260,8 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
setIsFirstMessage(false); setIsFirstMessage(false);
} }
// Log the final user message being sent to LLM // Process user input through the stream processor
console.log('------------ USER ------------'); const assistantResponse = await streamProcessor.processStream(filterResult.finalMessageForLLM, 'USER');
console.log(filterResult.finalMessageForLLM);
console.log('------------------------------');
for await (const chunk of agentCore.processUserInput(filterResult.finalMessageForLLM)) {
// Check if request was aborted
if (controller.signal.aborted) {
return;
}
if (chunk.type === 'text') {
assistantResponse += chunk.content;
tokenCount++;
// Update streaming message with token count and abort link
setMessages(prev => prev.map(msg =>
msg.id === assistantMsgId
? { ...msg, content: `<span class="processing-wave">Processing...</span> ${tokenCount} tokens received. click here to abort.`, tokenCount }
: msg
));
} else if (chunk.type === 'done') {
// Replace with final response
setMessages(prev => prev.map(msg =>
msg.id === assistantMsgId
? { ...msg, content: assistantResponse, isStreaming: false, tokenCount: undefined }
: msg
));
// Log the complete assistant response
console.log('------------ ASSISTANT ------------');
console.log(assistantResponse);
console.log('-----------------------------------');
// Check if response contains tools to execute // Check if response contains tools to execute
const hasTools = await executeToolsFromResponse(assistantResponse); const hasTools = await executeToolsFromResponse(assistantResponse);
@@ -464,27 +270,6 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
if (!hasTools) { if (!hasTools) {
agentCore.getAgentState().setIsWorkingOnTask(false); agentCore.getAgentState().setIsWorkingOnTask(false);
} }
break;
}
}
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
// Request was aborted, don't show error
return;
}
console.error('Error processing message:', error);
// Update with error message
setMessages(prev => prev.map(msg =>
msg.id === assistantMsgId
? { ...msg, content: 'Error: Failed to process message', isStreaming: false, tokenCount: undefined }
: msg
));
} finally {
setAbortController(null);
setIsProcessing(false);
}
} }
}; };
+119
View File
@@ -0,0 +1,119 @@
import { useState, useCallback } from 'react';
import { AgentCore } from '../agent/core/AgentCore';
import { createStreamingMessage } from '../utils/chatMessageUtils';
import type { ChatMessage } from '../types/projectTypes';
interface StreamProcessorOptions {
onMessageUpdate: (messageId: string, updater: (msg: ChatMessage) => ChatMessage) => void;
onMessageAdd: (message: ChatMessage) => void;
onProcessingChange: (isProcessing: boolean) => void;
}
interface StreamProcessorResult {
processStream: (input: string, logPrefix?: string) => Promise<string>;
abortController: AbortController | null;
isProcessing: boolean;
}
export const useStreamProcessor = (options: StreamProcessorOptions): StreamProcessorResult => {
const { onMessageUpdate, onMessageAdd, onProcessingChange } = options;
const [abortController, setAbortController] = useState<AbortController | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
const processStream = useCallback(async (input: string, logPrefix: string = 'USER'): Promise<string> => {
setIsProcessing(true);
onProcessingChange(true);
// Create abort controller for this request
const controller = new AbortController();
setAbortController(controller);
// Add streaming assistant message
const streamingMessage = createStreamingMessage();
onMessageAdd(streamingMessage);
try {
const agentCore = AgentCore.instance();
let assistantResponse = '';
let tokenCount = 0;
let streamCompleted = false;
// Log the input being sent to LLM
console.log(`------------ ${logPrefix} ------------`);
console.log(input);
console.log('------------------------------');
for await (const chunk of agentCore.processUserInput(input)) {
// Check if request was aborted
if (controller.signal.aborted) {
return '';
}
if (chunk.type === 'text') {
assistantResponse += chunk.content;
tokenCount++;
// Update streaming message with token count and abort link
onMessageUpdate(streamingMessage.id, (msg) => ({
...msg,
content: `<span class="processing-wave">Processing...</span> ${tokenCount} tokens received. click here to abort.`,
tokenCount
}));
} else if (chunk.type === 'done') {
streamCompleted = true;
// Replace with final response
onMessageUpdate(streamingMessage.id, (msg) => ({
...msg,
content: assistantResponse,
isStreaming: false,
tokenCount: undefined
}));
// Log the complete assistant response
console.log('------------ ASSISTANT ------------');
console.log(assistantResponse);
console.log('-----------------------------------');
break;
}
}
// If stream didn't complete normally, finalize the message
if (!streamCompleted && !controller.signal.aborted) {
onMessageUpdate(streamingMessage.id, (msg) => ({
...msg,
content: assistantResponse || 'Stream was interrupted unexpectedly',
isStreaming: false,
tokenCount: undefined
}));
}
return assistantResponse;
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
// Request was aborted, don't show error
return '';
}
console.error('Error processing stream:', error);
// Update with error message
onMessageUpdate(streamingMessage.id, (msg) => ({
...msg,
content: 'Error: Failed to process message',
isStreaming: false,
tokenCount: undefined
}));
return '';
} finally {
setAbortController(null);
setIsProcessing(false);
onProcessingChange(false);
}
}, [onMessageUpdate, onMessageAdd, onProcessingChange]);
return {
processStream,
abortController,
isProcessing
};
};
+8
View File
@@ -5,6 +5,14 @@ export interface TimeSignature {
denominator: number; denominator: number;
} }
export interface ChatMessage {
id: string;
role: 'user' | 'assistant';
content: string;
isStreaming?: boolean;
tokenCount?: number;
}
/** /**
* A reusable class-transformer decorator to apply a default value during deserialization. * A reusable class-transformer decorator to apply a default value during deserialization.
* @param defaultValue The default value to apply if the field is undefined. * @param defaultValue The default value to apply if the field is undefined.
+42
View File
@@ -0,0 +1,42 @@
import { processUserMessage } from '../util/messageFilter/UserMessageFilter';
import type { ChatMessage } from '../types/projectTypes';
export const generateMessageId = (): string => {
return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
};
export const createMessage = (role: 'user' | 'assistant', content: string): ChatMessage => {
return {
id: generateMessageId(),
role,
content,
};
};
export const createStreamingMessage = (content: string = '<span class="processing-wave">Processing...</span> 0 tokens received. click here to abort.'): ChatMessage => {
return {
id: generateMessageId(),
role: 'assistant',
content,
isStreaming: true,
tokenCount: 0,
};
};
export const createToolResultMessage = (toolName: string, success: boolean, result: string): ChatMessage => {
const friendlyDisplay = `${success ? '✅' : '❌'} __**${toolName}**__ \n\n └── ${result}`;
return createMessage('user', friendlyDisplay);
};
export const addWelcomeMessage = async (): Promise<ChatMessage | null> => {
try {
const result = await processUserMessage('/welcome');
if (result.pseudoAssistantResponse) {
return createMessage('assistant', result.pseudoAssistantResponse);
}
return null;
} catch {
// ignore errors, just don't show welcome if it fails
return null;
}
};
+102
View File
@@ -0,0 +1,102 @@
import { XMLToolExecutor } from '../agent/core/XMLToolExecutor';
import { extractXMLFromString } from '../util/xmlUtil';
import { createToolResultMessage } from './chatMessageUtils';
import type { ChatMessage } from '../types/projectTypes';
interface ToolExecutionResult {
success: boolean;
result: string;
}
interface ToolExecutionOptions {
onMessageAdd: (message: ChatMessage) => void;
onStatusUpdate: (status: string) => void;
}
export const extractActionableTools = (response: string): string[] => {
const xmlBlocks = extractXMLFromString(response);
// Consider only actionable tools (exclude think/thinking)
return xmlBlocks.filter((block) => {
const match = block.match(/<([a-zA-Z_][a-zA-Z0-9_-]*)/);
const name = match ? match[1].toLowerCase() : '';
return name !== 'think' && name !== 'thinking';
});
};
export const extractToolName = (xmlBlock: string): string => {
const toolNameMatch = xmlBlock.match(/<([a-zA-Z_][a-zA-Z0-9_-]*)/);
return toolNameMatch ? toolNameMatch[1] : 'unknown_tool';
};
export const executeSingleTool = async (
xmlBlock: string,
toolName: string,
options: ToolExecutionOptions
): Promise<ToolExecutionResult> => {
const { onMessageAdd } = options;
try {
const executor = XMLToolExecutor.instance();
const results = await executor.executeXMLTools(xmlBlock);
const result = results[0]; // Single block should give single result
if (result) {
// Add friendly display message
const toolMessage = createToolResultMessage(toolName, result.success, result.result);
onMessageAdd(toolMessage);
return {
success: result.success,
result: result.result
};
}
return {
success: false,
result: 'No result returned from tool execution'
};
} catch (error) {
// Handle individual tool error
const errorMessage = `Tool execution failed: ${error}`;
const toolMessage = createToolResultMessage(toolName, false, errorMessage);
onMessageAdd(toolMessage);
return {
success: false,
result: errorMessage
};
}
};
export const formatToolResultForLLM = (toolName: string, result: ToolExecutionResult): string => {
// Skip thinking tools
if (toolName === 'thinking' || toolName === 'think') {
return '';
}
return `tool: ${toolName}\nsuccess: ${result.success}\nresult:\n${result.result}\n------------\n`;
};
export const executeAllTools = async (
actionableBlocks: string[],
options: ToolExecutionOptions
): Promise<string> => {
const { onStatusUpdate } = options;
onStatusUpdate(`Executing ${actionableBlocks.length} tool(s)...`);
let accumulatedResults = '';
// Execute tools sequentially with real-time updates
for (let i = 0; i < actionableBlocks.length; i++) {
onStatusUpdate(`Executing tool ${i + 1} of ${actionableBlocks.length}...`);
const toolName = extractToolName(actionableBlocks[i]);
const result = await executeSingleTool(actionableBlocks[i], toolName, options);
// Accumulate formatted result for LLM
accumulatedResults += formatToolResultForLLM(toolName, result);
}
return accumulatedResults;
};