Everyone is talking about self-improving agents right now. This is the honest version: the agent doesn't rewrite itself. It notices things and hands you the decision.
Tonight you'll build:
- A phone agent that calls you, checks order status with one tool, gives up gracefully when that tool is slow, and transfers anything it can't handle
- A test suite that replays your agent's logic in under a second
- A review loop that reads your call history and turns repeated behavior into plain-English questions
- A local dashboard where you answer those questions with one click
01What you'll need
Set these up before the session, so you spend it building instead of filling in signup forms.
Vonage
Sign up with your own mobile number. A free trial can only call the number it was registered with, and that's the number the agent will ring.
Sign up free →Deepgram
Speech-to-text, the AI model, and the voice. New accounts come with free credit, more than enough for tonight.
Sign up →On your laptop you'll need Node.js 20 or newer, git, and ngrok, logged in with ngrok config add-authtoken <your-token>. If you use a coding agent like Claude Code, it can do the setup for you:
Help me get this computer ready for the Vonage voice agent workshop.
Check whether Git, Node.js 20 or newer, and npm are installed. Install anything missing using the official installer or my system's package manager. Then check whether ngrok is installed, and install it if it isn't.
Run git --version, node --version, npm --version, and ngrok version yourself and tell me the results. Don't create any accounts or enter any keys for me. I'll do that part.
.env file that git ignores, so they never get committed. When you're done, delete the Vonage application from the dashboard. That kills the key.02How it works
A voice agent has two very different jobs, and the trick is keeping them apart.
The live call has to be fast. Someone is waiting on the line. So the agent gets one job, one tool, a hard 1.5-second limit on that tool, and a fixed line to say when anything goes wrong. When a caller asks for something else, it hands them to a person instead of improvising.
Improving the agent happens later, offline. After every call, the agent writes a record of what was said, what the tool returned, how long it took, and how the call ended. Afterwards, two small programs read those records.
| Piece | What it does | Where it lives |
|---|---|---|
| The agent | Answers the phone, looks up orders, hands off | src/ |
| The tool | A fake order database with a few test orders | src/tools/order-status.ts |
| Call records | One row per call, written before hang-up | data/calls.db |
| The review loop | Finds patterns in calls and proposes tests | loops/src/review/ |
| The eval runner | Replays every test against the agent's logic | loops/src/runner/ |
| The dashboard | Shows the questions and lets you answer them | loops/src/dashboard/ |
The review loop never changes the agent, and it doesn't use an AI model. It counts. When the same thing happens enough times, it writes a question, and you decide.
03The build
Every step tells you what you should see. If you don't see it, jump to If you get stuck.
1Clone and install
$ git clone https://github.com/Vonage-Community/workshop-voice-node_deepgram-agent_loops.git $ cd workshop-voice-node_deepgram-agent_loops $ npm install
found 0 vulnerabilities twice. One install sets up both the agent and the loops.2Create your Vonage application
- Open the Vonage dashboard → Applications and create a new application. Name it something like
voice-agent-workshop. - Click Generate public and private key. A file called
private.keydownloads. Move it into theworkshop-voice-node_deepgram-agent_loopsfolder, keeping that exact name. - Turn on Voice. For the Answer URL and Event URL, enter
https://example.com/answerandhttps://example.com/event. This app sends its own URLs with every call, so these are never used. - Create the application with the button at the bottom of the page, then copy the Application ID.
3Get your Deepgram key
In the Deepgram console, go to API Keys and create a key. Copy it now; Deepgram only shows it once.
4Start the tunnel
In a new terminal window:
$ ngrok http 3000Forwarding line with an https://… URL. Copy it and leave this window running.5Fill in your settings
$ cp .env.example .envOpen .env and set these four:
| Setting | What goes there |
|---|---|
VONAGE_APP_ID | The Application ID from step 2 |
MY_PHONE_NUMBER | The mobile number you signed up to Vonage with. Digits only, country code first: 15551234567 |
DEEPGRAM_API_KEY | The key from step 3 |
BASE_URL | The ngrok URL from step 4 |
Leave everything else as it is. VONAGE_NUMBER and SUPPORT_PHONE_NUMBER stay blank on a trial account.
6Start the agent
$ npm run devOrder-status voice agent listening on :3000. Leave it running.7Call yourself
In a third terminal window:
$ npm run call-meCall … placed. Answer your phone. Your phone rings, showing caller ID 123456789, the test number Vonage uses on trial accounts.Answer it. The agent introduces itself. Say:
"Where is order A1001?"
It looks the order up and tells you it's in transit. In the npm run dev window you'll see the whole conversation go by: what you said, the tool call, how long it took, and finally outcome=completed.
8Open the dashboard
$ npm run dashboardOpen localhost:4000. Your call is under Recent Calls. The other panels are empty, and each one tells you the command that fills it.
9Run the loop
Run these in order, and watch the dashboard after each one.
$ npm run eval4 passed, 0 failed. These four hand-written tests are the agent's job description: answer known orders, fall back when the lookup is too slow, treat "not found" as an answer, and hand billing questions to a person. No phone call happens. The runner replays the agent's real decision code with a fake tool answer plugged in.$ npm run seed-calls10 inserted. That's ten days of made-up call history, so there's something to find.$ npm run review"3 callers asked about returns and got transferred. Is transferring the right behavior, or should this agent handle returns?"
Click Add to Suite on one. It moves to Decisions → Working as intended. Then run the tests again:
$ npm run eval10Make a pattern of your own
Now give the loop something real to find. Run npm run call-me twice more, and both times say:
"Hi, I want to cancel my order."
The agent says it'll connect you to support. On a trial account there's no one to transfer to, so just hang up. The handoff is still recorded.
After the first cancellation, run npm run review. Nothing new: one call isn't a pattern. After the second, run it again.
04Make it yours
Change the voice
Open .env and set:
DEEPGRAM_SPEAK_MODEL=aura-2-draco-en AGENT_VERSION=order-status-v2
aura-2-draco-en and aura-2-pandora-en are British, aura-2-hyperion-en is Australian, and there are about forty more. Changing AGENT_VERSION labels every call from now on, so you can tell the new agent from the old one.
Settings are read when the server starts, so stop npm run dev (Ctrl-C), start it again, and run npm run call-me. The new call shows up on the dashboard as order-status-v2.
Try to trip it up
| Say | What should happen |
|---|---|
| "Where is order A1002?" | Delivered, with a date |
| "Where is order SLOW123?" | Any ID starting with SLOW takes too long. You'll hear the fallback line, not a made-up answer |
| "Where is order XYZ999?" | Not found. That's an answer, not a failure |
| "There's a charge on my card I don't recognize" | Handed off as billing |
| "Can you connect me to Dave? I think he works there" | Handed off as a request for a person. One call is enough for a proposal, because a caller asking for a human is always worth a look |
| Ask about two different orders | The second one hits the one-lookup-per-call limit |
Call enough times and run npm run review again. Patterns you created start showing up as questions.
Break it on purpose
This is what the test suite is for. Open src/agent/tool-policy.ts and find the line that starts with ["billing", …. Delete it, save, and run:
$ npm run evalNow put it back:
$ git checkout src/agent/tool-policy.ts05Point it at a real API
The agent's one tool is a fake order database. Swap it for something real, and keep the rules that make it safe on a phone call: one lookup per call, a 1.5-second limit, and the fixed fallback line when it fails. These need no API key, and their answers are short enough to say out loud:
- Open-Meteo — weather anywhere"Do I need an umbrella in Brooklyn tomorrow?"
- PokéAPI — every Pokémon, type, and move"What's Pikachu weak against?"
- Open Library — books and authors"Who wrote The Left Hand of Darkness, and when?"
- REST Countries — capitals, currencies, populations"What currency do they use in Peru?"
- TheMealDB — recipes by name or ingredient"Give me a dinner idea with chickpeas."
- icanhazdadjoke — exactly what it sounds like"Tell me a joke."
It's a bigger change than tonight's steps, so let a coding agent do the work, and let the tests tell you if it went right:
In this project, the voice agent's one tool is a fake order lookup in src/tools/order-status.ts. Replace it with a real call to [API NAME] ([LINK TO ITS DOCS]) so the agent can answer [WHAT CALLERS WILL ASK].
Keep the same rules: one tool call per phone call, a 1500ms timeout, and the fixed fallback line when the call fails or times out. Update the agent's instructions in src/agent/agent-config.ts to match the new job.
Then update the four test cases in loops/eval-cases/seed-cases.json for the new tool. Run npm test and npm run eval, and don't stop until both pass.
06Quick reference
All commands run from the project folder.
| Command | What it does |
|---|---|
npm install | Installs the agent and the loops |
npm run dev | Starts the agent server on port 3000 |
npm run call-me | Rings MY_PHONE_NUMBER and connects it to the agent |
npm run dashboard | Opens the dashboard on localhost:4000 |
npm run eval | Runs every test and reports pass/fail |
npm run seed-calls | Loads ten days of sample call history |
npm run review | Reads the calls and writes proposals |
| File | What it is |
|---|---|
.env | Your settings and keys. Never committed |
src/agent/agent-config.ts | The agent's instructions and models |
src/agent/tool-policy.ts | Which requests get handed to a person |
src/tools/order-status.ts | The fake order database |
loops/eval-cases/seed-cases.json | The four hand-written tests |
data/calls.db | Every call, every test, every decision |
07If you get stuck
"Set these in .env first: …"
It lists exactly which settings are missing. Fill them in and try again.
"Vonage returned 401"
The Application ID and private.key don't match. Check that private.key is the file downloaded when you created this application, and that it's in the project folder.
The call is placed, but your phone doesn't ring
On a trial account, MY_PHONE_NUMBER must be the exact number you signed up with: digits only, country code first, no + or spaces.
Your phone rings, but you hear nothing or it hangs up
Vonage couldn't reach your laptop. Check that ngrok is still running and that BASE_URL matches its current URL. If you restarted ngrok, the URL changed: update BASE_URL, then restart npm run dev.
"NODE_MODULE_VERSION … requires …"
The database module was built for a different Node version than the one you're running, which is common with nvm. Run npm rebuild better-sqlite3, then the same command again inside loops/.
A call doesn't show up on the dashboard
Calls are recorded when they end. Hang up, and give the dashboard a few seconds to refresh.
Your trial account can't place calls at all
Ask the organizer. There are backup credentials for tonight.
Or hand it to your coding agent:
I'm following the Vonage voice agent workshop guide at https://vonage-community.github.io/workshop-voice-node_deepgram-agent_loops/ and I'm stuck at step [NUMBER]. Here's what I see: [PASTE THE ERROR, OR DESCRIBE IT].
The project is in this folder. Check that .env has VONAGE_APP_ID, VONAGE_PRIVATE_KEY_PATH, MY_PHONE_NUMBER, DEEPGRAM_API_KEY, and BASE_URL filled in, without showing me their values. Check that private.key exists, that BASE_URL matches the ngrok URL that's running right now, and that npm run dev is running.
Explain what's wrong in plain language and fix it if you can.
Keep Going After Tonight
Run the review on a schedule and email yourself the questions. Put npm run eval in CI, so every change to the agent is tested before it ships. The whole story behind each piece is in the blog series: Add Tools and Human Transfer to a Vonage + Deepgram Voice Agent.
Have fun, and bring the weird one to the next build night.