Skip to content

AI scenarios

Tool approval

Bit.BlazorUI.AI Scripted transport

A destructive tool stops and waits for a human. Allow once, allow always, or decline - and a provider can ask for the same decision itself.

Live demo

Ask for something destructive: delete_records is declared destructive, so the session will not run it unattended.

A call that waits for a decision

Ask for something destructive
delete_records is declared destructive, so the session will not run it unattended.

Approval the transport asks for

Ask about the weather: get_weather is read-only, so this app's policy would run it unattended - but this transport asks for the user's consent itself, and says why.

A decision the transport asks for

Ask about the weather
The transport asks for consent before get_weather runs, whatever the app's policy says.

How it works

The transport asks for the tool as any tool call is asked for (BitAiToolCallStartedEvent, the arguments streamed as BitAiToolCallDeltaEvents, BitAiToolCallCompletedEvent) and ends the reply with BitAiFinishReason.ToolCalls. Before the session runs the call, it asks the approval policy (IBitAiToolApprovalPolicy.RequiresApprovalAsync(definition, invocation)). When a decision is needed it moves the call to AwaitingApproval and raises BitAiChatSession.ApprovalRequested, and the call's card shows the prompt, whose buttons record the decision with BitAiChatSession.Approve(toolCallId, decision) - Approved, ApprovedAlways or Rejected.

Allowing once and allowing always are different decisions. Collapsing them into a single button forces the user either to re-approve constantly or to grant more than they meant to, so both are offered, beside a decline.

What needs a decision is the IBitAiToolApprovalPolicy's call. The default one asks for every tool whose Risk is Destructive (or that says RequiresApproval) and remembers the tools a user allowed always - which is why it is scoped on Blazor Server, where a singleton would carry one user's "always" over to every other user. ai.UseApprovalPolicy<T>() replaces it, for a decision that depends on the arguments or on the user.

While a call waits, the turn is genuinely paused: the tool has not run, the model has not been asked again, and stopping the turn cancels the wait. A declined call is marked Rejected and given a failed result, which the transport reads off the call on the next request, so the model can explain that nothing was done.

A provider whose own policy wants consent asks for it with a BitAiToolApprovalRequestedEvent, as the second demo's transport does right after starting the call. It puts the call in AwaitingApproval with its Reason, which the prompt shows as the question, and the session waits for the user just as it would for its own policy - even for a tool that policy runs unattended, and whatever the user allowed always before: the request is the provider's, not the policy's to overrule. The session still runs the tool once it is allowed, so the tool is registered as usual.

Components used

Feedback

Found a mistake, a gap, or something that could be clearer? Every page and every component is one click from its source.