MentorAgent.Abstractions 1.0.0-rc.15

This is a prerelease version of MentorAgent.Abstractions.
dotnet add package MentorAgent.Abstractions --version 1.0.0-rc.15
                    
NuGet\Install-Package MentorAgent.Abstractions -Version 1.0.0-rc.15
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="MentorAgent.Abstractions" Version="1.0.0-rc.15" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MentorAgent.Abstractions" Version="1.0.0-rc.15" />
                    
Directory.Packages.props
<PackageReference Include="MentorAgent.Abstractions" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add MentorAgent.Abstractions --version 1.0.0-rc.15
                    
#r "nuget: MentorAgent.Abstractions, 1.0.0-rc.15"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package MentorAgent.Abstractions@1.0.0-rc.15
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=MentorAgent.Abstractions&version=1.0.0-rc.15&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=MentorAgent.Abstractions&version=1.0.0-rc.15&prerelease
                    
Install as a Cake Tool

MentorAgent.Abstractions

Release candidate — this version is a release candidate for 1.0.0, so dotnet add package needs --prerelease. Breaking changes are still possible before 1.0.0; the release notes mark each one Breaking.

You do not need to install this package directly. It is included automatically as a transitive dependency of MentorAgent and MentorAgent.Blazor.

This package is the shared foundation of the MentorAgent family. It contains all the contracts, models, and UI components shared between the server-side and client-side packages — with no AI or ASP.NET Core server dependencies, making it fully compatible with Blazor WebAssembly.


Table of Contents


Package Family

Package Install when
MentorAgent Blazor Server app
MentorAgent.Server Web API / headless backend, or Blazor Auto server-side project
MentorAgent.Blazor Blazor WASM / Blazor Auto client project
MentorAgent.Abstractions ← you are here Never directly — it arrives with any of the above
MentorAgent.Declarative Optional — define Level-2 specialist agents in YAML instead of C#

What's included

Interfaces

Interface Description
IMentorOrchestrator AI orchestrator contract — implemented by MentorOrchestrator (server) and WasmMentorOrchestrator (WASM)
IMentorStateService Thread-safe event bus between the AI layer and the UI layer
IMentorPageContext Page context and UI actions registry
IMentorAgent Marker interface for Level 2 specialized agents
IMentorTeam Marker interface for Level 3 collaborative teams
IMentorStructured Typed generation — GenerateAsync<T>(input, instructions) returns a deserialized T, with the JSON schema derived from your type
IMentorMetrics Read-side of the token/cost counters — GetSnapshot() feeds MentorDashboard
IMentorTour Supplies the onboarding tour steps. The default implementation generates them from the app's own pages and tools; register your own to script the tour by hand
IMentorMetricsStore Optional persistence/aggregation for those metrics. Implement it to survive restarts or to read an external aggregate (Prometheus, Azure Monitor) instead of the in-RAM snapshot

Models

Type Description
ConfirmationRequest HITL confirmation request shown in ConfirmationBanner
MentorRagResult A single RAG search result with content, URL, title, and score, plus optional Metadata the model is shown with it
MentorWidgetOptions UI-only options for ChatWidget (theme, position, language, etc.)
UIActionRegistration Handler registration for a page-level UI action
PageContextSnapshot Serializable page context sent from WASM client to server hub
UIActionInfo UI action descriptor in a PageContextSnapshot
AgentDisplayInfo Remote agent display info for the A2A detail badge
MentorAttachment One image on a user turn — DataBase64 (upload/paste) or Url (remote), mapped to the Agent Framework's DataContent / UriContent
MentorUserMessage A user turn as text + attachments; an image-only turn (empty text) is valid
MentorTourStep One step of the onboarding tour — title, body, the screen it describes and a ready-made question the user can send with one tap
MentorCard A generative-UI card a tool returns instead of prose — kind, title, subtitle, fields, actions, image, accent
MentorCardField / MentorCardAction One label/value row, and one button (SendMessage / Navigate / UIAction)
MentorMetricsSnapshot Point-in-time totals, per-model breakdown, hourly series, top actions and cost — the payload of GET /mentor/admin/metrics
MentorModelUsage One model's slice of that snapshot (tokens, calls, latency, cost)
ModelPrice record ModelPrice(decimal InputPer1M, decimal OutputPer1M) — you supply prices; the library ships no price table, because they change and a stale one lies

Enums

Enum Values
MentorLanguage English, Italian, French, German, Spanish, Portuguese, Dutch, Polish, Japanese, Chinese
MentorTheme Default, Dark, Minimal, Custom
ChatPosition BottomRight, BottomLeft, TopRight, TopLeft, SideRight, SideLeft
MentorshipLevel Minimal, Standard, Proactive
MentorHostedTools [Flags] — None, WebSearch, CodeInterpreter, FileSearch, ImageGeneration, HostedMcp. The set the model provider may run on its own infrastructure; also what the server publishes on connect so a client badge never drifts from the server
MentorModelKind Cheap, Strong, Embedding, Other — how a model's usage is attributed in the metrics snapshot. Other is any model id that is none of the three configured ones, e.g. a ClassifierChatClient on its own deployment
MentorCardActionKind SendMessage, Navigate, UIAction — what a card button does
MentorCardAccent Default, Success, Warning, Danger, Info — the card's colour accent

UI Components (Razor)

All MentorAgent UI components live here so they work identically in both Blazor Server and Blazor WASM:

Component Description
ChatWidget Main floating chat widget — injects CSS/JS automatically
MessageBubble A chat message: Markdown rendering, citation chips and any images the message carries. XSS-safe by construction — every piece of model text is HTML-encoded before a tag is emitted, so the model supplies data, never markup
ChatInput Text input with voice recognition and the image composer (upload, paste, drag & drop, remote URL)
MentorDashboard Admin token & cost dashboard — per-model breakdown, temporal SVG charts, cost table. Presentational: hand it a MentorMetricsSnapshot, from DI in Blazor Server or from GET /mentor/admin/metrics in WASM. Never rendered by ChatWidget — keep it off end-user pages
ConfirmationBanner HITL approval dialog
OnboardingTour Guided first-run tour. Presentational — it is handed the steps and raises callbacks, which is what lets the same component serve Blazor Server (steps from DI) and WASM (steps over HTTP)
MentorCardView Built-in renderer for a MentorCard. XSS-safe by construction — every value goes through Razor interpolation and no MarkupString is used, because a card carries data and never markup
WelcomePanel Empty-state welcome screen with suggestion chips
TypingIndicator Animated typing dots while AI processes
RagSourcePanel Citation chips below AI responses
McpStatusBadge / McpDetailBar MCP server connection status
A2AStatusBadge / A2ADetailBar A2A remote agent status

Core

Type Description
MentorLocalizer In-memory i18n for 10 languages — no .resx files, no external dependencies. Registered by AddMentorAgent() / AddMentorAgentBlazor(); inject it as MentorLocalizer and call L.Get("key") if you build UI that must match the widget's language
MentorLanguageExtensions MentorLanguage.Italian.ToIsoCode() → "it" (ISO 639-1; an unknown value gives "en"), and ToFullName() → "Italian". ToIsoCode() is what the components hand the JS layer, which expands it to the BCP-47 tag the browser Speech APIs expect ("it" → "it-IT"). A custom voice control that talks to SpeechRecognition / speechSynthesis directly has to do that expansion itself to speak the same language as the widget
TourProgress (internal) Where the onboarding tour resumes. Not API — documented only because its edge cases are user-visible

Component parameter reference

Every component is presentational in the sense that matters: it takes its data through parameters and raises callbacks out, never calling the AI itself. That is what lets the identical component serve Blazor Server (data from DI) and WebAssembly (data over HTTP), and it is what lets you compose the parts on your own pages instead of taking the whole widget.

ChatWidget is the exception — it is the assembled widget and resolves IMentorOrchestrator, IMentorStateService and navigation for itself.

