Skip to content

Fix(workflow)/ add _process_content_object function to extract output from event.content before assigning it to child.output in _reconstruct_node_states #5668

Open
samarth1224 wants to merge 3 commits into
google:v2from
samarth1224:fixes/issue-5553
Open

Fix(workflow)/ add _process_content_object function to extract output from event.content before assigning it to child.output in _reconstruct_node_states #5668
samarth1224 wants to merge 3 commits into
google:v2from
samarth1224:fixes/issue-5553

Conversation

@samarth1224
Copy link
Copy Markdown

Link to Issue

1. Link to an existing issue (if applicable):

NOTE

The reason for opening this PR is to provide an alternative approach to the one proposed in the PR #5587 I have compared both the approaches at the end based on my knowledge of codebase.
I have read the contributing guide, and I understand opening PR for the same issue is not advised, but I opened it to present a different solution, which could be taken in consideration.

Problem:

During fresh execution of a node with message_as_output == True, process_llm_agent_output extracts text from the Content parts, parses it, validates it against the schema, and assigns the resulting output to event.output.

However, before the event is persisted to the session, _consume_event_queue optimizes for message_as_output nodes by stripping event.output (setting it to None) and only saving the raw event.content

Upon workflow resumption, _reconstruct_node_states rebuilds the node's state. Because event.output is None, it fell back to assigning the raw event.content (a Content object) directly to child.output.

Solution:

Added _process_content_object() to extract the output from the raw event.content during rehydration.
This function mirrors the logic used in the process_llm_agent_output function in _llm_agent_wrapper.py` file

  1. It extracts text from all parts of the Content object.
  2. It correctly filters out Parts.thought parts to prevent Chain-of-Thought reasoning text from leaking into the final output.
  3. It attempts to parse the extracted text as JSON, if fails it returns the raw text.

Testing Plan

I have added new unit tests for the _process_content_object function. These tests cover various scenarios, including plain text extraction, JSON parsing for structured outputs, and the correct filtering of thought parts to prevent internal reasoning from leaking into the final output.

Additionally, I have updated the existing rehydration test _test_scan_message_as_output that previously asserted the raw Content object was returned; It now correctly verify that the output is reconstructed during node state rehydration.

Unit Tests:

  • [ x] I have added or updated unit tests for my change.
  • [ x] All unit tests pass locally.

Summary Unit Test


tests\unittests\workflow\utils\test_rehydration_utils.py ........................................                [100%]

================================================== warnings summary ===================================================
src\google\adk\features\_feature_decorator.py:72
  D:\Projects\adk-python\adk-python\src\google\adk\features\_feature_decorator.py:72: UserWarning: [EXPERIMENTAL] feature FeatureName.PLUGGABLE_AUTH is enabled.
    check_feature_enabled()

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================================== 40 passed, 1 warning in 33.21s ============================================

Manual End-to-End (E2E) Tests:

I have already provided the necessary setup and instruction in the description of Issue #5553, along with minimal reproducible code.
The same setup can be used as Manual E2E test.
I am presentig the minimal reprouducible code and successful mitigation of the bug in the screenshot.

from google.adk import Workflow
from google.adk.events import RequestInput
from google.adk import Context
from google.adk.agents import Agent
from google.adk.workflow import node,FunctionNode
from pydantic import BaseModel

from typing import Any

class MySchema(BaseModel):
    greeting: str

analyzer = Agent(
    name="my_agent",
    model="gemini-3.1-flash-lite-preview",
    output_schema=MySchema,
    instruction="Your job is to greet the user. "
                "",
)
@node(rerun_on_resume=False)
async def get_user_approval(ctx: Context, node_input: Any):
    """Yields a RequestInput to pause the workflow and wait for user input."""
    yield RequestInput(message=f'please approve or reject.',response_schema = str)

@node(rerun_on_resume=True)
async def handle_process(ctx: Context, node_input: Any):
    """The orchestrator calling the interactive step."""
    user_response = await ctx.run_node(get_user_approval,node_input)
    if user_response.lower() == "yes":
        yield 'approved'
    yield "Denied"
    return

@node(rerun_on_resume=True)
async def my_workflow(ctx:Context):
    agent_input = "my agent input"
    agent_output = await ctx.run_node(analyzer,agent_input)
    agent_output = MySchema.validate(agent_output)
    result = await ctx.run_node(handle_process)
    yield result

root_agent = Workflow(
    name="greet_user",
    edges=[("START", my_workflow)]
)

Output

image

Checklist

  • [x ] I have read the CONTRIBUTING.md document.
  • [ x] I have performed a self-review of my own code.
  • [ x] I have commented my code, particularly in hard-to-understand areas.
  • [x ] I have added tests that prove my fix is effective or that my feature works.
  • [ x] New and existing unit tests pass locally with my changes.
  • [ x] I have manually tested my changes end-to-end.
  • [ x] Any dependent changes have been merged and published in downstream modules.

Comparison of Both the approaches.

PR #5587

.....
message_as_output: bool | None = None
  """When True, this event's content is the node's output.

  No separate output event is needed — the content event already
  carries the output value.
  """
.....

Therefore, I feel output is not needed to be preserved.

  • The specific conditional branches that checks whether message_as_output is True or False, in the _reconstruct_node_state and _track_event_in_context, effectively becomes redundant.

This PR

  • The main concern in this PR is that there is no way to validate the output schema of the node with the output (contained in the event.output). This does not affect much because output is already validated against the schema during the fresh run in the _process_llm_agent_output function in _llm_agent_wrapper.py.
  • It's not possible to get the current node's instance without explicitly passing it down to the _reconstruct_node_states all the way from the dynamic node scheduler.
  • Although there is DynamicNodeRegistry file , which I suppose is WIP.

Conclusion:

This PR addresses a bug where message_as_output nodes fail to rehydrate the correct output, proposing a solution to process event.content during resumption rather than persisting it. This approach adheres to the design of message_as_output by extracting, filtering, and parsing the content upon rehydration, offering an alternative to PR #5587.

…tract output from event.content object before assigning it to child.output, in _reconstruct_node_states and add unittest for _process_content_object and modify exisiting test test_scan_message_as_output
@samarth1224 samarth1224 changed the title Fixes/issue 5553 Fixes(workflow)/ add _process_content_object function to extract output from event.content before assigning it to child.output in _reconstruct_node_states May 11, 2026
@samarth1224 samarth1224 changed the title Fixes(workflow)/ add _process_content_object function to extract output from event.content before assigning it to child.output in _reconstruct_node_states Fix(workflow)/ add _process_content_object function to extract output from event.content before assigning it to child.output in _reconstruct_node_states May 11, 2026
@rohityan rohityan self-assigned this May 12, 2026
@rohityan rohityan added workflow [Component] This issue is related to ADKworkflow request clarification [Status] The maintainer need clarification or more information from the author labels May 12, 2026
@rohityan
Copy link
Copy Markdown
Collaborator

Hi @samarth1224 , Thank you for your contribution! Can you please fix the failing tests

@samarth1224
Copy link
Copy Markdown
Author

Hi @rohityan ,
I’ve updated a few of the utility classes in the codebase. Pytest was incorrectly flagging them as test suites because their names started with the Test prefix, which triggered a PytestCollectionWarning since they have init constructors.

To resolve this without renaming the classes—which could have been disruptive—I’ve explicitly assigned test = False to them. This tells Pytest to skip them during the collection phase.

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

request clarification [Status] The maintainer need clarification or more information from the author workflow [Component] This issue is related to ADKworkflow

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants