Sagents.Middleware.PatchToolCalls (Sagents v0.8.0-rc.2)

Copy Markdown

Middleware 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:

  1. User Interruption: User sends a new message before tool execution completes
  2. Agent Resets: Agent state is restored with incomplete tool calls
  3. Error Handling: Tool execution fails without generating a tool result
  4. State Corruption: Incomplete state updates or message history corruption

Solution

This middleware runs in the before_model phase and:

  1. Scans message history for assistant messages with tool calls
  2. For each tool call, searches forward for a corresponding tool result message
  3. Creates synthetic tool result messages for any dangling tool calls
  4. 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

patch_dangling_tool_calls(messages)

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.