MentorAgent.Declarative
1.0.0-rc.12
dotnet add package MentorAgent.Declarative --version 1.0.0-rc.12
NuGet\Install-Package MentorAgent.Declarative -Version 1.0.0-rc.12
<PackageReference Include="MentorAgent.Declarative" Version="1.0.0-rc.12" />
<PackageVersion Include="MentorAgent.Declarative" Version="1.0.0-rc.12" />
<PackageReference Include="MentorAgent.Declarative" />
paket add MentorAgent.Declarative --version 1.0.0-rc.12
#r "nuget: MentorAgent.Declarative, 1.0.0-rc.12"
#:package MentorAgent.Declarative@1.0.0-rc.12
#addin nuget:?package=MentorAgent.Declarative&version=1.0.0-rc.12&prerelease
#tool nuget:?package=MentorAgent.Declarative&version=1.0.0-rc.12&prerelease
MentorAgent.Declarative
Preview Release — MentorAgent is currently in public preview. APIs may change before the stable release.
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
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
MIT — the full text ships in the repository's LICENSE file.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- MentorAgent (>= 1.0.0-rc.12)
- 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.12 | 46 | 9/23/2026 |
| 1.0.0-rc.11 | 51 | 9/23/2026 |
| 1.0.0-rc.10 | 54 | 9/19/2026 |
| 1.0.0-rc.9 | 56 | 9/19/2026 |
| 1.0.0-rc.8 | 58 | 9/18/2026 |
| 1.0.0-rc.7 | 56 | 9/16/2026 |
| 1.0.0-rc.6 | 63 | 9/14/2026 |
| 1.0.0-rc.5 | 58 | 9/13/2026 |
| 1.0.0-rc.4 | 68 | 9/9/2026 |
| 1.0.0-rc.3 | 72 | 9/4/2026 |
| 1.0.0-rc.2 | 78 | 8/24/2026 |
| 1.0.0-rc.1 | 78 | 8/19/2026 |
| 1.0.0-preview.5 | 72 | 8/12/2026 |
1.0.0-rc.12
Found by re-driving the published rc.11 on the six NuGet sample applications, with a Blazor Server host driven as the A2A peer for the first time on published packages and every remote answer held against the peer's REST data, then by a pre-publication pass of this package on the same six applications. One API addition, opt-in: MentorOptions.ToolFilterMinTools (default 0 - tool selection is unchanged from rc.11).
=== FIXED
- BUG-100 (S2): a confirmation-gated action (RequiresConfirmation or the RequiresApproval predicate) called on an A2A task held the task until the calling agent gave up: both the blocking and the native approval path waited for an answer nobody could give. It is now refused at once, in both modes, and the model is told it has to be done from the application itself. The agent card already left these actions out.
- BUG-098 (S2): an action with NavigateTo ran, then the auto-navigation read NavigationManager.Uri, which throws in a scope with no circuit, and the tool was reported failed - so the model ran it again and the caller was told it had not happened. Seen over A2A on a Blazor Server host, reachable since rc.11 made that work (BUG-083), and possible on any turn served without a circuit on a host that registers a Blazor NavigationManager. A turn from another agent no longer auto-navigates, and a page URL that cannot be read no longer turns a completed action into a failure. An exception from your own OnToolResult hook still reports an action that already ran as failed: keep that hook from throwing.
- BUG-097 (S3): on a Blazor Server host with memory or RateLimitPerUser on, every A2A task logged BUG-072's warning ("The AuthenticationStateProvider threw ... check the provider can be read from a scoped service"), and a role-gated tool the model reached for was logged as an Error with a stack trace and answered "Error during authorization check". The provider is still asked, so a headless host keeps an authenticated peer's identity as in rc.11. When it cannot be read on a task from another agent, the task is anonymous with a Debug line, and a gated tool is refused as for any anonymous caller - the usual "Tool ... blocked" warning, no Error, no stack trace.
=== MITIGATED, NOT FIXED - read this if you use EnableToolFiltering and have actions that change data
- BUG-099 (S1, present since tool filtering existed; the published rc.10 did the same): on hosts with EnableToolFiltering on (off by default), a question about stock set the stock to 0. The filter sends only the actions whose cosine score clears ToolFilterMinScore; for "Quante unita' di iPad Air ci sono in magazzino?" that was one action, the stock UPDATE (0.351), while the actions that read stock ranked just below (0.29-0.32). Handed one write action for a read question, the model called it with a quantity of 0 - over SSE 3 times of 3, over A2A 4 of 4. NEW: ToolFilterMinTools - once one action clears the threshold, send at least this many best-ranked ones, whatever their score. It is a trade-off, measured both ways, so it is off by default: with 3 the stock question read the stock and wrote nothing, but "Quanto ha speso Anna Ferrari?", whose lone match above the line was the right search, got a by-email lookup the floor had added and answered "no such customer" 0 times of 4, against 4 of 4 with the threshold alone. What protects you is [MentorAction(RequiresConfirmation = true)] on every action that changes data: a person is asked, and over A2A the action is refused (BUG-100). The sample applications now gate their stock update.
=== VERIFIED ON THE PUBLISHED rc.11 (no change)
- BUG-083: a Blazor Server host serving A2A tasks, questions put straight to its /a2a - 6 of 6 true (rc.10: every task empty).
- BUG-082: from the React sample, 8 remote-addressed questions, 8 tasks on the peer, none answered by a local specialist.
- BUG-084 (SafetyCheckTimeout with SafetyCheckFailure = Block) on a Blazor Server and a headless host; BUG-086 (a signed-in WebAssembly user's confirmation and Stop reach the hub's origin with the token); BUG-094 (a webSearch entry in a YAML definition removed with a warning).
=== PERFORMANCE
- rc.11 against this package on the six NuGet sample applications' ApiServer, same host and day, nothing else running, six questions in three rounds: median time to first character 8.9 s against 7.2 s, whole reply 12.6 s against 11.2 s, each faster in 9 of 18 paired turns, the same number of model calls, input tokens equal within 0.1 %. rc.10 against rc.11, measured the same way before: 11.8 s against 10.9 s, input tokens +1-2 % on delegated turns (the note rc.11 added to each delegated answer, saying whose answer it is) and under 1 % elsewhere.
=== STILL OPEN
- BUG-099 (S1): mitigated as above; a structural answer (actions declared read-only) is for a later version.
- BUG-074 (S3): MentorshipLevel.Proactive offers actions the application does not have. Measured, recorded, a product-voice decision.
1,733 tests green, build 0 warnings / 0 errors.
1.0.0-rc.11
Found by re-driving F2 (A2A client -> live peer) on the published rc.10 with every answer held against the peer's REST data AND its task log, and by running the Blazor Server sample on a local model (Ollama). BUG-081 is closed on the published packages. API additions: SafetyCheckTimeout and SafetyCheckFailure; auditing the five READMEs against the code then found eleven code defects behind the text, all fixed below.
=== FIXED
- BUG-082 (S2): a LOCAL specialist's answer reached the user as the REMOTE agent's. "Chiedi a ShopFlowRemote: quanto ha speso Anna Ferrari?" came back in 8.8 s with no task on the peer, reading "... EUR 5.800,00 sull'istanza ShopFlowRemote": the coordinator had called route_to_specialist with the addressee tidied out of the request and no specialist, the router gave a question about a customer to the local CustomerAgent, and the tool returned that agent's words with nothing to say whose they were. The figure matched only because the samples share their seed data. On a host that has remote agents every delegated answer now opens with its source: a local specialist answered -> a note that no remote agent was contacted and an instruction to call once more with specialist set if the user meant one (the second call carries the name, so the retry is bounded); the remote agent answered -> "that system's data, not this application's"; the remote agent was named and never took part -> "NOT X'S ANSWER", plus a warning for the operator. "Remote" means every configured peer, including one whose card could not be fetched when the session was built. The router's list marks remote entries, and its rules say that a request addressed to a remote agent is about that system's data whatever its subject. Hosts without remote agents, and turns that arrived over A2A, read byte for byte what they read before.
- BUG-083 (S2, present in rc.10): a Blazor Server host could not answer another agent - every A2A task it received completed with an empty message. A turn served over A2A has no circuit; AppContextProvider read NavigationManager.Uri unguarded, and it threw "'RemoteNavigationManager' has not been initialized". The caller saw an empty reply and told its user the remote agent was unavailable. The read is guarded, and the A2A handler now FAILS a task that produced no words instead of completing it empty. Headless hosts (MentorAgent.Server) were not affected.
- BUG-084 (S2): the safety checks had a fixed 15-second limit and failed open, so on a slow model (a local one on a CPU) the input check was skipped on every message, with a stack trace; Stop could not interrupt the input check; and a custom InputGuardrail that honoured its cancellation token threw out of SendMessageAsync, leaving the widget busy for good. NEW: SafetyCheckTimeout (default 15 s; zero or negative = no limit, and Stop still cancels) and SafetyCheckFailure (Allow - the default and the previous behaviour - or Block: refuse the message with a localized "can't check it right now, try again", or withhold the reply). Both apply to the input and the output check, built-in or custom. A timeout logs one line that says what to change; an error keeps a single stack trace per turn (BUG-071). The output check now runs on ClassifierChatClient, like the input check. Nothing changes for a host that sets neither option.
=== FIXED - found by auditing the five READMEs against the code (285 findings confirmed by a second reader: 264 were documentation, the rest code)
- WebAssembly client: answering a confirmation and Stop now go to the hub's origin with the hub's bearer token (AccessTokenProvider). They used the host's HttpClient with a relative URL and no token, so a signed-in user's confirmations and Stop were answered 401 by the owner check.
- ConfigureChatClientPipeline is built with the host's services: the documented b => b.UseLogging() made every coordinator build fail.
- ChatWidget CardTemplate: a MentorCardView inside a template reaches the widget's action dispatcher; its buttons did nothing.
- UI actions registered by a hub client (WebAssembly, React, MAUI) keep their parameter hint, and the WebAssembly page context generates the same hints as Blazor Server.
- navigate_to no longer waits for SignalReady where no page can send it (hub, SSE and A2A turns): each navigation to a page with HasUIActions cost the full ReadyTimeout and a warning.
- McpServerPath and A2AServerPath are the default paths of MapMentorAgentMcp() / MapMentorAgentA2A(); nothing read them.
- [MentorAction(ProactiveHint)] reaches the model, appended to the tool description as "Guidance: ..." (not used by semantic tool filtering); it was stored and never read.
- SkillsRefreshInterval caches the composed skill sources once per process; each session built its own cache, so the option did nothing.
- MentorAgent.Declarative: webSearch, codeInterpreter, fileSearch and mcp entries in a YAML definition are removed with a warning; only kind: function is kept. They created provider-hosted tools outside the gate, against the package's promise that a definition file cannot add a capability.
- ChatInput composed without ChatWidget no longer throws on the first message (an unguarded JS call ended the Blazor Server circuit). The output safety check is metered under ClassifierChatClient's model id.
- Documentation: 264 corrections across the five READMEs, among them attribute examples that did not compile, SignalR enums arriving as numbers, the anonymous-identity and rate-limit text (per-session since rc.5), how to protect and what to expect from /mcp and /a2a. XML docs and the server's startup warning match the PerSession default.
=== VERIFIED ON THE PUBLISHED rc.10 (no change)
- BUG-081: F2 on all five package columns - 38 remote-addressed turns, 36 delegated, 36 true figures, none invented, none bounced; "quanti clienti Premium" put straight to the published ApiServer's /a2a endpoint: 10 of 10 (rc.9: 3, 4), and "ordini Pending" 5 of 5.
- BUG-080: one task per turn on the peer, none bounced back, with ApiServer and React up together.
=== STILL OPEN, UNCHANGED
- BUG-074 (S3): MentorshipLevel.Proactive offers actions the application does not have. Measured, recorded, a product-voice decision.
1,721 tests green, build 0 warnings / 0 errors.
1.0.0-rc.10
Found by re-driving the release matrix's open cells on the published rc.9. BUG-080 holds: in the topology that looped (two hosts, each the other's remote agent) a delegated request is one task on the peer and none bounced back, and A2A context is per scope on every caller. With remote answers finally arriving, they could be compared with the data - and several were not true. No API change.
=== FIXED
- BUG-081 (S2): a turn SERVED over A2A stated figures no tool had returned. "Quanti clienti Premium ha?" asked through a caller came back as 18; put straight to the peer's /a2a endpoint, as 3, 4, 11, 16, 6, 6, 8 - there are 2. Tools were offered every time and none was called, while the same server on the same question over SSE called search_customers and said 2. The only difference was the line rc.8 added to an A2A turn's context - "carry the request out with your tools and reply with the result itself" - obeyed in the wrong order: a result at once. (BUG-079's "87 customers" was very likely this.) The notice is now a procedure: FIRST call the tool; every number, name, date or status comes from a tool result of THIS turn; if no tool has it, reply only that the application cannot provide it. A rule in a prompt lowers a rate and does not remove a behaviour - measured live, the notice alone gave 3 grounded answers in 5 - so there is a structural backstop, each step of it added because the one before was measured and was not enough. When an A2A-served turn ends without the coordinator reaching for ANY tool, the handler discards the reply and puts the same request once more, with the turn's context saying why and with a tool call REQUIRED on that attempt's first model call. If the second attempt too is tool-less, the classifier model is asked whether the reply states a value of the application's live records (a model, not a pattern: "9 clienti Premium" and "reso entro 30 giorni" both contain a number); on anything but a clear no the caller receives "NOT GROUNDED: ... do not present a figure" instead of the value. Two attempts, never three; the extra turn is paid only by tool-less A2A requests; a tool-less reply that states no data is returned as it is. Measured on the package-built ApiServer, the hardest host: 15 of 15 correct, none invented, none withheld (rc.9: 18, 3, 4 for a true 2).
- BUG-081, second half: the caller's own name for the peer travels inside the request ("quanti prodotti ha ShopFlowRemote?") and nothing told the peer that name means ITSELF - it answered "I have no access to ShopFlowRemote" 3 times in 6, without calling a tool. The notice now says so, and a MentorAgent caller sends its name for the peer as A2A message metadata (mentoragent.addressedAs), which the peer reads into the notice. The value arrives from another machine and goes into a prompt: only one token of letters, digits and - _ . (64 characters at most) is accepted; anything else is dropped whole.
=== VERIFIED ON THE PUBLISHED rc.9 (no change)
- BUG-080: API and React up together, remote turns driven from every column - one "Task received" per turn on the peer, each followed by "This turn arrived over A2A: remote agent(s) ... are not offered to it", zero tasks bounced. A2A context per scope closed on the React client and the WebAssembly client (one context across a connection's turns, a different one for a second connection).
- BUG-073, residual: the cold first turn of a fresh Chat Completions process delegated this time (route_to_specialist -> OrderAgent, real data).
=== STILL OPEN, UNCHANGED
- BUG-074 (S3): MentorshipLevel.Proactive offers actions the application does not have. Measured, recorded, a product-voice decision.
1,667 tests green, build 0 warnings / 0 errors.
1.0.0-rc.9
Found by re-driving the release matrix's open cells on the published rc.8 - the first published build on which an A2A round trip completes (BUG-078 had kept every one from finishing). T6/T7 closed on every host: route_to_specialist reaches OrderAgent and the declarative ShippingAgent, with real data, and the router's call is metered. The first remote delegation ever driven between the PUBLISHED samples found what was behind it. No API change.
=== FIXED
- BUG-080 (S2): a request delegated over A2A was delegated ONWARD by the peer, and two hosts that peer each other never stopped. Live: "Delega a ShopFlowRemote: quanti prodotti a catalogo?" on one sample produced five tasks on its peer and four on the peer's peer (each ~5,500 input tokens), 141 seconds, no answer - until a server was stopped by hand. Two causes. (1) rc.8 made route_to_specialist prefix the workflow's input with "[Specialist requested: NAME]" for the LOCAL router (BUG-076), and the A2A client sent the last user message verbatim: the marker crossed the wire and the peer's coordinator read it as its own order - every sample calls its remote agent "ShopFlowRemote". The marker now has one writer and one remover (SpecialistMarker), and the A2A client strips it on both the streaming and the non-streaming path. (2) Structural: a turn that ARRIVED over A2A was offered the host's remote agents like any other. It no longer is - they are not built for that scope and are absent from the coordinator's prompt, the router's prompt and the description of route_to_specialist; one Debug line names what was withheld. The host's own specialists, declarative agents, teams and tools still serve the request. Consequence, documented in the README: chaining A -> B -> C through a MentorAgent host is not supported in 1.0 (before rc.8 no chain could complete a single hop). Verified live on the fixed source in the mirror topology that looped (two hosts, each the other's "ShopFlowRemote"): one task per turn on the peer, zero bounced back, "8 prodotti a catalogo" in 32 s.
=== VERIFIED ON THE PUBLISHED rc.8 (no change)
- BUG-076/077: three hosts' logs show route_to_specialist returned: main_coordinator -> OrderAgent[FunctionCall] -> OrderAgent[FunctionResult] -> OrderAgent[Text]; the router's call costs ~400 input tokens and is metered.
- BUG-078: both peers' cards advertise JSONRPC at an absolute URL; tasks are received and logged with their context ids. A2A context per scope holds: one context across a circuit's turns, a different one for a second circuit (Blazor Server and MAUI callers).
- BUG-075, second pass: "Ricorda che il mio codice privato e' ..." and asking for it back are both SAFE / IN scope; no UNSAFE verdict anywhere in the process log.
- BUG-079: with the peer unreachable or looping, the user read "il sistema remoto non ha fornito il numero" - no invented figure.
- BUG-073, residual rate: on the Chat Completions host the cold first turn of a process narrated a handoff without calling the tool, once in five. The fix lowers a rate; it does not remove a behaviour.
=== SAMPLES (not part of the packages)
- MentorAgentServer's remote agent can be renamed and re-pointed from the command line (--A2ARemote:Name / --A2ARemote:Url), so the source pair can mirror the published topology (two hosts, each the other's remote, same name). The source pair that verified rc.8 could not show BUG-080 because it did not.
=== STILL OPEN, UNCHANGED
- BUG-074 (S3): MentorshipLevel.Proactive offers actions the application does not have. Measured, recorded, a product-voice decision.
1,654 tests green, build 0 warnings / 0 errors.
1.0.0-rc.8 (condensed: BUG-075 to BUG-079)
The first delegated turns ever driven live, on the published rc.7. One additive API change: route_to_specialist takes an optional `specialist` argument.
=== FIXED - BUG-078 (S2): the A2A server had never completed a round trip - the card advertised the wrong protocol binding, the handler never submitted the task and disposed its scope the wrong way, and MentorAgent's own A2A client read only Message events. BUG-076 (S2): the handoff router was built from the coordinator's whole prompt (about 5,000 input tokens paid again on every delegated turn) and narrated instead of routing; it now has a router's prompt, the tool returns what the specialists said, and "NOT DELEGATED" / "DELEGATION FAILED" say what happened. BUG-077 (S3): every model call under the coordinator (router, specialists, team members, source-built agents) was unmetered; they now run on the host's ChatClient inside the metering wrapper, and an empty model id falls back to the client's own deployment. BUG-079 (S3): generated specialist and team-member prompts carry a grounding rule (state only what a tool returned); a custom [MentorAgent(Instructions = ...)] is used exactly as written and must carry its own. BUG-075, second pass: on hosts with memory on, the safety classifier is told that what users say about themselves is theirs, not a system secret (verified on the two reported messages); the model itself may still decline to store something its user calls private.
=== CHANGED - A turn that arrives over A2A is told that no human is present.
1.0.0-rc.7
Found by the release matrix: the same ~70 checks driven on all eight sample applications against the published rc.6 packages, signed in and anonymous, on Blazor Server (cookie), WebAssembly and React (JWT), Blazor Auto, .NET MAUI (over CDP) and the two source-referenced apps. 584 cells, every option of MentorOptions and MentorAgentBlazorOptions with a verdict. No API change.
=== FIXED
- BUG-073 (S2): the coordinator was told it COULD delegate to its specialists and never told how - the tool name route_to_specialist appeared nowhere in the prompt and the tool's own description named no agent. Told a specialist existed (BUG-069), the model narrated the handoff ("inoltro subito la domanda allo ShippingAgent, attendo la sua risposta") and ended the turn without calling anything, three times, once under an explicit order. The capabilities block now says how to delegate and forbids announcing an unperformed handoff; the tool's description names every specialist it reaches, attribute-declared, source-supplied and remote.
- BUG-075 (S3): "Ricorda che il mio codice privato e' ZULU-2200" was classified UNSAFE on a host with memory on. Remembering a fact the user asks to keep is a configured capability and is now named to the classifier, the way MCP servers and image input already were (BUG-067).
- BUG-072 (S3): a signed-in user whose AuthenticationStateProvider threw was silently keyed as anonymous - and since rc.5, as a different anonymous in every circuit, so the same user would have had a different memory in every tab with nothing in the log. The fallback stays (fail closed); it is now said once per session, without a stack trace per visitor. The live check on a real cookie principal shows the catch does not fire.
- BUG-068 addendum: one collaborating builder was still announcing per circuit at Information ("Remote agent 'X' connected from ..."). Routed through the same quiet logger; the general test now includes a remote agent.
=== SAMPLES (not part of the packages, recorded for whoever reads them)
- The three samples with authentication expose GET /account/dev-login?as=admin|manager|user in Development only (404 in Production, verified), so the authenticated rows of the matrix can be driven by a script; the WebAssembly and React clients accept ?as= on their login route for the same reason.
- The Blazor Server sample binds RefuseOutOfScope, WarmUpAtStartup, CompactionMaxTurns and RateLimitPerUser from configuration, so command-line overrides actually reach them.
- The samples point their A2A peer at a live server (the headless sample) instead of a port nobody listened on.
=== MEASURED, NOT CHANGED
- BUG-074 (S3, open): MentorshipLevel.Proactive still offers actions the application does not have - 3 in 5 turns ("esportare l'elenco", "cercare con il nome"). The rule is in the prompt; a structural fix (offers must name a listed tool or page) is a product-voice decision, recorded rather than taken.
- A compaction pass writes no log line; it is pinned in-process (CompactionTests). A Debug line when a pass runs would make it observable live.
1,623 tests green, build 0 warnings / 0 errors.
1.0.0-rc.6
Four defects found by running the published rc.5 packages against the sample applications - the pass rc.5's own notes implied but had not yet been done. No API change: every fix restores behaviour rc.5 already claimed.
=== FIXED
- BUG-068 (S3): the configuration summary was NOT written once per process, as rc.5's notes said it was. A second browser circuit still reprinted seven Information lines - the skills catalogue, the external agents, the hosted image model, the Azure image-header note, the hosted MCP server, the hosted tool list and the declarative handoff. The sentinel was claimed halfway through the build, after everything above it had already announced itself, and three collaborating builders never consulted it at all. It is now claimed first, and a later build demotes Information to Debug instead of discarding it, so an operator who turns Debug on to investigate one circuit can still see what it was built with. Degradation warnings stay per session, unchanged.
- BUG-069 (S2): an agent supplied by an IMentorAgentSource - a declarative YAML specialist, or a host's own - joined the handoff graph and was never named in the coordinator's instructions. route_to_specialist names no agent either, so nothing the model could see said the specialist existed: asked about it, the assistant answered that there is no such agent, while the log recorded it being added to the workflow. Source-supplied agents are now listed with their descriptions beside the attribute-declared ones.
- BUG-070 (S4): every skill on the published A2A card carried a flattened name ("Getallordersforanalysis"). The friendly-name helper splits on underscores and was being handed the PascalCase method name. That card is the one artefact whose entire audience is another machine's directory.
- BUG-071 (S4): a turn whose input safety check met the first failure printed two stack traces instead of one - the turn's fault log was reset after that check, so its cause was recorded and immediately discarded. The reset now happens before every early return.
1,616 tests green, build 0 warnings / 0 errors.
1.0.0-rc.5 (condensed; the full account is in the repository's BUGS.md, BUG-062 to BUG-067)
Latency and cost per visitor: time to the first character fell from 2.6-3.0 s to 1.6-2.0 s on a live host - one embedding per text instead of one per consumer, one classifier call for safety and scope, the composer handed back before the post-turn fact extraction, shared MCP sessions and a process-wide agent-card cache.
=== NEW - ClassifierChatClient (a small, fast deployment for MentorAgent's own one-word decisions); AnonymousIdentity (PerSession by default - BREAKING for single-user hosts that want one shared memory: set Shared); RefuseOutOfScope; WarmUpAtStartup; MentorMcpServer.Shared; a configure callback on MapMentorAgentMcp / MapMentorAgentA2A to protect them; IMentorQueryEmbedding.
=== FIXED - BUG-064 (S1): two anonymous visitors shared one memory. BUG-062, 063, 065, 066, 067 (S3/S4): text glued across a tool call, the Native-HITL banner shown before the role check, a spurious RAG scale warning, an unmetered scope classifier, legitimate requests refused by the safety classifier.
=== CHANGED - RAG chips show only cited documents; Proactive mentorship asks for one sentence before acting; the A2A card publishes skills, modes and streaming; the configuration summary is logged once per process.
1.0.0-rc.4
No change in this package. Version aligned with MentorAgent 1.0.0-rc.4, which fixes one S3 in the provider error classifier - see that package's notes. The five packages ship as a set and are meant to be upgraded together.
1.0.0-rc.3
No changes to this package's own surface: the YAML dialect, the file scanning rules and the tool-resolution guarantees are exactly as in 1.0.0-rc.2.
=== Why the version moved ==================================================
- All five packages ship together and share one version. rc.3 closes six findings in the core, Blazor and Abstractions packages, including two S1s - see those packages' notes.
- If you reference Microsoft.Extensions.AI.OpenAI or Microsoft.Agents.AI.Foundry directly, note that the core package now FAILS THE BUILD (error MENTOR001) when OpenAI resolves to 2.11.0 or later, rather than letting the application die at startup.
=== Unchanged, and worth restating ========================================
- A tool named in YAML still resolves to a tool the application already registered, already wrapped in MentorAgent's gate, so RequiredRoles and human approval keep working inside a declarative agent's own function-calling loop.
- ConfigurationSection still exposes one named section and nothing else. Null means expose nothing, never expose everything.
- A definition file is code. Load it only from deploy-time locations.