Skip to content

Commit 59f7bdf

Browse files
spectaclehongboyangsvl
authored andcommitted
fix(workflow): Preserve explicit single-turn contents
Merge #6047 **Please ensure you have read the [contribution guide](https://github.com/google/adk-python/blob/main/CONTRIBUTING.md) before creating a pull request.** ### Link to Issue or Description of Change **1. Link to an existing issue (if applicable):** - Closes: #6046 **Problem:** `single_turn` `LlmAgent` workflow nodes currently force `include_contents` to `none` at runtime, even when the user explicitly configures `include_contents="default"`. That silently changes the agent configuration and prevents one-shot workflow nodes from intentionally receiving relevant session or workflow history. **Solution:** Only apply the workflow `single_turn` default of `include_contents="none"` when `include_contents` was not explicitly set by the user. This preserves the existing implicit default behavior while respecting explicit `include_contents="default"` and `include_contents="none"` values. Regression coverage was added for implicit and explicit `include_contents` cases. ### Testing Plan **Unit Tests:** - [x] I have added or updated unit tests for my change. - [x] All unit tests pass locally. Targeted workflow tests pass locally: ```shell uv run pytest tests/unittests/workflow/test_llm_agent_as_node.py -q ``` Result: ```text 28 passed, 8 skipped, 4 xfailed ``` Python 3.10 tox unit suite passes locally: ```shell uv tool run --from tox --with tox-uv tox -e py310 ``` Result: ```text 7044 passed, 21 skipped, 31 xfailed, 9 xpassed ``` Pre-commit hooks pass for changed files: ```shell uv run pre-commit run --files src/google/adk/workflow/_llm_agent_wrapper.py tests/unittests/workflow/test_llm_agent_as_node.py ``` **Manual End-to-End (E2E) Tests:** Validated manually with a local workflow application that uses a `single_turn` workflow `LlmAgent` configured with `include_contents="default"`. The run confirmed that the explicit contents mode is preserved instead of being overwritten to `none`. No ADK Web or UI behavior is changed. ### Checklist - [x] I have read the [CONTRIBUTING.md](https://github.com/google/adk-python/blob/main/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. ### Additional context No documentation update was made because this change does not add a new user-facing API or guide behavior; it preserves explicit configuration that was already supported by `LlmAgent.include_contents`. No dependent changes are required. Co-authored-by: Bo Yang <ybo@google.com> COPYBARA_INTEGRATE_REVIEW=#6047 from spectaclehong:fix/single-turn-include-contents a98ad26 PiperOrigin-RevId: 931343211
1 parent b99546b commit 59f7bdf

3 files changed

Lines changed: 68 additions & 1 deletion

File tree

.agents/skills/adk-unit-guide/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Use the following structure and instructions to create the guide for the code un
5050
- Present a single, minimum implementation of the code unit to demonstrate its use.
5151
- Show enough of the containing classes to make it clear where the code could be used.
5252
- Use unit test code as a starting point for the code example, if available.
53+
- When writing a sample agent, do not set the `model` attribute.
5354
5455
## How it works
5556

src/google/adk/workflow/_llm_agent_wrapper.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,8 @@ async def run_llm_agent_as_node(
296296
f" but agent '{agent.name}' has mode='{agent.mode}'."
297297
)
298298

299-
if agent.mode == 'single_turn':
299+
include_contents_explicit = 'include_contents' in agent.model_fields_set
300+
if agent.mode == 'single_turn' and not include_contents_explicit:
300301
agent.include_contents = 'none'
301302

302303
agent_ctx = prepare_llm_agent_context(agent, ctx)

tests/unittests/workflow/test_llm_agent_as_node.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,71 @@ def test_default_mode_auto_set_to_single_turn(self):
246246

247247
assert node.mode == 'single_turn'
248248

249+
@pytest.mark.parametrize(
250+
('agent_kwargs', 'expected_include_contents'),
251+
[
252+
({}, 'none'),
253+
({'mode': 'single_turn'}, 'none'),
254+
(
255+
{'mode': 'single_turn', 'include_contents': 'default'},
256+
'default',
257+
),
258+
({'mode': 'single_turn', 'include_contents': 'none'}, 'none'),
259+
],
260+
)
261+
@pytest.mark.asyncio
262+
async def test_single_turn_defaults_include_contents_only_when_unset(
263+
self,
264+
monkeypatch: pytest.MonkeyPatch,
265+
agent_kwargs: dict[str, Any],
266+
expected_include_contents: str,
267+
):
268+
"""Single-turn workflow nodes preserve explicit content inclusion."""
269+
from unittest.mock import MagicMock
270+
271+
from google.adk.workflow import _llm_agent_wrapper
272+
273+
agent = LlmAgent(
274+
name='test_agent',
275+
model='gemini-2.5-flash',
276+
instruction='Test.',
277+
**agent_kwargs,
278+
)
279+
wrapper = build_node(agent)
280+
seen_include_contents = []
281+
282+
async def mock_run_async(*args, **kwargs):
283+
seen_include_contents.append(wrapper.include_contents)
284+
yield Event(
285+
invocation_id='inv',
286+
author=wrapper.name,
287+
content=types.Content(parts=[types.Part(text='ok')]),
288+
)
289+
290+
object.__setattr__(wrapper, 'run_async', mock_run_async)
291+
monkeypatch.setattr(
292+
_llm_agent_wrapper,
293+
'prepare_llm_agent_context',
294+
lambda agent, ctx: ctx,
295+
)
296+
monkeypatch.setattr(
297+
_llm_agent_wrapper,
298+
'prepare_llm_agent_input',
299+
lambda agent, ctx, node_input: None,
300+
)
301+
ctx = MagicMock(spec=Context)
302+
ic = MagicMock()
303+
ctx.get_invocation_context.return_value = ic
304+
ic.model_copy.return_value = ic
305+
306+
events = [
307+
event async for event in wrapper._run_impl(ctx=ctx, node_input='hi')
308+
]
309+
310+
assert seen_include_contents == [expected_include_contents]
311+
assert wrapper.include_contents == expected_include_contents
312+
assert events[0].content.parts[0].text == 'ok'
313+
249314
def test_name_override(self):
250315
"""build_node respects explicit name override."""
251316
node = build_node(_make_agent(mode='task'), name='override')

0 commit comments

Comments
 (0)