MentorAgent Docs
Menu

ReferencePackage: Novior.MentorAgent

Attributes

The parameters of MentorAction, MentorAgent, MentorTeam, TeamMember, MentorPage and MentorSkill, with their defaults and rules.

View as Markdown
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.

AttributeApplies toNamespaceWhat it declares
[MentorAction]MethodNovior.MentorAgent.AttributesA tool the agent can call
[MentorAgent]ClassNovior.MentorAgent.AttributesA specialist agent the coordinator hands requests to
[MentorTeam]ClassNovior.MentorAgent.AttributesA team of agents that discuss a request in a group chat
[TeamMember]Propertyglobal namespaceA member of a team
[TeamTerminationCondition]Methodglobal namespaceThe rule that ends a team's discussion
[MentorPage]ClassNovior.MentorAgent.AttributesA page the agent can open
[MentorSkill]ClassNovior.MentorAgent.AttributesA 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.

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.

CaseService.cs
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);
}
ParameterTypeDefaultEffect
DescriptionstringRequiredThe tool's description: what the model reads to decide when to call it
Categorystring?nullUsed 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
RequiresConfirmationboolfalseShows 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
ReadOnlyboolfalseDeclares 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
RequiredRolesstring[]emptyThe 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
ProactiveHintstring?nullGuidance 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
NavigateTostring?nullA 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 Async is dropped first. CloseCaseAsync is close_case. Options that name tools, such as RequiresApproval, use this name.
  • Roles on a server. On the SignalR hub, on /mentor/chat and over A2A, MentorAgent reads the user from the request. AddMentorAgentServer() (or AddMentorAgentA2A() for A2A alone) gives it the caller, whatever the order of AddRazorComponents() and AddMentorAgentServer() in a Blazor Web App. In a Blazor circuit, it asks the AuthenticationStateProvider of the host. A claim that only your own provider adds is not seen on the hub, on /mentor/chat or over A2A: add it with an IClaimsTransformation, which ASP.NET Core applies to HttpContext.User and 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 the Tools of a team member.
  • Every level. The same checks apply when a specialist or a team member calls the method. See Roles, confirmation and governance.

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.

CaseAgent.cs
[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);
}
ParameterTypeDefaultEffect
NamestringRequiredThe agent's name: the key in the handoff workflow and in the coordinator's prompt
DescriptionstringRequiredWhat the agent can do. The coordinator reads it to decide when to delegate
HandoffTostring[]emptyThe 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
Instructionsstring?nullThe 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

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.

EscalationReviewTeam.cs
[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));
}
ParameterTypeDefaultEffect
NamestringRequiredThe team's name, and the name of its tool
DescriptionstringRequiredWhat the team is for. The coordinator reads it to decide when to use the team
TriggerOnstring[]emptyKeywords 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
MaxIterationsint10The maximum number of times the members speak before the discussion is stopped
HandoffTostring[]emptySpecialists 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

Put it on a property of a [MentorTeam] class, one property per member, as in the example above.

ParameterTypeDefaultEffect
RolestringRequiredThe member's role in the team, for example "Analyst"
InstructionsstringRequiredThe member's instructions. MentorAgent appends the language, the role, the grounding rule and the decision protocol described below
ToolsType[]emptyClasses 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.

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.

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.

Cases.razor
@page "/cases"
@using Novior.MentorAgent.Attributes
@attribute [MentorPage(Url = "/cases", Name = "Cases",
    Description = "The list of support cases, with filters by status")]
ParameterTypeDefaultEffect
UrlstringRequiredThe page's URL, for example "/cases"
NamestringRequiredThe page's name in the system prompt
Descriptionstring?nullWhat the page offers, shown to the model. The classifiers of RefuseOutOfScope and RequireActionForChangeRequests see it too
HasUIActionsboolfalseAfter 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
ReadyTimeoutint2000How long navigate_to waits for SignalReady(), in milliseconds

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.

CaseEscalationSkill.cs
[MentorSkill(
    Name = "case-escalation",
    Description = "Rules for escalating a support case to a supervisor",
    InstructionsFile = "Skills/case-escalation/SKILL.md")]
public class CaseEscalationSkill
{
}
ParameterTypeDefaultEffect
NamestringRequiredThe skill's unique name in kebab-case: the key for load_skill
DescriptionstringRequiredOne sentence shown in the skill catalog, the only text the model sees before it loads the skill
InstructionsFilestring?nullA 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
Instructionsstring?nullThe 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.

  • 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.