AnyBio

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.

ToolWhat it doesWhat the patient receives
askChooses 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_symptomRecords 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
escalateRoutes the interview to the care team urgently and stops asking.The protocol's static emergency_guidance, sent verbatim; the model does not write it
handoffRoutes 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:

FieldMeaning
id, version, conditionIdentity. A rule's start_symptom_session names the id.
opening_message, opening_fieldThe first patient-facing message and the question it asks. The opening must carry the non-emergency-line disclaimer.
disclosureThe AI-identity disclosure: identity (what the patient is talking to) and human_contact (how to reach a person). Required.
required_fieldsWhat the interview collects: key and a field_type (bool, severity_scale with min and max, an enum, free_note).
question_bankThe vetted question per field; pinned questions are sent verbatim.
red_flagsProgram-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_fenceAdditive items on the never list.
routingurgent_priority and the verbatim emergency_guidance text.
completionThe done condition and closing_message; optional cannot_continue_message.
sessionper_turn_window_secs, overall_ttl_secs, max_reengagements.
triggerWhen 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:

CategoryRules
Cardiaccardiac.chest_discomfort, cardiac.jaw_discomfort
Respiratoryrespiratory.breathing_difficulty, respiratory.shortness_of_breath, respiratory.cyanosis
Neurologicalneuro.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-harmself_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_flags rules, 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

DecisionMeaningDelivered to a patient?
allowNo violation of the active policyYes
rewriteA violation the policy resolves by term substitutionNo. Held for a person. Delivered only to a clinician audience.
block_and_reviewA violation the policy resolves by withholdingNo. Held for a person.
policy_missingNo active communication policy for the scopeNo. 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}/release with released_with (original or rewritten; defaults to rewritten when one exists) and optional notes. The record becomes released_after_approval, or release_delivery_failed if the re-delivery did not happen, so the audit trail never claims a delivery that did not occur. Re-delivery is automated today for agent_nudge and agent_notification; other channels record the approval and leave delivery to the channel's own path.
  • POST /api/v1/compliance/checks/{check_id}/reject with optional notes. The record becomes rejected_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.

On this page