Last updated: 2026-06-29
Status: Architecture finalized. Phase 6A (intelligence layer, no storage change) ready; Phase 6B (DO/D1 storage) deferred. Verified against live source 2026-06-30.
What is Chat Engine?
Chat Engine is vlozi's customer conversation intelligence platform — the layer between a business's contacts and their AI bot, turning raw messages into meaningful, configurable, and measurable conversations.
Vision Statement
Every business should be able to deploy a conversational AI that matches their brand, adapts to their customers, and gets smarter over time — without writing a single line of code.
Design Principles
- Configuration over code — every behavior is a setting, not a deployment
- Progressive intelligence — start simple, unlock depth as the business grows
- Cost-aware by design — LLM token cost is the dominant cost; every architectural decision optimizes for it
- Resilient by default — the contact always gets a response, even when dependencies fail
- Privacy as default — data is tenant-scoped; deep memory requires explicit opt-in
- Voice-ready — text architecture is designed so voice is an additive layer, not a rewrite
Memory Tiers
| Tier | Use Case | History | Summary | CI Integration |
|---|---|---|---|---|
| 0 | FAQ widget, docs, landing page | None | No | No |
| 1 | Product page assistant, transactional | Last 3–5 msgs | No | No |
| 2 | Customer support desk, order handling | Full session | Yes (brief) | No |
| 3 | Companion, coach, high-value service | Full session | Yes (detailed) | Yes — L1+L2+L3 |
| 4 | Proactive sales, win-back, campaigns | Full session | Yes (detailed) | Yes + outreach delivery |
Tier drives default settings. Every dimension can be individually overridden.
Storage Stack
CE today runs entirely on Neon (ce_bot_config, ce_channels, ce_sessions, ce_messages). That stays the default. The DO/D1 evolution below is Phase 6B — deferred, and justified by Neon cost/load at scale + voice-readiness, NOT by text latency (text response time is ~90% LLM; shaving DB ms is invisible to the user).
Phase 6B target (deferred)
SQLite DO → active session working set (new DO class — see note below)
D1 → message archive ~5-10ms, ULID composite key
Neon → sessions, config, channels stays — complex queries / inbox
Analytics Engine → intent events, metrics write ~1msThe DO reality (important correction)
The current SessionScheduler DO (scheduler.ts) uses the KV-style ctx.storage API, not SQLite tables, and is instantiated only on escalation (it self-destructs after the SLA alarm fires). It cannot be "extended" into a per-message store:
- A DO's storage backend (KV vs SQLite) is fixed at class creation via wrangler
new_sqlite_classesmigration — you generally cannot add SQLite to an already-deployed KV-backed DO. Phase 6B needs a new SQLite-backed DO class. - Making it the message hot path routes every message through a DO stub — a request-flow re-architecture, not an extension.
- Latency note: DO storage is sub-ms inside the DO, but the Worker→DO
fetchhop is ~1–5ms in-colo. That beats Neon's ~15–30ms, but it is not "0.2ms" — and the LLM (~1.2s) dominates regardless. This is a cost/scale play, not a speed play.
D1 message archive (Phase 6B)
CREATE TABLE ce_messages (
id TEXT, -- ULID: lexicographic sort = chronological order
session_id TEXT,
tenant_id TEXT,
role TEXT,
content TEXT,
intent TEXT,
model TEXT,
latency_ms INTEGER,
PRIMARY KEY (session_id, id)
);
CREATE INDEX idx_tenant ON ce_messages (tenant_id, id DESC);ULID as message ID (current code uses crypto.randomUUID() — ULID is a change): first 10 chars encode the millisecond timestamp, so lexicographic sort = chronological. ORDER BY id DESC LIMIT N on the composite key is a covering-index hit, O(1) regardless of total rows.
Neon (stays — the default today and after 6A)
ce_bot_config, ce_channels, ce_sessions, ce_messages — complex filters, joins, paginated inbox. Until 6B is justified, all message reads/writes stay on Neon, exactly as the orchestrator does today.
Cloudflare Analytics Engine (Phase 6B)
One event per message:
{ tenant_id, session_id, intent, model, latency_ms, resolved, escalated, crisis_level }Dashboard aggregates (intent distribution, resolution rate, p50/p95) read from here — zero Neon cost for analytics.
Verified Against Code (2026-06-30)
A pass over the live CE source confirmed these gaps the roadmap targets:
| Gap | Location |
|---|---|
intent column exists, never populated |
schema.ts:141 |
ce_audit_log table exists, never written |
schema.ts:166 |
leadCaptureRules JSONB exists, no engine |
schema.ts:39 |
recall_memory is the only LLM tool |
llm.ts:43 |
| Message IDs are UUID, not ULID | orchestrator.ts:161 |
No initiated_by on ce_sessions |
schema.ts:90 |
| COMMS alert on SLA breach is a TODO | scheduler.ts:78 |
| ~80% duplication processChat/streamChat | orchestrator.ts |
session_count note: the orchestrator passes ce_sessions.messageCount to CI as session_count (orchestrator.ts:201). It increments per message, so it's really "messages so far this session," used as a rough cold-start proxy. Flagged for CI alignment, not a blocker.
Pipeline (Phase 6A)
Message arrives
↓
load session (summary + history + config) ← Neon today; SQLite DO in 6B
↓
Promise.all([
fetchCIContext(), ← Tier 3+
searchKnowledge(), ← if enabled
classifyIntent(), ← always, Haiku
])
↓
model = routeModel(intent, crisis_level, memoryTier)
↓
LLM(system[cache_control], history, message, tools)
↓
Return response to contact
↓
waitUntil (non-blocking):
├── persist messages (Neon today; D1 batch in 6B)
├── update session (messageCount, lastMessageAt, summary)
├── populate ce_messages.intent
└── AnalyticsEngine.writeEvent (6B)The Promise.all is a genuine, cheap win — but its value is keeping preprocessing cost flat as we add steps (intent, moderation), not cutting user-perceived latency. For text the LLM (~1.2s) dwarfs the ~30–40ms of preprocessing either way.
Intent → Model Routing
Every message is classified before the LLM sees it. Classification result drives model selection.
Intent labels: greeting | short_question | chitchat | complaint | product_question | order_status | purchase_intent | escalation_request | feedback | off_topic
Routing rules:
greeting | short_question | chitchat → haiku (10× cheaper)
complaint | purchase_intent | complex → sonnet
crisis_level = distress | crisis → sonnet (always, no exception)
memoryTier = 0 | 1 → haiku (always, no deep reasoning needed)70% of customer messages are simple. 70% Haiku + 30% Sonnet routing = ~90% LLM cost reduction vs all-Sonnet.
Prompt Caching
System prompt is identical across all messages for a tenant (persona + rules + knowledge).
Marking it with cache_control: { type: "ephemeral" } → Anthropic charges ~10% of normal rate on cache hits.
At 10M messages/month with a 500-token system prompt:
- Without caching: ~$15,000/mo in system prompt tokens
- With caching: ~$1,500/mo
Single most impactful cost change. But the change lives in @repo/brain, not CE. Today CE passes system as a plain string (llm.ts:67). cache_control must be attached to a structured system block inside the @repo/brain Anthropic provider and threaded through the shared LLMProvider interface. That package is used by other services — so this touches shared code and needs regression checks beyond CE.
Configuration System
interface BotConfig {
// Memory
memoryTier: 0 | 1 | 2 | 3 | 4
historyWindow: 3 | 6 | 10 | 20
summaryEnabled: boolean
summaryDepth: "brief" | "detailed"
ciEnabled: boolean // requires tier 3+
// Model
primaryModel: "haiku" | "sonnet" | "opus"
fallbackModel: "haiku" | "sonnet"
perChannelModel?: Record<string, string> // e.g. { whatsapp: "haiku", widget: "sonnet" }
// Context
knowledgeEnabled: boolean
knowledgeSources: string[]
crisisDetectionEnabled: boolean
moodAwarenessEnabled: boolean
languageMode: "auto" | string
// Behavior
responseLength: "brief" | "balanced" | "detailed"
tone: "formal" | "casual" | "friendly" | "professional"
handoffTrigger: string
maxDailyMessagesPerContact?: number
contentModerationEnabled: boolean
fallbackMessage: string
// Channels
enabledChannels: ChannelType[]
perChannelResponseFormat?: Record<string, string>
perChannelMaxLength?: Record<string, number>
// Actions
builtinTools: BuiltinTool[]
customTools: CustomTool[] // webhook-configured, no code change needed
// Proactive (tier 4 only)
proactiveEnabled: boolean
quietHoursStart?: number
quietHoursEnd?: number
maxOutreachPerDay?: number
reEngagementThresholdDays?: number
}Config is read once from Neon on first message, then cached in DO for the session lifetime.
Cost at Scale
| Item | 10M messages/month |
|---|---|
| LLM — prompt caching + 70% Haiku / 30% Sonnet | ~$3,200 |
| CF Workers + DO invocations | ~$150 |
| D1 writes (2 per message, batched) | ~$10 |
| Neon (sessions + config only, no message queries) | ~$40 |
| Analytics Engine | ~$2.50 |
| Total | ~$3,400/mo |
Without optimization: ~$35,000/mo (all Sonnet, no caching).
Savings: 90% — almost entirely from LLM routing and prompt caching, not from DB architecture.
The DB architecture (DO + D1) does not drive cost savings — those come from LLM routing + caching, which need zero storage change. DO/D1 is about Neon load at scale and voice-readiness. That's why it's split into 6B below.
Key Capabilities (Phased)
Phase 6A — Intelligence Layer (Next, no storage change)
- Parallel preprocessing (
Promise.allover CI + knowledge + intent) - Intent classification on every message → populate
ce_messages.intent - Intent → model routing (Haiku vs Sonnet)
- Prompt caching in
@repo/brainAnthropic provider (the real cost lever) - Write
ce_audit_logon config / channel / session changes - COMMS alert on SLA breach (scheduler.ts:78)
- Consolidate processChat / streamChat duplication
- Circuit breaker on CI calls
Phase 6B — Storage Evolution (deferred — gate on Neon cost or voice)
- New SQLite-backed DO class (not the KV SessionScheduler) for the session working set
- D1
ce_messageswith ULID composite key - Analytics Engine events
- Justification: Neon cost/load + voice-readiness — not text latency
Phase 7 — Action Layer
- Tool registry (built-in + custom webhook tools)
recall_memory,search_knowledge,collect_lead,escalate_to_human,submit_form- Tenant-configured HTTP webhook tools via dashboard
Phase 8 — Proactive Sessions
- Outgoing session model (
initiatedBy: "inbound" | "proactive") - CF Queue consumer for outreach delivery
- CI B3 bug fix (outreach triggers now actually delivered)
Phase 9 — Human Collaboration
copilotmode — AI drafts, human approvessupervisedmode — AI responds, human can inject- Draft holding (
role: "draft", not delivered until approved)
Phase 10 — Conversation Flows
- Flow graph data model (nodes + edges)
- Node types: message, question, tool_call, branch, handoff, end
- Session variables for slot filling
- Flow execution engine wrapping orchestrator
Phase 11 — Multimodal Input
- WhatsApp / Telegram: images → vision, audio → transcription, documents → text extraction
- All transformed to text before LLM pipeline
Phase 12 — Rich Response Formats
- WhatsApp interactive buttons and list messages
- Widget: markdown, quick replies, cards
- Per-channel format configuration
Phase 13 — Webhook Event System
session.started,message.received,message.sent,session.escalated,session.resolved,lead.captured,intent.detected- Tenant subscribes per event in dashboard
Phase 14 — Cross-Channel Continuity
- Same contact on WhatsApp + widget = one session, shared CI memory
- Uses CI
ci_contact_channelsmerge model
Phase 15 — A/B Testing + CSAT
- Persona variant testing with outcome tracking
- Optional CSAT rating on session close
Phase 16 — Voice Agent
- WebSocket handler (bidirectional, replaces SSE for voice)
- STT integration (Deepgram Nova-2)
- TTS integration (Cartesia Sonic or ElevenLabs Turbo)
- Barge-in / interruption handling
- Voice-optimized model routing (Groq Llama for lowest TTFT)
- Separate architecture doc:
docs/chat-engine/voice-agent.md
What Is Intentionally Preserved
- Billing accumulator pattern —
canChatProceed()+accumulateAndMaybeSettleChatCost()works well - Durable Object SLA scheduler — extend with COMMS alert on breach, don't replace
- Session-per-channel-key model (
wa-{phone}) — stable, simple, no change - Hono router structure — add routes, don't restructure
- Neon for sessions + config — D1 migration deferred; risk not worth it now
What Is NOT in CE's Scope
- Contact Intelligence (memory extraction, relationship tracking) — separate service
- Content scheduling, blog, forms — separate services; CE calls them as tools
- Authentication — handled by gateway before CE ever sees a request
- Media storage — CE receives media URLs from channels, passes to media-service