MentorAgent.Abstractions
1.0.0-rc.12
dotnet add package MentorAgent.Abstractions --version 1.0.0-rc.12
NuGet\Install-Package MentorAgent.Abstractions -Version 1.0.0-rc.12
<PackageReference Include="MentorAgent.Abstractions" Version="1.0.0-rc.12" />
<PackageVersion Include="MentorAgent.Abstractions" Version="1.0.0-rc.12" />
<PackageReference Include="MentorAgent.Abstractions" />
paket add MentorAgent.Abstractions --version 1.0.0-rc.12
#r "nuget: MentorAgent.Abstractions, 1.0.0-rc.12"
#:package MentorAgent.Abstractions@1.0.0-rc.12
#addin nuget:?package=MentorAgent.Abstractions&version=1.0.0-rc.12&prerelease
#tool nuget:?package=MentorAgent.Abstractions&version=1.0.0-rc.12&prerelease
MentorAgent.Abstractions
Preview Release — MentorAgent is currently in public preview. APIs may change before the stable release.
You do not need to install this package directly. It is included automatically as a transitive dependency of
MentorAgentandMentorAgent.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 |
|---|---|
| MentorAgent | Blazor Server app |
| MentorAgent.Server | Web API / headless backend, or Blazor Auto server-side project |
| MentorAgent.Blazor | Blazor WASM / Blazor Auto client project |
| MentorAgent.Abstractions ← you are here | Never directly — it arrives with any of the above |
| MentorAgent.Declarative | Optional — define Level-2 specialist agents in YAML instead of C# |
What's included
Interfaces
| Interface | Description |
|---|---|
IMentorOrchestrator |
AI orchestrator contract — implemented by MentorOrchestrator (server) and WasmMentorOrchestrator (WASM) |
IMentorStateService |
Thread-safe event bus between the AI layer and the UI layer |
IMentorPageContext |
Page context and UI actions registry |
IMentorAgent |
Marker interface for Level 2 specialized agents |
IMentorTeam |
Marker interface for Level 3 collaborative teams |
IMentorStructured |
Typed generation — GenerateAsync<T>(input, instructions) returns a deserialized T, with the JSON schema derived from your type |
IMentorMetrics |
Read-side of the token/cost counters — GetSnapshot() feeds MentorDashboard |
IMentorTour |
Supplies the onboarding tour steps. The default implementation generates them from the app's own pages and tools; register your own to script the tour by hand |
IMentorMetricsStore |
Optional persistence/aggregation for those metrics. Implement it to survive restarts or to read an external aggregate (Prometheus, Azure Monitor) instead of the in-RAM snapshot |
Models
| Type | Description |
|---|---|
ConfirmationRequest |
HITL confirmation request shown in ConfirmationBanner |
MentorRagResult |
A single RAG search result with content, URL, title, and score |
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
MentorAgent.Abstractions.Components, the models and enums inMentorAgent.Abstractions.Models, the interfaces inMentorAgent.Abstractions.InterfacesandMentorLocalizerinMentorAgent.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 MentorAgent.Abstractions.Components @using MentorAgent.Abstractions.Models @using MentorAgent.Abstractions.Interfaces @using MentorAgent.Abstractions.Core @using Microsoft.Extensions.AI @inject IMentorOrchestrator Orchestrator @inject IMentorStateService State @inject NavigationManager Nav @inject IMentorTour Tour
ChatWidget
| Parameter | Type | Description |
|---|---|---|
CardTemplate |
RenderFragment<MentorCard>? |
Optional custom renderer for generative-UI cards. Not supplied → MentorCardView is used. Match on card.Kind and fall back to the built-in renderer for kinds you do not handle |
@* A MentorCardView inside the template needs no OnAction: ChatWidget cascades its own
dispatcher around the cards, so these buttons send / navigate / run the UI action. *@
<ChatWidget>
<CardTemplate Context="card">
@if (card.Kind == "order") { <OrderCard Card="card" /> }
else { <MentorCardView Card="card" /> }
</CardTemplate>
</ChatWidget>
The template replaces the widget's card rendering for every card, but not its button handling:
a <MentorCardView Card="card" /> rendered inside the template without OnAction uses the widget's
dispatcher, so its SendMessage, Navigate and UIAction buttons work as they do on an untemplated
card. An OnAction you set explicitly still wins. Buttons your template draws itself (inside
OrderCard above) do not get the dispatcher: handle them yourself, or render MentorCardView for
the cards whose buttons should just work.
Everything else about the widget comes from MentorWidgetOptions in DI, not from parameters — see
the option tables in the MentorAgent or
MentorAgent.Blazor README, depending on which
host you use.
MentorDashboard
| Parameter | Type | Default | Description |
|---|---|---|---|
Snapshot |
MentorMetricsSnapshot? |
null |
An override, not the only source. Pass it in WASM/headless. In Blazor Server leave it null and the component resolves the data itself |
OnRefresh |
EventCallback |
— | Fired after the Refresh button re-resolves. Handle it in WASM to re-fetch; in Blazor Server you can ignore it |
Currency |
string |
"$" |
Symbol prefixed to every cost figure. The library never converts — supply ModelPricing already in the currency you want shown |
Language |
MentorLanguage? |
null |
null → the MentorLocalizer from DI, which follows the Language option on both hosts (AddMentorAgent() and AddMentorAgentBlazor() both register it). Pass it only to override that language, or in an app that registered neither; there the dashboard falls back to English |
The Refresh button is part of the populated view; OnRefresh only decides whether anything of yours
runs when it is pressed. With no data at all the component renders its empty state rather than
throwing, and that state has no Refresh button. In WASM, a first fetch that failed cannot be
retried from the dashboard: retry it yourself (a button of your own, or a timer) and pass the new
Snapshot.
Its resolution order when Snapshot is null is: an external IMentorMetricsStore.QueryAsync()
first (so a Prometheus/Azure Monitor aggregate wins), then the live in-RAM IMentorMetrics snapshot.
@* Blazor Server — no parameters needed *@
<MentorDashboard />
@* WASM — you own the fetch *@
<MentorDashboard Snapshot="_snapshot"
OnRefresh="Reload"
Currency="€"
Language="MentorLanguage.Italian" />
MentorCardView
| Parameter | Type | Description |
|---|---|---|
Card |
MentorCard |
Required. The card to render |
OnAction |
EventCallback<MentorCardAction> |
Raised when a button is pressed. Left unset inside ChatWidget (including in a CardTemplate), the widget's own dispatcher sends / navigates / runs the UI action; an explicit OnAction wins. Wire it yourself when you render cards outside the widget |
<MentorCardView Card="_card" OnAction="RunCardAction" />
@code {
private readonly MentorCard _card = new("order")
{
Title = "Ordine #1002",
Subtitle = "Laura Bianchi",
Accent = MentorCardAccent.Info,
Fields = [new("Stato", "Shipped"), new("Totale", "1.249,98 €")],
Actions =
[
new("Apri", MentorCardActionKind.Navigate, "/orders/1002"),
new("Dettagli", MentorCardActionKind.SendMessage, "Dammi i dettagli dell'ordine 1002"),
],
};
// @inject IMentorOrchestrator Orchestrator, NavigationManager Nav, IMentorPageContext PageContext
private async Task RunCardAction(MentorCardAction action)
{
switch (action.Kind)
{
case MentorCardActionKind.SendMessage:
await Orchestrator.SendMessageAsync(action.Value);
break;
case MentorCardActionKind.Navigate:
Nav.NavigateTo(action.Value);
break;
// Resolved at click time, as ChatWidget does: the page that registered the action
// may have changed since the card was rendered.
case MentorCardActionKind.UIAction
when PageContext.UIActions.TryGetValue(action.Value, out var reg):
if (reg.HandlerAsync is not null) await reg.HandlerAsync(null);
else reg.Handler(null);
break;
}
}
}
The buttons are written by the tool that produced the card, never by the model's prose — which is why a card can safely carry actions. Inside
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 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 MentorAgent:
_content/MentorAgent.Abstractions/css/MentorAgent.css
_content/MentorAgent.Abstractions/js/MentorAgent.js
_content/MentorAgent.Abstractions/IconMA.png ← default avatar
ChatWidget injects the first two itself through <HeadContent>, so a Blazor Server or Blazor Web
App host needs no markup. A standalone WASM host references them manually in index.html
instead — and neither file is fingerprinted, so the ?v= query string is the cache buster and you
must bump it by hand after changing either. A stale MentorAgent.js fails silently and looks like
missing features rather than a caching problem.
That injection happens only where ChatWidget is rendered, and it relies on the <HeadOutlet />
that the Blazor Web App template already includes. A page that composes the parts without
ChatWidget (see Composing your own chat surface) gets neither
file: the components render unstyled, and ChatInput's microphone and paste / drag & drop stay off
without an error. Sending a message still works: the call that resets the textarea after a send is
guarded, so a missing script neither throws nor ends a Blazor Server circuit. Reference both files yourself in that case, with the same paths and version
ChatWidget uses, in App.razor (Blazor Server / Web App) or index.html:
<link href="_content/MentorAgent.Abstractions/css/MentorAgent.css?v=10" rel="stylesheet" />
<script src="_content/MentorAgent.Abstractions/js/MentorAgent.js?v=10"></script>
The script can safely load a second time on a page where ChatWidget also renders: the later tag
wins and the voice state carries over.
The JS layer covers text-to-speech (sentence-queued, barge-in, hands-free), speech recognition, the
image composer (upload, paste, drag & drop, client-side downscaling) and localStorage helpers. It
is called through IJSRuntime by the components; there is no supported way to call it directly.
For the complete CSS variable reference — palette, widget size, fonts, the dashboard and chart
colours — see Widget customization → CSS variables
in the MentorAgent README. The variables are defined here but documented there, next to the
options that set them.
When you actually touch this package
Most of the time you never reference these types by name — MentorAgent and MentorAgent.Blazor
wire them for you. Three cases where you do:
1. Rendering the admin dashboard yourself. MentorDashboard is presentational, so the same
component works in-process and over HTTP. In Blazor Server, inject the metrics; in WASM, fetch the
snapshot from the admin endpoint:
@* Blazor Server — in-process. It resolves the metrics itself; no parameters needed. *@
<MentorDashboard />
@* WASM — over HTTP. There is no IMentorMetrics in the browser, so pass the snapshot. Language is optional: it follows AddMentorAgentBlazor's Language unless you override it. *@
@inject HttpClient Http
<MentorDashboard Snapshot="_snapshot" Language="MentorLanguage.Italian" />
@code {
MentorMetricsSnapshot? _snapshot;
protected override async Task OnInitializedAsync() =>
_snapshot = await Http.GetFromJsonAsync<MentorMetricsSnapshot>("/mentor/admin/metrics");
}
Put it behind authorization. The snapshot is operator data — cost, volumes, top actions — and must never appear on an end-user page.
The endpoint is gated on its own as well:
GET /mentor/admin/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?
MentorAgent (server) depends on Microsoft.Agents.AI, ModelContextProtocol.AspNetCore, and other server-only packages that are not compatible with Blazor WebAssembly. MentorAgent.Blazor (WASM) cannot reference MentorAgent directly.
MentorAgent.Abstractions contains only WebAssembly-safe dependencies:
Microsoft.AspNetCore.Components.WebMicrosoft.AspNetCore.Components.AuthorizationMicrosoft.Extensions.AI
Both MentorAgent and MentorAgent.Blazor reference MentorAgent.Abstractions — sharing interfaces, models, and UI components without cross-contaminating dependencies.
The practical rule: anything that must run in the browser goes here. If you are adding a type
and it needs Microsoft.Agents.AI, an HttpContext, or the file system, it does not belong in this
package — the WASM build will not fail at compile time, it will fail at runtime in the browser.
Requirements
- .NET 10.0 or later
Microsoft.AspNetCore.Components.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 |
|---|---|
| MentorAgent | Blazor Server — full AI assistant |
| MentorAgent.Server | Any ASP.NET Core app — headless AI backend |
| MentorAgent.Blazor | Blazor WASM — SignalR client |
| MentorAgent.Declarative | Optional — Level-2 specialists defined in YAML |
License
MIT — the full text ships in the repository's LICENSE file.
| 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 MentorAgent.Abstractions:
| Package | Downloads |
|---|---|
|
MentorAgent
A Blazor Razor Class Library that adds an AI-powered floating chat assistant to any Blazor application, built on Microsoft Agent Framework. Supports multi-level agent orchestration (tools, handoff, group chat), page navigation, UI actions, contextual memory, streaming, voice, and security — all with zero boilerplate. |
|
|
MentorAgent.Blazor
Blazor WebAssembly client for MentorAgent. Install in your Blazor WASM / Blazor Auto client project. Connects to a MentorAgent.Server hub via SignalR and provides the same ChatWidget experience as Blazor Server. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0-rc.12 | 63 | 9/23/2026 |
| 1.0.0-rc.11 | 70 | 9/23/2026 |
| 1.0.0-rc.10 | 77 | 9/19/2026 |
| 1.0.0-rc.9 | 73 | 9/19/2026 |
| 1.0.0-rc.8 | 75 | 9/18/2026 |
| 1.0.0-rc.7 | 72 | 9/16/2026 |
| 1.0.0-rc.6 | 79 | 9/14/2026 |
| 1.0.0-rc.5 | 79 | 9/13/2026 |
| 1.0.0-rc.4 | 101 | 9/9/2026 |
| 1.0.0-rc.3 | 85 | 9/4/2026 |
| 1.0.0-rc.2 | 98 | 8/24/2026 |
| 1.0.0-rc.1 | 111 | 8/19/2026 |
| 1.0.0-preview.5 | 91 | 8/12/2026 |
| 1.0.0-preview.4 | 91 | 8/4/2026 |
| 1.0.0-preview.3 | 88 | 7/24/2026 |
| 1.0.0-preview.2 | 92 | 6/22/2026 |
| 1.0.0-preview | 96 | 6/22/2026 |
1.0.0-rc.12
Found by re-driving the published rc.11 on the six NuGet sample applications, with a Blazor Server host driven as the A2A peer for the first time on published packages and every remote answer held against the peer's REST data, then by a pre-publication pass of this package on the same six applications. One API addition, opt-in: MentorOptions.ToolFilterMinTools (default 0 - tool selection is unchanged from rc.11).
=== FIXED
- BUG-100 (S2): a confirmation-gated action (RequiresConfirmation or the RequiresApproval predicate) called on an A2A task held the task until the calling agent gave up: both the blocking and the native approval path waited for an answer nobody could give. It is now refused at once, in both modes, and the model is told it has to be done from the application itself. The agent card already left these actions out.
- BUG-098 (S2): an action with NavigateTo ran, then the auto-navigation read NavigationManager.Uri, which throws in a scope with no circuit, and the tool was reported failed - so the model ran it again and the caller was told it had not happened. Seen over A2A on a Blazor Server host, reachable since rc.11 made that work (BUG-083), and possible on any turn served without a circuit on a host that registers a Blazor NavigationManager. A turn from another agent no longer auto-navigates, and a page URL that cannot be read no longer turns a completed action into a failure. An exception from your own OnToolResult hook still reports an action that already ran as failed: keep that hook from throwing.
- BUG-097 (S3): on a Blazor Server host with memory or RateLimitPerUser on, every A2A task logged BUG-072's warning ("The AuthenticationStateProvider threw ... check the provider can be read from a scoped service"), and a role-gated tool the model reached for was logged as an Error with a stack trace and answered "Error during authorization check". The provider is still asked, so a headless host keeps an authenticated peer's identity as in rc.11. When it cannot be read on a task from another agent, the task is anonymous with a Debug line, and a gated tool is refused as for any anonymous caller - the usual "Tool ... blocked" warning, no Error, no stack trace.
=== MITIGATED, NOT FIXED - read this if you use EnableToolFiltering and have actions that change data
- BUG-099 (S1, present since tool filtering existed; the published rc.10 did the same): on hosts with EnableToolFiltering on (off by default), a question about stock set the stock to 0. The filter sends only the actions whose cosine score clears ToolFilterMinScore; for "Quante unita' di iPad Air ci sono in magazzino?" that was one action, the stock UPDATE (0.351), while the actions that read stock ranked just below (0.29-0.32). Handed one write action for a read question, the model called it with a quantity of 0 - over SSE 3 times of 3, over A2A 4 of 4. NEW: ToolFilterMinTools - once one action clears the threshold, send at least this many best-ranked ones, whatever their score. It is a trade-off, measured both ways, so it is off by default: with 3 the stock question read the stock and wrote nothing, but "Quanto ha speso Anna Ferrari?", whose lone match above the line was the right search, got a by-email lookup the floor had added and answered "no such customer" 0 times of 4, against 4 of 4 with the threshold alone. What protects you is [MentorAction(RequiresConfirmation = true)] on every action that changes data: a person is asked, and over A2A the action is refused (BUG-100). The sample applications now gate their stock update.
=== VERIFIED ON THE PUBLISHED rc.11 (no change)
- BUG-083: a Blazor Server host serving A2A tasks, questions put straight to its /a2a - 6 of 6 true (rc.10: every task empty).
- BUG-082: from the React sample, 8 remote-addressed questions, 8 tasks on the peer, none answered by a local specialist.
- BUG-084 (SafetyCheckTimeout with SafetyCheckFailure = Block) on a Blazor Server and a headless host; BUG-086 (a signed-in WebAssembly user's confirmation and Stop reach the hub's origin with the token); BUG-094 (a webSearch entry in a YAML definition removed with a warning).
=== PERFORMANCE
- rc.11 against this package on the six NuGet sample applications' ApiServer, same host and day, nothing else running, six questions in three rounds: median time to first character 8.9 s against 7.2 s, whole reply 12.6 s against 11.2 s, each faster in 9 of 18 paired turns, the same number of model calls, input tokens equal within 0.1 %. rc.10 against rc.11, measured the same way before: 11.8 s against 10.9 s, input tokens +1-2 % on delegated turns (the note rc.11 added to each delegated answer, saying whose answer it is) and under 1 % elsewhere.
=== STILL OPEN
- BUG-099 (S1): mitigated as above; a structural answer (actions declared read-only) is for a later version.
- BUG-074 (S3): MentorshipLevel.Proactive offers actions the application does not have. Measured, recorded, a product-voice decision.
1,733 tests green, build 0 warnings / 0 errors.
1.0.0-rc.11
Found by re-driving F2 (A2A client -> live peer) on the published rc.10 with every answer held against the peer's REST data AND its task log, and by running the Blazor Server sample on a local model (Ollama). BUG-081 is closed on the published packages. API additions: SafetyCheckTimeout and SafetyCheckFailure; auditing the five READMEs against the code then found eleven code defects behind the text, all fixed below.
=== FIXED
- BUG-082 (S2): a LOCAL specialist's answer reached the user as the REMOTE agent's. "Chiedi a ShopFlowRemote: quanto ha speso Anna Ferrari?" came back in 8.8 s with no task on the peer, reading "... EUR 5.800,00 sull'istanza ShopFlowRemote": the coordinator had called route_to_specialist with the addressee tidied out of the request and no specialist, the router gave a question about a customer to the local CustomerAgent, and the tool returned that agent's words with nothing to say whose they were. The figure matched only because the samples share their seed data. On a host that has remote agents every delegated answer now opens with its source: a local specialist answered -> a note that no remote agent was contacted and an instruction to call once more with specialist set if the user meant one (the second call carries the name, so the retry is bounded); the remote agent answered -> "that system's data, not this application's"; the remote agent was named and never took part -> "NOT X'S ANSWER", plus a warning for the operator. "Remote" means every configured peer, including one whose card could not be fetched when the session was built. The router's list marks remote entries, and its rules say that a request addressed to a remote agent is about that system's data whatever its subject. Hosts without remote agents, and turns that arrived over A2A, read byte for byte what they read before.
- BUG-083 (S2, present in rc.10): a Blazor Server host could not answer another agent - every A2A task it received completed with an empty message. A turn served over A2A has no circuit; AppContextProvider read NavigationManager.Uri unguarded, and it threw "'RemoteNavigationManager' has not been initialized". The caller saw an empty reply and told its user the remote agent was unavailable. The read is guarded, and the A2A handler now FAILS a task that produced no words instead of completing it empty. Headless hosts (MentorAgent.Server) were not affected.
- BUG-084 (S2): the safety checks had a fixed 15-second limit and failed open, so on a slow model (a local one on a CPU) the input check was skipped on every message, with a stack trace; Stop could not interrupt the input check; and a custom InputGuardrail that honoured its cancellation token threw out of SendMessageAsync, leaving the widget busy for good. NEW: SafetyCheckTimeout (default 15 s; zero or negative = no limit, and Stop still cancels) and SafetyCheckFailure (Allow - the default and the previous behaviour - or Block: refuse the message with a localized "can't check it right now, try again", or withhold the reply). Both apply to the input and the output check, built-in or custom. A timeout logs one line that says what to change; an error keeps a single stack trace per turn (BUG-071). The output check now runs on ClassifierChatClient, like the input check. Nothing changes for a host that sets neither option.
=== FIXED - found by auditing the five READMEs against the code (285 findings confirmed by a second reader: 264 were documentation, the rest code)
- WebAssembly client: answering a confirmation and Stop now go to the hub's origin with the hub's bearer token (AccessTokenProvider). They used the host's HttpClient with a relative URL and no token, so a signed-in user's confirmations and Stop were answered 401 by the owner check.
- ConfigureChatClientPipeline is built with the host's services: the documented b => b.UseLogging() made every coordinator build fail.
- ChatWidget CardTemplate: a MentorCardView inside a template reaches the widget's action dispatcher; its buttons did nothing.
- UI actions registered by a hub client (WebAssembly, React, MAUI) keep their parameter hint, and the WebAssembly page context generates the same hints as Blazor Server.
- navigate_to no longer waits for SignalReady where no page can send it (hub, SSE and A2A turns): each navigation to a page with HasUIActions cost the full ReadyTimeout and a warning.
- McpServerPath and A2AServerPath are the default paths of MapMentorAgentMcp() / MapMentorAgentA2A(); nothing read them.
- [MentorAction(ProactiveHint)] reaches the model, appended to the tool description as "Guidance: ..." (not used by semantic tool filtering); it was stored and never read.
- SkillsRefreshInterval caches the composed skill sources once per process; each session built its own cache, so the option did nothing.
- MentorAgent.Declarative: webSearch, codeInterpreter, fileSearch and mcp entries in a YAML definition are removed with a warning; only kind: function is kept. They created provider-hosted tools outside the gate, against the package's promise that a definition file cannot add a capability.
- ChatInput composed without ChatWidget no longer throws on the first message (an unguarded JS call ended the Blazor Server circuit). The output safety check is metered under ClassifierChatClient's model id.
- Documentation: 264 corrections across the five READMEs, among them attribute examples that did not compile, SignalR enums arriving as numbers, the anonymous-identity and rate-limit text (per-session since rc.5), how to protect and what to expect from /mcp and /a2a. XML docs and the server's startup warning match the PerSession default.
=== VERIFIED ON THE PUBLISHED rc.10 (no change)
- BUG-081: F2 on all five package columns - 38 remote-addressed turns, 36 delegated, 36 true figures, none invented, none bounced; "quanti clienti Premium" put straight to the published ApiServer's /a2a endpoint: 10 of 10 (rc.9: 3, 4), and "ordini Pending" 5 of 5.
- BUG-080: one task per turn on the peer, none bounced back, with ApiServer and React up together.
=== STILL OPEN, UNCHANGED
- BUG-074 (S3): MentorshipLevel.Proactive offers actions the application does not have. Measured, recorded, a product-voice decision.
1,721 tests green, build 0 warnings / 0 errors.
1.0.0-rc.10
Found by re-driving the release matrix's open cells on the published rc.9. BUG-080 holds: in the topology that looped (two hosts, each the other's remote agent) a delegated request is one task on the peer and none bounced back, and A2A context is per scope on every caller. With remote answers finally arriving, they could be compared with the data - and several were not true. No API change.
=== FIXED
- BUG-081 (S2): a turn SERVED over A2A stated figures no tool had returned. "Quanti clienti Premium ha?" asked through a caller came back as 18; put straight to the peer's /a2a endpoint, as 3, 4, 11, 16, 6, 6, 8 - there are 2. Tools were offered every time and none was called, while the same server on the same question over SSE called search_customers and said 2. The only difference was the line rc.8 added to an A2A turn's context - "carry the request out with your tools and reply with the result itself" - obeyed in the wrong order: a result at once. (BUG-079's "87 customers" was very likely this.) The notice is now a procedure: FIRST call the tool; every number, name, date or status comes from a tool result of THIS turn; if no tool has it, reply only that the application cannot provide it. A rule in a prompt lowers a rate and does not remove a behaviour - measured live, the notice alone gave 3 grounded answers in 5 - so there is a structural backstop, each step of it added because the one before was measured and was not enough. When an A2A-served turn ends without the coordinator reaching for ANY tool, the handler discards the reply and puts the same request once more, with the turn's context saying why and with a tool call REQUIRED on that attempt's first model call. If the second attempt too is tool-less, the classifier model is asked whether the reply states a value of the application's live records (a model, not a pattern: "9 clienti Premium" and "reso entro 30 giorni" both contain a number); on anything but a clear no the caller receives "NOT GROUNDED: ... do not present a figure" instead of the value. Two attempts, never three; the extra turn is paid only by tool-less A2A requests; a tool-less reply that states no data is returned as it is. Measured on the package-built ApiServer, the hardest host: 15 of 15 correct, none invented, none withheld (rc.9: 18, 3, 4 for a true 2).
- BUG-081, second half: the caller's own name for the peer travels inside the request ("quanti prodotti ha ShopFlowRemote?") and nothing told the peer that name means ITSELF - it answered "I have no access to ShopFlowRemote" 3 times in 6, without calling a tool. The notice now says so, and a MentorAgent caller sends its name for the peer as A2A message metadata (mentoragent.addressedAs), which the peer reads into the notice. The value arrives from another machine and goes into a prompt: only one token of letters, digits and - _ . (64 characters at most) is accepted; anything else is dropped whole.
=== VERIFIED ON THE PUBLISHED rc.9 (no change)
- BUG-080: API and React up together, remote turns driven from every column - one "Task received" per turn on the peer, each followed by "This turn arrived over A2A: remote agent(s) ... are not offered to it", zero tasks bounced. A2A context per scope closed on the React client and the WebAssembly client (one context across a connection's turns, a different one for a second connection).
- BUG-073, residual: the cold first turn of a fresh Chat Completions process delegated this time (route_to_specialist -> OrderAgent, real data).
=== STILL OPEN, UNCHANGED
- BUG-074 (S3): MentorshipLevel.Proactive offers actions the application does not have. Measured, recorded, a product-voice decision.
1,667 tests green, build 0 warnings / 0 errors.
1.0.0-rc.9
Found by re-driving the release matrix's open cells on the published rc.8 - the first published build on which an A2A round trip completes (BUG-078 had kept every one from finishing). T6/T7 closed on every host: route_to_specialist reaches OrderAgent and the declarative ShippingAgent, with real data, and the router's call is metered. The first remote delegation ever driven between the PUBLISHED samples found what was behind it. No API change.
=== FIXED
- BUG-080 (S2): a request delegated over A2A was delegated ONWARD by the peer, and two hosts that peer each other never stopped. Live: "Delega a ShopFlowRemote: quanti prodotti a catalogo?" on one sample produced five tasks on its peer and four on the peer's peer (each ~5,500 input tokens), 141 seconds, no answer - until a server was stopped by hand. Two causes. (1) rc.8 made route_to_specialist prefix the workflow's input with "[Specialist requested: NAME]" for the LOCAL router (BUG-076), and the A2A client sent the last user message verbatim: the marker crossed the wire and the peer's coordinator read it as its own order - every sample calls its remote agent "ShopFlowRemote". The marker now has one writer and one remover (SpecialistMarker), and the A2A client strips it on both the streaming and the non-streaming path. (2) Structural: a turn that ARRIVED over A2A was offered the host's remote agents like any other. It no longer is - they are not built for that scope and are absent from the coordinator's prompt, the router's prompt and the description of route_to_specialist; one Debug line names what was withheld. The host's own specialists, declarative agents, teams and tools still serve the request. Consequence, documented in the README: chaining A -> B -> C through a MentorAgent host is not supported in 1.0 (before rc.8 no chain could complete a single hop). Verified live on the fixed source in the mirror topology that looped (two hosts, each the other's "ShopFlowRemote"): one task per turn on the peer, zero bounced back, "8 prodotti a catalogo" in 32 s.
=== VERIFIED ON THE PUBLISHED rc.8 (no change)
- BUG-076/077: three hosts' logs show route_to_specialist returned: main_coordinator -> OrderAgent[FunctionCall] -> OrderAgent[FunctionResult] -> OrderAgent[Text]; the router's call costs ~400 input tokens and is metered.
- BUG-078: both peers' cards advertise JSONRPC at an absolute URL; tasks are received and logged with their context ids. A2A context per scope holds: one context across a circuit's turns, a different one for a second circuit (Blazor Server and MAUI callers).
- BUG-075, second pass: "Ricorda che il mio codice privato e' ..." and asking for it back are both SAFE / IN scope; no UNSAFE verdict anywhere in the process log.
- BUG-079: with the peer unreachable or looping, the user read "il sistema remoto non ha fornito il numero" - no invented figure.
- BUG-073, residual rate: on the Chat Completions host the cold first turn of a process narrated a handoff without calling the tool, once in five. The fix lowers a rate; it does not remove a behaviour.
=== SAMPLES (not part of the packages)
- MentorAgentServer's remote agent can be renamed and re-pointed from the command line (--A2ARemote:Name / --A2ARemote:Url), so the source pair can mirror the published topology (two hosts, each the other's remote, same name). The source pair that verified rc.8 could not show BUG-080 because it did not.
=== STILL OPEN, UNCHANGED
- BUG-074 (S3): MentorshipLevel.Proactive offers actions the application does not have. Measured, recorded, a product-voice decision.
1,654 tests green, build 0 warnings / 0 errors.
1.0.0-rc.8 (condensed: BUG-075 to BUG-079)
The first delegated turns ever driven live, on the published rc.7. One additive API change: route_to_specialist takes an optional `specialist` argument.
=== FIXED - BUG-078 (S2): the A2A server had never completed a round trip - the card advertised the wrong protocol binding, the handler never submitted the task and disposed its scope the wrong way, and MentorAgent's own A2A client read only Message events. BUG-076 (S2): the handoff router was built from the coordinator's whole prompt (about 5,000 input tokens paid again on every delegated turn) and narrated instead of routing; it now has a router's prompt, the tool returns what the specialists said, and "NOT DELEGATED" / "DELEGATION FAILED" say what happened. BUG-077 (S3): every model call under the coordinator (router, specialists, team members, source-built agents) was unmetered; they now run on the host's ChatClient inside the metering wrapper, and an empty model id falls back to the client's own deployment. BUG-079 (S3): generated specialist and team-member prompts carry a grounding rule (state only what a tool returned); a custom [MentorAgent(Instructions = ...)] is used exactly as written and must carry its own. BUG-075, second pass: on hosts with memory on, the safety classifier is told that what users say about themselves is theirs, not a system secret (verified on the two reported messages); the model itself may still decline to store something its user calls private.
=== CHANGED - A turn that arrives over A2A is told that no human is present.
1.0.0-rc.7
Found by the release matrix: the same ~70 checks driven on all eight sample applications against the published rc.6 packages, signed in and anonymous, on Blazor Server (cookie), WebAssembly and React (JWT), Blazor Auto, .NET MAUI (over CDP) and the two source-referenced apps. 584 cells, every option of MentorOptions and MentorAgentBlazorOptions with a verdict. No API change.
=== FIXED
- BUG-073 (S2): the coordinator was told it COULD delegate to its specialists and never told how - the tool name route_to_specialist appeared nowhere in the prompt and the tool's own description named no agent. Told a specialist existed (BUG-069), the model narrated the handoff ("inoltro subito la domanda allo ShippingAgent, attendo la sua risposta") and ended the turn without calling anything, three times, once under an explicit order. The capabilities block now says how to delegate and forbids announcing an unperformed handoff; the tool's description names every specialist it reaches, attribute-declared, source-supplied and remote.
- BUG-075 (S3): "Ricorda che il mio codice privato e' ZULU-2200" was classified UNSAFE on a host with memory on. Remembering a fact the user asks to keep is a configured capability and is now named to the classifier, the way MCP servers and image input already were (BUG-067).
- BUG-072 (S3): a signed-in user whose AuthenticationStateProvider threw was silently keyed as anonymous - and since rc.5, as a different anonymous in every circuit, so the same user would have had a different memory in every tab with nothing in the log. The fallback stays (fail closed); it is now said once per session, without a stack trace per visitor. The live check on a real cookie principal shows the catch does not fire.
- BUG-068 addendum: one collaborating builder was still announcing per circuit at Information ("Remote agent 'X' connected from ..."). Routed through the same quiet logger; the general test now includes a remote agent.
=== SAMPLES (not part of the packages, recorded for whoever reads them)
- The three samples with authentication expose GET /account/dev-login?as=admin|manager|user in Development only (404 in Production, verified), so the authenticated rows of the matrix can be driven by a script; the WebAssembly and React clients accept ?as= on their login route for the same reason.
- The Blazor Server sample binds RefuseOutOfScope, WarmUpAtStartup, CompactionMaxTurns and RateLimitPerUser from configuration, so command-line overrides actually reach them.
- The samples point their A2A peer at a live server (the headless sample) instead of a port nobody listened on.
=== MEASURED, NOT CHANGED
- BUG-074 (S3, open): MentorshipLevel.Proactive still offers actions the application does not have - 3 in 5 turns ("esportare l'elenco", "cercare con il nome"). The rule is in the prompt; a structural fix (offers must name a listed tool or page) is a product-voice decision, recorded rather than taken.
- A compaction pass writes no log line; it is pinned in-process (CompactionTests). A Debug line when a pass runs would make it observable live.
1,623 tests green, build 0 warnings / 0 errors.
1.0.0-rc.6
Four defects found by running the published rc.5 packages against the sample applications - the pass rc.5's own notes implied but had not yet been done. No API change: every fix restores behaviour rc.5 already claimed.
=== FIXED
- BUG-068 (S3): the configuration summary was NOT written once per process, as rc.5's notes said it was. A second browser circuit still reprinted seven Information lines - the skills catalogue, the external agents, the hosted image model, the Azure image-header note, the hosted MCP server, the hosted tool list and the declarative handoff. The sentinel was claimed halfway through the build, after everything above it had already announced itself, and three collaborating builders never consulted it at all. It is now claimed first, and a later build demotes Information to Debug instead of discarding it, so an operator who turns Debug on to investigate one circuit can still see what it was built with. Degradation warnings stay per session, unchanged.
- BUG-069 (S2): an agent supplied by an IMentorAgentSource - a declarative YAML specialist, or a host's own - joined the handoff graph and was never named in the coordinator's instructions. route_to_specialist names no agent either, so nothing the model could see said the specialist existed: asked about it, the assistant answered that there is no such agent, while the log recorded it being added to the workflow. Source-supplied agents are now listed with their descriptions beside the attribute-declared ones.
- BUG-070 (S4): every skill on the published A2A card carried a flattened name ("Getallordersforanalysis"). The friendly-name helper splits on underscores and was being handed the PascalCase method name. That card is the one artefact whose entire audience is another machine's directory.
- BUG-071 (S4): a turn whose input safety check met the first failure printed two stack traces instead of one - the turn's fault log was reset after that check, so its cause was recorded and immediately discarded. The reset now happens before every early return.
1,616 tests green, build 0 warnings / 0 errors.
1.0.0-rc.5 (condensed; the full account is in the repository's BUGS.md, BUG-062 to BUG-067)
Latency and cost per visitor: time to the first character fell from 2.6-3.0 s to 1.6-2.0 s on a live host - one embedding per text instead of one per consumer, one classifier call for safety and scope, the composer handed back before the post-turn fact extraction, shared MCP sessions and a process-wide agent-card cache.
=== NEW - ClassifierChatClient (a small, fast deployment for MentorAgent's own one-word decisions); AnonymousIdentity (PerSession by default - BREAKING for single-user hosts that want one shared memory: set Shared); RefuseOutOfScope; WarmUpAtStartup; MentorMcpServer.Shared; a configure callback on MapMentorAgentMcp / MapMentorAgentA2A to protect them; IMentorQueryEmbedding.
=== FIXED - BUG-064 (S1): two anonymous visitors shared one memory. BUG-062, 063, 065, 066, 067 (S3/S4): text glued across a tool call, the Native-HITL banner shown before the role check, a spurious RAG scale warning, an unmetered scope classifier, legitimate requests refused by the safety classifier.
=== CHANGED - RAG chips show only cited documents; Proactive mentorship asks for one sentence before acting; the A2A card publishes skills, modes and streaming; the configuration summary is logged once per process.
1.0.0-rc.4
No change in this package. Version aligned with MentorAgent 1.0.0-rc.4, which fixes one S3 in the provider error classifier - see that package's notes. The five packages ship as a set and are meant to be upgraded together.
1.0.0-rc.3
Two accessibility and localisation fixes in the widget's own markup, and the component reference finally has code in it. No behavioural or protocol change.
=== DOCS - every component now has a worked example =========================
- The parameter tables were complete and accurate; nine of the fourteen components had a table and nothing to copy. The section's own premise is that you can compose these components on your own pages instead of taking the whole widget, and nothing demonstrated that.
- Added: MentorCardView, MessageBubble, ChatInput, OnboardingTour, ConfirmationBanner, WelcomePanel, RagSourcePanel, the four MCP/A2A badges and TypingIndicator - plus a full 'Composing your own chat surface' walkthrough that wires the state service, streams into a bubble, shows citations, answers a confirmation and unsubscribes on dispose.
- Pinned by Docs/OptionExampleCoverageTests: every member documented in a README table must appear in a code example somewhere. 38 of 220 failed that before this release.
=== FIXED - icon-only controls had no accessible name (S3) =================
- Of the nine buttons in an open panel, seven render no text and only the send button carried an aria-label. The voice-output toggle, replay tour, new conversation, close, the MCP and A2A pills, the voice-input button, the attach-file label and the image-URL button were all named by "title" alone.
- "title" is the last resort in the accessible-name computation, several screen readers do not announce it, and it never surfaces on touch - so those controls were announced as an unnamed "button". aria-label is now set alongside every title, reading the same localiser key so the two cannot drift.
- Pinned structurally rather than by a list of selectors: any control the widget renders with no text content must carry a non-empty aria-label, so an icon button added later is caught without anyone remembering to extend the test.
=== FIXED - the MCP and A2A badge copy was hard-coded (S3) =================
- These were the only widget strings built from literals instead of the localiser table, and the two components that render the badge disagreed: ChatWidget carried Italian copy, the standalone McpStatusBadge English. Whichever language a host configured, at least one was wrong - and for the other eight languages the package ships, both were.
- Six new keys in all ten languages (mcp.connecting, mcp.allOnline, mcp.someOnline, mcp.offline, a2a.pill, a2a.agents) and both components now read them.
- The existing localisation suite could not see this: it asks whether every L.Get("...") resolves to a real key, which is the right question for a string that goes through the table and no question at all about one that never does. The new test starts from the phrase instead.