They still need DI. Every component except MentorDashboard and MentorCardView injects IOptions<MentorWidgetOptions>, MentorLocalizer or both, so AddMentorAgent() / AddMentorAgentBlazor() must have run in that host. Rendering MessageBubble on a page of an app that never registered MentorAgent throws at build-render time, not at first interaction.

Namespaces and injections. The components live in MentorAgent.Abstractions.Components, the models and enums in MentorAgent.Abstractions.Models, the interfaces in MentorAgent.Abstractions.Interfaces and MentorLocalizer in MentorAgent.Abstractions.Core. ChatMessage / ChatRole come from Microsoft.Extensions.AI. The examples below assume these in your _Imports.razor, plus the services they call injected on the page:

@using MentorAgent.Abstractions.Components
@using MentorAgent.Abstractions.Models
@using MentorAgent.Abstractions.Interfaces
@using MentorAgent.Abstractions.Core
@using Microsoft.Extensions.AI

@inject IMentorOrchestrator Orchestrator
@inject IMentorStateService State
@inject NavigationManager   Nav
@inject IMentorTour         Tour

ChatWidget

Parameter Type Description
CardTemplate RenderFragment<MentorCard>? Optional custom renderer for generative-UI cards. Not supplied → MentorCardView is used. Match on card.Kind and fall back to the built-in renderer for kinds you do not handle
@* A MentorCardView inside the template needs no OnAction: ChatWidget cascades its own
   dispatcher around the cards, so these buttons send / navigate / run the UI action. *@
<ChatWidget>
    <CardTemplate Context="card">
        @if (card.Kind == "order") { <OrderCard Card="card" /> }
        else                       { <MentorCardView Card="card" /> }
    </CardTemplate>
</ChatWidget>

The template replaces the widget's card rendering for every card, but not its button handling: a <MentorCardView Card="card" /> rendered inside the template without OnAction uses the widget's dispatcher, so its SendMessage, Navigate and UIAction buttons work as they do on an untemplated card. An OnAction you set explicitly still wins. Buttons your template draws itself (inside OrderCard above) do not get the dispatcher: handle them yourself, or render MentorCardView for the cards whose buttons should just work.

Everything else about the widget comes from MentorWidgetOptions in DI, not from parameters — see the option tables in the MentorAgent or MentorAgent.Blazor README, depending on which host you use.

MentorDashboard

Parameter Type Default Description
Snapshot MentorMetricsSnapshot? null An override, not the only source. Pass it in WASM/headless. In Blazor Server leave it null and the component resolves the data itself
OnRefresh EventCallback — Fired after the Refresh button re-resolves. Handle it in WASM to re-fetch; in Blazor Server you can ignore it
Currency string "$" Symbol prefixed to every cost figure. The library never converts — supply ModelPricing already in the currency you want shown
Language MentorLanguage? null null → the MentorLocalizer from DI, which follows the Language option on both hosts (AddMentorAgent() and AddMentorAgentBlazor() both register it). Pass it only to override that language, or in an app that registered neither; there the dashboard falls back to English

The Refresh button is part of the populated view; OnRefresh only decides whether anything of yours runs when it is pressed. With no data at all the component renders its empty state rather than throwing, and that state has no Refresh button. In WASM, a first fetch that failed cannot be retried from the dashboard: retry it yourself (a button of your own, or a timer) and pass the new Snapshot.

Its resolution order when Snapshot is null is: an external IMentorMetricsStore.QueryAsync() first (so a Prometheus/Azure Monitor aggregate wins), then the live in-RAM IMentorMetrics snapshot.

@* Blazor Server — no parameters needed *@
<MentorDashboard />

@* WASM — you own the fetch *@
<MentorDashboard Snapshot="_snapshot"
                 OnRefresh="Reload"
                 Currency="€"
                 Language="MentorLanguage.Italian" />

MentorCardView

Parameter Type Description
Card MentorCard Required. The card to render
OnAction EventCallback<MentorCardAction> Raised when a button is pressed. Left unset inside ChatWidget (including in a CardTemplate), the widget's own dispatcher sends / navigates / runs the UI action; an explicit OnAction wins. Wire it yourself when you render cards outside the widget
<MentorCardView Card="_card" OnAction="RunCardAction" />

@code {
    private readonly MentorCard _card = new("order")
    {
        Title    = "Ordine #1002",
        Subtitle = "Laura Bianchi",
        Accent   = MentorCardAccent.Info,
        Fields   = [new("Stato", "Shipped"), new("Totale", "1.249,98 €")],
        Actions  =
        [
            new("Apri",     MentorCardActionKind.Navigate,    "/orders/1002"),
            new("Dettagli", MentorCardActionKind.SendMessage, "Dammi i dettagli dell'ordine 1002"),
        ],
    };

    // @inject IMentorOrchestrator Orchestrator, NavigationManager Nav, IMentorPageContext PageContext
    private async Task RunCardAction(MentorCardAction action)
    {
        switch (action.Kind)
        {
            case MentorCardActionKind.SendMessage:
                await Orchestrator.SendMessageAsync(action.Value);
                break;

            case MentorCardActionKind.Navigate:
                Nav.NavigateTo(action.Value);
                break;

            // Resolved at click time, as ChatWidget does: the page that registered the action
            // may have changed since the card was rendered.
            case MentorCardActionKind.UIAction
                when PageContext.UIActions.TryGetValue(action.Value, out var reg):
                if (reg.HandlerAsync is not null) await reg.HandlerAsync(null);
                else reg.Handler(null);
                break;
        }
    }
}

The buttons are written by the tool that produced the card, never by the model's prose — which is why a card can safely carry actions. Inside ChatWidget the buttons are handled for you, in a CardTemplate too as long as the card is rendered by MentorCardView; wire OnAction yourself only when you render cards outside the widget.

MessageBubble

Parameter Type Description
Message ChatMessage Required. The MEAI message. Role decides the side and styling
IsStreaming bool Renders the trailing caret while text is still arriving
SentAt DateTime? Timestamp under the bubble; omitted when null
Sources IReadOnlyList<MentorRagResult>? RAG citation chips below the message, rendered through RagSourcePanel, so only on an assistant message and only when MentorWidgetOptions.ShowRagSources is on
@foreach (var m in _messages)
{
    <MessageBubble Message="m.Message" SentAt="m.At" Sources="m.Sources" />
}

@* the one still arriving — the caret is the only difference *@
@if (_streaming is not null)
{
    <MessageBubble Message="_streaming" IsStreaming="true" />
}

Message is MEAI's own ChatMessage, so the role decides the side and the styling. Model text is HTML-encoded before any Markdown tag is emitted, so a reply containing <script> renders as text.

ChatInput

Parameter Type Description
OnSend EventCallback<MentorUserMessage> Required. Raised with text and attachments — an image-only turn (empty text) is valid
Disabled bool Blocks input while a turn is in flight
Placeholder string? Overrides the localised default. MentorWidgetOptions.InputPlaceholder is not read here: ChatWidget passes it in, so pass it yourself on your own surface

The rest comes from MentorWidgetOptions. The microphone appears only with EnableVoiceInput (and a browser that supports speech recognition). The image composer appears only with EnableImageInput, limited by MaxImageBytes, MaxImagesPerMessage and AllowedImageTypes. MaxMessageLength sets the textarea's maxlength.

<ChatInput OnSend="Send" Disabled="_busy" Placeholder="Chiedi qualcosa sugli ordini…" />

@code {
    private bool _busy;

    private async Task Send(MentorUserMessage message)
    {
        _busy = true;
        try
        {
            // Text AND attachments. An image-only turn — empty Text, one attachment — is valid.
            await Orchestrator.SendMessageAsync(message.Text, message.Attachments);
        }
        finally { _busy = false; }
    }
}

OnboardingTour

