@xmoxmo/bncr 0.2.5 → 0.2.7

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 (41) hide show
  1. package/README.md +9 -3
  2. package/index.ts +30 -15
  3. package/package.json +4 -3
  4. package/scripts/check-pack.mjs +61 -0
  5. package/scripts/selfcheck.mjs +10 -0
  6. package/src/channel.ts +892 -255
  7. package/src/core/connection-reachability.ts +41 -14
  8. package/src/core/diagnostics.ts +7 -2
  9. package/src/core/downlink-health.ts +7 -2
  10. package/src/core/outbox-entry-builders.ts +3 -2
  11. package/src/core/policy.ts +9 -0
  12. package/src/core/register-trace.ts +6 -1
  13. package/src/core/status.ts +7 -2
  14. package/src/core/targets.ts +10 -1
  15. package/src/core/types.ts +1 -0
  16. package/src/messaging/inbound/commands.ts +330 -77
  17. package/src/messaging/inbound/context-facts.ts +200 -0
  18. package/src/messaging/inbound/dispatch.ts +429 -119
  19. package/src/messaging/inbound/gate.ts +66 -26
  20. package/src/messaging/inbound/parse.ts +8 -0
  21. package/src/messaging/inbound/runtime-compat.ts +39 -0
  22. package/src/messaging/inbound/session-label.ts +115 -0
  23. package/src/messaging/outbound/diagnostics.ts +16 -0
  24. package/src/messaging/outbound/durable-message-adapter.ts +107 -0
  25. package/src/messaging/outbound/durable-queue-adapter.ts +157 -0
  26. package/src/messaging/outbound/media.ts +3 -1
  27. package/src/messaging/outbound/queue-selectors.ts +7 -2
  28. package/src/messaging/outbound/reasons.ts +4 -0
  29. package/src/messaging/outbound/reply-enqueue.ts +2 -2
  30. package/src/messaging/outbound/reply-target-policy.ts +13 -0
  31. package/src/messaging/outbound/retry-policy.ts +12 -3
  32. package/src/messaging/outbound/send.ts +6 -0
  33. package/src/messaging/outbound/session-route.ts +2 -2
  34. package/src/openclaw/config-runtime.ts +52 -0
  35. package/src/openclaw/inbound-session-runtime.ts +94 -0
  36. package/src/openclaw/ingress-runtime.ts +35 -0
  37. package/src/openclaw/media-runtime.ts +73 -0
  38. package/src/openclaw/reply-runtime.ts +104 -0
  39. package/src/openclaw/routing-runtime.ts +48 -0
  40. package/src/openclaw/sdk-helpers.ts +20 -0
  41. package/src/openclaw/session-route-runtime.ts +15 -0
@@ -1,10 +1,27 @@
1
1
  import { emitBncrLogLine } from '../../core/logging.ts';
2
+ import { resolveBncrChannelPolicy } from '../../core/policy.ts';
2
3
  import {
3
4
  formatDisplayScope,
4
5
  normalizeInboundSessionKey,
5
6
  withTaskSessionKey,
6
7
  } from '../../core/targets.ts';
7
8
  import { buildBncrReplyConfig } from './reply-config.ts';
9
+ import { resolveBncrChannelInboundRuntime } from './runtime-compat.ts';
10
+ import {
11
+ buildBncrInboundSessionIdentityPatch,
12
+ recordAndPatchBncrInboundSessionEntry,
13
+ wrapBncrInboundRecordSessionLabelCorrection,
14
+ } from './session-label.ts';
15
+ import { dispatchOpenClawReplyWithBufferedBlockDispatcher } from '../../openclaw/reply-runtime.ts';
16
+ import {
17
+ resolveOpenClawAgentRoute,
18
+ resolveOpenClawInboundLastRouteSessionKey,
19
+ } from '../../openclaw/routing-runtime.ts';
20
+ import {
21
+ recordBncrInboundSession,
22
+ resolveBncrInboundSessionStorePath,
23
+ resolveBncrPinnedMainDmOwnerFromAllowlist,
24
+ } from '../../openclaw/inbound-session-runtime.ts';
8
25
 
