Skip to content
# AiToolApproval ## Description BitAiToolApproval is the prompt shown when a tool call needs a human decision. It offers three answers, from the most cautious to the most permissive - decline, always allow, allow - because allowing once and allowing always are different decisions. The prompt follows the risk the tool declares: the allow button of a destructive tool is drawn in the error role and the prompt is washed with it, so the answer that runs it never looks like the safe one. The decision goes to the session that is waiting, which resumes the paused turn. ## Parameters | Name | Type | Default Value | Description | | :--- | :--- | :------------ | :---------- | | Call | `BitAiToolCallPart` | | The call awaiting a decision (required). Its ApprovalReason is shown as the question when it has one, and the Risk of its Definition decides how the prompt looks - read again on every render, since a session resolves the definition onto the call only as it starts running it. Without a Session, the decision is recorded on it (BitAiToolCallPart.SetApproval). | | Classes | `BitAiToolApprovalClassStyles?` | null | Custom CSS classes for different parts of the tool approval. | | OnDecision | `EventCallback<BitAiApprovalDecision>` | | Raised after a decision - Approved, ApprovedAlways or Rejected - once it has been handed to the session, or recorded on the call when there is none. Nothing is raised while the prompt is disabled, nor for a call that is no longer awaiting a decision - which the session refuses (BitAiChatSession.Approve returns false), and the prompt without one ignores: a call is decided once. | | Session | `BitAiChatSession?` | null | The session that is waiting for the decision, which it is handed to (BitAiChatSession.Approve): that is what resumes the paused turn, and what lets the approval policy remember an always-allow. Required for the decision to take effect in a session; without one, it is only recorded on the Call, which is what a layout driving its own tool loop reads. | | Styles | `BitAiToolApprovalClassStyles?` | null | Custom CSS styles for different parts of the tool approval. | | AriaLabel | `string?` | null | Gets or sets the accessible label for the component, used by assistive technologies. | | Class | `string?` | null | Gets or sets the CSS class name(s) to apply to the rendered element. | | Dir | `BitDir?` | null | Gets or sets the text directionality for the component's content. | | Disabled | `bool` | false | Gets or sets a value indicating whether the component is disabled and cannot respond to user interaction. | | ForceAnimation | `bool` | false | Gets or sets a value indicating whether the component's animations play at their full duration even when reduced motion is requested. | | HtmlAttributes | `Dictionary<string, object>` | new Dictionary<string, object>() | Captures additional HTML attributes to be applied to the rendered element, in addition to the component's parameters. An attribute written on the component wins over an entry of the same name. | | Id | `string?` | null | Gets or sets the unique identifier for the component's root element. | | Style | `string?` | null | Gets or sets the CSS style string to apply to the rendered element. | | TabIndex | `string?` | null | Gets or sets the tab order index for the component when navigating with the keyboard. | | Visibility | `BitVisibility` | BitVisibility.Visible | Gets or sets the visibility state (visible, hidden, or collapsed) of the component. | ## Public Members | Name | Type | Default Value | Description | | :--- | :--- | :------------ | :---------- | | UniqueId | `Guid` | Guid.NewGuid() | Gets the readonly unique identifier for the component's root element, assigned when the component instance is constructed. | | RootElement | `ElementReference` | | Gets the reference to the root HTML element associated with this component. | ## Enums ### BitAiToolRisk Enum | Name | Value | Description | | :--- | :--- | :---------- | | ReadOnly | 0 | Reads data and has no side effects. | | Write | 1 | Changes state in a way that can be undone. | | Destructive | 2 | Destructive, irreversible or visible outside the app. The default policy asks before every call, and the prompt is drawn in the error role. | ### BitAiApprovalDecision Enum | Name | Value | Description | | :--- | :--- | :---------- | | Undecided | 0 | No decision has been made yet. | | Approved | 1 | Allowed for this call only. | | Rejected | 2 | Declined: the call does not run, and is reported to the model as a failure. | | ApprovedAlways | 3 | Allowed, and later calls of the same tool are not asked about again. | ### BitVisibility Enum | Name | Value | Description | | :--- | :--- | :---------- | | Visible | 0 | The content of the component is visible. | | Hidden | 1 | The content of the component is hidden, but the space it takes on the page remains (visibility:hidden). | | Collapsed | 2 | The component is hidden (display:none). | ### BitDir Enum | Name | Value | Description | | :--- | :--- | :---------- | | Ltr | 0 | Ltr (left to right) is to be used for languages that are written from the left to the right (like English). | | Rtl | 1 | Rtl (right to left) is to be used for languages that are written from the right to the left (like Arabic). | | Auto | 2 | Auto lets the user agent decide. It uses a basic algorithm as it parses the characters inside the element until it finds a character with a strong directionality, then applies that directionality to the whole element. | ## Sub Classes ### BitAiToolApprovalClassStyles Properties | Name | Type | Default Value | Description | | :--- | :--- | :------------ | :---------- | | Root | `string?` | null | Custom CSS classes/styles for the root element of the BitAiToolApproval. | | Text | `string?` | null | Custom CSS classes/styles for the question (or the reason the approval was asked for) of the BitAiToolApproval. | | Actions | `string?` | null | Custom CSS classes/styles for the row of the answer buttons of the BitAiToolApproval. | | RejectButton | `BitButtonClassStyles?` | null | Custom CSS classes/styles for the decline button of the BitAiToolApproval. | | ApproveAlwaysButton | `BitButtonClassStyles?` | null | Custom CSS classes/styles for the always-allow button of the BitAiToolApproval. | | ApproveButton | `BitButtonClassStyles?` | null | Custom CSS classes/styles for the allow-once button of the BitAiToolApproval. | ### BitAiToolDefinition Properties The provider-independent description of a tool: what it is called, what it does, what arguments it takes - and how the UI presents it. A tool created with BitAiTool.Create carries one (adjust it through the configure delegate: definition => definition with { Title = ..., Risk = ... }); the session copies it onto every call of the tool (BitAiToolCallPart.Definition). | Name | Type | Default Value | Description | | :--- | :--- | :------------ | :---------- | | BitAiToolDefinition(name) | `constructor` | | Creates a definition for the tool the model calls by that name. | | Name | `string` | | The identifier the model uses to call the tool. | | Title | `string?` | null | A human-friendly label, which the approval question and the tool card name the tool by. | | Description | `string?` | null | What the tool does - the text the model reads when deciding to call it. | | ParametersSchema | `JsonElement?` | null | A JSON Schema object describing the arguments. | | Risk | `BitAiToolRisk` | BitAiToolRisk.ReadOnly | How much damage the tool can do. The default approval policy asks before every call of a Destructive tool, and the approval prompt of one is drawn in the error role. | | RequiresApproval | `bool?` | null | Whether a human must approve each call before it runs, either way. Left null, the approval policy decides - by default, from the Risk. | | Icon | `string?` | null | The name of the icon drawn on the header of the tool-call card (BitAiToolCallView) in place of the default wrench - a Fabric MDL2 name (BitIconName.*); a name outside the core glyphs needs the Bit.BlazorUI.Icons package. | | Metadata | `IReadOnlyDictionary<string, object?>?` | null | Values of the application's own, carried through to custom tool renderers. | | DisplayName | `string` | | The label to show for the tool: Title, else Name. | ### IBitAiToolApprovalPolicy Properties Decides which calls wait for a human decision before they run. AddBitBlazorUIAIServices registers BitAiDefaultToolApprovalPolicy - scoped, or a singleton with trySingleton, which a Blazor Server app must not pass since the policy remembers what a user allowed always. Replace it with ai.UseApprovalPolicy<T>() (always scoped), or hand a session one of its own (new BitAiChatSession(transport, approvalPolicy: ...)). | Name | Type | Default Value | Description | | :--- | :--- | :------------ | :---------- | | RequiresApprovalAsync(definition, invocation, cancellationToken?) | `ValueTask<bool>` | | Whether this call of the tool needs a decision. Handed the definition and the invocation (with the arguments), so a policy of the app's own can decide by argument or by user. A policy that throws is treated as asking for approval. | | BitAiDefaultToolApprovalPolicy.RememberAlwaysApproved(toolName) | `void` | | The default policy asks for every Destructive tool and honours RequiresApproval either way, except for the tools a user allowed always - which the session records here when the decision is ApprovedAlways. | ## CSS Variables Public CSS custom properties read off the component's root. Set one on `:root` to restyle every instance, on any ancestor to restyle the ones inside it, or on the `Style` of one instance to restyle it and every instance nested inside it, which inherits the value too - a parameter changes one alone, as does the real CSS property on its `Style` / `Styles` when that property does not inherit (`opacity`); an inherited one (`color`, `font-family`) reaches the nested instances that do not set it themselves. | Name | Default Value | Description | | :--- | :------------ | :---------- | | `--bit-AiToolApproval-color` | var(--bit-clr-fg-pri) | Text of the question. | | `--bit-AiToolApproval-background` | var(--bit-clr-wrn-tint) | Surface of the prompt. | | `--bit-AiToolApproval-destructive-background` | var(--bit-clr-err-tint) | Surface of the prompt of a tool declared Destructive, which wins over --bit-AiToolApproval-background. | | `--bit-AiToolApproval-radius` | var(--bit-shp-radius-surface) | Corner radius of the prompt. Inside a BitAiToolCallView the prompt is the foot of the card and square. | | `--bit-AiToolApproval-padding` | spacing(1.5) | Inset of the prompt. | | `--bit-AiToolApproval-gap` | spacing(1) | Room between the question and the buttons, and between the buttons. | ## Examples \n**Basic**: ```razor @if (basicCall.Status is BitAiToolCallStatus.AwaitingApproval) { } else {
Decision: @basicDecision, status of the call: @basicCall.Status
} Ask again ``` ```csharp private BitAiToolCallPart basicCall = AwaitingApproval("archive_thread", "Archive the thread", BitAiToolRisk.Write); private BitAiApprovalDecision basicDecision; private BitButton askAgainButton = default!; private bool focusAskAgain; private void HandleBasicDecision(BitAiApprovalDecision decision) { basicDecision = decision; // The decision takes the prompt away with the button it was made with; Ask again, which stays, takes the focus. focusAskAgain = true; } protected override async Task OnAfterRenderAsync(bool firstRender) { if (focusAskAgain) { focusAskAgain = false; await askAgainButton.FocusAsync(); } } private void ResetBasic() { basicCall = AwaitingApproval("archive_thread", "Archive the thread", BitAiToolRisk.Write); basicDecision = BitAiApprovalDecision.Undecided; } // A call the way a session leaves it while it waits: its arguments complete, its definition resolved. private static BitAiToolCallPart AwaitingApproval(string toolName, string title, BitAiToolRisk risk, string? reason = null) { var call = new BitAiToolCallPart(BitAiId.NewToolCallId(), toolName) { Definition = new BitAiToolDefinition(toolName) { Title = title, Risk = risk, RequiresApproval = true }, ApprovalReason = reason, }; call.CompleteArguments(); call.SetStatus(BitAiToolCallStatus.AwaitingApproval); return call; } ``` \n**Reason**: ```razor ``` ```csharp private readonly BitAiToolCallPart reasonCall = AwaitingApproval("send_email", "Send email", BitAiToolRisk.Write, "Send the weekly summary to the 3 people on the thread?"); private static BitAiToolCallPart AwaitingApproval(string toolName, string title, BitAiToolRisk risk, string? reason = null) { var call = new BitAiToolCallPart(BitAiId.NewToolCallId(), toolName) { Definition = new BitAiToolDefinition(toolName) { Title = title, Risk = risk, RequiresApproval = true }, ApprovalReason = reason, }; call.CompleteArguments(); call.SetStatus(BitAiToolCallStatus.AwaitingApproval); return call; } ``` \n**Risk**: ```razor
Last decision: @riskDecision
Ask again ``` ```csharp private BitAiToolCallPart readOnlyCall = AwaitingApproval("read_logs", "Read the logs", BitAiToolRisk.ReadOnly); private BitAiToolCallPart writeCall = AwaitingApproval("send_email", "Send email", BitAiToolRisk.Write); private BitAiToolCallPart destructiveCall = AwaitingApproval("delete_records", "Delete records", BitAiToolRisk.Destructive, "Permanently delete the 128 archived records? This cannot be undone."); private string riskDecision = "none yet"; // A call is decided once, so asking again means asking about fresh calls. private void ResetRisk() { readOnlyCall = AwaitingApproval("read_logs", "Read the logs", BitAiToolRisk.ReadOnly); writeCall = AwaitingApproval("send_email", "Send email", BitAiToolRisk.Write); destructiveCall = AwaitingApproval("delete_records", "Delete records", BitAiToolRisk.Destructive, "Permanently delete the 128 archived records? This cannot be undone."); riskDecision = "none yet"; } private static BitAiToolCallPart AwaitingApproval(string toolName, string title, BitAiToolRisk risk, string? reason = null) { var call = new BitAiToolCallPart(BitAiId.NewToolCallId(), toolName) { Definition = new BitAiToolDefinition(toolName) { Title = title, Risk = risk, RequiresApproval = true }, ApprovalReason = reason, }; call.CompleteArguments(); call.SetStatus(BitAiToolCallStatus.AwaitingApproval); return call; } ``` \n**Session**: ```razor @implements IAsyncDisposable Delete the archived records @if (SessionCall is { } call) { @if (call.Status is BitAiToolCallStatus.AwaitingApproval) { }
Status of the call: @call.Status @if (call.Result?.GetDisplayText() is { } outcome) { - @outcome }
} ``` ```csharp [Inject] private IBitAiToolRegistry ToolRegistry { get; set; } = default!; private BitAiRenderScheduler sessionRenders = default!; private BitAiChatSession session = default!; private BitButton deleteButton = default!; private bool focusDeleteButton; // The call of the latest turn, whatever it is waiting for. private BitAiToolCallPart? SessionCall => session.Conversation.Messages .LastOrDefault(m => m.Role.IsAssistant)? .PartsOf() .FirstOrDefault(); protected override void OnInitialized() { // At most one render of the page per frame interval, however often the session changes. sessionRenders = new BitAiRenderScheduler(() => InvokeAsync(StateHasChanged)); // A scripted model that asks for delete_records - registered with AddBitBlazorUIAIServices and declared // Destructive - so the session stops and waits before running it. A policy of its own keeps an "Always allow" // to this session. session = new BitAiChatSession( AiDemoToolTransports.CallsTool( "delete_records", """{"filter":"status = 'archived'"}""", "Done."), tools: ToolRegistry, approvalPolicy: new BitAiDefaultToolApprovalPolicy()); session.StateChanged += sessionRenders.Request; } private Task AskToDeleteAsync() => session.SubmitAsync("Delete the archived records"); // The answer takes the prompt away with the button it was given with; the button that asked takes the focus back. private void HandleSessionDecision() => focusDeleteButton = true; protected override async Task OnAfterRenderAsync(bool firstRender) { if (focusDeleteButton) { focusDeleteButton = false; await deleteButton.FocusAsync(); } } public async ValueTask DisposeAsync() { await session.DisposeAsync(); await sessionRenders.DisposeAsync(); } ``` \n**Disabled**: ```razor ``` ```csharp private BitAiToolCallPart destructiveCall = AwaitingApproval("delete_records", "Delete records", BitAiToolRisk.Destructive, "Permanently delete the 128 archived records? This cannot be undone."); private static BitAiToolCallPart AwaitingApproval(string toolName, string title, BitAiToolRisk risk, string? reason = null) { var call = new BitAiToolCallPart(BitAiId.NewToolCallId(), toolName) { Definition = new BitAiToolDefinition(toolName) { Title = title, Risk = risk, RequiresApproval = true }, ApprovalReason = reason, }; call.CompleteArguments(); call.SetStatus(BitAiToolCallStatus.AwaitingApproval); return call; } ``` \n**Cascading parameters**: ```razor ``` ```csharp private readonly BitAiToolApprovalParams[] approvalParams = [ new() { Styles = new() { ApproveButton = new() { Root = "border-radius: 1rem" }, Text = "font-style: italic" } }, ]; private BitAiToolCallPart readOnlyCall = AwaitingApproval("read_logs", "Read the logs", BitAiToolRisk.ReadOnly); private BitAiToolCallPart writeCall = AwaitingApproval("send_email", "Send email", BitAiToolRisk.Write); private BitAiToolCallPart destructiveCall = AwaitingApproval("delete_records", "Delete records", BitAiToolRisk.Destructive, "Permanently delete the 128 archived records? This cannot be undone."); private static BitAiToolCallPart AwaitingApproval(string toolName, string title, BitAiToolRisk risk, string? reason = null) { var call = new BitAiToolCallPart(BitAiId.NewToolCallId(), toolName) { Definition = new BitAiToolDefinition(toolName) { Title = title, Risk = risk, RequiresApproval = true }, ApprovalReason = reason, }; call.CompleteArguments(); call.SetStatus(BitAiToolCallStatus.AwaitingApproval); return call; } ``` \n**Style & Class**: ```razor

