diff --git a/navi/core/agent.py b/navi/core/agent.py index cafadfc..0275924 100644 --- a/navi/core/agent.py +++ b/navi/core/agent.py @@ -600,7 +600,7 @@ """ tool_map = {t.name: t for t in tools} for tc in turn_tool_calls: - yield ToolStarted(tool_name=tc.name, arguments=tc.arguments) + yield ToolStarted(tool_name=tc.name, arguments=tc.arguments, tool_call_id=tc.id) sink: asyncio.Queue = asyncio.Queue() sink_token = current_event_sink.set(sink) @@ -647,6 +647,7 @@ yield ToolEvent( tool_name=tc.name, arguments=tc.arguments, result="Tool execution was stopped by the user.", success=False, + tool_call_id=tc.id, ) session.messages.append(Message( role="tool", content="Tool execution was stopped by the user.", @@ -664,6 +665,7 @@ tool_event = ToolEvent( tool_name=tc.name, arguments=tc.arguments, result=f"Error: {r}", success=False, + tool_call_id=tc.id, ) msg = Message(role="tool", content=f"Error: {r}", tool_call_id=tc.id, name=tc.name, metadata={}) image_msg = None diff --git a/navi/core/events.py b/navi/core/events.py index 6449d7f..33f5ffc 100644 --- a/navi/core/events.py +++ b/navi/core/events.py @@ -10,6 +10,7 @@ tool_name: str arguments: dict is_subagent: bool = False # True when emitted from inside run_ephemeral + tool_call_id: str = "" # Stable id for client-side tool_started → tool_call pairing def to_wire(self) -> dict: return { @@ -17,6 +18,7 @@ "tool": self.tool_name, "args": self.arguments, "is_subagent": self.is_subagent, + "tool_call_id": self.tool_call_id, } @@ -30,6 +32,7 @@ success: bool is_subagent: bool = False # True when emitted from inside run_ephemeral metadata: dict = field(default_factory=dict) # Extra data for client rendering + tool_call_id: str = "" # Stable id for client-side tool_started → tool_call pairing def to_wire(self) -> dict: return { @@ -40,6 +43,7 @@ "success": self.success, "is_subagent": self.is_subagent, "metadata": self.metadata, + "tool_call_id": self.tool_call_id, } diff --git a/navi/core/subagent_runner.py b/navi/core/subagent_runner.py index 72f8210..1c6c202 100644 --- a/navi/core/subagent_runner.py +++ b/navi/core/subagent_runner.py @@ -350,6 +350,7 @@ tool_name=tc.name, arguments=tc.arguments, is_subagent=True, + tool_call_id=tc.id, ) ) @@ -398,6 +399,7 @@ result=content, success=success, is_subagent=True, + tool_call_id=tc.id, ) ) diff --git a/navi/core/tool_executor.py b/navi/core/tool_executor.py index 4cc77c9..7c049b5 100644 --- a/navi/core/tool_executor.py +++ b/navi/core/tool_executor.py @@ -97,7 +97,8 @@ if tool is None: content = f"Error: tool '{tc.name}' not found." event = ToolEvent(tool_name=tc.name, arguments=tc.arguments, - result=content, success=False) + result=content, success=False, + tool_call_id=tc.id) else: log.info("tool.execute", tool=resolved_name, requested_tool=tc.name, args=tc.arguments) middlewares = getattr(self._tools, "_middlewares", []) @@ -110,7 +111,8 @@ metadata = result.metadata or {} event = ToolEvent(tool_name=resolved_name, arguments=tc.arguments, result=content, success=result.success, - metadata=metadata) + metadata=metadata, + tool_call_id=tc.id) if result.success and result.metadata and result.metadata.get("is_image"): b64 = result.metadata.get("base64") if b64: diff --git a/tests/unit/core/test_events.py b/tests/unit/core/test_events.py index b3b1222..e023397 100644 --- a/tests/unit/core/test_events.py +++ b/tests/unit/core/test_events.py @@ -30,6 +30,7 @@ "tool": "fs", "args": {"action": "read"}, "is_subagent": False, + "tool_call_id": "", } def test_to_wire_subagent(self): @@ -54,6 +55,7 @@ "success": True, "is_subagent": False, "metadata": {"size": 42}, + "tool_call_id": "", } diff --git a/webclient/src/stores/chat.js b/webclient/src/stores/chat.js index 5f787a7..524226f 100644 --- a/webclient/src/stores/chat.js +++ b/webclient/src/stores/chat.js @@ -312,6 +312,7 @@ const card = { kind: 'tool', id: `tool_${Date.now()}`, + toolCallId: data.tool_call_id ?? null, name: data.tool, args: data.args, result: null, @@ -338,7 +339,10 @@ let step = null for (let i = spawn.steps.length - 1; i >= 0; i--) { const t = spawn.steps[i] - if (t.kind === 'tool' && t.name === data.tool && t.pending) { step = t; break } + const match = data.tool_call_id + ? t.toolCallId === data.tool_call_id + : t.kind === 'tool' && t.name === data.tool && t.pending + if (match) { step = t; break } } if (step) { step.result = data.result @@ -352,7 +356,10 @@ let card = null for (let i = msg.tools.length - 1; i >= 0; i--) { const t = msg.tools[i] - if (t.kind === 'tool' && t.name === data.tool && t.pending) { card = t; break } + const match = data.tool_call_id + ? t.toolCallId === data.tool_call_id + : t.kind === 'tool' && t.name === data.tool && t.pending + if (match) { card = t; break } } if (card) { card.result = data.result