ADR-0113: Vendor communications — extend the existing agents with a vendor lane, not a new agent (yet)
- Status: Proposed
- Date: 2026-07-18 (revised 2026-07-24 per review; direction ratified at the 2026-07-22 founders meeting)
- Deciders: Fede, Gera
- Related: PRs #4037, #4038, #4050, #4053, #4068 (the A&K friction ladder, now merged); ADR-0030 (PMS owns WO identifiers); ADR-0033 (VendorCompany / VendorMembership decoupling); ADR-0060 (preferred-vendor roster + resolver, PR #4345); ADR-0111 (outbound vendor-calling agent, PR #4341 — complementary, see scope boundary below);
docs/vendor-coordination/PLAN-2026-07.md(the roadmap of record);docs/vendor-coordination/camellia-vendor-corpus.md(the rated corpus);docs/maintenance-workflow.md
Renumbered from 0103 (collision:
0103-post-tour-reengagement.mdand0103-dashboard-widget-catalog.mdboth claimed it; 0104–0112 are all taken, including duplicate claims on 0110 and 0112 — 0113 is the lowest genuinely-free number as of 2026-07-24).
Context
The incident class
A vendor (A&K Appliance Service) called Camellia's line three times in one day and got friction on every call: Clara couldn't identify them (no phone on file in AppFolio), treated an on-site access request as a "the team will follow up" message, and cold-started the interrogation on each successive call. The PR ladder listed in Related fixed the acute failures. This ADR decides the durable architecture.
What vendor communications look like today (measured)
Voice. There is no vendor specialist in the ElevenLabs roster (agents/clara/lib/voice-agents/: triage, maintenance-tenant, maintenance-handyman, leasing, lease-and-billing, renewal-in/out, turnover-intake, unknown-caller, emergency-relay). Vendor callers land on triage and get routed to maintenance-tenant (tenant-shaped prompts) or unknown-caller (message capture). maintenance-handyman is the closest fit — triage already routes identify_caller type='vendor' and handyman phrasings to it — but it is scoped today to the ONE in-house handyman with a pre-loaded WO slate (handyman-context.ts); external vendors without a WO fall through to unknown-caller, which is exactly where the corpus's failures live.
SMS. The inbound router's vendor lane handles a narrow case: a known vendor (phone-matched) replying about an active WO (handle-vendor-completion.ts — completion claims, scheduling replies). A vendor texting from an unknown number, or about anything other than an active WO, falls through.
Email. Vendor email (invoices, POs, scheduling, access requests — ~148 vendor emails at Camellia in 2026 H1) flows to the office inbox. Clara has zero visibility into it.
Call volume reality (ElevenLabs, Camellia line, Feb–May 2026): the initial 2026-07-18 audit counted 16 confirmed vendor calls (~3.4% of 473 inbound); the completed rated corpus (docs/vendor-coordination/camellia-vendor-corpus.md) corrected that to 46 genuine vendor voice calls (+3 SMS/bridged-email) across ~3 months — a ~3× undercount, and a floor: 139 of 481 calls were never per-call analyzed, and the office Outlook inbox is not yet enumerated via Graph. Volume is meaningfully higher than first drafted, and the friction is disproportionate: a vendor standing at a locked boiler room is a same-hour operational failure, and vendor impressions leak to the PM relationship.
The two recurring failure classes (rated corpus: 30 GOOD / 9 ADEQUATE / 5 POOR / 5 FAILED):
- (A) Post-transfer hallucination — after a successful transfer Clara keeps talking as staff and fabricates data (fake unit/PO/name; a fake occupancy figure).
- (B) Signal-drop on tool error — a
capture_unknown_caller_note/transfer_to_numberis_errorsilently loses the signal (a fire-panel low-battery alert and an appliance-delivery window were both lost this way).
Both are cheap, high-severity Phase-1 hardening targets (Decision, item 5).
The data gaps (why Clara can't coordinate even when routing works)
Measured against the jpco AppFolio account:
- Vendor contact coverage: 78 of 814 vendors have any phone; 27 have any email. A&K — 211 bills at Camellia — has neither. Phone-based identity can never work at this coverage level.
- Trade taxonomy: AppFolio's
VendorTradesis populated on 1 of 814 vendors;VendorTypeis uniformly "General". The PropFlow-owned surfaces have since shipped: the canonical trade vocabulary (src/lib/data/statuses/trades.ts, PR #4340) and the preferred-vendor roster + resolver (src/lib/domain/vendors/resolve-preferred-vendors.ts, PR #4345, ADR-0060). The remaining gap is population, not plumbing: a comprehensive trade classifier for the ~814 blank-trade vendors is running in dry-run (backfill applies only on Fede's approval), and inferred trades are marked as inferred by design, per the no-fabrication rule. - WO↔vendor attribution: only 13 of Camellia's 562 work orders carry a vendor. The usable frequency signal is AP-bill count, which lives in accounting, not in any surface Clara reads. This is also why nothing in the vendor lane may require a WO: the open-WO leg of any roster is nearly empty today, and some properties are paper-based or run a PMS with no work-order module at all (the hybrid reality PLAN-2026-07 anchors on).
- No per-property vendor roster at call-start.
handyman-context.tsproves the seam (pre-load context during the ring via the personalization webhook); nothing equivalent exists for external vendors. - No cross-channel vendor memory. A vendor's email thread (PO #671, prep instructions) is invisible to a voice call about the same job. #4053 adds episode memory for returning unknown callers, but it is call-to-call, not channel-to-channel.
Decision
Extend the existing agent topology with a vendor lane; do not mint a dedicated vendor EL agent yet. Specifically (Fede, 2026-07-19): reuse maintenance_handyman as the vendor lane. It already receives identify_caller type='vendor' and handyman phrasings from Triage; Phase 1 widens it to WO-less vendor callers and hardens the two corpus failure classes. Every vendor call hits the one published number, so disambiguation must live in the agent topology that answers — a forked vendor agent would duplicate the specialist, add a second prompt to keep in sync, and widen the drift surface for zero routing benefit. A new specialist agent is Phase 2, gated on evidence.
Phase 1 — context + identity + routing (the 80%; = PLAN-2026-07 Phase A)
vendor-context.tsinjection (sibling ofhandyman-context.ts): at call-start, inject the property's known-vendor roster — company names, trades, recent-activity signals, open/recent WOs where they exist — intotriageandmaintenance-handymanvia the personalization webhook. The roster's primary key is AP-bill / recent-activity frequency; WO linkage is an enricher, not a requirement — with 13/562 WOs carrying a vendor, an open-WO-keyed roster would be nearly empty, and every Phase-1 behavior must work with zero WOs on file. This is what makes #4050's company-name identification land: the LLM gets a candidate set instead of a blind lookup (the greeting-drift root cause found 2026-07-22 — the #4050 matcher is never invoked by the Triage prompt, fix in flight onfede/vendor-recognition-fix— makes the pairing explicit: candidate set from this lane, two-call identify flow from that fix). Consumed via a{{vendor_context}}dynamic variable; extendvoice-agents-context-wiring.drift.test.ts(emitted ≠ consumed is a known silent-drop failure mode).- Vendor data hygiene consumes the shipped surfaces. Contact backfill for the active roster (the 2026-07-18 audit already sourced phones/emails for the Camellia top-5). Trades ride the already-shipped canonical vocabulary (
trades.ts, #4340) and ADR-0060 resolver (#4345) — this ADR builds nothing there; it wires the vendor lane to read them. The dry-run trade classifier closes the 813/814 population gap on Fede's approval, inferred-marked-as-inferred. - Vendor prompt block on the
maintenance_handymanlane (withunknown-callerkeeping a residual capture path): identify company → match againstvendor_context→ branch to vendor behaviors. Coordination anchors on property + unit + free-text job description, with the WO optional: scheduling and commitments are captured as a vendor visit/commitment record keyed to property+unit+job ("tub refinish, units 204/205") that links to a WO when one exists — never dead at paper-based or PMS-without-WO properties. - Urgency/escalation taxonomy in the prompt block (Fede's direction; the full tier table lives in PLAN-2026-07 §4): life-safety / urgent (gas leak, flood, no-heat in winter, vendor blocked on an emergency job, access-now) → escalate to a human live (transfer per #4038 or immediate PM page), with a durable fallback if the transfer errors; time-boxed operational (delivery windows, repair ETAs) → structured capture + confirm; routine (scheduling, invoices, FYI notices) → capture + notify, no interruption. Enumerated in the prompt with examples; eval cases pin both directions — urgent never becomes a message; routine never pages.
- Corpus failure-class hardening. (A) a hard no-turns-after-transfer contract — after a successful
transfer_*the agent emits no further turns, enforced in prompt and by a server-side guard; regression fixtures from the corpus's fabrication calls. (B) a durable PM-task fallback on tool error — acapture_unknown_caller_note/transfer_to_numberis_errorpersists a high-priority PM task + notification instead of silently dropping; fixtures: the lost fire-panel alert and delivery window. - SMS parity. Widen the SMS vendor lane from "completion claims on active WOs" to the same identify-then-branch behavior, reusing the voice lane's classification. One vendor-conversation policy across channels.
- Local-first WO↔vendor linkage where a WO exists. When PropFlow dispatches or a PM assigns a vendor, record the assignment on the local WO mirror (ADR-0030-compliant: PMS still owns the WO id; we own the assignment annotation we created). This enriches the context injection and closes gap 3 going forward — but per item 3, it is an enricher, not the lane's backbone.
- Email enters the same machinery — the channel is not deferred. Per the 2026-07-22 mandate, all vendor coordination is channel-agnostic: voice, SMS, and email run through one session/outcome machinery (ADR-0111 §0; its §5 callback/context binding explicitly treats an inbound vendor email as the same session on a different medium). What defers is only the inbox-ingestion plumbing (Graph-mail wiring, threading, PII handling), not the architecture. First slice when ingestion lands: detect vendor emails + draft replies gated behind human review — no autonomous send, no threading problem, early cross-channel validation.
Phase 2 — dedicated vendor specialist agent (gated)
Mint voice-agents/vendor.ts + a vendor slug in the specialists registry ONLY when Phase 1 evidence shows the widened maintenance_handyman lane can't hold the behavior: vendor-call eval regressions attributable to prompt crowding, or vendor call volume materially above the corpus baseline (46+/quarter and rising). The trigger is an eval signal, not a hunch. Naming per src/lib/temporal/README.md (domain, not persona). Expected toolset if/when it lands: WO status lookup, access-instruction retrieval, live transfer, scheduling capture, invoice/PO message capture.
Explicitly rejected
- New EL vendor agent now. Even at the corrected 46+ calls/quarter, the observed failures were identity + context + contract failures (wrong candidate set, post-transfer turns, dropped signals), not "wrong specialist" failures — a vendor agent with today's data gaps would fail identically, while duplicating the lane triage already routes vendors to.
- A separate email-architecture ADR. The original draft deferred email wholesale to its own ADR; that conflicts with the 2026-07-22 channel-agnostic mandate. Email is in-scope architecture (Decision item 8); only ingestion plumbing is sequenced later.
- Doing nothing / human-only. The measured friction is exactly the class PropFlow exists to remove.
Relationship to PLAN-2026-07 (scope boundary)
docs/vendor-coordination/PLAN-2026-07.md is the umbrella roadmap of record (Fede-approved): Phase A inbound → Phase B turnover offer-flow + outbound placement → Phase C full loop (tenant notifications, completion detection, calendar, email lane). This ADR is the Phase-A decision record — inbound vendor communications. Deliberately out of scope here, and owned by the plan's Phase B/C — silence on them in this document is sequencing, not a decision against them:
- Outbound vendor coordination (Clara calling/texting/emailing vendors on the PM team's behalf; turnover dispatch is the first use case, behind human review with its own harness). The decision record for that lane is ADR-0111 (outbound vendor-calling agent, PR #4341) — complementary to this ADR, not overlapping: it owns outbound sessions; this ADR owns the inbound lane. (Fede's review comments reference it as "ADR-0108" — its number before the renewal-pipeline collision forced its own renumber.) The two lanes meet in the middle: the
vendor-context.tscall-start injection (Phase 1 item 1) is exactly what ADR-0111 §5's callback binding consumes, and the Phase-1 data model is built to serve outbound too — the roster carries our→them contactability (dialable phone, dispatchable email), not just caller identification. - The calendar seam — vendor announces an arrival window ("coming at 2:30") on any channel → property-calendar event + tour-slot block + PM/vendor reminder. Cheap, high-trust, explicitly asked for; owned by Phase C, and a natural fast-follow ("Phase 1.5") once the commitment record (Decision item 3) exists to project from.
- Turnover offer-flow, competitive bidding, tenant-side completion notifications — Phase B/C per the plan.
What context are we missing? (summary table)
| Missing context | Fix | Phase |
|---|---|---|
| Who this caller's company is | vendor_context roster injection + #4050 name match (Triage-prompt wiring fix in flight) |
1 |
| Vendor's phone/email on file | Contact backfill + hygiene surface | 1 |
| What trade a vendor covers | Shipped — consume trades.ts (#4340) + ADR-0060 resolver (#4345); classifier backfill on approval |
1 |
| Which job the vendor is here for | Property+unit+job commitment record; WO linked when one exists (local-first assignment) | 1 |
| How urgent this call is | Escalation taxonomy in prompt block + eval pins (urgent never becomes a message; routine never pages) | 1 |
| What was agreed last call | Episode memory (#4053) extended to identified vendors | 1 |
| Access instructions (boiler room, lockbox, office hours) | Property access knowledge in vendor prompt block; access-now → live transfer (#4038) | 1 |
| A dropped signal on tool error | Durable PM-task fallback (corpus class B) | 1 |
| The email thread about the same job (POs, invoices) | Same session machinery (ADR-0111 §0/§5); ingestion plumbing sequenced later — first slice detect + draft-for-review | 1 (arch) / deferred (plumbing) |
| Vendor's arrival window on the calendar | Calendar seam — owned by PLAN-2026-07 Phase C (Phase-1.5 candidate) | plan B/C |
Consequences
- Positive: vendor calls stop cold-starting; access-now and life-safety cases reach a human live; the #4050 name-match gets a candidate set; SMS and voice converge on one vendor policy; the two highest-severity corpus failure classes get hard contracts; the lane works at zero-WO properties; the WO↔vendor gap closes going forward where WOs exist; the data model is outbound-ready for ADR-0111.
- Negative / cost: one more dynamic variable + drift-test surface; a vendor roster read-model to keep fresh (write-through on assignment, nightly backstop — same shape as
VoiceInjectionCache); a new commitment-record entity to own; the AppFolio contact-coverage problem is only mitigated per-property, not solved account-wide. - Risk: prompt crowding on the widened
maintenance_handyman— watched by the daily voice-regression sweep (#4068), which is the Phase-2 trigger metric. The no-turns-after-transfer server guard must not clip legitimate pre-transfer confirmations — pinned by regression fixtures from the corpus's GOOD transfers.
Rollback (transfer-first dial-down — PR #5469, interim measure)
Until Phase 1 lands, Property.vendorTransferFirstEnabled (arm vendor.transferFirst) warm-transfers
recognized inbound vendors to the office WHILE THE OFFICE IS OPEN instead of solving in-lane. It
used to do this at any hour, on the reasoning that after hours the office line's voicemail took the
leg (PR #5528, 2026-08-07 A&K pre-open call); that was refuted on 2026-09-06 when a recognized
vendor was transferred into a closed office at 9:49pm and left a voicemail nobody would hear until
Monday, and the posture is now suspended whenever the office is closed.
Known stale text, and how it gets cleaned up. Triage's own after-hours
message-desk section still names the vendor transfer-first override as a second
thing that outranks the closed-office standing order. That is false since the
suspension landed, and it contradicts the injected order three screens above it
("THREE things outrank this order, and nothing else does"). It is inert rather
than dangerous — after hours the posture variable reads false, and by the
override's own closing sentence a false posture means none of the override
exists — so the sentence points at something that is not there on exactly the
calls where it would matter. It was not fixed in the same change because
specialist-prompt-drift.test.ts compares the LIVE agent against repo HEAD, so
on a PR branch any prompt-text edit is drift by construction and cannot go
green before it merges. Removal trigger: delete that sentence in the first
change that touches triage.ts for any other reason, or as its own change once
the drift guard compares a PR against main rather than against the PR head — a stopgap
for the failure class in Context (the 2026-08-05 A&K repeat of the 2026-07-18 incident: an agreed day
acknowledged and never recorded). Removal trigger, per property: flip the flag OFF when (a) this
ADR's Phase-1 capture path (commitment record + capture-and-notify) is live for the property, and
(b) the inbound vendor gauntlet — including the transfer-first pair IN-TF-1/IN-TF-2 — passes at parity with
the flag off. Delete the flag and the prompt override once no property has it set. The posture suspends
solve-first rules ONLY — the reschedule_vendor_visit MOVE carve-out (the one live inbound write
path) and the life-safety carve-out hold under any posture, pinned by gauntlet IN-TF-3 and IN-TF-2.