Skip to lesson

MicrosoftMicrosoft Certified: Multi-Agent AI Solutions Expert2026-102.4 Implement multi-agent orchestration

Enforce shared policy at middleware layers

Completed by the knowledge check

Learning objective

Design reusable middleware for logging, authorization, validation, and exception handling.

Core idea

Middleware wraps execution with reusable processing. Choose its scope: an agent run, model request, or function invocation.

Microsoft Agent Framework exposes separate middleware layers for those operations. Put request-level admission around the agent, model-request telemetry around chat calls, and tool-specific authorization around function calls. A run-level check cannot authorize a resource selected later.

Define trusted identity, allowed operations, validation, and failure outcomes in middleware context.

How it works

Select the layer by execution scope

The Python API provides AgentMiddleware with AgentContext, ChatMiddleware with ChatContext, and FunctionMiddleware with FunctionInvocationContext. You can implement a class’s process() method, define a function with the appropriate context type, or use the explicit @agent_middleware, @chat_middleware, and @function_middleware decorators.

Each Python middleware receives a zero-argument call_next callback. Awaiting call_next() continues to the next middleware or the wrapped operation. Processing before the call handles admission or input transformation; processing afterward handles results. Design the ordinary wrapper path to call it once so downstream execution is not repeated.

AgentContext includes the agent, messages, session, options, streaming flag, metadata, and result. It also separates client-specific runtime values in client_kwargs from values forwarded to tools in function_invocation_kwargs.

ChatContext represents a request to the model. Chat middleware executes inside the function invocation loop, including model calls that send tool results back for further reasoning. Count those as separate model requests rather than treating one agent run as one inference call.

FunctionInvocationContext identifies the selected function through function, exposes validated arguments, and carries runtime kwargs, session, metadata, and result. This is the place to inspect the actual tool and its resource parameters before execution.

Make logging reusable without copying sensitive inputs

At the agent layer, log the request’s correlation ID and terminal outcome. At the chat layer, record each model-call duration. At the function layer, record context.function.name, operation duration, and accepted, denied, or failed outcome. These are application log fields backed by framework context.

Use metadata to share correlation and diagnostic values across middleware at the relevant invocation scope. Keep per-request state in context rather than mutable fields on a reused middleware object. Otherwise concurrent runs can contaminate each other’s identity or timing evidence.

Place outcome recording in a finally path so exceptions and early termination still leave evidence. Record selected safe fields rather than serializing complete messages, arguments, or function results by default. Apply one logging contract across agents.

Authorize the operation the tool will perform

Agent middleware can reject a request before reasoning begins. Function middleware should also validate whether the authenticated caller may invoke the selected function against the requested resource. The tool name comes from context.function.name; resource identifiers come from the validated context.arguments.

Identity is a trusted runtime input, not a value the model is allowed to invent in tool arguments. Pass trusted values through runtime context, such as function_invocation_kwargs, and inspect the resulting context.kwargs at the function layer. Validate that those runtime values came from your authenticated service boundary.

The documentation demonstrates enriching context.kwargs before call_next(). For authorization, define collision behavior explicitly: a model-supplied tenant argument must not override the tenant established by authentication. Compare or reject mismatched values before execution, and preserve the trusted runtime value.

Middleware is one application enforcement point. The target service must still enforce its own access controls. See 1.2.4 for compliance controls and 2.4.10 for independently deployed agents; do not treat an in-process function wrapper as authentication for a remote service.

Validate arguments and results at different points

The function middleware context exposes validated arguments, but structural validation is not the same as business acceptance. A validly typed account ID can identify an account the caller cannot access. A positive number can exceed the allowed adjustment amount.

Before call_next(), check application constraints that depend on trusted context and operation data. After call_next(), inspect context.result for the result contract before forwarding it as accepted evidence. Keep authorization, input validation, and result validation distinct so their outcomes can be diagnosed.

For an explicit early exit, the Python documentation provides MiddlewareTermination. Set context.result when a custom response is needed, then raise MiddlewareTermination(result=context.result). This stops the remaining middleware chain and skips the normal execution path. The pattern applies to agent, function, and chat middleware; choose a result appropriate to that layer.

Do not replace a denied function invocation with a fabricated successful result. Record a denied or invalid outcome in the application contract, so downstream reasoning cannot mistake the absence of execution for completed work.

Preserve exception and streaming semantics

Function middleware normally converts an ordinary exception into a tool-error result and lets the agent loop continue. The Python significant-changes guide separately documents MiddlewareFailure: import it from agent_framework and raise it from the underlying exception when function middleware must abort instead. The runtime propagates that failure to the caller. Do not catch MiddlewareFailure in middleware, because doing so permits execution to continue. A function call leaves function middleware as a denied outcome, a checked result, a tool-error result, or a failure that reaches the caller.

