Skip to content

Conversation

@sureshattaluri
Copy link

Summary

Fixed a critical bug in PatchToolCallsMiddleware where RemoveMessage instances were being returned on every request, causing them to be streamed to external clients and triggering message coercion errors in UIs.

Context

The PatchToolCallsMiddleware.before_agent() method was unconditionally returning a state update containing RemoveMessage(id=REMOVE_ALL_MESSAGES) on every agent invocation, even when there were no dangling tool calls to patch.

This caused problems for:

  • UI integrations: UIs like deep-agents-ui couldn't handle type: "remove" messages and threw errors
  • Performance: Unnecessary state updates and message list rebuilding on every request
  • Streaming: RemoveMessage is an internal LangGraph construct that should never be sent to external clients

The issue was particularly visible on the first message of any conversation, where there are definitely no dangling tool calls yet the middleware still returned a RemoveMessage.

Changes

  • Modified PatchToolCallsMiddleware.before_agent() in src/deepagents/middleware/patch_tool_calls.py:

    • Now only returns a state update when there are actually dangling tool calls that need patching
    • Returns None in the common case where no patches are needed
    • Added clear comment explaining the streaming prevention
  • Updated test expectations in tests/test_middleware.py:

    • test_no_missing_tool_calls now expects None when there are no missing tool calls
    • This reflects the correct new behavior where RemoveMessage is only used when needed

Implementation notes

Before:

# Always returned state update, even with no patches
return {"messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES), *patched_messages]}

After:

# Check if patches are needed first
if not patches_to_add:
    return None  # No state update = no RemoveMessage sent

# Only rebuild message list when we have patches
if patches_to_add:
    # ... build patched_messages ...
    return {"messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES), *patched_messages]}

The logic still correctly handles dangling tool calls when they occur, but avoids the overhead and streaming issues when they don't.

Breaking changes

Behavior change: The middleware now returns None instead of a state update when there are no dangling tool calls.

This is not a breaking change for normal usage because:

  • The external behavior is the same (messages are unchanged when there are no dangling tool calls)
  • RemoveMessage was never meant to be visible to external clients
  • This actually fixes broken integrations that were failing due to RemoveMessage streaming

Tests that explicitly checked for the RemoveMessage in all cases have been updated to match the corrected behavior.

Test plan

Unit tests

  • Updated test_no_missing_tool_calls to expect None return value
  • Existing tests for actual dangling tool call scenarios still pass
  • All other PatchToolCallsMiddleware tests remain unchanged

Integration testing

  • Tested with deep-agents-ui and RDS Manifest Generator agent
  • Verified first message no longer causes coercion errors
  • Verified tool calls still work correctly
  • Verified actual dangling tool calls (if they occur) are still patched properly

Expected behavior

  • Common case (no dangling calls): Middleware returns None, no RemoveMessage created or streamed
  • Edge case (dangling calls): Middleware returns state update with RemoveMessage + patches, working as before

Risks

  • Low risk: This change only affects the case where there are no dangling tool calls (the vast majority)
  • Backward compatibility: External behavior is unchanged; only internal state updates differ
  • Rollback: Simple revert if any unforeseen issues arise

Checklist

  • Docs updated (comments in code explain the change)
  • Tests added/updated (test expectations corrected)
  • Backward compatible (fixes broken behavior, doesn't change working functionality)

@sureshattaluri sureshattaluri deleted the fix/middleware-prevent-removemessage-streaming branch October 29, 2025 13:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant