Novior.MentorAgent.Blazor
1.2.0
Prefix Reserved
dotnet add package Novior.MentorAgent.Blazor --version 1.2.0
NuGet\Install-Package Novior.MentorAgent.Blazor -Version 1.2.0
<PackageReference Include="Novior.MentorAgent.Blazor" Version="1.2.0" />
<PackageVersion Include="Novior.MentorAgent.Blazor" Version="1.2.0" />
<PackageReference Include="Novior.MentorAgent.Blazor" />
paket add Novior.MentorAgent.Blazor --version 1.2.0
#r "nuget: Novior.MentorAgent.Blazor, 1.2.0"
#:package Novior.MentorAgent.Blazor@1.2.0
#addin nuget:?package=Novior.MentorAgent.Blazor&version=1.2.0
#tool nuget:?package=Novior.MentorAgent.Blazor&version=1.2.0
Novior.MentorAgent.Blazor
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.
Blazor WebAssembly client for MentorAgent. Install this 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 — same features, same API, no code changes needed when switching between render modes.
All AI processing happens server-side — configured in your server project with AddMentorAgent(). The client sends messages, registers page context and UI actions, and receives streaming events.
Table of Contents
- What MentorAgent can do
- Package Family
- Getting started
- Widget customization
- Page context and UI actions
- Page navigation
- UI Action overloads reference
[MentorPage]parameters (server project)- All
AddMentorAgentBlazor()options - How it works
- Events — IMentorStateService
- Blazor Auto tip
- Requirements
- Related Packages
- License
What MentorAgent can do
All features below are available. Configure them server-side in AddMentorAgent().
| Feature | Description |
|---|---|
| 🤖 Multi-agent orchestration | Coordinator + specialized agents via Handoff Workflow |
| 👥 Group Chat teams | Multiple agents collaborate before acting |
| 🛠️ Tool discovery | C# methods become AI tools via [Description] or [MentorAction] |
| 🎯 UI Actions | AI invokes page-level actions (highlight rows, open modals, pre-fill forms) as individually named tools with typed parameters and async support |
| 📖 Agent Skills | Domain knowledge loaded on demand (load_skill) — progressive disclosure |
| 🧠 Contextual memory | Remembers user preferences across sessions |
| 🗺️ Page navigation | AI navigates to pages decorated with [MentorPage] |
| 📚 RAG | Inject relevant documents into every AI response, read through an IMentorRagSource you implement on the server |
| 🔌 MCP Client | Consume external MCP servers as additional tools |
| 🖥️ MCP Server | Expose [MentorAction] methods to Claude Desktop, VS Code, Cursor |
| 🌐 A2A Consumer | Connect to remote A2A agents in the Handoff workflow |
| 📡 A2A Server | Expose as a federatable A2A agent |
| 🎨 Customizable widget | Themes, colors, position, avatar, bot name |
| 🌍 Multi-language | 10 languages for AI responses and widget UI |
| 🎤 Voice input/output | Browser Speech Recognition + Speech Synthesis — speaks while the answer streams, stops when the user takes the floor, optional hands-free loop |
| 🧭 Onboarding tour | First-run guide generated on the server from its own pages and tools |
| 🃏 Generative UI cards | A server-side tool returns a card and the widget renders it — fields, accent, buttons |
| 🖼️ Multimodal image input | Attach images to a message — upload, clipboard paste, drag & drop or URL |
| 🌐 Hosted tools | The model provider runs web search, a code-interpreter sandbox and file search — configured server-side, results arrive in the stream |
| 🔒 Safety check | AI-based prompt injection detection |
| ⏱️ Rate limiting | Per-user message limit |
| ✅ Confirmation dialogs | Destructive actions ask for approval (HITL) — including MCP tools, in either the MentorAgent or the Agent Framework native flow |
| 🔐 Role-based actions | Actions restricted by ASP.NET Core identity roles |
| 💸 Token & cost optimization | Slim cache-friendly prompt, semantic tool filtering, history compaction, RAG/memory gating |
Package Family
| Package | Install when |
|---|---|
| Novior.MentorAgent | Blazor Server app |
| Novior.MentorAgent.Server | Server project (Web API / Blazor Auto server) |
| Novior.MentorAgent.Blazor ← you are here | Blazor WASM / Blazor Auto client project |
| Novior.MentorAgent.Abstractions | Never directly — it arrives with any of the above |
| Novior.MentorAgent.Declarative | Optional — Level-2 specialists in YAML, added on the server project |
Getting started
Installation
# Client project
dotnet add package Novior.MentorAgent.Blazor
# Server project — MentorAgent is included automatically as a transitive dependency
dotnet add package Novior.MentorAgent.Server
Server project setup
All AI behaviour is configured on the server (the transitive
Novior.MentorAgentcore): agents, RAG, memory, skills, MCP/A2A and the token/cost optimizations below. The client only renders the widget.
// Server/Program.cs
builder.Services.AddMentorAgent(options =>
{
options.AppName = "My App";
options.AppDescription = "An order management application";
options.ChatClient = chatClient;
options.ScanAssemblies = [typeof(Program).Assembly];
// All features configured here: agents, RAG, MCP, A2A, memory, skills...
options.UseMemoryContext = true;
options.UseRag = true;
options.McpServerEnabled = true;
options.A2AServerEnabled = true;
options.EnableSkills = true;
options.RateLimitPerUser = 20;
});
builder.Services.AddMentorAgentServer();
app.MapMentorAgentServer(); // /mentor-hub, /mentor/chat + /mentor/approve, /mentor/cancel, /mentor/session, /mentor/tour, /mentor/admin/metrics
app.MapMentorAgentMcp(); // optional
app.MapMentorAgentA2A(); // optional
AddRazorComponents() and AddMentorAgentServer() can come in either order: Blazor's circuits keep their user, and the hub, /mentor/chat and A2A get theirs from the request.
Token & cost optimization + reliable memory (server project)
These options cut the tokens sent per request and make memory reliable — all on the server. Full details: MentorAgent core README → Token & cost optimization.
builder.Services.AddMentorAgent(options =>
{
// ...ChatClient, ScanAssemblies as above...
// Embedding model — powers semantic tool filtering AND semantic memory relevance.
options.EmbeddingGenerator = new AzureOpenAIClient(endpoint, credential)
.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator();
// Send only the tools semantically relevant to the message (requires EmbeddingGenerator).
options.EnableToolFiltering = true;
options.ToolFilterMinScore = 0.35f;
// Compact long conversation history before each call.
options.EnableCompaction = true;
options.CompactionTokenThreshold = 4000;
// Memory: reliable post-turn fact capture (default true) + inject only relevant memories.
options.UseMemoryContext = true;
options.MemoryAutoCapture = true; // default — reliable writer on Path A
options.MemoryRelevanceFiltering = true; // requires EmbeddingGenerator
});
Robustness, observability & cost dashboard (server project)
Also configured on the server — see the MentorAgent core README for full details.
builder.Services.AddMentorAgent(options =>
{
// Middleware hooks
options.OnException = ex => ex.Message.Contains("rate", StringComparison.OrdinalIgnoreCase)
? "The service is busy, please retry shortly." : null;
options.ConfigureChatClientPipeline = b => b.UseLogging();
// Observability (add an OpenTelemetry exporter to the app as usual)
options.EnableObservability = true;
// Dashboard cost pricing (supply your own; none built in)
options.ModelPricing = new Dictionary<string, ModelPrice>(StringComparer.OrdinalIgnoreCase)
{
["gpt-4o"] = new ModelPrice(2.50m, 10.00m), ["gpt-4o-mini"] = new ModelPrice(0.15m, 0.60m),
};
});
The admin dashboard ships as a Blazor component. Drop it on a protected page (you own the authorization). On Blazor Server / Auto it reads IMentorMetrics in-process; on standalone WASM, fetch GET /mentor/admin/metrics from the server and pass the snapshot:
@* Blazor Server / Auto — in-process (the component reads IMentorMetrics itself) *@
@attribute [Authorize(Roles = "Admin")]
@using Novior.MentorAgent.Abstractions.Components
<MentorDashboard Currency="$" />
@* Standalone WASM — fetch the snapshot from the server and pass it in *@
@attribute [Authorize(Roles = "Admin")]
@using System.Net.Http.Json
@using Novior.MentorAgent.Abstractions.Components
@using Novior.MentorAgent.Abstractions.Models
@inject HttpClient Http
<MentorDashboard Snapshot="_snapshot" OnRefresh="LoadAsync" Currency="$" />
@code {
private MentorMetricsSnapshot? _snapshot;
protected override Task OnInitializedAsync() => LoadAsync();
// HttpClient must target the server; the endpoint is gated by options.DashboardRole.
private async Task LoadAsync() =>
_snapshot = await Http.GetFromJsonAsync<MentorMetricsSnapshot>("mentor/admin/metrics");
}
Also configured/available on the server (see the core README for full examples):
- Rich responses — with
options.EnableRichResponses(server, default on) the assistant formats structured data as Markdown; this widget renders the tables, lists and links automatically — no client wiring, XSS-safe, tables scroll horizontally on narrow screens. - Model routing (
options.StrongChatClient+options.RoutingStrategy:Semantic/Classifier/Cascade/Custom) — cheap↔strong per turn. - Structured outputs — inject
IMentorStructured(GenerateAsync<T>) for typed results / auto-filled forms. - Evaluation — inject
MentorEvaluator(wraps the Agent Framework's nativeagent.EvaluateAsync) in tests to gate CI on token/quality regressions; plugFoundryEvals/MEAI evaluators for quality & safety.
Client project setup
// Client/Program.cs
using Novior.MentorAgent.Abstractions.Models; // MentorLanguage, MentorTheme, ChatPosition
using Novior.MentorAgent.Blazor.Extensions;
// Stop and confirmations go through an HttpClient. The standalone WebAssembly template registers one;
// the client project of a Blazor Web App does not.
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddMentorAgentBlazor(options =>
{
options.HubUrl = "/mentor-hub"; // URL of Novior.MentorAgent.Server hub
options.BotName = "My Assistant";
options.Language = MentorLanguage.English;
options.Theme = MentorTheme.Default;
options.PrimaryColor = "#2563eb";
});
⚠️
HubUrl— relative vs absolute.
- Same origin (Blazor Auto hosted, or WASM served by the same ASP.NET Core host): use a relative path —
options.HubUrl = "/mentor-hub". It is resolved against the page's base address, as the browser resolves it:/mentor-hubat the origin's root,mentor-hubunder the app's base path.- Different origin (standalone WASM on
:5001connecting to a server on:5169): use the server's absolute URL —options.HubUrl = "http://localhost:5169/mentor-hub"— and configure CORS on the server (see the Novior.MentorAgent.Server README → CORS). Without server-side CORS the SignalR handshake is silently blocked by the browser.
// Standalone WASM example — different origin
builder.Services.AddMentorAgentBlazor(options =>
{
options.HubUrl = "http://localhost:5169/mentor-hub"; // absolute — server on a different port
options.BotName = "My Assistant";
});
Authenticating the connection — AccessTokenProvider
If your users sign in, set this. The server resolves the caller from the SignalR
HubCallerContext.User, and a browser cannot send an Authorization header on a WebSocket or
Server-Sent Events connection — so the token travels the way SignalR expects, and
AccessTokenProvider is where you supply it. It is the
WASM equivalent of the JavaScript client's accessTokenFactory.
var tokenStore = new TokenStore(); // your own: holds the signed-in user's token
builder.Services.AddSingleton(tokenStore); // same instance for your login page
builder.Services.AddMentorAgentBlazor(options =>
{
options.HubUrl = "https://api.example.com/mentor-hub";
options.AccessTokenProvider = () => Task.FromResult(tokenStore.Token); // null while signed out
});
⚠️ Without it every user is anonymous to the server, and every
RequiredRolesaction is blocked. What else follows depends on the server'sAnonymousIdentity. With the defaultPerSession, each hub connection is its own anonymous user: nothing is shared between people, but memory and theRateLimitPerUserallowance start over on every reconnect. WithShared, every user is the single key"anonymous": one rate limit for everybody and one memory bucket. WithMemoryAutoCaptureon (the default), one person's name and preferences then reach another person's prompt.
The server must accept the token from the query string. A browser cannot send an
Authorizationheader on a WebSocket or Server-Sent Events connection, so SignalR sends the token as?access_token=…. With JWT bearer on the server, forward it inOnMessageReceived, or the hub still sees an anonymous caller:// Server Program.cs builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => options.Events = new JwtBearerEvents { OnMessageReceived = ctx => { var token = ctx.Request.Query["access_token"]; if (!string.IsNullOrEmpty(token) && ctx.HttpContext.Request.Path.StartsWithSegments("/mentor-hub")) ctx.Token = token; return Task.CompletedTask; } });
For the hub, the provider is read on connect and on every reconnect, not per message. (The Stop and confirmation requests read it again each time they are sent.) The widget opens the hub connection lazily, when the user sends the first message, and keeps it for the life of the page. A sign-in before that first message is picked up automatically. A user who signs in after it stays on the old, anonymous connection:
MentorHubClienthas no way to restart it, so reload the page after sign-in (Navigation.NavigateTo(Navigation.Uri, forceLoad: true)) and restore the persisted token during startup. A reconnect is a new connection on the server, so it also starts a new conversation.
For anything else the connection needs — a custom header, a transport restriction, cookies —
ConfigureConnection runs on the same HttpConnectionOptions immediately afterwards:
options.ConfigureConnection = o => o.Headers["X-Tenant"] = tenantId;
Stop and confirmations — plain HTTP to the hub's server
■ Stop (POST /mentor/cancel) and the answer to a confirmation (POST /mentor/approve) cannot
go through the hub (see How it works), so the widget sends them as HTTP requests.
They follow the hub settings above:
- Where they go. When
HubUrlis an absolutehttp(s)URL, both are posted to the hub's origin (https://api.example.com/mentor/approveforHubUrl = "https://api.example.com/mentor-hub"). With a relativeHubUrl(same-origin hosting) they stay relative and resolve against the host'sHttpClient.BaseAddress. - What they carry. When
AccessTokenProvideris set, each request sendsAuthorization: Bearer <token>with the token it returns — the same credential the hub uses. Both endpoints accept a Stop or an answer only from the user who opened the hub connection (401for an anonymous caller,403for another user), so this is what lets a signed-in user's Stop and confirmations through.
Two things are still yours:
- Register an
HttpClient. The widget sends these requests through theHttpClientin your client's DI container, andAddMentorAgentBlazor()does not register one. The standalone WebAssembly template registers one. The client project of a Blazor Web App does not, and without it the WebAssembly runtime does not start (CannotResolveService … HttpClient): add the line of the setup snippet above to itsProgram.cs. With an absoluteHubUrltheBaseAddressdoes not matter for these two calls. - CORS, cross-origin. The server's CORS policy must allow the client's origin for these POSTs as
well as for the hub, including the
Authorizationheader when you send a token. On the server,UseCorscomes beforeUseAuthenticationandUseAuthorization, and those beforeMap…: with a protected hub, any other order blocks the client on another origin (see the Novior.MentorAgent.Server README → CORS).
A failure is only logged (the widget has already closed the prompt), so check the browser console if Stop or a confirmation seems to be ignored.
Add the widget
In MainLayout.razor or any page:
@using Novior.MentorAgent.Abstractions.Components
<ChatWidget />
Important — a standalone Blazor WASM app requires manual CSS/JS links in index.html.
Unlike Blazor Server (where the widget injects CSS automatically via <HeadContent>), a standalone Blazor WASM app uses a static index.html that is served before the .NET runtime starts. Add these two lines to your wwwroot/index.html:
<head>
...
<link href="_content/Novior.MentorAgent.Abstractions/css/MentorAgent.css?v=1.2.0" rel="stylesheet" />
</head>
<body>
...
<script src="_content/Novior.MentorAgent.Abstractions/js/MentorAgent.js?v=1.2.0"></script>
</body>
Without this, the widget will render unstyled until after WASM initializes (flash of unstyled content).
⚠️ Set the
?v=to the package version and change it on every upgrade. On Blazor Server the widget writes these tags itself and versions them for you, with the package version and its commit; here they are yours, nothing fingerprints them, and a returning visitor's browser will reuse the copy it already has. A staleMentorAgent.jsfails silently and selectively — the widget still works, but the calls that did not exist in the older file are swallowed by theirtry/catch, so the onboarding tour never appears and voice falls back to reading the whole answer at the end. If a feature seems missing after an upgrade, check the served file before anything else: it should containflagGetandbeginSpeech.
On Blazor Server, <ChatWidget /> injects its own CSS and JS automatically — no changes to _Host.cshtml or App.razor needed. A Blazor Web App (Auto or WebAssembly) has no index.html: prerendered — the default — the widget writes the two tags itself, as on Blazor Server; with prerender: false add them to the <head> of App.razor.
Widget customization
Theme and appearance
options.Theme = MentorTheme.Minimal; // Default | Dark | Minimal | Custom | Auto
options.PrimaryColor = "#7c3aed"; // any hex color
options.Position = ChatPosition.BottomRight; // BottomRight | BottomLeft | TopRight | TopLeft | SideRight | SideLeft
options.AvatarUrl = "/my-avatar.png";
options.BotName = "ShopFlow Assistant";
Welcome message and input
options.WelcomeMessage = "Hello! How can I help you today?";
options.InputPlaceholder = "Ask anything...";
options.EnableSuggestions = true; // show suggestion chips in the welcome panel
Language (10 supported)
options.Language = MentorLanguage.Italian;
// English | Italian | French | German | Spanish | Portuguese | Dutch | Polish | Japanese | Chinese
This sets the widget's UI strings, read again on every use. The language the assistant answers in is the server's options.Language in AddMentorAgent(), and the prompt enforces it whatever language the user writes in. A widget given its own language, <ChatWidget Language="MentorLanguage.Italian" />, sends it with every turn, and the server answers in it (see A language per page in the MentorAgent README). Otherwise set both:
using Novior.MentorAgent.Abstractions.Models; // MentorLanguage, in both Program.cs files
// Server Program.cs
builder.Services.AddMentorAgent(options => { /* ... */ options.Language = MentorLanguage.Italian; });
// Client Program.cs
builder.Services.AddMentorAgentBlazor(options => { options.Language = MentorLanguage.Italian; });
Voice
options.EnableVoiceInput = true; // microphone button (browser Speech Recognition)
options.EnableVoiceOutput = true; // text-to-speech for AI responses
options.VoiceStreaming = true; // default — speak sentence by sentence while the answer streams
options.VoiceBargeIn = true; // default — taking the floor stops playback
options.VoiceHandsFree = false; // opt-in — keep the conversation going by voice alone
options.VoiceRate = 1.0; // 0.5–2.0
Unlike image input and the hosted-tools badge below, these are not a mirror of server settings. Voice is entirely browser behaviour: your server sees only the transcript, as an ordinary message, and these options are the real thing rather than a copy of something the server enforces. The audio is the browser's business: some browsers, Chrome among them, send it to their maker's speech service for recognition (MDN, Using the Web Speech API).
VoiceStreaming is what makes voice output usable on anything longer than a sentence. Without it the assistant stays silent for the whole response and then recites it. Text is buffered to a sentence boundary and queued as its own utterance, so speech keeps pace with the stream — and the boundary detection knows that 8.459 is one number and that a fenced code block must be dropped whole rather than read out.
VoiceBargeIn stops playback when the user presses the microphone, sends a message, presses ■ Stop or starts a new conversation. It is press-to-interrupt, not acoustic: a browser cannot listen through its own playback without echo cancellation.
VoiceHandsFree sends on silence and reopens the microphone after the spoken answer ends. It needs both voice options on — with nothing to listen to there is nothing to wait for, and the loop would transcribe the assistant. If you set only one, it is ignored.
Onboarding tour
options.EnableOnboardingTour = true; // shown once, on this user's first open of the widget
options.TourUrl = "/mentor/tour"; // default — only change it if you remapped the server endpoints
The steps are generated on the server — only it knows the registered [MentorPage] pages and the assistant's tools — and fetched from GET /mentor/tour, which MapMentorAgentServer() maps whatever the server's own EnableOnboardingTour says. The flag on this client alone decides whether to show the tour; the server needs only the [MentorPage] pages, and with none it answers [].
That split follows the same rule as the hosted-tools badge: the server is the single source of truth, and a client that reproduces server state locally eventually displays something the server no longer agrees with.
Each step can carry a ready-made question the user sends with one tap — which is the point, since the usual failure of an in-app assistant is not that people cannot find it but that they do not know what to ask. A ? button in the header replays the tour later.
If the fetch fails the widget opens normally and logs a warning: a missing tour is a missing nicety, not a broken chat.
Generative UI cards
Nothing to configure on the client. When a server-side tool returns a MentorCard, the server pushes it on the Cards hub event and the widget renders it under the reply — fields, accent colour and buttons.
Buttons work the same as in Blazor Server: SendMessage sends the text as a user turn, Navigate routes to the URL, UIAction runs an action the current page registered (resolved at click time, so a stale handler from a page you have navigated away from is skipped rather than invoked).
To render a card kind yourself, supply a template and fall back to the built-in renderer for the rest:
<ChatWidget>
<CardTemplate Context="card">
@if (card.Kind == "order") { <OrderCard Card="card" /> }
else { <MentorCardView Card="card" /> }
</CardTemplate>
</ChatWidget>
The fallback's buttons work without any wiring: ChatWidget cascades its own card-action dispatcher around the cards, and a <MentorCardView> inside the template that sets no OnAction uses it, with the same SendMessage / Navigate / UIAction behaviour as above. An explicit OnAction on the MentorCardView still wins. The dispatcher reaches only MentorCardView: buttons you draw yourself in a template (as OrderCard might) get nothing from it, so render a MentorCardView for any card whose buttons should behave like the built-in ones.
Cards are built by server-side application code, not by the model, which is why they can safely carry buttons: nothing said in the conversation can add one or change where it points.
Image input (multimodal)
Lets the user attach images to a message. The widget accepts them four ways — 📎 file picker, Ctrl+V clipboard paste, drag & drop onto the composer, and 🔗 remote URL — and shows removable thumbnails before sending and inside the message bubble afterwards.
// Client Program.cs — mirrors the server settings (the server is what actually enforces them)
builder.Services.AddMentorAgentBlazor(options =>
{
options.EnableImageInput = true;
options.MaxImageBytes = 4 * 1024 * 1024; // per image (default 4 MB)
options.MaxImagesPerMessage = 4; // per turn (default 4)
options.AllowedImageTypes = ["image/png", "image/jpeg", "image/webp"]; // MIME allow-list
});
The server must also enable it, with a vision-capable model:
// Server Program.cs
builder.Services.AddMentorAgent(options =>
{
// ...AppName, ScanAssemblies as above...
options.ChatClient = azure.GetChatClient("gpt-4.1").AsIChatClient(); // vision-capable
options.EnableImageInput = true;
});
The client values only drive the UI and give the user instant feedback; every attachment is re-validated server-side (allow-list, size, count) before it reaches the model. Images travel over the hub as the second argument of SendMessage(text, attachments). The client always sends both arguments, with null for a text-only turn, because SignalR binds hub arguments by count. The images ride inside that one hub message as base64, so AddMentorAgentServer() raises MentorHub's MaximumReceiveMessageSize from the server's MaxImageBytes × MaxImagesPerMessage (+512 KB). Keep the client values at or below the server's: a message larger than the hub limit makes SignalR abort the connection before the server can validate it, and the user sees their own bubble and no reply.
Sending images from your own code:
@inject IMentorOrchestrator Orchestrator
await Orchestrator.SendMessageAsync("Cosa non va in questo screenshot?", [
new MentorAttachment { MimeType = "image/png", DataBase64 = base64, FileName = "error.png" },
new MentorAttachment { MimeType = "image/jpeg", Url = "https://cdn.example.com/product.jpg" },
]);
Image bytes are streamed to .NET through
IJSStreamReference, never marshalled as one big interop payload — so this works unchanged on Blazor Server too, without touchingHubOptions.MaximumReceiveMessageSize.
RAG citations
options.ShowRagSources = true; // show citation chips below AI responses
MCP and A2A status badges
When the server has MCP client servers or remote A2A agents configured, the widget can display live status badges in the header. Because the WASM client doesn't read the server configuration directly, you must mirror the relevant settings:
// Client Program.cs
builder.Services.AddMentorAgentBlazor(options =>
{
// MCP badge — mirror McpServers names from the server
options.ShowMcpStatus = true;
options.HasMcpServers = true;
options.McpServerNames = ["time", "filesystem"]; // must match server McpServers[].Name
// A2A badge — mirror RemoteAgents from the server
options.ShowA2AStatus = true;
options.HasRemoteAgents = true;
options.RemoteAgentDisplays = [
new AgentDisplayInfo { Name = "ShopFlow-B", AgentCardUrl = "http://localhost:5001" }
];
});
The MCP badge updates dynamically — when a server connects or disconnects the hub fires McpServerStatusChanged and the badge turns green/red. The A2A badge is static (shows configured agents, no live status).
Note:
McpServerNamesmust match theNamefields inMentorMcpServer[]configured on the server. A mismatch shows a stale "connecting" badge.
Hosted tools badge
If the server enables hosted tools (provider-side web search, code interpreter, file search, image generation, remote MCP), an amber pill lists them in the header. Do not list them here — turn the badge on and let the server say what it enabled:
options.ShowHostedToolsStatus = true; // that's all
The server sends its active set on the HostedToolsDeclared hub event the moment the client connects, and the widget prefers it over any local value. On WebAssembly the connection opens when the user sends the first message, so until then the badge shows options.HostedTools (nothing, by default). Setting options.HostedTools by hand still works as a pre-connection placeholder, but it is a copy that goes stale: change the server's configuration and the badge starts claiming tools that are not there — which is worse than no badge, because it is believed.
Unlike MCP the badge has no live status afterwards: hosted tools are configuration, not a connection.
Live activity needs no configuration here. When the server has ShowHostedToolActivity on, the widget already shows what the provider is doing, over the hub events it consumes anyway:
- "Ricerca sul web… · .NET 10" in the feedback line, via
ActionExecuting/ActionCompleted - pages cited by web search as citation chips, via
RagSourcesReady— the same panel as RAG, so it also needsShowRagSources - generated images attached to the finished message, via the
GeneratedImagesevent
The WASM client handles all three out of the box.
Page context and UI actions
IMentorPageContext works identically to Blazor Server. Register context data and UI actions in any page — they are sent to the server as a snapshot before each AI message.
Inject page context
@inject IMentorPageContext PageContext
@implements IDisposable
@code {
protected override void OnInitialized()
{
PageContext
.SetPageName("Orders")
.Set("ActiveFilter", "Pending")
.Set("VisibleRows", _orders.Count);
}
public void Dispose() => PageContext.Clear("Orders"); // this page's entries only, by its name
}
Register UI actions (no parameter)
PageContext.RegisterUIAction(
"open_create_modal",
"Opens the modal to create a new order",
_ => OpenCreateModal());
Register UI actions with typed parameter
// The AI calls highlight_row(42) — parameter deserialized automatically
PageContext.RegisterUIAction<int>(
"highlight_row",
"Highlights the specified order row",
id => HighlightRow(id),
parameterHint: "integer: order ID");
Async UI actions
PageContext.RegisterUIActionAsync<OrderModel>(
"prefill_form",
"Pre-fills the edit form with order data",
async model => {
_formModel = model;
await InvokeAsync(StateHasChanged);
},
parameterHint: "JSON: { orderId, amount, status }");
Remove an action
PageContext.UnregisterUIAction("highlight_row");
Signal page ready (Blazor Server and MAUI Blazor Hybrid only)
On WebAssembly SignalReady() is a no-op and costs nothing, so you can keep the call in pages shared with a Blazor Server host:
protected override async Task OnInitializedAsync()
{
await LoadDataAsync();
PageContext.SignalReady(); // Blazor Server, MAUI Blazor Hybrid: tells navigate_to the UI actions are ready. WASM: no-op
}
What matters on WebAssembly is when the server learns about the page. Its name, data and UI actions reach the server only with the page-context snapshot sent before the user's next message (UpdatePageContext). So when the assistant moves the user with navigate_to, the server does not wait for the new page — even on a [MentorPage] with HasUIActions = true, because on a hub connection nothing could ever signal it. The new page's UI actions become callable from the user's next message, not later in the same answer.
(On Blazor Server and in MAUI Blazor Hybrid, where navigate_to does wait on HasUIActions pages, call SignalReady() as the last line of OnInitialized / OnInitializedAsync, not from OnAfterRenderAsync, or the wait may already have timed out.)
Page navigation
⚠️
[MentorPage]attributes are defined in the server project (scanned byScanAssemblies), not in the client project.
// Server project — scanned via options.ScanAssemblies
[MentorPage(Url = "/orders", Name = "Orders", Description = "Order management")]
public class OrdersPage { }
[MentorPage(Url = "/products", Name = "Products", HasUIActions = true, ReadyTimeout = 3000)]
public class ProductsPage { }
The AI calls navigate_to("/orders") — the WASM widget handles navigation automatically via Blazor's NavigationManager.
HasUIActions and ReadyTimeout only affect hosts where the page runs in the same process as the turn: Blazor Server, or a MAUI Blazor Hybrid app. For a turn that arrives over the hub, navigate_to returns without waiting for SignalReady (it logs this at Debug level), and the new page's UI actions reach the model with the next page-context snapshot, on the user's next message — see Signal page ready.
UI Action overloads reference
Four overloads are available, from simple to fully typed and async:
| Overload | Parameter | Execution | Use when |
|---|---|---|---|
RegisterUIAction(name, desc, Action<object?>) |
Raw object? |
Synchronous | Simple no-param or legacy code |
RegisterUIAction<TParam>(name, desc, Action<TParam>) |
Auto-deserialized from JSON | Synchronous | Typed param, sync handler |
RegisterUIActionAsync(name, desc, Func<object?, Task>) |
Raw object? |
Async | No-param async actions (e.g. async _ => { await LoadAsync(); }) |
RegisterUIActionAsync<TParam>(name, desc, Func<TParam, Task>) |
Auto-deserialized from JSON | Async | Typed param, async handler (recommended) |
UnregisterUIAction(name) |
— | — | Remove a specific action dynamically |
Automatic parameterHint generation
For typed overloads, parameterHint is auto-generated from the type when omitted:
TParam |
Auto-generated hint |
|---|---|
int, long |
"integer" |
float, double, decimal |
"number" |
bool |
"boolean" |
string |
"string" |
Guid |
"string (GUID)" |
DateTime |
"string (ISO 8601 date)" |
Status (enum) |
"string (Active\|Inactive\|Pending)" |
List<int> |
"array<integer>" |
OrderFormModel (class) |
"{ customerId: integer, productName: string, ... }" |
Override only when extra clarity is needed:
.RegisterUIAction<int>(
"highlight_row", "Highlights an order row",
id => HighlightRow(id),
parameterHint: "integer: order ID") // ← manual override
On WebAssembly the AI does not wait for your handler. The server forwards the call as
UIActionRequestedand reports the action complete as soon as it is sent. The browser then runs the handler and reports nothing back. As a result, an exception in the handler never reaches the model, andOnUIActionCompletedcan fire before an async handler has finished. Make each action self-contained (load what it needs inside the handler) rather than relying on the model to chain actions that depend on the previous one having finished. (On Blazor Server, where the handler runs in-process, async handlers are awaited.)
[MentorPage] parameters (server project)
| Parameter | Required | Description |
|---|---|---|
Url |
✅ | Page URL (e.g. "/orders") |
Name |
✅ | Human-readable page name injected into the system prompt |
Description |
— | Optional feature description |
HasUIActions |
— | If true, navigate_to waits for PageContext.SignalReady() before UI actions — only where the page runs in the turn's process: Blazor Server or MAUI Blazor Hybrid. On a hub turn from this WASM client it does not wait: the page's actions arrive with the user's next message. Default: false |
ReadyTimeout |
— | Timeout in ms for SignalReady() (Blazor Server and MAUI Blazor Hybrid only). Default: 2000 |
All AddMentorAgentBlazor() options
| Option | Type | Default | Description |
|---|---|---|---|
HubUrl |
string |
"/mentor-hub" |
URL of the Novior.MentorAgent.Server SignalR hub. A relative value is resolved against the page's base address. When absolute (http(s)), its origin is also where Stop (/mentor/cancel) and confirmation answers (/mentor/approve) are posted |
AccessTokenProvider |
Func<Task<string?>>? |
null |
Bearer token for the hub, read on connect and on every reconnect, and sent as Authorization: Bearer on every Stop and confirmation request. Without it every user is anonymous: RequiredRoles blocked; memory and rate limit are per connection (server default AnonymousIdentity = PerSession) or one shared bucket (Shared) |
ConfigureConnection |
Action<HttpConnectionOptions>? |
null |
Escape hatch on the hub's connection options — headers, transports, cookies. Runs after AccessTokenProvider |
BotName |
string |
"Mentor AI" |
Bot name in the widget header |
WelcomeMessage |
string? |
null |
Welcome message (HTML supported) |
ShowAiDisclosure |
bool |
true |
Shows the notice that tells people they are talking to an AI, under the widget's header for the whole conversation. false hides it; the replies' marker stays |
AiDisclosureMessage |
string? |
null |
The notice's text, shown as written (as text, never as markup). null or white space: the default, translated into the configured language |
AvatarUrl |
string? |
null |
Custom avatar URL |
InputPlaceholder |
string? |
null |
Input box placeholder. null uses the localized text in Language ("Write a message..." in English) |
Theme |
MentorTheme |
Default |
Widget visual theme |
Position |
ChatPosition |
BottomRight |
Widget position on screen |
PrimaryColor |
string? |
null |
Custom hex accent color |
Language |
MentorLanguage |
English |
Language for widget UI strings (10 languages supported) |
EnableVoiceInput |
bool |
false |
Show microphone button (browser Speech Recognition) |
EnableVoiceOutput |
bool |
false |
Text-to-speech for AI responses (browser Speech Synthesis) |
VoiceStreaming |
bool |
true |
Speak each sentence as it streams instead of reading the finished answer back |
VoiceBargeIn |
bool |
true |
Stop playback when the user takes the floor |
VoiceHandsFree |
bool |
false |
Voice-only conversation loop. Ignored unless both voice options are on |
VoiceRate |
double |
1.0 |
SpeechSynthesisUtterance.rate — useful range 0.5–2.0 |
EnableOnboardingTour |
bool |
false |
Show the guided tour on first open. This flag alone decides: the server answers GET /mentor/tour whatever its own EnableOnboardingTour says |
TourUrl |
string |
"/mentor/tour" |
Where to fetch the tour steps from. A relative value is resolved against HubUrl's origin when HubUrl is absolute (the tour is served by the same server as the hub), otherwise against the app base address. An absolute URL is used as-is |
EnableSuggestions |
bool |
false |
Suggestion chips in the welcome panel |
EnableImageInput |
bool |
false |
Image attachments — 📎 upload, paste, drag & drop and 🔗 URL. Must also be enabled server-side |
MaxImageBytes |
int |
4194304 |
Client-side size cap per image (4 MB). Mirrors the server option |
MaxImagesPerMessage |
int |
4 |
Client-side cap on images per message. Mirrors the server option |
AllowedImageTypes |
List<string> |
png, jpeg, gif, webp |
Client-side MIME allow-list. Mirrors the server option |
ShowRagSources |
bool |
false |
Citation chips below AI responses |
ShowHostedToolsStatus |
bool |
false |
Show the hosted-tools badge (provider-side web search / code interpreter / file search / image generation / remote MCP) |
HostedTools |
MentorHostedTools |
None |
Pre-connection placeholder only. The server publishes its actual set on HostedToolsDeclared at connect and the widget prefers that — leave this unset rather than keeping a copy that goes stale |
ShowMcpStatus |
bool |
false |
Show MCP server connection status badge in the widget header |
HasMcpServers |
bool |
false |
Whether the server has MCP client servers configured (enables the MCP badge) |
McpServerNames |
List<string> |
[] |
Names of MCP servers configured on the server — pre-populates the badge in "connecting" state at startup |
ShowA2AStatus |
bool |
false |
Show A2A remote agent status badge in the widget header |
HasRemoteAgents |
bool |
false |
Whether the server has remote A2A agents configured (enables the A2A badge) |
RemoteAgentDisplays |
List<AgentDisplayInfo> |
[] |
Remote A2A agents to display in the A2A badge and detail bar |
How it works
[Blazor WASM Browser]
ChatWidget
↓ IMentorOrchestrator (WasmMentorOrchestrator)
↓ SignalR
[ASP.NET Core Server — Novior.MentorAgent.Server]
MentorHub
↓ MentorOrchestrator (full AI pipeline)
↓ AI Provider (Azure OpenAI, OpenAI, Ollama...)
↑ Streaming events (chunks, confirmations, navigation, UI actions...)
↑ SignalR
ChatWidget renders response
- Page context (page name, data, UI action descriptions) is sent to the server before every message via
UpdatePageContext. - UI action invocations arrive from the server as
UIActionRequestedand are executed locally in the browser byWasmMentorStateService. - Confirmation dialogs (HITL) are shown inline by
ChatWidget. The response is sent viaPOST /mentor/approve?actionId=...&approved=true|false(HTTP) — not via a hub method. SignalR processes hub messages sequentially per connection, so callingRespondToApprovalvia hub whileSendMessageis awaiting would deadlock.WasmMentorStateServicesends it to the hub's origin with the hub's bearer token — see Stop and confirmations. - Stop (cancelling an in-flight turn) is sent via
POST /mentor/cancel?connectionId=...(HTTP) — not via theCancelRequesthub method, for the same sequential-dispatch reason: whileSendMessageis streaming, SignalR cannot dispatch another hub invocation on that connection, so the hub call would only run after the turn it was meant to abort.WasmMentorOrchestratorsends it the same way as the approval: to the hub's origin, with the hub's bearer token. - UI actions after
navigate_to: the server does not wait for the new page (nothing on a hub connection can callSignalReady); the page's actions reach the model with the nextUpdatePageContext, on the user's next message. - Navigation triggered by the AI arrives as
NavigationRequestedand is handled by Blazor'sNavigationManager.
Conversation lifetime and session snapshots
The conversation lives on the server, in a DI scope that belongs to one SignalR connection. A reconnect (network drop, server restart, a laptop waking up) gets a new connection id and therefore a new, empty conversation on the server, even though the widget still shows the old messages.
IMentorOrchestrator.SerializeSessionAsync / RestoreSessionAsync throw NotSupportedException on WebAssembly, because the AgentSession is not in the browser. Save and restore it over HTTP instead, with the live connection id. That id is null until the first message opens the connection.
@inject Novior.MentorAgent.Blazor.Hub.MentorHubClient Hub
// BaseAddress = the server, carrying the user's token
@inject HttpClient Http
// Save — 204 means there is no conversation yet: keep what you already have
var save = await Http.GetAsync($"mentor/session?connectionId={Uri.EscapeDataString(Hub.ConnectionId!)}");
if (save.StatusCode == HttpStatusCode.OK)
_saved = await save.Content.ReadAsStringAsync();
// Restore on the new connection (after its first message; an earlier request can get 404)
await Http.PostAsync($"mentor/session?connectionId={Uri.EscapeDataString(Hub.ConnectionId!)}",
new StringContent(_saved, Encoding.UTF8, "application/json"));
Both endpoints check that the caller is the user who opened the connection (401/403 otherwise), before the body is read. The snapshot is opaque — protected with ASP.NET Core Data Protection and bound to that user (on a host without sign-in every visitor is the same anonymous user, so there it is bound to the application only) — so store it as the text GET returned. A body that is not a snapshot this server issued for this user, or was edited, or was saved before 1.0.0-rc.14, is refused with 400 and leaves the live conversation untouched. Restoring after a server restart needs the server's Data Protection key ring persisted (see the Novior.MentorAgent.Server README).
Events — IMentorStateService
ChatWidget handles all events automatically. If you need to subscribe to events directly in your own components, inject IMentorStateService:
@inject IMentorStateService State
@implements IDisposable
protected override void OnInitialized()
{
State.OnStreamingChunk += OnChunk;
State.OnStreamingCompleted += OnCompleted;
State.OnBusyChanged += OnBusy;
State.OnError += OnError;
State.OnActionExecuting += OnActionExecuting;
State.OnActionCompleted += OnActionCompleted;
State.OnActionFailed += OnActionFailed;
State.OnConfirmationRequired += OnConfirmationRequired;
State.OnNavigationRequested += OnNavigation;
State.OnRagSourcesReady += OnRagSources;
State.OnTeamMemberSpeaking += OnTeamSpeaking;
State.OnUIActionExecuting += OnUIActionStart;
State.OnUIActionCompleted += OnUIActionEnd;
}
public void Dispose()
{
State.OnStreamingChunk -= OnChunk;
// ... unsubscribe all
}
Complete event reference
| Event | Signature | Fired when | Typical use |
|---|---|---|---|
OnStreamingChunk |
Action<string> |
Each streaming token | Append text to a custom chat bubble |
OnStreamingCompleted |
Action |
Full response received | Finalize message, re-enable input |
OnBusyChanged |
Action<bool> |
AI starts/stops processing | Show/hide spinner |
OnError |
Action<string> |
Critical error (rate limit, safety block) | Show error banner |
OnActionExecuting |
Action<string> |
Tool/agent is executing. Carries the text to show (the action's Description, or the routing, team or hosted-tool label), not the tool name |
Show action feedback bar. To log which tool ran, use OnActionCompleted, which carries the tool name |
OnActionStarted |
Action<MentorActionStarted> |
Never raised by this client: the hub sends it only the text shown, not the tool name | Record actions on the server, where it is raised with ToolName and DisplayName |
OnActionCompleted |
Action<string> |
Tool execution succeeded | Hide feedback bar |
OnActionFailed |
Action<string> |
Tool execution failed — tool: message; with the server's OnException set, never the exception's own text |
Show error in feedback |
OnConfirmationRequired |
Action<ConfirmationRequest> |
Destructive action needs user approval | Show custom confirmation dialog |
OnApprovalResponse |
Action<ConfirmationRequest, bool> |
The answer reached the waiting turn: raised once the server accepted it, never after Stop or for a second answer | An audit trail of decisions |
OnNavigationRequested |
Action<string> |
AI triggered navigation | Custom routing logic |
OnRagSourcesReady |
Action<IReadOnlyList<MentorRagResult>> |
RAG documents the finished answer cited (plus hosted web/file-search citations); not raised when nothing was cited | Show custom citation UI |
OnTeamMemberSpeaking |
Action<string, string> |
GroupChat member speaking | Show "Team · Role" feedback |
OnUIActionExecuting |
Action<string> |
UI action started | Custom feedback |
OnUIActionCompleted |
Action<string> |
UI action completed | Custom feedback |
OnMcpServerStatusChanged |
Action<string, bool> |
MCP server connects (true) or disconnects (false) |
Update the MCP status badge — forwarded over the hub as McpServerStatusChanged |
OnCardsReady |
Action<IReadOnlyList<MentorCard>> |
A tool returned generative-UI cards | Render them yourself instead of using CardTemplate |
OnGeneratedImages |
Action<IReadOnlyList<string>> |
The hosted image tool produced images | Show them in your own gallery. Each string is a data: URI or URL |
OnHostedToolsDeclared |
Action<MentorHostedTools> |
Once per connection, right after connect | Drive your own capability badge from what the server actually enabled, rather than from a client-side guess |
@inject IMentorStateService State
@inject IMentorOrchestrator Orchestrator
@inject NavigationManager Nav
@implements IDisposable
@if (_capabilities.HasFlag(MentorHostedTools.WebSearch)) { <span class="badge">🌐 web</span> }
@foreach (var url in _images) { <img src="@url" alt="generata" /> }
@* Outside ChatWidget no dispatcher is cascaded: without OnAction the buttons do nothing *@
@foreach (var c in _cards) { <MentorCardView Card="c" OnAction="OnCardAction" /> }
<span>MCP: @_mcp.Count(x => x.Value)/@_mcp.Count</span>
@code {
private MentorHostedTools _capabilities;
private IReadOnlyList<string> _images = [];
private IReadOnlyList<MentorCard> _cards = [];
private readonly Dictionary<string, bool> _mcp = new();
protected override void OnInitialized()
{
State.OnHostedToolsDeclared += Declared;
State.OnGeneratedImages += Images;
State.OnCardsReady += Cards;
State.OnMcpServerStatusChanged += Mcp;
}
// Fires once per connection, before any message. Render your capability badge from THIS rather
// than from a client-side copy of the server's configuration — the two drift, and a badge
// claiming web search the model never got is worse than no badge.
private void Declared(MentorHostedTools flags) { _capabilities = flags; InvokeAsync(StateHasChanged); }
private void Images(IReadOnlyList<string> urls) { _images = urls; InvokeAsync(StateHasChanged); }
// Only if you are NOT using ChatWidget's CardTemplate. Ignoring cards loses them outright: the
// model is told they are already on screen, so it will not repeat their content as text.
private void Cards(IReadOnlyList<MentorCard> cards) { _cards = cards; InvokeAsync(StateHasChanged); }
private async Task OnCardAction(MentorCardAction action)
{
if (action.Kind == MentorCardActionKind.SendMessage) await Orchestrator.SendMessageAsync(action.Value);
else if (action.Kind == MentorCardActionKind.Navigate) Nav.NavigateTo(action.Value);
// UIAction: run the current page's registered action named action.Value, as ChatWidget does
}
private void Mcp(string name, bool connected) { _mcp[name] = connected; InvokeAsync(StateHasChanged); }
public void Dispose()
{
State.OnHostedToolsDeclared -= Declared;
State.OnGeneratedImages -= Images;
State.OnCardsReady -= Cards;
State.OnMcpServerStatusChanged -= Mcp;
}
}
All eighteen events are listed above — that is the whole of IMentorStateService's read side.
All events fire on a background thread from the SignalR connection. Always use
InvokeAsync(StateHasChanged)when updating Blazor component state from these handlers.
Sending state changes — the Notify* side
Each event has a matching Notify* method on the same interface (NotifyStreamingChunk,
NotifyCardsReady, …). Those are how the orchestrator raises the events; application code
subscribes and does not call them.
The two members you do call are the HITL replies:
| Method | Purpose |
|---|---|
ConfirmAsync(Guid actionId) |
Approve the pending action |
Cancel(Guid actionId) |
Reject it |
actionId is ConfirmationRequest.ActionId from OnConfirmationRequired. In WASM these travel to
the server over POST /mentor/approve (at the hub's origin, with the AccessTokenProvider token —
see Stop and confirmations), never as a hub method — SignalR dispatches one hub call at a
time per connection, so an approval sent through the hub would deadlock behind the turn that is
waiting for it.
Custom HITL confirmation dialog
ChatWidget always shows its own inline confirmation prompt when OnConfirmationRequired fires, and no parameter turns it off. Subscribing therefore adds your dialog next to the widget's prompt instead of replacing it. Answering from your dialog also does not close the widget's prompt, which reacts only to its own buttons. To show only your dialog, hide the built-in prompt with CSS (.bm-confirm { display: none !important; }: the package stylesheet sets display: flex on the same selector), then subscribe to OnConfirmationRequired and call ConfirmAsync / Cancel yourself.
This works identically whichever
MentorOptions.HitlModethe server uses (Blockingor the Agent Framework'sNativeflow) — the WASM client sees the sameConfirmationRequiredevent and replies over the samePOST /mentor/approveendpoint, so switching mode server-side needs no client change.
@inject IMentorStateService State
@implements IDisposable
@if (_pendingConfirmation is not null)
{
<div class="my-confirm-dialog">
<p>@_pendingConfirmation.Message</p>
<button @onclick="Approve">Conferma</button>
<button @onclick="Reject">Annulla</button>
</div>
}
@code {
private ConfirmationRequest? _pendingConfirmation;
protected override void OnInitialized()
{
State.OnConfirmationRequired += OnConfirmationRequired;
State.OnBusyChanged += OnBusyChanged;
}
private void OnConfirmationRequired(ConfirmationRequest req)
=> InvokeAsync(() => { _pendingConfirmation = req; StateHasChanged(); });
// The turn is over (answered, stopped or failed): nothing waits for this dialog any more.
private void OnBusyChanged(bool busy)
{
if (!busy) InvokeAsync(() => { _pendingConfirmation = null; StateHasChanged(); });
}
private async Task Approve()
{
if (_pendingConfirmation is not { } req) return;
_pendingConfirmation = null;
await State.ConfirmAsync(req.ActionId);
}
private void Reject()
{
if (_pendingConfirmation is not { } req) return;
_pendingConfirmation = null;
State.Cancel(req.ActionId);
}
public void Dispose()
{
State.OnConfirmationRequired -= OnConfirmationRequired;
State.OnBusyChanged -= OnBusyChanged;
}
}
Close the dialog on the click and when the turn ends, as here. OnApprovalResponse is raised only when
the answer reached a turn that was waiting for it, so it suits an audit trail of decisions, not closing
a dialog: a click after Stop raises nothing.
// An audit trail: one entry per answer that a waiting turn received
State.OnApprovalResponse += (req, approved) =>
logger.LogInformation("{Tool} {Decision} by the user", req.ToolName, approved ? "approved" : "rejected");
ConfirmationRequesthas three properties:ActionId(Guid),ToolName(string), andMessage(string). UseToolNameto customize the dialog copy per action type if needed.
Blazor Auto tip
In a Blazor Auto app, you can keep <ChatWidget /> with @rendermode="InteractiveServer" — it stays server-side with no changes to your existing Novior.MentorAgent setup. Use Novior.MentorAgent.Blazor only when you need the widget to run fully in WebAssembly.
This needs the template's Interactivity location: Per page/component. With Global the layout runs under Auto and, once in WebAssembly, cannot host a server component (NotSupportedException): use Novior.MentorAgent.Blazor there. In the WebAssembly template, first add .AddInteractiveServerComponents() and .AddInteractiveServerRenderMode() to the server's Program.cs, or every page fails with HTTP 500.
@* Keep server-side in Blazor Auto — zero changes needed *@
<ChatWidget @rendermode="InteractiveServer" />
Requirements
- .NET 10.0+
- Server project must have
Novior.MentorAgent+Novior.MentorAgent.Serverinstalled and configured - Browser with WebAssembly support
Related Packages
| Package | Purpose |
|---|---|
| Novior.MentorAgent | Blazor Server app — AI orchestration engine |
| Novior.MentorAgent.Server | Any ASP.NET Core backend — SignalR hub + SSE + MCP + A2A |
| Novior.MentorAgent.Abstractions | Shared UI components (transitive dep — no need to install directly) |
| Novior.MentorAgent.Declarative | Optional — define Level-2 specialist agents in YAML instead of C# |
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.SignalR.Client (>= 10.0.13)
- Novior.MentorAgent.Abstractions (= 1.2.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
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.