@canonmsg/agent-sdk 7.0.1 → 7.1.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.
@@ -1,4 +1,4 @@
1
- import { type AddMemberResult, type CanonContact, type CanonConversation, type CreateConversationResult, type CanonRuntimeActivityItem, type CanonRuntimeCommandDescriptor, type CanonRuntimeFact, type CanonRuntimePrimitiveId, type ContactCardPayload, type ClearRuntimeActivityOptions, type CreateContactRequestResult } from '@canonmsg/core';
1
+ import { type AddMemberResult, type CanonContact, type CanonConversation, type CreateConversationResult, type CanonRuntimeActivityItem, type CanonRuntimeCommandDescriptor, type CanonRuntimeFact, type CanonRuntimePrimitiveId, type ContactCardPayload, type ClearRuntimeActivityOptions, type CreateContactRequestResult, type CanonVoiceSession, type CanonVoiceSessionToken, type CreateVoiceSessionOptions, type VoiceSessionEventPayload } from '@canonmsg/core';
2
2
  import type { CanonAgentConnectionOptions, CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, CreateConversationOptions, MessageHandler, MessageUpdatedHandler, ReachOutOptions, ReachOutResult, ContactRequestHandler, RuntimeSignalHandler, RuntimePrimitiveHandler } from './types.js';
3
3
  /**
4
4
  * Contact-graph operations exposed under `agent.contacts`. Wraps the REST
@@ -40,6 +40,10 @@ export declare class CanonAgent {
40
40
  private interruptHandler;
41
41
  private stopAndDropHandler;
42
42
  private newSessionHandler;
43
+ private callStartedHandler;
44
+ private callEndedHandler;
45
+ /** Whether the live SSE stream was opened with the voice family attached. */
46
+ private voiceEventsEnabled;
43
47
  private readonly primitiveHandlers;
44
48
  private primitiveFallbackHandler;
45
49
  /** Contact-graph operations (`agent.contacts.*`). Initialized in the constructor. */
