Skip to main content

Overriding AdminBase<TInput, TOutput>

The six built-in admins (Sequential, GroupChat, Parallel, MapReduce, Reflexion, Graph) cover almost every orchestration pattern you'll need. Custom admins are the right answer when:

  • You're modelling a domain-specific workflow that doesn't map onto any built-in shape (a state machine driven by external triggers, a human-in-the-loop checkpoint, a distributed execution split across workers).
  • You want to bake fixed prompt sequencing or special tool-use rules that aren't expressible as agent prompts alone.

For everything else, prefer the built-ins — they handle event firing, cost accumulation, and budget enforcement for you.

What you get from AdminBase​

public abstract class AdminBase<TInput, TOutput>
{
public abstract string Name { get; }
protected abstract LlmClientBase LlmClient { get; }
protected abstract IList<IAgent> Agents { get; }
protected virtual AdminOptions Options => new AdminOptions();

public List<LlmMessage> MessageHistory { get; } // persistent across the run

protected CostEstimate RunCost { get; } // accumulated this run
protected void ResetRunState(); // call at start of RunAsync
protected void AccumulateCost(CostEstimate cost); // add per-call cost
protected Task CheckBudgetAsync(AgentContext ctx); // fire warning / throw on cap
}

MessageHistory is the difference between agents and admins: agents reset, admins persist. That's how a GroupChatAdmin lets agent N+1 see what agents 1..N said.

A minimal custom admin​

A round-robin admin that runs every agent in a fixed cycle for N turns, regardless of output content.

using LogicGrid.Core.Admins;
using LogicGrid.Core.Agents;
using LogicGrid.Core.Events;
using LogicGrid.Core.Llm;

public sealed class RoundRobinAdmin : AdminBase<string, string>
{
private readonly IList<IAgent> _agents;
private readonly LlmClientBase _llm;
private readonly int _turns;
private readonly IAgentEventBus? _bus;

public override string Name { get; }
protected override LlmClientBase LlmClient => _llm;
protected override IList<IAgent> Agents => _agents;

public RoundRobinAdmin(
string name,
LlmClientBase llm,
IList<IAgent> agents,
int turns,
IAgentEventBus? bus = null)
{
Name = name;
_llm = llm;
_agents = agents;
_turns = turns;
_bus = bus;
}

public async Task<string> RunAsync(string input, CancellationToken ct = default)
{
ResetRunState();
var ctx = new AgentContext(input) { EventBus = _bus };

if (_bus != null)
await _bus.PublishAsync(new RunStartedEvent(
ctx.RunId, Name, input, DateTimeOffset.UtcNow));

string current = input;
try
{
for (int i = 0; i < _turns; i++)
{
var agent = _agents[i % _agents.Count];
agent.EventBus = _bus;
current = await agent.RunAsync(current, ctx, ct);

AccumulateCost(/* obtained from the agent's last span — see below */);
await CheckBudgetAsync(ctx);
}

if (_bus != null)
await _bus.PublishAsync(new RunCompletedEvent(
ctx.RunId, Name, current, _agents.Count, _turns,
DateTimeOffset.UtcNow - DateTimeOffset.UtcNow));

return current;
}
catch (Exception ex)
{
if (_bus != null)
await _bus.PublishAsync(new RunFailedEvent(ctx.RunId, Name, ex, DateTimeOffset.UtcNow));
throw;
}
}
}

A few things to notice:

  • Set agent.EventBus before each call. Agents only emit events when their bus is set — that's how the framework correlates each agent's events to the run.
  • Pass the same ctx into every agent. That's the bus + run-id channel.
  • Call ResetRunState(), AccumulateCost, and CheckBudgetAsync. That's how RunCost and MaxBudgetUsd enforcement work — the base class doesn't auto-instrument; you opt in by calling these.

Reading per-call cost​

AgentBase calculates and emits Cost on AgentCompletedEvent. Subscribe to it inside your admin and accumulate:

public RoundRobinAdmin(/* … */)
{
/* … */
if (_bus != null)
{
_bus.Subscribe<AgentCompletedEvent>(e =>
{
AccumulateCost(e.Cost);
CheckBudgetAsync(ctxRef).GetAwaiter().GetResult();
});
}
}

(ctxRef here is captured from the active run — in practice, hold the active context in a field cleared at the end of the run.)

Persistent MessageHistory​

Append to MessageHistory every time an agent runs. The next agent gets to see the conversation when you supply it via the scoped vs admin history distinction:

foreach (var agent in _agents)
{
var input = string.Join("\n\n", MessageHistory.Select(m => $"[{m.Role}] {m.Content}"));
var output = await agent.RunAsync(input, ctx, ct);
MessageHistory.Add(LlmMessage.Assistant(output));
}

This is how GroupChatAdmin builds context: every agent's output is appended to MessageHistory, which the admin then renders into the next selection prompt.

Don't reinvent the wheel​

If your admin is starting to look like a state machine:

  • For "do this, then that, then that": use SequentialAdmin.
  • For "let an LLM pick the next step": use GroupChatAdmin with the finalAgentName constructor argument set.
  • For "run these in any order, then collect": use ParallelAdmin.
  • For "process every item then summarise": use MapReduceAdmin.
  • For "actor → critic → revise": use ReflexionAdmin.
  • For everything else with explicit transitions: use GraphAdmin.

A custom admin earns its keep when it carries domain logic the built-ins genuinely don't express — most of the time, building a GraphAdmin is the right answer.