Skip to content

Add tools and handle approvals ​

Harness composes application-owned tools into the Agentic Loop. Tool implementations stay in your process and share the same dependency injection container as the rest of the application.

Define a tool source ​

Write ordinary typed methods with [GeneratedTool], then expose their generated wrappers through AIFunctionToolSource. The Harness NuGet package includes the generator; no separate analyzer reference is needed. This example assumes the application assembly is named MyApp.

csharp
using System.ComponentModel;
using DotCraft.GeneratedTools.MyApp;
using DotCraft.Tools;
using Microsoft.Extensions.AI;

public sealed class ClockToolSource : AIFunctionToolSource
{
    public override string SourceId => "sample.clock";

    protected override IEnumerable<AIFunction> CreateFunctions(
        ToolPlanningContext context)
    {
        yield return GeneratedToolFunctions.ClockToolSource_GetUtcTime(this);
    }

    [GeneratedTool]
    [Description("Return the current UTC time.")]
    public DateTimeOffset GetUtcTime() => DateTimeOffset.UtcNow;
}

CreateFunctions receives immutable planning context for the current Thread and Turn. Use it to decide whether to emit a function that belongs only to a particular workspace, mode, or provider capability.

The generator owns schema generation and typed argument binding. Describe each model parameter with [Description]; C# defaults become optional arguments. [Tool] uses the same generator and adds built-in catalog/presentation metadata. Plugins normally use [GeneratedTool], which is not catalog-visible by default.

Access live invocation identity only when needed ​

Most tools need only typed parameters, constructor-injected services, and an optional CancellationToken. Request ToolInvocationContext only when the method needs the live calling identity, and return ToolExecutionResult only when it needs explicit success or failure semantics. See Write typed tools for both contracts.

Register the source ​

Register the source in the same service collection as Harness:

csharp
builder.Services.AddSingleton<IToolSource, ClockToolSource>();
builder.Services.AddDotCraftHarness(appConfig, options =>
{
    options.WorkspacePath = workspacePath;
});

Harness collects all IToolSource registrations when it builds the tool plan. Keep source IDs stable and tool names descriptive. Both become part of the model-visible tool contract.

TIP

Inject application services into the tool source constructor. This keeps credentials, databases, and UI state out of static helpers and makes tools straightforward to test.

Process tool events ​

SessionEventHandler converts the session event stream into focused callbacks:

csharp
var handler = new SessionEventHandler
{
    OnTextDelta = text => ui.AppendTextAsync(text),
    OnToolStarted = (name, icon, display, callId) =>
        ui.ShowToolStartedAsync(name, display),
    OnToolCompleted = (callId, result) =>
        ui.ShowToolResultAsync(callId, result),
    OnApprovalRequested = request =>
        approvalDialog.RequestDecisionAsync(request),
    OnTurnCompleted = usage => ui.CompleteTurnAsync(usage)
};

await handler.ProcessAsync(
    sessions.SubmitInputAsync(thread.Id, prompt, ct: cancellationToken),
    (threadId, turnId, requestId, decision) =>
        sessions.ResolveApprovalAsync(
            threadId,
            turnId,
            requestId,
            decision,
            cancellationToken),
    cancellationToken);

The handler waits for OnApprovalRequested and sends its decision back to Session Core before execution resumes.

Choose an approval decision ​

DecisionEffect
AcceptOnceAllow this request only.
AcceptForSessionAllow the request and remember it for the current Thread.
AcceptAlwaysAllow the request and write the approval into workspace approval state. Neither the current Thread nor later sessions ask again.
RejectReject the operation and let the Turn continue.
CancelTurnReject the operation and cancel the active Turn.

Prefer AcceptOnce for unfamiliar or high-impact operations. Offer AcceptAlways only where the user understands the scope of a permanent approval.

Approval requests do not expire. A request waits until a decision arrives or its Turn ends; when the Turn ends first, Session Core resolves the request as CancelTurn and the operation does not run.

CAUTION

Do not approve tools automatically based only on their display name. Present the operation, arguments, affected resources, and approval scope to the user.

Present shell command approvals ​

A shell request carries request.Shell with what the safety check found: Reasons explains the prompt. Show the command and its directory as they arrive in operation and target.

RememberedPrefixes and RemembersExactCommand describe what AcceptAlways will store: an allow rule per prefix in the workspace's shell-rules.json, the exact command key, or both. State that scope in the AcceptAlways option itself so the user sees it before choosing. AcceptForSession always keys on the exact command, shell, and directory.