Skip to content

Commit 3ce740c

Browse files
java-lbbLearningGp
andauthored
fix(tool): handle empty tool publisher results (#1498)
## AgentScope-Java Version 1.1.0-SNAPSHOT ## Description This PR fixes an edge case where a tool returns `Mono.empty()`. Previously, when a tool publisher completed without emitting a result, `ToolExecutor.executeAll()` collected fewer `ToolResultBlock`s than tool calls. `ReActAgent.executeToolCalls()` then paired tool calls and results by index, which could throw `IndexOutOfBoundsException`. Changes made: - Convert empty tool publishers into an error `ToolResultBlock`. - Keep one result per tool call so `ReActAgent` can safely pair tool calls and results. - Add a regression test for a tool that returns `Mono.empty()`. Minimal reproduction tool: ```java @tool( name = "empty_result_tool", description = "Simulate a tool that completes without returning a result") public Mono<String> emptyResultTool() { return Mono.empty(); } ``` Test command: ```bash mvn -pl agentscope-core -Dtest=ToolExecutorTest test ``` Local result: ```text Tests run: 9, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS ``` Fixes #1497 ## Checklist Please check the following items before code is ready to be reviewed. - [x] Code has been formatted with `mvn spotless:apply` - [ ] All tests are passing (`mvn test`) - [ ] Javadoc comments are complete and follow project conventions - [ ] Related documentation has been updated (e.g. links, examples, etc.) - [x] Code is ready for review Co-authored-by: LearningGp <learninggp4.74@gmail.com>
1 parent 0738e78 commit 3ce740c

2 files changed

Lines changed: 53 additions & 1 deletion

File tree

agentscope-core/src/main/java/io/agentscope/core/tool/ToolExecutor.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,12 @@ private Mono<ToolResultBlock> executeCore(ToolCallParam param) {
260260
: e.getClass().getSimpleName();
261261
return Mono.just(
262262
ToolResultBlock.error("Tool execution failed: " + errorMsg));
263-
});
263+
})
264+
.switchIfEmpty(
265+
Mono.just(
266+
ToolResultBlock.error(
267+
"Tool execution failed: Tool completed without returning a"
268+
+ " result")));
264269
}
265270

266271
// ==================== Batch Tool Execution ====================

agentscope-core/src/test/java/io/agentscope/core/tool/ToolExecutorTest.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,53 @@ void shouldReturnErrorWhenToolThrows() {
126126
"Error message should be wrapped by executor");
127127
}
128128

129+
@Test
130+
@DisplayName("Should convert empty tool publishers to error responses")
131+
void shouldReturnErrorWhenToolCompletesEmpty() {
132+
toolkit.registerTool(
133+
new AgentTool() {
134+
@Override
135+
public String getName() {
136+
return "empty_tool";
137+
}
138+
139+
@Override
140+
public String getDescription() {
141+
return "Tool that completes without a result";
142+
}
143+
144+
@Override
145+
public Map<String, Object> getParameters() {
146+
return Map.of("type", "object", "properties", Map.of());
147+
}
148+
149+
@Override
150+
public Mono<ToolResultBlock> callAsync(ToolCallParam param) {
151+
return Mono.empty();
152+
}
153+
});
154+
155+
Map<String, Object> emptyInput = Map.of();
156+
ToolUseBlock emptyCall =
157+
ToolUseBlock.builder()
158+
.id("call-empty")
159+
.name("empty_tool")
160+
.input(emptyInput)
161+
.content(JsonUtils.getJsonCodec().toJson(emptyInput))
162+
.build();
163+
164+
List<ToolResultBlock> responses =
165+
toolkit.callTools(List.of(emptyCall), null, null, null).block(TIMEOUT);
166+
167+
assertNotNull(responses, "Executor should return an error response");
168+
assertEquals(1, responses.size(), "Empty completion should still yield one response");
169+
assertEquals("call-empty", responses.get(0).getId(), "Response should keep tool call id");
170+
assertEquals("empty_tool", responses.get(0).getName(), "Response should keep tool name");
171+
assertEquals(
172+
"Error: Tool execution failed: Tool completed without returning a result",
173+
extractFirstText(responses.get(0)));
174+
}
175+
129176
@Test
130177
@DisplayName("Should NOT specially handle InterruptedException in error path")
131178
void testToolErrorWithoutInterruptSpecialCase() {

0 commit comments

Comments
 (0)