Parameter Type Description
Steps IReadOnlyList<MentorTourStep> Required. From IMentorTour in Blazor Server, or GET /mentor/tour in WASM
StartIndex int Where to open. Clamped into range, so a stale stored index cannot throw
OnIndexChanged EventCallback<int> Fired on every step change — this is what the host persists to resume later
OnNavigate EventCallback<string> The step's URL, when the user chooses to go there
OnAsk EventCallback<string> The step's ready-made question, when the user taps it
OnClose EventCallback Skip or finish

OnNavigate and OnAsk are engagement, not dismissal: the host is expected to suspend the tour and resume at the next step, not mark it seen. ChatWidget already does this.

@if (_tourOpen)
{
    <OnboardingTour Steps="_steps"
                    StartIndex="_resumeAt"
                    OnIndexChanged="Persist"
                    OnNavigate="GoThere"
                    OnAsk="AskThat"
                    OnClose="Finish" />
}

@code {
    private IReadOnlyList<MentorTourStep> _steps = [];
    private bool _tourOpen;
    private int  _resumeAt;

    protected override async Task OnInitializedAsync()
    {
        // IMentorTour from DI on both hosts: generated in-process on Blazor Server,
        // fetched from GET /mentor/tour by MentorAgent.Blazor on WebAssembly.
        _steps    = await Tour.GetStepsAsync();
        _resumeAt = await LoadSavedIndex();          // your storage; clamped for you
        // At or past the end: the user already acted on the last step, so there is nothing to resume.
        _tourOpen = _steps.Count > 0 && _resumeAt < _steps.Count;
    }

    // Track where the user IS, not where the tour opened: the component moves on its own and
    // OnIndexChanged is the only way you learn it.
    private Task Persist(int index) { _resumeAt = index; return SaveIndex(index); }

    // Engagement, not dismissal: suspend and resume at the NEXT step rather than marking it seen.
    private async Task GoThere(string url)  { _tourOpen = false; await SaveIndex(_resumeAt + 1); Nav.NavigateTo(url); }
    private async Task AskThat(string q)    { _tourOpen = false; await SaveIndex(_resumeAt + 1); await Orchestrator.SendMessageAsync(q); }

    private Task Finish() { _tourOpen = false; return MarkSeen(); }
}

ConfirmationBanner

Parameter Type Description
Request ConfirmationRequest Required. What is being approved
OnConfirm EventCallback Required. Approve
OnCancel EventCallback Required. Reject
@if (_pending is not null)
{
    <ConfirmationBanner Request="_pending" OnConfirm="Approve" OnCancel="Reject" />
}

@code {
    private ConfirmationRequest? _pending;

    protected override void OnInitialized() =>
        State.OnConfirmationRequired += r => { _pending = r; InvokeAsync(StateHasChanged); };

    private async Task Approve() { var r = _pending!; _pending = null; await State.ConfirmAsync(r.ActionId); }
    private Task     Reject()    { var r = _pending!; _pending = null; State.Cancel(r.ActionId); return Task.CompletedTask; }
}

On WebAssembly the snippet above already answers over HTTP: State.ConfirmAsync / State.Cancel post to POST /mentor/approve?actionId=…&approved=true|false rather than calling a hub method, because SignalR dispatches one invocation at a time per connection, so a hub call would queue behind the very turn it is meant to release. The request goes to the hub's origin when MentorAgentBlazorOptions.HubUrl is an absolute http(s) URL (relative to the host HttpClient's BaseAddress when it is relative, i.e. same-origin hosting) and carries Authorization: Bearer <token> from MentorAgentBlazorOptions.AccessTokenProvider when one is set, the same credential the hub uses, so a signed-in user's answer is not refused with 401. Stop takes the same route (POST /mentor/cancel). The host must still register an HttpClient, as the WASM template does. Call the endpoints yourself only from a client that has no IMentorStateService (JavaScript, React), and send the same bearer token there.

WelcomePanel

Parameter Type Default Description
BotName string "Mentor AI" Shown in the empty state heading and as the avatar's alt text
WelcomeMessage string? null null or blank → the localised default. Rendered as HTML, not encoded: pass only text you wrote, never user or model input
EnableSuggestions bool true Whether to render the three built-in, localised suggestion chips. They also need OnChipClick; with no handler they are not rendered
OnChipClick EventCallback<string> — The chip's text, to be sent as a user turn
@if (_messages.Count == 0)
{
    <WelcomePanel BotName="Assistente ShopFlow"
                  WelcomeMessage="Ciao! Posso aiutarti con ordini e prodotti."
                  EnableSuggestions="true"
                  OnChipClick="Send" />
}

The chip's text arrives as an ordinary user turn — there is nothing special about it, which is why OnChipClick and the input's OnSend can share one handler.

RagSourcePanel

Parameter Type Description
Sources IReadOnlyList<MentorRagResult>? Citation chips: the first three, then a "+N more" chip that expands the rest. Renders nothing when null, empty, or when MentorWidgetOptions.ShowRagSources is off, which is the default: set options.ShowRagSources = true on either host
<RagSourcePanel Sources="_sources" />

@code {
    // null or empty renders nothing at all, so there is no need to guard the element.
    private IReadOnlyList<MentorRagResult>? _sources;

    protected override void OnInitialized() =>
        State.OnRagSourcesReady += s => { _sources = s; InvokeAsync(StateHasChanged); };
}

Status badges

Component Parameters
McpStatusBadge ServerStatus (IReadOnlyDictionary<string, bool?>? — name → connected/failed/unknown), OnToggle
McpDetailBar Visible (bool), ServerStatus, OnClose
A2AStatusBadge OnToggle — the agent list comes from MentorWidgetOptions.RemoteAgentDisplays
A2ADetailBar Visible (bool), OnClose

bool? in ServerStatus is three-valued on purpose: true connected, false failed, null not yet probed. Rendering null as "failed" would show a red badge during startup.

The badges are also gated by MentorWidgetOptions, so on a page of your own they render nothing until the matching options are set. McpStatusBadge needs ShowMcpStatus and HasMcpServers, plus at least one entry in ServerStatus. McpDetailBar needs HasMcpServers and a non-empty ServerStatus. A2AStatusBadge needs ShowA2AStatus and HasRemoteAgents. A2ADetailBar needs a non-empty RemoteAgentDisplays. On Blazor Server, AddMentorAgent() fills HasMcpServers, HasRemoteAgents, RemoteAgentDisplays and McpServerNames from your MCP and A2A configuration. On WebAssembly you set them on MentorAgentBlazorOptions, because the client cannot see the server's configuration.

<McpStatusBadge ServerStatus="_mcp" OnToggle="() => _mcpOpen = !_mcpOpen" />
<McpDetailBar   Visible="_mcpOpen" ServerStatus="_mcp" OnClose="() => _mcpOpen = false" />

<A2AStatusBadge OnToggle="() => _a2aOpen = !_a2aOpen" />
<A2ADetailBar   Visible="_a2aOpen" OnClose="() => _a2aOpen = false" />

@code {
    private bool _mcpOpen, _a2aOpen;

    // name → connected. Seed every configured server as null ("connecting") rather than false:
    // nothing has been probed yet. An EMPTY dictionary hides the badge until the first status
    // event, which with lazily-connected MCP servers is the first message.
    // (@inject IOptions<MentorWidgetOptions> Options)
    private readonly Dictionary<string, bool?> _mcp = new(StringComparer.OrdinalIgnoreCase);

    protected override void OnInitialized()
    {
        foreach (var name in Options.Value.McpServerNames) _mcp[name] = null;

        State.OnMcpServerStatusChanged += (name, ok) =>
        {
            _mcp[name] = ok;
            InvokeAsync(StateHasChanged);
        };
    }
}

The A2A badge takes no data parameter on purpose — its agent list comes from MentorWidgetOptions.RemoteAgentDisplays, because a remote agent is configuration rather than runtime state.

TypingIndicator

No parameters — animated dots. Show it while a turn is in flight and nothing has streamed yet.

@if (_busy && _streaming is null)
{
    <TypingIndicator />
}

