Skip to main content
The SMS handler is the core message processing pipeline that orchestrates phone resolution, context filtering, AI response generation, and enforcement checks.

Pipeline Overview

Every inbound SMS flows through 13 sequential steps:
  1. Phone Resolution — Map phone number to family → member → role → access level
  2. Context Loading — Load family.md, conversation history, member profile
  3. Pre-Filter (Enforcement) — Strip restricted sections by access level
  4. PHI Audit — Log context access with WHO/WHAT/WHEN/WHY
  5. AI Generation — Call agent with filtered context
  6. Post-Check (Enforcement) — Scan response for PHI leakage
  7. Persistence — Apply family_file_updates (backup → edit → validate)
  8. Logging — Record interaction in conversation timeline
  9. Response Return — Return SMS text + metadata to caller
Additional optional steps:
  • Approval Gating — High-risk edits require YES/NO confirmation
  • Outreach — Send messages to other team members
  • Learning — Persist corrections to lessons.md
  • Routing Updates — Add new family members

Entry Point

async def handle_sms(
    from_phone: str,
    body: str,
    dry_run: bool = False,
    service: str = "SMS"
) -> dict
from_phone
string
required
Sender phone number in E.164 format (e.g., +16517037981)
body
string
required
SMS message text from the sender
dry_run
boolean
default:"false"
If true, runs resolution and context assembly without calling the AI. Returns metadata for testing.
service
string
default:"SMS"
Transport service type: "SMS", "iMessage", or "RCS". Affects agent tone and confirmation mechanics.

Return Value

success
boolean
Whether the handler completed successfully
response
string
SMS text to send back to the user
needs_outreach
array
List of outreach messages to send to other team members
family_file_updates
object
Result of applying edits to family.md
member
object
Resolved member information
enforcement
object
Enforcement layer metadata

Key Functions

Phone Resolution

def resolve_phone(phone: str) -> dict | None
Looks up a phone number across all family routing tables. Returns member data with family context if found. Location: runtime/scripts/sms_handler.py:122
def resolve_chat_id(chat_id: str) -> dict | None
Preferred for Linq/iMessage: resolves by persistent chat UUID instead of phone number. Location: runtime/scripts/sms_handler.py:139

Context Loading

def load_family_context(family_dir: str) -> str
Loads and concatenates family.md + schedule.md + medications.md if they exist. Location: runtime/scripts/sms_handler.py:185
def load_recent_conversations(phone: str, limit: int = 50) -> str
Reads the last N lines from the member’s conversation log. Location: runtime/scripts/sms_handler.py:204

System Prompt Assembly

def build_system_context(
    member: dict,
    family_context: str,
    conversation_history: str,
    service: str = "SMS",
    member_context: str = ""
) -> str
Assembles the full system prompt from:
  • SOUL.md (agent identity)
  • agent_root.md (routing instructions)
  • capabilities.md (CAN/CANNOT list)
  • skills/*.md (conversation patterns)
  • lessons.md (global + per-family corrections)
  • Channel-specific guidance (iMessage vs SMS)
  • Filtered family context
  • Recent conversation history
CRITICAL: The family_context passed to this function must already be filtered by role_filter.filter_family_context(). The agent only sees what the member is allowed to see.
Location: runtime/scripts/sms_handler.py:289

AI Response Generation

async def generate_response(
    system_context: str,
    user_message: str,
    member_name: str = "there"
) -> str
Calls OpenRouter API with structured JSON schema. Returns JSON string with:
  • sms_response (text to send)
  • internal_notes (agent reasoning)
  • needs_outreach (array of outreach messages)
  • family_file_updates (edits to apply)
  • self_corrections (lessons to persist)
  • member_updates (member profile edits)
  • routing_updates (new member registrations)
Location: runtime/scripts/sms_handler.py:487

Message Logging

def log_message(
    phone: str,
    direction: str,
    body: str,
    family_id: str = ""
)
Appends to:
  1. /conversations/{phone}/{YYYY-MM}.log (per-member timeline)
  2. /families/{family_id}/timeline/{YYYY-MM}.log (per-family timeline)
Location: runtime/scripts/sms_handler.py:236

Response Schema

The AI returns structured JSON matching this schema:
{
  "sms_response": "Message text to send (required)",
  "internal_notes": "Agent reasoning (required)",
  "needs_outreach": [
    {
      "phone": "+16514109390",
      "name": "Marcus",
      "message": "Liban is asking about tomorrow's schedule"
    }
  ],
  "family_file_updates": [
    {
      "section": "schedule",
      "operation": "append",
      "content": "- 2026-03-01 08:00: Doctor appointment (Marcus driving)",
      "old_content": ""
    }
  ],
  "self_corrections": [
    "[behavioral] Never use 'let me check' — always give the answer or say what's missing"
  ],
  "member_updates": [],
  "routing_updates": []
}
All array fields default to []. The schema uses strict: true mode, so the AI cannot deviate from this structure.

Access Levels

Phone resolution returns one of five access levels:
LevelSeesCan Approve
fullAll sectionsYes
schedule+medsMembers, schedule, medications, appointments, availability, active issuesNo
scheduleMembers, schedule, availability, active issuesNo
providerCare recipient, medications, appointments, membersNo
limitedMembers, care recipient (basic info only)No
The enforcement layer (see Enforcement Layer) mechanically strips sections not in the allowed list.

CLI Usage

# Dry run (test resolution without AI call)
python runtime/scripts/sms_handler.py \
  --from "+16517037981" \
  --body "Can someone take auntie to work tomorrow at 8am?" \
  --dry-run

# Full run (calls AI, applies updates)
python runtime/scripts/sms_handler.py \
  --from "+16517037981" \
  --body "Can someone take auntie to work tomorrow at 8am?"

Error Handling

Unknown Phone Number

If phone resolution fails:
  • PHI audit logs the unknown number attempt
  • Returns canned response: "Sorry, this number isn't set up to receive messages..."
  • Zero PHI disclosed (hard rule)

AI Timeout/Failure

If the AI call fails after 3 retries:
  • Returns fallback message: "I hit a technical glitch processing your last message, {name}. Can you send it again?"
  • Error logged to stderr
  • No edits applied

Family Lock

All message processing for the same family is serialized via family_lock(family_id) to prevent race conditions when two messages arrive simultaneously. Location: runtime/enforcement/message_lock.py

Build docs developers (and LLMs) love