Skip to content

Commit 0bacfdc

Browse files
updated latest and middleware changes (#921)
1 parent 7a5bd5a commit 0bacfdc

7 files changed

Lines changed: 440 additions & 2056 deletions

File tree

agent-framework/agents/middleware/chat-middleware.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ zone_pivot_groups: programming-languages
55
author: eavanvalkenburg
66
ms.topic: reference
77
ms.author: edvan
8-
ms.date: 02/09/2026
8+
ms.date: 03/16/2026
99
ms.service: agent-framework
1010
---
1111

@@ -199,7 +199,7 @@ async def security_and_override_middleware(
199199
]
200200
)
201201

202-
# Set terminate flag to stop execution
202+
# Raise MiddlewareTermination to stop execution after setting context.result
203203
raise MiddlewareTermination
204204

205205
# Continue to next middleware or AI execution
@@ -451,7 +451,7 @@ async def security_and_override_middleware(
451451
]
452452
)
453453

454-
# Set terminate flag to stop execution
454+
# Raise MiddlewareTermination to stop execution after setting context.result
455455
raise MiddlewareTermination
456456

457457
# Continue to next middleware or AI execution

agent-framework/agents/middleware/defining-middleware.md

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ zone_pivot_groups: programming-languages
55
author: dmytrostruk
66
ms.topic: tutorial
77
ms.author: dmytrostruk
8-
ms.date: 09/29/2025
8+
ms.date: 03/16/2026
99
ms.service: agent-framework
1010
---
1111

@@ -233,17 +233,19 @@ if __name__ == "__main__":
233233
Create a simple logging middleware to see when your agent runs:
234234

235235
```python
236+
from collections.abc import Awaitable, Callable
237+
236238
from agent_framework import AgentContext
237239

238240
async def logging_agent_middleware(
239241
context: AgentContext,
240-
next: Callable[[AgentContext], Awaitable[None]],
242+
call_next: Callable[[], Awaitable[None]],
241243
) -> None:
242244
"""Simple middleware that logs agent execution."""
243245
print("Agent starting...")
244246

245247
# Continue to agent execution
246-
await next(context)
248+
await call_next()
247249

248250
print("Agent finished!")
249251
```
@@ -267,33 +269,34 @@ async def main():
267269

268270
## Step 4: Create Function Middleware
269271

270-
If your agent uses functions, you can intercept function calls:
272+
If your agent uses functions, you can intercept function calls and set tool-only runtime values before the tool executes:
271273

272274
```python
275+
from collections.abc import Awaitable, Callable
276+
273277
from agent_framework import FunctionInvocationContext
274278

275-
def get_time():
279+
def get_time(ctx: FunctionInvocationContext) -> str:
276280
"""Get the current time."""
277281
from datetime import datetime
278-
return datetime.now().strftime("%H:%M:%S")
282+
source = ctx.kwargs.get("request_source", "direct")
283+
return f"[{source}] {datetime.now().strftime('%H:%M:%S')}"
279284

280-
async def logging_function_middleware(
285+
async def inject_function_kwargs(
281286
context: FunctionInvocationContext,
282-
next: Callable[[FunctionInvocationContext], Awaitable[None]],
287+
call_next: Callable[[], Awaitable[None]],
283288
) -> None:
284-
"""Middleware that logs function calls."""
285-
print(f"Calling function: {context.function.name}")
286-
287-
await next(context)
289+
"""Middleware that adds tool-only runtime values before execution."""
290+
context.kwargs.setdefault("request_source", "middleware")
288291

289-
print(f"Function result: {context.result}")
292+
await call_next()
290293

291294
# Add both the function and middleware to your agent
292295
async with AzureAIAgentClient(credential=credential).as_agent(
293296
name="TimeAgent",
294297
instructions="You can tell the current time.",
295298
tools=[get_time],
296-
middleware=[logging_function_middleware],
299+
middleware=[inject_function_kwargs],
297300
) as agent:
298301
result = await agent.run("What time is it?")
299302
```

