The problem

Operational software knows what was supposed to happen. When the plan breaks, it raises an exception and stops — because the information needed to recover is not in any connected system.

A carrier cancels a pickup. The TMS can tell you the shipment is at risk. It cannot tell you which of four other carriers can actually collect it before the warehouse closes at 17:30. That answer exists only in the heads of dispatchers, and the only way to get it is to call them.

So a human starts dialling. Meanwhile the clock runs.

What PHONEOPS does

PHONEOPS turns an exception into a recovery mission with a measurable objective and hard constraints, works out what it does not know, and uses CALL-E to find out.

The loop that matters:

EXCEPTION → OBJECTIVE → MISSING INFORMATION → CALL-E → EVIDENCE
   → CONSTRAINT CHECK → PLAN INVALIDATED → REPLAN → CALL-E AGAIN → RECOVERED

The flagship scenario, end to end:

  1. Carrier A cancels. Shipment #4821 must leave tonight; the warehouse cutoff is 17:30.
  2. PHONEOPS plans to use Carrier B, and records three things it cannot know without asking: availability, earliest pickup time, capacity.
  3. CALL-E calls Carrier B. The answer comes back as structured evidence: pickup_time = 18:00, confirmed, sourced to call #001.
  4. A deterministic constraint check fails: 18:00 ≤ 17:30 is false. The recovery strategy is invalidated — not retried, not escalated.
  5. PHONEOPS replans because of that evidence, and calls Carrier C.
  6. Carrier C confirms 16:45. Constraints satisfied. Departure protected, with 45 minutes to spare.

The call does not complete a workflow step. The call changes the plan.

Why this is a CALL-E project and not a phone bolt-on

Remove CALL-E and the engine has nothing to reason about. It is not a voice layer on top of a working system — it is the evidence acquisition layer for a system that cannot function without external facts.

PHONEOPS asks CALL-E for exactly the facts the mission is missing, through a strict result_schema with a *_confirmed flag per fact. That flag matters: a hedged answer — "we should probably be there around 5:30" — comes back unconfirmed, and unconfirmed evidence can never satisfy a hard constraint. It triggers a clarification call instead of a decision.

Three interchangeable modes behind one adapter: the Developer API (POST /v1/calls, GET /v1/calls/{id}, HMAC webhook), the MCP/CLI surface (plan_call, run_call, get_call_run), and a deterministic mock for CI and offline testing. Every call record stores which mode produced it, so a simulated run can never be mistaken for a real one — in the database, in the API, and on screen.

How we proved it is not scripted

The obvious objection to any demo like this is that the second call is hard-coded. So there is a script that runs the same mission twice, changing only what the carrier says on the phone:

Carrier B answers Strategy 01 Replans Second call
16:00 successful 0 never created
18:00 invalidated 1 Carrier C

Both runs recover the shipment. Only one ever creates a second phone operation, and that operation stores the evidence id, the call id and the violated constraint that caused it. make counterfactual — it needs no CALL-E key.

The boundary that makes it trustworthy

An LLM may propose The system decides
interpret the exception deadline and constraint comparison
generate a recovery hypothesis strategy invalidation
extract semantic evidence from a call state transitions
propose an alternative strategy policy, retry and replan limits

LLM_ENABLED=false is the default and the entire loop still runs. Models add semantics, never authority. A model cannot declare a hard constraint satisfied.

Other boundaries the product holds, each with a test:

  • A no-answer is not evidence. An unanswered call is retried, then changes the strategy. It is never recorded as "the carrier is unavailable" — and the UI says "could not be reached", never "missed the cutoff".
  • Silence is not an answer. A fact the carrier never mentioned produces no evidence record at all.
  • A deadline has two ends. A pickup time already in the past cannot satisfy "before 17:30", however true the comparison looks.
  • Autonomy is not authority. Gathering information and replanning run autonomously; a cost increase above the threshold stops at a human.
  • Escalation is a real outcome. When no candidate satisfies the mandatory constraints, PHONEOPS escalates. It does not invent Carrier E to finish the demo.

How the demo carriers answer

Carrier B and Carrier C are controlled PSTN test endpoints — two Telnyx numbers that answer with a fixed line, so the scenario repeats identically instead of depending on two people by a phone.

The answer still travels the whole way. CALL-E places a real outbound call over the network, hears the spoken reply, transcribes it and extracts the structured result. PHONEOPS receives evidence, not a payload:

Simulated Real
the carriers' personas the CALL-E call, and its call id
the shipment and its constraints the telephone network
the answers they give transcription and evidence extraction
constraint evaluation and the replan

The endpoint that plays them is mounted only behind a flag, holds no reference to missions or evidence, and cannot write to the database. A shortcut from there into the mission would be indistinguishable from hard-coding the demo.

How we built it

A modular monolith: FastAPI and async SQLAlchemy over PostgreSQL (17 tables of mission state), a Next.js control room fed by server-sent events, Alembic migrations, Docker Compose, CI.

Roughly 7,900 lines of Python and 187 tests, plus two gates that run before any recording: the counterfactual proof above, and a five-run reliability gate that checks the full causal chain each time.

Challenges

The interesting failures were not in the engine.

Next silently gzipped the event stream. The mission loop worked perfectly under curl and delivered nothing to a browser — because browsers advertise gzip and the encoder buffers. Diagnosed by testing the same endpoint with and without the header: 31 events versus 0.

Events were published before they were committed. Subscribers were told "something changed" about state no other transaction could read, so the UI spent its only signal, re-read the old state and froze there. Now each event commits before it is published — which also means a mission that crashes mid-run keeps the record of the calls it really placed.

The interface said something false about a carrier. On an unanswered call, the strategy diff read "Carrier B — misses the pickup cutoff" for a carrier that never picked up. Exactly the claim the product promises not to make. Four failure paths had been covered by tests but never actually looked at; rendering them found five defects, of which that was the worst.

What we learned

Tests are not enough, and neither is a passing demo. Every defect above passed a green suite. Two habits found them: driving the real interface in a real browser rather than trusting the API, and asking whether the numbers on screen are plausible rather than merely consistent — "time to recovery: 4 s" for a loop that visibly takes seconds is a metric measuring the wrong thing.

What's next

The recovery templates are already generic — the same engine runs supplier capacity loss and field-service dispatch, with only the constraints changing. The community contribution, mission-replan-loop, packages the primitive on its own, with zero dependency on this codebase, so any agent can let a phone answer invalidate a plan.

The honest gap: PHONEOPS decides who to call from a ranked candidate list. Real deployments would want that ranking informed by history — which carrier actually answers, which one's quoted times hold. That is the next thing worth building.

Built With

Share this project:

Updates

Submission history