Skip to content

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).

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.

createMessagingRouter builds:

[consentGate?] → [ownershipGate?] → closedWindowRecovery → interactiveRenderer → windowGuard (terminal)
  • windowGuard is always last and is not part of bridge.outbound. When the window is closed, free-form text/media/interactive defer with no client call.
  • closedWindowRecovery uses each policy’s strategy: WhatsApp templates, Instagram HUMAN_AGENT tags (text only), or web none.

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' },
],
);
  • sessionConsentStore + consentGate — opt-in/out; STOP halts proactive sends.
  • sessionOwnershipStore + ownershipGate — while owner(thread) === 'human', the bot does not send and inbound does not run flows.
  • broadcasts — pass broadcastPipeline into engagement() to enable broadcasts.send(); policies do not expose clients, so you construct the pipeline for the target platform explicitly.
  • Deep, end-to-end botspackages/kuralle-engagement/examples/ (booking, pharmacy, clothing): free-form extraction, template-based closed-window sends, and mixed conversations across WhatsApp / web / Instagram. See AUTHORING.md for the API + patterns.
  • Multi-platform serverpackages/kuralle-messaging-meta/examples/multi-platform/ (WhatsApp + Instagram webhooks + web SSE, one shared flow; offline-friendly).
  • Deployable WhatsApp serverpackages/kuralle-messaging-meta/examples/whatsapp-server/: a self-hostable bot wiring engagement() + createMessagingRouter to a real WhatsApp Cloud API number (bring your own token, no Embedded Signup). Boots on Bun or Node, optional Redis WindowStore via REDIS_URL. See Deployment.

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()).

Terminal window
npm install @kuralle-agents/engagement @kuralle-agents/messaging @kuralle-agents/messaging-meta @kuralle-agents/core ai zod

Package README: packages/kuralle-engagement/README.md.