``` ```csharp private BitAiToolCallPart writeCall = AwaitingApproval("send_email", "Send email", BitAiToolRisk.Write); private BitAiToolCallPart destructiveCall = AwaitingApproval("delete_records", "Delete records", BitAiToolRisk.Destructive, "Permanently delete the 128 archived records? This cannot be undone."); private static BitAiToolCallPart AwaitingApproval(string toolName, string title, BitAiToolRisk risk, string? reason = null) { var call = new BitAiToolCallPart(BitAiId.NewToolCallId(), toolName) { Definition = new BitAiToolDefinition(toolName) { Title = title, Risk = risk, RequiresApproval = true }, ApprovalReason = reason, }; call.CompleteArguments(); call.SetStatus(BitAiToolCallStatus.AwaitingApproval); return call; } ``` \n**RTL**: ```razor
Decision: @rtlDecision
``` ```csharp private readonly BitAiUiOptions persianOptions = BitAiUiOptions.Default with { Strings = new PersianAiStrings() }; private readonly BitAiToolCallPart rtlCall = AwaitingApproval("delete_records", "حذف رکوردها", BitAiToolRisk.Destructive, "۱۲۸ رکورد بایگانی‌شده برای همیشه حذف شوند؟ این کار برگشت‌پذیر نیست."); private BitAiApprovalDecision rtlDecision; private static BitAiToolCallPart AwaitingApproval(string toolName, string title, BitAiToolRisk risk, string? reason = null) { var call = new BitAiToolCallPart(BitAiId.NewToolCallId(), toolName) { Definition = new BitAiToolDefinition(toolName) { Title = title, Risk = risk, RequiresApproval = true }, ApprovalReason = reason, }; call.CompleteArguments(); call.SetStatus(BitAiToolCallStatus.AwaitingApproval); return call; } // The words of the prompt, in Persian. private sealed class PersianAiStrings : BitAiStrings { public override string ApproveTool(string tool) => $"اجازه‌ی اجرای {tool} داده شود؟"; public override string Approve => "اجازه"; public override string ApproveAlways => "همیشه اجازه"; public override string Reject => "رد"; } ```