Skip to main content

Agent Front Door

You are in the deep end

This page is the Front Door contract. The story is how it hangs together. The public sequence is the request lifecycle.

The Front Door is the only public way into the Fabric. Chat, jobs, and partner APIs all stop here. It checks who you are, freezes which agent will run, and starts Agent Runtime. It never classifies a request itself and it never runs the agent.

Try it: your first request.

What it is​

Two shapes, one contract: entitle → classify (chat) or entitle-only (jobs) → freeze → start.

  • Chat is a BFF. Humans send an utterance. The Front Door asks the Agent Data Plane to classify. Only outcome=route starts Runtime.
  • Jobs are an API. The caller already named route_id. Classification is skipped; entitlement is not.

Locally both shapes share one Front Door process. At production scale they are separate fleets so a hung chat pool cannot take down jobs. They still share the Agent Plane, the freeze store, and the start contract.

What it owns​

OwnsDoes not own
Channel auth, session_id, hints, SSE/poll, jobs APIClassify, eligible set, route catalogue
Frozen route/session, pin, async startThe run pin, the LangGraph loop
Fan-out of Runtime statusAgent identity mint, tool invoke

What it must not do​

  • Classify. That is the Data Plane.
  • Start Runtime on clarify or abstain.
  • Let a channel dial Runtime or the Agent Plane.
  • Show the chat UI a route_id, run_id, or agent_client_id.

How to talk to it​

MethodPathWho
POST/v1/assistant/turnsChat
GET/v1/assistant/hintsChat empty state
POST/v1/jobsJobs, partners, parent agents
GET/v1/jobs/{correlation_id}Jobs poll

Headers: stub auth. Fixtures: reference.


Design notes​

The sections below are the internals of this box — fleets, freeze record, APIs, and scale.

Business problem​

Channels and partner APIs invent their own dispatch. Chat sockets and job bursts share one process, so a hung SSE pool takes down Legal jobs (or the reverse). Callers wait for the memo, so the decide SLO dies. Browsers and domain services dial the Agent Plane or AR and leak auth, routing internals, and side doors into high-risk agents.

Key requirements​

IDRequirement (this box)
FR-1Only Chat AFD and API AFD call decide. Kafka ingest is the API AFD. {runs_topic} is return, not start.
FR-4On route, this AFD pins route_id + versions + agent_client_id, async-starts AR, returns slim UI or 202 + correlation_id.
FR-5Chat/UI never sees route_id, run_id, agent_client_id, confidence, or router_layer.
FR-6Empty-state chips are eligible + chat-visible. A tap is Layer ①. No start until outcome=route.
FR-7Generic chat classifies. Jobs name route_id and skip classifier + clarify, not entitle.
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, frozen-route/session store, and start contract.

Decide+pin+start-ack p99 < 100 ms. Wait only for AR 202, never the loop.

Architecture​

Two AFD fleets: Chat/UI and jobs enter separately, ask the Data Plane, freeze the route, then start AR. Kafka is return, not a third AFD.

Editorial source

Entitle → classify (chat) or entitle-only (jobs) → freeze → start. Callers never dial AR.

Key technology decisions​

ChoiceWhy
Two fleets, one contractChat is connection-bound (SSE/poll, hints, clarify). Jobs are bursty and skip classify. Shared process couples failure domains (FR-9).
AFD writes the freeze, not ACPDecide SLO stays classify. Pin + start sit on the box that already holds the channel session.
Shared KV for freeze, not replica memoryNext "yes" lands on another replica. Keyed by session_id, TTL 30–60 min.
Async start by defaultAR loop is seconds to minutes. Pattern 0 is the only sync wait.
Kafka {runs_topic} is returnSame slim status as HTTP GET run. AFD fans out. Not a public start.
Kafka job-start worker is API AFDTopic bind = Layer ① route_id. Prevents a third public ingress.

Expected outcomes​

  • One ingress decision for a shared experience; users do not pick chatbots.
  • Chat down does not take jobs; jobs burst does not starve SSE.
  • No public Agent Plane or AR URL. No channel-dial of Legal or payments.
  • Continuations ("yes", "$500") and job retries keep the same pinned route.

This box owns / does not own​

OwnsDoes not own
Channel auth surface, session_id, hints, SSE/poll, jobs OpenAPIClassify, eligible set, route catalogue
Frozen route/session, pin + async startRun pin, loop, Patterns 0–3
Fan-out of AR status (SSE / GET job / Kafka consume)Agent identity mint, dual check, tool invoke
Mapping opaque hint_id / option_id → route_idDecision audit (ACP writes it)

2. Two fleets, one contract​

