@canonmsg/agent-sdk 0.6.0 → 0.7.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.
- package/README.md +24 -0
- package/dist/canon-agent.js +65 -7
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/media.d.ts +32 -0
- package/dist/media.js +166 -0
- package/dist/types.d.ts +12 -0
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -77,6 +77,7 @@ The `message` event handler receives a context object with:
|
|
|
77
77
|
| `replyFinal` | `(text: string, options?) => Promise<{ messageId: string }>` | Send the durable final reply for a turn |
|
|
78
78
|
| `replyProgress` | `(text: string, options?) => Promise<{ turnId: string; durable: boolean; messageId: string \| null }>` | Update the live turn progress; add `durable: true` to also persist it |
|
|
79
79
|
| `agent` | `AgentContext` | Trusted Canon agent identity and access context |
|
|
80
|
+
| `media` | `{ materialize, uploadFile, replyWithFile }` | Canon-managed access to real media bytes via `~/.canon/media-cache` plus local-file uploads back into Canon |
|
|
80
81
|
| `session` | `SessionInfo \| undefined` | Per-conversation queue/session state when sessions are enabled |
|
|
81
82
|
| `turn` | `TurnController \| undefined` | Live turn-state helpers for thinking/streaming/tool/waiting-input |
|
|
82
83
|
|
|
@@ -114,6 +115,29 @@ When `sessions.enabled` is on, the SDK serializes work per conversation and expo
|
|
|
114
115
|
|
|
115
116
|
This is the easiest way to build agents that need per-conversation memory or queue awareness.
|
|
116
117
|
|
|
118
|
+
## Media
|
|
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`.
|
|
121
|
+
|
|
122
|
+
Use the handler `media` helpers when you need the actual file bytes:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
agent.on('message', async ({ messages, media }) => {
|
|
126
|
+
const files = await media.materialize(messages[messages.length - 1]);
|
|
127
|
+
console.log(files[0]?.path); // ~/.canon/media-cache/<agent>/<conversation>/<message>/...
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- `media.materialize(message?)` downloads the message's attachments on demand into `~/.canon/media-cache`.
|
|
132
|
+
- `media.uploadFile(path, options?)` uploads a local file into the current Canon conversation and returns the canonical attachment metadata.
|
|
133
|
+
- `media.replyWithFile(path, text?, options?)` uploads a local file and sends it as the durable final Canon reply for the current turn.
|
|
134
|
+
|
|
135
|
+
The public helpers are also available from the Node-only subpath export:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import { materializeMessageMedia, uploadMediaFile } from '@canonmsg/agent-sdk/media';
|
|
139
|
+
```
|
|
140
|
+
|
|
117
141
|
## Agent Registration
|
|
118
142
|
|
|
119
143
|
Register a new agent using the static helpers (no API key needed):
|
package/dist/canon-agent.js
CHANGED
|
@@ -2,6 +2,7 @@ import { CanonClient, FINAL_MESSAGE_HANDOFF_MS, initRTDBAuth, mergeWorkSessionCo
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { AuthManager } from './auth.js';
|
|
4
4
|
import { Debouncer } from './debouncer.js';
|
|
5
|
+
import { materializeMessageMedia, uploadMediaFile, } from './media.js';
|
|
5
6
|
import { PollingManager } from './polling.js';
|
|
6
7
|
import { SessionManager } from './session-manager.js';
|
|
7
8
|
const AUTO_MODE_THRESHOLD = 500;
|
|
@@ -153,12 +154,12 @@ export class CanonAgent {
|
|
|
153
154
|
// Clear session state if enabled (uses cached IDs — no network call during shutdown)
|
|
154
155
|
if (this.options.sessionState && this.agentId) {
|
|
155
156
|
for (const id of this.cachedConversationIds) {
|
|
156
|
-
clearSessionState(id, this.agentId).catch(() => { });
|
|
157
|
+
Promise.resolve(clearSessionState(id, this.agentId)).catch(() => { });
|
|
157
158
|
}
|
|
158
159
|
}
|
|
159
160
|
if (this.agentId) {
|
|
160
161
|
for (const id of this.cachedConversationIds) {
|
|
161
|
-
clearTurnState(id, this.agentId).catch(() => { });
|
|
162
|
+
Promise.resolve(clearTurnState(id, this.agentId)).catch(() => { });
|
|
162
163
|
}
|
|
163
164
|
}
|
|
164
165
|
await this.clearAgentRuntime();
|
|
@@ -197,7 +198,7 @@ export class CanonAgent {
|
|
|
197
198
|
async clearAgentRuntime() {
|
|
198
199
|
if (!this.agentId)
|
|
199
200
|
return;
|
|
200
|
-
await rtdbWrite(`/agent-runtime/${this.agentId}`, null).catch(() => { });
|
|
201
|
+
await Promise.resolve(rtdbWrite(`/agent-runtime/${this.agentId}`, null)).catch(() => { });
|
|
201
202
|
}
|
|
202
203
|
async handleMessages(conversationId, messages) {
|
|
203
204
|
if (!this.handler) {
|
|
@@ -226,7 +227,7 @@ export class CanonAgent {
|
|
|
226
227
|
if (!agentId)
|
|
227
228
|
return;
|
|
228
229
|
turnState = state;
|
|
229
|
-
await writeTurnState(conversationId, agentId, {
|
|
230
|
+
await Promise.resolve(writeTurnState(conversationId, agentId, {
|
|
230
231
|
turnId,
|
|
231
232
|
state,
|
|
232
233
|
queueDepth: queueDepth(),
|
|
@@ -236,7 +237,7 @@ export class CanonAgent {
|
|
|
236
237
|
...(state === 'completed' || state === 'interrupted' || state === 'idle'
|
|
237
238
|
? { completedAt: { '.sv': 'timestamp' } }
|
|
238
239
|
: {}),
|
|
239
|
-
}).catch(() => { });
|
|
240
|
+
})).catch(() => { });
|
|
240
241
|
};
|
|
241
242
|
const setLiveState = async (state, text, streamingStatus) => {
|
|
242
243
|
await writeTurn(state);
|
|
@@ -277,6 +278,16 @@ export class CanonAgent {
|
|
|
277
278
|
// Fetch hydrated history/context from API
|
|
278
279
|
const page = await this.apiClient.getMessagesPage(conversationId, this.options.historyLimit);
|
|
279
280
|
const history = page.messages;
|
|
281
|
+
const historyById = new Map(history.map((message) => [message.id, message]));
|
|
282
|
+
const hydratedMessages = messages.map((message) => {
|
|
283
|
+
const hydrated = historyById.get(message.id);
|
|
284
|
+
if (hydrated)
|
|
285
|
+
return hydrated;
|
|
286
|
+
return {
|
|
287
|
+
...message,
|
|
288
|
+
attachments: message.attachments ?? [],
|
|
289
|
+
};
|
|
290
|
+
});
|
|
280
291
|
// If sessions enabled, seed the session with fetched history
|
|
281
292
|
if (this.sessionManager && session) {
|
|
282
293
|
this.sessionManager.seedHistory(conversationId, history);
|
|
@@ -368,9 +379,43 @@ export class CanonAgent {
|
|
|
368
379
|
...options,
|
|
369
380
|
});
|
|
370
381
|
};
|
|
382
|
+
const uploadFile = (filePath, options) => uploadMediaFile(this.apiClient, conversationId, filePath, options);
|
|
383
|
+
const replyWithFile = async (filePath, text = '', options) => {
|
|
384
|
+
try {
|
|
385
|
+
await this.apiClient.setTyping(conversationId, true, 'typing');
|
|
386
|
+
}
|
|
387
|
+
catch { }
|
|
388
|
+
try {
|
|
389
|
+
const uploaded = await uploadFile(filePath, options);
|
|
390
|
+
const result = await this.apiClient.sendMessage(conversationId, text, {
|
|
391
|
+
...(options?.replyTo ? { replyTo: options.replyTo } : {}),
|
|
392
|
+
...(options?.replyToPosition != null
|
|
393
|
+
? { replyToPosition: options.replyToPosition }
|
|
394
|
+
: {}),
|
|
395
|
+
...(options?.mentions ? { mentions: options.mentions } : {}),
|
|
396
|
+
metadata: {
|
|
397
|
+
...(options?.metadata ?? {}),
|
|
398
|
+
turnId,
|
|
399
|
+
turnSemantics: 'turn_complete',
|
|
400
|
+
turnComplete: true,
|
|
401
|
+
},
|
|
402
|
+
...(options?.workSessionId ? { workSessionId: options.workSessionId } : {}),
|
|
403
|
+
contentType: uploaded.attachment.kind,
|
|
404
|
+
attachments: [uploaded.attachment],
|
|
405
|
+
});
|
|
406
|
+
await sleep(FINAL_MESSAGE_HANDOFF_MS);
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
finally {
|
|
410
|
+
try {
|
|
411
|
+
await this.apiClient.setTyping(conversationId, false);
|
|
412
|
+
}
|
|
413
|
+
catch { }
|
|
414
|
+
}
|
|
415
|
+
};
|
|
371
416
|
// Invoke handler
|
|
372
417
|
await this.handler({
|
|
373
|
-
messages,
|
|
418
|
+
messages: hydratedMessages,
|
|
374
419
|
history,
|
|
375
420
|
conversationId,
|
|
376
421
|
conversation,
|
|
@@ -390,6 +435,19 @@ export class CanonAgent {
|
|
|
390
435
|
agent,
|
|
391
436
|
workSession,
|
|
392
437
|
activeWorkSessions,
|
|
438
|
+
media: {
|
|
439
|
+
materialize: (message = hydratedMessages[hydratedMessages.length - 1], options) => {
|
|
440
|
+
if (!message)
|
|
441
|
+
return Promise.resolve([]);
|
|
442
|
+
return materializeMessageMedia(message, {
|
|
443
|
+
agentId: agent.agentId,
|
|
444
|
+
conversationId,
|
|
445
|
+
...(options ?? {}),
|
|
446
|
+
});
|
|
447
|
+
},
|
|
448
|
+
uploadFile,
|
|
449
|
+
replyWithFile,
|
|
450
|
+
},
|
|
393
451
|
session: session
|
|
394
452
|
? {
|
|
395
453
|
id: session.id,
|
|
@@ -461,7 +519,7 @@ export class CanonAgent {
|
|
|
461
519
|
}
|
|
462
520
|
catch { }
|
|
463
521
|
if (agentId && !shouldPersistTurnState) {
|
|
464
|
-
await clearTurnState(conversationId, agentId).catch(() => { });
|
|
522
|
+
await Promise.resolve(clearTurnState(conversationId, agentId)).catch(() => { });
|
|
465
523
|
}
|
|
466
524
|
}
|
|
467
525
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +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
6
|
export type { SessionConfig, Session } from './session-manager.js';
|
|
5
7
|
export type { AgentContext, CanonMessage, CanonConversation, CanonResolvedWorkSession, CanonWorkSession, CanonWorkSessionContext, CanonWorkSessionConversationRole, CanonWorkSessionDisclosureMode, CanonWorkSessionParticipant, CanonWorkSessionStatus, CreateWorkSessionOptions, SendLinkedMessageOptions, SendLinkedMessageResult, SendMessageOptions, CreateConversationOptions, UpdateWorkSessionConversationOptions, } from '@canonmsg/core';
|
|
6
8
|
export type { SDKMessage, SDKConversation, CanonAgentOptions, MessageHandler, MessageHandlerContext, ProgressMessageOptions, ProgressMessageResult, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +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';
|
package/dist/media.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { CanonClient, type CanonMessage, type MediaAttachment, type SendMessageOptions } from '@canonmsg/core';
|
|
2
|
+
export interface MaterializeMediaOptions {
|
|
3
|
+
agentId: string;
|
|
4
|
+
conversationId: string;
|
|
5
|
+
messageId: string;
|
|
6
|
+
rootDir?: string;
|
|
7
|
+
fetchImpl?: typeof fetch;
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
}
|
|
10
|
+
export interface UploadMediaFileOptions {
|
|
11
|
+
fileName?: string;
|
|
12
|
+
mimeType?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ReplyWithFileOptions extends Omit<SendMessageOptions, 'attachments' | 'contentType'>, UploadMediaFileOptions {
|
|
15
|
+
}
|
|
16
|
+
export interface MaterializedCanonAttachment extends MediaAttachment {
|
|
17
|
+
index: number;
|
|
18
|
+
path: string;
|
|
19
|
+
sourceUrl: string;
|
|
20
|
+
conversationId: string;
|
|
21
|
+
messageId: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function getMessageAttachments(message: Pick<CanonMessage, 'attachments' | 'imageUrl' | 'audioUrl' | 'audioDurationMs'>): MediaAttachment[];
|
|
24
|
+
export declare function materializeAttachment(attachment: MediaAttachment, options: MaterializeMediaOptions & {
|
|
25
|
+
index?: number;
|
|
26
|
+
}): Promise<MaterializedCanonAttachment>;
|
|
27
|
+
export declare function materializeMessageMedia(message: Pick<CanonMessage, 'id' | 'attachments' | 'imageUrl' | 'audioUrl' | 'audioDurationMs'>, options: Omit<MaterializeMediaOptions, 'messageId'>): Promise<MaterializedCanonAttachment[]>;
|
|
28
|
+
export declare function inferUploadMimeType(filePath: string, overrideMimeType?: string): string;
|
|
29
|
+
export declare function uploadMediaFile(client: CanonClient, conversationId: string, filePath: string, options?: UploadMediaFileOptions): Promise<{
|
|
30
|
+
url: string;
|
|
31
|
+
attachment: MediaAttachment;
|
|
32
|
+
}>;
|
package/dist/media.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, extname, join } from 'node:path';
|
|
3
|
+
import { CANON_DIR, } from '@canonmsg/core';
|
|
4
|
+
const DEFAULT_MEDIA_CACHE_DIR = join(CANON_DIR, 'media-cache');
|
|
5
|
+
const EXTENSION_BY_MIME = {
|
|
6
|
+
'application/json': 'json',
|
|
7
|
+
'application/pdf': 'pdf',
|
|
8
|
+
'application/zip': 'zip',
|
|
9
|
+
'audio/mp4': 'm4a',
|
|
10
|
+
'audio/mpeg': 'mp3',
|
|
11
|
+
'audio/ogg': 'ogg',
|
|
12
|
+
'audio/webm': 'webm',
|
|
13
|
+
'audio/wav': 'wav',
|
|
14
|
+
'image/gif': 'gif',
|
|
15
|
+
'image/jpeg': 'jpg',
|
|
16
|
+
'image/png': 'png',
|
|
17
|
+
'image/webp': 'webp',
|
|
18
|
+
'text/plain': 'txt',
|
|
19
|
+
};
|
|
20
|
+
const MIME_BY_EXTENSION = {
|
|
21
|
+
'.gif': 'image/gif',
|
|
22
|
+
'.jpeg': 'image/jpeg',
|
|
23
|
+
'.jpg': 'image/jpeg',
|
|
24
|
+
'.json': 'application/json',
|
|
25
|
+
'.m4a': 'audio/mp4',
|
|
26
|
+
'.mp3': 'audio/mpeg',
|
|
27
|
+
'.ogg': 'audio/ogg',
|
|
28
|
+
'.pdf': 'application/pdf',
|
|
29
|
+
'.png': 'image/png',
|
|
30
|
+
'.txt': 'text/plain',
|
|
31
|
+
'.wav': 'audio/wav',
|
|
32
|
+
'.webm': 'audio/webm',
|
|
33
|
+
'.webp': 'image/webp',
|
|
34
|
+
'.zip': 'application/zip',
|
|
35
|
+
};
|
|
36
|
+
function sanitizeSegment(value, fallback) {
|
|
37
|
+
const sanitized = value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
38
|
+
return sanitized || fallback;
|
|
39
|
+
}
|
|
40
|
+
function sanitizeFileName(fileName, fallback) {
|
|
41
|
+
const sanitized = basename(fileName).replace(/[^a-zA-Z0-9._-]+/g, '_');
|
|
42
|
+
return sanitized || fallback;
|
|
43
|
+
}
|
|
44
|
+
function inferExtension(input) {
|
|
45
|
+
const explicitExtension = extname(input.attachment.fileName ?? '').toLowerCase();
|
|
46
|
+
if (explicitExtension) {
|
|
47
|
+
return explicitExtension.replace(/[^a-z0-9.]/g, '') || '.bin';
|
|
48
|
+
}
|
|
49
|
+
const mimeType = input.responseMimeType ?? input.attachment.mimeType ?? '';
|
|
50
|
+
const byMime = EXTENSION_BY_MIME[mimeType.toLowerCase()];
|
|
51
|
+
if (byMime) {
|
|
52
|
+
return `.${byMime}`;
|
|
53
|
+
}
|
|
54
|
+
const urlPath = input.attachment.url.split('?')[0] ?? '';
|
|
55
|
+
const urlExtension = extname(urlPath).toLowerCase();
|
|
56
|
+
if (urlExtension) {
|
|
57
|
+
return urlExtension.replace(/[^a-z0-9.]/g, '') || '.bin';
|
|
58
|
+
}
|
|
59
|
+
return '.bin';
|
|
60
|
+
}
|
|
61
|
+
function buildCachePath(input) {
|
|
62
|
+
const rootDir = input.rootDir ?? DEFAULT_MEDIA_CACHE_DIR;
|
|
63
|
+
const extension = inferExtension({
|
|
64
|
+
attachment: input.attachment,
|
|
65
|
+
responseMimeType: input.responseMimeType,
|
|
66
|
+
});
|
|
67
|
+
const fallbackName = `${input.attachment.kind}-${input.index}${extension}`;
|
|
68
|
+
const preferredName = input.attachment.fileName
|
|
69
|
+
? sanitizeFileName(input.attachment.fileName, fallbackName)
|
|
70
|
+
: fallbackName;
|
|
71
|
+
const fileName = extname(preferredName) ? preferredName : `${preferredName}${extension}`;
|
|
72
|
+
return join(rootDir, sanitizeSegment(input.agentId, 'agent'), sanitizeSegment(input.conversationId, 'conversation'), sanitizeSegment(input.messageId, 'message'), `${String(input.index).padStart(2, '0')}-${fileName}`);
|
|
73
|
+
}
|
|
74
|
+
function ensureFetch(fetchImpl) {
|
|
75
|
+
if (fetchImpl)
|
|
76
|
+
return fetchImpl;
|
|
77
|
+
if (typeof fetch === 'function')
|
|
78
|
+
return fetch;
|
|
79
|
+
throw new Error('Global fetch is unavailable; provide fetchImpl explicitly.');
|
|
80
|
+
}
|
|
81
|
+
async function fileExists(path) {
|
|
82
|
+
try {
|
|
83
|
+
const info = await stat(path);
|
|
84
|
+
return info.isFile();
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
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 [];
|
|
110
|
+
}
|
|
111
|
+
export async function materializeAttachment(attachment, options) {
|
|
112
|
+
const path = buildCachePath({
|
|
113
|
+
agentId: options.agentId,
|
|
114
|
+
conversationId: options.conversationId,
|
|
115
|
+
messageId: options.messageId,
|
|
116
|
+
attachment,
|
|
117
|
+
index: options.index ?? 0,
|
|
118
|
+
rootDir: options.rootDir,
|
|
119
|
+
});
|
|
120
|
+
await mkdir(dirname(path), { recursive: true });
|
|
121
|
+
let responseMimeType = null;
|
|
122
|
+
if (!(await fileExists(path))) {
|
|
123
|
+
const fetchImpl = ensureFetch(options.fetchImpl);
|
|
124
|
+
const response = await fetchImpl(attachment.url, {
|
|
125
|
+
signal: options.signal,
|
|
126
|
+
});
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
throw new Error(`Failed to download Canon media (${response.status} ${response.statusText})`);
|
|
129
|
+
}
|
|
130
|
+
responseMimeType = response.headers.get('content-type');
|
|
131
|
+
const body = Buffer.from(await response.arrayBuffer());
|
|
132
|
+
await writeFile(path, body);
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
...attachment,
|
|
136
|
+
index: options.index ?? 0,
|
|
137
|
+
path,
|
|
138
|
+
sourceUrl: attachment.url,
|
|
139
|
+
conversationId: options.conversationId,
|
|
140
|
+
messageId: options.messageId,
|
|
141
|
+
...(attachment.mimeType
|
|
142
|
+
? {}
|
|
143
|
+
: responseMimeType
|
|
144
|
+
? { mimeType: responseMimeType }
|
|
145
|
+
: {}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
export async function materializeMessageMedia(message, options) {
|
|
149
|
+
const attachments = getMessageAttachments(message);
|
|
150
|
+
return Promise.all(attachments.map((attachment, index) => materializeAttachment(attachment, {
|
|
151
|
+
...options,
|
|
152
|
+
messageId: message.id,
|
|
153
|
+
index,
|
|
154
|
+
})));
|
|
155
|
+
}
|
|
156
|
+
export function inferUploadMimeType(filePath, overrideMimeType) {
|
|
157
|
+
if (overrideMimeType)
|
|
158
|
+
return overrideMimeType;
|
|
159
|
+
return MIME_BY_EXTENSION[extname(filePath).toLowerCase()] ?? 'application/octet-stream';
|
|
160
|
+
}
|
|
161
|
+
export async function uploadMediaFile(client, conversationId, filePath, options) {
|
|
162
|
+
const buffer = await readFile(filePath);
|
|
163
|
+
const mimeType = inferUploadMimeType(filePath, options?.mimeType);
|
|
164
|
+
const fileName = options?.fileName ?? basename(filePath);
|
|
165
|
+
return client.uploadMedia(conversationId, buffer.toString('base64'), mimeType, fileName);
|
|
166
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type { AgentClientType, CanonMessage, CanonConversation, AgentContext, CanonResolvedWorkSession, CanonWorkSession, CanonWorkSessionContext, CanonWorkSessionConversationRole, CanonWorkSessionDisclosureMode, CanonWorkSessionParticipant, CanonWorkSessionStatus, CreateWorkSessionOptions, SendLinkedMessageOptions, SendLinkedMessageResult, SendMessageOptions, CreateConversationOptions, TurnLifecycleState, UpdateWorkSessionConversationOptions, } from '@canonmsg/core';
|
|
2
2
|
import type { CanonMessage, CanonConversation, CreateWorkSessionOptions, SendMessageOptions, UpdateWorkSessionConversationOptions } from '@canonmsg/core';
|
|
3
|
+
import type { MaterializeMediaOptions, MaterializedCanonAttachment, ReplyWithFileOptions, UploadMediaFileOptions } from './media.js';
|
|
3
4
|
export type SDKMessage = CanonMessage;
|
|
4
5
|
export type SDKConversation = CanonConversation;
|
|
5
6
|
export interface ProgressMessageOptions extends SendMessageOptions {
|
|
@@ -73,6 +74,17 @@ export interface MessageHandlerContext {
|
|
|
73
74
|
workSession?: import('@canonmsg/core').CanonWorkSessionContext | null;
|
|
74
75
|
/** All active Canon work sessions currently linked to this conversation. */
|
|
75
76
|
activeWorkSessions?: import('@canonmsg/core').CanonWorkSessionContext[];
|
|
77
|
+
/** Canon-managed local media access for the current conversation. */
|
|
78
|
+
media: {
|
|
79
|
+
materialize: (message?: SDKMessage, options?: Omit<MaterializeMediaOptions, 'agentId' | 'conversationId' | 'messageId'>) => Promise<MaterializedCanonAttachment[]>;
|
|
80
|
+
uploadFile: (filePath: string, options?: UploadMediaFileOptions) => Promise<{
|
|
81
|
+
url: string;
|
|
82
|
+
attachment: import('@canonmsg/core').MediaAttachment;
|
|
83
|
+
}>;
|
|
84
|
+
replyWithFile: (filePath: string, text?: string, options?: ReplyWithFileOptions) => Promise<{
|
|
85
|
+
messageId: string;
|
|
86
|
+
}>;
|
|
87
|
+
};
|
|
76
88
|
/** Per-conversation session state. Present when sessions are enabled. */
|
|
77
89
|
session?: SessionInfo;
|
|
78
90
|
/** Turn lifecycle helpers for live-work rendering and progress reporting. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/agent-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Canon Agent SDK — build AI agents that participate in Canon conversations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
".": {
|
|
10
10
|
"import": "./dist/index.js",
|
|
11
11
|
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./media": {
|
|
14
|
+
"import": "./dist/media.js",
|
|
15
|
+
"types": "./dist/media.d.ts"
|
|
12
16
|
}
|
|
13
17
|
},
|
|
14
18
|
"files": [
|
|
@@ -17,6 +21,7 @@
|
|
|
17
21
|
"scripts": {
|
|
18
22
|
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
|
|
19
23
|
"dev": "tsc --watch",
|
|
24
|
+
"test": "vitest run",
|
|
20
25
|
"prepack": "npm run build"
|
|
21
26
|
},
|
|
22
27
|
"engines": {
|
|
@@ -30,7 +35,8 @@
|
|
|
30
35
|
},
|
|
31
36
|
"devDependencies": {
|
|
32
37
|
"@types/node": "^22.0.0",
|
|
33
|
-
"typescript": "~5.7.0"
|
|
38
|
+
"typescript": "~5.7.0",
|
|
39
|
+
"vitest": "^3.0.0"
|
|
34
40
|
},
|
|
35
41
|
"license": "MIT"
|
|
36
42
|
}
|