Show it only while a turn is in flight and nothing has streamed yet — once the first token lands, MessageBubble with IsStreaming="true" carries the signal instead.


Composing your own chat surface

The reason these components take parameters and raise callbacks instead of calling the AI is so you can build a surface that is not the floating widget — an inline panel, a side rail, a page of your own. Everything below is host-agnostic: in Blazor Server the state service comes from DI, on WebAssembly the same interface is backed by the hub.

Without ChatWidget on the page nothing injects the stylesheet and the script, so add both tags yourself: see Static assets — CSS and JS.

@inject IMentorOrchestrator Orchestrator
@inject IMentorStateService State
@implements IDisposable

<div class="my-chat">
    @if (_messages.Count == 0)
    {
        <WelcomePanel BotName="Assistente" OnChipClick="Send" />
    }

    @foreach (var m in _messages)
    {
        <MessageBubble Message="m" />
    }
    @if (_streaming is not null) { <MessageBubble Message="_streaming" IsStreaming="true" /> }
    @if (_busy && _streaming is null) { <TypingIndicator /> }

    <RagSourcePanel Sources="_sources" />

    @if (_pending is not null)
    {
        <ConfirmationBanner Request="_pending" OnConfirm="Approve" OnCancel="Reject" />
    }

    <ChatInput OnSend="OnSend" Disabled="_busy" />
</div>

@code {
    private readonly List<ChatMessage> _messages = [];
    private ChatMessage? _streaming;
    private IReadOnlyList<MentorRagResult>? _sources;
    private ConfirmationRequest? _pending;
    private bool _busy;
    private readonly System.Text.StringBuilder _buffer = new();

    protected override void OnInitialized()
    {
        State.OnStreamingChunk     += Chunk;
        State.OnStreamingCompleted += Completed;
        State.OnBusyChanged        += Busy;
        State.OnRagSourcesReady    += Rag;
        State.OnConfirmationRequired += Confirm;
    }

    private void Chunk(string c)
    {
        _buffer.Append(c);
        _streaming = new ChatMessage(ChatRole.Assistant, _buffer.ToString());
        InvokeAsync(StateHasChanged);
    }

    private void Completed()
    {
        if (_buffer.Length > 0) _messages.Add(new ChatMessage(ChatRole.Assistant, _buffer.ToString()));
        _buffer.Clear();
        _streaming = null;
        InvokeAsync(StateHasChanged);
    }

    private void Busy(bool b)                          { _busy = b; InvokeAsync(StateHasChanged); }
    private void Rag(IReadOnlyList<MentorRagResult> s) { _sources = s; InvokeAsync(StateHasChanged); }
    private void Confirm(ConfirmationRequest r)        { _pending = r; InvokeAsync(StateHasChanged); }

    private async Task Send(string text) => await OnSend(new MentorUserMessage(text));

    private async Task OnSend(MentorUserMessage m)
    {
        _messages.Add(new ChatMessage(ChatRole.User, m.Text));
        _sources = null;
        await Orchestrator.SendMessageAsync(m.Text, m.Attachments);
    }

    private async Task Approve() { var r = _pending!; _pending = null; await State.ConfirmAsync(r.ActionId); }
    private Task     Reject()    { var r = _pending!; _pending = null; State.Cancel(r.ActionId); return Task.CompletedTask; }

    public void Dispose()
    {
        State.OnStreamingChunk       -= Chunk;
        State.OnStreamingCompleted   -= Completed;
        State.OnBusyChanged          -= Busy;
        State.OnRagSourcesReady      -= Rag;
        State.OnConfirmationRequired -= Confirm;
    }
}

Unsubscribe. These are plain C# events on a scoped service that outlives your component, so a component that subscribes and never detaches is kept alive by the service and re-rendered after disposal. ChatWidget does this for you; a hand-built surface has to do it itself.

Subscribe to OnError too. A message the host refuses before the model runs produces no streamed text and no OnStreamingCompleted: only OnError with the localised reason, then OnBusyChanged(false). That covers a message that is too long or rate-limited, one blocked by the safety check (or, with SafetyCheckFailure = MentorSafetyCheckFailure.Block, one whose check could not run), and one refused as out of scope under RefuseOutOfScope. Provider failures arrive the same way. Without a handler, the surface above shows the user's bubble and then nothing:

@if (_error is not null) { <div class="my-chat__error">@_error</div> }

@code {
    private string? _error;

    // OnInitialized: State.OnError += Error;    Dispose: State.OnError -= Error;
    // OnSend:        _error = null;  before sending the next message
    private void Error(string message) { _error = message; InvokeAsync(StateHasChanged); }
}

Static assets — CSS and JS

The stylesheet and the JavaScript layer ship in this package, not in MentorAgent:

_content/MentorAgent.Abstractions/css/MentorAgent.css
_content/MentorAgent.Abstractions/js/MentorAgent.js
_content/MentorAgent.Abstractions/IconMA.png     ← default avatar

ChatWidget injects the first two itself through <HeadContent>, so a Blazor Server or Blazor Web App host needs no markup. A standalone WASM host references them manually in index.html instead — and neither file is fingerprinted, so the ?v= query string is the cache buster and you must bump it by hand after changing either. A stale MentorAgent.js fails silently and looks like missing features rather than a caching problem.

That injection happens only where ChatWidget is rendered, and it relies on the <HeadOutlet /> that the Blazor Web App template already includes. A page that composes the parts without ChatWidget (see Composing your own chat surface) gets neither file: the components render unstyled, and ChatInput's microphone and paste / drag & drop stay off without an error. Sending a message still works: the call that resets the textarea after a send is guarded, so a missing script neither throws nor ends a Blazor Server circuit. Reference both files yourself in that case, with the same paths and version ChatWidget uses, in App.razor (Blazor Server / Web App) or index.html:

<link href="_content/MentorAgent.Abstractions/css/MentorAgent.css?v=10" rel="stylesheet" />
<script src="_content/MentorAgent.Abstractions/js/MentorAgent.js?v=10"></script>

The script can safely load a second time on a page where ChatWidget also renders: the later tag wins and the voice state carries over.

The JS layer covers text-to-speech (sentence-queued, barge-in, hands-free), speech recognition, the image composer (upload, paste, drag & drop, client-side downscaling) and localStorage helpers. It is called through IJSRuntime by the components; there is no supported way to call it directly.

For the complete CSS variable reference — palette, widget size, fonts, the dashboard and chart colours — see Widget customization → CSS variables in the MentorAgent README. The variables are defined here but documented there, next to the options that set them.


When you actually touch this package

Most of the time you never reference these types by name — MentorAgent and MentorAgent.Blazor wire them for you. Three cases where you do:

1. Rendering the admin dashboard yourself. MentorDashboard is presentational, so the same component works in-process and over HTTP. In Blazor Server, inject the metrics; in WASM, fetch the snapshot from the admin endpoint:

@* Blazor Server — in-process. It resolves the metrics itself; no parameters needed. *@
<MentorDashboard />

@* WASM — over HTTP. There is no IMentorMetrics in the browser, so pass the snapshot. Language is optional: it follows AddMentorAgentBlazor's Language unless you override it. *@
@inject HttpClient Http
<MentorDashboard Snapshot="_snapshot" Language="MentorLanguage.Italian" />
@code {
    MentorMetricsSnapshot? _snapshot;
    protected override async Task OnInitializedAsync() =>
        _snapshot = await Http.GetFromJsonAsync<MentorMetricsSnapshot>("/mentor/admin/metrics");
}

Put it behind authorization. The snapshot is operator data — cost, volumes, top actions — and must never appear on an end-user page.

The endpoint is gated on its own as well: GET /mentor/admin/metrics requires the role in MentorOptions.DashboardRole ("Admin" by default; "" leaves it open, for development only). The HttpClient you fetch with must therefore send the signed-in admin's credentials (cookie or bearer token). Without them the request is refused (401/403, or a redirect to the login page under cookie authentication) and GetFromJsonAsync throws instead of the dashboard rendering its empty state. In a standalone WASM app, its BaseAddress must also be the server's origin, not the client's.

