Engagement & Messaging
The @kuralle-agents/engagement package is the channel-agnostic layer for messaging bots: consent, ownership, closed-window recovery, interactive rendering, and broadcasts. Pair it with @kuralle-agents/messaging (router + pipeline) and @kuralle-agents/messaging-meta (WhatsApp/Instagram clients).
One bot, many channels
Section titled “One bot, many channels”Write agents and flows once. Per-channel rules live in ChannelPolicy adapters — not duplicated bot code.
import { createMessagingRouter, InMemoryWindowStore } from '@kuralle-agents/messaging';import { engagement, whatsappPolicy, webPolicy, instagramPolicy,} from '@kuralle-agents/engagement';
const windowStore = new InMemoryWindowStore();
const { bridge } = engagement({ policies: [ whatsappPolicy({ client: whatsapp, selector, windowStore, wabaId }), webPolicy(), instagramPolicy({ client: instagram, windowStore }), ], windowStore,});
createMessagingRouter({ runtime, platforms: { whatsapp, instagram }, ...bridge,});Web chat uses createKuralleChatRouter on the same runtime; webPolicy() is the always-open null adapter.
Window-safe outbound
Section titled “Window-safe outbound”createMessagingRouter builds:
[consentGate?] → [ownershipGate?] → closedWindowRecovery → interactiveRenderer → windowGuard (terminal)windowGuardis always last and is not part ofbridge.outbound. When the window is closed, free-form text/media/interactive defer with no client call.closedWindowRecoveryuses each policy’s strategy: WhatsApp templates, InstagramHUMAN_AGENTtags (text only), or webnone.
Interactive choices
Section titled “Interactive choices”Use withChoices on collect or decide nodes so the runtime emits an interactive stream part. interactiveRenderer turns that into channel-native UI (WhatsApp buttons/list, Instagram quick replies/templates, web controls). Route by option id, not label:
import { decide, defineFlow } from '@kuralle-agents/core';import { withChoices } from '@kuralle-agents/engagement';import { z } from 'zod';
const triage = withChoices( decide({ id: 'triage', instructions: 'How can we help?', schema: z.object({ choice: z.string() }), decide: (id) => (id === 'billing' ? billingNode : supportNode), }), [ { id: 'billing', label: 'Billing' }, { id: 'support', label: 'Support' }, ],);Consent, handoff, broadcasts
Section titled “Consent, handoff, broadcasts”sessionConsentStore+consentGate— opt-in/out; STOP halts proactive sends.sessionOwnershipStore+ownershipGate— whileowner(thread) === 'human', the bot does not send and inbound does not run flows.broadcasts— passbroadcastPipelineintoengagement()to enablebroadcasts.send(); policies do not expose clients, so you construct the pipeline for the target platform explicitly.
Examples
Section titled “Examples”- Deep, end-to-end bots —
packages/kuralle-engagement/examples/(booking, pharmacy, clothing): free-form extraction, template-based closed-window sends, and mixed conversations across WhatsApp / web / Instagram. SeeAUTHORING.mdfor the API + patterns. - Multi-platform server —
packages/kuralle-messaging-meta/examples/multi-platform/(WhatsApp + Instagram webhooks + web SSE, one shared flow; offline-friendly). - Deployable WhatsApp server —
packages/kuralle-messaging-meta/examples/whatsapp-server/: a self-hostable bot wiringengagement()+createMessagingRouterto a real WhatsApp Cloud API number (bring your own token, no Embedded Signup). Boots on Bun or Node, optional RedisWindowStoreviaREDIS_URL. See Deployment.
Dev loop — simulate before you deploy
Section titled “Dev loop — simulate before you deploy”createSimulator({ runtime, bridge, channels, windowStore }) drives the real router + engagement through fake recording clients — multi-turn conversations across channels with no live model and no live Meta. Use it as the kuralle dev inner loop and in deterministic tests (send() / window() / sends()).
Install
Section titled “Install”npm install @kuralle-agents/engagement @kuralle-agents/messaging @kuralle-agents/messaging-meta @kuralle-agents/core ai zodPackage README: packages/kuralle-engagement/README.md.