Symptom agent and the gate
What the SMS symptom agent may do, what it never does, the deterministic red-flag screen, the three gate outcomes and the hold, and the AI-identity disclosure.
The symptom agent is a bounded, two-way SMS interview that a rule opens when a reading comes in out of range. It asks the clinician's questions, records the answers as observations, and routes to a person when a red flag or the protocol says so. Every message it sends passes the communication gate first. This page describes both as they run today.
What the agent may do
The agent runs a tool-calling loop with exactly four tools. Nothing it says to a patient is free text of its own authorship.
| Tool | What it does | What the patient receives |
|---|---|---|
ask | Chooses which protocol field to ask about next. The question text comes from the protocol's question_bank and is sent verbatim; the model cannot rephrase, shorten or add to it. | The clinician-authored question |
record_symptom | Records one structured answer (a boolean, a number within the declared scale, an enum option, or a short note) against a required field. The value must cite the patient message it came from; a value with no patient-attributable source is refused. | Nothing |
escalate | Routes the interview to the care team urgently and stops asking. | The protocol's static emergency_guidance, sent verbatim; the model does not write it |
handoff | Routes the interview to a person and stops asking - the patient asked for someone, the conversation went outside what the interview covers, or they ended it with fields still missing. | The protocol's cannot_continue_message |
Recorded answers become one observation per (patient, protocol field, source message), so what a patient reported is queryable and chartable like any other signal. Sessions move through open, awaiting_reply, re_engaging and end in one of completed, escalated, handed_off, abandoned or stopped. A sweep on the policy worker closes anything that stalls (per-turn window, overall time to live, re-engagement count), so every interview reaches a terminal state and every patient who was texted gets an answer or a person.
When a session closes it becomes an agent run in the dashboard's Review Queue and, when it surfaced something, an attention contribution on the episode. Reviewing the run clears the contribution; nothing else does.
What it never does
Four exclusions are always in force, on top of anything the program adds in scope_fence:
- Do not diagnose, or offer any statement that functions as a diagnosis.
- Do not give treatment, dosing or medication guidance of any kind.
- Do not provide therapy, counselling or mental-health treatment.
- Do not make independent therapeutic decisions; route to the care team instead.
permit_therapeutic exists on the protocol so that opening that fence would be a typed, reviewable act; today the validator rejects true outright. The agent also never invents a question, never sends anything the gate withheld, never texts an address whose consent is not open, and never opens a second interview for a patient who already has one live.
The protocol
A program carries the interview as spec.symptom_protocol, a clinician-authored, versioned document. Its fields:
| Field | Meaning |
|---|---|
id, version, condition | Identity. A rule's start_symptom_session names the id. |
opening_message, opening_field | The first patient-facing message and the question it asks. The opening must carry the non-emergency-line disclaimer. |
disclosure | The AI-identity disclosure: identity (what the patient is talking to) and human_contact (how to reach a person). Required. |
required_fields | What the interview collects: key and a field_type (bool, severity_scale with min and max, an enum, free_note). |
question_bank | The vetted question per field; pinned questions are sent verbatim. |
red_flags | Program-specific deterministic rules: id, an expression in the same grammar as rule conditions, a priority, and applies_to (the triggering reading, inbound text, or structured values). |
scope_fence | Additive items on the never list. |
routing | urgent_priority and the verbatim emergency_guidance text. |
completion | The done condition and closing_message; optional cannot_continue_message. |
session | per_turn_window_secs, overall_ttl_secs, max_reengagements. |
trigger | When a session starts: enabled, biosignal_slugs, expression, thresholds, cooldown_seconds, max_sessions_per_episode, max_reading_age_minutes. Saving the program materializes this as the rule that opens sessions. |
GET /api/v1/episode-profiles/{id}/symptom-readiness walks the program-scoped prerequisites (a valid protocol, a matching rule, an SMS sender, an active communication policy) and lists the patient-scoped ones it cannot answer (phone number, consent, no open session), so a green program is never mistaken for a delivery guarantee.
The deterministic red-flag screen
Before the model sees an inbound message, a pure function screens the text against a fixed vocabulary. It has no database, no network, no configuration and no error path: there is nothing to fail toward except routing. A hit routes the interview to the care team and sends the emergency guidance without waiting for the model to choose to.
Four categories are covered, each term traceable to a published clinical standard cited in code:
| Category | Rules |
|---|---|
| Cardiac | cardiac.chest_discomfort, cardiac.jaw_discomfort |
| Respiratory | respiratory.breathing_difficulty, respiratory.shortness_of_breath, respiratory.cyanosis |
| Neurological | neuro.balance_loss, neuro.coordination_loss, neuro.trouble_walking, neuro.vision_loss, neuro.trouble_seeing, neuro.face_droop, neuro.arm_weakness, neuro.slurred_speech, neuro.trouble_speaking |
| Self-harm | self_harm.wanting_to_die, self_harm.kill_myself, self_harm.killing_myself, self_harm.no_reason_to_live, self_harm.hopeless, self_harm.unbearable_pain, self_harm.feeling_trapped, self_harm.burden_to_others |
Three properties are stated in code and should be read before relying on it:
- It is tuned for recall and will fire on messages that are not emergencies. Negation is not suppressed: "no chest pain" fires. Matching is whole-word with a bounded inflection tolerance, and phrase rules allow qualifiers within a small window.
- The vocabulary is fixed and English only. It matches the phrasings of the cited standards. Lay phrasings outside them ("my chest hurts", "can't breathe") and constitutional symptoms are not matched; add the phrasings your population uses as the protocol's own
red_flagsrules, which union with the screen. - A hit carries no message text. The record names the rule id and the category only, so patient text cannot leak into a log by accident.
The screen runs at arrival, before the reply-coalescing window, so a fragmented message cannot delay it. It unions with the protocol's own red_flags rules.
The gate
Every patient-facing message - a rule's notification, an adherence nudge, an agent message, a symptom question, a chat reply - is evaluated by the communication gate before it is sent, and the evaluation is recorded whether or not the message goes out.
Three outcomes, and the missing-policy case
| Decision | Meaning | Delivered to a patient? |
|---|---|---|
allow | No violation of the active policy | Yes |
rewrite | A violation the policy resolves by term substitution | No. Held for a person. Delivered only to a clinician audience. |
block_and_review | A violation the policy resolves by withholding | No. Held for a person. |
policy_missing | No active communication policy for the scope | No. Held for a person. The gate fails closed. |
A rewrite is machine term substitution, not authored prose: a matched clinical term is swapped for the policy's wellness wording wherever it appears, which routinely produces sentences no human wrote. For that reason a rewrite bound for a patient is a withhold - the substituted text is stored for the reviewer to read beside the original, and it is never sent to the patient. For a clinician audience (a timeline insight) a rewrite may be delivered.
The audience is derived from the channel, never inferred from the presence of a patient id. Patient channels: patient_notification, adherence_nudge, agent_nudge, agent_notification, policy_notification, symptom_session_question, and patient chat. Clinician channels: agent_timeline_insight and clinician chat.
Review requirement
A message needs a person when the decision withholds it, and otherwise according to the policy's review_mode: none, on_violation (the default), or always. A message that needs a person is recorded with release_status: held_for_review and appears in the dashboard's Review Queue.
The hold and its release
A held record is released or rejected only by a signed-in person:
POST /api/v1/compliance/checks/{check_id}/releasewithreleased_with(originalorrewritten; defaults torewrittenwhen one exists) and optionalnotes. The record becomesreleased_after_approval, orrelease_delivery_failedif the re-delivery did not happen, so the audit trail never claims a delivery that did not occur. Re-delivery is automated today foragent_nudgeandagent_notification; other channels record the approval and leave delivery to the channel's own path.POST /api/v1/compliance/checks/{check_id}/rejectwith optionalnotes. The record becomesrejected_after_review.
GET /api/v1/compliance/checks lists records with filters (communication_channel, communication_ref, conversation_id, decision, review_required, release_status, project_id) and returns a release-status histogram beside the page. GET /api/v1/compliance/checks/{check_id} returns one record: decision, rationale, detected claims, original content, rewritten content, audience, review state, who released it and with which version. These routes are Admin scope.
The symptom agent's own outbound path adds one more check the gate cannot: after the gate returns a deliverable body, the sender verifies the mandated text (the disclaimer, the opt-out instruction) survived, and refuses the send if it did not. The emergency guidance is mandated verbatim; a rewrite of it is treated as a withhold.
Identity disclosure
Every program with an agent carries one AI-identity disclosure in its spec: an identity line that says what the patient is talking to, and a human_contact line that says how to reach a person. The identity text must not read as a licensed human; naming who reviews the answers belongs in the opening message, and the human route belongs in human_contact.
For the SMS interview the disclosure is part of the opening message, and the fact that it reached the patient is recorded as a gate decision keyed to the session. Whether a channel requires the disclosure is set per channel; when it is required and none is on record, the gate refuses substantive content. The consent invitation itself carries the disclosure text under a fixed version identifier, so a consent record names the wording the patient was shown.
Policy reference
The rule schema, the condition grammar, every policy action, and the attention formula, bands and defaults exactly as configured in code.
Care hand-offs API
The staff-only follow-up queue - list and detail, claim, contact attempt, resolve with a disposition, resume automation - with optimistic versioning and the messaging policy that routes it.