2. Persisting or aggregating metrics. Implement IMentorMetricsStore and register it before AddMentorAgent(), in a headless host too, where AddMentorAgentServer() comes after it. Registered any later, the store still answers QueryAsync, but LoadForSeedAsync and SaveAsync are never called, because the persistence service is added only when the store is already there. Without one the snapshot lives in RAM and resets on restart; with one you get durability, or you can serve an external aggregate instead:

Every method has a default no-op implementation, because the two scenarios use different halves of the contract — implement only the one you need:

// (A) Local durability — restore totals at startup, persist periodically and on shutdown.
//     QueryAsync stays default (null) so the dashboard keeps serving the live in-RAM snapshot.
public sealed class FileMetricsStore : IMentorMetricsStore
{
    public Task<MentorMetricsSnapshot?> LoadForSeedAsync(CancellationToken ct = default) => /* read  */;
    public Task SaveAsync(MentorMetricsSnapshot s, CancellationToken ct = default)       => /* write */;
}

// (B) External source — serve the aggregate your OpenTelemetry export already pushed to
//     Prometheus / Azure Monitor (multi-instance, historical). Nothing to seed, nothing to save.
public sealed class PrometheusMetricsStore : IMentorMetricsStore
{
    public Task<MentorMetricsSnapshot?> QueryAsync(CancellationToken ct = default) => /* query */;
}

builder.Services.AddSingleton<IMentorMetricsStore, FileMetricsStore>();   // BEFORE AddMentorAgent()

3. Building your own composer or transcript. MentorUserMessage + MentorAttachment are the shape of a user turn, and IMentorOrchestrator.SendMessageAsync(text, attachments) is what consumes it — so a custom input control can replace ChatInput without touching the orchestration layer.

Working implementations of all three ship in the MentorAgentSample repository.


Why a separate package?

MentorAgent (server) depends on Microsoft.Agents.AI, ModelContextProtocol.AspNetCore, and other server-only packages that are not compatible with Blazor WebAssembly. MentorAgent.Blazor (WASM) cannot reference MentorAgent directly.

MentorAgent.Abstractions contains only WebAssembly-safe dependencies:

  • Microsoft.AspNetCore.Components.Web
  • Microsoft.AspNetCore.Components.Authorization
  • Microsoft.Extensions.AI

Both MentorAgent and MentorAgent.Blazor reference MentorAgent.Abstractions — sharing interfaces, models, and UI components without cross-contaminating dependencies.

The practical rule: anything that must run in the browser goes here. If you are adding a type and it needs Microsoft.Agents.AI, an HttpContext, or the file system, it does not belong in this package — the WASM build will not fail at compile time, it will fail at runtime in the browser.


Requirements

  • .NET 10.0 or later
  • Microsoft.AspNetCore.Components.Web 10.0.3
  • Microsoft.AspNetCore.Components.Authorization 10.0.3
  • Microsoft.Extensions.AI 10.7.0

No AI provider, no server packages, no JavaScript dependencies beyond the browser's own Speech and File APIs.


Package Purpose
MentorAgent Blazor Server — full AI assistant
MentorAgent.Server Any ASP.NET Core app — headless AI backend
MentorAgent.Blazor Blazor WASM — SignalR client
MentorAgent.Declarative Optional — Level-2 specialists defined in YAML

License

Apache License 2.0 — the full text is in the LICENSE file inside this package, with the copyright notice in NOTICE. Versions up to and including 1.0.0-rc.14 were released under the MIT License, which still applies to them.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on MentorAgent.Abstractions:

Package Downloads
MentorAgent

A Blazor Razor Class Library that adds an AI-powered floating chat assistant to any Blazor application, built on Microsoft Agent Framework. Supports multi-level agent orchestration (tools, handoff, group chat), page navigation, UI actions, contextual memory, streaming, voice, and security — all with zero boilerplate.

MentorAgent.Blazor

Blazor WebAssembly client for MentorAgent. Install in your Blazor WASM / Blazor Auto client project. Connects to a MentorAgent.Server hub via SignalR and provides the same ChatWidget experience as Blazor Server.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0-rc.15 63 10/7/2026
1.0.0-rc.14 75 10/3/2026
1.0.0-rc.13 84 9/29/2026
1.0.0-rc.12 88 9/23/2026
1.0.0-rc.11 89 9/23/2026
1.0.0-rc.10 86 9/19/2026
1.0.0-rc.9 81 9/19/2026
1.0.0-rc.8 84 9/18/2026
1.0.0-rc.7 83 9/16/2026
1.0.0-rc.6 87 9/14/2026
1.0.0-rc.5 87 9/13/2026
1.0.0-rc.4 107 9/9/2026
1.0.0-rc.3 93 9/4/2026
1.0.0-rc.2 104 8/24/2026
1.0.0-rc.1 122 8/19/2026
1.0.0-preview.5 102 8/12/2026
1.0.0-preview.4 103 8/4/2026
1.0.0-preview.3 92 7/24/2026
1.0.0-preview.2 98 6/22/2026
1.0.0-preview 104 6/22/2026

1.0.0-rc.15 - 2026-10-07

### Added

- **MentorAgent.Server:** `AddMentorAgentMcp()` and `AddMentorAgentA2A()` register the MCP server and the A2A
 agent, each only when its option (`McpServerEnabled`, `A2AServerEnabled`) is on, as `AddMentorAgent` did up
 to rc.14. `AddMentorAgentServer()` calls both, so a headless host changes nothing. `MapMentorAgentMcp()` /
 `MapMentorAgentA2A()` without them stop the host at startup with an error that names the method to add.
- **MentorAgent:** a RAG source can see who is asking, from where and after what. MentorAgent now calls
 `IMentorRagSource.SearchAsync(MentorRagRequest, CancellationToken)`, whose request carries the message,
 `RagResultCount`, the last three turns of the conversation, the signed-in `ClaimsPrincipal`, and the
 current page's name and data. Its default implementation calls `SearchAsync(query, maxResults)`, so every
 existing source compiles and behaves as before.
- **MentorAgent.Abstractions:** `MentorRagResult.Metadata`, key/value facts about a document (a date, a
 version). The model sees them under the document's title. They also travel with the citation chips: hub
 clients receive `metadata` in `RagSourcesReady`. It is an init property, so the four-value constructor is
 unchanged.
- **MentorAgent:** `IMentorRagSource.DefaultMinScore` lets a source declare the relevance threshold for its own
 scale. It applies when the host has not set `RagMinScore`, in code or through configuration. A vector source
 that declares one, say 0.6, now gets its documents to the model with the default options; before, the
 default of 2 discarded every cosine score. The wrong-scale warning now says which threshold discarded
 everything: the option, the source's or the default.
- **MentorAgent:** `MentorOptions.ChatReducer` replaces the reducer of the built-in in-memory history, and
 `MentorChatReducer` (namespace `MentorAgent.Core`), the new default, can be given to a history provider of
 your own. A `ChatReducer` that cannot apply — next to a provider you supply, or with
 `UseServiceManagedHistory` — logs a warning.

### Changed

- The five packages are licensed under the Apache License 2.0 from this version, with a `NOTICE` file next to
 `LICENSE`. Versions up to and including 1.0.0-rc.14 keep the MIT License.
- Package metadata: the authors are Emanuele Longo and Antonio Macrì, the copyright holders named in `NOTICE`;
 the project page is https://mentoragent.net/.
