Attributes
The parameters of MentorAction, MentorAgent, MentorTeam, TeamMember, MentorPage and MentorSkill, with their defaults and rules.
On this page
Attributes on your own classes and methods tell MentorAgent what the agent may do. MentorAgent finds them in the assemblies listed in ScanAssemblies. Register every class whose methods become tools in dependency injection (DI). If the container cannot create a class, MentorAgent leaves its methods out and logs a warning.
At a glance
Section titled “At a glance”| Attribute | Applies to | Namespace | What it declares |
|---|---|---|---|
[MentorAction] | Method | Novior.MentorAgent.Attributes | A tool the agent can call |
[MentorAgent] | Class | Novior.MentorAgent.Attributes | A specialist agent the coordinator hands requests to |
[MentorTeam] | Class | Novior.MentorAgent.Attributes | A team of agents that discuss a request in a group chat |
[TeamMember] | Property | global namespace | A member of a team |
[TeamTerminationCondition] | Method | global namespace | The rule that ends a team's discussion |
[MentorPage] | Class | Novior.MentorAgent.Attributes | A page the agent can open |
[MentorSkill] | Class | Novior.MentorAgent.Attributes | A skill whose instructions the model loads when it needs them |
[TeamMember] and [TeamTerminationCondition] are in the global namespace, so they need no using. Which level to use explains when to use actions, specialists or teams.
[MentorAction]
Section titled “[MentorAction]”Put it on a public method of a service you register in DI: the method becomes a tool the agent can call. A method with only [Description] from System.ComponentModel is an action too, with no rules: no roles and no confirmation. The default confirmation of RequireConfirmationForWrites covers [MentorAction] methods only.
public class CaseService(ICaseStore store)
{
// Only reads: it never asks for confirmation
[MentorAction(Description = "Gets the status of a support case", ReadOnly = true)]
public Task<string> GetStatusAsync(string caseNumber)
=> store.GetStatusAsync(caseNumber);
[MentorAction(
Description = "Closes a support case with a resolution",
Category = "Cases",
RequiresConfirmation = true,
RequiredRoles = ["Supervisors", "Admins"],
ProactiveHint = "Suggest checking the case status first",
NavigateTo = "/cases")]
public Task CloseCaseAsync(string caseNumber, string resolution)
=> store.CloseAsync(caseNumber, resolution);
}| Parameter | Type | Default | Effect |
|---|---|---|---|
Description | string | Required | The tool's description: what the model reads to decide when to call it |
Category | string? | null | Used only on the A2A agent card, where an action is listed as an A2A skill: Category becomes that skill's tag. MentorAgent does not group actions by it |
RequiresConfirmation | bool | false | Shows the confirmation box before the method runs. Write true on an action that changes data or cannot be undone, and false on a write that must run without asking. Left out, the action follows RequireConfirmationForWrites: it asks unless it is ReadOnly. Tool filtering reads it: an action without confirmation can reach the model on any message, and one with confirmation never travels alone |
ReadOnly | bool | false | Declares that the action only reads: it changes no data. MentorAgent takes your word for it and does not check it. The MCP server publishes the action with readOnlyHint: some MCP clients skip their own confirmation for such a tool, so declare it only on an action that really only reads. RequireLookupForRecordQuestions can require the agent to call it. Ignored when the action also needs confirmation. For a method that has only [Description], use IsReadOnlyAction |
RequiredRoles | string[] | empty | The roles allowed to run the action: any one of them is enough. The user must be signed in. On the hub, on /mentor/chat and over A2A, the user is the caller of the request. In a Blazor circuit, it is the user that the AuthenticationStateProvider of the host reports. Empty means no role check, for anonymous users too |
ProactiveHint | string? | null | Guidance for the model, appended to the tool's description as Guidance: <hint> for the coordinator, the specialists and the team members. It is not added on the MCP server, and tool filtering ignores it |
NavigateTo | string? | null | A URL the agent opens after the method succeeds |
- Tool names. The tool name comes from the method name: PascalCase becomes snake case (lowercase words joined by underscores), and a trailing
Asyncis dropped first.CloseCaseAsyncisclose_case. Options that name tools, such asRequiresApproval, use this name. - Roles on a server. On the SignalR hub, on
/mentor/chatand over A2A, MentorAgent reads the user from the request.AddMentorAgentServer()(orAddMentorAgentA2A()for A2A alone) gives it the caller, whatever the order ofAddRazorComponents()andAddMentorAgentServer()in a Blazor Web App. In a Blazor circuit, it asks theAuthenticationStateProviderof the host. A claim that only your own provider adds is not seen on the hub, on/mentor/chator over A2A: add it with anIClaimsTransformation, which ASP.NET Core applies toHttpContext.Userand so to the user of the hub connection. - Which methods become tools. MentorAgent finds the public instance methods declared on the class itself that have
[MentorAction]or a[Description]with a text. It ignores inherited methods, static methods and an empty[Description]. The same rules apply to a specialist and to theToolsof a team member. - Every level. The same checks apply when a specialist or a team member calls the method. See Roles, confirmation and governance.
[MentorAgent]
Section titled “[MentorAgent]”Put it on a class to make it a specialist: its [Description] and [MentorAction] methods become the specialist's tools, and the coordinator hands it the requests it can handle. Register the class in DI. Specialists need ChatClient. Implementing IMentorAgent is optional: MentorAgent never reads it.
[MentorAgent(
Name = "CaseAgent",
Description = "Handles support cases: status, history and closing",
HandoffTo = ["BillingAgent"])]
public class CaseAgent(ICaseStore store) : IMentorAgent
{
[Description("Gets the status of a support case")]
public Task<string> GetCaseStatusAsync(string caseNumber)
=> store.GetStatusAsync(caseNumber);
}| Parameter | Type | Default | Effect |
|---|---|---|---|
Name | string | Required | The agent's name: the key in the handoff workflow and in the coordinator's prompt |
Description | string | Required | What the agent can do. The coordinator reads it to decide when to delegate |
HandoffTo | string[] | empty | The names of other [MentorAgent] agents it can hand off to, matched regardless of case. A name that matches no agent logs a warning, and that handoff is skipped |
Instructions | string? | null | The agent's system prompt, used exactly as written. With null, MentorAgent writes one from Name and Description, including the grounding rule: state only facts a tool returned. A custom prompt replaces that rule, so include your own |
[MentorTeam]
Section titled “[MentorTeam]”Put it on a class to define a group chat: the members discuss the request, then the coordinator answers with the result. The team is a tool of the coordinator, named after the team: EscalationReview becomes escalation_review. Teams need ChatClient. Implementing IMentorTeam is optional.
[MentorTeam(
Name = "EscalationReview",
Description = "Reviews a request to escalate a support case",
TriggerOn = ["escalation review"],
MaxIterations = 6,
HandoffTo = ["CaseAgent"])]
public class EscalationReviewTeam : IMentorTeam
{
[TeamMember(
Role = "Analyst",
Instructions = "Summarize the case history and propose a decision.",
Tools = [typeof(CaseReports)])]
public object? Analyst { get; set; }
[TeamMember(
Role = "Approver",
Instructions = "Review the proposal and approve or reject it, with reasons.")]
public object? Approver { get; set; }
[TeamTerminationCondition]
public bool ShouldTerminate(string lastMessage, string lastSpeaker)
=> lastSpeaker == "Approver"
&& (lastMessage.Contains("[TEAM:APPROVED]", StringComparison.Ordinal)
|| lastMessage.Contains("[TEAM:REJECTED]", StringComparison.Ordinal));
}| Parameter | Type | Default | Effect |
|---|---|---|---|
Name | string | Required | The team's name, and the name of its tool |
Description | string | Required | What the team is for. The coordinator reads it to decide when to use the team |
TriggerOn | string[] | empty | Keywords added to the coordinator's prompt as an "always delegate to this team" directive. The model still decides: choose specific words, because a broad one sends ordinary requests through the group chat |
MaxIterations | int | 10 | The maximum number of times the members speak before the discussion is stopped |
HandoffTo | string[] | empty | Specialists that carry out the plan once the team approves it. The team's tool description names them, and when [TEAM:APPROVED] appears anywhere in the discussion the result asks the coordinator to carry out the approved actions. The coordinator's model makes that call |
[TeamMember]
Section titled “[TeamMember]”Put it on a property of a [MentorTeam] class, one property per member, as in the example above.
| Parameter | Type | Default | Effect |
|---|---|---|---|
Role | string | Required | The member's role in the team, for example "Analyst" |
Instructions | string | Required | The member's instructions. MentorAgent appends the language, the role, the grounding rule and the decision protocol described below |
Tools | Type[] | empty | Classes whose tools the member can call during the discussion. They are the toolbox of this member only: the coordinator does not offer their methods, and the MCP server and the A2A card do not publish them. A [MentorSkill] class keeps its coordinator tools. Keep the tools read-only, declare them with ReadOnly = true, and register the classes in DI: a class the container cannot resolve is skipped with a warning, and the member runs without it |
Writes belong to the specialists named in the team's HandoffTo, which keep their own role and confirmation checks.
[TeamTerminationCondition]
Section titled “[TeamTerminationCondition]”Put it on one method of the team class, with the signature bool ShouldTerminate(string lastMessage, string lastSpeaker): the discussion ends when it returns true. Without it, the members speak in turn until MaxIterations. The attribute has no parameters.
MentorAgent instructs the members to mark their decision with [TEAM:APPROVED] or [TEAM:REJECTED], and removes both before the text reaches the user. Your condition can end the discussion on any text it looks for, but only [TEAM:APPROVED], anywhere in the discussion, makes the result ask the coordinator to hand off.
[MentorPage]
Section titled “[MentorPage]”Put it on a routable Blazor page to let the agent open it with navigate_to. The agent knows the page from the start, without visiting it first. In a .razor file, write it with @attribute. With a front end of your own, such as React, put it on an empty class, one per page, in a scanned assembly. Your client then receives the URL, in the NavigationRequested event or in the navigation frame over Server-Sent Events, and routes to it.
@page "/cases"
@using Novior.MentorAgent.Attributes
@attribute [MentorPage(Url = "/cases", Name = "Cases",
Description = "The list of support cases, with filters by status")]| Parameter | Type | Default | Effect |
|---|---|---|---|
Url | string | Required | The page's URL, for example "/cases" |
Name | string | Required | The page's name in the system prompt |
Description | string? | null | What the page offers, shown to the model. The classifiers of RefuseOutOfScope and RequireActionForChangeRequests see it too |
HasUIActions | bool | false | After opening the page, navigate_to waits until the page calls PageContext.SignalReady(). It waits only when the turn runs inside the Blazor app that shows the page, as on Blazor Server or .NET MAUI: turns on the hub, over Server-Sent Events (SSE) and over A2A never wait |
ReadyTimeout | int | 2000 | How long navigate_to waits for SignalReady(), in milliseconds |
[MentorSkill]
Section titled “[MentorSkill]”Put it on a class to register a skill: instructions the model loads with load_skill only when it needs them, while the prompt carries just the name and the description. The class's [Description] and [MentorAction] methods become tools, so register it in DI when it has any. Skills need EnableSkills.
[MentorSkill(
Name = "case-escalation",
Description = "Rules for escalating a support case to a supervisor",
InstructionsFile = "Skills/case-escalation/SKILL.md")]
public class CaseEscalationSkill
{
}| Parameter | Type | Default | Effect |
|---|---|---|---|
Name | string | Required | The skill's unique name in kebab-case: the key for load_skill |
Description | string | Required | One sentence shown in the skill catalog, the only text the model sees before it loads the skill |
InstructionsFile | string? | null | A Markdown file with the instructions, absolute or relative to the content root. With null, MentorAgent reads SKILL.md in the folder named after the skill, inside SkillsFolder |
Instructions | string? | null | The instructions inline, in Markdown. They win over InstructionsFile |
When no file is found, the instructions are just the skill's name and description; a path you gave that does not exist also logs a warning.
See also
Section titled “See also”- Configuration options — Every MentorOptions property with its type, default and effect, grouped by area, plus the options of the WebAssembly client and the YAML loader.
- The three agent levels — Actions, specialists and teams: how the three levels of MentorAgent map to Agent Framework, and when to use each one.
- Roles, confirmation and governance — How MentorAgent checks roles and asks for confirmation before an action runs, on every channel, and what it leaves to your code.
