Architecture Overview
What we built, how it works, and what each component is responsible for — written so that an engineer new to AI agents can follow every step.
First, three concepts
What is an Agent?
A normal chatbot only talks: you send text, the model sends text back. An agent can also act: the model is given a set of tools (functions that call real APIs), and it decides — on its own, mid-conversation — which tools to call, in what order, to get a job done. The model provides the judgement; the tools provide the hands.
What is the Agentic Loop?
The cycle that runs on every single broker message: the model thinks → calls a tool → reads the result → decides if it needs another tool → … → only when it has everything does it write the reply. One message from a broker can trigger several real API calls before any text comes back. That loop is what makes it an agent and not "one LLM call per turn."
What is the Gateway?
Agents need a home — a server that owns the conversation. Our gateway is that home: it receives chat messages over HTTP, remembers each session's history, gives the model its instructions (skills) and its tools, runs the agentic loop, and returns the finished reply to the widget. It's the piece we wrote that turns a raw model into our agent.
What we built
An in-house agent runtime — no third-party agent framework. It stands on two open standards so nothing is proprietary glue:
- MCP (Model Context Protocol) — the open standard for exposing tools to a model. Our 25 listing tools (spanning all five property families — Home, Office, Industrial, Retail, Land) are a real MCP server; any MCP-compatible host could use them unchanged.
- Agent Skills — the open format for giving an agent written procedures. Seven
SKILL.mdfiles: five property-family workflows plus two shared skills for conversational behaviour and the visual output protocol.
The model inside the loop is Claude (claude-opus-5) via the Anthropic API. The provider is a config switch — any OpenAI-compatible endpoint can be plugged in — but the agent itself (loop, skills, tools, guardrails) is entirely ours.
The agentic loop, drawn
This is a real trace from our first verified conversation — one broker message, four tool calls, then the reply:
The full stack
1 · Broker surface — embeddable widget SDK + portal
One <script> line embeds the chat in any web app or PWA — as a floating bubble, or full-page via inline mode (that's the ChatGPT-style broker portal on the Demo page). Plain JavaScript inside Shadow DOM, so the host page's CSS can't break it. Sessions are minted lazily: opening a chat creates nothing server-side until the first message is sent.
2 · Agent gateway — our runtime
Owns every conversation. Builds the model's instructions from the skills, persists every session durably (resume across restarts and devices), runs the agentic loop, streams replies, and serves the session list the portal sidebar renders — with hard delete, admin-gated.
3 · MCP tool server — all the domain logic
Twenty-five tools wrapping Rezide's real APIs, covering all five property families. Everything the model should never have to juggle lives here: ID mapping, payload building, defaults, identity, write protection.
4 · Rezide platform — unchanged, reused
The exact same APIs the existing listing form calls. No new listing backend was built. Every create lands as pending, behind the existing moderation gate.
One broker message, step by step
- Widget sends the text to
/api/chat/streamwith the session id and shows a typing indicator (if this is the chat's first message, it mints the session viaPOST /api/sessionfirst — opening a chat alone creates nothing). - Gateway appends it to the session's history and calls the model with three things: the skills (as the system prompt, cached), the full history, and the schemas of all 13 tools.
- Model extracts every piece of information in the message at once — "2BHK in Oberoi 360 West for rent, 1L, 5L deposit" fills five slots in one pass — then asks for the tool calls it needs.
- Gateway executes each call through the MCP client; the MCP server calls the Rezide API and returns clean JSON (ids + labels together). Validation/preview calls also emit a live
previewevent — the portal's right-hand "Draft listing" pane fills in as slots are captured. - Model reads the results and chains further calls —
get_projectauto-fills the location block,get_unit_detailsauto-fills floor, area, BHK, bathrooms, directions. The loop repeats until nothing is missing. - Model writes the reply: short text plus, when useful, a
uiblock — chips for short choices, a list for pickers, a card for confirmation. - Gateway saves the turn, separates the
uiJSON from the text, and responds with{reply, ui, tool_trace}. - Widget renders it. Tapping a chip simply sends that chip's value as the broker's next message — the loop starts again.
Sessions — the durable conversation journey
Chats are not throwaway: every session is a durable record with a lifecycle, and the Demo page is now a ChatGPT-style portal over exactly that.
- Deferred creation — opening a chat shows the greeting locally; the session is created server-side only when the first message is sent. Abandoned "new chats" leave nothing behind.
- Draft — an in-progress chat. Persisted to Mongo on every turn, so it survives restarts, reloads and gateway redeploys; the widget resumes it exactly where the broker left off, pending chips still tappable.
- Completed — the chat created its listing. It stays readable (transcript + confirmation card replay read-only) and badges green in the sidebar with its listing id.
- Deleted — the ✕ on a sidebar row (or
DELETE /api/session/:id) hard-deletes the transcript from memory and the store. Idle sessions can also be auto-purged by the retention window (REZIDE_SESSION_RETENTION_DAYS). - Listing & deletion are admin-gated — the embed key that ships inside host pages can never enumerate or destroy other brokers' chats; in keyed deployments the sidebar endpoints require
REZIDE_ADMIN_KEY.
Who is responsible for what
Widget SDK + portal
The face. Renders messages, chips, lists and cards; sends broker input to the gateway. Two embed modes: floating bubble for any host page, or inline full-page — the broker portal, with its sidebar of past chats (Draft / Completed), open-any-session, delete, and new-chat. Contains zero business logic.
Gateway server
The front door. An Express server exposing /api/auth/otp/send|verify (temporary broker login — OTP proxied to the auth service, tokens live only in the browser), /api/session (create), /api/chat/stream (send a message, SSE reply), /api/session/:id/history (resume), GET /api/sessions + DELETE /api/session/:id (portal sidebar — admin-gated), /api/health. Serves the demo pages and the widget file. Boots the MCP server as a child process on startup.
Agent loop
The brain-stem. Assembles the system prompt from the active domain's skills, runs the think→act→observe loop against the model, trims stale tool payloads to keep context lean, and extracts ui blocks from replies.
Session store
The memory. Every session persists to Mongo (file fallback when Mongo is absent) on every turn — chats survive gateway restarts and resume on any node. Powers the portal sidebar (list newest-first with Draft/Completed status), history replay, hard delete, and the retention purge.
Model provider
The brain. Talks to Claude through the official Anthropic SDK: converts our messages to the Messages API format, preserves the model's thinking between tool rounds, caches the skills prompt and the rolling conversation so repeat turns cost ~10% on cached input, and auto-recovers if a safety filter declines a request.
MCP client
The adapter between brain and hands. Connects the gateway to the MCP server over stdio, translates MCP tool schemas into the model's function-calling format, and executes the calls the model requests.
MCP tool server
The hands. Twenty-five tools across the five property families, each a thin, safe wrapper over one Rezide capability. Read tools are open; every create_*_listing is double-locked: it refuses unless the call carries confirmed:true, and env-level write guards must be on.
Payload composers
The form-fillers. Turn the agent's simple slot values ("2 BHK", "₹20L rent") into the exact JSON each listing API expects — per family, legacy duplicate keys both spellings, defaults applied, listing name computed. Validates completeness before any create.
Repo cache / ID mapper
The translator. Rezide's cascade returns display labels ("5 BHK", "Marble") but the create API stores database ObjectIds. This module caches the enum lists and translates label→id, so the model never touches a raw id.
Masters resolution
The lookup chain. Project search → project detail (address, towers) → unit search (filtered by unit type) → full unit spec. One selected unit auto-fills nearly the entire details step — that's why the chat needs so few questions.
Schema registries (5)
The rulebook data. One registry per property family; every field tagged with its source — ask the broker / derive from project / derive from unit / default. Defines the minimal ask-set per flow.
Skills (7)
The training. Written procedures loaded into the model every session: five family workflows (rezide-home-listing … rezide-land-listing) plus the shared conversation-behaviour (tone, Hinglish, lakh/crore rules, corrections, digressions) and visual-presentation (when to emit chips, lists, cards).
Validation probe
The scientist. A one-time harness that created and delisted test listings on dev, stripping one field at a time, to learn what the server truly enforces. Its findings (e.g. "send both spellings of duplicate keys") are baked into the composers and registries.
Rezide APIs
The system of record — untouched. Listings created by chat land in the same database, in the same pending moderation queue, as listings from the form. Broker identity comes from the same auth tokens the app uses.
How a listing actually gets created
The agent picks one of three payload shapes automatically, based on what exists in Rezide's masters:
A · Master unit
B · Custom unit
is_custom_unit.C · Custom project
onboarding_type:"custom". Verified live: Sunset Villa, Kihim.Whatever the variant, two gates always apply before the create call: the agent must show a confirmation card (built from a preview_listing dry-run) listing every value and every default, and the broker must explicitly say yes — create_listing physically refuses without confirmed:true. The result always lands as pending for moderation review.
Safety rails built in
- Nothing is created without an explicit yes — enforced twice: in the skill's instructions and in the tool's code.
- The agent never invents values — every field comes from the broker, the masters, or a default that's displayed on the confirmation card.
- Writes are environment-gated — creates require
REZIDE_ALLOW_WRITES=trueand a development host; the guard refuses anything else. - Our validation is the real gate — probe-proven fact: the listing API accepts any payload, so the composer's completeness check is what guarantees clean data.
- Prices are sanity-checked against the project's own price range from the masters; the broker is warned once, and their confirmation wins.
- Moderation backstop — chat can never publish directly; every listing waits in the same
pendingqueue as form submissions.