Skip to content

receive_response() streams tool results as UserMessage, causing agent loops #792

@srinathh

Description

@srinathh

Problem

receive_response() streams MCP tool results as UserMessage containing ToolResultBlock, indistinguishable from real user messages without inspecting the content blocks. This makes it easy for the model to misinterpret tool results as new user input, causing infinite agent loops.

Reproduction

When using create_sdk_mcp_server to register a custom MCP tool (e.g. send_message), the receive_response() stream looks like:

AssistantMessage  → [ToolUseBlock(name='mcp__clawless__send_message', ...)]
UserMessage       → [ToolResultBlock(tool_use_id='...', content='Message sent')]
AssistantMessage  → [ToolUseBlock(name='mcp__clawless__send_message', ...)]  # agent loops
UserMessage       → [ToolResultBlock(...)]
... repeats indefinitely

The agent sees the UserMessage (tool result) and interprets it as requiring a response, calling the tool again, creating an infinite loop.

Expected behavior

One of:

  1. Distinct message type: Stream tool results as a ToolResultMessage (or similar) rather than UserMessage, so consumers can differentiate without inspecting content blocks.
  2. Filtered stream: Provide an option to only yield "meaningful" messages (assistant text, result) and handle tool round-trips internally.
  3. Documentation: At minimum, document that UserMessage in the stream may contain ToolResultBlock and is not always a real user message.

Workaround

We worked around this with:

  • System prompt instructions explicitly telling the model that tool results are not user messages
  • Content validation in the tool handler (rejecting trivially short messages)
  • Per-turn rate limiting on tool calls

These are defense-in-depth but the root cause is the ambiguous message typing.

Environment

  • claude-agent-sdk (Python)
  • MCP tools via create_sdk_mcp_server

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions