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.EventBusbefore 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
ctxinto every agent. That's the bus + run-id channel. - Call
ResetRunState(),AccumulateCost, andCheckBudgetAsync. That's howRunCostandMaxBudgetUsdenforcement 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
GroupChatAdminwith thefinalAgentNameconstructor 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.