Termination in the Microsoft Agent Framework means that a middleware component returns an AgentResponse directly without ever calling innerAgent.RunAsync(). The agent's actual processing is completely skipped. This pattern is the foundation of guardrail middleware: a protective layer that sits in front of (or behind) an agent and enforces safety, compliance, or quality rules.
Why Guardrails?
Production AI agents need more than just good prompts. They need deterministic walls:
- Input guardrails – Block requests that contain sensitive keywords, off-topic content, or policy violations before they ever reach the LLM.
- Output guardrails – Inspect or transform the agent's response after it is generated. Truncate excessively long replies, redact PII, or inject a disclaimer.
The Termination Pattern
A normal middleware delegates to the next agent in the chain:
// Normal pass-through middleware
AgentResponse response = await innerAgent.RunAsync(messages, session, options, ct);
return response;
Termination middleware skips that delegation and returns a synthetic response:
// Termination — innerAgent is NEVER called
return new AgentResponse(
[
new ChatMessage(ChatRole.Assistant, "Sorry, that request is not allowed.")
]);
The AgentResponse(IList<ChatMessage>) constructor lets you craft a fully custom response as if the agent had replied.
Demo: GuardrailMiddleware
The demo in Lesson6/Demo1_Termination.cs registers a single middleware function that performs both a pre-check and a post-check.
Step 1 – Build the base agent
AIAgent baseAgent = new OpenAIClient(apiKey)
.GetChatClient("gpt-4o-mini")
.AsIChatClient()
.AsAIAgent(
instructions: "You are a helpful assistant.",
name: "Assistant");
Step 2 – Wrap it with the guardrail
AIAgent guardedAgent = baseAgent
.AsBuilder()
.Use(runFunc: GuardrailMiddleware, runStreamingFunc: null)
.Build();
Every call to guardedAgent.RunAsync() now passes through GuardrailMiddleware first.
Step 3 – The middleware itself
static async Task<AgentResponse> GuardrailMiddleware(
IEnumerable<ChatMessage> messages,
AgentSession? session,
AgentRunOptions? options,
AIAgent innerAgent,
CancellationToken cancellationToken)
{
// --- Pre-execution: block sensitive input ---
string lastMessage = messages.LastOrDefault()?.Text?.ToLower() ?? "";
string[] blockedWords = ["password", "secret", "credentials"];
foreach (string word in blockedWords)
{
if (lastMessage.Contains(word))
{
Console.WriteLine($" [Guardrail] Blocked - input contains '{word}'.");
// Return WITHOUT calling innerAgent — this is termination.
return new AgentResponse(
[
new ChatMessage(ChatRole.Assistant,
$"Sorry, I cannot process requests containing '{word}'.")
]);
}
}
// Input is clean — delegate to the real agent.
AgentResponse response = await innerAgent
.RunAsync(messages, session, options, cancellationToken)
.ConfigureAwait(false);
// --- Post-execution: truncate excessively long responses ---
string responseText = response.Messages.LastOrDefault()?.Text ?? "";
if (responseText.Length > 5000)
{
Console.WriteLine(" [Guardrail] Response too long - truncating.");
return new AgentResponse(
[
new ChatMessage(ChatRole.Assistant,
responseText.Substring(0, 5000) + "... [truncated]")
]);
}
return response;
}
Step 4 – Run the demo
// Run 1 — passes the guardrail, LLM responds normally.
Console.WriteLine("User: What's the weather in Seattle?");
AgentResponse r1 = await guardedAgent.RunAsync("What's the weather in Seattle?");
Console.WriteLine($"Agent: {r1.Text}");
// Run 2 — blocked. innerAgent.RunAsync is never called.
Console.WriteLine("User: What is my password?");
AgentResponse r2 = await guardedAgent.RunAsync("What is my password?");
Console.WriteLine($"Agent: {r2.Text}");
// Output: "Sorry, I cannot process requests containing 'password'."
How the Two Checks Differ
| Check | Position | Calls innerAgent? | Purpose |
|---|
| Pre-execution | Before innerAgent.RunAsync() | No (terminated) | Block forbidden input, enforce topic restrictions |
| Post-execution | After innerAgent.RunAsync() | Yes (already ran) | Truncate, redact, or replace unwanted output |
Composing Multiple Guardrails
Because guardrails are just middleware, you can stack them with .Use() calls:
AIAgent fullyGuardedAgent = baseAgent
.AsBuilder()
.Use(runFunc: TopicGuardrail, runStreamingFunc: null)
.Use(runFunc: OutputLengthGuardrail, runStreamingFunc: null)
.Build();
Middleware is applied in reverse registration order: the last .Use() call wraps outermost. Keep that in mind when ordering input vs output checks.
Key Types
| Type / Member | Package | Role |
|---|
AgentResponse(IList<ChatMessage>) | Microsoft.Agents.AI | Construct a synthetic response to return from a terminated middleware |
AIAgent.AsBuilder().Use().Build() | Microsoft.Agents.AI | Register middleware on an existing agent |
AgentRunMiddleware delegate | Microsoft.Agents.AI | Task<AgentResponse>(messages, session, options, innerAgent, ct) |
ChatMessage(ChatRole, string) | Microsoft.Extensions.AI | Create a single message to include in the synthetic response |
Required Packages
<PackageReference Include="Microsoft.Agents.AI" Version="1.0.0-rc1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.3.0" />