Repository navigation
Description
Summary
ChatProtocolExecutor only configured a handler for List<ChatMessage>, causing workflows to fail when the runtime dispatched messages as IEnumerable<ChatMessage> or ChatMessage[]. This prevented initial messages from reaching child workflow agents when using raw WorkflowBuilder.
Problem
When creating workflows with raw WorkflowBuilder using AIAgent instances, the framework creates AIAgentHostExecutor (which extends ChatProtocolExecutor). On the first turn, InProcessExecution.StreamAsync dispatches the initial input to the start executor, but the messages were never received because the route handler type didn't match.
Reproduction
AIAgent agent = await someWorkflow.AsAgentAsync();
// This fails - messages never reach the agent
Workflow workflow = new WorkflowBuilder(agent)
.AddEdge(agent, otherAgent)
.Build();
List<ChatMessage> initialMessages = [
new ChatMessage(ChatRole.User, "Hello")
];
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, initialMessages);Expected: Agent receives the initial message and can respond.
Actual: Agent receives empty history on first turn. Messages are dispatched but not routed because ChatProtocolExecutor only had a List<ChatMessage> handler, and the runtime may send IEnumerable<ChatMessage> or ChatMessage[].
Why BuildSequential Worked
AgentWorkflowBuilder.BuildSequential() worked because it wraps agents in AgentRunStreamingExecutor, which already had handlers for all three collection types:
// AgentRunStreamingExecutor.ConfigureRoutes (lines 152-159)
return routeBuilder
.AddHandler<List<ChatMessage>>(...)
.AddHandler<IEnumerable<ChatMessage>>(...)
.AddHandler<ChatMessage[]>(...);Root Cause
The workflow routing system (RouteBuilder) uses exact type matching, not polymorphic/covariance matching. A List<ChatMessage> handler will not match when the runtime sends IEnumerable<ChatMessage> or ChatMessage[].
This is by design - the routing system requires explicit handlers for each concrete type that may be dispatched.
Solution
Add handlers for all collection types to ChatProtocolExecutor.ConfigureRoutes():
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
{
if (this._stringMessageChatRole.HasValue)
{
routeBuilder = routeBuilder.AddHandler<string>(...);
}
// Handle various collection types for ChatMessage. The workflow routing system
// requires exact type matches, so we need handlers for List<T>, IEnumerable<T>,
// and T[] since the runtime may send any of these.
return routeBuilder.AddHandler<ChatMessage>(...)
.AddHandler<List<ChatMessage>>(...)
.AddHandler<IEnumerable<ChatMessage>>(...)
.AddHandler<ChatMessage[]>(...)
.AddHandler<TurnToken>(...);
}Why All Three Handlers Are Necessary
Testing confirmed that all three collection type handlers are required:
- Removing
List<ChatMessage>: Runtime may dispatch asList<T>- no match, messages lost - Removing
IEnumerable<ChatMessage>: Runtime may dispatch asIEnumerable<T>- no match, messages lost - Removing
ChatMessage[]: Runtime may dispatch as array - no match, messages lost
The routing system does not use type covariance. Each concrete type requires its own handler.
Impact
Before Fix:
- ❌ Raw
WorkflowBuilderwithAIAgentnodes: Initial messages not received - ✅
AgentWorkflowBuilder.BuildSequential(): Worked (had all handlers) - ❌ Any custom executor extending
ChatProtocolExecutor: Potentially broken
After Fix:
- ✅ Raw
WorkflowBuilderwithAIAgentnodes: Messages properly received - ✅
AgentWorkflowBuilder.BuildSequential(): Still works (no regression) - ✅ All
ChatProtocolExecutorderivatives: Consistent behavior
Files Changed
dotnet/src/Microsoft.Agents.AI.Workflows/ChatProtocolExecutor.cs- Added
IEnumerable<ChatMessage>handler - Added
ChatMessage[]handler - Added explanatory comment about exact type matching requirement
- Added
Validation
Created POC sample demonstrating sequential child group chats in parent workflow:
// Both approaches now work correctly
Workflow workflow = useRaw
? new WorkflowBuilder(firstGroupAgent)
.AddEdge(firstGroupAgent, secondGroupAgent)
.Build()
: AgentWorkflowBuilder.BuildSequential(firstGroupAgent, secondGroupAgent);Test Results:
- ✅ Raw
WorkflowBuilder: Agents receive initial messages, facilitator sees conversation history - ✅
BuildSequential: No regression, continues to work correctly - ✅ Sequential execution: Both group chats execute in order with proper message flow
PR Status: Preparing pull request...
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
stephentoub commentedon Oct 8, 2025
Does adding a handler for
IEnumerable<ChatMessage>actually help? As of #782, it wouldn't. The right fix is to address that.This is impossible :)
ltwlf commentedon Oct 8, 2025
It resolved the issue - I believe I recall this why the sequential builder worked.
You only have to map all types you use internally ;)
Happy if you have a better (quick) fix :)
ltwlf commentedon Oct 8, 2025
I suggest implementing the hot fix first. It’s incredibly frustrating to run into this issue and figure it out. I want to avoid others experiencing the same pain. Afterward, I recommend exploring a more elegant solution.
stephentoub commentedon Oct 8, 2025
What fix? Special-casing just
ChatMessage[]as well?ltwlf commentedon Oct 9, 2025
See #1337