Getting started
Typeshift lets you declare tools and agents in a few lines of C#. In four steps, this page builds an agent that finds the companies of a city.
1. Install
Typeshift needs the .NET 10 SDK and C# 14, its default language version. Add the package to every project that declares toolsets or agents or calls AddTypeshift(). The package carries the source generator, and NuGet does not pass a generator on to the projects that reference yours, so each of them needs the package itself (TSH020):
dotnet add package Typeshift --prereleaseThe types on this page are in the Typeshift namespace, which the package does not import for you, so add using Typeshift; to your files. Step 4 also needs using Microsoft.Extensions.AI; and using Microsoft.Extensions.DependencyInjection;, and in a console app the Microsoft.Extensions.DependencyInjection package, which builds the service provider.
Turn on XML docs in a Directory.Build.props next to your solution, since tools take their descriptions from them; without them the build warns with TSH011. The same file sets TypeshiftIdType, the type of the settings ids and conversation ids you pass to runs. Leave it out, and ids are Guids, so step 4 does not compile with the ids 42 and 1:
<Project>
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- Ids are Guids unless you name another type. This sample uses int. -->
<TypeshiftIdType>int</TypeshiftIdType>
</PropertyGroup>
</Project>2. Write a toolset
Steps 2 and 3 each go in a file of their own, and step 4 goes in Program.cs. A toolset is a partial class, so the generator can add members to it (TSH001). It declares its tools in its constructor, and each tool is a static method:
internal partial class CompanyToolset : Toolset
{
public CompanyToolset()
{
WithDisplayName("Companies");
AddTool(FindCompanies)
.WithSettings<FindCompaniesSettings>()
.Configurable(s => s.Limit);
}
/// <summary>
/// Finds the companies of a city.
/// </summary>
/// <param name="city">The city the companies are based in.</param>
public static IReadOnlyList<Company> FindCompanies(string city, FindCompaniesSettings settings) =>
[
.. Companies
.Where(company => company.City.Equals(city, StringComparison.OrdinalIgnoreCase))
.Where(company => company.Active || settings.IncludeInactive)
.Take(settings.Limit)
];
private static readonly Company[] Companies =
[
new("Danube Robotics", "Vienna", Active: true),
new("Prater Motion", "Vienna", Active: true),
new("Graz Gearworks", "Graz", Active: false)
];
}
internal sealed record Company(string Name, string City, bool Active);- The model calls the tool as
find_companiesand reads its XML docs. - The model never sees the
FindCompaniesSettingsparameter. Typeshift fills it with the tool's settings when it calls the tool. Configurable(s => s.Limit)lets stored settings and runs changeLimit, whileIncludeInactivestays as your code sets it.
The settings record sets the defaults:
internal record FindCompaniesSettings
{
public int Limit { get; init; } = 5;
public bool IncludeInactive { get; init; }
}3. Write an agent
An agent is a partial class too, for the same reason, and derives from Agent. It picks a model, its instructions and its tools:
internal partial class ResearchAgent : Agent
{
public ResearchAgent()
{
WithDescription("Researches the companies of a city.");
WithChatOptions()
.WithModel("openai", "gpt-5-mini")
.WithTemperature(0.2f)
.Configurable(o => o.Instructions)
.Configurable(o => o.Temperature);
UseToolset<CompanyToolset>();
ConfigureFindCompanies()
.WithLimit(3);
}
protected override string GetInstructions() =>
"""
Find companies with the find_companies tool and list them.
""";
}Configurablelets stored settings and runs change the instructions and the temperature. The model stays as the code sets it.- The generator writes
ConfigureFindCompanies()because the agent usesCompanyToolset, andWithLimit(3)sets this agent's limit over the record's 5. Configuring a tool the agent does not offer fails the build.
4. Run it
AddTypeshift() registers every toolset and agent. Chain a store, which keeps settings and conversations, and a chat client provider, which returns the IChatClient that serves each model:
var services = new ServiceCollection();
services.AddTypeshift()
.WithStore<InMemoryTypeshiftStore>()
.WithChatClientProvider<KeyedChatClientProvider>();
services.AddKeyedSingleton<IChatClient>("openai", new CannedChatClient());
await using var provider = services.BuildServiceProvider(validateScopes: true);CannedChatClient is a fake client from the docs samples, so they run without a network. In your app, register your provider's client instead. With the Microsoft.Extensions.AI.OpenAI package and an OpenAI API key, that is:
var openAi = new OpenAIClient(apiKey)
.GetChatClient("gpt-5-mini")
.AsIChatClient();
services.AddKeyedSingleton("openai", openAi);OpenAIClient is in the OpenAI namespace, and apiKey comes from your app's configuration, for example an environment variable. The key "openai" is a name you choose: it is the provider the agent names in WithModel("openai", "gpt-5-mini"). A real client shows a local server and other providers. InMemoryTypeshiftStore comes with Typeshift and stands in for your database.
KeyedChatClientProvider is your IChatClientProvider. For each run, Typeshift asks it for the client of the provider and model the agent names, here "openai" and "gpt-5-mini", and it returns the IChatClient registered under the provider's key:
internal sealed class KeyedChatClientProvider(IServiceProvider services) : IChatClientProvider
{
// One client per provider serves all its models: Typeshift puts the model into every request
public ValueTask<IChatClient> GetChatClientAsync(string provider, string model, CancellationToken ct) =>
ValueTask.FromResult(
services.GetKeyedService<IChatClient>(provider)
?? throw new InvalidOperationException($"No IChatClient is registered for the provider '{provider}'."));
}You configure and run the agent through its host, AgentHost<ResearchAgent>, which AddTypeshift() registered for it. The next snippet starts by creating a scope and resolving the host from it. In a web app, you take the host as a constructor parameter of your controller or service instead (Using an agent).
A settings id is an id you choose, for example one per customer. The store keeps the agent's settings under it, and every run that passes the id uses them. ConfigureAsync loads the agent's generated ResearchAgent.Settings record, which holds only what the code marked Configurable, applies your change and saves it:
await using var scope = provider.CreateAsyncScope();
var research = scope.ServiceProvider.GetRequiredService<AgentHost<ResearchAgent>>();
var settingsId = 42;
await research.ConfigureAsync(settingsId, settings => settings with
{
Temperature = 0.4f,
FindCompanies = settings.FindCompanies with { Limit = 2 }
});Every run with settings id 42 now finds at most 2 companies. Both ids are optional: a run without a settings id uses the agent's code alone, and a run with neither id needs no store. With the conversation id, Typeshift stores the run under conversation 1, and a later run with the same id continues the conversation:
var answer = await research.RunAsync("Vienna", settingsId, conversationId: 1);
Console.WriteLine($"{research.Agent.Name}: {answer.Text}");The limit passes through three layers: 5 in the record, 3 in the agent and 2 in the stored settings. Each layer overrides the one before, so the stored 2 applies. A run can pass settings of its own, a fourth layer over the stored ones, as Settings layering explains.
How the pieces fit
AddTypeshift() registers an AgentHost<TAgent> per agent. The host resolves the settings, calls the model, runs the tools and stores the conversation.
| Piece | What it is | Read |
|---|---|---|
| Toolset | A partial class deriving from Toolset that declares the tools of one domain | Toolsets |
| Tool settings | A record of values a tool needs that the model must not choose | Tool settings |
| Agent | A partial class deriving from Agent, with chat options, instructions and tools | Agents |
| Agent host | AgentHost<TAgent>, which runs one agent in the scope it was resolved from | Using an agent |
| Store | Your ITypeshiftStore, or an ISettingsStore and an IConversationStore, which keep settings and conversations | Settings and stores |
The glossary defines every other term.
What your app implements
Your app implements a few interfaces. Every run needs a chat client provider, and the others come with the feature that uses them:
| Interface | Needed for | Read |
|---|---|---|
IChatClientProvider | Every run. It returns the IChatClient for a provider and a model. | Models and providers |
ITypeshiftStore, or ISettingsStore and IConversationStore | Settings ids and conversation ids. A singleton; InMemoryTypeshiftStore serves tests and samples. | Implementing a store |
IContextProvider | Memory, documents or other data added to every run of an agent. | Context providers |
IToolApprovalRule, IRunMiddleware, IToolMiddleware, IMcpToolProvider | Approval rules, middleware and MCP servers. | The chain |
Docs for coding assistants
The guide is also published as Markdown for coding assistants such as GitHub Copilot, Claude Code or Cursor. Point your assistant at one of these files, so it looks up the API these pages describe instead of guessing from what it learned in training:
| File | Holds |
|---|---|
https://weboost-at.github.io/Typeshift/llms.txt | The guide's pages, each with a link and its one-line description |
https://weboost-at.github.io/Typeshift/llms-full.txt | Every page of the guide in one file |
A guide page's address with .md, such as toolsets.md | That page alone |
llms.txt suits an assistant that fetches pages as it needs them, because it then reads only the pages of the task at hand. Add llms-full.txt to the context of one that cannot fetch. llms.txt follows the llms.txt format, and all three are rebuilt with the site, so they match the pages you read here.
Related
- Toolsets: tools, their names, parameters and results in depth.
- Tool settings: what a settings record may hold and what may change it.
- Agents: everything an agent's constructor can declare.
- Using an agent: the host in a controller, an endpoint or a background job.
- Running agents: real chat clients, structured output and streaming.
- Conversations: resuming, steering and watching runs.
- Glossary: the words these docs use.