microsoft / microsoft/agent-framework
.NET: [Bug]: AgentWorkflowBuilder Handoff orchestration breaks when handoffs bounce back, especially with reasoning
@peibekwe is already working on this.
Since Aug 25, 2026.
- Dominant language
- Python
- Stars
- 13.6k
- Forks
- 2.3k
- Avg merge
- 2d 45m
- Merged PRs (30d)
- 358
Description
### Description
`HarnessAgent.BuildInnerAgent` unconditionally builds its inner `ChatClientAgent`
with `ChatHistoryProvider` and `RequirePerServiceCallChatHistoryPersistence = true`
— no `HarnessAgentOptions` flag disables either.
This conflicts with Handoff orchestration
(`Microsoft.Agents.AI.Workflows.Specialized.HandoffAgentExecutor`), which owns
the full multi-party conversation itself and re-invokes each participant as a
**stateless** function, resending the relevant message slice every turn. The
official Handoff sample (`dotnet/samples/03-workflows/Orchestration/Handoff`)
builds participants with `chatClient.AsAIAgent(...)` — stateless, not
`HarnessAgent`.
Using a `HarnessAgent` as a Handoff participant instead breaks the handoff
tool call — a declaration-only `AIFunction`
(`AIFunctionFactory.CreateDeclaration`) intentionally left un-invoked so
`HandoffAgentExecutor` can intercept it. Observed symptoms in our app:
- A turn producing no response at all (silent hang).
- The handoff tool's arguments emitted as literal assistant **text** instead
of a `FunctionCallContent`, e.g. `{"reasonForHandoff":"..."}` printed as
chat output rather than executed as a tool call.
Likely cause: the executor reuses one `AgentSession` per participant across
turns, resending the full conversation slice itself each time; `HarnessAgent`'s
forced `ChatHistoryProvider` tracks its *own* copy of the conversation against
that same session. The two diverge, and the model ends up with a
confused/duplicated context instead of emitting a clean tool call.
**Caveat:** this is backed by the observed app-level symptoms plus a source
read of `HarnessAgent.cs`/`HandoffAgentExecutor.cs`, not an isolated minimal
console repro. No official sample combines `HarnessAgent` with
`AgentWorkflowBuilder`, and we found no existing issue covering this
combination.
### Code Sample
```markdown
// Works today — the official Handoff sample's pattern.
AIAgent workingParticipant = chatClient.AsAIAgent(instructions: "...", name: "triage");
// Breaks — HarnessAgent with everything non-essential disabled.
AIAgent brokenParticipant = chatClient.AsHarnessAgent(new HarnessAgentOptions
{
Name = "triage",
HarnessInstructions = string.Empty,
ChatOptions = new ChatOptions { Instructions = "..." },
DisableToolAutoApproval = true,
DisableWebSearch = true,
DisableFileMemory = true,
DisableTodoProvider = true,
DisableAgentModeProvider = true,
DisableAgentSkillsProvider = true,
DisableOpenTelemetry = true,
// No flag disables ChatHistoryProvider / RequirePerServiceCallChatHistoryPersistence.
});
#pragma warning disable MAAIW001
Workflow workflow = AgentWorkflowBuilder
.CreateHandoffBuilderWith(brokenParticipant) // swap in workingParticipant to compare
.WithHandoff(brokenParticipant, someExpertAgent, "reason for handoff")
.Build();
#pragma warning restore MAAIW001
```
### Error Messages / Stack Traces
```markdown
No exception — the failure is behavioral:
Turn 1 (expected a silent handoff): no response streamed at all.
Turn 2 (expected a silent handoff to a different expert):
Assistant: {"reasonForHandoff":"order status question, belongs to the Orders domain"}
The second line is the handoff tool's JSON arguments as plain text, not a
`FunctionCallContent` named `handoff_N`.
```
### Package Versions
Microsoft.Agents.AI: 1.15.0, Microsoft.Agents.AI.Harness: 1.15.0, Microsoft.Agents.AI.Workflows: 1.15.0, Microsoft.Agents.AI.Hosting.AGUI.AspNetCore: 1.15.0-preview.260722.1
### .NET Version
.NET 10.0
### Additional Context
- `dotnet/samples/02-agents/Harness/*` (`Harness_Step01_Research` …
`Harness_Step05_Loop`, `BuildYourOwnClaw`) are all single-agent console apps
— none combine `HarnessAgent` with `AgentWorkflowBuilder`.
- `HarnessAgent.BuildInnerAgent` (`dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs`):
```csharp
return chatClientBuilder.BuildAIAgent(new ChatClientAgentOptions
{
ChatHistoryProvider = chatHistoryProvider, // InMemoryChatHistoryProvider by default
RequirePerServiceCallChatHistoryPersistence = true, // hardcoded
...
});
```
- `HandoffAgentExecutor.InvokeAgentAsync` (`dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs`):
```csharp
this._session ??= await this._agent.CreateSessionAsync(cancellationToken).ConfigureAwait(false);
this._agent.RunStreamingAsync(messages, this._session, this._agentOptions, cancellationToken);
```
- Possibly related (same class of problem — persisted `ChatHistoryProvider`
state fighting an external conversation owner), none an exact match:
#5621, #6967, PR #5805.
### Suggested Fix
- Add a `HarnessAgentOptions` flag to opt out of `ChatHistoryProvider`/
`RequirePerServiceCallChatHistoryPersistence`, so `HarnessAgent` can be used
as a stateless Workflow participant, **or**
- document that `HarnessAgent` isn't meant to be used as a Workflow/Handoff
participant.
Happy to help narrow this down further if useful.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.