Skip to content

feat: add streaming output for studio#565

Open
lokidundun wants to merge 8 commits into
agentscope-ai:mainfrom
lokidundun:addStreamingOutput
Open

feat: add streaming output for studio#565
lokidundun wants to merge 8 commits into
agentscope-ai:mainfrom
lokidundun:addStreamingOutput

Conversation

@lokidundun

Copy link
Copy Markdown

AgentScope-Java Version

1.0.8
[The version of AgentScope-Java you are working on, e.g. 1.0.7, check your pom.xml dependency version or run mvn dependency:tree | grep agentscope-parent:pom(only mac/linux)]

Description

[Please describe the background, purpose, changes made, and how to test this PR]
Feat #416

Checklist

Please check the following items before code is ready to be reviewed.

  • Code has been formatted with mvn spotless:apply
  • All tests are passing (mvn test)
  • Javadoc comments are complete and follow project conventions
  • Related documentation has been updated (e.g. links, examples, etc.)
  • Code is ready for review

@lokidundun lokidundun requested a review from a team January 14, 2026 16:21
@cla-assistant

cla-assistant Bot commented Jan 14, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@cla-assistant

cla-assistant Bot commented Jan 14, 2026

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 Jan 14, 2026

Copy link
Copy Markdown

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

@zhijianma PTAL

@LearningGp LearningGp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The current implementation feels more like sending discrete messages (each chunk is a separate message) rather than streaming, which isn't quite what we expected.
Consider reusing StudioWebSocketClient to handle the streaming implementation.
Also, we need to filter out the isLast flag in the events to avoid duplicates.
Let us know if you'd like to discuss this further.

@lokidundun

lokidundun commented Apr 7, 2026

Copy link
Copy Markdown
Author

The current implementation feels more like sending discrete messages (each chunk is a separate message) rather than streaming, which isn't quite what we expected. Consider reusing StudioWebSocketClient to handle the streaming implementation. Also, we need to filter out the isLast flag in the events to avoid duplicates. Let us know if you'd like to discuss this further.

Thanks a lot for the detailed feedback, this is very helpful!
You're absolutely right
Based on your suggestion, my updated plan is:

  1. Reuse StudioWebSocketClient for streaming

    • Extend StudioWebSocketClient with outbound methods like sendStreamEvent(Event event) and sendStreamCompleted().
    • These will reuse the existing Socket.IO connection and send lightweight JSON payloads for each chunk instead of creating separate messages.
  2. Stream from the agent layer using Flux<Event>

    • Use the existing StreamableAgent / AgentBase.stream(...) APIs to produce a Flux<Event>.
    • Introduce a small bridge (e.g. StudioStreamingBridge) that subscribes to this Flux<Event> and forwards:
      • EventType.TOKEN_TEXT → incremental tokens via StudioWebSocketClient.sendStreamEvent(...)
      • EventType.AGENT_RESULT → final message via WebSocket, and then once via the existing StudioClient.pushMessage(...) so the final result is still persisted as today.
  3. Handle isLast correctly to avoid duplicates

    • In the bridge, I’ll treat the isLast flag as a control signal only, not as another “content” event.
    • That means:
      • while isLast == false: only forward the incremental content to Studio via WebSocket;
      • when isLast == true: don’t create a separate duplicate chunk, just trigger sendStreamCompleted() on StudioWebSocketClient and push the final consolidated message once via StudioClient.pushMessage(...).

@LearningGp I‘m looking forward to any feedback.

@LearningGp

Copy link
Copy Markdown
Member

The current implementation feels more like sending discrete messages (each chunk is a separate message) rather than streaming, which isn't quite what we expected. Consider reusing StudioWebSocketClient to handle the streaming implementation. Also, we need to filter out the isLast flag in the events to avoid duplicates. Let us know if you'd like to discuss this further.当前的实现更像是在发送离散消息(每个块都是独立的消息),而非流式传输,这与我们预期的不太一致。建议复用 StudioWebSocketClient 来处理流式传输的实现。此外,我们需要在事件中过滤掉 isLast 标志,以避免重复。如果您希望进一步讨论此事,请告诉我们。

