From c6a610bf06dfd3dcf8e167bffd4184d800495246 Mon Sep 17 00:00:00 2001 From: jujn <2087687391@qq.com> Date: Tue, 5 May 2026 22:39:26 +0800 Subject: [PATCH 01/20] refactor(agui): redesign event dispatching with strategy pattern and deferred queue --- .../agentscope/core/message/ContentBlock.java | 7 +- .../agentscope/core/message/CustomBlock.java | 116 +++++ .../core/agui/adapter/AguiAgentAdapter.java | 468 +++--------------- .../core/agui/adapter/StreamContext.java | 191 +++++++ .../adapter/strategy/BlockEventConverter.java | 45 ++ .../strategy/CustomBlockConverter.java | 43 ++ .../adapter/strategy/TextBlockConverter.java | 56 +++ .../strategy/ThinkingBlockConverter.java | 61 +++ .../strategy/ToolResultBlockConverter.java | 92 ++++ .../strategy/ToolUseBlockConverter.java | 61 +++ .../agui/adapter/AguiAgentAdapterTest.java | 30 +- 11 files changed, 769 insertions(+), 401 deletions(-) create mode 100644 agentscope-core/src/main/java/io/agentscope/core/message/CustomBlock.java create mode 100644 agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/StreamContext.java create mode 100644 agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/BlockEventConverter.java create mode 100644 agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/CustomBlockConverter.java create mode 100644 agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/TextBlockConverter.java create mode 100644 agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ThinkingBlockConverter.java create mode 100644 agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ToolResultBlockConverter.java create mode 100644 agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ToolUseBlockConverter.java diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/ContentBlock.java b/agentscope-core/src/main/java/io/agentscope/core/message/ContentBlock.java index 55afe0393c..d9b9ac10a7 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/message/ContentBlock.java +++ b/agentscope-core/src/main/java/io/agentscope/core/message/ContentBlock.java @@ -35,6 +35,7 @@ *
Uses Jackson annotations for polymorphic JSON serialization with the "type" discriminator @@ -49,7 +50,8 @@ @JsonSubTypes.Type(value = AudioBlock.class, name = "audio"), @JsonSubTypes.Type(value = VideoBlock.class, name = "video"), @JsonSubTypes.Type(value = ToolUseBlock.class, name = "tool_use"), - @JsonSubTypes.Type(value = ToolResultBlock.class, name = "tool_result") + @JsonSubTypes.Type(value = ToolResultBlock.class, name = "tool_result"), + @JsonSubTypes.Type(value = CustomBlock.class, name = "custom") }) public sealed class ContentBlock implements State permits TextBlock, @@ -58,4 +60,5 @@ public sealed class ContentBlock implements State VideoBlock, ThinkingBlock, ToolUseBlock, - ToolResultBlock {} + ToolResultBlock, + CustomBlock {} diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/CustomBlock.java b/agentscope-core/src/main/java/io/agentscope/core/message/CustomBlock.java new file mode 100644 index 0000000000..c0658e5d8d --- /dev/null +++ b/agentscope-core/src/main/java/io/agentscope/core/message/CustomBlock.java @@ -0,0 +1,116 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.core.message; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a custom extension block specifically designed to trigger AG-UI Custom events. + * + *
This block allows for the transmission of arbitrary JSON-serializable payloads + * to the frontend. It is commonly used for real-time progress pushes, workflow + * state changes, and any other customized interactive behaviors within the AG-UI protocol. + * + *
The name identifies the custom event type, and the value can be a Map, String, + * Number, or any JSON-serializable object containing the event payload. + */ +public final class CustomBlock extends ContentBlock { + + private final String name; + private final Object value; + + /** + * Creates a new custom block for JSON deserialization. + * + * @param name The name of the custom event + * @param value The arbitrary payload value associated with the event + */ + @JsonCreator + public CustomBlock( + @JsonProperty("name") String name, + @JsonProperty("value") Object value) { + this.name = name; + this.value = value; + } + + /** + * Gets the name of this custom block. + * + * @return The event name + */ + public String getName() { + return name; + } + + /** + * Gets the payload value of this custom block. + * + * @return The payload object + */ + public Object getValue() { + return value; + } + + /** + * Creates a new builder for constructing CustomBlock instances. + * + * @return A new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for constructing CustomBlock instances. + */ + public static class Builder { + + private String name; + private Object value; + + /** + * Sets the name for the custom block event. + * + * @param name The custom event name + * @return This builder for chaining + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * Sets the arbitrary payload value for the block. + * + * @param value payload object + * @return This builder for chaining + */ + public Builder value(Object value) { + this.value = value; + return this; + } + + /** + * Builds a new CustomBlock with the configured properties. + * + * @return A new CustomBlock instance + */ + public CustomBlock build() { + return new CustomBlock(name, value); + } + } +} \ No newline at end of file diff --git a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/AguiAgentAdapter.java b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/AguiAgentAdapter.java index 2314e72b9d..f0e88b0339 100644 --- a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/AguiAgentAdapter.java +++ b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/AguiAgentAdapter.java @@ -20,24 +20,28 @@ import io.agentscope.core.agent.EventType; import io.agentscope.core.agent.StreamOptions; import io.agentscope.core.agui.converter.AguiMessageConverter; +import io.agentscope.core.agui.adapter.strategy.BlockEventConverter; +import io.agentscope.core.agui.adapter.strategy.CustomBlockConverter; +import io.agentscope.core.agui.adapter.strategy.TextBlockConverter; +import io.agentscope.core.agui.adapter.strategy.ThinkingBlockConverter; +import io.agentscope.core.agui.adapter.strategy.ToolResultBlockConverter; +import io.agentscope.core.agui.adapter.strategy.ToolUseBlockConverter; import io.agentscope.core.agui.event.AguiEvent; import io.agentscope.core.agui.model.RunAgentInput; import io.agentscope.core.message.ContentBlock; +import io.agentscope.core.message.CustomBlock; import io.agentscope.core.message.Msg; import io.agentscope.core.message.TextBlock; import io.agentscope.core.message.ThinkingBlock; import io.agentscope.core.message.ToolResultBlock; import io.agentscope.core.message.ToolUseBlock; -import io.agentscope.core.util.JsonException; -import io.agentscope.core.util.JsonUtils; +import reactor.core.publisher.Flux; + import java.util.ArrayList; -import java.util.LinkedHashSet; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; -import java.util.UUID; -import reactor.core.publisher.Flux; /** * Adapter that bridges AgentScope agents to the AG-UI protocol. @@ -48,15 +52,15 @@ *
Event Mapping: *
Reasoning Support: *
An anonymous tool result can only be matched safely when there is exactly one active tool
+ * call. If multiple tool calls are active, the result is ambiguous and should not be associated
+ * with any particular tool call by guesswork.
+ *
+ * @return the only active tool call ID, or an empty Optional if the result cannot be matched
+ */
+ public Optional When streaming tool arguments, multiple ToolUseBlock chunks may belong to the same anonymous
+ * tool call. This method reuses an active anonymous tool call with the same normalized tool name
+ * whenever the match is unambiguous. If no matching anonymous tool call exists, a new local ID is
+ * generated and tracked for subsequent chunks.
+ *
+ * @param toolName The tool name from the ToolUseBlock
+ * @return a stable local tool call ID for the anonymous tool use
+ */
+ public String resolveOrCreateAnonymousToolUseId(String toolName) {
+ String normalizedName =
+ toolName != null && !toolName.isBlank() ? toolName : "unknown";
+
+ String matchedId = null;
+ for (String id : anonymousToolIds) {
+ if (activeToolIds.contains(id)
+ && Objects.equals(anonymousToolNames.get(id), normalizedName)) {
+ matchedId = id;
+ }
+ }
+
+ if (matchedId != null) {
+ return matchedId;
+ }
+
+ String generatedId = UUID.randomUUID().toString();
+ anonymousToolIds.add(generatedId);
+ anonymousToolNames.put(generatedId, normalizedName);
+ return generatedId;
}
}
diff --git a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ToolResultBlockConverter.java b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ToolResultBlockConverter.java
index 5bcf8585d7..799ac01480 100644
--- a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ToolResultBlockConverter.java
+++ b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ToolResultBlockConverter.java
@@ -39,9 +39,7 @@ public void convert(ToolResultBlock block, Event event, StreamContext ctx) {
String toolCallId =
block.getId() != null
? block.getId()
- : (ctx.getLastActiveToolId() != null
- ? ctx.getLastActiveToolId()
- : UUID.randomUUID().toString());
+ : ctx.resolveAnonymousToolResultId().orElseGet(() -> UUID.randomUUID().toString());
String result = extractToolResultText(block);
// Closing Start/End Phase
diff --git a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ToolUseBlockConverter.java b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ToolUseBlockConverter.java
index 98e2aff46a..c290b5be22 100644
--- a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ToolUseBlockConverter.java
+++ b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ToolUseBlockConverter.java
@@ -34,22 +34,21 @@ public boolean isApplicable(Event event) {
@Override
public void convert(ToolUseBlock block, Event event, StreamContext ctx) {
+ String toolName =
+ block.getName() != null && !block.getName().isBlank()
+ ? block.getName()
+ : "unknown";
+
String toolCallId =
block.getId() != null
? block.getId()
- : (ctx.getLastActiveToolId() != null
- ? ctx.getLastActiveToolId()
- : UUID.randomUUID().toString());
+ : ctx.resolveOrCreateAnonymousToolUseId(toolName);
if (!ctx.isToolActive(toolCallId)) {
// End any active Text/Reasoning message before starting tool call
ctx.flushAllActiveTexts();
ctx.flushAllActiveReasonings();
- String toolName =
- block.getName() != null && !block.getName().isBlank()
- ? block.getName()
- : "unknown";
ctx.emit(
new AguiEvent.ToolCallStart(
ctx.getThreadId(), ctx.getRunId(), toolCallId, toolName));
From 5609ac5691254fdc6fcac47ea30b9d5c10ef9328 Mon Sep 17 00:00:00 2001
From: jujn <2087687391@qq.com>
Date: Sat, 16 May 2026 22:50:33 +0800
Subject: [PATCH 10/20] fix: code format
---
.../core/agui/adapter/StreamContext.java | 17 ++++-------------
.../strategy/ToolResultBlockConverter.java | 3 ++-
.../adapter/strategy/ToolUseBlockConverter.java | 5 +----
3 files changed, 7 insertions(+), 18 deletions(-)
diff --git a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/StreamContext.java b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/StreamContext.java
index 17e30957a9..ba9b753119 100644
--- a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/StreamContext.java
+++ b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/StreamContext.java
@@ -146,22 +146,14 @@ public void flushEndEvent(String id) {
/**
* Flushes all remaining deferred end events.
- * Typically called during stream termination or error recovery to ensure all UI components are closed.
+ * Commonly used when the stream completes or fails to ensure all pending
+ * component end events are emitted before the run is finished.
*
- * @return A list of all remaining deferred events
+ * @return A list of all remaining deferred end events
*/
public List An anonymous tool result can only be matched safely when there is exactly one active tool
- * call. If multiple tool calls are active, the result is ambiguous and should not be associated
- * with any particular tool call by guesswork.
- *
- * @return the only active tool call ID, or an empty Optional if the result cannot be matched
- */
- public Optional When streaming tool arguments, multiple ToolUseBlock chunks may belong to the same anonymous
- * tool call. This method reuses an active anonymous tool call with the same normalized tool name
- * whenever the match is unambiguous. If no matching anonymous tool call exists, a new local ID is
- * generated and tracked for subsequent chunks.
- *
- * @param toolName The tool name from the ToolUseBlock
- * @return a stable local tool call ID for the anonymous tool use
- */
- public String resolveOrCreateAnonymousToolUseId(String toolName) {
- String normalizedName = toolName != null && !toolName.isBlank() ? toolName : "unknown";
-
- String matchedId = null;
- for (String id : anonymousToolIds) {
- if (activeToolIds.contains(id)
- && Objects.equals(anonymousToolNames.get(id), normalizedName)) {
- matchedId = id;
- }
- }
-
- if (matchedId != null) {
- return matchedId;
- }
-
- String generatedId = UUID.randomUUID().toString();
- anonymousToolIds.add(generatedId);
- anonymousToolNames.put(generatedId, normalizedName);
- return generatedId;
}
}
diff --git a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ToolResultBlockConverter.java b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ToolResultBlockConverter.java
index 06c638221f..497ccb24a9 100644
--- a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ToolResultBlockConverter.java
+++ b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ToolResultBlockConverter.java
@@ -37,10 +37,10 @@ public boolean isApplicable(Event event) {
@Override
public void convert(ToolResultBlock block, Event event, StreamContext ctx) {
String toolCallId =
- block.getId() != null
+ block.getId() != null && !block.getId().isBlank()
? block.getId()
- : ctx.resolveAnonymousToolResultId()
- .orElseGet(() -> UUID.randomUUID().toString());
+ : UUID.randomUUID().toString();
+
String result = extractToolResultText(block);
// Closing Start/End Phase
diff --git a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ToolUseBlockConverter.java b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ToolUseBlockConverter.java
index 485b55fe2a..77a2ea47c5 100644
--- a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ToolUseBlockConverter.java
+++ b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ToolUseBlockConverter.java
@@ -20,6 +20,7 @@
import io.agentscope.core.agui.adapter.StreamContext;
import io.agentscope.core.agui.event.AguiEvent;
import io.agentscope.core.message.ToolUseBlock;
+import java.util.UUID;
/**
* Converter for handling ToolUseBlock events, transforming them into AG-UI ToolCallStart and ToolCallArgs events.
@@ -33,19 +34,21 @@ public boolean isApplicable(Event event) {
@Override
public void convert(ToolUseBlock block, Event event, StreamContext ctx) {
- String toolName =
- block.getName() != null && !block.getName().isBlank() ? block.getName() : "unknown";
-
String toolCallId =
- block.getId() != null
+ block.getId() != null && !block.getId().isBlank()
? block.getId()
- : ctx.resolveOrCreateAnonymousToolUseId(toolName);
+ : UUID.randomUUID().toString();
if (!ctx.isToolActive(toolCallId)) {
// End any active Text/Reasoning message before starting tool call
ctx.flushAllActiveTexts();
ctx.flushAllActiveReasonings();
+ String toolName =
+ block.getName() != null && !block.getName().isBlank()
+ ? block.getName()
+ : "unknown";
+
ctx.emit(
new AguiEvent.ToolCallStart(
ctx.getThreadId(), ctx.getRunId(), toolCallId, toolName));
From a229876261a5f74750684fa580906067b324d15a Mon Sep 17 00:00:00 2001
From: jujn <2087687391@qq.com>
Date: Mon, 18 May 2026 23:25:47 +0800
Subject: [PATCH 13/20] fix: remove ag-ui custom event
---
.../agentscope/core/message/ContentBlock.java | 7 +-
.../agentscope/core/message/CustomBlock.java | 114 ------------------
.../core/agui/adapter/AguiAgentAdapter.java | 4 -
.../strategy/CustomBlockConverter.java | 40 ------
.../agui/adapter/AguiAgentAdapterTest.java | 93 --------------
5 files changed, 2 insertions(+), 256 deletions(-)
delete mode 100644 agentscope-core/src/main/java/io/agentscope/core/message/CustomBlock.java
delete mode 100644 agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/CustomBlockConverter.java
diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/ContentBlock.java b/agentscope-core/src/main/java/io/agentscope/core/message/ContentBlock.java
index d9b9ac10a7..55afe0393c 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/message/ContentBlock.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/message/ContentBlock.java
@@ -35,7 +35,6 @@
* Uses Jackson annotations for polymorphic JSON serialization with the "type" discriminator
@@ -50,8 +49,7 @@
@JsonSubTypes.Type(value = AudioBlock.class, name = "audio"),
@JsonSubTypes.Type(value = VideoBlock.class, name = "video"),
@JsonSubTypes.Type(value = ToolUseBlock.class, name = "tool_use"),
- @JsonSubTypes.Type(value = ToolResultBlock.class, name = "tool_result"),
- @JsonSubTypes.Type(value = CustomBlock.class, name = "custom")
+ @JsonSubTypes.Type(value = ToolResultBlock.class, name = "tool_result")
})
public sealed class ContentBlock implements State
permits TextBlock,
@@ -60,5 +58,4 @@ public sealed class ContentBlock implements State
VideoBlock,
ThinkingBlock,
ToolUseBlock,
- ToolResultBlock,
- CustomBlock {}
+ ToolResultBlock {}
diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/CustomBlock.java b/agentscope-core/src/main/java/io/agentscope/core/message/CustomBlock.java
deleted file mode 100644
index 515a360ae5..0000000000
--- a/agentscope-core/src/main/java/io/agentscope/core/message/CustomBlock.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright 2024-2026 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package io.agentscope.core.message;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-/**
- * Represents a custom extension block specifically designed to trigger AG-UI Custom events.
- *
- * This block allows for the transmission of arbitrary JSON-serializable payloads
- * to the frontend. It is commonly used for real-time progress pushes, workflow
- * state changes, and any other customized interactive behaviors within the AG-UI protocol.
- *
- * The name identifies the custom event type, and the value can be a Map, String,
- * Number, or any JSON-serializable object containing the event payload.
- */
-public final class CustomBlock extends ContentBlock {
-
- private final String name;
- private final Object value;
-
- /**
- * Creates a new custom block for JSON deserialization.
- *
- * @param name The name of the custom event (null will be converted to empty string)
- * @param value The arbitrary payload value associated with the event
- */
- @JsonCreator
- public CustomBlock(@JsonProperty("name") String name, @JsonProperty("value") Object value) {
- this.name = name != null ? name : "";
- this.value = value;
- }
-
- /**
- * Gets the name of this custom block.
- *
- * @return The event name
- */
- public String getName() {
- return name;
- }
-
- /**
- * Gets the payload value of this custom block.
- *
- * @return The payload object
- */
- public Object getValue() {
- return value;
- }
-
- /**
- * Creates a new builder for constructing CustomBlock instances.
- *
- * @return A new builder instance
- */
- public static Builder builder() {
- return new Builder();
- }
-
- /**
- * Builder for constructing CustomBlock instances.
- */
- public static class Builder {
-
- private String name;
- private Object value;
-
- /**
- * Sets the name for the custom block event.
- *
- * @param name The custom event name
- * @return This builder for chaining
- */
- public Builder name(String name) {
- this.name = name;
- return this;
- }
-
- /**
- * Sets the arbitrary payload value for the block.
- *
- * @param value payload object
- * @return This builder for chaining
- */
- public Builder value(Object value) {
- this.value = value;
- return this;
- }
-
- /**
- * Builds a new CustomBlock with the configured properties.
- *
- * @return A new CustomBlock instance (null name will be converted to empty string)
- */
- public CustomBlock build() {
- return new CustomBlock(name != null ? name : "", value);
- }
- }
-}
diff --git a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/AguiAgentAdapter.java b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/AguiAgentAdapter.java
index b7cfbd63b1..7dff36a397 100644
--- a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/AguiAgentAdapter.java
+++ b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/AguiAgentAdapter.java
@@ -20,7 +20,6 @@
import io.agentscope.core.agent.EventType;
import io.agentscope.core.agent.StreamOptions;
import io.agentscope.core.agui.adapter.strategy.BlockEventConverter;
-import io.agentscope.core.agui.adapter.strategy.CustomBlockConverter;
import io.agentscope.core.agui.adapter.strategy.TextBlockConverter;
import io.agentscope.core.agui.adapter.strategy.ThinkingBlockConverter;
import io.agentscope.core.agui.adapter.strategy.ToolResultBlockConverter;
@@ -29,7 +28,6 @@
import io.agentscope.core.agui.event.AguiEvent;
import io.agentscope.core.agui.model.RunAgentInput;
import io.agentscope.core.message.ContentBlock;
-import io.agentscope.core.message.CustomBlock;
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.TextBlock;
import io.agentscope.core.message.ThinkingBlock;
@@ -54,7 +52,6 @@
* Reasoning Support:
@@ -87,7 +84,6 @@ public AguiAgentAdapter(Agent agent, AguiAdapterConfig config) {
converters.put(ThinkingBlock.class, new ThinkingBlockConverter());
converters.put(ToolUseBlock.class, new ToolUseBlockConverter());
converters.put(ToolResultBlock.class, new ToolResultBlockConverter());
- converters.put(CustomBlock.class, new CustomBlockConverter());
}
/**
diff --git a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/CustomBlockConverter.java b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/CustomBlockConverter.java
deleted file mode 100644
index 27e7844765..0000000000
--- a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/CustomBlockConverter.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright 2024-2026 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package io.agentscope.core.agui.adapter.strategy;
-
-import io.agentscope.core.agent.Event;
-import io.agentscope.core.agui.adapter.StreamContext;
-import io.agentscope.core.agui.event.AguiEvent;
-import io.agentscope.core.message.CustomBlock;
-
-/**
- * Converter for handling CustomBlock events, transforming them into AG-UI Custom events.
- */
-public class CustomBlockConverter implements BlockEventConverter This allows users to override the default conversion strategies for specific
+ * ContentBlock types, enabling deep customization of AG-UI event generation.
+ *
+ * @param blockClass The class of the ContentBlock to convert
+ * @param converter The custom converter strategy
+ * @param When enabled, intermediate tool execution outputs (such as progress)
+ * will be streamed to the frontend.
+ *
+ * @return true if acting chunks should be included
+ */
+ public boolean isEnableActingChunk() {
+ return enableActingChunk;
+ }
+
/**
* Get the run timeout duration.
*
@@ -146,6 +160,7 @@ public static class Builder {
private boolean emitStateEvents = true;
private boolean emitToolCallArgs = true;
private boolean enableReasoning = false;
+ private boolean enableActingChunk = true;
private Duration runTimeout = Duration.ofMinutes(10);
private String defaultAgentId;
private final Map When enabled, tools can emit intermediate chunks (e.g., Custom events for progress
+ * or real-time logs) during execution. Default is true.
+ *
+ * @param enableActingChunk true to enable acting chunks
+ * @return This builder
+ */
+ public Builder enableActingChunk(boolean enableActingChunk) {
+ this.enableActingChunk = enableActingChunk;
+ return this;
+ }
+
/**
* Set the run timeout duration.
*
diff --git a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/AguiAgentAdapter.java b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/AguiAgentAdapter.java
index 84380bdb43..be2f93144d 100644
--- a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/AguiAgentAdapter.java
+++ b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/AguiAgentAdapter.java
@@ -114,6 +114,7 @@ public Flux When enabled, tools can emit intermediate chunks (e.g., Custom events for progress
+ * or real-time logs) during execution.
+ */
+ private boolean enableActingChunk = true;
+
/** Default agent ID to use when not specified in the request. */
private String defaultAgentId = "default";
@@ -176,6 +185,14 @@ public void setEnableReasoning(boolean enableReasoning) {
this.enableReasoning = enableReasoning;
}
+ public boolean isEnableActingChunk() {
+ return enableActingChunk;
+ }
+
+ public void setEnableActingChunk(boolean enableActingChunk) {
+ this.enableActingChunk = enableActingChunk;
+ }
+
public String getDefaultAgentId() {
return defaultAgentId;
}
From b5547c54c7c97d5e5d8be94f438f2b8bc1094493 Mon Sep 17 00:00:00 2001
From: jujn <2087687391@qq.com>
Date: Tue, 19 May 2026 13:31:39 +0800
Subject: [PATCH 16/20] feat: custom block converters support springboot
auto-configuration
---
.../core/agui/adapter/AguiAdapterConfig.java | 10 +-
.../core/agui/adapter/AguiAgentAdapter.java | 19 ++--
.../adapter/strategy/BlockEventConverter.java | 7 ++
.../adapter/strategy/TextBlockConverter.java | 5 +
.../strategy/ThinkingBlockConverter.java | 5 +
.../strategy/ToolResultBlockConverter.java | 5 +
.../strategy/ToolUseBlockConverter.java | 5 +
.../agui/adapter/AguiAdapterConfigTest.java | 94 ++++++++++++++++++-
.../agui/adapter/AguiAgentAdapterTest.java | 9 +-
.../AgentscopeAguiMvcAutoConfiguration.java | 25 ++++-
...gentscopeAguiWebFluxAutoConfiguration.java | 25 ++++-
11 files changed, 182 insertions(+), 27 deletions(-)
diff --git a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/AguiAdapterConfig.java b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/AguiAdapterConfig.java
index 86aed4731b..bb640045e4 100644
--- a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/AguiAdapterConfig.java
+++ b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/AguiAdapterConfig.java
@@ -17,7 +17,6 @@
import io.agentscope.core.agui.adapter.strategy.BlockEventConverter;
import io.agentscope.core.agui.model.ToolMergeMode;
-import io.agentscope.core.message.ContentBlock;
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
@@ -255,14 +254,13 @@ public Builder defaultAgentId(String defaultAgentId) {
* This allows users to override the default conversion strategies for specific
* ContentBlock types, enabling deep customization of AG-UI event generation.
*
- * @param blockClass The class of the ContentBlock to convert
* @param converter The custom converter strategy
- * @param Before overriding any default BlockEventConverter, please ensure you have a
+ * thorough understanding of the default implementation.
+ *
+ * IMPORTANT: In production environments, please modify with caution other than tool
+ * progress events. Otherwise, you assume all consequences and risks.
+ *
+ * This is currently provided as a demonstration version only. Please modify it
+ * according to your own requirements.
+ *
+ * Feel free to submit an issue on GitHub if you encounter any problems.
+ */
+@Component
+public class CustomToolResultBlockConverter extends ToolResultBlockConverter {
+
+ @Override
+ public boolean isApplicable(Event event) {
+ // return event.getType() == EventType.TOOL_RESULT && event.isLast();
+ // The check for event.isLast() needs to be removed, otherwise only the final result event
+ // will be emitted
+ return event.getType() == EventType.TOOL_RESULT;
+ }
+
+ @Override
+ public void convert(ToolResultBlock block, Event event, StreamContext ctx) {
+ // Extract the tool invocation progress from metadata and package it into a ToolCallResult
+ // event
+ // This is currently provided as a demonstration version only.
+ // Please modify it according to your own requirements.
+ if (block.getMetadata() != null && block.getMetadata().size() != 0) {
+ ctx.emit(
+ new AguiEvent.ToolCallResult(
+ ctx.getThreadId(),
+ ctx.getRunId(),
+ // TODO: Currently using tool name to replace tool call id
+ block.getName(),
+ JsonUtils.getJsonCodec().toJson(block.getMetadata()),
+ "tool",
+ event.getMessage().getId()));
+ return;
+ }
+
+ String toolCallId =
+ block.getId() != null && !block.getId().isBlank()
+ ? block.getId()
+ : UUID.randomUUID().toString();
+
+ String result = super.extractToolResultText(block);
+
+ // Closing Start/End Phase
+ if (ctx.isToolActive(toolCallId)) {
+ ctx.flushEndEvent(StreamContext.PREFIX_TOOL + toolCallId);
+ } else {
+ // Fall-back: The previous process did not proceed to Start for some reason
+ // (e.g., recovery directly from the context)
+ String toolName =
+ block.getName() != null && !block.getName().isBlank()
+ ? block.getName()
+ : "unknown";
+ ctx.emit(
+ new AguiEvent.ToolCallStart(
+ ctx.getThreadId(), ctx.getRunId(), toolCallId, toolName));
+ ctx.emit(new AguiEvent.ToolCallEnd(ctx.getThreadId(), ctx.getRunId(), toolCallId));
+ }
+
+ ctx.emit(
+ new AguiEvent.ToolCallResult(
+ ctx.getThreadId(),
+ ctx.getRunId(),
+ toolCallId,
+ result,
+ "tool",
+ event.getMessage().getId()));
+
+ if (ctx.isToolActive(toolCallId)) {
+ ctx.removeActiveTool(toolCallId);
+ }
+ }
+}
diff --git a/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/tools/ExampleTools.java b/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/tools/ExampleTools.java
index 079190a4f9..2bfab6ce75 100644
--- a/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/tools/ExampleTools.java
+++ b/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/tools/ExampleTools.java
@@ -17,9 +17,11 @@
import io.agentscope.core.message.ToolResultBlock;
import io.agentscope.core.tool.Tool;
+import io.agentscope.core.tool.ToolEmitter;
import io.agentscope.core.tool.ToolParam;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
+import java.util.Map;
import java.util.Random;
/**
@@ -43,17 +45,33 @@ public class ExampleTools {
@Tool(name = "get_weather", description = "Get current weather information for a city")
public ToolResultBlock getWeather(
@ToolParam(name = "city", description = "The city name (e.g., 'Beijing', 'New York')")
- String city) {
+ String city,
+ ToolEmitter emitter) {
// Mock weather data
String[] conditions = {"Sunny", "Cloudy", "Partly Cloudy", "Rainy", "Overcast"};
String condition = conditions[random.nextInt(conditions.length)];
int temperature = random.nextInt(35) + 5; // 5-40 degrees
int humidity = random.nextInt(60) + 30; // 30-90%
+ // Pass progress information in metadata
+ // To take effect, it is necessary to customize the ToolResultBlockConverter to override the
+ // framework default
+ emitter.emit(
+ ToolResultBlock.builder()
+ .name("get_weather")
+ .metadata(Map.of("progress", "50%"))
+ .build());
+
String result =
String.format(
"Weather in %s:\n- Condition: %s\n- Temperature: %d°C\n- Humidity: %d%%",
city, condition, temperature, humidity);
+
+ emitter.emit(
+ ToolResultBlock.builder()
+ .name("get_weather")
+ .metadata(Map.of("progress", "100%"))
+ .build());
return ToolResultBlock.text(result);
}
diff --git a/agentscope-examples/agui/src/main/resources/application.yml b/agentscope-examples/agui/src/main/resources/application.yml
index 8443b62a7b..3590d8eb05 100644
--- a/agentscope-examples/agui/src/main/resources/application.yml
+++ b/agentscope-examples/agui/src/main/resources/application.yml
@@ -45,6 +45,7 @@ agentscope:
max-thread-sessions: 1000
session-timeout-minutes: 30
enable-reasoning: false
+ enable-acting-chunk: false
# Logging
logging:
diff --git a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ToolResultBlockConverter.java b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ToolResultBlockConverter.java
index 0aabcebbeb..d46605ffb0 100644
--- a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ToolResultBlockConverter.java
+++ b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/adapter/strategy/ToolResultBlockConverter.java
@@ -84,7 +84,7 @@ public void convert(ToolResultBlock block, Event event, StreamContext ctx) {
* @param toolResult The tool result block
* @return The text content, or null if not present
*/
- private String extractToolResultText(ToolResultBlock toolResult) {
+ protected String extractToolResultText(ToolResultBlock toolResult) {
if (toolResult.getOutput() == null || toolResult.getOutput().isEmpty()) {
return null;
}
From 9e03c96b6c2a7e2d4621a31374909708fa539dd7 Mon Sep 17 00:00:00 2001
From: jujn <2087687391@qq.com>
Date: Wed, 20 May 2026 22:33:50 +0800
Subject: [PATCH 18/20] docs: update the ag-ui sample project and docs
---
.../integration/agui/README.md | 89 +++++++++++++++++++
.../agui/config/AgentConfiguration.java | 2 +-
.../agui/src/main/resources/static/index.html | 12 +++
.../main/resources/static/js/agui-client.js | 4 +
docs/en/intro.md | 4 +-
docs/zh/intro.md | 4 +-
docs/zh/task/agui.md | 3 +-
7 files changed, 112 insertions(+), 6 deletions(-)
create mode 100644 agentscope-examples/integration/agui/README.md
diff --git a/agentscope-examples/integration/agui/README.md b/agentscope-examples/integration/agui/README.md
new file mode 100644
index 0000000000..483dc149f6
--- /dev/null
+++ b/agentscope-examples/integration/agui/README.md
@@ -0,0 +1,89 @@
+# Quick Start
+
+本案例展示了如何基于 Spring Boot 构建支持 [AgentScope](https://github.com/agentscope-ai/agentscope-java) AG-UI 协议的 Web 服务。通过实现标准的交互协议,本项目提供了包括多智能体路由、智能体推理展示(需在 yml 中开启)和工具调用进度反馈(需在 yml 中开启)在内的前后端交互体验。
+
+## 环境要求
+
+* JDK 17 及以上
+* Maven 3.8+
+* 有效的 DashScope API Key(本项目默认使用 `qwen-plus` 模型)
+
+## 示例运行
+
+### 1. AG-UI 服务端参数配置
+
+项目的核心配置位于 `application.yml` 中:
+
+```yaml
+agentscope:
+ agui:
+ path-prefix: /agui # AG-UI 接口统一前缀
+ cors-enabled: true # 开启跨域支持
+ default-agent-id: default # 默认处理请求的 Agent ID
+ agent-id-header: X-Agent-Id # 通过 HTTP Header 进行 Agent 路由
+ enable-path-routing: true # 允许通过 URL 路径进行 Agent 路由
+ server-side-memory: true # 是否按 threadId 在后端管理会话内存
+ session-timeout-minutes: 30 # 会话超时时间
+ enable-reasoning: false # 是否启用推理/思考内容输出(模型也需要支持并开启)
+ emit-tool-call-args: true # 是否发出工具调用参数事件
+ enable-acting-chunk: false # 是否发出工具调用进度信息
+```
+
+### 2. 运行应用
+
+本项目基于 Spring Boot,使用以下 Maven 命令启动应用:
+
+```bash
+mvn spring-boot:run
+```
+
+### 3. 开始体验
+
+应用启动后,将在本机的 `8080` 端口提供服务。本项目提供了两种体验方式:
+
+#### 方式一:可视化 Web 界面
+
+直接在浏览器中打开内置的交互式页面:
+**http://localhost:8080**
+该页面原生对接了 AG-UI 协议,支持实时显示流式打字效果、工具调用状态以及内部推理过程。
+
+#### 方式二:API / cURL 调用体验多智能体路由
+
+基于 `AgentConfiguration.java` 的注册逻辑,本项目实现了三种不同的 Agent。你可以通过不同的调用策略来体验路由机制:
+
+**1. 访问默认 Agent(带工具,支持天气和计算):**
+
+```shell
+curl -N -X POST http://localhost:8080/agui/run \
+ -H "Content-Type: application/json" \
+ -d '{"threadId":"test-1","runId":"1","messages":[{"id":"m1","role":"user","content":"北京今天天气如何?"}]}'
+```
+
+**2. 通过 URL 路径路由访问纯聊天 Agent(`chat`):**
+
+```shell
+curl -N -X POST http://localhost:8080/agui/run/chat \
+ -H "Content-Type: application/json" \
+ -d '{"threadId":"test-2","runId":"1","messages":[{"id":"m1","role":"user","content":"你好,我们来聊聊天吧。"}]}'
+```
+
+**3. 通过 HTTP Header 路由访问计算器 Agent(`calculator`):**
+
+```shell
+curl -N -X POST http://localhost:8080/agui/run \
+ -H "Content-Type: application/json" \
+ -H "X-Agent-Id: calculator" \
+ -d '{"threadId":"test-3","runId":"1","messages":[{"id":"m1","role":"user","content":"计算 12.5 乘以 4"}]}'
+```
+
+---
+
+## 核心代码架构说明
+
+为便于二次开发,可重点关注以下核心类:
+
+* **`AgentConfiguration.java`**:演示了如何通过 `AguiAgentRegistryCustomizer` 注册多个独立 Agent 工厂(Factory 模式),确保并发场景下每个请求获取纯净的 Agent 实例。
+* **`CustomToolResultBlockConverter.java`**:高阶特性演示。通过继承并重写框架底层的 Event 转换逻辑(基于 @Component 注册生效),允许将带有进度 Metadata 的工具(如 get_weather)封装为 AguiEvent.ToolCallResult 事件,从而向前端实时推送工具执行的中间状态。(注:需在 application.yml 中开启 enable-acting-chunk: true)
+* **`ExampleTools.java`**:Agent 可用的工具集合。展示了 `ToolEmitter` 的高级用法,实现了执行过程的多阶段状态通知。
+* **`index.html`**:实现了一个完整的 AG-UI 客户端界面,处理流式文本 (`onTextContent`)、推理过程 (`onReasoningContent`) 以及工具调用 (`onToolCallStart`) 的事件分发。
+
diff --git a/agentscope-examples/integration/agui/src/main/java/io/agentscope/examples/agui/config/AgentConfiguration.java b/agentscope-examples/integration/agui/src/main/java/io/agentscope/examples/agui/config/AgentConfiguration.java
index dae080f07e..737c77b605 100644
--- a/agentscope-examples/integration/agui/src/main/java/io/agentscope/examples/agui/config/AgentConfiguration.java
+++ b/agentscope-examples/integration/agui/src/main/java/io/agentscope/examples/agui/config/AgentConfiguration.java
@@ -92,7 +92,7 @@ private Agent createDefaultAgent() {
.model(
DashScopeChatModel.builder().apiKey(apiKey).modelName("qwen-plus").stream(
true)
- .enableThinking(false)
+ .enableThinking(true)
.formatter(new DashScopeChatFormatter())
.build())
.toolkit(toolkit)
diff --git a/agentscope-examples/integration/agui/src/main/resources/static/index.html b/agentscope-examples/integration/agui/src/main/resources/static/index.html
index 97c72c4257..dbd32b0066 100644
--- a/agentscope-examples/integration/agui/src/main/resources/static/index.html
+++ b/agentscope-examples/integration/agui/src/main/resources/static/index.html
@@ -472,6 +472,18 @@ AgentScope Java is the open-source agent framework for the JVM. ReAct reasoning, Harness engineering infrastructure, multi-agent orchestration, and MCP/A2A protocol support — from local prototype to enterprise-scale deployment. AgentScope Java is the open-source agent framework for the JVM. ReAct reasoning, Harness engineering infrastructure, multi-agent orchestration, and MCP/A2A/AG-UI protocol support — from local prototype to enterprise-scale deployment. AgentScope Java 是面向 JVM 的开源 Agent 框架。提供 ReAct 推理、Harness 工程化基础设施、多智能体编排与 MCP/A2A 协议支持,覆盖从本地原型到企业级分布式部署全链路。 AgentScope Java 是面向 JVM 的开源 Agent 框架。提供 ReAct 推理、Harness 工程化基础设施、多智能体编排与 MCP/A2A/AG-UI 协议支持,覆盖从本地原型到企业级分布式部署全链路。 This application demonstrates how to expose AgentScope agents via the AG-UI protocol
- * using Spring WebFlux.
- *
- * Usage:
- * This example demonstrates how to register multiple agents with different IDs.
- * Clients can select which agent to use via:
- * This agent is configured with:
- * This agent is a pure conversational assistant.
- */
- private Agent createChatAgent() {
- String apiKey = getRequiredApiKey();
-
- return ReActAgent.builder()
- .name("Chat Assistant")
- .sysPrompt(
- "You are a friendly conversational assistant. "
- + "Engage in natural conversation and help users "
- + "with general questions and discussions.")
- .model(
- DashScopeChatModel.builder().apiKey(apiKey).modelName("qwen-plus").stream(
- true)
- .formatter(new DashScopeChatFormatter())
- .build())
- .memory(new InMemoryMemory())
- .maxIters(1)
- .build();
- }
-
- /**
- * Create a calculator agent specialized for mathematical operations.
- */
- private Agent createCalculatorAgent() {
- String apiKey = getRequiredApiKey();
-
- // Create toolkit with only calculation tools
- Toolkit toolkit = new Toolkit();
- toolkit.registerTool(new ExampleTools());
-
- return ReActAgent.builder()
- .name("Calculator Agent")
- .sysPrompt(
- "You are a mathematical assistant specialized in calculations. "
- + "Use the calculate tool to perform mathematical operations. "
- + "Always show your work and explain the results.")
- .model(
- DashScopeChatModel.builder().apiKey(apiKey).modelName("qwen-plus").stream(
- true)
- .formatter(new DashScopeChatFormatter())
- .build())
- .toolkit(toolkit)
- .memory(new InMemoryMemory())
- .maxIters(5)
- .build();
- }
-
- private String getRequiredApiKey() {
- String apiKey = System.getenv("DASHSCOPE_API_KEY");
- if (apiKey == null || apiKey.isEmpty()) {
- throw new IllegalStateException(
- "DASHSCOPE_API_KEY environment variable is required. "
- + "Please set it before starting the application.");
- }
- return apiKey;
- }
-}
diff --git a/agentscope-examples/integration/agui/src/main/java/io/agentscope/examples/agui/config/CustomToolResultBlockConverter.java b/agentscope-examples/integration/agui/src/main/java/io/agentscope/examples/agui/config/CustomToolResultBlockConverter.java
deleted file mode 100644
index 7d803f8368..0000000000
--- a/agentscope-examples/integration/agui/src/main/java/io/agentscope/examples/agui/config/CustomToolResultBlockConverter.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright 2024-2026 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package io.agentscope.examples.agui.config;
-
-import io.agentscope.core.agent.Event;
-import io.agentscope.core.agent.EventType;
-import io.agentscope.core.agui.adapter.StreamContext;
-import io.agentscope.core.agui.adapter.strategy.ToolResultBlockConverter;
-import io.agentscope.core.agui.event.AguiEvent;
-import io.agentscope.core.message.ToolResultBlock;
-import io.agentscope.core.util.JsonUtils;
-import java.util.UUID;
-import org.springframework.stereotype.Component;
-
-/**
- * Custom converter that overrides the framework's default ToolResultBlockConverter
- * to emit ag-ui tool progress events.
- *
- * Before overriding any default BlockEventConverter, please ensure you have a
- * thorough understanding of the default implementation.
- *
- * IMPORTANT: In production environments, please modify with caution other than tool
- * progress events. Otherwise, you assume all consequences and risks.
- *
- * This is currently provided as a demonstration version only. Please modify it
- * according to your own requirements.
- *
- * Feel free to submit an issue on GitHub if you encounter any problems.
- */
-@Component
-public class CustomToolResultBlockConverter extends ToolResultBlockConverter {
-
- @Override
- public boolean isApplicable(Event event) {
- // return event.getType() == EventType.TOOL_RESULT && event.isLast();
- // The check for event.isLast() needs to be removed, otherwise only the final result event
- // will be emitted
- return event.getType() == EventType.TOOL_RESULT;
- }
-
- @Override
- public void convert(ToolResultBlock block, Event event, StreamContext ctx) {
- // Extract the tool invocation progress from metadata and package it into a ToolCallResult
- // event
- // This is currently provided as a demonstration version only.
- // Please modify it according to your own requirements.
- if (block.getMetadata() != null && block.getMetadata().size() != 0) {
- ctx.emit(
- new AguiEvent.ToolCallResult(
- ctx.getThreadId(),
- ctx.getRunId(),
- // TODO: Currently using tool name to replace tool call id
- block.getName(),
- JsonUtils.getJsonCodec().toJson(block.getMetadata()),
- "tool",
- event.getMessage().getId()));
- return;
- }
-
- String toolCallId =
- block.getId() != null && !block.getId().isBlank()
- ? block.getId()
- : UUID.randomUUID().toString();
-
- String result = super.extractToolResultText(block);
-
- // Closing Start/End Phase
- if (ctx.isToolActive(toolCallId)) {
- ctx.flushEndEvent(StreamContext.PREFIX_TOOL + toolCallId);
- } else {
- // Fall-back: The previous process did not proceed to Start for some reason
- // (e.g., recovery directly from the context)
- String toolName =
- block.getName() != null && !block.getName().isBlank()
- ? block.getName()
- : "unknown";
- ctx.emit(
- new AguiEvent.ToolCallStart(
- ctx.getThreadId(), ctx.getRunId(), toolCallId, toolName));
- ctx.emit(new AguiEvent.ToolCallEnd(ctx.getThreadId(), ctx.getRunId(), toolCallId));
- }
-
- ctx.emit(
- new AguiEvent.ToolCallResult(
- ctx.getThreadId(),
- ctx.getRunId(),
- toolCallId,
- result,
- "tool",
- event.getMessage().getId()));
-
- if (ctx.isToolActive(toolCallId)) {
- ctx.removeActiveTool(toolCallId);
- }
- }
-}
diff --git a/agentscope-examples/integration/agui/src/main/java/io/agentscope/examples/agui/tools/ExampleTools.java b/agentscope-examples/integration/agui/src/main/java/io/agentscope/examples/agui/tools/ExampleTools.java
deleted file mode 100644
index 2bfab6ce75..0000000000
--- a/agentscope-examples/integration/agui/src/main/java/io/agentscope/examples/agui/tools/ExampleTools.java
+++ /dev/null
@@ -1,194 +0,0 @@
-/*
- * Copyright 2024-2026 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package io.agentscope.examples.agui.tools;
-
-import io.agentscope.core.message.ToolResultBlock;
-import io.agentscope.core.tool.Tool;
-import io.agentscope.core.tool.ToolEmitter;
-import io.agentscope.core.tool.ToolParam;
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.Map;
-import java.util.Random;
-
-/**
- * Example tools for the AG-UI demo.
- *
- * These tools demonstrate how to create tool functions that can be called
- * by the agent during conversation.
- */
-public class ExampleTools {
-
- private final Random random = new Random();
-
- /**
- * Get weather information for a city.
- *
- * Note: This is a mock implementation for demo purposes.
- *
- * @param city The city name to get weather for
- * @return Weather information
- */
- @Tool(name = "get_weather", description = "Get current weather information for a city")
- public ToolResultBlock getWeather(
- @ToolParam(name = "city", description = "The city name (e.g., 'Beijing', 'New York')")
- String city,
- ToolEmitter emitter) {
- // Mock weather data
- String[] conditions = {"Sunny", "Cloudy", "Partly Cloudy", "Rainy", "Overcast"};
- String condition = conditions[random.nextInt(conditions.length)];
- int temperature = random.nextInt(35) + 5; // 5-40 degrees
- int humidity = random.nextInt(60) + 30; // 30-90%
-
- // Pass progress information in metadata
- // To take effect, it is necessary to customize the ToolResultBlockConverter to override the
- // framework default
- emitter.emit(
- ToolResultBlock.builder()
- .name("get_weather")
- .metadata(Map.of("progress", "50%"))
- .build());
-
- String result =
- String.format(
- "Weather in %s:\n- Condition: %s\n- Temperature: %d°C\n- Humidity: %d%%",
- city, condition, temperature, humidity);
-
- emitter.emit(
- ToolResultBlock.builder()
- .name("get_weather")
- .metadata(Map.of("progress", "100%"))
- .build());
- return ToolResultBlock.text(result);
- }
-
- /**
- * Perform a simple calculation.
- *
- * @param expression The math expression to evaluate
- * @return The calculation result
- */
- @Tool(
- name = "calculate",
- description = "Perform a simple arithmetic calculation (supports +, -, *, /)")
- public ToolResultBlock calculate(
- @ToolParam(
- name = "expression",
- description =
- "The arithmetic expression to calculate (e.g., '2 + 3 * 4')")
- String expression) {
- try {
- // Simple expression evaluator (for demo purposes)
- double result = evaluateExpression(expression);
- return ToolResultBlock.text("Result: " + formatNumber(result));
- } catch (Exception e) {
- return ToolResultBlock.error("Failed to calculate: " + e.getMessage());
- }
- }
-
- /**
- * Get the current date and time.
- *
- * @return Current date and time
- */
- @Tool(name = "get_current_time", description = "Get the current date and time")
- public ToolResultBlock getCurrentTime() {
- LocalDateTime now = LocalDateTime.now();
- DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
- return ToolResultBlock.text("Current time: " + now.format(formatter));
- }
-
- /**
- * Simple expression evaluator.
- * Supports basic arithmetic: +, -, *, /
- */
- private double evaluateExpression(String expression) {
- // Remove whitespace
- expression = expression.replaceAll("\\s+", "");
-
- // Handle addition and subtraction (lowest precedence)
- int lastPlusOrMinus = -1;
- int parenDepth = 0;
- for (int i = expression.length() - 1; i >= 0; i--) {
- char c = expression.charAt(i);
- if (c == ')') parenDepth++;
- else if (c == '(') parenDepth--;
- else if (parenDepth == 0 && (c == '+' || c == '-') && i > 0) {
- lastPlusOrMinus = i;
- break;
- }
- }
-
- if (lastPlusOrMinus > 0) {
- String left = expression.substring(0, lastPlusOrMinus);
- String right = expression.substring(lastPlusOrMinus + 1);
- char op = expression.charAt(lastPlusOrMinus);
- if (op == '+') {
- return evaluateExpression(left) + evaluateExpression(right);
- } else {
- return evaluateExpression(left) - evaluateExpression(right);
- }
- }
-
- // Handle multiplication and division
- int lastMultOrDiv = -1;
- parenDepth = 0;
- for (int i = expression.length() - 1; i >= 0; i--) {
- char c = expression.charAt(i);
- if (c == ')') parenDepth++;
- else if (c == '(') parenDepth--;
- else if (parenDepth == 0 && (c == '*' || c == '/')) {
- lastMultOrDiv = i;
- break;
- }
- }
-
- if (lastMultOrDiv > 0) {
- String left = expression.substring(0, lastMultOrDiv);
- String right = expression.substring(lastMultOrDiv + 1);
- char op = expression.charAt(lastMultOrDiv);
- if (op == '*') {
- return evaluateExpression(left) * evaluateExpression(right);
- } else {
- return evaluateExpression(left) / evaluateExpression(right);
- }
- }
-
- // Handle parentheses
- if (expression.startsWith("(") && expression.endsWith(")")) {
- return evaluateExpression(expression.substring(1, expression.length() - 1));
- }
-
- // Parse number
- return parseNumber(expression);
- }
-
- private double parseNumber(String expression) {
- try {
- return Double.parseDouble(expression);
- } catch (NumberFormatException e) {
- throw new IllegalArgumentException(
- "Invalid numeric value in expression: '" + expression + "'", e);
- }
- }
-
- private String formatNumber(double value) {
- if (value == (long) value) {
- return String.valueOf((long) value);
- }
- return String.format("%.4f", value);
- }
-}
diff --git a/agentscope-examples/integration/agui/src/main/resources/application.yml b/agentscope-examples/integration/agui/src/main/resources/application.yml
deleted file mode 100644
index 3590d8eb05..0000000000
--- a/agentscope-examples/integration/agui/src/main/resources/application.yml
+++ /dev/null
@@ -1,55 +0,0 @@
-# Copyright 2024-2026 the original author or authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# AG-UI Example Application Configuration
-
-server:
- port: 8080
-
-spring:
- main:
- banner-mode: off
-
-# AG-UI Configuration
-agentscope:
- agui:
- path-prefix: /agui
- cors-enabled: true
- cors-allowed-origins:
- - "*"
- run-timeout: 10m
- emit-state-events: true
- emit-tool-call-args: true
- default-agent-id: default
- # Agent ID routing configuration
- # Agent ID can be passed via:
- # 1. URL path variable: POST /agui/run/{agentId} (highest priority)
- # 2. HTTP header: X-Agent-Id (configurable)
- # 3. Request body: forwardedProps.agentId
- # 4. Default: uses default-agent-id
- agent-id-header: X-Agent-Id
- enable-path-routing: true
- # Server-side memory management
- server-side-memory: true
- max-thread-sessions: 1000
- session-timeout-minutes: 30
- enable-reasoning: false
- enable-acting-chunk: false
-
-# Logging
-logging:
- level:
- root: INFO
- io.agentscope: DEBUG
-
diff --git a/agentscope-examples/integration/agui/src/main/resources/logback.xml b/agentscope-examples/integration/agui/src/main/resources/logback.xml
deleted file mode 100644
index 4146594095..0000000000
--- a/agentscope-examples/integration/agui/src/main/resources/logback.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
- Chat with an AI agent via the AG-UI protocol AgentScope Java is the open-source agent framework for the JVM. ReAct reasoning, Harness engineering infrastructure, multi-agent orchestration, and MCP/A2A/AG-UI protocol support — from local prototype to enterprise-scale deployment. AgentScope Java is the open-source agent framework for the JVM. ReAct reasoning, Harness engineering infrastructure, multi-agent orchestration, and MCP/A2A protocol support — from local prototype to enterprise-scale deployment. AgentScope Java 是面向 JVM 的开源 Agent 框架。提供 ReAct 推理、Harness 工程化基础设施、多智能体编排与 MCP/A2A/AG-UI 协议支持,覆盖从本地原型到企业级分布式部署全链路。 AgentScope Java 是面向 JVM 的开源 Agent 框架。提供 ReAct 推理、Harness 工程化基础设施、多智能体编排与 MCP/A2A 协议支持,覆盖从本地原型到企业级分布式部署全链路。AgentScope AG-UI Demo
onToolCallEnd: (toolCallId) => {
// Tool call completed
},
+ onToolCallResult: (toolCallId, content) => {
+ try {
+ const resultObj = JSON.parse(content);
+ if (resultObj && resultObj.progress) {
+ appendMessage('tool', `⏳ 进度: ${resultObj.progress}`);
+ } else {
+ appendMessage('tool', `📄 执行结果: ${content}`);
+ }
+ } catch (e) {
+ appendMessage('tool', `✅ 返回结果: ${content}`);
+ }
+ },
onError: (error) => {
hideTypingIndicator();
appendMessage('error', `Error: ${error}`);
diff --git a/agentscope-examples/integration/agui/src/main/resources/static/js/agui-client.js b/agentscope-examples/integration/agui/src/main/resources/static/js/agui-client.js
index 58dc260d50..fe6c0f443b 100644
--- a/agentscope-examples/integration/agui/src/main/resources/static/js/agui-client.js
+++ b/agentscope-examples/integration/agui/src/main/resources/static/js/agui-client.js
@@ -252,6 +252,10 @@ class AguiClient {
callbacks.onToolCallEnd?.(event.toolCallId);
break;
+ case 'TOOL_CALL_RESULT':
+ callbacks.onToolCallResult?.(event.toolCallId, event.content);
+ break;
+
case 'STATE_SNAPSHOT':
callbacks.onStateSnapshot?.(event.snapshot);
break;
diff --git a/docs/en/intro.md b/docs/en/intro.md
index 6a1e05ee68..becdfd6757 100644
--- a/docs/en/intro.md
+++ b/docs/en/intro.md
@@ -11,7 +11,7 @@ hide-toc: true
Harness framework for distributed, enterprise-grade agents.
- 专为分布式、企业级智能体
-
打造的 Harness 框架。
- *
- */
-@SpringBootApplication
-public class AguiExampleApplication {
-
- public static void main(String[] args) {
- SpringApplication.run(AguiExampleApplication.class, args);
- printStartupInfo();
- }
-
- private static void printStartupInfo() {
- System.out.println("\n=== AG-UI Example Application Started ===");
- System.out.println("Open: http://localhost:8080");
- System.out.println("API: POST http://localhost:8080/agui/run");
- System.out.println("\nExample curl command:");
- System.out.println(" curl -N -X POST http://localhost:8080/agui/run \\");
- System.out.println(" -H \"Content-Type: application/json\" \\");
- System.out.println(
- " -d"
- + " '{\"threadId\":\"test\",\"runId\":\"1\",\"messages\":[{\"id\":\"m1\",\"role\":\"user\",\"content\":\"Hello!\"}]}'");
- System.out.println("\nPress Ctrl+C to stop.");
- }
-}
diff --git a/agentscope-examples/integration/agui/src/main/java/io/agentscope/examples/agui/config/AgentConfiguration.java b/agentscope-examples/integration/agui/src/main/java/io/agentscope/examples/agui/config/AgentConfiguration.java
deleted file mode 100644
index 737c77b605..0000000000
--- a/agentscope-examples/integration/agui/src/main/java/io/agentscope/examples/agui/config/AgentConfiguration.java
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Copyright 2024-2026 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package io.agentscope.examples.agui.config;
-
-import io.agentscope.core.ReActAgent;
-import io.agentscope.core.agent.Agent;
-import io.agentscope.core.formatter.dashscope.DashScopeChatFormatter;
-import io.agentscope.core.memory.InMemoryMemory;
-import io.agentscope.core.model.DashScopeChatModel;
-import io.agentscope.core.tool.Toolkit;
-import io.agentscope.examples.agui.tools.ExampleTools;
-import io.agentscope.spring.boot.agui.common.AguiAgentRegistryCustomizer;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-/**
- * Configuration class that registers agents with the AG-UI registry.
- *
- *
- *
- */
-@Configuration
-public class AgentConfiguration {
-
- @Bean
- public AguiAgentRegistryCustomizer aguiAgentRegistryCustomizer() {
- AguiAgentRegistryCustomizer aguiAgentRegistryCustomizer =
- registry -> {
- // Register a factory for the default agent
- // Using a factory ensures each request gets a fresh agent instance
- registry.registerFactory("default", this::createDefaultAgent);
-
- // Register additional agents with different IDs
- // Example: a simple chat agent without tools
- registry.registerFactory("chat", this::createChatAgent);
-
- // Example: an agent specialized for calculations
- registry.registerFactory("calculator", this::createCalculatorAgent);
- };
-
- System.out.println("Registered agents with AG-UI registry: default, chat, calculator");
- System.out.println("Access agents via:");
- System.out.println(" - POST /agui/run (uses default-agent-id from config)");
- System.out.println(" - POST /agui/run/chat (uses 'chat' agent)");
- System.out.println(" - POST /agui/run with X-Agent-Id header");
-
- return aguiAgentRegistryCustomizer;
- }
-
- /**
- * Create the default agent instance.
- *
- *
- *
- */
- private Agent createDefaultAgent() {
- String apiKey = getRequiredApiKey();
-
- // Create toolkit with example tools
- Toolkit toolkit = new Toolkit();
- toolkit.registerTool(new ExampleTools());
-
- // Create the agent
- return ReActAgent.builder()
- .name("AG-UI Assistant")
- .sysPrompt(
- "You are a helpful AI assistant exposed via the AG-UI protocol. "
- + "You can help users with various tasks including weather queries "
- + "and calculations. Be concise and helpful in your responses.")
- .model(
- DashScopeChatModel.builder().apiKey(apiKey).modelName("qwen-plus").stream(
- true)
- .enableThinking(true)
- .formatter(new DashScopeChatFormatter())
- .build())
- .toolkit(toolkit)
- .memory(new InMemoryMemory())
- .maxIters(10)
- .build();
- }
-
- /**
- * Create a simple chat agent without tools.
- *
- * AgentScope AG-UI Demo
- Harness framework for distributed, enterprise-grade agents.
- 专为分布式、企业级智能体
-
打造的 Harness 框架。