Skip to content

Commit fc43128

Browse files
cnpierrepapicnpierrepapi
andauthored
feat(call-state-reconciler): read what actually happened to a call (#337)
A finished call hands back a status word, a task_completed boolean and a confidence score. task_completed is the one everything branches on, and it comes back true for a voicemail box: the recording answers, the agent asks its question into the tone, and the call ends completed with a schema-valid result. Nothing in that payload is wrong. There is no field in it that says a person was on the line. Adds skills/call-state-reconciler, which reads a call as three separate answers, how it ended, whether the job got done, and whether the answer came from anybody speaking, each carrying whether it was quoted from a field, inferred from other fields, or absent because nothing carries it. Adds apps/web/asheard, a runnable console over the same engine, which ships as asheard on npm and is published from CI with provenance. Paste a payload, read a batch worst-first, or see which endings each surface can actually express. All of that runs on fixtures with no credentials. The one route that can dial reads back only a call it placed, checked by a token it issued, and answers with an allowlisted projection rather than the payload. Transcript turns cross as a count, never as words, and anything phone-shaped is masked wherever it appears. The client sends a bearer token to the API origin and nowhere else unless a caller writes that down explicitly. No dialable number ships in the tree; the built-in destination list is the range reserved for fiction, and an operator adds their own through the environment. The skill's references cover what was measured against the live API and how to repeat it, including the attempt timestamps being rewritten shortly after a call settles, which makes duration depend on when you looked and rounds a sub-second attempt to zero. 111 tests in the app, no network and no credentials, run against the package's own fixtures. Repository validation passes. Co-authored-by: cnpierrepapi <[email protected]>
1 parent 90c3ed2 commit fc43128

62 files changed

Lines changed: 13782 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ This project is an awesome list for AI-agent phone-call workflows. Add resources
145145
- [`scope-signal`](skills/scope-signal/) - Previews and reconciles one authorized CALL-E project-brief verification call, producing transcript-evidenced GO, CAUTION, or NO-GO guidance while leaving acceptance to a human.
146146
- [`verify-by-phone`](skills/verify-by-phone/) - Single disclosed verification call that checks a directory listing against the published line, grounds every answer in a transcript span, and abstains instead of guessing when the call does not establish one.
147147
- [`verity-verification-core`](skills/verity-verification-core/) - Gates a CALL-E phone task's `task_completed` claim behind an independent transcript read-back before any real-world action; catches a mid-sentence self-correction, voicemail, or a value the caller never confirmed, returns ALLOW or BLOCK plus the exact value to re-confirm on a second channel, and is deterministic, no-network, and fail-closed.
148+
- [`call-state-reconciler`](skills/call-state-reconciler/) - Works out what actually happened to a call by reading the call task, its attempts and its event stream together, and names the field behind each conclusion. Covers the states where the platform's own signals disagree.
148149
- [`call-summarizer`](skills/call-summarizer/) - Post-call analysis skill that turns a completed CALL-E transcript into a masked, actionable brief with a one-line outcome, extracted action items with owners and due dates, caller sentiment, and a one-way caller fingerprint for de-duplicating repeat callers.
149150
- [`ringer-consumer-tasks`](skills/ringer-consumer-tasks/) - Compose and safely place the dreaded consumer phone calls (bill negotiation, cancellation, refund, booking, quote comparison, inquiry) as CALL-E tasks with strict result schemas, dry-run-by-default previews, and human-in-the-loop decision authority.
150151
- [`incident-escalation-call`](skills/incident-escalation-call/) - Walks an on-call escalation ladder one phone call at a time and records an acknowledgement only when an owner and an ETA are both quoted by words the recipient spoke, then re-reads the call over a second transport before the incident is reported as owned.
@@ -189,6 +190,7 @@ Runnable demo apps live under [`apps/`](apps/). They are not a CALL-E SDK and do
189190
| [`apps/typescript/sparescout`](apps/typescript/sparescout/) | TypeScript | Approval-gated multi-supplier vehicle-part sourcing with strict fitment schemas, durable evidence history, interruption-safe CALL-E monitoring, global market localization, and a no-call fixture default. |
190191
| [`apps/typescript/asyncfounders`](apps/typescript/asyncfounders/) | TypeScript | Callback-first persistent team memory: consented CALL-E interviews capture updates, brief unseen company deltas, and resolve open questions into evidence-linked typed memory. |
191192
| [`apps/typescript/one-more-story`](apps/typescript/one-more-story/) | TypeScript | Consent-first oral-history call that discloses AI use, preserves the storyteller's correction, and creates no story until the corrected read-back is explicitly confirmed. |
193+
| [`apps/web/asheard`](apps/web/asheard/) | TypeScript / Next.js | Reads what actually happened to a call as three separate answers rather than one status word, shows the field behind every sentence, and ranks a batch by what costs something if ignored. |
192194
| [`apps/web/callproof`](apps/web/callproof/) | Ruby / Python | Closed-loop CALL-E workflow that checks transcript evidence against an immutable call contract and routes policy exceptions to persisted AgentKit human review. |
193195
| [`apps/web/centre-remix`](apps/web/centre-remix/) | JavaScript / Node | SAMPLE shopping-centre leasing board: humans and agents share one floor plan; `place_tenant_call` writes honestly fake-only CALL-E-shaped fixtures (public function never dials; reserved SAMPLE phones; masked responses). |
194196
| [`apps/typescript/evidence-grounded-callback`](apps/typescript/evidence-grounded-callback/) | TypeScript | Compiles owner-reviewed source evidence and positive callback consent into a masked CALL-E preview and separately gated MCP plan. |

‎apps/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Current apps:
1919
| [`python/audition-agent`](python/audition-agent/) | Python / React | Catalog and setup guide for producer-reviewed CALL-E role-disclosure callbacks, with upstream source and no-call verification. |
2020
| [`typescript/e-mploye-for-calle`](typescript/e-mploye-for-calle/) | TypeScript | Human-approved virtual employee for appointment, lead follow-up, and shift coordination workflows, with a fake-only public mode and opt-in CALL-E SDK integration. |
2121
| [`typescript/asyncfounders`](typescript/asyncfounders/) | TypeScript | Callback-first persistent team memory: consented CALL-E interviews capture updates, brief unseen company deltas, and resolve open questions into evidence-linked typed memory. |
22+
| [`web/asheard`](web/asheard/) | TypeScript / Next.js | Reads what actually happened to a call as three separate answers rather than one status word, shows the field behind every sentence, and ranks a batch by what costs something if ignored. Fixture and paste paths need no credentials; the one page that dials uses fixed non-personal destinations behind a fail-closed budget. |
2223
| [`web/fieldclose`](web/fieldclose/) | TypeScript / Next.js | Human-approved commercial HVAC closeout workflow with a fake-only public path, durable recipient suppression, one-attempt duplicate protection, structured CALL-E results, and explicit human disposition. |
2324
| [`web/supplycall-ai`](web/supplycall-ai/) | TypeScript / Next.js | Catalog and setup guide for SupplyCall AI: procurement exception recovery that uses CALL-E for one supplier confirmation call, deterministic resolve/escalate rules, shared-number-pool outbound, and Vitest no-call verification. |
2425
| [`typescript/kincall`](typescript/kincall/) | TypeScript | Consent-first check-in and trusted-circle coordination: a stated request for help overrides the agent's own judgement, contacts are called one at a time until somebody commits, and the monitored person is called back with the outcome. |

‎apps/web/asheard/.gitignore‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.*
7+
.yarn/*
8+
!.yarn/patches
9+
!.yarn/plugins
10+
!.yarn/releases
11+
!.yarn/versions
12+
13+
# testing
14+
/coverage
15+
16+
# next.js
17+
/.next/
18+
/out/
19+
20+
# production
21+
/build
22+
23+
# misc
24+
.DS_Store
25+
*.pem
26+
27+
# debug
28+
npm-debug.log*
29+
yarn-debug.log*
30+
yarn-error.log*
31+
.pnpm-debug.log*
32+
33+
# env files (can opt-in for committing if needed)
34+
.env*
35+
36+
# vercel
37+
.vercel
38+
39+
# typescript
40+
*.tsbuildinfo
41+
next-env.d.ts
42+
43+
.vercel
44+
.env*

‎apps/web/asheard/README.md‎

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# asheard
2+
3+
Read what actually happened to a CALL-E call, and see which field said so.
4+
5+
A finished call hands you a status word, a `task_completed` boolean and a
6+
confidence score. That comes back saying the job is done for a voicemail box:
7+
the recording picks up, the agent asks its question into the tone, and the call
8+
ends `completed` with a schema-valid result and a high score.
9+
10+
Nothing in that payload is wrong. There is just no field in it that says a
11+
person was ever on the line.
12+
13+
This app reads a call as three separate answers instead of one, and puts the
14+
field it read next to every sentence.
15+
16+
## Try it without placing a call
17+
18+
No credentials, no phone, nothing stored.
19+
20+
```bash
21+
npm install
22+
npm run dev
23+
```
24+
25+
Then:
26+
27+
- `/read` takes a payload you paste and reads it. Four synthetic sample payloads
28+
are loaded for you.
29+
- `/briefing` shows a batch of calls in the order somebody should deal with
30+
them, from fixtures, with the boring ones collapsed into a line.
31+
- `/matrix` prints which endings each surface can actually express, generated
32+
from the mappers rather than written by hand.
33+
34+
```bash
35+
npm test
36+
```
37+
38+
No network and no credentials. The suite runs against the package's own
39+
synthetic fixtures and a stubbed `fetch`, so it exercises what the app actually
40+
imports rather than a copy of it.
41+
42+
## The pages that need a key or a token
43+
44+
`/key` reads one call by id with your own key. The key travels in a header, gets
45+
used for one request and is written nowhere. It lives in React state and dies
46+
with the tab. What comes back is a masked projection of the call, never the raw
47+
payload.
48+
49+
`/hook` is a webhook inbox. CALL-E deliveries are unsigned, so the inbox never
50+
believes one. It takes the call id out of the delivery, fetches that call from
51+
CALL-E with the server's key, and keeps only that. The posted body is never
52+
stored or shown. The inbox address carries a post token only the operator can
53+
mint, and reading the inbox back needs a second token.
54+
55+
`/live` places a real call, and it's the only thing here that can. It needs an
56+
operator token minted for the exact number being dialled, so a browser can't
57+
make one up and a token for one number won't dial another. The built-in
58+
destinations are all in the 555-0100 range, held back for fiction and assigned
59+
to nobody. Other test lines go in `ASHEARD_EXTRA_DESTINATIONS` on your own
60+
deployment.
61+
62+
## Configuration
63+
64+
```bash
65+
CALLE_API_KEY=... # server only. Without it nothing reaches CALL-E
66+
ASHEARD_OPERATOR_SECRET=... # 32+ characters. Without it /live and /hook are off
67+
KV_REST_API_URL=... # Redis, for the budget, the intents and the inbox
68+
KV_REST_API_TOKEN=...
69+
```
70+
71+
Mint tokens on the machine that holds the secret:
72+
73+
```bash
74+
npm run operator-token -- dial +13035550100
75+
npm run operator-token -- inbox
76+
```
77+
78+
Every response from a route that touches CALL-E goes through a deep mask on the
79+
way out. A phone number that turns up anywhere in it, an error message or a
80+
structured result quoting one back, gets masked. Timestamps, scores and ids are
81+
left alone.
82+
83+
## Side effects
84+
85+
`/live` places real phone calls and spends credit on the configured account,
86+
and only with an operator token. Everything else in this app is read-only.
87+
88+
Nothing here schedules anything or creates a recurring job, so there is nothing
89+
to cancel. Close the tab.
90+
91+
Each press of the button gets an id from the page, and that id is for
92+
idempotency, not permission. The server writes the intent down before dialling
93+
and derives the idempotency key from it, so a retry after a lost response finds
94+
the call that already exists. If a request goes out and the answer never comes
95+
back, the record says the call may have gone out and stops there. Retrying a
96+
submission you can't see the outcome of is how somebody gets rung twice.
97+
98+
## What it will not do
99+
100+
It does not interpret a transcript, score anything, or ask a model what it
101+
thinks. Every line it produces traces to a field and says the same thing every
102+
time.
103+
104+
It never resolves the business outcome for you. Where a reading rests on
105+
inference it says so and routes to a person, and where no field carries the fact
106+
it says that instead of filling the gap.
107+
108+
## The engine
109+
110+
The reading itself lives in [`asheard`](https://www.npmjs.com/package/asheard) on
111+
npm, published from CI with provenance, and this app is a front end over it.
112+
113+
- `disposition/` maps a payload from any of the three surfaces onto three axes,
114+
each carrying whether it was quoted from a field, derived from other fields,
115+
or absent because nothing carries it.
116+
- `reconciler/` builds one evidence document per call, runs four checks over it,
117+
ranks a batch by what costs you something if ignored, and writes the sentences.
118+
- `ledger/` records what was authorized before a call goes out, with the
119+
idempotency key derived from the authorization rather than the attempt.
120+
121+
The paired skill is [`skills/call-state-reconciler`](../../../skills/call-state-reconciler/).
122+
Its `references/observed-shapes.md` shows each platform behaviour the reading
123+
relies on, with invented values, and how to check it yourself.

‎apps/web/asheard/eslint.config.mjs‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { defineConfig, globalIgnores } from "eslint/config";
2+
import nextVitals from "eslint-config-next/core-web-vitals";
3+
import nextTs from "eslint-config-next/typescript";
4+
5+
const eslintConfig = defineConfig([
6+
...nextVitals,
7+
...nextTs,
8+
// Override default ignores of eslint-config-next.
9+
globalIgnores([
10+
// Default ignores of eslint-config-next:
11+
".next/**",
12+
"out/**",
13+
"build/**",
14+
"next-env.d.ts",
15+
]),
16+
]);
17+
18+
export default eslintConfig;

‎apps/web/asheard/next.config.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { NextConfig } from "next";
2+
3+
const nextConfig: NextConfig = {
4+
/* config options here */
5+
};
6+
7+
export default nextConfig;

0 commit comments

Comments
 (0)