# MentorOptions class

> The MentorOptions class of Novior.MentorAgent.Options: Configuration options for AddMentorAgent().

- URL: https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions
- Verified with: MentorAgent 1.2.0
- Last updated: 2026-10-27
- Packages: Novior.MentorAgent

Configuration options for `AddMentorAgent()`.

## Definition

Namespace: `Novior.MentorAgent.Options`. Package: `Novior.MentorAgent`.

`public sealed class MentorOptions`

```shell
dotnet add package Novior.MentorAgent
```

## Constructors

`public MentorOptions()`

## Properties

### `A2AServerEnabled`

`public bool A2AServerEnabled { get; set; }`

Set to true to expose MentorAgent as an A2A agent. The agent lives in the Novior.MentorAgent.Server package: also call `builder.Services.AddMentorAgentA2A()` after `AddMentorAgent()` (`AddMentorAgentServer()` includes it) and `app.MapMentorAgentA2A()` in Program.cs to map the endpoints. Exposes: `/.well-known/agent-card.json` (Agent Card) + the A2A HTTP endpoint. Default: false

### `A2AServerPath`

`public string A2AServerPath { get; set; }`

Path of the A2A task endpoint, used by `app.MapMentorAgentA2A()` when no path is passed to it. The agent card stays at `/.well-known/agent-card.json`. When other agents call this one, set [`MentorOptions.A2AServerUrl`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#a2aserverurl) to the full public URL **including** this path. Default: "/a2a"

### `A2AServerUrl`

`public string? A2AServerUrl { get; set; }`

The public URL of this agent's A2A endpoint, e.g. "http://localhost:5001/a2a". This value is written into the agent card served at /.well-known/agent-card.json so that remote orchestrators know where to POST tasks. Required when other MentorAgent instances (or any A2A client) need to call this agent as a remote. If null, the card's interface carries only the path ([`MentorOptions.A2AServerPath`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#a2aserverpath)): a MentorAgent caller resolves it against the card's address, another A2A client may not. Example (second instance on port 5001):

```text title="Example"
options.A2AServerUrl = "http://localhost:5001/a2a";
```

### `ActionPolicyTimeout`

`public TimeSpan ActionPolicyTimeout { get; set; }`

How long each call to an `IMentorActionPolicy` may take (`AddMentorActionPolicy`). A policy that has not answered by then is no longer awaited, and its token is cancelled: the call it was asked about does not run (`PolicyFailed`, `policy-timeout`), and when MentorAgent decides what to offer, the action is left out. `TimeSpan.Zero` or any negative value means no limit; Stop still cancels the wait. Default: 5 seconds. MentorAgent does not read it from configuration: set it in code, for example `options.ActionPolicyTimeout = TimeSpan.FromSeconds(10)`; a host that binds its own section to these options writes `"00:00:10"`.

### `Agent`

`public AIAgent? Agent { get; set; }`

Pre-built AIAgent — use when your provider does NOT produce an IChatClient. Required for: AIProjectClient (Microsoft Foundry), AnthropicClient, AnthropicFoundryClient, and any provider whose SDK exposes .AsAIAgent() directly. MentorAgent will attach its tools (via AIContextProvider.AIContext.Tools), additional instructions (appended via AIContext.Instructions), and middleware via .AsBuilder().UseAIContextProviders(...).Use(...). ⚠️ IMPORTANT: ChatHistoryProvider cannot be added post-construction. Configure it directly in the agent you pass here. Two calls with one id in a response of your agent's model are not made unique (BUG-225): your agent runs the calls before MentorAgent's clients see them.

### `AgentCard`

`public AgentCardInfo? AgentCard { get; set; }`

Optional override for the auto-generated Agent Card metadata. If null, the card is built from AppName, AppDescription, and discovered tools.

### `AiDisclosureMessage`

`public string? AiDisclosureMessage { get; set; }`

The notice's text, shown as written in every language. If null, the localized default in [`MentorOptions.Language`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#language) ("You are talking to an AI assistant: its replies are generated automatically." in English).

### `AllowedImageTypes`

`public IReadOnlyList<string> AllowedImageTypes { get; set; }`

Allowed image MIME types (allow-list, per AF Agent Safety). Default: png, jpeg, gif, webp.

### `AnonymousIdentity`

`public MentorAnonymousIdentity AnonymousIdentity { get; set; }`

Who an **unauthenticated** visitor is, for memory and for the rate-limit counter. Default: [`MentorAnonymousIdentity.PerSession`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoranonymousidentity) — one key per circuit/connection, so two visitors of the same site never read each other's memories.

Set [`MentorAnonymousIdentity.Shared`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoranonymousidentity) in a **single-user** application (MAUI, a desktop tool) to get the previous behaviour, where every session shares the key `"anonymous"` and memory therefore survives a restart.

Ignored once a user is signed in: their `ClaimTypes.NameIdentifier` is the key, as before, else their `sub`, `oid` or name with the claim in the key (`sub:…`, `name:…`). A signed-in principal with none of them is keyed as an anonymous one.

### `AppDescription`

`public string AppDescription { get; set; }`

Application domain description. More detail = more contextual AI.

### `AppName`

`public required string AppName { get; set; }`

Host application name. Injected into the system prompt.

### `AutoApprovalRules`

`public IList<Func<ToolAutoApprovalRuleContext, ValueTask<bool>>> AutoApprovalRules { get; set; }`

Rules that auto-approve a tool call which would otherwise raise a confirmation banner, using the Agent Framework's own `ToolApprovalAgent` middleware. Evaluated in order; the first rule returning `true` approves the call. Only applies when [`MentorOptions.HitlMode`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#hitlmode) is [`MentorHitlMode.Native`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorhitlmode), which is the mode whose approvals travel as framework content rather than through MentorAgent's own banner.

Enabling this also enables the framework's **"don't ask again"** behaviour: when a user approves a call, a standing rule is recorded for the rest of that session and matching calls stop prompting.

**This is an approval gate, not an authorization gate.** Auto-approving a call does not bypass `[MentorAction(RequiredRoles = …)]` — the role check lives inside MentorAgent's own tool wrapper and still refuses a caller who lacks the role. One is a prompt the user sees, the other is who is allowed to act, and a rule here can only ever waive the first.

Usage:

```text title="Example"
options.HitlMode = MentorHitlMode.Native;
options.AutoApprovalRules =
[
    // A scheduled job has no one to answer a dialog: approve reads, never writes.
    ctx => ValueTask.FromResult(ctx.FunctionCallContent.Name.StartsWith("get_")),
];
```

Default: empty — every gated tool prompts, exactly as before.

### `AvatarUrl`

`public string? AvatarUrl { get; set; }`

Bot avatar image URL. If null, uses the built-in SVG avatar.

### `BotName`

`public string BotName { get; set; }`

Bot name displayed in the widget header. Default: "Mentor AI"

### `ChatClient`

`public IChatClient? ChatClient { get; set; }`

Custom AI client — supports any provider implementing IChatClient. The provider's SDK is a package the host adds: Azure.AI.OpenAI for Azure, and Microsoft.Extensions.AI.OpenAI for AsIChatClient() on the OpenAI clients (it brings OpenAI). Examples:

```text title="Example"
// Azure OpenAI — ChatCompletion
options.ChatClient = new AzureOpenAIClient(endpoint, credential)
    .GetChatClient("gpt-4o-mini").AsIChatClient();

// Azure OpenAI — Responses API (service-managed history)
// NOTE: GetResponsesClient() takes NO argument — the deployment name goes to AsIChatClient.
options.ChatClient = new AzureOpenAIClient(endpoint, credential)
    .GetResponsesClient().AsIChatClient("gpt-4o-mini");

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

// OpenAI direct — Responses API (same shape as Azure)
options.ChatClient = new OpenAIClient("sk-...")
    .GetResponsesClient().AsIChatClient("gpt-4o");

// Ollama — through its OpenAI-compatible endpoint, with the same package
options.ChatClient = new OpenAIClient(
        new ApiKeyCredential("ollama"),   // Ollama ignores it; the SDK requires one
        new OpenAIClientOptions { Endpoint = new Uri("http://localhost:11434/v1") })
    .GetChatClient("llama3.2").AsIChatClient();
```

### `ChatHistoryProvider`

`public ChatHistoryProvider? ChatHistoryProvider { get; set; }`

Custom provider for conversation history persistence. If null, uses InMemoryChatHistoryProvider (default — data lost on restart). For real persistence, register a custom provider BEFORE AddMentorAgent():

```text title="Example"
// CosmosDB
options.ChatHistoryProvider = new CosmosChatHistoryProvider(cosmosClient, "db", "container");

// Custom (implement ChatHistoryProvider)
options.ChatHistoryProvider = new MyRedisChatHistoryProvider(redisConn);
```

Ref: https://learn.microsoft.com/en-us/agent-framework/agents/conversations/storage

### `ChatHistoryProviderFactory`

`public Func<IServiceProvider, ChatHistoryProvider>? ChatHistoryProviderFactory { get; set; }`

Builds a [`MentorOptions.ChatHistoryProvider`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chathistoryprovider)**once per coordinator** — that is, once per circuit, per SignalR connection, per MAUI app instance. Prefer this to [`MentorOptions.ChatHistoryProvider`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chathistoryprovider) in any application with more than one user.

**Why it exists (BUG-055).**[`MentorOptions.ChatHistoryProvider`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chathistoryprovider) is a single object on options, which are a singleton — so one instance serves every user in the process. The default is the opposite: with both left null MentorAgent constructs a fresh `InMemoryChatHistoryProvider` inside each coordinator build, isolated by construction. Setting the instance property therefore *inverts* the isolation model, and the natural implementation — one class, one `List<ChatMessage>` — then replays every user's conversation to every other user. Driven live, a private code written by one authenticated principal came back verbatim to two others.

The factory receives the coordinator's `IServiceProvider`, so a provider can resolve whatever identifies the current user in your application and key its storage by it.

```text title="Example"
// In-memory, isolated because each scope gets its own instance:
options.ChatHistoryProviderFactory = _ => new InMemoryChatHistoryProvider();

// Shared backing store, partitioned by the caller:
options.ChatHistoryProviderFactory = sp =>
    new CosmosChatHistoryProvider(cosmos, "db", "conversations",
        partitionKey: sp.GetRequiredService<IUserContext>().UserId);
```

Setting this **and**[`MentorOptions.ChatHistoryProvider`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chathistoryprovider) throws when the host starts (the options are validated on start; without a generic host, the first time they are read): they answer the same question with opposite isolation semantics, and picking one silently would make an application's safety depend on which line was written last.

### `ChatReducer`

`public IChatReducer? ChatReducer { get; set; }`

Replaces the reducer that keeps the built-in in-memory history bounded. Default `null`: a [`MentorChatReducer`](https://docs.mentoragent.net/api/novior-mentoragent-core/mentorchatreducer) sized by [`MentorOptions.MaxSessionMessages`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#maxsessionmessages), which trims whole turns and keeps every tool call together with its result.

When set, it is used as it is and [`MentorOptions.MaxSessionMessages`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#maxsessionmessages) is not read. One instance serves every session, so it must not keep per-conversation state (the framework's reducers do not). It applies only to the built-in history: a [`MentorOptions.ChatHistoryProvider`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chathistoryprovider) / [`MentorOptions.ChatHistoryProviderFactory`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chathistoryproviderfactory) you supply is used as it is, and with [`MentorOptions.UseServiceManagedHistory`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#useservicemanagedhistory) there is no local history; both cases log a warning.

The reducer used up to 1.0.0-rc.14, which drops tool calls and results from the history:

```text title="Example"
#pragma warning disable MEAI001 // MessageCountingChatReducer is experimental
options.ChatReducer = new MessageCountingChatReducer(50);
#pragma warning restore MEAI001
```

### `ClassifierChatClient`

`public IChatClient? ClassifierChatClient { get; set; }`

Optional client for MentorAgent's own yes/no decisions — the input and output safety checks, the hosted-tool scope check and the post-turn fact extraction. Defaults to [`MentorOptions.ChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chatclient). With a pre-built [`MentorOptions.Agent`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#agent) (Path B) it is the only client MentorAgent can call on its own: without it the safety checks are skipped and memory falls back to the remember tool.

These calls send a few hundred tokens and read back one word, and they sit **in front of** the answer the user is waiting for: measured live on gpt-4.1, the safety check alone took 707–2091 ms, about half the time to the first character. A small, fast deployment (`gpt-4.1-mini` and the like) answers the same question in a fraction of that and at a fraction of the price, with no change in behaviour.

```text title="Example"
options.ClassifierChatClient = azure.GetChatClient("gpt-4.1-mini").AsIChatClient();
```

Its calls are metered like every other, so the dashboard shows what the classifier costs under its own model id.

### `CompactionMaxTurns`

`public int CompactionMaxTurns { get; set; }`

Maximum number of recent user turns kept by the sliding-window step of compaction. Only used when [`MentorOptions.EnableCompaction`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#enablecompaction) is true. Default: 8

### `CompactionTokenThreshold`

`public int CompactionTokenThreshold { get; set; }`

Token budget that triggers compaction (and the truncation backstop). Only used when [`MentorOptions.EnableCompaction`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#enablecompaction) is true. Default: 4000

### `ConfigureChatClientPipeline`

`public Func<ChatClientBuilder, ChatClientBuilder>? ConfigureChatClientPipeline { get; set; }`

Public extension point for custom middleware. Invoked while assembling the Path A ([`MentorOptions.ChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chatclient)) pipeline, letting you insert your own `DelegatingChatClient` or Agent Framework middleware via the builder (e.g. logging, caching, extra guardrails). Applied outermost, after MentorAgent's own middleware. `null` = none.

Built once per coordinator — one per scope, so one per circuit in Blazor Server and one per hub connection: a semaphore, a concurrency limit or a counter kept in it does not limit the process. Keep process-wide state in a singleton you inject.

Only the coordinator's own model calls go through it. MentorAgent's auxiliary calls (the request classifier behind the out-of-scope refusal and the lookup and action checks, the safety checks, the hosted-tool domain gate, the memory extraction) use [`MentorOptions.ClassifierChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#classifierchatclient) ?? [`MentorOptions.ChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chatclient), and the agents under the coordinator (the handoff router, `[MentorAgent]` specialists, `[MentorTeam]` members) use [`MentorOptions.ChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chatclient); both are wrapped only in MentorAgent's token metering.

### `ConfirmationMessage`

`public Func<MentorConfirmationContext, string?>? ConfirmationMessage { get; set; }`

Optional function that writes the text of the confirmation banner, for example in the application's own words or language. It gets the action's name and description, the arguments as the banner shows them — in the order of the method's parameters, cleaned of control characters and cut at 200 characters — the configured [`MentorOptions.Language`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#language), the text MentorAgent would show, and the services of the conversation's scope (an `IStringLocalizer`, say).

```text title="Example"
options.ConfirmationMessage = c => c.ToolName == "cancel_order"
    ? $"Cancel order {c.Arguments.First(a => a.Name == "orderId").Value}?"
    : null;
```

The banner's title stays the localized "Confirmation required". When the function returns `null` or white space, or throws, the default text is shown, with a warning for the last two: a banner never fails to appear because of it. The text reaches the widget and the SignalR clients as it is; for an MCP tool the server's name can still follow it. `null` (default) = the action's description followed by its arguments.

### `ConfirmationTimeout`

`public TimeSpan ConfirmationTimeout { get; set; }`

How long a confirmation waits for its answer, from when the banner is shown. Nobody has answered by then: the confirmation counts as rejected, the action does not run (`Rejected`, `confirmation-timeout`), and the model is told nobody confirmed it in time. An answer that comes later is ignored, as one after Stop is: `POST /mentor/approve` answers 404. The banner goes when the turn ends. The same for a confirmation an `IMentorActionPolicy` asks for, in Blocking and in Native mode, for specialists and team members too.

`TimeSpan.Zero` or any negative value (the default) means no limit: the confirmation waits until it is answered, until Stop, or until the session ends (the circuit, the hub connection or the request closes). MentorAgent does not read it from configuration: set it in code, for example `options.ConfirmationTimeout = TimeSpan.FromMinutes(5)`; a host that binds its own section to these options writes `"00:05:00"`.

### `DashboardRole`

`public string DashboardRole { get; set; }`

Role required by the admin metrics endpoint, `GET /mentor/admin/metrics` (Novior.MentorAgent.Server); "" leaves it open (development only). The `MentorDashboard` component does not read it: protect the page that hosts the component yourself, for example with `[Authorize(Roles = "Admin")]`. Default: "Admin".

### `EmbeddingGenerator`

`public IEmbeddingGenerator<string, Embedding<float>>? EmbeddingGenerator { get; set; }`

Optional embedding model, configured like [`MentorOptions.ChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chatclient). When set, it enables **semantic** tool filtering (see [`MentorOptions.EnableToolFiltering`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#enabletoolfiltering)) — tools and the user message are compared by embedding similarity instead of keyword overlap, which is reliable and multilingual. When not set and filtering is on, every tool is sent (with a startup warning): filtering is semantic-only, and there is no keyword fallback. Example (Azure OpenAI — a separate embedding deployment):

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

### `EnableActionFeedback`

`public bool EnableActionFeedback { get; set; }`

Visual feedback during tool call execution. Default: true

### `EnableCompaction`

`public bool EnableCompaction { get; set; }`

Enables history compaction (Wave 1 \#5) — intelligently shrinks the in-flight conversation context before each LLM call instead of just truncating by count. When enabled, MentorAgent installs a non-LLM compaction pipeline: collapse old tool results → keep the last [`MentorOptions.CompactionMaxTurns`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#compactionmaxturns) turns → hard token-budget truncation backstop at [`MentorOptions.CompactionTokenThreshold`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#compactiontokenthreshold). ⚠️ Only for the agent MentorAgent builds (Path A / ChatClient) with history kept locally — the default in-memory provider, [`MentorOptions.ChatHistoryProviderFactory`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chathistoryproviderfactory) or a provider instance. It shrinks what is SENT, never what is stored (BUG-104). Not installed with [`MentorOptions.UseServiceManagedHistory`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#useservicemanagedhistory) (Foundry, Responses API with store), where the service keeps the history; a startup warning says so. Uses the experimental Agent Framework compaction API. Default: false

### `EnableGenerativeCards`

`public bool EnableGenerativeCards { get; set; }`

Renders a [`MentorCard`](https://docs.mentoragent.net/api/novior-mentoragent-abstractions-models/mentorcard) returned by a tool as a real card in the chat, instead of letting the model describe it in prose — generative UI Level 2 (roadmap \#14). Default: `true`.

Level 1 ([`MentorOptions.EnableRichResponses`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#enablerichresponses)) is Markdown the model writes. This is the level above: your tool returns the structure, so fields, labels, links and buttons come from your code and the model only chooses when to call the tool. A card can therefore carry actions safely — nothing the conversation says can add a button or change where one points.

Set to `false` to fall back to text: the cards are still returned to the model, so the assistant keeps answering, it just describes the data instead of showing it.

### `EnableImageInput`

`public bool EnableImageInput { get; set; }`

Enables image attachments in the chat (roadmap \#13). When on, the widget shows an attach button and accepts uploaded / pasted / dragged images and image URLs; the user turn is then sent as a multimodal `ChatMessage` (text + image content). Requires a **vision-capable**[`MentorOptions.ChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chatclient) (e.g. gpt-4o / gpt-4.1). Default: `false`.

### `EnableObservability`

`public bool EnableObservability { get; set; }`

Emit OpenTelemetry traces/metrics (GenAI semantic conventions) for the coordinator's model calls — its [`MentorOptions.ChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chatclient) pipeline on Path A, innermost so the spans show the real provider calls — plus MentorAgent-level spans and counters (turns, tool calls, errors, guardrail blocks) on both paths. Nothing else is wrapped: the auxiliary calls ([`MentorOptions.ClassifierChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#classifierchatclient)), the agents under the coordinator, a pre-built [`MentorOptions.Agent`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#agent) and the [`MentorOptions.EmbeddingGenerator`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#embeddinggenerator) are traced only if the host wraps them with `UseOpenTelemetry()` before handing them over. The host app wires the exporter (App Insights, Aspire, OTLP) and adds the sources/meters. Default: `false`.

### `EnableOnboardingTour`

`public bool EnableOnboardingTour { get; set; }`

Shows a short guided tour the first time a user opens the widget (roadmap \#18). Default: `false`.

The steps are generated once per process from the pages registered with `[MentorPage]` and the assistant's own tools, so the tour describes the application as it is rather than as it was when someone wrote the script. Each step can carry a ready-made question, which is the part that actually helps: users do not fail to use an in-app assistant because they cannot find it, they fail because they do not know what to ask it.

Register your own [`IMentorTour`](https://docs.mentoragent.net/api/novior-mentoragent-abstractions-interfaces/imentortour) before `AddMentorAgent()` to write the tour by hand.

### `EnableOutputSafetyCheck`

`public bool EnableOutputSafetyCheck { get; set; }`

Built-in **output** safety check — the counterpart of [`MentorOptions.EnableSafetyCheck`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#enablesafetycheck) for the reply. When `true`, the assistant's completed reply is classified by the LLM and blocked (replaced with a safe message) if it leaks system instructions/secrets, discloses someone's personal data, or contains harmful content. Like the output guardrail, the reply is buffered and revealed after moderation (no live streaming that turn). A custom [`MentorOptions.OutputGuardrail`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#outputguardrail) takes precedence over this built-in check. Runs on [`MentorOptions.ClassifierChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#classifierchatclient), or on [`MentorOptions.ChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chatclient) when none is set: with a pre-built [`MentorOptions.Agent`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#agent) (Path B) it needs a [`MentorOptions.ClassifierChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#classifierchatclient), and without either the reply is shown unchecked and a warning is logged when the coordinator is built. Adds one lightweight model call per reply. Default: `false`.

### `EnableRichResponses`

`public bool EnableRichResponses { get; set; }`

When `true` (default), the coordinator is instructed to format answers with GitHub-flavoured Markdown — rendering tabular/comparative data as **tables** and enumerations as **lists** in the chat widget (Generative UI, Level 1). The widget always renders Markdown safely (text is HTML-encoded before any tag is emitted); this flag only controls the prompt nudge. Set to `false` for terse plain-text replies. Default: `true`.

### `EnableSafetyCheck`

`public bool EnableSafetyCheck { get; set; }`

Enables AI safety check on input messages. Before processing each message, asks the LLM whether it is a prompt injection, jailbreak, or sensitive data extraction attempt. Works in any language automatically. Runs on [`MentorOptions.ClassifierChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#classifierchatclient), or on [`MentorOptions.ChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chatclient) when none is set: with a pre-built [`MentorOptions.Agent`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#agent) (Path B) it needs a [`MentorOptions.ClassifierChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#classifierchatclient), and without either every message goes through unchecked and a warning is logged when the coordinator is built. A custom [`MentorOptions.InputGuardrail`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#inputguardrail) replaces it and needs no client. ⚠️ Adds one extra API call per message (~200-500ms). Default: false

### `EnableSkills`

`public bool EnableSkills { get; set; }`

Enables Agent Skills — domain knowledge packages with progressive disclosure. When true, MentorAgent discovers skills from:

- File-based: `{SkillsFolder}/{name}/SKILL.md`
- Class-based: classes decorated with `[MentorSkill]` in [`MentorOptions.ScanAssemblies`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#scanassemblies)

The skill catalogue is injected into the system prompt; full instructions are loaded on demand via the `load_skill` tool (~100 tokens overhead per skill instead of loading all instructions upfront). Usage:

```text title="Example"
// Skills/expense-report/SKILL.md  → auto-discovered

[MentorSkill(Name = "refund-policy", Description = "Return and refund rules",
    Instructions = "## Refund Policy\n- Items returnable within 30 days...")]
public class RefundPolicySkill { }

options.EnableSkills = true;
```

Default: false

### `EnableSuggestions`

`public bool EnableSuggestions { get; set; }`

Shows proactive suggestion chips. Default: true

### `EnableToolFiltering`

`public bool EnableToolFiltering { get; set; }`

Enables dynamic tool filtering (Wave 1 \#4) — before each LLM call, only the tools semantically relevant to the user's latest message are sent, cutting the per-call token cost of tool schemas (often the single largest contributor). **Requires [`MentorOptions.EmbeddingGenerator`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#embeddinggenerator).** Relevance is computed by embedding similarity, which ranks tools by meaning across languages; the absolute score is not calibrated. When no action clears [`MentorOptions.ToolFilterMinScore`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#toolfilterminscore), the three best-ranked actions that need no confirmation and no role (an MCP tool only when its server marks it read-only) are sent anyway — a question that needs data can score below a greeting (BUG-106) — so give every action that changes data `RequiresConfirmation`. When only actions that need confirmation clear it, the three best-ranked that do not travel with them (BUG-099). If this is enabled but no embedding model is configured, filtering is skipped and **all tools are sent** (a warning is logged) — there is no keyword fallback. Safe by design: core tools (navigation, memory, routing, skills, UI actions) are always kept. Verify the effect with the token counter. Default: false

### `EnableVoiceInput`

`public bool EnableVoiceInput { get; set; }`

Enables microphone in the widget — native browser Speech Recognition API. Supported on Chrome/Edge. Firefox partial. Requires HTTPS in production. Default: false

### `EnableVoiceOutput`

`public bool EnableVoiceOutput { get; set; }`

Enables speech synthesis — the Mentor reads responses aloud. Native browser Speech Synthesis API. Supported on all modern browsers. Default: false

### `FileSearchMaxResults`

`public int? FileSearchMaxResults { get; set; }`

Optional bound on how many matches [`MentorHostedTools.FileSearch`](https://docs.mentoragent.net/api/novior-mentoragent-abstractions-models/mentorhostedtools) returns. Lower = fewer tokens injected into the answer. `null` (default) lets the provider decide.

### `FileSearchVectorStoreIds`

`public IReadOnlyList<string>? FileSearchVectorStoreIds { get; set; }`

Vector store ids searched by [`MentorHostedTools.FileSearch`](https://docs.mentoragent.net/api/novior-mentoragent-abstractions-models/mentorhostedtools) — created by uploading your documents to the provider (Azure OpenAI / OpenAI vector stores, Foundry). Required when file search is enabled: without at least one id the tool is skipped and a warning is logged, because an unbounded file search is both useless and a data-exposure risk.

### `FilterHostedTools`

`public bool FilterHostedTools { get; set; }`

Let the semantic tool filter decide, per turn, whether each provider-hosted tool is relevant to the user's message — instead of declaring them on every turn. Default: `true`.

Hosted tools are the expensive ones (a web search is billed per call, a file-search turn injects thousands of extra input tokens), yet they were the only group the filter never touched: they are not `AIFunction`s and carry no description to embed. Descriptions now come from an internal catalogue, so they are scored exactly like every other tool.

Requires [`MentorOptions.EnableToolFiltering`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#enabletoolfiltering) and an [`MentorOptions.EmbeddingGenerator`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#embeddinggenerator). Without them nothing is filtered and a warning is logged — **fail-open**: an assistant must not lose capabilities because a model was not configured.

### `HitlMode`

`public MentorHitlMode HitlMode { get; set; }`

How tool approval is implemented (roadmap \#16). [`MentorHitlMode.Blocking`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorhitlmode) (default) keeps MentorAgent's own flow — one agent run per turn, streaming stays alive. [`MentorHitlMode.Native`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorhitlmode) switches to the Agent Framework standard (`ApprovalRequiredAIFunction` + `ToolApprovalRequestContent`/`ToolApprovalResponseContent`) for interop with AF workflows and AF-native hosts, with one more agent run per round of confirmations. Both modes make the same model calls.

The user-facing banner and the client protocol are identical in both modes, so this is safe to flip without touching any client. Only meaningful with [`MentorOptions.RequireConfirmation`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#requireconfirmation) on.

### `HostedImageModel`

`public string? HostedImageModel { get; set; }`

Model used by [`MentorHostedTools.ImageGeneration`](https://docs.mentoragent.net/api/novior-mentoragent-abstractions-models/mentorhostedtools) (e.g. `gpt-image-1-mini`).

⚠️ **On Azure OpenAI this option is not enough on its own.** Azure resolves the image deployment from the `x-ms-oai-image-generation-deployment`**request header**, not from the tool payload — without it every image turn fails with *"imagegen deployment must be provided through header"*. MentorAgent is handed an already-built [`MentorOptions.ChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chatclient), so it cannot add the header: attach it as a pipeline policy where you construct the `AzureOpenAIClient`. This option still carries the model id for providers that read it (OpenAI proper), and is logged at startup together with the header reminder.

### `HostedImageSize`

`public string? HostedImageSize { get; set; }`

Size of the generated image as `WIDTHxHEIGHT` (e.g. `"1024x1024"`). This is the cost knob for image generation — a larger image is billed more — so set it rather than inheriting a provider default you did not choose. An unparsable value is ignored with a warning. `null` (default) lets the provider decide.

### `HostedMcpServers`

`public IReadOnlyList<MentorHostedMcpServer>? HostedMcpServers { get; set; }`

Remote MCP servers the **provider** connects to, used by [`MentorHostedTools.HostedMcp`](https://docs.mentoragent.net/api/novior-mentoragent-abstractions-models/mentorhostedtools). Required when hosted MCP is enabled: with an empty list the tool is skipped and a warning is logged.

Distinct from [`MentorOptions.McpServers`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#mcpservers), where your own process is the MCP client. Here the server must be reachable from the provider's network, and approval is enforced by the provider through [`MentorHostedMcpServer.RequireApproval`](https://docs.mentoragent.net/api/novior-mentoragent-models/mentorhostedmcpserver#requireapproval).

### `HostedToolDomainCheck`

`public bool HostedToolDomainCheck { get; set; }`

Before declaring a provider-hosted tool, ask a small model whether the message is within this application's scope at all. Default: `false`.

A different question from [`MentorOptions.FilterHostedTools`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#filterhostedtools). That one asks "does this message need *this tool*?" — and `"draw me a dog"` passes, because it genuinely is an image request. This one asks "does it have anything to do with *this application*?", which is what stops an e-commerce assistant from paying to generate a dog.

It runs **once per turn, and only when a hosted tool has already passed relevance filtering** — that is, only when money is about to be spent anyway. A couple of hundred tokens against a per-call web search or a generated image is three orders of magnitude in the right direction. **Fails open**: a classifier that errors or answers unexpectedly lets the turn through.

This deliberately does *not* use embedding similarity. Raw cosine has no stable zero point: measured on a real turn, `"genera immagine di un cane"` scored 0.398 against a shop's page names — high not because it was related but because both were short phrases in the same language — and the same request crossed the threshold on a one-word rewording.

### `HostedToolDomainClassifier`

`public Func<string, CancellationToken, Task<bool>>? HostedToolDomainClassifier { get; set; }`

Replaces the built-in classifier with your own decision (`true` = in scope). Use it to reuse an existing intent classifier, apply per-user policy, or make the check free. Same shape as [`MentorOptions.InputGuardrail`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#inputguardrail).

### `HostedToolDomainScope`

`public string? HostedToolDomainScope { get; set; }`

The scope sentence the classifier judges against. `null` (default) derives it from [`MentorOptions.AppName`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#appname), [`MentorOptions.AppDescription`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#appdescription) and the application's page names and, when hosted tools are enabled, appends them with the rule that they do not widen the subject (BUG-050) — a cost rule, strict on purpose. When set, it is also the subject [`MentorOptions.RefuseOutOfScope`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#refuseoutofscope) judges against, verbatim.

Worth writing yourself when the derived text is too narrow — a shop assistant may legitimately field questions about shipping rules or VAT that no page covers — or when it is too broad, since a vague description makes the classifier permissive. One or two concrete sentences beat a paragraph.

### `HostedToolFilterMinScore`

`public float? HostedToolFilterMinScore { get; set; }`

Similarity threshold for hosted tools specifically. `null` (default) uses `0.15` — deliberately **lower** than [`MentorOptions.ToolFilterMinScore`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#toolfilterminscore), not equal to it.

Two reasons it must be its own, permissive value. First, the scores are not on the same scale: hosted tools are matched through long English descriptions while your actions carry short descriptions in the user's own language, so their scores run systematically lower and a threshold tuned for one population mis-fires on the other. Second, this filter is now a coarse pre-cut, not the decision — [`MentorOptions.HostedToolDomainCheck`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#hostedtooldomaincheck) makes the judgement call, so this only needs to drop what is plainly irrelevant.

Measured on real turns: a shop assistant asked to generate an image scored `web_search` 0.04–0.07, `code_interpreter` 0.05–0.08, `image_generation` 0.29–0.35. At 0.35 the verdict flipped between two rewordings of the same request; at 0.15 it is stable and the irrelevant tools are still dropped. Raising this to "be safe" reintroduces exactly that flakiness. Every score is logged at Debug, so tune from data rather than intuition.

### `HostedTools`

`public MentorHostedTools HostedTools { get; set; }`

Provider-hosted tools to expose to the coordinator (roadmap \#15): web search, code interpreter, file search, image generation, hosted MCP. They are **declared, not executed** by MentorAgent — the model provider runs them on its own infrastructure during inference, so no code runs on your servers.

```text title="Example"
options.HostedTools = MentorHostedTools.WebSearch | MentorHostedTools.CodeInterpreter;
```

⚠️ **Provider support is not universal.** Requires [`MentorOptions.ChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chatclient) (Path A). The Azure OpenAI / OpenAI *Responses* client supports the full set; the *Chat Completions* client supports web search only, and asking it for the others makes the provider reject the call. MentorAgent logs the enabled set at startup so a misconfiguration is visible. Default: [`MentorHostedTools.None`](https://docs.mentoragent.net/api/novior-mentoragent-abstractions-models/mentorhostedtools).

### `IncludeWorkflowExceptionDetails`

`public bool IncludeWorkflowExceptionDetails { get; set; }`

When true, exception messages from workflow agents are included in the agent response. Useful for debugging. Do NOT enable in production. Default: false

### `InputGuardrail`

`public Func<string, CancellationToken, Task<bool>>? InputGuardrail { get; set; }`

Optional custom input guardrail. Return `true` if the message is safe to process, `false` to block it. When set it runs on every message (whether or not [`MentorOptions.EnableSafetyCheck`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#enablesafetycheck) is on) and **replaces** the built-in LLM prompt-injection check. Leave `null` to use the built-in check (gated by [`MentorOptions.EnableSafetyCheck`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#enablesafetycheck)).

### `InputPlaceholder`

`public string? InputPlaceholder { get; set; }`

Input box placeholder text. If null, uses the localized default in [`MentorOptions.Language`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#language) ("Write a message..." in English).

### `IsReadOnlyAction`

`public Func<string, bool>? IsReadOnlyAction { get; set; }`

Optional predicate that declares, by tool name, the actions that only read — the same declaration as `[MentorAction(ReadOnly = true)]`, for actions marked only with `[Description]` or to declare them in one place. MentorAgent takes your word for it, as an MCP client takes a server's `readOnlyHint`: such actions are published to MCP clients with that hint, and with [`MentorOptions.RequireLookupForRecordQuestions`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#requirelookupforrecordquestions) on they are the ones the assistant may be required to call before answering a question about a value of your records.

```text title="Example"
options.IsReadOnlyAction = name => name.StartsWith("get_") || name.StartsWith("search_");
```

A tool that needs confirmation is never read-only, whatever this returns. Must be a pure function of the name. `null` (default) = only the attribute declares.

### `Language`

`public MentorLanguage Language { get; set; }`

Language for AI responses and the widget interface. Propagated to all agents, the Coordinator, and the MentorLocalizer. Default: English

The instructions the model could echo into a reply are written in this language too: in the coordinator's prompt, the rules with an example sentence (before a confirmation, after a declined one, describing an action, outside the configured scope, Proactive's closing rule) and the grounding rule; in the prompts MentorAgent generates for specialists and team members, the grounding rule; in the tool results, every instruction MentorAgent adds (declined, stopped, not confirmed in time, result withheld, never run, outcome unknown, nobody to confirm). The tool names, JSON keys and markers such as `not_run` in them are the same in every language. The notice a request carries when its turn left out an action the user may not run is written in this language too, under its English heading (BUG-224).

The rest of the instructions stays in English, whatever the language: the headings and the other rules of the prompts, the line that names the reply language, the per-request context and the other notices, the prompts of the classifiers and of the onboarding tour's generation, the results of a delegation, and the error messages of a failed action. In [`MentorHitlMode.Native`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorhitlmode) mode a declined approval also reaches the model after a sentence of Microsoft.Extensions.AI's own, in English. The `Instructions` a host writes for a specialist or a team member are sent as written.

It is read again on every turn: a change made while the application runs applies to the next turn of every session, and to the widget's texts. A page can have a language of its own with `ChatWidget.Language`, which every turn of that widget carries (`MentorUserMessage.Language`, the hub's `SendUserMessage`, `language` on `/mentor/chat`): that turn is answered and written for in the page's language, as above. A2A and MCP requests carry none, and are in this language.

### `MaxAutoApprovalIterations`

`public int? MaxAutoApprovalIterations { get; set; }`

Safety cap on how many times a single turn may be re-run because every surfaced approval was auto-approved by [`MentorOptions.AutoApprovalRules`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#autoapprovalrules). Each re-invocation is a fresh, billable model call, and a per-request cap such as the function-invoking client's own iteration limit restarts every time and cannot bound this loop.

The same shape as BUG-018, where a custom team termination condition removed the iteration cap instead of tightening it and ran 770 model calls against a limit of 2. A model that keeps requesting an auto-approved tool is an unbounded spend, and this is the only thing that stops it. Leave it at the framework default unless you have measured a reason.

Default: null (the Agent Framework's own default cap applies)

### `MaxHostedToolCallsPerSession`

`public int MaxHostedToolCallsPerSession { get; set; }`

Maximum provider-hosted tool calls allowed in one session (Blazor circuit / hub connection); beyond it they stop being declared. `0` (default) means no cap.

Complementary to [`MentorOptions.FilterHostedTools`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#filterhostedtools), not an alternative: relevance scoring is probabilistic and lowers how often an expensive tool fires on a message that did not need it, while a counter is the only thing that puts a ceiling on a session's spend.

### `MaxImageBytes`

`public int MaxImageBytes { get; set; }`

Maximum size, in bytes, of a single inline image attachment (larger is rejected). Default: 4 MB.

### `MaxImagesPerMessage`

`public int MaxImagesPerMessage { get; set; }`

Maximum number of images per message. Default: 4.

### `MaxMessageLength`

`public int MaxMessageLength { get; set; }`

Maximum message length in characters. 0 = unlimited. Default: 4000

### `MaxSessionMessages`

`public int MaxSessionMessages { get; set; }`

Maximum number of messages in the built-in in-memory session history. Default: 50.

Every message counts, tool calls and tool results included, and the history is trimmed by whole turns, oldest first ([`MentorChatReducer`](https://docs.mentoragent.net/api/novior-mentoragent-core/mentorchatreducer)), so the data a tool read stays visible to the model on the next turn. `0` or negative: no trimming (a warning is logged). Not read when [`MentorOptions.ChatReducer`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chatreducer) is set, nor for a [`MentorOptions.ChatHistoryProvider`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chathistoryprovider) / [`MentorOptions.ChatHistoryProviderFactory`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chathistoryproviderfactory) you supply, nor with [`MentorOptions.UseServiceManagedHistory`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#useservicemanagedhistory).

### `McpCallerPrincipal`

`public Func<ClaimsPrincipal?>? McpCallerPrincipal { get; set; }`

Resolves the `ClaimsPrincipal` behind an MCP request, so that role-gated actions can be published to callers entitled to them (ROADMAP \#32).

**Default `null`, and the default is the safe one.** With no resolver, nothing an author protected is published at all — the behaviour BUG-021 and BUG-035 forced, on the only surface where a tool is invoked by someone who is not the application's own user.

Supply this only when your host can genuinely say *who* is calling — an mTLS subject, a validated bearer token, a gateway-injected header you trust. Then a `[MentorAction(RequiredRoles = [...])]` becomes publishable, and it is published **only** when the resolved principal is authenticated and holds one of those roles. This is an identity, not an override: a resolver that returns `null`, returns an unauthenticated principal, or throws falls back to withholding.

Roles are tested with `ClaimsPrincipal.IsInRole`, which honours each identity's `RoleClaimType` (a `roles` claim works, not only `ClaimTypes.Role`, once the identity names it: with JwtBearer and `MapInboundClaims = false`, set `TokenValidationParameters.RoleClaimType = "roles"`, or every role-gated action stays withheld). The resolver is called **once per MCP request** — `tools/list` and `tools/call` alike — and its answer is never cached per caller (BUG-103), so keep it cheap: read the request's already-validated principal, do not introspect a token per call.

**Confirmation gates are never satisfied by it.**`RequiresConfirmation` and [`MentorOptions.RequiresApproval`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#requiresapproval) both mean "ask a human first", and there is no human on the far end of an MCP call. Those actions stay unpublished however privileged the caller — waiving them is exactly what BUG-021 was about.

```text title="Example"
builder.Services.AddHttpContextAccessor();
builder.Services.AddOptions<MentorOptions>().Configure<IHttpContextAccessor>(
    (options, http) => options.McpCallerPrincipal = () => http.HttpContext?.User);
```

### `McpServerEnabled`

`public bool McpServerEnabled { get; set; }`

Set to true to expose MentorAgent as an MCP server. The server lives in the Novior.MentorAgent.Server package: also call `builder.Services.AddMentorAgentMcp()` after `AddMentorAgent()` (`AddMentorAgentServer()` includes it) and `app.MapMentorAgentMcp()` in Program.cs to map the endpoint. Default: false

### `McpServerPath`

`public string McpServerPath { get; set; }`

Endpoint path for the MCP server, used by `app.MapMentorAgentMcp()` when no path is passed to it. Default: "/mcp"

### `McpServers`

`public MentorMcpServer[]? McpServers { get; set; }`

External MCP (Model Context Protocol) servers to connect to as a client. Each server's tools are automatically added to the coordinator as L1 tools. Example:

```text title="Example"
options.McpServers = [
    new MentorMcpServer { Name = "filesystem", Command = "npx",
        Arguments = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] }
];
```

### `MemoryAutoCapture`

`public bool MemoryAutoCapture { get; set; }`

Reliably capture durable user facts with a dedicated post-turn extraction, instead of relying on the coordinator to proactively call the `remember` tool (which weaker models do inconsistently — e.g. saving on "Mi chiamo Antonio" but not on "Ciao, mi chiamo Antonio"). After each user message a small, focused LLM call extracts personal facts (name, role, team, company, location, preferences) and stores them directly. This is the single memory-writing path when it can run, with [`MentorOptions.UseMemoryContext`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#usememorycontext) on: the extraction goes to [`MentorOptions.ClassifierChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#classifierchatclient) if set, else to [`MentorOptions.ChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chatclient). Then the redundant `remember` tool, and on **Path A** its forceful prompt block, are dropped (saving per-call tokens). That holds on **Path B** too (a pre-built [`MentorOptions.Agent`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#agent)) when a [`MentorOptions.ClassifierChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#classifierchatclient) is set. With a pre-built agent and no classifier, auto-capture cannot run, so the `remember` tool is kept as the fallback writer. The build log names the writer. The `forget_all` tool is always available. Cost: one lightweight model call per user message, to the classifier when there is one. Set to `false` to opt out and go back to the tool-based writing (cheaper, but the model saves facts less reliably). Default: `true`.

### `MemoryContextCount`

`public int MemoryContextCount { get; set; }`

Maximum number of recent memories injected into the system prompt on each call. Increase for more historical context, reduce to limit tokens. Default: 10

### `MemoryRelevanceFiltering`

`public bool MemoryRelevanceFiltering { get; set; }`

Inject only the memories relevant to the current message instead of the last [`MentorOptions.MemoryContextCount`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#memorycontextcount). Saves tokens on memory-heavy users. Relevance is **semantic**: each memory is embedded and compared to the message by cosine similarity (multilingual, no keyword lists) — consistent with semantic tool filtering. Requires [`MentorOptions.EmbeddingGenerator`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#embeddinggenerator); without it, memory silently falls back to last-N by recency. Identity/preference facts (keys starting with `user_` or `pref`) are always kept, so the AI can still greet by name and honour preferences. Default: false (last-N, preserving existing behavior).

### `MentorshipLevel`

`public MentorshipLevel MentorshipLevel { get; set; }`

Proactivity and mentorship level. Default: Standard.

### `MetricsPersistenceInterval`

`public TimeSpan MetricsPersistenceInterval { get; set; }`

How often the durable-dashboard persistence service flushes the current snapshot to a registered `IMentorMetricsStore` (it also flushes once on graceful shutdown). Only active when a custom store is registered. Default: 30 seconds.

### `MetricsRetention`

`public TimeSpan MetricsRetention { get; set; }`

How far back the dashboard keeps per-model time-series buckets (roadmap \#9 temporal charts). Older hourly buckets are pruned. Default: 7 days.

### `ModelPricing`

`public IReadOnlyDictionary<string, ModelPrice>? ModelPricing { get; set; }`

Per-model token prices for the admin dashboard's cost estimate. Keyed by model id. When `null` (default) or a model is missing, tokens are shown without cost. MentorAgent ships no built-in price table — you supply current prices.

### `ObservabilityIncludeSensitiveData`

`public bool ObservabilityIncludeSensitiveData { get; set; }`

Include sensitive data (prompt and completion content) in telemetry. **Development only** — leave `false` in production to avoid logging user/model content. Default: false.

### `ObservabilitySourceName`

`public string ObservabilitySourceName { get; set; }`

Name of the `ActivitySource`/`Meter` MentorAgent emits under. The host must add this name via `.AddSource(name).AddMeter(name)` on its OpenTelemetry setup. Default: "MentorAgent".

### `OnException`

`public Func<Exception, string?>? OnException { get; set; }`

Optional hook to map an exception to a user-facing message. Return a message to show the user, or `null` to fall back to the built-in friendly mapping (content filter, rate limit, timeout, generic). Applies to turn-level and tool-level failures.

For a tool that throws, the message goes to the chat and to the model. With the hook set the chat never shows the exception's own message: `null`, an empty string or a hook that throws shows the localized "The action could not be completed." The model then gets the exception's message, which it needs to answer; return a message for any exception whose text must not reach it. Without the hook both get the exception's message. The log has only the exception's type, and the whole exception at Debug.

### `OnToolResult`

`public Func<string, object?, object?>? OnToolResult { get; set; }`

Optional hook to transform or redact a tool result before it is returned to the model (Result Overrides). Receives the tool name and its raw result; return the (possibly modified) result. Runs for every tool invocation, before the result reaches the model — and on the MCP server (`tools/call`) before it reaches the MCP caller, with the same input (BUG-107). A hook that throws there withholds the result: the caller gets an `isError` result. `null` = no transformation.

### `OutputGuardrail`

`public Func<string, CancellationToken, Task<bool>>? OutputGuardrail { get; set; }`

Optional output guardrail that moderates the assistant's **completed** reply before it is shown. Return `true` if the reply is safe, `false` to replace it with a blocked message. **Important:** when set, the reply for that turn is buffered and revealed all at once after moderation (no live streaming) so unmoderated text is never shown. Leave `null` (default) to keep live streaming with no output moderation.

### `Position`

`public ChatPosition Position { get; set; }`

Widget position. Default: BottomRight.

### `PrimaryColor`

`public string? PrimaryColor { get; set; }`

Widget primary color (hex, e.g.: "\#2563EB"). If null, uses the theme color.

### `ProactiveNextSteps`

`public bool ProactiveNextSteps { get; set; }`

Whether [`MentorshipLevel.Proactive`](https://docs.mentoragent.net/api/novior-mentoragent-abstractions-models/mentorshiplevel) closes its answers with suggested next steps. Default: false. Told to offer only what the request's tools, pages, skills and specialists can do, the model still offered steps the application does not have, 3 in 15 measured turns (BUG-074); off, the level asks for no closing offers. The other levels ignore it.

### `RagMinScore`

`public float RagMinScore { get; set; }`

Minimum relevance score a document must reach to be included in the RAG context. Documents below this threshold are discarded before injection into the prompt and before being shown as source chips. The scale depends on the [`IMentorRagSource`](https://docs.mentoragent.net/api/novior-mentoragent-rag/imentorragsource) implementation: - Full-text / keyword search (e.g. `ShopFlowRagSource`): integer-like scores where 1 = single term match in content, 3 = title match. A value of `2` requires at least two content matches or one title match. - Vector / cosine similarity search: scores in \[0, 1\]. Typical thresholds: 0.5 (loose) to 0.75 (strict). Which threshold applies: this option when the host sets it (in code or through configuration binding); otherwise the source's own [`IMentorRagSource.DefaultMinScore`](https://docs.mentoragent.net/api/novior-mentoragent-rag/imentorragsource#defaultminscore) when it declares one — a vector source should; otherwise this option's default, 2. Set to `0` to disable filtering (return all results from `SearchAsync`). Default: 2

### `RagResultCount`

`public int RagResultCount { get; set; }`

Maximum number of documents to retrieve per query. Default: 5

### `RagSystemPromptTemplate`

`public string RagSystemPromptTemplate { get; set; }`

Template for the RAG context injection in the system prompt. Use `{documents}` as placeholder for the retrieved content. Default: "Use the following documents to answer:\\n{documents}", in English whatever [`MentorOptions.Language`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#language) is.

### `RateLimitPerUser`

`public int RateLimitPerUser { get; set; }`

Maximum number of messages per user within the time window. 0 = disabled (default). E.g.: 20 → max 20 messages every RateLimitWindowSecs seconds. ⚠️ IMPORTANT — User identification: - With ASP.NET authentication configured: each user is identified by their `ClaimTypes.NameIdentifier` claim, else `sub`, `oid` or their name — the key memory uses. Each user gets an independent counter. - Without authentication: the key follows [`MentorOptions.AnonymousIdentity`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#anonymousidentity). PerSession (default) gives every circuit, hub connection and SSE request its own counter — a reload, a reconnect or a new request starts from zero, so this does not bound an anonymous caller's spend. Shared makes one global counter for every anonymous visitor. For a limit that holds per person, configure ASP.NET Core authentication. The MCP server's `tools/call` is bounded too (BUG-107), with the same limit and window but an allowance of its own — a tool call is not a model turn, and an automation must not lock its user out of the chat. The caller is the principal `McpCallerPrincipal` resolves, else the request's user, counted by its `NameIdentifier`, `sub` or `oid` claim (else its name). A caller with none is counted by remote address when `IHttpContextAccessor` is registered (behind a proxy, apply forwarded headers); otherwise every such caller shares one allowance — never one per MCP session, which costs nothing to open — and one busy client can use it up for the others. A refused call is answered with an `isError` result.

### `RateLimitWindowSecs`

`public int RateLimitWindowSecs { get; set; }`

Duration in seconds of the rate limiting window. Default: 60. E.g.: 60 → the window resets every minute.

### `RefuseOutOfScope`

`public bool RefuseOutOfScope { get; set; }`

Refuse, politely and without calling the coordinator, a message that is not about this application. Default: `false` — every message is answered, which is what a general-purpose assistant should do.

It asks a question of its own, which defaults to answering (BUG-105): a name the classifier does not recognise may be one of the application's own records, so a question about its price, stock or status is answered; greetings, follow-ups, requests about the conversation and questions about the assistant are answered; only a subject that plainly has nothing to do with the application — creative writing, the weather, sport — is refused, and when unsure the message is answered. It used to reuse the hosted-tool cost gate's strict sentence, and refused catalogue price questions.

The question is shown the end of the assistant's last reply, so a short reply is judged with the question it answers: seen alone, "Sì" after "Vuoi vedere i dettagli?" was refused (BUG-223). The safety question still judges the message alone, and the verdict is kept for the message and that reply together.

With [`MentorOptions.EnableSafetyCheck`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#enablesafetycheck) on it travels in the same classifier call; on its own it adds one small call per turn. The hosted-tool check ([`MentorOptions.HostedToolDomainCheck`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#hostedtooldomaincheck)) then asks its own, strict question — only on a turn where a hosted tool is about to be declared.

Judged against [`MentorOptions.HostedToolDomainScope`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#hostedtooldomainscope) when set, otherwise the application's own name, description, and pages with their descriptions — never the hosted-tool capability rule. The refusal is localized and the turn ends there — no tools, no coordinator call, nothing billed for the answer — and it is counted in `GuardrailBlockCount`.

### `RemoteAgents`

`public MentorRemoteAgent[]? RemoteAgents { get; set; }`

Remote A2A (Agent-to-Agent) agents to include in the Handoff workflow. Each remote agent is discoverable from its agent card URL and will appear in the coordinator's routing options alongside local \[MentorAgent\] classes. Example:

```text title="Example"
options.RemoteAgents = [
    new MentorRemoteAgent {
        Name = "InventoryAgent",
        Description = "Manages warehouse operations",
        AgentCardUrl = "https://inventory.example.com"   // the base URL: the card is read from /.well-known/agent-card.json
    }
];
```

### `RequireActionForChangeRequests`

`public bool RequireActionForChangeRequests { get; set; }`

Opt-in. When a classifier model says a message asks to carry out one of your actions that ask for confirmation — and gives every input that action needs — the assistant's first model call must call one of those actions that semantic tool filtering selected for that message, or hand the request to a specialist or a team: the confirmation banner is then shown before anything is said. When it says the message asks to go to one of your pages (`[MentorPage]`), the first call must be `navigate_to`.

Why (BUG-118): asked "Imposta lo stock dell'iPad Air a 30", the assistant answered "Stock aggiornato" with no tool called — no banner, nothing changed — about one time in five on the ApiServer sample, and "Ecco la pagina Prodotti" without navigating. A rule in the prompt cannot stop it: worded generally it would also forbid reporting what a specialist did, or an action of an earlier turn.

A request that leaves out something the action needs is never forced. The classifier is shown the inputs each selected action requires — the schema's required parameters, with their type and description — and answers that it is not a complete request when an input is missing ("Annulla l'ordine 1004" when `cancel_order` needs a reason), when the entry is only described ("the last order"), when a name is given where the input is an ID, or when the new value has to be worked out from the current one ("add 5 units"). The model is then free to ask, as without this option — a forced call would have had to invent the missing value, and the banner would have shown it as the user's. Required means required in the schema: an input the action declares optional is not asked for first, whatever its `ProactiveHint` says — make it required if it must be. An action whose input schema cannot be shown faithfully (a `$ref`, a combination of schemas, parameters none of which is marked required) is never required.

Only actions that will ask a person first are ever required: declared confirmation (`[MentorAction(RequiresConfirmation = true)]`, [`MentorOptions.RequiresApproval`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#requiresapproval), an MCP server with `RequiresConfirmation`) with [`MentorOptions.RequireConfirmation`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#requireconfirmation) on. With [`MentorHitlMode.Native`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorhitlmode), no [`MentorOptions.AutoApprovalRules`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#autoapprovalrules) rule is consulted for a required call — it is always shown to a person; the calls the model makes on its own are the rules' to decide, as before. A page request is matched against your `[MentorPage]` pages: a page the application does not have, or something within the page already open, is not forced. A request from another agent is never required to navigate, and its confirmation-gated actions are refused there as always. The turn that answers an approval a Stop left open is never forced.

What changes: on those turns the banner, or the page, comes before any sentence. One small classifier call ([`MentorOptions.ClassifierChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#classifierchatclient) when set, bounded by [`MentorOptions.SafetyCheckTimeout`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#safetychecktimeout)) is paid on every turn while the application has a page or the message selected such an action — shared with [`MentorOptions.RequireLookupForRecordQuestions`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#requirelookupforrecordquestions) when both are on. Unlike that option, without [`MentorOptions.EnableToolFiltering`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#enabletoolfiltering) and an [`MentorOptions.EmbeddingGenerator`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#embeddinggenerator), or on Path B, it has no effect, and a startup warning says so. A classifier that fails or answers anything unreadable leaves the turn as it would be without this. Default: false

### `RequireConfirmation`

`public bool RequireConfirmation { get; set; }`

On/off for the confirmation dialogs of your actions (`[MentorAction(RequiresConfirmation = true)]`, and the undeclared writes of [`MentorOptions.RequireConfirmationForWrites`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#requireconfirmationforwrites)), of MCP-client servers with `MentorMcpServer.RequiresConfirmation`, and of [`MentorOptions.RequiresApproval`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#requiresapproval). Default: true

A hosted MCP server's approvals ([`MentorHostedMcpServer.RequireApproval`](https://docs.mentoragent.net/api/novior-mentoragent-models/mentorhostedmcpserver#requireapproval), [`MentorHostedMcpServer.AlwaysRequireApprovalTools`](https://docs.mentoragent.net/api/novior-mentoragent-models/mentorhostedmcpserver#alwaysrequireapprovaltools)) still show the banner: the provider asks for them, and they are set on that server. Switching this off silences the banner only: `RequiredRoles` keeps refusing a caller without the role.

### `RequireConfirmationForWrites`

`public bool RequireConfirmationForWrites { get; set; }`

A `[MentorAction]` that declares neither `ReadOnly = true` nor `RequiresConfirmation` is treated as an action that changes data, and asks for confirmation like one declared `RequiresConfirmation = true`. Default: true.

An action is excluded by declaring it: `ReadOnly = true` for one that only reads (or [`MentorOptions.IsReadOnlyAction`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#isreadonlyaction)), `RequiresConfirmation = false` to run a write without asking. Being gated, such an action is also withheld from the MCP server and from the A2A card, like every confirmation-gated action: nobody is there to confirm.

**It covers `[MentorAction]` methods only.** A method marked with `[Description]` alone asks for no confirmation and is published on `/mcp`, as before: mark it `[MentorAction]` to have this default. At startup a warning lists both kinds of write that run without confirmation.

Set to false for the behaviour up to 1.0.0-rc.14, where an action asked only when it declared `RequiresConfirmation = true`.

### `RequireLookupForRecordQuestions`

`public bool RequireLookupForRecordQuestions { get; set; }`

Opt-in. When a classifier model says a message asks for a value of your records — a price, a stock level, an order's status — the assistant's first model call must call one of the actions you declared read-only (`[MentorAction(ReadOnly = true)]`, [`MentorOptions.IsReadOnlyAction`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#isreadonlyaction), an MCP tool with `readOnlyHint`), or hand the request to a specialist or a team. With semantic tool filtering, one of those it selected for that message — the one that cleared [`MentorOptions.ToolFilterMinScore`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#toolfilterminscore), the companions of a confirmation-gated match, or the declared reads among the three actions the zero-match fallback sent; without it, any of them the user may run. The reply is then written from what it returned. An action that needs a role (`RequiredRoles`) or a confirmation is never required.

Why: with the read actions on the turn the model still answered "I can't see the price, shall I look it up?" about one time in three on the ApiServer sample (BUG-106), and with a read that matched on its own it could still write "price not available" without calling it; stronger wording in the prompt made it invent figures instead. Forcing is structural: the first thing the model produces is the call. It once worked with semantic tool filtering only, and asked in Italian which expenses were still waiting for approval, a host without an embedding model got a list the model made up (BUG-222).

What changes: on those turns the reply starts after the result, so a Proactive "I'm looking it up…" sentence no longer comes first; one small classifier call ([`MentorOptions.ClassifierChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#classifierchatclient) when set, bounded by [`MentorOptions.SafetyCheckTimeout`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#safetychecktimeout)) is paid on every turn whose selection holds a declared read — with filtering's zero-match fallback, and without filtering, that is nearly every turn, greetings included. With [`MentorOptions.RequireActionForChangeRequests`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#requireactionforchangerequests) on too and filtering on, the same call answers both questions. Without at least one declared read-only action that needs no role, or on Path B, it has no effect, and a startup warning says so. A classifier that fails or answers anything unreadable leaves the turn as it would be without this. Default: false

### `RequiresApproval`

`public Func<string, bool>? RequiresApproval { get; set; }`

Optional predicate that forces human approval for a tool by name, **on top of**`[MentorAction(RequiresConfirmation = true)]` and `MentorMcpServer.RequiresConfirmation`. Return `true` to gate the tool.

Use it to apply the Agent Framework's Agent Safety guidance to tools you do not own — MCP tools and skills: gate anything with side effects, that touches sensitive data, or that cannot be undone. It receives tool names: an MCP tool under the name its server gives it, with no server prefix. A remote A2A agent is reached by handoff, not by a tool call, and never passes here.

```text title="Example"
options.RequiresApproval = name =>
    name.StartsWith("delete_") || name is "send_email" or "write_file";
```

Asked whenever MentorAgent needs the answer, not once: while the coordinator is built, then again on every call to a tool — in [`MentorHitlMode.Native`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorhitlmode) too, where a tool it gates after the coordinator was built is confirmed at the call — on every `tools/list` and `tools/call` of the MCP server, on every turn the tool filter matches a tool, and for every action an `IMentorActionPolicy` is asked about. A tool whose confirmation is already declared is never asked about. Keep it a cheap, pure function of the tool name: read a file or a database once, outside it. `null` (default) = only the declared confirmations apply.

### `RoutingClassifierClient`

`public IChatClient? RoutingClassifierClient { get; set; }`

[`MentorRoutingStrategy.Classifier`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorroutingstrategy) / [`MentorRoutingStrategy.Cascade`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorroutingstrategy) only: optional dedicated client for the routing judgment. Defaults to the cheap [`MentorOptions.ChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chatclient) so the routing decision itself stays inexpensive.

### `RoutingComplexExemplars`

`public IReadOnlyList<string>? RoutingComplexExemplars { get; set; }`

[`MentorRoutingStrategy.Semantic`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorroutingstrategy) only: example phrases describing the kind of turn that should go to the strong model. The user message is escalated when its cosine similarity to the nearest exemplar reaches [`MentorOptions.RoutingThreshold`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#routingthreshold). `null` uses a built-in multilingual default set (analysis, comparison, long-form, coding, multi-step reasoning).

### `RoutingStrategy`

`public MentorRoutingStrategy RoutingStrategy { get; set; }`

Selects how the cheap↔strong decision is made when [`MentorOptions.StrongChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#strongchatclient) is set (roadmap \#11). Default [`MentorRoutingStrategy.Custom`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorroutingstrategy) — uses [`MentorOptions.UseStrongModelAsync`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#usestrongmodelasync) / [`MentorOptions.UseStrongModel`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#usestrongmodel). Set to [`MentorRoutingStrategy.Semantic`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorroutingstrategy), [`MentorRoutingStrategy.Classifier`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorroutingstrategy) or [`MentorRoutingStrategy.Cascade`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorroutingstrategy) for a built-in professional router.

### `RoutingThreshold`

`public float RoutingThreshold { get; set; }`

[`MentorRoutingStrategy.Semantic`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorroutingstrategy) only: cosine-similarity floor \[0..1\]. The turn is escalated to the strong model when the best exemplar score is at least this value. Higher = stricter (fewer escalations). Default: 0.35 (same scale as tool filtering).

### `SafetyCheckFailure`

`public MentorSafetyCheckFailure SafetyCheckFailure { get; set; }`

What a safety check does when it cannot reach a verdict: it timed out ([`MentorOptions.SafetyCheckTimeout`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#safetychecktimeout)), the provider returned an error, or a custom guardrail threw. [`MentorSafetyCheckFailure.Allow`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorsafetycheckfailure) (default) processes the message and shows the reply; [`MentorSafetyCheckFailure.Block`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorsafetycheckfailure) refuses the message, or withholds the reply, with a localized "try again" message. Both log a warning.

A verdict of UNSAFE blocks under both settings — this is only about having no verdict. A provider-side content filter that rejects the input check's own call counts as UNSAFE, not as a failure.

### `SafetyCheckTimeout`

`public TimeSpan SafetyCheckTimeout { get; set; }`

How long a safety check may take before it counts as failed — the input check ([`MentorOptions.EnableSafetyCheck`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#enablesafetycheck) or [`MentorOptions.InputGuardrail`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#inputguardrail)) and the output check ([`MentorOptions.EnableOutputSafetyCheck`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#enableoutputsafetycheck) or [`MentorOptions.OutputGuardrail`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#outputguardrail)). What happens then is [`MentorOptions.SafetyCheckFailure`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#safetycheckfailure). Default: 15 seconds.

The input check runs **before** the turn and blocks it, so it needs a bound: a provider that never answers would otherwise leave the assistant busy for good. On a hosted model it takes 0.7–2 s (measured on gpt-4.1); on a local model running on a CPU a single call can take close to a minute, and 15 s then fails every time. Raise it, or point the checks at a fast [`MentorOptions.ClassifierChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#classifierchatclient).

`TimeSpan.Zero` or any negative value (including `Timeout.InfiniteTimeSpan`) means **no limit**. The user can still press Stop: a check waiting on the provider is cancelled with the turn, whatever this is. MentorAgent does not read it from configuration: set it in code, for example `options.SafetyCheckTimeout = TimeSpan.FromSeconds(90)`; a host that binds its own section to these options writes `"00:01:30"`, and `"00:00:00"` for no limit.

### `ScanAssemblies`

`public required Assembly[] ScanAssemblies { get; set; }`

Assemblies to scan for \[Description\], \[MentorAction\], \[MentorAgent\], \[MentorTeam\], \[MentorSkill\], \[TeamMember\]. Usually: `options.ScanAssemblies = [typeof(Program).Assembly];`

### `SessionContextPlacement`

`public MentorSessionContextPlacement SessionContextPlacement { get; set; }`

Where the context of each request goes: the page open, the data it shows (`IMentorPageContext`), its address, the date and time. Default: [`MentorSessionContextPlacement.Instructions`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorsessioncontextplacement), as before.

At the end of the instructions they change with every minute and every page, and come before the whole conversation: a model that caches the prompt by its prefix, a local engine above all, reads the conversation again, which on a CPU can be many seconds before the first word. With [`MentorSessionContextPlacement.LastUserMessage`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorsessioncontextplacement) they go at the start of the user's last message, in a block the instructions name, and the instructions stay the same through the session: what changes is read again from the last message only. The user's name and roles, what the assistant remembers and the documents of [`MentorOptions.UseRag`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#userag) stay in the instructions; with RAG on, a turn with documents still changes them.

The block is put in the request sent to the model only: the conversation keeps the message as the user wrote it. A user's text cannot pass for the block: the mark that opens it is neutralised in the user's messages sent, and it carries a code only this session's instructions know. The classifiers, the specialists and the team members never see it.

Path A with local history only. With [`MentorOptions.UseServiceManagedHistory`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#useservicemanagedhistory), where the service would keep the block in every message, or with a pre-built [`MentorOptions.Agent`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#agent) (Path B), it has no effect, and a startup warning says so.

### `ShowA2AStatus`

`public bool ShowA2AStatus { get; set; }`

Show remote A2A agent status in the widget header. Displays a badge listing all configured remote agents. Useful during development. Default: false.

### `ShowAiDisclosure`

`public bool ShowAiDisclosure { get; set; }`

Shows, under the widget's header, a notice that tells people they are talking to an AI assistant, for the whole conversation: it does not scroll with the messages and cannot be closed. Default: true. Turning it off leaves the machine-readable marker every reply carries (`data-ai-generated` in the widget, `aiGenerated` on the hub, `/mentor/chat` and A2A): the two are separate tools for article 50 of the EU AI Act, and whether and how it applies is the application's to decide.

### `ShowHostedToolActivity`

`public bool ShowHostedToolActivity { get; set; }`

Report hosted-tool activity in the widget while the answer streams — "Searching the web…", "Running code…", the queries the provider issued, the pages it cited, the images it generated.

Hosted tools run remotely and can take several seconds during which nothing is streamed, so without this the widget looks frozen. Web results are shown through the same citation chips as RAG (they also need [`MentorOptions.ShowRagSources`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#showragsources)). Default: true.

### `ShowHostedToolsStatus`

`public bool ShowHostedToolsStatus { get; set; }`

Show the hosted-tools badge in the widget header (which provider tools are active), mirroring [`MentorOptions.ShowMcpStatus`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#showmcpstatus). Useful in development; leave off in production. Default: false.

### `ShowMcpStatus`

`public bool ShowMcpStatus { get; set; }`

Show MCP connection status in the widget header. Useful during development. Default: false (hidden in production).

### `ShowRagSources`

`public bool ShowRagSources { get; set; }`

Show citation chips (source links) below AI responses that used RAG context. Requires sources to include a `SourceUrl` or `Title`. Default: false

### `SkillFilter`

`public Func<AgentSkill, bool>? SkillFilter { get; set; }`

Filters the skills contributed by [`MentorOptions.SkillSources`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#skillsources). Return `true` to keep a skill. Applied through the framework's `FilteringAgentSkillsSource`, and it runs *before* de-duplication, so filtering a name out lets the next source offering that name through rather than leaving a hole.

Does not apply to skills MentorAgent discovered itself — those are your own code, and a predicate that silently removed them would be a debugging trap.

Usage:

```text title="Example"
// Only publish skills the current deployment is compatible with
options.SkillFilter = skill => skill.Frontmatter.Compatibility is null
                             || skill.Frontmatter.Compatibility.Contains("v2");
```

Default: null (every skill from every source is kept)

### `SkillSources`

`public IList<AgentSkillsSource> SkillSources { get; set; }`

Additional skill sources, using the Agent Framework's own `AgentSkillsSource` abstraction. Skills they yield reach the assistant through the same `load_skill` / `read_skill_resource` tools, the same catalogue prompt and the same gate as skills MentorAgent discovered itself — a host can therefore serve skills from a database, a remote catalogue, or Microsoft's own file source, without MentorAgent knowing where they came from.

The list is composed with the framework's decorators: `AggregatingAgentSkillsSource` preserves your registration order, `FilteringAgentSkillsSource` applies [`MentorOptions.SkillFilter`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#skillfilter) when set, `DeduplicatingAgentSkillsSource` keeps the first occurrence of a name, and `CachingAgentSkillsSource` is added when [`MentorOptions.SkillsRefreshInterval`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#skillsrefreshinterval) is set.

**An application skill always wins a name collision** — sources are consulted after MentorAgent's own discovery, and a duplicate name is skipped with a warning naming it. A host source can add capability; it cannot silently replace one of yours.

**The framework's file source reads a different dialect.**`AgentFileSkillsSource` validates each `SKILL.md` for YAML frontmatter, while MentorAgent's own convention is a heading followed by a prose paragraph. Both work — register the framework's source here and keep [`MentorOptions.SkillsFolder`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#skillsfolder) for yours.

Usage:

```text title="Example"
options.EnableSkills = true;
options.SkillSources =
[
    // Skills written to the Agent Skills specification, with YAML frontmatter
    new AgentFileSkillsSource("/srv/shared-skills"),
    new MyDatabaseSkillsSource(connectionString),
];
```

Default: empty — nothing here runs and skills behave exactly as before.

### `SkillsFolder`

`public string SkillsFolder { get; set; }`

Folder to scan for file-based skills. Each sub-directory must contain a `SKILL.md` file. Can be absolute or relative to the application content root. Default: "Skills"

### `SkillsRefreshInterval`

`public TimeSpan? SkillsRefreshInterval { get; set; }`

How long a skill list fetched from [`MentorOptions.SkillSources`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#skillsources) stays fresh before it is re-fetched. When set, the composition is wrapped in the framework's `CachingAgentSkillsSource` and shared by every session in the process: sessions reuse the list, and the first one built after the interval has passed fetches it again.

Without it (the default) every new session — each circuit, hub connection or SSE request — re-reads the sources when its coordinator is built, and keeps that list for its lifetime. Set it for a source that is remote or expensive.

Usage:

```text title="Example"
options.SkillsRefreshInterval = TimeSpan.FromMinutes(15);   // re-read the remote catalogue
```

Default: null (no cache: the sources are read once per session)

### `StrongChatClient`

`public IChatClient? StrongChatClient { get; set; }`

Optional **strong** model, used for the turns [`MentorOptions.RoutingStrategy`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#routingstrategy) sends to it; [`MentorOptions.ChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chatclient) is the **cheap** default — a real cost lever (small model for simple turns, strong model for complex ones). Routing is active when this is set and the strategy can decide: [`MentorRoutingStrategy.Custom`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorroutingstrategy) needs [`MentorOptions.UseStrongModel`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#usestrongmodel) or [`MentorOptions.UseStrongModelAsync`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#usestrongmodelasync), [`MentorRoutingStrategy.Semantic`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorroutingstrategy) an [`MentorOptions.EmbeddingGenerator`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#embeddinggenerator); [`MentorRoutingStrategy.Classifier`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorroutingstrategy) and [`MentorRoutingStrategy.Cascade`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorroutingstrategy) need nothing more.

### `Theme`

`public MentorTheme Theme { get; set; }`

Widget visual theme. Default: Default.

### `ToolFilterMaxTools`

`public int ToolFilterMaxTools { get; set; }`

Maximum number of matched business tools to keep when [`MentorOptions.EnableToolFiltering`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#enabletoolfiltering) is active. Core tools are always kept on top of this. Default: 12

### `ToolFilterMinScore`

`public float ToolFilterMinScore { get; set; }`

Minimum cosine similarity (0–1) a tool must reach to count as a match in **semantic** tool filtering (requires [`MentorOptions.EmbeddingGenerator`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#embeddinggenerator)). Higher = stricter (fewer tools sent). Cosine is a ranking, not a calibrated relevance: this line decides which actions match, not whether a message needs one (BUG-106). Default: 0.35

### `ToolFilterMinTools`

`public int ToolFilterMinTools { get; set; }`

When [`MentorOptions.EnableToolFiltering`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#enabletoolfiltering) keeps at least one action for a message, keep at least this many: the best-ranked ones, whatever their score, up to [`MentorOptions.ToolFilterMaxTools`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#toolfiltermaxtools). It does not apply to a message where no action clears [`MentorOptions.ToolFilterMinScore`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#toolfilterminscore): that one gets the three best-ranked actions that need no confirmation and no role (see [`MentorOptions.EnableToolFiltering`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#enabletoolfiltering)). Default: 0 — the threshold alone decides, as before this option existed.

A trade-off, measured both ways (BUG-099). Asked "Quante unità di iPad Air ci sono in magazzino?", a shop's stock-update action (0.351) was the only one above 0.35 and the actions that read stock ranked just below; sent alone, the write action was called with a quantity of 0. With 3, the readers travelled with it and nothing was written. But asked "Quanto ha speso Anna Ferrari?", the lone match above the line was the right search (0.361), and with 3 the model picked a by-email lookup the floor had added and answered "no such customer" four times of four, where the threshold alone answered correctly four of four. Neither setting tells a wrong lone match from a right one. The protection against a write answering a read is `[MentorAction(RequiresConfirmation = true)]` on actions that change data.

### `UseMemoryContext`

`public bool UseMemoryContext { get; set; }`

Enables contextual memory — the Mentor remembers preferences and actions across different sessions. One writer saves the facts: the post-turn extraction of [`MentorOptions.MemoryAutoCapture`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#memoryautocapture) when it can run, else the "remember" tool. The "forget\_all" tool clears them. ⚠️ IMPORTANT — userId and data isolation: - With authentication configured: each user has their own isolated memory, keyed by the ClaimTypes.NameIdentifier claim, else "sub", "oid" or the name, with the claim in the key ("sub:…", "name:…") so a name never matches another user's identifier. - Without authentication: the key follows [`MentorOptions.AnonymousIdentity`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#anonymousidentity). PerSession (default) gives each circuit, hub connection or SSE request its own key, so nothing is shared and nothing outlives the session; Shared uses the literal "anonymous" key for everyone — right for a single-user app, a cross-user leak on a website. For production with multiple users who should keep their memory, configure authentication. For real persistence across app restarts, register your own implementation of IMentorMemoryStore BEFORE AddMentorAgent():

```text title="Example"
// Redis
builder.Services.AddSingleton<IMentorMemoryStore, MyRedisStore>();

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

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

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

Default: false

### `UseRag`

`public bool UseRag { get; set; }`

Enables RAG — before each coordinator call the knowledge base is searched and relevant documents are injected into the system prompt. Requires an [`IMentorRagSource`](https://docs.mentoragent.net/api/novior-mentoragent-rag/imentorragsource) registered in DI. An `ITextSearch` is not wrapped for you (`TextSearchRagSource` is a placeholder): call it from your own [`IMentorRagSource`](https://docs.mentoragent.net/api/novior-mentoragent-rag/imentorragsource). Default: false

### `UseServiceManagedHistory`

`public bool UseServiceManagedHistory { get; set; }`

Set this when the [`MentorOptions.ChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chatclient) keeps the conversation on the **service** rather than in your process — the OpenAI / Azure OpenAI **Responses** client (which is what hosted tools require), Microsoft Foundry agents, Copilot Studio.

Those services return a conversation id, and the Agent Framework refuses to combine that with a local `ChatHistoryProvider` — every turn would fail with *"Only ConversationId or ChatHistoryProvider may be used, but not both"*. With this flag on, MentorAgent installs no history provider and lets the service own the conversation.

Consequences: [`MentorOptions.MaxSessionMessages`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#maxsessionmessages) and [`MentorOptions.EnableCompaction`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#enablecompaction) no longer apply (there is no local history to trim — a warning is logged if compaction is on), and any [`MentorOptions.ChatHistoryProvider`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chathistoryprovider) you configured is ignored. Everything else — tools, memory, RAG, guardrails, metrics — is unaffected. Default: `false`.

One more: a model response that gives two calls the same id is passed on as it came, with a warning. Without the flag MentorAgent gives the later call an id of its own (BUG-225); here the service holds the calls under the ids it generated, and a result sent under another id would answer none of them.

### `UseStrongModel`

`public Func<string, bool>? UseStrongModel { get; set; }`

Legacy synchronous predicate ([`MentorRoutingStrategy.Custom`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorroutingstrategy)): decides, from the latest user message, whether to escalate to [`MentorOptions.StrongChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#strongchatclient) (`true`) or stay on the cheap [`MentorOptions.ChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#chatclient). Prefer [`MentorOptions.UseStrongModelAsync`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#usestrongmodelasync), which is async and sees the whole conversation; avoid naive keyword/length rules here.

### `UseStrongModelAsync`

`public Func<IReadOnlyList<ChatMessage>, CancellationToken, Task<bool>>? UseStrongModelAsync { get; set; }`

Async, context-aware custom router ([`MentorRoutingStrategy.Custom`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorroutingstrategy)): given the whole conversation, returns `true` to escalate to [`MentorOptions.StrongChatClient`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#strongchatclient). Takes precedence over [`MentorOptions.UseStrongModel`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#usestrongmodel) when both are set — lets you call your own classifier/embedding logic. The decision applies to the whole turn.

### `VoiceBargeIn`

`public bool VoiceBargeIn { get; set; }`

Stops playback the moment the user takes the floor — presses the microphone or sends a message (roadmap \#17). Default: `true`.

This is press-to-interrupt, not acoustic barge-in: a browser cannot reliably listen through its own playback, and a microphone left open during synthesis transcribes the assistant.

### `VoiceHandsFree`

`public bool VoiceHandsFree { get; set; }`

Hands-free conversation: the transcript is sent when the user stops speaking, and the microphone reopens once the spoken answer finishes (roadmap \#17). Default: `false` — it holds the microphone across turns, which no application should do without being asked. Requires [`MentorOptions.EnableVoiceInput`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#enablevoiceinput) and [`MentorOptions.EnableVoiceOutput`](https://docs.mentoragent.net/api/novior-mentoragent-options/mentoroptions#enablevoiceoutput).

### `VoiceRate`

`public double VoiceRate { get; set; }`

Speech rate passed to `SpeechSynthesisUtterance.rate`. 1.0 is the browser default and the useful range is roughly 0.5–2.0. Default: 1.0.

### `VoiceStreaming`

`public bool VoiceStreaming { get; set; }`

Speaks each sentence as it streams, instead of reading the finished answer back (roadmap \#17). Default: `true`.

Voice output used to wait for the whole response, so the assistant stayed silent for the length of a paragraph and then recited it. Sentence-by-sentence playback keeps pace with the text. Turn it off only if your answers are reliably one line long.

### `WarmUpAtStartup`

`public bool WarmUpAtStartup { get; set; }`

Build one coordinator at start-up instead of making the first visitor wait for it. Default: `false`.

The work it moves off that first message is the shared kind: embedding the tool catalogue and the routing exemplars into the process cache, opening the shared MCP sessions, fetching the remote agent cards. Measured on the sample, the first message of a cold process took 3 seconds before a token appeared, and up to 50 when several circuits were starting their own MCP servers at once.

It runs once the host has started (after a web host starts listening) and a failure of the warm-up itself never fails the boot: an application that refuses to start because a remote MCP server is down would be the worse failure. An invalid configuration does stop the host, because the options are validated when it starts. It needs a generic host (ASP.NET Core, a worker): a MAUI app runs no hosted service, so there the option does nothing.

### `WelcomeMessage`

`public string? WelcomeMessage { get; set; }`

Welcome message when the chat is empty. If null, uses the default.

---

- Previous: [MentorHitlMode enum](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorhitlmode.md)
- Next: [MentorRoutingStrategy enum](https://docs.mentoragent.net/api/novior-mentoragent-options/mentorroutingstrategy.md)
