Skip to content

feat: tool call events refactor#175

Open
sr-anam wants to merge 2 commits intomainfrom
tool_call_events_refactor
Open

feat: tool call events refactor#175
sr-anam wants to merge 2 commits intomainfrom
tool_call_events_refactor

Conversation

@sr-anam
Copy link
Contributor

@sr-anam sr-anam commented Feb 16, 2026

Summary by cubic

Refactored tool calls to a start/completed/failed lifecycle with a centralized ToolCallManager and a simple handler API. WebRTC TOOL_CALL_* events are handled end-to-end; client tool compatibility is preserved by emitting the legacy ClientToolEvent on start and by still accepting legacy CLIENT_TOOL_EVENT messages.

  • New Features

    • ToolCallManager.registerHandler(eventName | '*') returns an unregister; runs onStart/onComplete/onFail; tracks execution time; auto-completes client tools; aggregates handler errors as failed events.
    • AnamClient.registerToolCallHandler(eventName, handler) to register app handlers.
    • StreamingClient routes TOOL_CALL_* to ToolCallManager and, for tool_type='client', emits WEBRTC_CLIENT_TOOL_EVENT_RECEIVED and public CLIENT_TOOL_EVENT_RECEIVED from started events; legacy CLIENT_TOOL_EVENT messages remain supported.
    • ToolCallHandler and ToolCall payload types are exported.
  • Refactors

    • Added DataChannelMessage.TOOL_CALL_STARTED/COMPLETED/FAILED with matching internal events and callbacks.
    • Added WebRtcToolCallStarted/Completed/Failed types; retained legacy ClientToolEvent converters (including started->ClientToolEvent).
    • ToolCallManager remains internal (not exported from modules/index).
    • Clears pending tool calls when the connection closes.

Written for commit f3fd675. Summary will update on new commits.

Copilot AI review requested due to automatic review settings February 16, 2026 10:06
@sr-anam sr-anam changed the title Tool call events refactor feat: tool call events refactor Feb 16, 2026
};

