microsoft / microsoft/agent-framework
.NET: Automated Type Adapters - Seamlessly Connect Executors and Agents Without Boilerplate
- Dominant language
- Python
- Stars
- 13.6k
- Forks
- 2.3k
- Avg merge
- 2d 45m
- Merged PRs (30d)
- 358
Description
## Summary
Simplify workflow construction by automatically inserting type adapter executors when connecting components with incompatible types. This eliminates the need for developers to manually create and wire adapter executors (like `StringToChatMessageExecutor`, `JailbreakSyncExecutor`, etc.) when connecting string-based executors to AI agents.
**Value Proposition:**
- 🚀 **less boilerplate code** - No more manual adapter executors
- ✨ **Intuitive API** - Connect executors directly, framework handles the "glue"
- 🐛 **Fewer bugs** - Eliminates common wiring mistakes
- 📊 **Better debugging** - Auto-generated adapters are clearly marked and visualized - if we want to
- 🔧 **Backward compatible** - Opt-in feature, existing code continues to work
---
## Current Issue
### **Problem: Connecting Executors to Agents Requires Manual "Glue" Code**
When building workflows that mix regular executors (which work with simple types like `string`) and AI agents (which expect `ChatMessage` + `TurnToken`), developers must manually create adapter executors to bridge the type gap.
**Example - Current Approach (Verbose):**
```
// 1. Create a custom adapter executor to convert string →
ChatMessage internal sealed class StringToChatMessageExecutor(string id) : Executor(id) { public override async ValueTask HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default) { // Convert string to ChatMessage ChatMessage chatMessage = new(ChatRole.User, message); await context.SendMessageAsync(chatMessage, cancellationToken: cancellationToken);
// Send TurnToken to trigger agent
await context.SendMessageAsync(new TurnToken(emitEvents: true), cancellationToken: cancellationToken);
}
}
// 2. Create another adapter to connect agents together
internal sealed class JailbreakSyncExecutor() : Executor("JailbreakSync") { public override async ValueTask HandleAsync(ChatMessage message, IWorkflowContext context, CancellationToken cancellationToken = default) { string processedContent = ProcessMessage(message); // Custom logic
// Forward to next agent
await context.SendMessageAsync(new ChatMessage(ChatRole.User, processedContent), cancellationToken: cancellationToken);
await context.SendMessageAsync(new TurnToken(emitEvents: true), cancellationToken: cancellationToken);
}
}
// 3. Wire everything together with manual adapters
var workflow = new WorkflowBuilder(userInput) .AddEdge(userInput, stringToChat) // Manual adapter
.AddEdge(stringToChat, jailbreakDetector) // Agent
.AddEdge(jailbreakDetector, jailbreakSync) // Manual adapter
.AddEdge(jailbreakSync, responseAgent) // Agent
.Build();
```
**Problems:**
1. **Boilerplate Explosion** - Every workflow needs these same adapters
2. **Cognitive Overhead** - Developers must understand ChatMessage/TurnToken protocol
3. **Error-Prone** - Easy to forget TurnToken or use wrong ChatRole
4. **Poor Discoverability** - New users don't know they need adapters until runtime errors
5. **Noise in Workflow** - Adapters obscure the actual business logic
---
## Proposed Solution (Simplified)
### **Auto-Insert Type Adapters with Naming Convention + Metadata Tags for Tracking**
Enable `WorkflowBuilder.AddEdge()` to automatically detect type mismatches and inject adapter executors when needed.
**Phase 1: Naming Convention (Immediate - No Schema Changes)**
Use `_Auto_` prefix for auto-generated executors:
**Phase 2: Metadata Enhancement (Optional - Rich Debugging)**
Add metadata tags to `ExecutorInfo` for visualization:
Note: this would make Workflow deserialization-serialization easier-better due to this
---
## Developer Experience Improvements
### **Before (Manual Adapters):**
```
// Create 3 custom adapter executors
var stringToChat = new StringToChatMessageExecutor("Adapter1");
var agentSync = new JailbreakSyncExecutor();
var finalOutput = new FinalOutputExecutor();
// Wire with 6 edges var workflow = new WorkflowBuilder(userInput)
.AddEdge(userInput, stringToChat)
.AddEdge(stringToChat, jailbreakDetector)
.AddEdge(jailbreakDetector, agentSync)
.AddEdge(agentSync, responseAgent)
.AddEdge(responseAgent, finalOutput)
.Build();
```
### **After (Auto-Adapters):**
// Just connect components directly!
var workflow = new WorkflowBuilder(userInput)
.AddEdge(userInput, jailbreakDetector) // Auto-inserts string→ChatMessage adapter
.AddEdge(jailbreakDetector, responseAgent) // Auto-inserts Agent→Agent adapter
.Build();
// Framework auto-generates:
// _Auto_Adapter_userInput_To_jailbreakDetector
// _Auto_Adapter_jailbreakDetector_To_responseAgent
---
## Key Benefits
### **1. Reduced Code**
- **fewer lines** in common scenarios
- **Eliminates ~2-4 adapter classes** per workflow (depends on complexity, though)
### **2. Better Error Messages**
// Before: Cryptic runtime error InvalidOperationException: No handler found for type 'System.String'
// After: Clear error with auto-fix suggestion InvalidOperationException: Type mismatch detected between 'UserInput' (outputs string) and 'JailbreakDetector' (expects ChatMessage).
Suggestion: Enable auto-adaptation with .WithAutoTypeAdaptation()
### **3. Enhanced Visualization**
**Workflow JSON with Metadata:**
`{ "executors": { "UserInput": { "executorId": "UserInput" }, "_Auto_Adapter_UserInput_To_Agent": { "executorId": "_Auto_Adapter_UserInput_To_Agent", "metadata": { "auto-generated": "true", "adapter-type": "StringToAgent", "source": "UserInput", "target": "JailbreakDetector" } }, "JailbreakDetector": { "executorId": "JailbreakDetector" } } }`
## Backward Compatibility
✅ **100% Backward Compatible**
- Opt-in via `WithAutoTypeAdaptation()` (or enabled by default with opt-out)
- Existing manual adapters continue to work
- No breaking changes to public API
---
## Success Metrics
- ✅ Reduced workflow setup code for mixed executor/agent workflows
- ✅ Improve new developer onboarding (measured by sample completion time)
- ✅ Reduce common "type mismatch" issues reported in GitHub issues
- ✅ Maintain 100% backward compatibility (all existing tests pass)
---
## Open Questions
1. **Should auto-adaptation be opt-in or enabled by default?**
- Opt-in: `WithAutoTypeAdaptation()` - safer for existing code
- Default: Enabled by default with `WithoutAutoTypeAdaptation()` opt-out - better DX
2. **Should we provide base classes for common adapter patterns?**
- `StringToAgentExecutor`, `AgentRelayExecutor`, `FanOutExecutor`, `FanInAggregatorExecutor`
- Makes it easy for users to create custom adapters with custom logic
Contributor guide
Assessment
This issue has not been assessed yet.