@canonmsg/agent-sdk 0.7.1 → 0.8.1

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.
package/README.md CHANGED
@@ -117,7 +117,7 @@ This is the easiest way to build agents that need per-conversation memory or que
117
117
 
118
118
  ## Media
119
119
 
120
- Normalized Canon messages always expose `attachments[]` as the canonical media contract. The SDK also keeps `imageUrl` and `audioUrl` on messages for legacy compatibility, but new integrations should prefer `attachments`.
120
+ Normalized Canon messages always expose `attachments[]` as the single canonical media contract. Legacy flat fields (`imageUrl`, `audioUrl`, `audioDurationMs`) are no longer part of the message shape agents must consume `attachments` directly.
121
121
 
122
122
  Use the handler `media` helpers when you need the actual file bytes:
123
123
 
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export { CanonAgent } from './canon-agent.js';
2
2
  export { CanonApiError } from '@canonmsg/core';
3
3
  export { SessionManager } from './session-manager.js';
4
- export { getMessageAttachments, inferUploadMimeType, materializeAttachment, materializeMessageMedia, uploadMediaFile, } from './media.js';
5
- export type { MaterializeMediaOptions, MaterializedCanonAttachment, ReplyWithFileOptions, UploadMediaFileOptions, } from './media.js';
4
+ export { getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, resolveAttachmentMimeType, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
5
+ export type { AnthropicImageBlock, AnthropicImageMimeType, MaterializeMediaOptions, MaterializedCanonAttachment, ReplyWithFileOptions, UploadMediaFileOptions, } from './media.js';
6
6
  export type { SessionConfig, Session } from './session-manager.js';
7
7
  export type { AgentContext, CanonMessage, CanonConversation, CanonResolvedWorkSession, CanonWorkSession, CanonWorkSessionContext, CanonWorkSessionConversationRole, CanonWorkSessionDisclosureMode, CanonWorkSessionParticipant, CanonWorkSessionStatus, CreateWorkSessionOptions, SendLinkedMessageOptions, SendLinkedMessageResult, SendMessageOptions, CreateConversationOptions, UpdateWorkSessionConversationOptions, } from '@canonmsg/core';
8
8
  export type { SDKMessage, SDKConversation, CanonAgentOptions, MessageHandler, MessageHandlerContext, ProgressMessageOptions, ProgressMessageResult, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { CanonAgent } from './canon-agent.js';
2
2
  export { CanonApiError } from '@canonmsg/core';
3
3
  export { SessionManager } from './session-manager.js';
4
- export { getMessageAttachments, inferUploadMimeType, materializeAttachment, materializeMessageMedia, uploadMediaFile, } from './media.js';
4
+ export { getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, resolveAttachmentMimeType, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
package/dist/media.d.ts CHANGED
@@ -20,13 +20,55 @@ export interface MaterializedCanonAttachment extends MediaAttachment {
20
20
  conversationId: string;
21
21
  messageId: string;
22
22
  }
23
- export declare function getMessageAttachments(message: Pick<CanonMessage, 'attachments' | 'imageUrl' | 'audioUrl' | 'audioDurationMs'>): MediaAttachment[];
23
+ /**
24
+ * Anthropic `image` content blocks only accept these MIME types for
25
+ * base64 sources. Anything outside this set must either be re-encoded or
26
+ * degraded to a text reference.
27
+ */
28
+ export type AnthropicImageMimeType = 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp';
29
+ /**
30
+ * Structural shape of an Anthropic `ImageBlockParam` (base64 source). We
31
+ * do not import from `@anthropic-ai/sdk` here because the agent-sdk must
32
+ * stay dependency-free; the Claude Code plugin re-casts to the SDK type.
33
+ */
34
+ export interface AnthropicImageBlock {
35
+ type: 'image';
36
+ source: {
37
+ type: 'base64';
38
+ media_type: AnthropicImageMimeType;
39
+ data: string;
40
+ };
41
+ }
42
+ export declare function getMessageAttachments(message: Pick<CanonMessage, 'attachments'>): MediaAttachment[];
24
43
  export declare function materializeAttachment(attachment: MediaAttachment, options: MaterializeMediaOptions & {
25
44
  index?: number;
26
45
  }): Promise<MaterializedCanonAttachment>;
27
- export declare function materializeMessageMedia(message: Pick<CanonMessage, 'id' | 'attachments' | 'imageUrl' | 'audioUrl' | 'audioDurationMs'>, options: Omit<MaterializeMediaOptions, 'messageId'>): Promise<MaterializedCanonAttachment[]>;
46
+ export declare function materializeMessageMedia(message: Pick<CanonMessage, 'id' | 'attachments'>, options: Omit<MaterializeMediaOptions, 'messageId'>): Promise<MaterializedCanonAttachment[]>;
28
47
  export declare function inferUploadMimeType(filePath: string, overrideMimeType?: string): string;
29
48
  export declare function uploadMediaFile(client: CanonClient, conversationId: string, filePath: string, options?: UploadMediaFileOptions): Promise<{
30
49
  url: string;
31
50
  attachment: MediaAttachment;
32
51
  }>;
52
+ /**
53
+ * Resolve the effective MIME type of a materialized attachment, falling back
54
+ * to filename/URL extensions when the server didn't tell us explicitly.
55
+ */
56
+ export declare function resolveAttachmentMimeType(attachment: MaterializedCanonAttachment): string | null;
57
+ /**
58
+ * True when a materialized attachment can be sent to Anthropic as a base64
59
+ * `image` block (jpeg, png, gif, webp). Other images (heic, tiff, svg, …)
60
+ * must be re-encoded before they can be delivered as a vision block.
61
+ */
62
+ export declare function isAnthropicImageAttachment(attachment: MaterializedCanonAttachment): boolean;
63
+ /**
64
+ * Read the materialized bytes and build an Anthropic `image` content block.
65
+ * Callers should first check `isAnthropicImageAttachment`; this function
66
+ * throws if the MIME type is not supported.
67
+ */
68
+ export declare function toAnthropicImageBlock(attachment: MaterializedCanonAttachment): Promise<AnthropicImageBlock>;
69
+ /**
70
+ * Return the local path to pass to Codex via its `-i/--image` flag when the
71
+ * attachment is an image Codex can consume. Codex accepts the same MIME types
72
+ * as Anthropic in practice (jpeg/png/gif/webp), so we reuse the allowlist.
73
+ */
74
+ export declare function getCodexImagePath(attachment: MaterializedCanonAttachment): string | null;
package/dist/media.js CHANGED
@@ -1,6 +1,12 @@
1
1
  import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
2
2
  import { basename, dirname, extname, join } from 'node:path';
3
3
  import { CANON_DIR, } from '@canonmsg/core';
4
+ const ANTHROPIC_IMAGE_MIME_TYPES = new Set([
5
+ 'image/jpeg',
6
+ 'image/png',
7
+ 'image/gif',
8
+ 'image/webp',
9
+ ]);
4
10
  const DEFAULT_MEDIA_CACHE_DIR = join(CANON_DIR, 'media-cache');
5
11
  const EXTENSION_BY_MIME = {
6
12
  'application/json': 'json',
@@ -88,25 +94,7 @@ async function fileExists(path) {
88
94
  }
89
95
  }
90
96
  export function getMessageAttachments(message) {
91
- if (Array.isArray(message.attachments) && message.attachments.length > 0) {
92
- return message.attachments;
93
- }
94
- if (message.audioUrl) {
95
- return [{
96
- kind: 'audio',
97
- url: message.audioUrl,
98
- ...(typeof message.audioDurationMs === 'number'
99
- ? { durationMs: message.audioDurationMs }
100
- : {}),
101
- }];
102
- }
103
- if (message.imageUrl) {
104
- return [{
105
- kind: 'image',
106
- url: message.imageUrl,
107
- }];
108
- }
109
- return [];
97
+ return Array.isArray(message.attachments) ? message.attachments : [];
110
98
  }
111
99
  export async function materializeAttachment(attachment, options) {
112
100
  const path = buildCachePath({
@@ -164,3 +152,63 @@ export async function uploadMediaFile(client, conversationId, filePath, options)
164
152
  const fileName = options?.fileName ?? basename(filePath);
165
153
  return client.uploadMedia(conversationId, buffer.toString('base64'), mimeType, fileName);
166
154
  }
155
+ /**
156
+ * Resolve the effective MIME type of a materialized attachment, falling back
157
+ * to filename/URL extensions when the server didn't tell us explicitly.
158
+ */
159
+ export function resolveAttachmentMimeType(attachment) {
160
+ if (attachment.mimeType)
161
+ return attachment.mimeType.toLowerCase();
162
+ const pathExt = extname(attachment.path).toLowerCase();
163
+ const fromPath = MIME_BY_EXTENSION[pathExt];
164
+ if (fromPath)
165
+ return fromPath;
166
+ const fileExt = extname(attachment.fileName ?? '').toLowerCase();
167
+ const fromFileName = MIME_BY_EXTENSION[fileExt];
168
+ if (fromFileName)
169
+ return fromFileName;
170
+ const urlExt = extname(attachment.sourceUrl.split('?')[0] ?? '').toLowerCase();
171
+ const fromUrl = MIME_BY_EXTENSION[urlExt];
172
+ if (fromUrl)
173
+ return fromUrl;
174
+ return null;
175
+ }
176
+ /**
177
+ * True when a materialized attachment can be sent to Anthropic as a base64
178
+ * `image` block (jpeg, png, gif, webp). Other images (heic, tiff, svg, …)
179
+ * must be re-encoded before they can be delivered as a vision block.
180
+ */
181
+ export function isAnthropicImageAttachment(attachment) {
182
+ if (attachment.kind !== 'image')
183
+ return false;
184
+ const mime = resolveAttachmentMimeType(attachment);
185
+ return mime != null && ANTHROPIC_IMAGE_MIME_TYPES.has(mime);
186
+ }
187
+ /**
188
+ * Read the materialized bytes and build an Anthropic `image` content block.
189
+ * Callers should first check `isAnthropicImageAttachment`; this function
190
+ * throws if the MIME type is not supported.
191
+ */
192
+ export async function toAnthropicImageBlock(attachment) {
193
+ if (!isAnthropicImageAttachment(attachment)) {
194
+ throw new Error(`Canon attachment ${attachment.index} is not a supported Anthropic image (kind=${attachment.kind}, mime=${attachment.mimeType ?? 'unknown'})`);
195
+ }
196
+ const mediaType = resolveAttachmentMimeType(attachment);
197
+ const buffer = await readFile(attachment.path);
198
+ return {
199
+ type: 'image',
200
+ source: {
201
+ type: 'base64',
202
+ media_type: mediaType,
203
+ data: buffer.toString('base64'),
204
+ },
205
+ };
206
+ }
207
+ /**
208
+ * Return the local path to pass to Codex via its `-i/--image` flag when the
209
+ * attachment is an image Codex can consume. Codex accepts the same MIME types
210
+ * as Anthropic in practice (jpeg/png/gif/webp), so we reuse the allowlist.
211
+ */
212
+ export function getCodexImagePath(attachment) {
213
+ return isAnthropicImageAttachment(attachment) ? attachment.path : null;
214
+ }
package/dist/realtime.js CHANGED
@@ -37,13 +37,16 @@ export class RealtimeManager {
37
37
  isOwner: m.isOwner ?? false,
38
38
  contentType: m.contentType ?? 'text',
39
39
  text: m.text ?? null,
40
- imageUrl: m.imageUrl ?? null,
41
- audioUrl: m.audioUrl ?? null,
42
- audioDurationMs: m.audioDurationMs ?? null,
43
40
  attachments: m.attachments ?? [],
44
41
  mentions: m.mentions ?? [],
45
42
  replyTo: m.replyTo ?? null,
46
43
  replyToPosition: m.replyToPosition ?? null,
44
+ ...(m.forwarded === true || m.forwardedFrom
45
+ ? { forwarded: true }
46
+ : {}),
47
+ ...(m.forwardedFrom
48
+ ? { forwardedFrom: m.forwardedFrom }
49
+ : {}),
47
50
  status: 'sent',
48
51
  deleted: false,
49
52
  createdAt: m.createdAt ?? new Date().toISOString(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "description": "Canon Agent SDK — build AI agents that participate in Canon conversations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -28,7 +28,7 @@
28
28
  "node": ">=18.0.0"
29
29
  },
30
30
  "dependencies": {
31
- "@canonmsg/core": "^0.7.0"
31
+ "@canonmsg/core": "^0.7.1"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"