@canonmsg/agent-sdk 1.1.3 → 1.2.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.
@@ -32,6 +32,7 @@ export declare class CanonAgent {
32
32
  private contactRemovedHandler;
33
33
  private interruptHandler;
34
34
  private stopAndDropHandler;
35
+ private newSessionHandler;
35
36
  /** Contact-graph operations (`agent.contacts.*`). Initialized in the constructor. */
36
37
  readonly contacts: AgentContactsAPI;
37
38
  /** Block/unblock operations (`agent.users.*`). Initialized in the constructor. */
@@ -53,6 +54,7 @@ export declare class CanonAgent {
53
54
  on(event: 'contactRemoved', handler: ContactRemovedHandler): void;
54
55
  on(event: 'interrupt', handler: RuntimeSignalHandler): void;
55
56
  on(event: 'stopAndDrop', handler: RuntimeSignalHandler): void;
57
+ on(event: 'newSession', handler: RuntimeSignalHandler): void;
56
58
  /**
57
59
  * Resolve admission live for a target user (typically read off a shared
58
60
  * contact card) and route into either an immediate message or a contact
@@ -94,6 +96,7 @@ export declare class CanonAgent {
94
96
  stop(): Promise<void>;
95
97
  private hasInterruptSupport;
96
98
  private hasStopAndDropSupport;
99
+ private hasNewSessionSupport;
97
100
  private hasRuntimeSignalSupport;
98
101
  private buildRuntimeDescriptor;
99
102
  private buildRuntimeCapabilities;
@@ -1,8 +1,8 @@
1
- import { CanonClient, createRuntimeStatePublisher, FINAL_MESSAGE_HANDOFF_MS, initRTDBAuth, rtdbRead, rtdbWrite, normalizeTurnMetadata, reachOutToCanonContact, } from '@canonmsg/core';
1
+ import { CanonClient, createRuntimeStatePublisher, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, initRTDBAuth, rtdbRead, rtdbWrite, normalizeTurnMetadata, reachOutToCanonContact, } 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
+ import { materializeMessageMedia, sendMediaFileMessage, uploadMediaFile, } from './media.js';
6
6
  import { SessionManager } from './session-manager.js';
7
7
  const AGENT_RUNTIME_HEARTBEAT_MS = 30_000;
8
8
  const SDK_RUNTIME_CAPABILITIES = {
@@ -18,26 +18,6 @@ const DEFAULT_SDK_RUNTIME_DESCRIPTOR = {
18
18
  supportsInterrupt: false,
19
19
  streamingTextMode: 'snapshot',
20
20
  };
21
- const SDK_STOP_ACTION = {
22
- id: 'stop',
23
- label: 'Stop',
24
- description: 'Interrupt the current SDK agent turn.',
25
- aliases: ['stop'],
26
- category: 'turn',
27
- placements: ['composer_slash', 'command_palette'],
28
- availability: ['busy'],
29
- dispatch: { kind: 'signal', signal: 'interrupt' },
30
- };
31
- const SDK_STOP_AND_DROP_ACTION = {
32
- id: 'stop-and-clear-queue',
33
- label: 'Stop & clear queue',
34
- description: 'Interrupt the current SDK agent turn and drop queued Canon messages.',
35
- aliases: ['stop-clear', 'clear-queue'],
36
- category: 'turn',
37
- placements: ['composer_slash', 'command_palette', 'session_strip'],
38
- availability: ['busy_with_queue'],
39
- dispatch: { kind: 'signal', signal: 'stop_and_drop' },
40
- };
41
21
  function sleep(ms) {
42
22
  return new Promise((resolve) => setTimeout(resolve, ms));
43
23
  }
@@ -55,6 +35,7 @@ export class CanonAgent {
55
35
  contactRemovedHandler = null;
56
36
  interruptHandler = null;
57
37
  stopAndDropHandler = null;
38
+ newSessionHandler = null;
58
39
  /** Contact-graph operations (`agent.contacts.*`). Initialized in the constructor. */
59
40
  contacts;
60
41
  /** Block/unblock operations (`agent.users.*`). Initialized in the constructor. */
@@ -100,6 +81,7 @@ export class CanonAgent {
100
81
  }
101
82
  this.interruptHandler = options.runtimeControls?.onInterrupt ?? null;
102
83
  this.stopAndDropHandler = options.runtimeControls?.onStopAndDrop ?? null;
84
+ this.newSessionHandler = options.runtimeControls?.onNewSession ?? null;
103
85
  }
104
86
  on(event, handler) {
105
87
  if (event === 'message') {
@@ -138,6 +120,16 @@ export class CanonAgent {
138
120
  void this.publishAgentRuntime().catch(() => { });
139
121
  return;
140
122
  }
123
+ if (event === 'newSession') {
124
+ this.newSessionHandler = handler;
125
+ if (this.running) {
126
+ void this.baselineRuntimeControlSignals(this.cachedConversationIds)
127
+ .then(() => this.startRuntimeControlPolling())
128
+ .catch(() => { });
129
+ }
130
+ void this.publishAgentRuntime().catch(() => { });
131
+ return;
132
+ }
141
133
  this.contactRemovedHandler = handler;
142
134
  }
143
135
  /**
@@ -366,13 +358,17 @@ export class CanonAgent {
366
358
  hasStopAndDropSupport() {
367
359
  return Boolean(this.stopAndDropHandler);
368
360
  }
361
+ hasNewSessionSupport() {
362
+ return Boolean(this.newSessionHandler);
363
+ }
369
364
  hasRuntimeSignalSupport() {
370
- return this.hasInterruptSupport() || this.hasStopAndDropSupport();
365
+ return this.hasInterruptSupport() || this.hasStopAndDropSupport() || this.hasNewSessionSupport();
371
366
  }
372
367
  buildRuntimeDescriptor() {
373
368
  const source = this.options.runtimeDescriptor ?? DEFAULT_SDK_RUNTIME_DESCRIPTOR;
374
369
  const hasInterrupt = this.hasInterruptSupport();
375
370
  const hasStopAndDrop = this.hasStopAndDropSupport();
371
+ const hasNewSession = this.hasNewSessionSupport();
376
372
  const actions = [...(source.actions ?? [])].filter((action) => {
377
373
  if (action.dispatch.kind !== 'signal')
378
374
  return true;
@@ -380,15 +376,21 @@ export class CanonAgent {
380
376
  return hasInterrupt;
381
377
  if (action.dispatch.signal === 'stop_and_drop')
382
378
  return hasStopAndDrop;
379
+ if (action.dispatch.signal === 'new_session')
380
+ return hasNewSession;
383
381
  return false;
384
382
  });
385
383
  const hasInterruptAction = actions.some((action) => action.dispatch.kind === 'signal' && action.dispatch.signal === 'interrupt');
386
384
  const hasStopAndDropAction = actions.some((action) => action.dispatch.kind === 'signal' && action.dispatch.signal === 'stop_and_drop');
385
+ const hasNewSessionAction = actions.some((action) => action.dispatch.kind === 'signal' && action.dispatch.signal === 'new_session');
387
386
  if (hasInterrupt && !hasInterruptAction) {
388
- actions.push(SDK_STOP_ACTION);
387
+ actions.push(RUNTIME_STOP_ACTION);
389
388
  }
390
389
  if (hasStopAndDrop && this.sessionManager && !hasStopAndDropAction) {
391
- actions.push(SDK_STOP_AND_DROP_ACTION);
390
+ actions.push(RUNTIME_STOP_AND_DROP_ACTION);
391
+ }
392
+ if (hasNewSession && !hasNewSessionAction) {
393
+ actions.push(RUNTIME_NEW_SESSION_ACTION);
392
394
  }
393
395
  return {
394
396
  ...source,
@@ -476,23 +478,27 @@ export class CanonAgent {
476
478
  if (!this.agentId)
477
479
  return;
478
480
  const signal = raw.type;
479
- if (signal !== 'interrupt' && signal !== 'stop_and_drop')
481
+ if (signal !== 'interrupt' && signal !== 'stop_and_drop' && signal !== 'new_session')
480
482
  return;
481
483
  const timestamp = Number(raw.updatedAt ?? 0);
482
484
  if (timestamp <= (this.lastSeenSignal.get(conversationId) ?? 0))
483
485
  return;
484
486
  this.lastSeenSignal.set(conversationId, timestamp);
485
- const handler = signal === 'stop_and_drop'
486
- ? this.stopAndDropHandler
487
- : this.interruptHandler;
487
+ const handler = signal === 'new_session'
488
+ ? this.newSessionHandler
489
+ : signal === 'stop_and_drop'
490
+ ? this.stopAndDropHandler
491
+ : this.interruptHandler;
488
492
  if (!handler) {
489
493
  await Promise.resolve(rtdbWrite(`/control/${conversationId}/${this.agentId}/signal`, null)).catch(() => { });
490
494
  return;
491
495
  }
492
496
  const abortSignal = this.abortActiveTurns(conversationId);
493
- const droppedMessages = signal === 'stop_and_drop'
494
- ? this.sessionManager?.dropQueued(conversationId) ?? []
495
- : [];
497
+ const droppedMessages = signal === 'new_session'
498
+ ? this.sessionManager?.resetSession(conversationId) ?? []
499
+ : signal === 'stop_and_drop'
500
+ ? this.sessionManager?.dropQueued(conversationId) ?? []
501
+ : [];
496
502
  const droppedMessageIds = droppedMessages.map((message) => message.id);
497
503
  await Promise.all(droppedMessages.map((message) => {
498
504
  if (message.metadata?.inboundDisposition !== 'queued')
@@ -741,8 +747,7 @@ export class CanonAgent {
741
747
  }
742
748
  catch { }
743
749
  try {
744
- const uploaded = await uploadFile(filePath, options);
745
- const result = await this.apiClient.sendMessage(conversationId, text, {
750
+ const result = await sendMediaFileMessage(this.apiClient, conversationId, filePath, text, {
746
751
  ...(options?.replyTo ? { replyTo: options.replyTo } : {}),
747
752
  ...(options?.replyToPosition != null
748
753
  ? { replyToPosition: options.replyToPosition }
@@ -757,8 +762,9 @@ export class CanonAgent {
757
762
  turnSemantics: 'turn_complete',
758
763
  turnComplete: true,
759
764
  },
760
- contentType: uploaded.attachment.kind,
761
- attachments: [uploaded.attachment],
765
+ ...(options?.fileName ? { fileName: options.fileName } : {}),
766
+ ...(options?.mimeType ? { mimeType: options.mimeType } : {}),
767
+ ...(options?.durationMs != null ? { durationMs: options.durationMs } : {}),
762
768
  });
763
769
  await sleep(FINAL_MESSAGE_HANDOFF_MS);
764
770
  return result;
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export type { AgentContactsAPI, AgentUsersAPI } from './canon-agent.js';
3
3
  export { CanonApiError, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, } from '@canonmsg/core';
4
4
  export type { CanonContact, CanonResolveAdmissionResult, ContactAddedPayload, ContactCardPayload, ContactRemovedPayload, ContactSource, HostAdmissionActionCapabilities, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, } from '@canonmsg/core';
5
5
  export { SessionManager } from './session-manager.js';
6
- export { getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, resolveAttachmentMimeType, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
6
+ export { DEFAULT_MEDIA_CACHE_DIR, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
7
7
  export type { AnthropicImageBlock, AnthropicImageMimeType, MaterializeMediaOptions, MaterializedCanonAttachment, ReplyWithFileOptions, UploadMediaFileOptions, } from './media.js';
8
8
  export type { SessionConfig, Session } from './session-manager.js';
9
9
  export type { AgentContext, CanonContactRequest, CanonMessage, CanonConversation, CanonSelfContext, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, CreateConversationOptions, } from '@canonmsg/core';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { CanonAgent } from './canon-agent.js';
2
2
  export { CanonApiError, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, } from '@canonmsg/core';
3
3
  export { SessionManager } from './session-manager.js';
4
- export { getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, resolveAttachmentMimeType, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
4
+ export { DEFAULT_MEDIA_CACHE_DIR, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
package/dist/media.d.ts CHANGED
@@ -10,6 +10,7 @@ export interface MaterializeMediaOptions {
10
10
  export interface UploadMediaFileOptions {
11
11
  fileName?: string;
12
12
  mimeType?: string;
13
+ durationMs?: number;
13
14
  }
14
15
  export interface ReplyWithFileOptions extends Omit<SendMessageOptions, 'attachments' | 'contentType'>, UploadMediaFileOptions {
15
16
  }
@@ -39,6 +40,7 @@ export interface AnthropicImageBlock {
39
40
  data: string;
40
41
  };
41
42
  }
43
+ export declare const DEFAULT_MEDIA_CACHE_DIR: string;
42
44
  export declare function getMessageAttachments(message: Pick<CanonMessage, 'attachments'>): MediaAttachment[];
43
45
  export declare function materializeAttachment(attachment: MediaAttachment, options: MaterializeMediaOptions & {
44
46
  index?: number;
@@ -49,6 +51,9 @@ export declare function uploadMediaFile(client: CanonClient, conversationId: str
49
51
  url: string;
50
52
  attachment: MediaAttachment;
51
53
  }>;
54
+ export declare function sendMediaFileMessage(client: CanonClient, conversationId: string, filePath: string, text?: string, options?: ReplyWithFileOptions): Promise<{
55
+ messageId: string;
56
+ }>;
52
57
  /**
53
58
  * Resolve the effective MIME type of a materialized attachment, falling back
54
59
  * to filename/URL extensions when the server didn't tell us explicitly.
package/dist/media.js CHANGED
@@ -7,7 +7,7 @@ const ANTHROPIC_IMAGE_MIME_TYPES = new Set([
7
7
  'image/gif',
8
8
  'image/webp',
9
9
  ]);
10
- const DEFAULT_MEDIA_CACHE_DIR = join(CANON_DIR, 'media-cache');
10
+ export const DEFAULT_MEDIA_CACHE_DIR = join(CANON_DIR, 'media-cache');
11
11
  const EXTENSION_BY_MIME = {
12
12
  'application/json': 'json',
13
13
  'application/pdf': 'pdf',
@@ -117,7 +117,7 @@ export async function materializeAttachment(attachment, options) {
117
117
  }
118
118
  responseMimeType = response.headers.get('content-type');
119
119
  const body = Buffer.from(await response.arrayBuffer());
120
- await writeFile(path, body);
120
+ await writeFile(path, body, { mode: 0o644 });
121
121
  }
122
122
  return {
123
123
  ...attachment,
@@ -150,7 +150,33 @@ export async function uploadMediaFile(client, conversationId, filePath, options)
150
150
  const buffer = await readFile(filePath);
151
151
  const mimeType = inferUploadMimeType(filePath, options?.mimeType);
152
152
  const fileName = options?.fileName ?? basename(filePath);
153
- return client.uploadMedia(conversationId, buffer.toString('base64'), mimeType, fileName);
153
+ const uploaded = await client.uploadMedia(conversationId, buffer.toString('base64'), mimeType, fileName);
154
+ if (uploaded.attachment.kind === 'audio'
155
+ && typeof options?.durationMs === 'number'
156
+ && Number.isFinite(options.durationMs)
157
+ && options.durationMs > 0) {
158
+ return {
159
+ ...uploaded,
160
+ attachment: {
161
+ ...uploaded.attachment,
162
+ durationMs: Math.round(options.durationMs),
163
+ },
164
+ };
165
+ }
166
+ return uploaded;
167
+ }
168
+ export async function sendMediaFileMessage(client, conversationId, filePath, text = '', options) {
169
+ const { fileName, mimeType, durationMs, ...sendOptions } = options ?? {};
170
+ const uploaded = await uploadMediaFile(client, conversationId, filePath, {
171
+ ...(fileName ? { fileName } : {}),
172
+ ...(mimeType ? { mimeType } : {}),
173
+ ...(durationMs != null ? { durationMs } : {}),
174
+ });
175
+ return client.sendMessage(conversationId, text, {
176
+ ...sendOptions,
177
+ contentType: uploaded.attachment.kind,
178
+ attachments: [uploaded.attachment],
179
+ });
154
180
  }
155
181
  /**
156
182
  * Resolve the effective MIME type of a materialized attachment, falling back
@@ -65,6 +65,8 @@ export declare class SessionManager {
65
65
  getQueueDepth(conversationId: string): number;
66
66
  /** Drop queued, not-yet-running batches for a conversation. */
67
67
  dropQueued(conversationId: string): CanonMessage[];
68
+ /** Drop queued work and clear retained context for a conversation. */
69
+ resetSession(conversationId: string): CanonMessage[];
68
70
  /** Clean up all state */
69
71
  destroy(): void;
70
72
  }
@@ -213,6 +213,14 @@ export class SessionManager {
213
213
  }
214
214
  return droppedMessages;
215
215
  }
