Skip to content

fix(runner): keep callbackContextData on a resumed invocation - #1696

Merged
copybara-service[bot] merged 1 commit into
google:mainfrom
svetanis:fix/runner-resume-callback-data
Nov 2, 2026
Merged

copybara-service[bot] merged 1 commit into
google:mainfrom
svetanis:fix/runner-resume-callback-data

Conversation

@svetanis

@svetanis svetanis commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

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

2. Or, if no issue exists, describe the change:

Problem:

On a resumed invocation, resumeCore runs onUserMessageCallback on initialContext, but runResumedAgent
builds the agent's context from a second newInvocationContextBuilder, with a new callbackContextData. Data a
plugin stores in onUserMessageCallback is gone by beforeAgentCallback. A new invocation does not have this
problem: runAgentWithUpdatedSession derives the agent's context with initialContext.toBuilder(), which shares
the map. The second builder also resets endInvocation, so a plugin that ends the invocation in
onUserMessageCallback does not stop a resumed run. On every resume, onRunErrorCallback also runs on
initialContext, so it does not see what beforeRunCallback stored, even when no new message is sent. It surfaced in #1412: SaveFilesAsArtifactsPlugin saves a
file in onUserMessageCallback and reports its version from beforeAgentCallback.

Solution:

runResumedAgent takes initialContext instead of session, resolvedInvocationId and runConfig, which
initialContext already carries, and
builds rootContext with initialContext.toBuilder().agent(rootAgent).userContent(...). Its three callers in
resumeCore pass initialContext. The resume path now derives the agent's context from initialContext, as
the new-invocation path does.

Besides callbackContextData, the copy carries endInvocation, the cost manager and the checkpoint maps from
initialContext, as on the new-invocation path. initialContext makes no model call, so the cost manager's
call count is unchanged; a plugin that ends the invocation in onUserMessageCallback now ends the resumed run too, as it
already did a new one. agent and userContent are set as before, and the invocation id, run config, session,
node and branch have the same values the second builder gave them.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.
Test Resumes On main
runAsync_resume_keepsCallbackContextDataFromOnUserMessage the root agent ❌ beforeRunCallback reads [message-1, null], beforeAgentCallback [agent:message-1, agent:null]
runAsync_resumeRoutedToSubAgent_keepsCallbackContextDataFromOnUserMessage a sub-agent, routed past the root ❌ […, sub_agent_1:message-2, sub_agent_1:null]
runAsync_resume_endInvocationFromOnUserMessage_skipsTheModel the root agent, after a plugin ends the invocation ❌ the model is asked again (2 requests, expected 1)
runAsync_resumeWithoutMessage_onRunErrorSeesBeforeRunData the root agent, with no new message ❌ onRunErrorCallback reads null, expected run-2
runAsync_resumeWithStateDeltaOnly_onRunErrorSeesBeforeRunData the root agent, with only a state delta ❌ onRunErrorCallback reads null, expected run-2

All five fail when Runner.java alone is reverted, and no other test in RunnerResumabilityTest does. The
first two cover the two branches of runResumedAgent: giving only the sub-agent's context a new map fails only
the second test. The last two cover the two resumeCore calls without a new message: giving only the
state-delta call a new map fails only the fifth test.

RunnerResumabilityTest: Tests run: 48, Failures: 0, Errors: 0.

Manual End-to-End (E2E) Tests:

SaveFilesAsArtifactsPlugin from #1412 with a stock resumable Runner: turn 1 pauses on a long-running tool;
turn 2 sends the function response plus report.pdf.

without this change   artifactDelta, turn 2 (resumed): {}               artifacts stored: [report.pdf]
with this change      artifactDelta, turn 2 (resumed): {report.pdf=0}   artifacts stored: [report.pdf]

Checklist

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

@utafrali utafrali left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The fix is correct and well-scoped: switching runResumedAgent to derive its root context via initialContext.toBuilder() instead of newInvocationContextBuilder() closes the data-loss gap and aligns the resume path with the new-invocation path. The five new tests directly reproduce the reported bug and cover all described side effects including endInvocation propagation and onRunErrorCallback visibility.

String resolvedInvocationId,
@Nullable Content userContent,
RunConfig runConfig) {
@Nullable Content userContent) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

After this refactor, resolvedInvocationId inside runResumedAgent is used only to call userContentFor(session, resolvedInvocationId, userContent). Its value is already carried by initialContext.invocationId(), so they are always equal. As an optional follow-up, replacing that argument with initialContext.invocationId() and dropping the parameter would make the method fully self-contained through initialContext, eliminating the redundant parameter. Not blocking for this PR.

@hirematha

Copy link
Copy Markdown

Hi @svetanis, thank you for your contribution. We appreciate you taking the time to submit this pull request. The PR is currently under review by our team, and we will keep you posted if any additional information is required. Thank you.

String resolvedInvocationId,
@Nullable Content userContent,
RunConfig runConfig) {
@Nullable Content userContent) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LOW: +1 to the earlier suggestion, and the same applies to session: both values come from initialContext (initialContext.session() and initialContext.invocationId()), so passing them separately duplicates their source. Removing both parameters also trims the new @param block to the single line that adds context.

@Test
public void runAsync_resume_keepsCallbackContextDataFromOnUserMessage() {
CallbackDataRelayPlugin plugin = new CallbackDataRelayPlugin();
TestLlm testLlm =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LOW: pausingAgent("agent", pauseThenSay("lro_call_id", "resumed and summarized")) from ResumabilityTestUtils, already imported here, builds the same model script and agent. The other single-agent tests can use these helpers too to keep the new tests shorter.

@damianmomotgoogle damianmomotgoogle added waiting on reporter Waiting for reaction by reporter. Failing that, maintainers will eventually closed it as stale. and removed needs review labels Oct 29, 2026
@svetanis
svetanis force-pushed the fix/runner-resume-callback-data branch from d1cc634 to 08876c4 Compare October 29, 2026 14:50
@svetanis

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Both comments are addressed in a single commit rebased on main:

  1. Duplicated parameters: runResumedAgent now takes only initialContext and userContent, and reads the session and the invocation id from initialContext; the @param block is the initialContext line alone. A refactor only, covered by the five new tests.
  2. Test helpers: runAsync_resume_keepsCallbackContextDataFromOnUserMessage, runAsync_resume_endInvocationFromOnUserMessage_skipsTheModel and the shared setup of the two onRunErrorSeesBeforeRunData tests now build their model script and agent with pausingAgent and pauseThenSay. The sub-agent test keeps its own setup for the transfer.

@damianmomotgoogle damianmomotgoogle removed the waiting on reporter Waiting for reaction by reporter. Failing that, maintainers will eventually closed it as stale. label Oct 29, 2026
@copybara-service
copybara-service Bot merged commit a79b61b into google:main Nov 2, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Runner drops callbackContextData written in onUserMessageCallback when it resumes an invocation

4 participants