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
// 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
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
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.
Description
HarnessAgent.BuildInnerAgentunconditionally builds its innerChatClientAgentwith
ChatHistoryProviderandRequirePerServiceCallChatHistoryPersistence = true— no
HarnessAgentOptionsflag disables either.This conflicts with Handoff orchestration
(
Microsoft.Agents.AI.Workflows.Specialized.HandoffAgentExecutor), which ownsthe 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, notHarnessAgent.Using a
HarnessAgentas a Handoff participant instead breaks the handofftool call — a declaration-only
AIFunction(
AIFunctionFactory.CreateDeclaration) intentionally left un-invoked soHandoffAgentExecutorcan intercept it. Observed symptoms in our app:of a
FunctionCallContent, e.g.{"reasonForHandoff":"..."}printed aschat output rather than executed as a tool call.
Likely cause: the executor reuses one
AgentSessionper participant acrossturns, resending the full conversation slice itself each time;
HarnessAgent'sforced
ChatHistoryProvidertracks its own copy of the conversation againstthat 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 minimalconsole repro. No official sample combines
HarnessAgentwithAgentWorkflowBuilder, and we found no existing issue covering thiscombination.
Code Sample
// 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 MAAIW001Error Messages / Stack Traces
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
HarnessAgentwithAgentWorkflowBuilder.HarnessAgent.BuildInnerAgent(dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs):HandoffAgentExecutor.InvokeAgentAsync(dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs):ChatHistoryProviderstate fighting an external conversation owner), none an exact match:
.NET: [Bug]: Checkpointing in Handoff Workflow #5621, .NET: [Bug]: FilterServerToolsFromMixedToolInvocationsAsync causes HTTP 400 when LLM returns mixed frontend + server-side tool calls in AGUI hosted mode #6967, PR .NET: Fix function approvals persistance with PerServiceCallChatHistoryPersistingChatClient #5805.
Suggested Fix
HarnessAgentOptionsflag to opt out ofChatHistoryProvider/RequirePerServiceCallChatHistoryPersistence, soHarnessAgentcan be usedas a stateless Workflow participant, or
HarnessAgentisn't meant to be used as a Workflow/Handoffparticipant.
Happy to help narrow this down further if useful.