216
+ /** Drop queued work and clear retained context for a conversation. */
217
+ resetSession(conversationId) {
218
+ const droppedMessages = this.dropQueued(conversationId);
219
+ this.sessions.delete(conversationId);
220
+ this.seenMessages.delete(conversationId);
221
+ this.seededSessions.delete(conversationId);
222
+ return droppedMessages;
223
+ }
216
224
  /** Clean up all state */
217
225
  destroy() {
218
226
  if (this.sweepTimer) {
package/dist/types.d.ts CHANGED
@@ -115,6 +115,7 @@ export type RuntimeSignalHandler = (context: RuntimeSignalContext) => void | Pro
115
115
  export interface RuntimeControlHandlers {
116
116
  onInterrupt?: RuntimeSignalHandler;
117
117
  onStopAndDrop?: RuntimeSignalHandler;
118
+ onNewSession?: RuntimeSignalHandler;
118
119
  }
119
120
  export interface CanonAgentOptions {
120
121
  apiKey: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "1.1.3",
3
+ "version": "1.2.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",
@@ -28,7 +28,7 @@
28
28
  "node": ">=18.0.0"
29
29
  },
30
30
  "dependencies": {
31
- "@canonmsg/core": "^0.15.5"
31
+ "@canonmsg/core": "^0.16.0"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"