@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 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):
@@ -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 history = await this.apiClient.getMessages(conversationId, this.options.historyLimit);
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';
@@ -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.5.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.5.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
  }