| Model-supplied identifiers | Model copied account numbers, quote tokens and slot fields by hand. Sim batches: ~49 bad runs/batch from invented accounts, token-as-account, date of one slot + provider of another. | account_number, token, encounter_id, call_id, caller_phone hidden from the schema and filled from prior replies. Slots tagged S#, visits V#; tools take only the tag and the layer expands it. | tool-layer.ts prepare / fillCodeValues, tagAvailability, tagVisits |
| Request contract | Inbound API returns 400 BAD_INPUT on any extra, missing or mistyped key. First live run: 3/7 calls 400 on spoken DOB; provider_info / route_patient 405 (POST vs GET); booking commit 400 (full quote body sent). | normalizeArgs: DOB → YYYY-MM-DD (en/es), spelled names, 2-letter state, phone NNN-NNN-NNNN, lower-case sex, blanks/non-zips dropped. Per-endpoint body shaping (commit = token + reason + flags; change quotes carry no reason). call_id in every POST body. Hub tools → GET. | tool-layer.ts normalizeArgs, shapeReason; tool-executor.ts; Clara migration |
| Pre-request gating | Prompt-only rules were skipped: details shared before identity check, commits without a read-back yes, quotes for times the caller never picked, values the caller never said. | Local refusals before any request: VERIFY_FIRST, READ_BACK_REQUIRED, CALLER_HAS_NOT_CHOSEN, DOB_NOT_FROM_CALLER, VALUE_MISSING, NOT_DENIED (third-party callers). Each refusal returns the next action to the model. | tool-layer-checks.ts |
| Write idempotency | Resent commits in sim. Live: a commit with an unclear result (504, timeout, no envelope) left the quote path open → re-quote + commit = double booking. | One commit per quote (QUOTE_USED / ALREADY_COMMITTED). Unconfirmed commits locked per change (staff verify). Retry only on BUSY / SLOT_ / PATIENT_ / CREATE_ / KEY_BUSY, once, same token (API answers these before any write). Call log retried until a non-BUSY reply. | tool-layer-checks.ts commitOnceRefusal; tool-layer-flows.ts unconfirmed lock; tool-executor.ts |
| Tool exposure and recovery | gpt-4o-mini EOL 2027-01-14 → gpt-6-luna (reasoning_effort none). With all tools visible and a long prompt, the model skipped steps and dead-ended on errors. | flowStep exposes only the current step's tools (via llmNode) plus a one-line next step. Recovery paths: taken time → re-search, 2 taken → other days, expired quote → re-quote, SAME_TIME, NOT_BUILDABLE → re-ask field, PATIENT_MAY_EXIST → lookup, STATUS_NOT_BOOKED → re-read brief, NAME_MISMATCH → ask once then staff. | tool-layer-flows.ts; agent.ts |
| Result wording | Model stated "booked" after failed commits and misread times. | Spoken confirmations and transfer lines generated from the API reply (en/es); the model is told not to repeat them. | tool-layer-voice.ts commitLine, transferLine |
| Emergency path | Red-flag detection depended on the model. Live: a bare "yes, book it" was once routed to the emergency transfer. | Triage classifier on every caller line → emergency transfer. Guard: an emergency transfer is held once only when a quote is open, the last caller line is a bare yes and no urgent terms were said; a second attempt always passes. | tool-layer.ts triage; tool-layer-checks.ts transferRefusal |
| Audit trail | No record on the chart of calls handled by the agent. | Automatic /call/log at hang-up: transcript, accounts found or created, end reason, human_review / emergency / identity flags. | tool-layer.ts sendLog; tool-executor.ts log sender |
| Concurrency | Multiple inbound calls run at once in one server process. | All call state lives in one CallLayer per call; no module-level state. Verified with parallel real calls: every write landed on its own call's chart, no crossed transcripts. Throughput limit is the API's eCW pacer (~10 concurrent calls at p95 < 3 s), not TeleCall. | tool-layer.ts; load runs on the inbound API |
| Rollout / rollback | Change must not affect other agents and must be reversible. | New nullable agents.tool_layer; NULL keeps the legacy path in wireAgentTools. Clara migration backs up her agent row and 5 tool rows to agent_config_backups; rollback SQL in the migration header. | migrations …000000, …000100 |