Technical design
This is the internals paper: one decide contract, two Front Door fleets at scale, three principals. If you want the story, start at how it hangs together or the idea. APIs and failure modes live on the component pages.
How a request is split. Agent Front Door calls the Agent Data Plane to classify. The Agent Control Plane is the sibling control box. Front Door pins route_id + versions and async-starts Agent Runtime.
How ingress is split. One contract (entitle, classify, freeze, start). Two fleets at production scale: Chat Front Door and API Front Door. Kafka {runs_topic} is Runtime return, not a third door. Locally, chat and jobs share one Front Door process — same contract.
| Job | Component |
|---|---|
| Front door | AFD — entitles, freezes, starts. Only way in. |
| Intent router | Agent Plane — AFD calls Agent Data Plane (catalogue + classify). ACP is the control box. Does not do the work. |
| Agentic app | AR — specialised agents underneath. Pin, hydrate, LangGraph loop. |
| Memory / RAG / tools | Shared — called from AR. Not inside AR. |
| Tool manifests | Agent Capability Plane — sits with Agent Plane. Publishers append id@version. AR hydrates at pin. Agent-start invoke is API AFD. |
Running locally (v1)
Seven processes prove the fabric chain. Channels talk only to Front Door.
| Box | Job |
|---|---|
| Agent Front Door | Chat /v1/assistant/* and jobs /v1/jobs* on one process |
| Agent Control Plane | Catalogue UI only — no database, no decide API |
| Agent Data Plane | Catalogue + classify (POST /v1/intent/decide) |
| Agent Runtime | Pin, hydrate, LangGraph (Patterns 0–3) |
| Agent Capability Plane | Published capabilities; hydrate once at pin |
| Agent Audit Data Plane (AADP) | Append-only evidence ingest + query |
| Agent Audit Control Plane (AACP) | Ops UI; queries AADP only |
Four autonomy patterns (autonomy_mode 0–3). Pin → hydrate → graph is wired. Seed packs live under agent-fabric-scripts/stack/route/.
v1 deferrals (production target, not local yet): second AFD fleet (FR-9), decision audit store, Layer ③ LLM classifier, real Kafka / IdP / dual-check PEP, Shared Memory and long-term retrieve.
Intent lock: enterprise-agent-fabric-v1.md.
1. Job of the system
Two AFD fleets (production) and one Agent Plane per trust domain. Every turn is entitled, decided, and (on route) async-started into AR. Users do not pick chatbots. Channels never dial Legal or payments URLs.
Today (typical enterprise): users open Finance, then HR, then Legal chatbots. Partner APIs invent dispatch. Not everyone should see Legal.
Target: Chat/UI hits the Chat AFD. Partners, systems, and Kafka job starts hit the API AFD. That AFD calls the Agent Data Plane to decide which governed path owns this turn. On route, the AFD pins it and starts AR.
Agent Data Plane classifies against the route/workflow catalogue. ACP is the control box in the same Agent Plane. The Agent Capability Plane sits next to those two boxes. AR does the work (Memory, RAG, Tools via Shared). AFD freezes and starts. The two AFDs share that contract and that Agent Plane. They do not share a process.
| With one shared Agent Plane | Without |
|---|---|
| One ingress decision for a shared experience | Each UI or if/else guesses the agent |
| Two AFD fleets, one contract | Chat SSE and jobs share a process, or Kafka is a third start |
| One versioned route/workflow catalogue + audit | Fragmented tables, inconsistent entitlements |
| One eval surface for misroutes | Cannot gate routing quality in CI |
| Clarify / abstain / safety in one place | Side doors into high-risk agents |
Scope: one Agent Plane per shared UX / trust domain. Not one mega-agent. Routes stay specialised. AFDs are separate fleets so chat sockets cannot take down job start, and the reverse.
| Audience | Cares about |
|---|---|
| End users | One assistant, slim replies, no routing internals |
| Platform | Chat AFD + API AFD (pin + start, separate fleets), Agent Plane (ACP + data plane), Shared, eval CI |
| Domain teams | Route rows and agent behaviour behind activation_target |
2. Functional requirements
Testable statements for this trust domain. Classifier layers, JSON bodies, and field dictionaries live in playbooks. Channel and internal APIs: Front Door · Agent Plane · Runtime.
| ID | Requirement |
|---|---|
| FR-1 | Two AFDs, one contract. Only Chat AFD and API AFD call decide. Kafka ingest is the API AFD. {runs_topic} is AR return, not a start. AR may GET the catalogue row at the pinned version. Agent Plane and AR are not public URLs. |
| FR-2 | Decide = active table ∩ required_claims ∩ channel. Explicit route_id still looks up, entitles, and records. |
| FR-3 | Outcomes: route, clarify, abstain. Only route starts AR. Jobs with no user fail closed or escalate_human. |
| FR-4 | On route, AFD pins route_id + route_table_version + agent_client_id, async-starts AR, returns 202 + correlation_id. AR fetches the catalogue row at that version from the Agent Data Plane, copies the freeze, then requests a token for that agent_client_id (FR-10). Nobody re-reads active. Clarify does not pin. |
| FR-5 | Chat/UI never sees route_id, run_id, agent_client_id, confidence, or router_layer. Decision record is server-side. |
| FR-6 | Empty-state chips are eligible + chat-visible only. A tap is Layer ①, not a side door. No start until outcome=route. |
| FR-7 | Generic chat still classifies. Jobs name route_id and skip classifier + clarify, not Plane ①. |
| FR-8 | Channels never dial activation_target. UI, jobs caller, and model never pin. |
| FR-9 | Chat AFD and API AFD are separate fleets (process, autoscaler, failure domain). They share Agent Plane (ACP + data plane), frozen-route/session store, and start contract. |
| FR-10 | Three principals. User (who is asking), agent (which software is acting), AR workload (which box runs the loop). Catalogue row carries agent_client_id. On route, AFD freezes that pointer. AR requests a token for that client, downscoped to this route. Tool calls use that bearer plus user claims (or a stored approval). AR workload identity is not the downstream caller. Shared AR compute is allowed; one agent bearer for every route is not. |
Out of scope on this page: Java hexagonal ports, Temporal plugin choices, model-gateway product pick. Those are later ADRs or playbooks.
3. Non-functional requirements
Decide SLO and run SLO are different numbers. An AFD module may sit next to Agent Plane while the domain is small. The two AFDs may not: UI BFF and jobs must not share a process (FR-9). Decide must not wait on a 12-step loop. Chat sockets must not wait on job bursts.
| Concern | Decide path (AFD + Agent Plane) | Run path (AR) |
|---|---|---|
| Latency | p99 entitle + classify + pin + start-ack. Layer ② <50ms. Decide+pin+202 p99 <100ms. Pattern 0 is the only sync wait. | Loop may take seconds to minutes. Not on the decide clock. |
| Availability | ACP accepts turns if one AR is down. One AFD down does not take the other (FR-9). | Controlled error after 202. Not a fake success. |
| Correctness | Adversarial routing cases pass at 100% before release. Golden set in CI. | PEP, pattern budgets, tool allowlists (not this page). |
| Durability | Decision record is async. Decide does not wait for audit commit. | Run pin is durable. |
| Security | No public Agent Plane URL. Empty eligible set: do not start. Fail closed. | Tools and PEP run only after the freeze. Dual check: user + agent. Shared is not a side door. AR workload identity is not the API caller. |
| Consistency | AFD freeze: route_id + versions + activation_target + agent_client_id + correlation_id. | AR copies that freeze. Pin miss: AR API, not AR DB. |
Capacity assumptions (this domain, for review)
Tune per trust domain.
| Assumption | Working target |
|---|---|
| Route/workflow catalogue | 5–8 chat-visible routes plus a few event-only rows (local seed: 32 proof routes) |
| Peak decide | Hundreds of turns/s mixed chat + jobs, not millions |
| Layer ③ | Rare. If saturated, degrade to clarify/abstain, not a slower ACP |
| Route pin TTL | 30–60 min on AFD, keyed by session_id |
| AR instances | Per activation_target, independent of Agent Plane |
How those numbers become fleets: Front Door scaling · Agent Plane scaling · Runtime scaling.
4. High-level architecture
Enterprise Agent Fabric is the outer box. Identity & Security and Quality & Evals run as bands down the left. Events sits at the top. Agent Front Door, Agent Control Plane, and Agent Runtime emit into Events. Channels are on the left and reach nothing but the Front Door. The middle column is the Agent Plane with the Agent Front Door beneath it; Agent Runtime and Shared (Memory, RAG, Model, Tools) sit to the right. Do not collapse them. Inside the Agent Plane, the Agent Control Plane sits on top and the Agent Data Plane and Agent Capability Plane sit side by side beneath it, over one Postgres instance holding both the catalogue (adp) and the registry (acr). A small database mark on Front Door and Runtime means each owns its own store (frozen route/session, runs). AFD is two fleets with one contract (Chat BFF and API, including Kafka ingest). Observability and Agent audit sit side by side. Both feed Dashboard. Dashboard feeds Responsible / Governed AI.
Agent Front Door calls the Agent Data Plane to classify. The data plane does not call or start AR. After outcome=route, the Front Door pins the frozen route/session (route_id + versions), then async-starts Agent Runtime on a straight start. AR fetches the catalogue row and manifest pointers from the Agent Data Plane at that pinned version, hydrates each capability from the Agent Capability Plane (same Agent Plane band, not Shared), copies the run pin, and returns 202 plus correlation_id to the Front Door. Memory, RAG, Model, and Tools sit in Shared.
Child pages: Agent Front Door · Agent Plane · Agent Runtime · Agent Capability Plane · Agent Audit.
Roles and responsibilities
| Component | Owns | Does not own | Scale unit | If it dies |
|---|---|---|---|---|
| Frontends | Chat/UI, chips, slim replies | Route table, tools, PEP | Per channel app | That UI down; API AFD still works |
| Agent security | User, agent, and AR workload identity. Least privilege, mint after pin, revoke, no side doors | Classify, pin, loop | Platform | Fabric can run; not governed |
| Agent evals | Routing golden set, adversarial cases, CI gate | Pin, start, loop | Platform | Fabric can run; cannot gate misroutes |
| Chat AFD | Auth, session, hints, SSE, freeze, start | Classify, jobs OpenAPI, Kafka job start | Connections + turn RPS | Chat down; jobs still work |
| API AFD | Partner auth, {jobs_url}, Kafka ingest, freeze, start | Classify, clarify UX, SSE | Job POST RPS + consume lag | Jobs down; chat still works |
| Agent Control Plane (ACP) | Policy, versions, decision audit (target) | Pin, start, loop, AFD hot-path classify | Stateless replicas / UI | Audit lags; in-flight runs continue |
| Agent Data Plane | Eligible set, classify, route/workflow catalogue. Called by AFD (classify) and AR (row at pinned version) | Pin, start, loop | Versioned table + decide replicas | Classify fails closed |
| Decision audit | Async router_layer, outcome, eligible_routes (target) | Chat transcript | Independent queue | Decide still succeeds |
| AR | Run pin, Pattern 0–3, LangGraph loop. Fetches catalogue row at pinned version. Requests route-scoped agent token after freeze | Next-utterance classify, active lookup, Shared stores, agent IdP registration, dual check | Per activation_target | Controlled error after 202 |
| Shared | Memory, RAG, Tools | Route choice, pin, start | Platform | Runs degrade; routing still works |
| Observability | Traces, metrics, logs from AFD, Agent Plane, AR, Shared | Route choice, evidence chain | Platform | Runs continue; you cannot see them |
| Dashboard | Operator view of Observability and Agent audit | Pin, start, classify | Platform | Operators blind; fabric still runs |
| Responsible / Governed AI | Policy, model risk, exam evidence | Pin, start, classify | Platform / risk | Runs continue; cannot evidence them |
| Memory (in Shared) | Short / long term via session_id | Ingress classify | Platform | Runs degrade; routing still works |
| RAG (in Shared) | Corpus retrieve | Ingress classify | Platform | Grounding degrades; routing still works |
| Tools / PEP (in Shared) | Actions scoped by pinned manifest. Dual check: user entitlements (or approval) and agent scopes | Route choice, mint | With AR | Side effects stop |
| Agent Capability Plane (with Agent Plane) | Immutable id@version. Domain invoke and agent-start invoke. AR hydrates at pin | Pin, start, PEP permit | Platform | New runs cannot hydrate; in-flight pins still have schemas |
| Agent Audit (AADP + AACP) | Append-only fabric evidence chain. Async emit from AFD, ADP, AR, ACR. Ops query by correlation_id | Decide, pin, start, loop | AADP ingest + query | New events may drop (fail-open); in-flight runs continue |
| Model router | Which LLM. Called by AR | Which route_id | LLM gateway | Inference fails; decide unaffected |
Three routers (do not collapse)
| Router | Question | Who calls it |
|---|---|---|
| Agent Data Plane | Which workflow / manifest / app? | Agent Front Door (classify). AR (catalogue row at pinned version, not classify) |
| Agent planner (inside AR) | Which tool / step next? | AR (Pattern 1 LLM loop or Pattern 2/3 workflow) |
| Model router | Which LLM endpoint for this inference? | AR |
Autonomy patterns (inside AR)
autonomy_mode answers who picks the next step inside AR — not how the run is started. Full contracts: autonomy.
| Mode | Name | Who decides |
|---|---|---|
| 0 | Single inference | Nobody — one LLM call |
| 1 | Autonomous | LLM CALL / DONE over a manifest |
| 2 | Deterministic | Workflow designer — fixed stages, branch, human_gate |
| 3 | Guided | Designer stages; LLM inside an allowlist |
Orthogonal attachments — retrieval, memory, kind=agent child jobs, checkpoint resume — compose beside the mode. They are not a fifth pattern.
Human and specialist composition
Three human shapes and one specialist shape are first-class in the catalogue:
| Shape | Mechanism | Run behaviour |
|---|---|---|
| Process gate | Workflow human_gate | Run pauses (waiting); resume via /turns with approval packet |
| Async handoff | Domain tool escalate_to_human | Ticket opened; parent completes when handoff succeeds |
| LLM signal | Structured human_review_* in notes | No pause — evidence for a later branch |
| Specialist child | kind=agent capability | Parent AR POSTs API AFD /v1/jobs; optional join waits for child result |
Detail: generic shapes · escalate-to-human · process gate.
Data: route pin vs run pin
Two records. Field dictionary: playbooks session custody.
| Record | Who writes it | Lives where | Job |
|---|---|---|---|
| Frozen route/session | Agent Front Door (AFD) | AFD store, keyed by session_id or job:{idempotency_key}, TTL | Pinned route (route_id, versions, activation_target, agent_client_id, correlation_id) |
| Decision | Agent Control Plane (ACP) | Audit store (target) | Why this turn routed, clarified, or abstained |
| Route/workflow catalogue | Platform / domain teams | Agent Data Plane | Versioned rows the data plane classifies over. Includes agent_client_id per row. |
| Run pin | AR | Durable run store | Copy of those versions plus loop checkpoint, token reference, run_id |
The AFD stores the frozen route/session so "yes" or a jobs retry keeps the same route_id and versions. Pin miss: AR API, not AR Postgres. session_id is the chat thread.
Three principals (do not collapse)
AR is the executor. It is not the agent. Shared AR is a compute choice. Identity lives on the pinned route.
| Principal | Answers | Issued / held | On the wire |
|---|---|---|---|
| User | Who is asking? | AFD: IdP token, sub, emts | Claims or approval id. Not the tool-call bearer. |
| Agent | Which software is acting? | Catalogue agent_client_id (e.g. payments-agent-v2). After freeze: IdP token for that client, downscoped to this route. | Authorization bearer on Shared Tools calls |
| AR workload | Which box is running the loop? | Platform: pod identity to Shared, IdP, model router | Workload auth to fabric internals. Not a downstream API principal. |
activation_target isolates the box (PCI, release, network). agent_client_id names the robot. Omit activation_target and routes share AR compute. They do not share one agent bearer. A new use case is a new catalogue row and a new agent_client_id, not a new AR.
Mint order: pin first, then agent token. Route change mid-session: new downscoped token. Do not widen the existing one. Jobs use the same robot badge; only how you carry the user changes (live claims vs stored approval). Dual check is Shared Tools / PEP: may this person, and may this robot.
Concepts write-up: identities. Authorization that hangs off user and agent: claims and scopes.
5. Request path
The Agent Data Plane classifies. Agent Front Door calls it, then freezes and starts. AR fetches the row at the pin, hydrates, then runs. 202 comes from AR, not from the data plane.
Chat / generic turn
- AFD authenticates. Mint or reuse
session_id. - Frozen route/session + continuation (
"yes","$500"): skip classify, resume AR. - Else Agent Front Door calls the Agent Data Plane (message + claims +
session_id). - Data plane: eligible = catalogue ∩ claims ∩ channel. Returns
route/clarify/abstain. Writes the decision record (target). Does not pin or start. clarify/abstain: talk to the user (jobs: fail closed orescalate_human). No AR. After a pick, back to step 3.route: AFD stores the freeze (route_id+ versions +activation_target+agent_client_id),POSTs AR.- AR fetches the catalogue row and manifest pointers from the Agent Data Plane at that pinned version, hydrates from Registry, copies the run pin, requests a token for the pinned
agent_client_id, returns202 { "correlation_id" }. Nobody re-readsactive. - AFD returns slim UI payload, or
202to the jobs caller. Shared Tools / PEP dual-checks user + agent on every call.
API AFD: caller already named route_id. Skip classifier and clarify. Still entitle and record (step 4), then freeze + start (steps 6–8). {runs_topic} is not this path.
Channel APIs and swim lanes: Front Door. Decide JSON: Agent Plane. Start / 202 / {runs_topic}: Runtime.
6. Isolation and shed
| Failure | Agent Front Door (AFD) | Agent Control Plane (ACP) |
|---|---|---|
| One AR down | Other activation_targets still start. That route returns a controlled error after 202 or on start-ack timeout. | Still accepts new turns. |
| One AFD down | The other ingress still works. | Unaffected. |
| Frozen-route/session store down | Continuations degrade to open-run on AR. New route can still start if start-ack works. | Unaffected. |
| Catalogue unreadable | Do not start. | Fail closed. |
| Shared down | Routing still works. AR degrades (no memory / RAG / tools). | Unaffected. |
| Layer ③ saturated | Serve clarify / abstain. | Do not enqueue behind Layer ②. |
| Audit lag | Decide still succeeds. | Exam trail lags. |
Anti-patterns
- Freeze in replica memory (the next
"yes"lands on another replica and misses the pin). - One autoscaler for AFD + Agent Plane + AR workers.
- One process or autoscaler for UI BFF and jobs URL (FR-9) in production.
- Public Agent Plane URL (then you scale auth, OpenAPI, and slim UX into Plane ①).
- Kafka ingest as a third public ingress, or
{runs_topic}as a job start. - Caller waits for the memo (decide SLO dies).
- Scaling Layer ② by adding Layer ③ replicas.
- Putting event-only rows on chat chips to "reduce classify load" (that is a side door, not a scale tactic).
- Re-reading
activeafter pin (wrong, and it also adds origin QPS). - AR
POSTs another AR'sactivation_target(A2A skips API AFD). - AR workload identity as
Authorizationto downstream APIs (AR becomes a fatsvc-agents). - One agent token at AR boot with every route's scopes.
- A new
activation_targetper route only to get a different identity (that inflates fleets to fix a token problem). - Deriving
agent_client_idfromroute_idinstead of pinning an explicit pointer. - The user bearer as the tool-call caller, or skipping the agent check because AFD already entitled the user.
7. What examiners and operators should be able to show
- Which
route_table_versionwas active for this session. - Which routes were eligible for this identity (and which were hinted).
- Why this turn routed, clarified, or abstained (
router_layer, confidence). - That adversarial routing cases pass at 100% before release.
- That a routing incident was added to the golden set and blocked in CI.
- That jobs HTTP and Kafka ingest still have a Plane ① decision record (explicit
route_idis not a skip). - Which
agent_client_idwas frozen for this run, which agent called this tool, and that user and agent both passed (FR-10). - Which human shape fired — gate, handoff, or signal — and whether the run paused or completed.
8. Design review checklist
- Boxes distinct: AFD, Agent Plane (ACP on top, Data Plane and Agent Capability Plane side by side over one Postgres), AR, Shared (Memory, RAG, Model, Tools). Small database mark inside Front Door and Runtime. Identity & Security and Quality & Evals as bands on the left. Events at the top. Observability and Agent audit side by side → Dashboard → Responsible / Governed AI
- Two AFD fleets, one contract, one Agent Plane, separate process (FR-9) — local v1 may colocate until scale demands split
-
{runs_topic}is AR return; Kafka job start is the API AFD - Jobs name
route_idand still entitle; generic chat still classifies - Clarify / abstain never start AR. Default start is async
- UI never sees
route_id/run_id/agent_client_id. Route pin ≠ run pin. Nobody re-readsactive - Decide SLO ≠ run SLO. Frozen-route/session store is shared KV. Agent Plane and AR are not public URLs
- User ≠ agent ≠ AR workload. Catalogue
agent_client_idon the freeze. Dual check in Shared. Shared AR does not share one agent bearer (FR-10) - Human gate, async handoff, and
kind=agentchild are catalogue shapes — not side doors
Non-goals
- One mega-agent with every tool.
- One AR bearer for every route (shared compute is not a shared robot).
- The user token as the tool-call caller.
- One process or autoscaler for chat SSE and jobs start in production (FR-9).
- Browser, partner, or domain MS calling the Agent Plane or AR directly.
- Kafka as a third public ingress into the Agent Control Plane.
{runs_topic}as a job start.- ACP waiting on Temporal / the 12-step loop.
- Event-only routes shown as chat chips.
- Entitlements stored per user in the route table (use ingress claims).
- Replacing published playbooks with this local page.
Appendix: decisions locked
FRs above are the testable form.
- Four jobs. AFD (entitle, pin, start), Agent Data Plane (classify + catalogue; called by AFD), ACP (control + audit), AR (run pin + LangGraph loop; requests agent token). Pin sits on AFD. Agent Capability Plane sits with Agent Plane. Memory, RAG, and Tools sit in Shared.
- Two AFD fleets, one contract. Separate process (FR-9). Shared Agent Plane.
{runs_topic}is return. Kafka job start is the API AFD. - Jobs name the route. Skip classifier and clarify. Do not skip Plane ①.
- Generic chat still classifies. Hints are eligible + chat-visible. A tap is Layer ①.
- Clarify / abstain never start AR. On
route: freeze, async-start,202. - One Agent Plane per trust domain. Control Plane and Data Plane stay separate boxes. Not one mega-agent.
- Three principals. User, agent, AR workload. Catalogue
agent_client_idon the freeze. Pin first, then mint. Dual check in Shared Tools / PEP. Shared AR is compute, not identity (FR-10). - Capabilities are references. Publishers append
id@versionnext to Control Plane and Data Plane. Manifests refer. AR hydrates the whole pinned manifest before the LLM.kind=agentinvokeis API AFD jobs, not the callee AR.
Read next
Agent Front Door · Agent Plane · Agent Runtime · Agent Capability Plane · Agent Audit · Patterns