Add handoff agent-workflow builder (implements #520)#545
Conversation
…uilder) Implements the handoff orchestration gap tracked in microsoft#520. A handoff workflow is a group chat whose next speaker is chosen by the current agent itself: each agent is given a handoff tool for every target it may hand off to, and calling that tool routes the shared conversation to the target. The first agent is the entry agent; a turn with no handoff call ends the workflow. Reuses the existing group chat host executor and GroupChatManager plumbing rather than introducing a new engine: NewHandoffWorkflowBuilder injects per-agent handoff tools and supplies a handoff-flavored manager whose SelectNextAgent routes on the most recent handoff tool call in the previous speaker's turn. Adds a RunOptions field to the hosting Config so the builder can inject those tools into each hosted agent's run. Mirrors the existing builders' API (WithName/WithDescription/WithHandoff/ WithOutputFrom/Build) and the round-robin manager's checkpoint pattern. Covers routing, hand-back, no-handoff termination, builder validation, and manager checkpoint restore. Updates the .NET/Go feature comparison. Implements microsoft#520
- Add WithMaximumIterationCount to cap handoff loops (e.g. two agents that keep handing off), wired through the manager's ShouldTerminate; defaults to the group chat's 40-turn cap. - Reject self-handoff (WithHandoff(a, a)) in Build with a clear error. - Document that participant agents must have automatic function-tool calling enabled so injected handoff tools execute (otherwise the handoff call is unresolved and stalls the run). - On an inconsistent restored cursor beyond the history, end cleanly instead of rescanning from the start and routing on a stale handoff. - Comment the manager factory's ignored agents argument. Adds tests: last-of-multiple handoff calls wins, unknown handoff tool is ignored, the iteration cap stops a ping-pong loop, and colliding sanitized tool names are rejected at Build.
There was a problem hiding this comment.
Pull request overview
Adds a first-class “handoff” workflow builder to the workflow/agentworkflow package by composing on top of the existing group-chat hosting/manager plumbing, and updates hosting configuration to support builder-injected per-run agent options.
Changes:
- Introduces
NewHandoffWorkflowBuilderplus a handoff-specificGroupChatManagerimplementation that routes based on handoff tool calls. - Extends
agentworkflow.ConfigwithRunOptions []agent.Optionand applies those options during hosted agent runs. - Adds
handoff_test.gocoverage for routing/termination/validation/checkpoint restore, and updates the .NET vs Go feature comparison docs.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| workflow/agentworkflow/hosting.go | Adds Config.RunOptions and applies them when invoking hosted agents. |
| workflow/agentworkflow/handoff.go | New handoff builder + manager implementation that injects per-agent handoff tools and routes the next speaker. |
| workflow/agentworkflow/handoff_test.go | New tests covering handoff routing, validation, termination behavior, and checkpoint restore. |
| docs/dotnet-go-sdk-feature-comparison.md | Updates feature comparison to reflect the new handoff builder. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -404,6 +411,7 @@ | |||
| // Run the agent in streaming mode only when update events are to be emitted. | |||
| agent.Stream(emitUpdates), | |||
| } | |||
| runOpts = append(runOpts, h.cfg.RunOptions...) | |||
There was a problem hiding this comment.
Fixed — RunOptions are now applied before the host's WithSession/Stream, which are resolved last and take precedence, so a RunOptions entry can no longer override the managed session or streaming mode.
| // RunOptions are additional [agent.Option] values passed to the hosted | ||
| // agent on every run, appended after the host's own options (session and | ||
| // streaming mode). They let a workflow builder inject per-agent behavior — | ||
| // for example the handoff tools added by [NewHandoffWorkflowBuilder] — that | ||
| // is not part of the agent's own configuration. | ||
| RunOptions []agent.Option |
There was a problem hiding this comment.
Fixed — the doc now states RunOptions are applied before the host's session/stream options, which are resolved last and take precedence, so RunOptions cannot override the managed session or stream mode.
| call, ok := content.(*message.FunctionCallContent) | ||
| if !ok { | ||
| continue | ||
| } | ||
| if next, ok := m.toolTargets[call.Name]; ok { |
There was a problem hiding this comment.
Enforcement is at the tool level: each agent is only injected the handoff_to_<target> tools for the targets granted via WithHandoff, so an agent cannot emit a handoff call for a target it was not granted. toolTargets is the shared resolver the routing cursor reads. Happy to add an explicit per-agent guard for defense-in-depth if you'd prefer.
Apply RunOptions before the host's own WithSession/Stream so those are resolved last and take precedence; a RunOptions entry can no longer override the managed session or streaming mode. Update the doc to match.
Implements the handoff orchestration gap from #520. Opening as a draft to align on the API/mechanism before finalizing — happy to adjust to the team's steer (the design questions in #520 are resolved here with sensible defaults, noted below).
What
A first-class handoff builder alongside the existing sequential / concurrent / group-chat builders:
A handoff workflow is a group chat whose next speaker is chosen by the current agent: each agent is given a
handoff_to_<target>tool for every target it may hand off to; calling that tool routes the shared conversation to the target. The first agent is the entry agent; a turn with no handoff call ends the workflow.How (reuse, not a new engine)
NewHandoffWorkflowBuilderinjects per-agent handoff tools and supplies a handoff-flavoredGroupChatManagerwhoseSelectNextAgentroutes on the most recent handoff tool call in the previous speaker's turn (a cursor tracks per-turn scanning and is checkpoint-persisted, mirroring the round-robin manager'sNextIndex).groupChatHostExecutor, edges, output designations, andprefixingWorkflowContextcheckpoint plumbing.agentworkflow.Config: aRunOptions []agent.Optionfield so the builder can inject the handoff tools into each hosted agent's run (hosting.go). No existing signatures change.Design decisions (the open questions from #520)
.NET-faithful auto-injectedhandoff_to_<target>function tools. Because they carry a handler they auto-execute, so the (resolved) call flows into history for the manager to read — noInterceptUnterminatedFunctionCallsneeded.GroupChatManagervariant; no duplicate engine.WithHandoff(billing, triage)).Tests
handoff_test.go(white-box, matching this package's convention) covers: routing on a handoff call, hand-back (triage→billing→triage), no-handoff termination, builder validation (nil/non-participant agents & targets), and handoff-manager checkpoint restore. All green under-race -shuffle=on; full suite passes. Updatesdocs/dotnet-go-sdk-feature-comparison.md.Open for steer
Naming (
WithHandoffvs edge-style), whether handoff instructions should be auto-added to agent system prompts (.NETdoes), and whether to add anexamples/03-workflowssample once the API is agreed — I'll follow up per the team's preference.