Skip to content

Fall back to default output when WithOutputFrom is called with no agents - #1062

Open
PratikDhanave (PratikDhanave) wants to merge 1 commit into
microsoft:mainfrom
PratikDhanaveFork:agentworkflow-empty-outputfrom-noop
Open

PratikDhanave (PratikDhanave) wants to merge 1 commit into
microsoft:mainfrom
PratikDhanaveFork:agentworkflow-empty-outputfrom-noop

Conversation

@PratikDhanave

Copy link
Copy Markdown
Contributor

Problem

outputDesignations.explicit() returned d != nil, but withOutputFrom allocates a non-nil map even for zero agents:

func (d outputDesignations) explicit() bool { return d != nil }

func (d outputDesignations) withOutputFrom(agents ...*agent.Agent) (outputDesignations, error) {
    if d == nil { d = make(outputDesignations) } // allocates even when len(agents)==0
    ...
}

So WithOutputFrom() / WithIntermediateOutputFrom() called with no agents — e.g. spreading a computed slice builder.WithOutputFrom(outs...) that happens to be empty — makes the designation set non-nil-but-empty. explicit() then reports true, so applyDefaults() (which designates the concurrent end / sequential output / group-chat host as the terminal output) is skipped, and the designation loop registers nothing. The built workflow has zero output executors: it runs but yields no result (only a dead-end executors detected WARN, no error). This affects the concurrent, sequential, and group-chat builders via the shared path.

Fix

Treat an empty designation set as non-explicit (len(d) != 0), so an empty WithOutputFrom() falls back to the default terminal output instead of silently disabling all output. A designation with ≥1 agent is unaffected.

Test

TestConcurrentWorkflowBuilder_NoArgWithOutputFromFallsBackToDefault builds a concurrent workflow with .WithOutputFrom() (no agents) and asserts OutputExecutorIDs() is non-empty. Fails before the fix (zero outputs), passes after.

outputDesignations.explicit() returned d != nil, but withOutputFrom allocates a
non-nil map even for zero agents. So WithOutputFrom() / WithIntermediateOutputFrom()
called with no agents (e.g. spreading an empty slice) made the designation set
non-nil-but-empty: explicit() reported true, applyDefaults() was skipped, and the
designation loop registered nothing - producing a workflow with zero output
executors that runs but yields nothing (only a dead-end WARN). Affects the
concurrent, sequential, and group-chat builders via the shared path.

Treat an empty designation set as non-explicit so the default terminal output
still applies.
Copilot AI lite review requested due to automatic review settings September 13, 2026 13:06
@github-actions github-actions Bot added area:workflow Changes files in the workflow area size:small At most 30 changed lines across at most 2 files labels Sep 13, 2026

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.

🟢 Approval recommended

The fix is covered by regression testing and no unresolved issues were identified.

Pull request overview

Updates output designation handling so empty WithOutputFrom calls retain default workflow outputs.

Changes:

  • Treat empty designation maps as non-explicit.
  • Add regression coverage for concurrent workflows.
File summaries
File Summary
workflow/agentworkflow/concurrent_test.go Verifies default output remains enabled.
workflow/agentworkflow/builders.go Corrects explicit designation detection.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions github-actions Bot added kind:code Changes production behavior or code kind:tests Changes tests, fixtures, or test infrastructure pending-auto-risk and removed pending-auto-risk labels Sep 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Scope: internal-only (bug fix in unexported helper; no exported API surface changed)

Changed Go contract: None. outputDesignations.explicit() and withOutputFrom are unexported helpers in workflow/agentworkflow/builders.go. The exported methods WithOutputFrom/WithIntermediateOutputFrom on ConcurrentWorkflowBuilder, SequentialWorkflowBuilder, and GroupChatWorkflowBuilder keep their existing signatures; only the observable behavior of calling them with zero agents changes (falls back to default terminal output instead of silently producing zero output executors).

Upstream evidence reviewed:

  • .NET: dotnet/src/Microsoft.Agents.AI.Workflows/OrchestrationBuilderBase.csOutputDesignations is null until any output-designation call is made (WithOutputFrom/WithIntermediateOutputFrom do this.OutputDesignations ??= new(...)), and ApplyOutputDesignations checks this.OutputDesignations is null to decide whether to invoke applyDefaults(). This is exactly the "null vs. non-null" designation semantics the Go outputDesignations map is modeling — the bug was that Go's non-nil-but-empty map was being treated as "explicit" while .NET's memoized dictionary would only be treated as non-explicit if truly untouched (null), which matches the intent the Go code is going for, but the byte-for-byte parity concern is a separate axis:
  • Python: python/packages/orchestrations/agent_framework_orchestrations/_participant_output_config.py::_resolve_participant_output_config explicitly rejects empty selections outright: if explicit_config and not (output_from or intermediate_output_from): raise ValueError("output_from and intermediate_output_from cannot both be empty."), exercised by python/packages/orchestrations/tests/test_orchestration_intermediate_vs_terminal.py::test_participant_output_config_validation ({"output_from": [], "intermediate_output_from": []}"cannot both be empty").

Result: aligned (with a minor note for maintainers). The Go fix's chosen behavior — silently falling back to the default terminal output when WithOutputFrom()/WithIntermediateOutputFrom() is called with an empty/no-arg list — is a defensible fix for the "dead workflow, no error" bug described in the PR, and matches .NET's "no explicit designation was made" fallback path once the empty map is normalized away. However, it is not identical to Python's behavior for the same input shape: Python treats calling output_from=[] explicitly (as opposed to never calling it / passing None) as an error ("cannot both be empty"), not a silent fallback to defaults. .NET does not expose an equivalent single "was any explicit call made at all, and was the net result empty" check with an error — it only exposes the null/non-null memoized-dictionary state, so this PR's Go behavior is closer to .NET's model than to Python's stricter validation. Given Go's WithOutputFrom API takes a variadic ...*agent.Agent (no None vs [] distinction is expressible in the exported signature the way Python's output_from: Sequence[...] | Literal["all"] | None distinguishes "omitted" from "empty list"), silently falling back to defaults is a reasonable and idiomatic choice given the language's calling convention, and this is a bug fix restoring intended behavior rather than a new design decision — no changes requested.

This is a straightforward, well-tested (TestConcurrentWorkflowBuilder_NoArgWithOutputFromFallsBackToDefault) internal bug fix. No public API changed, no divergent runtime default introduced beyond fixing an unintended defect, and no examples/samples are affected.

Generated by Go API Consistency Review Agent · copilot · auto · 130.6 AIC · ⌖ 6.81 AIC · ⊞ 9.6K ·

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 kind:code Changes production behavior or code kind:tests Changes tests, fixtures, or test infrastructure size:small At most 30 changed lines across at most 2 files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants