Skip to content

feat(agui): convert reasoning messages - #3239

Open
akkupratap323 wants to merge 1 commit into
agentscope-ai:mainfrom
akkupratap323:fix/agui-reasoning-messages
Open

akkupratap323 wants to merge 1 commit into
agentscope-ai:mainfrom
akkupratap323:fix/agui-reasoning-messages

Conversation

@akkupratap323

Copy link
Copy Markdown

AgentScope-Java Version

2.0.4-SNAPSHOT

Description

AguiMessageConverter currently drops ThinkingBlock content when converting AgentScope messages to AG-UI messages. This leaves non-streaming consumers without the reasoning message defined by the AG-UI protocol.

This change:

  • adds the reasoning role and a reasoning-message factory to AguiMessage;
  • converts reasoning messages back to AgentScope ThinkingBlock instances;
  • emits reasoning-only messages from toAguiMessage;
  • adds toAguiMessages and updates list conversion so mixed reasoning/assistant turns retain both parts with distinct IDs; and
  • adds regression coverage for reasoning-only, mixed, flattened, and reverse conversions.

Closes #2859.

Validation

  • mvn -pl agentscope-extensions/agentscope-extensions-protocol/agentscope-extensions-agui spotless:apply
  • mvn -pl agentscope-extensions/agentscope-extensions-protocol/agentscope-extensions-agui -am -Dtest=AguiMessageConverterTest,AguiModelTest -Dsurefire.failIfNoSpecifiedTests=false test (98 tests, 0 failures)

Checklist

  • Code has been formatted with mvn spotless:apply
  • Focused tests are passing
  • Javadoc comments are complete and follow project conventions
  • Documentation changes are not required for this converter fix
  • Code is ready for review

@CLAassistant

CLAassistant commented Sep 21, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@codecov

codecov Bot commented Sep 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.05128% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...cope/core/agui/converter/AguiMessageConverter.java 81.08% 3 Missing and 4 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

This PR closes a real gap: AguiMessageConverter used to drop ThinkingBlock content entirely, so non-streaming AG-UI consumers never saw reasoning. Adding a reasoning role plus reasoningMessage(...) / toAguiMessages(...) and wiring toAguiMessageList through flatMap is the right shape, the mixed-turn split is tested in both directions, and the appendContent extraction removes the repeated length() > 0 boilerplate cleanly. Thanks for the thorough test coverage on a first contribution!

No blocking issues — I left the findings as inline comments, mostly about round-trip fidelity and about keeping the new non-streaming behaviour consistent with the existing streaming path:

  • [Warning] the streaming path gates reasoning behind AguiAdapterConfig.isEnableReasoning() (ThinkingBlockEventConverter too), while the converter path now always emits it — worth aligning or documenting.
  • [Warning] msg.getId() + "-reasoning" is neither collision-proof nor reversible; toMsg cannot restore the original id.
  • [Warning] ThinkingBlock.getMetadata() (METADATA_REASONING_DETAILS, OpenRouter/Gemini encrypted reasoning) is dropped on both directions.
  • [Warning] toMsg on a reasoning message with MessageContent.Blocks throws the generic "only supported for AG-UI user messages" error.
  • [Info] toAguiMessageList becomes 1:N — public API behaviour change worth a changelog note; toAguiMessage (singular) still loses reasoning for mixed turns.
  • [Info] reasoning comes from the AG-UI Reasoning draft; linking the spec in the javadoc/description helps consumers who validate against the stable role enum.

Verdict: COMMENT — the design is sound and the concerns above are non-blocking polish. Looks ready for maintainer attention once CI is green.


Automated review by github-manager-bot

