MentorAgent 1.0.0-preview.3

This is a prerelease version of MentorAgent.
dotnet add package MentorAgent --version 1.0.0-preview.3
                    
NuGet\Install-Package MentorAgent -Version 1.0.0-preview.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="MentorAgent" Version="1.0.0-preview.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MentorAgent" Version="1.0.0-preview.3" />
                    
Directory.Packages.props
<PackageReference Include="MentorAgent" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add MentorAgent --version 1.0.0-preview.3
                    
#r "nuget: MentorAgent, 1.0.0-preview.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package MentorAgent@1.0.0-preview.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=MentorAgent&version=1.0.0-preview.3&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=MentorAgent&version=1.0.0-preview.3&prerelease
                    
Install as a Cake Tool

MentorAgent

Preview Release — MentorAgent is currently in public preview. APIs may change before the stable release.

Package Family

Package Install when
MentorAgent ← you are here Blazor Server app (the most common case)
MentorAgent.Server Web API, headless backend, or when you need MCP/A2A/SignalR/SSE endpoints for any ASP.NET Core app
MentorAgent.Blazor Blazor WASM / Blazor Auto client project
MentorAgent.Abstractions Never install directly — transitive dependency included automatically

MentorAgent is a .NET library that embeds a fully functional AI assistant directly into any Blazor application — with a floating chat widget, multi-agent orchestration, contextual memory, RAG, MCP integration, A2A federation, Agent Skills, and voice support — all configured via a single AddMentorAgent() call.

Built on top of Microsoft Agent Framework (Microsoft.Agents.AI), MentorAgent abstracts the complexity of multi-agent systems into a clean, attribute-driven programming model designed specifically for Blazor developers.


Table of Contents


What is MentorAgent?

MentorAgent turns any Blazor application into an AI-assisted experience. It provides:

  • A floating chat widget rendered automatically in the UI (no manual HTML needed).
  • A Main Coordinator agent that understands the application context and routes requests.
  • An attribute-driven discovery system that scans your assemblies and turns plain C# methods and classes into AI tools and agents — zero boilerplate.
  • Full integration with Microsoft Agent Framework, supporting Handoff Workflows, Group Chat teams, and any AI provider compatible with the IChatClient abstraction.
  • RAG — inject relevant documents from any vector database into every AI response.
  • MCP — consume external MCP servers as additional tools, and expose MentorAgent's own actions as an MCP server for other AI clients.
  • A2A — connect to remote AI agents via the Agent-to-Agent protocol, and expose MentorAgent as a federatable A2A agent.

The developer defines what the AI can do using attributes. MentorAgent handles everything else: prompt building, agent wiring, session management, rate limiting, safety checks, memory, RAG retrieval, MCP connections, A2A federation, Agent Skills, and UI.

Before every LLM call, MentorAgent automatically enriches the system prompt with real-time context: the current URL, the active page name, the authenticated user and their roles, the data registered by the current page via IMentorPageContext, the user's contextual memory, the Agent Skills catalogue, and — when RAG is enabled — the most relevant documents retrieved from your vector store.

UI actions registered by the current page are injected as individually named AI tools (one tool per action, each with its own description and parameter schema), rather than a generic dispatcher. This gives the AI precise knowledge of exactly what it can do on the current page, reducing hallucinations and enabling direct invocation: highlight_row(5) instead of invoke_ui_action("highlight_row", 5).


What can it do?

Feature Description
🤖 Multi-agent orchestration Coordinator + specialized agents connected via Handoff Workflow
👥 Group Chat teams Multiple agents collaborate in a structured conversation before executing
🛠️ Tool discovery C# methods become AI tools via [Description] or [MentorAction]
🎯 UI Actions AI invokes page-level actions (highlight rows, open modals, pre-fill forms) as individually named tools with typed parameters and async support
📖 Agent Skills Domain knowledge + execution packages — knowledge loaded on demand via load_skill (progressive disclosure), methods registered as L1 tools
🧠 Contextual memory Remembers user preferences and actions across sessions
🗺️ Page navigation AI navigates to any page decorated with [MentorPage]
💬 Session management Conversation history managed by Agent Framework (AgentSession)
💾 Persistent history Pluggable ChatHistoryProvider (CosmosDB, Redis, custom)
📚 RAG Retrieval-Augmented Generation — inject relevant documents from any vector DB into every AI response
🔌 MCP Client Consume external MCP servers as additional Level-1 tools for the coordinator
🖥️ MCP Server Expose every [MentorAction] method as an MCP tool — any MCP client can connect (Claude Desktop, VS Code, etc.)
🌐 A2A Consumer Connect to remote A2A agents; they participate in the Handoff workflow like local agents
📡 A2A Server Expose MentorAgent as a federatable A2A agent — other orchestrators can discover and call it
🎨 Customizable widget Themes, colors, position, avatar, bot name
🌍 Multi-language Configurable language for AI responses and the widget UI
🎤 Voice input/output Browser-native Speech Recognition and Speech Synthesis
🔒 Safety check Optional AI-based prompt injection and jailbreak detection
⏱️ Rate limiting Per-user message limit with configurable time window (requires authentication for true per-user isolation)
⏹️ Stop button Cancel any in-flight AI request mid-stream — partial response is preserved in the chat
Confirmation dialogs Destructive actions ask for user confirmation before execution
🔐 Role-based actions Actions restricted by ASP.NET Core identity roles
📦 Pluggable memory store Default in-memory, replaceable with Redis/EF Core/MongoDB
💸 Token & cost optimization Slim cache-friendly prompt, semantic tool filtering (embeddings), history compaction, RAG/memory gating
🔌 Any AI provider Azure OpenAI, OpenAI, Ollama, Azure AI Foundry, Anthropic, and more

Architecture overview

┌──────────────────────────────────────────────────────────────────────┐
│                         Blazor Application                           │
│                                                                      │
│   ┌──────────────┐      ┌────────────────────────────────────────┐   │
│   │  ChatWidget  │◄────►│          MentorOrchestrator            │   │
│   │  (Razor UI)  │      │          (Main Coordinator)            │   │
│   └──────────────┘      └────────────────┬───────────────────────┘   │
│                                          │                            │
│   ┌──────────────────────────────────────┤                            │
│   │   Per-call context injection         │                            │
│   │   · AppContextProvider (page,user)   │                            │
│   │   · RagContextProvider (documents)   │                            │
│   │   · SkillsContextProvider (catalogue)│                            │
│   │   · UIActionsMiddleware (per-action  │                            │
│   │     tools from IMentorPageContext)   │                            │
│   └──────────────────────────────────────┘                            │
│                                          │                            │
│              ┌───────────────────────────┼──────────────┐             │
│              ▼                           ▼              ▼             │
│        [MentorAgent]             [MentorAgent]    [MentorTeam]       │
│        OrderAgent                CustomerAgent    AnalysisTeam       │
│        (L2 Handoff)              (L2 Handoff)    (L3 GroupChat)      │
│              │                           │              │             │
│        [MentorAction]            [MentorAction]  [TeamMember]        │
│        C# methods                C# methods      sub-agents          │
│                                                                      │
│   ┌──────────────────────────────────────────────────────────────┐   │
│   │                    External Integrations                     │   │
│   │  MCP Client  │  MCP Server   │  A2A Consumer │  A2A Server        │   │
│   │  (tools from │  ([MentorAct] │  (remote       │ (agent-card.json + │   │
│   │   ext. MCPs) │   as MCP)     │   agents as L2)│  /a2a endpoint)    │   │
│   └──────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   ┌──────────────────────────────────────────────────────────────┐   │
│   │  Static tools always registered on the Coordinator           │   │
│   │  navigate_to │ load_skill │ read_skill_resource               │   │
│   │  remember    │ forget_all │ invoke_ui_action (Path B only)    │   │
│   └──────────────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────────────────┘
                               │
               Microsoft Agent Framework
               (ChatClientAgent, HandoffWorkflow,
                GroupChatWorkflow, AgentSession)

The MentorOrchestrator builds and drives a HandoffWorkflow (or GroupChatWorkflow for teams). Before every LLM call, three AIContextProviders enrich the system prompt: AppContextProvider (page name, URL, user, roles, memory), RagContextProvider (retrieved documents), and SkillsContextProvider (skill catalogue). UI actions registered by the current page are injected as individual named tools by UIActionsMiddleware at the IChatClient level — with their own function-calling loop handled internally before any response reaches the outer pipeline. MCP tools from external servers are added as Level-1 tools alongside local [MentorAction] methods. Remote A2A agents participate in the Handoff graph exactly like local [MentorAgent] classes.


Getting started

Installation

dotnet add package MentorAgent --prerelease

Minimal setup

In Program.cs:

using MentorAgent.Extensions;

builder.Services.AddMentorAgent(options =>
{
	options.AppName        = "My App";
	options.AppDescription = "An order management application";
	options.Language       = MentorLanguage.Italian;

	// Choose your AI provider (see "AI providers" section below)
	options.ChatClient = new AzureOpenAIClient(
			new Uri("https://myresource.openai.azure.com"),
			new AzureKeyCredential(builder.Configuration["AzureOpenAI:Key"]!))
		.GetChatClient("gpt-4o-mini")
		.AsIChatClient();

	// Assemblies to scan for agents, actions, and pages
	options.ScanAssemblies = [typeof(Program).Assembly];
});

Add the widget to your layout

In MainLayout.razor (or App.razor):

@using MentorAgent.Abstractions.Components

<ChatWidget />

That's it. The widget injects its own CSS and JS automatically — no changes to App.razor or _Host.cshtml are required.


AI providers

