Clara's old and new workflow

Exists today, unchanged Exists today, changed New Hover or tap a box for details

Old workflow

Old workflow: the model's arguments go straight to the API Caller on the phoneLiveKit voice Clara (LLM) + promptsees every tool at every moment;types every argument itself wireAgentTools (tool-executor.ts)model's arguments → HTTP request as isno checks · no ids filled · no retries Scheduling hubavailability, offices, providers eCW inbound APIlookup, brief, quote, commit eCW What went wronginvented account numbersmixed slot fieldscommit before a yesno call log
Every argument the model writes reaches the API unchanged; correctness depends entirely on the prompt.

New workflow

New workflow, step by step: every tool call goes through the CallLayer in order Caller on the phoneLiveKit voice Clara (gpt-6-luna) + prompt v18decides what to do and calls a tool CallLayer: one per call, runs these in order 1 · Guided flowsis this tool allowed at this step? 2 · Normalize + checksfix formats; refuse unsafe calls 3 · Code fills the idsaccount, token, call_id, S1 / V1 → fields 4 · tool-executor sends ittimeouts · call_id in body · BUSY retry 6 · Finish the replytag S1 / V1 · fixed spoken linenext step for Clara refused: reason back to Clara reply back to Clara, next turn 5 · request and reply Scheduling hub (GET)open times, offices eCW inbound APIlookup, quote, commit Also running, outside the tool steps: Triageon every caller line Automatic call logonce, at hang-up
Every tool call runs steps 1 to 6 in order. A failed step 1 or 2 answers Clara with the reason and sends nothing; after step 6 Clara speaks and the cycle starts again on the next tool call.

Change register

PieceStatusBeforeNowWhy
CallLayer
tool-layer.ts
NewDid not exist.One instance per call; every tool call goes through prepare and finish. Config from agents.tool_layer.Puts correctness in code instead of the prompt; simultaneous calls share no state.
Ids filled by codeNewThe model typed account numbers, tokens and encounter ids.Hidden from the model; filled from earlier replies and the call (call_id, caller phone).Invented and copied ids were the worst mistakes in simulation (~49 bad runs per batch → almost 0).
Slot and visit idsNewThe model copied date, time, provider and office into each booking.Slots tagged S1, S2…; visits V1, V2…; booking tools take only the id and code expands it.Stops mixing one slot's date with another slot's provider.
Checks
tool-layer-checks.ts
NewNone.Refuse locally: identity before details, caller chose the slot, clear yes after read-back, commit once, authorized caller, values the caller said.Unsafe or wrong requests never reach eCW; the refusal tells the model what to do.
Guided flows
tool-layer-flows.ts
NewAll tools visible all the time.verify_caller → find_patient → choose_task → task steps, with error recovery.A small model follows a step list far more reliably than a long prompt.
Fixed spoken lines
tool-layer-voice.ts
NewThe model phrased results itself.Code says "You're booked for…", "is cancelled", transfer lines.The caller always hears what eCW really did.
Triage and call logNewPrompt only; no call log.Red-flag screening on every caller line; /call/log at every hang-up.Safety and a record on the chart must not depend on the model.
tool-executorChangedModel args sent as is; 3 s timeout; no retry.Uses the layer when configured; per-tool timeouts; call_id in every POST body; one BUSY retry.Match the real eCW API.
Agent wiring, Clara's configChangedNo layer; older prompt and model.Per-call layer and per-step tools in agent.ts; migration with prompt v18, gpt-6-luna, tool_layer JSON, backup and rollback.Code and config ship together.
Other agents, hub, eCW, LiveKitUnchanged—Same path as today.Blast radius limited to Clara.

Why these changes

AreaProblem / evidenceChangeWhere
Model-supplied identifiersModel 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 contractInbound 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 gatingPrompt-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 idempotencyResent 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 recoverygpt-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 wordingModel 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 pathRed-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 trailNo 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
ConcurrencyMultiple 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 / rollbackChange 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