MentorAgent.Declarative
1.0.0-rc.15
dotnet add package MentorAgent.Declarative --version 1.0.0-rc.15
NuGet\Install-Package MentorAgent.Declarative -Version 1.0.0-rc.15
<PackageReference Include="MentorAgent.Declarative" Version="1.0.0-rc.15" />
<PackageVersion Include="MentorAgent.Declarative" Version="1.0.0-rc.15" />
<PackageReference Include="MentorAgent.Declarative" />
paket add MentorAgent.Declarative --version 1.0.0-rc.15
#r "nuget: MentorAgent.Declarative, 1.0.0-rc.15"
#:package MentorAgent.Declarative@1.0.0-rc.15
#addin nuget:?package=MentorAgent.Declarative&version=1.0.0-rc.15&prerelease
#tool nuget:?package=MentorAgent.Declarative&version=1.0.0-rc.15&prerelease
MentorAgent.Declarative
Release candidate — this version is a release candidate for 1.0.0, so
dotnet add packageneeds--prerelease. Breaking changes are still possible before 1.0.0; the release notes mark each one Breaking.
Optional package. Install it only if you want to define specialist agents in YAML files instead of C# classes. Everything MentorAgent does works without it.
Define a Level-2 specialist in a text file, drop it next to your application, and the assistant can hand off to it — no new class, no [MentorAgent] attribute, no recompile of the agent's behaviour.
Built on the Agent Framework's declarative agent factory (Microsoft.Agents.AI.Declarative).
Package Family
| Package | Install when |
|---|---|
| MentorAgent | Blazor Server app |
| MentorAgent.Server | Web API / headless backend, or Blazor Auto server-side project |
| MentorAgent.Blazor | Blazor WASM / Blazor Auto client project |
| MentorAgent.Abstractions | Never directly — it arrives with any of the above |
| MentorAgent.Declarative ← you are here | You want YAML-defined agents. Add it alongside MentorAgent or MentorAgent.Server |
Table of Contents
- What it does
- Getting started
- The YAML format
- Tools: named, not defined
- Security — a definition file is code
- When to use YAML and when to use C#
- Loading definitions from somewhere else
- Configuration options
- How to test it
- Why a separate package
- Requirements
- Related Packages
- License
What it does
MentorAgent's three-level agent model has a coordinator (L1) that can hand off to specialists (L2). Normally a specialist is a C# class:
[MentorAgent(Name = "ShippingAgent", Description = "Answers questions about deliveries")]
public class ShippingAgent // register it: builder.Services.AddScoped<ShippingAgent>();
{
[MentorAction(Description = "Looks up a tracking number")] // tool name: get_tracking
public string GetTracking(int orderId) => /* … */;
}
This package adds a second way to declare the agent — its name, its instructions, its model settings and which tools it may use — as a file:
kind: Prompt
name: ShippingAgent
description: Answers questions about deliveries and shipping costs
instructions: |
You handle shipping questions only. Use the available tools to look up real orders;
never invent a tracking number. If the question is not about shipping, say so and stop.
model:
options:
temperature: 0.2
tools:
- kind: function
name: get_order_status
- kind: function
name: get_all_orders
Both kinds end up in the same handoff graph, so route_to_specialist reaches them identically and the user cannot tell which is which.
Getting started
Installation
dotnet add package MentorAgent.Declarative --prerelease
Registration
using MentorAgent.Declarative; // AddMentorAgentDeclarative
using MentorAgent.Extensions; // AddMentorAgent
builder.Services.AddMentorAgentDeclarative(o => o.Directory = "Agents");
builder.Services.AddMentorAgent(o =>
{
o.ChatClient = chatClient;
o.AppName = "ShopFlow";
o.ScanAssemblies = [typeof(Program).Assembly];
});
Order does not matter — MentorAgent asks every registered agent source while it builds a coordinator. A coordinator is built lazily for each session scope (each Blazor circuit, each SignalR connection to MentorAgent.Server, each SSE request), on its first message, or once at startup when WarmUpAtStartup is on. So the definitions are read again for every new scope. An edited file reaches new sessions without a restart, existing sessions keep the agents they were built with, and the log lines shown under How to test it appear then, not when the application starts.
Make sure the files reach the output folder
A definition that is not copied is the most common way this feature appears not to work. In your .csproj:
<ItemGroup>
<Content Include="Agents\**\*.agent.yaml" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
If the directory is missing you get a warning naming the resolved path, [MentorAgent:Declarative] Directory '…' does not exist., when a coordinator is built: on a session's first message, or at startup with WarmUpAtStartup. That message exists because the failure is otherwise silent.
The YAML format
The schema comes from the Agent Framework, not from MentorAgent. The fields that matter in practice:
| Field | Meaning |
|---|---|
kind |
Prompt for a prompt-based agent. Required |
name |
The specialist's name. This is what appears in handoff logs — keep it stable |
description |
What this specialist is for. The coordinator reads it to decide when to route here |
instructions |
The agent's system prompt, used exactly as written. Use a \| block for multiple lines. MentorAgent adds nothing to it, including the grounding rule its generated specialist prompts carry, so write your own: state only facts a tool returned; if nothing provides what is asked, say so, and never estimate or invent a number, a name, a date or a status. Without it, a specialist asked for a figure its tools cannot return may make one up |
model.options |
temperature, topP, and other per-agent model settings |
tools |
Names of tools this agent may call — each entry needs kind and name, see below |
outputSchema |
Optional JSON-schema-style shape for a typed answer |
description is worth care: it is the coordinator's only basis for choosing this agent over another. "Handles orders" competes badly with "Handles orders, shipping and returns for existing customers".
Two format details that cost an afternoon
Neither is in the Agent Framework guide, and both fail in a way that points somewhere else.
Every tools entry needs kind. The reader accepts codeInterpreter, fileSearch,
function, webSearch and mcp, but function is the only one MentorAgent keeps: it binds to a
tool of your application. Omit kind and loading fails with NotSupportedException — not a
validation message naming the line.
The other four are not names of your tools, and they do not add anything. The Agent Framework would turn them into provider-hosted tools (web search, code interpreter, file search, or a remote MCP server at whatever endpoint the file gives), configured by the file alone. MentorAgent removes them from the agent when it loads and logs a warning naming them:
[MentorAgent:Declarative] 'shipping.agent.yaml': removed web_search. A definition file can only use the application's own tools (`kind: function`); web search, code interpreter, file search and MCP servers are configured by the host, through MentorOptions.HostedTools and McpServers.
The agent still loads, with its function tools only. If those tools cannot be taken out, the
whole agent is skipped with an error instead. To give the assistant these capabilities, configure
them in the host (MentorOptions.HostedTools, McpServers), where the host's own controls apply.
Folded scalars (>) are not supported by this reader. Use |, or a single line. With > the
parse error reports the end of the file, so you will look everywhere except at the block that
caused it:
description: > # ✗ parse error, blamed on the last line of the file
Handles shipping and delivery.
description: | # ✓
Handles shipping and delivery.
description: Handles shipping. # ✓
Tools: named, not defined
A tools: entry names a tool; it does not create one. The name must match one of your application's Level-1 methods: a public instance method with [MentorAction] or [Description] on a class in ScanAssemblies that is registered in DI, including a [MentorSkill] class. That is the whole list the factory is given. The methods of a C# [MentorAgent] specialist, MCP client tools, [MentorTeam] tools and the built-in tools (navigation, memory, UI actions) are not in it, so naming one of them binds nothing real (see below).
tools:
- kind: function # required — see below
name: get_order_status # must match exactly; a typo gives the agent a declaration with nothing behind it
The tool name is the snake_case of the C# member, with a trailing Async dropped:
GetOrderStatusAsync() → get_order_status, matched exactly and case-sensitively. Get it wrong and
nothing tells you at load time. The agent does not simply go without the tool: for a name that matches
nothing, the Agent Framework creates a declaration-only function of that name and gives it to the
agent. The model sees it and may call it, but there is no code behind it, so the call never runs and
the specialist has no data to answer with. Check every name against your [MentorAction] /
[Description] methods.
This is the design point of the whole package. MentorAgent hands the factory the application's real tool list, already wrapped in its gate, so a YAML agent calling create_order still hits:
RequiredRoles— the role check, exactly as a C# specialist does- human approval — the confirmation banner, if the tool requires one
- action feedback, per-tool metrics and tracing
An agent defined in a file therefore has no capability your application did not already have, and no shortcut around the controls on it. That holds for the other kind values too: webSearch, codeInterpreter, fileSearch and mcp entries would add provider-hosted tools outside your list, so MentorAgent removes them with a warning (see Two format details that cost an afternoon).
Security — a definition file is code
Read this before pointing Directory anywhere.
A definition chooses the model, writes the system instructions, and names the tools the agent may call. Anyone who can write that file can rewrite the assistant's persona and widen which tools it reaches for. That makes it code, whatever its file extension says.
- Load only from deploy-time locations. An application directory or an embedded resource. Never an upload folder, never a user-writable path, never a path built from request input.
- Review definitions like source. Put them in version control and through the same review as a
.csfile. - The gate still holds. A file cannot invent a tool or bypass a role check — that is enforced, not advisory. A
kind: functionentry resolves only against your own, gated tools, and awebSearch,codeInterpreter,fileSearchormcpentry is removed with a warning, so a file cannot add web search, a code interpreter or an MCP server of its own choosing. But it can instruct the agent to try things, so the controls on your tools remain the thing that actually stops it.
The second point is the one people skip: YAML feels like configuration, and configuration feels safe to let more people edit.
When to use YAML and when to use C#
| YAML | C# [MentorAgent] |
|
|---|---|---|
| Change an agent's instructions | Edit a file | Recompile |
| New tool / new logic | Not possible — tools stay in C# | Where it belongs |
| Compile-time checking | None; a bad tool name is silent | Full |
| Who can author it | Anyone who can edit a reviewed file | Developers |
| Fits when | Wording and routing get tuned often | The agent has real behaviour |
A good rule: behaviour in C#, phrasing in YAML. If you find yourself wanting a loop or a branch in a definition file, that agent wants to be a class.
You can mix freely — both kinds coexist in the same handoff graph.
Loading definitions from somewhere else
Definitions do not have to be files. Pass them as strings for agents stored in a database, a configuration service, or a test:
builder.Services.AddMentorAgentDeclarative(o =>
{
o.Definitions.Add("""
kind: Prompt
name: FaqAgent
description: Answers frequently asked questions about the shop
instructions: Answer briefly, in the user's language. Say so when you do not know.
""");
});
To ship definitions inside the assembly, the safest deploy-time location, embed them and pass their text as inline definitions:
<ItemGroup>
<EmbeddedResource Include="Agents\**\*.agent.yaml" />
</ItemGroup>
builder.Services.AddMentorAgentDeclarative(o =>
{
var asm = typeof(Program).Assembly;
foreach (var name in asm.GetManifestResourceNames()
.Where(n => n.EndsWith(".agent.yaml", StringComparison.Ordinal))
.Order(StringComparer.Ordinal))
{
using var reader = new StreamReader(asm.GetManifestResourceStream(name)!);
o.Definitions.Add(reader.ReadToEnd());
}
});
For a fully custom source — one that hits your own store, or refreshes on a schedule — implement IMentorAgentSource from the MentorAgent package directly and register it. AddMentorAgentDeclarative is one implementation of that interface, not a privileged path:
using MentorAgent.Core; // IMentorAgentSource, MentorAgentSourceContext
using Microsoft.Agents.AI; // AIAgent
public sealed class DatabaseAgentSource : IMentorAgentSource
{
// Called once per coordinator build, i.e. once per session, not once per process.
// Cache here if reading your store is expensive.
public async Task<IReadOnlyList<AIAgent>> GetAgentsAsync(
MentorAgentSourceContext context, CancellationToken ct = default)
{
// context.ChatClient — your MentorOptions.ChatClient wrapped only in the token meter
// (not the coordinator's pipeline), so usage lands in the metrics
// context.Tools — the Level-1 tools, already gated
…
}
}
builder.Services.AddSingleton<IMentorAgentSource, DatabaseAgentSource>();
Configuration options
| Option | Type | Default | Description |
|---|---|---|---|
Directory |
string? |
null |
Folder to scan, absolute or relative to the app base directory. Deploy-time paths only |
SearchPattern |
string |
"*.agent.yaml" |
File pattern inside Directory |
Recursive |
bool |
false |
Scan subdirectories too |
Definitions |
IList<string> |
empty | YAML supplied inline, loaded in addition to Directory |
ConfigurationSection |
string? |
null |
Name of the configuration section the YAML may reference. null exposes nothing |
SearchPattern and Recursive — what the scan is allowed to reach
Both defaults are deliberately narrow, and widening them is a decision worth making on purpose rather than by accident:
builder.Services.AddMentorAgentDeclarative(o =>
{
o.Directory = "Agents";
// Default "*.agent.yaml", not "*.yaml". A deployment folder holds other YAML — a CI file,
// a Helm values file — and reading one of those as an agent definition would at best fail
// loudly. Widen it only if your definitions genuinely do not carry the suffix.
o.SearchPattern = "*.agent.yaml";
// Default false. A nested folder is exactly where a definition gets added without review,
// and a definition file is code: it picks the model, writes the instructions and names the
// callable tools. Turn it on when your layout needs it, not "just in case".
o.Recursive = true; // now Agents/support/*.agent.yaml is loaded too
});
Inline Definitions are loaded first (named inline[0], inline[1], … in the log), then files in a stable order (sorted by path), so two definitions declaring the same agent
name resolve identically on every machine rather than depending on the file system's enumeration
order. A missing Directory is warned about — almost always a CopyToOutputDirectory miss, where
the assistant otherwise starts fine and is simply missing a specialist nobody thinks to look for.
ConfigurationSection — exposing configuration to a definition
Default null: no configuration reaches the YAML at all, and the definitions are self-contained.
Set it to expose one section, so a definition can reference values instead of hardcoding them:
// appsettings.json
{
"AgentSettings": {
"SupportEmail": "help@contoso.com",
"MaxRefund": "250"
}
}
builder.Services.AddMentorAgentDeclarative(o =>
{
o.Directory = "Agents";
o.ConfigurationSection = "AgentSettings"; // only AgentSettings:* reaches the YAML
});
The values arrive as Power Fx variables, one per key, named by the key's full configuration path: AgentSettings:SupportEmail, AgentSettings:MaxRefund, plus one for the section AgentSettings itself. The factory enumerates the section with AsEnumerable(), which does not make paths relative.
How a definition references them is part of the Agent Framework's declarative schema, not something
MentorAgent defines, so check the framework's documentation for the expression syntax before
relying on it.
Name a section. Never hand over the whole IConfiguration. The factory loads whatever
configuration it is given into the Power Fx engine as variables — one per key, in its
constructor. A single key that Power Fx rejects as a name (in the version this package uses, an
empty or whitespace-only key) takes the entire factory down before any definition is read. The
exception does not say which key it was: Power Fx's message is the fixed text Invalid name: ${name}.
The placeholder is never filled in, so do not look for a key literally named ${name}. This is not
hypothetical: a key contributed by an unrelated configuration provider produced
ArgumentException: Invalid name: ${name}
and no agents loaded at all. Naming one section bounds the blast radius to keys you control.
MentorAgent catches that failure and logs the cause rather than letting it surface as a generic startup error, then returns no agents:
[MentorAgent:Declarative] The agent factory rejected the configuration exposed to YAML
(AgentSettings). Every key in it becomes a Power Fx variable and must be a valid identifier.
Narrow MentorDeclarativeOptions.ConfigurationSection, or leave it null.
If you see it, the fix is a narrower section — or null, which is the right setting unless you
actually need substitution.
How to test it
- Put
shipping.agent.yamlin anAgentsfolder, withCopyToOutputDirectory. - Start the app and send a first message (or set
WarmUpAtStartup = true, which builds a coordinator at startup). The log should then show, at Information level (the tool count is every Level-1 tool handed to the factory, not the number the file names):[MentorAgent:Declarative] Agent 'ShippingAgent' loaded from shipping.agent.yaml (12 tool(s) available to it). [MentorAgent] Handoff: declarative agent 'ShippingAgent' added to workflow. - Ask something in that agent's area — "where is order 1001?". The answer should come back through it.
- Negative check — a bad file does not take the app down. Break the YAML deliberately: you get
'shipping.agent.yaml' could not be loaded — skipped, and everything else still starts. - Negative check — the gate holds. Name a tool carrying
RequiredRolesin the YAML and ask the agent to use it while unauthenticated. It must be refused, the same way a C# specialist is, and no action taken.
Why a separate package
Microsoft.Agents.AI.Declarative brings the Power Fx interpreter (YAML expressions are Power Fx), the Agents object model in three assemblies, Microsoft.ML.Tokenizers and several more.
That is a fair price for file-based authoring and pure overhead for everyone else, so it stays out of the MentorAgent core package. Installing this one is an explicit decision to pay it.
The reference is Microsoft.Agents.AI.Declarative 1.18.0-rc1, on the same 1.18 line as Microsoft.Agents.AI 1.18.0 in the core package, so adding it does not move the rest of the library onto a different Agent Framework version. The declarative package is still a prerelease upstream.
Requirements
- .NET 10
MentorAgent(orMentorAgent.Server) configured with aChatClient— declarative agents need one, and are skipped with a warning without it- A provider supporting the model options you use in the definitions
Related Packages
| Package | Purpose |
|---|---|
| MentorAgent | Blazor Server — full AI assistant |
| MentorAgent.Server | Any ASP.NET Core app — headless AI backend |
| MentorAgent.Blazor | Blazor WASM — SignalR client |
| MentorAgent.Abstractions | Shared contracts and UI components |
License
Apache License 2.0 — the full text is in the LICENSE file inside this package, with the copyright notice in NOTICE. Versions up to and including 1.0.0-rc.14 were released under the MIT License, which still applies to them.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- MentorAgent (>= 1.0.0-rc.15)
- Microsoft.Agents.AI.Declarative (>= 1.18.0-rc1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0-rc.15 | 47 | 10/7/2026 |
| 1.0.0-rc.14 | 62 | 10/3/2026 |
| 1.0.0-rc.13 | 66 | 9/29/2026 |
| 1.0.0-rc.12 | 67 | 9/23/2026 |
| 1.0.0-rc.11 | 70 | 9/23/2026 |
| 1.0.0-rc.10 | 67 | 9/19/2026 |
| 1.0.0-rc.9 | 68 | 9/19/2026 |
| 1.0.0-rc.8 | 67 | 9/18/2026 |
| 1.0.0-rc.7 | 72 | 9/16/2026 |
| 1.0.0-rc.6 | 73 | 9/14/2026 |
| 1.0.0-rc.5 | 71 | 9/13/2026 |
| 1.0.0-rc.4 | 76 | 9/9/2026 |
| 1.0.0-rc.3 | 78 | 9/4/2026 |
| 1.0.0-rc.2 | 85 | 8/24/2026 |
| 1.0.0-rc.1 | 83 | 8/19/2026 |
| 1.0.0-preview.5 | 80 | 8/12/2026 |
1.0.0-rc.15 - 2026-10-07
### Added
- **MentorAgent.Server:** `AddMentorAgentMcp()` and `AddMentorAgentA2A()` register the MCP server and the A2A
agent, each only when its option (`McpServerEnabled`, `A2AServerEnabled`) is on, as `AddMentorAgent` did up
to rc.14. `AddMentorAgentServer()` calls both, so a headless host changes nothing. `MapMentorAgentMcp()` /
`MapMentorAgentA2A()` without them stop the host at startup with an error that names the method to add.
- **MentorAgent:** a RAG source can see who is asking, from where and after what. MentorAgent now calls
`IMentorRagSource.SearchAsync(MentorRagRequest, CancellationToken)`, whose request carries the message,
`RagResultCount`, the last three turns of the conversation, the signed-in `ClaimsPrincipal`, and the
current page's name and data. Its default implementation calls `SearchAsync(query, maxResults)`, so every
existing source compiles and behaves as before.
- **MentorAgent.Abstractions:** `MentorRagResult.Metadata`, key/value facts about a document (a date, a
version). The model sees them under the document's title. They also travel with the citation chips: hub
clients receive `metadata` in `RagSourcesReady`. It is an init property, so the four-value constructor is
unchanged.
- **MentorAgent:** `IMentorRagSource.DefaultMinScore` lets a source declare the relevance threshold for its own
scale. It applies when the host has not set `RagMinScore`, in code or through configuration. A vector source
that declares one, say 0.6, now gets its documents to the model with the default options; before, the
default of 2 discarded every cosine score. The wrong-scale warning now says which threshold discarded
everything: the option, the source's or the default.
- **MentorAgent:** `MentorOptions.ChatReducer` replaces the reducer of the built-in in-memory history, and
`MentorChatReducer` (namespace `MentorAgent.Core`), the new default, can be given to a history provider of
your own. A `ChatReducer` that cannot apply — next to a provider you supply, or with
`UseServiceManagedHistory` — logs a warning.
### Changed
- The five packages are licensed under the Apache License 2.0 from this version, with a `NOTICE` file next to
`LICENSE`. Versions up to and including 1.0.0-rc.14 keep the MIT License.
- Package metadata: the authors are Emanuele Longo and Antonio Macrì, the copyright holders named in `NOTICE`;
the project page is https://mentoragent.net/.
- **Breaking — MentorAgent:** the MCP server and the A2A agent moved to **MentorAgent.Server**, together with
`MapMentorAgentMcp()` and `MapMentorAgentA2A()` (`EndpointRouteBuilderExtensions`, same namespace
`MentorAgent.Extensions`). `AddMentorAgent` no longer registers them. The reason: their packages,
`ModelContextProtocol.AspNetCore` and `Microsoft.Agents.AI.Hosting.A2A.AspNetCore`, require
`Microsoft.AspNetCore.App`, which does not exist on Android, iOS or Mac Catalyst. A MAUI app that referenced
MentorAgent built only for Windows (`NETSDK1082`). Now the MAUI Blazor Hybrid template, unchanged, builds
for `net10.0-android`, and it no longer needs its `Microsoft.Extensions.Logging.Debug` pin raised (`NU1605`).
MentorAgent now references `ModelContextProtocol` 1.3.0 and `A2A` 1.0.0-preview2, for the MCP and A2A
clients. MentorAgent.Server references `ModelContextProtocol.AspNetCore` 1.3.0 and `A2A.AspNetCore`
1.0.0-preview2.
**Migration.** A host that calls `AddMentorAgentServer()` changes nothing in its code. A Blazor Server host
that serves MCP or A2A without the hub adds the `MentorAgent.Server` package and calls
`builder.Services.AddMentorAgentMcp()` and/or `AddMentorAgentA2A()` after `AddMentorAgent()`; its usings
stay as they are. Code compiled against rc.14 must be rebuilt, because the type changed assembly.
A host no longer receives these packages transitively from MentorAgent, so it references the ones it uses:
`Microsoft.Agents.AI.Hosting`, `Microsoft.Agents.AI.Hosting.A2A`, `Microsoft.Agents.AI.Hosting.AspNetCore`,
`Microsoft.Agents.AI.Hosting.A2A.AspNetCore`, `Microsoft.Extensions.Hosting`, `Microsoft.Extensions.Logging.Console`
and the other `Microsoft.Extensions.Configuration.*` / `Logging.*` packages that came with it (BUG-119, S2).
- **MentorAgent:** the READMEs now place MCP and A2A where the code has them: in MentorAgent.Server.
- **Breaking — MentorAgent:** writes ask for confirmation by default. A `[MentorAction]` that declares neither
`ReadOnly = true` nor `RequiresConfirmation` is treated as an action that changes data. It shows the
confirmation banner, in the Blocking flow and in `HitlMode.Native` alike, like one declared
`RequiresConfirmation = true` (`RequireConfirmationForWrites`, new, on by default). Being gated, it is also
withheld from the MCP server (`tools/list`) and from the A2A agent card, and it is no longer among the
ungated actions the zero-match fallback may send on a message that matches nothing. With
`RequireActionForChangeRequests` on, more actions can now be required as the first call, because only
actions that ask are ever required. **Only `[MentorAction]` methods are covered**: a method marked with
`[Description]` alone asks for nothing and stays on `/mcp`, as before; mark it `[MentorAction]` to have the
default. **Migration:** declare `ReadOnly = true` on the actions that only read and
`RequiresConfirmation = false` on the writes that must run unasked, or set
`options.RequireConfirmationForWrites = false` for the rc.14 behaviour. At startup a warning lists the writes
that run without confirmation, in two lists: the `[MentorAction]` methods opted out, and the
`[Description]`-only methods not declared read-only with `IsReadOnlyAction`.
- **Breaking — MentorAgent:** no AI provider SDK comes with the package any more: it no longer references
`Azure.AI.OpenAI` 2.9.0-beta.1, `Azure.Identity` and `Microsoft.Extensions.AI.OpenAI [10.6.0]`, which the
library never used. It references `System.ClientModel` 1.10.0, the version those packages brought, because it
reads the HTTP status of a provider's `ClientResultException`. A host now adds its provider at the version it
chooses — for Azure OpenAI, `Azure.AI.OpenAI` and `Microsoft.Extensions.AI.OpenAI` (`AsIChatClient()`), plus
`Azure.Identity` for `DefaultAzureCredential`. With the Azure **Responses** API keep `OpenAI` at 2.10
(`Microsoft.Extensions.AI.OpenAI [10.6.0]`). On 2.11 or later, `GetResponsesClient()` from `Azure.AI.OpenAI`
2.9.0-beta.1 throws when it is called: `MissingMethodException: Method not found: 'Void
OpenAI.Responses.ResponsesClient..ctor(System.ClientModel.Primitives.ClientPipeline, OpenAI.OpenAIClientOptions)'`.
Chat Completions needs no hold: on `OpenAI` 2.14 its client is built where the Responses one throws (BUG-120,
S2).
- **Breaking — MentorAgent:** the built-in history keeps tool calls and their results. Up to rc.14 it was
trimmed by `MessageCountingChatReducer`, which drops every message carrying a function call or result: from
the next turn on, the model no longer saw the data a tool had read, and answered a follow-up by guessing or by
calling the tool again. The default is now `MentorChatReducer`: it counts every message, tool calls and
results included, against `MaxSessionMessages`, trims whole turns, oldest first, and never sends a result
without the call that produced it. In a lab run of 84 turns the turns answered correctly went from 85% to
95%, and tokens grew by about 9%. With the same `MaxSessionMessages` a session holds fewer turns (BUG-121,
S2). **Migration:** nothing to change. For the rc.14 behaviour set
`options.ChatReducer = new MessageCountingChatReducer(50)` (experimental, `MEAI001`).
- **Breaking — MentorAgent:** the options are validated when the host starts (`ValidateOnStart`). An ASP.NET
Core or worker host with an invalid configuration (no `ChatClient` or `Agent`, empty `ScanAssemblies`, both
`ChatHistoryProvider` and `ChatHistoryProviderFactory`…) now stops at start with the reason. Up to rc.14 it
started, listened and failed on the first request, although the READMEs said "throws at startup". A MAUI app,
which has no such start, still gets the error the first time MentorAgent is used (BUG-123, S4).
**Migration:** a host that started with an invalid MentorAgent configuration fixes it.
- **Breaking — MentorAgent:** a class listed in `[TeamMember(Tools = ...)]` is that member's toolbox only. Up to
rc.14 the scan also made its methods coordinator actions, published on `/mcp` and on the A2A card, because
the class is in DI as the README asks (BUG-125, S3). A `[MentorSkill]` class keeps its coordinator tools, by
the skill's own role. Member tools are also discovered by the same rules as every other action: public
instance methods declared on the class itself, with `[MentorAction]` or a non-empty `[Description]`. A
member no longer gets inherited or static methods, or a method with an empty `[Description]` (BUG-126, S3).
**Migration:** at startup an Information line names each class taken from the coordinator, the members
that list it and its methods (`… is listed in the Tools of …`). An action the coordinator must offer too
goes on a class not listed in `Tools`; a member tool inherited from a base class is declared on the listed
class.
- **Breaking — MentorAgent.Server:** `POST /mentor/approve` answers `404` when no turn waits for the answer any
more: it was answered already, the turn was stopped, or the connection that raised it has closed. Up to rc.14
every call got a `200`, so a client could not tell an approval that ran from one that reached nothing.
**Migration:** a client that treats any non-2xx answer as an error treats `404` as "nothing left to answer"
and closes its dialog. The WebAssembly client of MentorAgent.Blazor already does.
- **Breaking — MentorAgent:** `IMentorStateService.OnApprovalResponse` is no longer raised for an answer that
found no turn waiting: given after Stop, after the connection or the circuit closed, or a second time. Up to
rc.14 it was, in process and in the WebAssembly client, so a custom confirmation dialog that closed only on
this event closed in those cases too; now it stays open (BUG-129). **Migration:** close a custom dialog
when its buttons are clicked and on `OnBusyChanged(false)`, as the README's *Custom HITL confirmation
dialog* example does; keep `OnApprovalResponse` for an audit trail of the decisions that were acted on.
- **Breaking — MentorAgent:** on the hub, on `/mentor/chat` and over A2A, MentorAgent takes the caller from the
request — the hub connection's user, or `HttpContext.User` — before asking the host's
`AuthenticationStateProvider`, which now answers only where no MentorAgent transport runs, such as a Blazor
circuit (BUG-131). A host whose own provider added claims or roles to the user no longer has them seen
there. **Migration:** add them with an `IClaimsTransformation`, which ASP.NET Core applies to
`HttpContext.User` and so to the hub connection's user too.
- **Breaking — MentorAgent:** memory and `RateLimitPerUser` key a signed-in user by `NameIdentifier`, as
before, else by `sub`, `oid` or the name with the claim in the key (`sub:…`, `name:…`), like the owner checks
(BUG-142). A user known by name alone in rc.14 (Windows authentication) gets a new key. **Migration:** in
your memory store, rename those users' keys from `<name>` to `name:<name>`.
- **MentorAgent:** README — `ReadOnly` and `IsReadOnlyAction` say what `readOnlyHint` means on `/mcp`: some MCP clients
skip their own confirmation for such a tool, so a write declared read-only by mistake runs from them with nobody
asked. The same in the MentorAgent.Server README's MCP section.
- **MentorAgent:** README — `OnActionExecuting` carries the text shown to the user (the action's `Description`, or a
routing, team or hosted-tool label), not the tool name; `OnActionCompleted` carries the tool name, `OnActionFailed`
the tool name and the error. The same in the MentorAgent.Server (`ActionExecuting`) and MentorAgent.Blazor READMEs.
- **MentorAgent:** README — a tool result is a card by its shape, not by its .NET type: an object with a non-empty
string `kind`, or an array of them. Records of your own with a `kind` property are drawn as cards; the README says
how to avoid it.
- **MentorAgent:** README — the banner of the five READMEs says this is a release candidate for 1.0.0, not a
public preview. The *Extensibility* table lists `IMentorAgentSource`, `IMentorQueryEmbedding`,
`ClassifierChatClient` and `SkillSources`/`SkillFilter`, and its history-provider row points to
`ChatHistoryProviderFactory`: `ChatHistoryProvider` is one instance shared by every user.
- **MentorAgent:** `MentorOptions` XML documentation matches the code: `RequiresApproval` gets tool names
(MCP ones unprefixed, never a remote agent), `DashboardRole` guards the metrics endpoint only, routing per
`RoutingStrategy`, `AgentCardUrl` is a base URL (`agent-card.json`), no `ITextSearch` wrapper, an English
`RagSystemPromptTemplate`.
- **MentorAgent.Server:** the A2A comments say a task runs as the request's user; an anonymous caller is not held
by `RateLimitPerUser` across tasks.
### Removed
- **Breaking — MentorAgent:** the `MENTOR001` build check (`buildTransitive/MentorAgent.targets`) and its
`MentorAgentSkipOpenAIVersionCheck` switch. They stopped every build that resolved `OpenAI` 2.11 or later — a
project on the current Agent Framework, which never calls `GetResponsesClient()`, included (BUG-120).
### Fixed
- **MentorAgent:** README — the Requirements table lists the package's actual references, and its Blazor row
no longer contradicts the server package (any ASP.NET Core app with MentorAgent.Server). The MENTOR001 section
is replaced by *Provider package versions*, which says which call fails and on which versions instead of "the
process dies before serving a request".
- **MentorAgent:** the warm-up (`WarmUpAtStartup`) is registered always and decided on the real options when
the host starts. It was registered only if the registration-time snapshot asked for it, and that snapshot
swallowed whatever the `AddMentorAgent` callback threw: a callback that failed before
`WarmUpAtStartup = true` lost the warm-up without a word. Such an exception is now logged as a warning at
start, naming the options decided at registration (BUG-122, S3).
- **MentorAgent:** the warm-up starts on `ApplicationStarted`, after a web host is listening, as its
documentation said. It started inside `StartAsync`, before the other hosted services.
- **MentorAgent:** README — *When the options are read and checked*: the `AddMentorAgent` callback runs twice,
and `EnableObservability`, `ObservabilitySourceName`, `McpServerEnabled`, `A2AServerEnabled`, `AppName`,
`AppDescription` and `AgentCard` are read at registration, so a `Configure<MentorOptions>` registered later
does not reach them (BUG-124, S3). The same note is in the MentorAgent.Server README and in the XML
documentation of `AddMentorAgent`.
- **MentorAgent:** a team member is told *"These tools are read-only."* only when every one of its tools is
declared read-only (`ReadOnly = true` or `IsReadOnlyAction`) and none asks for confirmation. Every member
was told so, whatever its tools did (BUG-125).
- **MentorAgent:** README — *Which methods become tools*: inherited and static methods are not discovered, and
a method with `[Description]` alone has no roles and no confirmation, so with tool filtering it can be sent
when a message matches nothing (BUG-127, S4). The same in the MentorAgent.Server README.
- **MentorAgent.Server:** a confirmation outlived the connection that raised it. When the owner's hub
connection closed, the hub forgot who had raised it but the turn kept waiting: anybody who knew the action id
could approve it with `POST /mentor/approve`, with no owner left to check, and the action ran. Now the end of
a connection — and of a Blazor Server circuit — cancels the turn running in it, with its confirmation
(BUG-128, S2).
- **MentorAgent:** `OnApprovalResponse` is raised only when the answer reached the turn that was waiting for it.
After Stop the request stayed pending, so a late Confirm raised `OnApprovalResponse(…, true)` for an action
that never ran — in process, and in the WebAssembly client, which raised it before sending the answer. Now
the request leaves the pending list with its wait, and the WebAssembly client raises the event once the
server has accepted the answer (BUG-129, S3).
- **MentorAgent.Server:** the owner of a connection is recognised without a `NameIdentifier` claim.
`/mentor/approve`, `/mentor/cancel` and `/mentor/session` read `NameIdentifier` alone, and so did the key that
binds a session snapshot to its owner: with Windows authentication, or JwtBearer with
`MapInboundClaims = false`, the owner was refused (`403`) its own confirmations, Stop and session. Now they
read `NameIdentifier`, then `sub`, then `oid`, then the identity's name, as the MCP rate limit does. A name
never matches another user's identifier. Snapshots saved by rc.14 for a user with `NameIdentifier` still
restore (BUG-130, S3).
- **MentorAgent.Server:** in a Blazor Web App the hub, `/mentor/chat` and A2A know who is calling. MentorAgent
read the user from the host's `AuthenticationStateProvider`; with `AddRazorComponents()` that is Blazor's,
which throws outside a circuit, and the server's bridge, registered with `TryAdd`, lost to it. Every
`RequiredRoles` action was refused to everyone with an error logged on each call, and memory and
`RateLimitPerUser` followed a connection instead of a person. A Blazor Server app serving A2A without the hub
had no bridge at all. Now each transport records its caller and MentorAgent reads it first; the host's
registration is left as it is, whatever the order of `AddRazorComponents()` and `AddMentorAgentServer()`.
When the server's own provider is the one registered, it also takes a circuit's user from Blazor
(`IHostEnvironmentAuthenticationStateProvider`), as Blazor's own does (BUG-131, S2).
- **MentorAgent:** a signed-in user with only `sub` or `oid` (JwtBearer with `MapInboundClaims = false`) was
keyed per session, so memory and `RateLimitPerUser` did not follow the person (BUG-142, S3).
- **MentorAgent.Server:** README — *CORS* and the JWT example under *Authentication* show the whole pipeline:
`UseCors` → `UseAuthentication` → `UseAuthorization` → `Map…`. A `Program.cs` that protected the hub and did not
call `UseAuthentication()` and `UseAuthorization()` itself got them from ASP.NET Core ahead of `UseCors`: the
browser's preflight for `/mentor-hub/negotiate` was answered `401` without CORS headers, and a client on another
origin never connected (BUG-132, S2). The MentorAgent.Blazor README says the same under *Stop and confirmations*.
- **MentorAgent.Server:** README — the complete React, Angular and Vue components send the hub's identity on
approve, cancel and session (a `getToken` function, or cookies with `credentials: 'include'`), and show an error
when the server refuses an answer or a Stop. They sent a bare `fetch`: with an authenticated hub the server
answered `401`, the dialog closed and the turn stayed waiting, with nothing on screen (BUG-133, S2). The Angular and
Vue components also take an absolute hub URL (`MENTOR_HUB_CONFIG`, `useMentorHub(hubUrl, getToken)`) and send
approve and cancel to the hub's origin.
- **MentorAgent.Server:** README — the three components free the UI when the connection drops (`onreconnecting`,
`onclose`) and restore the saved conversation in `onreconnected`; the events table lists the three callbacks. A
connection lost mid-turn left the UI busy, even after the client reconnected: the new connection never sends
`BusyChanged(false)` for that turn (BUG-134, S3).
- **MentorAgent.Server:** README — the complete React component shows the assistant's replies. It added an empty
bubble for every reply: it reset the streaming buffer before React ran the update that read it (BUG-136, S2). The
three components no longer add an empty bubble when a segment closes before a confirmation.
- **MentorAgent.Blazor:** README — the client setup example has `using MentorAgent.Abstractions.Models;`. Without
it `MentorLanguage` and `MentorTheme` did not compile (`CS0103`) (BUG-135, S3).
- **MentorAgent:** README and XML documentation — `RequireConfirmation = false` turns off the confirmations of your
actions, MCP-client servers and `RequiresApproval`, not the approvals a hosted MCP server asks for
(`RequireApproval`, `AlwaysRequireApprovalTools`). The README and the XML documentation said it disabled every
confirmation dialog (BUG-137, S3).
- **MentorAgent:** README — voice input: your server receives only the transcript, but some browsers, Chrome among
them, send the audio to their maker's speech service. The README said nothing left the page except the transcript
(BUG-138, S3). The same in the MentorAgent.Blazor README.
- **MentorAgent:** with `HitlMode.Blocking`, Confirm ran a `RequiredRoles` action for a user who had lost the role
while the banner waited: the roles were read only before the banner. They are read again after the answer, as in
Native mode, also in specialists and teams (BUG-143, S3).
- **MentorAgent:** after Cancel on a specialist's confirmation, reached through `route_to_specialist`, the answer
could report a problem that never happened: the coordinator got only the specialists' words, and a team with
`HandoffTo` had just told it to execute. The delegation's result now starts with `DECLINED BY THE USER`, names the
declined action and any that ran beside it, and carries the rule of a declined action; an approved team's result
says a declined confirmation is the user's decision (BUG-144, S3).
- **MentorAgent:** README — Blazor Web App: `@rendermode="InteractiveServer"` on `<ChatWidget />`, the simplest
setup offered for every Auto app, needs *Interactivity location: Per page/component*. With *Global* it throws
`NotSupportedException` once the layout runs in WebAssembly, and the WebAssembly template fails with HTTP 500 until
its server adds `AddInteractiveServerComponents()` and `AddInteractiveServerRenderMode()`. The README also sent the
CSS/JS links to an `index.html` a Web App does not have: prerendered, the widget writes them itself; otherwise they
go in `App.razor` (BUG-140, S2). The same in the MentorAgent.Blazor README.
- **MentorAgent.Blazor:** README — the client project of a Blazor Web App registers no `HttpClient`, and without one
the WebAssembly runtime did not start (`CannotResolveService`); the README said the template's was enough. The
client snippets now register one (BUG-139, S2). The same in the MentorAgent and MentorAgent.Server READMEs.
- **MentorAgent.Blazor:** in WebAssembly a relative `HubUrl`, the default `/mentor-hub` among them, never reached
the server: the .NET SignalR client parsed it as `file:///mentor-hub` and the widget did not connect (`TypeError:
Failed to fetch`). It is now resolved against the page's base address, as a browser does (BUG-145, S2).
- **MentorAgent:** a `[MentorTeam]` stopped discussing after its first activations in a session: every run shared
one group-chat manager, whose iteration count was never reset, so once `MaxIterations` was spent the team was
activated with no member speaking and returned nothing. Each run now gets its own manager (BUG-146, S2).
- **MentorAgent:** a tool that threw showed the exception's text in the chat even with `OnException` set, and a
failure after the action (`OnToolResult`, the cards, a screen update) reported an action that had run as failed,
so the model could run it again. Only the tool's own exception is a failure now. With `OnException` set the chat
shows the app's message or a localized "The action could not be completed.", never the exception's text; the
model still gets the app's message, else the exception's. An `OnToolResult` that throws withholds the result, as
on the MCP server. A Stop during the action is not logged as a failure. The widget no longer shows the text of a
send that failed (BUG-147, S2).
- **MentorAgent:** the gate's Warnings named the user and carried a tool's whole exception. They name a fingerprint
(`caller#…`, the same as the MCP audit line) and the exception's type; the whole exception is logged at Debug
(BUG-148, S3).
- **MentorAgent:** with a `Language` other than English the input box of a server-hosted widget still read "Type
a message...": `MentorOptions.InputPlaceholder` defaulted to that text and hid the translation. It is now
`string?` with no default, as in MentorAgent.Blazor, and an unset placeholder uses the localized one. In
English the default text is now "Write a message..." (BUG-149, S3).
- **MentorAgent.Abstractions:** the widget's stylesheet loaded Sora and JetBrains Mono from Google Fonts, a
request to a third party on every page that rendered the widget, refused under `default-src 'self'` and absent
offline. The two fonts now ship in the package (`wwwroot/fonts`: variable WOFF2, Latin and Latin Extended, SIL
Open Font License 1.1), which is about 110 KB larger, and the stylesheet requests nothing from another origin.
The avatar's size and a table column's alignment moved from style attributes to the stylesheet, so with the
default options the widget needs no `style-src 'unsafe-inline'`; `Theme` Dark or Minimal and `PrimaryColor`
still write a `<style>` block, and `MentorDashboard` uses style attributes (BUG-150, S2).
- **MentorAgent:** the error for a missing model suggested `new OllamaChatClient(...)`, a type in no package
the library brings, and `AIProjectClient` without its package. Each example now names its package; Ollama
goes through its OpenAI-compatible endpoint (BUG-151, S3).
- **MentorAgent.Declarative:** README — the install command lacked `--prerelease`, which `dotnet add package`
needs while only prereleases exist; so did the two `MentorAgent.Server` commands in the MentorAgent README.
The *License* sections point to the package's `LICENSE` (BUG-141, S3).
### Known issues
- **MentorAgent:** BUG-074 (S3): with `MentorshipLevel.Proactive`, the assistant can still offer next steps the
application does not have — 3 in 15 turns on the Blazor Server sample after the rc.14 rule, 5 before ("add it
to the cart", "the available deals", "plan a reorder"). Left open for 1.0.0 by the product owner's decision:
the closing offers keep their current wording.
- **MentorAgent:** BUG-118 (S2) is closed for hosts that opt in to `RequireActionForChangeRequests` (added in
1.0.0-rc.14). Without it the model can still narrate an action it never performed — "Stock aggiornato" with no
tool called, or "Ecco la pagina Prodotti" with no navigation — and with it a complete request the classifier
does not recognise runs as without the option.
- BUG-106's residue is closed for hosts that opt in to `RequireLookupForRecordQuestions` (added in 1.0.0-rc.14);
without it, a catalogue question can still be answered with an offer to look the value up.
- **MentorAgent:** with `HitlMode.Native`, a confirmation closed by Stop is closed only in memory: a snapshot saved
in that state and restored still carries the open request, which can make the next turn fail as in BUG-116.
The full history is in CHANGELOG.md, inside this package.