/architecture
How we build agentic AI that survives production
The framework we build agentic systems on — a swappable model gateway, vector store, state layer and tracer — deployed cloud-native on any cloud.
Most agent demos are one prompt and a model call. What breaks in production is everything around that: where the knowledge comes from, what happens when retrieval returns nothing, which action the agent is allowed to take, who reviews it, and how you find out afterwards what it actually did. We build on a framework that answers those questions once, so each project starts from a working platform rather than a blank page. Its defining property is that every external dependency — the model, the embeddings, the vector store, the state backend, the tracer, the cloud — sits behind an interface. You are not adopting our stack; you are adopting an architecture that runs on yours.
The platform, in four tiers
Build-time accelerators
internal generatorsGenerate the platform instead of hand-writing it.
- Project scaffolding — services, shared library, first agent, containers
- Agent generation — agent class, graph, state, routes, prompts, tools
- Architecture components — auth, orchestrator, BFF, frontend, microservices
- Infrastructure wiring — model gateway, embeddings, vector store, database, tracing
- Building blocks — tools, skills, services, guardrails
- Delivery planning — epics, stories, acceptance criteria, developer tasks
Runtime application
what actually serves trafficThe request path, and the agent pipeline inside it.
Request path
- Frontend — React + Vite
- BFF — auth + proxy
- Agents — FastAPI + LangGraph
Agent pipeline
- input guardrails
- planning
- retrieval
- response
Shared foundation
the agentic-infra libraryOne library every agent stands on, so nothing is rebuilt per project.
- AI gateway
- embeddings
- vector store
- state & cache
- observability
Pluggable external providers
your choice, not oursThe concrete services behind each interface.
LLMs & embeddings
- OpenAI
- Anthropic
- Azure OpenAI
- AWS Bedrock
- Google Gemini
- Vertex AI
- Cohere
- Hugging Face
Vector databases
- pgvector
- Chroma
- Pinecone
- Qdrant
State & cache
- Postgres
- Redis
- MongoDB
- DynamoDB
Observability
- Langfuse
- LangSmith
- OpenTelemetry
1Build-time accelerators
A new project does not start from a blank page. We maintain our own generators for the shape every agentic system needs — the service layout, an agent with its graph, state, routes and prompts, the infrastructure wiring, the auth and BFF layers, and the backlog to build against. That is most of why a first production agent takes days rather than a quarter: the scaffolding is generated, reviewed and consistent across projects, so senior time goes to the part that is actually yours — the workflow, the domain rules, and where the human stays in the loop.
2Runtime application
Three deployables, each independently releasable. The frontend is a free choice — the contract is HTTP and server-sent events, so React and Vite, Next.js, or a panel in the app you already run all work. The BFF is the only public door: it verifies the caller, rate limits, persists the turn and strips anything the agent has no business seeing, then calls the agent service over private networking. Inside the agent, the work is a pipeline with named stages, which is what makes it inspectable rather than one opaque model call — and what lets a stage branch, loop or resume when the workflow needs it.
3Shared foundation
This is the layer that makes the rest portable. Five capabilities every agentic system needs, each behind a single interface: the AI gateway, embeddings, the vector store, state and cache, and observability. Application code calls the interface and never the vendor, which is what turns 'move to Bedrock' or 'put traces in Datadog' into a configuration change. It is also why a second project starts ahead of the first: the hard, boring parts — streaming, retries, token accounting, span plumbing — are solved once and inherited.
4Pluggable external providers
Everything in this tier is a configuration value. That matters most in an enterprise, where the answers are usually already decided: the model has to be the one in your cloud account, the traces have to land in the tool your SRE team watches, the vectors have to sit in the database you already run. None of those are ports — they are the seams the foundation exists to keep open.
Nothing here is locked in
The fourth tier drawn out in full, plus the two seams a diagram cannot show. Each row is one interface and the implementations that drop into it — no application code changes.
Model provider
The model you start on is rarely the model you finish on.
- OpenAI
- Anthropic
- Google Gemini
- AWS Bedrock
- Google Vertex
- Azure OpenAI
- LiteLLM
Embeddings
Hosted or self-hosted, depending on what may leave your network.
- OpenAI
- Azure
- AWS Bedrock
- Google Vertex
- Cohere
- Hugging Face (self-hosted)
Vector store
Start inside your database; move out when the workload earns it.
- pgvector
- Qdrant
- Pinecone
- Chroma
State and memory
Match the store to the read pattern, not to a default.
- Postgres
- Redis
- MongoDB
- DynamoDB
- In-process
Observability
Traces belong wherever your team already looks.
- Langfuse
- LangSmith
- OpenTelemetry
Frontend
The agent API is HTTP and SSE, so the client is not our decision.
- Next.js
- Vite and React
- Your existing app
Runtime
The same container image, wherever it needs to run.
- Kubernetes on any cloud
- Managed container hosting
- A single VM
One request, end to end
The layers
01
Knowledge and ingestion
Turn the source material into something retrievable.
An agent is only as good as what it can look up. Content is chunked with overlap and embedded once at seed time, not at request time. The critical discipline is having a single source: the same files that render these pages are the files that seed the knowledge base, so the site and the agent cannot drift into telling a visitor two different stories.
- →One source of truth for content, consumed by both the site and the index
- →Chunking with overlap, so a retrieved fragment still carries its context
- →Re-seeding is a scripted step, not a manual copy-paste
02
Retrieval
Find the few passages that are actually relevant.
We run vectors in the same Postgres that holds the application data, via pgvector. That is a deliberate choice over a dedicated vector database: one datastore to operate, one backup, one connection pool, and joins between an embedding and the row it describes. A separate vector service earns its place at a scale most products never reach. Retrieval is best-effort by design — if the index is empty or embeddings fail, the agent still answers from its grounded persona instead of erroring at the visitor.
- →pgvector inside the application database — one system to run and back up
- →Provider-swappable: pgvector, Qdrant, Pinecone or Chroma behind one interface
- →Failure degrades to a grounded answer, never to a stack trace
03
Model gateway
One call site for every model, so the provider stays a config value.
Every model call in the platform goes through a single gateway interface. Nothing in the agent code names a vendor. That is what makes a provider change a configuration change rather than a refactor — and it is worth doing on day one, because the model you start on is rarely the model you finish on. The same seam makes it practical to run different models for different jobs, or to fail over.
- →OpenAI, Anthropic, Gemini, Bedrock, Vertex, Azure or LiteLLM behind one interface
- →The agent code names no vendor — the provider is an environment variable
- →Token usage is captured per call for cost attribution
04
Orchestration
Sequence the steps, and keep every one of them observable.
Agents inherit a base class that opens a trace, records the request, times each step and closes the span — including on the failure paths, which is where you actually need the data. Complexity is added only when the work needs it: a single-pass retrieve-and-answer flow stays a pipeline, and the graph engine and checkpointer come out when a workflow genuinely branches, loops or has to resume mid-run. Reaching for a graph framework to draw a straight line is cost with no return.
- →A shared base class — subclass it and observability is automatic
- →Streaming and non-streaming share one instrumentation path
- →Graph orchestration and state checkpointing available, used when the work branches
05
State and memory
Remember what happened between turns, and between runs.
Conversation history, run checkpoints and cached work all need somewhere to live, and the right somewhere differs by project: a shared Postgres for a modest deployment, Redis when the read path is hot, DynamoDB or MongoDB when that is what the client already operates. It sits behind one interface for the same reason the model does — this is a decision that gets revisited the moment the traffic profile changes, and revisiting it should cost a config change. Checkpointing is also what turns a long workflow into something that can resume, rather than a request that has to succeed in a single pass.
- →Postgres, Redis, MongoDB, DynamoDB or in-process, behind one interface
- →Conversation history and run state are separable — they need not share a store
- →Checkpointing lets a long or interrupted workflow resume instead of restarting
06
Tools and actions
Where the agent stops describing the work and does it.
This is the line between a chatbot and an agent, and the one worth engineering hardest. Every action is a typed tool with an explicit contract, and every external system sits behind a provider interface with a no-op implementation. That means the integration can be developed, demoed and tested without touching the customer's real calendar, ledger or system of record — and switching to the real one is configuration, not a rewrite.
- →Typed tools with explicit contracts, not free-form model output
- →Every external system behind a provider interface, with a no-op for development
- →Actions are enumerated — the agent cannot reach a system nobody wired up
07
Guardrails and the human boundary
Decide what the agent may assert, and where a person takes over.
The useful question is not whether a model can do something, but whether it should be the one doing it unsupervised. We draw that line explicitly and in code. Our own estimator is the example: it produces scope, phases and a timeline, and is prohibited from producing a price — pricing comes from a senior engineer who has read the brief. Answers stream token by token, so a constraint that is not in the prompt is a constraint that arrives on the visitor's screen before anything could strip it.
- →Constraints live in the prompt, because streamed output cannot be filtered after the fact
- →Judgement calls hand off to a person with the full context attached
- →The agent is instructed to refuse claims it cannot ground, and to say what it does not know
08
Observability
Know what the agent did, on the runs that went wrong.
An agent that cannot be inspected cannot be improved, and 'it gave a weird answer last Tuesday' is not a bug report. Tracing is built into the base class rather than sprinkled through the agents, so a new agent is instrumented the day it is written — spans per step, retrieval counts, token usage, latency and the failure path. The tracer is pluggable across Langfuse, LangSmith and OpenTelemetry, so traces land in whatever the client already runs.
- →Instrumentation in the base class — new agents are traced by default
- →Langfuse, LangSmith or OpenTelemetry behind one tracer interface
- →Retrieval counts, token usage and latency captured per run
09
Services and the edge
Authenticate, rate limit and persist — before anything reaches a model.
The agent service is never exposed to the internet. A backend-for-frontend sits in front of it: it verifies the caller's token, applies rate limits, writes the conversation down, and only then calls the agent over private networking with a service key. Cost control and abuse control belong here, in front of the expensive part — and this is also the seam where data the agent has no business seeing is stripped from the payload. Past that boundary the split is ordinary microservices: independently deployable services that own their data and scale on their own curve. Because everything the browser touches is HTTP and server-sent events, the frontend is your choice — Next.js, Vite and React, or a panel inside the app you already run.
- →The agent service has no public route — only the BFF can reach it
- →Token verification, rate limiting and persistence happen before the model call
- →Independently deployable services, each owning its data and scaling separately
- →An HTTP and SSE contract, so the frontend framework is your choice, not ours
10
Deployment and portability
Run the same architecture on whatever cloud you already pay for.
The services are stateless containers: configuration comes from the environment, health is an endpoint, and nothing reaches for a host-specific SDK. That is what makes the target a choice rather than a commitment — a cloud-native Kubernetes rollout on AWS, Azure or GCP, a managed container platform, or a single VM for a pilot, all from the same image. It is also why the parts an enterprise usually has to fix are the parts we keep behind seams: which cloud, which model, which identity provider, which observability stack. Right-sizing is part of the discipline — we run a cluster when the workload earns one, not by default.
- →Stateless containers, configuration from the environment, health checks as endpoints
- →Cloud-native Kubernetes on AWS, Azure or GCP — or managed hosting, from the same image
- →No host-specific SDK in application code, so the cloud stays a deployment decision
- →Migrations run as a pre-deploy step — a bad schema change stops the rollout
The stack we run
Agent runtime
- Python + FastAPI
- async services, streaming over SSE
- Shared platform library
- gateway, embeddings, vector store, state and tracing behind one set of interfaces
- Pydantic
- typed request, response and settings schemas
Models
- OpenAI
- the provider this site runs on — swappable at the gateway
- Anthropic, Gemini, Bedrock, Vertex, Azure
- first-class through the same gateway interface
- LiteLLM
- a bridge to everything else, when a client has an unusual requirement
- text-embedding-3-small
- 1536-dimension embeddings, called as a hosted API
Data and state
- Postgres
- application data and vectors in one database
- pgvector
- similarity search next to the rows it describes
- Qdrant, Pinecone, Chroma
- alternative vector stores behind the same interface
- Redis, MongoDB, DynamoDB
- alternative state backends, chosen per workload
- SQL migrations
- forward-only, applied on deploy, one source of truth for schema
Frontend
- Next.js (App Router)
- what this site runs on — server-rendered, static where it can be
- Vite and React
- when a single-page app is the better fit
- Your existing app
- the agent API is HTTP and SSE — a panel is enough to start
- TypeScript
- end to end, including the content schemas
Identity
- Supabase Auth
- sign-in, sessions and email verification
- Asymmetric JWTs
- verified at the edge against a published key set
- Service keys
- how internal services trust each other on the private network
- Your identity provider
- the verification seam takes OIDC or SAML equally well
Operations
- Docker
- one image, the same locally and in production
- Kubernetes
- cloud-native rollout on AWS, Azure or GCP
- Managed container hosting
- the same image, when a cluster is not yet warranted
- Langfuse / LangSmith / OpenTelemetry
- tracing, wherever the client already collects it
How we decide
Every external dependency sits behind an interface
Model provider, embeddings, vector store, tracer, calendar. Not for the sake of abstraction, but because each one is a decision that gets revisited — and revisiting it should cost a config change, not a sprint.
Degrade, never invent
When retrieval finds nothing or a dependency is down, the agent answers within what it can ground and says what it does not know. An agent that fabricates under failure is worse than one that stops.
Complexity is earned, not assumed
A straight-line flow stays a straight line. We add graph orchestration, a queue or a separate datastore when the workload demands it, and not before — every layer added on speculation is a layer someone maintains forever.
The human boundary is designed, not left over
We decide up front which judgements the agent makes and which it escalates, and build the handoff so the person arrives with the full context rather than starting over.
Instrumented from the first commit
Tracing, health checks and structured errors ship with the first version, not after the first incident. Observability retrofitted is observability missing exactly where it was needed.
Common questions
What tech stack do you use for agentic AI?
Python and FastAPI for the agent services, built on a shared platform library that puts the model gateway, embeddings, vector store, state and tracing behind one set of interfaces. Postgres with pgvector holds application data and embeddings together. Frontends are TypeScript — Next.js or Vite, or a panel in the app you already run. Services ship as Docker images and deploy cloud-native to Kubernetes on any cloud, or to managed hosting when a cluster is not yet warranted. The important part is not the list: no application code names a vendor, so each of those is a configuration choice rather than a commitment.
Which LLM providers can you work with?
Every model call goes through a single gateway interface that supports OpenAI, Anthropic, Google Gemini, AWS Bedrock, Google Vertex, Azure OpenAI and LiteLLM. Because no agent code names a provider, moving between them — or running different models for different jobs — is a configuration change. If you already have a provider relationship or a model that has to stay inside your own cloud account, we build against that.
Which vector databases do you support?
Usually not a separate one. We run pgvector inside the same Postgres that holds the application data, which means one datastore to operate and back up, and the ability to join an embedding to the record it describes. The vector store sits behind an interface that also supports Qdrant, Pinecone and Chroma, so if the workload outgrows pgvector the swap is a config change rather than a rewrite. A dedicated vector service is a real answer at a scale most products never reach.
How do you stop an agent from hallucinating?
Three things, in order of how much they matter. Ground it: answers are built from retrieved passages, and the agent is instructed to say what it does not know rather than fill the gap. Constrain it: the boundaries live in the prompt, because output streams token by token and a constraint applied after generation arrives too late. Watch it: every run is traced with its retrieval counts and its output, so the failure cases are inspectable instead of anecdotal. Nothing removes the risk entirely, which is why the fourth thing is deciding which judgements a person makes.
Can you deploy this to Kubernetes on our own cloud?
Yes — that is the intended shape. The services are stateless containers with health endpoints and configuration from the environment, and no application code reaches for a host-specific SDK, so a cloud-native Kubernetes rollout on AWS, Azure or GCP is a deployment decision rather than a port. The same image runs on managed container hosting when a cluster is not yet warranted. Inside your account, the model can be Bedrock or Vertex, the database your managed Postgres, identity your existing provider, and traces go wherever your team already looks — those are exactly the seams the architecture keeps open.
How long does it take to get an agent into production?
Days to weeks for a first production agent on a scoped workflow, because the platform layers — gateway, retrieval, tracing, auth, the streaming edge — already exist and are not rebuilt per project. What sets the timeline is your side of the integration: access to the systems of record, the quality of the source material, and how many review cycles the human boundary needs. Describe the workflow and we will come back with a phased plan; scope and pricing come from a senior engineer, not from a form.
What does observability actually mean for an AI agent?
That you can answer 'what did it do, on the run that went wrong'. Concretely: a trace per request, a span per step with its timing, how many passages retrieval returned, the token usage, and the failure path recorded rather than swallowed. It is built into the base class every agent inherits, so instrumentation cannot be forgotten, and the tracer is pluggable across Langfuse, LangSmith and OpenTelemetry.
Can we use our own frontend framework?
Yes. Nothing in the agent platform assumes a frontend: the contract is HTTP for requests and server-sent events for streamed responses, which any modern client speaks. We build Next.js when server rendering and SEO matter, Vite and React when a single-page app is the better fit, and often neither — the fastest route to value is frequently a panel inside the application your team already runs, rather than a new destination you have to persuade people to visit.
Do you build this as microservices or a monolith?
Independently deployable services, split where the split earns its keep. The agent runtime, the backend-for-frontend and the operator console are separate deployables with their own scaling and their own release cadence — which matters because the agent runtime is the expensive, slow, bursty part and should not be coupled to the rest. What we do not do is split for the sake of a diagram: services that always deploy together and share a database are one service with extra network calls.
How do you handle conversation history and long-running agent runs?
Through a state layer that is pluggable the same way everything else is — Postgres, Redis, MongoDB, DynamoDB, or in-process for development. Conversation history and run state are separable, so the hot read path can live in Redis while the durable record stays in Postgres. Checkpointing is what makes a long workflow resumable: a run that spans minutes and several tool calls picks up from its last good step rather than starting the whole sequence again.
// this architecture, applied to a domain: where we've built