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.

1. The problem nobody puts on the slide

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:

  • Authority is the hard part, not intelligence. An agent that can actis a liability unless a human can approve, deny, or steerbeforethe action lands. But a naïve approval step turns your autonomous system back into a ticket queue.
  • Agents multiply, and every pair is a potential integration. N agents that call each other directly is an O(N2) wiring problem. Add the sixth agent and you are editing the other five.
  • One human, many agents. The economics only work if a single operator can supervise dozens of concurrent agent sessions. That is a user-experience and routingproblem as much as an AI one.
  • Models are the wrong tool for half the pipeline. Language models are superb at judgment and explanation and terribleat moving bulk data or doing exact arithmetic. Ask them to do the whole job and they silently return 1% of the data and call it done.
  • A one-shot recommendation is not a system. The value is in the loop: the agent proposes, the human decides, something executes, the result is recorded, and the next proposal is smarter because of it. Cut the loop after "send a notification" and you have built a very expensive alert.

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.

2. The mental model: a forum, not a chatbot

The user-facing metaphor is a forum, not a chat window. This is not cosmetic - it is the scaling primitive.

  • A Workstream is a domain of related work (e.g. incident-response,supply remediation,content moderation). It has an owner and a policy.
  • A Thread is one correlated unit of work. Every agent that touches that unit - the one that detected it, the one that proposed an action, the one that executed it - posts into the samethread, because they all share one correlation id.
  • A Message is one of three types in a tiny, fixed vocabulary.

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.

2.1 The three-word language every agent speaks

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:

  • HQ blocks. An agent that asks a question does not act until the answer arrives.
  • HR is authoritative. When the human responds, the agent mustrespect it - approve routes straight to execution with no re-reasoning;updatere-opens the proposal;rejectdead-ends.
  • HI never asks for anything. It keeps the human in context - "I'm working," "here's the result" - without demanding attention.

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.

3. Architecture at a glance

Three ideas do the heavy lifting:

  • A shared pub/sub fabric that nobody owns. Agents and the control plane are all just publishers and subscribers on a set of topics. No agent calls another agent. No agent calls the control plane. Adding or removing an agent re-wires nothing.
  • A frozen generic engine + a Capability Catalog. Three small services (Watcher, Synthesizer, Notification/Hub) never change. What the platform can perceiveanddolives in declarative config - the Catalog - loaded at deploy.
  • Routing by capability, not by address. Work is carried as an Action Unit tagged with a capability_id. Executors registerwhich capability they serve. The proposer publishes an approved Action Unit keyed by capability and never names an executor.

3.1 The components, and why each exists

| 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 | One thin watcher per source; a new source is a new watcher, nothing else changes. |
| Synthesizer / Orchestrator | Turn a Signal into a | Generic and use-case-agnostic - it narrates and routes; the domain logic is a plug-in. |
| Capability Catalog | System of record for | A new signal or action is a config entry, not code. Agents |
| 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 | Durable "who decided what, when." Required for an agentic system. |
| Executor | Carry out the approved action through a | 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.

4. The full closed loop - the part everyone forgets

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.

Read the lifecycle as a state machine on a single durable record - the WorkItem, keyed by correlation_id:

Two things in that picture are what separate a system from a demo:

  • update (re-synthesis). The human doesn't just approve or reject - they can steer: "no, split the first part out and merge the rest." The Synthesizer re-reasons and emits a fresh proposal on the same thread (same correlation_id, new action_id). The human approves thefinalshape. Re-synthesis never bypasses the approval gate.
  • The outcome loop. Execution results land on an outcome topic that (a) writes the durable audit record and (b) feeds back to the Synthesizer as context for future decisions. This is where the system starts to "learn" - not by retraining a model in the loop, but by accumulating a ground-truth record of what was proposed, what was approved, and what actually happened.

4.1 Two approval modes, one gate

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.

5. Why it scales - the properties that matter

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.

Walking through the load-bearing ones:

  • O(N), not O(N2) integration. Because no agent calls another, adding the k-th agent costs one subscription, notk−1new integrations. The wiring cost of the swarm is linear in agents andzeroin existing agents.
  • Capability routing decouples proposer from executor. The Synthesizer publishes "do orders.confirm with these params." Someexecutor that registered orders.confirm picks it up. You can add a second executor, replace one, or shard them - the proposer is oblivious. This is the agent-world equivalent of a service mesh routing by capability rather than by host.
  • Correlation-bound threads bound the human's cognitive load. The operator never reads a 1,000-message mega-conversation. They read one thread- one unit of work with its full, self-contained history - then move to the next. The system scales by fanning out threads across workstreams, each independently ordered. This is the difference between "make the chatbot smarter" (doesn't scale) and "give the human a triage queue of self-contained units" (scales to as many operators × threads as you can staff).
  • The HR priority lane keeps approvals fast under load. The hitl topic groups traffic by correlation andruns the human's approval on a preemptive lane. An approval is never stuck behind a busy session's chatter. Latency of the human decision is decoupled from throughput of the agents.
  • The control plane is a subscriber, not the bus. Nothing funnels through a single owner of the transport. The plane can subscribe to more topics later; the fabric scales out underneath everyone. There is no central component whose throughput caps the system.
  • Config-driven onboarding makes the marginaluse case nearly free. This is the one that compounds. Adding the tenth workstream is not a project - it is a handful of declarative files plus, at most, one plug-in. The engineering effort to add capabilities growssub-linearlyin the number of use cases, because the expensive part (the engine) is written exactly once.

