Skip to content

Python: [Bug]: Inconsistent session states from MiddlewareTermination with store=False vs store=True #4609

@TaoChenOSU

Description

@TaoChenOSU

Description

When we have a function middleware that terminates a function, it leaves the session in different states depending on the store parameter.

When store=False, the function call result returned by the middleware will be appended to the session (a local history provider).

When store=True, the function call result returned by the middleware will NOT be appended to the remote session.

Code Sample

When store=False and providing the last message in the response (which contains the tool call result) from the first agent run will result in duplicated tool result:
Image

# Copyright (c) Microsoft. All rights reserved.

"""Test script demonstrating middleware interception and manual function result injection.

This script demonstrates:
1. Using AzureOpenAIResponsesClient with an Agent
2. Creating a middleware that intercepts tool calls
3. Manually providing function results back to the agent by calling run again
"""

import asyncio
import os
from collections.abc import Awaitable, Callable

from agent_framework import Agent, tool
from agent_framework._middleware import FunctionInvocationContext, FunctionMiddleware, MiddlewareTermination
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv

load_dotenv()


class InterceptingMiddleware(FunctionMiddleware):
    """Middleware that intercepts specific tool calls and raises MiddlewareTermination.

    When a matching tool is called, this middleware short-circuits execution
    and signals that external handling is required.
    """

    def __init__(self, tool_names_to_intercept: list[str]) -> None:
        """Initialize with a list of tool names to intercept."""
        self._tool_names = set(tool_names_to_intercept)
        self.intercepted_call: FunctionInvocationContext | None = None

    async def process(
        self,
        context: FunctionInvocationContext,
        call_next: Callable[[], Awaitable[None]],
    ) -> None:
        """Intercept matching tool calls and raise MiddlewareTermination."""
        if context.function.name not in self._tool_names:
            # Not a tool we care about, let it proceed normally
            await call_next()
            return

        print(f"[Middleware] Intercepted tool call: {context.function.name}")
        print(f"[Middleware] Arguments: {context.arguments}")

        # Store the intercepted call for external handling
        self.intercepted_call = context

        # Raise MiddlewareTermination to signal that we've handled this
        # The result will be provided externally
        raise MiddlewareTermination(result="Something were wrong")


@tool(name="get_order_status", approval_mode="never_require")
def get_order_status(order_id: str) -> str:
    """Get the status of an order by its ID.

    Args:
        order_id: The ID of the order to check.

    Returns:
        The status of the order.
    """
    # This function body won't be executed because the middleware intercepts it
    return f"Order {order_id} status: Delivered"


async def main() -> None:
    """Run the test script."""
    # Create the client
    client = AzureOpenAIResponsesClient(
        project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
        deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
        credential=AzureCliCredential(),
    )

    # Create the intercepting middleware
    middleware = InterceptingMiddleware(tool_names_to_intercept=["get_order_status"])

    # Create an agent with the tool and middleware
    agent = Agent(
        client=client,
        name="OrderAgent",
        instructions=(
            "You are a helpful assistant that can check order statuses. "
            "When asked about an order, use the get_order_status tool."
        ),
        tools=[get_order_status],
        middleware=[middleware],
        default_options={"store": False},  # Use local history management
    )

    # Create a session to maintain conversation state
    session = agent.create_session()

    print("=" * 60)
    print("Step 1: Initial user request")
    print("=" * 60)

    # First call - the agent should invoke the tool, which gets intercepted
    user_message = "What's the status of order #12345?"
    print(f"User: {user_message}")

    response = await agent.run(user_message, session=session)

    print(f"\nAgent response (after interception): {response.text}")
    print(f"Response messages: {len(response.messages)}")

    # Check if we have an intercepted call
    if middleware.intercepted_call:
        print("\n" + "=" * 60)
        print("Step 2: Middleware intercepted the tool call")
        print("=" * 60)

        intercepted = middleware.intercepted_call
        print(f"Intercepted function: {intercepted.function.name}")
        print(f"Arguments: {intercepted.arguments}")

        # The last message from the first response
        final_response = await agent.run([response.messages[-1]], session=session, options={"tool_choice": "none"})

        print(f"Final agent response: {final_response.text}")

    else:
        print("No tool call was intercepted - the agent may have responded directly")


if __name__ == "__main__":
    asyncio.run(main())

However, if we don't provide the last message from first response and have store=True, the service will raise an error.

        final_response = await agent.run([], session=session, options={"tool_choice": "none"})

Error Messages / Stack Traces

agent_framework.exceptions.ChatClientInvalidRequestException: Messages are required for chat completions

Package Versions

agent-framework

Python Version

No response

Additional Context

Users should expect the session to be in a consistent state after middleware termination regardless of store.

Metadata

Metadata

Labels

bugSomething isn't workingpython

Type

Projects

Status

No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions