Skip to content

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

Open
PratikDhanave (PratikDhanave) wants to merge 6 commits into
microsoft:mainfrom
PratikDhanaveFork:feat-handoff-workflow-builder
Open

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

Conversation

@PratikDhanave

@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.

@PratikDhanave
PratikDhanave (PratikDhanave) marked this pull request as ready for review July 19, 2026 08:29
@PratikDhanave
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.

@github-actions

This comment has been minimized.

@github-actions github-actions Bot 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.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

Generated by Go API Consistency Review Agent · 87.4 AIC · ⌖ 4.57 AIC · ⊞ 5.7K

// WithHandoff allows from to hand off the conversation to each of targets. It
// may be called multiple times to extend an agent's set of targets. Both from
// and every target must be participants passed to [NewHandoffWorkflowBuilder].
func (b *HandoffWorkflowBuilder) WithHandoff(from *agent.Agent, targets ...*agent.Agent) *HandoffWorkflowBuilder {

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.

Parity gap — WithHandoff missing per-edge handoffReason

The .NET HandoffWorkflowBuilder.WithHandoff(from, to, handoffReason?) accepts an optional reason string per edge that becomes the tool description surfaced to the LLM. Go currently accepts only source and variadic targets; every resulting tool description is a hardcoded generic sentence (see newHandoffTool).

In .NET the reason falls back to the target agent's Description when not supplied. Models therefore receive richer context about when to hand off versus the current Go behavior.

Upstream reference: dotnet/src/Microsoft.Agents.AI.Workflows/HandoffWorkflowBuilder.csWithHandoff(AIAgent from, AIAgent to, string? handoffReason = null).

Suggestion: keep the variadic convenience form but add a per-edge option (e.g. a scalar overload WithHandoff(from, to, reason) or an options struct) and fall back to to.Description() when not provided — matching the .NET resolution logic.

Description: fmt.Sprintf("Hand off the conversation to the %q agent. Call this when the request should be handled by that agent instead of you.", targetName),
},
func(context.Context, handoffToolArgs) (string, error) {
return fmt.Sprintf("Handing off to %s.", targetName), nil

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.

Parity gap — handoff system-prompt instructions not injected

The .NET builder auto-injects a HandoffInstructions string into each participating agent's system prompt:

You are one agent in a multi-agent system. You can hand off the conversation to another agent if appropriate. Handoffs are achieved by calling a handoff function, named in the form `handoff_to_<agent_id>`...

This is configurable via WithHandoffInstructions(string?) and defaults to a well-crafted paragraph that also asks agents not to mention handoffs to the user. The Go implementation currently omits this entirely — models receive only the tool descriptions and no system-level context about being part of a handoff workflow.

Upstream reference: dotnet/src/Microsoft.Agents.AI.Workflows/HandoffWorkflowBuilder.csHandoffInstructions property, WithHandoffInstructions() method, DefaultHandoffInstructions constant.

The PR description acknowledges this as an open question, which is good. It should be resolved before the builder is considered aligned with .NET. At minimum, add a WithHandoffInstructions(string) builder method and an opt-out path (empty/nil) — injecting the instructions via the existing RunOptions/agent.WithSystemPromptSuffix mechanism should work without changing the agent itself.


// handoffToolName derives the handoff tool name presented to the model for a
// target agent, from its name (or ID when unnamed).
func handoffToolName(target *agent.Agent) string {

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.

Subtle parity divergence — tool name derived from agent name, not agent ID

The .NET FunctionPrefix constant documents: "the full name is then handoff_to_<agent_id>, where <agent_id> is the ID of the target agent to hand off to." The Go implementation instead derives the tool name from the agent's display name (falling back to ID only when the name is blank):

base := target.Name()
if strings.TrimSpace(base) == "" {
    base = target.ID()
}

This means:

  1. Two agents with the same display name (but different IDs) will produce a tool-name collision detected at build time — which is correct and caught.
  2. An agent whose display name changes between runs could produce a different tool name, breaking checkpoint-restored routing (though in practice the agent name is set at construction).
  3. The tool names presented in the LLM context will differ from the .NET convention when agent IDs and names diverge.

This is a minor but real divergence. If the intent is to keep names human-readable for the model, it is a reasonable Go-specific choice; if so, a brief doc comment explaining the deliberate difference from .NET would help maintainers.

| Workflow as agent | Workflow host agent / `AsAIAgent`, sample. | `agentworkflow.NewAgent`. | Aligned | Go chooses in-process environment based on concurrency; .NET is integrated with `AIAgent` extensions. |
| Subworkflows | `ConfigureSubWorkflow`, `BindAsExecutor`, subworkflow sample. | `inproc.BindSubworkflowAsExecutor` plus in-process subworkflow execution. | Aligned | Go exposes subworkflow binding from the in-process execution package. |
| Handoff orchestration | Handoff workflow builder with handoff instructions, tool-call filtering, return-to-previous, response/update events. | No first-class handoff builder. | .NET only | Could be modeled manually with tools/workflows, but no SDK feature. |
| Handoff orchestration | Handoff workflow builder with handoff instructions, tool-call filtering, return-to-previous, response/update events. | `agentworkflow.NewHandoffWorkflowBuilder` with `WithHandoff` handoff edges; per-agent handoff tools route the shared conversation to the chosen target, with return-to-previous supported by declaring reverse handoffs. | Aligned | Go now has a first-class handoff builder; the current turn's agent hands off by calling an injected `handoff_to_<target>` tool, reusing the group chat host and manager plumbing. |

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.

Docs parity assessment is premature — should remain "Partial"

This row changes the parity verdict from .NET only to Aligned, but several meaningful .NET features are still absent from the Go builder:

.NET feature Go status
WithHandoffInstructions / auto-injected system prompt Missing (open question in PR)
Per-edge handoffReason (used as tool description) Missing
WithTerminationCondition (custom callback) Missing (Go only has max-iteration count)
WithToolCallFilteringBehavior Missing
WithAutonomousMode Missing (lower priority, but a .NET feature)
EnableReturnToPrevious Present in Go (via reverse WithHandoff edges)

Suggesting changing the verdict to Partial and updating the description to note the gaps remaining, so the comparison table stays accurate.

@github-actions

This comment has been minimized.

@github-actions github-actions Bot added the public-api-change Pull Request changes public APIs label Jul 24, 2026
…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.
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.
@github-actions

This comment has been minimized.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Parity Review: PR #545 — Handoff Workflow Builder

The PR adds public Go APIs (HandoffWorkflowBuilder, WithHandoff, WithOutputFrom, WithIntermediateOutputFrom, WithMaximumIterationCount, WithName, WithDescription, Build) and extends Config with RunOptions. These are all user-visible exports; the public-api-change label is already correctly applied.

The implementation is well-grounded in the upstream .NET pattern (tools named handoff_to_<target>, group-chat reuse, checkpoint-persisted manager state). However, there are three meaningful parity gaps against the .NET (HandoffWorkflowBuilder.cs) and Python (_handoff.py) implementations that are worth tracking before the API is finalised.


1. System-prompt handoff instructions are not auto-injected (🔴 substantive gap)

Upstream behaviour (.NET): HandoffWorkflowBuilderCore maintains a HandoffInstructions string (with a baked-in default) and automatically injects it into every participant agent's system prompt when the workflow is built. The instructions explain the handoff_to_<target> mechanic to the model and tell it not to narrate handoffs to the user. A builder method WithHandoffInstructions(string?) lets callers override or suppress the default.

Go behaviour: The Go builder only injects the handoff tools; it does not add any system-level instruction to tell the model how or when to use them. Without this context, models that are not already aware of the handoff convention will tend to ignore or mis-use the tools.

Suggested alignment: Add a default instructions string (mirroring the .NET default) and inject it via each agent's cfg.RunOptions (e.g. agent.WithSystemPrompt or equivalent). Expose WithHandoffInstructions(instructions string) on the builder to allow callers to override or clear the default, matching the .NET surface.

Reference: dotnet/src/Microsoft.Agents.AI.Workflows/HandoffWorkflowBuilder.csDefaultHandoffInstructions, WithHandoffInstructions.


2. Per-edge handoff reason / tool description is not customisable (🟡 minor gap)

Upstream behaviour (.NET & Python): Both SDKs let callers provide a custom reason (description) for each handoff edge — WithHandoff(from, to, handoffReason: string?) in .NET and add_handoff(..., description=...) in Python. When omitted, the reason is derived from the target agent's description or name. This becomes the tool's description string shown to the model, so it is load-bearing for routing quality.

Go behaviour: newHandoffTool hard-codes the description as "Hand off the conversation to the \"<name>\" agent. Call this when the request should be handled by that agent instead of you." Callers have no way to supply a custom reason per edge.

Suggested alignment: Add an optional reason string parameter (or variadic option) to WithHandoff, and thread it through to newHandoffTool so it becomes the tool description when provided. Fall back to the current auto-generated string when absent.

Reference: dotnet/src/Microsoft.Agents.AI.Workflows/HandoffWorkflowBuilder.csWithHandoff(AIAgent from, AIAgent to, string? handoffReason).


3. No example added — upstream has a dedicated sample (🟡 tracking item)

The PR description explicitly defers the example (examples/03-workflows). The upstream .NET repo already has dotnet/samples/03-workflows/Orchestration/Handoff/ demonstrating the full handoff flow; Python has equivalent coverage in its samples tree. Go examples/ has no handoff counterpart yet.

This is a known gap, not a blocker, but it should be tracked as a follow-up to maintain sample parity once the API shape is agreed.


What is well-aligned

  • Tool naming convention (handoff_to_<target>) matches both .NET (FunctionPrefix = "handoff_to_") and Python (get_handoff_tool_name).
  • Checkpoint persistence of manager state (handoffManagerState{Started, Cursor}) mirrors the round-robin manager pattern and matches the spirit of on_checkpoint_save/restore in Python.
  • Reuse of the group-chat executor rather than a new engine matches the .NET HandoffsWorkflowBuilder approach.
  • Return-to-previous is supported via reverse WithHandoff declarations — semantically equivalent to .NET EnableReturnToPrevious (which auto-wires reverse edges under the hood).
  • The WithMaximumIterationCount / 40-turn default aligns with both SDKs' loop-guard semantics.

The most important item to resolve before the API is marked stable is gap 1 (system-prompt injection); gap 2 is strongly recommended for routing quality parity.

Generated by Go API Consistency Review Agent · sonnet46 · 38.4 AIC · ⌖ 4.97 AIC · ⊞ 5.7K ·

@gdams

Copy link
Copy Markdown
Member

PratikDhanave (@PratikDhanave) can you resolve the parity gaps?

@github-actions github-actions Bot added area:workflow Changes files in the workflow area size:xlarge More than 300 changed lines or 10 files pending-auto-risk Automatic risk classification is in progress labels Aug 20, 2026

@github-actions github-actions Bot 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.

Generated by Go API Consistency Review Agent · sonnet46 · 37.8 AIC · ⌖ 4.91 AIC · ⊞ 6K

Description: fmt.Sprintf("Hand off the conversation to the %q agent. Call this when the request should be handled by that agent instead of you.", targetName),
},
func(context.Context, handoffToolArgs) (string, error) {
return fmt.Sprintf("Handing off to %s.", targetName), nil

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.

Parity gap: missing handoff system-prompt injection (.NET / Python)

Both the .NET and Python upstream implementations automatically append handoff instructions to each participating agent's system prompt when handoff tools are injected. This orients the agent so it knows when and how to call the tool.

.NET default (HandoffWorkflowBuilder.DefaultHandoffInstructions): appended to each agent that receives handoff tools, explaining the transfer_to_(id) function mechanics and instructing the agent never to narrate handoffs.

Python (_prepare_agent_with_handoffs): similarly augments each cloned agent's options with per-target context.

The Go implementation injects the handoff tools but does not append any system-level instructions. Without this, agents may call the wrong tool, ignore it, or narrate the handoff to the user — all behaviors the upstream defaults suppress.

The PR author acknowledges this is open. To reach parity before this API is stable:

  1. Add default handoff instruction injection (e.g., via a new agent option) applied in Config.RunOptions alongside the tool injection.
  2. Expose WithHandoffInstructions(string) on the builder so callers can customize or suppress the default, mirroring .NET's WithHandoffInstructions(string?).

Upstream references:

  • dotnet/src/Microsoft.Agents.AI.Workflows/HandoffWorkflowBuilder.cs — DefaultHandoffInstructions, WithHandoffInstructions()
  • python/packages/orchestrations/agent_framework_orchestrations/_handoff.py — _prepare_agent_with_handoffs()


// handoffToolArgs is the (empty) argument object for a handoff tool. The model
// calls the tool with {} to hand off; no parameters are required.
type handoffToolArgs struct{}

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.

Parity gap: handoff tool description ignores target agent description (.NET / Python)

In .NET, WithHandoff(from, to, handoffReason?) derives the tool description from the target agent's Description, then Name, then Instructions. If none is available it throws at build time, forcing callers to either supply agent metadata or pass an explicit reason. This gives the LLM meaningful guidance on why to route to a specific agent.

In Python, _create_handoff_tool similarly accepts a description (defaulting to 'Handoff to the (id) agent.' only when none is provided).

The Go implementation produces a fixed generic description ('Hand off the conversation to the "X" agent. Call this when the request should be handled by that agent instead of you.') regardless of what description the target agent carries.

Suggested fix: fall back to the target agent's Description (if exposed) before using the generic template, and optionally expose a per-handoff reason parameter on WithHandoff, mirroring .NET's handoffReason.

Upstream reference:

  • dotnet/src/Microsoft.Agents.AI.Workflows/HandoffWorkflowBuilder.cs — WithHandoff(from, to, handoffReason?) description resolution

@github-actions github-actions Bot added failed-auto-risk Automatic risk classification was inconclusive or failed and removed pending-auto-risk Automatic risk classification is in progress labels Aug 20, 2026
@github-actions github-actions Bot added pending-auto-risk Automatic risk classification is in progress risk:medium Contained production impact requiring normal review depth and removed failed-auto-risk Automatic risk classification was inconclusive or failed pending-auto-risk Automatic risk classification is in progress labels Aug 22, 2026

@github-actions github-actions Bot 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.

Generated by Go API Consistency Review Agent · sonnet46 · 36.4 AIC · ⌖ 5.29 AIC · ⊞ 6K

func (b *HandoffWorkflowBuilder) WithName(name string) *HandoffWorkflowBuilder {
if b == nil || b.err != nil {
return b
}

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.

Parity gap — handoff instructions not injected into agent system prompts

Both the .NET and Python implementations automatically inject a default system-level instruction into each participating agent explaining when and how to call the handoff_to_<target> tool. Without these instructions, agents may not reliably call the tool.

.NET (HandoffWorkflowBuilderCore):

public string? HandoffInstructions { get; private set; } = DefaultHandoffInstructions;
// Default: "You are one agent in a multi-agent system. You can hand off the conversation to another agent..."
public TBuilder WithHandoffInstructions(string? instructions) { ... }

Python (HandoffWorkflowBuilder): a default handoff_instructions string is injected into each HandoffAgentExecutor when building.

Go (this PR): agents receive the injected tool but no guidance about when to use it.

The PR description acknowledges this as "open for steer", but upstream treats it as a load-bearing default. Recommend either injecting the instructions unconditionally (matching the upstream default) or adding a WithHandoffInstructions(instructions string) *HandoffWorkflowBuilder method that defaults to the same canonical text. Either way it should be on-by-default, not opt-in, to match upstream semantics.

cursor int
}

func newHandoffManager(entry *agent.Agent, toolTargets map[string]*agent.Agent, maximumIterationCount int) *GroupChatManager {

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.

Parity gap — autonomous mode not present

Both upstream SDKs provide an opt-in autonomous mode: when the current agent produces a response that does not include a handoff call, instead of immediately ending the workflow the agent is re-invoked with a continuation prompt (up to a configurable per-agent turn limit). This is useful for long-running research/coding tasks where the agent needs multiple model turns before it decides to hand off.

.NET (HandoffWorkflowBuilderCore):

public TBuilder EnableAutonomousMode(
    int turnLimit = HandoffWorkflowBuilderDefaults.DefaultAutonomousTurnLimit,
    string? continuationPrompt = null) { ... }
// per-agent overrides: EnableAutonomousModeForAgent(agent, turnLimit, prompt)
// termination condition: WithTerminationCondition(Func<..., ValueTask<bool>>)

Python (HandoffWorkflowBuilder):

autonomous_mode: bool = False,
autonomous_mode_prompt: str | None = None,
autonomous_mode_turn_limit: int | None = None,

Go (this PR): a turn with no handoff call always terminates the workflow immediately; there is no autonomous-mode opt-in and no termination-condition callback.

This is not a blocking correctness bug for the basic handoff scenario, but it is a significant behavioral gap versus the published .NET and Python APIs. Recommend tracking in a follow-up issue and noting the absence in the docstring / feature-comparison doc so callers know the limitation.

return builder.Build()
}

// handoffToolName derives the handoff tool name presented to the model for a

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.

Minor parity note — WithHandoff per-edge description/reason not supported

In .NET, WithHandoff(AIAgent from, AIAgent to, string? handoffReason = null) accepts an optional per-edge description. When absent, it falls back to the target agent's .Description or .Instructions. If none are available the builder throws, ensuring the model always receives a meaningful tool description for each handoff edge.

Go (this PR): the tool description is always derived from the target agent's Name()/ID() ("Hand off the conversation to the \"<name>\" agent."). There is no per-edge description override and no fallback to the agent's own description/instructions field. This is a minor gap but could produce less effective prompting when agent names are opaque IDs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:workflow Changes files in the workflow area public-api-change Pull Request changes public APIs risk:medium Contained production impact requiring normal review depth size:xlarge More than 300 changed lines or 10 files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants