/** * Shared types used across the public surface. * * Importing the runtime types from `@cloudflare/workers-types` works when the * consumer has that package; we don't want a hard requirement, so we declare * the bits we need locally as structural interfaces. They match the shape of * the real bindings well enough that consumers can pass theirs unchanged. */ export type Json = string | number | boolean | null | { [k: string]: Json } | Json[]; // ---- Inbound webhook ---- export type Tier = 'free' | 'premium' | 'lifetime' | (string & {}); export type WindowType = 'free' | 'paid'; /** * Agent rollout stage. Affects whether `reply.ai()` actually sends and * whether each turn is auto-escalated for human review: * * - `shadow` — pipeline runs, the answer is logged + events emitted, * but the framework never calls `client.sendText`. For * pre-launch validation against real traffic without * user-visible side effects. * - `assisted` — answer is sent + every successful AI turn is recorded * in `escalationStore` with urgency `low` and reason * `assisted_review`. For first-week safety nets. * - `operator` — same framework behavior as `autonomous`; the label is * passed through to handlers via `ctx.mode` so app code * can gate side-effecting tool execution. * - `autonomous` — current default. Send everything; no extra audit. */ export type AgentMode = 'shadow' | 'assisted' | 'operator' | 'autonomous'; export interface InboundReferral { source_url?: string; source_id?: string; source_type?: string; headline?: string; body?: string; media_type?: string; image_url?: string; video_url?: string; thumbnail_url?: string; ctwa_clid?: string; } export interface InboundEnvelope { entry?: Array<{ changes?: Array<{ value?: { messages?: Array; contacts?: Array<{ wa_id: string; profile?: { name?: string } }>; statuses?: Array<{ status: string }>; }; }>; }>; /** Filled in by D1CoalesceQueue when multiple messages were buffered. */ _combinedText?: string; } export interface RawMessage { id: string; from: string; timestamp?: string; type: string; text?: { body?: string }; audio?: { id?: string }; image?: { id?: string; caption?: string }; video?: { id?: string; caption?: string }; document?: { id?: string; filename?: string }; interactive?: { type?: string; button_reply?: { id: string; title?: string }; list_reply?: { id: string; title?: string }; }; button?: { payload?: string; text?: string }; location?: { latitude: number; longitude: number }; referral?: InboundReferral; /** * Set by Meta when the user used the "reply to message" UI. `id` is the * wamid of the message they replied to; `from` is the original sender's * E.164 number (typically the bot's own number when they replied to a * bot message). */ context?: { id?: string; from?: string }; } export interface InboundMessageBase { kind: 'message'; wamid: string; whatsapp: string; name: string | null; timestamp?: string; raw: RawMessage; referral?: InboundReferral; fromAd?: boolean; /** * wamid of the message this one is a reply-to. Populated when the user * tapped Meta's "reply to message" UI. Lets handlers tie a correction or * follow-up to a specific earlier bot reply — e.g. "the meal you logged * 10 minutes ago had too much rice" → look up by `MessageLog.byWamid(...)`. */ inReplyToWamid?: string; /** All optional content fields below — populate only the ones that match `type`. */ type: string; text?: string; audioId?: string; imageId?: string; videoId?: string; documentId?: string; caption?: string; filename?: string; subtype?: 'button_reply' | 'list_reply' | 'interactive' | 'template_button'; buttonId?: string; buttonTitle?: string; listId?: string; listTitle?: string; buttonPayload?: string; buttonText?: string; location?: { latitude: number; longitude: number }; } export type InboundMessage = InboundMessageBase; export type InboundEvent = | InboundMessage | { kind: 'status'; statuses: Array<{ status: string }> } | { kind: 'unknown' }; // ---- Outbound message helpers ---- export interface ReplyButton { id: string; title: string; } export interface ButtonsPayload { body: string; footer?: string | null; buttons: Array; } export interface CtaUrlPayload { body: string; displayText: string; url: string; } // ---- AI ---- export interface AIChatArgs { threadId: string | null | undefined; text: string; } export interface AIChatResult { answer: string | null; threadId: string; } export interface AIClient { chat(args: AIChatArgs): Promise; } export interface SummarizerLike { summarize(text: string): Promise; } // ---- Tier gating ---- export interface TierResult { authorized: boolean; tier: Tier; } // ---- Per-turn handler context ---- export interface ReplyHelper { text(body: string, opts?: { previewUrl?: boolean; inReplyToWamid?: string | null }): Promise; /** * v0.8 sugar — send a text reply that WhatsApp threads visually under * `inReplyToWamid` in the user's chat UI. Equivalent to * `reply.text(body, { inReplyToWamid: wamid })`. * * agent.onText(async ({ inbound, reply }) => { * await reply.replyTo(inbound.wamid, 'Got your message — here is more.'); * }); */ replyTo(wamid: string, body: string, opts?: { previewUrl?: boolean }): Promise; buttons(data: ButtonsPayload): Promise; cta(data: CtaUrlPayload): Promise; image(data: { url: string; caption?: string }): Promise; video(data: { url: string; caption?: string }): Promise; audio(data: { url: string }): Promise; template(data: { name: string; language?: string; components?: unknown[] }): Promise; markRead(): Promise; ai(text: string, opts?: { threadId?: string | null }): Promise; } export interface HandlerContext { inbound: InboundMessage; user: { whatsapp: string; name: string | null; lead: Record | null }; session: Record | null; text: string; reply: ReplyHelper; db: import('./db/client.js').DB; client: import('./client/whatsapp.js').WhatsAppClient; ai: AIClient | null; summarizer: SummarizerLike | null; window: import('./window/message_window.js').MessageWindow; leads: import('./lead/lead_store.js').LeadStore; log: import('./session/message_log.js').MessageLog; tier: import('./gate/tier_provider.js').TierProvider | null; gate: import('./gate/access_gate.js').AccessGate | null; isFirstContact: boolean; fromAd: boolean; /** * Resolved agent rollout stage for this turn. Mirrors `AgentOptions.mode` * after the (sync or async) resolver runs. App code can read this to * gate its own tool execution (e.g. skip side-effects in `operator` / * `shadow`). */ mode: AgentMode; [k: string]: unknown; } export interface ButtonContext extends HandlerContext { buttonId: string; suffix: string; }