ADR-0113: Vendor communications — extend the existing agents with a vendor lane, not a new agent (yet)

Renumbered from 0103 (collision: 0103-post-tour-reengagement.md and 0103-dashboard-widget-catalog.md both 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):

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:

  1. 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.
  2. Trade taxonomy: AppFolio's VendorTrades is populated on 1 of 814 vendors; VendorType is 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.
  3. 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).
  4. No per-property vendor roster at call-start. handyman-context.ts proves the seam (pre-load context during the ring via the personalization webhook); nothing equivalent exists for external vendors.
  5. 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)

  1. vendor-context.ts injection (sibling of handyman-context.ts): at call-start, inject the property's known-vendor roster — company names, trades, recent-activity signals, open/recent WOs where they exist — into triage and maintenance-handyman via 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 on fede/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; extend voice-agents-context-wiring.drift.test.ts (emitted ≠ consumed is a known silent-drop failure mode).
  2. 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.
  3. Vendor prompt block on the maintenance_handyman lane (with unknown-caller keeping a residual capture path): identify company → match against vendor_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.
  4. 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.
  5. 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 — a capture_unknown_caller_note / transfer_to_number is_error persists a high-priority PM task + notification instead of silently dropping; fixtures: the lost fire-panel alert and delivery window.
  6. 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.
  7. 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.
  8. 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

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:

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

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.