Inspiration
We kept watching phone-agent demos that completed a single call and stopped. The operational pain is the opposite case: two people already spoke, they already disagree, and the system has no type for contradiction. Email is a graveyard of partial truths. The awesome-phone-call-agents list is full of scheduling, qualification, and one-recipient confirmation. Nobody compiles a second call as a falsification test of the first. CallParity started from that gap, and from a cold-chain ticket where an $18,000-an-hour pallet sat between Dock 3 and Dock 4 while both sides were sure.
What it does
CallParity ingests a two-sided ops ticket and runs a closed loop:
- Preview compiles the Party A extraction and the Party B refutation plan without placing a call.
- Run parity executes Party A through CALL-E or a high-fidelity fixture behind the same CallePort.
- Claims are typed.
- A planner builds the cheapest observable questions that can confirm or falsify those claims.
- The leak check is structural: a question is dropped when Party B could recover what Party A asserted. The workbench shows the naive recap being dropped.
- Party B is called against that schema. Voicemail and silence are UNREACHABLE and never confirm.
- A merger emits a claim graph and one action card (RESTAGE_AND_RECALL, RELEASE_TRUCK, or HOLD_FOR_HUMAN) with quoted spans.
On FR-1842 the workbench also Imports two recorded CALL-E calls (GET only) and emits RESTAGE_AND_RECALL.
Demo seed FR-1842, controls FR-1900 and FR-1888.
How we built it
FastAPI engine, Vite/React workbench, Postgres plus Redis, CallePort. USE_FIXTURES=true is FixtureCalle; false is LiveCalleSdk (POST /v1/calls, GET /v1/calls/{id}). docker compose up -d --build boots it seeded. The reusable piece is ClaimKill, merged as CALLE-AI/awesome-phone-call-agents#220. The live adapter uses the official POST /v1/calls body (task, recipients phones, result_schema, metadata, Idempotency-Key). A diner hours call, call_Sv3d5Dt3jj0YabV9IJZh7g, reached a person who said they close at 11. The FR-1842 pair is also live: warehouse call_vzro922bOACJjf19ML7vQQ (pallet staged at dock 3) and driver call_2kxhpDvknUJ444kKfJLsyA (dock 3 empty, never saw PL-9F21). Importing those two records emits RESTAGE_AND_RECALL. USE_FIXTURES=true stays the compose default. CI never dials.
Then we hardened it, because an ops desk cannot run a demo. Alembic migrates the schema at boot under a Postgres advisory lock; four concurrent migrators on Postgres 16 serialize instead of racing DDL. Kill -9 the API mid-parity and the orphaned job converges to failed on reboot with an operator-facing error, and nothing redials on its own, because in live mode a silent retry would call humans back. Mutating routes share a rate limit per operator fingerprint, and floods without a valid token get metered by client IP into 429s before the 401 goes out. Every response carries an X-Request-ID, logs edact phone numbers, Hypothesis fuzzes that redaction, GET /metrics serves Prometheus text, and docs/OPERATIONS.md documents every environment variable for whoever deploys it. scripts/production_proof.sh runs the crash drill, the rate-limit demonstration, and a metrics scrape in about a minute.
Challenges we ran into
A token blacklist was not enough. A recap can leak without a banned phrase. The leak check had to become structural. Voicemail is a first-class UNREACHABLE. Fixtures so the demo cannot die on a busy signal. Crash recovery had one hard constraint: never re-execute automatically, because a retry in live mode dials a human back. The reconciler marks orphans failed, releases the idempotency key, and waits for an operator. Rate limiting had an ordering problem too. Metering after authentication gives forged-token floods unmetered 401s, so we meter by client IP before the 401 goes out.
Accomplishments that we're proud of
One compose command. On FR-1842, Import live records, then the RESTAGE_AND_RECALL card. 165 tests passed, 1 skipped (live dial). Operator token required on mutating routes, rotatable without downtime. ClaimKill merged upstream as PR 220. Two independent humans answered official outbound calls on FR-1842 (call_vzro922bOACJjf19ML7vQQ, call_2kxhpDvknUJ444kKfJLsyA). The demo video opens on Import, then the restage card, with those two call ids on screen. A third live call was placed through the API during the making of the demo video (call_5ZZm1bd1AfUySKDMwYFzyQ): the phone rang, the driver answered on tape, and the transcript captured dock three empty, pallet never seen.
What's next for CallParity
The live pair already exists. Fixtures stay the compose default. CI never dials. Domain packs that swap entity schemas. Export the claim ledger into a TMS.
Built With
- docker
- fastapi
- postgresql
- python
- react
- redis
- typescript
- vite
Log in or sign up for Devpost to join the conversation.