microsoft / microsoft/agent-framework

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

Open
#7,946 1 comment 0 reactions 1 assignee View on GitHub

@peibekwe is already working on this.

Since Aug 31, 2026.

.NET reproduced workflows
Dominant language
Python
Stars
13.6k
Forks
2.3k
Avg merge
2d 45m
Merged PRs (30d)
358

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:

```csharp
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:

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

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

protected override async IAsyncEnumerable RunCoreStreamingAsync(
IEnumerable messages,
AgentSession? session = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
yield return new AgentResponseUpdate(ChatRole.Assistant,
[new FunctionCallContent("call-1", "doThing", new Dictionary { ["a"] = 1 })]);
yield return new AgentResponseUpdate(ChatRole.Assistant,
[new FunctionCallContent("call-1", "doThing", new Dictionary { ["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 updates = await workflow.AsAIAgent("WorkflowAgent", includeExceptionDetails: true)
.RunStreamingAsync(new ChatMessage(ChatRole.User, "Hi"))
.ToListAsync();
```

Observed update stream:

```text
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:

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

### Error Messages / Stack Traces

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.