Sagents.Middleware.PatchToolCalls (Sagents v0.8.0-rc.7)
Copy MarkdownMiddleware that identifies and resolves "dangling tool calls" in the message history.
A "dangling tool call" occurs when an AI message contains tool calls that lack corresponding tool result messages in the conversation history. This creates an incomplete request-response cycle that can confuse the agent and cause LLM API errors.
Problem Scenarios
Dangling tool calls commonly occur due to:
- User Interruption: User sends a new message before tool execution completes
- Agent Resets: Agent state is restored with incomplete tool calls
- Error Handling: Tool execution fails without generating a tool result
- State Corruption: Incomplete state updates or message history corruption
Solution
This middleware runs in the before_model phase and:
- Scans message history for assistant messages with tool calls
- For each tool call, searches forward for a corresponding tool result message
- Creates synthetic tool result messages for any dangling tool calls
- Returns updated state with patched message history
Position in Middleware Stack
This middleware should run relatively late in the before_model phase, after middleware that might generate or modify messages but before any middleware that expects complete tool call sequences (like HumanInTheLoop).
Usage
# Add to agent with default middleware
{:ok, agent} = Agent.new(
model: model,
middleware: [PatchToolCalls]
)
# Or with custom middleware stack
{:ok, agent} = Agent.new(
model: model,
replace_default_middleware: true,
middleware: [
TodoList,
Filesystem,
PatchToolCalls, # Position before HITL
HumanInTheLoop,
MyMiddleware
]
)Example
# Before patching:
messages = [
%Message{role: :system, content: "You are helpful"},
%Message{role: :assistant, tool_calls: [
%ToolCall{call_id: "123", name: "search", arguments: %{q: "test"}}
]},
%Message{role: :user, content: "Never mind"} # User interrupted!
]
# After patching:
messages = [
%Message{role: :system, content: "You are helpful"},
%Message{role: :assistant, tool_calls: [
%ToolCall{call_id: "123", name: "search", arguments: %{q: "test"}}
]},
%Message{role: :tool, tool_results: [
%ToolResult{
tool_call_id: "123",
name: "search",
content: "Tool call search with id 123 was cancelled - another message came in before it could be completed."
}
]},
%Message{role: :user, content: "Never mind"}
]
Summary
Functions
Scan messages for dangling tool calls and create synthetic tool results.
Functions
Scan messages for dangling tool calls and create synthetic tool results.
A tool call is "dangling" if there is no corresponding tool result message with a matching tool_call_id in any subsequent message.
Returns the patched message list. If no patches are needed, returns the original list unchanged.