Skip to content

.NET: keep the first request seen for a repeated request ID - #7947

Open
Yashvant Mahadev Hange (YashvantHange) wants to merge 2 commits into
microsoft:mainfrom
YashvantHange:dotnet-coalesce-repeated-request-ids
Open

.NET: keep the first request seen for a repeated request ID#7947
Yashvant Mahadev Hange (YashvantHange) wants to merge 2 commits into
microsoft:mainfrom
YashvantHange:dotnet-coalesce-repeated-request-ids

Conversation

@YashvantHange

Copy link
Copy Markdown
Contributor

Motivation & Context

A repeated CallId or RequestId in an agent's stream took down the whole workflow run.

AIAgentUnservicedRequestsCollector.ProcessAIContents threw InvalidOperationException when an ID was already in its dictionary. It is called once per streamed AgentResponseUpdate, so an agent that re-emits the same call across updates never reached its request: the throw escaped InvokeAgentAsync and the run ended in ExecutorFailedEvent plus WorkflowErrorEvent. Both AIAgentHostExecutor and HandoffAgentExecutor build this collector, and it is what workflow.AsAIAgent() runs on.

Repeating an ID inside one run identifies one pending request, not two. Two layers already say so and neither fails: AIContentExternalHandler.ProcessRequestContentAsync treats a repeat as an idempotent re-emission, and ApprovalResponseBindingChatClient keeps the first request recorded for an ID and binds the response against a snapshot of it.

Description & Review Guide

  • What are the major changes?

ProcessAIContents keeps the first content seen for a request ID instead of throwing, so all three layers now agree on one rule and the response a caller approves is bound to the request they were shown.

A later content under that ID that is not the same request — a different ToolCall.CallId under one approval ID, or a different function Name under one call ID — is still worth reporting, since only one of them can be serviced. SubmitAsync emits a single WorkflowWarningEvent naming those IDs rather than dropping them silently. A plain re-emission reports nothing.

HandoffAgentExecutor.CollectHandoffRequestsFilter had the same defect in its own collection: it appended on every emission, so one re-emitted handoff call produced Duplicate handoff requests in single turn ([handoff_to_x, handoff_to_x]). It now skips a candidate whose CallId is already collected, so that warning fires only for genuinely different handoff targets.

  • What is the impact of these changes?

A run survives a repeated request ID and raises the request once. No public API changed: AIAgentUnservicedRequestsCollector is internal and no signature moved.

Two things worth deciding rather than skimming. First, a genuinely distinct request colliding on an outstanding ID goes from fatal to dropped-with-a-warning; the reachable shape of that is an empty-string CallId fallback such as GitHubCopilotAgent.cs's toolStart.Data?.ToolCallId ?? string.Empty, where two tool starts key on "". Second, Magentic/StreamingToolCallResultPairMatcher still throws on a repeated CallId in a stream, so Specialized/ holds two answers to the same question until someone reconciles them; that path is separate and untouched here.

  • What do you want reviewers to focus on?

Whether first-wins is the rule you want, and whether the warning belongs in SubmitAsync or should stay silent.

Related Issue

Fixes #7946

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

AIAgentUnservicedRequestsCollector.ProcessAIContents threw when a CallId or
RequestId was already recorded. It runs once per streamed AgentResponseUpdate,
so an agent that re-emits one call across updates never reached its request:
the throw escaped InvokeAgentAsync and ended the run in ExecutorFailedEvent and
WorkflowErrorEvent. AIAgentHostExecutor and HandoffAgentExecutor both build this
collector, so both were affected.

A repeated ID inside one run is one pending request. The collector now keeps the
first content seen, matching AIContentExternalHandler, which treats a repeat as
an idempotent re-emission, and ApprovalResponseBindingChatClient, which binds a
response against the first request recorded for the ID.

A later content that is not the same request cannot be serviced alongside the
recorded one, so SubmitAsync reports those IDs in a WorkflowWarningEvent instead
of dropping them silently.

HandoffAgentExecutor collected handoff candidates on every emission, so one
re-emitted handoff call read as two competing handoffs. It now skips a candidate
whose CallId is already collected.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Coalesces repeated request IDs in .NET agent streams while preserving the first request and warning on conflicting requests.

Changes:

  • Implements first-wins request collection with collision warnings.
  • Deduplicates repeated streamed handoff calls.
  • Adds regression coverage for function, approval, and handoff requests.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
AIAgentUnservicedRequestsCollector.cs Coalesces repeated IDs and reports conflicts.
HandoffAgentExecutor.cs Deduplicates streamed handoff requests.
AIAgentHostExecutorTests.cs Tests warnings and first-wins behavior.
HandoffAgentExecutorTests.cs Tests repeated handoff handling.
WorkflowHostSmokeTests.cs Tests end-to-end repeated request handling.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs Outdated
The handoff candidate filter coalesced on call ID alone, so two different
handoff targets sharing one call ID collapsed into the first and the duplicate
handoff warning no longer fired for them. It now compares the target name
alongside the ID, which is the same rule the collector applies to a repeated
call, so only a genuine re-emission is coalesced.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

.NET Usage: [Issues, PRs], Target: .Net workflows Usage: [Issues, PRs], Target: Workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants