May 14, 20264 min read/2026/05/14/getting-started-github-copilot-sdk-part-6-ask-user-input/

Getting Started with the GitHub Copilot SDK — Part 6: Asking the User for Input

This is Part 6 of the series. The companion code lives on GitHub at
egarim/GettingStartedWithGithubCopilotSDK
— each numbered folder runs on its own.

So far the conversation has been one-way: you ask, the model answers.

But sometimes the model is the one with the question.

"What color do you want?"
"Pizza or sushi?"
"Should I overwrite this file?"

That's what ask_user is for.

The idea

The model has a built-in tool called ask_user. When it decides it needs
something only you can provide, it calls that tool — and the SDK fires a
callback in your code: OnUserInputRequest.

Your handler gets the question, optionally a list of choices, and returns the
answer. The model takes that answer and keeps going.

You send a prompt
     │
     ▼
Model needs input → OnUserInputRequest
     │  Question: "What color?"
     │  Choices:  ["Red", "Blue"]   (or none → freeform)
     ▼
Your handler returns UserInputResponse
     │  { Answer = "Blue", WasFreeform = false }
     ▼
Model incorporates it → final response

You wire it up once, on the session:

var session = await client.CreateSessionAsync(new SessionConfig
{
    OnUserInputRequest = (request, invocation) =>
    {
        // request.Question — the model's question
        // request.Choices  — optional List<string>? (may be null/empty)
        return Task.FromResult(new UserInputResponse
        {
            Answer = "...",
            WasFreeform = true
        });
    }
});

Choices — pick from a list

The most common case: the model offers options and you pick one.

OnUserInputRequest = (request, invocation) =>
{
    Console.WriteLine(
quot;[AskUser] {request.Question}"); if (request.Choices is { Count: > 0 }) { Console.WriteLine(
quot;[AskUser] Options: [{string.Join(", ", request.Choices)}]"); Console.WriteLine(
quot;[AskUser] Auto-selecting: {request.Choices[0]}"); return Task.FromResult(new UserInputResponse { Answer = request.Choices[0], WasFreeform = false }); } return Task.FromResult(new UserInputResponse { Answer = "I'll go with the default option", WasFreeform = true }); }

Note that WasFreeform = false. The user picked from a list — they didn't
type anything. That flag matters for your UX later.

Verify the choices the model sent

You don't have to trust the model blindly. Collect the requests and inspect them.

var requests = new List<UserInputRequest>();

// inside the handler:
requests.Add(request);
if (request.Choices is { Count: > 0 })
    for (int i = 0; i < request.Choices.Count; i++)
        Console.WriteLine(
quot; [{i + 1}] {request.Choices[i]}");

Prompt the model with "Use ask_user to ask me to pick between 'Red' and 'Blue'",
then check:

var withChoices = requests.Where(r => r.Choices is { Count: > 0 }).ToList();
Console.WriteLine(
quot;Requests with choices: {withChoices.Count}"); // -> 1

Now you can assert the model actually used ask_user the way you expected —
the same options, the right count. Handy for testing agent behavior.

Freeform — let the user type

When there are no choices, the user types whatever they want. Set
WasFreeform = true so the model knows this was free text, not a selection.

OnUserInputRequest = (request, invocation) =>
{
    Console.WriteLine(
quot;[AskUser] {request.Question}"); return Task.FromResult(new UserInputResponse { Answer = "emerald green", WasFreeform = true }); }

Ask the model "What is your favorite color?" and the final response will
weave "emerald green" right back in. The model heard you.

Put it together — a live interactive chat

The real payoff is a streaming chat where the model can interrupt mid-turn
to ask. If there are choices, show them numbered — let the user pick by number
or type their own answer.

OnUserInputRequest = (request, invocation) =>
{
    Console.WriteLine(
quot;\nModel asks: {request.Question}"); var answer = ""; var wasFreeform = true; if (request.Choices is { Count: > 0 }) { for (int i = 0; i < request.Choices.Count; i++) Console.WriteLine(
quot; [{i + 1}] {request.Choices[i]}"); Console.Write("Pick a number or type: "); answer = Console.ReadLine() ?? ""; if (int.TryParse(answer, out var idx) && idx >= 1 && idx <= request.Choices.Count) { answer = request.Choices[idx - 1]; // they picked a choice wasFreeform = false; } } else { Console.Write("Your answer: "); answer = Console.ReadLine() ?? ""; } return Task.FromResult(new UserInputResponse { Answer = answer, WasFreeform = wasFreeform }); }

The number-vs-text branch is the whole trick: a numeric input inside range
collapses to a choice (WasFreeform = false); anything else stays freeform.

Try prompting it with "Ask me what programming language I prefer using ask_user."
The model stops, asks, waits for you, then carries on — exactly like a person would.

Takeaways

  • ask_user is the model's way to ask you a question. The SDK delivers it
    via the OnUserInputRequest callback on the session.
  • request.Choices is List<string>?may be null or empty. Always guard
    with { Count: > 0 } before indexing.
  • Return a UserInputResponse { Answer, WasFreeform }. Set WasFreeform = false
    for a list pick, true for typed text — it's a real UX signal, not decoration.
  • Collect the requests if you want to verify the model used the tool correctly.
  • Combined with tools, hooks, and permissions, this closes the loop: a true
    two-way conversation between model and user.

This wraps the bidirectional-communication piece. In Part 7 — Infinite
Sessions & Context Compaction
we tackle the long game: how to keep a
conversation running forever without drowning in tokens.
Read Part 7 →

Want to follow along? You'll need the .NET 10 SDK and GitHub Copilot
access (log in via VS Code or gh auth login). Then
dotnet run --project 06.AskUserDemo from the
course repo.