Skip to content

Running agents ​

You run an agent through its host, AgentHost<TAgent>, which your app takes from the scope of the request or job that runs the agent (Using an agent).

Registration ​

cs
services.AddTypeshift()
    .WithStore<EfTypeshiftStore>()
    .WithChatClientProvider<KeyedChatClientProvider>()
    .WithRealtimeTracking()
    .WithRunMiddleware<BlockedTopics>()
    .WithToolMiddleware<AuditToolCalls>();

services.AddKeyedSingleton("openai", fast);
services.AddKeyedSingleton("anthropic", reasoning);

snippet source | anchor

AddTypeshift() registers the toolsets and agents of this project and the projects it references, plus a host per agent. fast and reasoning stand for the IChatClient instances your app creates, one per provider. This page needs two calls of the chain: WithChatClientProvider<T>() registers your class that returns the client of a provider's model, and WithStore<T>() registers the store that keeps settings and conversations. Registration covers the rest.

Chat clients ​

Your IChatClientProvider returns the IChatClient for a provider and a model, and Typeshift resolves it from the run's services. Models and providers shows how to write one, how an agent names its model and how to create a real client.

Run an agent ​

cs
await using var scope = provider.CreateAsyncScope();
var support = scope.ServiceProvider.GetRequiredService<AgentHost<SupportAgent>>();

var answer = await support.RunAsync("Where is my refund?");

Console.WriteLine($"{answer.Messages[^1].AuthorName}: {answer.Text}");

snippet source | anchor

A run returns Microsoft.Extensions.AI's ChatResponse: the model's messages, with the tool calls and results between them. Each message names the agent that wrote it in AuthorName.

Stored settings ​

Pass a settings id, and the run layers the settings stored under it over the agent's code:

cs
var settingsId = 42;

var stored = await support.RunAsync("Where is my refund?", settingsId);

snippet source | anchor

Run settings ​

A run can also pass settings of its own, as an instance of the agent's generated Settings record. They override the stored settings for that run only:

cs
var run = new SupportAgent.Settings
{
    Provider = "anthropic",
    Model = "claude-sonnet-5-5",
    Temperature = 0,
    SearchTickets = new TicketToolset.SearchTicketsOverrides { IncludeClosed = true },
    SearchArticles = new KnowledgeToolset.SearchArticlesOverrides { Language = "de" }
};

var thorough = await support.RunAsync("Where is my refund?", settingsId, settings: run);

snippet source | anchor

Values the run leaves null keep what the stored settings or the agent's code set, as settings layering explains.

Conversations ​

Pass a conversation id, and the run continues that conversation and stores each of its steps in it. If a crash cuts a run off, ResumeAsync continues it. Conversations covers both.

Structured output ​

RunAsync<T> asks the model for JSON in T's schema and returns it typed, in Result:

cs
var report = await support.RunAsync<RefundStatus>("Summarize the refund.", settingsId);

Console.WriteLine($"{report.Result.Invoice}: {report.Result.State}");

snippet source | anchor

cs
internal sealed record RefundStatus(string Invoice, RefundState State, DateOnly? Expected);

internal enum RefundState
{
    Requested,
    Approved,
    Paid
}

snippet source | anchor

T's schema leaves out XML docs, which describe only tool parameters. Describe its properties with [Description] attributes when the model needs to know more than their names. Under Native AOT, T needs JSON metadata.

Streaming ​

RunStreamingAsync returns the model's updates as they arrive. When the model calls tools, the tools run between its updates, and the stream goes on with the model's next response:

cs
await foreach (var update in support.RunStreamingAsync("Where is my refund?", settingsId))
    Console.Write(update.Text);

snippet source | anchor

With WithRealtimeTracking(), anyone watching the conversation sees the updates too.

The raw path ​

If your app needs the IChatClient itself, for example to hand it to a library that takes one, split a run into three steps: resolve the options, create the client, and call the client with those options. Here run is the SupportAgent.Settings from Run settings:

cs
var options = await support.ResolveOptionsAsync(settingsId, run);

using var client = await support.CreateChatClientAsync(options);
var response = await client.GetResponseAsync([new ChatMessage(ChatRole.User, "Where is my refund?")], options);

snippet source | anchor

The client is the run's whole pipeline, the tool loop and the agent's run options included. It serves concurrent calls, each with the limits of its own options. Disposing it does not dispose the client your provider returned. To run in a conversation, set ChatOptions.ConversationId on the options to the conversation's id, written as a string of the build's id type. Typeshift reads the conversation from it and never sends it to the provider.

How a run works ​

A run is one call of RunAsync, RunAsync<T>, RunStreamingAsync, ResumeAsync or ResumeStreamingAsync, or of a client from the raw path. Streaming or not, every run passes through the same steps in the same order, and that order tells you what your middleware sees, when context is added, which calls ask for approval and what is stored when.

Each step wraps the steps below it, so the answer passes back up through them in reverse order: run middleware sees the response before the conversation stores how the run ended.

  1. Resolve the options. The host builds the run's ChatOptions from the agent's code, then the settings stored under the settings id, then the run's own settings. It creates the tools the agent offers, their settings resolved through the same layers, and adds the sub-agents and the tools of MCP servers after them. The layers resolve once, when the run starts (settings layering).
  2. Resume the conversation. With a conversation id, the run puts the stored entries ahead of the new input, so the model sees the whole conversation, and stores the input. An input that answers an approval request that has closed throws instead, before anything is stored (When a request closes). The run keeps the conversation's mode unless the run's settings name one. If an earlier run handed the conversation off to another agent, the agent that answered last answers this run too. Without a conversation id, the run starts fresh and stores nothing. Within one process, a conversation has one run at a time, and starting a second while the first goes on throws (Conversations).
  3. Run middleware. Each IRunMiddleware wraps the rest of the run (Middleware).
  4. Context providers. Each context provider the agent uses can add instructions, messages and tools to this run. The conversation stores none of it, so every run calls the providers again (Context providers).
  5. Approval and mode. If the input carries your app's answers to the approval requests the conversation's last run left open, the run matches each answer to its request. In plan mode, it offers the model only the read-only tools and exit_plan_mode. After each pass of the tool loop, approval rules, standing approvals and auto mode decide every call they can. If calls remain that only your app can decide, the run stores their requests and ends (Approval and modes).
  6. The tool loop. Microsoft.Extensions.AI's FunctionInvokingChatClient calls the model, runs the tools it asks for, each through the tool middleware, and calls the model again until it answers. The run options set its limits, such as the most rounds of tool calls.

Before every model call inside the loop, the run appends the new steps to the conversation: tool results, input your app steered into the run, and the answers of sub-agents running in the background. Each model response is stored as it arrives, so a crash loses at most the calls in flight.

When compaction is on and a call would exceed the token limit, the run compacts what the model sees, while the stored conversation keeps everything (compaction). Typeshift traces every call to the provider's client (Observability).

What each step can change ​

StepSeesCan
Run middlewareThe agent, the messages, the options and the modeChange the messages and options of this run, or answer in place of the model
Context providerThe runAdd instructions, messages and tools to this run
Approval ruleEach call that requires approval, with the arguments the model sentApprove it, reject it with a reason for the model, or abstain
Tool middlewareEach tool call: the function, its arguments, the messages so farAnswer in place of the tool, or end the tool loop

Released under the MIT License.