Skip to content

Agents ​

An agent bundles what a model needs for one job: chat options, instructions and tools. You declare it in its constructor, and the generator gives it a Settings record of what may change at runtime.

Declaring an agent ​

An agent is a partial class deriving from Agent. This one answers customers from their tickets and the help center:

cs
internal partial class SupportAgent : Agent
{
    public SupportAgent()
    {
        WithDescription("Answers customers from their tickets and the help center.");

        WithChatOptions()
            .WithModel("openai", "gpt-5-mini")
            .WithTemperature(0.3f)
            .Configurable(o => o.ModelId)
            .Configurable(o => o.Instructions)
            .Configurable(o => o.Temperature);

        UseToolset<TicketToolset>()
            .Include(t => t.SearchTickets)
            .Include(t => t.CloseTicket);

        UseToolset<KnowledgeToolset>()
            .Exclude(t => t.DeleteArticle);

        ConfigureSearchTickets()
            .WithLimit(5);

        UseMcpServer("status_page")
            .ReadOnly();

        UseContext<CustomerProfileContext>();
    }

    protected override string GetInstructions() =>
        """
        Answer from the customer's tickets and cite help center articles.
        """;
}

snippet source | anchor

The tools and their settings have sections of their own below. The other calls declare the rest:

CallDeclares
WithName(...)The agent's name. Messages the agent writes carry it as their author, and other agents call this one by it. Without WithName, SupportAgent is named support_agent.
WithDescription(...)What the agent does. The model of an agent that calls this one reads it.
WithChatOptions()The options every run starts from: WithModel, WithTemperature, WithMaxOutputTokens and a With... per other option. WithTools adds tools that belong to no toolset, such as a hosted web search.
UseMcpServer(...)The tools of an MCP server. ReadOnly() lets plan mode offer them.
UseContext<T>()A context provider, whose instructions, messages and tools join every run.
UseAgent<T>(), UseHandoff<T>()Another agent as a tool, or one the model can hand the conversation to. See Multiple agents.
GetInstructions()The instructions, as an override. It runs whenever a run's options are resolved, so the text may be computed.

Because the generator reads the constructor rather than running it, WithName and WithDescription take constants (TSH002), and every declaration sits directly in the constructor as one chain (TSH014, TSH015). An agent derives from Agent directly, never from a base agent of yours (TSH017). To share between agents, put values in constants and code in static methods, and keep every declaration in each agent's constructor.

Because an agent is declared in code, you cannot define one at runtime, and stored settings change only what its code marks configurable.

Selecting tools ​

UseToolset<T>() offers every tool of a toolset. Chain Include to offer only the tools you pick, in the order you pick them, or Exclude to offer all but those:

cs
UseToolset<TicketToolset>()
    .Exclude(t => t.CloseTicket);

snippet source | anchor

The lambda picks from a list the generator writes for each toolset, with one property per tool, named after the tool: search_tickets is t.SearchTickets, and close_ticket, from CloseTicketAsync, is t.CloseTicket. A tool of another toolset is not on that list, so picking it does not compile. Use each toolset once and pick each tool once (TSH025). A toolset from another project needs the Typeshift package in that project (TSH019).

Add a tool of the agent's own ​

A tool that only one agent needs can live in the agent itself, without a toolset of its own. Declare it with AddTool in the agent's constructor, backed by a static method of the agent:

cs
internal partial class BillingAgent : Agent
{
    public BillingAgent()
    {
        WithDescription("Settles charges and invoices.");

        WithChatOptions()
            .WithModel("openai", "gpt-5-mini");

        AddTool(GetToday)
            .ReadOnly();
    }

    /// <summary>
    /// Returns today's date, to tell whether an invoice is overdue.
    /// </summary>
    private static DateOnly GetToday(IServiceProvider services)
    {
        var time = services.GetService(typeof(TimeProvider)) as TimeProvider ?? TimeProvider.System;
        return DateOnly.FromDateTime(time.GetLocalNow().DateTime);
    }

    protected override string GetInstructions() => "Settle the customer's charges and invoices.";
}

snippet source | anchor

  • The tool takes the same chain as a toolset's tool, such as ReadOnly() here or RequiresApproval(), and follows the same rules for its name and description, parameters and results. So GetToday becomes get_today, its <summary> describes it to the model, and the IServiceProvider it takes is the run's, which the model never sees.
  • The method must be static and may be private, since only the agent's generated code calls it.
  • The agent offers its own tools after the tools of its toolsets, in the order it adds them.
  • A tool with settings declares them with WithSettings<T>() and takes the record as a parameter (Tool settings). It then gets a Configure{Tool}(), and its configurable members join the agent's Settings, as a toolset's tool does.
  • Its name must differ from the tools of the toolsets the agent uses (TSH025) and from the tools of the agents it hands a conversation to or receives one from, directly or through other agents, and of its sub-agents and their callers (TSH007). Other agents may have a tool of the same name.

No other agent can offer this agent's tools, so move the tool into a toolset once a second agent needs it. Include and Exclude pick only from toolsets, so the agent always offers its own tools.

Configurable chat options ​

Configurable(o => o.Temperature) marks an option that stored settings and runs may change, whether the code sets it or not. The model is an option like any other. Without Configurable(o => o.ModelId), stored settings and runs cannot change it, and the agent's Settings has no Provider or Model for a settings form to offer.

Builders lists which options may be configurable. The rest belong to the run rather than to its settings, and the analyzer reports a Configurable call that names one of them (TSH012).

Run options ​

WithRunOptions() sets how a run's tool loop goes, and Configurable marks the options that stored settings and runs may change:

cs
WithRunOptions()
    .WithMaxToolIterations(10)
    .WithConcurrentToolCalls(true)
    .Configurable(r => r.Mode)
    .Configurable(r => r.MaxToolIterations);

snippet source | anchor

Run options set the mode a run starts in, the limits of its tool loop, whether the calls of one response run at the same time, and when the history is compacted. Builders lists each one with its default.

Tool settings ​

Configure{Tool}() sets a tool's settings for this agent, over the record's code defaults, as ConfigureSearchTickets().WithLimit(5) does above. It can set every settable member of the record, configurable or not, for every run of this agent. A tool the agent does not offer has none, so configuring it does not compile. Generated members lists what the builder offers.

The generated settings ​

Every agent gets a nested Settings record, here SupportAgent.Settings, holding exactly what may change at runtime:

MemberFrom
Provider, ModelPresent when Configurable(o => o.ModelId) allows them. They override what WithModel sets.
One per configurable chat optionInstructions and Temperature above.
One per configurable run optionMode and MaxToolIterations from the run options above.
One per offered tool with configurable settingsSearchTickets, SearchArticles: their overrides.

Option values are nullable, and so are the values in a tool's overrides. A null keeps the layer below. When a tool's member would be named like an option or like one of the record's own members, such as Kind, the generator adds a Tool suffix to it.

Store the record under a settings id, pass it for one run, or bind a settings form or an API to it. The record's Serialize() and System.Text.Json write the same JSON, so either works for an API. In a store, WithStorageKind sets the name the record is kept under in place of the class name, and WithSchemaVersion raises its schema version after you rename or remove a configurable member (Migrate older settings).

Released under the MIT License.