Skip to content

reason is documented as surfaced to human approvers, but user-approval cannot carry one #19654

Description

@ladanjohari

Description

The policy documentation for tool approvals says:

reason is optional and is surfaced back to the model (for deny) or to the human
approver (for requires-approval).

The second half of that does not happen. In @ai-sdk/policy-opa, the normalizer that turns
a policy decision into an approval status reads the reason off the record and then passes it
on three of four branches:

switch (record.decision) {
  case "allow":             return withReason("approved", reason);
  case "deny":              return withReason("denied", reason);
  case "requires-approval": return { type: "user-approval" };   // reason dropped here
  case "not-applicable":    return { type: "not-applicable" };
}

The one branch that discards it is the one that goes to a person. The doc comment on that
same function confirms the mapping is intended: requires-approval "maps to approved /
denied / user-approval respectively".

There is also nowhere downstream to put it, so this is not a one line oversight.
user-approval is the single status the type forbids a reason on:

type ToolApprovalStatus =
  | { type: 'approved';       reason?: string }
  | { type: 'denied';         reason?: string }
  | { type: 'user-approval';  reason?: never }    // the human approver's case
  | { type: 'not-applicable'; reason?: never };

And ai matches, in both the generate path and the stream path. Simplified from the build
output of ai@7.0.79, with the surrounding assignments removed:

case 'user-approval':
  // what the person is shown. no reason field exists on it
  { type: 'tool-approval-request', approvalId, toolCall, signature? }

case 'approved':
  // what no person reads. carries the reason
  { type: 'tool-approval-response', approvalId, toolCall, approved: true,
    reason: toolApprovalStatus.reason, providerExecuted }

The obvious objection, and why I do not think it holds. approved and denied carry
the reason on the approval response, and a pending user-approval has no response yet, so
it can look as though the reason has nowhere to go by construction. But ToolApprovalRequest
has no field for it either, and the request is the only thing that reaches the person before
they decide. So the single path where the documentation promises a reason is the single path
with nowhere to put one, at the policy layer, in the type, and in the request.

By the time execution reaches this branch the policy has already worked out why the call
needs a human. That explanation is discarded exactly where a human is the audience.

I ran into this building an approval interface for the SDK. At the moment somebody has to
decide, the application holds the tool name, the raw input and an id, and has nothing it can
put in front of them.

Happy to open a PR for this on its own if that would help.

Related: #15733 asks for per-call context on approval requests more broadly. This is the
narrower half of it and looks independently fixable.

Reproduction

Not a runtime reproduction. This is a contradiction between the documentation and the
published code, so it reproduces by reading them.

  1. @ai-sdk/policy-opa@1.0.79 normalizes a policy decision into an approval status. The
    allow and deny branches call withReason(...). The requires-approval branch
    returns { type: 'user-approval' } and drops the reason.
  2. ToolApprovalStatus in ai@7.0.79 permits reason?: string on approved and denied,
    and reason?: never on user-approval, so there is nowhere to carry it even if the
    branch above kept it.
  3. In the ai build output the user-approval branch constructs a tool-approval-request
    as { type, approvalId, toolCall, signature? }. There is no reason field on the request.
  4. The policy tool approvals documentation states that reason is surfaced to the human
    approver for requires-approval.

Happy to turn this into a failing type test if that is more useful than prose.

AI SDK Version

  • ai: 7.0.79
  • @ai-sdk/provider-utils: 5.0.30
  • @ai-sdk/react: 4.0.82
  • @ai-sdk/policy-opa: 1.0.79

Read off the published type definitions and build output on 25 August 2026.

Code of Conduct

  • I agree to follow this project's Code of Conduct

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions