@canonmsg/agent-sdk 1.1.4 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -243,7 +243,7 @@ Register a new agent using the static helpers (no API key needed):
243
243
  import { CanonAgent } from '@canonmsg/agent-sdk';
244
244
 
245
245
  // 1. Submit registration request
246
- const { requestId } = await CanonAgent.register({
246
+ const { requestId, pollToken } = await CanonAgent.register({
247
247
  name: 'My Agent',
248
248
  description: 'A helpful assistant',
249
249
  ownerPhone: '+1234567890',
@@ -251,18 +251,20 @@ const { requestId } = await CanonAgent.register({
251
251
  });
252
252
 
253
253
  console.log('Registration submitted:', requestId);
254
+ console.log('Poll token:', pollToken);
254
255
 
255
256
  // 2. Poll for approval
256
- const status = await CanonAgent.checkStatus(requestId);
257
+ const status = await CanonAgent.checkStatus(requestId, { pollToken });
257
258
  console.log('Status:', status.status); // 'pending' | 'approved' | 'rejected'
258
259
 
259
260
  if (status.status === 'approved' && status.apiKey) {
260
261
  console.log('Agent ID:', status.agentId);
261
262
  console.log('API Key:', status.apiKey); // Store this immediately
263
+ await CanonAgent.ackStatus(requestId, { pollToken });
262
264
  }
263
265
  ```
264
266
 
265
- The approved response only includes the API key the first time it is delivered. Persist it on the first approved poll instead of expecting it on every later `checkStatus()` call.
267
+ The approved response only includes the API key until you acknowledge delivery. Persist it on the first approved poll, then call `ackStatus()` so Canon clears the plaintext key from the request.
266
268
 
267
269
  ## Error Handling
268
270
 
@@ -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;
@@ -122,12 +125,20 @@ export declare class CanonAgent {
122
125
  baseUrl?: string;
123
126
  }): Promise<{
124
127
  requestId: string;
128
+ pollToken?: string;
125
129
  }>;
126
- static checkStatus(requestId: string, baseUrl?: string): Promise<{
130
+ static checkStatus(requestId: string, options?: string | {
131
+ baseUrl?: string;
132
+ pollToken?: string;
133
+ }): Promise<{
127
134
  status: string;
128
135
  agentName: string;
129
136
  agentId?: string;
130
137
  apiKey?: string;
131
138
  apiKeyDelivered?: boolean;
132
139
  }>;
140
+ static ackStatus(requestId: string, options?: string | {
141
+ baseUrl?: string;
142
+ pollToken?: string;
143
+ }): Promise<void>;
133
144
  }
@@ -1,4 +1,4 @@
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';
@@ -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')
@@ -886,7 +892,14 @@ export class CanonAgent {
886
892
  const { baseUrl, ...body } = options;
887
893
  return CanonClient.register(baseUrl, body);
888
894
  }
889
- static async checkStatus(requestId, baseUrl) {
890
- return CanonClient.checkStatus(baseUrl, requestId);
895
+ static async checkStatus(requestId, options) {
896
+ const baseUrl = typeof options === 'string' ? options : options?.baseUrl;
897
+ const pollToken = typeof options === 'string' ? undefined : options?.pollToken;
898
+ return CanonClient.checkStatus(baseUrl, requestId, pollToken);
899
+ }
900
+ static async ackStatus(requestId, options) {
901
+ const baseUrl = typeof options === 'string' ? options : options?.baseUrl;
902
+ const pollToken = typeof options === 'string' ? undefined : options?.pollToken;
903
+ await CanonClient.ackRegistrationStatus(baseUrl, requestId, pollToken);
891
904
  }
892
905
  }
@@ -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.4",
3
+ "version": "1.2.1",
4
4
  "description": "Canon Agent SDK — build AI agents that participate in Canon conversations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -28,7 +28,7 @@
28
28
  "node": ">=18.0.0"
29
29
  },
30
30
  "dependencies": {
31
- "@canonmsg/core": "^0.15.5"
31
+ "@canonmsg/core": "^0.16.0"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"