Hey Amara is an AI agent platform for recruitment agencies. Recruiters talk to it in plain language, and it reads and writes their applicant tracking system for them. As the founding engineering hire I built the backend underneath: the queue it runs on, the tenant boundary around it, and the memory that lets a conversation outlive its context window.

  • 13Months building the backend
  • 4Retrieval sources behind the email pipeline

The problem

A recruiter's question does not fit in one request. One answer can mean a research agent reading a company for a minute, an applicant-tracking write a human approves first, and a document generated before anything shows, all while a browser tab waits. Over synchronous HTTP, each of those holds its caller open, and every failure travels upstream.

Every conversation has to stay useful next month and stay inside one agency. A thread outgrows its context window, so it has to compact without losing the task in flight, and every query, cache read and retrieval carries a tenant identifier so the boundary holds even when calling code asks for the wrong thing.

The system I built

A queued agent backend with two tiers of memory.

What it is made of

  • A gateway that authenticates the caller and validates the tenant, then hands the request to the orchestrator as a durable message instead of a blocking call.
  • A three-stage router: a rule-based match that calls no model, then a fast classifier, then a router model. The chosen agent assembles its context, runs a bounded tool loop and streams back to the client.
  • Two tiers of memory: in-thread compaction for the live conversation, and a long-term memory service returning hybrid vector and graph retrieval, gated on consent.
  • Queue plumbing under all of it: durable topic exchanges per agent category, dead-letter queues with bounded retries and backoff, circuit breakers around failing consumers, and an explicit degraded mode when a dependency is down.

I moved the message-queuing layer from Redis to RabbitMQ and rebuilt the gateway, orchestrator, circuit breaker and service launcher around it. I built the authentication middleware and its hardening pass, the conversation compaction system, the email generation pipeline with four retrieval sources feeding a composition service, the streaming research agent, the human-in-the-loop confirmation cards for applicant-tracking writes, and the integration test suite. The long-term memory service and the routing broker I built against from the consumer side.

Key decisions

RabbitMQ, not Redis, for agent work. A chat request is now acknowledged at the edge and handed to the orchestrator as a durable message. Staying on Redis lists worked and cost nothing new to run. The broker bought load levelling, dead-lettering and per-queue back-pressure, and one slow agent stopped holding its caller open. The price is a broker to operate and at-least-once delivery, which makes idempotency and cancellation the application's job.

Compaction that preserves a schema, not a paragraph. When a thread crosses the token threshold, the compactor folds earlier turns into a validated structure of task state, personalisation, technical context and artifact references, then rewrites the conversation behind a lock with crash recovery. The cheap option was dropping the oldest turns. Structured state survives the boundary, so an agent mid-task still knows which artifact it was working on. The schema became an interface: versioned, and validated so a malformed model response fails loudly.

The outcome

The platform runs in production with the request path, the auth and tenant middleware, the compaction system, the email pipeline and the research agent inside it. The queue migration shaped everything after it: bounded pools, iteration caps and back-pressure are in the system because synchronous coupling had already failed once.