Skip to content

fix(agui): reconcile replayed tool results with session state - #3046

Open
guslegend0510 wants to merge 12 commits into
agentscope-ai:mainfrom
guslegend0510:codex/fix-3043-agui-tool-result-replay
Open

fix(agui): reconcile replayed tool results with session state#3046
guslegend0510 wants to merge 12 commits into
agentscope-ai:mainfrom
guslegend0510:codex/fix-3043-agui-tool-result-replay

Conversation

@guslegend0510

Copy link
Copy Markdown
Contributor

AgentScope-Java Version

2.0.3-SNAPSHOT

Description

Fixes #3043

Stateful AG-UI clients can replay tool results across multiple local assistant turns. Previously, transcript extraction could discard a result the server still needed, while forwarding already-consumed results that caused subsequent requests to fail.

This change:

  • Selects replayed input after loading authoritative user/session state, preserving pending tool results anywhere in the transcript.
  • Removes previously consumed results before recovery or context insertion.
  • Preserves duplicate-ID, unknown-ID, partial-results-with-text, and permission confirmation checks.
  • Keeps pending-tool recovery controlled by enablePendingToolRecovery.
  • Adds 22 regression cases and updates English/Chinese documentation.

Orphan tool results are now rejected even when no calls are pending. Initial client-owned history can still include results paired with preceding assistant tool calls.

Validation:

  • Reproduced failures on unmodified main; corresponding cases pass with this fix.
  • Full Core, Harness, AG-UI, and AG-UI Spring Starter suites: 3,778 tests, 0 failures, 0 errors, 14 skipped.
  • Spotless and git diff --check passed.

Checklist

  • Code has been formatted with mvn spotless:apply
  • Relevant module tests pass
  • Javadoc comments follow project conventions
  • Related documentation has been updated
  • Code is ready for review

Preserve pending results across client assistant turns and deduplicate consumed results before recovery or context insertion. Keep unknown IDs, duplicates, partial results with text, and permission confirmation strict.

Fixes agentscope-ai#3043
@codecov

codecov Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.98131% with 15 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...e/src/main/java/io/agentscope/core/ReActAgent.java 84.21% 5 Missing and 10 partials ⚠️

📢 Thoughts on this report? Let us know!

@oss-maintainer oss-maintainer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Summary

Reconciles replayed AG-UI tool results with server-side session state: the AG-UI processor now keeps the full client transcript for ReAct-backed agents (RuntimeContext.REPLAYED_INPUT), and ReActAgent selects the follow-up messages after authoritative state is loaded, while consumed results are stripped before recovery/context insertion. Good, well-scoped fix for a genuinely subtle protocol bug, and 22 regression cases plus EN/ZH docs is a strong evidence base. I have two robustness questions about transcript shapes that the current selection window may not cover, plus API-surface nits. No blocking correctness issue found in the paths I could trace.

CLA: no license/cla status context was found on this PR's head commit (unknown), so this review is posted as a comment rather than an approval — approvals are reserved for PRs where CLA state is verifiable.

Findings:

  • [Warning] ReActAgent.java:2077 — selection only rescans indices <= lastAssistant; a pending result appearing after the last assistant message falls into the "replay the old user prompt" fallback.
  • [Warning] ReActAgent.java:761 — unchecked (CallExecution) callScope cast in the new prepareInputMessages override.
  • [Info] RuntimeContext.java:43 — new cross-module flag modelled as a raw string attribute.
  • [Info] AguiRequestProcessor.java:166addResumeInterrupts hoisting changes input for the non-ReAct + server-memory branch.

Automated review by github-manager-bot


@Override
protected List<Msg> prepareInputMessages(List<Msg> msgs, Object callScope) {
CallExecution scope = (CallExecution) callScope;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

prepareInputMessages does an unchecked cast (CallExecution) callScope and then calls selectReplayedMessages. Two things worth hardening: (1) if callScope is ever null or a different type (a subclass of ReActAgent overriding beforeAgentExecution, or a future hook that passes a different scope) this becomes a raw ClassCastException/NPE inside the call path instead of a clear error; (2) the base-class contract for prepareInputMessages is not documented here. An instanceof check that returns msgs unchanged on mismatch (or making the hook signature generic/typed) would keep the replay optimisation strictly best-effort and non-breaking for subclasses.

break;
}
}
if (lastAssistant < 0) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

selected.addAll(msgs.subList(lastAssistant + 1, msgs.size())) keeps everything after the last assistant message, and in the selected.isEmpty() fallback the single last USER message is re-appended. Please confirm the ordering guarantees for the case where a tool result for a pending call appears after the last assistant message in the client transcript — with the fallback path, a mid-transcript pending result would be dropped and only the old user prompt replayed, which is exactly the failure mode the PR sets out to fix. If that transcript shape is possible with the AG-UI client, a scan over the whole transcript (not only indices <= lastAssistant) would be more robust; if it is guaranteed impossible, it would help to state that as an assumption in the javadoc of RuntimeContext.REPLAYED_INPUT.

* incremental input or stateless, client-owned history.
*/
public static final String REPLAYED_INPUT = "agentscope_replayed_input";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit on the public API surface: this introduces a new cross-module contract (agentscope-extensions-agui writes the flag, agentscope-core reads it) as a raw string-keyed boolean attribute. Since it is a first-class behavioural switch rather than user data, consider a typed accessor on RuntimeContext (e.g. isReplayedInput() / builder().replayedInput(true)) so the flag cannot be misspelled or set to a non-Boolean value, and so it shows up in the typed-attribute API rather than in the free-form get(String) bag.

}

try {
RuntimeContext effectiveRuntimeContext =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Moving addResumeInterrupts(input, runtimeContext) above the memory/extractLatestUserMessage branch changes which input the resume interrupts are computed from (full transcript instead of the extracted last user message, for the non-ReAct-with-memory path it is now built before truncation). It looks intentional and the tests cover the ReAct case; could you add (or point to) an assertion for the non-ReAct + server-memory path so the ordering change stays pinned? That is the only branch where the pre-existing behaviour could silently drift.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]:: Message: Invalid tool result IDs: [call_01_w7wlz4ch9Hiw4Um74zwv8117]. Expected: [call_00_OugZmGqpB71ykFO5IjxM8342]

3 participants