A function call leaves function middleware as a denied outcome, a checked result, a tool-error result, or a failure that reaches the callerFunction middleware checks the call before call_next. A denied call records a denied outcome through MiddlewareTermination and skips the normal execution path. Otherwise call_next invokes the selected function, and context.result is inspected before it is forwarded. An ordinary exception becomes a tool-error result and the agent loop continues; when middleware must abort, MiddlewareFailure propagates to the caller.deniedcall_next()ordinary exceptionmust abortCheck beforecall_next()Denied outcome viaMiddlewareTerminationInvoke the selectedfunctionInspectcontext.resultTool-error result,loop continuesMiddlewareFailurepropagates to thecaller

Concurrent sibling tool calls are cancelled cooperatively: a synchronous tool already running in a worker thread can finish its side effects, although its result is discarded. Agent and chat middleware propagate ordinary exceptions; do not assume identical exception semantics across layers.

Treat MiddlewareTermination as deliberate control flow. Broad exception handlers must preserve it rather than converting a policy rejection into a retryable model error. See 2.4.9 for execution budgets; a reusable wrapper should not introduce an unbounded retry loop.

Streaming requires separate care. If content has already reached the caller, post-execution validation cannot retract it. Decide whether a particular policy must act before release or whether the operation returns a complete validated object. Agent Hooks is experimental in Python and unavailable in .NET. Its documentation names Buffered streaming and Verdict-gated persistence as enforcement guarantees that prevent release or storage before the applicable verdict. Do not promise those gates from an ordinary after-call logger.

Register mandatory and optional concerns separately

Configure middleware=[...] at agent construction for policy that applies to every run. Supply run-level middleware=[...] on agent.run() for request-specific additions. The documented order places agent-level middleware outside run-level middleware, followed by agent execution.

Keep mandatory authorization at the persistent agent or function configuration rather than depending on each caller to remember a run-level addition. Use run-level middleware for optional diagnostics or narrowly scoped request behavior.

The .NET interface is different. Function-calling middleware is supported for an AIAgent that uses FunctionInvokingChatClient, such as ChatClientAgent. Its FunctionInvocationContext.Terminate controls the function-call loop and can leave incomplete tool-call history. It is not the Python MiddlewareTermination interface; do not interchange them.

Key distinctions and trade-offs

Run versus model call versus tool call: Each layer has its own denominator for timing, failures, and policy. One run can contain several model and tool calls.

Structural versus business validation: Typed arguments establish shape. Authorization and business constraints establish whether the requested operation is permitted.

Reusable configuration versus shared mutable state: Reuse middleware behavior across agents, but isolate request identity and outcomes in invocation context.

Before execution versus after output release: Pre-call enforcement can prevent an operation. Post-call inspection can classify its result, but cannot undo an external side effect or retract streamed content.

Architecture / decision scenario

A finance agent can retrieve invoices and request adjustments. Register agent-level middleware that records the authenticated request and rejects unsupported callers. Register chat middleware that times each inference request, including reasoning after tool results.

Function middleware checks context.function.name, the invoice ID and amount in context.arguments, and the trusted tenant in context.kwargs. A disallowed adjustment produces an explicit denied result through MiddlewareTermination; it never invokes the tool. An allowed call invokes the tool once, then validates the returned invoice identity and status.

An outer logging wrapper records denial and exceptions as well as success. A recoverable service error becomes a tool-error result. If enforcement cannot safely continue, function middleware raises MiddlewareFailure so the run aborts; it does not fabricate a successful adjustment. Optional run-level diagnostics add visibility without supplying mandatory authorization. This design covers all four middleware concerns while keeping their execution scopes distinct.

Common exam traps

  • Logging one agent run as one model call.
  • Authorizing a tenant value selected by the model.
  • Treating validated argument types as complete business validation.
  • Invoking call_next() more than once unintentionally.
  • Swallowing MiddlewareTermination or MiddlewareFailure in a generic exception handler.
  • Expecting an ordinary exception in function middleware to abort the run.
  • Depending on optional run-level middleware for mandatory enforcement.
  • Assuming after-call validation retracts streaming output.
  • Applying Python termination semantics to .NET’s function loop.

What to remember

Choose the middleware layer that owns the operation. Carry trusted identity through runtime context, validate before and after execution where appropriate, and record every terminal outcome. Reuse behavior while preserving request isolation, explicit denial, exception propagation, and streaming semantics.

Knowledge check

References