chat-listing agent

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 thinkscalls a toolreads 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:

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

Broker types a message GATEWAY — runs the loop for this session Model (Claude) reads skills + history thinks, decides next step up to 10 rounds MCP tools (25) search_projects · get_unit_details preview_listing · create_listing … each call hits a real Rezide API calls tool reads result Reply text + chips / card

This is a real trace from our first verified conversation — one broker message, four tool calls, then the reply:

broker: "List my 2BHK in Oberoi 360 West for rent, 1L rent, 5L deposit" tool 1: whoami → broker_id, firm "Popaya Test" tool 2: search_projects → Oberoi 360 West (674176…) tool 3: get_project → Worli Naka · towers A,B · rent range 18L–60L tool 4: search_units → Apartment units in both towers agent: "Got it — rent, ₹1L/month, ₹5L deposit. One flag: rents here run 18L–60L, so ₹1L is well off-range — confirm and I'll carry on. Which tower, and what's the unit number?" [chips: Tower A · Tower B]

The full stack

packages/widget-sdk/widget.js

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.

bubble or inline portalchips — tap to answerlists — pick a project/unitcards — confirm & successvoice notesnewChat / openSession API
↓  HTTP JSON — POST /api/session, POST /api/chat/stream, GET /api/sessions, DELETE /api/session/:id  ↓
services/gateway/

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.

agentic loop (≤10 tool rounds)skills → system promptdurable sessions (Mongo / file)usage caps + counters (Redis)session list · delete (admin-gated)model provider (Claude)prompt caching
↓  MCP over stdio — the open tool protocol  ↓
domains/listing/mcp/

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.

search_projects · get_projectsearch_towers · search_unitsget_unit_detailsreference_datasearch_locationsvalidate · preview · create (Home)validate · preview · create × Office / Retail / Land / Industrialget_my_listings · whoami · get_listing
↓  HTTPS + broker's Bearer token  ↓
broker.development.rezide.in · api.rezide.in

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.

listing service /v1/adminproject / tower / unit mastersrepositories (enums)auth service /api/v1/auth

One broker message, step by step

  1. Widget sends the text to /api/chat/stream with the session id and shows a typing indicator (if this is the chat's first message, it mints the session via POST /api/session first — opening a chat alone creates nothing).
  2. 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.
  3. 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.
  4. 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 preview event — the portal's right-hand "Draft listing" pane fills in as slots are captured.
  5. Model reads the results and chains further calls — get_project auto-fills the location block, get_unit_details auto-fills floor, area, BHK, bathrooms, directions. The loop repeats until nothing is missing.
  6. Model writes the reply: short text plus, when useful, a ui block — chips for short choices, a list for pickers, a card for confirmation.
  7. Gateway saves the turn, separates the ui JSON from the text, and responds with {reply, ui, tool_trace}.
  8. 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.

Who is responsible for what

Widget SDK + portal

packages/widget-sdk/widget.js · public/demo.html

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.

Think of it as: a WhatsApp window you can embed anywhere — or expand into a full ChatGPT-style workspace.

Gateway server

services/gateway/src/server.ts

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.

Think of it as: the receptionist who routes every request to the right place.

Agent loop

packages/agent-runtime/src/agent.ts

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.

Think of it as: the project manager who keeps the conversation's memory and drives the model to a finished answer.

Session store

packages/agent-runtime/src/session.ts · stores.ts

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.

Think of it as: the filing room where every conversation is kept until you shred it.

Model provider

packages/agent-runtime/src/llm.ts · llm-anthropic.ts

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.

Think of it as: the phone line to the intelligence.

MCP client

packages/agent-runtime/src/mcp-client.ts

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.

Think of it as: a universal power adapter — model on one side, tools on the other.

MCP tool server

domains/listing/mcp/server.ts

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.

Think of it as: a toolbox where the dangerous tools have safety catches.

Payload composers

domains/listing/mcp/payload.ts · office- · retail- · land- · industrial-payload.ts

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.

Think of it as: the clerks who fill the official forms perfectly, every time.

Repo cache / ID mapper

domains/listing/mcp/repo-cache.ts

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.

Think of it as: the interpreter between human words and database ids.

Masters resolution

domains/listing/mcp/masters.ts

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.

Think of it as: the filing cabinet that knows every building, tower and flat.

Schema registries (5)

domains/listing/schema/*-schema-registry.json

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.

Think of it as: the syllabus — what must be collected, and from where.

Skills (7)

domains/listing/skills/* · shared-skills/*

The training. Written procedures loaded into the model every session: five family workflows (rezide-home-listingrezide-land-listing) plus the shared conversation-behaviour (tone, Hinglish, lakh/crore rules, corrections, digressions) and visual-presentation (when to emit chips, lists, cards).

Think of it as: the employee handbook — job process, personality, and dress code.

Validation probe

domains/listing/probe/probe.ts

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.

Think of it as: the lab experiments the implementation's facts came from.

Rezide APIs

dev listing service + auth service

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.

Think of it as: the same back office; chat is just a new front desk.

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

Project, tower and unit all exist in Rezide's database
Everything references database ids; the unit cascade fills floor, area, BHK, baths, directions automatically. Verified live: A-601, Oberoi 360 West.

B · Custom unit

Project exists, the specific unit doesn't
Unit number stored as free text; the broker supplies floor, area, configuration and bathrooms; flagged is_custom_unit.

C · Custom project

Property isn't in Rezide's masters at all
Project and location stored as typed-in names, flagged 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 yescreate_listing physically refuses without confirmed:true. The result always lands as pending for moderation review.

Safety rails built in