Skip to content

Python: [Bug]: Anthropic response stream is not closed on early tool-call validation failure #9480

Closed

Description

@coding-totoro

Observed Behavior

When the Anthropic client rejects an invalid streamed local tool call, it raises ResponseInvalidatedException without closing the underlying HTTP response body.

The tool is correctly not executed. The defect reported here is response cleanup on early validation failure.

Expected Behavior

Close the response stream when validation exits early, while preserving the exception and preventing tool execution.

Steps to Reproduce

Install:

pip install agent-framework-core==1.22.0 agent-framework-anthropic==1.0.0b261015 anthropic==1.11.0 httpx2==2.13.1

Run the standalone script below. It uses stock MAF and an instrumented mock response body; no credentials, network, application code, or custom middleware are needed.

Minimal Reproduction

import asyncio
import json

import httpx2 as httpx
from anthropic import AsyncAnthropic
from agent_framework import tool
from agent_framework.anthropic import AnthropicClient
from agent_framework.exceptions import ResponseInvalidatedException


async def main():
    closed, executed = [], []

    @tool
    def lookup() -> str:
        """Return a test value."""
        executed.append(True)
        return "ok"

    # Deliberately malformed: a local tool call ends with end_turn, not tool_use.
    events = [
        {"type": "message_start", "message": {
            "id": "msg_test", "type": "message", "role": "assistant",
            "model": "claude-sonnet-4-6", "content": [], "stop_reason": None,
            "stop_sequence": None, "usage": {"input_tokens": 1, "output_tokens": 1},
        }},
        {"type": "content_block_start", "index": 0, "content_block": {
            "type": "tool_use", "id": "call_test", "name": "lookup", "input": {},
        }},
        {"type": "content_block_stop", "index": 0},
        {"type": "message_delta", "delta": {
            "stop_reason": "end_turn", "stop_sequence": None,
        }, "usage": {"output_tokens": 2}},
        {"type": "message_stop"},
    ]

    class Body(httpx.AsyncByteStream):
        async def __aiter__(self):
            for event in events:
                yield f"event: {event['type']}\ndata: {json.dumps(event)}\n\n".encode()

        async def aclose(self):
            closed.append(True)

    def transport(request):
        return httpx.Response(
            200, headers={"content-type": "text/event-stream"}, stream=Body(),
        )

    async with httpx.AsyncClient(transport=httpx.MockTransport(transport)) as http:
        async with AsyncAnthropic(api_key="unused-offline-test", http_client=http) as raw:
            client = AnthropicClient(anthropic_client=raw, model="claude-sonnet-4-6")
            try:
                await client.get_response(
                    "Use lookup", stream=True,
                    options={"tools": [lookup], "max_tokens": 512},
                ).get_final_response()
            except ResponseInvalidatedException:
                print("ResponseInvalidatedException raised")
            print(f"tool_executed={bool(executed)}, body_closed={bool(closed)}")
    print(f"body_closed_after_client_exit={bool(closed)}")


asyncio.run(main())

Actual Output

ResponseInvalidatedException raised
tool_executed=False, body_closed=False
body_closed_after_client_exit=False

Expected: the exception still occurs, the tool is not executed, and the body is closed.

Environment

  • agent-framework-core==1.22.0
  • agent-framework-anthropic==1.0.0b261015
  • anthropic==1.11.0
  • httpx2==2.13.1
  • Python 3.14.7, macOS
  • Regression: unknown

Additional Context

The streaming branch of _inner_get_response iterates the stream returned by beta.messages.create(..., stream=True) and can raise during validation without a context manager or a finally block closing that stream (source).

Also reproduced using stock AnthropicFoundryClient. Similar early rejection occurs with an unclosed local tool block.

Scope: this is a deterministic cleanup reproduction using deliberately malformed provider events. It does not establish how often such events occur with the real service, a production socket leak, or worker crashes. Normal successful responses are not the case reported here.

Activity

  1. added
    pythonUsage: [Issues, PRs], Target: Python
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 18, 2026
  2. added
    reproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
    on Oct 18, 2026
  3. github-actions commented on Oct 18, 2026

    @github-actions
    Contributor

    🤖 Automated triage reproduction notes (agent-authored — trust but verify)

    Agent analysis

    Repro: python/packages/anthropic/agent_framework_anthropic/_chat_client.py::RawAnthropicClient._inner_get_response around lines 617-672 leaves the provider response body open when a streamed local tool call terminates with an invalid stop reason. The trigger is a closed tool_use block followed by end_turn and message_stop. Run test_streaming_local_tool_call_invalidation_closes_response_body with its instrumented httpx2.AsyncByteStream; the exception is preserved and the tool is not executed, but closed remains empty.

    • Failing test: python/packages/anthropic/tests/test_anthropic_client.py::test_streaming_local_tool_call_invalidation_closes_response_body
    • Files examined: python/packages/anthropic/agent_framework_anthropic/_chat_client.py, python/packages/anthropic/agent_framework_anthropic/_foundry_client.py, python/packages/anthropic/agent_framework_anthropic/_bedrock_client.py, python/packages/anthropic/agent_framework_anthropic/_vertex_client.py, python/packages/anthropic/tests/test_anthropic_client.py, python/packages/anthropic/pyproject.toml
    • Tests run: test_streaming_local_tool_call_invalid_terminal_sequences_raise, test_streaming_local_tool_call_invalidation_closes_response_body
    • Reported version: 1.0.0b261015
    • Current version: 1.0.0b261015
  4. added
    agentsUsage: [Issues, PRs], Target: Single agent
    and removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 18, 2026
  5. Epsilon006 commented on Oct 18, 2026

    @Epsilon006

    Hi, I'd be happy to work on this issue.

    I noticed that the automated triage has reproduced the stream cleanup failure and that the issue is assigned to Eduard van Valkenburg (@eavanvalkenburg) .

    Is this open for external contribution, or is a fix already in progress?

    If it's available, I can add a focused offline regression test and implement a minimal fix to ensure the response stream is properly closed on validation failure, while preserving exception propagation and preventing tool execution.

    Happy to coordinate to avoid duplicating work. Thanks!

  6. eavanvalkenburg commented on Oct 26, 2026

    @eavanvalkenburg
    Member

    /plan Correct per-request Anthropic response-stream ownership on early validation failure. Current main directly iterates beta.messages.create(stream=True) and can raise ResponseInvalidatedException without deterministic response closure. Use the supported SDK stream/context lifecycle, preserving the original invalidation or caller cancellation and zero local tool execution; define cleanup-error precedence rather than masking the primary failure. Close the individual response, not an injected caller-owned SDK/HTTP client. Cover normal completion, invalid terminal tool sequences, provider errors, cancellation and explicit early consumer closure with an instrumented offline response body; verify inherited Foundry/Bedrock/Vertex paths as applicable. Distinguish provider HTTP-body ownership from the related AgentLoop inner-stream work in #9331 and reuse any applicable existing lifecycle helper rather than starting overlapping work. Follow spec 004 for affected history/streaming/provider validation. The report demonstrates deterministic mock-body nonclosure, not a proven production socket leak or service frequency. Coordinate with volunteer Alpinist (@Epsilon006) after core/provider owners agree on scope; no ready label or new assignment while planning is pending.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

agentsUsage: [Issues, PRs], Target: Single agentpythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions