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.
Description
An agent host executor fails the whole workflow run when an agent's stream carries the same
CallIdorRequestIdin more than oneAgentResponseUpdate.AIAgentUnservicedRequestsCollector.ProcessAgentResponseUpdateis called once per streamed update, andProcessAIContentsthrows when the ID is already in its dictionary:The throw escapes
InvokeAgentAsync, so the run ends inExecutorFailedEvent+WorkflowErrorEventrather than reaching the agent's request. BothAIAgentHostExecutorandHandoffAgentExecutorconstruct this collector, so both are affected, and the same executor is whatworkflow.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:So one layer treats the repeat as idempotent while the layer above it kills the run.
Expected behavior
A repeated
CallIdorRequestIdinside 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
AIAgentis 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.Observed update stream:
Two updates carrying
new ToolApprovalRequestContent("req-1", mcpCall)withInterceptUserInputRequests = falsefail the same way:Error Messages / Stack Traces
Package Versions
Microsoft.Agents.AI.Workflowsfrommainatedfe115e.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
AIContentExternalHandleralready does, with regression tests on both the function-call and approval paths for the plain agent binding and for handoff.