--- url: https://weboost-at.github.io/Typeshift/guide/getting-started.md description: >- Build and run a first agent in four steps, then see how the pieces fit, what your app implements, and the docs for coding assistants. --- # 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](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh020)): ```bash dotnet add package Typeshift --prerelease ``` The 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](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#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 `Guid`s, so step 4 does not compile with the ids 42 and 1: ```xml true int ``` ## 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](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh001)). It declares its tools in its constructor, and each tool is a static method: ```cs internal partial class CompanyToolset : Toolset { public CompanyToolset() { WithDisplayName("Companies"); AddTool(FindCompanies) .WithSettings() .Configurable(s => s.Limit); } /// /// Finds the companies of a city. /// /// The city the companies are based in. public static IReadOnlyList 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_companies` and reads its XML docs. * The model never sees the `FindCompaniesSettings` parameter. Typeshift fills it with the tool's settings when it calls the tool. * `Configurable(s => s.Limit)` lets stored settings and runs change `Limit`, while `IncludeInactive` stays as your code sets it. The settings record sets the defaults: ```cs 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: ```cs 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(); ConfigureFindCompanies() .WithLimit(3); } protected override string GetInstructions() => """ Find companies with the find_companies tool and list them. """; } ``` * `Configurable` lets 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 uses `CompanyToolset`, and `WithLimit(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: ```cs var services = new ServiceCollection(); services.AddTypeshift() .WithStore() .WithChatClientProvider(); services.AddKeyedSingleton("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: ```cs 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](https://weboost-at.github.io/Typeshift/guide/models.md#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: ```cs internal sealed class KeyedChatClientProvider(IServiceProvider services) : IChatClientProvider { // One client per provider serves all its models: Typeshift puts the model into every request public ValueTask GetChatClientAsync(string provider, string model, CancellationToken ct) => ValueTask.FromResult( services.GetKeyedService(provider) ?? throw new InvalidOperationException($"No IChatClient is registered for the provider '{provider}'.")); } ``` You configure and run the agent through its host, `AgentHost`, 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](https://weboost-at.github.io/Typeshift/guide/using-an-agent.md)). 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: ```cs await using var scope = provider.CreateAsyncScope(); var research = scope.ServiceProvider.GetRequiredService>(); 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: ```cs 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](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#settings-layering) explains. ## How the pieces fit ```mermaid flowchart LR subgraph compile [Compile time] toolset[Toolset] --> generator[Source generator] agent[Agent] --> generator generator --> functions[An AIFunction per tool] generator --> settings[A Settings record per agent] generator --> registration["AddTypeshift()"] end subgraph runtime [Runtime] host["AgentHost<TAgent>"] --> provider[Your IChatClientProvider] provider --> client[The model's IChatClient] host --> store[Your store] end registration --> host ``` `AddTypeshift()` registers an `AgentHost` 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](https://weboost-at.github.io/Typeshift/guide/toolsets.md) | | Tool settings | A record of values a tool needs that the model must not choose | [Tool settings](https://weboost-at.github.io/Typeshift/guide/tool-settings.md) | | Agent | A `partial` class deriving from `Agent`, with chat options, instructions and tools | [Agents](https://weboost-at.github.io/Typeshift/guide/defining-agents.md) | | Agent host | `AgentHost`, which runs one agent in the scope it was resolved from | [Using an agent](https://weboost-at.github.io/Typeshift/guide/using-an-agent.md) | | Store | Your `ITypeshiftStore`, or an `ISettingsStore` and an `IConversationStore`, which keep settings and conversations | [Settings and stores](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md) | The [glossary](https://weboost-at.github.io/Typeshift/guide/glossary.md) 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](https://weboost-at.github.io/Typeshift/guide/models.md) | | `ITypeshiftStore`, or `ISettingsStore` and `IConversationStore` | Settings ids and conversation ids. A singleton; `InMemoryTypeshiftStore` serves tests and samples. | [Implementing a store](https://weboost-at.github.io/Typeshift/guide/implementing-a-store.md) | | `IContextProvider` | Memory, documents or other data added to every run of an agent. | [Context providers](https://weboost-at.github.io/Typeshift/guide/middleware.md#context-providers) | | `IToolApprovalRule`, `IRunMiddleware`, `IToolMiddleware`, `IMcpToolProvider` | Approval rules, middleware and MCP servers. | [The chain](https://weboost-at.github.io/Typeshift/guide/registration.md#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`](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`](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`](https://weboost-at.github.io/Typeshift/guide/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](https://llmstxt.org) format, and all three are rebuilt with the site, so they match the pages you read here. ## Related * [Toolsets](https://weboost-at.github.io/Typeshift/guide/toolsets.md): tools, their names, parameters and results in depth. * [Tool settings](https://weboost-at.github.io/Typeshift/guide/tool-settings.md): what a settings record may hold and what may change it. * [Agents](https://weboost-at.github.io/Typeshift/guide/defining-agents.md): everything an agent's constructor can declare. * [Using an agent](https://weboost-at.github.io/Typeshift/guide/using-an-agent.md): the host in a controller, an endpoint or a background job. * [Running agents](https://weboost-at.github.io/Typeshift/guide/running-agents.md): real chat clients, structured output and streaming. * [Conversations](https://weboost-at.github.io/Typeshift/guide/conversations.md): resuming, steering and watching runs. * [Glossary](https://weboost-at.github.io/Typeshift/guide/glossary.md): the words these docs use. --- --- url: https://weboost-at.github.io/Typeshift/guide/using-an-agent.md description: >- Get an agent's host from dependency injection and run the agent from a web request, a console app or a background job. --- # Using an agent Your app uses an agent through its host, `AgentHost`, which `services.AddTypeshift()` registers for every agent ([Registration](https://weboost-at.github.io/Typeshift/guide/registration.md)). You get the host like any other service, from the scope of the request or job that runs the agent, so the agent's tools see that scope's services. [Running agents](https://weboost-at.github.io/Typeshift/guide/running-agents.md) covers what a run takes and returns. ## Inject the host in a web app In a web app, add the host to the constructor of the class that runs the agent, such as a controller or a service of your own, or to the parameters of a minimal API endpoint: ```cs internal sealed class RefundDesk(AgentHost support, ICurrentCustomer customer) { public async Task AnswerAsync(string question, CancellationToken ct) { var answer = await support.RunAsync(question, settingsId: customer.Id, ct: ct); return answer.Text; } } ``` `ICurrentCustomer` stands for a scoped service of your app, and the customer's id is the settings id, so that customer's stored settings apply ([Stored settings](https://weboost-at.github.io/Typeshift/guide/running-agents.md#stored-settings)). Register `RefundDesk` as scoped, with `services.AddScoped()`, so ASP.NET Core creates it in each request's scope, together with its host and that request's customer. Controllers and endpoint parameters need no registration of their own. ## Run from a console app Outside a request, such as in a console app, nothing creates a scope for you. Create one from your app's service provider and resolve the host from it: ```cs await using var scope = provider.CreateAsyncScope(); var support = scope.ServiceProvider.GetRequiredService>(); var answer = await support.RunAsync("Where is my refund?"); Console.WriteLine($"{answer.Messages[^1].AuthorName}: {answer.Text}"); ``` ::: warning Resolve the host from a scope, never from the root provider A host resolved from the root provider, such as `app.Services` or what `BuildServiceProvider()` returns, runs with the root's services. With [scope validation](https://learn.microsoft.com/dotnet/core/extensions/dependency-injection#scope-validation) on, as in ASP.NET Core's development environment, the run fails as soon as it resolves a scoped service, such as your chat client provider. Without it, every run shares one instance of each scoped service, so one customer's `ICurrentCustomer` reaches the tools of another. ::: ## Run from a background job A `BackgroundService` is a singleton, so a host it took in its constructor would come from the root provider. Take `IServiceScopeFactory` instead, and create a scope for each job: ```cs internal sealed class TicketDigest(IServiceScopeFactory scopes, TimeProvider time) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { using var timer = new PeriodicTimer(TimeSpan.FromDays(1), time); while (await timer.WaitForNextTickAsync(stoppingToken)) { await DigestAsync(stoppingToken); } } public async Task DigestAsync(CancellationToken ct) { // A scope for each job, so the run gets scoped services of its own, as a request would await using var scope = scopes.CreateAsyncScope(); var support = scope.ServiceProvider.GetRequiredService>(); var answer = await support.RunAsync("Summarize the tickets opened today.", ct: ct); return answer.Text; } } ``` Register the job with `services.AddHostedService()`, and the clock its timer waits on with `services.AddSingleton(TimeProvider.System)`. A test can register a fake `TimeProvider` instead. ## Related * [Getting started](https://weboost-at.github.io/Typeshift/guide/getting-started.md): a first toolset, agent and run. * [Running agents](https://weboost-at.github.io/Typeshift/guide/running-agents.md): settings ids, run settings, typed output, streaming and the raw path. * [Registration](https://weboost-at.github.io/Typeshift/guide/registration.md#what-addtypeshift-registers): every service `AddTypeshift()` registers and its lifetime. * [Agent host](https://weboost-at.github.io/Typeshift/guide/generated-members.md#the-agent-host): every member of the host and its signature. * [Serving a chat endpoint](https://weboost-at.github.io/Typeshift/guide/chat-endpoint.md): the endpoints a chat UI talks to. --- --- url: https://weboost-at.github.io/Typeshift/guide/implementing-a-store.md description: >- Write the store that keeps your agents' settings and conversations in your own database, from its lifetime and versions to conversation summaries. --- # Implementing a store Typeshift keeps your agents' settings through an `ISettingsStore` and your conversations through an `IConversationStore`, which you implement over your own database. This page walks through a store in the order you build it: you register it, implement its interfaces, load and save settings, append conversations, migrate older settings and check it. `InMemoryTypeshiftStore` is for tests and samples, so a production app writes its own. ## Register your store `WithStore()` registers your store as a singleton. One instance handles every run of every request, and several runs often call it at the same time, so plan for that before you write its first method: * **It cannot take scoped services**, such as a `DbContext` or the current customer. Take a factory instead, such as `IDbContextFactory` or `IServiceScopeFactory`, and create what a call needs inside the call. Singleton services, such as options or a logger, are fine. * **Any method can run while others are running**, even on the same document or conversation. Keep no state between calls, or guard what you keep. * **It knows nothing of the caller.** Your app decides who may use a settings id or a conversation id before it passes one to a run ([Conversations](https://weboost-at.github.io/Typeshift/guide/conversations.md#tracking-a-conversation)), and the store keeps whatever it is given. ::: warning A scoped DbContext breaks the store A singleton that takes a `DbContext` keeps the first one it got for the life of the app and shares it between requests, which `DbContext` does not allow. Register `IDbContextFactory` with `AddDbContextFactory` and create a context in each method. ::: This page builds a store on Entity Framework Core. Register its context factory next to your `services.AddTypeshift().WithStore()` call: ```cs services.AddDbContextFactory(options => options.UseSqlite("Data Source=typeshift.db")); ``` ## Implement the interfaces A store has five methods in two interfaces. `ISettingsStore` keeps settings with the first two, and `IConversationStore` keeps conversations with the other three. The store on this page implements `ITypeshiftStore`, which is both. When settings and conversations live in different databases, implement each interface in a class of its own and [register each](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#registering-a-store). You implement four of the methods, and `LoadConversationSinceCompactionAsync` has a default you may replace by declaring the method in your store. Every id has your build's [id type](https://weboost-at.github.io/Typeshift/guide/registration.md#the-id-type), which is `Guid` unless `TypeshiftIdType` names another; the samples on this page set it to `int`. Each method takes a `CancellationToken` last. | Method | Does | |---|---| | `LoadAsync(id, kind)` | Returns the document of a kind under an id as a `StoredDocument`, with its JSON and version, or null when none is stored. | | `SaveAsync(id, kind, json, loadedVersion)` | Saves a document's JSON if its stored version still equals `loadedVersion` (0 when nothing is stored yet), and returns the new version. Throws a `StoreConflictException` when another save came first. | | `LoadConversationAsync(conversation)` | Returns a conversation's entries, oldest first. | | `LoadConversationSinceCompactionAsync(conversation)` | Returns the conversation's last summary and the entries after it ([Start from the last summary](#start-from-the-last-summary)). | | `AppendConversationAsync(conversation, entry)` | Appends one entry to a conversation. The first entry creates the conversation. | The database on this page has a table for settings, keyed by id and kind together ([Key settings by id and kind](#key-settings-by-id-and-kind)), and one for conversations, keyed by a number the database assigns: ```cs internal sealed class TypeshiftDb(DbContextOptions options) : DbContext(options) { public DbSet Documents => Set(); public DbSet Entries => Set(); protected override void OnModelCreating(ModelBuilder model) { model.Entity() .HasKey(document => new { document.Id, document.Kind }); model.Entity() .HasKey(entry => entry.Sequence); model.Entity() .HasIndex(entry => entry.Conversation); } } internal sealed class DocumentRow { public required int Id { get; init; } public required string Kind { get; init; } public required long Version { get; init; } public required string Json { get; init; } } internal sealed class EntryRow { public long Sequence { get; init; } public required int Conversation { get; init; } public required bool IsSummary { get; init; } public required string Json { get; init; } } ``` Typeshift creates no tables. The docs sample creates them with `EnsureCreatedAsync` at startup, and your app creates them as it does its other tables, such as with EF Core migrations. ## Key settings by id and kind A document is an agent's `Settings` record, the one `ConfigureAsync` and `SaveSettingsAsync` save. Typeshift writes the record as JSON before it calls `SaveAsync` and reads it back after `LoadAsync`, so your store handles strings and numbers only. Keep each document in a row keyed by the id and the kind, with the JSON and the [version](#save-with-a-version): * **The id** is the settings id your app passed, such as a customer's. * **The kind** is the agent's class name, or what its `WithStorageKind("...")` sets. One settings id holds a document per agent of a run, sub-agents and handoffs included ([Sub-agents and handoffs](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#sub-agents-and-handoffs)), so the id and the kind together are the key. * **The JSON** is the record as Typeshift wrote it ([The stored JSON](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#the-stored-json)). Keep it as text and return it unchanged. Loading reads the row by its id and kind and returns its JSON and version as a `StoredDocument`: ```cs internal sealed class EfTypeshiftStore(IDbContextFactory contexts) : ITypeshiftStore { public async Task LoadAsync(int id, string kind, CancellationToken ct) { await using var db = await contexts.CreateDbContextAsync(ct); return await db.Documents.FindAsync([id, kind], ct) is { } row ? new StoredDocument(row.Json, row.Version) : null; } ``` ## Save with a version A version is the number of the stored document's current state, kept for optimistic concurrency. It lets a save find out that the document changed since the caller loaded it, so one save never silently overwrites another. Typeshift does not interpret the number: it hands `SaveAsync` back the version `LoadAsync` returned, as `loadedVersion`, and your store compares the two. `loadedVersion` is always the old version. Your store writes the new one itself and returns it, and the caller passes that to its next save: | Call | Stored before | Your store | Returns | |---|---|---|---| | `LoadAsync` | nothing | finds no row | `null` | | `SaveAsync(json, loadedVersion: 0)` | nothing | inserts the row at version 1 | `1` | | `SaveAsync(json, loadedVersion: 1)` | version 1 | updates the row, which still holds 1, to version 2 | `2` | | `SaveAsync(json, loadedVersion: 1)` | version 2 | changes nothing: the row no longer holds 1 | throws `StoreConflictException` | | `SaveAsync(json, loadedVersion: 0)` | version 2 | changes nothing: a row exists | throws `StoreConflictException` | ```mermaid sequenceDiagram participant A as Settings form A participant S as Your store participant B as Settings form B A->>S: LoadAsync: version 3 B->>S: LoadAsync: version 3 A->>S: SaveAsync with version 3 S-->>A: Saved, version 4 B->>S: SaveAsync with version 3 S-->>B: StoreConflictException ``` The diagram shows two forms editing the same customer's settings. Both load version 3, and the first to save wins and moves the document to version 4. The second still names version 3, so its save fails instead of erasing the first form's change, and that form loads the settings again. Runs only load settings; they never save them. Saves come from your app, through `ConfigureAsync`, which loads again and reapplies its change after a lost save, and `SaveSettingsAsync`, which leaves the conflict to your code ([Agent settings](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#agent-settings)). ### Write the save in one statement Run one `INSERT` or `UPDATE` that checks the version as it writes, and throw a `StoreConflictException` when it changed no row. The `loadedVersion` the caller passes tells you which state of the document it loaded: * **Version 0 means the caller found no document**, so insert it. A row that is there already was created by another save in between. * **Any other version is the one the caller loaded**, so update the row only while it still holds that version. A row that holds another version was saved by someone else since, and a missing row was deleted after the caller loaded it. Both are conflicts. Check and write in one statement. If you read the row, compare its version and then write, two saves that both loaded version 3 can both pass the check before either writes, and the second silently overwrites the first. Each of these statements reports how many rows it changed, and none means another save won: ```sql INSERT INTO documents (id, kind, version, json) VALUES (@id, @kind, 1, @json) ON CONFLICT (id, kind) DO NOTHING; UPDATE documents SET json = @json, version = @loaded + 1 WHERE id = @id AND kind = @kind AND version = @loaded; ``` SQL Server has no `ON CONFLICT`. There, run a plain `INSERT` and treat the duplicate-key error, number 2627, as the conflict, passing it to the `StoreConflictException` as its inner exception. Return the new version. Any number works as long as it is not 0 and was never this document's version before, and counting up by one is the simple choice. Throw `StoreConflictException` only for a lost race, with a message that names the document. Any other failure, such as a lost connection, goes to the caller as it is. The EF Core store runs one of the two statements per save. `ExecuteSqlAsync` inserts with `ON CONFLICT DO NOTHING`, which SQLite and PostgreSQL support, and turns each interpolated value into a SQL parameter. Its SQL quotes the table and column names as EF Core names them by default, so match it to your own mapping. `ExecuteUpdateAsync` updates with the version in its `WHERE`. Both return the number of rows they changed: ```cs public async Task SaveAsync(int id, string kind, string json, long loadedVersion, CancellationToken ct) { await using var db = await contexts.CreateDbContextAsync(ct); var rows = loadedVersion == 0 ? await InsertAsync(db, id, kind, json, ct) : await UpdateAsync(db, id, kind, json, loadedVersion, ct); // Each statement checks and writes in one step, so a save that changed no row lost to another save return rows == 1 ? loadedVersion + 1 : throw new StoreConflictException($"{kind} {id} was saved after version {loadedVersion}."); } private static Task InsertAsync(TypeshiftDb db, int id, string kind, string json, CancellationToken ct) => db.Database.ExecuteSqlAsync( $""" INSERT INTO "Documents" ("Id", "Kind", "Version", "Json") VALUES ({id}, {kind}, 1, {json}) ON CONFLICT ("Id", "Kind") DO NOTHING """, ct); private static Task UpdateAsync( TypeshiftDb db, int id, string kind, string json, long loadedVersion, CancellationToken ct) => db.Documents .Where(row => row.Id == id && row.Kind == kind && row.Version == loadedVersion) .ExecuteUpdateAsync( set => set .SetProperty(row => row.Json, json) .SetProperty(row => row.Version, loadedVersion + 1), ct); ``` ## Append to a conversation A conversation only grows. Runs append each step as it happens: the input, every model response, the tool results of each round of calls, steering input and the entry that ends a run ([What is stored](https://weboost-at.github.io/Typeshift/guide/conversations.md#what-is-stored)). Your store never changes or removes an entry: * **Keep the append order.** `LoadConversationAsync` returns the entries in the order they were appended, oldest first. Let the database number the rows, as an identity column does; a timestamp can tie. * **Store each entry as its JSON**, `entry.Serialize()`, and read it back with `ConversationEntry.Deserialize(json)`. A database client that serializes with `System.Text.Json` or Newtonsoft.Json may take the entry as it is ([What is stored](https://weboost-at.github.io/Typeshift/guide/conversations.md#what-is-stored)). * **Keep the append cheap.** Runs append while they work, so `AppendConversationAsync` is the hot path, and a row per entry suits it. ### Start from the last summary When clearing old tool results does not shrink the history enough, [compaction](https://weboost-at.github.io/Typeshift/guide/conversations.md#compaction) summarizes a run's oldest turns. It appends one entry of its own that holds the summary and the messages it kept after the summary, with `IsSummary` set to true. Every later run starts from the latest such entry, because it carries what the runs need from before it, including the conversation's mode and its standing approvals. `LoadConversationSinceCompactionAsync` returns that entry and everything after it, oldest first, or every entry when the conversation has no summary. Its default calls `LoadConversationAsync` and drops what came before the summary, so a long conversation is read whole on every run. To read only what a run needs, copy `entry.IsSummary` into a column of its own when you append, and declare the method in your store to start at the last row that has it: ```cs public async Task> LoadConversationAsync(int conversation, CancellationToken ct) { await using var db = await contexts.CreateDbContextAsync(ct); var rows = await db.Entries .Where(entry => entry.Conversation == conversation) .OrderBy(entry => entry.Sequence) .ToListAsync(ct); return [.. rows.Select(row => ConversationEntry.Deserialize(row.Json))]; } // A run starts from the last summary, so the rows before it stay in the database public async Task> LoadConversationSinceCompactionAsync( int conversation, CancellationToken ct) { await using var db = await contexts.CreateDbContextAsync(ct); var entries = db.Entries.Where(entry => entry.Conversation == conversation); var start = await entries .Where(entry => entry.IsSummary) .MaxAsync(entry => (long?)entry.Sequence, ct) ?? 0; var rows = await entries .Where(entry => entry.Sequence >= start) .OrderBy(entry => entry.Sequence) .ToListAsync(ct); return [.. rows.Select(row => ConversationEntry.Deserialize(row.Json))]; } public async Task AppendConversationAsync(int conversation, ConversationEntry entry, CancellationToken ct) { await using var db = await contexts.CreateDbContextAsync(ct); db.Entries.Add(new EntryRow { Conversation = conversation, IsSummary = entry.IsSummary, Json = entry.Serialize() }); await db.SaveChangesAsync(ct); } } ``` `LoadConversationAsync` still returns every entry, summaries included, for your app's own use, such as showing the whole history. Runs call it too in one case: when the last summary has no `StandingApprovals`, as one stored by a Typeshift version before 0.1.0-alpha.3, they read the whole conversation. Compaction always fills them in, even as an empty list, so set them on any summary you build for a test of your own, as [`VerifyAsync`](#check-your-store) does. ## Migrate older settings A settings document's JSON names the agent's schema version, the one `WithSchemaVersion(n)` sets, in `"$schemaVersion"` ([The stored JSON](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#the-stored-json)), and JSON without it is at schema version 1. Typeshift refuses a schema version newer than the code's, so an app rolled back to older code fails loudly instead of reading settings it does not understand. An agent raises its schema version when a configurable member is renamed or removed, and migrating the stored JSON of older schema versions is then up to you. You can do it in `LoadAsync`, where the kind tells you which agent's settings a row holds: read `"$schemaVersion"`, rewrite the JSON of an older schema version and return it with the version the row holds, or migrate the rows with a script over your database. Each save writes the current schema version, so a migrated document reaches it in your database with its next save. Typeshift reads a schema version you leave as it is, so a renamed or removed member reads as missing ([The stored JSON](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#the-stored-json)). ## Check your store `VerifyAsync` runs this page's rules for keys, versions and conversation entries against your store. Call it on your store from a test of your own in any test framework, with the store over a test database: ```cs var store = new EfTypeshiftStore(contexts); await store.VerifyAsync(9001); ``` * It runs the rules of every interface your store implements: the settings rules for an `ISettingsStore`, the conversation rules for an `IConversationStore`, and both for a store that is both, even when you call it through one of the interfaces. * A broken rule throws an `InvalidOperationException` whose message names the rule and what to change, such as keying documents by id and kind together. An exception your store throws itself passes through as it is, except where a `StoreConflictException` belongs: there the message names the type your store threw instead. * For settings, it saves a document, then one of another kind under the same id, and loads both back. It then saves the first document at version 0, and with its old version after a save with the current one. Those two saves must throw a `StoreConflictException` and leave the document as it was. Every save that succeeds must return a new version other than 0. * For conversations, it appends a run's input and its end, then two summaries with `IsSummary` and `StandingApprovals` set, each followed by a run's end. After the first run and again at the end, it loads the conversation whole and from the last summary on, which is every entry while there is no summary. Each entry must come back unchanged, in the order it was appended. * It writes under the id you pass, as a settings id and as a conversation id, so calling it twice with one id fails with a message that asks for a new id. Give each call an id nothing else uses, such as a new `Guid`, or start from an empty database, as the docs sample does. * It makes one call at a time, so it cannot catch two saves that race. A store that reads the version and then writes passes it and still loses saves under load ([Write the save in one statement](#write-the-save-in-one-statement)). ## Related * [Settings and stores](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md): how stored settings layer over the agent's code, and the JSON a store keeps. * [Conversations](https://weboost-at.github.io/Typeshift/guide/conversations.md): what a run stores, resuming, and compaction. * [Registration](https://weboost-at.github.io/Typeshift/guide/registration.md#the-chain): `WithStore()` among the other chain calls. * [Generated members](https://weboost-at.github.io/Typeshift/guide/generated-members.md#settings): the `Settings` record a store keeps, with its `Kind` and `SchemaVersion`. --- --- url: https://weboost-at.github.io/Typeshift/guide/toolsets.md description: >- Group the tools that belong together into a toolset, declare each one as a static method, and let the generator turn it into an AIFunction. --- # Toolsets A toolset holds tools that belong together, such as the tools for your support tickets. Each tool is a static method, and the generator turns it into an `AIFunction` at compile time, without reflection. ## Design your toolsets You may group tools by feature, by domain or by permission level. How you group them is up to you. An agent offers whole toolsets and can [include or exclude](https://weboost-at.github.io/Typeshift/guide/defining-agents.md#selecting-tools) single tools, so a toolset works best when it holds tools an agent usually offers together. A toolset whose tools only read, for example, suits a sub-agent that must change nothing. A tool that only one agent needs can also [live in that agent](https://weboost-at.github.io/Typeshift/guide/defining-agents.md#add-a-tool-of-the-agent-s-own). ## Declaring tools A toolset is a `partial` class deriving from `Toolset`. This one searches tickets, reads one and closes one, which cannot be undone: ```cs internal partial class TicketToolset : Toolset { public TicketToolset() { WithDisplayName("Tickets"); AddTool(SearchTickets) .ReadOnly() .WithSettings() .Configurable(s => s.IncludeClosed) .Configurable(s => s.Limit); AddTool(GetTicketAsync) .WithDescription("Reads one ticket by its number.") .ReadOnly(); AddTool(CloseTicketAsync) .RequiresApproval(); } /// /// Searches the customer's support tickets, newest first. /// /// Words the ticket's title contains. public static IReadOnlyList SearchTickets( string query, SearchTicketsSettings settings, IServiceProvider services) => services.GetRequiredService().Search(query, settings.IncludeClosed, settings.Limit); /// The ticket's number. public static Task GetTicketAsync(int number, IServiceProvider services, CancellationToken ct) => services.GetRequiredService().GetAsync(number, ct); /// /// Closes a ticket for good. /// /// The ticket's number. /// Why it is closed, as the customer will read it. public static Task CloseTicketAsync(int number, string reason, IServiceProvider services, CancellationToken ct) => services.GetRequiredService().CloseAsync(number, reason, ct); } ``` The calls chained onto `AddTool` describe the tool: | Call | Means | |---|---| | `ReadOnly()` | The tool changes nothing. [Plan mode](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#plan-mode) offers only these. | | `RequiresApproval()` | The tool's calls need approval. Unless a rule, a standing approval or auto mode decides a call, the run ends and your app approves or rejects it in the conversation's next run. See [Approval and modes](https://weboost-at.github.io/Typeshift/guide/tool-approval.md). | | `WithSettings()` | The tool gets [settings](https://weboost-at.github.io/Typeshift/guide/tool-settings.md) the model never sees, such as a search's limit. Put it last in the chain. `WithConfigurableSettings()` does the same, but opens every settable member to stored settings and runs, except the ones `NotConfigurable` keeps in code. | `WithDisplayName("Tickets")` names the toolset for people, for example in a tool picker. `AddTool` takes a static method group, so the generated function can call it directly ([TSH003](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh003)). The generator reads these chains from your source at compile time instead of running the constructor. That is why each `AddTool` sits directly in the constructor ([TSH014](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh014)), as one chain ([TSH015](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh015)). [Builders](https://weboost-at.github.io/Typeshift/guide/builders.md#toolbuilder) lists every call. ## Names and descriptions A tool is named after its method in snake_case, without `Async`: `GetTicketAsync` becomes `get_ticket`. `WithName(...)` picks another name. It must be lower_snake_case ([TSH008](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh008)) and unique across the toolsets of your project and of the projects it references ([TSH007](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh007)). The description matters more than the name. The model reads it to decide when to call the tool, and reads each `` to fill in the arguments. It comes from the method's `` or from `WithDescription(...)`, and the analyzer warns about a tool with neither ([TSH009](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh009)). Your XML docs reach the generator only when the project sets `GenerateDocumentationFile` ([TSH011](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh011)). ::: warning Renaming a method renames its tool Stored settings are keyed by the tool's name. Rename `SearchTickets`, and what your customers stored for `search_tickets` stops applying. Pin the old name with `WithName` before you rename. ::: ## Parameters Parameters become the tool's JSON schema, with their `` docs and default values. Typeshift fills four kinds of parameters itself when it calls the tool. The model never sees them, so they need no doc: | Type | Receives | |---|---| | The record of `WithSettings()` or `WithConfigurableSettings()` | The tool's resolved settings. | | `IServiceProvider` | The run's services: the scope the host was resolved from. | | `CancellationToken` | The call's token. | | `AIFunctionArguments` | The raw arguments the model sent. | The `IServiceProvider` is the scope you resolved the agent's host from. So when you resolve the host in a web request, every tool of the run reaches that request's scoped services, such as the current customer. * A required argument the model leaves out fails the call with an `ArgumentException`. The tool loop reports it back to the model. * An optional argument the model leaves out takes its default. A nullable reference parameter accepts null in the schema, but the model must still send it unless it has a default value. * The properties of a record or class parameter are described by their XML docs, a positional record's `` docs included. Use them to tell the model how to fill a property, such as the format of a code. [Registration](https://weboost-at.github.io/Typeshift/guide/registration.md#xml-docs-of-parameter-types) shows how those docs are collected. * The analyzer rejects `ref` and `out` parameters ([TSH004](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh004)). ## Results A tool returns a value, nothing (`void`), `Task`, `Task`, `ValueTask`, `ValueTask` or `IAsyncEnumerable`. The model receives the result as JSON, serialized through its type's `JsonTypeInfo`, so under [Native AOT](https://weboost-at.github.io/Typeshift/guide/production.md#native-aot) that type needs JSON metadata. The model takes one result per call, so Typeshift reads a stream to its end and sends its items as one array. Under Native AOT, list `List` in your JSON context for it. When a tool throws, the model reads the exception's message. Write it as a hint, such as "No ticket 4711 exists. Search for it first.", and the model can correct its call. Keep secrets out of it, because the message goes to the model provider. ## What a toolset cannot be A toolset derives from `Toolset` directly and has no type parameters ([TSH017](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh017)). Share code between toolsets through static methods rather than a base class. ## Related * [Tool settings](https://weboost-at.github.io/Typeshift/guide/tool-settings.md): values a tool needs that the model must not choose. * [Agents](https://weboost-at.github.io/Typeshift/guide/defining-agents.md#selecting-tools): offering a toolset's tools to an agent. * [Builders](https://weboost-at.github.io/Typeshift/guide/builders.md#toolset): every call a toolset's constructor can make. * [Generated members](https://weboost-at.github.io/Typeshift/guide/generated-members.md#per-toolset): what the generator writes for each toolset. --- --- url: https://weboost-at.github.io/Typeshift/guide/tool-settings.md description: >- Give a tool values the model must not choose, such as a result limit, and decide which of them may change at runtime. --- # Tool settings Some values a tool needs are not the model's to choose, such as how many tickets a search returns. They go in a settings record that the tool receives and the model never sees. ## The settings record A settings record holds values a tool needs. Its parameterless constructor sets the code defaults, here through property initializers: ```cs internal record SearchTicketsSettings { public int Limit { get; init; } = 10; public bool IncludeClosed { get; init; } public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(5); } ``` It must be a record ([TSH005](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh005)). Settings resolve in [layers](#the-layers), from these code defaults up to a single run, and each layer applies its values to a `with` copy of the defaults. ## Configurable members A configurable member is one that stored settings and runs may change. You choose which members are configurable, and you can declare that from either side. `WithSettings()` keeps every member in code unless `Configurable(...)` names it: ```cs AddTool(SearchTickets) .ReadOnly() .WithSettings() .Configurable(s => s.IncludeClosed) .Configurable(s => s.Limit); ``` `WithConfigurableSettings()` opens every settable member unless `NotConfigurable(...)` names it: ```cs AddTool(SearchArticlesAsync) .WithConfigurableSettings() .NotConfigurable(s => s.Limit); ``` The difference shows when you add a member to the record later. Under `WithSettings()` it stays in code until you name it. Under `WithConfigurableSettings()`, stored settings and runs can change it right away, without another code change. * `WithSettings()` takes only `Configurable` after it, and `WithConfigurableSettings()` only `NotConfigurable`. * The generator reads each lambda from your source and never runs it, so each one must be a plain member access, `s => s.Member` ([TSH015](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh015)). * Under `WithConfigurableSettings()`, members marked `[Obsolete]` stay in code without a `NotConfigurable(...)`. * A configurable member needs a public `set` or `init` accessor ([TSH013](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh013)). ::: warning A renamed member loses its stored values Stored settings keep each member under its name. Rename or remove a configurable member, and its stored value reads as missing, so the layer below applies. When that matters, raise the schema version the agent passes to `WithSchemaVersion` and migrate the stored documents, as [Migrate older settings](https://weboost-at.github.io/Typeshift/guide/implementing-a-store.md#migrate-older-settings) explains. ::: ### Storable types A configurable member needs a type settings can store ([TSH012](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh012)): * strings, Booleans, enums (stored by name), `Guid`, `DateTime`, `DateTimeOffset`, `TimeSpan`, `DateOnly`, `TimeOnly`, `Uri` and `ReasoningOptions`; * the numbers `byte`, `short`, `int`, `long`, `float`, `double` and `decimal`; * non-generic classes and records with a public parameterless constructor, no `required` members and storable settable members, replaced as a whole and never merged, and record structs that meet the same rules; * arrays, lists and sets of storable values, and dictionaries keyed by `string` or an enum. Items and dictionary values cannot be nullable value types, and a type that contains itself is not storable. Other structs are not storable, because most keep their state in fields that no public setter writes. ## Receiving settings in the tool The tool takes a parameter of its settings type, and Typeshift fills it with the resolved settings when it calls the tool. The analyzer reports a tool that has settings but no such parameter ([TSH010](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh010)), since its settings would reach nothing: ```cs /// /// Searches the customer's support tickets, newest first. /// /// Words the ticket's title contains. public static IReadOnlyList SearchTickets( string query, SearchTicketsSettings settings, IServiceProvider services) => services.GetRequiredService().Search(query, settings.IncludeClosed, settings.Limit); ``` [Toolsets](https://weboost-at.github.io/Typeshift/guide/toolsets.md#parameters) lists the other parameters the call fills. ## The generated overrides For a tool with configurable members, the generator writes a `{Tool}Overrides` record into the toolset, such as `TicketToolset.SearchTicketsOverrides`, with each configurable member as nullable. Stored settings and run settings set a tool's values through this record, and a null keeps the value of the layer below. [Generated members](https://weboost-at.github.io/Typeshift/guide/generated-members.md#per-toolset) shows it in full. ## The layers A tool's settings resolve through four [settings layers](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#settings-layering): the record's code defaults, the agent's [`Configure{Tool}()`](https://weboost-at.github.io/Typeshift/guide/defining-agents.md#tool-settings), the [stored settings](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#agent-settings) and the [run's settings](https://weboost-at.github.io/Typeshift/guide/running-agents.md#run-settings). The last two set values through the overrides, so they can change only the configurable members. The agent's `Configure{Tool}()` can set every settable member. ## Related * [Toolsets](https://weboost-at.github.io/Typeshift/guide/toolsets.md): declaring the tools that take settings. * [Settings layering](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#settings-layering): which layer wins, and what a layer cannot do. * [Agents](https://weboost-at.github.io/Typeshift/guide/defining-agents.md#tool-settings): setting a tool's settings in one agent. * [Builders](https://weboost-at.github.io/Typeshift/guide/builders.md#toolbuilder): every call of a tool's declaration. --- --- url: https://weboost-at.github.io/Typeshift/guide/defining-agents.md description: >- Declare an agent in its constructor, with its chat options, instructions, tools and their settings. --- # 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() .Include(t => t.SearchTickets) .Include(t => t.CloseTicket); UseToolset() .Exclude(t => t.DeleteArticle); ConfigureSearchTickets() .WithLimit(5); UseMcpServer("status_page") .ReadOnly(); UseContext(); } protected override string GetInstructions() => """ Answer from the customer's tickets and cite help center articles. """; } ``` The tools and their settings have sections of their own below. The other calls declare the rest: | Call | Declares | |---|---| | `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](https://weboost-at.github.io/Typeshift/guide/mcp.md). `ReadOnly()` lets plan mode offer them. | | `UseContext()` | A [context provider](https://weboost-at.github.io/Typeshift/guide/middleware.md#context-providers), whose instructions, messages and tools join every run. | | `UseAgent()`, `UseHandoff()` | Another agent as a tool, or one the model can hand the conversation to. See [Multiple agents](https://weboost-at.github.io/Typeshift/guide/multi-agent.md). | | `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](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh002)), and every declaration sits directly in the constructor as one chain ([TSH014](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh014), [TSH015](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh015)). An agent derives from `Agent` directly, never from a base agent of yours ([TSH017](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#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()` 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() .Exclude(t => t.CloseTicket); ``` 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](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh025)). A toolset from another project needs the Typeshift package in that project ([TSH019](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#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(); } /// /// Returns today's date, to tell whether an invoice is overdue. /// 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."; } ``` * 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](https://weboost-at.github.io/Typeshift/guide/toolsets.md#names-and-descriptions), [parameters](https://weboost-at.github.io/Typeshift/guide/toolsets.md#parameters) and [results](https://weboost-at.github.io/Typeshift/guide/toolsets.md#results). So `GetToday` becomes `get_today`, its `` 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()` and takes the record as a parameter ([Tool settings](https://weboost-at.github.io/Typeshift/guide/tool-settings.md)). 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](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#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](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#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](https://weboost-at.github.io/Typeshift/guide/builders.md#chatoptionsbuilder) 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](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#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); ``` Run options set the [mode](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#the-three-modes) 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](https://weboost-at.github.io/Typeshift/guide/conversations.md#compaction). [Builders](https://weboost-at.github.io/Typeshift/guide/builders.md#runoptionsbuilder) lists each one with its default. ## Tool settings `Configure{Tool}()` sets a tool's [settings](https://weboost-at.github.io/Typeshift/guide/tool-settings.md) 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](https://weboost-at.github.io/Typeshift/guide/generated-members.md#configure-tool) 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: | Member | From | |---|---| | `Provider`, `Model` | Present when `Configurable(o => o.ModelId)` allows them. They override what `WithModel` sets. | | One per configurable chat option | `Instructions` and `Temperature` above. | | One per configurable run option | `Mode` and `MaxToolIterations` from the run options above. | | One per offered tool with configurable settings | `SearchTickets`, `SearchArticles`: their [overrides](https://weboost-at.github.io/Typeshift/guide/tool-settings.md#the-generated-overrides). | Option values are nullable, and so are the values in a tool's overrides. A null keeps the [layer below](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#settings-layering). 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](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#agent-settings), pass it [for one run](https://weboost-at.github.io/Typeshift/guide/running-agents.md#run-settings), 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](https://weboost-at.github.io/Typeshift/guide/implementing-a-store.md#migrate-older-settings)). ## Related * [Toolsets](https://weboost-at.github.io/Typeshift/guide/toolsets.md): declaring the tools an agent offers. * [Tool settings](https://weboost-at.github.io/Typeshift/guide/tool-settings.md): the record a `Configure{Tool}()` sets. * [Running agents](https://weboost-at.github.io/Typeshift/guide/running-agents.md): running an agent through its host. * [Builders](https://weboost-at.github.io/Typeshift/guide/builders.md#agent): every call an agent's constructor can make. * [Generated members](https://weboost-at.github.io/Typeshift/guide/generated-members.md#per-agent): what the generator writes for each agent. --- --- url: https://weboost-at.github.io/Typeshift/guide/mcp.md description: >- Offer the tools of MCP servers to an agent, connected by your app's own MCP client or by the model's provider. --- # MCP servers An agent can offer an MCP server's tools next to its toolsets. Typeshift takes no MCP dependency, so you pick the client. ## Servers your app connects `UseMcpServer` names a server in the agent's constructor. The name is a key you choose, and your app decides which server it stands for: ```cs UseMcpServer("status_page") .ReadOnly(); ``` Your app connects the server by implementing `IMcpToolProvider`. Its `GetToolsAsync` receives the name and returns that server's tools. This provider uses the official `ModelContextProtocol.Core` client and builds the server's URL from the name: ```cs internal sealed class McpToolProvider : IMcpToolProvider, IAsyncDisposable { private readonly Dictionary _clients = []; public async ValueTask> GetToolsAsync(string server, CancellationToken ct) { if (!_clients.TryGetValue(server, out var client)) { var transport = new HttpClientTransport(new() { Endpoint = new Uri($"https://mcp.example.com/{server}") }); _clients[server] = client = await McpClient.CreateAsync(transport, cancellationToken: ct); } return [.. await client.ListToolsAsync(cancellationToken: ct)]; } // The scope that resolved the provider disposes it, and its connections with it public async ValueTask DisposeAsync() { foreach (var client in _clients.Values) { await client.DisposeAsync(); } } } ``` ## Registering the provider Register the provider with `WithMcpToolProvider()`: ```cs services.AddTypeshift() .WithMcpToolProvider(); ``` The provider is scoped, and Typeshift resolves it from the run's services, so each scope, such as a web request, gets its own. That lets it connect each customer with their own credentials. When the scope ends, it disposes the provider along with its connections, so the provider above opens one connection per server for each scope. ## How the agent offers the tools In the tool list the model receives, the servers' tools come after the agent's own. `UseMcpServer(...).RequiresApproval()` makes every call of the server's tools need approval, as [Approval and modes](https://weboost-at.github.io/Typeshift/guide/tool-approval.md) describes. `ReadOnly()` marks every tool of the server as reading only, so [plan mode](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#plan-mode) offers them all. Use it only for a server whose tools change nothing. The run fails when a server's tool is named like one of the agent's own tools or like a tool of another server it uses, because the model would otherwise see only one of them. The error names the server and the tool. An agent that uses servers also fails its run when no provider is registered. ::: danger A server's tools are untrusted Treat a server's tool descriptions and results as untrusted text, never as instructions. The model reads both, so a server you do not control puts its words into the model's context. ::: ## Servers the model provider connects Some model providers call MCP servers themselves. For those, put Microsoft.Extensions.AI's `HostedMcpServerTool` in the chat options: ```cs WithChatOptions() .WithModel("anthropic", "claude-sonnet-5-5") .WithTools(new HostedMcpServerTool("status_page", new Uri("https://mcp.example.com/status"))); ``` The model provider then connects to the server itself, and your `IMcpToolProvider` takes no part. A hosted tool carries no `ReadOnly()` mark, so plan mode hides it. ## Related * [Approval and modes](https://weboost-at.github.io/Typeshift/guide/tool-approval.md): asking before a server's tools run. * [Plan mode](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#plan-mode): which tools plan mode offers. * [Agents](https://weboost-at.github.io/Typeshift/guide/defining-agents.md#selecting-tools): offering toolsets next to servers. * [Builders](https://weboost-at.github.io/Typeshift/guide/builders.md): `UseMcpServer` among the other calls of an agent's constructor. --- --- url: https://weboost-at.github.io/Typeshift/guide/multi-agent.md description: >- Let an agent call another agent as a sub-agent, or hand the conversation to it. --- # Multiple agents An agent can work with others in two ways. It calls one as a sub-agent, a tool that answers with text, or it hands the conversation over to one. ## Sub-agent or handoff | | Sub-agent | Handoff | |---|---|---| | Declared with | `UseAgent()` | `UseHandoff()` | | The model calls | A tool named like the agent, with one `input` | `handoff_to_{name}` | | Works with | A fresh context and no conversation of its own | The whole conversation | | Answers | The calling agent, which gets the text and goes on working | The run, in place of the calling agent | | Needs a conversation | No | Yes | ## Sub-agents `UseAgent()` offers another agent as a tool. The tool is named like the agent, described by its `WithDescription`, and takes one `input` parameter: ```cs internal partial class ReportAgent : Agent { public ReportAgent() { WithChatOptions() .WithModel("anthropic", "claude-sonnet-5-5"); UseAgent(); } protected override string GetInstructions() => "Write a market report. Ask research_agent for the companies of each city you cover."; } ``` A call runs the sub-agent in a fresh context, with no conversation of its own, and returns its answer's text. It runs with the caller's settings id, so the settings stored for the sub-agent under that id apply. It also runs in the caller's mode and shares the caller's [standing approvals](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#approving-for-the-conversation) and [approval rules](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#approval-rules). In [plan mode](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#plan-mode) it researches with its read-only tools and answers the caller, which presents the plan. A sub-agent that fails, for example because no client serves its model, answers its caller with the exception's message, and the calling run goes on. Its tool calls and tokens stay out of the caller's history, which keeps a long run's context small. A sub-agent can read twenty files and answer in one paragraph, and only the paragraph reaches the caller. When the calling agent's run options set `WithConcurrentToolCalls(true)`, the sub-agents the model calls in one response work at the same time. Run the calling agent like any other: ```cs var reports = scope.ServiceProvider.GetRequiredService>(); var report = await reports.RunAsync("Compare the robotics scenes of Vienna and Graz.", settingsId); ``` A sub-agent needs a description and a lower_snake_case name ([TSH023](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh023)), because its name becomes the tool's and the model reads its description to decide when to call it. The analyzer rejects agents that call each other in a circle through `UseAgent` ([TSH026](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh026)), but handoffs may form circles. ::: warning A sub-agent cannot ask for approval A sub-agent cannot stop to ask your app for approval. In the default mode, each of its calls that needs approval is rejected unless a standing approval or a rule covers it. The analyzer warns about such tools ([TSH024](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh024)), so add a rule, or hand off to the agent instead. ::: ### In the background `InBackground()` starts the sub-agent and returns at once, so the caller goes on working: ```cs UseAgent() .InBackground(); ``` When the sub-agent finishes, its answer reaches the calling run as a message before the run's next model call. * A run does not end while its sub-agents work. Once the model stops calling tools, the run waits for the next answer and continues with it. * A run that ends some other way, for example with an error, stops the sub-agents still working. * When a background sub-agent never answered, because the calling run ended first or the process crashed, the conversation's next run tells the model it was lost, so the model can call it again. The sub-agent itself is an agent like any other. This one explores the workspace with a fast model and offers only the reading tools of the app's `WorkspaceToolset`, so none of its calls needs approval: ```cs internal partial class ExploreAgent : Agent { public ExploreAgent() { WithDescription("Explores the workspace and answers one question about it, citing files and lines."); WithChatOptions() .WithModel("openai", "gpt-5-mini"); UseToolset() .Include(t => t.ReadFile) .Include(t => t.ListFiles) .Include(t => t.SearchFiles); } protected override string GetInstructions() => "Answer the question from the files. Read only what you need; quote the lines you rely on."; } ``` ## Handoff `UseHandoff()` gives the model a tool, `handoff_to_{name}`, that carries the other agent's description. For `BillingAgent` it is `handoff_to_billing_agent`. When the model calls it, the other agent takes over and answers in the same run, with the whole conversation and its own instructions, tools and stored settings. Like a sub-agent, it needs a description and a lower_snake_case name ([TSH023](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh023)): ```cs internal partial class TriageAgent : Agent { public TriageAgent() { WithChatOptions() .WithModel("openai", "gpt-5-mini"); UseToolset() .Exclude(t => t.CloseTicket); WithRunOptions() .WithMaxToolIterations(10) .WithConcurrentToolCalls(true) .Configurable(r => r.Mode) .Configurable(r => r.MaxToolIterations); UseHandoff(); UseHandoff(); } protected override string GetInstructions() => "Find out what the customer needs, then hand off to support or billing."; } ``` The conversation records which agent answered last, and its later runs go to that agent. That holds whichever host you start them on, as long as that host's agent can reach the last agent through its handoffs, directly or through other agents' handoffs. Each message carries its agent's name as `AuthorName`: ```cs var triage = scope.ServiceProvider.GetRequiredService>(); var conversationId = 21; var first = await triage.RunAsync("I was charged twice.", settingsId, conversationId); var second = await triage.RunAsync("It was invoice 2024-17.", settingsId, conversationId); Console.WriteLine($"{second.Messages[^1].AuthorName}: {second.Text}"); ``` The `Settings` you pass to one run apply only to the agent you started it on. An agent it hands off to runs with its own settings stored under the same settings id. ::: warning Handoffs need a conversation A run without a conversation id does not offer the handoff tools, so its agent cannot hand off. ::: ## Related * [Agents](https://weboost-at.github.io/Typeshift/guide/defining-agents.md#declaring-an-agent): declaring the other agents an agent uses. * [Settings layering](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#settings-layering): which settings sub-agents and handoffs run with. * [Approval rules](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#approval-rules): rules that decide a sub-agent's calls. * [Compaction](https://weboost-at.github.io/Typeshift/guide/conversations.md#compaction): sub-agents that keep a long run's context small. * [Conversations](https://weboost-at.github.io/Typeshift/guide/conversations.md): where a handoff's conversation is stored. --- --- url: https://weboost-at.github.io/Typeshift/guide/running-agents.md description: >- Run an agent through its host with stored settings, run settings, typed output or streaming, and the steps every run passes through. --- # Running agents You run an agent through its host, `AgentHost`, which your app takes from the scope of the request or job that runs the agent ([Using an agent](https://weboost-at.github.io/Typeshift/guide/using-an-agent.md)). ## Registration ```cs services.AddTypeshift() .WithStore() .WithChatClientProvider() .WithRealtimeTracking() .WithRunMiddleware() .WithToolMiddleware(); services.AddKeyedSingleton("openai", fast); services.AddKeyedSingleton("anthropic", reasoning); ``` `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()` registers your class that returns the client of a provider's model, and `WithStore()` registers the store that keeps settings and conversations. [Registration](https://weboost-at.github.io/Typeshift/guide/registration.md#the-chain) 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](https://weboost-at.github.io/Typeshift/guide/models.md) shows how to write one, how an agent names its model and how to create [a real client](https://weboost-at.github.io/Typeshift/guide/models.md#a-real-client). ## Run an agent ```cs await using var scope = provider.CreateAsyncScope(); var support = scope.ServiceProvider.GetRequiredService>(); var answer = await support.RunAsync("Where is my refund?"); Console.WriteLine($"{answer.Messages[^1].AuthorName}: {answer.Text}"); ``` 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](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md) under it over the agent's code: ```cs var settingsId = 42; var stored = await support.RunAsync("Where is my refund?", settingsId); ``` ## Run settings A run can also pass settings of its own, as an instance of the agent's generated [`Settings`](https://weboost-at.github.io/Typeshift/guide/generated-members.md#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); ``` Values the run leaves null keep what the stored settings or the agent's code set, as [settings layering](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#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](https://weboost-at.github.io/Typeshift/guide/conversations.md) covers both. ## Structured output `RunAsync` asks the model for JSON in `T`'s schema and returns it typed, in `Result`: ```cs var report = await support.RunAsync("Summarize the refund.", settingsId); Console.WriteLine($"{report.Result.Invoice}: {report.Result.State}"); ``` ```cs internal sealed record RefundStatus(string Invoice, RefundState State, DateOnly? Expected); internal enum RefundState { Requested, Approved, Paid } ``` `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](https://weboost-at.github.io/Typeshift/guide/production.md#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); ``` With `WithRealtimeTracking()`, anyone [watching the conversation](https://weboost-at.github.io/Typeshift/guide/conversations.md#live-updates) 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](#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); ``` 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`, `RunStreamingAsync`, `ResumeAsync` or `ResumeStreamingAsync`, or of a client from the [raw path](#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. ```mermaid flowchart TD start(["RunAsync(input, settingsId, conversationId, settings)"]) --> resolve[Resolve the options] resolve --> conversation[Resume the conversation] conversation --> middleware[Run middleware] middleware --> context[Context providers] context --> approval[Approval and mode] approval --> before subgraph loop [Tool loop] before[Store new steps, then compact] --> model[Call the model] model -->|tool calls| tools[Tool middleware, then the tools] tools --> before end model -->|an answer| done([ChatResponse]) ``` 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](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#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](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#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](https://weboost-at.github.io/Typeshift/guide/multi-agent.md#handoff) 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](https://weboost-at.github.io/Typeshift/guide/conversations.md)). 3. **Run middleware.** Each `IRunMiddleware` wraps the rest of the run ([Middleware](https://weboost-at.github.io/Typeshift/guide/middleware.md)). 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](https://weboost-at.github.io/Typeshift/guide/middleware.md#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](https://weboost-at.github.io/Typeshift/guide/tool-approval.md)). 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](https://weboost-at.github.io/Typeshift/guide/defining-agents.md#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](https://weboost-at.github.io/Typeshift/guide/conversations.md#steering) into the run, and the answers of sub-agents running [in the background](https://weboost-at.github.io/Typeshift/guide/multi-agent.md#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](https://weboost-at.github.io/Typeshift/guide/conversations.md#compaction)). Typeshift traces every call to the provider's client ([Observability](https://weboost-at.github.io/Typeshift/guide/production.md#observability)). ### What each step can change | Step | Sees | Can | |---|---|---| | Run middleware | The agent, the messages, the options and the mode | Change the messages and options of this run, or answer in place of the model | | Context provider | The run | Add instructions, messages and tools to this run | | Approval rule | Each call that requires approval, with the arguments the model sent | Approve it, reject it with a reason for the model, or abstain | | Tool middleware | Each tool call: the function, its arguments, the messages so far | Answer in place of the tool, or end the tool loop | ## Related * [Using an agent](https://weboost-at.github.io/Typeshift/guide/using-an-agent.md): getting the host in a web app, a console app or a background job. * [Models and providers](https://weboost-at.github.io/Typeshift/guide/models.md): the chat client provider and how agents name their models. * [Agent host](https://weboost-at.github.io/Typeshift/guide/generated-members.md#the-agent-host): every member of the host and its signature. * [Registration](https://weboost-at.github.io/Typeshift/guide/registration.md#the-chain): the chain calls and their lifetimes. * [Conversations](https://weboost-at.github.io/Typeshift/guide/conversations.md): storing, resuming and steering runs. * [Serving a chat endpoint](https://weboost-at.github.io/Typeshift/guide/chat-endpoint.md): runs, streaming and approvals behind ASP.NET Core endpoints. * [Settings and stores](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#settings-layering): where each of a run's values comes from. * [Architecture](../contributing/architecture.md#a-run): the same pipeline from the code's side. --- --- url: https://weboost-at.github.io/Typeshift/guide/models.md description: >- Name an agent's model by provider and model, return its client from your IChatClientProvider, configure providers and keys per customer, handle a failing provider, and pick the summary model. --- # Models and providers An agent names its model by a provider and a model, and your `IChatClientProvider` returns the client for that pair. Typeshift sends each provider its own model id, so your app never builds composite names like `openai/gpt-5-mini`. A model a user picks is stored like any other setting. ## The chat client provider You implement one method. Typeshift calls it for every run with the provider and the model the run resolved, and again when [compaction](https://weboost-at.github.io/Typeshift/guide/conversations.md#compaction) needs the summary model: ```cs internal sealed class KeyedChatClientProvider(IServiceProvider services) : IChatClientProvider { // One client per provider serves all its models: Typeshift puts the model into every request public ValueTask GetChatClientAsync(string provider, string model, CancellationToken ct) => ValueTask.FromResult( services.GetKeyedService(provider) ?? throw new InvalidOperationException($"No IChatClient is registered for the provider '{provider}'.")); } ``` Typeshift puts the model into every request as `ChatOptions.ModelId`, which is why the sample ignores its `model` parameter and returns one client per provider. Register the provider on the chain, and register your clients however you like, as long as none runs a tool loop of its own ([Wrapping a client](#wrapping-a-client) says why). Here `openAi` and `anthropic` stand for the `IChatClient` instances your app creates, one per provider: ```cs services.AddTypeshift() .WithChatClientProvider(); services.AddKeyedSingleton("openai", openAi); services.AddKeyedSingleton("anthropic", anthropic); ``` ::: info The provider is scoped Typeshift resolves your provider from each run's scope, so it can use that request's services, such as the current customer or your `DbContext` ([A key per customer](#a-key-per-customer)). Typeshift never disposes a client you return, and it instruments each provider and model once, whatever instance you return. ::: ## A real client Each provider's client library has an adapter to `IChatClient` for Microsoft.Extensions.AI. For OpenAI, add the `Microsoft.Extensions.AI.OpenAI` package: ```bash dotnet add package Microsoft.Extensions.AI.OpenAI ``` Then register one client for the provider: ```cs var openAi = new OpenAIClient(apiKey) .GetChatClient("gpt-5-mini") .AsIChatClient(); services.AddKeyedSingleton("openai", openAi); ``` * Typeshift names the model in every request as `ChatOptions.ModelId`, so this one client serves every OpenAI model your agents name. The model you pass to `GetChatClient` only names the client. * `OpenAIClient` is in the `OpenAI` namespace. Read the API key from your app's configuration, never from code. A local server with an OpenAI-compatible API, such as LM Studio or Ollama, works with the same package. Point the client at its endpoint, and name its models in `WithModel`, here as `WithModel("local", "qwen3-8b")`. `ApiKeyCredential` is in the `System.ClientModel` namespace: ```cs var options = new OpenAIClientOptions { Endpoint = new Uri("http://localhost:1234/v1") }; var local = new OpenAIClient(new ApiKeyCredential("none"), options) .GetChatClient("qwen3-8b") .AsIChatClient(); services.AddKeyedSingleton("local", local); ``` For other providers, such as Anthropic or Azure OpenAI, register the `IChatClient` of their own package. [Microsoft.Extensions.AI](https://learn.microsoft.com/dotnet/ai/microsoft-extensions-ai) lists them. A fake client for tests reads the same requests: the model in `ChatOptions.ModelId`, the agent's instructions in `ChatOptions.Instructions` and the run's tools in `ChatOptions.Tools`. It calls a tool by answering with a `FunctionCallContent`, and Typeshift runs the call. ## Providers from configuration Your app can read its providers from [configuration](https://learn.microsoft.com/dotnet/core/extensions/configuration) instead of code, so adding a provider or moving one to another endpoint changes only `appsettings.json`. A local server takes any key, so its entry may hold one, such as `none`: ```json { "Providers": { "openai": {}, "local": { "Endpoint": "http://localhost:1234/v1", "ApiKey": "none" } } } ``` Each entry under `Providers` becomes a keyed singleton `IChatClient`, registered under the entry's name, here for providers with an OpenAI-compatible API: ```cs foreach (var provider in configuration.GetSection("Providers").GetChildren()) { var options = new OpenAIClientOptions(); if (provider["Endpoint"] is { } endpoint) options.Endpoint = new Uri(endpoint); var key = provider["ApiKey"] ?? throw new InvalidOperationException($"Providers:{provider.Key}:ApiKey is not configured."); var client = new OpenAIClient(new ApiKeyCredential(key), options) .GetChatClient("default") .AsIChatClient(); services.AddKeyedSingleton(provider.Key, client); } ``` * The entry's name, such as `openai`, is the provider your agents name in `WithModel` and your users pick in their settings, so the `KeyedChatClientProvider` above finds each client by it. * Keep real keys out of `appsettings.json`. The .NET host reads `Providers:openai:ApiKey` from [user secrets](https://learn.microsoft.com/aspnet/core/security/app-secrets) in development and from the environment variable `Providers__openai__ApiKey` in every environment. * A provider without a key fails at startup, so a missing secret never surfaces in the middle of a run. ```bash dotnet user-secrets set "Providers:openai:ApiKey" "" ``` ## A key per customer When each customer brings their own API key, the client depends on who is asking. Because your provider is scoped, it can read the current customer and fetch that customer's key for each run: ```cs internal sealed class TenantChatClientProvider(ICurrentCustomer customer, ITenantKeys keys) : IChatClientProvider { // A client per run, with the customer's current key public async ValueTask GetChatClientAsync(string provider, string model, CancellationToken ct) { if (provider != "openai") throw new InvalidOperationException($"No client for the provider '{provider}'."); var key = await keys.GetApiKeyAsync(customer.Id, provider, ct); return new OpenAIClient(key) .GetChatClient(model) .AsIChatClient(); } } ``` Register it on the chain, in place of a provider for your app's own keys: ```cs services.AddTypeshift() .WithChatClientProvider(); ``` * `ICurrentCustomer` and `ITenantKeys` stand for services of your app: who the request is for, and where you keep the keys your customers entered. Both may be scoped, since the provider is. * A new client per run costs little, because every `OpenAIClient` shares one `HttpClient`, and it always holds the customer's current key. A client library whose clients each open their own connections needs a cache instead, kept in a singleton and keyed by the API key, so a changed key gets a new client. * `GetChatClientAsync` throws for any provider but `openai`, so a run that names another fails before it starts ([When a provider fails](#when-a-provider-fails)). ## Wrapping a client You may wrap a provider's client before Typeshift gets it, with the `Use...` calls of Microsoft.Extensions.AI's [client pipeline](https://learn.microsoft.com/dotnet/ai/microsoft-extensions-ai), for example to log each request. `AddKeyedChatClient` registers the client and returns its builder, which resolves the `ILoggerFactory` from your services: ```cs var openAi = new OpenAIClient(apiKey) .GetChatClient("gpt-5-mini") .AsIChatClient(); services.AddKeyedChatClient("openai", openAi) .UseLogging(); ``` Wrap what concerns a single request, such as logging or a policy of your own written as a `DelegatingChatClient`. At the `Trace` level, `UseLogging()` logs every message and answer in full, and those are your customers' data ([Message content](https://weboost-at.github.io/Typeshift/guide/production.md#message-content)). Two wrappers repeat what Typeshift already does: * `UseFunctionInvocation()` runs a second tool loop inside the one Typeshift runs. Its tool calls bypass your [tool middleware](https://weboost-at.github.io/Typeshift/guide/middleware.md) and the run's limits, such as `WithMaxToolIterations`, and a run that answers an [approval request](https://weboost-at.github.io/Typeshift/guide/tool-approval.md) fails. * `UseOpenTelemetry()` records every model call again, on Microsoft.Extensions.AI's own source and meter. Typeshift already writes a `chat {model}` span and the metrics for each call ([Spans](https://weboost-at.github.io/Typeshift/guide/production.md#spans)), so an app that listens to both sees every call twice. Leave retries to the client library, which knows which of its errors are worth another try. The OpenAI client retries timeouts, rate limits and server errors three times by default, and its `RetryPolicy` sets how many times. `ClientRetryPolicy` is in the `System.ClientModel.Primitives` namespace: ```cs var options = new OpenAIClientOptions { RetryPolicy = new ClientRetryPolicy(maxRetries: 5) }; var openAi = new OpenAIClient(new ApiKeyCredential(apiKey), options) .GetChatClient("gpt-5-mini") .AsIChatClient(); services.AddKeyedSingleton("openai", openAi); ``` ## The provider id type A provider is a string unless you choose another type. Set `TypeshiftProviderIdType` once for the build, in `Directory.Build.props`, as you set the [id type](https://weboost-at.github.io/Typeshift/guide/registration.md#the-id-type): ```csproj Typeshift.Samples.Chat.Provider ``` Because the agent's `Settings` store a provider as a single value, the type must be a string, a number, a `Guid` or an enum. A `Guid` suits providers that are rows in your database, and an enum suits providers known in code: the snippet names the enum `Typeshift.Samples.Chat.Provider`, so the compiler checks every provider you name, as in `WithModel(Provider.Scripted, "scripted-model")`. Your provider class still implements `IChatClientProvider`. The generator writes that name as an alias for your provider id type, so `GetChatClientAsync` takes a provider of your type instead of a string. A public agent with a configurable model names the type in its `Settings`, so the type must be public too ([TSH006](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh006)). The analyzer reports a provider of another type ([TSH030](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh030)), a type it cannot use ([TSH029](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh029)), and a referenced project built with another provider id type ([TSH021](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh021)). ## Naming a model `WithModel` takes the provider first, then the model as that provider's API names it: ```cs WithChatOptions() .WithModel("openai", "gpt-5-mini") .WithTemperature(0.3f) .Configurable(o => o.ModelId) .Configurable(o => o.Instructions) .Configurable(o => o.Temperature); ``` `Configurable(o => o.ModelId)` adds `Provider` and `Model` to the agent's [`Settings`](https://weboost-at.github.io/Typeshift/guide/generated-members.md#settings), so stored settings or a run can pick another model. The two values [layer](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#settings-layering) separately: a layer that sets only `Model` keeps the provider of the layer below, so set both when you switch providers. Leave out `WithModel` but keep `Configurable(o => o.ModelId)`, and the choice is the user's. Every run then needs a provider and a model from stored settings or from the run's own settings, and a run without them fails before it starts ([When a provider fails](#when-a-provider-fails)). Store the pick under a settings id, from a settings page that offers your providers and their models, for example: ```cs await support.ConfigureAsync(settingsId, settings => settings with { Provider = "anthropic", Model = "claude-sonnet-5-5", Instructions = "Answer in German.", SearchTickets = settings.SearchTickets with { Limit = 20 } }); ``` ## The summary model [Compaction](https://weboost-at.github.io/Typeshift/guide/conversations.md#compaction) summarizes the oldest turns with the run's own model unless you name another. In code you name only the model, and the run's provider serves it: ```cs WithRunOptions() .WithMode(AgentMode.Plan) .WithMaxToolIterations(500) .WithMaxInputTokens(150_000) .WithSummaryModel("claude-haiku-5-5") .Configurable(r => r.Mode) .Configurable(r => r.MaxToolIterations) .Configurable(r => r.SummaryModel); ``` With `Configurable(r => r.SummaryModel)`, stored settings or a run may name another provider for it too: ```cs var settings = new CodingAgent.Settings { SummaryProvider = "openai", SummaryModel = "gpt-5-mini" }; ``` | Named | The summary runs on | |---|---| | Nothing | The run's provider and model | | `WithSummaryModel(model)`, or `SummaryModel` in settings | The run's provider, that model | | `SummaryProvider` and `SummaryModel` in settings | That provider, that model | ::: warning The summary model follows the run's provider The run's provider is whichever provider the layers resolved for that run, so it changes when a user switches the agent to another provider. The summary model named in code then goes to that provider too, and if that provider cannot serve the model, the run fails. Make the summary model configurable wherever users switch providers. ::: ## When a provider fails What your code sees depends on whether the run fails before or after it touches the conversation: | What fails | Your code gets | The run's conversation | |---|---|---| | No chat client provider is registered | An `InvalidOperationException` that names `WithChatClientProvider()` | Unchanged | | The run resolved no provider or no model | An `InvalidOperationException` that names the agent | Unchanged | | Your provider throws for the run's model, such as for a provider it does not know | Your provider's exception | Unchanged | | Your provider throws for the summary model, when compaction asks for it during the run | Your provider's exception | Ends with an entry that holds an `ErrorContent` | | The model call fails, such as on a wrong key, an unknown model or a rate limit that outlasts the retries | The client library's exception | Ends with an entry that holds an `ErrorContent` | * Typeshift asks your provider for the run's client before it touches the conversation, so the first three fail before anything is stored, and before the run's `invoke_agent` span starts. Run the agent again once you fix the cause. * A run that fails later has stored its input already ([What is stored](https://weboost-at.github.io/Typeshift/guide/conversations.md#what-is-stored)), so a retry with the same input puts it into the conversation twice. `ResumeAsync` refuses such a conversation, because its last run ended. * The OpenAI client throws a `ClientResultException`, from `System.ClientModel`, with the HTTP status in `Status`. A timeout that outlasts the retries throws an `AggregateException` instead. * A streaming run throws each of these from the loop over its updates. * Typeshift never checks a model's name: the provider rejects an unknown one when a run calls it, so a typo saved on a settings page fails a later run. Check the pick against the models the provider offers when your settings page saves it. ## Related * [Running agents](https://weboost-at.github.io/Typeshift/guide/running-agents.md): the run's services and the steps of a run. * [Settings and stores](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#settings-layering): where each of a run's values comes from. * [Registration](https://weboost-at.github.io/Typeshift/guide/registration.md#the-chain): the chain calls and their lifetimes. * [Builders](https://weboost-at.github.io/Typeshift/guide/builders.md#chatoptionsbuilder): every chat and run option. * [In production](https://weboost-at.github.io/Typeshift/guide/production.md#observability): the spans and metrics of each model call. --- --- url: https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md description: >- How a run's settings layer from the code through stored settings to the run, and the one store that keeps settings and conversations. --- # Settings and stores Typeshift stores each agent's `Settings` under a settings id, and conversations under a conversation id. Settings go through an `ISettingsStore` and conversations through an `IConversationStore`, which you [implement](https://weboost-at.github.io/Typeshift/guide/implementing-a-store.md) with the id type you already use, in one class or in two. ## Settings layering A run's settings come from four layers, and each sets values over the one below. Tool settings, chat options and run options all resolve this way, once, when the run starts. ### One value, four layers Follow the `find_companies` limit of the [getting started](https://weboost-at.github.io/Typeshift/guide/getting-started.md) agent up through the layers: ```mermaid flowchart BT code["1. The code's defaults: Limit = 5"] --> agent["2. The agent's code: WithLimit(3)"] agent --> stored["3. The stored settings: Limit = 2"] stored --> run["4. The run: Limit = 1"] run --> tool["find_companies runs with Limit = 1"] ``` **1. The code's defaults.** `FindCompaniesSettings` initializes `Limit` to 5, so a record made with its parameterless constructor holds 5. This layer sets every member, so a tool always receives a whole record. **2. The agent's code.** `ResearchAgent` calls `ConfigureFindCompanies().WithLimit(3)`, so every run of the agent searches with 3 unless a layer above sets another limit. Its chat and run options, such as `WithTemperature(0.2f)`, live here too. Because this layer is reviewed, compiled code, it may set any settable member, configurable or not. **3. The stored settings.** `ConfigureAsync(42, ...)` stores `Limit = 2` and `Temperature = 0.4f`, so every run with settings id 42 searches with 2, at a temperature of 0.4. `IncludeInactive` cannot be stored, because `ResearchAgent.Settings` has only the configurable members. **4. The run.** A run that passes `FindCompanies = new() { Limit = 1 }` in its settings searches with 1, for that run only. It leaves `Temperature` null, so the stored 0.4 holds. ### Precedence The highest layer that sets a value wins: | Layer | Set by | May set | Holds for | |---|---|---|---| | 4. The run | The `settings` argument of `RunAsync` and the other runs | Configurable members | That run | | 3. Stored | `Settings` under the run's settings id: `ConfigureAsync`, `SaveSettingsAsync` | Configurable members | Every run with that settings id | | 2. The agent's code | `Configure{Tool}()`, `WithChatOptions()`, `WithRunOptions()`, `GetInstructions()` | Every settable member | Every run of the agent | | 1. The code's defaults | The settings record's parameterless constructor; the run options' defaults | Every member | Every run | Because the layers resolve when a run starts, settings saved during a run apply from the next run on. ### Null values and nested records A layer either replaces a value below it or leaves it alone. It never merges, so a configurable member that is itself a record or a class is replaced as a whole. ::: warning No layer clears a value Null keeps the layer below, so neither stored settings nor a run can take away a temperature the agent's code set. ::: ### The model and the instructions The model resolves through the layers like any other value. It stays as the code sets it unless `Configurable(o => o.ModelId)` lets the layers above choose it, so stored settings can change the model only when the agent's code allows that. Its provider is a value of its own, so a layer that sets only `Model` keeps the provider of the layer below. Stored or run `Instructions` replace the text that `GetInstructions()` returns. Context providers and plan mode then append to whatever instructions the layers resolved. ### The mode of a conversation A run with a conversation id continues in the mode (`Default`, `Plan` or `Auto`) that the conversation's last run ended in. That mode overrides the agent's code and the stored settings, but not the run's own settings. A run whose own settings name a mode switches the conversation to it, and so does an approved plan ([The three modes](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#the-three-modes)). ### Sub-agents and handoffs Sub-agents and handoffs keep the settings id, but not the run's settings. A sub-agent runs with its own settings stored under the caller's settings id, in the caller's mode. An agent handed off to answers with its own stored settings under the same id. Run settings apply only to the agent the run started on ([Multiple agents](https://weboost-at.github.io/Typeshift/guide/multi-agent.md)). ## Registering a store Register the store in the `AddTypeshift()` chain: ```cs var services = new ServiceCollection(); services.AddTypeshift() .WithStore() .WithChatClientProvider(); services.AddKeyedSingleton("openai", new CannedChatClient()); await using var provider = services.BuildServiceProvider(validateScopes: true); ``` `InMemoryTypeshiftStore` keeps everything in memory and serves tests and samples. For your database, write a store of your own: [Implementing a store](https://weboost-at.github.io/Typeshift/guide/implementing-a-store.md) covers its methods, its versions and the conversation summaries it reads. `WithStore()` registers each store interface your class implements, so an `ITypeshiftStore` takes one call. A store of settings and a store of conversations take one call each. ::: warning The store is a singleton `WithStore()` registers the store as a singleton, so one instance serves every run at the same time. It cannot take scoped services, such as a `DbContext`: [Register your store](https://weboost-at.github.io/Typeshift/guide/implementing-a-store.md#register-your-store) shows how to build one around that. ::: Without a store, agents run from their code alone. A run that passes neither a settings id nor a conversation id works, tools included. Every call that loads or saves, such as a run with either id or `ConfigureAsync`, throws an `InvalidOperationException` that names `WithStore()` and the interface no registered store implements. ## Agent settings A settings id stands for whatever you decide: a customer, a team, a user, or a settings profile of your own. Under one settings id, each agent has its own document, because each agent's `Settings` is its own kind, so a store keys documents by id and kind together. `ConfigureAsync` loads an agent's settings, applies your change and saves them. When another save came in between, it loads them again and reapplies your change, ten tries in all. If the tenth save loses too, it throws the store's `StoreConflictException`: ```cs await support.ConfigureAsync(settingsId, settings => settings with { Provider = "anthropic", Model = "claude-sonnet-5-5", Instructions = "Answer in German.", SearchTickets = settings.SearchTickets with { Limit = 20 } }); ``` A settings form does it in two steps: it loads the settings with their version and saves them back with it. The version tells the store which state the form loaded, so the save throws a `StoreConflictException` when someone saved in between ([Save with a version](https://weboost-at.github.io/Typeshift/guide/implementing-a-store.md#save-with-a-version) explains the number): ```cs var stored = await support.LoadSettingsAsync(settingsId); try { await support.SaveSettingsAsync(settingsId, stored.Data with { Instructions = null }, stored.Version); } catch (StoreConflictException) { stored = await support.LoadSettingsAsync(settingsId); } ``` Saving `Instructions = null` removes the stored instructions, so runs with this settings id use those of `GetInstructions()` again. After a conflict, the sample loads the settings again, so you can show the user the current values before they save. ## The stored JSON What is stored is the record's JSON: camelCase, with the tools nested under `"tools"` by name and nulls left out. ```json { "provider": "anthropic", "model": "claude-sonnet-5-5", "instructions": "Answer in German.", "tools": { "search_tickets": { "limit": 20 } } } ``` An agent whose `WithSchemaVersion(n)` raised its schema version above 1 also writes `"$schemaVersion": n` first. `System.Text.Json` writes and reads the same JSON, so an endpoint that binds `SupportAgent.Settings` accepts and returns the JSON the store keeps. A stored member that cannot be read, because its value has the wrong type or its name is unknown, counts as missing, so the layer below applies. When a configurable member is itself a record or a class, a stored value replaces the whole object, so members missing from its stored JSON take that type's defaults, not the values of the layer below. ::: warning A renamed member loses its stored value A configurable member that is renamed or removed reads as missing, so its stored value no longer applies. When that matters, raise the agent's schema version with `WithSchemaVersion(2)` and migrate the documents of the older schema version ([Migrate older settings](https://weboost-at.github.io/Typeshift/guide/implementing-a-store.md#migrate-older-settings)). ::: ## Related * [Tool settings](https://weboost-at.github.io/Typeshift/guide/tool-settings.md): declaring a tool's settings and opening members to the layers. * [Implementing a store](https://weboost-at.github.io/Typeshift/guide/implementing-a-store.md): the store's methods, its versions and conversation summaries. * [Generated members](https://weboost-at.github.io/Typeshift/guide/generated-members.md#settings): the `Settings` record a store keeps. * [Running agents](https://weboost-at.github.io/Typeshift/guide/running-agents.md#run-settings): passing a settings id and run settings. * [Conversations](https://weboost-at.github.io/Typeshift/guide/conversations.md): what a run stores in a conversation, and resuming it. * [Registration](https://weboost-at.github.io/Typeshift/guide/registration.md#the-id-type): the id type and the other chain calls. --- --- url: https://weboost-at.github.io/Typeshift/guide/conversations.md description: >- Store a conversation under an id, resume it after a crash, steer and watch a run, and compact a long one. --- # Conversations Pass a conversation id to a run, and Typeshift stores every step under it. The next run picks up where the last one ended, even after a crash. ## Tracking a conversation ```cs var conversationId = 7; await support.RunAsync("Where is my refund?", conversationId: conversationId); await support.RunAsync("When will it arrive?", conversationId: conversationId); ``` The first run starts the conversation and the second resumes it. Typeshift puts the stored messages ahead of the new input, so the model sees the whole conversation. Because Typeshift sends the history itself, it never passes the conversation id on to the provider. Settings ids and conversation ids are independent, and a run may pass either or both. A conversation does not remember a settings id, so pass it to every run of the conversation that should use the stored settings, the run that answers an approval included. Typeshift never creates ids ([the id type](https://weboost-at.github.io/Typeshift/guide/registration.md#the-id-type)). You choose the conversation id, for example the key of your own record of the conversation, and pass the same id to every run of that conversation. ::: danger Check the caller before you pass a conversation id Typeshift resumes whichever conversation it is given, so deciding who may see one is your app's job. Check the caller against your own record of the conversation, as you would for any other id from a request. ::: ## What is stored A conversation is a list of entries, appended as they happen: each run's input, every model response with its model and token usage, the tool results of each round of calls, and an entry that ends the run and records its mode. Your store receives each entry through `AppendConversationAsync` ([Implementing a store](https://weboost-at.github.io/Typeshift/guide/implementing-a-store.md#append-to-a-conversation)). Each entry is a `ConversationEntry`: the name of its agent in `Agent`, its `Messages` and the time it was stored in `At`, plus the `Model` and `Usage` of a model response, and `EndsRun` on the entry that ends a run. `IsSummary` is true on the entry [compaction](#compaction) appends when it summarizes, which is how your code and your store tell a summary apart ([Start from the last summary](https://weboost-at.github.io/Typeshift/guide/implementing-a-store.md#start-from-the-last-summary)). An entry serializes itself with `Serialize()` and `ConversationEntry.Deserialize(json)`, and JSON libraries handle it as it is. `System.Text.Json` writes and reads that same JSON, and Newtonsoft.Json writes it as a string, so a store or an API can pass entries on with either library and no settings. ```cs var store = provider.GetRequiredService(); var entries = await store.LoadConversationAsync(conversationId, ct); Console.WriteLine($"{entries.Count} entries, {entries.Sum(entry => entry.Usage?.TotalTokenCount)} tokens"); ``` Runs that fail or stop early are stored too: * A failed run stores an entry with an `ErrorContent` that ends the run, then rethrows the exception to your code. * A streaming run that fails, is cancelled, or that your code stops reading early stores the updates that had arrived. * A cancelled run is stored too, because Typeshift stores without the run's cancellation token. * A run cut off by a crash has no ending entry, and [resuming](#resuming) continues it. When the store fails to save, the failure is logged and recorded on the run's span, and the run still returns its answer. Whenever a run loads the conversation, it leaves tool calls that never got a result and error messages out of what the model sees, because providers reject them. Some providers, such as local servers with an OpenAI-compatible API, number their tool calls per response, so a later call can get an id the conversation holds already. Typeshift renames such a call, as `call_1_2`, before it stores or runs it, so the stored entries, `ApprovalRequests` and the tool's result all carry the new id. ## Resuming Each model response is stored as it arrives, and tool results before the next model call, so an app that crashes mid-run loses at most the calls in flight. `ResumeAsync` continues the conversation's interrupted run from its last stored step, without new input: ```cs var resumed = await support.ResumeAsync(interrupted, settingsId); ``` On a conversation whose last run ended, `ResumeAsync` throws an `InvalidOperationException`. After a restart, the entries tell you which conversations to resume: when the last entry that `LoadConversationAsync` returns has `EndsRun` false, a crash cut that run off. A new run with input works on an interrupted conversation too: it starts from the interrupted run's steps and adds its own input. ::: warning A tool can be called twice A call interrupted while its tool ran is left out on resume. The model decides again and may call the tool a second time, so make tools with side effects safe to repeat. ::: ## One run at a time Within a process, a conversation has one run at a time. Starting a second while the first goes on throws an `InvalidOperationException`, so [steer](#steering) the running one or wait until it ends. A run that starts while another is still loading the conversation waits until that run has begun. Across processes, keeping to one run per conversation is up to your app. ## Live runs `ConversationTracker` lists the runs with a conversation id that are going on in this process, each with its conversation, the agent that answers, its model and when it started: ```cs var tracker = provider.GetRequiredService(); foreach (var run in tracker.Runs) Console.WriteLine($"{run.Agent} is answering conversation {run.Conversation} on {run.Model}"); ``` `WatchRunsAsync` tells a UI when that list changes, so a list of running conversations needs no polling. It yields the runs going on, then each run as it starts and again as it ends, with `Ended` set, until you cancel its token. Cancelling ends the loop with an `OperationCanceledException`, so catch it where you stop watching. It works without `WithRealtimeTracking()`: ```cs await foreach (var run in tracker.WatchRunsAsync(ct)) Console.WriteLine($"Conversation {run.Conversation} is {(run.Ended ? "idle" : "running")}"); ``` ## Steering `TrySteer` hands new input to a conversation's run while it works, for example when a user adds a request halfway through. The model receives it before its next call, and the run does not end before then. The input is stored like any other. When the run had asked for approval, its requests close once it answers the input, as they would for a new message ([When a request closes](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#when-a-request-closes)). `TrySteer` returns false when no run of the conversation is going on in this process, or when that run is already ending. Start a run with the input instead: ```cs var input = "Send me the invoice too."; if (!tracker.TrySteer(conversationId, input)) await support.RunAsync(input, settingsId, conversationId); ``` To stop a run, cancel its token. It is stored up to where it stopped. ## Live updates With `WithRealtimeTracking()`, Typeshift publishes the updates of runs as they happen, and `WatchAsync` reads one conversation's updates until you cancel its token, which ends the loop with an `OperationCanceledException`, as with `WatchRunsAsync`. A streaming run publishes its updates as they stream; a run that does not stream publishes each model response and tool result as it stores it. A UI can forward them over SignalR, for example: ```cs await foreach (var update in tracker.WatchAsync(conversationId, ct)) Console.Write(update.Text); ``` Text, reasoning and usage updates are merged and published at most every 100 ms, and other updates go out at once. A run never waits for a watcher, so a watcher that falls more than 256 updates behind loses the oldest ones. You can change both limits with `RealtimeTrackingOptions`: ```cs services.AddTypeshift() .WithRealtimeTracking(new RealtimeTrackingOptions { PublishInterval = TimeSpan.FromMilliseconds(250), MaxPendingUpdates = 1024 }); ``` Without `WithRealtimeTracking()`, `WatchAsync` throws an `InvalidOperationException`. ::: warning Live updates stay in their process A watcher sees only the runs of its own process. When your app runs in several processes, watch in the process that runs the conversation. ::: ## Compaction A coding agent can make hundreds of model calls in one run, and its history soon outgrows the model's context. Compaction shrinks the history each call sees. It is off until `WithMaxInputTokens` sets a limit, and the other [run options](https://weboost-at.github.io/Typeshift/guide/builders.md#runoptionsbuilder) let such a run go on for long enough: here `WithMaxToolIterations(500)` raises the limit on rounds of tool calls to 500. The agent also starts in [plan mode](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#plan-mode), which has nothing to do with compaction: ```cs WithRunOptions() .WithMode(AgentMode.Plan) .WithMaxToolIterations(500) .WithMaxInputTokens(150_000) .WithSummaryModel("claude-haiku-5-5") .Configurable(r => r.Mode) .Configurable(r => r.MaxToolIterations) .Configurable(r => r.SummaryModel); ``` Before every model call, Typeshift estimates the call's input tokens at about four characters per token, corrected by what the provider reported for the run's last call, or for the conversation's last call before the run. When the estimate reaches the maximum, it shrinks the history to `WithTargetInputTokens`, half the maximum unless set: 1. It clears the results of the oldest tool calls the model has already read. It keeps the calls, so the model knows what it did and can call again. 2. If that is not enough, it summarizes the oldest turns with the [summary model](https://weboost-at.github.io/Typeshift/guide/models.md#the-summary-model), the run's own unless you name another. The instructions and the latest call of a tool named `write_todos`, if the agent offers one, always stay, and a cut never separates a tool call from its result. Every later model call of the same run starts from the history as compaction left it, with the same results cleared and the same summary. The stored conversation keeps everything. Compaction appends an entry of its own, with `IsSummary` set to true, that holds the summary and the messages it kept after it. Later runs start from that entry instead of summarizing again, and read nothing before it: the entry also keeps the conversation's mode, its [approvals for the conversation](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#approving-for-the-conversation) and the background agents still working. ## Related * [Running agents](https://weboost-at.github.io/Typeshift/guide/running-agents.md): passing a conversation id to a run. * [How a run works](https://weboost-at.github.io/Typeshift/guide/running-agents.md#how-a-run-works): where a run resumes and stores its steps. * [Implementing a store](https://weboost-at.github.io/Typeshift/guide/implementing-a-store.md#append-to-a-conversation): the store methods that keep conversations. * [Builders](https://weboost-at.github.io/Typeshift/guide/builders.md#runoptionsbuilder): the run options of compaction and their defaults. * [Models and providers](https://weboost-at.github.io/Typeshift/guide/models.md#the-summary-model): which model summarizes, and its provider. * [Multiple agents](https://weboost-at.github.io/Typeshift/guide/multi-agent.md#handoff): handoffs, which need a conversation. * [Serving a chat endpoint](https://weboost-at.github.io/Typeshift/guide/chat-endpoint.md): a conversation per chat, behind ASP.NET Core endpoints. --- --- url: https://weboost-at.github.io/Typeshift/guide/chat-endpoint.md description: >- Run an agent from ASP.NET Core endpoints, stream its answer to a chat UI as server-sent events, and answer tool approvals by their ids. --- # Serving a chat endpoint A chat UI talks to your agent through endpoints of your web app. The UI posts the user's message to a chat, gets the answer back whole or as it streams, and posts the user's answers to the tool calls that wait for approval. This page builds those endpoints with ASP.NET Core minimal APIs, on the [runs](https://weboost-at.github.io/Typeshift/guide/running-agents.md), [conversations](https://weboost-at.github.io/Typeshift/guide/conversations.md) and [approvals](https://weboost-at.github.io/Typeshift/guide/tool-approval.md) the other pages describe. ## Post a message Each chat is a conversation, and the chat's id in the route is its conversation id. The endpoint runs the agent in that conversation, with the current customer's id as the [settings id](https://weboost-at.github.io/Typeshift/guide/running-agents.md#stored-settings), so the customer's stored settings apply: ```cs var chat = app.MapGroup("/chats/{chatId:int}") .AddEndpointFilter(); chat.MapPost("/messages", async ( int chatId, ChatInput input, AgentHost support, ICurrentCustomer customer, CancellationToken ct) => { var response = await support.RunAsync( input.Text, settingsId: customer.Id, conversationId: chatId, ct: ct); return ChatReply.From(response); }); ``` * The host and `ICurrentCustomer` come from the request's scope, as [Using an agent](https://weboost-at.github.io/Typeshift/guide/using-an-agent.md#inject-the-host-in-a-web-app) describes. `ICurrentCustomer` stands for a scoped service of your app, filled from the request's authentication, so put your authentication on the group too, for example with `RequireAuthorization()`. * The `CancellationToken` is the request's, so the run is cancelled when the user leaves the page. A run that should finish anyway takes no token. * `ChatReply` is the JSON the UI gets back: the answer's text and the approvals it waits for. The filter on the group checks that the chat belongs to the caller before any endpoint of the group runs, because Typeshift resumes whichever conversation it is given ([Tracking a conversation](https://weboost-at.github.io/Typeshift/guide/conversations.md#tracking-a-conversation)). Map every endpoint that takes a chat id on this group: ```cs internal sealed class ChatOwnerFilter(IChats chats, ICurrentCustomer customer) : IEndpointFilter { public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) { var http = context.HttpContext; var chatId = int.Parse((string)http.Request.RouteValues["chatId"]!); return await chats.BelongsToAsync(chatId, customer.Id, http.RequestAborted) ? await next(context) : TypedResults.NotFound(); } } ``` `IChats` stands for your app's record of its chats, in which each chat names its customer. Another customer's chat answers 404, as a chat that does not exist does, so the UI learns nothing about the chats of others. ## Return the answer The endpoints answer with records of your own, so the UI's contract stays yours when Microsoft.Extensions.AI's types change: ```cs internal sealed record ChatInput(string Text); internal sealed record ChatReply(string Text, IReadOnlyList Approvals) { public static ChatReply From(ChatResponse answer) => new(answer.Text, [.. answer.ApprovalRequests.Select(PendingApproval.From).OfType()]); } // What the UI shows to ask, and sends back with the user's answer internal sealed record PendingApproval( string RequestId, string CallId, string Tool, IDictionary? Arguments) { public static PendingApproval? From(ToolApprovalRequestContent request) => request.ToolCall is FunctionCallContent call ? new(request.RequestId, call.CallId, call.Name, call.Arguments) : null; } ``` `ApprovalRequests` lists the calls that wait for approval ([A run that asks](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#a-run-that-asks)). `PendingApproval` keeps the tool and its arguments, which the UI shows when it asks, and the ids the UI sends back with the user's answer. Show the arguments as the model sent them, because those are the arguments the call runs with. ## Stream the answer The streaming endpoint runs the agent the same way, and hands its updates to [`TypedResults.ServerSentEvents`](https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis/responses): ```cs chat.MapPost("/messages/stream", ( int chatId, ChatInput input, AgentHost support, ICurrentCustomer customer, CancellationToken ct) => { var updates = support.RunStreamingAsync( input.Text, settingsId: customer.Id, conversationId: chatId, ct: ct); return TypedResults.ServerSentEvents(Events(updates)); }); ``` Each update becomes a `text` event with the text it adds, and each approval request an `approval` event: ```cs private static async IAsyncEnumerable> Events(IAsyncEnumerable updates) { await foreach (var update in updates) { if (update.Text is { Length: > 0 } text) yield return new SseItem(text, "text"); foreach (var request in update.Contents.OfType()) { if (PendingApproval.From(request) is { } approval) yield return new SseItem(approval, "approval"); } } } ``` * Both events carry JSON, written with your app's JSON options: a `text` event holds its text as a JSON string, and an `approval` event holds the `PendingApproval` with camelCase names. Parse each event's data with `JSON.parse`. * A browser reads the events with `fetch` and a reader over the response's body, because [`EventSource`](https://developer.mozilla.org/docs/Web/API/EventSource) sends only GET requests. * A run that asks for approval stores the requests, sends its `approval` events last, and then ends. Post the answers once the stream has ended, when the reader reports that it is done. A reader that stops at an `approval` event leaves the run to end on its own, and the answers still work. ## Answer the approvals The UI posts the user's answer to each pending approval. The endpoint turns the answers into `ToolApprovalResponseContent`s and runs the conversation's next run with them: ```cs chat.MapPost("/approvals", async Task ( int chatId, ApprovalAnswer[] answers, AgentHost support, ICurrentCustomer customer, CancellationToken ct) => { // No answers would drop every pending request if (answers.Length == 0) return TypedResults.BadRequest(); var reply = new ChatMessage(ChatRole.User, [.. answers.Select(answer => answer.ToResponse())]); try { var resumed = await support.RunAsync( reply, settingsId: customer.Id, conversationId: chatId, ct: ct); return TypedResults.Ok(ChatReply.From(resumed)); } catch (ApprovalClosedException) { // Answered already, in another tab or by a retry, or passed by a newer message: the UI drops it return TypedResults.Conflict(); } }); ``` An answer to a request that has closed, for example one the user approved in another tab already, throws `ApprovalClosedException` before the run calls the model. The endpoint answers it with 409 Conflict, so the UI can drop the prompt ([When a request closes](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#when-a-request-closes)). `ToResponse` rebuilds each request from its ids and answers it, with a [standing approval](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#approving-for-the-conversation) of the tool when the user chose one: ```cs internal sealed record ApprovalAnswer( string RequestId, string CallId, string Tool, bool Approved, bool ForConversation = false, string? Reason = null) { // The call only fills in the request: Typeshift runs the call it stored under RequestId public ToolApprovalResponseContent ToResponse() { var request = new ToolApprovalRequestContent(RequestId, new FunctionCallContent(CallId, Tool)); return Approved && ForConversation ? request.ApproveForConversation() : request.CreateResponse(Approved, Reason); } } ``` * The `CallId` and `Tool` the UI sends back only fill in the request. Typeshift runs the call it stored under `RequestId` ([Approving or rejecting](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#approving-or-rejecting)), and a standing approval goes to that call's tool. Decide on the stored call with an [approval rule](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#approval-rules), never on the `Tool` the UI sent. * Post an answer to every pending approval at once, since the next run closes each request it has no answer for, and clear the UI's pending approvals when the user sends a new message instead. * The answering run passes the customer's settings id again ([Tracking a conversation](https://weboost-at.github.io/Typeshift/guide/conversations.md#tracking-a-conversation)). ::: warning A chat runs one answer at a time A conversation has one run at a time in a process ([One run at a time](https://weboost-at.github.io/Typeshift/guide/conversations.md#one-run-at-a-time)), so a second run of a chat throws an `InvalidOperationException` before it stores anything. That happens to a message posted while an answer still streams, a double click on send, or answers posted before the run that asked has ended. ASP.NET Core answers it with a 500 and logs an error. Disable the UI's send button while a run goes on, or pass a message to the running answer with `TrySteer` ([Steering](https://weboost-at.github.io/Typeshift/guide/conversations.md#steering)). With several instances of your app, nothing throws, so route each chat to one instance or keep its runs apart yourself. ::: ## Related * [Using an agent](https://weboost-at.github.io/Typeshift/guide/using-an-agent.md): the host from a request's scope, a console app or a background job. * [Streaming](https://weboost-at.github.io/Typeshift/guide/running-agents.md#streaming): what `RunStreamingAsync` returns. * [Approval and modes](https://weboost-at.github.io/Typeshift/guide/tool-approval.md): the approval round trip, rules and modes. * [Conversations](https://weboost-at.github.io/Typeshift/guide/conversations.md): what a chat stores, resuming, steering and live updates. --- --- url: https://weboost-at.github.io/Typeshift/guide/production.md description: >- The spans and metrics every run records for OpenTelemetry, and what your app needs to publish with Native AOT. --- # In production Every run records OpenTelemetry spans and metrics with no Typeshift setting to turn on: you only add the `Typeshift` source and meter to your OpenTelemetry setup. Your app can publish with Native AOT once the types its tools take and return have JSON metadata. ## Observability Typeshift traces and measures every run with `System.Diagnostics`, following OpenTelemetry's semantic conventions for generative AI. Each run, model call and tool call shows up as a span. ### Listening Listen to the `Typeshift` source and meter: ```cs services.AddOpenTelemetry() .WithTracing(tracing => tracing.AddSource("Typeshift")) .WithMetrics(metrics => metrics.AddMeter("Typeshift")); ``` ### Spans | Span | For | |---|---| | `invoke_agent {agent}` | A run, with the agent, the requested model, the conversation and the token usage. | | `chat {model}` | Each model call, including the calls that write a compaction summary. | | `execute_tool {tool}` | Each tool call. | The `invoke_agent` span carries these tags: | Tag | Holds | |---|---| | `gen_ai.operation.name` | `invoke_agent` | | `gen_ai.agent.name`, `gen_ai.agent.description` | The agent's name and description | | `gen_ai.request.model` | The model the run asks for | | `gen_ai.conversation.id` | The conversation's id, when the run has one | | `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens` | The run's token usage | | `error.type` | The exception's type, when the run fails; the span's status is then an error | ::: warning A failed save does not fail the run When the store fails to save an entry, the run goes on. Typeshift logs the failure as an error and records the exception on the run's span, so watch your logs or spans for it. ::: ### Metrics The metrics are Microsoft.Extensions.AI's `gen_ai.client.operation.duration` and `gen_ai.client.token.usage`, tagged with the model. ### Message content Spans leave out messages and tool arguments unless the environment variable `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` is `true`: ```bash export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true ``` ::: warning Captured content is your customers' data With the variable set, your traces hold every run's messages and every tool call's arguments, including what your customers wrote. Turn it on only where your tracing backend may keep that data. ::: ## Native AOT The generator writes the tool bindings and the settings serialization, so generated code never uses reflection over your types. What you add is JSON metadata for the types your tools take and return, and the analyzer reports each type that lacks it at compile time. ### The project To publish your app with Native AOT, set `PublishAot` in its project. Keep `GenerateDocumentationFile` on, because tools take their descriptions from XML docs ([TSH011](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh011)): ```csproj Exe net10.0 true true true ``` ### Tool parameter and result types Under Native AOT, the types your tools take and return need source-generated metadata: a `JsonSerializerContext` in the toolset's assembly that lists them. Outside Native AOT, serialization falls back to reflection, so an app that does not publish with Native AOT needs no context. Set `IsAotCompatible` in a toolset library that such apps use, so the analyzer checks it. ```cs [JsonSerializable(typeof(IReadOnlyList))] [JsonSerializable(typeof(CompanyCount))] internal partial class AppJsonContext : JsonSerializerContext; ``` You don't register the context yourself: `AddTypeshift()` does. Each assembly's generated module adds that assembly's contexts, `internal` ones included, so a toolset library ships its own contexts and your app registers nothing for them. Strings, `char`, `bool`, `short`, `int`, `long` and their unsigned forms, `float`, `double`, `decimal`, `DateTime`, `DateTimeOffset`, `TimeSpan`, `IEnumerable` and the JSON element, document and node types need no context. In a project that publishes with Native AOT or sets `IsAotCompatible`, the analyzer reports [TSH027](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh027) for every other tool type that no context lists. ### Everything else The generated `Settings` and overrides records serialize member by member, conversations go through Microsoft.Extensions.AI's own contracts. None of these needs a context from you. `RunAsync` does need one: list `T` in a context too. In the sample, `AppJsonContext` above lists `CompanyCount` because the app calls `RunAsync`. ## Related * [How a run works](https://weboost-at.github.io/Typeshift/guide/running-agents.md#how-a-run-works): the steps the spans of a run cover. * [Conversations](https://weboost-at.github.io/Typeshift/guide/conversations.md#live-updates): live updates of a run, for a UI. * [Toolsets](https://weboost-at.github.io/Typeshift/guide/toolsets.md#results): how a tool's result reaches the model as JSON. * [Running agents](https://weboost-at.github.io/Typeshift/guide/running-agents.md#structured-output): structured output with `RunAsync`. * [Registration](https://weboost-at.github.io/Typeshift/guide/registration.md#what-addtypeshift-registers): what the generated modules register. --- --- url: https://weboost-at.github.io/Typeshift/guide/tool-approval.md description: >- Pause a run until your app approves a tool call, let rules decide the calls you are sure about, and pick the default, plan or auto mode. --- # Approval and modes Some tools must not run unasked, such as closing a ticket. When the model calls one, the run ends and hands the call to your app, which approves or rejects it in the conversation's next run. Approval rules, standing approvals and the run's mode decide which calls need asking at all. ## The round trip ```mermaid sequenceDiagram participant App as Your code participant Host as AgentHost participant Model participant Tool as close_ticket App->>Host: RunAsync("Close ticket 1234.") Host->>Model: The conversation and the input Model-->>Host: Call close_ticket Note over Host: No rule or standing approval decides,
so the run stores the request and ends Host-->>App: The response, with ApprovalRequests App->>Host: RunAsync(the answers) Host->>Tool: The call the conversation stored Host->>Model: The tool's result Model-->>Host: The answer Host-->>App: The response ``` Because no [approval rule](#approval-rules) or [standing approval](#approving-for-the-conversation) decides the call to `close_ticket`, the run stores the request in the conversation and ends. Your app sends its answer in the conversation's next run, and the stored call runs once that answer approves it. Requests and answers are Microsoft.Extensions.AI's `ToolApprovalRequestContent` and `ToolApprovalResponseContent`. ## Requiring approval `RequiresApproval()` makes every call to a tool wait for approval: ```cs AddTool(CloseTicketAsync) .RequiresApproval(); ``` `UseMcpServer(...).RequiresApproval()` does the same for every tool of an [MCP server](https://weboost-at.github.io/Typeshift/guide/mcp.md). A tool that a [context provider](https://weboost-at.github.io/Typeshift/guide/middleware.md#context-providers) adds needs approval when you wrap it in `ApprovalRequiredAIFunction`. ## A run that asks When the model calls such a tool, the run ends without making the call. `ApprovalRequests`, which Typeshift adds to `ChatResponse`, lists each waiting call, with the tool's name and the arguments the model sent: ```cs var answer = await support.RunAsync("Close ticket 1234.", conversationId: conversationId); foreach (var request in answer.ApprovalRequests) if (request.ToolCall is FunctionCallContent call) Console.WriteLine($"Approve {call.Name}({string.Join(", ", call.Arguments!.Values)})?"); ``` If the model called other tools in the same response, those calls wait too, even when they need no approval, and run once your app has answered. A run that asks needs a [conversation](https://weboost-at.github.io/Typeshift/guide/conversations.md) to store the request in. A run without a conversation id throws, naming the tool, so run with one, approve the calls with a rule, or use [auto mode](#auto-mode). ## Approving or rejecting Answer each request in the conversation's next run with `CreateResponse(approved)`: ```cs var approvals = answer.ApprovalRequests.Select(request => request.CreateResponse(approved: true)); var approval = new ChatMessage(ChatRole.User, [.. approvals]); var resumed = await support.RunAsync(approval, conversationId: conversationId); ``` Approved calls run. A rejected call reaches the model as rejected, and the run goes on. To tell the model why, pass a reason with the rejection: `CreateResponse(approved: false, reason)`. A conversation does not remember a settings id, so the run that answers passes the same settings id as the run that asked, or it runs on the agent's code alone. `WithMaxToolIterations` limits how many rounds of tool calls one run makes. The calls you approve run in the next run, because the run that asked for approval has ended. The next run starts its own count, so those calls run even when the run that asked had used up its rounds. Calls that a rule, a standing approval or auto mode approves go on in the same run and count toward its limit. Such a run may make one round more than the limit. Typeshift matches each answer to the request the conversation stored and runs the call stored with it. An answer can carry a call of its own, but the call that runs is always the one your app saw, with the same arguments. When a run carries more than one answer to a request, the first one counts. ## When a request closes A request stays open until the conversation goes on without it. It closes when: * a run answers it, or an approval rule does; * the conversation's next run starts with other input, such as a new message, since that run drops every request it has no answer for, and the model sees the call as never made; * its own run takes [steering input](https://weboost-at.github.io/Typeshift/guide/conversations.md#steering) after it and answers the input. The run's `ApprovalRequests` then leave it out. A background agent's answer closes no request, since the run waits for it before it ends. An answer to a closed request throws `ApprovalClosedException` before the run stores anything or calls the model, so your app learns that its prompt is out of date. That happens when your user approved in a second tab after the first answer's run ended, when a retry posts the same answers again, or when a UI keeps an old prompt clickable after a new message. `Requests` says why each request is closed, as an `ApprovalClosedReason` (`Answered`, `PassedBy` or `Unknown`), so your app can tell the user that the approval has expired: ```cs try { await support.RunAsync(approval, conversationId: conversationId); } catch (ApprovalClosedException closed) { foreach (var (request, reason) in closed.Requests) Console.WriteLine($"The approval {request} has expired: {reason}."); } ``` One closed answer rejects the whole input: the answers to open requests in it don't run, and nothing of it is stored. Run again without the answers `Requests` names to keep the rest. ::: warning Answer every request in the same run The next run closes every request it has no answer for, so answer all of `ApprovalRequests` at once. A streaming run sends its requests the moment it asks. If steering input then reaches the run and the run answers it, those requests close although the stream has carried them, and an answer to them throws. ::: ## Approving for the conversation `ApproveForConversation()` approves the call and every later call to the same tool in the conversation. It is stored with the conversation, so it holds across runs and processes: ```cs var again = await support.RunAsync("Close ticket 1234 again.", conversationId: conversationId); var always = new ChatMessage(ChatRole.User, [again.ApprovalRequests[0].ApproveForConversation()]); await support.RunAsync(always, conversationId: conversationId); ``` ## Approval rules A rule decides a call before your app is asked. It approves what you trust, rejects what you never allow, and abstains on the rest. Register it with `WithApprovalRule()`, and Typeshift resolves it from the run's services: ```cs services.AddTypeshift() .WithApprovalRule(); ``` Every rule sees every call that requires approval, with the arguments the model sent, so it decides on exactly the call that runs. This one rejects the shell commands it never allows: ```cs internal sealed class ShellGuard : IToolApprovalRule { private static readonly string[] Never = ["sudo", "rm -rf", "curl", "wget", "ssh"]; // A provider sends each argument as a JsonElement, whose ToString() is the string itself public ValueTask DecideAsync(FunctionCallContent call, AgentRun run, CancellationToken ct) => ValueTask.FromResult(call is { Name: "run_command", Arguments: { } arguments } && arguments.TryGetValue("command", out var value) && value?.ToString() is { } command && Never.Any(command.Contains) ? ToolDecision.Reject("This command is never allowed here.") : ToolDecision.Abstain); } ``` | Decision | Means | |---|---| | `ToolDecision.Reject(reason)` | The call is rejected in every mode, and the model receives the reason. A rejection wins over every approval. | | `ToolDecision.Approve` | The call runs, unless another rule rejects it. | | `ToolDecision.Abstain` | No opinion. Another rule, a standing approval or the [mode](#the-three-modes) decides: auto mode runs the call, and the default and plan modes ask your app. | ::: danger Approve only what you can read in full A command that looks harmless may run code the model wrote. That is why the rule above only rejects and leaves the rest to your app. ::: ## The three modes Every run is in one of three modes. The mode decides which tools the model is offered and what happens to the tools that require approval: | Mode | Offers | Tools that require approval | |---|---|---| | `Default` | every tool | ask your app, unless a rule or a standing approval decides | | `Plan` | read-only tools and `exit_plan_mode` | offered only if also marked `ReadOnly()`, and then ask as in `Default` | | `Auto` | every tool | run without asking, unless a rule rejects the call | ## Setting the mode The mode is a [run option](https://weboost-at.github.io/Typeshift/guide/defining-agents.md#run-options). `WithMode(mode)` in the agent's `WithRunOptions()` sets the mode runs start in, `Default` unless set. Add `Configurable(r => r.Mode)` to the same chain, and the agent's generated `Settings` gets a `Mode`, so stored settings or a run can change it. `TriageAgent` makes `Mode` configurable, so this run can pass `Mode = AgentMode.Plan` and start in plan mode: ```cs var plan = new TriageAgent.Settings { Mode = AgentMode.Plan }; var planned = await triage.RunAsync("Sort out ticket 1234.", settingsId, conversationId, plan); ``` A conversation keeps its mode. The agent's code and its stored settings decide only the mode of a conversation's first run. After that, each run continues in the mode the conversation's last run ended in, unless the settings you pass to the run name a mode, which switches the conversation to it. ::: warning A conversation stays in the mode it ended in After your app approves a plan with `ApprovePlan(AgentMode.Auto)`, later runs of the conversation run in auto mode, whatever mode the agent's code starts runs in. To switch back, pass a mode in a run's settings. That needs `Configurable(r => r.Mode)`, so add it to every agent that can enter plan mode. ::: ## Plan mode Plan mode lets the model research and propose a plan before it changes anything, so your app approves the whole plan before any step runs. Approve the plan into auto mode, and its steps run without asking, unless a rule rejects them. The model sees only the tools marked `ReadOnly()`, a toolset's or an [MCP server's](https://weboost-at.github.io/Typeshift/guide/mcp.md), plus its [sub-agents](https://weboost-at.github.io/Typeshift/guide/multi-agent.md#sub-agents) and `exit_plan_mode`, and Typeshift adds a short instruction to research, then present the plan. A sub-agent called in plan mode sees only its own read-only tools and answers its caller, which presents the plan. `exit_plan_mode(plan)` ends the run with an approval request that carries the plan: ```cs var proposal = planned.ApprovalRequests.Single(); if (proposal.ToolCall is FunctionCallContent { Arguments: { } arguments }) Console.WriteLine(arguments["plan"]); ``` Approve it with `ApprovePlan(mode)` to continue in the mode your app picks, `Default` or `Auto`: ```cs var approval = new ChatMessage(ChatRole.User, [proposal.ApprovePlan(AgentMode.Auto)]); await triage.RunAsync(approval, settingsId, conversationId); ``` To reject the plan, answer with `CreateResponse(approved: false, reason)`. The model receives the reason as feedback and goes back to planning. ::: warning Plan mode hides every tool without the mark A tool without `ReadOnly()` is hidden in plan mode, whatever it does. Mark every tool that only reads, so the model can research with it. ::: ## Auto mode In auto mode, tools that require approval run without asking your app. Rules still see each such call first, and a rule that rejects it wins. ::: danger In auto mode, only your rules stop a call Guard auto mode with [approval rules](#approval-rules), for example a guard that never lets `sudo` through, or a classifier that asks a model. ::: ## Related * [Conversations](https://weboost-at.github.io/Typeshift/guide/conversations.md): where requests, standing approvals and the mode are stored. * [Agents](https://weboost-at.github.io/Typeshift/guide/defining-agents.md#run-options): the run options that set the mode runs start in. * [MCP servers](https://weboost-at.github.io/Typeshift/guide/mcp.md): marking a server's tools read-only, so plan mode offers them. * [How a run works](https://weboost-at.github.io/Typeshift/guide/running-agents.md#how-a-run-works): where approval sits among a run's steps. * [Serving a chat endpoint](https://weboost-at.github.io/Typeshift/guide/chat-endpoint.md#answer-the-approvals): answering approvals over HTTP. --- --- url: https://weboost-at.github.io/Typeshift/guide/middleware.md description: >- Wrap every run or tool call for logging, guardrails or audits, and add memory, documents or the customer's profile to an agent's runs. --- # Middleware and context Middleware wraps every run or tool call of every agent, so you add logging, guardrails or limits in one place. A context provider, such as a memory, adds instructions, messages or tools only to the runs of the agents that use it. ## Middleware ### Around runs ```cs internal sealed class BlockedTopics : IRunMiddleware { public Task InvokeAsync(AgentRun run, Func next, CancellationToken ct) { if (run.Messages[^1].Text.Contains("password", StringComparison.OrdinalIgnoreCase)) { run.Response = new ChatResponse(new ChatMessage(ChatRole.Assistant, "I can't help with passwords.")); return Task.CompletedTask; } return next(); } } ``` An `IRunMiddleware` receives the run as an `AgentRun`: | Member | Holds | |---|---| | `Agent` | The agent that runs, such as its `Name`. | | `Messages` | What the model receives: the resumed conversation, then the input. | | `Options` | The run's resolved `ChatOptions`, with every layer of settings applied. | | `Mode` | The mode the run is in. | | `Services` | The run's services. | | `Response` | The answer, once `next` returns. | Middleware may change the messages and the options before it calls `next`. Changes to the messages hold for this run only, because the conversation stores the messages you sent. Middleware runs for every agent, so check `run.Agent` when it should act on some of them only. ### Answering in place of the model Set `run.Response` and skip `next`, as `BlockedTopics` does above, and the middleware answers in place of the model. The conversation stores that answer. If your middleware returns without calling `next` or setting `run.Response`, the run throws an `InvalidOperationException`. ### Streaming runs The same `InvokeAsync` serves streaming runs. The updates stream on to the caller while `next` runs, and `next` returns when the stream ends. A response set in place of the model reaches a streaming caller all at once, when the middleware returns. ::: warning A steered run passes through middleware again [Steering](https://weboost-at.github.io/Typeshift/guide/conversations.md#steering) hands new input to a run while it works. Input that arrives after the run's answer starts another pass of the run, and that pass goes through the middleware again, so middleware can see one run more than once. ::: ### Around tool calls ```cs internal sealed class AuditToolCalls(ICurrentCustomer customer, ILogger logger) : IToolMiddleware { public async ValueTask InvokeAsync( FunctionInvocationContext call, Func> next, CancellationToken ct) { logger.LogInformation("Customer {Customer} calls {Tool}", customer.Id, call.Function.Name); return await next(); } } ``` An `IToolMiddleware` wraps every tool call that runs and receives Microsoft.Extensions.AI's `FunctionInvocationContext`: the function, its arguments and the messages so far. A call that a rule or your app rejects never reaches it. What it returns is the result the model receives, so returning without calling `next` answers in place of the tool. Setting `call.Terminate` ends the tool loop: the model is not called again with this round's results, and other calls of the same round may be skipped. ### Registering `WithRunMiddleware()` and `WithToolMiddleware()` register middleware as scoped, so it sees the run's services, such as the current customer. Middleware runs in the order you register it, so the first one you register sees the run or the call first and finishes last: ```cs services.AddTypeshift() .WithStore() .WithChatClientProvider() .WithRealtimeTracking() .WithRunMiddleware() .WithToolMiddleware(); services.AddKeyedSingleton("openai", fast); services.AddKeyedSingleton("anthropic", reasoning); ``` The other lines register the store, the chat clients and live updates, which [the chain](https://weboost-at.github.io/Typeshift/guide/registration.md#the-chain) explains. ## Context providers ### Writing a provider ```cs internal sealed class CustomerProfileContext(ICurrentCustomer customer) : IContextProvider { public ValueTask GetContextAsync(AgentRun run, CancellationToken ct) => ValueTask.FromResult(new AgentContext { Instructions = $"The customer is {customer.Name}, on the {customer.Plan} plan." }); } ``` `GetContextAsync` returns an `AgentContext`, and each of its parts is appended to what the run already has: | Part | Where it goes | |---|---| | `Instructions` | After the agent's instructions | | `Messages` | Ahead of the input, after the resumed conversation | | `Tools` | After the agent's tools, for this run only | Typeshift asks the provider after it resumes the conversation and before it calls the model. The conversation stores none of what the provider adds, so Typeshift asks the provider again on every run. ### Using a provider The agent names its providers in its constructor: ```cs UseContext(); ``` A provider needs no registration. Typeshift resolves it from the run's services, or creates it from them when none is registered, so its constructor can take scoped services. ### Context tools Context tools, the tools a provider returns in `AgentContext.Tools`, are offered like the agent's own. Wrap one in `ApprovalRequiredAIFunction`, and its calls wait for [approval](https://weboost-at.github.io/Typeshift/guide/tool-approval.md). ::: warning Plan mode hides context tools Context tools carry no `ReadOnly()` mark, so [plan mode](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#plan-mode) hides them. To offer a tool in plan mode, declare it in a toolset and mark it `ReadOnly()`. ::: ## Related * [How a run works](https://weboost-at.github.io/Typeshift/guide/running-agents.md#how-a-run-works): where middleware and context sit among a run's steps. * [Agents](https://weboost-at.github.io/Typeshift/guide/defining-agents.md): `UseContext()` among the other calls in an agent's constructor. * [Approval and modes](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#approval-rules): rules, which decide tool calls before your app is asked. * [Registration](https://weboost-at.github.io/Typeshift/guide/registration.md): the other calls in the `AddTypeshift()` chain. --- --- url: https://weboost-at.github.io/Typeshift/guide/registration.md description: >- What a project needs, the id type, and every service AddTypeshift() and its chain register, with lifetimes. --- # Registration `services.AddTypeshift()` registers every toolset and agent of your project and the projects it references. It returns a `TypeshiftBuilder`, whose `With...` calls register what your app supplies, such as the store and the chat client provider. ## Project setup ```bash dotnet add package Typeshift --prerelease ``` | Requirement | Why | |---|---| | .NET 10 and C# 14 | Generated members are extension blocks. | | The package in every project that declares toolsets or agents, or calls `AddTypeshift()` | The package carries the generator, and NuGet does not pass a generator on to the projects that reference yours ([TSH019](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh019), [TSH020](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh020)). | | `GenerateDocumentationFile` | Tools and the types they take get their descriptions from XML docs ([TSH011](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh011)). | | `Typeshift.Generated` in `InterceptorsNamespaces` | The generator intercepts `AddTypeshift()` from that namespace. The package adds it for you. | ## XML docs of parameter types The package brings [Weboost.SchemaAnnotations](https://github.com/weboost-at/SchemaAnnotations), which turns the XML docs of your types into descriptions of the properties a tool's parameters hold. Typeshift adds a hidden class to every project that sets `GenerateDocumentationFile`. Weboost.SchemaAnnotations collects the project's XML docs in that class, and the project's [registration module](#what-addtypeshift-registers) registers them when `AddTypeshift()` runs. A project of contracts with no toolsets or agents gets a module too, so the tools of the projects that reference it describe its types; it needs the package and `GenerateDocumentationFile` like any other. To leave these descriptions out, set: ```xml false ``` ::: warning Delete your own `[SchemaAnnotations]` class If your project already marks a class with `[SchemaAnnotations]`, adding the Typeshift package gives it a second one. Weboost.SchemaAnnotations then generates no `AIJsonSchemaCreateOptions.Annotated`, and code that uses it stops compiling. Delete your own class, and the one the package adds brings `Annotated` back. ::: ## The id type Settings ids and conversation ids share one type across the whole build. It is `Guid` unless `TypeshiftIdType` names another, set once in `Directory.Build.props`: ```csproj int ``` `TypeshiftIdType` takes a C# keyword (`int`, `long`, `string`), the name of a type in the `System` namespace, such as `Guid`, or any other type's full name with its namespace ([TSH029](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh029)). A public agent needs a public id type, since its host members take ids of it ([TSH006](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh006)). Generated members take the id type, as in `RunAsync(input, settingsId: 42)`. The generator also writes aliases that close `ITypeshiftStore`, `ISettingsStore`, `IConversationStore`, `InMemoryTypeshiftStore`, `ConversationTracker` and `LiveRun` over it, so you use these names without a type argument. The library never creates ids; your code chooses them. If a referenced project was built with another id type, the analyzer reports [TSH021](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh021), and for a store of another id type it reports [TSH028](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh028). Providers have an id type of their own, `TypeshiftProviderIdType`, which is `string` unless set. See [the provider id type](https://weboost-at.github.io/Typeshift/guide/models.md#the-provider-id-type). ## What `AddTypeshift()` registers A typical registration, with the chain: ```cs services.AddTypeshift() .WithStore() .WithChatClientProvider() .WithRealtimeTracking() .WithRunMiddleware() .WithToolMiddleware(); services.AddKeyedSingleton("openai", fast); services.AddKeyedSingleton("anthropic", reasoning); ``` `fast` and `reasoning` are `IChatClient`s your app creates. The last two lines register them under the keys `openai` and `anthropic`, and `KeyedChatClientProvider` returns the client registered under a run's provider ([the chat client provider](https://weboost-at.github.io/Typeshift/guide/models.md#the-chat-client-provider)). `AddTypeshift()` itself registers these: | Service | Lifetime | |---|---| | Each toolset | Singleton | | Each agent | Singleton | | `AgentHost` per agent | Transient, so it runs with the services of the scope it was resolved from ([Using an agent](https://weboost-at.github.io/Typeshift/guide/using-an-agent.md)) | | The JSON contexts of each assembly, which tools ask before the defaults | Singleton | | `ConversationTracker` and the core services runs need | Singleton | The generator writes a registration module for each project that declares toolsets or agents. `AddTypeshift()` runs the modules of the project that calls it and of every project it references that declares toolsets or agents. Each module registers once per service collection, so a second call to `AddTypeshift()` only returns another chain. Because the generator intercepts the call, it works only in a project that references the package, and only when you call it on the service collection, such as `services.AddTypeshift()` or `builder.Services.AddTypeshift()`. A call the generator did not intercept throws an `InvalidOperationException` that says what to check. ::: warning A library's call registers the library's agents, not your app's A library that calls `AddTypeshift()` in its own `AddX()` registers its own toolsets and agents and those of its references. Your app's own toolsets and agents need a call to `AddTypeshift()` in your app. ::: ## The chain | Call | Registers | Lifetime | Needed by | |---|---|---|---| | `WithStore()` | `TStore`, and each of `ISettingsStore`, `IConversationStore` and `ITypeshiftStore` it implements, resolving to it. Call it once for each class. | Singleton, so it takes no scoped services ([Implementing a store](https://weboost-at.github.io/Typeshift/guide/implementing-a-store.md#register-your-store)) | Settings ids, conversation ids, approval and handoffs. Without a store, every call that loads or saves throws, naming this method. | | `WithChatClientProvider()` | `TProvider`, and `IChatClientProvider` resolving to it | Scoped | Every run. | | `WithRealtimeTracking(RealtimeTrackingOptions? options = null)` | The options. By default, `PublishInterval` is 100 ms and `MaxPendingUpdates` 256. | Singleton | `ConversationTracker.WatchAsync`. | | `WithApprovalRule()` | One more `IToolApprovalRule` | Scoped | Deciding calls before your app is asked. | | `WithRunMiddleware()` | One more `IRunMiddleware`. The first registered is the outermost. | Scoped | Wrapping every run. | | `WithToolMiddleware()` | One more `IToolMiddleware`. The first registered is the outermost. | Scoped | Wrapping every tool call. | | `WithMcpToolProvider()` | `IMcpToolProvider` | Scoped | Agents with `UseMcpServer`. | Scoped services resolve from the run's scope, so they can differ per request and use the current customer's credentials, folder or rules, for example. ## Related * [Getting started](https://weboost-at.github.io/Typeshift/guide/getting-started.md): the setup, step by step. * [Using an agent](https://weboost-at.github.io/Typeshift/guide/using-an-agent.md): injecting a host or resolving it from a scope. * [Running agents](https://weboost-at.github.io/Typeshift/guide/running-agents.md): running an agent. * [Settings and stores](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md): the store `WithStore()` registers. * [Generated members](https://weboost-at.github.io/Typeshift/guide/generated-members.md#per-project): the module, the aliases and the interceptor the generator writes. * [Diagnostics](https://weboost-at.github.io/Typeshift/guide/diagnostics.md): what the analyzer reports when the setup is incomplete. --- --- url: https://weboost-at.github.io/Typeshift/guide/builders.md description: >- Every call a toolset or agent constructor can make, and the chat and run options with their defaults. --- # Builders Toolsets and agents declare themselves in their constructors, and this page lists every call they can make. To learn the calls in context, read [Toolsets](https://weboost-at.github.io/Typeshift/guide/toolsets.md), [Tool settings](https://weboost-at.github.io/Typeshift/guide/tool-settings.md) and [Agents](https://weboost-at.github.io/Typeshift/guide/defining-agents.md). ## Rules for every chain The generator reads each chain from your source at compile time, and at runtime the calls only record values. Because the generator reads source, put every declaration directly in the constructor ([TSH014](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh014)) and write it as one chain in one statement ([TSH015](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh015)). Names, descriptions, the storage kind and the schema version must be constants ([TSH002](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh002)). Each toolset derives from `Toolset` and each agent from `Agent` directly ([TSH017](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh017)). ## Toolset | Call | Does | |---|---| | `WithDisplayName(string displayName)` | Names the toolset for people, for example in a tool picker. `DisplayName` reads it back. | | `AddTool(Delegate method)` | Declares a tool backed by a static method group, such as `AddTool(SearchTickets)`, and returns its [`ToolBuilder`](#toolbuilder). For a lambda, an instance method or a local function, the analyzer reports [TSH003](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh003). | [Toolsets](https://weboost-at.github.io/Typeshift/guide/toolsets.md) explains a tool's [name and description](https://weboost-at.github.io/Typeshift/guide/toolsets.md#names-and-descriptions), its [parameters](https://weboost-at.github.io/Typeshift/guide/toolsets.md#parameters), including those the call fills instead of the model, and the [results](https://weboost-at.github.io/Typeshift/guide/toolsets.md#results) it may return. ## ToolBuilder | Call | Does | |---|---| | `WithName(string name)` | Replaces the name derived from the method. It must be lower_snake_case ([TSH008](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh008)). | | `WithDescription(string description)` | Replaces the ``. For a tool with neither, the analyzer reports [TSH009](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh009). | | `RequiresApproval()` | Makes the tool's calls need approval. Unless a rule, a standing approval or auto mode decides a call, the run ends, and the call runs once your app approves it in the conversation's next run. See [Approval and modes](https://weboost-at.github.io/Typeshift/guide/tool-approval.md). | | `ReadOnly()` | Marks a tool that only reads and changes nothing. [Plan mode](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#plan-mode) offers only these. | | `WithSettings()` | Gives the tool a settings record that stays in code, except the members `Configurable` names. Returns `ToolBuilder`. | | `WithConfigurableSettings()` | Gives the tool a settings record whose settable members may all change, except those `NotConfigurable` names. Returns `ConfigurableSettingsBuilder`. | Put the settings call last in the tool's chain, because it returns `ToolBuilder` or `ConfigurableSettingsBuilder`, which offer only the calls below: `ReadOnly()` after it is a compiler error (CS1061). `TSettings` is a record whose parameterless constructor sets the code defaults ([TSH005](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh005)), and the tool's method takes a parameter of that type ([TSH010](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh010)). ### Configurable members | Call | On | Does | |---|---|---| | `Configurable(s => s.Member)` | `ToolBuilder` | Lets the member change at runtime, through stored settings or a run. | | `NotConfigurable(s => s.Member)` | `ConfigurableSettingsBuilder` | Keeps the member in code. | The generator reads these lambdas and never runs them, so each one must name a member directly, as in `s => s.Member` ([TSH015](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh015)). A configurable member needs a public `set` or `init` accessor ([TSH013](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh013)) and a type that settings can store ([TSH012](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh012)); [Tool settings](https://weboost-at.github.io/Typeshift/guide/tool-settings.md#configurable-members) lists those types. `WithConfigurableSettings` leaves members marked `[Obsolete]` out without a `NotConfigurable` call. ## Agent | Call | Does | |---|---| | `WithName(string name)` | Names the agent, which is otherwise its class name in snake_case. Messages the agent writes carry the name as their author. | | `WithDescription(string description)` | Says what the agent does. A sub-agent or a handoff target needs one ([TSH023](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh023)). | | `WithStorageKind(string kind)` | Sets the kind a store keys the agent's `Settings` by, in place of its class name, so renaming the class keeps its stored settings. | | `WithSchemaVersion(int version)` | Sets the schema version of the stored `Settings`, 1 unless raised. Raise it after you rename or remove a configurable member, so you can [migrate older documents](https://weboost-at.github.io/Typeshift/guide/implementing-a-store.md#migrate-older-settings). | | `WithChatOptions()` | Returns the [`ChatOptionsBuilder`](#chatoptionsbuilder). | | `WithRunOptions()` | Returns the [`RunOptionsBuilder`](#runoptionsbuilder). | | `UseToolset()` | Offers every tool of the toolset. `.Include(t => t.X)` offers only the tools picked, in that order, and `.Exclude(t => t.X)` all but those. One use includes or excludes, never both. Use each toolset once and pick each tool once ([TSH025](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh025)). | | `AddTool(Delegate method)` | Declares a tool of the agent's own, backed by a static method of the agent, and returns its [`ToolBuilder`](#toolbuilder). The agent offers it after the tools of its toolsets. See [Agents](https://weboost-at.github.io/Typeshift/guide/defining-agents.md#add-a-tool-of-the-agent-s-own). | | `UseAgent()` | Offers another agent as a tool named like it, with one `input` parameter. `.InBackground()` returns at once and delivers the answer later. See [Sub-agents](https://weboost-at.github.io/Typeshift/guide/multi-agent.md#sub-agents). | | `UseHandoff()` | Offers a tool `handoff_to_{name}` that hands the conversation to the other agent. See [Handoff](https://weboost-at.github.io/Typeshift/guide/multi-agent.md#handoff). | | `UseMcpServer(string server)` | Offers the tools of an MCP server that your app's `IMcpToolProvider` connects. `.RequiresApproval()` makes every call to its tools wait for approval, and `.ReadOnly()` lets plan mode offer them. | | `UseContext()` | Adds what an `IContextProvider` returns to every run. The run's services resolve the provider, or create it when none is registered. | The agent's instructions come from `GetInstructions()`, a virtual method you override. Typeshift calls it each time it resolves a run's options, so it runs for every run. ## ChatOptionsBuilder `WithChatOptions()` sets the chat options every run starts from. `Configurable(o => o.X)` lets stored settings or a run change an option, even one the code never sets, and makes it a member of the agent's [`Settings`](https://weboost-at.github.io/Typeshift/guide/generated-members.md#settings). An option with "No" in the last column cannot be configurable ([TSH012](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh012)): it belongs to the run, as the tools do, or settings cannot store its type. | Call | Sets | May be configurable | |---|---|---| | `WithModel(TProviderId provider, string model)` | `ModelId`, and the [provider](https://weboost-at.github.io/Typeshift/guide/models.md) that serves it | Yes, as `Provider` and `Model` | | `GetInstructions()`, overridden in the agent | `Instructions` | Yes | | `WithTemperature(float temperature)` | `Temperature` | Yes | | `WithMaxOutputTokens(int tokens)` | `MaxOutputTokens` | Yes | | `WithTopP(float topP)` | `TopP` | Yes | | `WithTopK(int topK)` | `TopK` | Yes | | `WithFrequencyPenalty(float penalty)` | `FrequencyPenalty` | Yes | | `WithPresencePenalty(float penalty)` | `PresencePenalty` | Yes | | `WithSeed(long seed)` | `Seed` | Yes | | `WithStopSequences(params string[] sequences)` | `StopSequences` | Yes | | `WithAllowMultipleToolCalls(bool allow)` | `AllowMultipleToolCalls` | Yes | | `WithReasoning(ReasoningOptions reasoning)` | `Reasoning` | Yes | | `WithResponseFormat(ChatResponseFormat format)` | `ResponseFormat` | No | | `WithToolMode(ChatToolMode mode)` | `ToolMode` | No | | `WithTools(params AITool[] tools)` | Adds tools of no toolset to `Tools`, such as a `HostedWebSearchTool` or a function from `AIFunctionFactory` | No | | `WithAdditionalProperty(string key, TValue value)` | A provider-specific option in `AdditionalProperties` | No | ## RunOptionsBuilder `WithRunOptions()` sets how a run goes: its mode, the limits of its tool loop and when its history is compacted. Every run option may be configurable: `Configurable(r => r.X)` puts it into the agent's `Settings`, like a chat option. | Call | Default | Does | |---|---|---| | `WithMode(AgentMode mode)` | `Default` | Sets the [mode](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#the-three-modes) runs start in: `Default`, `Plan` or `Auto`. | | `WithMaxToolIterations(int iterations)` | 40 | Sets the most rounds of tool calls one run makes. Calls your app [approves](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#approving-or-rejecting) run in the next run, which counts its own. | | `WithMaxConsecutiveToolErrors(int errors)` | 3 | Sets how many rounds of tool calls in a row may fail before the failing tool's exception ends the run and reaches your code. | | `WithConcurrentToolCalls(bool concurrent)` | off | Runs the tool calls in one model response at the same time. | | `WithMaxInputTokens(int tokens)` | off | Turns [compaction](https://weboost-at.github.io/Typeshift/guide/conversations.md#compaction) on at this many input tokens. | | `WithTargetInputTokens(int tokens)` | half the maximum | Sets how far compaction shrinks the history. | | `WithSummaryModel(string model)` | the run's model | Sets the model that summarizes the oldest turns when clearing old tool results does not shrink the history enough, served by the run's provider. Made configurable, it also adds `SummaryProvider` to `Settings` ([the summary model](https://weboost-at.github.io/Typeshift/guide/models.md#the-summary-model)). | ## Related * [Toolsets](https://weboost-at.github.io/Typeshift/guide/toolsets.md): declaring tools, and their names, parameters and results. * [Tool settings](https://weboost-at.github.io/Typeshift/guide/tool-settings.md): settings records and their configurable members. * [Agents](https://weboost-at.github.io/Typeshift/guide/defining-agents.md): declaring an agent and choosing its tools. * [Generated members](https://weboost-at.github.io/Typeshift/guide/generated-members.md): what the generator writes from these chains. * [Diagnostics](https://weboost-at.github.io/Typeshift/guide/diagnostics.md): what the analyzer reports when a chain breaks a rule. --- --- url: https://weboost-at.github.io/Typeshift/guide/generated-members.md description: >- Every member the generator writes for your toolsets and agents, what triggers it, and every member of AgentHost with its signature. --- # Generated members and the host The generator adds members to your toolsets and agents, and to `AgentHost`, the class that runs an agent. Each section names the declaration that adds a member. ## Names and hidden members * Member names come from the tool's name in PascalCase, so `search_tickets` gives `SearchTickets`. * Members marked *hidden* are `[EditorBrowsable(Never)]` plumbing that only generated code calls. Code completion leaves them out, but their names are taken all the same, so the analyzer reports [TSH018](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh018) for a member of yours with one of them. ## Per toolset ### `{Tool}Overrides` **Triggered by** a tool with at least one configurable settings member. The record is a `public sealed record` nested in the toolset, or in the agent for a tool of the agent's own. `TicketToolset.SearchTicketsOverrides`, with `int? Limit` and `bool? IncludeClosed`, comes from this declaration: ```cs AddTool(SearchTickets) .ReadOnly() .WithSettings() .Configurable(s => s.IncludeClosed) .Configurable(s => s.Limit); ``` `KnowledgeToolset.SearchArticlesOverrides`, with `string? Language`, `IReadOnlyList? Sources` and `ArticleRanking? Ranking`, comes from this one, where `Limit` stays in code: ```cs AddTool(SearchArticlesAsync) .WithConfigurableSettings() .NotConfigurable(s => s.Limit); ``` | Member | Is | |---|---| | One `init` property per configurable member | The member's type, nullable. Null keeps the layer below. | | `ApplyTo(TSettings)` (hidden) | Returns the settings of the layer below, with every value this layer sets applied over them. | | `WriteJson`, `TryReadJson` (hidden) | Write and read the record's JSON, in camelCase with nulls left out. A `JsonConverter` on the record makes `System.Text.Json` read and write the same shape. | An agent's `Settings` holds the overrides of every tool it offers. So a toolset with configurable settings must be as accessible as a public agent that offers it ([TSH006](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh006)). ### `Create{Tool}` and `{Tool}Function` **Triggered by** every `AddTool(...)`. This one gives `CreateCloseTicket` and `CloseTicketFunction`: ```cs AddTool(CloseTicketAsync) .RequiresApproval(); ``` | Member | Is | |---|---| | `Create{Tool}(settings, json)` (hidden) | A static factory that returns the tool's `AIFunction`. `json`, a `ToolJson`, sets how the tool binds its arguments and serializes its result. It takes `settings`, the tool's settings after layering, only when the tool has settings. | | `{Tool}Function` | A private nested class deriving from `GeneratedToolFunction`. It holds the name, the description, the schema of the parameters the model fills, and the binding that calls the method. | ### `{Toolset}ToolList` **Triggered by** every toolset. A static class, as accessible as the toolset, holds an extension block on `ToolList`. The block has one property per tool, named after the tool and of type `ToolName`, and `Include` and `Exclude` pick from these properties: ```cs UseToolset() .Exclude(t => t.CloseTicket); ``` `TicketToolsetToolList` gives `ToolList` the properties `SearchTickets`, `GetTicket` and `CloseTicket`. A tool named like a member of `object` gets a `Tool` suffix, so `to_string` becomes `ToStringTool`. ### Stamps **Triggered by** every tool. A hidden `[GeneratedTool]` attribute on the toolset names the tool, its list property, its factory, its settings and overrides types, and whether it requires approval. Agents in other assemblies read it to offer the tool. ## Per agent ### `Settings` **Triggered by** every agent. The record is a `public sealed record` nested in the agent, and holds exactly what may change at runtime. This agent gets `SupportAgent.Settings` with `Provider`, `Model`, `Instructions`, `Temperature`, `SearchTickets` and `SearchArticles`: ```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() .Include(t => t.SearchTickets) .Include(t => t.CloseTicket); UseToolset() .Exclude(t => t.DeleteArticle); ConfigureSearchTickets() .WithLimit(5); UseMcpServer("status_page") .ReadOnly(); UseContext(); } protected override string GetInstructions() => """ Answer from the customer's tickets and cite help center articles. """; } ``` | Declaration | Member | |---|---| | `WithChatOptions().Configurable(o => o.ModelId)` | Two members: `Provider`, of the [provider id type](https://weboost-at.github.io/Typeshift/guide/models.md#the-provider-id-type), and `string? Model`. They override the provider and model that `WithModel` sets in code. | | `WithRunOptions().Configurable(r => r.SummaryModel)` | `SummaryProvider`, of the provider id type, and `string? SummaryModel`. If either is still null after every layer, the summary uses the run's own provider or model. | | `WithChatOptions().Configurable(o => o.X)` | `X`, nullable: `float? Temperature`, `string? Instructions`. | | `WithRunOptions().Configurable(r => r.X)` | `X`, nullable: `AgentMode? Mode`, `int? MaxToolIterations`. | | `UseToolset()` offering a tool with `{Tool}Overrides` | `{Tool}`, its overrides, never null. A name taken by an option or by one of the record's own members, such as `Kind`, gets a `Tool` suffix. | The option members and the members of each tool's overrides are nullable, and null keeps the layer below ([Settings layering](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#settings-layering)). [Builders](https://weboost-at.github.io/Typeshift/guide/builders.md#chatoptionsbuilder) lists the options that may be configurable. The record is also the document a store keeps under a settings id: | Member | Is | |---|---| | `static string Kind` | The name a store keys it by, which the store's methods receive as `kind`: `WithStorageKind("...")`, or else the class name, so moving the agent to another namespace keeps its data. For two agents of the same kind, the analyzer reports [TSH022](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh022). | | `static int SchemaVersion` | 1, or what `WithSchemaVersion(n)` sets, which `Serialize()` writes into the JSON ([The stored JSON](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#the-stored-json)). | | `Serialize()` | Writes the record as JSON. | | `static Deserialize(json)` | Reads the JSON. Throws a `JsonException` for a `"$schemaVersion"` newer than the code's. | | `WriteJson`, `TryReadJson` (hidden) | What `Serialize`, `Deserialize` and the record's `JsonConverter` use. | [The stored JSON](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#the-stored-json) covers the JSON's shape, how it reads back, and what to do when a configurable member is renamed. ### `Configure{Tool}()` **Triggered by** an offered tool with settings, from `WithSettings()` or `WithConfigurableSettings()`, whether it has configurable members or not. `ConfigureSearchTickets()` in the `SupportAgent` above is a private method. It returns a builder with one `With{Member}(value)` per settable member of the settings record, configurable or not. `SearchTicketsSettings` has `Limit`, `IncludeClosed` and a `Timeout` that is not configurable, so the builder has `WithLimit(int)`, `WithIncludeClosed(bool)` and `WithTimeout(TimeSpan)`. The values apply over the record's code defaults, for every run of the agent. A tool the agent does not offer has no `Configure{Tool}()`, so configuring it does not compile. A `Configure{Tool}()` the agent declares itself replaces the generated one. ### Tools of the agent's own **Triggered by** an `AddTool(...)` in the agent's constructor. The agent gets the tool's `Create{Tool}` and `{Tool}Function`, and its `{Tool}Overrides` when it has configurable members, as a toolset does. It gets no tool list and no toolset stamp. ### `{Agent}HostExtensions` **Triggered by** every agent the generated code can reach: one that is not nested as `private` or `protected` and has a public or internal parameterless constructor. For any other agent, the analyzer reports [TSH016](https://weboost-at.github.io/Typeshift/guide/diagnostics.md#tsh016). A static class, as accessible as the agent, holds an extension block on `AgentHost` with the agent's [typed members](#typed-members). ### Hidden members Every agent also gets: * an override of `Declaration`, which holds what the generator read from its constructor; * a private nested class `TypeshiftGenerated`; * a private field `_typeshift`, when it has a `Configure{Tool}()`. ## Per project | Member | Triggered by | Is | |---|---|---| | `{Assembly}Module` (hidden) | A toolset or an agent in the assembly | Registers the assembly's toolsets, agents, hosts, JSON contexts and XML docs; see [Registration](https://weboost-at.github.io/Typeshift/guide/registration.md#what-addtypeshift-registers). | | `[GeneratedModule]` (hidden) | The same | Stamps the module, its version and the id type for the projects that reference the assembly. | | `TypeshiftSchemaAnnotations_{Assembly}` (hidden) | `GenerateDocumentationFile`, unless `TypeshiftSchemaAnnotations` is `false` | Added by the package's build, not the generator. It carries the `[SchemaAnnotations]` attribute that makes Weboost.SchemaAnnotations collect the XML docs of your types; see [Registration](https://weboost-at.github.io/Typeshift/guide/registration.md#xml-docs-of-parameter-types). | | `ITypeshiftStore`, `ISettingsStore`, `IConversationStore`, `InMemoryTypeshiftStore`, `ConversationTracker`, `LiveRun`, `IChatClientProvider` | A reference to the package | `global using` aliases that close the library's types over the [id type](https://weboost-at.github.io/Typeshift/guide/registration.md#the-id-type), and `IChatClientProvider` over the [provider id type](https://weboost-at.github.io/Typeshift/guide/models.md#the-provider-id-type). | | The `AddTypeshift()` interceptor | A call of `services.AddTypeshift()` | See [Registration](https://weboost-at.github.io/Typeshift/guide/registration.md). | ## The agent host `AgentHost` runs one agent. Its own members are `Agent` and `CreateChatClientAsync`, and the generator adds the typed runs and settings of each agent. ### The host's scope `AddTypeshift()` registers a host per agent as transient, so a host runs with the services of the scope it was resolved from. Tools, the chat client provider, middleware, context providers, approval rules and the MCP tool provider all resolve from that scope. Every member that loads or saves settings needs an `ISettingsStore`, and every member that loads or appends to a conversation needs an `IConversationStore`. [Registering a store](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#registering-a-store) says what happens without one. ### `Agent` `TAgent Agent { get; }` is the agent as its code declares it, a singleton. `Agent.Name` and `Agent.Description` read what `WithName` and `WithDescription` declared. ### `CreateChatClientAsync` `Task CreateChatClientAsync(ChatOptions options, CancellationToken ct = default)` `CreateChatClientAsync` creates the client of the [raw path](https://weboost-at.github.io/Typeshift/guide/running-agents.md#the-raw-path) for options that `ResolveOptionsAsync` returned. It asks the `IChatClientProvider` for the options' provider and model and wraps that client in the run's pipeline, with the agent's run options and the tool loop. Calling the client with those options runs and stores like `RunAsync`. `ChatOptions.ConversationId` names the conversation, as a string of the id type. Disposing the client does not dispose the one your provider returned. It throws an `InvalidOperationException` when the options name no provider or no model, or when no `IChatClientProvider` is registered. ### Typed members In these signatures, `TId` is the build's [id type](https://weboost-at.github.io/Typeshift/guide/registration.md#the-id-type) and `Settings` the agent's [generated `Settings` record](#settings). Every member takes a `CancellationToken ct = default` last. | Member | Returns | |---|---| | `RunAsync(string input, TId? settingsId = null, TId? conversationId = null, Settings? settings = null)` | `Task` | | `RunAsync(ChatMessage input, TId? settingsId = null, TId? conversationId = null, Settings? settings = null)` | `Task` | | `RunAsync(string input, TId? settingsId = null, TId? conversationId = null, Settings? settings = null)` | `Task>` | | `RunStreamingAsync(string input, TId? settingsId = null, TId? conversationId = null, Settings? settings = null)` | `IAsyncEnumerable` | | `RunStreamingAsync(ChatMessage input, TId? settingsId = null, TId? conversationId = null, Settings? settings = null)` | `IAsyncEnumerable` | | `ResumeAsync(TId conversationId, TId? settingsId = null, Settings? settings = null)` | `Task` | | `ResumeStreamingAsync(TId conversationId, TId? settingsId = null, Settings? settings = null)` | `IAsyncEnumerable` | | `ResolveOptionsAsync(TId? settingsId = null, Settings? settings = null)` | `Task` | | `LoadSettingsAsync(TId settingsId)` | `Task>` | | `SaveSettingsAsync(TId settingsId, Settings settings, long loadedVersion)` | `Task` | | `ConfigureAsync(TId settingsId, Func configure)` | `Task` | ### `RunAsync` `RunAsync` runs the agent on one message, a `string` or a `ChatMessage`, which can carry images or approval answers. It layers the agent's code, the settings stored under `settingsId`, the mode the conversation's last run ended in, then `settings` ([Settings layering](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#settings-layering)). With a `conversationId`, it stores the run as it goes ([What a run stores](#what-a-run-stores)). Without one, it stores nothing and offers no handoffs. If the model then calls a tool that requires approval and no rule or mode approves the call, it throws an `InvalidOperationException`. To avoid that, run with a conversation id, approve the call with a rule, or run in `Auto` mode. `RunAsync` returns the model's messages with the tool calls and results between them, and each message carries the answering agent's name as `AuthorName`. `RunAsync` asks the model for JSON of `TResult`'s schema and returns it typed in `Result`. Under [Native AOT](https://weboost-at.github.io/Typeshift/guide/production.md#native-aot), `TResult` needs JSON metadata. ### `RunStreamingAsync` `RunStreamingAsync` layers settings and stores the run like `RunAsync`, but streams the updates, with the tool calls running between them. With `WithRealtimeTracking()`, the updates are also published to `ConversationTracker.WatchAsync`. ### `ResumeAsync` and `ResumeStreamingAsync` Both continue the conversation's interrupted run from its last stored step, without new input. They layer like `RunAsync` and store the continuation in the same conversation. A tool call that was still running when the run stopped is left out, so the model decides again and may call the tool a second time ([Resuming](https://weboost-at.github.io/Typeshift/guide/conversations.md#resuming)). They throw an `InvalidOperationException` when the conversation has no interrupted run. ### `ResolveOptionsAsync` `ResolveOptionsAsync` layers the agent's code, the settings stored under `settingsId`, then `settings`, and returns the run's `ChatOptions` without storing anything. The options hold the model, the instructions, the configurable options and the tools. The tools come in this order: the toolsets' tools with their layered settings, the sub-agents, the tools of `WithTools`, then the MCP servers' tools. ### `LoadSettingsAsync` `LoadSettingsAsync` loads the `Settings` stored under an id, with their version, or empty settings at version 0 when none are stored. It layers nothing, so the record holds only what was stored. ### `SaveSettingsAsync` `SaveSettingsAsync` saves `Settings` under an id if the stored version is still `loadedVersion`, the version `LoadSettingsAsync` returned, or 0 when it loaded none. It returns the new version, which your app passes to its next save. It throws a `StoreConflictException` when another save came first. ### `ConfigureAsync` `ConfigureAsync` loads the settings, applies `configure`, and saves them with the loaded version. On a conflict it loads and applies again until its save wins, then returns what it saved. After ten conflicts in a row it throws the last `StoreConflictException`. ### What a run stores A run with a conversation id stores its input, each model response, each tool result and an entry that ends the run. [Conversations](https://weboost-at.github.io/Typeshift/guide/conversations.md#what-is-stored) lists what is stored when a run fails, is cancelled or is cut off, and [compaction](https://weboost-at.github.io/Typeshift/guide/conversations.md#compaction) what a compacted run stores. ## Related * [Toolsets](https://weboost-at.github.io/Typeshift/guide/toolsets.md): the declarations behind the per-toolset members. * [Agents](https://weboost-at.github.io/Typeshift/guide/defining-agents.md): the declarations behind the per-agent members. * [Using an agent](https://weboost-at.github.io/Typeshift/guide/using-an-agent.md): getting a host from a request's or a job's scope. * [Running agents](https://weboost-at.github.io/Typeshift/guide/running-agents.md): running an agent with settings, typed output or streaming. * [Conversations](https://weboost-at.github.io/Typeshift/guide/conversations.md): storing, resuming and steering a conversation. * [Settings and stores](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#agent-settings): loading, saving and configuring an agent's settings. * [Builders](https://weboost-at.github.io/Typeshift/guide/builders.md): every call the generator reads. --- --- url: https://weboost-at.github.io/Typeshift/guide/diagnostics.md description: >- Every TSHnnn error and warning the Typeshift analyzer reports, what causes it and how to fix it. --- # Diagnostics The analyzer reports these errors and warnings while you type. Each one points at a mistake in your code or your build, so you fix it there instead of debugging a compiler error inside generated code. | ID | Severity | Title | |---|---|---| | [TSH001](#tsh001) | Error | Toolset or agent is not partial | | [TSH002](#tsh002) | Error | Declared value is not a constant | | [TSH003](#tsh003) | Error | Tool is not a static method group | | [TSH004](#tsh004) | Error | Tool parameter cannot be bound | | [TSH005](#tsh005) | Error | Settings type is not a record with a parameterless constructor | | [TSH006](#tsh006) | Error | Settings are less accessible than what exposes them | | [TSH007](#tsh007) | Error | Tool name is not unique | | [TSH008](#tsh008) | Error | Tool name is not lower_snake_case | | [TSH009](#tsh009) | Warning | Tool has no description | | [TSH010](#tsh010) | Warning | Tool does not take its settings | | [TSH011](#tsh011) | Warning | Tool relies on XML docs that are not generated | | [TSH012](#tsh012) | Error | Configurable member has a type settings cannot store | | [TSH013](#tsh013) | Error | Configurable member cannot be set | | [TSH014](#tsh014) | Error | Declaration outside the constructor | | [TSH015](#tsh015) | Error | Chain the generator cannot read | | [TSH016](#tsh016) | Error | Toolset or agent is out of the generated code's reach | | [TSH017](#tsh017) | Error | Toolset or agent is generic or derives from another | | [TSH018](#tsh018) | Error | Member clashes with a generated member | | [TSH019](#tsh019) | Error | Toolset was not compiled with the generator | | [TSH020](#tsh020) | Warning | Referenced assembly was not compiled with the generator | | [TSH021](#tsh021) | Error | Referenced assembly has another id type | | [TSH022](#tsh022) | Error | Two agents share a settings kind | | [TSH023](#tsh023) | Error | Agent cannot be a tool or a handoff target | | [TSH024](#tsh024) | Warning | Sub-agent offers tools that need approval | | [TSH025](#tsh025) | Error | Tool is offered twice | | [TSH026](#tsh026) | Error | Agents call each other in a circle | | [TSH027](#tsh027) | Error | Tool type has no JSON metadata under Native AOT | | [TSH028](#tsh028) | Error | Store has another id type | | [TSH029](#tsh029) | Error | Id type cannot be used | | [TSH030](#tsh030) | Error | Provider has another id type | ## Severities and suppression An error fails the build. A warning lets it pass, but points at code that does not do what it seems to, such as settings that reach no parameter ([TSH010](#tsh010)). Each diagnostic's help link leads to its section on this page. Most diagnostics are reported on the declaration they are about, so the usual [suppression](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/suppress-warnings) reaches them: `#pragma warning disable TSH009` around one declaration, or a `dotnet_diagnostic.TSH009.severity` entry in `.editorconfig`. [TSH020](#tsh020), [TSH021](#tsh021) and [TSH029](#tsh029) concern the whole build and have no source location, so no `#pragma` reaches them. To silence TSH020, the only warning of the three, add it to `` in the project file. [TSH007](#tsh007) and [TSH022](#tsh022) have no location either when the clash lies between two referenced projects. ## TSH001 **Toolset or agent is not partial.** The generator adds members to every toolset and agent, so the class and every class it is nested in must be `partial`. ## TSH002 **Declared value is not a constant.** The generator reads `WithName`, `WithDescription`, `WithStorageKind` and `WithSchemaVersion` at compile time. Pass a literal or a `const`. ## TSH003 **Tool is not a static method group.** The generated function calls your tool method directly, so `AddTool` needs a static method that is not generic. A lambda, an instance method, a local function, a generic method or a delegate variable cannot be called that way. Pass a static method, of any accessibility: `AddTool(Find)`. ## TSH004 **Tool parameter cannot be bound.** The model's JSON arguments cannot fill a `ref` or `out` parameter, a pointer, or a ref struct such as `Span`. ## TSH005 **Settings type is not a record with a parameterless constructor.** That constructor sets the code defaults. Each layer of settings then changes a copy of them, made with `with`. ## TSH006 **Settings are less accessible than what exposes them.** Make the record, the toolset, the provider id type or the id type as accessible as what exposes it. There are four cases: * A settings record is less accessible than its toolset. The toolset's generated members name the record wherever the toolset can be named. * A toolset with configurable settings is less accessible than a public agent that offers it. The agent's `Settings` record names the toolset's overrides. * A public agent makes its model or its summary model configurable, and the [provider id type](https://weboost-at.github.io/Typeshift/guide/models.md#the-provider-id-type) is less accessible. The agent's `Settings` record names the type for the model's provider. * The build's [id type](https://weboost-at.github.io/Typeshift/guide/registration.md#the-id-type) is less accessible than a public agent. The agent's host members take settings ids and conversation ids of that type, so the generator keeps those members internal. ## TSH007 **Tool name is not unique.** A tool name must be unique across the toolsets of your project and of the projects it references. Names that differ only in their underscores, such as `get_v2` and `get_v_2`, count as the same, because they map to the same generated property. Give one of the tools another name with `WithName("...")`. An agent's own tool needs a unique name only among the tools of the agents its standing approvals reach, and of those whose approvals reach it: the agents it hands off to, directly or through other agents, the agents that hand off to it, its sub-agents and the agents that call it as one. A standing approval names its tool, so it would reach another tool of the same name. Two agents that never share approvals may each have a tool of one name. ## TSH008 **Tool name is not lower_snake_case.** The providers' function names follow that convention, so tool names must too. With `WithName`, the diagnostic is on the string you passed, so change it. Otherwise the tool is [named after its method](https://weboost-at.github.io/Typeshift/guide/toolsets.md#names-and-descriptions), and the diagnostic is on the method in `AddTool(...)`: rename the method, or set a name with `WithName("...")`. ## TSH009 **Tool has no description.** The model chooses tools by their descriptions. Add a `` to the method, or call `WithDescription`. ## TSH010 **Tool does not take its settings.** The tool has `WithSettings()` but no parameter of type `T`, so its settings reach nothing. Add the parameter to the method. ## TSH011 **Tool relies on XML docs that are not generated.** The compiler reads XML docs only with `GenerateDocumentationFile`, so set it in the project. `WithDescription` is a way around it only when the parameters have no `` docs, since it replaces only the ``. ## TSH012 **Configurable member has a type settings cannot store.** The same error covers a `Configurable(o => ...)` pick of a chat option that belongs to a single run. Remove the pick, or change the member's type. A member that `WithConfigurableSettings()` makes configurable is reported on that call, and you can leave it out with `NotConfigurable(...)`. * [Tool settings](https://weboost-at.github.io/Typeshift/guide/tool-settings.md#configurable-members) lists the types a configurable settings member may have. * Of the chat options, the model, the instructions, the sampling options, the stop sequences, multiple tool calls and reasoning may be configurable. `Tools`, `ConversationId`, `ResponseFormat`, `ToolMode` and the additional properties may not, as [Builders](https://weboost-at.github.io/Typeshift/guide/builders.md#chatoptionsbuilder) shows. ## TSH013 **Configurable member cannot be set.** Give it a public `set` or `init` accessor. ## TSH014 **Declaration outside the constructor.** The generator reads `AddTool`, `UseToolset` and the other declarations only directly in the constructor, not in a helper method, a lambda or a local function. It also skips them under an `if`, in a loop or in another nested block, because there they would run only sometimes, while the generated code always includes them. Move each declaration to the top level of the constructor body. ## TSH015 **Chain the generator cannot read.** Keep each declaration one expression. The generator cannot follow a chain split over variables, such as `var u = UseToolset(); u.Include(...)`. It also reads a pick only when it names one member of the lambda's parameter, as `t => t.SearchTickets` in `Include` or `s => s.Limit` in `Configurable` do. ## TSH016 **Toolset or agent is out of the generated code's reach.** Generated code names the class and the module creates it, so it cannot be nested as `private` or `protected`, and needs a public or internal parameterless constructor. ## TSH017 **Toolset or agent is generic or derives from another.** Derive each toolset from `Toolset` and each agent from `Agent` directly, without type parameters. Share code through static methods. ## TSH018 **Member clashes with a generated member.** A member of yours has a name the generator writes, such as `Settings` in an agent, or `CreateSearchTickets`, a tool's factory, in the toolset or the agent that declares the tool. Rename your member. [Generated members](https://weboost-at.github.io/Typeshift/guide/generated-members.md) lists the names. ## TSH019 **Toolset was not compiled with the generator.** It is reported on `UseToolset()`. Reference the package in the toolset's project. ## TSH020 **Referenced assembly was not compiled with the generator.** It declares toolsets or agents but carries no module of this Typeshift version, so `AddTypeshift()` cannot register them. Reference the package in its project, at the version the rest of the build uses. ## TSH021 **Referenced assembly has another id type.** Every project of a build shares one id type and one provider id type. Set `TypeshiftIdType` and `TypeshiftProviderIdType` once, in `Directory.Build.props`. ## TSH022 **Two agents share a settings kind.** Their settings would overwrite each other in the store. It happens when two agent classes have the same name in different namespaces, or when two agents set the same `WithStorageKind`, in one project or across a project and the projects it references. Give one of them a `WithStorageKind` of its own. ## TSH023 **Agent cannot be a tool or a handoff target.** The diagnostic sits on `UseAgent()` or `UseHandoff()` and is about the agent `T`. The model needs that agent's `WithDescription` to decide when to call it, and a lower_snake_case name to call it by: its class name in snake_case, unless `WithName(...)` sets another. Add a `WithDescription(...)` to that agent's constructor, or fix its name. An agent of a referenced project is checked too. ## TSH024 **Sub-agent offers tools that need approval.** A sub-agent cannot stop and ask your app for approval, so in `Default` mode its calls of such tools are rejected unless a standing approval or a rule approves them. Add an [approval rule](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#approval-rules), or offer the agent with `UseHandoff()` instead of `UseAgent()`. ## TSH025 **Tool is offered twice.** An agent offers the same tool twice when it: * uses the same toolset twice; * picks the same tool twice; * uses the same agent twice as a sub-agent, or twice as a handoff target; * uses two toolsets that each have a tool of the same name. Leave one of the two tools out, for example with `Exclude(...)`; * has a tool of its own named like a tool of a toolset it uses. Rename its own tool with `WithName("...")`. ## TSH026 **Agents call each other in a circle.** The agents offer each other as sub-agents through `UseAgent()`, so a call could never end. Break the circle by removing one of these `UseAgent()` calls, or by replacing one with `UseHandoff()`, because handoffs may form circles. ## TSH027 **Tool type has no JSON metadata under Native AOT.** In a project that publishes with Native AOT or sets `IsAotCompatible`, the types of the parameters the model fills and the type the tool returns need source-generated metadata. List each type as `[JsonSerializable(typeof(T))]` on a `JsonSerializerContext` of the toolset's assembly ([Native AOT](https://weboost-at.github.io/Typeshift/guide/production.md#native-aot)). A context nested as `private` or `protected` does not count. These types need none: `string`, `char`, `bool`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `float`, `double`, `decimal`, `DateTime`, `DateTimeOffset`, `TimeSpan`, `IEnumerable`, `JsonElement`, `JsonDocument` and the `System.Text.Json.Nodes` types. ## TSH028 **Store has another id type.** `WithStore()` takes a store of the build's id type. Implement `ISettingsStore`, `IConversationStore` or `ITypeshiftStore`, which is both. The generator writes these aliases for the build's id type, so they always have the right one. ## TSH029 **Id type cannot be used.** `TypeshiftIdType` or `TypeshiftProviderIdType` names a type the build does not know. Give a C# keyword (`int`, `long`, `string`), a type of `System` (`Guid`), or the type's full name with its namespace. A provider id is stored in the agent's `Settings` as one value, so `TypeshiftProviderIdType` also has to be a string, a number, a `Guid` or an enum. ## TSH030 **Provider has another id type.** `WithModel(provider, model)` takes a provider of the build's [provider id type](https://weboost-at.github.io/Typeshift/guide/models.md#the-provider-id-type), and `WithChatClientProvider()` a class that serves that type. Pass a provider of that type, or implement `IChatClientProvider`, the alias the generator writes for it. ## Related * [Toolsets](https://weboost-at.github.io/Typeshift/guide/toolsets.md): the rules most toolset diagnostics guard. * [Agents](https://weboost-at.github.io/Typeshift/guide/defining-agents.md): the rules of an agent's declarations. * [Builders](https://weboost-at.github.io/Typeshift/guide/builders.md): every call the analyzer checks. * [Registration](https://weboost-at.github.io/Typeshift/guide/registration.md): the package, the id type and what each project needs. * [Native AOT](https://weboost-at.github.io/Typeshift/guide/production.md#native-aot): the JSON metadata [TSH027](#tsh027) asks for. --- --- url: https://weboost-at.github.io/Typeshift/guide/glossary.md description: The words these docs use and what exactly they mean in Typeshift. --- # Glossary Typeshift reuses Microsoft.Extensions.AI's words wherever it can: a run returns a `ChatResponse`, and an `IChatClient` serves a model. The terms below are Typeshift's own, or mean something narrower here. | Term | Meaning | |---|---| | **Agent** | A `partial` class deriving from `Agent` that bundles chat options, instructions, tools and their settings for one job. See [Agents](https://weboost-at.github.io/Typeshift/guide/defining-agents.md). | | **Agent host** | `AgentHost`, which runs one agent with the services of the scope it was resolved from. See [Using an agent](https://weboost-at.github.io/Typeshift/guide/using-an-agent.md). | | **Approval request** | The `ToolApprovalRequestContent` a run ends with when it needs approval. You answer it in the conversation's next run. See [Approval and modes](https://weboost-at.github.io/Typeshift/guide/tool-approval.md). | | **Approval rule** | An `IToolApprovalRule` that approves, rejects or abstains on a tool call before your app is asked. | | **Chat client provider** | The `IChatClientProvider` your app registers with `WithChatClientProvider()`, which returns the `IChatClient` for a provider and a model. | | **Code defaults** | The values a settings record's parameterless constructor sets, the lowest [layer](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#settings-layering). | | **Compaction** | Shrinking the history a model call sees once it nears a token limit. See [Compaction](https://weboost-at.github.io/Typeshift/guide/conversations.md#compaction). | | **Configurable** | A settings member or an option that stored settings or a run may change, marked in code with `Configurable(...)` or `WithConfigurableSettings()`. Everything else keeps the value the code sets. | | **Context provider** | An `IContextProvider` that adds instructions, messages or tools to every run of the agents that use it. | | **Conversation** | The entries a run stores under a conversation id: inputs, model responses, tool results and run endings. See [Conversations](https://weboost-at.github.io/Typeshift/guide/conversations.md). | | **Declaration** | The chain of calls in a toolset's or agent's constructor, such as `AddTool(...).ReadOnly()`. The generator reads it at compile time, and at runtime it only records values. | | **Diagnostic** | A `TSHnnn` error or warning the analyzer reports for a mistake in a toolset, an agent or the setup. See [Diagnostics](https://weboost-at.github.io/Typeshift/guide/diagnostics.md). | | **Generated member** | A member the source generator writes into or next to your toolsets and agents. See [Generated members](https://weboost-at.github.io/Typeshift/guide/generated-members.md). | | **Handoff** | A tool, `handoff_to_{name}`, declared with `UseHandoff()`, that hands the conversation to another agent. See [Multiple agents](https://weboost-at.github.io/Typeshift/guide/multi-agent.md#handoff). | | **Host** | Short for agent host, `AgentHost`. The application that uses Typeshift is "your app". | | **Id type** | The type of every settings id and conversation id in a build: `Guid` unless `TypeshiftIdType` names another. | | **Kind** | The stable name a store keeps a document under. An agent's settings kind is its class name, or the name `WithStorageKind("...")` sets. | | **Layer** | One source of settings: the code defaults, the agent's code, the stored settings or the run. A higher layer overrides a lower one, and a conversation's mode sits between the stored settings and the run. | | **Middleware** | An `IRunMiddleware` around every run, or an `IToolMiddleware` around every tool call. | | **Mode** | `Default`, `Plan` or `Auto`, which decides what a run offers the model and what happens to tools that require approval. See [The three modes](https://weboost-at.github.io/Typeshift/guide/tool-approval.md#the-three-modes). | | **Module** | The registration the generator writes for each assembly that declares toolsets or agents. `AddTypeshift()` registers the modules of the project and its references. | | **Overrides** | The generated `{Tool}Overrides` record, a tool's configurable settings with each one nullable. Null keeps the layer below. | | **Provider id type** | The type of every provider id in a build, such as the provider in `WithModel(provider, model)`: `string` unless `TypeshiftProviderIdType` names another. See [Models and providers](https://weboost-at.github.io/Typeshift/guide/models.md#the-provider-id-type). | | **Raw path** | Resolving a run's options and creating its `IChatClient` yourself, for when your app needs the client. See [Running agents](https://weboost-at.github.io/Typeshift/guide/running-agents.md#the-raw-path). | | **Run** | One call of `RunAsync`, `RunAsync`, `RunStreamingAsync`, `ResumeAsync`, `ResumeStreamingAsync` or the raw path's client: the model calls and tool calls until the model answers. | | **Run options** | How a run's tool loop goes, with its mode, its limits and compaction. Set with `WithRunOptions()`. | | **Schema version** | The number in a stored document's JSON, `"$schemaVersion"`, that names the shape of the record that wrote it: 1 unless the agent's `WithSchemaVersion(n)` raises it. Reading a newer one fails. See [The stored JSON](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md#the-stored-json). | | **Settings** | An agent's generated `Settings` record, with exactly the values that may change at runtime. Stored under a settings id, or passed for one run. | | **Settings id** | The id your app stores an agent's `Settings` under, such as one per customer, team or user. | | **Settings record** | The record a tool receives its settings in, declared with `WithSettings()` or `WithConfigurableSettings()`. The model never sees it. | | **Stamp** | A hidden attribute the generator writes so that other assemblies can read a toolset's tools or a project's module. | | **Standing approval** | An approval for every later call of a tool in a conversation, given with `ApproveForConversation()`. | | **Steering** | Input a user sends while a run works, which you hand to that run with `ConversationTracker.TrySteer`. See [Steering](https://weboost-at.github.io/Typeshift/guide/conversations.md#steering). | | **Store** | What your app registers with `WithStore()` to keep settings (`ISettingsStore`), conversations (`IConversationStore`) or both (`ITypeshiftStore`). See [Settings and stores](https://weboost-at.github.io/Typeshift/guide/settings-and-stores.md). | | **Sub-agent** | Another agent offered as a tool with `UseAgent()`. It works in a context of its own and answers with text. | | **Tool** | A static method that a toolset or an agent declares with `AddTool`, offered to the model as a generated `AIFunction`. | | **Tool list** | `ToolList`, the type `Include` and `Exclude` pick tools from. The generated `{Toolset}ToolList` gives it one property per tool. | | **Toolset** | A `partial` class deriving from `Toolset` that declares the tools of one domain. See [Toolsets](https://weboost-at.github.io/Typeshift/guide/toolsets.md). | | **Version** | The number that marks a stored document's current state; each save gives the document a new one. A save passes the version it loaded, so it fails when another save came first. Not the schema version. See [Save with a version](https://weboost-at.github.io/Typeshift/guide/implementing-a-store.md#save-with-a-version). | ## Related * [Getting started](https://weboost-at.github.io/Typeshift/guide/getting-started.md#how-the-pieces-fit): how the pieces fit together. * [Writing style](../contributing/writing.md): for contributors, how these docs use the terms above.