9
26
  type ParsedInbound = ReturnType<typeof import('./parse.ts')['parseBncrInboundParams']>;
10
27
 
@@ -14,6 +31,33 @@ type NativeCommand = {
14
31
  body: string;
15
32
  };
16
33
 
34
+ type NativeVerboseCommand = {
35
+ handled: true;
36
+ verboseLevel?: 'on' | 'off' | 'full';
37
+ text: string;
38
+ };
39
+
40
+ function resolveBncrNativeVerboseCommand(command: NativeCommand): NativeVerboseCommand | null {
41
+ if (command.command !== 'verbose') return null;
42
+ const rawLevel = String(command.raw.slice('/verbose'.length) || '').trim().toLowerCase();
43
+ if (!rawLevel || rawLevel === 'status') {
44
+ return { handled: true, text: 'Current verbose level is unchanged.' };
45
+ }
46
+ if (rawLevel === 'on') return { handled: true, verboseLevel: 'on', text: 'Verbose logging enabled.' };
47
+ if (rawLevel === 'off') return { handled: true, verboseLevel: 'off', text: 'Verbose logging disabled.' };
48
+ if (rawLevel === 'full') return { handled: true, verboseLevel: 'full', text: 'Verbose logging set to full.' };
49
+ return { handled: true, text: `Unrecognized verbose level "${rawLevel}". Valid levels: off, on, full.` };
50
+ }
51
+
52
+ function logBncrNativeCommandEvent(
53
+ event: string,
54
+ fields: Record<string, unknown>,
55
+ options?: { debugOnly?: boolean; debugEnabled?: boolean },
56
+ ) {
57
+ if (options?.debugOnly && !options?.debugEnabled) return;
58
+ emitBncrLogLine('info', `[bncr] native-command ${JSON.stringify({ event, ...fields })}`);
59
+ }
60
+
17
61
  export function parseBncrNativeCommand(text: string): NativeCommand | null {
18
62
  const raw = String(text || '').trim();
19
63
  if (!raw.startsWith('/')) return null;
@@ -45,7 +89,10 @@ export async function handleBncrNativeCommand(params: {
45
89
  mediaLocalRoots?: readonly string[];
46
90
  }) => Promise<void>;
47
91
  logger?: { warn?: (msg: string) => void; error?: (msg: string) => void };
48
- }): Promise<{ handled: false } | { handled: true; command: string; sessionKey: string }> {
92
+ }): Promise<
93
+ | { handled: false }
94
+ | { handled: true; command: string; sessionKey: string; fallbackToAgent?: boolean }
95
+ > {
49
96
  const {
50
97
  api,
51
98
  channelId,
@@ -56,11 +103,23 @@ export async function handleBncrNativeCommand(params: {
56
103
  enqueueFromReply,
57
104
  logger,
58
105
  } = params;
59
- const { accountId, route, peer, sessionKeyfromroute, clientId, extracted, msgId } = parsed;
106
+ const { accountId, route, peer, sessionKeyfromroute, providedOriginatingTo, clientId, extracted, msgId } = parsed;
60
107
  const command = parseBncrNativeCommand(extracted.text);
61
108
  if (!command) return { handled: false };
109
+ const nativeCommandDebugEnabled = cfg?.channels?.[channelId]?.debug?.verbose === true;
110
+
111
+ logBncrNativeCommandEvent(
112
+ 'detected',
113
+ {
114
+ command: command.command,
115
+ accountId,
116
+ to: formatDisplayScope(route),
117
+ msgId: msgId || null,
118
+ },
119
+ { debugOnly: true, debugEnabled: nativeCommandDebugEnabled },
120
+ );
62
121
 
63
- const resolvedRoute = api.runtime.channel.routing.resolveAgentRoute({
122
+ const resolvedRoute = resolveOpenClawAgentRoute(api, {
64
123
  cfg,
65
124
  channel: channelId,
66
125
  accountId,
@@ -77,102 +136,296 @@ export async function handleBncrNativeCommand(params: {
77
136
  rememberSessionRoute(taskSessionKey, accountId, route);
78
137
 
79
138
  const displayTo = formatDisplayScope(route);
139
+ const originatingTo = providedOriginatingTo || displayTo;
80
140
  const body = command.body;
81
141
  if (!clientId) {
82
- emitBncrLogLine('warn', '[bncr] inbound missing clientId for native command identity');
83
- return { handled: false };
142
+ emitBncrLogLine(
143
+ 'warn',
144
+ '[bncr] inbound missing clientId for native command identity; using route identity fallback',
145
+ );
84
146
  }
85
- const senderIdForContext = clientId;
86
- const senderDisplayName = 'bncr-client';
87
- const storePath = api.runtime.channel.session.resolveStorePath(cfg?.session?.store, {
147
+ const senderIdForContext = clientId || displayTo;
148
+ const senderDisplayName = clientId ? 'bncr-client' : displayTo;
149
+ const storePath = resolveBncrInboundSessionStorePath({
150
+ storeConfig: cfg?.session?.store,
88
151
  agentId: resolvedRoute.agentId,
89
152
  });
90
153
 
91
- const ctxPayload = api.runtime.channel.reply.finalizeInboundContext({
92
- Body: body,
93
- BodyForAgent: body,
94
- RawBody: body,
95
- CommandBody: body,
96
- BodyForCommands: body,
97
- From: senderIdForContext,
98
- To: displayTo,
99
- SessionKey: sessionKey,
100
- CommandTargetSessionKey: sessionKey,
101
- CommandSource: 'native',
102
- CommandAuthorized: true,
103
- AccountId: accountId,
104
- ChatType: peer.kind,
105
- ConversationLabel: displayTo,
106
- SenderId: senderIdForContext,
107
- SenderName: senderDisplayName,
108
- SenderUsername: senderDisplayName,
109
- Provider: channelId,
110
- Surface: channelId,
111
- WasMentioned: true,
112
- MessageSid: msgId,
113
- Timestamp: Date.now(),
114
- OriginatingChannel: channelId,
115
- OriginatingTo: displayTo,
154
+ const ctxPayload = resolveBncrChannelInboundRuntime(api).buildContext({
155
+ channel: channelId,
156
+ provider: channelId,
157
+ surface: channelId,
158
+ accountId,
159
+ messageId: msgId,
160
+ timestamp: Date.now(),
161
+ from: senderIdForContext,
162
+ sender: {
163
+ id: senderIdForContext,
164
+ name: senderDisplayName,
165
+ username: senderDisplayName,
166
+ },
167
+ conversation: {
168
+ kind: peer.kind,
169
+ id: peer.id,
170
+ label: displayTo,
171
+ routePeer: {
172
+ kind: peer.kind,
173
+ id: peer.id,
174
+ },
175
+ },
176
+ route: {
177
+ agentId: resolvedRoute.agentId,
178
+ accountId,
179
+ routeSessionKey: resolvedRoute.sessionKey,
180
+ dispatchSessionKey: sessionKey,
181
+ mainSessionKey: resolvedRoute.mainSessionKey,
182
+ },
183
+ reply: {
184
+ to: displayTo,
185
+ originatingTo,
186
+ replyToId: msgId,
187
+ },
188
+ message: {
189
+ inboundEventKind: 'user_request',
190
+ body,
191
+ rawBody: body,
192
+ bodyForAgent: body,
193
+ commandBody: body,
194
+ envelopeFrom: originatingTo,
195
+ senderLabel: senderDisplayName,
196
+ },
197
+ commandTurn: {
198
+ kind: 'native',
199
+ source: 'native',
200
+ authorized: true,
201
+ body,
202
+ },
203
+ access: {
204
+ mentions: {
205
+ canDetectMention: true,
206
+ wasMentioned: true,
207
+ effectiveWasMentioned: true,
208
+ },
209
+ commands: {
210
+ authorized: true,
211
+ allowTextCommands: true,
212
+ useAccessGroups: false,
213
+ authorizers: [],
214
+ },
215
+ },
216
+ extra: {
217
+ OriginatingChannel: channelId,
218
+ },
219
+ });
220
+
221
+ const sessionIdentityPatch = buildBncrInboundSessionIdentityPatch({
222
+ channelId,
223
+ accountId,
224
+ chatType: peer.kind,
225
+ displayTo,
226
+ senderId: senderIdForContext,
116
227
  });
117
228
 
118
- await api.runtime.channel.session.recordInboundSession({
229
+ const nativeVerbose = resolveBncrNativeVerboseCommand(command);
230
+ if (nativeVerbose) {
231
+ logBncrNativeCommandEvent('handled-verbose', {
232
+ command: command.command,
233
+ accountId,
234
+ sessionKey,
235
+ to: displayTo,
236
+ msgId: msgId || null,
237
+ fallbackToAgent: false,
238
+ });
239
+ await recordAndPatchBncrInboundSessionEntry({
240
+ storePath,
241
+ sessionKey,
242
+ ctx: ctxPayload,
243
+ patch: {
244
+ ...sessionIdentityPatch,
245
+ ...(nativeVerbose.verboseLevel ? { verboseLevel: nativeVerbose.verboseLevel } : {}),
246
+ },
247
+ });
248
+ rememberSessionRoute(baseSessionKey, accountId, route);
249
+ await enqueueFromReply({
250
+ accountId,
251
+ sessionKey,
252
+ route,
253
+ payload: {
254
+ text: nativeVerbose.text,
255
+ replyToId: msgId || undefined,
256
+ },
257
+ });
258
+ return { handled: true, command: command.command, sessionKey };
259
+ }
260
+
261
+ await recordAndPatchBncrInboundSessionEntry({
119
262
  storePath,
120
263
  sessionKey,
121
264
  ctx: ctxPayload,
122
- onRecordError: (err: unknown) => {
123
- emitBncrLogLine(
124
- 'warn',
125
- `[bncr] inbound record native command session failed: ${String(err)}`,
126
- );
127
- },
265
+ patch: sessionIdentityPatch,
128
266
  });
129
267
 
130
268
  const effectiveReply = buildBncrReplyConfig(cfg);
269
+ const channelPolicy = resolveBncrChannelPolicy(cfg?.channels?.bncr || {});
270
+ const pinnedMainDmOwner =
271
+ peer.kind === 'direct'
272
+ ? resolveBncrPinnedMainDmOwnerFromAllowlist({
273
+ dmScope: cfg?.session?.dmScope,
274
+ allowFrom: channelPolicy.allowFrom,
275
+ normalizeEntry: (entry: string) => String(entry || '').trim(),
276
+ })
277
+ : null;
278
+ const inboundLastRouteSessionKey = resolveOpenClawInboundLastRouteSessionKey({
279
+ route: resolvedRoute,
280
+ sessionKey,
281
+ });
131
282
 
132
283
  let responded = false;
133
- await api.runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
134
- ctx: ctxPayload,
135
- cfg: effectiveReply.replyCfg,
136
- dispatcherOptions: {
137
- deliver: async (
138
- payload: { text?: string; mediaUrl?: string; mediaUrls?: string[]; audioAsVoice?: boolean },
139
- info?: { kind?: 'tool' | 'block' | 'final' },
140
- ) => {
141
- const kind = info?.kind;
142
- const shouldForwardTool = effectiveReply.blockStreaming && effectiveReply.allowTool;
143
-
144
- if (kind === 'tool' && !shouldForwardTool) {
145
- return;
146
- }
147
-
148
- const hasPayload = Boolean(
149
- payload?.text ||
150
- payload?.mediaUrl ||
151
- (Array.isArray(payload?.mediaUrls) && payload.mediaUrls.length > 0),
152
- );
153
- if (!hasPayload) return;
154
- responded = true;
155
- await enqueueFromReply({
156
- accountId,
157
- sessionKey,
158
- route,
159
- payload: {
160
- ...payload,
161
- kind: kind as 'tool' | 'block' | 'final' | undefined,
162
- replyToId: msgId || undefined,
163
- },
164
- });
165
- },
284
+ logBncrNativeCommandEvent(
285
+ 'dispatch-native-turn',
286
+ {
287
+ command: command.command,
288
+ accountId,
289
+ sessionKey,
290
+ to: displayTo,
291
+ msgId: msgId || null,
166
292
  },
167
- replyOptions: {
168
- disableBlockStreaming: !effectiveReply.blockStreaming,
169
- shouldEmitToolResult: effectiveReply.allowTool ? () => true : undefined,
293
+ { debugOnly: true, debugEnabled: nativeCommandDebugEnabled },
294
+ );
295
+ await resolveBncrChannelInboundRuntime(api).run({
296
+ channel: channelId,
297
+ accountId,
298
+ raw: parsed,
299
+ adapter: {
300
+ ingest: () => ({
301
+ id: msgId ?? `${displayTo}:${Date.now()}`,
302
+ timestamp: Date.now(),
303
+ rawText: body,
304
+ textForAgent: ctxPayload.BodyForAgent,
305
+ textForCommands: ctxPayload.CommandBody,
306
+ raw: parsed,
307
+ }),
308
+ resolveTurn: () => ({
309
+ channel: channelId,
310
+ accountId,
311
+ routeSessionKey: resolvedRoute.sessionKey,
312
+ storePath,
313
+ ctxPayload,
314
+ recordInboundSession: wrapBncrInboundRecordSessionLabelCorrection({
315
+ recordInboundSession: recordBncrInboundSession,
316
+ expectedLabel: displayTo,
317
+ }),
318
+ record: {
319
+ updateLastRoute:
320
+ peer.kind === 'direct'
321
+ ? {
322
+ sessionKey: inboundLastRouteSessionKey,
323
+ channel: channelId,
324
+ to: displayTo,
325
+ accountId,
326
+ mainDmOwnerPin:
327
+ inboundLastRouteSessionKey === resolvedRoute.mainSessionKey && pinnedMainDmOwner
328
+ ? {
329
+ ownerRecipient: pinnedMainDmOwner,
330
+ senderRecipient: senderIdForContext,
331
+ }
332
+ : undefined,
333
+ }
334
+ : undefined,
335
+ onRecordError: (err: unknown) => {
336
+ emitBncrLogLine(
337
+ 'warn',
338
+ `[bncr] inbound record native command session failed: ${String(err)}`,
339
+ );
340
+ },
341
+ },
342
+ runDispatch: () =>
343
+ dispatchOpenClawReplyWithBufferedBlockDispatcher(api, {
344
+ ctx: ctxPayload,
345
+ cfg: effectiveReply.replyCfg,
346
+ dispatcherOptions: {
347
+ deliver: async (
348
+ payload: {
349
+ text?: string;
350
+ mediaUrl?: string;
351
+ mediaUrls?: string[];
352
+ audioAsVoice?: boolean;
353
+ },
354
+ info?: { kind?: 'tool' | 'block' | 'final' },
355
+ ) => {
356
+ const kind = info?.kind;
357
+ const shouldForwardTool = effectiveReply.blockStreaming && effectiveReply.allowTool;
358
+
359
+ if (kind === 'tool' && !shouldForwardTool) {
360
+ return;
361
+ }
362
+
363
+ const hasPayload = Boolean(
364
+ payload?.text ||
365
+ payload?.mediaUrl ||
366
+ (Array.isArray(payload?.mediaUrls) && payload.mediaUrls.length > 0),
367
+ );
368
+ if (!hasPayload) return;
369
+ if (!responded) {
370
+ logBncrNativeCommandEvent(
371
+ 'payload-produced',
372
+ {
373
+ command: command.command,
374
+ accountId,
375
+ sessionKey,
376
+ to: displayTo,
377
+ msgId: msgId || null,
378
+ kind: kind || null,
379
+ fallbackToAgent: false,
380
+ },
381
+ { debugOnly: true, debugEnabled: nativeCommandDebugEnabled },
382
+ );
383
+ }
384
+ responded = true;
385
+ await enqueueFromReply({
386
+ accountId,
387
+ sessionKey,
388
+ route,
389
+ payload: {
390
+ ...payload,
391
+ kind: kind as 'tool' | 'block' | 'final' | undefined,
392
+ replyToId: msgId || undefined,
393
+ },
394
+ });
395
+ },
396
+ },
397
+ replyOptions: {
398
+ disableBlockStreaming: !effectiveReply.blockStreaming,
399
+ shouldEmitToolResult: effectiveReply.allowTool ? () => true : undefined,
400
+ },
401
+ }),
402
+ }),
170
403
  },
171
404
  });
172
405
 
173
406
  if (!responded) {
174
- return { handled: false };
407
+ logBncrNativeCommandEvent('no-payload-fallback-to-agent', {
408
+ command: command.command,
409
+ accountId,
410
+ sessionKey,
411
+ to: displayTo,
412
+ msgId: msgId || null,
413
+ fallbackToAgent: true,
414
+ });
415
+ return { handled: true, command: command.command, sessionKey, fallbackToAgent: true };
175
416
  }
176
417
 
418
+ logBncrNativeCommandEvent(
419
+ 'handled-with-payload',
420
+ {
421
+ command: command.command,
422
+ accountId,
423
+ sessionKey,
424
+ to: displayTo,
425
+ msgId: msgId || null,
426
+ fallbackToAgent: false,
427
+ },
428
+ { debugOnly: true, debugEnabled: nativeCommandDebugEnabled },
429
+ );
177
430
  return { handled: true, command: command.command, sessionKey };
178
431
  }
@@ -0,0 +1,200 @@
1
+ export type BncrStructuredContextFactsInput = {
2
+ channelId: string;
3
+ accountId: string;
4
+ route: {
5
+ agentId?: string;
6
+ routeSessionKey?: string;
7
+ dispatchSessionKey?: string;
8
+ mainSessionKey?: string;
9
+ };
10
+ conversation: {
11
+ kind: string;
12
+ id: string;
13
+ label: string;
14
+ };
15
+ reply: {
16
+ to: string;
17
+ originatingTo: string;
18
+ };
19
+ sender: {
20
+ id: string;
21
+ displayName?: string;
22
+ };
23
+ message: {
24
+ id?: string | null;
25
+ rawBody: string;
26
+ bodyForAgent?: string;
27
+ commandBody?: string;
28
+ envelopeBody?: string;
29
+ };
30
+ media?: Array<{
31
+ path: string;
32
+ contentType?: string;
33
+ kind?: string;
34
+ messageId?: string;
35
+ }>;
36
+ };
37
+
38
+ export function buildBncrStructuredContextFacts(input: BncrStructuredContextFactsInput) {
39
+ const rawBody = input.message.rawBody;
40
+ return {
41
+ channel: {
42
+ id: input.channelId,
43
+ accountId: input.accountId,
44
+ },
45
+ route: {
46
+ agentId: input.route.agentId,
47
+ routeSessionKey: input.route.routeSessionKey,
48
+ dispatchSessionKey: input.route.dispatchSessionKey,
49
+ mainSessionKey: input.route.mainSessionKey,
50
+ },
51
+ conversation: {
52
+ kind: input.conversation.kind,
53
+ id: input.conversation.id,
54
+ label: input.conversation.label,
55
+ },
56
+ reply: {
57
+ to: input.reply.to,
58
+ originatingTo: input.reply.originatingTo,
59
+ },
60
+ sender: {
61
+ id: input.sender.id,
62
+ displayName: input.sender.displayName || input.sender.id,
63
+ },
64
+ message: {
65
+ id: input.message.id || undefined,
66
+ rawBody,
67
+ bodyForAgent: input.message.bodyForAgent ?? rawBody,
68
+ commandBody: input.message.commandBody ?? rawBody,
69
+ envelopeBody: input.message.envelopeBody,
70
+ },
71
+ media: (input.media || []).map((item) => ({
72
+ path: item.path,
73
+ contentType: item.contentType,
74
+ kind: item.kind,
75
+ messageId: item.messageId,
76
+ })),
77
+ };
78
+ }
79
+
80
+ // Keep this payload intentionally small: OpenClaw already renders standard
81
+ // conversation/sender/message metadata as untrusted context. Only include
82
+ // bncr-specific facts that are not otherwise visible to the model, so normal
83
+ // text turns do not get a duplicate "Bncr inbound context" JSON block.
84
+ export function buildBncrPromptVisibleContextFacts(
85
+ facts: ReturnType<typeof buildBncrStructuredContextFacts>,
86
+ ) {
87
+ const result: {
88
+ reply?: {
89
+ to: string;
90
+ originatingTo: string;
91
+ };
92
+ media?: Array<{
93
+ contentType?: string;
94
+ kind?: string;
95
+ messageId?: string;
96
+ }>;
97
+ } = {};
98
+
99
+ if (facts.reply.originatingTo !== facts.reply.to) {
100
+ result.reply = {
101
+ to: facts.reply.to,
102
+ originatingTo: facts.reply.originatingTo,
103
+ };
104
+ }
105
+
106
+ if (facts.media.length > 0) {
107
+ result.media = facts.media.map((item) => ({
108
+ contentType: item.contentType,
109
+ kind: item.kind,
110
+ messageId: item.messageId,
111
+ }));
112
+ }
113
+
114
+ return result;
115
+ }
116
+
117
+ function inferBncrStructuredMediaKind(contentType: string | undefined) {
118
+ if (contentType?.startsWith('image/')) return 'image';
119
+ if (contentType?.startsWith('video/')) return 'video';
120
+ if (contentType?.startsWith('audio/')) return 'audio';
121
+ return 'document';
122
+ }
123
+
124
+ export type BncrStructuredContextFactsFromInboundPartsInput = {
125
+ channelId: string;
126
+ parsed: {
127
+ accountId: string;
128
+ peer: {
129
+ kind: string;
130
+ id: string;
131
+ };
132
+ clientId?: string;
133
+ msgId?: string;
134
+ mimeType?: string;
135
+ };
136
+ resolution: {
137
+ chatType: string;
138
+ canonicalTo: string;
139
+ originatingTo: string;
140
+ resolvedRoute: {
141
+ agentId?: string;
142
+ sessionKey?: string;
143
+ mainSessionKey?: string;
144
+ };
145
+ dispatchSessionKey?: string;
146
+ };
147
+ prepared: {
148
+ rawBody: string;
149
+ body?: string;
150
+ mediaPath?: string | null;
151
+ };
152
+ senderIdForContext: string;
153
+ senderDisplayName?: string;
154
+ };
155
+
156
+ export function buildBncrStructuredContextFactsFromInboundParts(
157
+ input: BncrStructuredContextFactsFromInboundPartsInput,
158
+ ) {
159
+ const mediaPath = input.prepared.mediaPath || undefined;
160
+ return buildBncrStructuredContextFacts({
161
+ channelId: input.channelId,
162
+ accountId: input.parsed.accountId,
163
+ route: {
164
+ agentId: input.resolution.resolvedRoute.agentId,
165
+ routeSessionKey: input.resolution.resolvedRoute.sessionKey,
166
+ dispatchSessionKey: input.resolution.dispatchSessionKey,
167
+ mainSessionKey: input.resolution.resolvedRoute.mainSessionKey,
168
+ },
169
+ conversation: {
170
+ kind: input.resolution.chatType,
171
+ id: input.parsed.peer.id,
172
+ label: input.resolution.canonicalTo,
173
+ },
174
+ reply: {
175
+ to: input.resolution.canonicalTo,
176
+ originatingTo: input.resolution.originatingTo,
177
+ },
178
+ sender: {
179
+ id: input.senderIdForContext,
180
+ displayName: input.senderDisplayName,
181
+ },
182
+ message: {
183
+ id: input.parsed.msgId,
184
+ rawBody: input.prepared.rawBody,
185
+ bodyForAgent: input.prepared.rawBody,
186
+ commandBody: input.prepared.rawBody,
187
+ envelopeBody: input.prepared.body,
188
+ },
189
+ media: mediaPath
190
+ ? [
191
+ {
192
+ path: mediaPath,
193
+ contentType: input.parsed.mimeType,
194
+ kind: inferBncrStructuredMediaKind(input.parsed.mimeType),
195
+ messageId: input.parsed.msgId,
196
+ },
197
+ ]
198
+ : [],
199
+ });
200
+ }