- **Breaking — MentorAgent:** the MCP server and the A2A agent moved to **MentorAgent.Server**, together with
 `MapMentorAgentMcp()` and `MapMentorAgentA2A()` (`EndpointRouteBuilderExtensions`, same namespace
 `MentorAgent.Extensions`). `AddMentorAgent` no longer registers them. The reason: their packages,
 `ModelContextProtocol.AspNetCore` and `Microsoft.Agents.AI.Hosting.A2A.AspNetCore`, require
 `Microsoft.AspNetCore.App`, which does not exist on Android, iOS or Mac Catalyst. A MAUI app that referenced
 MentorAgent built only for Windows (`NETSDK1082`). Now the MAUI Blazor Hybrid template, unchanged, builds
 for `net10.0-android`, and it no longer needs its `Microsoft.Extensions.Logging.Debug` pin raised (`NU1605`).
 MentorAgent now references `ModelContextProtocol` 1.3.0 and `A2A` 1.0.0-preview2, for the MCP and A2A
 clients. MentorAgent.Server references `ModelContextProtocol.AspNetCore` 1.3.0 and `A2A.AspNetCore`
 1.0.0-preview2.
 **Migration.** A host that calls `AddMentorAgentServer()` changes nothing in its code. A Blazor Server host
 that serves MCP or A2A without the hub adds the `MentorAgent.Server` package and calls
 `builder.Services.AddMentorAgentMcp()` and/or `AddMentorAgentA2A()` after `AddMentorAgent()`; its usings
 stay as they are. Code compiled against rc.14 must be rebuilt, because the type changed assembly.
 A host no longer receives these packages transitively from MentorAgent, so it references the ones it uses:
 `Microsoft.Agents.AI.Hosting`, `Microsoft.Agents.AI.Hosting.A2A`, `Microsoft.Agents.AI.Hosting.AspNetCore`,
 `Microsoft.Agents.AI.Hosting.A2A.AspNetCore`, `Microsoft.Extensions.Hosting`, `Microsoft.Extensions.Logging.Console`
 and the other `Microsoft.Extensions.Configuration.*` / `Logging.*` packages that came with it (BUG-119, S2).
- **MentorAgent:** the READMEs now place MCP and A2A where the code has them: in MentorAgent.Server.
- **Breaking — MentorAgent:** writes ask for confirmation by default. A `[MentorAction]` that declares neither
 `ReadOnly = true` nor `RequiresConfirmation` is treated as an action that changes data. It shows the
 confirmation banner, in the Blocking flow and in `HitlMode.Native` alike, like one declared
 `RequiresConfirmation = true` (`RequireConfirmationForWrites`, new, on by default). Being gated, it is also
 withheld from the MCP server (`tools/list`) and from the A2A agent card, and it is no longer among the
 ungated actions the zero-match fallback may send on a message that matches nothing. With
 `RequireActionForChangeRequests` on, more actions can now be required as the first call, because only
 actions that ask are ever required. **Only `[MentorAction]` methods are covered**: a method marked with
 `[Description]` alone asks for nothing and stays on `/mcp`, as before; mark it `[MentorAction]` to have the
 default. **Migration:** declare `ReadOnly = true` on the actions that only read and
 `RequiresConfirmation = false` on the writes that must run unasked, or set
 `options.RequireConfirmationForWrites = false` for the rc.14 behaviour. At startup a warning lists the writes
 that run without confirmation, in two lists: the `[MentorAction]` methods opted out, and the
 `[Description]`-only methods not declared read-only with `IsReadOnlyAction`.
- **Breaking — MentorAgent:** no AI provider SDK comes with the package any more: it no longer references
 `Azure.AI.OpenAI` 2.9.0-beta.1, `Azure.Identity` and `Microsoft.Extensions.AI.OpenAI [10.6.0]`, which the
 library never used. It references `System.ClientModel` 1.10.0, the version those packages brought, because it
 reads the HTTP status of a provider's `ClientResultException`. A host now adds its provider at the version it
 chooses — for Azure OpenAI, `Azure.AI.OpenAI` and `Microsoft.Extensions.AI.OpenAI` (`AsIChatClient()`), plus
 `Azure.Identity` for `DefaultAzureCredential`. With the Azure **Responses** API keep `OpenAI` at 2.10
 (`Microsoft.Extensions.AI.OpenAI [10.6.0]`). On 2.11 or later, `GetResponsesClient()` from `Azure.AI.OpenAI`
 2.9.0-beta.1 throws when it is called: `MissingMethodException: Method not found: 'Void
 OpenAI.Responses.ResponsesClient..ctor(System.ClientModel.Primitives.ClientPipeline, OpenAI.OpenAIClientOptions)'`.
 Chat Completions needs no hold: on `OpenAI` 2.14 its client is built where the Responses one throws (BUG-120,
 S2).
- **Breaking — MentorAgent:** the built-in history keeps tool calls and their results. Up to rc.14 it was
 trimmed by `MessageCountingChatReducer`, which drops every message carrying a function call or result: from
 the next turn on, the model no longer saw the data a tool had read, and answered a follow-up by guessing or by
 calling the tool again. The default is now `MentorChatReducer`: it counts every message, tool calls and
 results included, against `MaxSessionMessages`, trims whole turns, oldest first, and never sends a result
 without the call that produced it. In a lab run of 84 turns the turns answered correctly went from 85% to
 95%, and tokens grew by about 9%. With the same `MaxSessionMessages` a session holds fewer turns (BUG-121,
 S2). **Migration:** nothing to change. For the rc.14 behaviour set
 `options.ChatReducer = new MessageCountingChatReducer(50)` (experimental, `MEAI001`).
- **Breaking — MentorAgent:** the options are validated when the host starts (`ValidateOnStart`). An ASP.NET
 Core or worker host with an invalid configuration (no `ChatClient` or `Agent`, empty `ScanAssemblies`, both
 `ChatHistoryProvider` and `ChatHistoryProviderFactory`…) now stops at start with the reason. Up to rc.14 it
 started, listened and failed on the first request, although the READMEs said "throws at startup". A MAUI app,
 which has no such start, still gets the error the first time MentorAgent is used (BUG-123, S4).
 **Migration:** a host that started with an invalid MentorAgent configuration fixes it.
- **Breaking — MentorAgent:** a class listed in `[TeamMember(Tools = ...)]` is that member's toolbox only. Up to
 rc.14 the scan also made its methods coordinator actions, published on `/mcp` and on the A2A card, because
 the class is in DI as the README asks (BUG-125, S3). A `[MentorSkill]` class keeps its coordinator tools, by
 the skill's own role. Member tools are also discovered by the same rules as every other action: public
 instance methods declared on the class itself, with `[MentorAction]` or a non-empty `[Description]`. A
 member no longer gets inherited or static methods, or a method with an empty `[Description]` (BUG-126, S3).
 **Migration:** at startup an Information line names each class taken from the coordinator, the members
 that list it and its methods (`… is listed in the Tools of …`). An action the coordinator must offer too
 goes on a class not listed in `Tools`; a member tool inherited from a base class is declared on the listed
 class.
- **Breaking — MentorAgent.Server:** `POST /mentor/approve` answers `404` when no turn waits for the answer any
 more: it was answered already, the turn was stopped, or the connection that raised it has closed. Up to rc.14
 every call got a `200`, so a client could not tell an approval that ran from one that reached nothing.
 **Migration:** a client that treats any non-2xx answer as an error treats `404` as "nothing left to answer"
 and closes its dialog. The WebAssembly client of MentorAgent.Blazor already does.
- **Breaking — MentorAgent:** `IMentorStateService.OnApprovalResponse` is no longer raised for an answer that
 found no turn waiting: given after Stop, after the connection or the circuit closed, or a second time. Up to
 rc.14 it was, in process and in the WebAssembly client, so a custom confirmation dialog that closed only on
 this event closed in those cases too; now it stays open (BUG-129). **Migration:** close a custom dialog
 when its buttons are clicked and on `OnBusyChanged(false)`, as the README's *Custom HITL confirmation
 dialog* example does; keep `OnApprovalResponse` for an audit trail of the decisions that were acted on.
- **Breaking — MentorAgent:** on the hub, on `/mentor/chat` and over A2A, MentorAgent takes the caller from the
 request — the hub connection's user, or `HttpContext.User` — before asking the host's
 `AuthenticationStateProvider`, which now answers only where no MentorAgent transport runs, such as a Blazor
 circuit (BUG-131). A host whose own provider added claims or roles to the user no longer has them seen
 there. **Migration:** add them with an `IClaimsTransformation`, which ASP.NET Core applies to
 `HttpContext.User` and so to the hub connection's user too.
- **Breaking — MentorAgent:** memory and `RateLimitPerUser` key a signed-in user by `NameIdentifier`, as
 before, else by `sub`, `oid` or the name with the claim in the key (`sub:…`, `name:…`), like the owner checks
 (BUG-142). A user known by name alone in rc.14 (Windows authentication) gets a new key. **Migration:** in
 your memory store, rename those users' keys from `<name>` to `name:<name>`.
- **MentorAgent:** README — `ReadOnly` and `IsReadOnlyAction` say what `readOnlyHint` means on `/mcp`: some MCP clients
 skip their own confirmation for such a tool, so a write declared read-only by mistake runs from them with nobody
 asked. The same in the MentorAgent.Server README's MCP section.
- **MentorAgent:** README — `OnActionExecuting` carries the text shown to the user (the action's `Description`, or a
 routing, team or hosted-tool label), not the tool name; `OnActionCompleted` carries the tool name, `OnActionFailed`
 the tool name and the error. The same in the MentorAgent.Server (`ActionExecuting`) and MentorAgent.Blazor READMEs.
- **MentorAgent:** README — a tool result is a card by its shape, not by its .NET type: an object with a non-empty
 string `kind`, or an array of them. Records of your own with a `kind` property are drawn as cards; the README says
 how to avoid it.
- **MentorAgent:** README — the banner of the five READMEs says this is a release candidate for 1.0.0, not a
 public preview. The *Extensibility* table lists `IMentorAgentSource`, `IMentorQueryEmbedding`,
 `ClassifierChatClient` and `SkillSources`/`SkillFilter`, and its history-provider row points to
 `ChatHistoryProviderFactory`: `ChatHistoryProvider` is one instance shared by every user.
- **MentorAgent:** `MentorOptions` XML documentation matches the code: `RequiresApproval` gets tool names
 (MCP ones unprefixed, never a remote agent), `DashboardRole` guards the metrics endpoint only, routing per
 `RoutingStrategy`, `AgentCardUrl` is a base URL (`agent-card.json`), no `ITextSearch` wrapper, an English
 `RagSystemPromptTemplate`.
- **MentorAgent.Server:** the A2A comments say a task runs as the request's user; an anonymous caller is not held
 by `RateLimitPerUser` across tasks.

### Removed

- **Breaking — MentorAgent:** the `MENTOR001` build check (`buildTransitive/MentorAgent.targets`) and its
 `MentorAgentSkipOpenAIVersionCheck` switch. They stopped every build that resolved `OpenAI` 2.11 or later — a
 project on the current Agent Framework, which never calls `GetResponsesClient()`, included (BUG-120).

### Fixed

- **MentorAgent:** README — the Requirements table lists the package's actual references, and its Blazor row
 no longer contradicts the server package (any ASP.NET Core app with MentorAgent.Server). The MENTOR001 section
 is replaced by *Provider package versions*, which says which call fails and on which versions instead of "the
 process dies before serving a request".
- **MentorAgent:** the warm-up (`WarmUpAtStartup`) is registered always and decided on the real options when
 the host starts. It was registered only if the registration-time snapshot asked for it, and that snapshot
 swallowed whatever the `AddMentorAgent` callback threw: a callback that failed before
 `WarmUpAtStartup = true` lost the warm-up without a word. Such an exception is now logged as a warning at
 start, naming the options decided at registration (BUG-122, S3).
- **MentorAgent:** the warm-up starts on `ApplicationStarted`, after a web host is listening, as its
 documentation said. It started inside `StartAsync`, before the other hosted services.
- **MentorAgent:** README — *When the options are read and checked*: the `AddMentorAgent` callback runs twice,
 and `EnableObservability`, `ObservabilitySourceName`, `McpServerEnabled`, `A2AServerEnabled`, `AppName`,
 `AppDescription` and `AgentCard` are read at registration, so a `Configure<MentorOptions>` registered later
 does not reach them (BUG-124, S3). The same note is in the MentorAgent.Server README and in the XML
 documentation of `AddMentorAgent`.
- **MentorAgent:** a team member is told *"These tools are read-only."* only when every one of its tools is
 declared read-only (`ReadOnly = true` or `IsReadOnlyAction`) and none asks for confirmation. Every member
 was told so, whatever its tools did (BUG-125).
- **MentorAgent:** README — *Which methods become tools*: inherited and static methods are not discovered, and
 a method with `[Description]` alone has no roles and no confirmation, so with tool filtering it can be sent
 when a message matches nothing (BUG-127, S4). The same in the MentorAgent.Server README.
- **MentorAgent.Server:** a confirmation outlived the connection that raised it. When the owner's hub
 connection closed, the hub forgot who had raised it but the turn kept waiting: anybody who knew the action id
 could approve it with `POST /mentor/approve`, with no owner left to check, and the action ran. Now the end of
 a connection — and of a Blazor Server circuit — cancels the turn running in it, with its confirmation
 (BUG-128, S2).
- **MentorAgent:** `OnApprovalResponse` is raised only when the answer reached the turn that was waiting for it.
 After Stop the request stayed pending, so a late Confirm raised `OnApprovalResponse(…, true)` for an action
 that never ran — in process, and in the WebAssembly client, which raised it before sending the answer. Now
 the request leaves the pending list with its wait, and the WebAssembly client raises the event once the
 server has accepted the answer (BUG-129, S3).
- **MentorAgent.Server:** the owner of a connection is recognised without a `NameIdentifier` claim.
 `/mentor/approve`, `/mentor/cancel` and `/mentor/session` read `NameIdentifier` alone, and so did the key that
 binds a session snapshot to its owner: with Windows authentication, or JwtBearer with
 `MapInboundClaims = false`, the owner was refused (`403`) its own confirmations, Stop and session. Now they
 read `NameIdentifier`, then `sub`, then `oid`, then the identity's name, as the MCP rate limit does. A name
 never matches another user's identifier. Snapshots saved by rc.14 for a user with `NameIdentifier` still
 restore (BUG-130, S3).
- **MentorAgent.Server:** in a Blazor Web App the hub, `/mentor/chat` and A2A know who is calling. MentorAgent
 read the user from the host's `AuthenticationStateProvider`; with `AddRazorComponents()` that is Blazor's,
 which throws outside a circuit, and the server's bridge, registered with `TryAdd`, lost to it. Every
 `RequiredRoles` action was refused to everyone with an error logged on each call, and memory and
 `RateLimitPerUser` followed a connection instead of a person. A Blazor Server app serving A2A without the hub
 had no bridge at all. Now each transport records its caller and MentorAgent reads it first; the host's
 registration is left as it is, whatever the order of `AddRazorComponents()` and `AddMentorAgentServer()`.
 When the server's own provider is the one registered, it also takes a circuit's user from Blazor
 (`IHostEnvironmentAuthenticationStateProvider`), as Blazor's own does (BUG-131, S2).
- **MentorAgent:** a signed-in user with only `sub` or `oid` (JwtBearer with `MapInboundClaims = false`) was
 keyed per session, so memory and `RateLimitPerUser` did not follow the person (BUG-142, S3).
- **MentorAgent.Server:** README — *CORS* and the JWT example under *Authentication* show the whole pipeline:
 `UseCors` → `UseAuthentication` → `UseAuthorization` → `Map…`. A `Program.cs` that protected the hub and did not
 call `UseAuthentication()` and `UseAuthorization()` itself got them from ASP.NET Core ahead of `UseCors`: the
 browser's preflight for `/mentor-hub/negotiate` was answered `401` without CORS headers, and a client on another
 origin never connected (BUG-132, S2). The MentorAgent.Blazor README says the same under *Stop and confirmations*.
