From 9f966d8e071bb0a3f4374e7da4f4018e1fedfe33 Mon Sep 17 00:00:00 2001 From: Fancy-hjyp <2594297576@qq.com> Date: Sat, 21 Mar 2026 22:02:52 +0800 Subject: [PATCH 1/2] fix(core): handle pending tool calls when maxIters reached (#1005) --- .../java/io/agentscope/core/ReActAgent.java | 31 ++++ .../core/agent/ReActAgentSummarizingTest.java | 156 ++++++++++++++++++ 2 files changed, 187 insertions(+) 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 30d25e545..c9e50392d 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java +++ b/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java @@ -630,6 +630,37 @@ private Mono notifyPostActingHook( protected Mono summarizing() { log.debug("Maximum iterations reached. Generating summary..."); + // Handle pending tool calls that were not completed before max iterations + if (hasPendingToolUse()) { + List pendingTools = extractPendingToolCalls(); + log.warn( + "Max iterations reached with {} pending tool calls. Adding error results.", + pendingTools.size()); + + for (ToolUseBlock toolUse : pendingTools) { + ToolResultBlock errorResult = + ToolResultBlock.builder() + .id(toolUse.getId()) + .output( + TextBlock.builder() + .text( + "Error: Tool execution cancelled because maximum" + + " iterations limit (" + + maxIters + + ") was reached") + .build()) + .build(); + + Msg errorResultMsg = + Msg.builder() + .name(getName()) + .role(MsgRole.ASSISTANT) + .content(errorResult) + .build(); + memory.addMessage(errorResultMsg); + } + } + List messageList = prepareSummaryMessages(); GenerateOptions generateOptions = buildGenerateOptions(); diff --git a/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentSummarizingTest.java b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentSummarizingTest.java index 797c2f524..59e16618e 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentSummarizingTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentSummarizingTest.java @@ -28,6 +28,7 @@ import io.agentscope.core.message.Msg; import io.agentscope.core.message.MsgRole; import io.agentscope.core.message.TextBlock; +import io.agentscope.core.message.ToolResultBlock; import io.agentscope.core.message.ToolUseBlock; import io.agentscope.core.model.ChatResponse; import io.agentscope.core.model.ChatUsage; @@ -397,4 +398,159 @@ void testSummaryAddedToMemory() { assertEquals( MsgRole.ASSISTANT, lastMessage.getRole(), "Summary message should be ASSISTANT"); } + + @Test + @DisplayName("Should handle second call after maxIters with pending tool calls - Issue #1005") + void testSecondCallAfterMaxItersWithPendingToolCalls() { + // This test reproduces the bug reported in Issue #1005: + // 1. User has multi-round conversation with tool call + // 2. Tool doesn't respond (or times out), leaving pending tool calls + // 3. maxIters is reached, session auto-ends + // 4. User sends new message -> Should NOT throw IllegalStateException + + InMemoryMemory memory = new InMemoryMemory(); + final String toolId = "call_638e428da2cf48ceb8b05762"; + + // Mock model that returns a tool call on first call, then summary + final int[] callCount = {0}; + MockModel mockModel = + new MockModel( + messages -> { + int callNum = callCount[0]++; + if (callNum == 0) { + // First call: return tool use block (simulating tool call) + return List.of( + ChatResponse.builder() + .id("msg_0") + .content( + List.of( + ToolUseBlock.builder() + .name("search_tool") + .id(toolId) + .input(Map.of("query", "test")) + .build())) + .usage(new ChatUsage(10, 20, 30)) + .build()); + } else { + // Second call: summarizing (because maxIters=1 reached) + return List.of( + ChatResponse.builder() + .id("msg_summary") + .content( + List.of( + TextBlock.builder() + .text( + "I reached the maximum" + + " iteration limit." + + " Please try again.") + .build())) + .usage(new ChatUsage(10, 20, 30)) + .build()); + } + }); + + MockToolkit mockToolkit = new MockToolkit(); + + // Create agent with maxIters=1 to quickly trigger summarizing + ReActAgent agent = + ReActAgent.builder() + .name("TestAgent") + .sysPrompt("You are a helpful assistant.") + .model(mockModel) + .toolkit(mockToolkit) + .memory(memory) + .maxIters(1) + .build(); + + // First user message - triggers tool call and maxIters summarizing + Msg firstUserMsg = TestUtils.createUserMessage("User", "Please search for something"); + Msg firstResponse = + agent.call(firstUserMsg) + .block(Duration.ofMillis(TestConstants.DEFAULT_TEST_TIMEOUT_MS)); + + // Verify first response + assertNotNull(firstResponse, "First response should not be null"); + assertEquals(MsgRole.ASSISTANT, firstResponse.getRole()); + + // CRITICAL: Verify that the pending tool call has been resolved in memory + // Before the fix, memory would have pending tool calls without results + // After the fix, summarizing() should add error results for pending tools + List memoryMessages = memory.getMessages(); + + // Find if there's a tool result message for the pending tool + boolean hasToolResultForPendingTool = + memoryMessages.stream() + .flatMap(m -> m.getContentBlocks(ToolResultBlock.class).stream()) + .anyMatch(tr -> tr.getId() != null && tr.getId().equals(toolId)); + + assertTrue( + hasToolResultForPendingTool, + "Memory should contain error result for pending tool call after summarizing"); + + // Verify the tool result indicates cancellation due to max iterations + ToolResultBlock toolResult = + memoryMessages.stream() + .flatMap(m -> m.getContentBlocks(ToolResultBlock.class).stream()) + .filter(tr -> tr.getId() != null && tr.getId().equals(toolId)) + .findFirst() + .orElse(null); + + // Tool result should be present (either from toolkit or from summarizing fix) + assertNotNull(toolResult); + + // SECOND CALL - This is the critical test for Issue #1005 + // Before the fix, this would throw: + // IllegalStateException: Cannot add messages without tool results when pending tool calls exist + + // Reset model for second user interaction + final int[] secondCallCount = {0}; + MockModel secondMockModel = + new MockModel( + messages -> { + int callNum = secondCallCount[0]++; + if (callNum == 0) { + return List.of( + ChatResponse.builder() + .id("msg_second_0") + .content( + List.of( + TextBlock.builder() + .text( + "Hello! How can I help" + + " you today?") + .build())) + .usage(new ChatUsage(5, 10, 15)) + .build()); + } + return List.of(); + }); + + ReActAgent secondAgent = + ReActAgent.builder() + .name("TestAgent") + .sysPrompt("You are a helpful assistant.") + .model(secondMockModel) + .toolkit(mockToolkit) + .memory(memory) // Same memory + .maxIters(2) + .build(); + + // Second user message - this would throw IllegalStateException before the fix + Msg secondUserMsg = TestUtils.createUserMessage("User", "Hello again"); + + // This should NOT throw: "Cannot add messages without tool results when pending tool calls exist" + Msg secondResponse = + secondAgent.call(secondUserMsg) + .block(Duration.ofMillis(TestConstants.DEFAULT_TEST_TIMEOUT_MS)); + + // Verify second response succeeded + assertNotNull(secondResponse, "Second response should not be null"); + assertEquals(MsgRole.ASSISTANT, secondResponse.getRole()); + assertTrue( + secondResponse.getFirstContentBlock() instanceof TextBlock, + "Second response should contain TextBlock"); + + TextBlock secondText = (TextBlock) secondResponse.getFirstContentBlock(); + assertEquals("Hello! How can I help you today?", secondText.getText()); + } } From c6723150bed7188b90bb5fe721c5c6b930f1b9eb Mon Sep 17 00:00:00 2001 From: Fancy-hjyp <2594297576@qq.com> Date: Sat, 21 Mar 2026 22:14:02 +0800 Subject: [PATCH 2/2] style: apply spotless formatting --- .../java/io/agentscope/core/ReActAgent.java | 4 +-- .../core/agent/ReActAgentSummarizingTest.java | 28 +++++++++++++------ 2 files changed, 21 insertions(+), 11 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 c9e50392d..86c87f6e7 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java +++ b/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java @@ -644,8 +644,8 @@ protected Mono summarizing() { .output( TextBlock.builder() .text( - "Error: Tool execution cancelled because maximum" - + " iterations limit (" + "Error: Tool execution cancelled because" + + " maximum iterations limit (" + maxIters + ") was reached") .build()) diff --git a/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentSummarizingTest.java b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentSummarizingTest.java index 59e16618e..435a7ab82 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentSummarizingTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/agent/ReActAgentSummarizingTest.java @@ -427,7 +427,10 @@ void testSecondCallAfterMaxItersWithPendingToolCalls() { ToolUseBlock.builder() .name("search_tool") .id(toolId) - .input(Map.of("query", "test")) + .input( + Map.of( + "query", + "test")) .build())) .usage(new ChatUsage(10, 20, 30)) .build()); @@ -440,9 +443,12 @@ void testSecondCallAfterMaxItersWithPendingToolCalls() { List.of( TextBlock.builder() .text( - "I reached the maximum" - + " iteration limit." - + " Please try again.") + "I reached the" + + " maximum" + + " iteration" + + " limit." + + " Please try" + + " again.") .build())) .usage(new ChatUsage(10, 20, 30)) .build()); @@ -500,7 +506,8 @@ void testSecondCallAfterMaxItersWithPendingToolCalls() { // SECOND CALL - This is the critical test for Issue #1005 // Before the fix, this would throw: - // IllegalStateException: Cannot add messages without tool results when pending tool calls exist + // IllegalStateException: Cannot add messages without tool results when pending tool calls + // exist // Reset model for second user interaction final int[] secondCallCount = {0}; @@ -516,8 +523,9 @@ void testSecondCallAfterMaxItersWithPendingToolCalls() { List.of( TextBlock.builder() .text( - "Hello! How can I help" - + " you today?") + "Hello! How can I" + + " help you" + + " today?") .build())) .usage(new ChatUsage(5, 10, 15)) .build()); @@ -538,9 +546,11 @@ void testSecondCallAfterMaxItersWithPendingToolCalls() { // Second user message - this would throw IllegalStateException before the fix Msg secondUserMsg = TestUtils.createUserMessage("User", "Hello again"); - // This should NOT throw: "Cannot add messages without tool results when pending tool calls exist" + // This should NOT throw: "Cannot add messages without tool results when pending tool calls + // exist" Msg secondResponse = - secondAgent.call(secondUserMsg) + secondAgent + .call(secondUserMsg) .block(Duration.ofMillis(TestConstants.DEFAULT_TEST_TIMEOUT_MS)); // Verify second response succeeded