Clarification Chat

June 28, 2026 · View on GitHub

Clarification Chat

Overview

A clarification chat is the protocol's way for a server to ask the agent a follow-up question before it decides a deferred request, rather than approving or rejecting outright (§Clarification Chat). Two places use it:

  • Token exchange. When the agent exchanges a resource token, the PS may need more detail before granting the scope — it defers and asks a clarifying question.
  • Mission proposal and governance. When the agent proposes a mission or requests permission, the PS may ask the agent to refine the intent before approving.

The agent answers the question, replaces its request with a narrower one, or withdraws — and the exchange continues until the server decides or the round limit is reached.

The question: ClarificationRequirement

When a server needs clarification it returns AAuth-Requirement: requirement=clarification and carries the question in the response body. The SDK projects that into a typed ClarificationRequirement:

namespace AAuth.Headers;

public sealed record ClarificationRequirement(
    string Clarification,                   // the Markdown question — UNTRUSTED, sanitize before display
    int? TimeoutSeconds = null,             // optional deadline to respond by
    IReadOnlyList<string>? Options = null); // optional discrete choices for a closed question

Warning

The Clarification value is untrusted input from the server. Sanitize it before rendering it to a user (§Clarification Required).

The answer: ClarificationResponse

The agent replies with one of three actions (§Agent Response to Clarification):

namespace AAuth.Agent;

public sealed class ClarificationResponse
{
    public enum Kind { Respond, Update, Cancel }

    public static ClarificationResponse Respond(string markdown);                       // answer the question
    public static ClarificationResponse Update(string resourceToken, string? justification = null); // replace the request
    public static ClarificationResponse Cancel();                                       // withdraw
}
  • Respond posts a Markdown answer and resumes the exchange.
  • Update replaces the original request with a new resource token (for example a reduced scope) plus an optional justification.
  • Cancel withdraws the request entirely.

Driving the chat: ClarificationExchange

For manual control over a deferred pending URL, use ClarificationExchange. It tracks the round count and enforces a maximum (§Clarification Limits).

namespace AAuth.Agent;

public sealed class ClarificationExchange
{
    public const int DefaultMaxRounds = 5;

    public ClarificationExchange(HttpClient signedClient, Uri pendingUrl, int maxRounds = DefaultMaxRounds);

    public int MaxRounds { get; }
    public int Rounds { get; }

    public Task ApplyAsync(ClarificationResponse response, CancellationToken ct = default);
    public Task RespondAsync(string markdown, CancellationToken ct = default);
    public Task UpdateRequestAsync(string resourceToken, string? justification = null, CancellationToken ct = default);
    public Task CancelAsync(CancellationToken ct = default);
}

The supplied HttpClient must be wired with the agent's AAuthSigningHandler so every POST/DELETE to the pending URL is signed.

var exchange = new ClarificationExchange(signedClient, pendingUrl);

await exchange.ApplyAsync(ClarificationResponse.Respond(
    "The export is for the user's own tax records, read-only."));

// Cancelling throws AAuthClarificationCancelledException after withdrawing.
// Exceeding MaxRounds throws AAuthClarificationLimitException(MaxRounds).

Both Respond and Update consume a round; Cancel issues a DELETE and throws AAuthClarificationCancelledException. Once Rounds reaches MaxRounds the next attempt throws AAuthClarificationLimitException.

Automatic handling during token exchange

You rarely need to drive the exchange by hand. The token-exchange request exposes a callback the SDK invokes whenever the PS asks for clarification, looping until the PS decides or MaxClarificationRounds is hit.

var request = new TokenExchangeRequest
{
    MaxClarificationRounds = ClarificationExchange.DefaultMaxRounds,
    OnClarificationRequired = async (requirement, ct) =>
    {
        // requirement.Clarification is untrusted — sanitize before display.
        string question = Sanitize(requirement.Clarification);

        if (requirement.Options is { Count: > 0 } options)
        {
            string choice = await AskUserToPick(question, options);
            return ClarificationResponse.Respond(choice);
        }

        string answer = await AskUser(question);
        return ClarificationResponse.Respond(answer);
    },
};

var authToken = await exchangeClient.ExchangeAsync(personServer, resourceToken, request);

Automatic handling during governance

The same pattern applies to the governance clients. Supply OnClarificationRequired (and optionally MaxClarificationRounds) on GovernanceOptions when proposing a mission or requesting permission. The governance client is bound to its Person Server, so no per-call PS URL is needed:

var session = await governance.ProposeMissionAsync(
    new MissionProposal("Plan my weekend trip to Seattle."),
    new GovernanceOptions
    {
        MaxClarificationRounds = 3,
        OnClarificationRequired = async (requirement, ct) =>
            ClarificationResponse.Respond(await AskUser(Sanitize(requirement.Clarification))),
    });

When the callback is null and the server asks for clarification, the request fails rather than blocking.

Server side: emitting a clarification

A Person Server built on MapAAuthPersonServer gets the server half of the protocol for free. For an out-of-scope mission token request the helper calls the IMissionTokenConsent seam; returning Clarify makes the SDK emit the requirement=clarification 202, accept the agent's clarification_response / updated resource_token / DELETE on the pending URL, record each round in the mission log, and re-consult the seam:

public sealed class LlmMissionConsent : IMissionTokenConsent
{
    public async Task<MissionTokenConsentDecision> ReviewAsync(
        MissionTokenConsentContext ctx, CancellationToken ct = default)
    {
        // First pass with no answers yet → ask the agent to justify.
        if (ctx.ClarificationHistory.Count == 0)
            return MissionTokenConsentDecision.Clarify("Why does this mission need this scope?");

        // The agent answered — let the policy (here, an LLM) decide.
        return await _reviewer.IsJustified(ctx, ctx.ClarificationHistory)
            ? MissionTokenConsentDecision.Grant()
            : MissionTokenConsentDecision.Deny("not justified by the mission");
    }
}

The SDK owns the wire protocol and the mission log; the seam owns how the question is formed and the answer judged (a consent screen, a scripted test, or an LLM reviewer). See Mission Governance (Server).

Further reading