Thanks a lot for the detailed feedback, this is very helpful!非常感谢详细的反馈,这非常有帮助! You're absolutely right  你说得完全正确 Based on your suggestion, my updated plan is:根据您的建议,我的更新计划如下:

  1. Reuse StudioWebSocketClient for streaming  复用 StudioWebSocketClient 进行流式输出

    • Extend StudioWebSocketClient with outbound methods like sendStreamEvent(Event event) and sendStreamCompleted().扩展 StudioWebSocketClient ,添加与 sendStreamEvent(Event event)sendStreamCompleted() 类似的外向方法。
    • These will reuse the existing Socket.IO connection and send lightweight JSON payloads for each chunk instead of creating separate messages.这些将复用现有的 Socket.IO 连接,并为每个数据块发送轻量级的 JSON 负载,而不是创建单独的消息。
  2. Stream from the agent layer using Flux<Event>使用 Flux<Event> 从代理层进行流式传输

    • Use the existing StreamableAgent / AgentBase.stream(...) APIs to produce a Flux<Event>.使用现有的 StreamableAgent / AgentBase.stream(...) API 生成一个 Flux<Event>
    • Introduce a small bridge (e.g. StudioStreamingBridge) that subscribes to this Flux<Event> and forwards:
      引入一个小型桥接器(例如 StudioStreamingBridge ),该桥接器订阅此 Flux<Event> 并进行转发:
      • EventType.TOKEN_TEXT → incremental tokens via StudioWebSocketClient.sendStreamEvent(...) EventType.TOKEN_TEXT → 通过 StudioWebSocketClient.sendStreamEvent(...) 实现增量令牌
      • EventType.AGENT_RESULT → final message via WebSocket, and then once via the existing StudioClient.pushMessage(...) so the final result is still persisted as today. EventType.AGENT_RESULT → 通过 WebSocket 发送最终消息,然后通过现有的 StudioClient.pushMessage(...) 再发送一次,因此最终结果仍会像今天一样被持久化。
  3. Handle isLast correctly to avoid duplicates正确处理 isLast 以避免重复

    • In the bridge, I’ll treat the isLast flag as a control signal only, not as another “content” event.在桥接层中,我将把 isLast 标志仅视为控制信号,而非另一个“内容”事件。
    • That means:
        这意味着:
      • while isLast == false: only forward the incremental content to Studio via WebSocket;当 isLast == false 时:仅通过 WebSocket 将增量内容转发至 Studio;
      • when isLast == true: don’t create a separate duplicate chunk, just trigger sendStreamCompleted() on StudioWebSocketClient and push the final consolidated message once via StudioClient.pushMessage(...).当 isLast == true 时:不要创建单独的重复块,只需在 StudioWebSocketClient 上触发 sendStreamCompleted() ,并通过 StudioClient.pushMessage(...) 推送一次最终合并的消息。

@LearningGp I‘m looking forward to any feedback.期待任何反馈。

LGTM overall. Not sure if studio-side modifications are required, but feel free to proceed as designed. Let's focus on the final outcome.

Copilot AI review requested due to automatic review settings April 9, 2026 02:06

Copilot AI left a comment

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.

Pull request overview

Adds a Studio-side bridge for AgentScope’s StreamableAgent.stream(...) API so that selected Event emissions can be forwarded to AgentScope Studio in real time via WebSocket, while persisting the final message via the existing HTTP client.

Changes:

  • Added StudioStreamingBridge to forward a Flux<Event> to Studio WebSocket and persist a final Msg via StudioClient.
  • Extended StudioWebSocketClient with sendStreamEvent(...) / sendStreamCompleted() to emit new Socket.IO events.
  • Added a StudioManager.streamToStudio(...) entry point and a new unit test class.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
