MODULE 06 · LESSON 6.1
Upgrade the assistant to a harness
Upgrade the assistant from one turn to tracked multi-step work.
Lesson 33 of 86 · FabrCore 2.0
Overview
A plain ChatClientAgent finishes when its model stops. A harness adds todos, modes, completion loops and delegation so unfinished work can be observed and continued. CreateFabrCoreHarnessAgent also supplies FabrCore history, tracked model calls and snapshot integration.
What the harness adds to an AI call
A normal AI run may stop after one answer even when a larger investigation has unfinished steps. The harness keeps explicit todos, operating mode and delegation state around that reasoning call. Its completion policy can decide whether another iteration is needed. Those capabilities make remaining work inspectable; they do not make a model's claim of success authoritative.
Why the wrapper's RunAsync matters
CreateFabrCoreHarnessAgent supplies the model component together with FabrCore integration for tracked calls, history and session snapshots. Call the returned wrapper's RunAsync from OnMessage. Calling only the inner Agent bypasses the wrapper's completion/persistence behavior, producing an agent that can appear to have a harness while losing the lifecycle guarantees the tour is trying to demonstrate.
Run through the harness wrapper
- Use the complete harness agent example and select its alias for the assistant. Resolve the same configured business tools.
- Create the harness in OnInitialize and call FabrCoreHarnessResult.RunAsync from OnMessage. Do not call the inner Agent.RunAsync directly.
- Read remaining todos and include unfinished work in the response. Use AsFabrCoreHarnessAgent only outside a proxy when you will supply persistence and other host services yourself.
using System.ComponentModel;
using FabrCore.Core;
using FabrCore.Sdk;
[AgentAlias("researcher")]
[Description("Researches a question end to end and reports back.")]
[FabrCoreCapabilities("Breaks a research goal into tracked steps, delegates lookups to specialist agents, and reports a consolidated answer.")]
public class ResearcherAgent : FabrCoreAgentProxy
{
private FabrCoreHarnessResult harness = null!;
public ResearcherAgent(AgentConfiguration config, IServiceProvider serviceProvider, IFabrCoreAgentHost fabrcoreAgentHost)
: base(config, serviceProvider, fabrcoreAgentHost) { }
public override async Task OnInitialize()
{
var tools = await ResolveConfiguredToolsAsync();
harness = await CreateFabrCoreHarnessAgent(config.Models ?? "default", "main", tools);
}
public override async Task<AgentMessage> OnMessage(AgentMessage message)
{
var run = await harness.RunAsync(message);
var response = message.Response();
response.Message = run.Text;
return response;
}
}
public override async Task<AgentMessage> OnMessage(AgentMessage message)
{
SetStatusMessage("Planning...");
var run = await harness.RunAsync(message);
var text = run.Text;
// Delegations stranded by a restart — see references/durability.md.
if (harness.DescribeLostDelegations() is { } lost)
{
text += $"{Environment.NewLine}{Environment.NewLine}{lost}";
}
var remaining = await harness.GetRemainingTodosAsync();
if (remaining.Count > 0)
{
text += $"{Environment.NewLine}{Environment.NewLine}Not completed within the iteration budget:{Environment.NewLine}"
+ string.Join(Environment.NewLine, remaining.Select(item => $"- {item.Title}"));
}
SetStatusMessage(string.Empty);
var response = message.Response();
response.Message = text;
return response;
}
Compare a completed and unfinished investigation
- Create a disposable instance of the illustrated harness agent and ask for a two-step fixture investigation: look up SR-1042, then summarize the returned facts.
- Inspect its todos and tool results. A finished response should correspond to completed work, not merely a sentence saying “Done.”
- Make one dependency unavailable and repeat. Expect an unfinished/blocked item and an explanation of what remains, rather than a success message that omits the failed step.
The meaningful result is agreement between the final answer and recorded work state. The harness makes that state available; your application must present it honestly.
If the result is different
Calling the inner agent bypasses the wrapper's snapshot behavior. A successful model response is not proof every todo is complete.
Go deeper
Explore the related documentation.