agent-framework/agents/middleware/index.md

Lines changed: 57 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ zone_pivot_groups: programming-languages
55
author: dmytrostruk
66
ms.topic: reference
77
ms.author: dmytrostruk
8-
ms.date: 02/17/2026
8+
ms.date: 03/16/2026
99
ms.service: agent-framework
1010
---
1111

@@ -177,7 +177,7 @@ Agent Framework can be customized using three different types of middleware:
177177
2. **Function middleware**: Intercepts function (tool) calls made during agent execution, enabling input validation, result transformation, and execution control.
178178
3. **Chat middleware**: Intercepts the underlying chat requests sent to AI models, providing access to the raw messages, options, and responses.
179179

180-
All types support both function-based and class-based implementations. When multiple middleware of the same type are registered, they form a chain where each calls the `next` callable to continue processing.
180+
All types support both function-based and class-based implementations. When multiple middleware of the same type are registered, they form a chain where each calls the `call_next` callback to continue processing. `call_next` does not take the context as an argument; middleware mutates the shared context object directly and then awaits `call_next()`.
181181

182182
> [!NOTE]
183183
> Middleware order with mixed registration scopes:
@@ -192,29 +192,31 @@ Agent middleware intercepts and modifies agent run execution. It uses the `Agent
192192

193193
- `agent`: The agent being invoked
194194
- `messages`: List of chat messages in the conversation
195-
- `is_streaming`: Boolean indicating if the response is streaming
195+
- `session`: The current agent session, if any
196+
- `options`: Agent run options for this invocation
197+
- `stream`: Boolean indicating if the response is streaming
196198
- `metadata`: Dictionary for storing additional data between middleware
197199
- `result`: The agent's response (can be modified)
198-
- `terminate`: Flag to stop further processing
199-
- `kwargs`: Additional keyword arguments passed to the agent run method
200+
- `kwargs`: Legacy runtime keyword arguments passed to the agent run method
201+
- `client_kwargs`: Client-specific runtime values for downstream chat clients
202+
- `function_invocation_kwargs`: Runtime values that will be forwarded to tools
200203

201-
The `next` callable continues the middleware chain or executes the agent if it's the last middleware.
204+
The `call_next` callback continues the middleware chain or executes the agent if it's the last middleware.
202205

203206
### Function-based
204207

205208
```python
206-
async def logging_agent_middleware(
209+
async def inject_tool_runtime_defaults(
207210
context: AgentContext,
208-
next: Callable[[AgentContext], Awaitable[None]],
211+
call_next: Callable[[], Awaitable[None]],
209212
) -> None:
210-
"""Agent middleware that logs execution timing."""
211-
# Pre-processing: Log before agent execution
213+
"""Agent middleware that sets tool-only runtime defaults."""
212214
print("[Agent] Starting execution")
215+
context.function_invocation_kwargs.setdefault("tenant", "contoso")
216+
context.function_invocation_kwargs.setdefault("request_source", "agent-middleware")
213217

214-
# Continue to next middleware or agent execution
215-
await next(context)
218+
await call_next()
216219

217-
# Post-processing: Log after agent execution
218220
print("[Agent] Execution completed")
219221
```
220222

@@ -231,10 +233,10 @@ class LoggingAgentMiddleware(AgentMiddleware):
231233
async def process(
232234
self,
233235
context: AgentContext,
234-
next: Callable[[AgentContext], Awaitable[None]],
236+
call_next: Callable[[], Awaitable[None]],
235237
) -> None:
236238
print("[Agent Class] Starting execution")
237-
await next(context)
239+
await call_next()
238240
print("[Agent Class] Execution completed")
239241
```
240242

@@ -244,29 +246,25 @@ Function middleware intercepts function calls within agents. It uses the `Functi
244246

245247
- `function`: The function being invoked
246248
- `arguments`: The validated arguments for the function
249+
- `session`: The current agent session, if any
247250
- `metadata`: Dictionary for storing additional data between middleware
248251
- `result`: The function's return value (can be modified)
249-
- `terminate`: Flag to stop further processing
250-
- `kwargs`: Additional keyword arguments passed to the chat method that invoked this function
252+
- `kwargs`: Runtime keyword arguments that will be forwarded to the tool invocation
251253

252-
The `next` callable continues to the next middleware or executes the actual function.
254+
The `call_next` callback continues to the next middleware or executes the actual function.
253255

254256
### Function-based
255257

256258
```python
257-
async def logging_function_middleware(
259+
async def inject_function_kwargs(
258260
context: FunctionInvocationContext,
259-
next: Callable[[FunctionInvocationContext], Awaitable[None]],
261+
call_next: Callable[[], Awaitable[None]],
260262
) -> None:
261-
"""Function middleware that logs function execution."""
262-
# Pre-processing: Log before function execution
263-
print(f"[Function] Calling {context.function.name}")
263+
"""Function middleware that enriches tool runtime values."""
264+
context.kwargs.setdefault("tenant", "contoso")
265+
context.kwargs.setdefault("request_source", "function-middleware")
264266

265-
# Continue to next middleware or function execution
266-
await next(context)
267-
268-
# Post-processing: Log after function execution
269-
print(f"[Function] {context.function.name} completed")
267+
await call_next()
270268
```
271269

272270
### Class-based
@@ -280,10 +278,10 @@ class LoggingFunctionMiddleware(FunctionMiddleware):
280278
async def process(
281279
self,
282280
context: FunctionInvocationContext,
283-
next: Callable[[FunctionInvocationContext], Awaitable[None]],
281+
call_next: Callable[[], Awaitable[None]],
284282
) -> None:
285283
print(f"[Function Class] Calling {context.function.name}")
286-
await next(context)
284+
await call_next()
287285
print(f"[Function Class] {context.function.name} completed")
288286
```
289287

@@ -294,27 +292,27 @@ Chat middleware intercepts chat requests sent to AI models. It uses the `ChatCon
294292
- `chat_client`: The chat client being invoked
295293
- `messages`: List of messages being sent to the AI service
296294
- `options`: The options for the chat request
297-
- `is_streaming`: Boolean indicating if this is a streaming invocation
295+
- `stream`: Boolean indicating if this is a streaming invocation
298296
- `metadata`: Dictionary for storing additional data between middleware
299297
- `result`: The chat response from the AI (can be modified)
300-
- `terminate`: Flag to stop further processing
301298
- `kwargs`: Additional keyword arguments passed to the chat client
299+
- `function_invocation_kwargs`: Tool-only runtime values that will be forwarded by the chat layer
302300

303-
The `next` callable continues to the next middleware or sends the request to the AI service.
301+
The `call_next` callback continues to the next middleware or sends the request to the AI service.
304302

305303
### Function-based
306304

307305
```python
308306
async def logging_chat_middleware(
309307
context: ChatContext,
310-
next: Callable[[ChatContext], Awaitable[None]],
308+
call_next: Callable[[], Awaitable[None]],
311309
) -> None:
312310
"""Chat middleware that logs AI interactions."""
313311
# Pre-processing: Log before AI call
314312
print(f"[Chat] Sending {len(context.messages)} messages to AI")
315313

316314
# Continue to next middleware or AI service
317-
await next(context)
315+
await call_next()
318316

319317
# Post-processing: Log after AI response
320318
print("[Chat] AI response received")
@@ -331,10 +329,10 @@ class LoggingChatMiddleware(ChatMiddleware):
331329
async def process(
332330
self,
333331
context: ChatContext,
334-
next: Callable[[ChatContext], Awaitable[None]],
332+
call_next: Callable[[], Awaitable[None]],
335333
) -> None:
336334
print(f"[Chat Class] Sending {len(context.messages)} messages to AI")
337-
await next(context)
335+
await call_next()
338336
print("[Chat Class] AI response received")
339337
```
340338

@@ -346,21 +344,21 @@ Decorators provide explicit middleware type declaration without requiring type a
346344
from agent_framework import agent_middleware, function_middleware, chat_middleware
347345

348346
@agent_middleware
349-
async def simple_agent_middleware(context, next):
347+
async def simple_agent_middleware(context, call_next):
350348
print("Before agent execution")
351-
await next(context)
349+
await call_next()
352350
print("After agent execution")
353351

354352
@function_middleware
355-
async def simple_function_middleware(context, next):
353+
async def simple_function_middleware(context, call_next):
356354
print(f"Calling function: {context.function.name}")
357-
await next(context)
355+
await call_next()
358356
print("Function call completed")
359357

360358
@chat_middleware
361-
async def simple_chat_middleware(context, next):
359+
async def simple_chat_middleware(context, call_next):
362360
print(f"Processing {len(context.messages)} chat messages")
363-
await next(context)
361+
await call_next()
364362
print("Chat processing completed")
365363
```
366364

@@ -407,30 +405,34 @@ async with AzureAIAgentClient(credential=credential).as_agent(
407405

408406
## Middleware Termination
409407

410-
Middleware can terminate execution early using `context.terminate`. This is useful for security checks, rate limiting, or validation failures.
408+
Middleware can terminate execution early by setting `context.result` and raising `MiddlewareTermination`. This is useful for security checks, rate limiting, or validation failures.
411409

412410
```python
411+
from agent_framework import AgentContext, AgentResponse, Message, MiddlewareTermination
412+
413413
async def blocking_middleware(
414414
context: AgentContext,
415-
next: Callable[[AgentContext], Awaitable[None]],
415+
call_next: Callable[[], Awaitable[None]],
416416
) -> None:
417417
"""Middleware that blocks execution based on conditions."""
418418
# Check for blocked content
419419
last_message = context.messages[-1] if context.messages else None
420420
if last_message and last_message.text:
421421
if "blocked" in last_message.text.lower():
422422
print("Request blocked by middleware")
423-
context.terminate = True
424-
return
423+
context.result = AgentResponse(
424+
messages=[Message(role="assistant", text="This request was blocked by middleware.")]
425+
)
426+
raise MiddlewareTermination(result=context.result)
425427

426428
# If no issues, continue normally
427-
await next(context)
429+
await call_next()
428430
```
429431

430432
**What termination means:**
431-
- Setting `context.terminate = True` signals that processing should stop
432-
- You can provide a custom result before terminating to give users feedback
433-
- The agent execution is completely skipped when middleware terminates
433+
- Set `context.result` before raising `MiddlewareTermination` if you want to return a custom response
434+
- Raising `MiddlewareTermination` stops the remainder of the middleware chain and skips the normal execution path
435+
- This pattern works for agent, function, and chat middleware
434436

435437
## Middleware Result Override
436438

@@ -441,17 +443,17 @@ The result type in `context.result` depends on whether the agent invocation is s
441443
- **Non-streaming**: `context.result` contains an `AgentResponse` with the complete response
442444
- **Streaming**: `context.result` contains an async generator that yields `AgentResponseUpdate` chunks
443445

444-
You can use `context.is_streaming` to differentiate between these scenarios and handle result overrides appropriately.
446+
You can use `context.stream` to differentiate between these scenarios and handle result overrides appropriately.
445447

446448
```python
447449
async def weather_override_middleware(
448450
context: AgentContext,
449-
next: Callable[[AgentContext], Awaitable[None]]
451+
call_next: Callable[[], Awaitable[None]]
450452
) -> None:
451453
"""Middleware that overrides weather results for both streaming and non-streaming."""
452454

453455
# Execute the original agent logic
454-
await next(context)
456+
await call_next()
455457

456458
# Override results if present
457459
if context.result is not None:
@@ -462,7 +464,7 @@ async def weather_override_middleware(
462464
"Great day for outdoor activities!"
463465
]
464466

465-
if context.is_streaming:
467+
if context.stream:
466468
# Streaming override
467469
async def override_stream() -> AsyncIterable[AgentResponseUpdate]:
468470
for chunk in custom_message_parts:

0 commit comments

Comments
 (0)