diff --git a/agentscope-core/src/main/java/io/agentscope/core/tool/ToolMethodInvoker.java b/agentscope-core/src/main/java/io/agentscope/core/tool/ToolMethodInvoker.java index c0776541f..59a8ae737 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/tool/ToolMethodInvoker.java +++ b/agentscope-core/src/main/java/io/agentscope/core/tool/ToolMethodInvoker.java @@ -336,11 +336,25 @@ private Object convertFromString(String stringValue, Class targetType) { /** * Handle invocation errors with informative messages. * + *

Special handling for {@link ToolSuspendException}: if found in the exception chain, + * it will be re-thrown to allow proper suspension handling by {@link ToolExecutor}. + * * @param e the exception * @return ToolResultBlock with error message + * @throws ToolSuspendException if found in the exception chain */ private ToolResultBlock handleInvocationError(Exception e) { + // Check if the exception itself is ToolSuspendException + if (e instanceof ToolSuspendException) { + throw (ToolSuspendException) e; + } + Throwable cause = e.getCause(); + // Check for ToolSuspendException in the exception chain + if (cause instanceof ToolSuspendException) { + throw (ToolSuspendException) cause; + } + String errorMsg = cause != null ? ExceptionUtils.getErrorMessage(cause) diff --git a/agentscope-core/src/test/java/io/agentscope/core/tool/ToolMethodInvokerTest.java b/agentscope-core/src/test/java/io/agentscope/core/tool/ToolMethodInvokerTest.java index 73b5a2f4f..fcdd9838d 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/tool/ToolMethodInvokerTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/tool/ToolMethodInvokerTest.java @@ -15,6 +15,8 @@ */ package io.agentscope.core.tool; +import static org.junit.Assert.assertThrows; + import io.agentscope.core.message.ToolResultBlock; import io.agentscope.core.message.ToolUseBlock; import io.agentscope.core.tool.test.ToolTestUtils; @@ -121,6 +123,24 @@ public int parsableIntString( @ToolParam(name = "value", description = "value") String value) { return Integer.parseInt(value); } + + public String suspendTool( + @ToolParam(name = "reason", description = "reason") String reason) { + throw new ToolSuspendException(reason); + } + + public java.util.concurrent.CompletableFuture suspendToolAsync( + @ToolParam(name = "reason", description = "reason") String reason) { + return java.util.concurrent.CompletableFuture.supplyAsync( + () -> { + throw new ToolSuspendException(reason); + }); + } + + public reactor.core.publisher.Mono suspendToolMono( + @ToolParam(name = "reason", description = "reason") String reason) { + return reactor.core.publisher.Mono.error(new ToolSuspendException(reason)); + } } @Test @@ -493,6 +513,54 @@ void testConvertFromString_LargeNumbers() throws Exception { String.valueOf(Double.MAX_VALUE), ToolTestUtils.extractContent(response2)); } + @Test + void testToolSuspendException_SyncMethod() throws Exception { + TestTools tools = new TestTools(); + Method method = TestTools.class.getMethod("suspendTool", String.class); + + Map input = new HashMap<>(); + input.put("reason", "Waiting for external API"); + + assertThrows( + ToolSuspendException.class, + () -> { + invokeWithParam(tools, method, input); + }); + } + + @Test + void testToolSuspendException_CompletableFuture() throws Exception { + TestTools tools = new TestTools(); + Method method = TestTools.class.getMethod("suspendToolAsync", String.class); + + Map input = new HashMap<>(); + input.put("reason", "Async suspension required"); + + assertThrows( + ToolSuspendException.class, + () -> { + invokeWithParam(tools, method, input); + }); + } + + @Test + void testToolSuspendException_Mono() throws Exception { + TestTools tools = new TestTools(); + Method method = TestTools.class.getMethod("suspendToolMono", String.class); + + Map input = new HashMap<>(); + input.put("reason", "Reactive suspension needed"); + + try { + ToolResultBlock response = invokeWithParam(tools, method, input); + Assertions.fail("Should throw ToolSuspendException"); + } catch (ToolSuspendException e) { + Assertions.assertEquals("Reactive suspension needed", e.getReason()); + } catch (Exception e) { + Assertions.fail("Unexpected exception: " + e.getMessage()); + } + } + @Test void testConvertFromString_NegativeNumbers() throws Exception { TestTools tools = new TestTools();