Skip to content

GiveCare SMS Runtime Contract

Diátaxis: reference

This reviewed document is the canonical public contract for the current SMS runtime. Cross-repo consumers use its exact gc-sms.care care.runtime.project owner projection, pinned to a commit through the root projection-ref command. They do not infer current behavior from source files.

GiveCare SMS is organized around a small runtime kernel:

Inbound -> Gate -> Context -> Reason -> Commit

The product invariant is:

The model may propose; only the harness may make reality true.

State-Machine Kernel

The kernel above is one instance of a broader rule: every durable concern in this runtime is a revisioned state machine advanced by a pure transition policy, not a process that keeps running on its own.

  • One state machine per concern. Turns (received -> reasoning -> committed -> review -> failed), loops (open -> blocked -> closed -> dismissed, via the pure transitionLoop policy in domain/care/loop.ts), outbox rows (queued -> sending -> accepted -> delivered -> undelivered -> failed -> blocked), reviews (open -> resolved), memories (active -> superseded -> deleted), consent (active -> paused -> stopped, with the safety hold an independent timestamp beside it), and assessment runs (in_progress <-> paused, then complete or stale abandoned) are each one row with an enumerable set of legal transitions.
  • Gates are transition guards. A gate does not exist to classify a message; it exists to decide whether a transition may happen at all.
  • Evidence is the price of a transition. A loop or memory intent must carry an evidenceQuote from one message in the current pending input burst. Memory provenance retains that message's source turn. Source references must come from tool evidence observed in the same reasoning span. See Commit Rules below for enforcement.
  • Events are history, not state. Append-oriented event rows explain how a row got here; nothing reads them back as the source of truth for what happens next.
  • Observed action changes. A caregiver action advances only from new observed evidence. There are no action reminders or fixed assessment invitations. Mira may direct a needed assessment in an active conversation. Convex owns bounded transport jobs and checks consent before each send.

Design test for any new feature: what row, what transitions, what guard, what evidence, what revision? A feature that cannot answer all five does not yet fit this kernel.

Layers

Domain Layer

domain/ defines the product contract in plain TypeScript:

  • inbound shape
  • boundary decisions
  • context shape
  • ReasonResult
  • ProposedReply
  • ProposedIntent
  • deterministic gates
  • validation rules

This layer should not depend on Convex or Pi. It is the shared contract the runtime must preserve.

Agent Layer

agent/ adapts Pi Agent Core to the GiveCare contract.

It owns:

  • the Mira system prompt
  • approved AgentTool definitions
  • final ReasonResult TypeBox parameters
  • redacted Pi event trace capture
  • provider-request, tool-call, and total-span bounds
  • final result validation before returning to Convex

The live Convex action registers only the OpenAI Pi provider. agent/model.ts selects GPT-6 Luna in code with one shared prompt cache key. OPENAI_API_KEY supplies authentication. The bakeoff may register other providers inside an isolated evaluation runtime with outbox dispatch disabled.

Single-provider is a deliberate refusal, not a gap: a failover path would add a second model contract to verify (prompt parity, tool parity, safety-parity evidence) for an outage mode Twilio/Convex incidents have not yet produced. Model changes go through the bakeoff screen instead. Revisit only when a real provider outage or a bakeoff-proven second model creates the need.

It does not own memory, scheduling, outbox, consent, transport, proof, or learning promotion.

Care playbooks in agent/skills.ts supply static purposes and boundaries, rendered once into the system prompt. Mira chooses which fits the request. There is no skill-loading tool or per-turn skill-use receipt. Playbooks carry no caregiver state, current external facts, permissions, or safety authority.

eval/scenarioRunner.ts and eval/scenarioKernel.ts seed isolated Convex state and compose the production gate, context, Pi turn, commit, fallback, review, and outbox path for private scenario trees. State commits are local to the evaluation instance and outbox dispatch is disabled. This is an evaluation adapter over the runtime, not a second product commit path.

Convex Layer

convex/ owns durable reality:

  • caregiver and channel state
  • conversations, turns, messages
  • memory and loop commits
  • assessment runs and score snapshots
  • events and outbox
  • durable operator reviews
  • operator/proof projections
  • revisioned partner-health snapshots
  • scheduling the Pi action
  • validating and applying proposed intents

Convex is not the entire domain harness by itself, but it hosts and enforces most of the harness.

Owner Projection Layer

knowledge/ adapts generated JSON bundles into runtime search contracts:

  • benefits
  • resources
  • strategies
  • guidance

All generated inputs are downstream artifacts. Each source owner commits its projection to Git. Each gc-sms consumer resolves a pinned owner-projection ArtifactRef for an exact owner commit through the shared GiveCare protocol's projection-ref command, verifies the artifact digest, validates the owner schema, and replaces only fixed local files atomically.

  • gc-benefits.registry projects the benefits search bundle.
  • gc-wiki.knowledge projects resources, strategies, and reviewed guidance.
  • tools.assessments projects the assessment instruments.
  • evals.dataset projects the public cases used by the retrieval quality gate.

These consumers are declared as owner-projection-sync adapters in .givecare/module.json. They do not import another repository, fetch live owner data, accept an unverified file path, or maintain another source of truth.

Main Flow

  1. The signed Twilio webhook passes its posted message fields directly to convex/turns.ts#receiveInbound, which records durable inbound state.
  2. evaluateGate handles hard boundaries before model reasoning.
  3. Boundary outcomes commit immediately.
  4. Allowed turns enqueue convex/mira.ts#runTurn.
  5. convex/context.ts#assembleForMira supplies the pending input burst, every active memory and outcome candidate, the SMS thread (up to 20 earlier messages: what the caregiver said, except messages whose remembered fact was deleted, and what Mira delivered), and the source-linked GiveCare trajectory. Pi reads the thread directly as prior messages; no relevance judgment runs before Pi. Native outcomes retain their sources without a copied memory row.
  6. agent/run.ts#runMiraAgentTurn runs Pi Agent Core with approved tools (search, search_nearby_care, final_reason_result). After two completed provider requests, prepareNextTurn exposes only final_reason_result, which reserves one result request and one repair request. A nearby-care lookup records typed evidence of what the call returned, not provider absence or availability. Pi writes the reply itself, offering an alternative category or ZIP.
  7. Pi must call the single-assignment final_reason_result. Code checks shape and exact references before Jev. Invalid proposals receive field, reference, or assertion feedback for bounded repair; code does not delete substantive sentences. Tool-free normal and token-limit stops get a final-only follow-up within the same request and time limits. Only a validated result can terminate successfully.
  8. turns.commitAgentReasonResult verifies and commits. New-memory comparisons share the reply-validation request to detect changes that require an explicit correction. Semantic matches cannot suppress new observations; only identical same-source proposals reuse a row. Convex rereads compared evidence before acting. Semantic or contract failure recovers immediately; only a transient action or expired reasoning lease gets one retry before recovery and durable review.

Signup Flow

Web enrollment is a separate adapter, not an inbound turn or onboarding workflow:

  1. POST /api/signup strictly validates the phone against its numbering plan, normalizes it to E.164, and validates first name, email, explicit SMS consent, a separate optional GiveCare-update opt-in, and bounded attribution.
  2. Component-backed limits apply to hashed IP and phone keys.
  3. A supplied sponsor code must resolve to an active organization with capacity. Referral codes remain reported attribution.
  4. A supplied assessment handoff imports its reading only when unused, unexpired, and bound to the normalized signup email; otherwise enrollment proceeds without the reading and the drop is recorded as an event.
  5. One Convex mutation creates the caregiver, active SMS channel, conversation, outbound message, consent/signup events, email contact, and welcome outbox row; optional GiveCare subscription, organization count, and handoff redemption commit atomically with them. It schedules one transactional signup receipt after commit.
  6. Existing phone enrollment is a no-op; it does not overwrite identity or reactivate stopped consent.
  7. Transport re-checks consent and safety before delivery and blocks stale welcome work after 15 minutes.
  8. The welcome explains the care-support aim and offers an introduction, help, or a starting point. Taps and typed messages enter the same SMS flow. Convex opens a support view only from exact chip text or its typed label, before any model call; code renders the view from current state. Specific needs go straight to ordinary care reasoning. Mira learns care context gradually. Score views show stored readings and next choices; requesting a score alone never starts questions.

First-Party Web Adapter

The active web properties keep three narrow contracts: event capture, one named-publication opt-in, and transactional email delivery. Inputs are origin-restricted, rate-limited, allowlisted, and bounded. Transactional signup/assessment delivery is independent of marketing consent. Each publication unsubscribes independently; bounce and complaint suppress the contact globally. The server derives the native BSFC-s total and published interpretation from submitted answers; the browser cannot supply scoring or local subscales.

This surface is intentionally an adapter around current domain and Convex primitives. It does not add campaign workflows, generic admin actions, or client-authored assessment results. The /api/admin route exposes only an atomic authenticated read of the latest committed population-health snapshot consumed by gc-web; it never fans out over caregivers on request.

Boundary Paths

Boundary decisions are deterministic for:

  • exact STOP/HELP transport-managed SMS responses, including Twilio Advanced Opt-Out OptOutType when the carrier already replied
  • pause/restart language
  • safety-hold blocking
  • obvious emergency/self-harm/poison/abuse signals
  • identity-sensitive account requests when identity is unverified

The model is used only after the gate returns allow and Convex reserves the reasoning attempt. Exact assessment protocol commits before any model call:

gate -> exact protocol or reservation -> context -> input judge (message)
     -> reason (floored) -> reply judge (check) -> commit

domain/assessments/protocol.ts owns assessment decisions over the whole unanswered burst. convex/assessmentRuns.ts supplies trusted state and scoring. routeTurn checks consent and turn order before committing exact assessment or support-menu protocol, or reserving an attempt. Duplicate workers cannot spend another admission slot or make concurrent model calls for that attempt.

One Jev request reads the caregiver's message before Pi drafts anything: a safety route, the concrete-question, check-back, and no-request signals, whether the caregiver says Mira got the prior exchange wrong, and, when a typed assessment offer is still open, assent to it. A named current care problem seeking help now counts as a concrete question even beside a yes to an earlier offer. An offer's chip sends its accept keyword directly, so tapping starts the assessment without a model call; a typed "yes" goes to Pi, which proposes starting the assessment, and verification checks that assent and current state before the start commits. This reading is frozen for the turn: it gates which schema Pi may propose (whether an assessment, check-back, or loop intent is on the table) and it floors the draft's risk before the reply is checked, so a later check cannot lower a safety floor the message already set. An unavailable or invalid reading ends the turn with judge_unavailable before any Pi request.

A second Jev request, after Pi's draft, checks only the reply: its claims, clinical and legal authority, actor, effect, self-hedging, and intent support. Missing required reply evidence ends the turn with judge_unavailable. Each effect is read independently and follows its declared failure policy.

Mira chooses which care playbook fits the request herself, from playbooks rendered once into the system prompt; there is no move ranking, prerequisite classifier, or planner. Routine fact extraction does not run every turn. Benefits screening resolves the fields its matched programs use, including corrections to populated fields. search reads the current message and earlier caregiver statements in the relevance request, then screens with the resolved view. Later tools and reply verification share that view. An exact answer to a delivered question applies before Pi and can persist without another search. Earlier statements inform that reading but are not stored; only an observation from the current message can commit. A recipient-subject fact (an age, a diagnosis) carries the named recipient — Dad, Mom — so one recipient's facts never screen or answer for another; without a named recipient only unlabeled facts apply. The one search tool queries benefits, resources, guidance, and strategies in parallel and reranks all of them together with one relevance judgment after retrieval; a failed relevance judgment returns the unranked results rather than none. Code owns field bounds and eligibility arithmetic. Convex validates exact-source field observations before committing them to memory. A bad or uncertain field answer withholds only that field's old known value from the current reply and screening; valid fields remain usable, and a missing field judgment falls back to the already-known facts rather than ending the turn.

Every generated reply sentence is judged in one batch. Convex requires a complete receipt bound to the exact reply, redacted caregiver input, and cited evidence. jev/ owns model requests and response validation; domain/judgments.ts owns the product rules over normalized evidence. Missing reply judgments use recovery and review. riskFloor derives urgent or emergency handling from an observed inbound route and preserves it through generation, commit validation, and recovery. A lower-priority proposed route cannot remove an emergency hold. The deterministic safety gate remains independent of Jev.

Earlier calibration results in docs/model-selection.md describe the earlier question contract. They do not validate the current prompts or operating cutoffs. Current receipts and independent labels are required before release.

Safety is enforced at increasing-cost boundaries: deterministic Gate decisions, typed and tool-bounded Reason output, reply/intent verification, and effect-preflighted Commit. Offline gc-bench verification consumes transcripts afterward and never substitutes for the live gate.

The code-coupled hazard and proof contract is in safety-contract.md.

Commit Rules

Commit re-checks channel state before creating an outbound outbox row. This protects against races where a caregiver opts out while Pi is reasoning.

Transport owns delivery state. The committing mutation schedules due outbox work immediately; the one-minute drain is recovery, not the normal delivery path, and fans out each due row as its own send action. Convex mutations atomically move an outbox row from queued to sending before Twilio is called; the action records provider acceptance, and later callbacks reconcile delivered/undelivered/failed status. If a signed callback races ahead of SID attachment, Convex stores it by provider SID and replays it transactionally when acceptance commits. The model never participates in delivery state.

Consent is send-class based. Welcome messages, ordinary replies, deterministic system replies, safety replies, and accepted follow-ups have explicit delivery rules, so enrollment or a same-turn reply does not become blanket follow-up permission. The pure delivery policy returns send, defer, or block; transport is the sole final evaluator and reuses sendAfter for caregiver-local follow-up windows.

Action reminders and fixed assessment invitations are retired; historical rows of those classes cannot send. Two scheduled messages exist, each one consent-checked outbox row: the bounded signup welcome follow-up, and one check-back the caregiver asked for in their own words, sent at the time they named inside their daytime window.

Reply and intent effects are validated together. Effects commit before the reply row. Convex adds truthful receipts for memory correction, deletion, and action closure. An optional low-confidence new memory can be skipped without an operator review. Privacy and correction failures still require review.

Exact protocol follows the deterministic policy gate. Other allowed text receives the bounded Jev input reading before support admission. Per-caregiver/global windows bound Pi admission, while the Pi adapter independently caps provider tokens, each request's time, four provider requests, tool calls, and the total reasoning-span deadline. The final-result tool permits one in-span repair and never captures an invalid result. The single retry for a transient action or expired lease does not consume a second admission slot; semantic failures recover immediately. A quota refusal uses the recorded safety floor when present; otherwise repeated wait notices are suppressed. The quota cannot label an unread message ordinary.

The caregiver loop is deliberately small: one active loop stores the larger objective, one current action, the expected observation, and a stop condition. Observed results update that same row through a pure transition policy; append-only events retain history. Parallel active loops, nested plans, and model-owned workflow state are not allowed.

Mira offers an assessment only when code finds the gap (a missing baseline or an eligible targeted area, outside the offer cooldown) and the score would change the help now. The caregiver starts it: a command, or assent to the delivered offer. Convex rejects any other start. Convex owns the exact questions, observed answers, scores, and eligibility. A targeted GC-SDOH-30 refinement requires a valid baseline and eligible low domain, without an intervening support turn. Current needs interrupt without losing the question. No assessment completion schedules another invitation.

Current Production Gaps

The runtime is not yet a full SMS production service. Missing pieces include:

  • deployed Twilio smoke tests against the exact component and account configuration

Operator notification uses one timestamp-and-creation cursor per source. notifyOperatorWork runs every 15 minutes for open reviews, new safety holds, and blocked loops. sendOperatorAlert advances those cursors only after at least one configured channel accepts the alert (webhook 2xx or Resend enqueue). A failed channel set therefore re-alerts instead of dropping work; channel acceptance is not delivery confirmation.