Skip to content

fix(agui): give reasoning messages a distinct messageId from the text answer#1778

Open
ql-wade wants to merge 2 commits into
agentscope-ai:mainfrom
ql-wade:fix/agui-reasoning-distinct-message-id
Open

fix(agui): give reasoning messages a distinct messageId from the text answer#1778
ql-wade wants to merge 2 commits into
agentscope-ai:mainfrom
ql-wade:fix/agui-reasoning-distinct-message-id

Conversation

@ql-wade

@ql-wade ql-wade commented Jun 16, 2026

Copy link
Copy Markdown

Problem

When enableReasoning=true, a single assistant message carrying both a ThinkingBlock and a TextBlock emits REASONING_MESSAGE_* and TEXT_MESSAGE_* that share the same messageId (both use msg.getId()):

REASONING_MESSAGE_START  messageId=<msgId>
TEXT_MESSAGE_START       messageId=<msgId>   ← same id

AG-UI clients key message grouping on messageId, so they collapse the reasoning and the answer into one message bubble. Observed with CopilotKit: the answer gets folded into the "Thought for a few seconds" panel and cannot be shown as a standalone, always-visible message.

Root cause

In AguiAgentAdapter#convertEvent, the TextBlock branch (→ TEXT_MESSAGE_*) and the ThinkingBlock branch (→ REASONING_MESSAGE_*) both set:

String messageId = msg.getId();

Per the AG-UI protocol, a reasoning message (role "reasoning") and the text answer (role "assistant") are independent messages — associated only by runId and order, never by messageId (the event records carry no linkage field). Reusing one id therefore conflates two distinct messages.

Fix

Give the reasoning message a distinct id: msg.getId() + "-reasoning".

  • Renders independently of the answer; the answer stays always-visible while reasoning stays collapsible.
  • A suffix (rather than a fresh UUID) keeps multiple ThinkingBlocks in one message deduped into a single reasoning message, mirroring how the text answer is one message.

Verification

  • New regression test testReasoningAndTextAnswerHaveDistinctMessageIds: a message with both a ThinkingBlock and a TextBlock must emit start events with different messageIds (msg-both-reasoning vs msg-both). Full suite green (36/36).
  • End-to-end with CopilotKit consuming POST /agui/run: before the fix the reasoning+answer LCA was inside the collapsible cpk:grid container; after the fix the LCA is the top-level message list → reasoning and answer render as separate messages.

Scope

One-line behavioral change + comment + test. No changes to event types or wire shape.

beatwade and others added 2 commits June 14, 2026 23:11
…te_file is gated (D-08)

D-08 follow-up: the prior commit (admin-service 34d25c5d) seeded a non-trivial
permission context on the agent builder (b.permissionContext(...)) and its unit
test passed, but live write_file still ran unchecked. Root cause, confirmed by
reading the load path: ReActAgent.loadOrCreateAgentStateForSlot returns a
persisted AgentState verbatim, and only falls back to freshState(initialCtx) on
a store MISS. So the builder's initialPermissionContext only reached FRESH
sessions; any session persisted before the builder seeded a non-trivial context
kept its trivial context (DEFAULT, no rules) forever -> isTrivial() true ->
evaluatePermissions took the lightweight path -> write_file's passthrough
self-check ALLOWed it -> RequireUserConfirmEvent never fired.

Fix: in loadOrCreateAgentStateForSlot, merge the builder's initial permission
context onto the loaded state when (and only when) the loaded state's context is
still trivial. This upgrades stale persisted sessions in place while preserving
their conversation/tool state. A session whose mode was deliberately toggled via
setPermissionMode is left untouched (non-trivial by mode change).

Acceptance test (ReActAgentStalePermissionContextTest) drives the real ReActAgent
+ real AgentStateStore against a pre-seeded trivial persisted session and a
passthrough write_file tool (mirroring the harness FilesystemTool), asserting
PERMISSION_ASKING + RequireUserConfirmEvent. Red without the merge, green with.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AguiAgentAdapter used the same msg.getId() for both the TEXT_MESSAGE_*
(answer) and REASONING_MESSAGE_* (thinking) derived from a single agent
message. In AG-UI these are independent messages (roles 'assistant' and
'reasoning') associated only by runId and order, never by messageId.

