Skip to main content
Bluejay runs simulated callers, Digital Humans, against a voice agent and scores what happens: happy paths, interruptions, accents, the edge cases you would never sit through by hand. It reaches an agent over CHIRP, its own WebSocket protocol. The bluejay-aai-bridge repo is the piece in between. It translates CHIRP to the Voice Agent API so simulations run against the agent you actually ship:
The bridge is transport only. It doesn’t define an agent: you give it the id of a stored agent you already run, and the prompt, voice, tools, and turn detection all come from that agent. You need:

1. Clone and configure

Add your key and your agent to .env. The agent id is in the agent’s URL in the dashboard:
.env
That is the whole setup. If you’d rather keep the agent in the repo as a file you can edit between simulation runs, pull it in and publish it back when you change it:

2. Call it yourself first

call.py speaks CHIRP the way Bluejay does, so a call that sounds right through it will sound right in a simulation. It records the agent to out/agent.wav.
Give it something to say and it will hold a conversation:
Lower --delay so the question starts while the greeting is still playing, and you have tested barge-in.

3. Host the bridge

Bluejay dials in, so the bridge needs a public wss:// address. Any host that runs Python, terminates TLS, and passes WebSockets through will do. Before hosting, set the Basic-auth credentials Bluejay will send:
.env
Use the Railway template, or from an existing project: NewGitHub Repo → the bridge repo. Railway reads .python-version and requirements.txt to build, and the Procfile for the start command.Under Variables set ASSEMBLYAI_API_KEY, AGENT_ID, CHIRP_USER, and CHIRP_PASS; PORT arrives on its own. Then under SettingsNetworking generate a domain, and give Bluejay wss://<that domain>/voice.
To try it before deploying anything, put a tunnel in front of it. Bluejay only needs a reachable wss:// URL:
Two things to watch, both of which cost you failed simulations rather than errors you can see:
  • Don’t run it on an instance that sleeps when idle. Simulations arrive in bursts after a long quiet period, which is exactly when a scaled-to-zero instance is cold. The first calls of the run time out on the WebSocket upgrade, and Bluejay records them as INCOMPLETED.
  • Concurrent simulations need concurrent sessions. Each call is one Voice Agent session on your account. Past the limit AssemblyAI returns concurrency_exceeded, which the bridge passes to Bluejay as a failed call.

4. Point Bluejay at it

In Bluejay, create an Agent with connection type Websocket, URL wss://<your-host>/voice, and the same CHIRP_USER and CHIRP_PASS you set on the host. Run a simulation.

After a simulation

Bluejay has the transcript, the recording, and the evaluations. AssemblyAI has its own record of the same call, with a stereo recording, per-turn timings, and which replies were cut off:
The bridge prints the session id when each call ends. With BLUEJAY_API_KEY set, that id is also written onto the Bluejay simulation result, so the two records point at each other. See Recordings and transcripts.

Configuration

A .env next to bridge.py is loaded on startup, and real environment variables win over it.

How the two protocols meet

Two details are worth knowing, because both are the difference between a simulation that measures your agent and one that measures the bridge:
  • Reply audio is paced. The API sends a reply faster than real time. Forwarding it straight through would put seconds of speech in Bluejay’s playback buffer, and an interruption would arrive to find the agent already committed to talking. The bridge stays at most 200 ms ahead, so reply.done with status: "interrupted" actually stops the voice.
  • Hanging up ends the session. When Bluejay closes the call the bridge sends session.end and waits for session.ended. Dropping the socket instead leaves the session resumable, and billable, for another 30 seconds, which over a suite of simulations is real money.

Cost

Every simulated call is a Voice Agent session billed to the API key in your .env, and Bluejay bills its own side. Running a suite costs real money, so keep CHIRP_USER and CHIRP_PASS set on anything hosted: without them, anyone who finds the URL can start sessions on your key.