developer
  • Joined on 2025-12-29

Harness.Client (0.4.0-alpha.4)

Published 2026-11-07 17:08:27 +01:00 by developer

Installation

dotnet nuget add source --name developer --username your_username --password your_token 
dotnet add package --source developer --version 0.4.0-alpha.4 Harness.Client

About this package

.NET client for the harness host: typed run specs and events, an event stream that resumes after a dropped connection, caller tools and typed finish results.

Harness.Client

The .NET client of the harness host: typed run specs, messages and events, an event stream that resumes after a dropped connection, caller tools carried out in .NET, and typed finish results. .NET 10, no dependencies besides the base library.

The host runs the agent loop (docs/contract.md); this library is how a .NET app drives it. The C# types follow the host's JSON schemas in contract, which a test in the host keeps current; the tests here compare every C# type with them, so a field cannot change on one side only.

A run with a caller tool and a typed result

using Harness.Client;

using var harness = HarnessClient.Create(new Uri("http://127.0.0.1:18090/"), token);

var lookup = CallerTool.Create<LookupArguments>(
    "lookup", "Looks up the value of a key.",
    async (arguments, ct) => await store.GetAsync(arguments.Key, ct),
    mutates: false);

var spec = new RunSpec
{
    Model = "qwen/qwen3-coder",
    SystemPrompt = "You look things up and report them.",
    Messages = [UserMessage.FromText("Look up key a and report it.")],
    Limits = new RunLimits { MaxToolCalls = 20, MaxSeconds = 300 },
    End = EndMode.FinishTool,
    FinishTool = ToolSpec.For<Report>("report", "The result."),
    CallerTools = CallerTool.Specs([lookup]),
};

var result = await new RunDriver(harness).RunAsync(spec, new RunHandlers
{
    CallerTools = [lookup],
    OnEvent = (ev, ct) => { Console.WriteLine(ev.Type); return ValueTask.CompletedTask; },
});
Report report = result.FinishAs<Report>();   // checked against the schema by the host (lesson L42)

record LookupArguments(string Key);
record Report(string Summary, int Count);

What it covers

Need API
Every host endpoint HarnessClient: StartRunAsync, EventsAsync, SendToolResultsAsync, SendInputAsync, SendApprovalsAsync, SendMessageAsync (steer), CancelAsync, GetRunAsync, GetStateAsync, ResumeRunAsync, GetStatusAsync
A whole run with its pauses RunDriver.RunAsync / FollowAsync with RunHandlers (events, caller tools, questions, approvals)
Caller tools with typed arguments CallerTool.Create<T>: the schema comes from T; arguments that do not fit go back to the model as an error
Typed results ToolSpec.For<T> as finish tool, RunResult.FinishAs<T>()
Store and resume a paused run GetStateAsync → RunState.ToJson(); later RunState.Parse → ResumeRunAsync
The next turn from a run's messages RunState.Messages into the next RunSpec.Messages
Host refusals HarnessException with Status, Code (e.g. run_not_running) and Detail
Event types of newer hosts arrive as UnknownEvent with their JSON

Before it starts a run, RunDriver checks that the spec's caller tools, questions and approvals all have a handler. A dropped event stream is read on after the last sequence number seen (waits of 0, 0.5, 1, 2 and 5 seconds); a pause is never answered twice.

Build and test

dotnet test clients/dotnet/Harness.Client.slnx
HARNESS_DOTNET_TESTS=1 uv run pytest packages/harness-host/tests/test_dotnet_client.py

The second drives Harness.Client.Smoke against a real host with a scripted model. After a change of the host's types: uv run python -m harness_host.contract clients/dotnet/contract.

The package version equals the Python packages' version (0.4.0a3 is 0.4.0-alpha.3); a test keeps them equal.

Details
NuGet
2026-11-07 17:08:27 +01:00
0
Florian Honegger
69 KiB
Assets (2)
Versions (1) View all
0.4.0-alpha.4 2026-11-07