MentorAgent supports any provider via two entry points: IChatClient (recommended for most cases) or AIAgent (required for providers that don't expose IChatClient).

// ── Azure OpenAI — Chat Completions
options.ChatClient = new AzureOpenAIClient(endpoint, credential)
	.GetChatClient("gpt-4o-mini").AsIChatClient();

// ── Azure OpenAI — Responses API (service-managed history)
options.ChatClient = new AzureOpenAIClient(endpoint, credential)
	.GetResponsesClient("gpt-4o-mini").AsIChatClient();

// ── OpenAI direct
options.ChatClient = new OpenAIClient("sk-...")
	.GetChatClient("gpt-4o").AsIChatClient();

// ── Ollama (local)
options.ChatClient = new OllamaChatClient(new Uri("http://localhost:11434"), "llama3.2");

// ── Azure AI Foundry (AIProjectClient) — requires AIAgent
options.Agent = new AIProjectClient(endpoint, credential)
	.AsAIAgent(model: "gpt-4o-mini", instructions: "You are a helpful assistant.");

// ── Anthropic Claude — requires AIAgent
options.Agent = new AnthropicClient() { APIKey = apiKey }
	.AsAIAgent(model: "claude-haiku-4-5", instructions: "...");

⚠️ Set either ChatClient or Agent, not both.

Embedding model (optional)

Configure an embedding model to enable semantic tool filtering and semantic memory relevance (MemoryRelevanceFiltering) — see Token & cost optimization. Optional — everything works without it.

// Azure OpenAI — a separate embedding deployment
options.EmbeddingGenerator = new AzureOpenAIClient(endpoint, credential)
	.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator();

// OpenAI direct
options.EmbeddingGenerator = new OpenAIClient("sk-...")
	.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator();

Path A vs Path B — feature matrix

Feature ChatClient (Path A) AIAgent (Path B)
L1 actions
L2 Handoff agents ❌ requires IChatClient
L3 Group Chat teams ❌ requires IChatClient
A2A remote agents ❌ requires IChatClient
MCP client tools
RAG
Agent Skills
UI Actions (per-action tools) ✅ via UIActionsMiddleware ⚠️ single generic invoke_ui_action dispatcher
Safety check ❌ skipped automatically
Session history reduction ✅ configurable ✅ configured in the agent
Custom ChatHistoryProvider ✅ via options ⚠️ must be set in the pre-built agent

When using AIAgent (Path B), ChatHistoryProvider cannot be added after construction — configure it directly in the agent you pass to options.Agent.


The three-level agent model

MentorAgent organizes AI capabilities in three progressive levels of complexity.

Level 1 — Actions on a service

The simplest level. Decorate C# methods with [Description] (standard .NET, zero dependencies) or [MentorAction] (adds confirmation, roles, navigation, hints).

// Register the service in DI as usual
builder.Services.AddScoped<OrderService>();
public class OrderService
{
	// Simple — [Description] from System.ComponentModel, zero dependencies
	[Description("Returns the list of active orders")]
	public async Task<List<Order>> GetActiveOrdersAsync() { ... }

	// Advanced — full [MentorAction] with all parameters
	[MentorAction(
		Description          = "Cancels an existing order",
		Category             = "Orders",             // groups actions in proactive suggestions
		RequiresConfirmation = true,                 // shows confirmation banner before executing
		RequiredRoles        = ["Manager"],          // only users with this role can trigger it
		ProactiveHint        = "Suggest checking active orders first", // hint injected into AI prompt
		NavigateTo           = "/orders")]           // AI navigates here automatically after success
	public async Task<bool> CancelOrderAsync(int orderId, string reason) { ... }
}

MentorAgent discovers the service via ScanAssemblies and exposes its methods as AI tools automatically. The AI calls the right method based on the user's natural language request.

[MentorAction] parameters summary:

Parameter Description
Description Natural language description used as the AI tool description
Category Groups actions in proactive suggestion chips
RequiresConfirmation Shows a confirmation banner before executing. Use for destructive or irreversible operations
RequiredRoles ASP.NET Core identity roles required to invoke the action. Empty = accessible to all
ProactiveHint Hint injected into the AI prompt to guide proactive behaviour
NavigateTo URL the AI navigates to automatically after successful execution

Level 2 — Specialized agent with Handoff

Decorate a class with [MentorAgent] to create a dedicated AI agent connected to the Main Coordinator via a Handoff Workflow. The coordinator hands off the conversation to the right agent based on the request.

[MentorAgent(
	Name        = "OrderAgent",
	Description = "Handles everything related to orders: creation, tracking, cancellation",
	HandoffTo   = ["CustomerAgent", "InvoiceAgent"]  // can hand off further
)]
public class OrderAgent : IMentorAgent
{
	private readonly OrderService _orders;
	public OrderAgent(OrderService orders) => _orders = orders;

	[Description("Creates a new order for a customer")]
	public async Task<Order> CreateOrderAsync(int customerId, List<string> products) { ... }

	[Description("Retrieves the status of an order")]
	public async Task<OrderStatus> GetOrderStatusAsync(int orderId) { ... }
}

Register the agent in DI:

builder.Services.AddScoped<OrderAgent>();

MentorAgent builds a ChatClientAgent from the class, wires it into the Handoff graph, and generates the system prompt from Name and Description (or you can supply a custom Instructions).

Implementing IMentorAgent is optional but recommended — it gives compile-time safety, enables injecting the agent in tests, and ensures the DI container validates its registration.

[MentorAgent] parameters:

Parameter Required Description
Name Agent name — used as the key in the Handoff graph and in the coordinator's system prompt
Description Natural language description of the agent's capabilities. Used by the coordinator to decide when to delegate
HandoffTo Names of other [MentorAgent] agents this agent can hand off to. Must match the Name values exactly (case-insensitive)
Instructions Custom system prompt for this agent. If omitted, MentorAgent auto-generates one from Name and Description

⚠️ Important: agent names in HandoffTo must match exactly the Name in the target [MentorAgent] attribute (case-insensitive). A mismatch produces a warning in the logs at startup and silently skips that handoff edge.


Level 3 — Collaborative team (Group Chat)

Use [MentorTeam] to define a Group Chat where multiple AI agents collaborate — debating, analyzing, and approving — before a result is returned to the user or handed off to an L2 agent for execution.

[MentorTeam(
	Name          = "BusinessAnalysisTeam",
	Description   = "Analyzes business data and produces an approved action plan",
	MaxIterations = 8,                                      // forced termination after 8 turns
	TriggerOn     = ["analysis", "business plan", "budget"], // keywords that activate this team
	HandoffTo     = ["OrderAgent", "CustomerAgent"])]
public class BusinessAnalysisTeam : IMentorTeam            // IMentorTeam is optional, recommended
{
	[TeamMember(
		Role         = "DataAnalyst",
		Tools        = [typeof(ReportTools), typeof(OrderTools)],
		Instructions = "Analyze the data and propose solutions with numbers.")]
	public object? Analyst { get; set; }

	[TeamMember(
		Role         = "BusinessApprover",
		Instructions = "Review the analyst's proposal. Reply APPROVED or REJECTED with reasons.")]
	public object? Approver { get; set; }

	[TeamTerminationCondition]
	public bool ShouldTerminate(string lastMessage, string lastSpeaker)
		=> lastSpeaker == "BusinessApprover" &&
		   (lastMessage.Contains("APPROVED") || lastMessage.Contains("REJECTED"));
}

MentorAgent builds a GroupChatWorkflow from this declaration and connects it to the main Handoff graph.

[MentorTeam] parameters:

Parameter Required Description
Name Team name — used as the key in the routing graph and shown in logs
Description Description of the team's purpose. Used by the coordinator to decide when to activate it
TriggerOn Keywords injected into the coordinator's prompt as hints for when to activate this team. Not hard rules — the AI decides
MaxIterations Maximum number of turns in the Group Chat before forced termination. Default: 10
HandoffTo L2 agents to delegate execution to after the team approves a plan

[TeamMember] parameters:

Parameter Required Description
Role Role name within the team (e.g. "DataAnalyst", "Approver")
Instructions System prompt for this member — defines what it should do and how it should respond
Tools Tool classes this member can call during the discussion. Should be read-only — write operations belong in L2 agents via HandoffTo
  • [TeamTerminationCondition] is required when you need custom exit logic. Required method signature: bool ShouldTerminate(string lastMessage, string lastSpeaker). Without it, the team runs for MaxIterations turns with round-robin turn-taking and stops automatically.
  • TriggerOn keywords are injected into the Coordinator's prompt to help it decide when to activate the team — they are hints, not hard rules.
  • [TeamMember] tools should be read-only (queries, reports). Write operations should be delegated to L2 agents via HandoffTo.
  • Implementing IMentorTeam is optional but recommended for the same reasons as IMentorAgent.

Decorate Blazor pages with [MentorPage] to let the AI navigate to them on user request.

@* Simple page — navigation only *@
@attribute [MentorPage("/orders", "Orders")]

@* Page with UI actions — waits for SignalReady() after navigation *@
@attribute [MentorPage("/orders-interactive", "Interactive Orders",
	HasUIActions = true,
	ReadyTimeout = 3000,
	Description  = "Order management with inline editing and filtering")]

Pages are discovered at startup from ScanAssemblies — they don't need to be visited first. When HasUIActions = true, the AI waits for the page to call PageContext.SignalReady() before executing UI actions, ensuring the component is fully mounted.

[MentorPage] parameters:

Parameter Required Description
Url Page URL (e.g. "/orders"). Used by the navigate_to tool
Name Human-readable page name injected into the system prompt
Description Optional description of the page's features shown to the AI
HasUIActions If true, the AI waits for PageContext.SignalReady() before executing UI actions. Default: false
ReadyTimeout Timeout in milliseconds for SignalReady(). Used only when HasUIActions = true. Default: 2000

Page context and UI actions

IMentorPageContext is a scoped service injectable in any Blazor page. It has two responsibilities:

  1. Share visible data — key/value pairs serialized live into the AI system prompt before every call (filter state, selected item, visible row count, etc.).
  2. Register UI actions — C# lambdas the AI can invoke directly on the current page without going through a business service (highlight a row, open a modal, pre-fill a form).

How UI actions work (architecture)

Each registered UI action becomes its own named AI tool before every LLM call — injected dynamically via UIActionsMiddleware (a DelegatingChatClient wrapping the raw model). The AI sees highlight_row(parameter: integer: order ID) directly in its tool list, not a generic dispatcher. This:

  • Eliminates hallucinated action names (the AI sees exact names, not strings to guess)
  • Provides proper parameter schemas for each action
  • Enables reliable multi-action chains

UI actions are scoped to the current page: they appear in the tool list only while the page is mounted, and disappear the moment the page calls PageContext.Clear() in Dispose().

Registering UI actions

Four overloads are available, from simple to fully typed and async:

@inject IMentorPageContext PageContext
@implements IDisposable

protected override void OnInitialized()
{
    PageContext
        .SetPageName("Orders")
        // ── Share current UI state with the AI ───────────────────────────────
        .Set("ActiveFilter",  "Pending")
        .Set("VisibleRows",   _orders.Count)
        .Set("SelectedOrder", _selectedOrder)

        // ── 1. Simple action — no parameter ─────────────────────────────────
        .RegisterUIAction(
            "open_create_modal",
            "Opens the new order creation dialog",
            _ => OpenCreateModal())

        // ── 2. Typed parameter — no manual casting required ──────────────────
        .RegisterUIAction<int>(
            "highlight_row",
            "Highlights an order row by its ID",
            id => HighlightRow(id),          // id is int, not object?
            parameterHint: "integer: order ID")

        // ── 3. Typed async — awaited before the AI continues ─────────────────
        .RegisterUIActionAsync<int>(
            "select_order",
            "Selects an order and loads its details panel",
            async id => {
                await InvokeAsync(() => { _selectedId = id; StateHasChanged(); });
            },
            parameterHint: "integer: order ID")

        // ── 4. Complex typed parameter (DTO deserialized from JSON) ──────────
        .RegisterUIActionAsync<OrderFormModel>(
            "prefill_form",
            "Pre-fills the order form with the provided data",
            async model => {
                await InvokeAsync(() => { _form = model; StateHasChanged(); });
            },
            parameterHint: "JSON: { customerId, items, notes }");

    // Signal the AI that the page is ready (required when HasUIActions = true)
    PageContext.SignalReady();
}

// Always clear in Dispose — removes context and actions from the AI's view
public void Dispose() => PageContext.Clear();

UI action overloads reference

Overload Parameter Execution Use when
RegisterUIAction(name, desc, Action<object?>) Raw object? (cast manually) Synchronous Simple no-param or legacy code
RegisterUIAction<TParam>(name, desc, Action<TParam>) Auto-deserialized from JSON Synchronous Typed param, sync handler
RegisterUIActionAsync(name, desc, Func<object?, Task>) Raw object? Async No-param async actions
RegisterUIActionAsync<TParam>(name, desc, Func<TParam, Task>) Auto-deserialized from JSON Async Typed param, async handler (recommended)
UnregisterUIAction(name) Remove a specific action dynamically (e.g. when a feature becomes unavailable)

Context data methods reference

Method Description
SetPageName(name) Sets the current page name injected into the system prompt
Set(key, value) Adds or updates a context entry (any serializable value)
Remove(key) Removes a single context entry without clearing everything
Clear() Removes all context data and all registered UI actions. Always call from Dispose()

Automatic parameter schema generation

For the two typed overloads (RegisterUIAction<TParam> and RegisterUIActionAsync<TParam>), the parameterHint argument is automatically generated from the type via reflection when you omit it. You only need to provide it if you want to override the generated text.

TParam Auto-generated parameterHint
int, long, short, byte "integer"
float, double, decimal "number"
bool "boolean"
string "string"
Guid "string (GUID)"
DateTime, DateTimeOffset "string (ISO 8601 date)"
Status (enum) "string (Active\|Inactive\|Pending)"
int? "integer?"
List<int> "array<integer>"
Dictionary<string,int> "object<string,integer>"
OrderFormModel (class) "{ customerId: integer, productName: string, quantity: integer }"

This means the AI sees a precise, field-by-field description of what to pass — even for complex DTOs — without any manual work:

// parameterHint omitted → auto-generated as "{ customerId: integer, productName: string, quantity: integer }"
.RegisterUIActionAsync<OrderFormModel>(
    "prefill_form",
    "Pre-fills the order form with the provided data",
    async model => {
        await InvokeAsync(() => { _form = model; StateHasChanged(); });
    })

// Override only when the auto-generated hint is not descriptive enough
.RegisterUIAction<int>(
    "highlight_row",
    "Highlights an order row by its ID",
    id => HighlightRow(id),
    parameterHint: "integer: order ID")   // ← manual override for extra clarity

Async handlers are awaited — the AI waits for the handler to complete before generating its next response. This makes multi-action chains reliable: the AI can call select_order(42) and then highlight_row(42), knowing each step is done before it proceeds.

The AI automatically receives current page context (URL, page name, registered data) injected into its system prompt on every call — with no extra configuration required.


Agent Skills

Agent Skills are domain knowledge + execution packages with two complementary roles:

Role What it provides When used
Knowledge Instructions, policy, rules, workflows (markdown) Loaded on demand via load_skill
Tools [Description]/[MentorAction] methods on the skill class Always available as L1 tools

Rather than dumping all domain knowledge into the system prompt, MentorAgent uses progressive disclosure:

  1. The skill catalogue (name + one-line description, ~100 tokens per skill) is always injected into the system prompt.
  2. When the AI needs expertise, it calls load_skill("skill-name") to receive the full instructions — then uses that knowledge to call the right methods with the right parameters.
  3. Attached resource files (policy docs, FAQ pages, templates) are accessible via read_skill_resource.

10 registered skills cost roughly 1,000 tokens in the system prompt instead of the 50,000+ tokens you'd need to inline everything upfront.

Setup

builder.Services.AddMentorAgent(options =>
{
    options.EnableSkills   = true;
    options.SkillsFolder   = "Skills"; // relative to content root, or absolute path
    options.ScanAssemblies = [typeof(Program).Assembly];
});

Dual-role skill class (knowledge + tools)

This is the most powerful pattern. The class provides both the domain instructions and the executable tools:

// Register in DI — required when the class has methods
builder.Services.AddScoped<ExpenseReportSkill>();
[MentorSkill(
    Name             = "expense-report",
    Description      = "Handles expense report filing, validation, and policy checks",
    InstructionsFile = "Skills/expense-report/SKILL.md")]   // or use Instructions = "..."
public class ExpenseReportSkill
{
    private readonly ExpenseRepository _repo;
    public ExpenseReportSkill(ExpenseRepository repo) => _repo = repo;

    // ── Methods become L1 tools — always registered on the coordinator ──────

    [Description("Submits a new expense report for the current user")]
    public async Task<string> SubmitExpenseReportAsync(
        string category, decimal amount, string notes) { ... }

    [MentorAction(
        Description          = "Approves a pending expense report",
        RequiresConfirmation = true,
        RequiredRoles        = ["Manager"])]
    public async Task<bool> ApproveExpenseReportAsync(int reportId) { ... }

    [Description("Returns the reimbursement policy for a given expense category")]
    public string GetPolicyForCategory(string category) { ... }
}

The flow when a user asks "Submit a €47 expense for today's business lunch":

  1. AI sees submit_expense_report_async in its tool list
  2. Before calling it, AI calls load_skill("expense-report") — learns that meals have a €50 limit, require a category and description
  3. AI calls submit_expense_report_async("Meals", 47, "Business lunch") with policy-correct parameters

Without the skill, the AI guesses. With the skill, the AI knows the rules before it acts.

Knowledge-only skill (no methods)

Use this when the skill provides guidance but all execution is handled by existing service tools:

// No DI registration needed — no methods, no instance required
[MentorSkill(
    Name        = "refund-policy",
    Description = "Return and refund rules for the online store",
    Instructions = """
        ## Refund Policy
        - Items can be returned within 30 days of purchase.
        - Digital products are non-refundable once downloaded.
        - Damaged items: customer submits a photo via the support ticket tool.
        - Refunds are processed within 5–7 business days to the original payment method.
        """)]
public class RefundPolicySkill { }

File-based skills (auto-discovery)

MentorAgent also discovers skills automatically from the filesystem — no class needed:

Skills/
  expense-report/
    SKILL.md          ← required: skill instructions in markdown
    policy.md         ← optional: resource accessible via read_skill_resource
    examples.md       ← optional: additional resource
  refund-policy/
    SKILL.md
  shipping-rules/
    SKILL.md
    carrier-list.txt

SKILL.md format — the first paragraph after the heading becomes the catalogue description:

# Expense Report Filing

Handles expense report submission, validation, approval workflow, and policy enforcement.

## Eligible Expenses
- Meals: up to €50/day domestic, €80/day international
- Travel: economy class only for flights under 4 hours
...

Class-based overrides file-based — if both define a skill with the same name, the class attribute wins.

How the AI sees skills

System prompt (catalogue only — always injected, ~100 tokens total):

## Available Skills
When a request requires specialised expertise in any area below,
call load_skill with the skill name to get full instructions before responding:
- **expense-report**: Handles expense report filing, validation, and policy checks
- **refund-policy**: Return and refund rules for the online store
- **shipping-rules**: Shipping carrier rules and delivery SLAs

Full instructions and resources are never in the prompt by default — only loaded when the AI decides it needs them.

[MentorSkill] parameters

Parameter Description
Name Unique skill name in kebab-case (e.g. "expense-report"). Key for load_skill
Description One-sentence description shown to the AI in the skill catalogue
InstructionsFile Path to a markdown file (absolute or relative to content root). Falls back to Skills/{Name}/SKILL.md
Instructions Inline markdown text. Takes precedence over InstructionsFile

Agent Skills configuration options

Option Type Default Description
EnableSkills bool false Enables skill discovery and the load_skill / read_skill_resource tools
SkillsFolder string "Skills" Folder to scan for file-based skills. Relative to content root or absolute

Contextual memory

Enable the Mentor's ability to remember preferences and past actions across sessions:

options.UseMemoryContext  = true;
options.MemoryContextCount = 10; // last N memories injected into the system prompt

By default, memories are stored in RAM (InMemoryMentorMemoryStore). For real persistence, register your own store before AddMentorAgent():

// Redis
builder.Services.AddSingleton<IMentorMemoryStore, RedisMemoryStore>();

// EF Core
builder.Services.AddScoped<IMentorMemoryStore, EfCoreMemoryStore>();

// MongoDB
builder.Services.AddSingleton<IMentorMemoryStore, MongoMemoryStore>();

builder.Services.AddMentorAgent(options => {
	options.UseMemoryContext = true;
	...
});

⚠️ User isolation: With authentication configured, each user has their own memory (keyed by ClaimTypes.NameIdentifier). Without authentication, all users share the "anonymous" key — suitable only for single-user apps or development.

How automatic memory works

When UseMemoryContext = true, MentorAgent captures durable facts reliably — without depending on the model proactively calling a tool (which weaker models do inconsistently, e.g. saving on "Mi chiamo Antonio" but not on "Ciao, mi chiamo Antonio").

  • Path A (a ChatClient is configured) — default: after each user message a dedicated, minimal LLM call (MemoryAutoCapture, on by default) extracts durable personal facts and stores them directly. Because it is a separate focused task, it is robust regardless of how the coordinator replies. On this path the now-redundant remember tool and its forceful prompt block are dropped (fewer tokens per call); forget stays. Set options.MemoryAutoCapture = false to opt out — one fewer model call per message, but facts are then saved less reliably via the remember tool.
  • Path B (a pre-built Agent, no ChatClient): auto-capture cannot run, so the Coordinator is instructed to call the remember tool itself (forceful prompt block).

Examples of what gets saved automatically:

User says Saved as
"My name is Antonio" user_name = Antonio
"I work in the sales team" user_team = sales team
"Always show me Pending orders first" preferred_filter = Pending

Verify in the logs: [MentorAgent:Memory] Auto-capture saved 1 fact(s): user_name.

At the start of each session, stored memories are injected into the prompt so the AI greets the user by name and honours preferences immediately. With MemoryRelevanceFiltering (requires EmbeddingGenerator) only the memories semantically relevant to the current message are injected — identity/preference facts always kept.

InMemoryMentorMemoryStore — production limitations

The default store has two hard limitations:

  • Data is lost on app restart — memories do not survive deployments.
  • Does not scale across multiple instances — in a load-balanced environment each server has its own isolated dictionary.

For any production deployment with more than one server instance, or where persistence across restarts is required, register a custom IMentorMemoryStore implementation.


RAG — Retrieval-Augmented Generation

RAG enriches every AI response with documents retrieved from your vector store. Before the LLM call, MentorAgent searches for the most relevant documents and injects them into the coordinator's system prompt — grounding the AI's answers in your actual data.

Setup

Step 1 — Register your RAG source before AddMentorAgent():

// Implement IMentorRagSource with your preferred vector DB
builder.Services.AddScoped<IMentorRagSource, MyVectorDbRagSource>();

// Examples:
// Azure AI Search
builder.Services.AddScoped<IMentorRagSource, AzureSearchRagSource>();

// Qdrant
builder.Services.AddScoped<IMentorRagSource, QdrantRagSource>();

// Any custom implementation
builder.Services.AddScoped<IMentorRagSource, MyCustomRagSource>();

Step 2 — Enable RAG in options:

builder.Services.AddMentorAgent(options =>
{
	options.UseRag                 = true;
	options.RagResultCount         = 5;    // number of documents to inject
	options.RagSystemPromptTemplate = "Use the following documents to answer:\n{documents}";
	options.ShowRagSources         = true; // show citation chips below AI messages
});

Implement IMentorRagSource

public class MyVectorDbRagSource : IMentorRagSource
{
	private readonly MyVectorDb _db;
	public MyVectorDbRagSource(MyVectorDb db) => _db = db;

	public async Task<IReadOnlyList<MentorRagResult>> SearchAsync(
		string query, int maxResults, CancellationToken ct = default)
	{
		var hits = await _db.SearchAsync(query, maxResults, ct);
		return hits.Select(h => new MentorRagResult(
			Content:   h.Text,
			SourceUrl: h.Url,
			Title:     h.Title,
			Score:     h.Score)).ToList();
	}
}

RAG source citations in the widget

When ShowRagSources = true, citation chips appear below each AI message:

[AI response]
──────────────────────
📄 Product Manual v2  ↗
📄 Support FAQ        ↗

The chips link to the original SourceUrl of each retrieved document. If more than 3 sources are returned, a "+N more" button collapses the rest.

Architecture note

RAG is applied only at the coordinator level — not on L2/L3 specialist agents. The coordinator performs the vector search once per user message; retrieved documents flow naturally into the conversation context passed to any delegated agent. This avoids redundant searches and keeps costs low.

RAG configuration options

Option Default Description
UseRag false Enables RAG. Requires a registered IMentorRagSource
RagResultCount 5 Number of documents retrieved per query
RagMinScore 2 Minimum relevance score a document must reach to be injected into the prompt. Documents below this threshold are discarded. Scale depends on the IMentorRagSource implementation: for keyword search, 2 ≈ two content matches or one title match; for vector/cosine similarity, typical values are 0.50.75. Set to 0 to disable filtering
RagSystemPromptTemplate "Use the following documents to answer:\n{documents}" Template injected into the system prompt. Use {documents} as placeholder
ShowRagSources false Show citation chips below AI messages in the widget

RAG is fully semantic: the vector search embeds every message and RagMinScore discards anything below the threshold, so a pure command like "update the price of X" simply retrieves nothing relevant and injects nothing — no keyword pre-gate needed.


MCP — Model Context Protocol

MentorAgent supports MCP in both directions: as a client (consuming tools from external MCP servers) and as a server (exposing [MentorAction] methods to any MCP-compatible client).

MCP Client — consuming external MCP servers

External MCP servers (filesystem, databases, APIs, custom tools) become additional Level-1 tools for the coordinator — indistinguishable from local [MentorAction] methods.

HTTP transport (remote server)
builder.Services.AddMentorAgent(options =>
{
	options.McpServers = [
		new MentorMcpServer
		{
			Name      = "MyApiTools",
			ServerUrl = "https://mcp.example.com/mcp",
		}
	];
});
stdio transport (local process)

Windows note: on Windows, npx, uvx, python, and other script launchers are .cmd or shell scripts that cannot be started directly by .NET's Process.Start. MentorAgent detects this automatically and wraps the command as cmd.exe /c <command> <args> — no changes needed in your configuration.

options.McpServers = [
	new MentorMcpServer
	{
		Name      = "filesystem",
		Command   = "npx",
		Arguments = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
		AllowedTools         = ["read_file", "list_directory"], // null = all tools
		RequiresConfirmation = true, // shows confirmation banner for every call
	},
	new MentorMcpServer
	{
		Name      = "database",
		Command   = "my-db-mcp-server",
		Arguments = ["--connection-string", "Server=..."],
		EnvironmentVariables = new Dictionary<string, string>
		{
			["DB_PASS"] = builder.Configuration["DbPassword"]!
		}
	}
];
MCP status badge in the widget

When ShowMcpStatus = true, a 🔌 N badge appears in the widget header showing the number of connected servers. Clicking it opens a dropdown with the connection status of each server.

options.ShowMcpStatus = true; // default: false — recommended for development

MentorMcpServer properties:

Property Description
Name Display name for the server
ServerUrl HTTP/SSE endpoint URL (HTTP transport)
Command Executable to launch (stdio transport, e.g. "npx")
Arguments Command-line arguments for the process
EnvironmentVariables Environment variables injected into the process
AllowedTools Whitelist of tool names to expose. null = expose all
RequiresConfirmation If true, every call to a tool from this server shows a confirmation banner

MCP Server — exposing MentorAgent as an MCP server

Every [MentorAction] method discovered by MentorAgent can be exposed as an MCP tool, allowing any MCP-compatible client (Claude Desktop, VS Code Copilot, custom agents) to call your application's business logic directly.

Step 1 — Enable the MCP server in options:

builder.Services.AddMentorAgent(options =>
{
	options.McpServerEnabled = true;
	options.AppName          = "MyApp"; // becomes the MCP server name
});

Step 2 — Map the MCP endpoint in Program.cs:

app.MapMentorAgentMcp();          // default path: /mcp
app.MapMentorAgentMcp("/my-mcp"); // custom path

MCP clients can now connect to https://yourapp/mcp and discover all [MentorAction] tools.

ℹ️ [MentorAction] classes registered as Scoped are fully supported by the MCP server — MentorAgent creates a dedicated DI scope internally to resolve them. For classes that hold stateful resources (e.g. open DbContext), prefer Transient or Singleton registration.


A2A — Agent-to-Agent

MentorAgent supports the Agent-to-Agent (A2A) protocol in both directions: as a consumer (calling remote A2A agents) and as a server (exposing MentorAgent as a federatable agent).

A2A Consumer — calling remote A2A agents

Remote A2A agents participate in the Handoff workflow exactly like local [MentorAgent] classes. The coordinator can hand off to them; they return results to the coordinator when done. This lets you federate specialized agents deployed as separate services.

builder.Services.AddMentorAgent(options =>
{
	options.RemoteAgents = [
		new MentorRemoteAgent
		{
			Name         = "InventoryAgent",
			Description  = "Manages warehouse stock and inventory levels",
			AgentCardUrl = "https://inventory.example.com",  // base URL only — SDK auto-appends /.well-known/agent-card.json
		},
		new MentorRemoteAgent
		{
			Name         = "BillingAgent",
			Description  = "Handles invoicing and payment processing",
			AgentCardUrl = "https://billing.example.com",    // base URL only
			Headers      = new Dictionary<string, string>
			{
				["Authorization"] = $"Bearer {builder.Configuration["BillingApiKey"]}"
			}
		}
	];
});

MentorAgent automatically resolves each agent's Agent Card (/.well-known/agent-card.json) at startup, establishes an A2A connection, and adds the agent to the Handoff graph. The coordinator's system prompt is automatically enriched with each remote agent's name and description, so the AI knows when to delegate. The coordinator can then delegate requests to remote agents using natural language — no extra configuration needed.

MentorRemoteAgent properties:

Property Description
Name Display name used in the Handoff graph and in the coordinator's system prompt
Description Human-readable description of what this agent does. Injected into the coordinator's system prompt so the AI knows when to route to it
AgentCardUrl Base URL of the remote agent (e.g. https://inventory.example.com). The SDK automatically appends /.well-known/agent-card.json — do NOT include the path
Headers Optional custom HTTP headers (auth tokens, API keys, etc.) injected into all A2A requests to this agent
RequiredRoles Optional ASP.NET Core roles required to use this agent. ⚠️ Per-agent role enforcement is not yet fully implemented

A2A Server — exposing MentorAgent as an A2A agent

MentorAgent can expose itself as a fully compliant A2A agent, discoverable and callable by any A2A-compatible orchestrator.

Step 1 — Enable the A2A server in options:

builder.Services.AddMentorAgent(options =>
{
	options.A2AServerEnabled = true;
	options.AppName          = "MyApp";
	options.AppDescription   = "My Blazor AI assistant for order management";
	options.AgentCard        = new AgentCardInfo { Version = "2.0.0" };
});

Step 2 — Map the A2A endpoints in Program.cs:

app.MapMentorAgentA2A();          // default path: /a2a
app.MapMentorAgentA2A("/agent");  // custom path

This registers two endpoints:

  • GET /.well-known/agent-card.jsonAgent Card describing the agent's name, description, version, and supported interfaces
  • POST /a2aA2A task handler that receives messages, routes them through MentorOrchestrator, and streams back the response

Remote orchestrators can now discover this agent via https://yourapp/.well-known/agent-card.json and send A2A tasks to it.

⚠️ A2AServerUrl is required when other A2A instances will call this agent as a remote. Set it to the full public URL of your A2A endpoint (e.g. http://localhost:5001/a2a). Without it, the Agent Card's SupportedInterfaces contains only a relative path (/a2a), which causes a UriFormatException in remote consumers when they try to resolve the endpoint.

options.A2AServerUrl = "https://myapp.example.com/a2a";

AgentCardInfo properties:

Property Default Description
Version "1.0.0" Agent version string exposed in the Agent Card
IconUrl null URL to the agent's icon image
Provider null Provider or organization name
DocumentationUrl null URL to the agent's documentation
Tags null Additional tags describing capabilities — included in the Agent Card for discovery

How streaming works: Each incoming A2A task is processed by MentorAgentA2AHandler, which creates a dedicated DI scope, runs MentorOrchestrator.SendMessageAsync, collects streaming chunks, and returns the complete response via TaskUpdater.CompleteAsync.


Persistent conversation history

By default, conversation history lives in memory and is lost on app restart.

// CosmosDB
options.ChatHistoryProvider = new CosmosChatHistoryProvider(cosmosClient, "my-db", "conversations");

// Custom (implement ChatHistoryProvider from Microsoft Agent Framework)
options.ChatHistoryProvider = new MyRedisChatHistoryProvider(redisConnection);

See the Agent Framework documentation for available providers.


Built-in AI tools

MentorAgent automatically registers internal tools on the Coordinator. The developer does not declare or register them — they activate based on configuration.

Always-active tools

Tool When it is used Notes
navigate_to User asks to go to a page, or an agent needs to navigate before executing UI actions Discovers pages from [MentorPage] at startup; waits for SignalReady() if HasUIActions = true
route_to_specialist Coordinator delegates a request to a Level-2 agent or remote A2A agent Auto-registered when at least one [MentorAgent] or MentorRemoteAgent is configured. The coordinator calls it with the target agent's name — the Handoff Workflow routes execution accordingly
{action_name} AI invokes a UI action registered via PageContext.RegisterUIAction*() One tool per action — each has its own name, description, and parameter schema. Injected dynamically per-call by UIActionsMiddleware. Only visible when the page is mounted. (Path A only)
invoke_ui_action Generic UI action dispatcher (legacy) Used only on Path B (pre-built AIAgent). On Path A (ChatClient), each action is its own named tool — this dispatcher is not added.

Conditional tools (activated by options)

Tool Activated by Notes
remember UseMemoryContext = true AI saves a user preference or fact silently
forget_all UseMemoryContext = true User explicitly asks to reset their memory
load_skill EnableSkills = true Returns full markdown instructions for the requested skill
read_skill_resource EnableSkills = true + at least one skill has resource files Returns a specific resource file attached to a skill

Streaming responses

All AI responses are streamed token by token — no waiting for the full response. This is handled automatically by the Orchestrator via RunStreamingAsync. The developer does not need to configure anything.

Response chunks flow through IMentorStateService.OnStreamingChunkChatWidget → UI thread-safe update via InvokeAsync(StateHasChanged). When voice output is enabled, the full response is read aloud after streaming completes.

Stop button

While the AI is processing, a ■ Stop button appears above the chat input. Clicking it cancels the current request immediately — the CancellationToken propagated through the entire agent pipeline (RAG, LLM call, tool execution) is cancelled, and any partial response already streamed is kept in the chat.

This works for all request types: standard messages, team deliberations (L3), and post-confirmation responses.


Blazor Server vs Blazor WASM

MentorAgent registers its core services as Scoped, which behaves differently depending on the hosting model:

Service Blazor Web App (Server, Auto) Blazor Hybrid (MAUI)
MentorOrchestrator One per circuit One per app instance
IMentorSessionManager One per circuit One per app instance
IMentorPageContext One per circuit One per app instance
MentorMemoryService One per circuit One per app instance
IMentorMemoryStore Singleton (shared) Singleton (shared)
MentorDiscoveryService Singleton (shared) Singleton (shared)
MentorRateLimiter Singleton (shared) Singleton (shared)

Supported render modes

Template / Render mode Support Notes
Blazor Web App — Server Default. AddMentorAgent() only, no extra packages.
Blazor Web App — Auto Option A: @rendermode="InteractiveServer" on <ChatWidget /> (simplest). Option B: add MentorAgent.Server + MentorAgent.Blazor for full WASM support.
Blazor Web App — WebAssembly Via MentorAgent.Server (server project) + MentorAgent.Blazor (client project).
Blazor WASM Standalone Via MentorAgent.Server (API backend) + MentorAgent.Blazor (WASM project).
Blazor Hybrid (MAUI) Fully supported with MentorAgent alone.
React / Vue / Angular / mobile Via MentorAgent.Server — connect using SignalR or SSE.

The simplest setup. ChatWidget and MentorOrchestrator run in the same server process, communicating via in-memory C# events. No extra packages needed.

// Program.cs
builder.Services.AddMentorAgent(options => { ... });
@* MainLayout.razor *@
@using MentorAgent.Abstractions.Components
<ChatWidget />

✅ Blazor Web App — Auto render mode (simplest WASM option)

In Blazor Auto, the easiest approach is to pin <ChatWidget /> to InteractiveServer — it stays server-side while the rest of the app can be WASM. No new packages needed.

@* MainLayout.razor — server project *@
@using MentorAgent.Abstractions.Components

<ChatWidget @rendermode="InteractiveServer" />
// Program.cs — server project only
builder.Services.AddMentorAgent(options => { ... });

⚠️ Never call AddMentorAgent() in the client (WASM) project — only in the server project.


✅ Blazor WASM / Blazor Auto (full WASM) — via MentorAgent.Server + MentorAgent.Blazor

For fully client-side WASM rendering or non-Blazor frontends, install the companion packages:

# Server project — MentorAgent is included automatically as a transitive dependency
dotnet add package MentorAgent.Server --prerelease

# Client project (WASM)
dotnet add package MentorAgent.Blazor --prerelease
// Server/Program.cs
builder.Services.AddMentorAgent(options => { ... });
builder.Services.AddMentorAgentServer();
app.MapMentorAgentServer();   // /mentor-hub + /mentor/chat
// Client/Program.cs
builder.Services.AddMentorAgentBlazor(options =>
{
    options.HubUrl   = "/mentor-hub";
    options.BotName  = "My Assistant";
    options.Language = MentorLanguage.English;
});
@* Client Razor page — identical to Blazor Server *@
@using MentorAgent.Abstractions.Components
<ChatWidget />

⚠️ Two WASM-only setup steps not required in Blazor Server:

  1. CSS/JS links in index.html — the static index.html is served before the .NET runtime starts, so the widget cannot inject its own assets. Add <link href="_content/MentorAgent.Abstractions/css/MentorAgent.css" rel="stylesheet" /> and <script src="_content/MentorAgent.Abstractions/js/MentorAgent.js"></script> manually.
  2. CORS + absolute HubUrl when the WASM app and the server are on different origins (different ports). See the MentorAgent.Server and MentorAgent.Blazor READMEs.

How it works: MentorOrchestrator runs on the server; the WASM widget connects via SignalR. Credentials never reach the browser. All features (agents, RAG, MCP, A2A, memory, HITL) are fully supported.

See MentorAgent.Server and MentorAgent.Blazor for complete documentation.


✅ Non-Blazor frontends (React, Vue, Angular, MAUI, mobile)

Install MentorAgent.Server on your ASP.NET Core backend and connect any client via SignalR (@microsoft/signalr) or plain HTTP SSE (/mentor/chat).

See MentorAgent.Server for complete documentation and examples.


Session serialize and restore

IMentorSessionManager exposes two methods for saving and resuming the conversation state (e.g. across page reloads or server restarts):

@inject IMentorSessionManager SessionManager

// Serialize the current session to a JsonElement (e.g. save to localStorage or DB)
JsonElement? snapshot = await SessionManager.SerializeCurrentSessionAsync(agent);

// Restore a previously saved session
if (snapshot.HasValue)
    await SessionManager.RestoreSessionAsync(agent, snapshot.Value);

The widget's reset button (inside the chat header) calls ResetSessionAsync() and clears the message list — starting a brand new conversation.


Security

Input safety check

options.EnableSafetyCheck = true; // adds ~200-500ms per message

Before processing each message, the AI evaluates whether it is a prompt injection, jailbreak attempt, or sensitive data extraction. Works in any language automatically.

Rate limiting

options.RateLimitPerUser    = 20;  // max 20 messages...
options.RateLimitWindowSecs = 60;  // ...per minute, per user

⚠️ User identification: With ASP.NET Core authentication configured, each user is identified by their ClaimTypes.NameIdentifier claim and gets an independent counter. Without authentication, all sessions share the key "anonymous" — the limit applies globally across all browsers and users, not per individual. For true per-user rate limiting, configure ASP.NET Core authentication.

Role-based actions

[MentorAction(Description = "Approves a budget request", RequiredRoles = ["Finance", "Admin"])]
public async Task<bool> ApproveBudgetAsync(int requestId) { ... }

Roles are verified against the current user's ClaimTypes.Role claim via AuthenticationStateProvider.

Confirmation dialogs

[MentorAction(Description = "Deletes all orders for a customer", RequiresConfirmation = true)]
public async Task<bool> DeleteAllOrdersAsync(int customerId) { ... }

The widget shows a confirmation banner before executing the action. Disable globally with options.RequireConfirmation = false.


Widget customization

Themes

Value Description
MentorTheme.Default Light with blue accents (--mentor-primary: #2563EB). Suits most apps.
MentorTheme.Dark Dark background (#1E1E2E). Auto-adapts to dark-mode apps.
MentorTheme.Minimal No shadows, thin borders. Ideal for flat/clean designs.
MentorTheme.Custom No theme applied at all. You define every CSS variable manually in your own stylesheet — full control.

When using MentorTheme.Custom, override these CSS variables in your stylesheet:

:root {
    --bm-bg-base:       #ffffff;
    --bm-bg-panel:      rgba(255,255,255,0.97);
    --bm-bg-surface:    #f8fafc;
    --bm-bg-elevated:   #f1f5f9;
    --bm-bg-hover:      #e2e8f0;
    --bm-text-primary:  #0f172a;
    --bm-text-secondary:#475569;
    --bm-text-muted:    #94a3b8;
    --bm-accent:        #2563EB;   /* primary accent color */
    --bm-accent-dark:   #1d4ed8;
    --bm-accent-glow:   rgba(37,99,235,.35);
    --bm-border:        rgba(0,0,0,.1);
    --bm-border-accent: rgba(37,99,235,.3);
    --bm-shadow-panel:  0 8px 40px rgba(0,0,0,.15);
    --bm-shadow-fab:    0 8px 32px rgba(37,99,235,.4);
}

Mentorship level

Controls the AI's verbosity and proactivity:

Value Behaviour
MentorshipLevel.Minimal Executes silently. No unsolicited explanations or suggestions. Best for expert users.
MentorshipLevel.Standard Balanced. Full responses with contextual suggestions when relevant. (default)
MentorshipLevel.Proactive Guides the user: explains what it did and why, suggests next steps, warns of risks, proposes alternatives. Best for onboarding or less-experienced users.

Full example

options.Theme              = MentorTheme.Dark;
options.Position           = ChatPosition.BottomRight; // BottomLeft, TopRight, TopLeft, SideRight, SideLeft
options.PrimaryColor       = "#2563EB";                // overrides theme accent color
options.BotName            = "Aria";
options.AvatarUrl          = "/images/aria-avatar.png";
options.WelcomeMessage     = "Hi! I'm Aria, your assistant. How can I help?";
options.InputPlaceholder   = "Ask me anything...";
options.MentorshipLevel    = MentorshipLevel.Proactive;
options.EnableSuggestions   = true;   // proactive suggestion chips below the input
options.EnableActionFeedback = true;  // "Executing..." visual feedback during tool calls
options.EnableVoiceInput    = true;   // microphone — Chrome/Edge only, requires HTTPS in production
options.EnableVoiceOutput   = true;   // text-to-speech — all modern browsers

Built-in widget features (always active, no configuration needed)

Feature Description
Unread badge When the widget is closed and the AI responds, a numeric badge appears on the FAB button (capped at 9+)
Typing indicator Animated dots shown while the AI is processing and no streaming text has arrived yet
Action bar While a tool is executing, a pulsing bar shows the tool display name
Stop button A ■ Stop button appears above the input while the AI is processing. Clicking it cancels the current request immediately — the partial response (if any) is kept in the chat
Reset button Button in the widget header that clears the message list and starts a new AgentSession
Voice toggle When EnableVoiceOutput = true and the browser supports Speech Synthesis, a speaker toggle button appears in the header
MCP status badge When ShowMcpStatus = true, a 🔌 N badge in the header shows connected MCP servers. Click to see details
A2A status badge When ShowA2AStatus = true, a badge in the header shows configured remote A2A agents (name + URL). Click to expand the detail bar
RAG citations When ShowRagSources = true, source chips appear below AI messages linking to the retrieved documents

Token & cost optimization

MentorAgent minimizes the tokens sent on every request. Some optimizations are always on; two are opt-in.

Always on (no configuration):

  • Slim, cache-friendly system prompt — the static instructions are a stable prefix (so the provider can cache them), and volatile data (date/time, URL) is emitted last.
  • Token measurement — every model round-trip logs its usage, so you can verify the effect:
    [MentorAgent] Tokens — in: 740, out: 95, call total: 835 | session: 740+95=835 over 1 call(s)
    

Semantic tool filtering

Every tool you register is serialized as a JSON schema into each request — the biggest per-call cost when you have many tools. With tool filtering, only the tools semantically relevant to the user's latest message are sent; the AI still freely chooses among them.

Relevance is computed with an embedding model, so it is reliable and multilingual (an Italian message matches English tool names). It requires EmbeddingGenerator — if enabled without one, filtering is skipped and all tools are sent (with a warning); there is no keyword fallback.

options.EmbeddingGenerator = new AzureOpenAIClient(endpoint, credential)
    .GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator();

options.EnableToolFiltering = true;
options.ToolFilterMaxTools  = 12;    // max matched business tools (core tools always kept)
options.ToolFilterMinScore  = 0.35f; // cosine-similarity threshold (higher = stricter)

Core tools (navigation, memory, routing, teams, skills, UI actions) are always kept. When no tool is relevant (e.g. "hello"), only the core tools are sent. Tool embeddings are computed once and cached.

History compaction

As a conversation grows, its history is re-sent on every call. Compaction shrinks it intelligently instead of a hard cut. It uses the (experimental) Agent Framework compaction pipeline: collapse old tool results → keep the last N turns → hard token-budget backstop.

options.EnableCompaction         = true;
options.CompactionTokenThreshold = 4000;  // token budget that triggers compaction
options.CompactionMaxTurns       = 8;     // recent turns kept intact

Applies only to agents with in-memory history (Path A / ChatClient) — not to service-managed history (Foundry, Responses API with store).

Semantic RAG and relevance-ranked memory

Both inject context only when relevant, saving tokens — and both are fully semantic (embedding-based, no keyword heuristics):

  • RAG: the vector search + RagMinScore already ensure nothing irrelevant is injected on pure commands. See RAG configuration options.
  • Memory: with MemoryRelevanceFiltering (requires EmbeddingGenerator), only the memories most similar to the message are injected (identity/preference facts always kept). See Contextual memory.

Middleware & extensibility

Make the request pipeline robust and pluggable — all additive (defaults preserve current behaviour). Prefer the built-in toggles; drop to the custom hooks for full control.

// ── Built-in safety (no code) ─────────────────────────────────────────────────
options.EnableSafetyCheck       = true;   // LLM check on the USER MESSAGE (prompt-injection / jailbreak)
options.EnableOutputSafetyCheck = true;   // LLM check on the ASSISTANT REPLY (leaks system prompt / secrets / PII / harmful)

// ── Custom hooks (full control) ───────────────────────────────────────────────
// Custom input guardrail — REPLACES EnableSafetyCheck. Return true = safe.
options.InputGuardrail  = (message, ct) => Task.FromResult(IsSafe(message));
// Custom output guardrail — REPLACES EnableOutputSafetyCheck. Buffers the reply (no live streaming that turn).
options.OutputGuardrail = (reply, ct)   => Task.FromResult(IsSafeReply(reply));
// Transform a tool result before it goes back to the model (e.g. cap very long results to save tokens).
options.OnToolResult    = (toolName, result) => Truncate(result, maxChars: 2000);
// Map an exception to a friendly message (null → built-in mapping).
options.OnException     = ex => ex.Message.Contains("rate", StringComparison.OrdinalIgnoreCase)
    ? "The service is busy, please retry shortly." : null;
// Public extension point — insert your own DelegatingChatClient / AF middleware, outermost.
options.ConfigureChatClientPipeline = b => b.UseLogging();

Two layers: built-in togglesEnableSafetyCheck / EnableOutputSafetyCheck, LLM classifiers (one small model call each; require a ChatClient) — and custom hooks that replace or extend them. A custom InputGuardrail / OutputGuardrail takes precedence over the matching built-in. Output moderation (built-in or custom) buffers the reply for that turn, so no unmoderated text is ever streamed; leave both off to keep live token-by-token streaming.

Observability (OpenTelemetry)

Opt in with EnableObservability. MentorAgent then emits GenAI-convention traces/metrics for the chat client (LLM) calls, plus its own per-turn/tool spans and counters — all under the source/meter named by ObservabilitySourceName (default "MentorAgent"). Your app wires the exporter:

options.EnableObservability = true;
options.ObservabilityIncludeSensitiveData = builder.Environment.IsDevelopment(); // prompt/response text: dev only

// Host wiring (App Insights / Aspire / OTLP / console):
builder.Services.AddOpenTelemetry()
    .WithTracing(t => t.AddSource("MentorAgent").AddConsoleExporter())
    .WithMetrics(m => m.AddMeter("MentorAgent").AddConsoleExporter());

Token & cost dashboard

An admin-only view — never shown in the end-user widget — modelled on Azure OpenAI's monitoring page: a model selector, temporal charts (tokens / requests / latency over time), a per-model cost table, plus deflection and top actions. Usage is attributed per model — the cheap chat model, the strong routing model and the embedding model are tracked separately (tokens, requests, latency), so you see exactly where cost goes. Supply prices (MentorAgent ships none), then drop the component on a protected page:

options.ModelPricing = new Dictionary<string, ModelPrice>(StringComparer.OrdinalIgnoreCase)
{
    ["gpt-4.1"]                = new ModelPrice(InputPer1M: 2.00m, OutputPer1M: 8.00m),  // cheap chat
    ["o3"]                     = new ModelPrice(InputPer1M: 2.00m, OutputPer1M: 8.00m),  // strong routing
    ["text-embedding-3-small"] = new ModelPrice(InputPer1M: 0.02m, OutputPer1M: 0.00m), // embedding
};
@* Admin page — you own the authorization; the component reads IMentorMetrics in-process. *@
@attribute [Authorize(Roles = "Admin")]
@using MentorAgent.Abstractions.Components
<MentorDashboard Currency="$" />

Cost is shown only for models present in ModelPricing; otherwise tokens are shown with cost “n/d”. Key ModelPricing by the model id the dashboard reports — for Azure OpenAI that is your deployment name, not the base model name. Charts are inline SVG (no JS, no external libraries) so they render on Blazor Server and WASM under a strict CSP, and are theme-aware. Time-series buckets are hourly and kept for MetricsRetention (default 7 days). Headless clients read the same data from IMentorMetrics.GetSnapshot() (or, in MentorAgent.Server, the GET /mentor/admin/metrics endpoint) — the snapshot carries the per-model breakdown and series, so the WASM dashboard is identical.

The dashboard is localized like the rest of the UI (10 languages, English fallback). On Blazor Server it resolves MentorLocalizer from DI and follows options.Language automatically — nothing to pass. In WASM there is no MentorAgent DI, so set the language explicitly:

<MentorDashboard Snapshot="_snapshot" OnRefresh="LoadAsync" Currency="€" Language="MentorLanguage.Italian" />

Persistence (optional)

By default the metrics live only in RAM and reset when the process restarts. Register an IMentorMetricsStore before AddMentorAgent() to change that — same pluggable pattern as IMentorMemoryStore. Each implementation fills only the half of the contract it needs:

// (A) Local durability — persist a JSON/DB snapshot. The dashboard keeps reading live RAM,
//     re-seeded from the last save on startup. The library flushes on a timer + on shutdown.
builder.Services.AddSingleton<IMentorMetricsStore, FileMetricsStore>();   // your impl: LoadForSeed + Save

// (B) External source — the dashboard reads the aggregate the #8 OpenTelemetry export already
//     pushed to Prometheus / Azure Monitor (historical, multi-instance). SaveAsync stays a no-op
//     because OpenTelemetry is the writer; QueryAsync queries the backend on each read.
builder.Services.AddSingleton<IMentorMetricsStore, PrometheusMetricsStore>();   // your impl: QueryAsync

builder.Services.AddMentorAgent(options => { ... });

LoadForSeedAsync restores totals at startup, SaveAsync persists them (timer via MetricsPersistenceInterval, default 30s, + graceful shutdown), and QueryAsync lets an external source serve the authoritative aggregate — the admin endpoint returns await store.QueryAsync() ?? metrics.GetSnapshot(). The default NullMetricsStore keeps the RAM-only behaviour with zero overhead. Working FileMetricsStore, PrometheusMetricsStore and AzureMonitorMetricsStore implementations ship in the MentorAgentServer sample.


Model routing

Route each turn to a cheap model for simple messages and a strong model for complex ones — a real cost lever. ChatClient is the cheap default; set a strong client and pick a routing strategy. Routing is active only when StrongChatClient is set.

// options.ChatClient stays the cheap default (e.g. gpt-4o-mini)
options.StrongChatClient = new AzureOpenAIClient(endpoint, credential)
    .GetChatClient("gpt-4o").AsIChatClient();                     // strong (escalation)

options.RoutingStrategy = MentorRoutingStrategy.Semantic;         // pick a strategy (default: Custom)

Four strategies — all avoid naive keyword matching on user text:

Strategy How it decides Extra cost per turn Notes
Semantic Embeds the user message; escalates when cosine similarity to a "complex" exemplar ≥ RoutingThreshold (0.35) 1 embedding (~free) Multilingual; requires EmbeddingGenerator. Override RoutingComplexExemplars for your domain
Classifier A tiny LLM call labels the turn SIMPLE/COMPLEX 1 small LLM call Most precise. Uses the cheap client, or RoutingClassifierClient
Cascade Serves on cheap, judges completeness, re-runs on strong only if it fell short cheap + judge (+ strong on hard turns) Best quality/cost trade-off. Streaming pre-decides like Classifier
Custom Your own predicate — UseStrongModelAsync (async, whole conversation) or legacy UseStrongModel none The escape hatch
// Custom async router — full control, sees the whole conversation:
options.RoutingStrategy = MentorRoutingStrategy.Custom;
options.UseStrongModelAsync = async (messages, ct) => await MyClassifier.IsComplexAsync(messages, ct);

The decision uses the latest user message and applies to the whole turn; the chosen model is logged (… routing → strong/cheap model). Any routing failure falls back to the cheap model. Semantic without an EmbeddingGenerator, or Custom without a predicate, disables routing (cheap only) with a warning. The dashboard attributes tokens and cost per model, so cheap vs strong spend is broken out separately. Note: the routing decision's own auxiliary calls (Classifier's label, Cascade's cheap probe + completeness judge) are made below the token meter and are not counted.

Structured outputs

Ask the model for a strongly-typed result (its JSON schema is derived from your type) — ideal for auto-filling forms, extracting entities, or typed generative UI, outside the chat flow. Inject IMentorStructured:

public record ExtractedOrder(string Customer, string[] Products, decimal Total);

var order = await structured.GenerateAsync<ExtractedOrder>(
    prompt: userText,
    instructions: "Extract the order details from the text.");

Returns default when the output can't be parsed or no ChatClient is configured. Requires a provider with structured-output support (OpenAI / Azure OpenAI).

Rich responses (tables & lists)

The chat-facing companion to structured outputs: the widget renders the assistant's Markdown as real UI — tables for tabular/comparative data, bullet/numbered lists, plus fenced code and bold/italic. With EnableRichResponses on (the default), the coordinator is nudged to format data this way:

options.EnableRichResponses = true;   // default — set false for terse plain-text replies

Nothing to wire on the client — ask the assistant something tabular (e.g. "list the 5 cheapest products with price and stock") and the reply renders as a table that scrolls horizontally on narrow widgets. The renderer is XSS-safe by construction: every piece of model text is HTML-encoded before any tag is emitted, so the model supplies data, never markup. This is Level 1 of generative UI; richer interactive/typed UI blocks are on the roadmap (#14).

Evaluation & regression testing

MentorEvaluator is a thin, convenient wrapper over the Agent Framework's native evaluation framework: it runs each case through a ChatClientAgent via agent.EvaluateAsync(...), scores it with a native LocalEvaluator (deterministic checks) + the framework's token metering, and fails when a run exceeds a token budget or drops below a quality bar — so you can gate CI on prompt/token regressions. Inject MentorEvaluator:

var report = await evaluator.RunAsync(
    [
        new EvalCase("Hello", "A short friendly greeting.", MaxTokens: 300),
        new EvalCase("List 3 CRM benefits.", "Lists 3 clear benefits.", MaxTokens: 600),
    ],
    new MentorEvalOptions
    {
        SystemInstructions = mySystemPrompt,   // the prompt under test
        Judge = true, MinQuality = 0.6,        // built-in lightweight LLM-judge
        MaxTotalTokens = 4000,                 // token-regression gate
    });

report.ThrowIfFailed();   // fail the CI test on regression

The built-in path uses only already-referenced packages. For production-grade quality & safety, plug the framework's native evaluators — no reimplementation:

new MentorEvalOptions
{
    // Native deterministic checks applied to every case (Microsoft.Agents.AI).
    Checks = [ EvalChecks.KeywordCheck("benefit"), EvalChecks.ToolCalledCheck("get_orders") ],

    // Native LLM-judged evaluators — their pass/fail also gates the report:
    //  • FoundryEvals  → Azure AI Foundry (relevance, coherence, groundedness, violence/self-harm/…)
    //  • MEAI quality/safety evaluators (Microsoft.Extensions.AI.Evaluation.Quality/Safety)
    Evaluators = [ new FoundryEvals(projectClient, model, FoundryEvals.Relevance, FoundryEvals.Violence) ],
}

For full-pipeline token checks, drive the mentor and assert on IMentorMetrics.GetSnapshot().


All configuration options

Core

Property Type Default Description
AppName string (required) Application name injected into the system prompt
AppDescription string "" Domain description for richer AI context
Language MentorLanguage English Language for AI responses and widget UI (see supported values below)
MentorshipLevel MentorshipLevel Standard Proactivity level of the AI
ChatClient IChatClient? null AI provider via IChatClient (Azure OpenAI, OpenAI, Ollama…)
Agent AIAgent? null Pre-built AIAgent (Foundry, Anthropic…)
EmbeddingGenerator IEmbeddingGenerator<string, Embedding<float>>? null Optional embedding model — enables semantic tool filtering
ScanAssemblies Assembly[] (required) Assemblies to scan for agents, actions, and pages

Supported MentorLanguage values:

Value Language
MentorLanguage.English English
MentorLanguage.Italian Italian
MentorLanguage.French French
MentorLanguage.German German
MentorLanguage.Spanish Spanish
MentorLanguage.Portuguese Portuguese
MentorLanguage.Dutch Dutch
MentorLanguage.Polish Polish
MentorLanguage.Japanese Japanese
MentorLanguage.Chinese Chinese

Widget

Property Type Default Description
Theme MentorTheme Default Widget visual theme
Position ChatPosition BottomRight Widget position
PrimaryColor string? null Custom hex color
BotName string "Mentor AI" Bot name in widget header
AvatarUrl string? null Bot avatar image URL
WelcomeMessage string? null Initial welcome message
InputPlaceholder string "Type a message..." Input box placeholder
EnableSuggestions bool true Proactive suggestion chips
EnableActionFeedback bool true Visual feedback during tool calls
EnableVoiceInput bool false Microphone via browser Speech Recognition API
EnableVoiceOutput bool false Text-to-speech via browser Speech Synthesis API

Session & History

Property Type Default Description
MaxSessionMessages int 50 Max messages in session history
ChatHistoryProvider ChatHistoryProvider? null Persistent conversation history provider

Memory

Property Type Default Description
UseMemoryContext bool false Contextual memory across sessions
MemoryContextCount int 10 Number of recent memories in the prompt
MemoryRelevanceFiltering bool false Inject only the memories semantically relevant to the current message (embedding cosine; identity/preference facts always kept) instead of the last N — saves tokens. Requires EmbeddingGenerator; without it, falls back to last-N
MemoryAutoCapture bool true The reliable memory writer: a dedicated post-turn extraction saves durable user facts instead of relying on the model to call remember (weak models do this inconsistently). On Path A (a ChatClient is set) it becomes the only writer — the redundant remember tool + its prompt are dropped (saves tokens); on Path B it falls back to the remember tool. forget is always kept. Adds one small model call per user message; set false to opt out

Agent Skills

Property Type Default Description
EnableSkills bool false Enables skill discovery and the load_skill / read_skill_resource tools
SkillsFolder string "Skills" Folder to scan for file-based skills (SKILL.md). Relative to content root or absolute

RAG

Property Type Default Description
UseRag bool false Enable RAG. Requires a registered IMentorRagSource
RagResultCount int 5 Number of documents retrieved per query
RagMinScore float 2 Minimum relevance score to include a document. 0 = no filtering. Keyword search: integer-like (2 = two matches); vector/cosine: 0.50.75
RagSystemPromptTemplate string "Use the following documents to answer:\n{documents}" Prompt template. Use {documents} as placeholder
ShowRagSources bool false Show citation chips below AI messages in the widget

MCP

Property Type Default Description
McpServers MentorMcpServer[]? null External MCP servers to connect as L1 tools
McpServerEnabled bool false Expose MentorAgent as an MCP server. Also call app.MapMentorAgentMcp()
McpServerPath string "/mcp" Default path used as a reference — pass to MapMentorAgentMcp(path)
ShowMcpStatus bool false Show MCP server connection status badge in the widget header

A2A

Property Type Default Description
RemoteAgents MentorRemoteAgent[]? null Remote A2A agents to add to the Handoff workflow. Base URL only — SDK auto-appends /.well-known/agent-card.json
A2AServerEnabled bool false Expose MentorAgent as an A2A agent. Also call app.MapMentorAgentA2A()
A2AServerPath string "/a2a" Default path used as a reference — pass to MapMentorAgentA2A(path)
A2AServerUrl string? null Full public URL of this agent's A2A endpoint (e.g. http://localhost:5001/a2a). Written into the Agent Card SupportedInterfaces so remote consumers can resolve the absolute endpoint. Required when this instance is used as a remote agent by other MentorAgent instances
AgentCard AgentCardInfo? null Metadata for the A2A Agent Card (/.well-known/agent-card.json)
ShowA2AStatus bool false Show a badge in the widget header listing configured remote A2A agents. Click the badge to see agent names and URLs

Token & cost optimization

Property Type Default Description
EnableToolFiltering bool false Send only the tools semantically relevant to the message. Requires EmbeddingGenerator; without it, all tools are sent
ToolFilterMaxTools int 12 Max matched business tools to send (core tools always kept on top)
ToolFilterMinScore float 0.35 Minimum cosine similarity (0–1) for a tool to count as relevant. Higher = stricter
EnableCompaction bool false Compact long conversation history before each call. In-memory history only (Path A)
CompactionTokenThreshold int 4000 Token budget that triggers compaction and the truncation backstop
CompactionMaxTurns int 8 Recent user turns kept intact by the sliding-window step

EmbeddingGenerator (in the Core table) powers semantic tool filtering and semantic memory (MemoryRelevanceFiltering, in Memory). RAG relevance is handled by the vector search + RagMinScore (in RAG).

Middleware, observability & dashboard

Property Type Default Description
InputGuardrail Func<string,CancellationToken,Task<bool>>? null Custom input guardrail (true = safe). Replaces the built-in check; runs whenever set
OutputGuardrail Func<string,CancellationToken,Task<bool>>? null Moderate the completed reply (true = safe). When set, the reply is buffered and revealed after moderation (no live streaming that turn)
OnToolResult Func<string,object?,object?>? null Transform/redact a tool result before it returns to the model
OnException Func<Exception,string?>? null Map an exception to a user-facing message (null → built-in mapping)
ConfigureChatClientPipeline Func<ChatClientBuilder,ChatClientBuilder>? null Insert custom middleware into the Path A pipeline (outermost)
EnableObservability bool false Emit OpenTelemetry traces/metrics (GenAI conventions) + MentorAgent spans/counters
ObservabilityIncludeSensitiveData bool false Include prompt/response content in telemetry — Development only
ObservabilitySourceName string "MentorAgent" ActivitySource/Meter name; add via .AddSource(name).AddMeter(name)
ModelPricing IReadOnlyDictionary<string,ModelPrice>? null Per-model token prices for the dashboard cost estimate (none built in)
DashboardRole string "Admin" Role required for the admin metrics endpoint ("" = open, dev only)
MetricsPersistenceInterval TimeSpan 30s Flush cadence for a registered IMentorMetricsStore (durable dashboard); also flushes on shutdown
MetricsRetention TimeSpan 7d How far back the dashboard keeps per-model hourly time-series buckets (temporal charts)

Model routing & AI utilities

Property / service Type Default Description
StrongChatClient IChatClient? null Strong model to escalate to (ChatClient is the cheap default). Routing active only when set
RoutingStrategy MentorRoutingStrategy Custom Semantic / Classifier / Cascade / Custom — how the cheap↔strong decision is made
UseStrongModelAsync Func<IReadOnlyList<ChatMessage>,CancellationToken,Task<bool>>? null Custom: async, context-aware router (takes precedence over UseStrongModel)
UseStrongModel Func<string,bool>? null Custom: legacy sync predicate on the latest user message
RoutingComplexExemplars IReadOnlyList<string>? null Semantic: example "complex" turns (null → built-in multilingual set)
RoutingThreshold float 0.35 Semantic: cosine floor to escalate (higher = stricter)
RoutingClassifierClient IChatClient? null Classifier/Cascade: dedicated judge client (defaults to the cheap ChatClient)
IMentorStructured service GenerateAsync<T>(...) — typed/structured generation (#12)
MentorEvaluator service RunAsync(cases, options) — token/quality regression harness (#10)

Security & Limits

Property Type Default Description
EnableSafetyCheck bool false AI-based prompt injection detection
MaxMessageLength int 4000 Max message length in characters (0 = unlimited)
RateLimitPerUser int 0 Max messages per user per window (0 = disabled). ⚠️ Without authentication all sessions share "anonymous" — limit is global, not per-user
RateLimitWindowSecs int 60 Rate limiting window in seconds
RequireConfirmation bool true Global on/off for confirmation dialogs
IncludeWorkflowExceptionDetails bool false Include exception stack traces in agent responses. Never enable in production

Extensibility

Extension point How
Custom memory store Implement IMentorMemoryStore, register before AddMentorAgent()
Custom history provider Set options.ChatHistoryProvider
Custom AI provider Set options.ChatClient or options.Agent
Custom agent instructions Set Instructions on [MentorAgent] or [TeamMember]
Custom RAG source Implement IMentorRagSource, register before AddMentorAgent()
Custom MCP tools Add entries to options.McpServers (HTTP or stdio transport)
Custom remote agents Add entries to options.RemoteAgents (A2A protocol)
File-based skills Create Skills/{name}/SKILL.md in the project root with EnableSkills = true
Dual-role skill class Decorate with [MentorSkill] + add [Description]/[MentorAction] methods — knowledge + tools in one class. Register in DI when the class has methods
Knowledge-only skill Decorate with [MentorSkill] — no methods, no DI registration needed
Page UI actions (typed) Use RegisterUIAction<TParam> or RegisterUIActionAsync<TParam> for automatic JSON deserialization

Requirements

Requirement Version
.NET 10.0
Blazor Web App (Server or Auto render mode), or Hybrid (MAUI)
Microsoft.Agents.AI 1.6.1
Microsoft.Agents.AI.Workflows 1.6.1
Microsoft.Agents.AI.Hosting.A2A.AspNetCore 1.6.1-preview
Microsoft.Extensions.AI 10.6.0
ModelContextProtocol.AspNetCore 1.3.0

Blazor WebAssembly is supported via MentorAgent.Server + MentorAgent.Blazor. See Blazor Server vs Blazor WASM. ✅ Blazor Web App with Auto render mode is supported — either with @rendermode="InteractiveServer" on <ChatWidget /> (simplest) or via the full WASM setup with MentorAgent.Server + MentorAgent.Blazor.


License

MIT — see LICENSE for details.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on MentorAgent:

Package Downloads
MentorAgent.Server

Expose MentorAgent as a universal AI backend from any ASP.NET Core application. Adds a SignalR hub (/mentor-hub), SSE streaming endpoint (/mentor/chat), MCP server, and A2A agent — no Blazor required.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0-preview.3 34 7/24/2026
1.0.0-preview.2 65 6/22/2026
1.0.0-preview 71 6/22/2026

1.0.0-preview.3
- Token & cost dashboard reworked into an Azure-OpenAI-Monitoring-style operator view: usage is now attributed PER MODEL — the cheap chat model, the strong routing model and the embedding model are tracked separately (tokens, requests, latency, cost). Adds a model selector, temporal SVG charts (tokens / requests / latency over time, dependency-free and CSP-safe), and a per-model cost table. Configured models appear even before they serve traffic (e.g. a strong model no turn has escalated to yet). Hourly time-series with configurable MetricsRetention (default 7 days). Chat usage is attributed via response.ModelId; embeddings are metered too.
- Model routing is now strategy-based via MentorRoutingStrategy (Semantic embeddings / Classifier LLM SIMPLE-COMPLEX label / Cascade cheap-then-escalate / Custom async predicate) — no keyword matching on user text. Configure with StrongChatClient + RoutingStrategy (+ RoutingThreshold / RoutingComplexExemplars / RoutingClassifierClient / UseStrongModelAsync).
- Blazor components: event handlers in the shipped components silently compiled to LITERAL HTML attributes (with zero build warnings) because Microsoft.AspNetCore.Components.Web was not in Razor using-scope — so @onclick/@onchange/@bind never fired. This broke the dashboard model selector and Refresh, the WelcomePanel suggestion chips, the HITL confirmation banner buttons, and the MCP/A2A status badges. Fixed by adding a project-wide _Imports.razor. If you consume the components, upgrade to get working handlers.
- Fix: a dashboard summary value rendered the literal text ".ToString("N0")" instead of the formatted number.
- Refreshed the package icon (new MentorAgent "MA" logo).
- Token & cost optimization (Wave 1): per-call token counter/logging; slimmer, cache-friendly system prompt; opt-in semantic tool filtering (EnableToolFiltering + EmbeddingGenerator, embedding-based relevance) and history compaction (EnableCompaction).
- fully semantic: RAG relevance relies on the vector search + RagMinScore (no keyword question-gate — nothing is injected on pure commands); relevance-ranked memory (MemoryRelevanceFiltering) now ranks memories by embedding cosine similarity (requires EmbeddingGenerator; falls back to last-N), identity/preference facts always kept. Both save tokens with no keyword heuristics.
- Reliable memory (MemoryAutoCapture, on by default): a post-turn extraction saves durable user facts (name, role, team, preferences) with a dedicated minimal LLM call, so memory no longer depends on the model proactively calling the remember tool (which weaker models do inconsistently). On Path A it is the sole memory writer — the redundant remember tool and its forceful prompt block are dropped (fewer per-call tokens); on Path B (no ChatClient) it falls back to the remember tool. The forget tool is always available. Set MemoryAutoCapture=false to opt out (one extra small model call per user message).
- HITL confirmation message now spells out the concrete arguments the AI is about to pass , so the user can verify exactly what will change before approving. The text flows through every transport (in-process Blazor and the headless SignalR ConfirmationRequired event), the banner is responsive, and the full message is available as a tooltip.
- Fix: the system prompt no longer injects a bare "User: not authenticated" line for anonymous users. It made the model invent authorization rules and refuse tools that have no role requirement (e.g. "you must be an admin to change prices"). Role-gated tools remain enforced by the middleware.
- Middleware suite: built-in LLM safety checks for input (EnableSafetyCheck) and now output (EnableOutputSafetyCheck) — no keyword blacklists. Custom hooks remain and take precedence: InputGuardrail / OutputGuardrail (buffer-then-moderate), OnToolResult (transform a tool result before it reaches the model), OnException (+ built-in defaults), and ConfigureChatClientPipeline to plug your own DelegatingChatClient / Agent Framework middleware. All additive.
- Observability: opt-in EnableObservability emits OpenTelemetry GenAI-convention traces/metrics for the chat client (LLM) calls, plus MentorAgent per-turn/tool spans and counters, under ObservabilitySourceName ("MentorAgent"). Prompt/response content only when ObservabilityIncludeSensitiveData is set (Development).
- Token & cost dashboard: admin-only MentorDashboard Blazor component + IMentorMetrics snapshot (tokens, cost, deflection, top actions). Cost via developer-supplied ModelPricing (no built-in prices). Never shown in the end-user widget. Now optionally persistent via a pluggable IMentorMetricsStore (default RAM-only, zero overhead): register your own before AddMentorAgent() for local durability (LoadForSeed on startup + timed/shutdown Save, cadence via MetricsPersistenceInterval) or to read an external aggregate the #8 export already pushed to Prometheus/Azure Monitor (QueryAsync). Same pattern as IMentorMemoryStore. The MentorDashboard component was also refined: the top-actions list now shows relative share bars, the layout/styling is tightened for admin use, and every label is localized through MentorLocalizer (10 languages, English fallback) — automatic from options.Language on Blazor Server, via the new Language parameter in WASM.
- Model routing: route each turn to a cheap or strong model via StrongChatClient + UseStrongModel — a real cost lever, developer-configured.
- Structured outputs: IMentorStructured.GenerateAsync<T> returns a strongly-typed result (JSON schema derived from T) for auto-filled forms / typed generative UI.
- Eval / regression kit: MentorEvaluator now wraps the Agent Framework's native evaluation (agent.EvaluateAsync + LocalEvaluator/EvalChecks with native token metering) instead of a hand-rolled loop. Runs EvalCases, gates on token-budget/quality (report.ThrowIfFailed()), and exposes native Checks + Evaluators (FoundryEvals, MEAI quality/safety) for production-grade quality & safety scoring.
- Documentation improvements across the MentorAgent package family.