You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The latest client for Agent C has been developed exclusively by agents in a manner that I myself explicitly discourage.
My #1 rule is that agent instructions should be tuned by experts in the task the agents are being built for. I am most definitely NOT a Typescript / React developer, and I'm not all all good at CSS.
My #2 rule is that the "driver" know enough about the task being peroformed to be able to head off mistakes and help provide technical guidance to the agents. As I said, that's NOT me.
However, I am the architect behind this framework and have built MANY clients for it that were not web clients. While nowhere near as effective as an actual Typescript / React dev would have been I've been able to work the agents through many of their difficulties.
When things look ALMOST like a duck and they "quock" instead of "quack" it's easy to confuse agents
While there are many chat apps, and even many "chat with agents" apps out there, none of them have an an event stream like the one in Agent C. But for an agent, this looks SO CLOSE to all those other apps / frameworks they often make false assumptions.
One of the things that makes the Agent C Framework special is not just that it's fully event driven, but that our agent tools not only allow the agent to send things to the UI, it allows wools to fully participate in the event stream as well. The tool agents use to rename sessions talks to the same RealtimeBridge that the client does, it even send the exact same control event to rename the chat session as the client. Tools can render media in the chat, send error/warning/info messages, etc. And then there's the delegation tools which results likely thousands of events being generated as those funnel all of the session events for the delegated agent to the UI to display as well.
While the agents had produced an app far superior to the old client, it was clear to me that there had been fundimental mistakes made in processing the event stream that we leading to many issues with the handling of tool calls. In order to correct these sort of fundamental issues it is CRITICAL that one ensures that the agents actually understand the task at hand.
It's not enough to simply hand them new requirements and say "go fix it", you're risking yet another misunderstanding. My process for this is the same sort of thing I'd do with any junior dev working on their first complex assignment, just with way more grunt work for them.
Starting at the lowest level agent:
Give them the new requirements, and task them with coming up with a detailed deisgn for what needs to be changed, how and why.
Review, really review, their design.
Provide feedback on design, have them refine.
Repeat 2 & 3 as needed.
Work your way downstream
Give the next package dev the same requirements along with the design from the previous agent and task them with refining the design toe ensure accuracy for how their package needs to change.
Review, really review, their design.
Provide feedback on design, have them refine.
Repeat 2 & 3 as needed.
Fixing the event stream handling.
As I said above there had been fundamental mistakes made in the handling of the event stream. They did not become apparent until many of the more advanced events were being rendered in the chat that it became apparent. The notifications that a tool call was about to be made were being "orphaned" and left up long after the tool calls had completed. This made me realize both what mistake had been made and why it had been made.
I had laid out the sequence of events involved in a tool call to them tool_select -> tool_call_active -> tool_call_complete.
I told them what they should do in response to each event.
I did NOT tell them explicitly, "you almost certainly will receive other events in between"
I did not tell them explicitly "you will receive events from sub sessions" and all that entails
Because I did not do those things, and in spite of the fact that our events have fields like parent_session_id in them the agents saw what looked like evey other chat with agents app on the planet and wrote incorrect code.
Step 1: Give them new requirements
My opening message to the agent explained the issues I was seeing in the chat display, and how I believed that that made mes suspect there was a fundamental misunderstanding in how events we being handled. I explained the importance of tracking the session id for chat content all the way down to the UI so it could be used to display the tool calls properly. I explained how things could go wrong which would cause the client to not get sent an event and those lead to orphaned tool calls and gave them a simple way to add safety nets to ensure that didn't happen. I spent several hours both writing explanations and gathering example data.
Step 2: Review
Looking at their plan it was clear that there were still some misunderstandings involved. They were still fixated on the sequence of events, and treating things too linearly. The prompt I gave reframed things for them allowing them to both correct and simplify their design :
That’s CLOSE, but it has a critical flaw I think…
Backward attachment should be the norm but it's a little more complicated. The vast majority of the time an agent will start their turn with a message or a thought, followed by tool calls. Then another response, then more tool calls etc.
Because each message or thought the precedes the tool calls, explains why the tool calls are being done, the should be attached to that.
In some cases, not super common but common enough to warrant attention, the interaction will END with tool calls, so again attaching them to the prior message makes sense.
On very rare occasions, the agent will start the interaction with one or more tool calls. THEN says something, in which case it makes sense to attach them to the next thought / message
It’s that last case that we need to handle... The way you have the flow documented isn't quite right, or at least it's lacking specificity. I'm going to try and clarify below because it's largely my fault for not adding more events so things were more concrete...
First, as far as tool calls go, the only event that REALLY matters is that last tool_call with active set to false. The other ones are strictly for UX purposes so that the user doesn't think things are hung when an agent is writing out a long file or something.
Instead of thinking of this as a series of events at ALL really it's really more:
tool_select comes in
grab the session_id, the id of the tool call and the name of the tool
update the UI to show that the agent is preparing to use the tool.
tool_call with active set to true
grab the session_id, the id of the tool call and the name of the tool
update the UI to show that the agent is using the tool. (by matching session_id and tool use id)
tool_call (active=false) → Tool completed
update the UI to remove the "agent is using" notification (by matching session_id and tool use id)
If there isn't a previous assistant/thought message then hold on it it
When the next assistant thought / message come in for that session id, if we have tool call results we've been holding on to attach the results to that message.
The end result
The file below contains the final plan for this work. Once implemented will correct the issues we have now, and ensure that we're on good footing for the events to follow. Only because I am the definitive expert on this event stream an know every event, when certain events are or are not possible, etc was I able to correct the fundamental issues with the original implementations and even then, bcause I've finally gained enough knowledge of the Typescript code in the core package to follow it at least. An ACTUAL Typescript developer with access to me in order to ask questions would have gotten it right the first time.
The tool call notification and display system has fundamental architectural issues in core that require fixes across multiple layers. The primary problems are:
ToolCallManager lacks session tracking - Cannot distinguish tool calls from different sessions (main vs sub-sessions)
No cleanup mechanisms for orphaned notifications - Tools stay "active" after interactions end
Tool calls attached at wrong time - Need to attach to PREVIOUS message (normal case) or buffer for NEXT message (rare case)
Resumed sessions skip tool calls - Explicitly commented out with "skip for now"
These are NOT primarily UI issues. The core event processing logic needs significant fixes.
The Core Flow (Simplified)
Tip
Stop thinking about tool events as a sequence. Only the tool_call(active=false) event matters for results.
When tool_call (active=false) arrives:
1. Remove "Agent is using X" notification (match by session_id + tool_call_id)
2. Try to find PREVIOUS assistant/thought message in SAME session_id
✅ Found: Attach tool calls/results → emit message-updated event
❌ Not found: Buffer tool calls for this session_id
3. When NEXT assistant/thought arrives for this session_id:
- Check if we have buffered tool calls
- If yes, attach them to this message
Why this works:
Normal case (99% of time): Agent says something → uses tools → attach to previous message
Rare case: Interaction starts with tools → buffer → attach to next message
Session isolation: Buffering is per-session, so sub-sessions don't interfere
Critical Understanding: Event Stream Nature
Important
The Agent C event stream is HIGHLY INTERRUPTIBLE by design.
Between any two tool events (tool_select_delta → tool_call active → tool_call complete), the following can and WILL occur:
This is BY DESIGN and EXPECTED. The current implementation assumes sequential tool events without interruption - this is fundamentally wrong.
Tool Event Lifecycle - Simplified
Tip
Key Insight: tool_select_delta and tool_call(active=true) are ONLY for UX notifications. The tool_call(active=false) event is what actually matters for displaying tool results.
Remove notification: Clear "Agent is using" UI (match by session_id + tool_call.id)
Try backward attachment: Find PREVIOUS assistant/thought message in SAME session_id
If found: Attach tool calls/results to it + emit message-updated event
If NOT found: Buffer the tool calls (rare case - interaction starts with tools)
Next message: When next assistant/thought arrives for this session_id, attach any buffered tool calls
Between ANY of these events: System messages, render media, delegation sub-sessions, etc. can and WILL occur. This is BY DESIGN.
Why Session Tracking is Critical
Common Scenario:
1. Main session: Agent says "I'll check the file..."
2. Main session: tool_select_delta for workspace_read
3. Main session: tool_call (active=true)
4. [Tool uses act_oneshot - creates SUB-SESSION]
5. Sub-session: 1000+ events (entire delegated agent interaction)
6. Main session: tool_call (active=false) with results
Without session_id tracking:
Can't match step 6 back to step 2 (thousands of events in between)
Sub-session tool calls interfere with main session notifications
Can't find correct message to attach results to
With session_id tracking:
Match by session_id + tool_call_id → works regardless of intervening events
Sub-session has different session_id → no interference
Search for previous message IN SAME session_id → correct attachment
Session Context
All SessionEvents include:
session_id - The chat session this event came from (may be a sub-session)
role - Role that triggered the event
parent_session_id - Parent session if this is a sub-session (optional)
user_session_id - ALWAYS the user's main chat session ID
Why This Matters:
Tool calls from delegation tools (sub-sessions) have DIFFERENT session_ids
Messages from sub-sessions have DIFFERENT session_ids
Must match tool calls to messages in SAME session_id
Current code doesn't track session_id at all → everything breaks with sub-sessions
Problems Identified (From Code Review)
Problem 1: ToolCallManager Has No Session Tracking ⚠️⚠️⚠️ CRITICAL
File: packages/core/src/events/ToolCallManager.ts
Current Code:
exportclassToolCallManager{privateactiveTools: Map<string,ToolNotification>=newMap();// ^^^^^^ Only tool_call_id!onToolSelect(event: ToolSelectDeltaEvent): ToolNotification{consttoolCall=event.tool_calls[0];constnotification: ToolNotification={id: toolCall.id, ... };this.activeTools.set(toolCall.id,notification);// ❌ No session_id!}}
Problems This Causes:
Tool calls from main session and sub-sessions collide (same tool_call_id)
Cannot match tool events to correct session when events are interleaved
Cannot clear notifications for a specific session when interaction ends
System messages arriving between tool events can break matching
Evidence From User:
"Currently it seems like SystemMessageEvents at least seem to cause orphaned tool call notifications in the chat."
Why: When a SystemMessage event arrives between tool_select_delta and tool_call, the code can't distinguish which session's tool call it belongs to.
Problem 2: No Cleanup on Interaction End
File: packages/core/src/events/EventStreamProcessor.ts line ~360
Current Code:
privatehandleInteraction(event: InteractionEvent): void{if(event.started){this.messageBuilder.reset();this.toolCallManager.reset();// Only reset on NEW interaction}else{Logger.info(`[EventStreamProcessor] Interaction ended: ${event.id}`);// ❌ NO CLEANUP!}}
Problem: When an interaction ends (event.started === false), any tool calls still in "preparing" or "executing" state are orphaned but never cleared.
User Observation:
"When our last interaction completed there were three orphaned tool call notifications in the list: 'Agent is using workspace_read.', 'Agent is using act_oneshot.', 'Agent is using workspace_read'"
Why This Matters: If the user's turn is starting, the agent CANNOT be using tools. This is a guaranteed cleanup point that should clear ALL active notifications as a safety net.
Problem 4: Tool Calls Attached at Wrong Time ⚠️⚠️ CRITICAL
File: packages/core/src/events/EventStreamProcessor.ts line ~395
Current Flow (INCORRECT):
1. text_delta events → Building assistant message
2. completion event → Finalize message
handleCompletion() {
const completedToolCalls = toolCallManager.getCompletedToolCalls();
// ❌ Attaches tool calls from PREVIOUS interaction!
message = messageBuilder.finalize({ toolCalls, toolResults });
}
3. tool_select_delta → Tool starting
4. tool_call (active=true) → Tool executing
5. tool_call (active=false) → Tool completed
toolCallManager stores the completed calls
6. Next text_delta → New message starts
7. Next completion → Finalize WITH tool calls from step 5
// ❌ Tool calls appear on WRONG message!
Root Cause: Tool calls are attached in handleCompletion() which gets tool calls from ToolCallManager.getCompletedToolCalls(). But these are from the PREVIOUS interaction, not the current message.
Correct Flow Should Be:
1-2. Message finalized WITHOUT tool calls (correct - tools haven't run yet)
3-4. Tool events arrive
5. tool_call (active=false) → Tool completed
→ IMMEDIATELY find PREVIOUS assistant/thought message
→ ATTACH tool calls to THAT message
→ EMIT message-updated event
6-7. Next message finalized WITHOUT old tool calls
Problem 5: Resumed Sessions Explicitly Skip Tool Calls
File: packages/core/src/events/EventStreamProcessor.ts line ~1250+
Current Code:
privateprocessAssistantMessageForResume(...){if(isToolUseBlockParam(block)){if(block.name==='think'){// ... handle think tool ...}if(this.isDelegationTool(block.name)){// ... handle delegation ...}// Regular tool calls - skip for now in resumemessagesConsumed=1;// ❌ EXPLICITLY SKIPPED!}}
Problem: Tool calls in resumed sessions are intentionally skipped with a "TODO" comment. This is why tool calls don't appear in resumed chat sessions.
What's Needed: Extract tool_use blocks, match with tool_result blocks from next user message, attach to message.metadata.
Problem 6: Thought Messages Missing Footer
File: packages/ui/src/components/chat/Message.tsx
Problem: ThoughtMessage component doesn't use MessageFooter, so tool calls aren't displayed even if properly attached.
Note: This is a UI issue, but included here for completeness.
Solution Architecture
Fix 1: Add Session Tracking to ToolCallManager ⚠️ FOUNDATION
exportclassToolCallManager{// Keys are now: `${session_id}:${tool_call_id}`privateactiveTools: Map<string,ToolNotification>=newMap();privatemakeKey(sessionId: string,toolCallId: string): string{return`${sessionId}:${toolCallId}`;}}
1.3 Update all methods to use session_id:
onToolSelect(event: ToolSelectDeltaEvent): ToolNotification{consttoolCall=event.tool_calls[0];if(!toolCall)thrownewError('ToolSelectDeltaEvent has no tool calls');constnotification: ToolNotification={id: toolCall.id,sessionId: event.session_id,// ✅ Extract from eventtoolName: toolCall.name,status: 'preparing',timestamp: newDate(),arguments: JSON.stringify(toolCall.input)};constkey=this.makeKey(event.session_id,toolCall.id);// ✅ Use session_idthis.activeTools.set(key,notification);Logger.info(`[ToolCallManager] Tool selected: ${toolCall.name}`,{sessionId: event.session_id,id: toolCall.id});returnnotification;}onToolCallActive(event: ToolCallEvent): ToolNotification|null{if(!event.active)returnnull;consttoolCall=event.tool_calls[0];if(!toolCall)returnnull;constkey=this.makeKey(event.session_id,toolCall.id);// ✅ Use session_idconstnotification=this.activeTools.get(key);if(notification){notification.status='executing';Logger.info(`[ToolCallManager] Tool executing: ${notification.toolName}`,{sessionId: event.session_id,id: toolCall.id});returnnotification;}// Create notification if not found (edge case handling)constnewNotification: ToolNotification={id: toolCall.id,sessionId: event.session_id,// ✅ Extract from eventtoolName: toolCall.name,status: 'executing',timestamp: newDate(),arguments: JSON.stringify(toolCall.input)};this.activeTools.set(key,newNotification);returnnewNotification;}onToolCallComplete(event: ToolCallEvent): ToolCallWithResult[]{if(event.active)return[];constnewlyCompleted: ToolCallWithResult[]=[];event.tool_calls.forEach(toolCall=>{constkey=this.makeKey(event.session_id,toolCall.id);// ✅ Use session_id// Mark as complete and remove from activeconstnotification=this.activeTools.get(key);if(notification){notification.status='complete';}this.activeTools.delete(key);// Find result and add to completed listconstresult=event.tool_results?.find(r=>r.tool_use_id===toolCall.id);constcompletedCall: ToolCallWithResult={ ...toolCall, result };this.completedToolCalls.push(completedCall);newlyCompleted.push(completedCall);Logger.info(`[ToolCallManager] Tool completed: ${toolCall.name}`,{sessionId: event.session_id,id: toolCall.id,hasResult: !!result});});returnnewlyCompleted;}
1.4 Add session-specific cleanup methods:
/** * Clear tool notifications for a specific session * Used when an interaction ends or session changes */clearSessionNotifications(sessionId: string): void{constkeysToDelete: string[]=[];for(const[key,notification]ofthis.activeTools){if(notification.sessionId===sessionId){keysToDelete.push(key);Logger.debug(`[ToolCallManager] Clearing notification for ${notification.toolName} in session ${sessionId}`);}}keysToDelete.forEach(key=>this.activeTools.delete(key));if(keysToDelete.length>0){Logger.info(`[ToolCallManager] Cleared ${keysToDelete.length} orphaned notifications for session ${sessionId}`);}}/** * Clear all active tool notifications * Used as safety net when user_turn_start arrives */clearAllActiveNotifications(): void{constcount=this.activeTools.size;this.activeTools.clear();if(count>0){Logger.info(`[ToolCallManager] Cleared ${count} active notifications (user turn start safety net)`);}}
Rationale: Session tracking is the FOUNDATION for all other fixes. Without it, we cannot:
Match tool events across interleaved streams
Clear notifications for specific sessions
Handle sub-sessions correctly
Fix 2: Clear Notifications on Interaction End
File: packages/core/src/events/EventStreamProcessor.ts line ~360
Changes Required:
privatehandleInteraction(event: InteractionEvent): void{if(event.started){Logger.info(`[EventStreamProcessor] Interaction started: ${event.id}`);this.messageBuilder.reset();this.toolCallManager.reset();}else{Logger.info(`[EventStreamProcessor] Interaction ended: ${event.id}`);// ✅ Clear any orphaned tool notifications for this session// If interaction ended, any tools still "active" are orphanedthis.toolCallManager.clearSessionNotifications(event.session_id);// Emit event for UI updatesthis.sessionManager.emit('interaction-ended',{sessionId: event.session_id,interactionId: event.id});}}
Rationale: When InteractionEvent with started=false arrives, it signals the interaction has definitively ended. Any tools still "active" at this point are orphaned and should be cleaned up.
/** * Handle user turn start - safety net to clear orphaned notifications */privatehandleUserTurnStart(event: UserTurnStartEvent): void{
Logger.debug('[EventStreamProcessor] User turn started - clearing all tool notifications (safety net)');// ✅ Clear ALL active tool notifications as safety net// If user's turn is starting, agent CANNOT be using toolsthis.toolCallManager.clearAllActiveNotifications();// Emit event for UIthis.sessionManager.emit('user-turn-start',{});}
Rationale:
User turn start is a GUARANTEED signal that agent is not active
Acts as safety net to catch any orphaned notifications
User explicitly mentioned this: "If it's the users turn, there's ZERO chance they're ever going to complete"
Fix 4: Attach Tool Calls to Correct Message ⚠️ COMPLEX
4.1 Modify handleToolCall() - the ONLY place that matters for results:
privatehandleToolCall(event: ToolCallEvent): void{// ... existing code for think tool handling ...if(event.active){// Tool is executing - UX update onlyconstnotification=this.toolCallManager.onToolCallActive(event);if(notification){this.sessionManager.emit('tool-notification',notification);}}else{// ⚠️ Tool completed - THIS IS THE IMPORTANT PART// Step 1: Remove the notificationevent.tool_calls.forEach(tc=>{this.sessionManager.emit('tool-notification-removed',`${event.session_id}:${tc.id}`);});// Step 2: Store completed tool calls in managerconstcompletedToolCalls=this.toolCallManager.onToolCallComplete(event);// Step 3: Try to attach to PREVIOUS assistant/thought in SAME session_idconstattached=this.attachToolCallsToPreviousMessage(event.session_id,// Match by session_idevent.tool_calls,event.tool_results||[]);// Step 4: If no previous message, buffer for next message in this session_idif(!attached){Logger.debug(`[EventStreamProcessor] No previous message found - buffering tool calls for next message in session ${event.session_id}`);this.sessionManager.bufferPendingToolCalls(event.session_id,completedToolCalls);}// Step 5: Emit completion event for any listenersthis.sessionManager.emit('tool-call-complete',{sessionId: event.session_id,toolCalls: event.tool_calls,toolResults: event.tool_results});}}
4.2 Add method to find and attach to previous message:
/** * Find PREVIOUS assistant or thought message in SAME session and attach tool calls * Returns true if attached, false if no suitable message found (need to buffer) */privateattachToolCallsToPreviousMessage(sessionId: string,toolCalls: ToolCall[],toolResults: ToolResult[]): boolean {constsession=this.sessionManager.getCurrentSession();if(!session||!session.messages||session.messages.length===0){returnfalse;}// Search backward for most recent assistant or thought message// that matches this session_idfor(leti=session.messages.length-1;i>=0;i--){constmsg=session.messages[i];// Check if message is from the same session (important for sub-sessions)constisSameSession=msg.metadata?.sessionId===sessionId;if(!isSameSession)continue;// Check if it's an assistant or thought messageconstisAssistant=msg.role==='assistant';constisThought=msg.role==='assistant (thought)'||msg.isThought===true;if(isAssistant||isThought){Logger.info(`[EventStreamProcessor] Attaching ${toolCalls.length} tool calls to previous ${msg.role} message at index ${i}`);// Attach to message metadatamsg.metadata=msg.metadata||{};msg.metadata.toolCalls=toolCalls;msg.metadata.toolResults=toolResults;// Emit update so UI can refresh this specific messagethis.sessionManager.emit('message-updated',{sessionId: sessionId,messageIndex: i,message: msg});returntrue;}}// No previous assistant/thought found in this sessionreturnfalse;}
4.3 Modify handleCompletion() to attach any buffered tool calls:
privatehandleCompletion(event: CompletionEvent): void{if(!event.running&&this.messageBuilder.hasCurrentMessage()){// Check if there are buffered tool calls for THIS session// (from rare case where interaction started with tool calls)lettoolCalls: ToolCall[]|undefined;lettoolResults: ToolResult[]|undefined;if(this.sessionManager.hasPendingToolCalls(event.session_id)){constbuffered=this.sessionManager.getPendingToolCalls(event.session_id);toolCalls=buffered.map(tc=>({id: tc.id,type: tc.type,name: tc.name,input: tc.input}))asToolCall[];toolResults=buffered.filter(tc=>tc.result).map(tc=>tc.result!);Logger.debug(`[EventStreamProcessor] Attaching ${buffered.length} buffered tool calls to current message in session ${event.session_id}`);}// Finalize message with buffered tool calls ONLY (if any exist for this session)constmessage=this.messageBuilder.finalize({inputTokens: event.input_tokens,outputTokens: event.output_tokens,stopReason: event.stop_reason,toolCalls: toolCalls,toolResults: toolResults});// ... rest of existing completion handling ...}}
4.4 Add session-aware buffering to SessionManager:File: packages/core/src/session/SessionManager.ts
exportclassSessionManagerextendsEventEmitter{privatecurrentSession: ChatSession|null=null;// Buffer tool calls per session (key = session_id)privatependingToolCallsBySession: Map<string,ToolCallWithResult[]>=newMap();/** * Buffer tool calls for a specific session * Used when tool calls complete but no previous message exists (rare) */bufferPendingToolCalls(sessionId: string,toolCalls: ToolCallWithResult[]): void{constexisting=this.pendingToolCallsBySession.get(sessionId)||[];existing.push(...toolCalls);this.pendingToolCallsBySession.set(sessionId,existing);Logger.debug(`[SessionManager] Buffered ${toolCalls.length} tool calls for session ${sessionId}`);}/** * Get and clear buffered tool calls for a specific session */getPendingToolCalls(sessionId: string): ToolCallWithResult[]{constbuffered=this.pendingToolCallsBySession.get(sessionId)||[];this.pendingToolCallsBySession.delete(sessionId);returnbuffered;}/** * Check if there are pending buffered tool calls for a specific session */hasPendingToolCalls(sessionId: string): boolean{constbuffered=this.pendingToolCallsBySession.get(sessionId);returnbuffered!==undefined&&buffered.length>0;}/** * Clear all pending tool calls for a session (on session change) */clearPendingToolCalls(sessionId: string): void{this.pendingToolCallsBySession.delete(sessionId);}/** * Clear ALL pending tool calls (on disconnect, etc.) */clearAllPendingToolCalls(): void{this.pendingToolCallsBySession.clear();}}
Rationale:
Buffers tool calls PER SESSION (handles sub-sessions correctly)
Default: Attach to previous message (agent explains THEN uses tools)
Fallback: Attach to next message (rare case where interaction starts with tools)
Session-aware: Won't mix up tool calls from different sessions
Fix 5: Extract Tool Calls in Resumed Sessions
File: packages/core/src/events/EventStreamProcessor.ts line ~1250+
Changes Required:
Modify processAssistantMessageForResume():
privateprocessAssistantMessageForResume(message: MessageParam,nextMessage: MessageParam|undefined,sessionId: string
): { messages: Message[],messagesConsumed: number }{constmessages: Message[]=[];letmessagesConsumed=0;if(message.content&&Array.isArray(message.content)){lethasTextContent=false;consttextParts: string[]=[];consttoolCalls: ToolCall[]=[];consttoolResults: ToolResult[]=[];for(constblockofmessage.content){if(isTextBlockParam(block)){hasTextContent=true;textParts.push(block.text);}elseif(isToolUseBlockParam(block)){// THINK TOOL - existing special handlingif(block.name==='think'){// ... existing think tool code ...continue;}// DELEGATION TOOLS - existing special handlingif(this.isDelegationTool(block.name)){// ... existing delegation code ...continue;}// ✅ REGULAR TOOL CALLS - NOW EXTRACT THEMLogger.debug(`[EventStreamProcessor] Extracting tool call: ${block.name} (${block.id})`);toolCalls.push({id: block.id,type: block.typeas'tool_use',name: block.name,input: block.input});// ✅ Find corresponding result in next messageif(nextMessage&&nextMessage.role==='user'&&Array.isArray(nextMessage.content)){for(constresultBlockofnextMessage.content){if('type'inresultBlock&&resultBlock.type==='tool_result'){consttoolResult=resultBlockasany;if(toolResult.tool_use_id===block.id){Logger.debug(`[EventStreamProcessor] Found matching tool result for ${block.id}`);toolResults.push({type: 'tool_result',tool_use_id: toolResult.tool_use_id,content: toolResult.content||'',is_error: toolResult.is_error});}}}}// ✅ Consume the tool result messagemessagesConsumed=1;}}// ✅ Create message with tool calls attachedif(hasTextContent||toolCalls.length>0){constcombinedText=textParts.join('');constmsg: Message={role: 'assistant',content: combinedText||'[Tool execution]',timestamp: newDate().toISOString(),format: 'text',metadata: {}};// ✅ Attach tool calls if presentif(toolCalls.length>0){msg.metadata!.toolCalls=toolCalls;msg.metadata!.toolResults=toolResults;Logger.info(`[EventStreamProcessor] Attached ${toolCalls.length} tool calls to resumed assistant message`);}messages.push(msg);}}else{// ... existing simple text handling ...}return{ messages, messagesConsumed };}
Rationale:
Removes the "skip for now" TODO code
Extracts tool_use blocks from assistant messages
Matches with tool_result blocks from next user message
Attaches to message.metadata just like streaming messages
// In useChat hook, add to useEffect that sets up listenersconsthandleMessageUpdated=useCallback((event: unknown)=>{constupdateEvent=eventas{sessionId: string;messageIndex: number;message: MessageChatItem;};Logger.debug('[useChat] Message updated event:',updateEvent);setMessages(prev=>{constnewMessages=[...prev];if(updateEvent.messageIndex>=0&&updateEvent.messageIndex<newMessages.length){newMessages[updateEvent.messageIndex]={
...updateEvent.message,type: 'message',id: updateEvent.message.id||`msg-updated-${Date.now()}`};}returnnewMessages;});},[]);// Add to listenerssessionManager.on('message-updated',handleMessageUpdated);// Add to cleanupreturn()=>{sessionManager.off('message-updated',handleMessageUpdated);// ... other cleanup ...};
Rationale: Allows UI to reactively update when tool calls are attached to previous messages.
Implementation Order & Risk Assessment
Phase 1: Foundation (Session Tracking) ⚠️ CRITICAL FIRST
Risk: Medium - Changes key data structures but isolated
Files:
Verify all tool calls visible in MessageFooter dropdowns
Verify tool results displayed correctly
E2E Tests
User Turn Safety Net:
Start agent interaction with tools
Force user turn to start (send message)
Verify ALL tool notifications cleared
Verify no orphaned "Agent is using" in UI
Multiple Tools:
Send message requiring 5+ tools
Verify all tools tracked separately
Verify all attach to correct message
Verify notifications clear properly
Success Criteria
✅ No orphaned notifications: Tool notifications clear when user turn starts
✅ Correct message attachment: Tool calls appear on PREVIOUS assistant/thought message
✅ Sub-session isolation: Tool calls from sub-sessions don't interfere with main session
✅ Interrupted sequences handled: System messages and other events don't break tool tracking
✅ Resumed sessions work: Tool calls visible and correct in resumed chat sessions
✅ Thought messages have footer: Tool calls visible in thought message footers
✅ No regressions: All existing tool call functionality preserved
✅ Build passes: No TypeScript errors, all tests pass
✅ Clean logs: Appropriate logging for debugging without noise
Open Questions & Notes
Q1: What if tool_call complete event is lost?
A: Interaction end and user_turn_start provide cleanup safety nets. Acceptable to leave orphaned until next safety net.
Q2: Should buffering persist across session changes?
A: NO - SessionManager should clearPendingToolCalls() on session change to avoid cross-contamination.
Q3: Can we have nested sub-sessions?
A: YES - session_id tracking handles this. Each session tracked independently by session_id.
Q4: What's the maximum tool concurrency?
A: Unknown - need to confirm with server team. Current design handles any number via Map.
Q5: Should we add tool call analytics?
A: Out of scope for this fix. Can add in separate enhancement task.
Rollback Plan
If issues arise during implementation:
Phase 1 Rollback: Revert ToolCallManager changes, keep old key structure
Phase 2 Rollback: Comment out cleanup calls in handlers
Phase 3 Rollback: Revert to old handleCompletion() logic
Phase 4 Rollback: Re-add "skip for now" comment
Phase 5 Rollback: Remove MessageFooter from ThoughtMessage
Feature Flag Option: Consider adding feature flag for Phase 3 (message attachment) if concerns about stability.
Timeline Estimate
Phase 1: 4-6 hours (foundation changes + unit tests)