VoxCircuit: voice and chat that can book and update records, with a person approving the risky parts
Phone, web voice and web chat land in one conversation record. The AI agent can only propose an action; a plain-code policy decides whether it runs, waits for a person, or is refused. The product is called VoxDesk; the repository is VoxCircuit.
At a glance
Letting an AI answer the phone is easy. Letting it change your records is the hard part.
A chatbot that only talks can be wrong and nobody is hurt. Once it can book, cancel, update a contact or call someone back, every action needs to be the right customer, the right business, not a duplicate, and allowed.
VoxCircuit treats whatever the model asks for as a request, not a permission. The request is checked, scored and either run, held for a person, or refused, and every step is written down.
Proposal
During a call or chat the agent proposes a tool call, for example reschedule_appointment.
How it works
Each stage as it exists in the repository. Select a stage to see what it uses and what happens when it fails.
Phone, web voice, web chat
Each channel is normalised into one Conversation (a phone call also gets a Call record)
Telnyx adapter, ElevenLabs adapter, authenticated POST route
Simulation mode is the default; live mode refuses to start without full configuration
Execution sequence
One item, traced through the system boundaries.
- Caller to Voice agent: “Can I move Thursday to Friday?”
- Voice agent to VoxCircuit: propose reschedule_appointment
- VoxCircuit to VoxCircuit: schema + token + policy (identity not verified → ESCALATE)
- VoxCircuit to Staff approver: approval request (15 min)
- Staff approver replies to VoxCircuit: approve exact payload
- VoxCircuit replies to Voice agent: rescheduled; result stored once
- Voice agent to Caller: “You’re booked for Friday.”
One item, start to finish
A web voice session starts against a verified agent version; a Conversation and its state are created.
The caller asks to reschedule. The agent proposes the tool call with the new time.
Rescheduling needs a verified identity. It was not verified, so the policy escalates.
A staff member approves the exact payload. The change runs once and the decision is in the audit log.
The policy that decides what the agent may do
Pick the tool the agent wants to call and set the session facts. The scoring is the repository’s deterministic policy, copied rule for rule.
The voice or chat agent can only propose a tool call. This policy, in plain code, decides whether it runs, waits for a person, or is refused.
Logic copied from VoxCircuit lib/security/tool-governance.ts (policy version 2026-08-12.1). Inputs are yours to change; nothing here calls a model or a server.
Architecture
The layers and the decision path, taken from the repository code.
| Layer | Component | Tech Stack | Responsibility |
|---|---|---|---|
| Layer 1: Channels | Telephony and voice adapters | Telnyx (PSTN/SIP), ElevenLabs (realtime voice), simulation provider | Transport only; no business decisions |
| Layer 2: Conversation | Canonical conversationKEY | Next.js 16 API routes, Prisma, PostgreSQL 16 | One record per interaction, persisted state |
| Layer 3: Authority | Tool governanceKEY | Zod, signed context, deterministic policy, approvals | Decides ALLOW, DENY or ESCALATE |
| Layer 4: Operations | Domain services and audit | Serialisable transactions, event inbox, background jobs | Contacts, appointments, tasks, handoffs, reconciliation |
- Call / chat goes to Agent proposes: turn
- Agent proposes goes to Policy: tool call
- Policy goes to Execute once: ALLOW
- Policy falls back to Human approval: ESCALATE
- Policy falls back to Refused: DENY
- Human approval goes to Execute once: approved
Decisions and trade-offs
| Decision | Instead of | Why | Where in the code |
|---|---|---|---|
| The model proposes; code decides | Letting the agent call business APIs directly | A plausible payload is not permission. Policy, identity and approval sit between the model and the database. | lib/voice-agent/tool-executor.ts, lib/security/tool-governance.ts |
| Execute-once fingerprints | Trusting retries not to repeat | Network and agent retries are normal. The same action returns the stored result instead of booking twice. | lib/voice-agent/tool-executor.ts (operationFingerprint) |
| Simulation by default | Live telephony in development | Simulation creates sim_ records and never touches a carrier, so the whole flow can be exercised without paid calls. | lib/telephony/mode.ts, lib/telephony/providers/simulation.ts |
| Verify provider events on the raw body | Accepting webhooks after JSON parsing | Signatures are checked on the exact bytes received, with a five-minute window and event-ID de-duplication. | lib/telephony/providers/telnyx.ts |
What went wrong first
Web voice said “ready” too early
The first version decided the voice demo was ready before the provider had actually authorised a conversation. The fix made the signed conversation URL the readiness boundary and moved provider authorisation behind the authenticated bootstrap step.
Pull request #31, 14 August 2026 ↗An unchecked agent identifier
The voice bootstrap accepted the configured agent identifier without validating it. A follow-up change added the check.
Pull request #34, 15 August 2026 ↗Results
Only numbers produced by a committed, repeatable script appear here as measured. Until then, each result says how it will be measured.
Only stages that can be run locally will be timed; anything that needs live Telnyx calls is labelled not measured.
ALLOW / ESCALATE / DENY counts for a committed list of scenarios, run through the repository’s test suite.
Only measurable with a client running live telephony.
Run it yourself
Local development from the README. Telephony stays in simulation mode. Full instructions ↗
git clone https://github.com/arslanvuzmal/VoxCircuit.git
cd VoxCircuit
npm ci
cp .env.example .env.local
docker compose up -d
npx prisma migrate dev
npm run devQuestions people ask first
Can the AI change records on its own?
Only low-risk actions that pass the policy. Anything scored 60 or above, or needing a verified identity it does not have, waits for a person with approval permission.
What happens if the same request arrives twice?
Each action carries a fingerprint of its exact payload. A repeat returns the first result instead of running again.
Does it make real phone calls?
It can, through Telnyx and ElevenLabs, once those accounts and secrets are configured. Out of the box it runs in simulation mode and never contacts a carrier.
Is there a demo I can try?
Yes, the public demo at vox-desk.vercel.app/demo.
Related reading & systems
Want VoxCircuit working in your business?
Book a 20-minute scoping call. I will review your current systems, map where manual friction is costing you time, and give you an honest quote.