Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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