@kuralle-agents/engagement 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +114 -0
  3. package/dist/authoring.d.ts +2 -0
  4. package/dist/authoring.js +3 -0
  5. package/dist/broadcast-ledger.d.ts +10 -0
  6. package/dist/broadcast-ledger.js +25 -0
  7. package/dist/broadcast.d.ts +23 -0
  8. package/dist/broadcast.js +28 -0
  9. package/dist/catalog.d.ts +13 -0
  10. package/dist/catalog.js +109 -0
  11. package/dist/closed-window-recovery.d.ts +3 -0
  12. package/dist/closed-window-recovery.js +31 -0
  13. package/dist/consent.d.ts +11 -0
  14. package/dist/consent.js +75 -0
  15. package/dist/drip.d.ts +26 -0
  16. package/dist/drip.js +94 -0
  17. package/dist/engagement.d.ts +22 -0
  18. package/dist/engagement.js +66 -0
  19. package/dist/index.d.ts +24 -0
  20. package/dist/index.js +23 -0
  21. package/dist/interactive-renderer.d.ts +19 -0
  22. package/dist/interactive-renderer.js +120 -0
  23. package/dist/nodes.d.ts +10 -0
  24. package/dist/nodes.js +15 -0
  25. package/dist/ownership.d.ts +5 -0
  26. package/dist/ownership.js +62 -0
  27. package/dist/policies/instagram.d.ts +7 -0
  28. package/dist/policies/instagram.js +15 -0
  29. package/dist/policies/web.d.ts +3 -0
  30. package/dist/policies/web.js +22 -0
  31. package/dist/policies/whatsapp.d.ts +11 -0
  32. package/dist/policies/whatsapp.js +23 -0
  33. package/dist/policy.d.ts +27 -0
  34. package/dist/policy.js +1 -0
  35. package/dist/render-instagram-interactive.d.ts +14 -0
  36. package/dist/render-instagram-interactive.js +78 -0
  37. package/dist/resolve-inbound-instagram.d.ts +6 -0
  38. package/dist/resolve-inbound-instagram.js +10 -0
  39. package/dist/resolve-inbound-whatsapp.d.ts +7 -0
  40. package/dist/resolve-inbound-whatsapp.js +17 -0
  41. package/dist/scheduler.d.ts +27 -0
  42. package/dist/scheduler.js +36 -0
  43. package/dist/selector.d.ts +3 -0
  44. package/dist/selector.js +45 -0
  45. package/dist/simulator.d.ts +44 -0
  46. package/dist/simulator.js +161 -0
  47. package/dist/strategist-middleware.d.ts +3 -0
  48. package/dist/strategist-middleware.js +19 -0
  49. package/dist/strategist.d.ts +67 -0
  50. package/dist/strategist.js +55 -0
  51. package/package.json +44 -0