agentscope-extensions/agentscope-extensions-studio/src/main/java/io/agentscope/core/studio/StudioWebSocketClient.java Adds WebSocket emit methods for streaming events/completion (payload construction & text extraction).
agentscope-extensions/agentscope-extensions-studio/src/main/java/io/agentscope/core/studio/StudioStreamingBridge.java New bridge that consumes Flux<Event> and forwards selected events to Studio, persisting a final message.
agentscope-extensions/agentscope-extensions-studio/src/main/java/io/agentscope/core/studio/StudioStreamingRunner.java New runner wrapper around StreamableAgent.stream(...) + bridge forwarding.
agentscope-extensions/agentscope-extensions-studio/src/main/java/io/agentscope/core/studio/StudioManager.java Adds streamToStudio(...) convenience method for streaming integration.
agentscope-extensions/agentscope-extensions-studio/src/test/java/io/agentscope/core/studio/StudioStreamingBridgeTest.java Adds initial unit tests for streaming forwarding and completion/persistence behavior.

Comment on lines +403 to +409
/**
* Sends a stream event to Studio in real-time.
*/
public void sendStreamEvent(Event event) {
if (socket == null || !socket.connected()) {
logger.warn("Attempted to send stream event while WebSocket is not connected");
return;

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

sendStreamEvent/sendStreamCompleted Javadocs don’t follow the conventions used in this module (missing @param/@return details and behavioral notes such as what happens when the socket is disconnected). Please expand the Javadoc to include parameter description(s) and clarify whether these calls are best-effort or guaranteed delivery.

Copilot uses AI. Check for mistakes.
Comment on lines +465 to +472
private String extractTextFromMsg(Msg msg) {
if (msg == null || msg.getContent() == null || msg.getContent().isEmpty()) {
return null;
}
Object first = msg.getContent().get(0);
if (first instanceof TextBlock tb && tb.getText() != null) {
return tb.getText();
}

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

extractTextFromMsg only reads the first content block and only supports TextBlock. In actual streaming events, reasoning commonly uses ThinkingBlock and tool results use ToolResultBlock (with text nested in its output blocks), so Studio will often receive stream events with no text field. Update extraction to handle the relevant block types (and consider concatenating multiple text-bearing blocks) so streamed reasoning/tool output is visible.

Copilot uses AI. Check for mistakes.
Comment on lines +127 to +137
if (type == EventType.AGENT_RESULT && !isLast) {
webSocketClient.sendStreamEvent(event);
return;
}

if (!isLast
&& (type == EventType.REASONING
|| type == EventType.TOOL_RESULT
|| type == EventType.SUMMARY)) {
webSocketClient.sendStreamEvent(event);
}

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

handleEvent currently forwards REASONING / TOOL_RESULT / SUMMARY only when isLast == false. However, the core streaming implementation emits the final reasoning/tool messages with isLast == true (and may emit no chunks at all depending on StreamOptions), so these outputs can be dropped entirely. Consider forwarding these event types regardless of isLast, and reserve the special terminal-handling logic for the final AGENT_RESULT that ends the stream.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +40
public class StudioStreamingBridge {

private static final Logger logger = LoggerFactory.getLogger(StudioStreamingBridge.class);

private final StudioClient studioClient;
private final StudioWebSocketClient webSocketClient;

public StudioStreamingBridge(StudioClient studioClient, StudioWebSocketClient webSocketClient) {
this.studioClient = Objects.requireNonNull(studioClient, "studioClient must not be null");
this.webSocketClient =
Objects.requireNonNull(webSocketClient, "webSocketClient must not be null");
}

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

This class introduces a new public API but lacks class-level Javadoc and constructor Javadoc, while other public Studio integration classes document purpose/usage and public constructors. Please add class-level Javadoc (what is bridged, which events are forwarded, and how completion is signaled) and document the public constructor parameters.

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +39
public class StudioStreamingRunner {

private final StudioStreamingBridge streamingBridge;

public StudioStreamingRunner(StudioStreamingBridge streamingBridge) {
this.streamingBridge = streamingBridge;
}

public Mono<Void> runWithStreaming(StreamableAgent agent, Msg input, StreamOptions options) {
StreamOptions effectiveOptions =
options != null ? options : StreamOptions.defaults();

Flux<Event> eventFlux = agent.stream(input, effectiveOptions);
return streamingBridge.forwardToStudio(eventFlux);
}

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

StudioStreamingRunner is currently unused in the codebase (no references found), which adds public API surface without providing functionality. Either wire it into the integration entry points (e.g., StudioManager) or remove it to avoid maintaining an unused abstraction.

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +34
public class StudioStreamingRunner {

private final StudioStreamingBridge streamingBridge;

public StudioStreamingRunner(StudioStreamingBridge streamingBridge) {
this.streamingBridge = streamingBridge;
}

public Mono<Void> runWithStreaming(StreamableAgent agent, Msg input, StreamOptions options) {
StreamOptions effectiveOptions =

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

This public class/constructor/method are missing Javadoc, which is inconsistent with the rest of the Studio integration module’s public APIs. Please add class-level Javadoc and document the constructor and runWithStreaming(...) parameters/behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +129 to +140
/**
* Stream agent events to Studio.
*/
public static Mono<Void> streamToStudio(StreamableAgent agent, Msg input, StreamOptions options) {
if (!isInitialized() || client == null || wsClient == null) {
return Mono.error(new IllegalStateException("Studio is not initialized"));
}
StreamOptions effective = options != null ? options : StreamOptions.defaults();
Flux<Event> events = agent.stream(input, effective);
StudioStreamingBridge bridge = new StudioStreamingBridge(client, wsClient);
return bridge.forwardToStudio(events);
}

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

streamToStudio(...) has minimal Javadoc and is missing standard tags/behavioral notes (params, return, error conditions) that other public methods in StudioManager include. Please expand the Javadoc to describe parameters (agent/input/options), what events are forwarded, and what the returned Mono represents.

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +20
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

Avoid wildcard imports (e.g., import static org.mockito.Mockito.*;). Please import only the specific Mockito members used in this test to keep imports explicit and consistent with typical project style checks.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +65
Event reasoning1 = new Event(EventType.REASONING, msg("thinking 1"), false);
Event reasoning2 = new Event(EventType.REASONING, msg("thinking 2"), false);

Event tool1 = new Event(EventType.TOOL_RESULT, msg("tool out 1"), false);

Event answerChunk1 = new Event(EventType.AGENT_RESULT, msg("hello "), false);
Event answerChunk2 = new Event(EventType.AGENT_RESULT, msg("world"), false);

Msg finalMsg = msg("hello world");
Event answerFinal = new Event(EventType.AGENT_RESULT, finalMsg, true);

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

Current tests only use Msg with a single TextBlock, but real streaming events often carry ThinkingBlock for reasoning and ToolResultBlock for tool output (and the final reasoning/tool emissions are typically isLast == true). Add test cases that reflect these real event shapes so the streaming-to-Studio path is validated end-to-end for reasoning/tool output visibility.

Copilot uses AI. Check for mistakes.
@lokidundun lokidundun requested a review from LearningGp April 9, 2026 04:02
@lokidundun

Copy link
Copy Markdown
Author

@LearningGp I have submitted the code, could you please take another look when you are convenient❤️

@LearningGp

Copy link
Copy Markdown
Member

Based on E2E validation, I've found that the current implementation doesn't achieve streaming output. While there are a few bugs to address, the primary blocker is that the Studio Server doesn't support streaming yet.

Consequently, we’ll need to hold off on merging this PR for the time being. That said, would you be interested in contributing to the Studio project to help implement the necessary server-side support?

@lokidundun

Copy link
Copy Markdown
Author

Based on E2E validation, I've found that the current implementation doesn't achieve streaming output. While there are a few bugs to address, the primary blocker is that the Studio Server doesn't support streaming yet.

Consequently, we’ll need to hold off on merging this PR for the time being. That said, would you be interested in contributing to the Studio project to help implement the necessary server-side support?

Thanks for the clarification! I’d be happy to contribute to the Studio project and help implement the server-side streaming support. Please let me know what I need to do next.❤️

@AgentScopeJavaBot AgentScopeJavaBot added enhancement New feature or request area/ext/integration External protocols & middleware integrations labels May 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ext/integration External protocols & middleware integrations enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants