Skip to content

fix(agents): fail fast instead of hanging on the agent-service picker in non-TTY contexts#9015

Open
glharper wants to merge 1 commit into
mainfrom
glharper/8584-non-tty-agent-picker
Open

fix(agents): fail fast instead of hanging on the agent-service picker in non-TTY contexts#9015
glharper wants to merge 1 commit into
mainfrom
glharper/8584-non-tty-agent-picker

Conversation

@glharper

@glharper glharper commented Jul 7, 2026

Copy link
Copy Markdown
Member

Summary

Fixes the silent hang described in #8584. Several azd ai agent subcommands render an interactive "Select an agent service" picker when the target agent is ambiguous (a multi-service azure.yaml with more than one azure.ai.agent block). The picker is drawn by the azd host and blocks on stdin, so in a non-TTY context — CI, an agent harness, or output piped through tee/tail — it was invisible and the process hung forever with no error, spinner, or banner; only Ctrl+C recovered.

All of these commands funnel through a single shared helper, promptForAgentService, which is the shared root cause. This fixes it centrally.

Fixes #8584

What changed

promptForAgentService (internal/cmd/helpers.go):

  1. Fail fast in non-TTY contexts. When stdin/stdout is not a terminal, return a structured exterrors.Validation error (code non_interactive_agent_selection) that lists the available services and explains how to pass one, instead of rendering a picker that hangs:

    cannot prompt for agent service selection in a non-interactive context (stdin/stdout is not a TTY); multiple azure.ai.agent services are defined in azure.yaml: svc-a, svc-b

    with the suggestion to pass the agent service name explicitly (positional argument or --agent-name), or run with --no-prompt.

  2. Banner before the interactive picker. Even in TTY mode, print a one-line banner to stderr (azd is waiting for your agent service selection below...) before the picker. When the picker is visible this is harmless; when stdout is piped the banner still shows up in tee/tail/CI logs and explains why azd is blocked.

The existing --no-prompt fast-fail behavior is unchanged; the new guard covers the case where the user does not pass --no-prompt but has no TTY.

Why this covers all the reported commands

invoke --local, files upload, delete, show, monitor, endpoint show, eval, optimize, update, etc. all resolve the agent through resolveAgentServicepromptForAgentService, so fixing the one helper turns the multi-minute silent hang into an immediate, diagnosable error everywhere.

Testing

  • New unit tests: non-TTY fails fast with the right error code and never calls the host prompt; --no-prompt fails fast; interactive mode prints the stderr banner and prompts once.
  • Existing picker tests updated to simulate a TTY through a small, overridable seam (agentSelectionInteractive).
  • go build ./..., full go test ./... (all pass), gofmt, golangci-lint (0 issues), cspell (0 issues).

Notes

The TTY check is exposed as a package-level function variable so tests can simulate a terminal; tests that use it run sequentially to avoid racing on the shared seam.

…TTY contexts

The shared 'Select an agent service' picker (promptForAgentService) is drawn by the azd host and blocks on stdin. In a non-TTY context (CI, agent harness, or output piped through tee/tail) it was invisible and hung forever.

It now detects a non-interactive terminal and fails fast with a structured, actionable error listing the available services, and prints a one-line banner to stderr before the picker in interactive mode so piped/CI logs show why azd is waiting.

Fixes #8584

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 7, 2026 19:11
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

📋 Prioritization Note

Thanks for the contribution! The linked issue isn't in the current milestone yet.
Thank you for logging this issue; our team is reviewing it. If you need urgent prioritization, tag @RickWinter and @kristenwomack to let us know.

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

