Repository navigation
Description
🔴 Required Information
Describe the Bug:
FunctionNodes in an ADK 2 graph Workflow are invisible to BigQueryAgentAnalyticsPlugin. A workflow made of function nodes produces BigQuery rows for the invocation and for any Agent nodes, but not a single row that identifies a function node or carries its result.
Two independent causes, both narrow:
-
NodeRunnernever dispatches plugin callbacks.rg plugin_manager src/google/adk/workflow/returns zero matches across all 29 files.Agentnodes are covered only incidentally, because_llm_agent_wrapper.py:325callsagent.run_async(ic)and the callbacks fire from insideBaseAgent.FunctionNode._run_implhas no equivalent path, sobefore_agent_callback/after_agent_callback/ the tool callbacks never fire for it. -
BigQueryAgentAnalyticsPlugin.on_event_callbackignoresEvent.output. Function-node events carry their payload inEvent.outputwithEvent.contentunset (_base_node.py:179,Event(output=validated)). Every content path inon_event_callbackreadsevent.contentorevent.actions;rg 'event\.output' src/google/adk/plugins/bigquery_agent_analytics_plugin.pyreturns zero matches. So the one hook that does fire for function nodes discards their payload.
The net effect is that a node emits an event, the plugin receives it, and nothing is written.
Steps to Reproduce:
pip install google-adk==2.3.0- Run the script under Minimal Reproduction Code below. It needs no GCP access —
_log_eventis stubbed to record theevent_typeof each row the plugin would write. - Compare the recorded rows against the two nodes that ran.
Expected Behavior:
A two-node workflow should produce BigQuery rows that identify step_one and step_two and carry their outputs, comparable to the AGENT_STARTING / AGENT_COMPLETED coverage an Agent node gets.
Observed Behavior:
agent/tool callbacks fired: NONE
on_event_callback saw:
path='repro_wf@1/step_one@1' output=True content=False
path='repro_wf@1/step_two@1' output=True content=False
BigQuery rows the plugin would write: ['USER_MESSAGE_RECEIVED', 'INVOCATION_STARTING', 'INVOCATION_COMPLETED']
Three rows, none referencing either node. The events reach on_event_callback with correct node_info.path, so the identity information is present and simply unused.
In a real workflow the only node-derived rows are incidental STATE_DELTA entries from nodes that happen to write ctx.state — nodes that only return a value are entirely absent.
Environment Details:
- ADK Library Version:
google-adk==2.3.0(also verified againstmain@27548e39) - Desktop OS: macOS 15 (darwin 24.6.0)
- Python Version: 3.13
Model Information:
- Are you using LiteLLM: No
- Which model is being used: N/A — the repro contains no LLM call
Activity
[-]FunctionNode events invisible in BigQueryAgentAnalyticsPlugin[/-][+]FunctionNode events invisible in BigQueryAgentAnalyticsPlugin in graph workflow adk 2[/+]llalitkumarrr commentedon Aug 3, 2026
Hello @wdai-chwy ,
Could you please provide a minimal reproducible example to help us investigate this issue further? Additionally, please try testing with
google-adk==2.6.0and let us know your findings with this version.wdai-chwy commentedon Aug 3, 2026
@llalitkumarrr
example below for function node including mcp tool call. we can see events in adk web, but it is not logged into big query.
i checked 2.6, https://github.com/google/adk-python/releases/tag/v2.6.0, looks like nothing related to this issue.
We have to deploy to test env to check big query, not locally.
llalitkumarrr commentedon Aug 5, 2026
Hello @haiyuan-eng-google,
Could you please take a look at this?
haiyuan-eng-google commentedon Aug 5, 2026
I'm working on this issue for now, would keep you posted
caohy1988 commentedon Aug 5, 2026
Thanks for the unusually precise diagnosis — both causes confirmed at current HEAD, and your repro's observed output matches what the code does:
workflow/package contains no plugin dispatch at all;NodeRunneronly opens a telemetry span. Agent nodes are covered incidentally throughagent.run_asyncinside the LLM-agent wrapper.on_event_callbackhas no branch forevent.output— the function-node events do reach the plugin (via the runner's event queue, which is also why adk web displays them), and are then dropped because they match no content path.Two additional gaps we found while verifying, which your MCP example touches:
Event(error_code=...)fromNodeRunner) are equally invisible — no branch matches them either.FunctionNodebody (yourcall_mcp_tool) produces no TOOL_STARTING/TOOL_COMPLETED rows regardless of the above. This one needs its own design and will be tracked separately.Fix plan, in two stages:
Stage 1 (plugin-side, no framework change): handle output-bearing events in
on_event_callback— non-partial events withoutputset produce aNODE_OUTPUTrow carrying the (truncated) output payload plus the node identity the event already has (attributes.adk.node.path/run_id/parent_run_id— that enrichment plumbing already exists and just isn't triggered). A siblingNODE_ERRORrow covers node failures.event_typeis a plain STRING column, so existing tables need no schema migration; consumer views get the new types projected. This makes yourstep_one/step_twoidentifiable with their outputs.Stage 2 (framework): add node lifecycle plugin callbacks (
before_node_callback/after_node_callback/on_node_error_callback) dispatched fromNodeRunner, givingNODE_STARTING/NODE_COMPLETEDrows with latency, symmetric with the AGENT_* coverageAgentnodes get today.I'll post progress here as the stages land.
caohy1988 commentedon Aug 5, 2026
Follow-up after reproducing this end-to-end on current main: a workflow with two
FunctionNodeoutputs still produces onlyUSER_MESSAGE_RECEIVED,INVOCATION_STARTING, andINVOCATION_COMPLETEDrows in BigQuery.A scope refinement to my previous proposal: the missing node-lifecycle callback API is real, but it is not required to fix this issue.
NodeRunneralready sends each emitted nodeEventthrough the plugin’son_event_callback; the BigQuery plugin currently ignores the event output. Also, aFunctionNodeis neither an Agent nor a Tool, so reusing agent/tool callbacks would give misleading semantics.The focused fix should therefore be:
NODE_OUTPUTfromon_event_callbackfor a non-partial node event with output/content, using the existing event envelope and node path.NODE_ERRORfor node error events.NODE_OUTPUTis more accurate than a singleNODE_COMPLETEDrow.None, output plus state delta, node exceptions, and an agent-wrapped workflow regression case.Exact
NODE_STARTING/NODE_COMPLETEDlifecycle hooks should be a separate API design: their behavior around retries, resumes, short-circuiting, and multi-output nodes needs to be defined first. Imperatively calling a tool inside aFunctionNodeis likewise a separate instrumentation gap, not something this fix should infer as a normal tool callback.caohy1988 commentedon Aug 5, 2026
One implementation pitfall to design into Stage 1 before writing the
NODE_OUTPUTbranch:message_as_output/_output_delegateddedup semantics. The runner clearsoutputon event copies when a node's message is being delegated as output (runners.py:939-942,_node_runner.py:345-348). The branch must key off the event as seen at the queue consumer (runners.py:945) and skip events wheremessage_as_outputapplies — otherwise an LlmAgent node's final text gets logged twice: once asAGENT_RESPONSE(the existing content path) and again asNODE_OUTPUT. The FunctionNode case the reporter hit hascontent=Noneand no delegation, so it falls cleanly into the new branch once that guard exists.Test anchors: the A2A
on_event_callbacktest class (tests/unittests/plugins/test_bigquery_agent_analytics_plugin.py:8093+) is the right template for the new branch; workflow-side fixtures live undertests/unittests/workflow/(test_function_node.py,test_node_runner_integration.py).On Stage 2 vocabulary: reusing
before/after_agent_callbackfor deterministic nodes is a poor fit —FunctionNodeis not aBaseAgent, soCallbackContext.agent_namesemantics break (the plugin already works around this in_resolve_agent_label,:5603-5628). The node-nativeNODE_*family proposed above avoids that entirely and matches how the plugin already special-cases workflow-driven invocations.caohy1988 commentedon Aug 5, 2026
Stage 1 implementation follow-up: the plugin-only fix is now in draft PR caohy1988#12 (commit
438367d3). No workflow/framework files are changed.on_event_callbacknow emits:NODE_OUTPUTfor non-partial workflow-node events withcontent is Noneand non-Noneoutput;NODE_ERRORfor workflow-node events witherror_code.Both use the existing event envelope, so node path/run/parent identity and payload truncation/privacy behavior are preserved. State deltas remain separate rows. The content-less guard avoids double-logging agent/message output, including the
message_as_outputpath discussed above. Each output event is handled independently, so multi-yield nodes naturally produce one row per yielded event.Tests cover dict/list/scalar outputs, node identity, output plus state delta, errors, None, message-output deduplication, and typed NODE_OUTPUT/NODE_ERROR views. The complete plugin unit file passes: 477 tests. Node lifecycle callbacks and imperative in-node tool instrumentation remain explicitly out of scope.
fix(plugins): Capture workflow node results in BigQuery
fix(plugins): Capture workflow node results in BigQuery
caohy1988 commentedon Aug 11, 2026
Correction / final status: the Stage 1 fix is at upstream HEAD. It merged to
mainon 2026-08-10 in 04b8b72 (feat(plugins): add BigQuery Agent Analytics delivery and termination observability), landing via the internal review + Copybara flow. My earlier note describing it as still pending on a fork draft was out of date — apologies for the confusion.What shipped (re-verified line-by-line at current HEAD):
on_event_callbacknow emitsNODE_OUTPUTfor final (non-partial) workflow-node events carryingoutput, andNODE_ERRORfor node events carrying anerror_code. Both reuse the existing envelope, sonode_path/node_run_id/node_parent_run_idland in typed view columns, and output payloads go through the standard truncation/privacy pipeline. Multi-yield nodes produce one row per emitted output.message_as_outputdelegation is excluded (an LlmAgent node's final text is not double-logged as both AGENT_RESPONSE and NODE_OUTPUT); model finish/block codes are excluded fromNODE_ERROR, so an LLM SAFETY stop stays classified as LLM_RESPONSE rather than a node failure.With this, the repro from the issue description produces identifiable rows for
step_one/step_twowith their outputs, and @wdai-chwy's function node produces aNODE_OUTPUTrow carrying the returned patient payload.Availability: the merge postdates v2.6.3, so it ships in the next release; until then it's available from
main.Closing this issue as fixed, since the reported defect — function nodes invisible to the plugin — is resolved. Two follow-ups remain tracked separately and were deliberately out of scope here: (1) explicit node lifecycle boundary rows (
NODE_STARTING/completion with latency, symmetric with AGENT_* coverage), in progress; (2) instrumentation for tools invoked imperatively inside a node body (thecall_mcp_toolcase), which needs its own design. Please open/watch follow-up issues for those, and reopen here if the shipped behavior doesn't cover your workflow.caohy1988 commentedon Aug 11, 2026
Follow-up clarification: lifecycle boundary instrumentation has been narrowed into RFC haiyuan-eng-google/BigQuery-Agent-Analytics-SDK#98. The RFC proposes gated event-observation windows, not exact node start/completion or FunctionNode wall-time. Exact lifecycle events remain deferred pending node-native framework callbacks.