Proposed — awaiting review

Vendor Calls: Post-Call Feedback → Self-Improvement Loop

A design proposal. Nothing here is built or decided — this page is the review surface.

Owner: Fede Scope: ElevenLabs vendor-calling agent (agents/clara/lib/voice-agents/vendor-outbound.ts) Problem: the system currently learns nothing from its own calls
Shape in one line: every reviewed call produces either a regression eval scenario or nothing; a periodic job turns repeated feedback into a scoped prompt-rule PR that Fede approves. Nothing ever edits a prompt silently.

0. Two prerequisites (do these first — they're cheap)

  1. Persist the transcript. Today src/app/api/voice/call-ended/route.ts:536-551 hands the transcript to handleVendorCallOutcome, which drops it — only the ElevenLabs conversation id survives on transcriptRef. ElevenLabs retention is therefore our de-facto retention policy, and the whole loop depends on a live vendor fetch. Fix: in postcall-extraction.ts, write the transcript turns to a sibling DDB item PK=PROP#{propertyId} / SK=VENDORCALLTRANSCRIPT#{sessionId} — a separate item, so the session row stays small. Audio stays remote via fetchConversationAudio.
  2. Close the ElevenLabs sync gap. vendor-outbound.ts is not in SPECIALIST_AGENTS, so sync-specialist-prompts.ts never pushes it. simulate-battery.ts and real dials exercise the deployed agent, so a merged prompt PR changes nothing live and the battery cannot verify it. Either add vendor to the sync list, or make the manual push an explicit scripted step in the PR checklist.
Blocking caveat Until the sync gap is closed, the improvement half of the loop is unverifiable.

1. MVP capture spec (~1 day)

Fields on VendorCallSession

Copy the WorkOrder.rating precedent verbatim (types.ts:2469-2476) — numeric/enum verdict, verbatim free text kept separate, timestamp, actor.

FieldTypeNotes
callFeedback'good' | 'bad' | nullnull = reviewed and explicitly dismissed; absent = not reviewed. Binary, never 1–5.
callFeedbackNotestring | nullPrompt: "What should she have done instead?" Required on bad, optional on good.
callFeedbackTurnRefnumber | nullIndex of the transcript turn the reviewer anchored to. Optional but high value.
callFeedbackAtISO string
callFeedbackBystringPerson/User id — same precedent as reviewedBy (types.ts:296).
callFeedbackScenarioRefstring | nullSlug of the eval scenario minted from this call.

All six go in the NewVendorCallSession Omit list, or they become required at creation.

CI gotcha ci.yml:211-230 — touching types.ts makes vitest --changed select nearly the whole suite (35-min timeout), or blow AFFECTED_CAP and skip PR-lane testing entirely. So: land the type + repo-interface change as its own tiny PR, then build the route and UI on top of it. Two PRs, not one.

Write path

POST /api/vendor-outreach/[id]/feedback — a near-copy of src/app/api/vendor-outreach/[id]/decision/route.ts: zod .strict() body { rating: 'good'|'bad'|null, note?: string, turnRef?: number }, requireRole(request,'conversations','edit'), getUserPropertyScope 403, withErrorHandling. Differences: gate on a terminal session status (not awaiting_approval), and write via patchVendorCallSession — no optimistic-lock path needed. Re-rating is allowed and overwrites (last human wins); the raw prior value is not preserved, by choice.

UI placement

Resolved calls are deliberately invisible today (QUEUE_VISIBLE_SESSION_STATUSES = awaiting_approval | approved | cancelled, review-rows.ts:18-22) and no component renders a VendorCallSession. So capture needs a host built, not extended.

Build a Recent calls tab in the existing review/HITL surface (src/app/(workspace)/(operations)/review/page.tsx), one call per screen:

Copy stays plain English throughout — no "rubric", "judge", "criterion", "eval". The button says Save; the box says What should she have done instead?

Not in MVP: dropdown taxonomy, star ratings, dashboards, voice memos.

2. The distillation job

At rating time — synchronous, no LLM, ships day one

A bad rating with a note mints a regression scenario immediately: an evals/datasets/vendor-calling.yaml case named after the call (vc-2026-07-22-hvac-no-eta), body = the reconstructed vendor situation from the transcript, assertion = the note turned into an llm-rubric line plus any deterministic helper that fits (assertBriefTurn, assertPlainLanguage in evals/lib/assert-vendor-calling.ts). This is cheap, needs no judgment, and satisfies the standing rule that every fix ships with a regression test.

A distillation run that proposes no prompt change has still produced value.

Periodic — the prompt half

Output per mode — one PR, one concern:

  1. A scoped rule, not a prompt line: { when: <scenario>, then: <behavior>, evidence: [sessionIds] }, rendered into the prompt with a strength tier — ALWAYS: / PREFER: / AVOID: — calibrated to how strongly the human actually spoke. Offhand preference = PREFER, never ALWAYS.
  2. The verbatim note quotes + call links that motivated it.
  3. A before/after run of the entire vendor gauntlet (evals/promptfooconfig-vendor-calling.yaml via run-all.sh), including the originating scenarios.

Guardrails

GuardrailRule
BoundedMax 15 rules × 280 chars. At cap, retire oldest (FIFO) — the overlay rides in every call.
Amend, don't appendA recalibration replaces the superseded rule atomically, so two contradictory ALWAYS: lines never co-exist. Contradictions reconcile into a range, never last-wins.
Never-wipe floorA distill that would empty a populated rule set is rejected as a model error.
Pareto guardA rule that fixes 3 new scenarios and breaks 2 old ones is visibly rejected in the PR body, not merged. vendor-calling has no entry in evals/domain-baselines.json, so it runs at the strict default floor — a new failing case reds the domain immediately, which is the behavior we want.
Prompt-var driftAny new {{var}} needs an emitter in vendor-call-context.ts in the same PR, or vendor-prompt-vars-drift.test.ts fails.
Never auto-mergeThese PRs change customer-facing agent behavior — the same hold-for-Fede category as ADR PRs. CI green is necessary, not sufficient.
Held-out set20% of rated calls are reserved and never used to generate rules. It is the only way to tell "the rule generalizes" from "we overfit to nine calls".

Approval flow

The job posts to #updates-fede: the failure mode, the counted evidence, the proposed rule with its scope, the eval delta. Fede's ✅ is what opens the PR — approval and PR creation are one gesture, not two. Silence = no PR. Every landed change gets a one-line "what changed and why" post.

Silent learning is the failure mode.

3. Growth path to PM-facing feedback

Stage 1 — Fede + team only

Now

Binary verdict + note, in the review UI. Metric: calls reviewed per sitting, and % of bad ratings carrying a note (target ≥60%).

Stage 2 — Clara self-scores first

After ~50 labels

Clara scores every vendor call against a small written rubric before a human sees it — did she confirm scope, get a firm ETA, avoid committing to a price, capture a callback number — each verdict pass | fail | unknown with a required rationale quoting the transcript line. The reviewer then agrees or disagrees per line instead of composing from scratch. unknown is a legal verdict; an eval that can't tell must never count as a pass.

Stage 3 — Calibration gate

Before any dimension runs unsupervised

Log Fede's verdict beside the judge's and compute Cohen's κ, not raw agreement. κ ≥ 0.6 on a dimension is the bar for letting that dimension run unsupervised. The existing Clara conversation-eval loop sits at κ = 0.454 with diagnosed over-strictness — expect the same failure and the same fix (few-shot anchors drawn from over-flagged passes).

Stage 4 — PMs

Only once the judge is calibrated

PM capture is a single thumbs on the outcome notification they already receive (notify-pm-outcome.ts emails/SMSes the property PM with a /work-orders/{id} link — a signed thumbs link there is the lowest-friction point in the system). PM feedback is routing signal into the review queue, not a direct input to distillation: a PM thumbs-down surfaces a call for Fede, it does not vote on a prompt rule. Per-property gate on the whole thing, matching the outreach-cadence precedent.

Also stage 2+, and cheap Implicit signals populate the queue so review isn't limited to what a busy founder volunteers — Fede takes over a call, a vendor is re-dialed within the hour, a scheduled visit slips, the extraction produced no ETA. Route those in automatically; sample 5–10% of good calls too, so we can measure the true-negative rate and not only failures.

4. What we deliberately do NOT build

1–5 star ratings
Adjacent points are inconsistent between and within raters, and annotators cluster on the middle. Binary + critique.
A category dropdown at capture time
The taxonomy is supposed to be discovered from open-ended notes, and it drifts as the product changes. Pre-freezing it guarantees the rubric measures the wrong things within months.
A feedback dashboard
Aggregates with no exit path are the graveyard. A rated call has exactly three legal exits: became a scenario, became rule evidence, or was dismissed with a reason. Nothing terminates in a chart.
Any auto-apply path
No prompt mutation without a human-approved PR, at any confidence, ever. Not a v2 toggle either — promotion between autonomy notches is a separate, explicitly-made Fede decision, never a default the loop grows into.
A separate feedback store / Feedback entity / new table
Fields on the session row, patchVendorCallSession, done.
A/B experiments with statistical significance
Credible at Sierra's volume, not at Camellia's. Eval pass/fail plus Fede's subsequent thumbs is the only honest signal we have.
Voice memos for notes
Speech is ~2.9× faster than typing, but there is zero product precedent for voice-as-AI-feedback, and transcribed speech is rambling where critique needs to be tight enough to drop into a judge prompt. Run it as an A/B later, measured on usable rule yield per note — not as a v1 assumption.
An unpruned growing rule list
Nobody in the market documents conflict resolution; that is exactly how these systems rot. Cap, amend, retire.

5. Market patterns adopted

#PatternSourceVerification
1 Thumbs + free-text comment stored on the conversation record, separate from the machine's own verdict; unknown is a legal judge outcome and every verdict carries a rationale. Human signal is never overwritten by the judge. ElevenLabs (metadata.feedbackoverall_score like/dislike + nullable comment; success-evaluation success|failure|unknown + rationale) Confirmed — read directly from the API schema and Analysis docs. Our claim is a correct subset of the real shape.
2 "Create test from this conversation" — a failing real call converts into a prefilled regression scenario; results reported as pass-rate bands (green 100 / amber ≥80 / red <80) with failure bucketing, not a boolean. ElevenLabs Agent Testing (repeat_count 2–20, grouped-by-failure-reason results) Confirmed — exact UI strings and band thresholds verified in docs.
3 Proposal-with-diff in pending state, approved by a human in Slack or dashboard; the coach never applies changes automatically. This is our approval flow almost verbatim. ElevenLabs Coaching Confirmed — "The coach does not apply changes automatically", pending → Approve/Reject, Slack review, diff over prompt/memories/procedures, all verbatim.
4 Validate before asking: test each proposal against the originating conversation and the full golden set, and put issue → evidence → diff → validation results in the review surface so approval is one judgment, not an investigation. Human approval required before production. Decagon Duet Autopilot Confirmed — the load-bearing sentences quoted from Decagon's own post. "Overnight" is positioning language, not a documented schedule, so our monthly cadence is our own call.
5 Every rule carries a scope condition the reviewer argues with, auto-suggested by the model and edited by the human, kept narrow — one scenario per rule, never a catch-all. Ada Coaching ("When replying to" field; best-practice: "as specific as possible… only a singular scenario at a time") Confirmed — verified in Ada docs. Ada ships this with no approval gate and documents no conflict resolution or volume cap; we add all three deliberately.
Adopted but market-unattested — our differentiator, and our risk Measuring judge-vs-human agreement with Cohen's κ as the gate for autonomy. No vendor in the survey documents judge calibration against human labels — ElevenLabs stores human thumbs and its own call_successful side by side with nothing reconciling them. We are doing this because our own conversation-eval ADR already proved the metric bites (κ = 0.454, below the 0.6 bar), not because anyone else does it.
Rejected on evidence Cresta's one-click failure→test and Level AI's ">90% of scorecard metrics" / "10× faster" are marketing-page claims with no technical docs, changelog, or methodology behind them — the underlying idea (assisted grading; failure-to-test conversion) is worth stealing, the numbers are not citable.

Open questions for review

These are open, not settled. The design commits to a position on each, but every one is a live call for Fede.

  1. Close the ElevenLabs sync gap how? Add vendor-outbound.ts to SPECIALIST_AGENTS, or keep the push manual as a scripted PR-checklist step. Until one is chosen, the improvement half of the loop cannot be verified.
  2. Is monthly the right distillation cadence, with the ≥8-unprocessed-bad on-demand trigger? Weekly at Camellia's volume would mostly produce empty runs.
  3. Are the guardrail numbers right? 15 rules × 280 chars, 2+ occurrences to promote, 20% held out, ≥60% of bad ratings carrying a note. These are proposed values, not measured ones.
  4. Does the 50-label threshold for Stage 2 hold, and is κ ≥ 0.6 the right autonomy bar given the existing loop sits at 0.454?
  5. Voice memos for notes — deferred to a later A/B measured on usable rule yield per note, not shipped in v1. Confirm that's acceptable.
  6. Stage 4 PM thumbs stays routing-only signal (surfaces a call for Fede, never votes on a prompt rule), per-property gated. Confirm.

Status: Proposed — awaiting Fede's review. Nothing described here is built, merged, or decided. This page is faithful to the source design note and adds no numbers, dates, or commitments beyond it. Source: vendor-call-feedback-loop.md.

PropFlow Docs