diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index 147327d..8949844 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -208,7 +208,7 @@ class AgentLoop: await on_progress(self._tool_hint(response.tool_calls), tool_hint=True) tool_call_dicts = [ - self._build_tool_call_message(tc) + tc.to_openai_tool_call() for tc in response.tool_calls ] messages = self.context.add_assistant_message( @@ -249,22 +249,6 @@ class AgentLoop: return final_content, tools_used, messages - @staticmethod - def _build_tool_call_message(tc: Any) -> dict[str, Any]: - tool_call = { - "id": tc.id, - "type": "function", - "function": { - "name": tc.name, - "arguments": json.dumps(tc.arguments, ensure_ascii=False) - } - } - if getattr(tc, "provider_specific_fields", None): - tool_call["provider_specific_fields"] = tc.provider_specific_fields - if getattr(tc, "function_provider_specific_fields", None): - tool_call["function"]["provider_specific_fields"] = tc.function_provider_specific_fields - return tool_call - async def run(self) -> None: """Run the agent loop, dispatching messages as tasks to stay responsive to /stop.""" self._running = True diff --git a/nanobot/agent/subagent.py b/nanobot/agent/subagent.py index 5f98272..0049f9a 100644 --- a/nanobot/agent/subagent.py +++ b/nanobot/agent/subagent.py @@ -135,7 +135,7 @@ class SubagentManager: if response.has_tool_calls: # Add assistant message with tool calls tool_call_dicts = [ - self._build_tool_call_message(tc) + tc.to_openai_tool_call() for tc in response.tool_calls ] messages.append({ @@ -224,22 +224,6 @@ Stay focused on the assigned task. Your final response will be reported back to return "\n\n".join(parts) - @staticmethod - def _build_tool_call_message(tc: Any) -> dict[str, Any]: - tool_call = { - "id": tc.id, - "type": "function", - "function": { - "name": tc.name, - "arguments": json.dumps(tc.arguments, ensure_ascii=False), - }, - } - if getattr(tc, "provider_specific_fields", None): - tool_call["provider_specific_fields"] = tc.provider_specific_fields - if getattr(tc, "function_provider_specific_fields", None): - tool_call["function"]["provider_specific_fields"] = tc.function_provider_specific_fields - return tool_call - async def cancel_by_session(self, session_key: str) -> int: """Cancel all subagents for the given session. Returns count cancelled.""" tasks = [self._running_tasks[tid] for tid in self._session_tasks.get(session_key, []) diff --git a/nanobot/providers/base.py b/nanobot/providers/base.py index b41ce28..391f903 100644 --- a/nanobot/providers/base.py +++ b/nanobot/providers/base.py @@ -1,6 +1,7 @@ """Base LLM provider interface.""" import asyncio +import json from abc import ABC, abstractmethod from dataclasses import dataclass, field from typing import Any @@ -17,6 +18,22 @@ class ToolCallRequest: provider_specific_fields: dict[str, Any] | None = None function_provider_specific_fields: dict[str, Any] | None = None + def to_openai_tool_call(self) -> dict[str, Any]: + """Serialize to an OpenAI-style tool_call payload.""" + tool_call = { + "id": self.id, + "type": "function", + "function": { + "name": self.name, + "arguments": json.dumps(self.arguments, ensure_ascii=False), + }, + } + if self.provider_specific_fields: + tool_call["provider_specific_fields"] = self.provider_specific_fields + if self.function_provider_specific_fields: + tool_call["function"]["provider_specific_fields"] = self.function_provider_specific_fields + return tool_call + @dataclass class LLMResponse: diff --git a/tests/test_gemini_thought_signature.py b/tests/test_gemini_thought_signature.py index db57c7f..bc4132c 100644 --- a/tests/test_gemini_thought_signature.py +++ b/tests/test_gemini_thought_signature.py @@ -1,6 +1,5 @@ from types import SimpleNamespace -from nanobot.agent.loop import AgentLoop from nanobot.providers.base import ToolCallRequest from nanobot.providers.litellm_provider import LiteLLMProvider @@ -38,7 +37,7 @@ def test_litellm_parse_response_preserves_tool_call_provider_fields() -> None: assert parsed.tool_calls[0].function_provider_specific_fields == {"inner": "value"} -def test_agent_loop_replays_tool_call_provider_fields() -> None: +def test_tool_call_request_serializes_provider_fields() -> None: tool_call = ToolCallRequest( id="abc123xyz", name="read_file", @@ -47,7 +46,7 @@ def test_agent_loop_replays_tool_call_provider_fields() -> None: function_provider_specific_fields={"inner": "value"}, ) - message = AgentLoop._build_tool_call_message(tool_call) + message = tool_call.to_openai_tool_call() assert message["provider_specific_fields"] == {"thought_signature": "signed-token"} assert message["function"]["provider_specific_fields"] == {"inner": "value"}