This PR fixes a silent hang (issue #8584) in the azure.ai.agents extension. When an azd ai agent subcommand needs to resolve an ambiguous agent (a multi-service azure.yaml with more than one azure.ai.agent block), it renders an interactive "Select an agent service" picker via the azd host. In non-TTY contexts (CI, agent harnesses, piped output) the picker was invisible and blocked forever on stdin. All affected commands funnel through the shared helper promptForAgentService, so the fix is applied centrally there.

Changes:

  • Add a agentSelectionInteractive TTY-check seam and fail fast with a structured exterrors.Validation error (new code non_interactive_agent_selection) when stdin/stdout is not a terminal, listing available services and how to pass one.
  • Print a one-line hint banner to stderr before the interactive picker; extract a small serviceNames helper.
  • Add/adjust unit tests (using an overridable TTY seam, made non-parallel) and a CHANGELOG entry.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
cli/azd/extensions/azure.ai.agents/internal/exterrors/codes.go Adds the CodeNonInteractiveAgentSelection error code constant.
cli/azd/extensions/azure.ai.agents/internal/cmd/helpers.go Adds the TTY-aware fail-fast guard, stderr banner, and serviceNames/agentSelectionInteractive helpers in promptForAgentService.
cli/azd/extensions/azure.ai.agents/internal/cmd/helpers_test.go Adds tests for non-TTY fail-fast, --no-prompt fail-fast, and banner output; introduces the withInteractiveAgentSelection seam and removes parallelism where the seam is used.
cli/azd/extensions/azure.ai.agents/CHANGELOG.md Documents the fix under the Unreleased section.

Comment on lines +611 to +613
// Print a banner to stderr before the (interactive) picker. When the picker is visible this
// is harmless; when stdout is piped the banner still shows up in tee/tail/CI logs and
// explains why azd is blocked waiting for a selection.
return isTerminal(os.Stdin.Fd()) && isTerminal(os.Stdout.Fd())
}

// serviceNames returns the sorted-order names of the given services.

@jongio jongio left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One comment accuracy nit (already flagged): the banner on the stderr line is only reachable in fully interactive mode (both stdin+stdout are TTYs), so the inline comment's claim about showing up in piped/CI logs describes an unreachable path. Consider simplifying it to explain the banner provides context to the user while the picker loads.

@RickWinter RickWinter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This converts the silent hang in promptForAgentService into a fast, structured error when there is no TTY, and prints a one-line stderr banner before the interactive picker. The approach is the right shape: the shared helper is the correct single chokepoint for every command that resolves an agent service, the TTY seam matches the existing isTerminal(os.Stdin/os.Stdout) pattern already used across this extension (nextstep_output.go, doctor.go, run.go), and requiring both stdin and stdout to be terminals is correct since the host draws the picker to stdout. Tests cover the non-TTY fail-fast, the --no-prompt path, and the banner, and the process-global test seam is documented as serial.

One thing worth resolving before merge is the banner rationale, noted inline: the comment claims a piped-log benefit the control flow cannot deliver. It is harmless either way, so nothing here blocks. Disposition: COMMENT.

// Print a banner to stderr before the (interactive) picker. When the picker is visible this
// is harmless; when stdout is piped the banner still shows up in tee/tail/CI logs and
// explains why azd is blocked waiting for a selection.
fmt.Fprintln(os.Stderr, output.WithHintFormat("azd is waiting for your agent service selection below..."))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The comment above says this banner "still shows up in tee/tail/CI logs" when stdout is piped, but the non-TTY guard just above returns before we ever reach this line whenever stdout is not a terminal. So this only runs when both stdin and stdout are TTYs, which is exactly when the picker is already on screen. The banner is harmless, but its stated cross-piped-log value is unreachable as written. Either drop the piped-log justification from the comment, or, if you actually want the "why azd is waiting" hint to land in piped logs, emit it before the TTY guard.

}

defaultIndex := int32(0)
resp, err := azdClient.Prompt().Select(ctx, &azdext.SelectRequest{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

From what I understand, non-tty contexts should automatically default to --no-prompt, causing this handling to default to the SelectedIndex, so I'm surprised we need any additional logic here (unless we don't want that default handling). @JeffreyCA @vhvb1989 can you confirm?

Ideally, we shouldn't duplicate some logic that is already built into azd, so even if this prompt doesn't work the way I think it should, everywhere else in the extension we've relied on the no-prompt flag to tell us when we're in a situation where we can't rely on interactive input, so we should be able to use that to simplify the above logic.

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

Labels

ext-agents azure.ai.agents extension

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ext-agents] Interactive agent-service picker hangs silently in non-TTY contexts instead of failing fast

5 participants