LayerCountWhy
Agent PlaneOne per trust domainSame eligible set, entitlements, eval, audit
Ingress contractOneEntitle → classify → freeze → start. Callers never dial AR
AFD fleetsTwo (production)Chat/UI is connection-bound. Jobs are bursty and skip classify
KafkaReturn path, or jobs ingest{runs_topic} is how AR talks back. A Kafka worker that starts a job is the API AFD
#IngressWho is the AFDHow the route is chosenTypical response
1. UIChat/UI, copilot, portal, Teams, mobile, POST /v1/assistant/turnsChat AFD (own fleet)Eligible prune, then layered classify (unless a chip hits Layer ①)Slim message; SSE/poll for async
2. JobsPOST /v1/jobs; Kafka job startAPI AFD (own fleet)Explicit route_id in body or Layer ① topic bind202 + correlation_id. No user: fail closed or escalate_human

Two AFD fleets: Chat/UI hits Chat AFD. Jobs API and Kafka job start hit API AFD. AFD calls Agent Data Plane to classify. On route the AFD stores the frozen route/session and starts AR. Kafka runs_topic is return, not a third AFD.

If they skip the AFDWhat breaks
Browser or partner → Data PlaneAuth leaks into Plane ①
Caller waits for the memoDecide SLO dies
route_id without entitleSide door
Caller starts ARChannel dials Legal / payments
Kafka as a third public startDispatch outside Plane ①

Default start is async. After route, that AFD POSTs the freeze. AR returns 202 { "correlation_id" }. Sync wait is Pattern 0 only.


3. Context​

Humans hit Chat AFD. Jobs callers hit API AFD. Chat asks the Agent Plane; jobs start AR. Channels never dial Plane or AR.

Editorial source
ActorRelationship
Human on Chat/UIIdP user bearer. Sees slim messages and opaque options only.
Partner / systemIdP service bearer. Posts explicit route_id to {jobs_url}.
Parent ARCalling-agent bearer. Same jobs contract as partners (not Chat AFD).
Agent Data PlaneInternal. Decide, eligible, catalogue row. AFD is the only decide caller.
ARInternal. Start, resume, open-run, status. Channels never see this URL.
IdPMints user/caller bearer at ingress. After pin, AR asks it for the agent token.

4. Container / component architecture​

ComponentResponsibility
Chat turn handlerAuth, mint/reuse session_id, continuation vs new decide, freeze, start, slim reply.
HintsGET eligible ∩ chat-visible. Opaque hint_id. Cache per (claims-hash, channel, route_table_version).
SSE / pollAfter accepted turn. Reads AR HTTP or {runs_topic}. Not catalog JSON.
Jobs HTTPPOST {jobs_url} with route_id. Entitle, freeze, start. 202 + correlation_id.
Kafka job-start workerSame as jobs HTTP. route_id from body or Layer ① topic bind.
Status fan-outGET job and {runs_topic} consume. Does not call the data plane.
Frozen-route/session storeShared KV. Keyed by session_id or job:{idempotency_key}. Pin miss: AR open-run API.

Stateless handlers. Do not keep the freeze in replica memory.


5. Frozen route/session​

The AFD stores the frozen route/session so "yes" or a jobs retry keeps the same route_id, versions, and agent_client_id. Pin miss: AR API, not AR Postgres. See Three principals.

RecordWho writes itLives whereJob
Frozen route/sessionAFDAFD store, keyed by session_id or job:{idempotency_key}, TTLPinned route (route_id, versions, activation_target, agent_client_id, correlation_id)
FieldRole
session_idPartition key. Chat thread.
route_idPinned workflow. Absent until outcome=route.
route_table_versionPin. Nobody re-reads active.
activation_targetWhich AR to start.
agent_client_idWhich robot. Frozen here; AR mints after copy.
correlation_idWritten after AR 202.
idempotency_keyJobs only.
expires_atTTL 30–60 min from last pin / continuation.

6. APIs​

Agent Plane and AR are not public URLs. Both AFDs call decide, catalogue row, and start.

Chat AFD​

MethodPathWhen / response
POST/v1/assistant/turnsFree text, hint tap, or clarify pick. Slim message. On route, accepted; result on SSE/poll
GET/v1/assistant/hintsEmpty state. Eligible labels + opaque hint_ids
GET/v1/assistant/sessions/{session_id}/eventsSSE (or poll). Slim tokens / final message

Continuation: skip decide. Resume AR.

API AFD​

MethodPathWhen / response
POST{jobs_url} (default /v1/jobs)Partner / system / parent AR. 202 { "correlation_id" }
GET{jobs_url}/{correlation_id}Caller poll. Status / slim result from AR HTTP
consume{runs_topic}Result fan-out. Same slim body as GET. Not a start

Errors​

ConditionHTTPBehaviour
Missing / invalid bearer401Do not decide, do not start
Empty eligible / missing job claims403Fail closed
Duplicate jobs idempotency_key202Return the original correlation_id
AR start-ack timeout503No fake success. Do not invent correlation_id
UI turn request (JSON)
{
"session_id": "sess-88",
"message": "Why was I charged $42?",
"hint_id": null,
"option_id": null
}
Jobs start (JSON)
{
"route_id": "contract_review",
"idempotency_key": "job-4412:v1",
"payload": { "document_id": "doc-19", "matter_id": "m-88" }
}
{ "correlation_id": "corr-9f3c" }

Call map​

IngressAFD → Data PlaneAFD → AR
UI new turnDecide. Eligible for chips. Catalogue row on routeStart on route. Resume on continuation. Open-run on cache miss
Jobs HTTP / Kafka / AR agent-startDecide with explicit route_id. Catalogue row on routeStart
{runs_topic} consume / GET jobNoneStatus only

Decide JSON: Agent Plane. Start / 202: Agent Runtime.


7. Swim lanes​

UI turn​

UI happy path: IDP mints the user bearer. Chat/UI posts a turn. AFD authenticates, asks Agent Data Plane to classify, freezes the route/session, and async-starts AR. AR asks IDP to mint the agent token. The UI waits on SSE or poll.

Jobs API​

Jobs API happy path: Caller posts route_id. AFD authenticates, asks Agent Data Plane to entitle and record, freezes the route/session, and async-starts AR. Caller receives 202 and polls the job.


8. Deployment architecture​

Cloud-agnostic. One trust domain. Three availability zones.

ConcernStance
IngressChat AFD and API AFD are separate hostnames, deployments, HPAs. No shared process in production.
EgressClusterIP / mesh to Agent Plane and AR. No public Plane or AR URL.
Freeze storeManaged Redis/Valkey or equivalent, multi-AZ. Not an in-memory map.
KafkaJob-start topics consumed only by API AFD. {runs_topic} consumed by both fleets for fan-out.
ScalingChat: connections + turn RPS. API: HTTP RPS + ingest lag + result-consumer lag.
FailoverOne AFD down: the other ingress still works. Freeze store down: continuations degrade to AR open-run.

9. Event / messaging design​

TopicDirectionWhoSemantics
Job-start (configurable)InboundAPI AFD workerAt-least-once start. Dedup via idempotency_key on AR
{runs_topic}Inbound consumeChat AFD, API AFD fan-outSame slim schema as GET /v1/runs/{id}. Not a start
{runs_topic}Produced by AR—AFD must not publish start events here

10. Sequence diagrams​

Happy path — UI turn​

User posts a turn. Chat AFD classifies via the Data Plane, async-starts AR, and returns a slim accepted reply. Result arrives on SSE or poll.

Editorial source

Happy path — jobs API​

Caller posts route_id. API AFD entitles, async-starts AR, returns 202 plus correlation_id, then the caller polls job status.

Editorial source

11. Failure and resilience​

FailureBehaviour
Chat AFD downJobs still work. Inverse is true.
One AR / activation_target downOther targets still start. Controlled error after 202 or on start-ack timeout.
Freeze store downContinuations → AR open-run. New route can still start if start-ack works.
Agent Plane down / catalogue unreadableDo not start. Fail closed.
Shared downRouting still works. AFD unaffected; AR degrades.
{runs_topic} lagSSE/poll may stall; decide still succeeds. Scale fan-out independently.

Anti-patterns: freeze in replica memory; one autoscaler for UI BFF and jobs URL; Kafka ingest as a third public start; {runs_topic} as job start; caller waits for the memo; public Agent Plane URL.


12. Scaling​

Decide capacity is turns/s × (entitle + classify + pin + start-ack). Run capacity is a different fleet. Scale Chat AFD, API AFD, and Agent Plane as three fleets (FR-9). Numbers: Enterprise Agent Fabric §3.

AxisGrows withScale signal
Chat AFDConcurrent chats, SSE/poll, hint refreshConnections + turn RPS
API AFDJob POST RPS, Kafka ingest, {runs_topic} consumeHTTP RPS + ingest lag
Frozen-route/session storeSessions with a live pin (TTL 30–60 min)Active keys, not QPS

Start edge. Wait only for 202 + correlation_id, never the memo.

Plane and AR scale: Agent Plane §12 · Agent Runtime §12.


Agent Plane · Agent Runtime · Agent Capability Plane · Enterprise Agent Fabric