Questa pagina non è ancora tradotta in italiano: qui la trovi in inglese.
Production checklist
Settings to review before production: governance, safety check, rate limits, cost and routing, history, several instances, observability and outgoing data.
On this page
Use this page to go through the settings before real users reach your agent, and again after each upgrade of MentorAgent. Each item says what to check and where it is explained. The complete configuration at the end puts the options together.
If you have little time, check these five items first:
- every endpoint that you map requires authentication;
- users sign in, and every action that is not for everyone has
RequiredRoles; - every action that changes data asks for confirmation;
- the model key comes from a secret store or an environment variable;
RateLimitPerUserandMaxMessageLengthfit your use.
The roles and the confirmation boxes of your actions can be checked by automated tests on every build, without a model: see Test without a model.
Authentication and endpoints
Section titled “Authentication and endpoints”- Every endpoint you map requires authentication. The hub,
/mentor/chatand/a2arun turns on your model, and/mcpruns your actions directly. See Secure the endpoints. A Blazor Server app that maps none of them can skip this item. - Blazor Server has a signed-in user. Without authentication every user is anonymous and every action with
RequiredRolesis refused: see Blazor Server app. AnonymousIdentitystaysPerSessionon any application with more than one user.Sharedputs every anonymous user on one memory and one rate-limit count.- The admin views are protected. With
Novior.MentorAgent.Server,DashboardRoleguardsGET /mentor/admin/metrics. The cost dashboard component,MentorDashboard, checks nothing: put it on a page with[Authorize(Roles = "Admin")]. - CORS lists your real origins. The
/mcpendpoint has no Cross-Origin Resource Sharing (CORS) policy by default, which suits desktop and IDE clients. If a browser-based client calls it, define theMentorAgentMcppolicy with your origins and apply it withRequireCors: see Model Context Protocol (MCP) server.
Keys and configuration
Section titled “Keys and configuration”- Keys come from a secret store or environment variables. User secrets load only in the
Developmentenvironment. On Azure, Entra ID with a managed identity removes the key: see Model providers. - Public URLs are the production ones, such as
A2AServerUrl: see Agent-to-Agent (A2A) server.
Roles and confirmation
Section titled “Roles and confirmation”- Every action is declared. Check each action against Every declaration, on every channel. Declare
ReadOnly = trueon an action that only reads, andRequiresConfirmation = falseonly on a write that must run without asking. Mark a method that changes data with[MentorAction]: a method with only[Description]asks for nothing. GiveRequiredRolesto every action that only some users may run. WithoutRequiredRoles, every user can run the action. See Two checks, in this order. RequireConfirmationForWritesstaystrue. Withfalse, onlyRequiresConfirmation = trueasks.- Read the warning about writes. When MentorAgent first reads your classes, it logs a warning with two lists of writes that run without confirmation: the
[MentorAction]methods that opted out, and the methods with only[Description]that are not declared read-only. Treat each name as a decision. Confirmation by default explains when the warning appears. - Tools you cannot annotate are gated by name with
RequiresApproval: see Tools you cannot annotate. RequireConfirmationstaystrue.falseturns off the confirmation boxes of your actions, agents and gated tools. The only approvals that still appear are those for the MCP servers that the model provider calls directly (HostedMcpServers).IncludeWorkflowExceptionDetailsstaysfalse. Set totrue, it puts exception messages from specialists and teams in the reply the user reads.- Your methods still filter records per user. Roles decide who may call an action, not which records it returns. A rule per record or per company before a call runs is an action policy,
IMentorActionPolicy: see What stays in your code.
Make the agent call an action first
Section titled “Make the agent call an action first”Two options make the agent call an action before it says anything. Both are off by default, both need a ChatClient, and each adds one small classifier call to most turns. RequireActionForChangeRequests also needs EnableToolFiltering and an EmbeddingGenerator.
RequireLookupForRecordQuestionsmakes the agent read a value before it answers a question about your records. Without it, a question about a price can still be answered with an offer to look the value up. Declare your reads first, withReadOnly = trueorIsReadOnlyAction. The option can require only the actions that you declare as reads and that the user may run. Without tool filtering, the first call must be one of those reads, or a handoff to a specialist. With tool filtering, it must be one that the filter selected for the message.RequireActionForChangeRequestsmakes the agent call an action that needs confirmation before it says anything, when the message asks for it and gives every input. Without it, the model can answer "Stock updated" with no tool called: no box appears, and nothing changes. KeepRequireConfirmationon, because only actions that show a box are required. Every[MentorAction]that asks for confirmation and that the user may run can be required, the writes that ask by default included.
Limits and safety checks
Section titled “Limits and safety checks”MaxMessageLengthrefuses long messages before the model is called.RateLimitPerUseris off by default. It counts messages per signed-in user withinRateLimitWindowSecs.- The user id is the
NameIdentifierclaim or, if that claim is missing,sub,oidor the name. - MCP tool calls count too, in an allowance of their own.
- A caller with no such claim and no name is counted by remote address, when
IHttpContextAccessoris registered (AddMentorAgentServer()andAddMentorAgentA2A()register it). Otherwise all such callers share one allowance. - The limit does not bound anonymous chat users or unidentified A2A tasks: Secure the endpoints adds a limit on the endpoints.
- The user id is the
EnableSafetyCheckchecks each message with one classifier call, for prompt injection, jailbreaks and attempts to extract the system prompt or credentials. PointClassifierChatClientat a small, fast model.SafetyCheckFailuredecides what happens when a check gives no verdict, after a timeout or a provider error.Allow, the default, answers and logs a warning;Blockrefuses the message. ChooseBlockwhen a message the check could not screen must never reach the model;Allowkeeps the agent available when the classifier is down.EnableOutputSafetyCheckandRefuseOutOfScopeare further checks on the reply and on the topic. The output safety check holds the whole reply until it is checked, so the answer no longer streams.RefuseOutOfScoperefuses only a message that plainly has nothing to do with your application, and answers when unsure. Each refusal counts as a guardrail block in the dashboard. A guardrail is a check on a message or a reply, built in or written by you: seeInputGuardrail.OnExceptionmaps an exception to the message that the user reads. When you set it, the chat never shows the text of a tool's exception. It shows your message, or "The action could not be completed." if the hook returns none. The model receives your message too. If the hook returns none, the model receives the text of the exception and can repeat it in its reply. Return a message for every exception whose text must not reach the user.
Cost and model routing
Section titled “Cost and model routing”What each of these options reduces, what it costs in model calls, and how to measure your app: Cost and latency.
ClassifierChatClientruns MentorAgent's own short decisions: the safety checks, the off-topic check (RefuseOutOfScope), the facts to remember, and the checks behindRequireLookupForRecordQuestionsandRequireActionForChangeRequests. It defaults toChatClient. A small model makes them cheaper and faster.StrongChatClientwith aRoutingStrategysends only the hard questions to a stronger model. If the strategy lacks what it needs, MentorAgent logs a warning and every turn stays onChatClient. Each strategy needs something:Custom, the default, needs your router inUseStrongModelAsyncorUseStrongModel.Semanticneeds anEmbeddingGenerator, which turns text into numbers so MentorAgent can compare meanings.Classifierneeds nothing more. It makes one short call per turn to decide whether the turn needs the strong model. The call runs onRoutingClassifierClientor, if that is not set, onChatClient. The complete configuration below uses this strategy.
EnableToolFilteringsends only the relevant tools, and needs anEmbeddingGenerator. An action that changes data and asks for no confirmation can reach the model on any message, so the items on confirmation above matter here too: see Tool filtering and confirmation andEnableToolFiltering.EnableCompactionshrinks long conversations before each call. It changes what is sent to the model, never what is stored. It works withChatClientand a history kept by your application, and it is not installed withUseServiceManagedHistory.WarmUpAtStartupdoes the shared setup work, such as tool embeddings and, withChatClient, remote agent cards, once the host has started and is listening, so the first user does not wait for it. A failed warm-up never stops the app. An invalid configuration stops the app, because MentorAgent validates the options at start. A .NET MAUI app runs no hosted service, so there the option does nothing.ModelPricingturns the dashboard's token counts into costs. MentorAgent ships no prices. Enter them under the model id that the dashboard shows; on Azure OpenAI, that id is your deployment name. The counts live in memory unless you register anIMentorMetricsStorebeforeAddMentorAgent().
The providers and the embedding model are set up in Model providers.
Conversation history and memory
Section titled “Conversation history and memory”- The history is in memory by default, one per circuit or connection, and a restart loses it.
- To keep it, use
ChatHistoryProviderFactory. It builds one history provider per circuit, hub connection or app run. It receives theIServiceProviderof that circuit, connection or app run, so your history provider can store each user's history separately. SettingChatHistoryProviderinstead gives one instance to every user of the process. - Your history provider is not trimmed.
MaxSessionMessagesandChatReducerapply only to the built-in history. Give your history provider its own limit.MentorChatReducer, in theNovior.MentorAgent.Corenamespace, is the reducer that MentorAgent uses by default: it keeps every tool call together with its result and cuts whole turns. AChatReducerset next to your own history provider, or withUseServiceManagedHistory, is not used, and a warning says so. - The built-in history counts every message. Tool calls and their results count against
MaxSessionMessagesand stay in the history, so the model still sees the data that a tool read on the next turn. A session holds fewer turns for the same limit. SetChatReducerto use another reducer. - Memory is held in RAM too. With
UseMemoryContext, register your ownIMentorMemoryStorebeforeAddMentorAgent(). The built-in store loses everything on restart, and each server instance has its own. A signed-in user is keyed by theNameIdentifierclaim or, if that claim is missing, bysub,oidor the name. A key from a claim other thanNameIdentifiercarries the name of the claim, such assub:….ClearAsyncon the store is where you erase a user's data. - Your history provider keeps the transcript in the session, not in a field. One history provider serves a circuit or connection for its whole life. The widget's reset button, the hub's
ResetSessionandResetSessionAsyncstart a new conversation. A new conversation is a new session, not a new history provider. So a history provider that keeps the messages in a field carries the previous conversation into the new one. The same rule applies to the key of a database row: keep it in the session, for example as a conversation ID in theStateBag, not in a field of the history provider. - A saved conversation is the conversation in plain text.
SerializeSessionAsyncreturns it as it is, and the model trusts a restored one as history. Keep it on the server, or inProtectedLocalStorage, never as text that a user can read or edit.GET /mentor/sessionreturns a protected snapshot instead, bound to the signed-in user. To restore after a restart, or on another instance, persist and share the ASP.NET Core Data Protection key ring (external site), as you do for authentication cookies.
In a history provider of your own, keep the messages in the session's StateBag, as the built-in provider does. Agent Framework's ProviderSessionState<T> does the bookkeeping:
using Microsoft.Agents.AI; // ChatHistoryProvider, ProviderSessionState<T>
using Microsoft.Extensions.AI; // ChatMessage, AIJsonUtilities
public sealed class CaseHistoryProvider : ChatHistoryProvider
{
// The transcript lives in the session's StateBag, not in a field:
// a new conversation is a new session.
private readonly ProviderSessionState<List<ChatMessage>> _transcript =
new(_ => [], nameof(CaseHistoryProvider), AIJsonUtilities.DefaultOptions);
protected override ValueTask<IEnumerable<ChatMessage>>
ProvideChatHistoryAsync(
InvokingContext context, CancellationToken cancellationToken = default)
{
var history = _transcript.GetOrInitializeState(context.Session);
return ValueTask.FromResult<IEnumerable<ChatMessage>>([.. history]);
}
protected override ValueTask StoreChatHistoryAsync(
InvokedContext context, CancellationToken cancellationToken = default)
{
var history = _transcript.GetOrInitializeState(context.Session);
history.AddRange(context.RequestMessages);
history.AddRange(context.ResponseMessages ?? []);
_transcript.SaveState(context.Session, history);
return ValueTask.CompletedTask;
}
}Register it with the factory, so that each circuit and connection gets its own history provider:
options.ChatHistoryProviderFactory = _ => new CaseHistoryProvider();Observability
Section titled “Observability”EnableObservabilityemits OpenTelemetry traces and metrics. The coordinator's model calls on Path A follow the OpenTelemetry conventions for generative AI. MentorAgent traces no other model call: to trace the embedding generator, theClassifierChatClient, the agents under the coordinator or a prebuiltAgent, wrap the client withUseOpenTelemetry()before you pass it in. MentorAgent adds amentor.turnspan per turn, amentor.tool.{toolName}span per tool call, amentor.mcp.tool.{toolName}span per call to the MCP server, and four counters:mentoragent.turns,mentoragent.tool_calls,mentoragent.errorsandmentoragent.guardrail_blocks, which includes the refusals ofRefuseOutOfScope.- Your application exports them. Add the source and the meter named by
ObservabilitySourceName,MentorAgentby default, to your OpenTelemetry tracing and metrics. MentorAgent includes no OpenTelemetry exporter. ObservabilityIncludeSensitiveDatastaysfalseoutside development: it puts prompts and replies in the telemetry.- The logs. MentorAgent logs under categories that start with
Novior.MentorAgent.- At Debug level, the logs show what each session was built with and which model the model routing chose.
- Each MCP tool call writes one Information line, with a fingerprint of the caller and never the caller's ID.
- A reset of the conversation writes one Information line too.
- A refused role check names the user by a fingerprint (
caller#…), never by name. - A failing tool is logged by the type of its exception. The whole exception is logged at Debug.
To send the traces and metrics to an OpenTelemetry collector, add the OpenTelemetry packages for the host and for the OpenTelemetry Protocol (OTLP) exporter. The example is compiled with these versions:
dotnet add package OpenTelemetry.Extensions.Hosting --version 1.15.3
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol --version 1.15.3Turn the telemetry on in AddMentorAgent, then add the source and the meter to OpenTelemetry:
using Microsoft.Extensions.AI; // AsIChatClient
using Novior.MentorAgent.Extensions; // AddMentorAgent
using OpenAI; // OpenAIClient
using OpenTelemetry.Metrics; // AddMeter, AddOtlpExporter
using OpenTelemetry.Trace; // AddSource, AddOtlpExportervar openAI = new OpenAIClient(builder.Configuration["OpenAI:Key"]
?? throw new InvalidOperationException("Set OpenAI:Key in a secret store."));
builder.Services.AddMentorAgent(options =>
{
options.AppName = "Case management";
options.ChatClient = openAI.GetChatClient("gpt-4o-mini").AsIChatClient();
options.ScanAssemblies = [typeof(Program).Assembly];
options.EnableObservability = true;
// Prompts and replies in the spans: only on your own machine
options.ObservabilityIncludeSensitiveData =
builder.Environment.IsDevelopment();
});
// "MentorAgent" is the default of ObservabilitySourceName
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddSource("MentorAgent")
.AddOtlpExporter())
.WithMetrics(metrics => metrics
.AddMeter("MentorAgent")
.AddOtlpExporter());Where the data goes. The OTLP exporter sends to
http://localhost:4317unless theOTEL_EXPORTER_OTLP_ENDPOINTenvironment variable names another address. Use the exporter of your backend instead, if it has one.The tags of a tool span. Each
mentor.tool.{toolName}andmentor.mcp.tool.{toolName}span carries:mentoragent.tool, the tool name;mentoragent.tool.outcome, the last outcome of the call, such asexecuted,failed,deniedorrejected;mentoragent.tool.confirmation, such asnone,requested,confirmed,rejectedortimed-out;mentoragent.surface, the channel:in-process,hub,sse,a2aormcp.
The counter
mentoragent.tool_callscarries thementoragent.tooltag.On Path B you get the
mentor.*spans and thementoragent.*counters only. The model calls of your prebuilt agent are traced only if you instrument its chat client yourself.Several instances. Each instance exports its own data, and your backend adds it up. The dashboard's counts are per instance: see Run on several instances.
Run on several instances
Section titled “Run on several instances”MentorAgent keeps the state of a conversation in the memory of the process that serves it. Behind a load balancer, every request of a client must reach the instance that serves it:
- Turn on session affinity ("sticky sessions") in your load balancer, for example with an affinity cookie.
- Affinity covers the hub connection and the HTTP requests of the same client:
/mentor/approve,/mentor/canceland/mentor/session. Affinity on the SignalR connection alone is not enough. - Blazor Server already needs affinity for its circuits. A Blazor Server host that also maps the hub,
/mentor/chat, MCP or A2A needs it for those too. - No backplane is needed. MentorAgent sends each hub event only to the connection that asked for the turn, from the instance that holds it. A SignalR backplane, such as Redis or Azure SignalR Service, would not help: confirmations, Stop and sessions are found in the memory of the instance, not through SignalR groups.
Where each part of the state lives, and what it needs on several instances:
| State | Where it lives | On several instances |
|---|---|---|
| Conversation history | The built-in history is in the memory of the instance, one per circuit or hub connection. | Affinity keeps it. A SignalR reconnection is a new connection with an empty history, on any instance, unless the client restores its session snapshot. |
| Memory of facts | With UseMemoryContext, the built-in InMemoryMentorMemoryStore, one per process. | Register a shared IMentorMemoryStore before AddMentorAgent(). Otherwise a fact remembered on one instance is missing on the others. |
| Confirmations waiting for an answer, and Stop | The memory of the instance that runs the turn. | Affinity for /mentor/approve and /mentor/cancel. On another instance they answer 404, and the turn goes on waiting on its own instance. By default it waits until Stop or the end of the session: set ConfirmationTimeout, after which the confirmation counts as rejected. |
| Scope of a hub connection | A dependency injection scope that lives as long as the connection, on its instance. | Affinity for the connection. A new connection gets a new scope. |
| Page context | The circuit, or the hub connection, which receives it with UpdatePageContext. The Blazor WebAssembly client sends it before each message; a client of your own decides when. | It follows the connection. A client that sends it before each message gives a new connection the context from the next message on. |
| Session snapshot | The client keeps it, as GET /mentor/session returned it, protected with ASP.NET Core Data Protection. | It opens on any instance that shares the Data Protection keys and the application name. |
| Metrics of the dashboard | The memory of each instance. With an IMentorMetricsStore, each instance saves its own whole snapshot. | Counted per instance. On a shared store the last write wins: it is not a sum. Read the totals from your OpenTelemetry backend: see Observability. |
| Data Protection keys | The ASP.NET Core key ring. Windows and Azure App Service keep it by default; a container without a volume does not. | Persist one key ring where every instance reads it, with one application name, as in the example below. |
Some more things are held or counted per instance:
RateLimitPerUser: a user spread over N instances gets up to N times the limit.- The MCP server keeps each
Mcp-Session-Idin the memory of the process, and another instance answers that session with404. MCP clients need affinity too. MentorAgent always runs the MCP server with sessions, and has no option for the stateless mode of the MCP SDK. - The A2A server keeps its tasks in memory:
GetTaskandCancelTaskon another instance do not find them. Only the instance that runs a turn can stop it, so a sharedITaskStorewould not be enough. - The generated tour costs one model call per instance, and the caches of embeddings and A2A agent cards are per instance, like the connections to MCP servers and remote agents.
Each instance needs the same key ring for the snapshots, and a time limit for the confirmations whose answer can be lost on another instance:
using Microsoft.AspNetCore.DataProtection; // PersistKeysToFileSystem
using Microsoft.Extensions.AI; // AsIChatClient
using Novior.MentorAgent.Extensions; // AddMentorAgent
using OpenAI; // OpenAIClient// One key ring and one application name for every instance:
// a session snapshot saved on one instance opens on the others.
var keys = builder.Configuration["DataProtection:KeysPath"]
?? throw new InvalidOperationException("Set DataProtection:KeysPath.");
builder.Services.AddDataProtection()
.SetApplicationName("case-management")
.PersistKeysToFileSystem(new DirectoryInfo(keys));
var openAI = new OpenAIClient(builder.Configuration["OpenAI:Key"]
?? throw new InvalidOperationException("Set OpenAI:Key in a secret store."));
builder.Services.AddMentorAgent(options =>
{
options.AppName = "Case management";
options.ChatClient = openAI.GetChatClient("gpt-4o-mini").AsIChatClient();
options.ScanAssemblies = [typeof(Program).Assembly];
// An answer that reaches another instance gets 404:
// after five minutes the confirmation counts as rejected.
options.ConfirmationTimeout = TimeSpan.FromMinutes(5);
});A folder on a shared volume is the simplest place for the keys. ASP.NET Core can also keep them in Azure Blob Storage or Redis, and protect them at rest: see Configure ASP.NET Core Data Protection (external site).
Run in a container
Section titled “Run in a container”MentorAgent is a library inside your ASP.NET Core app, so the app goes in a container like any other. MentorAgent ships no image and no Dockerfile. This one is a common two-stage build for an app whose project is CaseManagement.csproj:
# Build and publish with the .NET SDK
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish CaseManagement.csproj -c Release -o /app
# Run on the ASP.NET Core runtime, as the non-root user of the image
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app .
RUN mkdir /keys && chown $APP_UID /keys
ENV DataProtection__KeysPath=/keys
USER $APP_UID
EXPOSE 8080
ENTRYPOINT ["dotnet", "CaseManagement.dll"]The model key comes from the environment, never from the image. Pass it when the container starts, for example
docker run -e OpenAI__Key=<your-key>, or from your platform's secret store. ASP.NET Core readsOpenAI__KeyasOpenAI:Key. User secrets are not loaded: the image runs in theProductionenvironment.The Data Protection keys need a volume. Without one, a new container starts with new keys, and the session snapshots saved before no longer open. The Dockerfile points
DataProtection:KeysPathof the example for several instances at/keys: mount a volume there, the same one for every instance, for exampledocker run -v case-keys:/keys.The app listens on port 8080. The .NET images set it, and run the app as a user without root rights. Put HTTPS on your proxy or ingress, and use the forwarded headers middleware (external site) behind it.
Files that MentorAgent reads must be in the image. The
SKILL.mdfiles of your skills are read from the content root,/apphere, and YAML agents from the application folder.dotnet publishcopies a Markdown or YAML file only if the project says so:<ItemGroup> <None Update="Skills\**\SKILL.md" CopyToPublishDirectory="PreserveNewest" /> </ItemGroup>Several containers need what several instances need: session affinity, a shared key ring and the other items of Run on several instances.
The tests of this documentation check the text of this Dockerfile: the .NET version, the user, the port and the key folder. They do not build or run it, and MentorAgent's release tests do not run the library in a container.
Test the rules on every build
Section titled “Test the rules on every build”Your roles and confirmation boxes can be checked by automated tests on every build, with a fake model: no key, no network, the same result every time. Test without a model shows the fake model, the app under test and the tests.
- What a fake model tests: which actions a user can run, which ones ask for confirmation, what runs after the user confirms or rejects, and what your methods return.
- What it does not test: whether your real model chooses the right action, or understands your users' words. Run a few conversations with the real model before each release: see Catch regressions with the real model.
Data that leaves your application
Section titled “Data that leaves your application”What your deployment sends to other services, for your privacy notice and your security review:
The model provider receives the conversation and its context:
- the user's messages and the replies;
- the system prompt, with the address and name of the current page, the signed-in user's name and roles, and the data the page hands over through
IMentorPageContext; - the results of your actions (the data your methods read, from your database too);
- retrieved documents and the user's memories, when you turn them on;
- the names, descriptions and required inputs of your actions and the descriptions of your pages, when you use
RequireLookupForRecordQuestionsorRequireActionForChangeRequests: the classifier model receives them.
With a local model, none of this leaves your network: see Ollama and other local models.
The remote systems you configure receive what is handed to them. The MCP servers in
McpServersreceive the tool calls, and the A2A agents inRemoteAgentsthe requests sent to them. The servers inHostedMcpServersare called by the provider, with the headers you set.Nothing goes to the authors of MentorAgent. The library sends no telemetry of its own. Traces and metrics go only to the exporter you configure, and carry the text of prompts and replies only with
ObservabilityIncludeSensitiveData.The widget requests nothing from a third party. The package ships its two fonts, Sora and JetBrains Mono, so the stylesheet loads no file from another origin, and the widget works offline.
Voice input goes through the browser. With
EnableVoiceInput, off by default, the widget uses the browser's speech recognition. In Chrome, for example, the browser sends the audio to its vendor's recognition service. Only the transcript reaches MentorAgent.
Content Security Policy
Section titled “Content Security Policy”With any option, the widget and <MentorDashboard /> work under a policy with default-src 'self' and without 'unsafe-inline'. They load their fonts and images from your own origin. The themes are stylesheets of the package, PrimaryColor is set by the widget's script, and the dashboard draws with classes and SVG.
A WebAssembly client has two requirements. They come from Blazor, not from MentorAgent:
script-src 'wasm-unsafe-eval', for the .NET runtime;- in a Blazor Web App, the inline import map that
<ImportMap />renders.
See the Content Security Policy guidance in the ASP.NET Core Blazor documentation (external site).
Complete configuration
Section titled “Complete configuration”The options from this list in one registration, next to the endpoint protection of Secure the endpoints:
using Novior.MentorAgent.Extensions; // AddMentorAgent
using Novior.MentorAgent.Options; // MentorRoutingStrategy and others
using Microsoft.Extensions.AI; // AsIChatClient
using OpenAI; // OpenAIClientvar openAI = new OpenAIClient(builder.Configuration["OpenAI:Key"]
?? throw new InvalidOperationException("Set OpenAI:Key in a secret store."));
builder.Services.AddMentorAgent(options =>
{
options.AppName = "Case management";
options.AppDescription = "Support cases: status, notes and closure";
options.ChatClient = openAI.GetChatClient("gpt-4o-mini").AsIChatClient();
options.ScanAssemblies = [typeof(Program).Assembly];
// Governance: the defaults, written out so nobody changes them by accident
options.RequireConfirmation = true;
options.RequireConfirmationForWrites = true;
options.IncludeWorkflowExceptionDetails = false;
// Limits, counted per signed-in user
options.MaxMessageLength = 2000;
options.RateLimitPerUser = 20;
options.RateLimitWindowSecs = 60;
// Safety checks, on a model smaller and faster than the main one
options.EnableSafetyCheck = true;
options.ClassifierChatClient =
openAI.GetChatClient("gpt-4.1-nano").AsIChatClient();
// No verdict, for example after a timeout: answer and log a warning
options.SafetyCheckFailure = MentorSafetyCheckFailure.Allow;
// Cost: a stronger model only for the hard questions, and shorter histories
options.StrongChatClient = openAI.GetChatClient("gpt-4o").AsIChatClient();
options.RoutingStrategy = MentorRoutingStrategy.Classifier;
options.EnableCompaction = true;
options.WarmUpAtStartup = true;
// OpenTelemetry: traces and metrics under the "MentorAgent" source and meter
options.EnableObservability = true;
// Prompts and replies stay out of telemetry
options.ObservabilityIncludeSensitiveData = false;
});Options for production
Section titled “Options for production”| Option | Why it matters in production |
|---|---|
RequireConfirmation, RequireConfirmationForWrites, IncludeWorkflowExceptionDetails | Defaults that keep the confirmation boxes on, make writes ask, and keep exception details out of replies |
MaxMessageLength, RateLimitPerUser, RateLimitWindowSecs | Bound what one user can send and spend |
EnableSafetyCheck, ClassifierChatClient, SafetyCheckFailure | Screen each message, on a fast model, with a decision for the case without a verdict |
StrongChatClient, RoutingStrategy, EnableCompaction, WarmUpAtStartup | Pay for the strong model only when needed, and send fewer tokens |
EnableObservability, ObservabilityIncludeSensitiveData | Traces and metrics, without the text of the conversations |
Every option is in Configuration options, and the limits under Security and limits.
Common errors
Section titled “Common errors”- The key is not found once deployed. User secrets are not loaded outside
Development. Set the key as an environment variable or in your secret store. - The strong model is never used, and the log says "routing disabled (cheap model only)". The strategy lacks what it needs:
UseStrongModelAsyncorUseStrongModelforCustom, anEmbeddingGeneratorforSemantic. - Users see each other's memories or history.
AnonymousIdentityisShared, or oneChatHistoryProviderobject serves every user. - A new conversation still knows the previous one. Your history provider keeps the transcript in a field: keep it in the session, as in Conversation history and memory.
- The app stops at startup with "Both ChatHistoryProvider and ChatHistoryProviderFactory are set." MentorAgent checks the options when the host starts. Keep
ChatHistoryProviderFactoryalone. - The safety check is skipped with a timeout warning. The classifier model is too slow: set a faster
ClassifierChatClientor raiseSafetyCheckTimeout. - Replies arrive all at once instead of streaming. A check on the reply is on:
EnableOutputSafetyCheckor anOutputGuardrailholds each reply until it is checked. - Behind a load balancer, Approve or Stop answers
404, and the confirmation box stays. The request reached another instance. Give the HTTP requests the same affinity as the hub connection, and setConfirmationTimeout: see Run on several instances. - A saved conversation does not open on another instance, and
/mentor/sessionanswers400. The instances do not share the Data Protection keys or the application name. - No traces arrive. The
MentorAgentsource and meter are not in your OpenTelemetry setup, orObservabilitySourceNamegives them another name.
Vedi anche
Sezione “Vedi anche”- Secure the endpoints — Protect the SignalR hub, Server-Sent Events, /mcp and /a2a endpoints with authentication and CORS before you expose them.
- Model providers — Connect your .NET AI agent to OpenAI, Azure OpenAI, Ollama and other local models, or to your own Agent Framework AIAgent, and choose models with tool calling.
- Ruoli, conferma e governance — Ruoli e approvazione human-in-the-loop per i tool di un agente AI in .NET: controlli nel codice, non nel prompt, su ogni canale, e cosa resta a te.
- Configuration options — Every MentorOptions property with its type, default and effect, grouped by area, plus the options of the WebAssembly client and the YAML loader.
