BigQueryAgentAnalyticsPlugin records error-bearing tool results as TOOL_COMPLETED with status OK
维护者通常 5 天内回复
@haiyuan-eng-google 已经在做这个了。
开始于 2026年9月16日。
评估
这个 Issue 还没有评估数据。
描述
Describe the bug
BigQueryAgentAnalyticsPlugin writes TOOL_ERROR (with status = 'ERROR' and error_message) only from on_tool_error_callback, that is, only when a tool raises and no plugin registered ahead of it answers the error. after_tool_callback writes TOOL_COMPLETED with status = 'OK' for whatever the tool returned, without looking at it. Two common failures therefore land as successes:
- An MCP tool whose server answers
isError: true.McpToolreturns theCallToolResultas a result dict (_dump_mcp_model, which restoresisError) instead of raising, which is the MCP protocol's shape for a tool-level failure. The row isTOOL_COMPLETED,status = 'OK',error_message = NULL; the failure only shows incontent.result.isError. - A tool that raises while
ReflectAndRetryToolPluginis registered ahead of the analytics plugin.PluginManager._run_callbacksstops at the first plugin that returns a value, so the reflect-and-retry response becomes the tool's result and reachesafter_tool_callback:TOOL_COMPLETED,status = 'OK', with the error incontent.result.error_details.ToolboxToolsettools raise on failure, so a Toolbox error lands here.
An audit, report or alert built on status, error_message or TOOL_ERROR reads both failures as successes.
To Reproduce
google-adk 2.9.0; no BigQuery is needed, because the rows are captured by replacing _log_event. The MCP case uses a FunctionTool that returns the dict McpTool returns for isError: true — the plugin never inspects the result, so the recorded row is the same.
import asyncio
from unittest.mock import AsyncMock
from google.adk.agents.llm_agent import LlmAgent
from google.adk.apps import App
from google.adk.models.base_llm import BaseLlm
from google.adk.models.llm_response import LlmResponse
from google.adk.plugins import ReflectAndRetryToolPlugin
from google.adk.plugins.bigquery_agent_analytics_plugin import BigQueryAgentAnalyticsPlugin
from google.adk.runners import InMemoryRunner
from google.adk.tools import FunctionTool
from google.genai import types
def mcp_style_failure() -> dict:
return {"content": [{"type": "text", "text": "Git reset to remote failed."}], "isError": True}
def raising_tool() -> str:
raise RuntimeError("MCP request failed with code 403: Access Denied")
def build(with_reflect: bool):
rows = []
analytics = BigQueryAgentAnalyticsPlugin(project_id="p", dataset_id="d", table_id="events")
async def record(event_type, callback_context, raw_content=None, is_truncated=False, event_data=None):
if event_type.startswith("TOOL_"):
rows.append((event_type, event_data.status if event_data else "OK", (raw_content or {}).get("tool")))
analytics._log_event = record
analytics._ensure_started = AsyncMock(return_value="ok")
analytics.flush = AsyncMock()
turns = [[types.Part(function_call=types.FunctionCall(name="mcp_style_failure", args={}, id="c0"))]]
if with_reflect: # without a plugin that answers the error, the raise ends the run
turns.append([types.Part(function_call=types.FunctionCall(name="raising_tool", args={}, id="c1"))])
turns.append([types.Part(text="done")])
class Scripted(BaseLlm):
async def generate_content_async(self, llm_request, stream=False):
yield LlmResponse(content=types.Content(role="model", parts=turns.pop(0)))
agent = LlmAgent(name="repro", model=Scripted(model="scripted"), instruction="x",
tools=[FunctionTool(mcp_style_failure), FunctionTool(raising_tool)])
plugins = ([ReflectAndRetryToolPlugin(max_retries=3)] if with_reflect else []) + [analytics]
return InMemoryRunner(app=App(name="repro", root_agent=agent, plugins=plugins)), rows
async def main():
for with_reflect in (False, True):
runner, rows = build(with_reflect)
await runner.session_service.create_session(app_name="repro", user_id="u", session_id="s")
async for _ in runner.run_async(user_id="u", session_id="s",
new_message=types.Content(role="user", parts=[types.Part(text="go")])):
pass
print(f"ReflectAndRetryToolPlugin ahead={with_reflect}:", rows)
asyncio.run(main())
Output:
ReflectAndRetryToolPlugin ahead=False: [('TOOL_STARTING', 'OK', 'mcp_style_failure'), ('TOOL_COMPLETED', 'OK', 'mcp_style_failure')]
ReflectAndRetryToolPlugin ahead=True: [('TOOL_STARTING', 'OK', 'mcp_style_failure'), ('TOOL_COMPLETED', 'OK', 'mcp_style_failure'), ('TOOL_STARTING', 'OK', 'raising_tool'), ('TOOL_COMPLETED', 'OK', 'raising_tool')]
Expected behavior
An error-bearing result is recorded as a failure — TOOL_ERROR, or status = 'ERROR' with a non-null error_message — while the tool's result still reaches the model unchanged. Two shapes ADK itself produces would cover both cases above: an MCP result with isError: true, and a ReflectAndRetryToolPlugin response (response_type == REFLECT_AND_RETRY_RESPONSE_TYPE). A way for an application to classify a result (for example a callable in BigQueryLoggerConfig returning a status and a message) would also cover application-specific shapes.
Related, minor
_get_tool_origin classifies McpTool, TransferToAgentTool, AgentTool and FunctionTool, and returns UNKNOWN for everything else. The tools of ADK's own ToolboxToolset (toolbox_adk.ToolboxTool, a BaseTool) and the SkillToolset tools therefore land as UNKNOWN, so the table cannot separate BigQuery calls from skill loads by origin.
Desktop
- OS: macOS
- Python version: 3.12
- ADK version: 2.9.0. On
main(checked 2026-09-13) the plugin still has no reference toisErroror to the Toolbox.
Model Information: not applicable; the reproduction uses a scripted BaseLlm.
Additional context
Searched existing issues for BigQueryAgentAnalyticsPlugin isError, BigQueryAgentAnalyticsPlugin TOOL_ERROR, tool_origin UNKNOWN and ToolboxToolset analytics and found none. The nearest precedent is #5073 (closed), a tool_origin classification gap for RemoteA2aAgent. We currently work around it with a subclass that classifies the result in after_tool_callback and writes the row through the plugin's _log_event, which depends on private helpers.
- 主要语言
- Python
- 星标
- 21.6k
- 派生
- 4k
- 平均合并
- 9 小时 59 分钟
- 30 天内合并 PR
- 5
环境准备
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
google/adk-python 的其他 Issue
-
难度 2/5 1-3 小时 新手友好度 84/100
google/adk-python#7385 ·
维护者通常 5 天内回复
-
难度 2/5 1-3 小时 新手友好度 62/100
google/adk-python#7381 ·
维护者通常 5 天内回复
-
[A2A] RemoteA2aAgent(use_legacy=False): extension header written to state['http_kwargs'], ignored by a2a-sdk 1.x transports可能已有人在做 @surajksharma07 于 4 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 84/100
google/adk-python#7334 · 2 条评论 · 已指派 1 人 ·
维护者通常 5 天内回复
-
GoogleOidcVerifier treats string "false" as a verified email claim可能已有人在做 @surajksharma07 于 5 天前认领。 未关闭core
难度 2/5 1-3 小时 新手友好度 82/100
google/adk-python#7289 · 5 条评论 · 已指派 1 人 ·
维护者通常 5 天内回复
-
RestApiTool raises uncaught KeyError when a required path param is omitted可能已有人在做 @llalitkumarrr 于 5 天前认领。 未关闭request clarification tools
难度 2/5 1-3 小时 新手友好度 78/100
google/adk-python#7282 · 5 条评论 · 已指派 1 人 ·
维护者通常 5 天内回复
查看 google/adk-python 的全部 Issue
相似的 Issue
-
good first issue hacktoberfest
难度 1/5 1 小时以内 新手友好度 92/100
维护者通常 1 天内回复
-
tool-calling
难度 2/5 1-3 小时 新手友好度 88/100
vllm-project/vllm#59838 ·
维护者通常 1 天内回复
-
难度 1/5 1 小时以内 新手友好度 92/100
raullenchai/Rapid-MLX#4042 ·
维护者通常 1 天内回复
-
documentation
难度 1/5 1 小时以内 新手友好度 92/100
transitmatters/mbta-slow-zone-bot#70 ·
维护者通常 1 天内回复
-
bug
难度 2/5 1-3 小时 新手友好度 66/100
open-webui/open-webui#31871 · 1 条评论 ·
维护者通常 1 天内回复