Skip to content

.NET: [Bug]: a repeated CallId or RequestId in an agent stream fails the whole workflow run #7946

Description

Description

An agent host executor fails the whole workflow run when an agent's stream carries the same CallId or RequestId in more than one AgentResponseUpdate.

AIAgentUnservicedRequestsCollector.ProcessAgentResponseUpdate is called once per streamed update, and ProcessAIContents throws when the ID is already in its dictionary:

if (this._userInputRequests.ContainsKey(userInputRequest.RequestId))
{
    throw new InvalidOperationException($"ToolApprovalRequestContent with duplicate RequestId: {userInputRequest.RequestId}");
}
...
if (this._functionCalls.ContainsKey(functionCall.CallId))
{
    throw new InvalidOperationException($"FunctionCallContent with duplicate CallId: {functionCall.CallId}");
}

The throw escapes InvokeAgentAsync, so the run ends in ExecutorFailedEvent + WorkflowErrorEvent rather than reaching the agent's request. Both AIAgentHostExecutor and HandoffAgentExecutor construct this collector, so both are affected, and the same executor is what workflow.AsAIAgent() runs on.

Repeating an ID across updates is a re-emission of one pending request, not two simultaneous requests. AIContentExternalHandler.ProcessRequestContentAsync, the layer this collector feeds, already says so and handles it without failing:

if (!this._pendingRequests.TryAdd(id, requestContent))
{
    // Request is already pending; treat as an idempotent re-emission.
    // Do not repost to the sink because request IDs must remain unique while pending.
    return default;
}

So one layer treats the repeat as idempotent while the layer above it kills the run.

Expected behavior

A repeated CallId or RequestId inside a single agent run is coalesced into the one pending request it belongs to, and the run continues to raise that request.

Code Sample

A custom AIAgent is a supported extension point, and this is the smallest thing that reproduces it. Streaming that repeats a call while argument chunks accumulate has the same shape.

internal sealed class RepeatingRequestAgent : AIAgent
{
    // Session/serialization members omitted.

    protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
        IEnumerable<ChatMessage> messages,
        AgentSession? session = null,
        AgentRunOptions? options = null,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        yield return new AgentResponseUpdate(ChatRole.Assistant,
            [new FunctionCallContent("call-1", "doThing", new Dictionary<string, object?> { ["a"] = 1 })]);
        yield return new AgentResponseUpdate(ChatRole.Assistant,
            [new FunctionCallContent("call-1", "doThing", new Dictionary<string, object?> { ["a"] = 1, ["b"] = 2 })]);

        await Task.CompletedTask.ConfigureAwait(false);
    }
}

ExecutorBinding binding = new RepeatingRequestAgent().BindAsExecutor(
    new AIAgentHostOptions { InterceptUnterminatedFunctionCalls = false, EmitAgentUpdateEvents = true });
Workflow workflow = new WorkflowBuilder(binding).Build();

List<AgentResponseUpdate> updates = await workflow.AsAIAgent("WorkflowAgent", includeExceptionDetails: true)
                                                  .RunStreamingAsync(new ChatMessage(ChatRole.User, "Hi"))
                                                  .ToListAsync();

Observed update stream:

WorkflowStartedEvent | SuperStepStartedEvent | ExecutorInvokedEvent | ExecutorCompletedEvent
| ExecutorInvokedEvent | [FunctionCallContent:call-1] | [FunctionCallContent:call-1]
| ExecutorFailedEvent [ErrorContent: FunctionCallContent with duplicate CallId: call-1]
| WorkflowErrorEvent [ErrorContent: FunctionCallContent with duplicate CallId: call-1]

Two updates carrying new ToolApprovalRequestContent("req-1", mcpCall) with InterceptUserInputRequests = false fail the same way:

ExecutorFailedEvent [ErrorContent: ToolApprovalRequestContent with duplicate RequestId: req-1]
| WorkflowErrorEvent [ErrorContent: ToolApprovalRequestContent with duplicate RequestId: req-1]

Error Messages / Stack Traces

System.InvalidOperationException: FunctionCallContent with duplicate CallId: call-1
   at Microsoft.Agents.AI.Workflows.Specialized.AIAgentUnservicedRequestsCollector.ProcessAIContents(...)
   at Microsoft.Agents.AI.Workflows.Specialized.AIAgentHostExecutor.InvokeAgentAsync(...)

System.InvalidOperationException: ToolApprovalRequestContent with duplicate RequestId: req-1
   at Microsoft.Agents.AI.Workflows.Specialized.AIAgentUnservicedRequestsCollector.ProcessAIContents(...)

Package Versions

Microsoft.Agents.AI.Workflows from main at edfe115e

.NET Version

net10.0

Additional Context

I would like to take this one. The fix I have in mind keeps the collector's dictionaries as the single record of a pending request and treats a repeat as an update of that record rather than an error, matching what AIContentExternalHandler already does, with regression tests on both the function-call and approval paths for the plain agent binding and for handoff.

Metadata

Metadata

Assignees

No one assigned

    Labels

    .NETUsage: [Issues, PRs], Target: .NettriageUsage: [Issues], Target: All issues that still need to be triaged

    Type

    No type

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions