Repository navigation
fix(runner): keep callbackContextData on a resumed invocation - #1696
Conversation
utafrali
left a comment
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
|
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) { |
There was a problem hiding this comment.
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 = |
There was a problem hiding this comment.
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.
d1cc634 to
08876c4
Compare
|
Thanks for the review. Both comments are addressed in a single commit rebased on main:
|
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,
resumeCorerunsonUserMessageCallbackoninitialContext, butrunResumedAgentbuilds the agent's context from a second
newInvocationContextBuilder, with a newcallbackContextData. Data aplugin stores in
onUserMessageCallbackis gone bybeforeAgentCallback. A new invocation does not have thisproblem:
runAgentWithUpdatedSessionderives the agent's context withinitialContext.toBuilder(), which sharesthe map. The second builder also resets
endInvocation, so a plugin that ends the invocation inonUserMessageCallbackdoes not stop a resumed run. On every resume,onRunErrorCallbackalso runs oninitialContext, so it does not see whatbeforeRunCallbackstored, even when no new message is sent. It surfaced in #1412:SaveFilesAsArtifactsPluginsaves afile in
onUserMessageCallbackand reports its version frombeforeAgentCallback.Solution:
runResumedAgenttakesinitialContextinstead ofsession,resolvedInvocationIdandrunConfig, whichinitialContextalready carries, andbuilds
rootContextwithinitialContext.toBuilder().agent(rootAgent).userContent(...). Its three callers inresumeCorepassinitialContext. The resume path now derives the agent's context frominitialContext, asthe new-invocation path does.
Besides
callbackContextData, the copy carriesendInvocation, the cost manager and the checkpoint maps frominitialContext, as on the new-invocation path.initialContextmakes no model call, so the cost manager'scall count is unchanged; a plugin that ends the invocation in
onUserMessageCallbacknow ends the resumed run too, as italready did a new one.
agentanduserContentare 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:
mainrunAsync_resume_keepsCallbackContextDataFromOnUserMessagebeforeRunCallbackreads[message-1, null],beforeAgentCallback[agent:message-1, agent:null]runAsync_resumeRoutedToSubAgent_keepsCallbackContextDataFromOnUserMessage[…, sub_agent_1:message-2, sub_agent_1:null]runAsync_resume_endInvocationFromOnUserMessage_skipsTheModelrunAsync_resumeWithoutMessage_onRunErrorSeesBeforeRunDataonRunErrorCallbackreadsnull, expectedrun-2runAsync_resumeWithStateDeltaOnly_onRunErrorSeesBeforeRunDataonRunErrorCallbackreadsnull, expectedrun-2All five fail when
Runner.javaalone is reverted, and no other test inRunnerResumabilityTestdoes. Thefirst two cover the two branches of
runResumedAgent: giving only the sub-agent's context a new map fails onlythe second test. The last two cover the two
resumeCorecalls without a new message: giving only thestate-delta call a new map fails only the fifth test.
RunnerResumabilityTest:Tests run: 48, Failures: 0, Errors: 0.Manual End-to-End (E2E) Tests:
SaveFilesAsArtifactsPluginfrom #1412 with a stock resumableRunner: turn 1 pauses on a long-running tool;turn 2 sends the function response plus
report.pdf.Checklist