Most "AI agent" demos die at the second agent. A single agent answering a single question is a solved problem; an organization of agents that continuously watch the world, propose actions, wait for human approval, execute, and learn from the outcome is not.
The hard part was never the model - it is coordination, authority, and scale.
This article presents a production-shaped reference architecture for a Human-in-the-Loop control plane: a small, frozen "engine" of three generic services connected by a shared publish/subscribe fabric, where every new capability is added as configuration and plug-ins, not new plumbing. The design lets one human safely supervise many concurrent, heterogeneous agent sessions from a single pane of glass, and it scales by adding threads and topics, not by complicating any single conversation.
We describe the full closed loop - detect → propose → approve → execute → record → learn - the message contracts that make agents interchangeable, the "right tool for each stage" discipline that keeps the system fast and correct, and the specific properties that make it scale sub-linearly in engineering effort as the number of agents grows. The architecture is drawn from a working proof-of-concept, but everything here is deliberately domain-neutral: the same skeleton runs a supply-chain remediation agent, an incident-response agent, and a work-item triage agent without a line of engine code changing.
The industry narrative is "give the agent tools and let it rip." That works for a toy. In a real enterprise, five uncomfortable truths show up the moment you try to run agents against production systems:
This article is about an architecture that takes all five seriously. The thesis in one line:
Freeze a tiny generic engine. Push all behavior into config and plug-ins. Route everything through a shared bus by capability, never by address. Put the human on a priority lane. Close the loop with a durable, append-only record - and scale by adding threads and topics, not code.
The user-facing metaphor is a forum, not a chat window. This is not cosmetic - it is the scaling primitive.
The human works one thread at a time, and each thread is self-contained context. The system does not scale by making a single conversation smarter or longer; it scales by spawning more threads across more workstreams, each independently orderable and independently routable. That distinction - scale-by-threads, not scale-by-context - is the whole game.
The entire human↔agent protocol is three message types. Everything else is payload.
|
Code |
Name |
Direction |
Semantics |
|---|---|---|---|
|
HQ |
HITL Query |
agent → human |
"I need a decision before I proceed." Blocking - the agent pauses. |
|
HR |
HITL Response |
human → agent |
Approve / reject / edit / answer. The authorizing act. |
|
HI |
HITL Inform |
agent → human |
Status / progress / result. Non-blocking - no response expected. |
Three rules give the system its safety and its shape:
Because agents only ever speak HQ/HR/HI, any agent is replaceable by any other agent that speaks the same three words. That is what makes the swarm pluggable.
Three ideas do the heavy lifting:
diagram
|
Component |
One job |
Why it scales |
|---|---|---|
|
Messaging fabric |
Decouple every stage via separate topics (signal, hitl, execute, outcome). |
Producers/consumers come and go independently; the fabric is shared infra, not a component anyone owns. |
|
Watcher / Detector |
Perceive the world cheaply; raise a Signal when something is worth acting on. |
One thin watcher per source; a new source is a new watcher, nothing else changes. |
|
Synthesizer / Orchestrator |
Turn a Signal into a proposal; ask the human (HQ) only where judgment or authority is required. |
Generic and use-case-agnostic - it narrates and routes; the domain logic is a plug-in. |
|
Capability Catalog |
System of record for what the platform can perceive and do. Config only, off the data path. |
A new signal or action is a config entry, not code. Agents discover what they may do. |
|
Hub + Approval Guard |
The plane's edge onto the fabric: authenticate the human, validate the contract, validate the HR, fan out to surfaces. |
Stateless; scales horizontally. State lives in the Registry/Store. |
|
Router / Binding |
Deterministic reply-to-origin: every reply routes back to the session that raised it, by correlation. |
One reason to change; correlation makes routing O(1). |
|
Message + Session Store |
Append-only history of every thread - backlog and audit log. |
Durable "who decided what, when." Required for an agentic system. |
|
Executor |
Carry out the approved action through a typed capability, then report the result. |
Registers a capability; the proposer never names it. Add executors freely. |
|
Surface Adapter |
Normalize any surface (Forum now, Teams/email later) into HQ/HR/HI. |
New surface = one adapter; agents and fabric untouched. |
The key architectural stance: the control plane is a subscriber, not the bus. It listens on the hitl topic. It is never a central chokepoint, and it never owns the transport.
A notification is not the finish line. The system is only interesting because the loop closes: the human approves, an agent picks the work back up, it executes, the outcome is recorded, and that record makes the next decision better. Here is the whole life of one unit of work.
diagram
Read the lifecycle as a state machine on a single durable record - the WorkItem, keyed by correlation_id:
diagram
Two things in that picture are what separate a system from a demo:
Not every action deserves a human interrupt. Policy declares, per capability, whether the human's HR is required or whether the action is auto-approved (the human is informed via HI, never blocked). The safety invariant is enforced in one place: the "publish to execute" step accepts only approved or auto - a required capability can never slip through as auto.
diagram
Scale is the biggest problem in multi-agent systems, and it is a design problem, not a horsepower problem. The architecture buys scale through eight concrete properties.
diagram
Walking through the load-bearing ones:
The single most important implementation decision: three generic services are written once and never edited per use case. Everything a new scenario needs is authored config and small plug-ins that sit beside the engine, never inside it.
diagram
Onboarding a brand-new scenario is a recipe, not an architecture exercise:
diagram
The declarative contracts are the seam. A SignalType defines what can be perceived; a Capability defines a typed, versioned action with an input schema, a result schema, a risk level, and whether it needs approval; a WorkstreamPolicy scopes which signals and capabilities a workstream may use and which are auto-approvable. Agents discover their permitted menu from the Catalog at runtime - they do not hard-code it. The Catalog rejects any signal kind or capability that has no authored contract, so every message on the wire is guaranteed to have a schema.
Capability = {
capability_id: "orders.confirm" # stable routing key
version: "1.2.0" # semver of the contract
input_schema: JSONSchema # params validated at propose AND at execute
result_schema: JSONSchema # shape of the HI result
risk: "medium"
approval: "required" | "auto" # whether the HR gate applies
}
Because the routing key is the capability_id and the contract is versioned, you can evolve an action, add a second executor for it, or swap the implementation - all without touching the agent that proposes it.
A recurring, expensive mistake is asking a language model to do the entire pipeline. In practice a pipeline has three very different kinds of work, and each wants a different tool:
diagram
The empirical finding that forced this discipline: when a conversational data agent was asked to return a full dataset, it silently returned under 1% of the rows - ~30 of ~3,456 - with no warning that it had truncated, and each call took 75–90 seconds versus ~1 second for a direct query. This is not a bug you can prompt away; it is structural. A model answers by writing a message, and a message has a size limit. It summarizes; it does not transfer. And it is slow, because every answer runs through a large model.
So the platform draws a hard line:
Crucially, this is a policy per stage, not a religion. A future use case with a genuinely small dataset and a genuinely ambiguous decision can plug in "AI reasoning over a small dataset" at the matching stage - same pipeline, different plug-in. The architecture optimizes today's workload without locking out model-driven reasoning where it truly fits.
One subtlety that bites naïve "fan out to a thousand agents" designs: when proposals compete for a shared, finite resource (the same aged unit of inventory, the same budget, the same rate limit), the allocation step is a contention problem and must be computed centrally and deterministically before any fan-out. If you fan out per-item, independent agents double-claim the same resource. So the pattern is: allocate centrally, then fan out for per-item enrichment / narration. The fan-out is for language and judgment on already-allocated items - never for the allocation itself.
"The agent is also learning" deserves precision, because there are two very different things people mean by it, and this architecture supports the durable one first.
1. Operational memory (built-in, durable). Every recommendation is persisted as a WorkItem keyed by correlation_id, carrying the proposal, the provenance, and an append-only history of status changes (who approved, when, what the outcome was). This unlocks three concrete behaviors:
2. Policy / model adaptation (the seam is already there). The outcome topic feeds results back to the Synthesizer as context. Today that closes the operational loop (de-dup, tracking); tomorrow the same feedback stream is the input to continuous evaluation and prompt/model optimization. The important architectural point is that learning is not bolted on - the closed loop produces the data that learning consumes, by construction.
diagram
The persistence backend is itself pluggable and policy-gated: a zero-setup local file store for a POC, swapped for a managed database in production without touching the Synthesizer or any builder. When the feature flag is off, nothing is read or written - the loop degrades gracefully to stateless.
An agentic system that can do things needs its safety story front and center.
To make the "config, not code" claim tangible, here is everything you touch to add a brand-new capability - say, an incident-response agent that rotates an exposed secret:
Nothing in the Watcher shell, transport, Synthesizer, Notification service, or Store changes. The new agent speaks HQ/HR/HI, registers its capability, and joins the fabric. The swarm grew by one, and the existing agents didn't notice.
That the same skeleton runs completely unrelated domains is the proof of generality: in the source system, this identical engine drives a supply-chain remediation workstream and a work-item triage agent that supervises a separate always-on triage system - the second use case reuses the entire engine and only authored its own bindings, capabilities, and executor shim.
The bottleneck in multi-agent systems is not model quality - it is coordination, authority, and scale. This architecture answers all three with a small set of durable choices: a shared pub/sub fabric nobody owns, a frozen generic engine driven entirely by a declarative Capability Catalog, routing by capability instead of by address, a three-word human protocol (HQ/HR/HI) that makes agents interchangeable, a priority lane that keeps the human's approval fast under load, and a closed loop whose append-only record is the memory the system learns from. The result is a swarm that a single operator can safely supervise, and that grows by adding threads, topics, and config - not by re-plumbing the system every time an agent joins.
The deepest idea is also the simplest: write the hard part once, and make everything after it a configuration change. Do that, and the marginal cost of the next agent trends toward zero - which is the only definition of "scales" that actually matters.
SessionKey = { session_id, correlation_id, workstream, thread_id, owner }Signal = { signal_id, ts, kind, severity(hot|med|cold), source, payload, classification }SignalType = { kind, version, payload_schema, default_severity, classification }Capability = { capability_id, version, title, input_schema, result_schema, risk, approval }Policy = { workstream, version, signals[], capabilities[], auto_approve[], owners[] }Binding = { capability_id, handler, subscribe:"execute", filter, reply_to:"hitl" }WatcherBinding = { watcher, emits[], publish:"signal" }WorkItem = { correlation_id, workstream, signal_id, capability_id,status(open|approved|executed|rejected), headline, proposals[], provenance, history[] }
|
Topic |
Publishers |
Subscribers |
Carries |
|---|---|---|---|
|
signal |
Watchers |
Synthesizer |
Contract-validated signals (optionally a pre-computed proposal) |
|
hitl |
Synthesizer (HQ/HI), Human (HR), Executor (HI) |
Hub, Synthesizer (HR), surfaces |
The human-in-the-loop exchange; HR on a priority lane |
|
execute |
Synthesizer (after approve/auto) |
Executors (by capability) |
Approved Action Units |
|
outcome |
Executors |
Store, Synthesizer (feedback) |
Results, audit trail, context for future decisions |
Microsoft Agent Framework Workflows - Human-in-the-loop (HITL)
This article was published under HackerNoon's Business Blogging program.