MentorAgent.Abstractions
1.0.0-rc.3
dotnet add package MentorAgent.Abstractions --version 1.0.0-rc.3
NuGet\Install-Package MentorAgent.Abstractions -Version 1.0.0-rc.3
<PackageReference Include="MentorAgent.Abstractions" Version="1.0.0-rc.3" />
<PackageVersion Include="MentorAgent.Abstractions" Version="1.0.0-rc.3" />
<PackageReference Include="MentorAgent.Abstractions" />
paket add MentorAgent.Abstractions --version 1.0.0-rc.3
#r "nuget: MentorAgent.Abstractions, 1.0.0-rc.3"
#:package MentorAgent.Abstractions@1.0.0-rc.3
#addin nuget:?package=MentorAgent.Abstractions&version=1.0.0-rc.3&prerelease
#tool nuget:?package=MentorAgent.Abstractions&version=1.0.0-rc.3&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 — how a model's usage is attributed in the metrics snapshot |
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-IT". This is what the browser Speech APIs are handed, so a custom voice control speaks 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 injects
IOptions<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.
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 |
<ChatWidget>
<CardTemplate Context="card">
@if (card.Kind == "order") { <OrderCard Card="card" /> }
else { <MentorCardView Card="card" /> }
</CardTemplate>
</ChatWidget>
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 language from DI. Pass it explicitly in WASM, where there is no MentorAgent DI, or the dashboard falls back to English |
The Refresh button is always rendered; 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.
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. ChatWidget wires this to send / navigate / run a UI action; 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"),
],
};
private Task RunCardAction(MentorCardAction action) => action.Kind switch
{
MentorCardActionKind.Navigate => Task.Run(() => Nav.NavigateTo(action.Value)),
MentorCardActionKind.SendMessage => Orchestrator.SendMessageAsync(action.Value),
_ => Task.CompletedTask,
};
}
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.
ChatWidgetwiresOnActionfor you; wire it yourself only when you render cards outside the widget.
MessageBubble
| Parameter | Type | Description |
|---|---|---|
Message |
ChatMessage |
Required. The MEAI message. Role decides the side and styling |
IsStreaming |
bool |
Renders the trailing caret while text is still arriving |
SentAt |
DateTime? |
Timestamp under the bubble; omitted when null |
Sources |
IReadOnlyList<MentorRagResult>? |
RAG citation chips below the message |
@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> |
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 |
<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()
{
// Blazor Server: IMentorTour from DI. WASM: GET /mentor/tour.
_steps = await Tour.GetStepsAsync();
_resumeAt = await LoadSavedIndex(); // your storage; clamped for you
_tourOpen = _steps.Count > 0;
}
private Task Persist(int index) => 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 answer over HTTP —
POST /mentor/approve?actionId=…&approved=true|false— not through a hub method: SignalR dispatches one invocation at a time per connection, so a hub call would queue behind the very turn it is meant to release.
WelcomePanel
| Parameter | Type | Default | Description |
|---|---|---|---|
BotName |
string |
— | Shown in the empty state heading |
WelcomeMessage |
string? |
null |
null → the localised default |
EnableSuggestions |
bool |
true |
Whether to render suggestion chips |
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. null or empty renders nothing |
<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.
<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. Start empty rather than false: nothing has been probed yet.
private Dictionary<string, bool?> _mcp = new();
protected override void OnInitialized() =>
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.
@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.
Static assets — CSS and JS---
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.
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. No MentorAgent DI here, so pass the language explicitly. *@
@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.
2. Persisting or aggregating metrics. Implement IMentorMetricsStore and register it before
AddMentorAgent() / AddMentorAgentServer(). 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.6.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.3 | 36 | 9/4/2026 |
| 1.0.0-rc.2 | 81 | 8/24/2026 |
| 1.0.0-rc.1 | 94 | 8/19/2026 |
| 1.0.0-preview.5 | 78 | 8/12/2026 |
| 1.0.0-preview.4 | 83 | 8/4/2026 |
| 1.0.0-preview.3 | 76 | 7/24/2026 |
| 1.0.0-preview.2 | 86 | 6/22/2026 |
| 1.0.0-preview | 90 | 6/22/2026 |
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.