-
Notifications
You must be signed in to change notification settings - Fork 385
Description
ReActAgent Tool Execution Error Handling Enhancement
Issue Summary
Problem: ReActAgent throws IllegalStateException when tool calls timeout or fail, causing the agent to crash and leaving pending tool call states that break subsequent requests.
Error Message:
java.lang.IllegalStateException: Cannot add messages without tool results when pending tool calls exist. Pending IDs: [call_xxx]
at io.agentscope.core.ReActAgent.validateAndAddToolResults(ReActAgent.java:335)
at io.agentscope.core.ReActAgent.doCall(ReActAgent.java:256)
Root Cause Analysis
Scenario 1: Tool Execution Timeout/Error
1. Model returns tool_call (ID: call_xxx)
2. Toolkit.callTools() times out or throws exception
3. Exception propagates up → No tool result written to memory
4. Pending tool call state remains in memory
5. Agent crashes with IllegalStateException
Scenario 2: Orphaned Pending State
1. Previous request's tool execution fails mid-way
2. Error occurs before tool result is written to memory
3. Pending state persists in memory
4. New request arrives → doCall() detects pending IDs
5. User message has no tool results → validateAndAddToolResults() throws
Solution
Fix 1: Error Recovery in executeToolCalls()
Add onErrorResume to catch tool execution failures and generate error tool results instead of propagating exceptions.
Location: ReActAgent.java - executeToolCalls() method
private Mono<List<Map.Entry<ToolUseBlock, ToolResultBlock>>> executeToolCalls(
List<ToolUseBlock> toolCalls) {
return toolkit.callTools(toolCalls, toolExecutionConfig, this, toolExecutionContext)
.map(results -> IntStream.range(0, toolCalls.size())
.mapToObj(i -> Map.entry(toolCalls.get(i), results.get(i)))
.toList())
// NEW: Error recovery - generate error results instead of failing
.onErrorResume(error -> {
log.error("Tool execution failed, generating error results for {} tool calls: {}",
toolCalls.size(), error.getMessage());
List<Map.Entry<ToolUseBlock, ToolResultBlock>> errorResults = toolCalls.stream()
.map(toolCall -> {
ToolResultBlock errorResult = ToolResultBlock.builder()
.id(toolCall.getId())
.output(List.of(TextBlock.builder()
.text("[ERROR] Tool execution failed: " + error.getMessage())
.build()))
.build();
return Map.entry(toolCall, errorResult);
})
.toList();
return Mono.just(errorResults);
});
}Fix 2: Auto-Recovery in doCall()
When pending tool calls are detected but user message contains no tool results, automatically generate error results to clear the pending state.
Location: ReActAgent.java - doCall() method
@Override
protected Mono<Msg> doCall(List<Msg> msgs) {
Set<String> pendingIds = getPendingToolUseIds();
if (pendingIds.isEmpty()) {
addToMemory(msgs);
return executeIteration(0);
}
// NEW: Check if user provided tool results
List<ToolResultBlock> providedResults = msgs == null ? List.of() :
msgs.stream()
.flatMap(m -> m.getContentBlocks(ToolResultBlock.class).stream())
.toList();
if (providedResults.isEmpty()) {
// NEW: Auto-generate error results for orphaned pending calls
log.warn("Pending tool calls detected without results, auto-generating error results. Pending IDs: {}", pendingIds);
generateAndAddErrorToolResults(pendingIds);
addToMemory(msgs);
return executeIteration(0);
}
validateAndAddToolResults(msgs, pendingIds);
return hasPendingToolUse() ? acting(0) : executeIteration(0);
}Fix 3: New Helper Method generateAndAddErrorToolResults()
Location: ReActAgent.java - new private method
/**
* Generate error tool results for pending tool calls and add them to memory.
* This is used to recover from situations where tool execution failed without
* properly writing results to memory.
*/
private void generateAndAddErrorToolResults(Set<String> pendingIds) {
Msg lastAssistant = findLastAssistantMsg();
if (lastAssistant == null) {
return;
}
List<ToolUseBlock> pendingToolCalls = lastAssistant.getContentBlocks(ToolUseBlock.class).stream()
.filter(toolUse -> pendingIds.contains(toolUse.getId()))
.toList();
for (ToolUseBlock toolCall : pendingToolCalls) {
ToolResultBlock errorResult = ToolResultBlock.builder()
.id(toolCall.getId())
.output(List.of(TextBlock.builder()
.text("[ERROR] Previous tool execution failed or was interrupted. Tool: " + toolCall.getName())
.build()))
.build();
Msg toolResultMsg = ToolResultMessageBuilder.buildToolResultMsg(errorResult, toolCall, getName());
memory.addMessage(toolResultMsg);
log.info("Auto-generated error result for pending tool call: {} ({})", toolCall.getName(), toolCall.getId());
}
}Behavior Changes
| Scenario | Before | After |
|---|---|---|
| Tool execution timeout | IllegalStateException thrown, agent crashes |
Error result generated, model receives error feedback, continues processing |
| Tool execution error | Exception propagates, pending state orphaned | Error result generated, agent continues normally |
| Orphaned pending state from previous request | New request fails with IllegalStateException |
Auto-generates error results, clears pending state, processes normally |
Verification
Log Indicators
After the fix, you should see these log messages instead of exceptions:
-
Tool execution failure recovery:
ERROR - Tool execution failed, generating error results for X tool calls: <error message> -
Orphaned pending state recovery:
WARN - Pending tool calls detected without results, auto-generating error results. Pending IDs: [...] INFO - Auto-generated error result for pending tool call: <tool_name> (<tool_id>)
Test Scenarios
-
Timeout Test: Configure a short
TOOL_TIMEOUTand invoke a long-running tool- Expected: Agent returns error message instead of crashing
-
Recovery Test: Manually interrupt a tool execution mid-way, then send a new message
- Expected: Agent auto-recovers and processes the new message
Files Changed
src/main/java/io/agentscope/core/ReActAgent.java- Modified:
executeToolCalls()- addedonErrorResumeerror handling - Modified:
doCall()- added pending state detection and auto-recovery - Added:
generateAndAddErrorToolResults()- new helper method
- Modified:
Compatibility
- Backward Compatible: Yes
- API Changes: None
- Breaking Changes: None
The fix only adds error recovery logic without changing the normal execution path.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status