@@ -0,0 +1,78 @@
1
+ /** Instagram button-template title limit (R-11 / Meta quick-reply parity). */
2
+ export const IG_TITLE_MAX = 20;
3
+ /** Instagram button template — max buttons per message. */
4
+ export const IG_BUTTON_COUNT_MAX = 3;
5
+ /** Instagram generic-template carousel — max elements. */
6
+ export const IG_CAROUSEL_COUNT_MAX = 10;
7
+ function assertIgTitle(label, optionId) {
8
+ if (label.length > IG_TITLE_MAX) {
9
+ throw new Error(`interactive: title for "${optionId}" exceeds ${IG_TITLE_MAX} characters (got ${label.length})`);
10
+ }
11
+ }
12
+ /**
13
+ * Renders author choices to a channel-neutral `InteractiveMessage` for Instagram:
14
+ * ≤3 → button template; 4–10 → generic-template carousel via list shape.
15
+ * Quick-replies (≤13) are a future enhancement — not used in this cut.
16
+ */
17
+ export function renderInstagramInteractive(options, prompt) {
18
+ if (options.length === 0) {
19
+ throw new Error('interactive: at least one option is required');
20
+ }
21
+ if (options.some((o) => o.flow)) {
22
+ throw new Error('interactive: flow messages are not supported on Instagram');
23
+ }
24
+ if (options.length > IG_CAROUSEL_COUNT_MAX) {
25
+ throw new Error(`interactive: too many options (max ${IG_CAROUSEL_COUNT_MAX} carousel elements)`);
26
+ }
27
+ if (options.some((o) => o.url)) {
28
+ const urlOptions = options.filter((o) => o.url);
29
+ if (urlOptions.length > IG_BUTTON_COUNT_MAX) {
30
+ throw new Error(`interactive: too many URL options (max ${IG_BUTTON_COUNT_MAX} buttons)`);
31
+ }
32
+ for (const o of urlOptions) {
33
+ assertIgTitle(o.label, o.id);
34
+ }
35
+ return {
36
+ type: 'buttons',
37
+ body: prompt,
38
+ action: {
39
+ type: 'buttons',
40
+ buttons: urlOptions.map((o) => ({ id: o.id, title: o.label })),
41
+ },
42
+ };
43
+ }
44
+ if (options.length <= IG_BUTTON_COUNT_MAX) {
45
+ for (const o of options) {
46
+ assertIgTitle(o.label, o.id);
47
+ }
48
+ return {
49
+ type: 'buttons',
50
+ body: prompt,
51
+ action: {
52
+ type: 'buttons',
53
+ buttons: options.map((o) => ({ id: o.id, title: o.label })),
54
+ },
55
+ };
56
+ }
57
+ for (const o of options) {
58
+ assertIgTitle(o.label, o.id);
59
+ }
60
+ return {
61
+ type: 'list',
62
+ body: prompt,
63
+ action: {
64
+ type: 'list',
65
+ button: 'Choose',
66
+ sections: [
67
+ {
68
+ title: 'Options',
69
+ rows: options.map((o) => ({
70
+ id: o.id,
71
+ title: o.label,
72
+ description: o.description,
73
+ })),
74
+ },
75
+ ],
76
+ },
77
+ };
78
+ }
@@ -0,0 +1,6 @@
1
+ import type { InboundMessage } from '@kuralle-agents/messaging';
2
+ import type { ResolvedSelection } from '@kuralle-agents/core';
3
+ export declare function resolveInboundInstagram(m: InboundMessage): {
4
+ input: string;
5
+ selection?: ResolvedSelection;
6
+ };
@@ -0,0 +1,10 @@
1
+ export function resolveInboundInstagram(m) {
2
+ const interactiveId = m.interactive?.id;
3
+ if (interactiveId) {
4
+ return { input: interactiveId, selection: { id: interactiveId } };
5
+ }
6
+ if (m.button?.payload) {
7
+ return { input: m.button.payload, selection: { id: m.button.payload } };
8
+ }
9
+ return { input: m.text ?? '', selection: undefined };
10
+ }
@@ -0,0 +1,7 @@
1
+ import type { InboundMessage } from '@kuralle-agents/messaging';
2
+ import type { ResolvedSelection } from '@kuralle-agents/core';
3
+ /** S3 interactive-then-text resolution (sync; mirrors InteractiveResolver + TextResolver). */
4
+ export declare function resolveInboundWhatsApp(m: InboundMessage): {
5
+ input: string;
6
+ selection?: ResolvedSelection;
7
+ };
@@ -0,0 +1,17 @@
1
+ /** S3 interactive-then-text resolution (sync; mirrors InteractiveResolver + TextResolver). */
2
+ export function resolveInboundWhatsApp(m) {
3
+ const interactiveId = m.interactive?.id;
4
+ if (interactiveId) {
5
+ return { input: interactiveId, selection: { id: interactiveId } };
6
+ }
7
+ if (m.button?.payload) {
8
+ return { input: m.button.payload, selection: { id: m.button.payload } };
9
+ }
10
+ if (m.interactive?.formResponse) {
11
+ return {
12
+ input: '__flow__',
13
+ selection: { formData: m.interactive.formResponse },
14
+ };
15
+ }
16
+ return { input: m.text ?? '', selection: undefined };
17
+ }
@@ -0,0 +1,27 @@
1
+ /** A unit of deferred work (broadcast step / drip step). Shape is engagement-internal. */
2
+ export interface SendJob {
3
+ kind: string;
4
+ payload: Record<string, unknown>;
5
+ }
6
+ export interface Scheduler {
7
+ enqueue(job: SendJob, opts?: {
8
+ delayMs?: number;
9
+ }): Promise<string>;
10
+ cancel(jobId: string): Promise<void>;
11
+ }
12
+ export type InjectableTimer = {
13
+ set(fn: () => void, ms: number): unknown;
14
+ clear(handle: unknown): void;
15
+ };
16
+ /**
17
+ * Default in-process scheduler (timer-based). For single-process/dev.
18
+ * Production adapters (interface-compatible, not implemented here):
19
+ * - BullMQ (Redis-backed queue)
20
+ * - Google Cloud Tasks
21
+ * - cron / system scheduler
22
+ * Inject a durable adapter for multi-process / serverless.
23
+ */
24
+ export declare function createInProcessScheduler(opts: {
25
+ run: (job: SendJob) => void | Promise<void>;
26
+ timer?: InjectableTimer;
27
+ }): Scheduler;
@@ -0,0 +1,36 @@
1
+ const defaultTimer = {
2
+ set: (fn, ms) => setTimeout(fn, ms),
3
+ clear: (handle) => clearTimeout(handle),
4
+ };
5
+ /**
6
+ * Default in-process scheduler (timer-based). For single-process/dev.
7
+ * Production adapters (interface-compatible, not implemented here):
8
+ * - BullMQ (Redis-backed queue)
9
+ * - Google Cloud Tasks
10
+ * - cron / system scheduler
11
+ * Inject a durable adapter for multi-process / serverless.
12
+ */
13
+ export function createInProcessScheduler(opts) {
14
+ const timer = opts.timer ?? defaultTimer;
15
+ let nextJobId = 0;
16
+ const handles = new Map();
17
+ return {
18
+ async enqueue(job, options) {
19
+ const jobId = String(++nextJobId);
20
+ const delayMs = options?.delayMs ?? 0;
21
+ const handle = timer.set(() => {
22
+ handles.delete(jobId);
23
+ void opts.run(job);
24
+ }, delayMs);
25
+ handles.set(jobId, handle);
26
+ return jobId;
27
+ },
28
+ async cancel(jobId) {
29
+ const handle = handles.get(jobId);
30
+ if (handle === undefined)
31
+ return;
32
+ timer.clear(handle);
33
+ handles.delete(jobId);
34
+ },
35
+ };
36
+ }
@@ -0,0 +1,3 @@
1
+ import { type LanguageModel } from 'ai';
2
+ import type { TemplateSelector } from './strategist.js';
3
+ export declare function aiTemplateSelector(model: LanguageModel): TemplateSelector;
@@ -0,0 +1,45 @@
1
+ import { generateObject } from 'ai';
2
+ import { z } from 'zod';
3
+ const selectionSchema = z.object({
4
+ fit: z.boolean(),
5
+ name: z.string().nullable(),
6
+ language: z.string().nullable(),
7
+ params: z.record(z.string(), z.string()).nullable(),
8
+ });
9
+ export function aiTemplateSelector(model) {
10
+ return {
11
+ async select({ text, intent, candidates, flowState }) {
12
+ if (candidates.length === 0)
13
+ return null;
14
+ const candidateLines = candidates
15
+ .map((c) => `- ${c.name} (${c.language}, ${c.category}): params=[${c.params
16
+ .map((p) => `${p.key}${p.required ? '*' : ''}`)
17
+ .join(', ')}]`)
18
+ .join('\n');
19
+ const flowHint = flowState && Object.keys(flowState).length > 0
20
+ ? `Flow state:\n${JSON.stringify(flowState)}\n\n`
21
+ : '';
22
+ const { object } = await generateObject({
23
+ model,
24
+ schema: selectionSchema,
25
+ system: 'You choose the best WhatsApp message template for a user message. ' +
26
+ 'Only pick from the listed candidates. Output schema fields only.',
27
+ prompt: `User message:\n${text}\n` +
28
+ (intent ? `Intent: ${intent}\n` : '') +
29
+ flowHint +
30
+ `Candidates:\n${candidateLines}\n\n` +
31
+ 'Return fit:true with name, language, and params when a candidate matches; otherwise fit:false.',
32
+ });
33
+ if (!object.fit || object.name == null)
34
+ return null;
35
+ const matched = candidates.find((c) => c.name === object.name);
36
+ if (!matched)
37
+ return null;
38
+ return {
39
+ name: object.name,
40
+ language: object.language ?? matched.language,
41
+ params: object.params ?? {},
42
+ };
43
+ },
44
+ };
45
+ }
@@ -0,0 +1,44 @@
1
+ import type { Runtime } from '@kuralle-agents/core';
2
+ import { createMessagingRouter } from '@kuralle-agents/messaging';
3
+ import type { PlatformClient, WindowStore } from '@kuralle-agents/messaging';
4
+ import type { EngagementBridge } from './engagement.js';
5
+ export interface SimChannel {
6
+ platform: string;
7
+ }
8
+ export type SimSendKind = 'text' | 'interactive' | 'template' | 'media' | 'tagged-text';
9
+ export interface SimSend {
10
+ channel: string;
11
+ kind: SimSendKind;
12
+ detail: string;
13
+ }
14
+ export type SimInboundInput = {
15
+ text: string;
16
+ } | {
17
+ interactive: {
18
+ id: string;
19
+ title?: string;
20
+ };
21
+ } | {
22
+ button: {
23
+ payload: string;
24
+ text?: string;
25
+ };
26
+ };
27
+ export interface Simulator {
28
+ send(channel: string, threadId: string, input: SimInboundInput): Promise<SimSend[]>;
29
+ window(threadId: string): Promise<{
30
+ open: boolean;
31
+ expiresAt: Date | null;
32
+ }>;
33
+ sends(channel?: string): SimSend[];
34
+ readonly platforms: Record<string, PlatformClient>;
35
+ readonly router: ReturnType<typeof createMessagingRouter>;
36
+ }
37
+ export interface CreateSimulatorOptions {
38
+ runtime: Runtime;
39
+ bridge: EngagementBridge;
40
+ channels: string[];
41
+ windowStore: WindowStore;
42
+ defaultCustomerId?: (threadId: string) => string;
43
+ }
44
+ export declare function createSimulator(opts: CreateSimulatorOptions): Simulator;
@@ -0,0 +1,161 @@
1
+ import { createMessagingRouter } from '@kuralle-agents/messaging';
2
+ function renderInteractiveDetail(interactive) {
3
+ const body = interactive.body;
4
+ if (interactive.action.type === 'buttons') {
5
+ const opts = interactive.action.buttons.map((b) => `(${b.id}:${b.title})`).join('');
6
+ return `[buttons] ${body} :: ${opts}`;
7
+ }
8
+ if (interactive.action.type === 'list') {
9
+ const rows = interactive.action.sections
10
+ .flatMap((s) => s.rows.map((r) => `(${r.id}:${r.title})`))
11
+ .join('');
12
+ const listBtn = interactive.action.button;
13
+ return `[list] ${body} :: ${listBtn} :: ${rows}`;
14
+ }
15
+ return `[${interactive.action.type}] ${body}`;
16
+ }
17
+ function renderMediaDetail(media) {
18
+ const cap = media.caption ? ` caption=${media.caption}` : '';
19
+ return `${media.type}/${media.mimeType}${cap}`;
20
+ }
21
+ function createRecordingPlatform(channel, allSends) {
22
+ const handlers = [];
23
+ let seq = 0;
24
+ const makeResult = (threadId) => ({
25
+ messageId: `sim-${channel}-${seq++}`,
26
+ threadId,
27
+ timestamp: new Date(),
28
+ });
29
+ const record = (kind, detail) => {
30
+ allSends.push({ channel, kind, detail });
31
+ };
32
+ return {
33
+ platform: channel,
34
+ handleWebhook: async () => new Response('OK'),
35
+ onMessage: (handler) => {
36
+ handlers.push(handler);
37
+ },
38
+ onStatus: () => { },
39
+ onReaction: () => { },
40
+ sendText: async (_to, text) => {
41
+ record('text', text);
42
+ return makeResult(_to);
43
+ },
44
+ sendTextWithTag: async (_to, text, tag) => {
45
+ record('tagged-text', `${text}[tag=${tag}]`);
46
+ return makeResult(_to);
47
+ },
48
+ sendTemplate: async (_to, template) => {
49
+ record('template', template.name);
50
+ return makeResult(_to);
51
+ },
52
+ sendInteractive: async (_to, interactive) => {
53
+ record('interactive', renderInteractiveDetail(interactive));
54
+ return makeResult(_to);
55
+ },
56
+ sendMedia: async (_to, media) => {
57
+ record('media', renderMediaDetail(media));
58
+ return makeResult(_to);
59
+ },
60
+ sendRaw: async (_to) => makeResult(_to),
61
+ markAsRead: async () => { },
62
+ sendTypingIndicator: async () => { },
63
+ uploadMedia: async () => ({ mediaId: 'mock' }),
64
+ downloadMedia: async () => ({ data: Buffer.from(''), mimeType: 'text/plain' }),
65
+ formatConverter: {
66
+ toPlainText: (t) => t,
67
+ toMarkdown: (t) => t,
68
+ toPlatformFormat: (t) => t,
69
+ },
70
+ webhookRouter: () => {
71
+ throw new Error('webhook not used in simulator');
72
+ },
73
+ deliver: async (message) => {
74
+ for (const handler of handlers) {
75
+ await handler(message, message);
76
+ }
77
+ },
78
+ };
79
+ }
80
+ function buildInboundMessage(channel, threadId, customerId, messageId, input) {
81
+ const base = {
82
+ id: messageId,
83
+ platform: channel,
84
+ threadId,
85
+ customerId,
86
+ from: { id: customerId, name: 'Simulator' },
87
+ timestamp: new Date(),
88
+ };
89
+ if ('text' in input) {
90
+ return { ...base, type: 'text', text: input.text };
91
+ }
92
+ if ('button' in input) {
93
+ if (channel === 'instagram') {
94
+ return {
95
+ ...base,
96
+ type: 'interactive',
97
+ button: { payload: input.button.payload, text: input.button.text ?? input.button.payload },
98
+ };
99
+ }
100
+ return {
101
+ ...base,
102
+ type: 'text',
103
+ text: input.button.payload,
104
+ };
105
+ }
106
+ const { id, title } = input.interactive;
107
+ if (channel === 'whatsapp' || channel === 'instagram') {
108
+ return {
109
+ ...base,
110
+ type: 'interactive',
111
+ interactive: { type: 'button_reply', id, title: title ?? id },
112
+ };
113
+ }
114
+ return {
115
+ ...base,
116
+ type: 'text',
117
+ text: id,
118
+ };
119
+ }
120
+ export function createSimulator(opts) {
121
+ const allSends = [];
122
+ const platforms = {};
123
+ const clients = {};
124
+ for (const channel of opts.channels) {
125
+ const client = createRecordingPlatform(channel, allSends);
126
+ clients[channel] = client;
127
+ platforms[channel] = client;
128
+ }
129
+ const router = createMessagingRouter({
130
+ runtime: opts.runtime,
131
+ platforms,
132
+ windowStore: opts.windowStore,
133
+ ...opts.bridge,
134
+ });
135
+ const customerFor = opts.defaultCustomerId ?? ((threadId) => threadId);
136
+ let inboundSeq = 0;
137
+ return {
138
+ platforms,
139
+ router,
140
+ async send(channel, threadId, input) {
141
+ const client = clients[channel];
142
+ if (!client) {
143
+ throw new Error(`Simulator channel "${channel}" is not configured`);
144
+ }
145
+ const start = allSends.length;
146
+ const messageId = `in-${++inboundSeq}`;
147
+ const customerId = customerFor(threadId);
148
+ await client.deliver(buildInboundMessage(channel, threadId, customerId, messageId, input));
149
+ return allSends.slice(start);
150
+ },
151
+ async window(threadId) {
152
+ const state = await opts.windowStore.get(threadId);
153
+ return { open: state.open, expiresAt: state.expiresAt };
154
+ },
155
+ sends(channel) {
156
+ if (channel === undefined)
157
+ return [...allSends];
158
+ return allSends.filter((s) => s.channel === channel);
159
+ },
160
+ };
161
+ }
@@ -0,0 +1,3 @@
1
+ import type { OutboundMiddleware } from '@kuralle-agents/messaging';
2
+ import type { SmartSendStrategist } from './strategist.js';
3
+ export declare function strategistMiddleware(strategist: SmartSendStrategist): OutboundMiddleware;
@@ -0,0 +1,19 @@
1
+ export function strategistMiddleware(strategist) {
2
+ return {
3
+ name: 'strategist',
4
+ async send(req, next) {
5
+ if (req.payload.kind !== 'text')
6
+ return next(req);
7
+ const input = { text: req.payload.text, window: req.meta.window };
8
+ const decision = await strategist.decide(input);
9
+ switch (decision.kind) {
10
+ case 'freeform':
11
+ return next(req);
12
+ case 'template':
13
+ return next({ ...req, payload: { kind: 'template', template: decision.template } });
14
+ case 'defer':
15
+ return { kind: 'deferred', reason: decision.reason };
16
+ }
17
+ },
18
+ };
19
+ }
@@ -0,0 +1,67 @@
1
+ import type { OutboundTemplate, WindowState } from '@kuralle-agents/messaging';
2
+ export interface TemplateDescriptor {
3
+ name: string;
4
+ language: string;
5
+ category: 'authentication' | 'marketing' | 'utility';
6
+ status: 'APPROVED' | 'PENDING' | 'REJECTED';
7
+ quality: 'GREEN' | 'YELLOW' | 'RED' | 'PAUSED' | 'DISABLED' | 'UNKNOWN';
8
+ params: {
9
+ key: string;
10
+ required: boolean;
11
+ }[];
12
+ }
13
+ export type DeferReason = 'no-approved-template' | 'no-template-fit' | 'param-validation-failed' | 'selector-error' | (string & {});
14
+ export interface ConversionAudit {
15
+ requestedText: string;
16
+ chosenTemplate: string;
17
+ params: Record<string, string>;
18
+ at: number;
19
+ }
20
+ export type SendDecision = {
21
+ kind: 'freeform';
22
+ text: string;
23
+ } | {
24
+ kind: 'template';
25
+ template: OutboundTemplate;
26
+ selected: TemplateDescriptor;
27
+ audit: ConversionAudit;
28
+ } | {
29
+ kind: 'defer';
30
+ reason: DeferReason;
31
+ };
32
+ export interface StrategistInput {
33
+ text: string;
34
+ window: WindowState;
35
+ intent?: string;
36
+ flowState?: Readonly<Record<string, unknown>>;
37
+ }
38
+ export interface TemplateSelector {
39
+ select(input: {
40
+ text: string;
41
+ intent?: string;
42
+ candidates: readonly TemplateDescriptor[];
43
+ flowState?: Readonly<Record<string, unknown>>;
44
+ }): Promise<{
45
+ name: string;
46
+ language: string;
47
+ params: Record<string, string>;
48
+ } | null>;
49
+ }
50
+ export interface TemplateCatalog {
51
+ approved(): Promise<TemplateDescriptor[]>;
52
+ validateParams(name: string, p: Record<string, string>): {
53
+ ok: boolean;
54
+ errors?: string[];
55
+ };
56
+ }
57
+ export interface AuditSink {
58
+ record(a: ConversionAudit): Promise<void> | void;
59
+ }
60
+ export interface SmartSendStrategist {
61
+ decide(input: StrategistInput): Promise<SendDecision>;
62
+ }
63
+ export declare function createSmartSendStrategist(opts: {
64
+ catalog: TemplateCatalog;
65
+ selector: TemplateSelector;
66
+ audit: AuditSink;
67
+ }): SmartSendStrategist;
@@ -0,0 +1,55 @@
1
+ export function createSmartSendStrategist(opts) {
2
+ const { catalog, selector, audit } = opts;
3
+ return {
4
+ async decide(input) {
5
+ if (input.window.open) {
6
+ return { kind: 'freeform', text: input.text };
7
+ }
8
+ const candidates = await catalog.approved();
9
+ if (candidates.length === 0) {
10
+ return { kind: 'defer', reason: 'no-approved-template' };
11
+ }
12
+ let pick;
13
+ try {
14
+ pick = await selector.select({
15
+ text: input.text,
16
+ intent: input.intent,
17
+ candidates,
18
+ flowState: input.flowState,
19
+ });
20
+ }
21
+ catch {
22
+ return { kind: 'defer', reason: 'selector-error' };
23
+ }
24
+ if (pick == null) {
25
+ return { kind: 'defer', reason: 'no-template-fit' };
26
+ }
27
+ const selected = candidates.find((c) => c.name === pick.name);
28
+ if (!selected) {
29
+ return { kind: 'defer', reason: 'no-template-fit' };
30
+ }
31
+ const v = catalog.validateParams(pick.name, pick.params);
32
+ if (!v.ok) {
33
+ return { kind: 'defer', reason: 'param-validation-failed' };
34
+ }
35
+ const conversionAudit = {
36
+ requestedText: input.text,
37
+ chosenTemplate: pick.name,
38
+ params: pick.params,
39
+ at: Date.now(),
40
+ };
41
+ await audit.record(conversionAudit);
42
+ const template = {
43
+ name: pick.name,
44
+ language: pick.language,
45
+ namedParams: pick.params,
46
+ };
47
+ return {
48
+ kind: 'template',
49
+ template,
50
+ selected,
51
+ audit: conversionAudit,
52
+ };
53
+ },
54
+ };
55
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@kuralle-agents/engagement",
3
+ "license": "Apache-2.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/kuralle/kuralle-agents.git",
7
+ "directory": "packages/kuralle-engagement"
8
+ },
9
+ "version": "0.3.0",
10
+ "description": "Channel-agnostic engagement layer for Kuralle agents (window-safe outbound, smart-send, interactive fidelity, handoff, consent, proactive).",
11
+ "type": "module",
12
+ "main": "dist/index.js",
13
+ "types": "dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.js"
18
+ }
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md"
26
+ ],
27
+ "dependencies": {
28
+ "ai": "^6.0.0",
29
+ "zod": "^3.24.2",
30
+ "@kuralle-agents/core": "0.3.0",
31
+ "@kuralle-agents/messaging-meta": "0.3.0",
32
+ "@kuralle-agents/messaging": "0.3.0"
33
+ },
34
+ "devDependencies": {
35
+ "typescript": "^5.8.2",
36
+ "@types/node": "^22.13.4"
37
+ },
38
+ "scripts": {
39
+ "prebuild": "rm -rf dist",
40
+ "build": "tsc -p tsconfig.json",
41
+ "clean": "rm -rf dist",
42
+ "test": "bun test"
43
+ }
44
+ }