From b12aef34aee4cb7dadfaee8ca8925cd3ba3edc55 Mon Sep 17 00:00:00 2001 From: keep simple <3132670669@qq.com> Date: Fri, 12 Jun 2026 00:12:05 +0800 Subject: [PATCH 1/4] Support title for McpTool --- .../java/io/agentscope/core/ReActAgent.java | 60 +++++++++++++++---- .../core/event/ToolCallDeltaEvent.java | 14 ++++- .../core/event/ToolCallEndEvent.java | 13 +++- .../core/event/ToolCallStartEvent.java | 13 +++- .../core/event/ToolResultDataDeltaEvent.java | 14 ++++- .../core/event/ToolResultEndEvent.java | 14 ++++- .../core/event/ToolResultStartEvent.java | 13 +++- .../core/event/ToolResultTextDeltaEvent.java | 14 ++++- .../io/agentscope/core/model/ToolSchema.java | 23 +++++++ .../io/agentscope/core/tool/AgentTool.java | 13 ++++ .../core/tool/McpClientManager.java | 1 + .../agentscope/core/tool/SchemaOnlyTool.java | 17 +++++- .../io/agentscope/core/tool/ToolBase.java | 19 +++++- .../core/tool/ToolSchemaProvider.java | 1 + .../io/agentscope/core/tool/mcp/McpTool.java | 19 +++--- .../agent/ReActAgentCoarseStreamTest.java | 2 +- .../core/agent/ReActAgentHitlTest.java | 14 ++++- .../core/agent/ReActAgentNewLoopE2ETest.java | 2 +- .../core/permission/PermissionEngineTest.java | 3 +- .../core/tool/McpClientManagerTest.java | 5 ++ .../io/agentscope/core/tool/ToolkitTest.java | 1 + .../agentscope/core/tool/mcp/McpToolTest.java | 6 ++ .../tracing/OtelTracingMiddlewareTest.java | 3 +- .../agent/middleware/PlanModeMiddleware.java | 4 +- 24 files changed, 250 insertions(+), 38 deletions(-) diff --git a/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java b/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java index 0fd76fed63..0612fd93bc 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java +++ b/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java @@ -1872,6 +1872,7 @@ private Mono reasoning(int iter, boolean ignoreMaxIters) { ToolSchema.builder() .name(soTool.getName()) .description(soTool.getDescription()) + .title(soTool.getTitle()) .parameters(soTool.getParameters()) .strict(soTool.getStrict()) .outputSchema(soTool.getOutputSchema()) @@ -2042,7 +2043,7 @@ private Flux modelCallStream( String replyId = UUID.randomUUID().toString().replace("-", ""); AtomicBoolean textStarted = new AtomicBoolean(false); AtomicBoolean thinkingStarted = new AtomicBoolean(false); - Map startedToolCalls = new ConcurrentHashMap<>(); + Map startedToolCalls = new ConcurrentHashMap<>(); Flux modelEvents = mci.model().stream(mci.messages(), mci.tools(), mci.options()) @@ -2069,7 +2070,8 @@ private Flux modelCallStream( thinkingStarted, withToolEvents ? startedToolCalls - : new ConcurrentHashMap<>(), + : new ConcurrentHashMap< + String, String[]>(), events); } return Flux.fromIterable(events); @@ -2085,10 +2087,13 @@ private Flux modelCallStream( if (thinkingStarted.get()) { events.add(new ThinkingBlockEndEvent(replyId, "thinking")); } - for (Map.Entry tc : startedToolCalls.entrySet()) { + for (Map.Entry tc : startedToolCalls.entrySet()) { events.add( new ToolCallEndEvent( - replyId, tc.getKey(), tc.getValue())); + replyId, + tc.getKey(), + tc.getValue()[0], + tc.getValue()[1])); } events.add(new ModelCallEndEvent(replyId, context.getChatUsage())); return Flux.fromIterable(events); @@ -2103,7 +2108,7 @@ private void emitBlockEvents( ReasoningContext context, AtomicBoolean textStarted, AtomicBoolean thinkingStarted, - Map startedToolCalls, + Map startedToolCalls, List events) { if (block instanceof TextBlock tb) { @@ -2123,9 +2128,12 @@ private void emitBlockEvents( } else if (block instanceof ToolUseBlock tub) { String toolId = resolveToolCallId(tub, context); String toolName = tub.getName(); - if (toolId != null && startedToolCalls.putIfAbsent(toolId, toolName) == null) { + String toolTitle = resolveToolTitle(toolName); + if (toolId != null + && startedToolCalls.putIfAbsent(toolId, new String[] {toolName, toolTitle}) + == null) { if (toolName != null && !toolName.startsWith("__")) { - events.add(new ToolCallStartEvent(replyId, toolId, toolName)); + events.add(new ToolCallStartEvent(replyId, toolId, toolName, toolTitle)); } } if (tub.getContent() != null && !tub.getContent().isEmpty()) { @@ -2134,6 +2142,7 @@ private void emitBlockEvents( replyId, toolId != null ? toolId : "", toolName, + toolTitle, tub.getContent())); } } @@ -2147,6 +2156,12 @@ private String resolveToolCallId(ToolUseBlock tub, ReasoningContext context) { return accumulated != null ? accumulated.getId() : null; } + private String resolveToolTitle(String toolName) { + if (toolName == null) return null; + AgentTool tool = toolkit.getTool(toolName); + return tool != null ? tool.getTitle() : null; + } + /** * Execute the acting phase. * @@ -2372,18 +2387,24 @@ private Flux runToolBatch( .concatMap( entry -> { ToolUseBlock use = entry.getKey(); + String toolTitle = resolveToolTitle(use.getName()); return Flux.just( new ToolResultStartEvent( - replyId, use.getId(), use.getName()), + replyId, + use.getId(), + use.getName(), + toolTitle), new ToolResultTextDeltaEvent( replyId, use.getId(), use.getName(), + toolTitle, "Permission denied by user"), new ToolResultEndEvent( replyId, use.getId(), use.getName(), + toolTitle, ToolResultState.DENIED)); }); @@ -2404,11 +2425,14 @@ private Flux runToolBatch( Flux.create( sink -> { for (ToolUseBlock tool : approved) { + String toolTitle = + resolveToolTitle(tool.getName()); sink.next( new ToolResultStartEvent( replyId, tool.getId(), - tool.getName())); + tool.getName(), + toolTitle)); } Set chunkedToolIds = @@ -2420,6 +2444,9 @@ private Flux runToolBatch( && !chunk.getOutput() .isEmpty()) { chunkedToolIds.add(toolUse.getId()); + String chunkToolTitle = + resolveToolTitle( + toolUse.getName()); for (ContentBlock block : chunk.getOutput()) { if (block @@ -2432,6 +2459,7 @@ private Flux runToolBatch( .getId(), toolUse .getName(), + chunkToolTitle, tb .getText())); } else { @@ -2442,6 +2470,7 @@ private Flux runToolBatch( .getId(), toolUse .getName(), + chunkToolTitle, block)); } } @@ -2478,6 +2507,10 @@ private Flux runToolBatch( determineToolResultState( entry .getValue()); + String endToolTitle = + resolveToolTitle( + entry.getKey() + .getName()); sink.next( new ToolResultEndEvent( replyId, @@ -2485,6 +2518,7 @@ private Flux runToolBatch( .getId(), entry.getKey() .getName(), + endToolTitle, state)); } sink.complete(); @@ -2594,6 +2628,7 @@ private void emitToolResultDelta( Set chunkedToolIds) { String toolId = entry.getKey().getId(); String toolName = entry.getKey().getName(); + String toolTitle = resolveToolTitle(toolName); if (chunkedToolIds.contains(toolId)) { return; } @@ -2604,9 +2639,12 @@ private void emitToolResultDelta( for (ContentBlock block : output) { if (block instanceof TextBlock tb) { sink.next( - new ToolResultTextDeltaEvent(replyId, toolId, toolName, tb.getText())); + new ToolResultTextDeltaEvent( + replyId, toolId, toolName, toolTitle, tb.getText())); } else { - sink.next(new ToolResultDataDeltaEvent(replyId, toolId, toolName, block)); + sink.next( + new ToolResultDataDeltaEvent( + replyId, toolId, toolName, toolTitle, block)); } } } diff --git a/agentscope-core/src/main/java/io/agentscope/core/event/ToolCallDeltaEvent.java b/agentscope-core/src/main/java/io/agentscope/core/event/ToolCallDeltaEvent.java index 2826c21975..43730b1046 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/event/ToolCallDeltaEvent.java +++ b/agentscope-core/src/main/java/io/agentscope/core/event/ToolCallDeltaEvent.java @@ -26,6 +26,7 @@ public class ToolCallDeltaEvent extends AgentEvent { private final String replyId; private final String toolCallId; private final String toolCallName; + private final String toolCallTitle; private final String delta; @JsonCreator @@ -35,19 +36,26 @@ public ToolCallDeltaEvent( @JsonProperty("replyId") String replyId, @JsonProperty("toolCallId") String toolCallId, @JsonProperty("toolCallName") String toolCallName, + @JsonProperty("toolCallTitle") String toolCallTitle, @JsonProperty("delta") String delta) { super(id, createdAt); this.replyId = replyId; this.toolCallId = toolCallId; this.toolCallName = toolCallName; + this.toolCallTitle = toolCallTitle; this.delta = delta; } public ToolCallDeltaEvent( - String replyId, String toolCallId, String toolCallName, String delta) { + String replyId, + String toolCallId, + String toolCallName, + String toolCallTitle, + String delta) { this.replyId = replyId; this.toolCallId = toolCallId; this.toolCallName = toolCallName; + this.toolCallTitle = toolCallTitle; this.delta = delta; } @@ -68,6 +76,10 @@ public String getToolCallName() { return toolCallName; } + public String getToolCallTitle() { + return toolCallTitle; + } + public String getDelta() { return delta; } diff --git a/agentscope-core/src/main/java/io/agentscope/core/event/ToolCallEndEvent.java b/agentscope-core/src/main/java/io/agentscope/core/event/ToolCallEndEvent.java index 69edd24f80..d6dc39ead1 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/event/ToolCallEndEvent.java +++ b/agentscope-core/src/main/java/io/agentscope/core/event/ToolCallEndEvent.java @@ -23,6 +23,7 @@ public class ToolCallEndEvent extends AgentEvent { private final String replyId; private final String toolCallId; private final String toolCallName; + private final String toolCallTitle; @JsonCreator public ToolCallEndEvent( @@ -30,17 +31,21 @@ public ToolCallEndEvent( @JsonProperty("createdAt") String createdAt, @JsonProperty("replyId") String replyId, @JsonProperty("toolCallId") String toolCallId, - @JsonProperty("toolCallName") String toolCallName) { + @JsonProperty("toolCallName") String toolCallName, + @JsonProperty("toolCallTitle") String toolCallTitle) { super(id, createdAt); this.replyId = replyId; this.toolCallId = toolCallId; this.toolCallName = toolCallName; + this.toolCallTitle = toolCallTitle; } - public ToolCallEndEvent(String replyId, String toolCallId, String toolCallName) { + public ToolCallEndEvent( + String replyId, String toolCallId, String toolCallName, String toolCallTitle) { this.replyId = replyId; this.toolCallId = toolCallId; this.toolCallName = toolCallName; + this.toolCallTitle = toolCallTitle; } @Override @@ -59,4 +64,8 @@ public String getToolCallId() { public String getToolCallName() { return toolCallName; } + + public String getToolCallTitle() { + return toolCallTitle; + } } diff --git a/agentscope-core/src/main/java/io/agentscope/core/event/ToolCallStartEvent.java b/agentscope-core/src/main/java/io/agentscope/core/event/ToolCallStartEvent.java index b1f6ec4e9e..18b95cc293 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/event/ToolCallStartEvent.java +++ b/agentscope-core/src/main/java/io/agentscope/core/event/ToolCallStartEvent.java @@ -23,6 +23,7 @@ public class ToolCallStartEvent extends AgentEvent { private final String replyId; private final String toolCallId; private final String toolCallName; + private final String toolCallTitle; @JsonCreator public ToolCallStartEvent( @@ -30,17 +31,21 @@ public ToolCallStartEvent( @JsonProperty("createdAt") String createdAt, @JsonProperty("replyId") String replyId, @JsonProperty("toolCallId") String toolCallId, - @JsonProperty("toolCallName") String toolCallName) { + @JsonProperty("toolCallName") String toolCallName, + @JsonProperty("toolCallTitle") String toolCallTitle) { super(id, createdAt); this.replyId = replyId; this.toolCallId = toolCallId; this.toolCallName = toolCallName; + this.toolCallTitle = toolCallTitle; } - public ToolCallStartEvent(String replyId, String toolCallId, String toolCallName) { + public ToolCallStartEvent( + String replyId, String toolCallId, String toolCallName, String toolCallTitle) { this.replyId = replyId; this.toolCallId = toolCallId; this.toolCallName = toolCallName; + this.toolCallTitle = toolCallTitle; } @Override @@ -59,4 +64,8 @@ public String getToolCallId() { public String getToolCallName() { return toolCallName; } + + public String getToolCallTitle() { + return toolCallTitle; + } } diff --git a/agentscope-core/src/main/java/io/agentscope/core/event/ToolResultDataDeltaEvent.java b/agentscope-core/src/main/java/io/agentscope/core/event/ToolResultDataDeltaEvent.java index 3b6c03efdc..2988ff376e 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/event/ToolResultDataDeltaEvent.java +++ b/agentscope-core/src/main/java/io/agentscope/core/event/ToolResultDataDeltaEvent.java @@ -24,6 +24,7 @@ public class ToolResultDataDeltaEvent extends AgentEvent { private final String replyId; private final String toolCallId; private final String toolCallName; + private final String toolCallTitle; private final ContentBlock data; @JsonCreator @@ -33,19 +34,26 @@ public ToolResultDataDeltaEvent( @JsonProperty("replyId") String replyId, @JsonProperty("toolCallId") String toolCallId, @JsonProperty("toolCallName") String toolCallName, + @JsonProperty("toolCallTitle") String toolCallTitle, @JsonProperty("data") ContentBlock data) { super(id, createdAt); this.replyId = replyId; this.toolCallId = toolCallId; this.toolCallName = toolCallName; + this.toolCallTitle = toolCallTitle; this.data = data; } public ToolResultDataDeltaEvent( - String replyId, String toolCallId, String toolCallName, ContentBlock data) { + String replyId, + String toolCallId, + String toolCallName, + String toolCallTitle, + ContentBlock data) { this.replyId = replyId; this.toolCallId = toolCallId; this.toolCallName = toolCallName; + this.toolCallTitle = toolCallTitle; this.data = data; } @@ -66,6 +74,10 @@ public String getToolCallName() { return toolCallName; } + public String getToolCallTitle() { + return toolCallTitle; + } + public ContentBlock getData() { return data; } diff --git a/agentscope-core/src/main/java/io/agentscope/core/event/ToolResultEndEvent.java b/agentscope-core/src/main/java/io/agentscope/core/event/ToolResultEndEvent.java index f9321f56e3..235df2ee0d 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/event/ToolResultEndEvent.java +++ b/agentscope-core/src/main/java/io/agentscope/core/event/ToolResultEndEvent.java @@ -24,6 +24,7 @@ public class ToolResultEndEvent extends AgentEvent { private final String replyId; private final String toolCallId; private final String toolCallName; + private final String toolCallTitle; private final ToolResultState state; @JsonCreator @@ -33,19 +34,26 @@ public ToolResultEndEvent( @JsonProperty("replyId") String replyId, @JsonProperty("toolCallId") String toolCallId, @JsonProperty("toolCallName") String toolCallName, + @JsonProperty("toolCallTitle") String toolCallTitle, @JsonProperty("state") ToolResultState state) { super(id, createdAt); this.replyId = replyId; this.toolCallId = toolCallId; this.toolCallName = toolCallName; + this.toolCallTitle = toolCallTitle; this.state = state; } public ToolResultEndEvent( - String replyId, String toolCallId, String toolCallName, ToolResultState state) { + String replyId, + String toolCallId, + String toolCallName, + String toolCallTitle, + ToolResultState state) { this.replyId = replyId; this.toolCallId = toolCallId; this.toolCallName = toolCallName; + this.toolCallTitle = toolCallTitle; this.state = state; } @@ -66,6 +74,10 @@ public String getToolCallName() { return toolCallName; } + public String getToolCallTitle() { + return toolCallTitle; + } + public ToolResultState getState() { return state; } diff --git a/agentscope-core/src/main/java/io/agentscope/core/event/ToolResultStartEvent.java b/agentscope-core/src/main/java/io/agentscope/core/event/ToolResultStartEvent.java index 5378903f40..5e67008e0c 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/event/ToolResultStartEvent.java +++ b/agentscope-core/src/main/java/io/agentscope/core/event/ToolResultStartEvent.java @@ -23,6 +23,7 @@ public class ToolResultStartEvent extends AgentEvent { private final String replyId; private final String toolCallId; private final String toolCallName; + private final String toolCallTitle; @JsonCreator public ToolResultStartEvent( @@ -30,17 +31,21 @@ public ToolResultStartEvent( @JsonProperty("createdAt") String createdAt, @JsonProperty("replyId") String replyId, @JsonProperty("toolCallId") String toolCallId, - @JsonProperty("toolCallName") String toolCallName) { + @JsonProperty("toolCallName") String toolCallName, + @JsonProperty("toolCallTitle") String toolCallTitle) { super(id, createdAt); this.replyId = replyId; this.toolCallId = toolCallId; this.toolCallName = toolCallName; + this.toolCallTitle = toolCallTitle; } - public ToolResultStartEvent(String replyId, String toolCallId, String toolCallName) { + public ToolResultStartEvent( + String replyId, String toolCallId, String toolCallName, String toolCallTitle) { this.replyId = replyId; this.toolCallId = toolCallId; this.toolCallName = toolCallName; + this.toolCallTitle = toolCallTitle; } @Override @@ -59,4 +64,8 @@ public String getToolCallId() { public String getToolCallName() { return toolCallName; } + + public String getToolCallTitle() { + return toolCallTitle; + } } diff --git a/agentscope-core/src/main/java/io/agentscope/core/event/ToolResultTextDeltaEvent.java b/agentscope-core/src/main/java/io/agentscope/core/event/ToolResultTextDeltaEvent.java index f9b464b0d7..f4e7a9353d 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/event/ToolResultTextDeltaEvent.java +++ b/agentscope-core/src/main/java/io/agentscope/core/event/ToolResultTextDeltaEvent.java @@ -23,6 +23,7 @@ public class ToolResultTextDeltaEvent extends AgentEvent { private final String replyId; private final String toolCallId; private final String toolCallName; + private final String toolCallTitle; private final String delta; @JsonCreator @@ -32,19 +33,26 @@ public ToolResultTextDeltaEvent( @JsonProperty("replyId") String replyId, @JsonProperty("toolCallId") String toolCallId, @JsonProperty("toolCallName") String toolCallName, + @JsonProperty("toolCallTitle") String toolCallTitle, @JsonProperty("delta") String delta) { super(id, createdAt); this.replyId = replyId; this.toolCallId = toolCallId; this.toolCallName = toolCallName; + this.toolCallTitle = toolCallTitle; this.delta = delta; } public ToolResultTextDeltaEvent( - String replyId, String toolCallId, String toolCallName, String delta) { + String replyId, + String toolCallId, + String toolCallName, + String toolCallTitle, + String delta) { this.replyId = replyId; this.toolCallId = toolCallId; this.toolCallName = toolCallName; + this.toolCallTitle = toolCallTitle; this.delta = delta; } @@ -65,6 +73,10 @@ public String getToolCallName() { return toolCallName; } + public String getToolCallTitle() { + return toolCallTitle; + } + public String getDelta() { return delta; } diff --git a/agentscope-core/src/main/java/io/agentscope/core/model/ToolSchema.java b/agentscope-core/src/main/java/io/agentscope/core/model/ToolSchema.java index 3dcf3a06df..926d48c7b5 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/model/ToolSchema.java +++ b/agentscope-core/src/main/java/io/agentscope/core/model/ToolSchema.java @@ -28,6 +28,7 @@ public class ToolSchema { private final String name; private final String description; + private final String title; private final Map parameters; private final Map outputSchema; private final Boolean strict; @@ -40,6 +41,7 @@ public class ToolSchema { private ToolSchema(Builder builder) { this.name = Objects.requireNonNull(builder.name, "name is required"); this.description = Objects.requireNonNull(builder.description, "description is required"); + this.title = builder.title; this.parameters = builder.parameters != null ? Collections.unmodifiableMap(new HashMap<>(builder.parameters)) @@ -69,6 +71,15 @@ public String getDescription() { return description; } + /** + * Gets the optional human-readable tool title. + * + * @return the tool title, or null if no title is defined + */ + public String getTitle() { + return title; + } + /** * Gets the tool parameters as a JSON Schema. * @@ -111,6 +122,7 @@ public static Builder builder() { public static class Builder { private String name; private String description; + private String title; private Map parameters; private Map outputSchema; private Boolean strict; @@ -137,6 +149,17 @@ public Builder description(String description) { return this; } + /** + * Sets the optional human-readable tool title. + * + * @param title the tool title + * @return this builder instance + */ + public Builder title(String title) { + this.title = title; + return this; + } + /** * Sets the tool parameters as a JSON Schema. * diff --git a/agentscope-core/src/main/java/io/agentscope/core/tool/AgentTool.java b/agentscope-core/src/main/java/io/agentscope/core/tool/AgentTool.java index f4dfa1b154..928e711416 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/tool/AgentTool.java +++ b/agentscope-core/src/main/java/io/agentscope/core/tool/AgentTool.java @@ -49,6 +49,19 @@ public interface AgentTool { */ String getName(); + /** + * Gets the title of the tool. + * + *

Intended for UI and end-user contexts — optimized to be human-readable and easily + * understood, even by those unfamiliar with domain-specific terminology. If not + * provided, the name should be used for display. + * + * @return The tool title (never null) + */ + default String getTitle() { + return getName(); + } + /** * Gets the description of the tool. * diff --git a/agentscope-core/src/main/java/io/agentscope/core/tool/McpClientManager.java b/agentscope-core/src/main/java/io/agentscope/core/tool/McpClientManager.java index bd300397b9..9bc667ec7f 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/tool/McpClientManager.java +++ b/agentscope-core/src/main/java/io/agentscope/core/tool/McpClientManager.java @@ -178,6 +178,7 @@ Mono registerMcpClient( mcpTool.description() != null ? mcpTool.description() : "", + mcpTool.title(), McpTool.convertMcpSchemaToParameters( mcpTool.inputSchema(), toolPresetParams != null diff --git a/agentscope-core/src/main/java/io/agentscope/core/tool/SchemaOnlyTool.java b/agentscope-core/src/main/java/io/agentscope/core/tool/SchemaOnlyTool.java index cf656749b6..7a4a8ea5d5 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/tool/SchemaOnlyTool.java +++ b/agentscope-core/src/main/java/io/agentscope/core/tool/SchemaOnlyTool.java @@ -68,6 +68,7 @@ public SchemaOnlyTool(ToolSchema schema) { this( Objects.requireNonNull(schema, "schema cannot be null").getName(), schema.getDescription(), + schema.getTitle(), schema.getParameters(), schema.getStrict()); } @@ -83,7 +84,7 @@ public SchemaOnlyTool(ToolSchema schema) { * @throws NullPointerException if name or description is null */ public SchemaOnlyTool(String name, String description, Map parameters) { - this(name, description, parameters, null); + this(name, description, null, parameters, null); } /** @@ -101,11 +102,25 @@ public SchemaOnlyTool(String name, String description, Map param */ public SchemaOnlyTool( String name, String description, Map parameters, Boolean strict) { + this(name, description, null, parameters, strict); + } + + /** + * Creates a new SchemaOnlyTool with the specified name, description, title, parameters, and + * strict mode configuration. + */ + public SchemaOnlyTool( + String name, + String description, + String title, + Map parameters, + Boolean strict) { super( ToolBase.builder() .name(Objects.requireNonNull(name, "name cannot be null")) .description( Objects.requireNonNull(description, "description cannot be null")) + .title(title) .inputSchema( parameters != null ? Collections.unmodifiableMap(new HashMap<>(parameters)) diff --git a/agentscope-core/src/main/java/io/agentscope/core/tool/ToolBase.java b/agentscope-core/src/main/java/io/agentscope/core/tool/ToolBase.java index 7334661b5f..95bff37786 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/tool/ToolBase.java +++ b/agentscope-core/src/main/java/io/agentscope/core/tool/ToolBase.java @@ -68,6 +68,7 @@ public abstract class ToolBase implements AgentTool { private final boolean stateInjected; private final boolean mcp; private final String mcpName; + private final String title; /** Sensitive files; subclasses may replace this list to widen or narrow protection. */ protected List dangerousFiles = ToolDangerousPathConstants.DEFAULT_DANGEROUS_FILES; @@ -87,7 +88,8 @@ protected ToolBase(Builder builder) { builder.mcp, builder.mcpName, builder.externalTool, - builder.stateInjected); + builder.stateInjected, + builder.title); if (builder.dangerousFiles != null) { this.dangerousFiles = List.copyOf(builder.dangerousFiles); } @@ -109,7 +111,8 @@ protected ToolBase( boolean mcp, String mcpName, boolean externalTool, - boolean stateInjected) { + boolean stateInjected, + String title) { this.name = Objects.requireNonNull(name, "name must not be null"); this.description = Objects.requireNonNull(description, "description must not be null"); this.inputSchema = Objects.requireNonNull(inputSchema, "inputSchema must not be null"); @@ -119,6 +122,7 @@ protected ToolBase( this.mcpName = mcpName; this.externalTool = externalTool; this.stateInjected = stateInjected; + this.title = title; if (mcp && (mcpName == null || mcpName.isBlank())) { throw new IllegalArgumentException("mcpName is required when mcp is true"); } @@ -163,6 +167,11 @@ public final String getMcpName() { return mcpName; } + @Override + public final String getTitle() { + return title; + } + /** * Default tool invocation. External tools must not be invoked locally; non-external subclasses * must override this method. @@ -281,6 +290,7 @@ public static final class Builder { private boolean stateInjected = false; private boolean mcp = false; private String mcpName; + private String title; private List dangerousFiles; private List dangerousDirectories; @@ -328,6 +338,11 @@ public Builder mcp(String mcpName) { return this; } + public Builder title(String title) { + this.title = title; + return this; + } + public Builder dangerousFiles(List dangerousFiles) { this.dangerousFiles = dangerousFiles; return this; diff --git a/agentscope-core/src/main/java/io/agentscope/core/tool/ToolSchemaProvider.java b/agentscope-core/src/main/java/io/agentscope/core/tool/ToolSchemaProvider.java index 40ac9e0e0e..016b5c8300 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/tool/ToolSchemaProvider.java +++ b/agentscope-core/src/main/java/io/agentscope/core/tool/ToolSchemaProvider.java @@ -98,6 +98,7 @@ private List buildSchemas(Set activeTools) { ToolSchema.builder() .name(toolName) .description(tool.getDescription()) + .title(tool.getTitle()) .parameters(registered.getExtendedParameters()) .strict(tool.getStrict()) .outputSchema(tool.getOutputSchema()) diff --git a/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpTool.java b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpTool.java index b7b3a9ec49..b5b72b011d 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpTool.java +++ b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpTool.java @@ -60,6 +60,7 @@ public class McpTool extends ToolBase { public McpTool( String name, String description, + String title, Map parameters, Map outputSchema, McpClientWrapper clientWrapper, @@ -70,6 +71,7 @@ public McpTool( ToolBase.builder() .name(Objects.requireNonNull(name, "name cannot be null")) .description(description != null ? description : "") + .title(title) .inputSchema(parameters != null ? parameters : new HashMap<>()) .readOnly(readOnly) .concurrencySafe(false) @@ -80,9 +82,9 @@ public McpTool( } /** - * @deprecated Use {@link #McpTool(String, String, Map, Map, McpClientWrapper, Map, String, - * boolean)} so the MCP server name and read-only hint are propagated. Kept for source - * compatibility with callers that pre-date the {@link ToolBase} integration. + * @deprecated Use {@link #McpTool(String, String, String, Map, Map, McpClientWrapper, Map, + * String, boolean)} so the title, MCP server name and read-only hint are propagated. Kept for + * source compatibility with callers that pre-date the {@link ToolBase} integration. */ @Deprecated(since = "2.0.0") public McpTool( @@ -93,6 +95,7 @@ public McpTool( this( name, description, + null, parameters, null, clientWrapper, @@ -102,8 +105,8 @@ public McpTool( } /** - * @deprecated Use {@link #McpTool(String, String, Map, Map, McpClientWrapper, Map, String, - * boolean)} so the MCP server name and read-only hint are propagated. + * @deprecated Use {@link #McpTool(String, String, String, Map, Map, McpClientWrapper, Map, + * String, boolean)} so the title, MCP server name and read-only hint are propagated. */ @Deprecated(since = "2.0.0") public McpTool( @@ -115,6 +118,7 @@ public McpTool( this( name, description, + null, parameters, null, clientWrapper, @@ -124,8 +128,8 @@ public McpTool( } /** - * @deprecated Use {@link #McpTool(String, String, Map, Map, McpClientWrapper, Map, String, - * boolean)} so the MCP server name and read-only hint are propagated. + * @deprecated Use {@link #McpTool(String, String, String, Map, Map, McpClientWrapper, Map, + * String, boolean)} so the title, MCP server name and read-only hint are propagated. */ @Deprecated(since = "2.0.0") public McpTool( @@ -138,6 +142,7 @@ public McpTool( this( name, description, + null, parameters, outputSchema, clientWrapper, diff --git a/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentCoarseStreamTest.java b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentCoarseStreamTest.java index e0a25971dc..fa99f782b0 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentCoarseStreamTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentCoarseStreamTest.java @@ -93,7 +93,7 @@ private static ChatResponse toolUseResponse(String id, String name, String q) { private static final class EchoTool extends ToolBase { EchoTool() { - super("echo", "echoes input", schema(), true, true, false, null, false, false); + super("echo", "echoes input", schema(), true, true, false, null, false, false, null); } private static Map schema() { diff --git a/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentHitlTest.java b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentHitlTest.java index 501fcb6dca..9ca4619d37 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentHitlTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentHitlTest.java @@ -112,7 +112,17 @@ private static ChatResponse toolUseResponse(String toolId, String toolName, Stri private static final class AskingTool extends ToolBase { AskingTool(String name) { - super(name, "asks for permission", schemaFor(), false, true, false, null, false, false); + super( + name, + "asks for permission", + schemaFor(), + false, + true, + false, + null, + false, + false, + null); } private static Map schemaFor() { @@ -141,7 +151,7 @@ public Mono callAsync(ToolCallParam param) { private static final class AllowingTool extends ToolBase { AllowingTool(String name) { - super(name, "auto-allow", schemaFor(), true, true, false, null, false, false); + super(name, "auto-allow", schemaFor(), true, true, false, null, false, false, null); } private static Map schemaFor() { diff --git a/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentNewLoopE2ETest.java b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentNewLoopE2ETest.java index 3bb514ff01..0dc8f030bb 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentNewLoopE2ETest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentNewLoopE2ETest.java @@ -107,7 +107,7 @@ private static ChatResponse toolUseResponse(String id, String name, String q) { private static final class AlwaysAllowTool extends ToolBase { AlwaysAllowTool(String name) { - super(name, "always allow", schema(), true, true, false, null, false, false); + super(name, "always allow", schema(), true, true, false, null, false, false, null); } private static Map schema() { diff --git a/agentscope-core/src/test/java/io/agentscope/core/permission/PermissionEngineTest.java b/agentscope-core/src/test/java/io/agentscope/core/permission/PermissionEngineTest.java index 6deb99a206..d19fe46a59 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/permission/PermissionEngineTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/permission/PermissionEngineTest.java @@ -67,7 +67,8 @@ private static final class FakePermissionTool extends ToolBase { /* isMcp */ false, /* mcpName */ null, /* isExternalTool */ false, - /* isStateInjected */ false); + /* isStateInjected */ false, + null); } FakePermissionTool withPermissionDecision(PermissionDecision decision) { diff --git a/agentscope-core/src/test/java/io/agentscope/core/tool/McpClientManagerTest.java b/agentscope-core/src/test/java/io/agentscope/core/tool/McpClientManagerTest.java index fe025a3132..4471a5c0f0 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/tool/McpClientManagerTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/tool/McpClientManagerTest.java @@ -169,6 +169,7 @@ void testRegisterMcpClient_WithPresetParametersMapping() { McpSchema.Tool mockMcpTool = mock(McpSchema.Tool.class); when(mockMcpTool.name()).thenReturn("test-tool"); when(mockMcpTool.description()).thenReturn("Test tool description"); + when(mockMcpTool.title()).thenReturn(null); // Create schema with properties Map properties = new HashMap<>(); @@ -227,6 +228,7 @@ void testRegisterMcpClient_WithPresetParametersKeySetExclusion() { McpSchema.Tool mockMcpTool = mock(McpSchema.Tool.class); when(mockMcpTool.name()).thenReturn("weather-tool"); when(mockMcpTool.description()).thenReturn("Weather tool"); + when(mockMcpTool.title()).thenReturn(null); // Create schema with multiple parameters Map properties = new HashMap<>(); @@ -283,6 +285,7 @@ void testRegisterMcpClient_WithEmptyPresetParameters() { McpSchema.Tool mockMcpTool = mock(McpSchema.Tool.class); when(mockMcpTool.name()).thenReturn("simple-tool"); when(mockMcpTool.description()).thenReturn("Simple tool"); + when(mockMcpTool.title()).thenReturn(null); when(mockMcpTool.inputSchema()) .thenReturn( @@ -328,6 +331,7 @@ void testRegisterMcpClient_WithNullPresetParametersForTool() { McpSchema.Tool mockMcpTool = mock(McpSchema.Tool.class); when(mockMcpTool.name()).thenReturn("null-param-tool"); when(mockMcpTool.description()).thenReturn("Tool with null preset params"); + when(mockMcpTool.title()).thenReturn(null); when(mockMcpTool.inputSchema()) .thenReturn( @@ -373,6 +377,7 @@ void testRegisterMcpClient_PreservesOutputSchemaInRegisteredTool() { McpSchema.Tool mockMcpTool = mock(McpSchema.Tool.class); when(mockMcpTool.name()).thenReturn("structured-tool"); when(mockMcpTool.description()).thenReturn("Tool with output schema"); + when(mockMcpTool.title()).thenReturn(null); when(mockMcpTool.inputSchema()) .thenReturn( new McpSchema.JsonSchema( diff --git a/agentscope-core/src/test/java/io/agentscope/core/tool/ToolkitTest.java b/agentscope-core/src/test/java/io/agentscope/core/tool/ToolkitTest.java index 2de122bfdd..9d08334a97 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/tool/ToolkitTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/tool/ToolkitTest.java @@ -1236,6 +1236,7 @@ void testGetToolSchemasIncludesMcpOutputSchema() { McpSchema.Tool mcpTool = mock(McpSchema.Tool.class); when(mcpTool.name()).thenReturn("structured_mcp_tool"); when(mcpTool.description()).thenReturn("Returns structured MCP output"); + when(mcpTool.title()).thenReturn(null); when(mcpTool.inputSchema()) .thenReturn( new McpSchema.JsonSchema("object", Map.of(), List.of(), null, null, null)); diff --git a/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/McpToolTest.java b/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/McpToolTest.java index 3aa115d8d7..da15d3bd74 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/McpToolTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/McpToolTest.java @@ -813,6 +813,7 @@ void testConstructor_FullConstructor_WithAllParams() { new McpTool( "test-tool", "Description", + "test tool", parameters, outputSchema, mockClientWrapper, @@ -822,6 +823,7 @@ void testConstructor_FullConstructor_WithAllParams() { assertEquals("test-tool", tool.getName()); assertEquals("Description", tool.getDescription()); + assertEquals("test tool", tool.getTitle()); assertEquals("custom-mcp-server", tool.getMcpName()); assertTrue(tool.isReadOnly()); assertNotNull(tool.getOutputSchema()); @@ -835,6 +837,7 @@ void testConstructor_FullConstructor_WithNullDescription() { new McpTool( "test-tool", null, + "test tool", parameters, null, mockClientWrapper, @@ -854,6 +857,7 @@ void testConstructor_FullConstructor_DefensiveCopyOutputSchema() { new McpTool( "test-tool", "Description", + "test tool", parameters, outputSchema, mockClientWrapper, @@ -875,6 +879,7 @@ void testConstructor_FullConstructor_DefensiveCopyPresetArgs() { new McpTool( "test-tool", "Description", + "test tool", parameters, null, mockClientWrapper, @@ -897,6 +902,7 @@ void testGetOutputSchema_ReturnsDefensiveCopy() { new McpTool( "test-tool", "Description", + "test tool", parameters, outputSchema, mockClientWrapper, diff --git a/agentscope-core/src/test/java/io/agentscope/core/tracing/OtelTracingMiddlewareTest.java b/agentscope-core/src/test/java/io/agentscope/core/tracing/OtelTracingMiddlewareTest.java index 1e8a0ee1fc..2a3f332108 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/tracing/OtelTracingMiddlewareTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/tracing/OtelTracingMiddlewareTest.java @@ -183,7 +183,8 @@ void onActing_createsExecuteToolSpan() { .build(); ToolResultEndEvent tre = - new ToolResultEndEvent("reply-1", "call-1", "testTool", ToolResultState.SUCCESS); + new ToolResultEndEvent( + "reply-1", "call-1", "testTool", "Test Tool", ToolResultState.SUCCESS); ActingInput input = new ActingInput(List.of(toolCall)); Flux result = middleware.onActing(agent, null, input, in -> Flux.just(tre)); result.collectList().block(); diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/PlanModeMiddleware.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/PlanModeMiddleware.java index 56cf3f95b2..69d14f1640 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/PlanModeMiddleware.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/PlanModeMiddleware.java @@ -210,18 +210,20 @@ public Flux onActing( state.contextMutable().add(msg); events.add( new ToolResultStartEvent( - replyId, call.getId(), call.getName())); + replyId, call.getId(), call.getName(), null)); events.add( new ToolResultTextDeltaEvent( replyId, call.getId(), call.getName(), + null, DENY_MESSAGE)); events.add( new ToolResultEndEvent( replyId, call.getId(), call.getName(), + null, ToolResultState.DENIED)); } return Flux.fromIterable(events); From 3fb9fc5674cdd583bd2811bfe51c3d503ef3b563 Mon Sep 17 00:00:00 2001 From: keep simple <3132670669@qq.com> Date: Fri, 12 Jun 2026 22:00:55 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E8=A7=A3=E5=86=B3=20Windows=20=E7=8E=AF?= =?UTF-8?q?=E5=A2=83=E4=B8=8B=20mvnd=20-B=20-T1=20clean=20verify=20?= =?UTF-8?q?=E4=B8=8D=E9=80=9A=E8=BF=87=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/tool/DangerousPathBypassTest.java | 16 ++++++++++++++++ ...ndboxFilesystemIsolationScopeExampleTest.java | 5 +++++ .../filesystem/ProjectAwareOverlayTest.java | 5 +++++ 3 files changed, 26 insertions(+) diff --git a/agentscope-core/src/test/java/io/agentscope/core/tool/DangerousPathBypassTest.java b/agentscope-core/src/test/java/io/agentscope/core/tool/DangerousPathBypassTest.java index 0cbfbf3d37..a73e917028 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/tool/DangerousPathBypassTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/tool/DangerousPathBypassTest.java @@ -16,6 +16,7 @@ package io.agentscope.core.tool; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import io.agentscope.core.permission.PermissionContextState; import io.agentscope.core.permission.PermissionDecision; @@ -62,6 +63,19 @@ public Mono checkPermissions( private static final ProbeToolBase PROBE = new ProbeToolBase(); + private static boolean canCreateSymlinks(Path tempDir) { + try { + Path target = Files.createTempFile(tempDir, "symcap", ".tmp"); + Path link = tempDir.resolve("symcap-link"); + Files.createSymbolicLink(link, target); + Files.deleteIfExists(link); + Files.deleteIfExists(target); + return true; + } catch (Exception e) { + return false; + } + } + @Test void dotEnvIsDetected() { assertTrue(PROBE.check("/home/user/.env")); @@ -99,6 +113,7 @@ void caseInsensitiveDotEnvIsDetected() { @Test void symlinkToSshIsDetected(@TempDir Path tempDir) throws IOException { + assumeTrue(canCreateSymlinks(tempDir), "Symlink creation not supported on this system"); Path sshDir = tempDir.resolve(".ssh"); Files.createDirectory(sshDir); Path sshConfig = sshDir.resolve("config"); @@ -112,6 +127,7 @@ void symlinkToSshIsDetected(@TempDir Path tempDir) throws IOException { @Test void symlinkToDotEnvIsDetected(@TempDir Path tempDir) throws IOException { + assumeTrue(canCreateSymlinks(tempDir), "Symlink creation not supported on this system"); Path envFile = tempDir.resolve(".env"); Files.writeString(envFile, "SECRET=value\n"); diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/example/SandboxFilesystemIsolationScopeExampleTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/example/SandboxFilesystemIsolationScopeExampleTest.java index af81a7dd35..317378b396 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/example/SandboxFilesystemIsolationScopeExampleTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/example/SandboxFilesystemIsolationScopeExampleTest.java @@ -38,6 +38,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.io.TempDir; import reactor.core.publisher.Flux; @@ -65,6 +67,9 @@ * step. The assertions count {@link InMemorySandboxClient#getCreateCount()} and * {@link InMemorySandboxClient#getResumeCount()} to verify isolation behaviour. */ +@DisabledOnOs( + value = OS.WINDOWS, + disabledReason = "InMemorySandbox.exec hardcodes `sh`, unavailable on Windows") class SandboxFilesystemIsolationScopeExampleTest { @TempDir Path workspace; diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/filesystem/ProjectAwareOverlayTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/filesystem/ProjectAwareOverlayTest.java index 69cdcf7837..38515407bf 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/filesystem/ProjectAwareOverlayTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/filesystem/ProjectAwareOverlayTest.java @@ -37,6 +37,8 @@ import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.io.TempDir; class ProjectAwareOverlayTest { @@ -235,6 +237,9 @@ void isWorkspacePath_absoluteUnderProject_returnsFalse() { // ==================== Shell execute delegates to upper ==================== @Test + @DisabledOnOs( + value = OS.WINDOWS, + disabledReason = "LocalFilesystemWithShell hardcodes `sh`, unavailable on Windows") void execute_delegatesToShellBackend() { var r = overlay.execute(rc, "echo hello", 10); assertTrue(r.output().contains("hello")); From 9995f204929b898a6cfa6004ee7b6900998536e1 Mon Sep 17 00:00:00 2001 From: keep simple <3132670669@qq.com> Date: Fri, 12 Jun 2026 23:20:12 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E6=8F=90=E9=AB=98=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E8=A6=86=E7=9B=96=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/agent/ReActAgentToolTitleTest.java | 211 +++++++++ .../core/event/ToolEventClassesTest.java | 399 ++++++++++++++++++ 2 files changed, 610 insertions(+) create mode 100644 agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentToolTitleTest.java create mode 100644 agentscope-core/src/test/java/io/agentscope/core/event/ToolEventClassesTest.java diff --git a/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentToolTitleTest.java b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentToolTitleTest.java new file mode 100644 index 0000000000..a0f53cbb78 --- /dev/null +++ b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentToolTitleTest.java @@ -0,0 +1,211 @@ +/* + * 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.agent; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import io.agentscope.core.ReActAgent; +import io.agentscope.core.event.AgentEvent; +import io.agentscope.core.event.ToolCallEndEvent; +import io.agentscope.core.event.ToolCallStartEvent; +import io.agentscope.core.event.ToolResultEndEvent; +import io.agentscope.core.event.ToolResultStartEvent; +import io.agentscope.core.message.ContentBlock; +import io.agentscope.core.message.Msg; +import io.agentscope.core.message.TextBlock; +import io.agentscope.core.message.ToolResultBlock; +import io.agentscope.core.message.ToolUseBlock; +import io.agentscope.core.model.ChatModelBase; +import io.agentscope.core.model.ChatResponse; +import io.agentscope.core.model.GenerateOptions; +import io.agentscope.core.model.ToolSchema; +import io.agentscope.core.tool.AgentTool; +import io.agentscope.core.tool.ToolCallParam; +import io.agentscope.core.tool.Toolkit; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * Coverage for the toolTitle resolution paths added in the McpTool-title work: + * the tool-found / tool-not-found branches of {@code ReasoningStream.resolveToolTitle} + * and the propagation of a non-default title through the tool-call / + * tool-result event chain. + */ +class ReActAgentToolTitleTest { + + @Test + void customToolTitlePropagatesThroughToolCallAndResultEvents() { + AgentTool tool = new TitledTool("search", "Web Search"); + Toolkit toolkit = new Toolkit(); + toolkit.registerAgentTool(tool); + + ChatModelBase model = + new ScriptedModel( + List.of( + () -> Flux.just(toolUseResponse("tc1", "search", "alpha")), + () -> Flux.just(textResponse("done")))); + ReActAgent agent = ReActAgent.builder().name("asst").model(model).toolkit(toolkit).build(); + + List events = agent.streamEvents(List.of()).collectList().block(); + assertNotNull(events); + + ToolCallStartEvent callStart = firstOf(events, ToolCallStartEvent.class); + assertNotNull(callStart); + assertEquals("search", callStart.getToolCallName()); + assertEquals("Web Search", callStart.getToolCallTitle()); + + ToolCallEndEvent callEnd = firstOf(events, ToolCallEndEvent.class); + assertNotNull(callEnd); + assertEquals("Web Search", callEnd.getToolCallTitle()); + + ToolResultStartEvent resultStart = firstOf(events, ToolResultStartEvent.class); + assertNotNull(resultStart); + assertEquals("Web Search", resultStart.getToolCallTitle()); + + ToolResultEndEvent resultEnd = firstOf(events, ToolResultEndEvent.class); + assertNotNull(resultEnd); + assertEquals("Web Search", resultEnd.getToolCallTitle()); + } + + @Test + void unknownToolNameYieldsNullTitleInToolCallStart() { + Toolkit toolkit = new Toolkit(); + toolkit.registerAgentTool(new TitledTool("search", "Web Search")); + + ChatModelBase model = + new ScriptedModel( + List.of( + () -> + Flux.just( + toolUseResponse( + "tc1", "ghost_tool_not_registered", "x")), + () -> Flux.just(textResponse("fallback")))); + ReActAgent agent = ReActAgent.builder().name("asst").model(model).toolkit(toolkit).build(); + + List events = agent.streamEvents(List.of()).collectList().block(); + assertNotNull(events); + + ToolCallStartEvent callStart = firstOf(events, ToolCallStartEvent.class); + assertNotNull(callStart); + assertEquals("ghost_tool_not_registered", callStart.getToolCallName()); + assertNull( + callStart.getToolCallTitle(), + "unknown tool name should resolve to null title (tool == null branch)"); + + ToolCallEndEvent callEnd = firstOf(events, ToolCallEndEvent.class); + assertNotNull(callEnd); + assertNull(callEnd.getToolCallTitle()); + } + + private static T firstOf(List events, Class type) { + for (AgentEvent e : events) { + if (type.isInstance(e)) { + return type.cast(e); + } + } + return null; + } + + private static final class ScriptedModel extends ChatModelBase { + private final List>> scripts; + private final AtomicInteger idx = new AtomicInteger(0); + + ScriptedModel(List>> scripts) { + this.scripts = scripts; + } + + @Override + public String getModelName() { + return "scripted"; + } + + @Override + protected Flux doStream( + List messages, List tools, GenerateOptions options) { + int i = idx.getAndIncrement(); + if (i >= scripts.size()) { + return Flux.just(textResponse("")); + } + return scripts.get(i).get(); + } + } + + private static ChatResponse textResponse(String text) { + return ChatResponse.builder() + .content(List.of(TextBlock.builder().text(text).build())) + .build(); + } + + private static ChatResponse toolUseResponse(String toolId, String toolName, String inputJson) { + Map input = new HashMap<>(); + input.put("query", inputJson); + return ChatResponse.builder() + .content( + List.of( + ToolUseBlock.builder() + .id(toolId) + .name(toolName) + .input(input) + .build())) + .build(); + } + + private record TitledTool(String name, String title) implements AgentTool { + + @Override + public String getTitle() { + return title; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getDescription() { + return "titled tool"; + } + + @Override + public Map getParameters() { + Map schema = new HashMap<>(); + schema.put("type", "object"); + Map props = new HashMap<>(); + Map q = new HashMap<>(); + q.put("type", "string"); + props.put("query", q); + schema.put("properties", props); + return schema; + } + + @Override + public Mono callAsync(ToolCallParam param) { + Object q = param.getInput() == null ? "" : param.getInput().get("query"); + return Mono.just(ToolResultBlock.text("titled:" + q)); + } + } +} diff --git a/agentscope-core/src/test/java/io/agentscope/core/event/ToolEventClassesTest.java b/agentscope-core/src/test/java/io/agentscope/core/event/ToolEventClassesTest.java new file mode 100644 index 0000000000..6c71b10b4e --- /dev/null +++ b/agentscope-core/src/test/java/io/agentscope/core/event/ToolEventClassesTest.java @@ -0,0 +1,399 @@ +/* + * 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.event; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.agentscope.core.message.ContentBlock; +import io.agentscope.core.message.TextBlock; +import io.agentscope.core.message.ToolResultState; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * Coverage for the tool-call / tool-result event POJOs: both constructors, + * every getter, {@link AgentEvent#getType()} discriminator, and Jackson + * polymorphic round-trip through the {@link AgentEvent} subtype registry. + */ +class ToolEventClassesTest { + + private static final String REPLY_ID = "reply-1"; + private static final String CALL_ID = "call-1"; + private static final String TOOL_NAME = "search"; + private static final String TOOL_TITLE = "Web Search"; + private static final String DELTA = "{\"q\":\"x\"}"; + private static final String EVENT_ID = "evt-1"; + private static final String CREATED_AT = "2026-01-01T00:00:00Z"; + + private final ObjectMapper mapper = new ObjectMapper(); + + @Nested + class ToolCallStartEventTests { + + @Test + void simpleConstructor_populatesFields() { + ToolCallStartEvent e = new ToolCallStartEvent(REPLY_ID, CALL_ID, TOOL_NAME, TOOL_TITLE); + + assertEquals(REPLY_ID, e.getReplyId()); + assertEquals(CALL_ID, e.getToolCallId()); + assertEquals(TOOL_NAME, e.getToolCallName()); + assertEquals(TOOL_TITLE, e.getToolCallTitle()); + assertEquals(AgentEventType.TOOL_CALL_START, e.getType()); + assertNotNull(e.getId()); + assertNotNull(e.getCreatedAt()); + } + + @Test + void simpleConstructor_acceptsNullTitle() { + ToolCallStartEvent e = new ToolCallStartEvent(REPLY_ID, CALL_ID, TOOL_NAME, null); + assertNull(e.getToolCallTitle()); + } + + @Test + void jacksonConstructor_setsIdAndTimestamp() { + ToolCallStartEvent e = + new ToolCallStartEvent( + EVENT_ID, CREATED_AT, REPLY_ID, CALL_ID, TOOL_NAME, TOOL_TITLE); + + assertEquals(EVENT_ID, e.getId()); + assertEquals(CREATED_AT, e.getCreatedAt()); + assertEquals(REPLY_ID, e.getReplyId()); + assertEquals(CALL_ID, e.getToolCallId()); + assertEquals(TOOL_NAME, e.getToolCallName()); + assertEquals(TOOL_TITLE, e.getToolCallTitle()); + } + + @Test + void jacksonRoundTrip_preservesAllFields() throws Exception { + ToolCallStartEvent original = + new ToolCallStartEvent( + EVENT_ID, CREATED_AT, REPLY_ID, CALL_ID, TOOL_NAME, TOOL_TITLE); + + String json = mapper.writeValueAsString(original); + AgentEvent decoded = mapper.readValue(json, AgentEvent.class); + + assertTrue(decoded instanceof ToolCallStartEvent); + ToolCallStartEvent r = (ToolCallStartEvent) decoded; + assertEquals(original.getId(), r.getId()); + assertEquals(original.getCreatedAt(), r.getCreatedAt()); + assertEquals(original.getReplyId(), r.getReplyId()); + assertEquals(original.getToolCallId(), r.getToolCallId()); + assertEquals(original.getToolCallName(), r.getToolCallName()); + assertEquals(original.getToolCallTitle(), r.getToolCallTitle()); + } + } + + @Nested + class ToolCallEndEventTests { + + @Test + void simpleConstructor_populatesFields() { + ToolCallEndEvent e = new ToolCallEndEvent(REPLY_ID, CALL_ID, TOOL_NAME, TOOL_TITLE); + + assertEquals(REPLY_ID, e.getReplyId()); + assertEquals(CALL_ID, e.getToolCallId()); + assertEquals(TOOL_NAME, e.getToolCallName()); + assertEquals(TOOL_TITLE, e.getToolCallTitle()); + assertEquals(AgentEventType.TOOL_CALL_END, e.getType()); + } + + @Test + void jacksonConstructor_setsIdAndTimestamp() { + ToolCallEndEvent e = + new ToolCallEndEvent(EVENT_ID, CREATED_AT, REPLY_ID, CALL_ID, TOOL_NAME, null); + + assertEquals(EVENT_ID, e.getId()); + assertEquals(CREATED_AT, e.getCreatedAt()); + assertNull(e.getToolCallTitle()); + } + + @Test + void jacksonRoundTrip_preservesAllFields() throws Exception { + ToolCallEndEvent original = + new ToolCallEndEvent( + EVENT_ID, CREATED_AT, REPLY_ID, CALL_ID, TOOL_NAME, TOOL_TITLE); + + String json = mapper.writeValueAsString(original); + ToolCallEndEvent r = (ToolCallEndEvent) mapper.readValue(json, AgentEvent.class); + + assertEquals(original.getReplyId(), r.getReplyId()); + assertEquals(original.getToolCallId(), r.getToolCallId()); + assertEquals(original.getToolCallName(), r.getToolCallName()); + assertEquals(original.getToolCallTitle(), r.getToolCallTitle()); + } + } + + @Nested + class ToolCallDeltaEventTests { + + @Test + void simpleConstructor_populatesFields() { + ToolCallDeltaEvent e = + new ToolCallDeltaEvent(REPLY_ID, CALL_ID, TOOL_NAME, TOOL_TITLE, DELTA); + + assertEquals(REPLY_ID, e.getReplyId()); + assertEquals(CALL_ID, e.getToolCallId()); + assertEquals(TOOL_NAME, e.getToolCallName()); + assertEquals(TOOL_TITLE, e.getToolCallTitle()); + assertEquals(DELTA, e.getDelta()); + assertEquals(AgentEventType.TOOL_CALL_DELTA, e.getType()); + } + + @Test + void jacksonConstructor_setsIdAndTimestamp() { + ToolCallDeltaEvent e = + new ToolCallDeltaEvent( + EVENT_ID, CREATED_AT, REPLY_ID, CALL_ID, TOOL_NAME, null, DELTA); + + assertEquals(EVENT_ID, e.getId()); + assertEquals(CREATED_AT, e.getCreatedAt()); + assertNull(e.getToolCallTitle()); + assertEquals(DELTA, e.getDelta()); + } + + @Test + void jacksonRoundTrip_preservesAllFields() throws Exception { + ToolCallDeltaEvent original = + new ToolCallDeltaEvent( + EVENT_ID, CREATED_AT, REPLY_ID, CALL_ID, TOOL_NAME, TOOL_TITLE, DELTA); + + String json = mapper.writeValueAsString(original); + ToolCallDeltaEvent r = (ToolCallDeltaEvent) mapper.readValue(json, AgentEvent.class); + + assertEquals(original.getReplyId(), r.getReplyId()); + assertEquals(original.getToolCallId(), r.getToolCallId()); + assertEquals(original.getToolCallName(), r.getToolCallName()); + assertEquals(original.getToolCallTitle(), r.getToolCallTitle()); + assertEquals(original.getDelta(), r.getDelta()); + } + } + + @Nested + class ToolResultStartEventTests { + + @Test + void simpleConstructor_populatesFields() { + ToolResultStartEvent e = + new ToolResultStartEvent(REPLY_ID, CALL_ID, TOOL_NAME, TOOL_TITLE); + + assertEquals(REPLY_ID, e.getReplyId()); + assertEquals(CALL_ID, e.getToolCallId()); + assertEquals(TOOL_NAME, e.getToolCallName()); + assertEquals(TOOL_TITLE, e.getToolCallTitle()); + assertEquals(AgentEventType.TOOL_RESULT_START, e.getType()); + } + + @Test + void jacksonConstructor_setsIdAndTimestamp() { + ToolResultStartEvent e = + new ToolResultStartEvent( + EVENT_ID, CREATED_AT, REPLY_ID, CALL_ID, TOOL_NAME, null); + + assertEquals(EVENT_ID, e.getId()); + assertEquals(CREATED_AT, e.getCreatedAt()); + assertNull(e.getToolCallTitle()); + } + + @Test + void jacksonRoundTrip_preservesAllFields() throws Exception { + ToolResultStartEvent original = + new ToolResultStartEvent( + EVENT_ID, CREATED_AT, REPLY_ID, CALL_ID, TOOL_NAME, TOOL_TITLE); + + String json = mapper.writeValueAsString(original); + ToolResultStartEvent r = + (ToolResultStartEvent) mapper.readValue(json, AgentEvent.class); + + assertEquals(original.getReplyId(), r.getReplyId()); + assertEquals(original.getToolCallId(), r.getToolCallId()); + assertEquals(original.getToolCallName(), r.getToolCallName()); + assertEquals(original.getToolCallTitle(), r.getToolCallTitle()); + } + } + + @Nested + class ToolResultEndEventTests { + + @Test + void simpleConstructor_populatesFields() { + ToolResultEndEvent e = + new ToolResultEndEvent( + REPLY_ID, CALL_ID, TOOL_NAME, TOOL_TITLE, ToolResultState.SUCCESS); + + assertEquals(REPLY_ID, e.getReplyId()); + assertEquals(CALL_ID, e.getToolCallId()); + assertEquals(TOOL_NAME, e.getToolCallName()); + assertEquals(TOOL_TITLE, e.getToolCallTitle()); + assertEquals(ToolResultState.SUCCESS, e.getState()); + assertEquals(AgentEventType.TOOL_RESULT_END, e.getType()); + } + + @Test + void simpleConstructor_acceptsAllStateValues() { + for (ToolResultState state : ToolResultState.values()) { + ToolResultEndEvent e = + new ToolResultEndEvent(REPLY_ID, CALL_ID, TOOL_NAME, TOOL_TITLE, state); + assertEquals(state, e.getState()); + } + } + + @Test + void jacksonConstructor_setsIdAndTimestamp() { + ToolResultEndEvent e = + new ToolResultEndEvent( + EVENT_ID, + CREATED_AT, + REPLY_ID, + CALL_ID, + TOOL_NAME, + null, + ToolResultState.ERROR); + + assertEquals(EVENT_ID, e.getId()); + assertEquals(CREATED_AT, e.getCreatedAt()); + assertNull(e.getToolCallTitle()); + assertEquals(ToolResultState.ERROR, e.getState()); + } + + @Test + void jacksonRoundTrip_preservesAllFields() throws Exception { + ToolResultEndEvent original = + new ToolResultEndEvent( + EVENT_ID, + CREATED_AT, + REPLY_ID, + CALL_ID, + TOOL_NAME, + TOOL_TITLE, + ToolResultState.DENIED); + + String json = mapper.writeValueAsString(original); + ToolResultEndEvent r = (ToolResultEndEvent) mapper.readValue(json, AgentEvent.class); + + assertEquals(original.getReplyId(), r.getReplyId()); + assertEquals(original.getToolCallId(), r.getToolCallId()); + assertEquals(original.getToolCallName(), r.getToolCallName()); + assertEquals(original.getToolCallTitle(), r.getToolCallTitle()); + assertEquals(original.getState(), r.getState()); + } + } + + @Nested + class ToolResultTextDeltaEventTests { + + @Test + void simpleConstructor_populatesFields() { + ToolResultTextDeltaEvent e = + new ToolResultTextDeltaEvent(REPLY_ID, CALL_ID, TOOL_NAME, TOOL_TITLE, "hello"); + + assertEquals(REPLY_ID, e.getReplyId()); + assertEquals(CALL_ID, e.getToolCallId()); + assertEquals(TOOL_NAME, e.getToolCallName()); + assertEquals(TOOL_TITLE, e.getToolCallTitle()); + assertEquals("hello", e.getDelta()); + assertEquals(AgentEventType.TOOL_RESULT_TEXT_DELTA, e.getType()); + } + + @Test + void jacksonConstructor_setsIdAndTimestamp() { + ToolResultTextDeltaEvent e = + new ToolResultTextDeltaEvent( + EVENT_ID, CREATED_AT, REPLY_ID, CALL_ID, TOOL_NAME, null, "chunk"); + + assertEquals(EVENT_ID, e.getId()); + assertEquals(CREATED_AT, e.getCreatedAt()); + assertNull(e.getToolCallTitle()); + assertEquals("chunk", e.getDelta()); + } + + @Test + void jacksonRoundTrip_preservesAllFields() throws Exception { + ToolResultTextDeltaEvent original = + new ToolResultTextDeltaEvent( + EVENT_ID, + CREATED_AT, + REPLY_ID, + CALL_ID, + TOOL_NAME, + TOOL_TITLE, + "partial"); + + String json = mapper.writeValueAsString(original); + ToolResultTextDeltaEvent r = + (ToolResultTextDeltaEvent) mapper.readValue(json, AgentEvent.class); + + assertEquals(original.getReplyId(), r.getReplyId()); + assertEquals(original.getToolCallId(), r.getToolCallId()); + assertEquals(original.getToolCallName(), r.getToolCallName()); + assertEquals(original.getToolCallTitle(), r.getToolCallTitle()); + assertEquals(original.getDelta(), r.getDelta()); + } + } + + @Nested + class ToolResultDataDeltaEventTests { + + @Test + void simpleConstructor_populatesFields() { + ContentBlock data = TextBlock.builder().text("payload").build(); + ToolResultDataDeltaEvent e = + new ToolResultDataDeltaEvent(REPLY_ID, CALL_ID, TOOL_NAME, TOOL_TITLE, data); + + assertEquals(REPLY_ID, e.getReplyId()); + assertEquals(CALL_ID, e.getToolCallId()); + assertEquals(TOOL_NAME, e.getToolCallName()); + assertEquals(TOOL_TITLE, e.getToolCallTitle()); + assertEquals(data, e.getData()); + assertEquals(AgentEventType.TOOL_RESULT_DATA_DELTA, e.getType()); + } + + @Test + void jacksonConstructor_setsIdAndTimestamp() { + ContentBlock data = TextBlock.builder().text("d").build(); + ToolResultDataDeltaEvent e = + new ToolResultDataDeltaEvent( + EVENT_ID, CREATED_AT, REPLY_ID, CALL_ID, TOOL_NAME, null, data); + + assertEquals(EVENT_ID, e.getId()); + assertEquals(CREATED_AT, e.getCreatedAt()); + assertNull(e.getToolCallTitle()); + assertEquals(data, e.getData()); + } + + @Test + void jacksonRoundTrip_preservesScalarFields() throws Exception { + ContentBlock data = TextBlock.builder().text("payload").build(); + ToolResultDataDeltaEvent original = + new ToolResultDataDeltaEvent( + EVENT_ID, CREATED_AT, REPLY_ID, CALL_ID, TOOL_NAME, TOOL_TITLE, data); + + String json = mapper.writeValueAsString(original); + ToolResultDataDeltaEvent r = + (ToolResultDataDeltaEvent) mapper.readValue(json, AgentEvent.class); + + assertEquals(original.getId(), r.getId()); + assertEquals(original.getReplyId(), r.getReplyId()); + assertEquals(original.getToolCallId(), r.getToolCallId()); + assertEquals(original.getToolCallName(), r.getToolCallName()); + assertEquals(original.getToolCallTitle(), r.getToolCallTitle()); + assertNotNull(r.getData()); + } + } +} From 79c9e885ae4976b6ba00f570b17c023ba5e85e56 Mon Sep 17 00:00:00 2001 From: keep simple <3132670669@qq.com> Date: Fri, 12 Jun 2026 23:23:02 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E6=8F=90=E9=AB=98=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E8=A6=86=E7=9B=96=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/io/agentscope/core/agent/ReActAgentToolTitleTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentToolTitleTest.java b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentToolTitleTest.java index a0f53cbb78..17d0d0ce58 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentToolTitleTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentToolTitleTest.java @@ -37,13 +37,11 @@ import io.agentscope.core.tool.AgentTool; import io.agentscope.core.tool.ToolCallParam; import io.agentscope.core.tool.Toolkit; - import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; - import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono;