ToolsPatterns

Patterns

Organize Runiq tools cleanly in a .NET application.

Keep tools close to the .NET capability they expose.

For a real application, create a Tools folder and put each tool in its own file or small feature folder. A tool should look like application code: typed input, typed output, dependency injection, validation, and one focused operation.

Tools are not prompt fragments. They are native .NET capabilities that agents can call through the Runiq runtime.

Text
MyApp/
  Program.cs
  Agents/
    SupportAgent.cs
    BillingAgent.cs
  Tools/
    CustomerSubscriptionTool.cs
    CreateSupportTicketTool.cs
    InvoiceLookupTool.cs
  Context/
    ProductSupportContext.cs

For a larger domain, group tool models with the tool:

Text
MyApp/
  Tools/
    CustomerSubscription/
      CustomerSubscriptionTool.cs
      CustomerSubscriptionInput.cs
      CustomerSubscriptionOutput.cs
    SupportTickets/
      CreateSupportTicketTool.cs
      CreateSupportTicketInput.cs
      CreateSupportTicketOutput.cs

For the travel sample:

Text
Runiq.WorkflowTravelPlanner/
  Agents/
    WeatherAgent.cs
    PlacesAgent.cs
    PlannerAgent.cs
  Tools/
    WeatherTool.cs
    PlacesTool.cs
    MealSuggestionTool.cs
    BudgetEstimatorTool.cs

Travel is only the sample domain. The pattern is the important part: tools live in Tools/, and agents attach only the tools they should be allowed to call.

For the expense sample:

Text
Runiq.ExpenseDesk/
  Agents/
    ExpenseDataAnalyst.cs
  Data/
    ExpenseDeskDatabase.cs
    ExpenseDeskSeedData.cs
  Tools/
    ExpenseSearchTool.cs
  Context/
    ExpenseDeskContext.cs
    expense-policy.md

This layout is closer to a business application. The tool does not own the data store; it calls the app's data service. The agent receives a business capability, not direct database access.

Keep Program.cs small

Most tools should be attached from agent factories, not wired manually in Program.cs.

C#
builder.Services.AddRuniqServer(options =>
{
    options.AddAgent(WeatherAgent.Create(openAiApiKey));
    options.AddAgent(PlacesAgent.Create(openAiApiKey));
    options.AddAgent(PlannerAgent.Create(openAiApiKey));
});

Program.cs shows which agents are registered. The agent files show which tools each agent can use.

Use options.AddTool<TTool>() only when you intentionally want a standalone tool available in the dashboard tool playground without attaching it to an agent.

C#
builder.Services.AddRuniqServer(options =>
{
    options.AddTool<ServerTimeTool>();
});

Standalone registration does not grant every agent access to that tool. Agents still need .AddTool<TTool>().

Put tool logic in tool files

A small tool can keep the tool class, input record, and output record in one file.

C#
using Runiq.Agents.Tools;

namespace Runiq.WorkflowTravelPlanner.Tools;

[RuniqTool(
    name: "weather",
    description: "Returns deterministic demo weather guidance for a city.")]
public sealed class WeatherTool : IRuniqTool<WeatherInput, WeatherOutput>
{
    public Task<WeatherOutput> ExecuteAsync(
        WeatherInput input,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(input);

        var city = string.IsNullOrWhiteSpace(input.City)
            ? "City"
            : input.City.Trim();

        return Task.FromResult(new WeatherOutput(
            City: city,
            TemperatureCelsius: 22,
            Condition: "Partly Cloudy",
            Advice: "Suitable for walking with a light jacket."));
    }
}

public sealed record WeatherInput(string City);

public sealed record WeatherOutput(
    string City,
    int TemperatureCelsius,
    string Condition,
    string Advice);

For larger domains, split the models into separate files. Keep the contract easy to inspect because the model, dashboard, and runtime all depend on that contract.

Attach tools from agent factories

The agent that needs a tool should attach it.

C#
public static Agent Create(string? apiKey)
{
    return new WeatherAgent(apiKey)
        .AddTool<WeatherTool>();
}

This keeps capability boundaries explicit. Weather Agent can call WeatherTool. Places Agent can call PlacesTool. Planner Agent can call MealSuggestionTool. They do not automatically share every tool in the application.

Use DI for application services

Tools should call your existing application services instead of duplicating infrastructure code.

C#
[RuniqTool(
    name: "customer_subscription",
    description: "Looks up the current subscription state for a customer.")]
public sealed class CustomerSubscriptionTool
    : IRuniqTool<CustomerSubscriptionInput, CustomerSubscriptionOutput>
{
    private readonly ICustomerRepository customers;

    public CustomerSubscriptionTool(ICustomerRepository customers)
    {
        this.customers = customers;
    }

    public async Task<CustomerSubscriptionOutput> ExecuteAsync(
        CustomerSubscriptionInput input,
        CancellationToken cancellationToken = default)
    {
        var subscription = await customers.GetSubscriptionAsync(
            input.CustomerId,
            cancellationToken);

        return new CustomerSubscriptionOutput(
            input.CustomerId,
            subscription.PlanName,
            subscription.IsActive);
    }
}

This keeps AI integration native to the .NET app. The agent gets a safe capability; the tool gets normal C# dependencies.

In a real system, those dependencies might be DbContext, repositories, billing services, CRM clients, authorization checks, or internal APIs. Runiq does not require those services to become prompts. You expose a small typed surface and keep the implementation in C#.

Good fits

Tools work well for read-only lookups, deterministic calculations, curated searches, safe write operations, and policy checks.

Examples:

Text
CustomerSubscriptionTool
CreateSupportTicketTool
InvoiceLookupTool
PolicyCheckTool
WeatherTool
BudgetEstimatorTool

These are good fits because the input can be modeled, the output can be structured, and the operation belongs to your application.

Common mistakes

Putting tool logic inside Program.cs makes the runtime registration harder to read. Put each tool in Tools/*.cs.

Returning a long final answer from a tool makes the agent less useful. Return structured data and let the agent write the final response.

Using one generic tool for many unrelated actions hides the contract from the model and the dashboard. Split capabilities into focused tools.

Hiding business rules in prompts makes behavior harder to test. Keep business rules in .NET code and expose them through typed tools.

Attaching every tool to every agent weakens the capability boundary. Attach only the tools each agent should use.

Swallowing exceptions and returning fake data makes debugging harder. Fail clearly so the dashboard and model can see what happened.

On this page