6. The engine is frozen; the behavior is config

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.

Onboarding a brand-new scenario is a recipe, not an architecture exercise:

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.

7. Right tool for each stage - the discipline that keeps it fast and correct

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:

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:

  • Bulk data movement → direct queries. No model in the loop. Complete, fast, exact.
  • Matching / decision arithmetic → deterministic code. The rules are fixed ("if it's a child, find its group, find the parent, check availability, take the smaller quantity"). That is spreadsheet work, not judgment work. It must be exact and it must see allthe data.
  • Language and judgment → the AI agent. Take the short, clean list of candidate actions and turn it into a human-readable recommendation, decide confirm vs. escalate, and answer the approver's follow-up questions. Tiny input, real judgment, no truncation risk.

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.

7.1 Where allocation must stay central

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.

8. Closing the loop: state, memory, and "learning"

"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:
  • De-duplication across cycles. Before proposing, a builder reads the still-open work items and avoids re-recommendingsomething already proposed but not yet adopted - instead annotating it "already recommended, awaiting action." The system doesn't nag.
  • Cumulative tracking. Because outcomes are recorded, the system can report running totals - what has been proposed, approved, and executed over time - not just today's snapshot.
  • A ground-truth training set, for free. The Store accumulates (context, proposal, human decision, actual outcome) tuples. That is exactly the data you need to later evaluate the agent, tune its prompts, or fine-tune a model - curated from real traces, not synthetic.
  1. 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.

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.

9. Trust, safety, and governance - because the agents can act

An agentic system that can do things needs its safety story front and center.

  • No authority without approval. The security-critical event is the HR approve. The Approval Guard authenticates it, attributes it to a specific human, records it immutably, and only then publishes it on the priority lane. Nothing executes without it. Agents hold nostanding authority.
  • The Catalog is the control point. Every Action Unit is validated against the Catalog - capability exists, params match the schema, the workstream policy permits it, the approval mode is honored. Changes to the Catalog are governed by review and policy owners. Integrity of the config isthe integrity of the system.
  • Everything is auditable by construction. One correlation_id threads the entire lifecycle - signal → proposal → approval → execution → outcome. A single query reconstructs "who decided what, when, and what happened." The append-only Store is the system of record.
  • Least privilege at the edges. Executors run with narrowly-scoped workload identity per capability. Data egress has exactly two sanctioned points - to the model endpoint (context for synthesis) and to a surface (a pointer/deep-link, not the payload) - and both are logged. No secrets ever live in agent code or a surface; they come from a secrets store via managed identity.
  • Typed capabilities, not raw shell. An executor exposes a typed, versioned contract with input and result schemas - never "run this arbitrary command." The blast radius of any action is bounded by its schema.

10. Onboarding a new agent, end to end (a concrete walk-through)

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:

  • signals/secret.exposed.yaml - declare the signal kind and its payload schema.
  • capabilities/secrets.rotate.yaml - declare the typed action: input schema, result schema, risk: high, approval: required.
  • policies/incident-response.yaml - wire the workstream: which agent, which signals it may act on, which capabilities are permitted, which (if any) are auto-approvable.
  • A proposal_builder - the domain logic that turns a signal into a concrete proposal (deterministic where it can be; that is one small class).
  • A thin Watcher - a cheap detector for the new signal source, plus one line in the registry.
  • A narrate-only agent - instructions that take the pre-computed proposal and produce the human-facing recommendation.
  • A surface template - how the HQ renders for the approver.

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.

11. Lessons learned (the stuff you only get from building it)

  • Design for the second agent, not the first. The first agent is easy and misleading. Every decision - pub/sub over direct calls, capability routing over addressing, config over code - pays off only when the second, fifth, and tenth agents arrive. If your architecture doesn't get cheaper per agent, it doesn't scale.
  • The human is a scaling dimension, not a speed bump. Treat "one human supervising many agents" as a first-class routing/UX problem - self-contained threads, a priority lane for approvals, a "needs me now" inbox. Get this wrong and your autonomous system is a ticket queue with extra steps.
  • Don't ask a model to move data. Language models summarize; they do not transfer. Put exact, bulk, and arithmetic work in code and reserve the model for language and judgment. Measure the truncation - it is silent and it will fool you.
  • Central allocation before fan-out. Shared-resource contention cannot be parallelized naïvely. Allocate deterministically and centrally, then fan out for enrichment.
  • Close the loop or you built an alert. Notification is the middleof the story. The value is in execution, the durable record, and the feedback that makes the next decision better.
  • Freeze the engine early. The moment the three services stop changing per use case, onboarding becomes a config exercise and your risk per new capability collapses to "a reviewed YAML file."

12. Conclusion

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.

Appendix A - Message contracts (reference)

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[] }

  • session_id is per-agent-run; correlation_id is the unit of work that binds separate agent sessions into one thread. One thread == one correlation_id, notone session.
  • signal_id is self-minted by the Watcher and carried forward unchanged as ActionUnit.provenance.signal_id - that is the trace continuity across the whole loop.

Appendix B - Topic map

| 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 |

Reference(s):

Microsoft Agent Framework Workflows - Human-in-the-loop (HITL)