return () => {
delete this.handlers[eventName][handlerId];

Check warning

Code scanning / CodeQL

Prototype-polluting assignment Medium

This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.

Copilot Autofix

AI 2 days ago

In general, to fix prototype-pollution risks from user-controlled property names, avoid using untrusted strings as keys on plain JavaScript objects that inherit from Object.prototype. Prefer Map/WeakMap, or at least validate/restrict keys against dangerous values like __proto__, constructor, and prototype.

For this code, the most direct, behavior-preserving fix is to change the handler registries from Record<string, ...> objects into Map instances. Map does not have the special prototype-related keys and is safe against attacks using __proto__ etc. We can keep the existing API the same: registerHandler still takes eventName: string, handler lookup by tool_name remains conceptually similar, and the return value (an unregister function) still works. Concretely:

  • Change the fields:
    • handlers from Record<string, Record<string, ToolCallHandler>> = {} to Map<string, Map<string, ToolCallHandler>> = new Map().
    • genericHandlers from Record<string, ToolCallHandler> = {} to Map<string, ToolCallHandler> = new Map().
    • pendingCalls from Record<string, PendingToolCall> = {} to Map<string, PendingToolCall> = new Map().
  • Update all uses:
    • clearPendingCalls() becomes this.pendingCalls.clear().
    • In registerHandler:
      • For the wildcard case (eventName === '*'), use this.genericHandlers.set(handlerId, handler) and this.genericHandlers.delete(handlerId).
      • For normal events: retrieve or create an inner Map with let handlersForEvent = this.handlers.get(eventName) ?? new Map(); and then handlersForEvent.set(handlerId, handler); this.handlers.set(eventName, handlersForEvent);. The unregister function then does handlersForEvent.delete(handlerId) (or re-fetches the map safely).
    • In processToolCallStartedEvent:
      • Replace const handlersForTool = this.handlers[tool_name] || {}; with something like const handlersForTool = this.handlers.get(tool_name); const handlers = handlersForTool ? Array.from(handlersForTool.values()) : [];.
      • Replace Object.values(this.genericHandlers) with Array.from(this.genericHandlers.values()).
      • Replace this.pendingCalls[toolCallEvent.tool_call_id] = { ... } with this.pendingCalls.set(toolCallEvent.tool_call_id, { ... }).
  • Elsewhere in ToolCallManager (not fully shown), any other use of this.handlers, this.genericHandlers, or this.pendingCalls must be converted to appropriate Map operations (get, set, delete, has, values, etc.). Because you asked me to only touch code shown, I’ll limit explicit replacements to the visible parts, but you should mirror this pattern for other references in the same file.

No new imports are required; Map is a built-in.

Suggested changeset 1
src/modules/ToolCallManager.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/modules/ToolCallManager.ts b/src/modules/ToolCallManager.ts
--- a/src/modules/ToolCallManager.ts
+++ b/src/modules/ToolCallManager.ts
@@ -28,40 +28,50 @@
 };
 
 export class ToolCallManager {
-  private handlers: Record<string, Record<string, ToolCallHandler>> = {};
-  private genericHandlers: Record<string, ToolCallHandler> = {};
-  private pendingCalls: Record<string, PendingToolCall> = {};
+  private handlers: Map<string, Map<string, ToolCallHandler>> = new Map();
+  private genericHandlers: Map<string, ToolCallHandler> = new Map();
+  private pendingCalls: Map<string, PendingToolCall> = new Map();
 
   clearPendingCalls(): void {
-    this.pendingCalls = {};
+    this.pendingCalls.clear();
   }
 
   registerHandler(eventName: string, handler: ToolCallHandler): () => void {
     const handlerId = Math.random().toString(36).substring(2, 15);
 
     if (eventName === '*') {
-      this.genericHandlers[handlerId] = handler;
+      this.genericHandlers.set(handlerId, handler);
       return () => {
-        delete this.genericHandlers[handlerId];
+        this.genericHandlers.delete(handlerId);
       };
     }
 
-    this.handlers[eventName] = {
-      ...this.handlers[eventName],
-      [handlerId]: handler,
-    };
+    let handlersForEvent = this.handlers.get(eventName);
+    if (!handlersForEvent) {
+      handlersForEvent = new Map<string, ToolCallHandler>();
+      this.handlers.set(eventName, handlersForEvent);
+    }
+    handlersForEvent.set(handlerId, handler);
 
     return () => {
-      delete this.handlers[eventName][handlerId];
+      const currentHandlersForEvent = this.handlers.get(eventName);
+      if (currentHandlersForEvent) {
+        currentHandlersForEvent.delete(handlerId);
+        if (currentHandlersForEvent.size === 0) {
+          this.handlers.delete(eventName);
+        }
+      }
     };
   }
 
   async processToolCallStartedEvent(toolCallEvent: WebRtcToolCallStartedEvent) {
     const { tool_name, timestamp } = toolCallEvent;
 
-    const handlersForTool = this.handlers[tool_name] || {};
-    const handlers = Object.values(handlersForTool);
-    const genericHandlers = Object.values(this.genericHandlers);
+    const handlersForTool = this.handlers.get(tool_name);
+    const handlers = handlersForTool
+      ? Array.from(handlersForTool.values())
+      : [];
+    const genericHandlers = Array.from(this.genericHandlers.values());
 
     const payload: ToolCallStartedPayload = {
       eventUid: toolCallEvent.event_uid,
@@ -76,10 +66,10 @@
     const parsedTimestamp = new Date(timestamp);
 
     // Store in pending calls before invoking handlers
-    this.pendingCalls[toolCallEvent.tool_call_id] = {
+    this.pendingCalls.set(toolCallEvent.tool_call_id, {
       payload: payload,
       timestamp: parsedTimestamp.getTime(),
-    };
+    });
 
     const errors: Error[] = [];
     const results: (string | void)[] = [];
EOF
Copilot is powered by AI and may make mistakes. Always verify output.
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 10 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/modules/ToolCallManager.ts">

<violation number="1" location="src/modules/ToolCallManager.ts:249">
P2: Completed tool call events should expose the tool result, not the original arguments. This mapping loses the output payload for clients.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors WebRTC “tool call” event handling by introducing explicit started/completed/failed event types and routing them through a centralized ToolCallManager, with a public registration API exposed on AnamClient.

Changes:

  • Added new WebRTC tool-call event models and corresponding data-channel message types.
  • Introduced tool-call payload types plus a ToolCallManager to process/dispatch tool-call lifecycle events.
  • Wired StreamingClient and AnamClient to use the shared ToolCallManager and exposed registerToolCallHandler.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/types/streaming/index.ts Adds exports for tool-call payload types (streaming barrel update).
src/types/streaming/WebRtcToolCallEvent.ts Introduces WebRTC wire-format interfaces for tool-call started/completed/failed.
src/types/streaming/ToolCallEvents.ts Defines SDK-facing tool-call payload shapes (camelCase).
src/types/streaming/DataChannelMessage.ts Adds new data channel message type constants for tool-call lifecycle events.
src/types/events/internal/InternalEventCallbacks.ts Extends internal callback typings to include tool-call lifecycle events.
src/types/events/internal/InternalEvent.ts Adds new internal event enum entries for tool-call lifecycle events.
src/modules/index.ts Removes ToolCallManager from the modules barrel export.
src/modules/ToolCallManager.ts Implements handler registration + tool-call lifecycle processing, payload conversion, and execution timing.
src/modules/StreamingClient.ts Routes tool-call data-channel messages into internal events and ToolCallManager; maintains client-tool backward emission on completed events.
src/AnamClient.ts Instantiates and passes ToolCallManager into StreamingClient; exposes registerToolCallHandler.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@sr-anam sr-anam force-pushed the tool_call_events_refactor branch 6 times, most recently from 653cddf to 0c7af9c Compare February 16, 2026 15:30
@sr-anam sr-anam force-pushed the tool_call_events_refactor branch from 0c7af9c to f3fd675 Compare February 16, 2026 20:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments