Novior.MentorAgent.Abstractions
1.1.0
Prefix Reserved
dotnet add package Novior.MentorAgent.Abstractions --version 1.1.0
NuGet\Install-Package Novior.MentorAgent.Abstractions -Version 1.1.0
<PackageReference Include="Novior.MentorAgent.Abstractions" Version="1.1.0" />
<PackageVersion Include="Novior.MentorAgent.Abstractions" Version="1.1.0" />
<PackageReference Include="Novior.MentorAgent.Abstractions" />
paket add Novior.MentorAgent.Abstractions --version 1.1.0
#r "nuget: Novior.MentorAgent.Abstractions, 1.1.0"
#:package Novior.MentorAgent.Abstractions@1.1.0
#addin nuget:?package=Novior.MentorAgent.Abstractions&version=1.1.0
#tool nuget:?package=Novior.MentorAgent.Abstractions&version=1.1.0
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 wereMentorAgent.*. 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.MentorAgentandNovior.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
- What's included
- Component parameter reference
- Static assets — CSS and JS
- When you actually touch this package
- Why a separate package?
- Requirements
- Related Packages
- License
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 |
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
MentorDashboardandMentorCardViewinjectsIOptions<MentorWidgetOptions>,MentorLocalizeror both, soAddMentorAgent()/AddMentorAgentBlazor()must have run in that host. RenderingMessageBubbleon 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 inNovior.MentorAgent.Abstractions.Models, the interfaces inNovior.MentorAgent.Abstractions.InterfacesandMentorLocalizerinNovior.MentorAgent.Abstractions.Core.ChatMessage/ChatRolecome fromMicrosoft.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 |
@* 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
ChatWidgetthe buttons are handled for you, in aCardTemplatetoo as long as the card is rendered byMentorCardView; wireOnActionyourself 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 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.Cancelpost toPOST /mentor/approve?actionId=…&approved=true|falserather 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 whenMentorAgentBlazorOptions.HubUrlis an absolutehttp(s)URL (relative to the hostHttpClient'sBaseAddresswhen it is relative, i.e. same-origin hosting) and carriesAuthorization: Bearer <token>fromMentorAgentBlazorOptions.AccessTokenProviderwhen 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 anHttpClient, as the WASM template does. Call the endpoints yourself only from a client that has noIMentorStateService(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.
ChatWidgetdoes 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. 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/Novior.MentorAgent.Abstractions/css/MentorAgent.css?v=10" rel="stylesheet" />
<script src="_content/Novior.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 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/metricsrequires the role inMentorOptions.DashboardRole("Admin"by default;""leaves it open, for development only). TheHttpClientyou 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) andGetFromJsonAsyncthrows instead of the dashboard rendering its empty state. In a standalone WASM app, itsBaseAddressmust 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.WebMicrosoft.AspNetCore.Components.AuthorizationMicrosoft.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.Web10.0.3Microsoft.AspNetCore.Components.Authorization10.0.3Microsoft.Extensions.AI10.7.0
No AI provider, no server packages, no JavaScript dependencies beyond the browser's own Speech and File APIs.
Related Packages
| 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 | Versions 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. |
-
net10.0
- Microsoft.AspNetCore.Components.Authorization (>= 10.0.3)
- Microsoft.AspNetCore.Components.Web (>= 10.0.3)
- Microsoft.Extensions.AI (>= 10.7.0)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Novior.MentorAgent.Abstractions:
| Package | Downloads |
|---|---|
|
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. |
|
|
Novior.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. |
GitHub repositories
This package is not used by any popular GitHub repositories.
1.1.0 - 2026-10-21
### Added
- **Novior.MentorAgent:** `MentorOptions.ConfirmationMessage` writes the text of the confirmation banner, in the
application's own words or language. It gets the tool, its description, the arguments as the banner shows them,
`Language` and the services of the conversation's scope. When it returns `null` the default text is shown; when it
throws or returns only white space, the default text and a warning (BUG-186).
- Every package carries its software bill of materials: SPDX 2.2, at `_manifest/spdx_2.2/manifest.spdx.json`
inside the `.nupkg`, with its SHA-256 beside it. It lists the package's files and the packages restored to build
it, build-time tools included.
- **Novior.MentorAgent:** `MentorActionAttribute.RequiresConfirmationDeclared` is public and read-only: whether the
attribute writes `RequiresConfirmation`, true or false. With `RequireConfirmationForWrites` on, an action that
declares nothing asks for confirmation and one that declares `RequiresConfirmation = false` does not, while
`RequiresConfirmation` reads false for both; a component outside the library can now tell them apart.
### Security
- **Novior.MentorAgent.Blazor:** BUG-173 (S2): the package depended on `Microsoft.AspNetCore.SignalR.Client`
10.0.3, whose `Microsoft.AspNetCore.SignalR.Common` is covered by advisory GHSA-334f-vvp4-7g6p
(CVE-2026-70159, high): a client using stateful reconnect can block a server's broadcasts. It now depends on
10.0.13, the patched release. With 1.0.0, add
`<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.13" />` to the client project.
MentorAgent turns stateful reconnect on neither in its hub nor in its client.
- **Novior.MentorAgent.Server:** the hub runs on the SignalR of the ASP.NET Core shared framework, not on a
package: a host is patched by the ASP.NET Core runtime 10.0.13 or later.
- A package with a high or critical advisory (NuGet audit NU1903, NU1904), direct or transitive, now fails the
Release build of the packages.
### Changed
- Every package is now checked at pack time against the public API of 1.0.0, so a 1.x release cannot break
code built against 1.0.0 by accident. An intended break is listed in this file as Breaking.
- **Novior.MentorAgent, .Server, .Blazor, .Abstractions, .Declarative:** NOTICE says that "MentorAgent" and its
logo are trademarks of Antonio Macrì and Emanuele Longo, to be assigned to Novior, not licensed under the Apache
License 2.0, and points to the trademark policy, TRADEMARKS.md, in the GitHub repository.
### Fixed
- **Novior.MentorAgent:** the README's *Migrating from the MentorAgent.\* packages* listed the build diagnostic
`MENTOR001` among what stays the same. That check, and its `MentorAgentSkipOpenAIVersionCheck` switch, were
removed in 1.0.0-rc.15: the core package has no build target, and there is no switch to keep (BUG-159, S3).
- **Novior.MentorAgent.Server:** three sentences of the README did not match the code. `IncludeWorkflowExceptionDetails`
puts the exception message of a failing handoff or group chat agent into the reply, not a stack trace.
`EnableSuggestions` and the widget options do take effect in a MAUI Blazor Hybrid app, which runs the Blazor
widget; only a MAUI app with its own native UI ignores them. And the `Authorization` header is missing from a
WebSocket or Server-Sent Events connection because a browser cannot send one, not because the protocol forbids
it; the Blazor README said the same and is corrected too (BUG-160, S4).
- **Novior.MentorAgent.Server:** the README's React example for image input did not compile: a `<textarea>` outside
any component and an `await` at the top level. It is now a component that keeps the attachments in its state and
sends them from an `async` handler, passing `null` when there are none (BUG-161, S3).
- **Novior.MentorAgent.Server:** the README said that `POST /mentor/cancel` answers `404` when Stop is pressed just as
the turn ends, and that it is not an error; its React, Angular and Vue components ignored it. The endpoint answers
`200` whenever the connection exists, with or without a turn running, and `404` only when the connection is closed
or unknown, which is the case a client must not hide. The README and the three components now say so (BUG-162, S3).
- **Novior.MentorAgent:** the option reference tables of the README and the Server README left out options that only
their own sections described: `SkillSources`, `SkillFilter`, `SkillsRefreshInterval`, `AutoApprovalRules` and
`MaxAutoApprovalIterations` in the README; `EnableRichResponses`, `IsReadOnlyAction`, `EnableOutputSafetyCheck`,
`RequireLookupForRecordQuestions`, `RequireActionForChangeRequests` and `McpCallerPrincipal` in the Server README.
Every public option of `MentorOptions` is now in a reference table of both, or among the Server README's options
not listed on purpose. The `[MentorAction]` tables mark `Description` as required, and the Server README's lists
`ReadOnly` (BUG-163, S4).
- **Novior.MentorAgent:** with a pre-built `Agent` (Path B), `MemoryAutoCapture` and a `ClassifierChatClient`, memory
had two writers: the post-turn extraction ran through the classifier, one call per message, and the `remember`
tool was sent too. The log and the documentation said the extraction does not run on Path B. The tool is now left
out whenever the extraction runs, and the build log names the writer: on Path B the extraction with a
`ClassifierChatClient`, the `remember` tool without one (BUG-174, S3).
- **Novior.MentorAgent:** a `[MentorTeam]` with `HandoffTo` was approved when `[TEAM:APPROVED]` appeared anywhere
in the discussion, so a member's "yes" followed by another's "no" still told the coordinator to execute the
actions. The team is now approved only if its final message carries `[TEAM:APPROVED]` and no member wrote
`[TEAM:REJECTED]`; a discussion that ends without a verdict is not approved. Without a `[TeamTerminationCondition]`
the discussion now ends at the first verdict, instead of running all `MaxIterations` rounds; a host's condition
still decides alone when to stop. The members' instructions keep `[TEAM:REJECTED]` for a definitive no and ask for
changes without a token (BUG-175, S3).
- **Novior.MentorAgent, .Server, .Blazor, .Abstractions, .Declarative:** the XML documentation that IntelliSense
shows no longer contradicts the code: anonymous callers share the memory key `"anonymous"` only with
`AnonymousIdentity = Shared`; an empty `AllowedTools` allows every tool, like `null` (the README says so too);
`MentorTheme.Custom` renders like `Default`; `IMentorAgent` is optional and validates nothing; the hub's events list
`Cards`, and `MapMentorAgentServer` lists `/mentor/session` (BUG-164, S4).
- **Novior.MentorAgent:** the README did not say that with `UseRag` the search runs on every message, before the
model is called, so its time (an embedding call, the search, a reranker) is added to every reply; nor that the
absence of a keyword pre-gate relies on `RagMinScore` being on the source's scale. It says both now, and so does
the comment in `RagContextProvider` (BUG-165, S4).
- **Novior.MentorAgent:** the XML documentation of `RequiresApproval` said the predicate is evaluated once per
tool while the coordinator is built. It is asked again on every call to a tool in Blocking mode and on every turn
the tool filter matches a tool, so a predicate that reads a file or a database pays for it each time. The
documentation now says when it is asked and that it must stay a cheap, pure function of the tool name
(BUG-176, S4).
- **Novior.MentorAgent:** the documentation of `ConfigureChatClientPipeline` said only that the pipeline goes
outermost. It is built once per coordinator, so once per circuit on Blazor Server and once per SignalR connection
on the hub: a limit placed there does not limit the process. And only the coordinator's own calls go through it:
the auxiliary calls — classifier, safety checks, out-of-scope refusal, memory extraction, the lookup and action
checks — use `ClassifierChatClient ?? ChatClient`, and specialists, team members and the handoff router use
`ChatClient`. The READMEs and the XML documentation now say both (BUG-166, S4).
- **Novior.MentorAgent:** the READMEs said that `navigate_to` waits for `SignalReady()` on a `HasUIActions` page
only in a Blazor Server circuit. It waits wherever the page runs in the same process as the turn, so in a MAUI
Blazor Hybrid app too; turns over the hub, SSE or A2A still do not wait. The READMEs now say so (BUG-171, S4).
- **Novior.MentorAgent:** on Path B (a pre-built `Agent`), `EnableSafetyCheck` and `EnableOutputSafetyCheck`
without a `ClassifierChatClient` now log a warning when the coordinator is built: neither check can run, so every
message and every reply goes through unchecked. Both checks run on `ClassifierChatClient`, or on `ChatClient` when
none is set, so on Path B a `ClassifierChatClient` turns them on; the README and the XML documentation said they
required a `ChatClient` (BUG-167, S3).
- **Novior.MentorAgent:** the README and the XML documentation of `HitlMode` said that `Native` costs one
model call more per approved action, and that `Blocking` is cheaper. Both modes make the same model calls:
`Native` adds one agent run per round of confirmations, and its reply arrives in two segments (BUG-168, S3).
- **Novior.MentorAgent.Blazor:** the README said twice that the server must enable `EnableOnboardingTour` too,
and the widget's log said so when the tour had no steps. `MapMentorAgentServer()` maps `GET /mentor/tour`
whatever the server's flag says: the client's flag alone decides whether the tour is shown. The README and the
log now say so (BUG-169, S3).
- **Novior.MentorAgent:** the README has an example of `RequiredRoles` with Windows groups (Negotiate, `DOMAIN\Group`)
and with Microsoft Entra ID (app roles, or the `groups` claim's ids). The XML documentation of `EnableObservability`
said the embedding generator is traced too, and the README "the chat client calls": only the coordinator's model
calls are traced, and both now say how to trace the rest (BUG-170, S4).
- **Novior.MentorAgent.Server:** the A2A agent card listed the actions of classes not registered in the DI container,
which no turn can run, and repeated a skill id when two actions share a tool name. It now lists only the actions a
turn can call, once per name, and leaves a name out when any action carrying it is gated (BUG-177, S3).
- **Novior.MentorAgent.Server:** the warning that lists the actions withheld from the MCP server was written on the
first MCP request, not at startup as the README says. `MapMentorAgentMcp()` now writes it when it maps the
endpoint; the tools are still built by the first request, and which of them a caller sees is still decided per
request (BUG-178, S3).
- **Novior.MentorAgent:** on Path B (a pre-built `Agent`) the `[MentorAgent]` specialists, the `[MentorTeam]` teams
and the `RemoteAgents` are not built, because they run on `options.ChatClient`, and nothing said so. A warning now
names each of them when the coordinator is built, and the remote agents' cards are no longer fetched on Path B,
where no turn could reach them (BUG-179, S3).
- **Novior.MentorAgent:** the two log lines that open and close the discovery were in Italian ("Discovery avviata",
"Discovery completata"). They now read "Discovery started on …" and "Discovery completed — …" (BUG-180, S4).
- **Novior.MentorAgent:** a team's discussion reached the coordinator as one run-on text ("… is solved.I agree"), so
it could not tell who said what. Each message is now a paragraph of its own, headed by its author. The team tool's
description gets a full stop before the handoff note when the team's `Description` has none (BUG-181, S4).
- **Novior.MentorAgent:** two methods carrying the same tool name were reported only with tool filtering on. A model
calls a tool by name, so an agent offered both reaches only one of them, and up to 1.0.0 one method's attributes
decided the other's call (BUG-188). A warning now names every shared tool name and its methods when the actions
are first read, with tool filtering on or off (BUG-182, S4).
- **Novior.MentorAgent:** the signed-in user's roles told to the model came from `ClaimTypes.Role` claims only, while
the gate decides with `IsInRole`, which reads each identity's own `RoleClaimType`. With roles in a `roles` claim —
Microsoft Entra ID, or JwtBearer with `MapInboundClaims = false` — an action passed the gate while the model was
told no role. The model is now told the roles `IsInRole` answers to, from every identity of the user; a Windows
identity's group SIDs are left out (BUG-183, S3).
- **Novior.MentorAgent:** the RAG citation chips knew one number per marker, "[1] [2]". An answer that cited "[1, 2]"
or "[1-2]", as models also write, showed no chip at all. A list or a range now raises a chip for each document it
names; a range with an end that names no document, such as "[0-9]" in a code sample, cites nothing (BUG-184, S3).
- **Novior.MentorAgent:** the instruction that tells the model how to cite the RAG documents was in English whatever
`Language` said. It is now written in the configured language, in each of the ten, with the same `[1], [2]`
markers the citation chips read (BUG-185, S3).
- **Novior.MentorAgent:** the confirmation banner showed each argument whole, control and bidirectional characters
included, in the order of the model's JSON. Each value is now cleaned and cut at 200 characters, and the arguments
follow the order of the method's parameters (BUG-186, S2).
- **Novior.MentorAgent:** with `HitlMode.Native`, a confirmation that a Stop or a failure left open was answered
from memory only. A snapshot saved in that state and restored in another scope, or after a reset, still held the
open request, and every later turn failed as in BUG-116 (a known issue since 1.0.0-rc.15). The open requests and
their answers now travel with the snapshot, and the first turn after the restore answers them (BUG-187, S3).
- **Novior.MentorAgent:** two methods carrying one tool name shared one set of rules. The roles, the confirmation,
the banner's text and the page to open came from the first method found with the name, level 1 first, so a
level-1 method without roles opened a specialist's role-gated action of the same name to every user. The gate now
applies the rules of the method the call runs, at every level and in both HITL modes (BUG-188, S2).
- **Novior.MentorAgent, .Abstractions, .Blazor:** the README taught `PageContext.Clear()` in a page's `Dispose()`.
Blazor disposes the previous page after it has initialized the next one, so from one page with UI actions to
another, the previous page's `Clear()` wiped the actions the next page had just registered, and the assistant
answered that the action did not exist. `IMentorPageContext.Clear(pageName)` clears only what that page
registered, or everything when it is still the current page: call it in `Dispose()` with the page's name.
`Clear()` still clears everything (BUG-192, S2).
- **Novior.MentorAgent.Abstractions:** in a table inside an assistant bubble, a number stood one digit per line. The
bubble lets a word break anywhere, so the table's columns could shrink to one character. The cells now break
between words only, a cell holding only a number, an amount, a date or a time stays on one line, and a table wider
than the bubble scrolls inside it instead of widening the conversation (BUG-193, S4).
- **Novior.MentorAgent:** `navigate_to` to a page with `HasUIActions` could return before the new page had registered
its actions. The page being left had called `SignalReady()` when it opened, and that signal ended the wait at once.
`navigate_to` now takes it back before asking for the navigation, so it waits for the new page's `SignalReady()`;
to the page already shown it does not wait, since Blazor does not initialize that page again (BUG-194, S3).
The full history is in CHANGELOG.md, inside this package.