Skip to content

.NET: ChatProtocolExecutor Missing Collection Type Handlers #1326

Closed

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 as List<T> - no match, messages lost
  • Removing IEnumerable<ChatMessage>: Runtime may dispatch as IEnumerable<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 WorkflowBuilder with AIAgent nodes: Initial messages not received
  • ✅ AgentWorkflowBuilder.BuildSequential(): Worked (had all handlers)
  • ❌ Any custom executor extending ChatProtocolExecutor: Potentially broken

After Fix:

  • ✅ Raw WorkflowBuilder with AIAgent nodes: Messages properly received
  • ✅ AgentWorkflowBuilder.BuildSequential(): Still works (no regression)
  • ✅ All ChatProtocolExecutor derivatives: 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

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...

Activity

  1. added
    .NETUsage: [Issues, PRs], Target: .Net
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 8, 2025
  2. stephentoub commented on Oct 8, 2025

    @stephentoub
    Member

    Does adding a handler for IEnumerable<ChatMessage> actually help? As of #782, it wouldn't. The right fix is to address that.

    Add handlers for all collection types

    This is impossible :)

  3. ltwlf commented on Oct 8, 2025

    @ltwlf
    ContributorAuthor

    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 :)

  4. ltwlf commented on Oct 8, 2025

    @ltwlf
    ContributorAuthor

    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.

  5. stephentoub commented on Oct 8, 2025

    @stephentoub
    Member

    I suggest implementing the hot fix first.

    What fix? Special-casing just ChatMessage[] as well?

  6. ltwlf commented on Oct 9, 2025

    @ltwlf
    ContributorAuthor

    See #1337

  7. added
    bugUsage: [Issues], Target: all issues (Legacy, prefer issue type: bug)
    and removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

.NETUsage: [Issues, PRs], Target: .NetbugUsage: [Issues], Target: all issues (Legacy, prefer issue type: bug)

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions