Novior.MentorAgent.Abstractions 1.2.0

Prefix Reserved
dotnet add package Novior.MentorAgent.Abstractions --version 1.2.0
                    
NuGet\Install-Package Novior.MentorAgent.Abstractions -Version 1.2.0
                    
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="Novior.MentorAgent.Abstractions" Version="1.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Novior.MentorAgent.Abstractions" Version="1.2.0" />
                    
Directory.Packages.props
<PackageReference Include="Novior.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 Novior.MentorAgent.Abstractions --version 1.2.0
                    
#r "nuget: Novior.MentorAgent.Abstractions, 1.2.0"
                    
#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 Novior.MentorAgent.Abstractions@1.2.0
                    
#: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=Novior.MentorAgent.Abstractions&version=1.2.0
                    
Install as a Cake Addin
#tool nuget:?package=Novior.MentorAgent.Abstractions&version=1.2.0
                    
Install as a Cake Tool

Novior.MentorAgent.Abstractions

New package IDs. From 1.0.0 the packages, their namespaces and their assemblies are Novior.MentorAgent.*; up to 1.0.0-rc.15 they were MentorAgent.*. See Migrating from the MentorAgent.* packages in the Novior.MentorAgent README.

You do not need to install this package directly. It is included automatically as a transitive dependency of Novior.MentorAgent and Novior.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
Novior.MentorAgent Blazor Server app
Novior.MentorAgent.Server Web API / headless backend, or Blazor Auto server-side project
Novior.MentorAgent.Blazor Blazor WASM / Blazor Auto client project
Novior.MentorAgent.Abstractions ← you are here Never directly — it arrives with any of the above
Novior.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, Auto
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 (links to http/https addresses included), 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. L.Language is the language it uses: the application's, read on every use
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 Novior.MentorAgent.Abstractions.Components, the models and enums in Novior.MentorAgent.Abstractions.Models, the interfaces in Novior.MentorAgent.Abstractions.Interfaces and MentorLocalizer in Novior.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 Novior.MentorAgent.Abstractions.Components
@using Novior.MentorAgent.Abstractions.Models
@using Novior.MentorAgent.Abstractions.Interfaces
@using Novior.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
Language MentorLanguage? This widget's language: its texts, voice and onboarding tour, and the language of every turn it sends. Not supplied → the application's Language, through the MentorLocalizer from DI. See A language per page in the MentorAgent README
@* 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 Novior.MentorAgent or Novior.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. Only an absolute http or https address becomes a link: javascript:, data: and relative ones stay 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 Novior.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 Novior.MentorAgent:

_content/Novior.MentorAgent.Abstractions/css/MentorAgent.css
_content/Novior.MentorAgent.Abstractions/js/MentorAgent.js
_content/Novior.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, and it versions them for you: the ?v= it writes is the package version and the commit the package was built from (?v=<version>+<commit>), so every release changes it. A standalone WASM host references them manually in index.html instead — and neither file is fingerprinted, and UseStaticFiles serves them without a Cache-Control, so the ?v= query string is the cache buster: set it to the version of the package you install and change it on every upgrade. 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 the package version as the ?v= (ChatWidget adds the commit; a link you write needs only the version), in App.razor (Blazor Server / Web App) or index.html:

<link href="_content/Novior.MentorAgent.Abstractions/css/MentorAgent.css?v=1.2.0" rel="stylesheet" />
<script src="_content/Novior.MentorAgent.Abstractions/js/MentorAgent.js?v=1.2.0"></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 Novior.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 — Novior.MentorAgent and Novior.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?

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

Novior.MentorAgent.Abstractions contains only WebAssembly-safe dependencies:

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

Both Novior.MentorAgent and Novior.MentorAgent.Blazor reference Novior.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.11.0

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


Package Purpose
Novior.MentorAgent Blazor Server — full AI assistant
Novior.MentorAgent.Server Any ASP.NET Core app — headless AI backend
Novior.MentorAgent.Blazor Blazor WASM — SignalR client
Novior.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 Novior.MentorAgent.Abstractions:

Package Downloads
Novior.MentorAgent

Add AI agents to your .NET app: your methods become tools with role checks and human confirmation, with a ready-made chat widget for Blazor and MAUI. Install Novior.MentorAgent.Server to connect any frontend through SignalR, SSE, MCP or A2A.

Novior.MentorAgent.Blazor

Blazor WebAssembly client for MentorAgent. Install in your Blazor WASM / Blazor Auto client project. Connects to a Novior.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.2.0 112 10/27/2026
1.1.0 153 10/21/2026
1.0.0 140 10/13/2026

1.2.0 - 2026-10-27

### Added

- **Novior.MentorAgent.Abstractions:** `IMentorStateService.OnActionStarted` gives the tool's technical name next to
 the text shown, in a `MentorActionStarted`: `OnActionExecuting` carries only the text shown, so whoever recorded
 the actions from it could not tell for sure which tool ran. It is raised where the turn runs, not by the WebAssembly
 client, and the interface implements it as an event nobody raises, so an implementation written for 1.1.0 still
 compiles. The documentation of `OnActionExecuting` now says it carries the text shown, not the tool name.

- **Novior.MentorAgent.Abstractions, Novior.MentorAgent:** `IMentorActionObserver` receives every decision of the tool
 gate, on every surface and for every agent; `AddMentorActionObserver<T>()` registers one. A `MentorActionDecision`
 carries the outcome (`MentorActionOutcome`), a reason code that stays the same across versions
 (`MentorActionReasons`), the tool, the agent, the surface, the session, turn and call ids, the user and who
 confirmed — never the arguments. A decision never waits for an observer: each has a queue of its own and 5 seconds a
 call.

- **Novior.MentorAgent.Abstractions:** an action that asks for confirmation knows who confirmed it and when:
 `MentorActionConfirmation.Current`, read inside the action's method, carries the confirmation's id, the tool, the
 time and who confirmed — the caller of `POST /mentor/approve`, or the session's user in a circuit and in the MAUI
 app — in Blocking and in Native mode. It is `null` for an action nobody confirmed. The approval event came when
 the action was already on its way, so a ledger of the writes could not say who approved one.

- **Novior.MentorAgent:** `MentorOptions.ConfirmationTimeout` bounds how long a confirmation waits for its answer,
 counted from when the banner is shown. Once it has passed with no answer, the confirmation counts as rejected: the
 action does not run, its decision is `Rejected` with the new reason code `confirmation-timeout`, the span's
 `mentoragent.tool.confirmation` is `timed-out`, and the model is told nobody confirmed it in time, not that the user
 declined. An answer that comes later is ignored, as one after Stop is: `POST /mentor/approve` answers 404. It applies
 to a confirmation a policy asks for too, in Blocking and in Native mode, and for specialists and team members. Zero or
 a negative value, the default, is no limit, as before.

- **Novior.MentorAgent, .Abstractions, .Blazor, .Server:** two tools for article 50 of the EU AI Act. A notice that
 tells people they are talking to an AI assistant, under the widget's header for the whole conversation
 (`ShowAiDisclosure`, `AiDisclosureMessage`, on `MentorOptions` and `MentorAgentBlazorOptions`), with a default text
 in the ten languages of `Language`; and a machine-readable marker on every reply the model writes:
 `data-ai-generated="true"` on the reply in the widget, `"aiGenerated": true` on the `chunk` and `completed` frames of
 `/mentor/chat`, the hub event `ReplyStarted` before each reply, whose argument is a `MentorReplyStarted`
 (`{ "aiGenerated": true }`), and `aiGenerated` in the metadata of the reply to an A2A task. Turning the notice off
 leaves the marker. The results of the MCP server, the spoken replies and the images and files of the provider's
 hosted tools carry no marker. Which words to show, and whether the article applies, is the application's to decide.

- **Novior.MentorAgent.Abstractions:** `MentorTheme.Auto` follows the visitor's system setting
 (`prefers-color-scheme`): the light theme, `Minimal`, when it asks for light, which is also what a browser reports
 when the system states no preference, and `Default` when it asks for dark. It follows the system, not the page: on
 a page that is always light, a visitor whose system is dark sees the dark widget. No script is involved, and the
 default theme stays `Default`.

- **Novior.MentorAgent, .Abstractions, .Blazor, .Server:** a language per page. `<ChatWidget Language="…" />` gives
 the widget a language of its own for its texts, its voice and its onboarding tour, and every turn it sends carries
 it: the assistant answers in it, and what MentorAgent writes for that turn follows it. The language travels as
 `MentorUserMessage.Language`, in the hub's new `SendUserMessage`, as `language` on `/mentor/chat` and on `GET
 /mentor/tour?language=`. `IMentorOrchestrator` and `IMentorTour` get default methods, so implementations written for
 1.1.0 compile. A change of `MentorOptions.Language` while the application runs applies from the next turn. A2A and
 MCP requests stay on `MentorOptions.Language`.
- **Novior.MentorAgent:** `MentorOptions.SessionContextPlacement`. With `LastUserMessage` the context of each request
 — the page, its data, its address, the date and time — goes at the start of the user's last message instead of the
 end of the instructions, which then stay the same through the session: a model that caches the prompt by its prefix,
 a local engine above all, no longer reads the whole conversation again on every new minute or page. The stored
 conversation keeps the user's words. Path A with local history only; the default, `Instructions`, changes nothing.

### Changed

- **Novior.MentorAgent, .Abstractions, .Blazor:** the widget now shows, by default, under its header: "You are talking
 to an AI assistant: its replies are generated automatically.", in the configured `Language`, for the whole
 conversation. Set `AiDisclosureMessage` to your own words, or `ShowAiDisclosure` to `false` to show none.

- **Novior.MentorAgent.Server:** the hub sends `ReplyStarted` before each reply. A client that does not register it
 keeps working, and SignalR logs a warning for every reply ("Failed to find handler" in .NET, "No client method with
 the name" in JavaScript): register it, even with a handler that does nothing — `connection.on("ReplyStarted", () =>
 {})`, or `hub.On<MentorReplyStarted>("ReplyStarted", _ => { })`. The WebAssembly client of Novior.MentorAgent.Blazor
 registers it.

- Every package is now checked at pack time against the public API of 1.1.0, so 1.2.0 cannot break code built
 against 1.1.0 by accident. An intended break is listed in this file as Breaking.

- The family moves onto the Microsoft Agent Framework 1.26.0 line (`Microsoft.Agents.AI` and
 `Microsoft.Agents.AI.Workflows` 1.26.0) and `Microsoft.Extensions.AI` 10.11.0. **Novior.MentorAgent.Declarative:**
 `Microsoft.Agents.AI.Declarative` moves to 1.26.0-rc1, on the same line. In Agent Framework 1.26
 `AgentSessionStore.DeleteSessionAsync` is abstract: a host with an `AgentSessionStore` of its own implements it.
 1.26 also refuses two tool approval requests with one id; MentorAgent gives each call of a response an id of its
 own (BUG-225), except with `UseServiceManagedHistory`.

- With Agent Framework 1.25 or later, when control returns to an agent of the handoff workflow behind `route_to_specialist`,
 the workflow repeats the user's request to it instead of leaving it a tool result alone: whoever reads the
 workflow's messages sees one more after each return. The answers do not change.

- `ModelContextProtocol` moves to 2.3.0: the MCP client in Novior.MentorAgent, and
 `ModelContextProtocol.AspNetCore` 2.3.0 in **Novior.MentorAgent.Server**. The SDK's HTTP server is stateless by
 default from 2.0; the MCP endpoint asks for the stateful transport, so a client keeps its `Mcp-Session-Id` and the
 endpoint answers as before, a `GET` without a session with 400 included.

- An MCP server of the host that lists a tool without `inputSchema` is no longer connected: ModelContextProtocol 2.0
 refuses such a tool where 1.x gave it an empty schema, and the server's whole tool list is lost with it. The
 warning names the server, and its exception names `inputSchema`; the assistant goes on without that server.

- The A2A SDK moves to 1.0.0-preview3: `A2A` in Novior.MentorAgent and `A2A.AspNetCore` in
 **Novior.MentorAgent.Server**.

- **Novior.MentorAgent:** a call refused for its roles is logged once, at Warning, in Blocking and in Native mode
 alike. In Native mode a user who was not signed in was refused before the banner with a Debug line only, and in
 Blocking mode a missing role wrote two Warnings. A confirmation cancelled by Stop or by the end of the session is
 now logged at Debug in Native mode too, and a tool call's Debug line is written once instead of twice.

- The `mentor.tool.{toolName}` and `mentor.mcp.tool.{toolName}` spans carry the outcome
 (`mentoragent.tool.outcome`), the reason code (`mentoragent.tool.reason`), the confirmation
 (`mentoragent.tool.confirmation`) and the surface (`mentoragent.surface`), and a failed call sets the span's
 status to error. In Native mode a call refused, rejected or stopped before it runs now has its span too.

- **Novior.MentorAgent.Server:** the MCP server's line for each `tools/call` tells apart a call refused for its roles
 (`denied-role`), an action the server never publishes (`withheld`) and a name nothing defines (`unknown`): the
 three were all `unknown`. A call the client cancels is `cancelled`. The caller is still told the same for every name
 it may not use.

- **Novior.MentorAgent.Abstractions, Novior.MentorAgent, .Server:** `IMentorActionPolicy` lets the application's own
 code decide whether a user may use an action and whether a call may run — a permission per company or per record, a
 rule on the arguments — on every surface: the coordinator on both paths and in both modes, the specialists, team
 members and agent sources, the MCP servers the application consumes, the MCP server and A2A tasks.
 `AddMentorActionPolicy<T>()` registers one. `IsAllowedAsync` decides what is offered and is asked again before every
 call; `BeforeActionAsync` answers `Proceed`, `Confirm` or `Stop`, and the strictest answer of all the policies wins.
 A policy that throws, or does not answer within the new `MentorOptions.ActionPolicyTimeout` (5 seconds), stops the
 call. The gate's decisions gain the outcomes `Blocked` and `PolicyFailed` and `MentorActionDecision.Detail`.

- **Novior.MentorAgent.Abstractions, Novior.MentorAgent:** `IMentorActionCatalog.GetActions()` lists the
 application's actions as MentorAgent sees them — the coordinator's, the specialists' and the team members' — with
 their roles, whether a call asks for a confirmation now, whether they only read, whether and to whom the MCP server
 publishes them (`MentorMcpExposure`) and whether the A2A card lists them, decided at every call by the rules of the
 MCP server and of the card.

- **Novior.MentorAgent:** a change of default. With `MentorshipLevel.Proactive` the prompt no longer asks for
 suggested next steps at the end of an answer, and asks for none: told to offer only what the request's tools,
 pages, skills and specialists can do, the model still offered steps the application does not have (BUG-074, S3).
 Measured on 15 turns of the same build, the closing offers went from 15 to 2, and the offers of something the
 application does not have from 4 to 0. The new `MentorOptions.ProactiveNextSteps` brings the old rule back, word for
 word, when set to `true`. The rest of the level, the explanations, the warnings and the one sentence before
 acting, and the other levels do not change.

- **Novior.MentorAgent:** the package README opens with what MentorAgent is today: AI agents for any .NET
 application, with the chat widget for Blazor and .NET MAUI, and any other frontend through
 `Novior.MentorAgent.Server`; it links the documentation at docs.mentoragent.net. The package description and tags
 say the same. The repository's front page on GitHub is now a short README.

- SECURITY.md says when a vulnerability report is answered (within 5 working days, best effort), how a fix is
 announced (a GitHub Security Advisory and the release notes) and how long each version gets security fixes: 3
 months after the next minor version, 6 months after the next major version; 1.0.x until 21 January 2027.
 SUPPORT.md and the README say the same.

- The README says how to run MentorAgent on more than one instance, and the policy by which it follows Agent
 Framework, Microsoft.Extensions.AI, ModelContextProtocol and A2A.

### Fixed

- **Novior.MentorAgent:** with Agent Framework 1.23 or later, an agent offered two tools with the same name — two
 level-1 methods, or one name from two MCP servers — failed every turn with "Duplicate tool name". Each agent is now
 offered one tool per name: the first, which is the one a call by that name already ran. A warning names every copy
 and the one kept. A name the coordinator shares with a specialist stays with both (BUG-195, S2).

- **Novior.MentorAgent.Declarative:** with Agent Framework 1.23 or later, `MentorDeclarativeOptions.ConfigurationSection`
 gave a definition no values: the framework loads configuration only for the keys a definition names through `Env`
 and the host lists as allowed. The package now loads the section itself, one Power Fx variable per key, named by its
 full configuration path, as before (BUG-196, S2).

- **Novior.MentorAgent, .Server:** on A2A 1.0.0-preview3 a turn delegated to a peer came back empty, with no error. A
 preview3 server refuses a streamed request unless its agent declares streaming, and the SDK's client reads the
 refusal as an empty stream; the agent MentorAgent registered with the SDK declared nothing. It now declares
 streaming, as the published card did. Towards any peer, `A2AChatClient` streams only when the peer's card declares
 streaming and otherwise asks once without a stream, and an empty stream logs a warning naming the peer (BUG-197, S2).

- **Novior.MentorAgent:** a shared MCP server that connected but did not answer `ping` was opened and dropped again
 in a loop with no limit, and the turn never reached the model. A session that does not answer `ping` right after it
 was opened now counts as a failed connect: the server is left out for the failure window, like one that is down,
 and a warning says it connected but does not answer ping (BUG-198, S3).

- **Novior.MentorAgent:** the coordinator's model was offered every action, those the user had no role for included:
 asked for one, it announced it and the gate refused it. It is now offered, and counts, only the actions the user may
 run, decided at the start of every turn on both paths (BUG-200, S3), and with `RequireActionForChangeRequests` such
 an action is no longer one the first call can be required to make (BUG-201, S3). A turn that leaves an action out
 says so in its instructions, naming no action and no role; a call made anyway is refused, as before.

- **Novior.MentorAgent, .Server:** the MCP server read the `RequiresApproval` predicate once, when it built its tool
 catalogue at the first request: an action the predicate gated after that stayed in `tools/list` and ran for a caller
 nobody could ask. The predicate is now read for every `tools/list` and every `tools/call`; an action it holds back is
 not listed and, called, is refused as withheld (BUG-202, S2).

- **Novior.MentorAgent:** in `MentorHitlMode.Native` the approval markers are decided when the coordinator is built,
 so a tool the `RequiresApproval` predicate gated after that ran with no confirmation, and with an auto-approval rule
 its decision read `auto-approved` though no rule was asked. The gate now asks for the confirmation of such a call,
 with the same banner and the same `POST /mentor/approve` as in Blocking mode, and a task from another agent refuses
 it with `nobody-to-confirm` (BUG-203, S2).

- **Novior.MentorAgent:** a Stop pressed after the user confirmed an action, before the action started, did not stop
 it. Nothing read the turn's token between the answer and the run, so an action that does not read it itself ran, in
 Blocking and in Native mode, and for a specialist's action too. The gate now reads it right before the action runs:
 the action does not run, its decision is `Stopped` with `stop`, and the model is told the user stopped the request.
 In Native mode the answers are no longer sent once Stop has come, so the next turn closes the request as never run
 instead of as one that may have (BUG-204, S2).

- **Novior.MentorAgent, .Server:** a caller that cancelled an A2A task (`tasks/cancel`) was answered `Working`, and the
 task never ended `Canceled`. A caller waiting for the reply saw the turn go on, its actions included, and the task
 end `Completed`; a streamed or `returnImmediately` task stopped, but stayed `Working` for good. Cancelling now stops
 the task's turn as Stop does — nothing starts after it, and the call in progress is `Stopped` with `stop`, not
 `session-ended` — and the task ends `Canceled`, in all three cases. The A2A SDK cancels the turn before it tells the
 agent, so MentorAgent puts its own request handler in front of the SDK's; `MapMentorAgentA2A()` logs a warning when
 a version of the SDK cannot be wrapped, and a cancelled task still ends `Canceled` then (BUG-199, S2).

- **Novior.MentorAgent:** specialists, team members and the agents of an `IMentorAgentSource` were offered every tool
 they hold, those the user had no role for included, and the gate then refused the call. They are now offered, and
 told, only the tools the user may run, by the coordinator's rule, in both modes; a request that leaves a tool out
 carries the coordinator's notice (BUG-205, S3).

- **Novior.MentorAgent, .Server:** the onboarding tour was written from every action of the application, those that
 need a role included: it could suggest one its reader may not use, and `GET /mentor/tour`, which answers anyone,
 signed in or not, could describe actions the MCP server describes to nobody without the role. The tour is still one
 for every visitor, generated once; it is now written from the actions that need no role (BUG-206, S3).

- **Novior.MentorAgent:** the onboarding tour was generated on the request of the first visitor who asked for it. A
 visitor who closed the page while the model wrote it, or whose `GET /mentor/tour` was cut short, ended the
 generation, and the built-in tour, with no question to try, was kept for every visitor until the process restarted.
 The generation now depends on no visitor's request: one who stops waiting only stops waiting, and the tour the model
 writes is the one everyone gets (BUG-207, S4).

- **Novior.MentorAgent:** in `HitlMode.Native` the banner of a coordinator's action took its text — the description
 and the order of the arguments — from the first method found with the tool's name, not from the method the call
 runs, though the 1.1.0 notes said the banner followed that method in both modes (BUG-188). Since an agent is
 offered one tool per name (BUG-195), the two differ when the first method's class is not registered and the
 coordinator is offered a later one: the user was asked to confirm what the other method does. The banner now reads
 as the method the call runs in Native mode too (BUG-208, S3).

- **Novior.MentorAgent.Abstractions:** the stylesheet and script links `ChatWidget` writes kept `?v=10` from the
 first release on, while both files changed in every release. Neither file is fingerprinted, and a host serving them
 as classic static files sends no `Cache-Control`, so a returning visitor's browser could keep the previous
 release's copies after an upgrade: new elements unstyled, and what the older script lacks failing silently. The
 links now carry the version the package was built as, with its commit (`?v=1.2.0+…`). A host that writes the two
 links itself — a standalone WASM `index.html`, a surface without `ChatWidget` — sets `?v=` to the package version,
 `?v=1.2.0` for this release, and changes it on every upgrade (BUG-209, S3).

- **Novior.MentorAgent:** a Stop pressed while a request was delegated to a remote agent over A2A stopped the local
 turn and told the peer nothing: the peer finished the task, actions that ask for no confirmation included. A turn
 that leaves a delegated task before its answer now cancels it on the peer (`CancelTask`), waiting two seconds at
 most for the peer to take it; a Stop that comes before the peer's first event, which names the task, keeps the
 request open up to two seconds for it. A peer whose card declares no streaming is now asked with
 `returnImmediately`, and its task read with `GetTask` until it ends, so that it can be cancelled too (BUG-210, S2).

- **Novior.MentorAgent:** a Stop during the first message of a session, while a remote agent's card was being read,
 was logged as the agent not answering ("did not answer: The operation was canceled") and kept the agent out of
 reach for a minute, and the session's coordinator was built without it for as long as the session lasted. A Stop
 there now ends the build: nothing is remembered against the agent, and the next message builds the coordinator
 with it (BUG-211, S3).

- **Breaking — Novior.MentorAgent:** a tool result became a generative card by its shape alone: any object with a
 non-empty string `kind`. A record of the application's with such a property, a support ticket with a link a
 customer wrote in it, was drawn as a card, the link a button, and its other fields never reached the model. A
 result is now read for cards only when the method that produced it is declared to return them: `MentorCard`, or a
 sequence of them, directly or through a task. A method that returned cards as `object`, as a `JsonElement` built
 by hand or as a type of its own shows them as text: declare `MentorCard` or `IEnumerable<MentorCard>`. A `MentorCard` an
 `OnToolResult` hook returns is still a card; an MCP tool's result never is (BUG-212, S3).

- **Novior.MentorAgent.Abstractions:** under a Content-Security-Policy without `style-src 'unsafe-inline'`, such as
 `default-src 'self'`, the Dark and Minimal themes and a `PrimaryColor` were dropped, and `<MentorDashboard />` lost
 the colours of its charts and the width of its bars. The themes are now stylesheets of the package, a `PrimaryColor`
 is set with `style.setProperty` once the widget's script runs, and the dashboard uses classes and SVG: both work
 under `default-src 'self'` with any option. A host's `html:root` rule no longer wins over the five variables a
 `PrimaryColor` sets (BUG-213, S3).

- **Novior.MentorAgent.Abstractions:** IntelliSense described `MentorTheme.Default` as "Light theme with blue accents"
 and `Minimal` as having no shadows. `Default` is the baseline's dark glass, a navy panel at 55% opacity, which on a
 light page shows as dark grey with the page behind it; the light theme is `Minimal`. The documentation now says
 which is which (BUG-214, S4).

- **Novior.MentorAgent.Abstractions:** a Markdown link in a reply, `[text](https://…)`, was shown as raw text, brackets
 and parentheses included, and could not be clicked. It is now a link that opens in a new tab, with
 `rel="noopener noreferrer nofollow"`, when its address is an absolute `http` or `https` one; any other scheme,
 `javascript:` and `data:` included, and a relative address stay text. The model's HTML is still always encoded
 (BUG-215, S3).

- **Novior.MentorAgent.Abstractions:** numbered steps with a line of text between them started again from 1 after the
 line. A numbered list now starts at its first step's number, and a line or a code block indented under a step stays
 inside it (BUG-216, S3).

- **Novior.MentorAgent.Abstractions:** a message holding the text the Markdown renderer uses as a placeholder for a
 code block, `@@BMCODE7@@` for instance, threw out of the render: on Blazor Server the circuit ended, and the host's
 page stopped responding until a reload. The text may come from the user, or from the model repeating data others
 wrote. It is now shown as it is (BUG-217, S1).

- **Novior.MentorAgent:** with a `Language` other than English, the instructions MentorAgent writes for the model —
 its example sentences, the grounding rule, the instructions in tool results — were in English, and a model could
 copy one into its reply ("Understood, l'email è stata annullata"). They now follow `Language`, in all ten languages;
 in English they are unchanged (BUG-221, S3).

- **Novior.MentorAgent:** with `RefuseOutOfScope`, a short reply to the assistant's question was judged on its own:
 on the demo the assistant asked "Vuoi vedere i dettagli…?", the user answered "Sì", and the turn was refused as out
 of scope, 5 times of 5 in Italian ("Yes" passed 3 of 3). The scope question is now shown the end of the
 assistant's last reply, in the same classifier call, and the verdict is kept for the message and that reply
 together, so the same "Sì" after another question is judged again. The safety question still judges the message
 alone, and the first message of a conversation is asked as before (BUG-223, S2).

- **Novior.MentorAgent:** the notice of a turn that leaves out an action the user may not run (BUG-200) was English
 in every language, and in Italian the model offered the action and, after "Sì", used another one or made up its
 result. It now follows the language of the turn, under its English heading (English unchanged), and ends with
 three rules: no offer of such an action, a yes does not make it available, no other tool in its place. On the demo
 the card of `show_expense` is now shown, its Approve button only for a manager or an administrator (BUG-224, S2; a
 residue is listed under *Known issues*).

- **Novior.MentorAgent:** in Native mode a response with two calls of one id broke the conversation until it was
 reset ("An item with the same key has already been added"). A call whose id the response or the conversation
 already holds now gets the next free `<id>_n`, on the coordinator and the agents under it; not with
 `UseServiceManagedHistory` (a warning says so) nor on path B. Two turns in a row that fail the same way on the
 requests left open log an error that suggests a new conversation (BUG-225, S2).

- **Novior.MentorAgent:** an A2A caller told the task at once (`ReturnImmediately`) could lose who it is: the SDK
 answers it as soon as the task is submitted, its request ends while the turn runs on, and the turn read the caller
 from that request only then. An authenticated peer became anonymous, its role-gated actions were refused, its
 memory and rate limit went under another key. The caller is now read when the task starts (BUG-226, S2).

- **Novior.MentorAgent.Server:** the Server README's option tables left out `MentorOptions.ConfirmationMessage`,
 although the 1.1.0 notes said every option was in a reference table of both READMEs or among the Server README's
 options not listed on purpose (BUG-163). It is now in the table of the confirmation options, and a test checks that
 the tables of both READMEs list every option of `MentorOptions` and every property of `[MentorAction]` (BUG-227,
 S4).

- **Novior.MentorAgent.Abstractions:** with the onboarding tour open, a change of the widget's `Language` translated
 the tour's buttons and left its steps in the language it had opened in. An open tour now fetches its steps in the
 new language and stays on the step it was at, or on the last one when the new language has fewer (BUG-228, S4).

- **Novior.MentorAgent:** `RequireLookupForRecordQuestions` had no effect without semantic tool filtering, and on the
 demo, which has no embedding model, a question about pending expenses asked in Italian was answered about one time
 in three with a list the model made up. Without the filter, the first call of a record question must now call one of
 the declared reads the user may run, or hand the request over; with the filter nothing changes. The demo turns the
 option on (BUG-222, S2).

### Known issues

- **Novior.MentorAgent:** BUG-074 (S3) is closed by default: since 1.2.0 `MentorshipLevel.Proactive` asks for no
 closing offers unless `ProactiveNextSteps` is on. With it on, the assistant can still offer next steps the
 application does not have — 4 of 15 closing offers on the headless server sample, measured on 1.2.0.
- **Novior.MentorAgent:** BUG-118 (S2) is closed for hosts that opt in to `RequireActionForChangeRequests`. 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.
- **Novior.MentorAgent:** BUG-220 (S3): with `MentorHitlMode.Blocking` and a Chat Completions client, the model can
 announce an action that asks for no confirmation — "Procedo a registrare la nota…" — and end its reply without
 calling it: once in 12 turns on the Blazor Server sample, on 1.2.0. `RequireActionForChangeRequests` does not
 prevent it, since it requires only actions that ask for a confirmation. The user can repeat the request; where it
 matters, give the action `RequiresConfirmation = true`, so that with the option a complete request calls it first,
 behind the banner; or use `MentorHitlMode.Native` with a Responses client, where an earlier measurement saw the
 action called almost every time.
- **Novior.MentorAgent:** BUG-224 (S2) leaves a residue in a turn in a language other than English: when the role
 filter leaves an action out, the assistant can still offer it, and after the user's "yes" it can, rarely, call
 another action the user may run in its place — once in 10 Italian tries on the demo, a new expense claim instead of
 an approval. The confirmation that write actions ask for by default stops that call: keep `RequireConfirmation`
 and `RequireConfirmationForWrites` on.
- **Novior.MentorAgent:** BUG-106's residue is closed for hosts that opt in to `RequireLookupForRecordQuestions`;
 without it, a catalogue question can still be answered with an offer to look the value up.
- **Novior.MentorAgent, .Server:** a 1.1.0 host that moves the A2A SDK to 1.0.0-preview3 on its own — `A2A`, or
 `A2A.AspNetCore`, which brings it — cannot answer A2A tasks. NuGet allows it without a warning, since 1.1.0 asks
 for preview2 or later, but preview3 changed the signatures of four `TaskUpdater` methods the 1.1.0 handler calls:
 the handler fails with `MissingMethodException` when a task arrives. Keep `A2A` and `A2A.AspNetCore` at
 1.0.0-preview2 with 1.1.0, or move to 1.2.0.

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