@canonmsg/agent-sdk 0.5.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 +97 -10
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/media.d.ts +32 -0
- package/dist/media.js +166 -0
- package/dist/types.d.ts +26 -2
- package/package.json +9 -3
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
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { CanonClient, FINAL_MESSAGE_HANDOFF_MS, initRTDBAuth, rtdbWrite, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from '@canonmsg/core';
|
|
1
|
+
import { CanonClient, FINAL_MESSAGE_HANDOFF_MS, initRTDBAuth, mergeWorkSessionContexts, rtdbWrite, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from '@canonmsg/core';
|
|
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);
|
|
@@ -274,8 +275,19 @@ export class CanonAgent {
|
|
|
274
275
|
}
|
|
275
276
|
}, 3500);
|
|
276
277
|
try {
|
|
277
|
-
// Fetch history from API
|
|
278
|
-
const
|
|
278
|
+
// Fetch hydrated history/context from API
|
|
279
|
+
const page = await this.apiClient.getMessagesPage(conversationId, this.options.historyLimit);
|
|
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
|
+
});
|
|
279
291
|
// If sessions enabled, seed the session with fetched history
|
|
280
292
|
if (this.sessionManager && session) {
|
|
281
293
|
this.sessionManager.seedHistory(conversationId, history);
|
|
@@ -331,6 +343,11 @@ export class CanonAgent {
|
|
|
331
343
|
m.isOwner = m.senderId === ownerId;
|
|
332
344
|
}
|
|
333
345
|
}
|
|
346
|
+
const explicitWorkSession = messages.find((message) => message.workSession)?.workSession
|
|
347
|
+
?? history.find((message) => message.workSession)?.workSession
|
|
348
|
+
?? null;
|
|
349
|
+
const activeWorkSessions = mergeWorkSessionContexts(explicitWorkSession, page.workSessions ?? []);
|
|
350
|
+
const workSession = explicitWorkSession;
|
|
334
351
|
// Build agent context (fallback to minimal if not yet received)
|
|
335
352
|
const agent = this.agentContext ?? {
|
|
336
353
|
agentId: this.agentId,
|
|
@@ -345,9 +362,60 @@ export class CanonAgent {
|
|
|
345
362
|
const react = (messageId, emoji) => this.apiClient.react(conversationId, messageId, emoji);
|
|
346
363
|
const addMember = (userId) => this.apiClient.addMember(conversationId, userId);
|
|
347
364
|
const removeMember = (userId) => this.apiClient.removeMember(conversationId, userId);
|
|
365
|
+
const createWorkSession = (options) => this.apiClient.createWorkSession({
|
|
366
|
+
conversationId,
|
|
367
|
+
...(options ?? {}),
|
|
368
|
+
});
|
|
369
|
+
const getWorkSession = (workSessionId, targetConversationId = conversationId) => this.apiClient.getWorkSession(workSessionId, targetConversationId);
|
|
370
|
+
const updateWorkSessionContext = (workSessionId, options) => this.apiClient.upsertWorkSessionConversation(workSessionId, conversationId, options);
|
|
371
|
+
const sendLinkedMessage = (targetConversationId, text, options) => {
|
|
372
|
+
if (!options?.workSessionId && !options?.createWorkSession) {
|
|
373
|
+
throw new Error('sendLinkedMessage requires workSessionId or createWorkSession');
|
|
374
|
+
}
|
|
375
|
+
return this.apiClient.sendLinkedMessage({
|
|
376
|
+
sourceConversationId: conversationId,
|
|
377
|
+
targetConversationId,
|
|
378
|
+
text,
|
|
379
|
+
...options,
|
|
380
|
+
});
|
|
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
|
+
};
|
|
348
416
|
// Invoke handler
|
|
349
417
|
await this.handler({
|
|
350
|
-
messages,
|
|
418
|
+
messages: hydratedMessages,
|
|
351
419
|
history,
|
|
352
420
|
conversationId,
|
|
353
421
|
conversation,
|
|
@@ -360,7 +428,26 @@ export class CanonAgent {
|
|
|
360
428
|
react,
|
|
361
429
|
addMember,
|
|
362
430
|
removeMember,
|
|
431
|
+
createWorkSession,
|
|
432
|
+
getWorkSession,
|
|
433
|
+
updateWorkSessionContext,
|
|
434
|
+
sendLinkedMessage,
|
|
363
435
|
agent,
|
|
436
|
+
workSession,
|
|
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
|
+
},
|
|
364
451
|
session: session
|
|
365
452
|
? {
|
|
366
453
|
id: session.id,
|
|
@@ -432,7 +519,7 @@ export class CanonAgent {
|
|
|
432
519
|
}
|
|
433
520
|
catch { }
|
|
434
521
|
if (agentId && !shouldPersistTurnState) {
|
|
435
|
-
await clearTurnState(conversationId, agentId).catch(() => { });
|
|
522
|
+
await Promise.resolve(clearTurnState(conversationId, agentId)).catch(() => { });
|
|
436
523
|
}
|
|
437
524
|
}
|
|
438
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
|
-
export type { AgentContext, CanonMessage, CanonConversation, SendMessageOptions, CreateConversationOptions, } from '@canonmsg/core';
|
|
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
|
-
export type { AgentClientType, CanonMessage, CanonConversation, AgentContext, SendMessageOptions, CreateConversationOptions, TurnLifecycleState, } from '@canonmsg/core';
|
|
2
|
-
import type { CanonMessage, CanonConversation, SendMessageOptions } from '@canonmsg/core';
|
|
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
|
+
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 {
|
|
@@ -59,8 +60,31 @@ export interface MessageHandlerContext {
|
|
|
59
60
|
addMember: (userId: string) => Promise<void>;
|
|
60
61
|
/** Remove a member from this conversation (requires owner/admin role) */
|
|
61
62
|
removeMember: (userId: string) => Promise<void>;
|
|
63
|
+
/** Create a Canon work session rooted in this conversation. */
|
|
64
|
+
createWorkSession: (options?: Omit<CreateWorkSessionOptions, 'conversationId'>) => Promise<import('@canonmsg/core').CanonResolvedWorkSession>;
|
|
65
|
+
/** Load this conversation's scoped view of a Canon work session. */
|
|
66
|
+
getWorkSession: (workSessionId: string, conversationId?: string) => Promise<import('@canonmsg/core').CanonResolvedWorkSession>;
|
|
67
|
+
/** Update or attach this conversation's scoped work-session context. */
|
|
68
|
+
updateWorkSessionContext: (workSessionId: string, options?: UpdateWorkSessionConversationOptions) => Promise<import('@canonmsg/core').CanonResolvedWorkSession>;
|
|
69
|
+
/** Send into another conversation under an existing or lazily created Canon work session. */
|
|
70
|
+
sendLinkedMessage: (targetConversationId: string, text: string, options?: Omit<import('@canonmsg/core').SendLinkedMessageOptions, 'sourceConversationId' | 'targetConversationId' | 'text'>) => Promise<import('@canonmsg/core').SendLinkedMessageResult>;
|
|
62
71
|
/** Trusted agent identity & access context */
|
|
63
72
|
agent: import('@canonmsg/core').AgentContext;
|
|
73
|
+
/** Canon-provided shared task context for this turn, when attached to inbound messages. */
|
|
74
|
+
workSession?: import('@canonmsg/core').CanonWorkSessionContext | null;
|
|
75
|
+
/** All active Canon work sessions currently linked to this conversation. */
|
|
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
|
+
};
|
|
64
88
|
/** Per-conversation session state. Present when sessions are enabled. */
|
|
65
89
|
session?: SessionInfo;
|
|
66
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,20 +21,22 @@
|
|
|
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": {
|
|
23
28
|
"node": ">=18.0.0"
|
|
24
29
|
},
|
|
25
30
|
"dependencies": {
|
|
26
|
-
"@canonmsg/core": "^0.
|
|
31
|
+
"@canonmsg/core": "^0.6.0"
|
|
27
32
|
},
|
|
28
33
|
"publishConfig": {
|
|
29
34
|
"access": "public"
|
|
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
|
}
|