/** * Agent — front-of-house composer for a WhatsApp bot on Cloudflare Workers. */ import { WhatsAppClient } from './client/whatsapp.js'; import { D1CoalesceQueue, type BatchInfo, type D1QueueOptions } from './queue/d1_coalesce_queue.js'; import { CommandRouter, type CommandHandler } from './router/command_router.js'; import { ButtonRouter, type ButtonHandler } from './router/button_router.js'; import { SessionStore } from './session/session_store.js'; import { MessageLog } from './session/message_log.js'; import { LeadStore } from './lead/lead_store.js'; import { MessageWindow } from './window/message_window.js'; import { type DB } from './db/client.js'; import type { TierProvider } from './gate/tier_provider.js'; import type { AccessGate } from './gate/access_gate.js'; import type { OnboardingFlow } from './flow/onboarding.js'; import { type Emit, type EventsBindings } from './events/emit.js'; import type { AgentPipeline } from './pipeline/pipeline.js'; import type { Blocklist } from './security/blocklist.js'; import { type ReplyEnricher, type ReplyEnricherFn } from './ai/reply_enricher.js'; import type { EscalateArgs, EscalationStore, EscalationUrgency } from './escalate/escalation_store.js'; import type { AgentReviewQueue } from './review/review_queue.js'; import type { AgentMode, AIClient, HandlerContext, SummarizerLike, InboundEnvelope } from './types.js'; export interface AgentOptions { whatsapp: { endpoint: string; token: string; verifyToken?: string; appSecret?: string; }; /** * D1 binding OR a pre-built Drizzle client. v0.7+: foreign Drizzle * clients (typed against another schema) are accepted and rebound * against the framework schema internally — see `normalizeDb`. */ db: D1Database | DB; ai?: AIClient | null; summarizer?: SummarizerLike | null; summarizeOver?: number; tierProvider?: TierProvider | null; gate?: AccessGate | null; onboarding?: OnboardingFlow | null; stores?: { session?: SessionStore; log?: MessageLog; leads?: LeadStore; window?: MessageWindow; }; queue?: Partial>; contextHook?: ((ctx: HandlerContext) => Promise>) | null; /** * Wire framework events to Cloudflare Analytics Engine. Pass `{ env }` to * pick up `env.EVENTS`; omit to silently no-op (useful for local dev or * bots that don't ship telemetry). `tenantId` flows through every emitted * event for multi-tenant SaaS use. */ events?: { env: EventsBindings; tenantId?: string; }; /** * Composable agent pipeline (intent → policy → LLM → audit). When set, * `reply.ai(text)` routes the turn through this pipeline instead of * calling `AIClient.chat()` directly. Backward-compatible: omit to keep * the simple direct-chat path from v0.1. */ pipeline?: AgentPipeline | null; /** Optional tenantId stamped on every per-turn pipeline context. */ tenantId?: string; /** * Abuse blocklist. When set, every inbound message is checked against it * before any handler runs. Blocked messages are silently dropped (no * lifecycle hooks fire); they ARE logged + emit an `error` event tagged * `source: 'blocklist'` for triage. */ blocklist?: Blocklist | null; /** * Post-LLM enrichment applied inside `reply.ai()` before the answer is * sent and logged. Runs on both the long answer and the summary, so any * appended footer survives summarization. Pass `null` (default) to skip. * * Use cases: append citation footers, CTA links, UTM-tagged URLs, or * affiliate-suffixes for free-tier users. Plain functions are accepted * for inline cases (no class needed). */ replyEnricher?: ReplyEnricher | ReplyEnricherFn | null; /** * Persist + (optionally) fan out escalations from the pipeline. When a * pipeline decision has `action: 'escalate'` and this is set, the Agent * records the escalation in D1 before sending the (possibly null) reply. * Manual `escalationStore.record({...})` calls from handlers also work. */ escalationStore?: EscalationStore | null; /** * Default urgency when the pipeline doesn't supply one. Default 'medium'. * Override per-app for noisier (low) or pre-escalated (high) defaults. */ escalationDefaultUrgency?: EscalationUrgency; /** * Transform the auto-record `EscalateArgs` before they reach * `escalationStore.record(...)`. Sync or async. Use this to augment the * framework defaults with app-specific data the Agent doesn't model: * * onEscalate: async (args, ctx) => { * const patientId = await resolvePatient(args.whatsapp, ctx.tenantId); * return { ...args, extraColumns: { patient_id: patientId } }; * } * * Closes the v0.6 gap where apps with rich escalation schemas (psico's * `patient_id` FK) couldn't use the auto-record path — the framework * had no way to add `extraColumns` to args it constructed. */ onEscalate?: (args: EscalateArgs, ctx: HandlerContext) => Promise | EscalateArgs; /** * Queue-and-approve store for `assisted` mode. When set + `mode === 'assisted'`, * `reply.ai()` enqueues a `pending` row INSTEAD of calling `client.sendText`. * A human-review dashboard reads the rows and acks; the per-tenant cron * `MultiTenantAgentRegistry.dispatchApprovedReviews` (or your own handler) * picks up approved rows and dispatches them. * * Without this option, `assisted` mode still records the AI turn as an * `assisted_review` escalation (v0.5) and sends the answer immediately — * the v0.8 review queue closes the loop by gating the send on approval. * * Single-tenant apps can also subscribe directly to approved rows via * a cron handler that calls `queue.list({ status: 'approved' })`. */ reviewQueue?: AgentReviewQueue | null; /** * Agent rollout stage. `'autonomous'` (default) keeps the framework's * v0.4 behavior. Set a string for a fixed mode across all turns; pass * a function to vary per-turn (typically per-tenant via a tenant * lookup against `ctx.user`): * * mode: 'shadow' * mode: async (ctx) => (await tenantStore.get(ctx.tenantId)).mode * * See `AgentMode` for the semantics of each stage. */ mode?: AgentMode | ((ctx: HandlerContext) => AgentMode | Promise); } type Lifecycle = 'onFirstContact' | 'onMessage' | 'afterReply' | 'onError'; /** Media types `agent.on` sugar covers (v0.17+). */ export type MediaType = 'image' | 'audio' | 'video' | 'document' | 'sticker' | 'location' | 'contacts'; /** * Guard verdict (v0.17). Return `null` to allow dispatch. Return an object to * deny — the framework replies with `.reply` (when set) then aborts dispatch. * The guard is a good place for paywall / trial / feature-flag / geo checks. */ export type GuardVerdict = null | { reply?: string; silent?: boolean; }; export type GuardFn = (ctx: HandlerContext) => GuardVerdict | Promise; export type MediaHandler = (ctx: HandlerContext) => void | Promise; export declare class Agent { readonly client: WhatsAppClient; /** Drizzle ORM client (v0.2+). Use `agent.db.select()/.insert()/...` in handlers. */ readonly db: DB; /** Bound event emitter (no-op when no `events` config was supplied). */ readonly emit: Emit; readonly queue: D1CoalesceQueue; readonly session: SessionStore; readonly log: MessageLog; readonly leads: LeadStore; readonly window: MessageWindow; readonly commands: CommandRouter; readonly buttons: ButtonRouter; /** Media-type handlers registered via `agent.on{Image,Audio,Video,Document,Sticker,Location,Contacts}` (v0.17). */ readonly _mediaHandlers: Map; /** Pre-dispatch guards registered via `agent.guard(fn)` (v0.17). */ readonly _guards: GuardFn[]; readonly verifyToken: string | null; readonly appSecret: string | null; readonly ai: AIClient | null; readonly summarizer: SummarizerLike | null; readonly summarizeOver: number; readonly tierProvider: TierProvider | null; readonly gate: AccessGate | null; readonly onboarding: OnboardingFlow | null; readonly pipeline: AgentPipeline | null; readonly tenantId: string | null; readonly blocklist: Blocklist | null; readonly replyEnricher: ReplyEnricher | null; readonly escalationStore: EscalationStore | null; readonly reviewQueue: AgentReviewQueue | null; readonly escalationDefaultUrgency: EscalationUrgency; readonly onEscalate: ((args: EscalateArgs, ctx: HandlerContext) => Promise | EscalateArgs) | null; readonly mode: AgentMode | ((ctx: HandlerContext) => AgentMode | Promise); readonly _lifecycleHooks: Record void | Promise>>; readonly _cronJobs: Map void | Promise>; readonly contextHook: ((ctx: HandlerContext) => Promise>) | null; constructor(opts: AgentOptions); command(aliases: string | string[], handler: CommandHandler): this; onText(handler: CommandHandler): this; buttonPrefix(prefix: string, handler: ButtonHandler): this; button(id: string, handler: ButtonHandler): this; /** * Register a handler for a specific media type (v0.17). Pre-v0.17 the only * way to handle non-text inbound was `agent.on('onMessage', ...)` with a * manual `switch (inbound.type)` — every non-text-only bot reimplemented * the switch. This method is sugar over `dispatch()` so consumers write * `agent.onImage(async (ctx) => ...)` and the framework routes correctly. * * When both a media handler AND `agent.on('onMessage', ...)` are set, * BOTH fire (lifecycle hook runs first per the existing contract, then * dispatch picks the media handler). */ onImage(handler: MediaHandler): this; onAudio(handler: MediaHandler): this; onVideo(handler: MediaHandler): this; onDocument(handler: MediaHandler): this; onSticker(handler: MediaHandler): this; onLocation(handler: MediaHandler): this; onContacts(handler: MediaHandler): this; private onMedia; /** * Register a pre-dispatch guard (v0.17). Runs after `onMessage` lifecycle * hooks and before button/command/media dispatch. Return `null` to allow. * Return `{reply?, silent?}` to deny — the framework sends `reply` (when * set) and short-circuits dispatch for THIS message. * * Guards run in the order they were registered. First denying guard wins; * later guards are not consulted. Errors thrown from a guard are caught * by the outer dispatch try/catch and the message is treated as an * `[Agent] dispatch error` (safest default: fail-closed, no dispatch). * * Distinct from `Blocklist` — Blocklist is a HARD DROP without a reply. * `guard` is a "deny + tell the user why" pattern (paywall, trial, geo). */ guard(fn: GuardFn): this; on(event: Lifecycle, handler: (payload: unknown) => void | Promise): this; /** * Sugar for `on('afterReply', ...)` with a typed handler — runs after * the per-message dispatch completes (button / command / fallback) but * before `handleBatch` returns. Use for opportunistic side-channel sends * (reactive ads, contextual tips), analytics nudges, etc. Errors are * caught + logged; they do NOT fail the inbound turn. */ afterReply(handler: (ctx: HandlerContext) => void | Promise): this; cron(pattern: string, handler: (args: { env: unknown; ctx: ExecutionContext; agent: Agent; }) => void | Promise): this; verifyChallenge(args: { mode?: string | null; token?: string | null; challenge?: string | null; }): import("./webhook/verify.js").VerifyChallengeResult; verifySignature(rawBody: ArrayBuffer | Uint8Array, header: string | null | undefined): Promise; enqueue(envelope: InboundEnvelope): Promise; drain(): Promise; handleBatch({ envelope, combinedText }: BatchInfo): Promise; private dispatch; private buildContext; /** * Resolve the configured `mode` (string or function form) into a concrete * `AgentMode` for this turn. Fail-safe: if the resolver throws or returns * an unknown value, falls back to `'autonomous'` and logs. */ private resolveMode; private replyHelper; /** * Record an `assisted_review` escalation per turn in `assisted` mode. * Never throws — a notifier failure shouldn't break the reply path. */ private recordAssistedReview; private enrichAnswer; private runLifecycle; scheduled(event: ScheduledEvent, env: unknown, ctx: ExecutionContext): Promise; } export {}; //# sourceMappingURL=agent.d.ts.map