* @param msg The AgentScope message to convert
* @return The converted AG-UI messages
*/
public List<AguiMessage> toAguiMessages(Msg msg) {

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.

Nice to see reasoning finally reaching non-streaming consumers. One asymmetry worth noting though: the streaming path in AguiAgentAdapter only emits REASONING_* events when AguiAdapterConfig.isEnableReasoning() is true, while toAguiMessages/toAguiMessage emit a reasoning message unconditionally. A deployment that turned reasoning off for streaming will now still get thinking content through the non-streaming conversion path. Would it make sense to gate this behind the same flag (e.g. an enableReasoning option on the converter, defaulting to the current behaviour) so both paths agree?


return List.of(
AguiMessage.reasoningMessage(
msg.getId() + "-reasoning", reasoningContent.toString()),

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.

Two related concerns with the derived ID:

  1. msg.getId() + "-reasoning" can collide with an ID that already exists in the conversation (IDs are caller-supplied), and AguiMessage only enforces non-null, not uniqueness.
  2. It is not reversible: feeding the pair back through toMsg produces an assistant Msg with id <id>-reasoning instead of the original <id>, so a Msg -> AguiMessage -> Msg round-trip no longer preserves identity.

Could the suffix be made collision-resistant (hash/UUID or a reserved separator) and/or the origin id carried in message metadata so the reverse conversion can restore it? At minimum a javadoc note on the ID contract would help downstream consumers.

content.append(tb.getText());
appendContent(content, tb.getText());
} else if (block instanceof ThinkingBlock tb) {
appendContent(reasoningContent, tb.getThinking());

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.

ThinkingBlock also carries getMetadata() (METADATA_REASONING_DETAILS, used by providers such as Gemini/OpenRouter to keep signed reasoning details across turns). Here only getThinking() is propagated, and addThinkingBlock on the reverse path never restores metadata, so a Msg -> AguiMessage -> Msg round-trip silently drops it. If reasoning details are needed to continue a conversation with those providers, that can degrade into provider errors or lost tool-call continuity. Either propagate the metadata (e.g. as an AG-UI message name/metadata field) or document explicitly that AG-UI reasoning messages are lossy by design.

MessageContent content = aguiMessage.getContent();
if (content instanceof MessageContent.Text text) {
addTextBlock(blocks, text.value(), aguiMessage);
if (aguiMessage.isReasoningMessage()) {

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.

The reverse conversion only handles MessageContent.Text. A reasoning message deserialized with structured MessageContent.Blocks falls into the blocksContent branch below, where !isUserMessage() throws IllegalArgumentException("Structured content blocks are only supported for AG-UI user messages"). That is technically consistent with assistant/system/tool messages, but since reasoning is a brand-new role, it would be friendlier to either convert the text parts into ThinkingBlocks or raise a role-specific error message. A test for this input shape would pin the intended behaviour.

public List<AguiMessage> toAguiMessageList(List<Msg> msgs) {
return msgs.stream().map(this::toAguiMessage).collect(Collectors.toList());
return msgs.stream()
.flatMap(msg -> toAguiMessages(msg).stream())

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.

toAguiMessageList changes from a 1:1 map to a 1:N flatMap, which is an observable behaviour change on a public API of this extension (callers that zipped the result back against the input list by index would break). Grep shows no in-repo production caller today, so this is mainly about the changelog: worth calling out in the release notes / class javadoc that the output size is no longer equal to the input size. Also, toAguiMessage (singular) still drops the reasoning part for mixed turns, so callers who use it directly keep the old data loss - consider deprecating it in favour of toAguiMessages.

* @param content The reasoning content as plain text
* @return A new reasoning message
*/
public static AguiMessage reasoningMessage(String id, String content) {

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.

Minor: reasoning is not part of the stable AG-UI Role enum, it comes from the Reasoning draft (the streaming adapter references the same draft). Downstream consumers that validate roles against the stable enum will reject these messages. Could the PR description or the javadoc link the exact draft spec being followed, so implementers know this is draft-gated and may change?

@oss-maintainer

Copy link
Copy Markdown
Collaborator

CI note

The only failing required check is build (windows-latest). It fails in agentscope-service/service-dataplane:

io.agentscope.builder.web.toolbus.ToolConfirmationCoordinatorTest
  #replacementTurnLeaseCannotReleaseOldTicketAndMayReuseToolUseId  <<< FAILURE!
Tests run: 73, Failures: 1, Errors: 0, Skipped: 0

service-dataplane does not depend on the agentscope-extensions-agui module touched by this PR, and the ubuntu-latest build plus codecov/patch are green, so this looks like a pre-existing Windows-only failure rather than something introduced here — please don't spend time on it; a maintainer can re-run the job to confirm.

Current mergeable_state is blocked because a maintainer review is still required (the automated review above is advisory, not an approval gate).


Automated note by github-manager-bot

This branch has not been deployed

No deployments
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.

[Feature]: AguiMessageConverter::toAguiMessage 为什么没有转化reasoning message

3 participants