Skip to content

runner: avoid duplicate user event append on invocation retry#4507

Open
davidahmann wants to merge 3 commits intogoogle:mainfrom
davidahmann:codex/issue-4506-runner-idempotent-retry
Open

runner: avoid duplicate user event append on invocation retry#4507
davidahmann wants to merge 3 commits intogoogle:mainfrom
davidahmann:codex/issue-4506-runner-idempotent-retry

Conversation

@davidahmann
Copy link

Problem

Retry/resume calls with the same invocation_id and same user message could append duplicate user events to session history.

What changed

  • Added a duplicate-user-event guard in Runner._append_new_message_to_session keyed by invocation id + content + state delta.
  • Skips append when the same user event already exists for that invocation.
  • Added runner tests for duplicate retry no-op and non-duplicate append behavior.

Validation

  • uv sync --extra test
  • uvx pyink --config pyproject.toml src/google/adk/runners.py tests/unittests/test_runners.py
  • uv run pytest tests/unittests/test_runners.py
  • uv run pytest tests/unittests/runners/test_resume_invocation.py

Refs #4506

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @davidahmann, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses an issue where retrying or resuming an invocation could lead to duplicate user events being recorded in the session history. By introducing a mechanism to detect and skip these redundant events, the change ensures the integrity of the session history and prevents unnecessary data duplication, particularly in scenarios involving invocation retries.

Highlights

  • Duplicate User Event Prevention: Implemented a guard in Runner._append_new_message_to_session to prevent duplicate user events from being appended to the session history during retry or resume operations. The check is based on invocation_id, message content, and state delta.
  • New Helper Method: Added a private helper method _has_duplicate_user_event_for_invocation to encapsulate the logic for identifying duplicate user events within a session.
  • Unit Tests: Introduced new unit tests to verify that duplicate user messages are correctly skipped and that distinct messages are still appended, ensuring the new logic behaves as expected.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/google/adk/runners.py
    • Added a call to _has_duplicate_user_event_for_invocation within _append_new_message_to_session to check for and skip duplicate user events.
    • Implemented the new private method _has_duplicate_user_event_for_invocation which checks for existing user events with matching invocation ID, content, and state delta.
  • tests/unittests/test_runners.py
    • Added test_append_new_message_to_session_skips_duplicate_retry_message to verify that duplicate user events are not appended.
    • Added test_append_new_message_to_session_keeps_non_duplicate_messages to confirm that unique user events are still correctly appended.
Activity
  • No human activity has been recorded on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@davidahmann
Copy link
Author

Implemented an invocation-scoped dedupe guard for user-message append in retry/resume paths.

Scope in this PR:

  • Added duplicate detection by (invocation_id, user content, state_delta).
  • Skips append when a matching user event already exists.
  • Added tests for duplicate retry no-op and non-duplicate behavior.

This contribution was informed by patterns from Gait: https://github.com/davidahmann/gait

@adk-bot adk-bot added the core [Component] This issue is related to the core interface and implementation label Feb 16, 2026
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request effectively addresses the issue of duplicate user events on invocation retries by introducing a check in _append_new_message_to_session. The new method _has_duplicate_user_event_for_invocation correctly identifies duplicates based on invocation ID, message content, and state delta. The accompanying tests cover the happy paths for skipping duplicates and keeping non-duplicates.

My review includes a suggestion to refactor the duplicate check for better readability and a recommendation to enhance test coverage to include scenarios with state_delta.

Comment on lines +943 to +951
for event in session.events:
if event.invocation_id != invocation_id or event.author != 'user':
continue
if (
event.content == new_message
and event.actions.state_delta == expected_state_delta
):
return True
return False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better readability and conciseness, you can refactor this loop into a single statement using a generator expression with any(). This is a more Pythonic way to check for the existence of an item in a sequence that matches a condition.

    return any(
        event.content == new_message
        and event.actions.state_delta == expected_state_delta
        for event in session.events
        if event.author == "user" and event.invocation_id == invocation_id
    )

Comment on lines +293 to +334
async def test_append_new_message_to_session_skips_duplicate_retry_message():
session_service = InMemorySessionService()
runner = Runner(
app_name="test_app",
agent=MockLlmAgent("root_agent"),
session_service=session_service,
artifact_service=InMemoryArtifactService(),
)
session = await session_service.create_session(
app_name="test_app",
user_id="test_user",
)
user_message = types.Content(
role="user",
parts=[types.Part(text="retry message")],
)
invocation_context = runner._new_invocation_context(
session,
invocation_id="inv-retry",
new_message=user_message,
run_config=RunConfig(),
)

await runner._append_new_message_to_session(
session=session,
new_message=user_message,
invocation_context=invocation_context,
)
await runner._append_new_message_to_session(
session=session,
new_message=user_message,
invocation_context=invocation_context,
)

matched_events = [
event
for event in session.events
if event.author == "user"
and event.invocation_id == "inv-retry"
and event.content == user_message
]
assert len(matched_events) == 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The new tests cover the duplicate detection logic well for messages. However, the check for duplicates also involves state_delta, which is not covered in the tests.

To improve test coverage, please consider adding a test case that verifies the behavior with different state_delta values. For example:

  1. Append a message with a specific state_delta.
  2. Append the same message with the same state_delta (should be skipped).
  3. Append the same message with a different state_delta (should be appended).
  4. Append the same message with state_delta=None (should be appended).

This will ensure the state_delta comparison in _has_duplicate_user_event_for_invocation works as expected.

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

Labels

core [Component] This issue is related to the core interface and implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants