Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* 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.
*
* <p>Before overriding any default BlockEventConverter, please ensure you have a
* thorough understanding of the default implementation.
*
* <p>IMPORTANT: In production environments, please modify with caution other than tool
* progress events. Otherwise, you assume all consequences and risks.
*
* <p>This is currently provided as a demonstration version only. Please modify it
* according to your own requirements.
*
* <p>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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ agentscope:
max-thread-sessions: 1000
session-timeout-minutes: 30
enable-reasoning: false
enable-acting-chunk: false

# Logging
logging:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@
*/
package io.agentscope.core.agui.adapter;

import io.agentscope.core.agui.adapter.strategy.BlockEventConverter;
import io.agentscope.core.agui.model.ToolMergeMode;
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
* Configuration for the AG-UI agent adapter.
Expand All @@ -30,16 +34,23 @@ public class AguiAdapterConfig {
private final boolean emitStateEvents;
private final boolean emitToolCallArgs;
private final boolean enableReasoning;
private final boolean enableActingChunk;
private final Duration runTimeout;
private final String defaultAgentId;
private final Map<Class<?>, BlockEventConverter<?>> customConverters;

private AguiAdapterConfig(Builder builder) {
this.toolMergeMode = builder.toolMergeMode;
this.emitStateEvents = builder.emitStateEvents;
this.emitToolCallArgs = builder.emitToolCallArgs;
this.enableReasoning = builder.enableReasoning;
this.enableActingChunk = builder.enableActingChunk;
this.runTimeout = builder.runTimeout;
this.defaultAgentId = builder.defaultAgentId;
this.customConverters =
builder.customConverters != null
? Map.copyOf(builder.customConverters)
: Collections.emptyMap();
}

/**
Expand Down Expand Up @@ -82,6 +93,18 @@ public boolean isEnableReasoning() {
return enableReasoning;
}

/**
* Check if intermediate acting chunk emissions should be included.
*
* <p>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.
*
Expand All @@ -100,6 +123,15 @@ public String getDefaultAgentId() {
return defaultAgentId;
}

/**
* Get the custom block event converters.
*
* @return Immutable map of custom converters
*/
public Map<Class<?>, BlockEventConverter<?>> getCustomConverters() {
return customConverters;
}

/**
* Creates a new builder for AguiAdapterConfig.
*
Expand Down Expand Up @@ -127,8 +159,10 @@ 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<Class<?>, BlockEventConverter<?>> customConverters = new HashMap<>();

/**
* Set the tool merge mode.
Expand Down Expand Up @@ -178,6 +212,20 @@ public Builder enableReasoning(boolean enableReasoning) {
return this;
}

/**
* Set whether to enable acting chunk emissions.
*
* <p>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.
*
Expand All @@ -200,6 +248,22 @@ public Builder defaultAgentId(String defaultAgentId) {
return this;
}

/**
* Register a custom block event converter.
*
* <p>This allows users to override the default conversion strategies for specific
* ContentBlock types, enabling deep customization of AG-UI event generation.
*
* @param converter The custom converter strategy
* @return This builder
*/
public Builder registerConverter(BlockEventConverter<?> converter) {
if (converter != null && converter.supportedBlockType() != null) {
this.customConverters.put(converter.supportedBlockType(), converter);
}
return this;
}

/**
* Build the configuration.
*
Expand Down
Loading
Loading