diff --git a/run_agent.py b/run_agent.py index 17b8b01db1..c8388bd0ae 100644 --- a/run_agent.py +++ b/run_agent.py @@ -5064,6 +5064,23 @@ class AIAgent: return tc.get("call_id", "") or tc.get("id", "") or "" return getattr(tc, "call_id", "") or getattr(tc, "id", "") or "" + @staticmethod + def _get_tool_call_name_static(tc) -> str: + """Extract function name from a tool_call entry (dict or object). + + Gemini's OpenAI-compatibility endpoint requires every `role: tool` + message to carry the matching function name. OpenAI/Anthropic/ollama + tolerate its absence, so the field is best-effort: callers fall back + to "" and the message still works elsewhere. + """ + if isinstance(tc, dict): + fn = tc.get("function") + if isinstance(fn, dict): + return fn.get("name", "") or "" + return "" + fn = getattr(tc, "function", None) + return getattr(fn, "name", "") or "" + _VALID_API_ROLES = frozenset({"system", "user", "assistant", "tool", "function", "developer"}) @staticmethod @@ -5126,6 +5143,7 @@ class AIAgent: if cid in missing_results: patched.append({ "role": "tool", + "name": AIAgent._get_tool_call_name_static(tc), "content": "[Result unavailable — see context summary above]", "tool_call_id": cid, }) @@ -9030,6 +9048,7 @@ class AIAgent: insert_at, { "role": "tool", + "name": function_name if function_name != "?" else "", "tool_call_id": tool_call_id, "content": marker, }, @@ -9434,6 +9453,7 @@ class AIAgent: for tc in tool_calls: messages.append({ "role": "tool", + "name": tc.function.name, "content": f"[Tool execution cancelled — {tc.function.name} was skipped due to user interrupt]", "tool_call_id": tc.id, }) @@ -9775,6 +9795,7 @@ class AIAgent: tool_msg = { "role": "tool", + "name": name, "content": function_result, "tool_call_id": tc.id, } @@ -9812,6 +9833,7 @@ class AIAgent: skipped_name = skipped_tc.function.name skip_msg = { "role": "tool", + "name": skipped_name, "content": f"[Tool execution cancelled — {skipped_name} was skipped due to user interrupt]", "tool_call_id": skipped_tc.id, } @@ -10162,6 +10184,7 @@ class AIAgent: tool_msg = { "role": "tool", + "name": function_name, "content": function_result, "tool_call_id": tool_call.id } @@ -10188,6 +10211,7 @@ class AIAgent: skipped_name = skipped_tc.function.name skip_msg = { "role": "tool", + "name": skipped_name, "content": f"[Tool execution skipped — {skipped_name} was not started. User sent a new message]", "tool_call_id": skipped_tc.id } @@ -13110,6 +13134,7 @@ class AIAgent: content = "Skipped: another tool call in this turn used an invalid name. Please retry this tool call." messages.append({ "role": "tool", + "name": tc.function.name, "tool_call_id": tc.id, "content": content, }) @@ -13201,6 +13226,7 @@ class AIAgent: tool_result = "Skipped: other tool call in this response had invalid JSON." messages.append({ "role": "tool", + "name": tc.function.name, "tool_call_id": tc.id, "content": tool_result, }) @@ -13717,6 +13743,7 @@ class AIAgent: if tc["id"] not in answered_ids: err_msg = { "role": "tool", + "name": AIAgent._get_tool_call_name_static(tc), "tool_call_id": tc["id"], "content": f"Error executing tool: {error_msg}", } diff --git a/tests/run_agent/test_agent_guardrails.py b/tests/run_agent/test_agent_guardrails.py index 032057d59f..b222b3320e 100644 --- a/tests/run_agent/test_agent_guardrails.py +++ b/tests/run_agent/test_agent_guardrails.py @@ -263,3 +263,34 @@ class TestGetToolCallIdStatic: def test_object_without_id_attr(self): tc = types.SimpleNamespace() assert AIAgent._get_tool_call_id_static(tc) == "" + + +# --------------------------------------------------------------------------- +# _get_tool_call_name_static +# --------------------------------------------------------------------------- + +class TestGetToolCallNameStatic: + + def test_dict_with_valid_name(self): + assert AIAgent._get_tool_call_name_static( + {"id": "call_1", "function": {"name": "terminal", "arguments": "{}"}} + ) == "terminal" + + def test_dict_with_missing_function(self): + assert AIAgent._get_tool_call_name_static({"id": "call_1"}) == "" + + def test_dict_with_none_function(self): + assert AIAgent._get_tool_call_name_static({"id": "call_1", "function": None}) == "" + + def test_dict_with_none_name(self): + assert AIAgent._get_tool_call_name_static( + {"function": {"name": None, "arguments": "{}"}} + ) == "" + + def test_object_with_valid_name(self): + tc = make_tc("read_file") + assert AIAgent._get_tool_call_name_static(tc) == "read_file" + + def test_object_without_function_attr(self): + tc = types.SimpleNamespace(id="call_1") + assert AIAgent._get_tool_call_name_static(tc) == "" diff --git a/tests/run_agent/test_tool_call_args_sanitizer.py b/tests/run_agent/test_tool_call_args_sanitizer.py index 79f4d82c5a..57ba9839fa 100644 --- a/tests/run_agent/test_tool_call_args_sanitizer.py +++ b/tests/run_agent/test_tool_call_args_sanitizer.py @@ -96,6 +96,7 @@ def test_marker_message_inserted_when_missing(): assert repaired == 1 assert messages[1] == { "role": "tool", + "name": "read_file", "tool_call_id": "call_1", "content": marker, }