Skip to content

Add handoff agent-workflow builder (implements #520)#545

Open
PratikDhanave wants to merge 4 commits into
microsoft:mainfrom
PratikDhanave:feat-handoff-workflow-builder
Open

Add handoff agent-workflow builder (implements #520)#545
PratikDhanave wants to merge 4 commits into
microsoft:mainfrom
PratikDhanave:feat-handoff-workflow-builder

Conversation

@PratikDhanave

@PratikDhanave PratikDhanave commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

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:

wf, err := agentworkflow.NewHandoffWorkflowBuilder(triage, billing, refunds).
    WithHandoff(triage, billing, refunds). // triage may hand off to billing or refunds
    WithHandoff(billing, triage).          // billing may hand back to triage
    WithOutputFrom(triage, billing, refunds).
    Build()

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)

  • NewHandoffWorkflowBuilder injects per-agent handoff tools and supplies a handoff-flavored GroupChatManager whose SelectNextAgent routes 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's NextIndex).
  • Rides the existing groupChatHostExecutor, edges, output designations, and prefixingWorkflowContext checkpoint plumbing.
  • One additive change to agentworkflow.Config: a RunOptions []agent.Option field 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)

  1. Tools, not edges.NET-faithful auto-injected handoff_to_<target> function tools. Because they carry a handler they auto-execute, so the (resolved) call flows into history for the manager to read — no InterceptUnterminatedFunctionCalls needed.
  2. Reuse the group-chat manager — handoff is a GroupChatManager variant; no duplicate engine.
  3. Termination — a turn with no handoff call ends the workflow (plus the host's existing 40-turn safety cap).
  4. Return-to-previous — supported by declaring reverse handoffs (WithHandoff(billing, triage)).
  5. Scope — handoff only; Magentic tracked separately.

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. Updates docs/dotnet-go-sdk-feature-comparison.md.

Open for steer

Naming (WithHandoff vs edge-style), whether handoff instructions should be auto-added to agent system prompts (.NET does), and whether to add an examples/03-workflows sample once the API is agreed — I'll follow up per the team's preference.

…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.
@PratikDhanave
PratikDhanave marked this pull request as ready for review July 19, 2026 08:29
@PratikDhanave
PratikDhanave requested a review from a team as a code owner July 19, 2026 08:29
Copilot AI review requested due to automatic review settings July 19, 2026 08:29

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

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 NewHandoffWorkflowBuilder plus a handoff-specific GroupChatManager implementation that routes based on handoff tool calls.
  • Extends agentworkflow.Config with RunOptions []agent.Option and applies those options during hosted agent runs.
  • Adds handoff_test.go coverage 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.

Comment thread workflow/agentworkflow/hosting.go Outdated
Comment on lines +410 to +414
@@ -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...)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment on lines +111 to +116
// 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment on lines +323 to +327
call, ok := content.(*message.FunctionCallContent)
if !ok {
continue
}
if next, ok := m.toolTargets[call.Name]; ok {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants