Every inbound WhatsApp message follows this pipeline: debounce batching → pre-filter rules → human takeover check → nano LLM routing (filters acknowledgments + routes to agents) → agent execution (with conversation context) → webhook dispatch. Each stage can exit early, saving tokens and quota.
CloudApiWebhookHandler parses the 31 message types + statuses → creates BusinessMessage record → enqueues RouteInboundBatchJob.
agent.debounce_seconds (1-60s) to collect multiple rapid messages from the same phone into one batch. If a newer job exists for this conversation, the current one exits — only the latest job runs.
ConversationPause.active_for?(ba_id, phone) — checks if a human has taken over this conversation. Pauses are created manually (API), on outbound message, or by agent proposal. Normalizes +33 vs 33 formats.
30 min — configurable via human_takeover_duration. Each manual outbound resets the timer.PreFilterRules — free, rule-based pre-filter (no LLM). Checks regex patterns, spam detection, min length, excluded numbers. Filters out greetings, emojis, "ok", "thanks" without spending tokens.
InboundRouter — single LLM call (gpt-4.1-nano, temp=0) classifies the message and routes to the right agent(s). Runs for all cases — including single-agent setups — to filter acknowledgments like "ok thanks", "got it", "super merci". Returns actionable (bool) + reason + agent_ids. If not actionable → exit. On error → fail-open to all agents.
gpt-4.1-nano — configurable via ROUTING_MODEL env varInboundFilter — per-agent rules: trigger_on (all/text_only), patterns, min_message_length, max_messages_per_hour, excluded_numbers. No LLM cost — pure config.
fetch_previous_run — finds the last completed AgentRun for this phone + agent within the TTL window. Uses metadata @> conversation_key (jsonb + GIN index).
context_ttl_minutes — default 60, set to 0 to disable. If last run is older → no context (new topic).context_summary (text) + last_agent_output (intent + actions from previous run)AgentRunner — builds system prompt + enriched context → calls AI::ModelAdapter → LLM returns structured output via propose_actions tool. Output includes intent, actions, suggested_reply, and the new context_summary.
gpt-4o-mini primary, deepseek-chat fallback500-600 input + ~100-200 output tokenscontext_summary is saved on the AgentRun record. The next inbound message from this phone (within TTL) will pick it up in step 7a — closing the loop.
system.noop.skip, system.conversation.pause) are auto-injected into all agents and stripped from webhook payloads. system.conversation.pause triggers ConversationPauseManager. Internal only — never reaches user's backend.
WebhookDispatcher sends agent_run.completed event to user's endpoint. HMAC-signed, with retry. Payload includes output, tokens, latency.
"I need towels for room B12"intent=request_towel, room=B12, size=null"Guest in B12 requested towels, asked about size"
"medium ones"intent=request_towel, room=B12, size=medium"Guest in B12 wants medium towels, fulfilled"
"I also need a taxi for tomorrow at 8am" — TTL expired, no context injected. Agent treats it as a fresh conversation. Clean slate.