@kuralle-agents/messaging 0.7.2 → 0.8.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.
@@ -3,6 +3,7 @@ import { MessageDeduplicator } from '../shared/deduplicator.js';
3
3
  import { InMemoryWindowStore } from './window-store.js';
4
4
  import { defaultSessionResolver } from './session-resolver.js';
5
5
  import { InboundResolverChain, defaultInboundChain, } from './input-resolver-chain.js';
6
+ import { attachInboundMedia } from './inbound-media.js';
6
7
  import { StreamMapper } from './stream-mapper.js';
7
8
  import { OutboundPipeline } from './outbound-pipeline.js';
8
9
  import { windowGuard } from './middleware/window-guard.js';
@@ -100,7 +101,10 @@ export function createMessagingRouter(config) {
100
101
  await recordInboundToHistory(config, sessionId, message, userId);
101
102
  return;
102
103
  }
103
- const { input, selection } = await inboundChain.resolve(message);
104
+ const { input: resolvedInput, selection } = await inboundChain.resolve(message);
105
+ // Multimodal intake: download any inbound media and attach it as AI SDK file
106
+ // parts so photos / voice notes / documents reach the model (REQ multimodal).
107
+ const input = await attachInboundMedia(message, resolvedInput, platform);
104
108
  try {
105
109
  const handle = config.runtime.run({
106
110
  input,
@@ -0,0 +1,17 @@
1
+ import type { UserInputContent } from '@kuralle-agents/core';
2
+ import type { InboundMessage } from '../types/messages.js';
3
+ import type { PlatformClient } from '../types/client.js';
4
+ /**
5
+ * Attach inbound media (image / audio / video / document / sticker) to the user
6
+ * turn as an AI SDK file part, so it reaches the model instead of being dropped.
7
+ *
8
+ * Bytes are downloaded via the platform client and base64-encoded — the durable
9
+ * runtime persists the user message through the session store (JSON/Redis/Postgres),
10
+ * so a raw `Buffer` is not safe; a base64 string is. When the platform already
11
+ * exposes a hosted URL on the reference, that URL is passed through instead.
12
+ *
13
+ * The caption (or any text the resolver produced) becomes a leading text part, so
14
+ * "here's my prescription 📷 + can you read it?" arrives as one multimodal turn.
15
+ * Non-media messages return `textInput` unchanged.
16
+ */
17
+ export declare function attachInboundMedia(message: InboundMessage, textInput: UserInputContent, client: Pick<PlatformClient, 'downloadMedia'>): Promise<UserInputContent>;
@@ -0,0 +1,50 @@
1
+ /** Fallback media type when the platform omits a MIME type, keyed by message kind. */
2
+ function fallbackMediaType(type) {
3
+ switch (type) {
4
+ case 'image':
5
+ return 'image/jpeg';
6
+ case 'audio':
7
+ return 'audio/ogg';
8
+ case 'video':
9
+ return 'video/mp4';
10
+ case 'sticker':
11
+ return 'image/webp';
12
+ default:
13
+ return 'application/octet-stream';
14
+ }
15
+ }
16
+ /**
17
+ * Attach inbound media (image / audio / video / document / sticker) to the user
18
+ * turn as an AI SDK file part, so it reaches the model instead of being dropped.
19
+ *
20
+ * Bytes are downloaded via the platform client and base64-encoded — the durable
21
+ * runtime persists the user message through the session store (JSON/Redis/Postgres),
22
+ * so a raw `Buffer` is not safe; a base64 string is. When the platform already
23
+ * exposes a hosted URL on the reference, that URL is passed through instead.
24
+ *
25
+ * The caption (or any text the resolver produced) becomes a leading text part, so
26
+ * "here's my prescription 📷 + can you read it?" arrives as one multimodal turn.
27
+ * Non-media messages return `textInput` unchanged.
28
+ */
29
+ export async function attachInboundMedia(message, textInput, client) {
30
+ const ref = message.media;
31
+ if (!ref)
32
+ return textInput;
33
+ const caption = ref.caption ?? (typeof textInput === 'string' ? textInput : '');
34
+ let data;
35
+ let mediaType;
36
+ if (ref.url) {
37
+ data = ref.url;
38
+ mediaType = ref.mimeType || fallbackMediaType(message.type);
39
+ }
40
+ else {
41
+ const download = await client.downloadMedia(ref.id);
42
+ data = download.data.toString('base64');
43
+ mediaType = ref.mimeType || download.mimeType || fallbackMediaType(message.type);
44
+ }
45
+ const parts = [];
46
+ if (caption.trim())
47
+ parts.push({ type: 'text', text: caption });
48
+ parts.push({ type: 'file', data, mediaType, filename: ref.filename });
49
+ return parts;
50
+ }
@@ -1,23 +1,23 @@
1
- import type { ResolvedSelection } from '@kuralle-agents/core';
1
+ import type { ResolvedSelection, UserInputContent } from '@kuralle-agents/core';
2
2
  import type { InboundMessage } from '../types/messages.js';
3
3
  export interface InboundResolverPlugin {
4
4
  readonly name: string;
5
5
  tryResolve(m: InboundMessage): Promise<{
6
- input: string;
6
+ input: UserInputContent;
7
7
  selection?: ResolvedSelection;
8
8
  } | undefined>;
9
9
  }
10
10
  export declare class InteractiveResolver implements InboundResolverPlugin {
11
11
  readonly name = "interactive";
12
12
  tryResolve(m: InboundMessage): Promise<{
13
- input: string;
13
+ input: UserInputContent;
14
14
  selection?: ResolvedSelection;
15
15
  } | undefined>;
16
16
  }
17
17
  export declare class TextResolver implements InboundResolverPlugin {
18
18
  readonly name = "text";
19
19
  tryResolve(m: InboundMessage): Promise<{
20
- input: string;
20
+ input: UserInputContent;
21
21
  selection?: ResolvedSelection;
22
22
  }>;
23
23
  }
@@ -25,7 +25,7 @@ export declare class InboundResolverChain {
25
25
  private readonly plugins;
26
26
  constructor(plugins: InboundResolverPlugin[]);
27
27
  resolve(m: InboundMessage): Promise<{
28
- input: string;
28
+ input: UserInputContent;
29
29
  selection?: ResolvedSelection;
30
30
  }>;
31
31
  }
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export { SessionResolverChain, ThreadIdResolver, PhoneLookupResolver, } from './
6
6
  export type { SessionResolverPlugin } from './adapter/session-resolver-chain.js';
7
7
  export { InboundResolverChain, InteractiveResolver, TextResolver, defaultInboundChain, } from './adapter/input-resolver-chain.js';
8
8
  export type { InboundResolverPlugin } from './adapter/input-resolver-chain.js';
9
+ export { attachInboundMedia } from './adapter/inbound-media.js';
9
10
  export { StreamMapper } from './adapter/stream-mapper.js';
10
11
  export { WindowTracker } from './adapter/window-tracker.js';
11
12
  export type { WindowStore, WindowState } from './adapter/window-store.js';
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ export { createMessagingRouter } from './adapter/createMessagingRouter.js';
9
9
  export { defaultSessionResolver } from './adapter/session-resolver.js';
10
10
  export { SessionResolverChain, ThreadIdResolver, PhoneLookupResolver, } from './adapter/session-resolver-chain.js';
11
11
  export { InboundResolverChain, InteractiveResolver, TextResolver, defaultInboundChain, } from './adapter/input-resolver-chain.js';
12
+ export { attachInboundMedia } from './adapter/inbound-media.js';
12
13
  export { StreamMapper } from './adapter/stream-mapper.js';
13
14
  export { WindowTracker } from './adapter/window-tracker.js';
14
15
  export { InMemoryWindowStore } from './adapter/window-store.js';
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "git+https://github.com/kuralle/kuralle-agents.git",
7
7
  "directory": "packages/kuralle-messaging"
8
8
  },
9
- "version": "0.7.2",
9
+ "version": "0.8.0",
10
10
  "description": "Core interfaces and Kuralle adapter for messaging platforms",
11
11
  "type": "module",
12
12
  "main": "dist/index.js",
@@ -26,7 +26,7 @@
26
26
  ],
27
27
  "dependencies": {
28
28
  "hono": "^4.7.4",
29
- "@kuralle-agents/core": "0.7.2"
29
+ "@kuralle-agents/core": "0.8.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "typescript": "^5.8.2",