- **MentorAgent.Server:** README — the complete React, Angular and Vue components send the hub's identity on
 approve, cancel and session (a `getToken` function, or cookies with `credentials: 'include'`), and show an error
 when the server refuses an answer or a Stop. They sent a bare `fetch`: with an authenticated hub the server
 answered `401`, the dialog closed and the turn stayed waiting, with nothing on screen (BUG-133, S2). The Angular and
 Vue components also take an absolute hub URL (`MENTOR_HUB_CONFIG`, `useMentorHub(hubUrl, getToken)`) and send
 approve and cancel to the hub's origin.
- **MentorAgent.Server:** README — the three components free the UI when the connection drops (`onreconnecting`,
 `onclose`) and restore the saved conversation in `onreconnected`; the events table lists the three callbacks. A
 connection lost mid-turn left the UI busy, even after the client reconnected: the new connection never sends
 `BusyChanged(false)` for that turn (BUG-134, S3).
- **MentorAgent.Server:** README — the complete React component shows the assistant's replies. It added an empty
 bubble for every reply: it reset the streaming buffer before React ran the update that read it (BUG-136, S2). The
 three components no longer add an empty bubble when a segment closes before a confirmation.
- **MentorAgent.Blazor:** README — the client setup example has `using MentorAgent.Abstractions.Models;`. Without
 it `MentorLanguage` and `MentorTheme` did not compile (`CS0103`) (BUG-135, S3).
- **MentorAgent:** README and XML documentation — `RequireConfirmation = false` turns off the confirmations of your
 actions, MCP-client servers and `RequiresApproval`, not the approvals a hosted MCP server asks for
 (`RequireApproval`, `AlwaysRequireApprovalTools`). The README and the XML documentation said it disabled every
 confirmation dialog (BUG-137, S3).
- **MentorAgent:** README — voice input: your server receives only the transcript, but some browsers, Chrome among
 them, send the audio to their maker's speech service. The README said nothing left the page except the transcript
 (BUG-138, S3). The same in the MentorAgent.Blazor README.
- **MentorAgent:** with `HitlMode.Blocking`, Confirm ran a `RequiredRoles` action for a user who had lost the role
 while the banner waited: the roles were read only before the banner. They are read again after the answer, as in
 Native mode, also in specialists and teams (BUG-143, S3).
- **MentorAgent:** after Cancel on a specialist's confirmation, reached through `route_to_specialist`, the answer
 could report a problem that never happened: the coordinator got only the specialists' words, and a team with
 `HandoffTo` had just told it to execute. The delegation's result now starts with `DECLINED BY THE USER`, names the
 declined action and any that ran beside it, and carries the rule of a declined action; an approved team's result
 says a declined confirmation is the user's decision (BUG-144, S3).
- **MentorAgent:** README — Blazor Web App: `@rendermode="InteractiveServer"` on `<ChatWidget />`, the simplest
 setup offered for every Auto app, needs *Interactivity location: Per page/component*. With *Global* it throws
 `NotSupportedException` once the layout runs in WebAssembly, and the WebAssembly template fails with HTTP 500 until
 its server adds `AddInteractiveServerComponents()` and `AddInteractiveServerRenderMode()`. The README also sent the
 CSS/JS links to an `index.html` a Web App does not have: prerendered, the widget writes them itself; otherwise they
 go in `App.razor` (BUG-140, S2). The same in the MentorAgent.Blazor README.
- **MentorAgent.Blazor:** README — the client project of a Blazor Web App registers no `HttpClient`, and without one
 the WebAssembly runtime did not start (`CannotResolveService`); the README said the template's was enough. The
 client snippets now register one (BUG-139, S2). The same in the MentorAgent and MentorAgent.Server READMEs.
- **MentorAgent.Blazor:** in WebAssembly a relative `HubUrl`, the default `/mentor-hub` among them, never reached
 the server: the .NET SignalR client parsed it as `file:///mentor-hub` and the widget did not connect (`TypeError:
 Failed to fetch`). It is now resolved against the page's base address, as a browser does (BUG-145, S2).
- **MentorAgent:** a `[MentorTeam]` stopped discussing after its first activations in a session: every run shared
 one group-chat manager, whose iteration count was never reset, so once `MaxIterations` was spent the team was
 activated with no member speaking and returned nothing. Each run now gets its own manager (BUG-146, S2).
- **MentorAgent:** a tool that threw showed the exception's text in the chat even with `OnException` set, and a
 failure after the action (`OnToolResult`, the cards, a screen update) reported an action that had run as failed,
 so the model could run it again. Only the tool's own exception is a failure now. With `OnException` set the chat
 shows the app's message or a localized "The action could not be completed.", never the exception's text; the
 model still gets the app's message, else the exception's. An `OnToolResult` that throws withholds the result, as
 on the MCP server. A Stop during the action is not logged as a failure. The widget no longer shows the text of a
 send that failed (BUG-147, S2).
- **MentorAgent:** the gate's Warnings named the user and carried a tool's whole exception. They name a fingerprint
 (`caller#…`, the same as the MCP audit line) and the exception's type; the whole exception is logged at Debug
 (BUG-148, S3).
- **MentorAgent:** with a `Language` other than English the input box of a server-hosted widget still read "Type
 a message...": `MentorOptions.InputPlaceholder` defaulted to that text and hid the translation. It is now
 `string?` with no default, as in MentorAgent.Blazor, and an unset placeholder uses the localized one. In
 English the default text is now "Write a message..." (BUG-149, S3).
- **MentorAgent.Abstractions:** the widget's stylesheet loaded Sora and JetBrains Mono from Google Fonts, a
 request to a third party on every page that rendered the widget, refused under `default-src 'self'` and absent
 offline. The two fonts now ship in the package (`wwwroot/fonts`: variable WOFF2, Latin and Latin Extended, SIL
 Open Font License 1.1), which is about 110 KB larger, and the stylesheet requests nothing from another origin.
 The avatar's size and a table column's alignment moved from style attributes to the stylesheet, so with the
 default options the widget needs no `style-src 'unsafe-inline'`; `Theme` Dark or Minimal and `PrimaryColor`
 still write a `<style>` block, and `MentorDashboard` uses style attributes (BUG-150, S2).
- **MentorAgent:** the error for a missing model suggested `new OllamaChatClient(...)`, a type in no package
 the library brings, and `AIProjectClient` without its package. Each example now names its package; Ollama
 goes through its OpenAI-compatible endpoint (BUG-151, S3).
- **MentorAgent.Declarative:** README — the install command lacked `--prerelease`, which `dotnet add package`
 needs while only prereleases exist; so did the two `MentorAgent.Server` commands in the MentorAgent README.
 The *License* sections point to the package's `LICENSE` (BUG-141, S3).

### Known issues

- **MentorAgent:** BUG-074 (S3): with `MentorshipLevel.Proactive`, the assistant can still offer next steps the
 application does not have — 3 in 15 turns on the Blazor Server sample after the rc.14 rule, 5 before ("add it
 to the cart", "the available deals", "plan a reorder"). Left open for 1.0.0 by the product owner's decision:
 the closing offers keep their current wording.
- **MentorAgent:** BUG-118 (S2) is closed for hosts that opt in to `RequireActionForChangeRequests` (added in
 1.0.0-rc.14). Without it the model can still narrate an action it never performed — "Stock aggiornato" with no
 tool called, or "Ecco la pagina Prodotti" with no navigation — and with it a complete request the classifier
 does not recognise runs as without the option.
- BUG-106's residue is closed for hosts that opt in to `RequireLookupForRecordQuestions` (added in 1.0.0-rc.14);
 without it, a catalogue question can still be answered with an offer to look the value up.
- **MentorAgent:** with `HitlMode.Native`, a confirmation closed by Stop is closed only in memory: a snapshot saved
 in that state and restored still carries the open request, which can make the next turn fail as in BUG-116.

The full history is in CHANGELOG.md, inside this package.