Skip to main content

Technical design

You are in the deep end

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.

JobComponent
Front doorAFD — entitles, freezes, starts. Only way in.
Intent routerAgent Plane — AFD calls Agent Data Plane (catalogue + classify). ACP is the control box. Does not do the work.
Agentic appAR — specialised agents underneath. Pin, hydrate, LangGraph loop.
Memory / RAG / toolsShared — called from AR. Not inside AR.
Tool manifestsAgent 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.

BoxJob
Agent Front DoorChat /v1/assistant/* and jobs /v1/jobs* on one process
Agent Control PlaneCatalogue UI only — no database, no decide API
Agent Data PlaneCatalogue + classify (POST /v1/intent/decide)
Agent RuntimePin, hydrate, LangGraph (Patterns 0–3)
Agent Capability PlanePublished 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 PlaneWithout
One ingress decision for a shared experienceEach UI or if/else guesses the agent
Two AFD fleets, one contractChat SSE and jobs share a process, or Kafka is a third start
One versioned route/workflow catalogue + auditFragmented tables, inconsistent entitlements
One eval surface for misroutesCannot gate routing quality in CI
Clarify / abstain / safety in one placeSide 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.

AudienceCares about
End usersOne assistant, slim replies, no routing internals
PlatformChat AFD + API AFD (pin + start, separate fleets), Agent Plane (ACP + data plane), Shared, eval CI
Domain teamsRoute 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.

IDRequirement
FR-1Two 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-2Decide = active table ∩ required_claims ∩ channel. Explicit route_id still looks up, entitles, and records.
FR-3Outcomes: route, clarify, abstain. Only route starts AR. Jobs with no user fail closed or escalate_human.
FR-4On 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-5Chat/UI never sees route_id, run_id, agent_client_id, confidence, or router_layer. Decision record is server-side.
FR-6Empty-state chips are eligible + chat-visible only. A tap is Layer ①, not a side door. No start until outcome=route.
FR-7Generic chat still classifies. Jobs name route_id and skip classifier + clarify, not Plane ①.
FR-8Channels never dial activation_target. UI, jobs caller, and model never pin.
FR-9Chat 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-10Three 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.

ConcernDecide path (AFD + Agent Plane)Run path (AR)
Latencyp99 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.
AvailabilityACP 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.
CorrectnessAdversarial routing cases pass at 100% before release. Golden set in CI.PEP, pattern budgets, tool allowlists (not this page).
DurabilityDecision record is async. Decide does not wait for audit commit.Run pin is durable.
SecurityNo 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.
ConsistencyAFD 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.

AssumptionWorking target
Route/workflow catalogue5–8 chat-visible routes plus a few event-only rows (local seed: 32 proof routes)
Peak decideHundreds of turns/s mixed chat + jobs, not millions
Layer ③Rare. If saturated, degrade to clarify/abstain, not a slower ACP
Route pin TTL30–60 min on AFD, keyed by session_id
AR instancesPer 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.

Enterprise Agent Fabric: Identity and Security and Quality and Evals as bands on the left. Events at the top. Chat/UI and API enter only through the Agent Front Door. Inside the Agent Plane the Agent Control Plane sits on top, with the Agent Data Plane and the Agent Capability Plane side by side beneath it over one Postgres store. Front Door asks the Agent Data Plane to classify, pins the route, then async-starts Agent Runtime. The Runtime fetches the pinned route row from the Agent Data Plane, hydrates tool metadata from the Agent Capability Plane, and calls Shared for Memory, RAG, Model and Tools. Observability and Agent audit sit side by side; both feed Dashboard. Dashboard feeds Responsible / Governed AI.

Child pages: Agent Front Door · Agent Plane · Agent Runtime · Agent Capability Plane · Agent Audit.

Roles and responsibilities​

ComponentOwnsDoes not ownScale unitIf it dies
FrontendsChat/UI, chips, slim repliesRoute table, tools, PEPPer channel appThat UI down; API AFD still works
Agent securityUser, agent, and AR workload identity. Least privilege, mint after pin, revoke, no side doorsClassify, pin, loopPlatformFabric can run; not governed
Agent evalsRouting golden set, adversarial cases, CI gatePin, start, loopPlatformFabric can run; cannot gate misroutes
Chat AFDAuth, session, hints, SSE, freeze, startClassify, jobs OpenAPI, Kafka job startConnections + turn RPSChat down; jobs still work
API AFDPartner auth, {jobs_url}, Kafka ingest, freeze, startClassify, clarify UX, SSEJob POST RPS + consume lagJobs down; chat still works
Agent Control Plane (ACP)Policy, versions, decision audit (target)Pin, start, loop, AFD hot-path classifyStateless replicas / UIAudit lags; in-flight runs continue
Agent Data PlaneEligible set, classify, route/workflow catalogue. Called by AFD (classify) and AR (row at pinned version)Pin, start, loopVersioned table + decide replicasClassify fails closed
Decision auditAsync router_layer, outcome, eligible_routes (target)Chat transcriptIndependent queueDecide still succeeds
ARRun pin, Pattern 0–3, LangGraph loop. Fetches catalogue row at pinned version. Requests route-scoped agent token after freezeNext-utterance classify, active lookup, Shared stores, agent IdP registration, dual checkPer activation_targetControlled error after 202
SharedMemory, RAG, ToolsRoute choice, pin, startPlatformRuns degrade; routing still works
ObservabilityTraces, metrics, logs from AFD, Agent Plane, AR, SharedRoute choice, evidence chainPlatformRuns continue; you cannot see them
DashboardOperator view of Observability and Agent auditPin, start, classifyPlatformOperators blind; fabric still runs
Responsible / Governed AIPolicy, model risk, exam evidencePin, start, classifyPlatform / riskRuns continue; cannot evidence them
Memory (in Shared)Short / long term via session_idIngress classifyPlatformRuns degrade; routing still works
RAG (in Shared)Corpus retrieveIngress classifyPlatformGrounding degrades; routing still works
Tools / PEP (in Shared)Actions scoped by pinned manifest. Dual check: user entitlements (or approval) and agent scopesRoute choice, mintWith ARSide effects stop
Agent Capability Plane (with Agent Plane)Immutable id@version. Domain invoke and agent-start invoke. AR hydrates at pinPin, start, PEP permitPlatformNew 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_idDecide, pin, start, loopAADP ingest + queryNew events may drop (fail-open); in-flight runs continue
Model routerWhich LLM. Called by ARWhich route_idLLM gatewayInference fails; decide unaffected

Three routers (do not collapse)​

RouterQuestionWho calls it
Agent Data PlaneWhich 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 routerWhich 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.

ModeNameWho decides
0Single inferenceNobody — one LLM call
1AutonomousLLM CALL / DONE over a manifest
2DeterministicWorkflow designer — fixed stages, branch, human_gate
3GuidedDesigner 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:

ShapeMechanismRun behaviour
Process gateWorkflow human_gateRun pauses (waiting); resume via /turns with approval packet
Async handoffDomain tool escalate_to_humanTicket opened; parent completes when handoff succeeds
LLM signalStructured human_review_* in notesNo pause — evidence for a later branch
Specialist childkind=agent capabilityParent 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.

RecordWho writes itLives whereJob
Frozen route/sessionAgent Front Door (AFD)AFD store, keyed by session_id or job:{idempotency_key}, TTLPinned route (route_id, versions, activation_target, agent_client_id, correlation_id)
DecisionAgent Control Plane (ACP)Audit store (target)Why this turn routed, clarified, or abstained
Route/workflow cataloguePlatform / domain teamsAgent Data PlaneVersioned rows the data plane classifies over. Includes agent_client_id per row.
Run pinARDurable run storeCopy 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.

PrincipalAnswersIssued / heldOn the wire
UserWho is asking?AFD: IdP token, sub, emtsClaims or approval id. Not the tool-call bearer.
AgentWhich 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 workloadWhich box is running the loop?Platform: pod identity to Shared, IdP, model routerWorkload 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

  1. AFD authenticates. Mint or reuse session_id.
  2. Frozen route/session + continuation ("yes", "$500"): skip classify, resume AR.
  3. Else Agent Front Door calls the Agent Data Plane (message + claims + session_id).
  4. Data plane: eligible = catalogue ∩ claims ∩ channel. Returns route / clarify / abstain. Writes the decision record (target). Does not pin or start.
  5. clarify / abstain: talk to the user (jobs: fail closed or escalate_human). No AR. After a pick, back to step 3.
  6. route: AFD stores the freeze (route_id + versions + activation_target + agent_client_id), POSTs AR.
  7. 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, returns 202 { "correlation_id" }. Nobody re-reads active.
  8. AFD returns slim UI payload, or 202 to 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.

Request path: AFD, Agent Data Plane plus route/workflow catalogue, and AR as three blocks. Agent Front Door calls the data plane to classify. On route the AFD stores the frozen route/session and async-starts AR.

Channel APIs and swim lanes: Front Door. Decide JSON: Agent Plane. Start / 202 / {runs_topic}: Runtime.


6. Isolation and shed​

FailureAgent Front Door (AFD)Agent Control Plane (ACP)
One AR downOther activation_targets still start. That route returns a controlled error after 202 or on start-ack timeout.Still accepts new turns.
One AFD downThe other ingress still works.Unaffected.
Frozen-route/session store downContinuations degrade to open-run on AR. New route can still start if start-ack works.Unaffected.
Catalogue unreadableDo not start.Fail closed.
Shared downRouting still works. AR degrades (no memory / RAG / tools).Unaffected.
Layer ③ saturatedServe clarify / abstain.Do not enqueue behind Layer ②.
Audit lagDecide 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 active after pin (wrong, and it also adds origin QPS).
  • AR POSTs another AR's activation_target (A2A skips API AFD).
  • AR workload identity as Authorization to downstream APIs (AR becomes a fat svc-agents).
  • One agent token at AR boot with every route's scopes.
  • A new activation_target per route only to get a different identity (that inflates fleets to fix a token problem).
  • Deriving agent_client_id from route_id instead 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​

  1. Which route_table_version was active for this session.
  2. Which routes were eligible for this identity (and which were hinted).
  3. Why this turn routed, clarified, or abstained (router_layer, confidence).
  4. That adversarial routing cases pass at 100% before release.
  5. That a routing incident was added to the golden set and blocked in CI.
  6. That jobs HTTP and Kafka ingest still have a Plane ① decision record (explicit route_id is not a skip).
  7. Which agent_client_id was frozen for this run, which agent called this tool, and that user and agent both passed (FR-10).
  8. 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_id and 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-reads active
  • 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_id on the freeze. Dual check in Shared. Shared AR does not share one agent bearer (FR-10)
  • Human gate, async handoff, and kind=agent child 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.

  1. 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.
  2. Two AFD fleets, one contract. Separate process (FR-9). Shared Agent Plane. {runs_topic} is return. Kafka job start is the API AFD.
  3. Jobs name the route. Skip classifier and clarify. Do not skip Plane ①.
  4. Generic chat still classifies. Hints are eligible + chat-visible. A tap is Layer ①.
  5. Clarify / abstain never start AR. On route: freeze, async-start, 202.
  6. One Agent Plane per trust domain. Control Plane and Data Plane stay separate boxes. Not one mega-agent.
  7. Three principals. User, agent, AR workload. Catalogue agent_client_id on the freeze. Pin first, then mint. Dual check in Shared Tools / PEP. Shared AR is compute, not identity (FR-10).
  8. Capabilities are references. Publishers append id@version next to Control Plane and Data Plane. Manifests refer. AR hydrates the whole pinned manifest before the LLM. kind=agent invoke is API AFD jobs, not the callee AR.

Agent Front Door · Agent Plane · Agent Runtime · Agent Capability Plane · Agent Audit · Patterns