Skip to content
23 changes: 21 additions & 2 deletions src/google/adk/plugins/bigquery_agent_analytics_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8191,13 +8191,29 @@ async def after_tool_callback(
span_id, duration = TraceManager.pop_span()
parent_span_id, _ = TraceManager.get_current_span_and_parent()

# MCP CallToolResult (isError) and plugin-rewritten failures
# (error_details) reach here as ordinary results. Classify them as
# TOOL_ERROR so status/error_message match on_tool_error_callback;
# otherwise audits treat them as successes.
error_message = None
if isinstance(result, dict):
if result.get("error_details"):
error_message = str(result["error_details"])
elif result.get("isError") or result.get("is_error"):
content = result.get("content")
first = content[0] if isinstance(content, list) and content else None
text = first.get("text") if isinstance(first, dict) else None
error_message = str(text) if text else "Tool returned an error"

event_data = EventData(
latency_ms=duration,
span_id_override=span_id,
parent_span_id_override=parent_span_id,
status="ERROR" if error_message is not None else "OK",
error_message=error_message,
)
await self._log_event(
"TOOL_COMPLETED",
"TOOL_ERROR" if error_message is not None else "TOOL_COMPLETED",
tool_context,
raw_content=content_dict,
is_truncated=is_truncated,
Expand All @@ -8211,7 +8227,10 @@ async def after_tool_callback(
# args (the final-answer payload the model supplied) as AGENT_RESPONSE so
# the visible response text is captured. Opt-in via
# ``config.final_response_tool_names`` (empty by default).
if tool.name in self.config.final_response_tool_names:
if (
error_message is None
and tool.name in self.config.final_response_tool_names
):
args_truncated, args_is_truncated = _recursive_smart_truncate(
tool_args, self.config.max_content_length
)
Expand Down
46 changes: 46 additions & 0 deletions tests/unittests/plugins/test_bigquery_agent_analytics_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2119,6 +2119,52 @@ async def test_after_tool_callback_logs_correctly(
assert content_dict["tool"] == "MyTool"
assert content_dict["result"] == {"res": "success"}

@pytest.mark.asyncio
@pytest.mark.parametrize(
("result", "expected_error_message"),
[
(
{
"content": [
{"type": "text", "text": "Git reset to remote failed."}
],
"isError": True,
},
"Git reset to remote failed.",
),
(
{"error_details": "MCP request failed with code 403"},
"MCP request failed with code 403",
),
],
)
async def test_after_tool_callback_logs_error_bearing_result_as_tool_error(
self, bq_plugin_inst, tool_context, result, expected_error_message
):
"""Error-bearing tool results are recorded as TOOL_ERROR, not TOOL_COMPLETED."""
mock_tool = mock.create_autospec(
base_tool_lib.BaseTool, instance=True, spec_set=True
)
type(mock_tool).name = mock.PropertyMock(return_value="MyTool")
type(mock_tool).description = mock.PropertyMock(return_value="Description")
log_event = mock.AsyncMock()
bq_plugin_inst._log_event = log_event
bigquery_agent_analytics_plugin.TraceManager.push_span(tool_context)

await bq_plugin_inst.after_tool_callback(
tool=mock_tool,
tool_args={"arg1": "val1"},
tool_context=tool_context,
result=result,
)

log_event.assert_awaited_once()
assert log_event.await_args.args[0] == "TOOL_ERROR"
event_data = log_event.await_args.kwargs["event_data"]
assert event_data.status == "ERROR"
assert event_data.error_message == expected_error_message
assert log_event.await_args.kwargs["raw_content"]["tool"] == "MyTool"

@pytest.mark.asyncio
async def test_after_tool_callback_no_state_delta_logging(
self, bq_plugin_inst, mock_write_client, tool_context, dummy_arrow_schema
Expand Down
Loading