@@ -96,6 +100,8 @@ export declare class CanonAgent {
96
100
  on(event: 'interrupt', handler: RuntimeSignalHandler): void;
97
101
  on(event: 'stopAndDrop', handler: RuntimeSignalHandler): void;
98
102
  on(event: 'newSession', handler: RuntimeSignalHandler): void;
103
+ on(event: 'callStarted', handler: (payload: VoiceSessionEventPayload) => void | Promise<void>): void;
104
+ on(event: 'callEnded', handler: (payload: VoiceSessionEventPayload) => void | Promise<void>): void;
99
105
  onPrimitive(primitive: CanonRuntimePrimitiveId | '*', handler: RuntimePrimitiveHandler): void;
100
106
  describeCommands(_provider?: string): ReadonlyArray<CanonRuntimeCommandDescriptor>;
101
107
  publishRuntimeFacts(conversationId: string, facts: ReadonlyArray<CanonRuntimeFact>): Promise<void>;
@@ -112,6 +118,16 @@ export declare class CanonAgent {
112
118
  private executeReachOut;
113
119
  start(): Promise<void>;
114
120
  createConversation(options: CreateConversationOptions): Promise<CreateConversationResult>;
121
+ /** Start (or rejoin) a call in a conversation and get the room token. */
122
+ startCall(options: CreateVoiceSessionOptions): Promise<CanonVoiceSessionToken>;
123
+ /** Join an active call session and get the room token. */
124
+ joinCall(conversationId: string, sessionId: string): Promise<CanonVoiceSessionToken>;
125
+ /** Decline an incoming call: stops this agent's ring only. */
126
+ declineCall(conversationId: string, sessionId: string): Promise<void>;
127
+ /** End an active call session for everyone. */
128
+ endCall(conversationId: string, sessionId: string): Promise<void>;
129
+ /** Fetch the current state of a call session. */
130
+ getCallState(conversationId: string, sessionId: string): Promise<CanonVoiceSession>;
115
131
  updateTopic(conversationId: string, topic: string): Promise<void>;
116
132
  leaveConversation(conversationId: string): Promise<void>;
117
133
  updateConversationName(conversationId: string, name: string): Promise<void>;
@@ -257,6 +257,10 @@ export class CanonAgent {
257
257
  interruptHandler = null;
258
258
  stopAndDropHandler = null;
259
259
  newSessionHandler = null;
260
+ callStartedHandler = null;
261
+ callEndedHandler = null;
262
+ /** Whether the live SSE stream was opened with the voice family attached. */
263
+ voiceEventsEnabled = false;
260
264
  primitiveHandlers = new Map();
261
265
  primitiveFallbackHandler = null;
262
266
  /** Contact-graph operations (`agent.contacts.*`). Initialized in the constructor. */
@@ -426,6 +430,20 @@ export class CanonAgent {
426
430
  });
427
431
  }
428
432
  on(event, handler) {
433
+ if (event === 'callStarted' || event === 'callEnded') {
434
+ // The voice SSE family is negotiated at connect from handler presence,
435
+ // so call handlers registered after start() would never fire.
436
+ if (this.running && !this.voiceEventsEnabled) {
437
+ console.warn('[canon-sdk] on(%s) registered after connect: the voice event family was not requested at stream start, so call events will not arrive until the agent restarts. Register call handlers before start().', event);
438
+ }
439
+ if (event === 'callStarted') {
440
+ this.callStartedHandler = handler;
441
+ }
442
+ else {
443
+ this.callEndedHandler = handler;
444
+ }
445
+ return;
446
+ }
429
447
  if (event === 'message') {
430
448
  this.handler = handler;
431
449
  return;
@@ -656,7 +674,22 @@ export class CanonAgent {
656
674
  this.startRuntimeControlPolling();
657
675
  // 4. Start delivery
658
676
  const { RealtimeManager } = await import('./realtime.js');
659
- const rtm = new RealtimeManager(this.options.apiKey, this.debouncer, agentId, this.options.streamUrl, this.apiClient);
677
+ this.voiceEventsEnabled = Boolean(this.callStartedHandler || this.callEndedHandler);
678
+ const rtm = new RealtimeManager(this.options.apiKey, this.debouncer, agentId, this.options.streamUrl, this.apiClient, { enableVoiceEvents: this.voiceEventsEnabled });
679
+ if (this.voiceEventsEnabled) {
680
+ rtm.setCallHandlers({
681
+ onCallStarted: (payload) => {
682
+ void Promise.resolve(this.callStartedHandler?.(payload)).catch((error) => {
683
+ console.error('[canon-sdk] callStarted handler failed:', error);
684
+ });
685
+ },
686
+ onCallEnded: (payload) => {
687
+ void Promise.resolve(this.callEndedHandler?.(payload)).catch((error) => {
688
+ console.error('[canon-sdk] callEnded handler failed:', error);
689
+ });
690
+ },
691
+ });
692
+ }
660
693
  rtm.setOnAgentContext((ctx) => {
661
694
  this.agentContext = ctx;
662
695
  this.ensureApprovalManager(ctx);
@@ -702,6 +735,31 @@ export class CanonAgent {
702
735
  async createConversation(options) {
703
736
  return this.apiClient.createConversation(options);
704
737
  }
738
+ // ── Calls ────────────────────────────────────────────────────────────
739
+ // These return the LiveKit room token payload; the agent brings its own
740
+ // RTC transport (e.g. @livekit/rtc-node as an optional, lazily imported
741
+ // dependency — the openclaw voice bridge is the reference implementation).
742
+ /** Start (or rejoin) a call in a conversation and get the room token. */
743
+ async startCall(options) {
744
+ return this.apiClient.createVoiceSession(options);
745
+ }
746
+ /** Join an active call session and get the room token. */
747
+ async joinCall(conversationId, sessionId) {
748
+ return this.apiClient.joinVoiceSession(conversationId, sessionId);
749
+ }
750
+ /** Decline an incoming call: stops this agent's ring only. */
751
+ async declineCall(conversationId, sessionId) {
752
+ return this.apiClient.declineVoiceSession(conversationId, sessionId);
753
+ }
754
+ /** End an active call session for everyone. */
755
+ async endCall(conversationId, sessionId) {
756
+ return this.apiClient.endVoiceSession(conversationId, sessionId);
757
+ }
758
+ /** Fetch the current state of a call session. */
759
+ async getCallState(conversationId, sessionId) {
760
+ const result = await this.apiClient.getVoiceSessionState(conversationId, sessionId);
761
+ return result.session;
762
+ }
705
763
  async updateTopic(conversationId, topic) {
706
764
  return this.apiClient.updateTopic(conversationId, topic);
707
765
  }
@@ -1,4 +1,4 @@
1
- import { type AgentContext, type CanonClient, type ContactAddedPayload, type ContactApprovedPayload, type ContactRemovedPayload, type ContactRequestPayload, type ConversationUpdatedPayload, type MessageUpdatedPayload } from '@canonmsg/core';
1
+ import { type AgentContext, type CanonClient, type ContactAddedPayload, type ContactApprovedPayload, type ContactRemovedPayload, type ContactRequestPayload, type ConversationUpdatedPayload, type MessageUpdatedPayload, type VoiceSessionEventPayload } from '@canonmsg/core';
2
2
  import { Debouncer } from './debouncer.js';
3
3
  /**
4
4
  * Wraps @canonmsg/core's CanonStream with SDK-specific features:
@@ -26,7 +26,11 @@ export declare class RealtimeManager {
26
26
  private onMessageDeleted;
27
27
  private onConnected;
28
28
  private onDisconnected;
29
- constructor(apiKey: string, debouncer: Debouncer, agentId: string, streamUrl?: string, apiClient?: CanonClient);
29
+ private onCallStarted;
30
+ private onCallEnded;
31
+ constructor(apiKey: string, debouncer: Debouncer, agentId: string, streamUrl?: string, apiClient?: CanonClient, options?: {
32
+ enableVoiceEvents?: boolean;
33
+ });
30
34
  private hasSeenInboundMessage;
31
35
  private recordSeenInboundMessage;
32
36
  private pruneRecentInboundMessageIds;
@@ -50,6 +54,10 @@ export declare class RealtimeManager {
50
54
  onConnected?: () => void;
51
55
  onDisconnected?: () => void;
52
56
  }): void;
57
+ setCallHandlers(handlers: {
58
+ onCallStarted?: (payload: VoiceSessionEventPayload) => void;
59
+ onCallEnded?: (payload: VoiceSessionEventPayload) => void;
60
+ }): void;
53
61
  start(): Promise<void>;
54
62
  stop(): void;
55
63
  }
package/dist/realtime.js CHANGED
@@ -33,7 +33,9 @@ export class RealtimeManager {
33
33
  onMessageDeleted = null;
34
34
  onConnected = null;
35
35
  onDisconnected = null;
36
- constructor(apiKey, debouncer, agentId, streamUrl, apiClient) {
36
+ onCallStarted = null;
37
+ onCallEnded = null;
38
+ constructor(apiKey, debouncer, agentId, streamUrl, apiClient, options) {
37
39
  this.debouncer = debouncer;
38
40
  this.agentId = agentId;
39
41
  this.stream = new CanonStream({
@@ -41,6 +43,21 @@ export class RealtimeManager {
41
43
  agentId,
42
44
  streamUrl,
43
45
  handler: {
46
+ // The voice family is requested from handler PRESENCE at stream
47
+ // construction, so these delegating closures exist only when the
48
+ // agent registered call handlers before connect — otherwise every
49
+ // SDK agent would cost the stream service a per-conversation
50
+ // voiceSessions listener for nothing.
51
+ ...(options?.enableVoiceEvents
52
+ ? {
53
+ onVoiceSessionStarted: (payload) => {
54
+ this.onCallStarted?.(payload);
55
+ },
56
+ onVoiceSessionEnded: (payload) => {
57
+ this.onCallEnded?.(payload);
58
+ },
59
+ }
60
+ : {}),
44
61
  onMessage: (payload) => {
45
62
  // Cross-flush id dedupe: SSE replay overlap must never double-fire
46
63
  // a turn for the same message.
@@ -183,6 +200,10 @@ export class RealtimeManager {
183
200
  this.onConnected = handlers.onConnected ?? null;
184
201
  this.onDisconnected = handlers.onDisconnected ?? null;
185
202
  }
203
+ setCallHandlers(handlers) {
204
+ this.onCallStarted = handlers.onCallStarted ?? null;
205
+ this.onCallEnded = handlers.onCallEnded ?? null;
206
+ }
186
207
  async start() {
187
208
  this.running = true;
188
209
  await this.stream.start();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "7.0.1",
3
+ "version": "7.1.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": "^7.0.2"
31
+ "@canonmsg/core": "^8.0.0"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"