
Working With Ms Agent Framework
- 41 installs
- 2 repo stars
- Updated August 1, 2026
- mhagrelius/dotfiles
Helps with ai & agent building tasks.
About
working-with-ms-agent-framework is a Claude Code skill in the AI & Agent Building category.
- working-with-ms-agent-framework
- AI & Agent Building
- AI-coding skill
Working With Ms Agent Framework by the numbers
- 41 all-time installs (skills.sh)
- Ranked #8,104 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mhagrelius/dotfiles --skill working-with-ms-agent-frameworkAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 41 |
|---|---|
| repo stars | ★ 2 |
| Last updated | August 1, 2026 |
| Repository | mhagrelius/dotfiles ↗ |
What it does
Helps with ai & agent building tasks.
Files
Working with Microsoft Agent Framework
Microsoft Agent Framework (October 2025) unifies Semantic Kernel and AutoGen into one SDK. Both legacy frameworks are in maintenance mode.
Core principle: Agents are stateless. All state lives in threads. Context providers enforce policy about what enters the prompt, how, and when it decays.
Additional reference files in this skill:
- context-providers.md - Policy-based memory, capsule pattern, Mem0 integration- orchestration-patterns.md - The 5 orchestration patterns with when-to-use guidance- design-patterns.md - Production patterns, testing, migrationWhen to Use
- Building AI agents with Microsoft's unified framework
- Implementing custom memory with Context Providers
- Creating multi-agent workflows with checkpointing
- Migrating from Semantic Kernel or AutoGen
When NOT to Use
- Simple single-turn LLM calls (use chat client directly)
- Projects staying on legacy SK/AutoGen
- Non-Microsoft frameworks (LangChain, CrewAI)
Technology Stack Hierarchy
Official guidance (Jeremy Licknes, PM): Start with ME AI, escalate only when needed.
| Layer | Use For | When to Escalate |
|---|---|---|
| ME AI (Microsoft.Extensions.AI) | Chat clients, structured outputs, embeddings, middleware | Need agents, workflows, memory |
| Agent Framework | Agents, threads, orchestration, context providers | Need specific SK adapters |
| Semantic Kernel | Specific adapters, utilities not in ME AI | Never start here |
ME AI (foundation) → Agent Framework (agents/workflows) → SK (specific utilities only)Key insight: ME AI provides universal APIs that work across OpenAI, Ollama, Foundry Local, etc. Agent Framework builds on ME AI for agentic patterns. SK primitives migrated to ME AI; only use SK for specific adapters not yet in ME AI.
ME AI features you get automatically:
- Structured outputs (typed responses via extension methods)
- Middleware (OpenTelemetry, chat reduction)
- Universal chat client abstraction
- Embeddings generation
Architecture Quick Reference
| Concept | C# Type | Purpose |
|---|---|---|
| Agent | AIAgent | Stateless LLM wrapper |
| Thread | AgentThread | Stateful conversation container |
| Context Provider | AIContextBehavior | Policy-based memory/context injection |
| Orchestration | SequentialOrchestration, etc. | Multi-agent coordination |
Installation
dotnet add package Microsoft.Agents.AI.OpenAI --prerelease
dotnet add package Azure.AI.OpenAI --version 2.1.0Agent Creation
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
// Azure OpenAI
AIAgent agent = new AzureOpenAIClient(
new Uri("https://<resource>.openai.azure.com"),
new AzureCliCredential())
.GetChatClient("gpt-4o-mini")
.CreateAIAgent(
instructions: "You are a helpful assistant.",
name: "Assistant");
// Direct OpenAI
var agent = new OpenAIClient("api-key")
.GetChatClient("gpt-4o-mini")
.AsIChatClient()
.CreateAIAgent(instructions: "...", name: "Assistant");
// With tools
[Description("Gets weather for a location")]
static string GetWeather(string location) => $"Sunny in {location}";
AIAgent agent = chatClient.CreateAIAgent(
instructions: "You help with weather queries.",
tools: [AIFunctionFactory.Create(GetWeather)]
);Execution
// Simple
Console.WriteLine(await agent.RunAsync("Hello!"));
// With thread for multi-turn
AgentThread thread = agent.GetNewThread();
await agent.RunAsync("My name is Alice.", thread);
await agent.RunAsync("What's my name?", thread); // Remembers "Alice"
// Streaming
await foreach (var update in agent.RunStreamingAsync("Tell me a story.", thread))
{
Console.Write(update.Text);
}Streaming with Resilience
For production streaming, add cancellation support and resilience:
// Basic streaming with cancellation
await foreach (var update in agent.RunStreamingAsync("Tell me a story.", thread)
.WithCancellation(cancellationToken))
{
Console.Write(update.Text);
}
// With Polly resilience pipeline
var pipeline = new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions { MaxRetryAttempts = 3 })
.AddTimeout(TimeSpan.FromMinutes(2))
.Build();
await pipeline.ExecuteAsync(async token =>
{
await foreach (var chunk in agent.RunStreamingAsync(userMessage, thread)
.WithCancellation(token))
{
Console.Write(chunk.Text);
}
}, cancellationToken);Error differentiation:
OperationCanceledException: User cancelledTimeoutRejectedException: Polly timeoutHttpRequestException: Network issues
Development UI (DevUI)
Lightweight web interface for testing agents and workflows. Development only—not for production.
Python Setup
Install:
pip install agent-framework-devui --preOption 1: Programmatic Registration
from agent_framework import ChatAgent
from agent_framework.openai import OpenAIChatClient
from agent_framework.devui import serve
agent = ChatAgent(
name="WeatherAgent",
chat_client=OpenAIChatClient(),
tools=[get_weather]
)
# Launch DevUI with tracing
serve(entities=[agent], auto_open=True, tracing_enabled=True)
# Opens browser to http://localhost:8080Option 2: Directory Discovery (CLI)
devui ./entities --port 8080 --tracingDirectory structure for discovery:
entities/
weather_agent/
__init__.py # Must export: agent = ChatAgent(...)
.env # Optional: API keys
my_workflow/
__init__.py # Must export: workflow = WorkflowBuilder()...C# Setup
C# embeds DevUI as SDK component (docs in progress):
var app = builder.Build();
app.MapOpenAIResponses();
app.MapConversation();
if (app.Environment.IsDevelopment())
{
app.MapAgentUI(); // Accessible at /ui
}Features
| Feature | Description |
|---|---|
| Web interface | Interactive testing of agents/workflows |
| OpenAI-compatible API | Use OpenAI SDK against local agents |
| Tracing | OpenTelemetry spans in debug panel |
| File uploads | Multimodal inputs (images, documents) |
| Auto-generated inputs | Workflow inputs based on first executor type |
Tracing in DevUI
Enable with --tracing flag or tracing_enabled=True. View in debug panel:
Agent Execution
├── LLM Call (prompt → response)
├── Tool Call
│ ├── Tool Execution
│ └── Tool Result
└── LLM Call (prompt → response)Export to external tools (Jaeger, Azure Monitor):
export OTLP_ENDPOINT="http://localhost:4317"
devui ./entities --tracingOpenAI SDK Integration
Interact with DevUI agents via OpenAI Python SDK:
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8080/v1", api_key="not-needed")
response = client.responses.create(
metadata={"entity_id": "weather_agent"},
input="What's the weather in Seattle?"
)CLI Options
devui [directory] [options]
--port, -p Port (default: 8080)
--tracing Enable OpenTelemetry tracing
--reload Auto-reload on file changes
--headless API only, no UI
--mode developer|user (default: developer)Thread Serialization (Critical Pattern)
// Serialize for persistence
JsonElement serialized = await thread.SerializeAsync();
await File.WriteAllTextAsync("thread.json", serialized.GetRawText());
// Later: restore and resume
string json = await File.ReadAllTextAsync("thread.json");
JsonElement element = JsonSerializer.Deserialize<JsonElement>(json);
AgentThread restored = agent.DeserializeThread(element, JsonSerializerOptions.Web);
await agent.RunAsync("Continue...", restored);Key behaviors:
- Service-managed threads: Only thread ID serialized
- In-memory threads: All messages serialized
- WARNING: Deserializing with different agent config may error
Context Providers (Policy-Based Memory)
Context providers are not "memory injection" — they're policy enforcement:
| Policy | What It Decides |
|---|---|
| Selection | What becomes memory |
| Gating | When it's retrieved |
| Decay | When it expires |
| Noise avoidance | When NOT to use |
ChatHistoryAgentThread thread = new();
// Long-term user memory
thread.AIContextProviders.Add(new Mem0Provider(httpClient, new() { UserId = "user123" }));
// Short-term conversation context
thread.AIContextProviders.Add(new WhiteboardProvider(chatClient));
// RAG integration
thread.AIContextProviders.Add(new TextSearchProvider(textSearch, new()
{
SearchTime = TextSearchProviderOptions.RagBehavior.OnDemandFunctionCalling
}));See context-providers.md for custom implementation patterns.
Orchestration Patterns
| Pattern | Use When |
|---|---|
| Sequential | Clear dependencies (draft → review → polish) |
| Concurrent | Independent perspectives, ensemble reasoning |
| Handoff | Unknown optimal agent upfront, dynamic expertise |
| GroupChat | Collaborative ideation, human-in-the-loop |
| Magentic | Complex open-ended problems |
// Sequential
SequentialOrchestration orchestration = new(analystAgent, writerAgent);
// Handoff - CRITICAL: Always set termination conditions!
var workflow = AgentWorkflowBuilder.StartHandoffWith(triageAgent)
.WithHandoffs(triageAgent, [mathTutor, historyTutor])
.WithHandoff(mathTutor, triageAgent) // Allows routing back
.WithHandoff(historyTutor, triageAgent)
.WithMaxHandoffs(10) // REQUIRED: Prevent infinite loops
.Build();
// Execute
InProcessRuntime runtime = new();
await runtime.StartAsync();
var result = await orchestration.InvokeAsync(task, runtime);CRITICAL for Handoffs: Missing .WithMaxHandoffs() causes infinite loops. Always set termination conditions.
See orchestration-patterns.md for detailed patterns and when-to-use guidance.
Workflow Checkpointing & Durability
For workflows that must survive process restarts:
// Basic checkpointing with CheckpointManager
var checkpointManager = CheckpointManager.Default;
await using Checkpointed<StreamingRun> checkpointedRun =
await InProcessExecution.StreamAsync(workflow, input, checkpointManager);
// Resume from checkpoint
await InProcessExecution.ResumeStreamAsync(savedCheckpoint, checkpointManager);Thread-Based Persistence Pattern
For long-running workflows, checkpoint thread state after each step:
// Save thread state after each workflow step
var serialized = await thread.SerializeAsync();
await checkpointStore.SaveAsync(workflowId, currentStep, serialized.GetRawText());
// Resume after restart
var json = await checkpointStore.GetAsync(workflowId);
var element = JsonSerializer.Deserialize<JsonElement>(json);
var restored = agent.DeserializeThread(element, JsonSerializerOptions.Web);
await agent.RunAsync(nextStep, restored);Recovery on Startup
public class WorkflowRecoveryService : BackgroundService
{
private readonly ICheckpointStore _store;
private readonly AIAgent _agent;
protected override async Task ExecuteAsync(CancellationToken ct)
{
var pending = await _store.GetPendingWorkflowsAsync();
foreach (var workflow in pending)
{
var thread = _agent.DeserializeThread(workflow.State, JsonSerializerOptions.Web);
await _agent.RunAsync(workflow.NextStep, thread, cancellationToken: ct);
}
}
}Key principle: Checkpoint after each step completes, not before. This ensures you can resume from the last successful step.
Migration Quick Reference
From Semantic Kernel
| SK | Agent Framework |
|---|---|
Kernel | AIAgent |
ChatHistory | AgentThread |
[KernelFunction] | [Description] on methods |
IPromptFilter | AIContextBehavior |
KernelFunctionFactory.CreateFromMethod | AIFunctionFactory.Create |
From AutoGen
| AutoGen | Agent Framework |
|---|---|
AssistantAgent | AIAgent via CreateAIAgent() |
FunctionTool | AIFunctionFactory.Create() |
| GroupChat/Teams | WorkflowBuilder patterns |
TopicSubscription | AgentWorkflowBuilder.WithHandoffs() |
BaseAgent, IHandle<> | AIAgent with tools |
Topic-Based to Handoff Migration:
// ❌ OLD AutoGen pattern (deprecated)
[TopicSubscription("queries")]
public class MyAgent : BaseAgent, IHandle<Query>
{
public async Task Handle(Query msg, CancellationToken ct)
{
// Process and publish to another topic
await PublishMessageAsync(new Response(...), "responses");
}
}
// ✅ NEW Agent Framework pattern
var triageAgent = chatClient.CreateAIAgent(
instructions: "Route queries to appropriate specialist.",
name: "Triage");
var mathAgent = chatClient.CreateAIAgent(
instructions: "Handle math queries.",
name: "Math");
var workflow = AgentWorkflowBuilder.StartHandoffWith(triageAgent)
.WithHandoffs(triageAgent, [mathAgent, otherAgent])
.WithMaxHandoffs(10) // REQUIRED
.Build();
await workflow.InvokeStreamingAsync(input, runtime);Anti-Patterns
| Don't | Do |
|---|---|
| Store state in agent instances | Use AgentThread for all state |
| Serialize only messages | Serialize entire thread |
| Share agent instances in workflows | Use factory pattern |
| Mix thread types across services | Threads are service-specific |
| Use Magentic when Sequential suffices | Use simplest pattern that works |
Skip UseImmutableKernel with ContextualFunctionProvider | Always set UseImmutableKernel = true |
| Start with Semantic Kernel for new projects | Start with ME AI, escalate to Agent Framework |
Red Flags - STOP
- Using
Kernelinstead ofAIAgent(old SK) - Using
AssistantAgentinstead ofAIAgent(old AutoGen) - Thread deserialization fails (missing serialization constructor in context provider)
- Memory lost between sessions (serializing messages instead of thread)
- Infinite handoff loops (need termination conditions)
Known Limitations
- Distributed runtime: In-process only; distributed execution planned
- C# Magentic: Most examples are Python
- C# message stores: Redis/database need custom implementation
- Token counting: Budget calculation in providers undocumented
- DevUI C# docs: Python has full docs; C# DevUI docs "coming soon" (embedded SDK approach differs)
- GA timeline: Agent Framework stable release "coming soon" (as of Jan 2025)
Resources
- GitHub:
github.com/microsoft/agent-framework - Docs:
learn.microsoft.com/en-us/agent-framework/ - Migration:
learn.microsoft.com/en-us/agent-framework/migration-guide/
Context Providers: Policy-Based Memory
Context providers are not "memory injection" — they're policy enforcement. RAG answers "what's semantically similar?" — Policy answers "should this enter the prompt at all, in what form, and should it decay?"
The Capsule Pattern
Bounded, fixed-budget context chunks injected as system messages:
┌─────────────────────────────────────────────────────────┐
│ Agent Run │
├─────────────────────────────────────────────────────────┤
│ 1. AF collects: current turn + thread history │
│ 2. For each ContextProvider (ordered): │
│ └─ OnModelInvokeAsync() → returns AIContextPart │
│ └─ Provider applies POLICY: budget, gating, │
│ retrieval, compression → "capsule" │
│ 3. Capsules prepended as system messages │
│ 4. Model inference │
│ 5. OnNewMessageAsync() for each provider │
│ └─ Provider applies POLICY: selection, │
│ schema, decay, dedup → WRITE memories │
└─────────────────────────────────────────────────────────┘Pattern characteristics:
- Each provider controls its own token budget
- Context assembly is deterministic and composable
- You can audit exactly what's in prompts (compliance/debugging)
Policy Decisions
| Policy | What It Decides |
|---|---|
| Selection | What becomes memory |
| Schema | How it's compressed/represented |
| Scope | Where it lives (user/thread/global) |
| Gating | When it's retrieved |
| Decay | When it mutates or expires |
| Noise avoidance | When it should NOT be used |
Two-Plane Architecture
| Plane | Carrier | Scope | Example |
|---|---|---|---|
| Ephemeral | AgentThread | Session | Current conversation |
| Persistent | Mem0 + Vector Store | Cross-session | User preferences |
Both connect through the same Context Provider hooks.
RED FLAG: Serialization Constructor Required
Thread deserialization WILL FAIL without a serialization constructor. This is the #1 cause of "memory lost between sessions" bugs.
// ❌ BROKEN - thread persistence fails silently
public class MyProvider : AIContextBehavior
{
public MyProvider(IChatClient client) { }
}
// ✅ WORKS - supports thread persistence
public class MyProvider : AIContextBehavior
{
public MyProvider(IChatClient client) { }
// REQUIRED for thread serialization
public MyProvider(
IChatClient client,
string? serializedState, // Restored state (may be null)
JsonSerializerOptions? options) // Serialization options
{
if (!string.IsNullOrEmpty(serializedState))
{
_state = JsonSerializer.Deserialize<MyState>(serializedState, options);
}
}
}If your context provider has state that must persist across sessions, you MUST implement both constructors.
AIContextBehavior Interface
public abstract class AIContextBehavior
{
// Called when thread is created
public virtual Task OnThreadCreatedAsync(string? threadId, CancellationToken ct) { }
// Called for each new message - use for WRITING memories
public virtual Task OnNewMessageAsync(string? threadId, ChatMessage msg, CancellationToken ct) { }
// Called before model invoke - use for READING/INJECTING context
public abstract Task<AIContextPart> OnModelInvokeAsync(
ICollection<ChatMessage> newMessages, CancellationToken ct);
// Called when thread is deleted
public virtual Task OnThreadDeleteAsync(string? threadId, CancellationToken ct) { }
// Suspend/Resume for long-running operations
public virtual Task OnSuspendAsync(string? threadId, CancellationToken ct) { }
public virtual Task OnResumeAsync(string? threadId, CancellationToken ct) { }
}Built-in Providers
Mem0Provider (Long-Term Cross-Session Memory)
Mem0 is not just a vector store — it's the policy layer:
using var httpClient = new HttpClient { BaseAddress = new Uri("https://api.mem0.ai") };
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Token", "<API_Key>");
var mem0Provider = new Mem0Provider(httpClient, new Mem0ProviderOptions
{
UserId = "user123", // Required: scope memories to user
// Optional additional scoping:
// ApplicationId = "myapp",
// AgentId = "support-agent",
// ThreadId = thread.Id
});
thread.AIContextProviders.Add(mem0Provider);Mem0 vs Azure AI Search:
| Mem0 Does | Azure AI Search Does |
|---|---|
| Extract "user prefers dark mode" from "I hate bright screens" | Store embeddings |
| Decide if new info or duplicate | Return top-k similar |
| Compress 5 similar memories into 1 | Index management |
| Expire memories after TTL | Nothing |
| Decide "this query doesn't need memory" | Always returns results |
Azure AI Search as Mem0 backend:
# Python example - C# pattern similar
config = {
"vector_store": {
"provider": "azure_ai_search",
"config": {
"service_name": "my-search-service",
"collection_name": "agent-memories",
},
},
"llm": {"provider": "azure_openai", "config": {"model": "gpt-4o-mini"}},
"embeddings": {"provider": "azure_openai", "config": {"deployment_name": "text-embedding-ada-002"}},
}
memory = Memory.from_config(config)WhiteboardProvider (Short-Term Conversation Context)
Captures requirements, proposals, decisions, actions within a conversation:
var whiteboardProvider = new WhiteboardProvider(chatClient);
thread.AIContextProviders.Add(whiteboardProvider);
await agent.InvokeAsync("I want to book a trip to Paris for 2 people.", thread);
// Whiteboard now contains: "Requirement: Trip to Paris, 2 travelers"TextSearchProvider (RAG Integration)
var options = new TextSearchProviderOptions
{
SearchTime = TextSearchProviderOptions.RagBehavior.OnDemandFunctionCalling,
// Or: RagBehavior.BeforeModelInvoke for automatic retrieval
};
var provider = new TextSearchProvider(textSearch, options);
thread.AIContextProviders.Add(provider);ContextualFunctionProvider (Dynamic Function Selection)
Selects only relevant functions based on semantic similarity:
var embeddingGenerator = new AzureOpenAIClient(endpoint, credential)
.GetEmbeddingClient("text-embedding-ada-002")
.AsIEmbeddingGenerator();
thread.AIContextProviders.Add(new ContextualFunctionProvider(
vectorStore: new InMemoryVectorStore(new InMemoryVectorStoreOptions {
EmbeddingGenerator = embeddingGenerator
}),
vectorDimensions: 1536,
functions: GetAllAvailableFunctions(), // e.g., 100 functions
maxNumberOfFunctions: 3 // Only top 3 advertised to model
));
// REQUIRED: Set this flag when using ContextualFunctionProvider
agent.Options.UseImmutableKernel = true;CRITICAL: You MUST set UseImmutableKernel = true when using ContextualFunctionProvider. Without this flag, the provider cannot dynamically modify which functions are advertised to the model. Omitting this causes silent failures where function selection doesn't work.
Combining Providers (Layered Context)
public AgentThread CreateEnrichedThread(string userId)
{
var thread = new ChatHistoryAgentThread();
// Layer 1: Long-term user memory (cross-session)
thread.AIContextProviders.Add(new Mem0Provider(httpClient, new() { UserId = userId }));
// Layer 2: Short-term conversation context (within session)
thread.AIContextProviders.Add(new WhiteboardProvider(chatClient));
// Layer 3: RAG for external knowledge
thread.AIContextProviders.Add(new TextSearchProvider(textSearch));
// Layer 4: Dynamic function selection
thread.AIContextProviders.Add(new ContextualFunctionProvider(...));
return thread;
}Custom Provider Implementation
public class UserProfileProvider : AIContextBehavior
{
private readonly IChatClient _chatClient;
private readonly IUserRepository _userRepo;
private UserProfile _profile = new();
// Standard constructor
public UserProfileProvider(IChatClient chatClient, IUserRepository userRepo)
{
_chatClient = chatClient;
_userRepo = userRepo;
}
// REQUIRED: Serialization constructor for thread persistence
public UserProfileProvider(
IChatClient chatClient,
IUserRepository userRepo,
string? serializedState,
JsonSerializerOptions? options)
{
_chatClient = chatClient;
_userRepo = userRepo;
if (!string.IsNullOrEmpty(serializedState))
{
_profile = JsonSerializer.Deserialize<UserProfile>(serializedState, options) ?? new();
}
}
// READ: Inject context before model invoke
public override async Task<AIContextPart> OnModelInvokeAsync(
ICollection<ChatMessage> newMessages,
CancellationToken ct)
{
// Apply POLICY: gating, budget, compression
if (_profile.IsEmpty)
return new AIContextPart(); // No context to inject
return new AIContextPart
{
Instructions = $"User: {_profile.Name}, Preferences: {_profile.PreferencesSummary}"
};
}
// WRITE: Extract memories after model response
public override async Task OnNewMessageAsync(
string? threadId,
ChatMessage message,
CancellationToken ct)
{
if (message.Role != ChatRole.User) return;
// Apply POLICY: selection, schema, dedup
var extracted = await ExtractUserInfo(message.Text, ct);
if (extracted != null)
{
_profile.Merge(extracted);
await _userRepo.SaveAsync(_profile, ct);
}
}
// Serialize for thread persistence
public string Serialize(JsonSerializerOptions? options = null)
=> JsonSerializer.Serialize(_profile, options);
}
// Register via factory for proper deserialization
AIAgent agent = chatClient.CreateAIAgent(new ChatClientAgentOptions
{
Instructions = "You are a helpful assistant.",
AIContextProviderFactory = ctx => new UserProfileProvider(
chatClient.AsIChatClient(),
userRepo,
ctx.SerializedState,
ctx.JsonSerializerOptions
)
});Context Window Management
Truncation
var reducer = new ChatHistoryTruncationReducer(
targetCount: 15, // Ideal maximum messages
thresholdCount: 5 // Buffer before triggering
);Use when: Real-time chatbots, resource-constrained environments
Summarization
Merges older messages into concise summary tagged with __summary__ metadata.
Use when: Long dialogues where historical context matters
Common Gotchas
Missing Serialization Constructor
// ❌ Thread deserialization will FAIL
public class MyProvider : AIContextBehavior
{
public MyProvider(IChatClient client) { }
// Missing: constructor with serializedState parameter
}
// ✅ Supports thread persistence
public class MyProvider : AIContextBehavior
{
public MyProvider(IChatClient client) { }
public MyProvider(IChatClient client, string? serializedState, JsonSerializerOptions? options)
{
// Restore state
}
}Blocking in Async Methods
// ❌ Blocks thread pool
public override async Task<AIContextPart> OnModelInvokeAsync(...)
{
var data = httpClient.GetStringAsync(url).Result; // BLOCKS!
}
// ✅ Properly async
public override async Task<AIContextPart> OnModelInvokeAsync(...)
{
var data = await httpClient.GetStringAsync(url);
}Unhandled Exceptions Crash Agent Run
// ❌ Exception propagates, fails entire run
public override async Task OnNewMessageAsync(...)
{
await externalService.SaveAsync(data); // Can throw!
}
// ✅ Graceful degradation
public override async Task OnNewMessageAsync(...)
{
try
{
await externalService.SaveAsync(data);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Memory save failed, continuing");
}
}Provider Ordering
Order of AIContextProviders may affect how contexts combine. Place more critical providers earlier.
When to Use What
| Scenario | Provider |
|---|---|
| Simple RAG over documents | TextSearchProvider or AI Search directly |
| Conversational memory that evolves | Mem0Provider |
| Track conversation goals/decisions | WhiteboardProvider |
| Large function catalog | ContextualFunctionProvider |
| Custom business rules | Custom AIContextBehavior |
| Full control, custom policies | Custom provider + your own vector store |
Production Design Patterns
Practical patterns for building production-ready agent systems.
Local Development with Ollama
Same code works locally with Ollama as with Azure OpenAI. Cost-effective for iteration.
// Local development with Ollama (via OllamaSharp)
using OllamaSharp;
var ollama = new OllamaApiClient("http://localhost:11434");
IChatClient chatClient = ollama.AsChatClient("mistral"); // or llama3, deepseek, etc.
AIAgent agent = chatClient.CreateAIAgent(
instructions: "You are a helpful assistant.",
tools: [AIFunctionFactory.Create(GetWeather)]);
// Same code works for Azure OpenAI
var azureClient = new AzureOpenAIClient(
new Uri("https://<resource>.openai.azure.com"),
new DefaultAzureCredential())
.GetChatClient("gpt-4o-mini")
.AsIChatClient();
AIAgent cloudAgent = azureClient.CreateAIAgent(
instructions: "You are a helpful assistant.",
tools: [AIFunctionFactory.Create(GetWeather)]);Recommended local models:
mistral/ministral3- Good general purposellama3.2- Solid performance, widely usedphi4- Microsoft's small model, optimized for localdeepseek-r1- Strong reasoning (larger)
Tips:
- Run Ollama in container for isolation:
docker run -d --gpus all -p 11434:11434 ollama/ollama - 8GB GPU can run 5B-7B models comfortably
- Test locally, deploy to cloud with same agent code
Pattern 1: Stateless Service with Thread Injection
Thread state stored externally; agent instances are stateless and scalable.
public class ConversationService
{
private readonly AIAgent _agent;
private readonly IThreadStore _threadStore;
public ConversationService(IChatClient chatClient, IThreadStore threadStore)
{
_agent = chatClient.CreateAIAgent(
instructions: "You are a helpful assistant.",
name: "Assistant");
_threadStore = threadStore;
}
public async Task<ConversationResponse> ProcessMessageAsync(
string conversationId,
string userMessage)
{
// Load thread from external store
var threadJson = await _threadStore.GetAsync(conversationId);
var thread = threadJson != null
? _agent.DeserializeThread(JsonSerializer.Deserialize<JsonElement>(threadJson))
: _agent.GetNewThread();
// Process message
var response = await _agent.RunAsync(userMessage, thread);
// Persist updated thread
var serialized = await thread.SerializeAsync();
await _threadStore.SaveAsync(conversationId, serialized.GetRawText());
return new ConversationResponse
{
Text = response.Text,
ConversationId = conversationId
};
}
}
// Usage in ASP.NET
app.MapPost("/chat", async (ChatRequest request, ConversationService service) =>
{
return await service.ProcessMessageAsync(request.ConversationId, request.Message);
});Pattern 2: Agent Factory for DI
Register agent creation as factory for proper dependency injection.
public static class AgentServiceExtensions
{
public static IServiceCollection AddAgentServices(this IServiceCollection services)
{
services.AddSingleton<IChatClient>(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
return new AzureOpenAIClient(
new Uri(config["AzureOpenAI:Endpoint"]!),
new DefaultAzureCredential())
.GetChatClient(config["AzureOpenAI:Model"]!)
.AsIChatClient();
});
services.AddTransient<AIAgent>(sp =>
{
var chatClient = sp.GetRequiredService<IChatClient>();
var userRepo = sp.GetRequiredService<IUserRepository>();
return chatClient.CreateAIAgent(new ChatClientAgentOptions
{
Instructions = "You are a helpful assistant.",
Name = "Assistant",
AIContextProviderFactory = ctx => new UserProfileProvider(
chatClient,
userRepo,
ctx.SerializedState,
ctx.JsonSerializerOptions)
});
});
return services;
}
}Pattern 3: Enriched Thread Builder
Standardize thread creation with appropriate context providers.
public class ThreadBuilder
{
private readonly IChatClient _chatClient;
private readonly HttpClient _mem0Client;
private readonly ITextSearch _textSearch;
public ThreadBuilder(IChatClient chatClient, HttpClient mem0Client, ITextSearch textSearch)
{
_chatClient = chatClient;
_mem0Client = mem0Client;
_textSearch = textSearch;
}
public AgentThread CreateForUser(string userId, ThreadCapabilities capabilities)
{
var thread = new ChatHistoryAgentThread();
if (capabilities.HasFlag(ThreadCapabilities.LongTermMemory))
{
thread.AIContextProviders.Add(new Mem0Provider(_mem0Client, new()
{
UserId = userId
}));
}
if (capabilities.HasFlag(ThreadCapabilities.ConversationTracking))
{
thread.AIContextProviders.Add(new WhiteboardProvider(_chatClient));
}
if (capabilities.HasFlag(ThreadCapabilities.KnowledgeRetrieval))
{
thread.AIContextProviders.Add(new TextSearchProvider(_textSearch, new()
{
SearchTime = TextSearchProviderOptions.RagBehavior.OnDemandFunctionCalling
}));
}
return thread;
}
}
[Flags]
public enum ThreadCapabilities
{
None = 0,
LongTermMemory = 1,
ConversationTracking = 2,
KnowledgeRetrieval = 4,
All = LongTermMemory | ConversationTracking | KnowledgeRetrieval
}
// Usage
var thread = threadBuilder.CreateForUser("user123", ThreadCapabilities.All);Pattern 4: Resilient Agent Wrapper
Add retry logic, circuit breaking, and observability.
public class ResilientAgent
{
private readonly AIAgent _agent;
private readonly ILogger<ResilientAgent> _logger;
private readonly ResiliencePipeline _pipeline;
public ResilientAgent(AIAgent agent, ILogger<ResilientAgent> logger)
{
_agent = agent;
_logger = logger;
_pipeline = new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromSeconds(1),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
ShouldHandle = new PredicateBuilder()
.Handle<HttpRequestException>()
.Handle<TaskCanceledException>()
})
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
MinimumThroughput = 10,
BreakDuration = TimeSpan.FromSeconds(30)
})
.AddTimeout(TimeSpan.FromMinutes(2))
.Build();
}
public async Task<AgentRunResponse> RunAsync(
string message,
AgentThread thread,
CancellationToken ct = default)
{
using var activity = AgentDiagnostics.StartActivity("agent.run");
return await _pipeline.ExecuteAsync(async token =>
{
var response = await _agent.RunAsync(message, thread, cancellationToken: token);
activity?.SetTag("response.length", response.Text?.Length ?? 0);
activity?.SetTag("messages.count", response.Messages.Count);
return response;
}, ct);
}
}Error Handling Strategies
Differentiate Error Types
public async Task<AgentRunResponse> RunWithErrorHandlingAsync(
string message,
AgentThread thread)
{
try
{
return await _agent.RunAsync(message, thread);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
{
// Rate limit - retry with backoff
_logger.LogWarning("Rate limited, retrying...");
await Task.Delay(TimeSpan.FromSeconds(5));
return await _agent.RunAsync(message, thread);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
{
// Auth failure - fail fast, don't retry
_logger.LogError("Authentication failed");
throw;
}
catch (TaskCanceledException)
{
// Timeout - may retry with longer timeout
_logger.LogWarning("Request timed out");
throw;
}
catch (JsonException ex)
{
// Model returned garbage - retry with different temperature
_logger.LogWarning(ex, "Model response parsing failed");
throw;
}
}Agent Failure Modes
| Failure Mode | Symptom | Recovery |
|---|---|---|
| Thinking failure | Model outputs garbage | Retry with lower temperature |
| Acting failure | Tool call fails | Retry tool, or skip and continue |
| Observing failure | Can't parse tool result | Simplify output format |
| Loop failure | Stuck in infinite loop | Add iteration limit, break condition |
Testing Strategies
Three-Tier Approach
// 1. Unit Tests - Deterministic components
[Fact]
public async Task Tool_GetWeather_ReturnsFormattedString()
{
var result = GetWeather("Seattle");
Assert.Contains("Seattle", result);
}
// 2. Agent Evaluation - Behavioral tests
[Fact]
public async Task Agent_AnswersWeatherQuestion()
{
var agent = CreateTestAgent();
var thread = agent.GetNewThread();
var response = await agent.RunAsync("What's the weather in Seattle?", thread);
// Check behavior, not exact output
Assert.Contains("Seattle", response.Text, StringComparison.OrdinalIgnoreCase);
Assert.True(response.Messages.Any(m => m.Role == ChatRole.Tool),
"Should have called weather tool");
}
// 3. Integration Tests - Full scenario
[Fact]
public async Task Workflow_ProcessesCustomerRequest()
{
var workflow = CreateSupportWorkflow();
var runtime = new InProcessRuntime();
await runtime.StartAsync();
var result = await workflow.InvokeAsync(
"I need to return my order #12345",
runtime);
var output = await result.GetValueAsync();
// Verify workflow completed with appropriate response
Assert.Contains("return", output, StringComparison.OrdinalIgnoreCase);
}Multi-Trial Evaluation
// Run multiple trials to assess consistency
public async Task<EvaluationResult> EvaluateAgentAsync(
AIAgent agent,
string prompt,
Func<string, bool> successCriteria,
int trials = 5)
{
var results = new List<bool>();
for (int i = 0; i < trials; i++)
{
var thread = agent.GetNewThread();
var response = await agent.RunAsync(prompt, thread);
results.Add(successCriteria(response.Text));
}
return new EvaluationResult
{
PassRate = results.Count(r => r) / (double)trials,
AllPassed = results.All(r => r), // pass^k - consistency
AnyPassed = results.Any(r => r) // pass@k - capability
};
}Migration from Semantic Kernel
Before (SK)
// Semantic Kernel pattern
Kernel kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion(modelId, apiKey)
.Build();
KernelFunction weatherFunc = KernelFunctionFactory.CreateFromMethod(
(string location) => $"Weather in {location}: Sunny",
"GetWeather",
"Gets weather for a location");
KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions(
"Weather", [weatherFunc]);
kernel.Plugins.Add(plugin);
ChatCompletionAgent agent = new()
{
Instructions = "You help with weather queries.",
Kernel = kernel
};
ChatHistory history = new();
await agent.InvokeAsync(history, "What's the weather in Seattle?");After (Agent Framework)
// Agent Framework pattern
[Description("Gets weather for a location")]
static string GetWeather(string location) => $"Weather in {location}: Sunny";
AIAgent agent = new OpenAIClient(apiKey)
.GetChatClient(modelId)
.AsIChatClient()
.CreateAIAgent(
instructions: "You help with weather queries.",
tools: [AIFunctionFactory.Create(GetWeather)]);
AgentThread thread = agent.GetNewThread();
await agent.RunAsync("What's the weather in Seattle?", thread);Migration Checklist
- [ ] Replace
KernelwithAIAgentviaCreateAIAgent() - [ ] Replace
ChatHistorywithAgentThread - [ ] Replace
[KernelFunction]with[Description] - [ ] Replace
KernelFunctionFactorywithAIFunctionFactory - [ ] Replace
IPromptFilterwithAIContextBehavior - [ ] Update namespace from
Microsoft.SemanticKernel.*toMicrosoft.Extensions.AI.* - [ ] Implement thread serialization for state persistence
- [ ] Add context providers for memory (replaces separate memory stores)
Observability
Aspire Integration ("AI Sparkles")
Aspire dashboard shows AI operations with special sparkle icons (✨). Invaluable for debugging agent workflows.
Setup:
var builder = DistributedApplication.CreateBuilder(args);
var backend = builder.AddProject<Projects.Backend>("backend")
.WithOpenTelemetry(); // Enables AI trace collection
builder.Build().Run();What you see in Aspire traces:
- Question/prompt submitted
- Semantic search operations
- Tool calls (function invocations)
- LLM responses
- Full timing breakdown
Debugging workflow: 1. Open Aspire dashboard → Traces 2. Look for sparkle icons (AI operations) 3. Expand to see: prompt → tool calls → response 4. Check timing for bottlenecks
Custom Diagnostics
public static class AgentDiagnostics
{
private static readonly ActivitySource Source = new("AgentFramework");
public static Activity? StartActivity(string name)
{
return Source.StartActivity(name, ActivityKind.Internal);
}
}
// Configure in startup
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddSource("AgentFramework")
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddOtlpExporter());Key Metrics to Track
| Metric | Why |
|---|---|
| Agent run duration | Performance baseline |
| Token consumption | Cost tracking |
| Tool call success rate | Reliability |
| Handoff frequency | Workflow efficiency |
| Context provider latency | Memory system health |
| Thread serialization size | Storage costs |
Production Checklist
- [ ] Thread persistence implemented and tested
- [ ] Context providers have serialization constructors
- [ ] Error handling with appropriate retry strategies
- [ ] Circuit breaker for external dependencies
- [ ] Observability (traces, metrics, logs)
- [ ] Rate limiting for API calls
- [ ] Timeout configuration for long-running operations
- [ ] Graceful degradation when memory services unavailable
- [ ] Termination conditions for orchestrations
- [ ] Load testing with realistic conversation patterns
Orchestration Patterns
Five core patterns for multi-agent coordination. Choose based on task structure.
Pattern Selection Guide
| Pattern | Use When | Avoid When |
|---|---|---|
| Sequential | Clear dependencies, pipeline stages | Tasks can parallelize |
| Concurrent | Independent perspectives needed | Agents must build on each other |
| Handoff | Unknown optimal agent upfront | Risk of infinite loops |
| GroupChat | Collaborative ideation needed | More than 3 agents |
| Magentic | Complex open-ended problems | Simple deterministic workflows |
1. Sequential Orchestration
Agents execute in predefined order. Each agent's output becomes next agent's input.
ChatCompletionAgent analystAgent = new()
{
Name = "Analyst",
Instructions = "You are a marketing analyst. Analyze the market trends.",
Kernel = kernel,
};
ChatCompletionAgent writerAgent = new()
{
Name = "Copywriter",
Instructions = "You are a copywriter. Write compelling copy based on the analysis.",
Kernel = kernel,
};
ChatCompletionAgent reviewerAgent = new()
{
Name = "Reviewer",
Instructions = "You review and improve the copy.",
Kernel = kernel,
};
SequentialOrchestration orchestration = new(analystAgent, writerAgent, reviewerAgent);
InProcessRuntime runtime = new();
await runtime.StartAsync();
var result = await orchestration.InvokeAsync("Analyze electric vehicle market", runtime);
Console.WriteLine(await result.GetValueAsync());Best for: Draft → review → polish, ETL pipelines, approval workflows
2. Concurrent Orchestration
Multiple agents execute simultaneously on same input. Results aggregated.
// Three analysts with different perspectives
var optimistAgent = chatClient.CreateAIAgent(
instructions: "Analyze from optimistic perspective.", name: "Optimist");
var pessimistAgent = chatClient.CreateAIAgent(
instructions: "Analyze from pessimistic perspective.", name: "Pessimist");
var realistAgent = chatClient.CreateAIAgent(
instructions: "Analyze from realistic perspective.", name: "Realist");
ConcurrentOrchestration<string, Analysis> orchestration = new(
optimistAgent, pessimistAgent, realistAgent)
{
ResultTransform = async (results) =>
{
// Aggregate all perspectives into final analysis
return new Analysis { Perspectives = results.ToList() };
}
};
var result = await orchestration.InvokeAsync("Should we enter the EV market?", runtime);Best for: Multiple independent analyses, ensemble reasoning, time-sensitive parallel work
3. Handoff Orchestration
Agents dynamically transfer control based on context. Mesh topology — no central orchestrator.
var triageAgent = chatClient.CreateAIAgent(
instructions: "You route questions to the appropriate expert.",
name: "Triage");
var mathTutor = chatClient.CreateAIAgent(
instructions: "You help with math questions.",
name: "MathTutor");
var historyTutor = chatClient.CreateAIAgent(
instructions: "You help with history questions.",
name: "HistoryTutor");
var workflow = AgentWorkflowBuilder.StartHandoffWith(triageAgent)
.WithHandoffs(triageAgent, [mathTutor, historyTutor])
.WithHandoff(mathTutor, triageAgent) // Can route back
.WithHandoff(historyTutor, triageAgent) // Can route back
.WithMaxHandoffs(10) // CRITICAL: Prevent infinite loops
.Build();
await foreach (var message in workflow.InvokeStreamingAsync("What is calculus?", runtime))
{
Console.Write(message.Text);
}Handoff vs Agent-as-Tools
| Aspect | Handoff | Agent-as-Tools |
|---|---|---|
| Control Flow | Explicitly passed; no central authority | Primary agent delegates, control returns |
| Task Ownership | Receiving agent takes full ownership | Primary agent retains responsibility |
| Context | Full context transferred | Only relevant info provided |
| Use Case | Dynamic expertise routing | Specialized subtask delegation |
Best for: Customer service routing, dynamic expertise selection, exploratory workflows
CRITICAL: Always set termination conditions to prevent infinite handoff loops:
.WithMaxHandoffs(10) // Limit total handoffs
.WithTerminationStrategy(new MaxTurnsTerminationStrategy(15)) // Or custom strategy4. GroupChat Orchestration
Managed conversation where agents collaborate through shared thread.
# Python example - shows selector pattern
def smart_selector(state: GroupChatStateSnapshot) -> str | None:
round_idx = state["round_index"]
conversation = state["conversation"]
if round_idx >= 10:
return None # Stop after 10 rounds
if round_idx == 0:
return "Researcher" # Always start with researcher
last_speaker = conversation[-1].speaker
last_text = getattr(conversation[-1], "text", "").lower()
# Ping-pong between researcher and writer
if "?" in last_text and last_speaker == "Researcher":
return "Writer"
return "Writer" if last_speaker == "Researcher" else "Researcher"
group_chat = GroupChat(
agents=[researcher, writer, reviewer],
selector=smart_selector,
max_rounds=10
)Best for: Brainstorming, collaborative editing, decision-making through debate
Best practice: Limit to 3 or fewer agents to maintain control.
5. Magentic Orchestration
Based on Microsoft Research's Magentic-One. Manager agent builds dynamic task ledger.
Two-Loop Architecture
┌─────────────────────────────────────────────────────────┐
│ OUTER LOOP │
│ Manages Task Ledger (persistent plan) │
├─────────────────────────────────────────────────────────┤
│ Task Ledger contains: │
│ - Facts: Verified information │
│ - Guesses: Hypotheses to verify │
│ - Plan: Steps to complete task │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ INNER LOOP │
│ Manages Progress Ledger (execution) │
├─────────────────────────────────────────────────────────┤
│ Progress Ledger contains: │
│ - Current progress toward task │
│ - Next agent assignment │
│ - Stall detection (2+ cycles = revise plan) │
└─────────────────────────────────────────────────────────┘Default Agent Team
| Agent | Role |
|---|---|
| Orchestrator | High-level planning, directing, tracking progress |
| WebSurfer | Commands Chromium-based browser for web tasks |
| FileSurfer | Navigates local files |
| Coder | Writes and analyzes code |
| ComputerTerminal | Console access for code execution |
Best for: Complex problems without predetermined solutions, autonomous exploration
C# Magentic Status (as of Jan 2026)
Magentic orchestration is Python-first. For C#:
1. Use GroupChat for most multi-agent scenarios - it's the closest C# equivalent 2. Use Handoff with termination conditions for dynamic routing needs 3. Recommended: Use Sequential or Handoff for most workflows
C# Magentic Implementation Pattern
If you truly need Magentic's two-loop architecture:
// Ledger structures
public record TaskLedger(
List<string> Facts, // Verified information
List<string> Guesses, // Hypotheses to verify
List<string> Plan); // Steps to complete task
public record ProgressLedger(
int CurrentStep,
int StallCounter, // Increments when no progress
string? LastAgentOutput);
// Outer Loop: Task Ledger Management
public class OuterLoop(IChatClient chatClient)
{
public async Task<TaskLedger> CreateInitialPlanAsync(string task)
{
var response = await chatClient.CompleteAsync(
$"Analyze task and create plan. Task: {task}. " +
"Output JSON: {{facts: [], guesses: [], plan: []}}");
return JsonSerializer.Deserialize<TaskLedger>(response.Text);
}
public async Task<TaskLedger> RevisePlanAsync(
TaskLedger current, ProgressLedger progress, string stallReason)
{
var response = await chatClient.CompleteAsync(
$"Plan stalled: {stallReason}. Current: {JsonSerializer.Serialize(current)}. " +
"Revise the plan. Output JSON.");
return JsonSerializer.Deserialize<TaskLedger>(response.Text);
}
}
// Inner Loop: Progress Ledger Management
public class InnerLoop(AIAgent[] agentTeam)
{
private ProgressLedger _progress = new(0, 0, null);
public bool IsStalled => _progress.StallCounter >= 2;
public async Task<string> ExecuteStepAsync(TaskLedger task, AgentThread thread)
{
var step = task.Plan[_progress.CurrentStep];
var agent = SelectAgentForStep(step);
var result = await agent.RunAsync(step, thread);
// Detect progress
if (result == _progress.LastAgentOutput)
_progress = _progress with { StallCounter = _progress.StallCounter + 1 };
else
_progress = new(_progress.CurrentStep + 1, 0, result);
return result;
}
}
// Orchestrator coordinates both loops
public class MagenticOrchestrator(IChatClient chatClient, AIAgent[] agentTeam)
{
public async IAsyncEnumerable<OrchestrationUpdate> ExecuteStreamingAsync(string task)
{
var outer = new OuterLoop(chatClient);
var inner = new InnerLoop(agentTeam);
var taskLedger = await outer.CreateInitialPlanAsync(task);
var thread = new ChatHistoryAgentThread();
while (!IsComplete(taskLedger))
{
if (inner.IsStalled)
{
yield return new("Replanning", "Stall detected, revising plan");
taskLedger = await outer.RevisePlanAsync(taskLedger, /* progress */, "No progress");
}
var result = await inner.ExecuteStepAsync(taskLedger, thread);
yield return new("Progress", result);
}
}
}When to use Magentic: Complex open-ended problems requiring multiple specialists and adaptive replanning.
When NOT to use: Simple tasks where Sequential or Handoff suffices. Magentic adds significant complexity.
Workflow Execution Runtime
// Create and start runtime
InProcessRuntime runtime = new();
await runtime.StartAsync();
// Execute orchestration
OrchestrationResult result = await orchestration.InvokeAsync(task, runtime);
string output = await result.GetValueAsync();
// Wait for all background work
await runtime.RunUntilIdleAsync();Workflow-as-Agent Pattern
Wrap workflows to expose unified agent API:
var workflow = AgentWorkflowBuilder
.CreateSequentialPipeline(researchAgent, writerAgent, reviewerAgent)
.Build();
// Wrap as agent
AIAgent pipelineAgent = workflow.AsAgent(
id: "content-pipeline",
name: "Content Pipeline",
description: "Multi-agent content creation workflow"
);
// Use like any other agent
AgentThread thread = pipelineAgent.GetNewThread();
var response = await pipelineAgent.RunAsync("Write about AI trends", thread);Checkpointing and Fault Tolerance
var checkpointManager = CheckpointManager.Default;
// Execute with checkpointing
await using Checkpointed<StreamingRun> checkpointedRun =
await InProcessExecution.StreamAsync(workflow, input, checkpointManager);
// Save checkpoint for later
var savedCheckpoint = await checkpointedRun.CreateCheckpointAsync();
// Resume from checkpoint (after crash/restart)
await InProcessExecution.ResumeStreamAsync(savedCheckpoint, checkpointManager);Executor State Persistence
public class MyExecutor : Executor
{
private int _processedCount;
protected override async ValueTask OnCheckpointingAsync(
IWorkflowContext context, CancellationToken ct)
{
// Save state before checkpoint
await context.QueueStateUpdateAsync("processedCount", _processedCount, ct);
}
protected override async ValueTask OnCheckpointRestoredAsync(
IWorkflowContext context, CancellationToken ct)
{
// Restore state after checkpoint
_processedCount = await context.ReadStateAsync<int>("processedCount", ct);
}
}Human-in-the-Loop
# Python example
@ai_function(approval_mode="always_require")
def process_refund(order_number: str) -> str:
return f"Refund processed for order {order_number}."
# Tool call will pause for human approval before executingAnti-Patterns
| Don't | Do |
|---|---|
| Use Magentic for simple pipelines | Use Sequential |
| Add agents without specialization | Each agent needs clear expertise |
| Share mutable state between agents | Use explicit message passing |
| Ignore handoff loop risk | Implement max handoff counts |
| Skip termination conditions | Add round limits, completion criteria |
Choosing the Right Pattern
digraph pattern_selection {
"Task structure?" [shape=diamond];
"Dependencies?" [shape=diamond];
"Dynamic routing?" [shape=diamond];
"Collaborative?" [shape=diamond];
"Sequential" [shape=box];
"Concurrent" [shape=box];
"Handoff" [shape=box];
"GroupChat" [shape=box];
"Magentic" [shape=box];
"Task structure?" -> "Dependencies?" [label="pipeline"];
"Task structure?" -> "Concurrent" [label="parallel"];
"Task structure?" -> "Dynamic routing?" [label="routing"];
"Task structure?" -> "Magentic" [label="open-ended"];
"Dependencies?" -> "Sequential" [label="yes"];
"Dependencies?" -> "Concurrent" [label="no"];
"Dynamic routing?" -> "Handoff" [label="yes"];
"Dynamic routing?" -> "Collaborative?" [label="no"];
"Collaborative?" -> "GroupChat" [label="yes"];
"Collaborative?" -> "Sequential" [label="no"];
}