Skip to content

Using an agent ​

Your app uses an agent through its host, AgentHost<TAgent>, which services.AddTypeshift() registers for every agent (Registration). 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 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<SupportAgent> support, ICurrentCustomer customer)
{
    public async Task<string> AnswerAsync(string question, CancellationToken ct)
    {
        var answer = await support.RunAsync(question, settingsId: customer.Id, ct: ct);
        return answer.Text;
    }
}

snippet source | anchor

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). Register RefundDesk as scoped, with services.AddScoped<RefundDesk>(), 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<AgentHost<SupportAgent>>();

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

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

snippet source | anchor

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 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<string> 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<AgentHost<SupportAgent>>();

        var answer = await support.RunAsync("Summarize the tickets opened today.", ct: ct);
        return answer.Text;
    }
}

snippet source | anchor

Register the job with services.AddHostedService<TicketDigest>(), and the clock its timer waits on with services.AddSingleton(TimeProvider.System). A test can register a fake TimeProvider instead.

Released under the MIT License.