Sharing one messageId makes clients group them into a single message
bubble — e.g. the answer gets folded into the reasoning/thought panel
instead of rendering as a standalone, always-visible message.

Use msg.getId() + "-reasoning" for the reasoning message so it renders
independently and stays collapsible. A suffix (rather than a fresh UUID)
keeps multiple ThinkingBlocks in one message deduped into a single
reasoning message, mirroring the text answer.

Added a regression test asserting distinct messageIds.
@ql-wade ql-wade requested a review from a team June 16, 2026 11:14
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@codecov

codecov Bot commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...e/src/main/java/io/agentscope/core/ReActAgent.java 82.75% 1 Missing and 4 partials ⚠️

📢 Thoughts on this report? Let us know!

@AgentScopeJavaBot AgentScopeJavaBot added bug Something isn't working area/core/agent Agent runtime, pipeline, hooks, plan area/ext/integration External protocols & middleware integrations labels Jun 17, 2026

@AgentScopeJavaBot AgentScopeJavaBot 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.

🤖 AI Review

This PR contains two unrelated but individually correct fixes: (1) giving AG-UI reasoning messages a distinct messageId with -reasoning suffix to prevent client-side message collapsing, and (2) fixing stale PermissionContextState being discarded when the builder's initial trivial state overwrites loaded non-trivial state. Both fixes are correct in logic and well-tested. However, the PR description only mentions the first fix, and the two changes should ideally be in separate PRs.

* @param initialPermCtx the builder's initial permission context (may be {@code null})
* @return {@code loaded} unchanged when it already has a non-trivial context or no initial
* context was supplied; otherwise a copy of {@code loaded} with the initial context applied
*/

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.

[recommended] PR contains two unrelated fixes (AG-UI messageId + ReActAgent permission context merge) but only describes the first. Consider splitting into two PRs for easier review, bisect, and revert. Also, the mergeInitialPermissionContext method manually copies 10+ fields from AgentState — if AgentState gains new fields in the future, this method will silently drop them. Consider adding a Builder.from(AgentState) factory method to prevent this maintenance hazard.


@Test
void testRunWithStreamingThinkingBlockEvents() {
// Test streaming reasoning events when enabled

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.

[nitpick] The streaming reasoning test asserts event counts but does not verify the messageId ends with -reasoning. Consider adding an assertion on the actual messageId value.

@AgentScopeJavaBot AgentScopeJavaBot 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.

🤖 AI Review

This PR contains two unrelated but individually correct fixes: (1) giving AG-UI reasoning messages a distinct messageId with -reasoning suffix to prevent client-side message collapsing, and (2) fixing stale PermissionContextState being discarded when the builder's initial trivial state overwrites loaded non-trivial state. Both fixes are correct in logic and well-tested. However, the PR description only mentions the first fix, and the two changes should ideally be in separate PRs.

* @param initialPermCtx the builder's initial permission context (may be {@code null})
* @return {@code loaded} unchanged when it already has a non-trivial context or no initial
* context was supplied; otherwise a copy of {@code loaded} with the initial context applied
*/

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.

[recommended] PR contains two unrelated fixes (AG-UI messageId + ReActAgent permission context merge) but only describes the first. Consider splitting into two PRs for easier review, bisect, and revert. Also, the mergeInitialPermissionContext method manually copies 10+ fields from AgentState — if AgentState gains new fields in the future, this method will silently drop them. Consider adding a Builder.from(AgentState) factory method to prevent this maintenance hazard.


@Test
void testRunWithStreamingThinkingBlockEvents() {
// Test streaming reasoning events when enabled

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.

[nitpick] The streaming reasoning test asserts event counts but does not verify the messageId ends with -reasoning. Consider adding an assertion on the actual messageId value.

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

Labels

area/core/agent Agent runtime, pipeline, hooks, plan area/ext/integration External protocols & middleware integrations bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants