@canonmsg/agent-sdk 1.2.1 → 1.3.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.
@@ -46,6 +46,8 @@ export declare class CanonAgent {
46
46
  private runtimeControlPollTimer;
47
47
  private readonly lastSeenSignal;
48
48
  private readonly activeAbortControllers;
49
+ private readonly conversationMemberIds;
50
+ private readonly pendingMembershipChanges;
49
51
  constructor(options: CanonAgentOptions);
50
52
  on(event: 'message', handler: MessageHandler): void;
51
53
  on(event: 'contactRequest', handler: ContactRequestHandler): void;
@@ -105,6 +107,9 @@ export declare class CanonAgent {
105
107
  private stopRuntimeHeartbeat;
106
108
  private clearAgentRuntime;
107
109
  private rememberConversationId;
110
+ private rememberConversationMembers;
111
+ private handleConversationUpdated;
112
+ private buildGroupContext;
108
113
  private baselineRuntimeControlSignals;
109
114
  private startRuntimeControlPolling;
110
115
  private stopRuntimeControlPolling;
@@ -1,4 +1,4 @@
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';
1
+ import { CanonClient, buildCanonGroupContext, createRuntimeStatePublisher, diffCanonMemberIds, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, initRTDBAuth, rtdbRead, rtdbWrite, normalizeTurnMetadata, reachOutToCanonContact, resolveMessageActiveSelfContextId, selectActiveSelfContexts, } from '@canonmsg/core';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { AuthManager } from './auth.js';
4
4
  import { Debouncer } from './debouncer.js';
@@ -49,6 +49,8 @@ export class CanonAgent {
49
49
  runtimeControlPollTimer = null;
50
50
  lastSeenSignal = new Map();
51
51
  activeAbortControllers = new Map();
52
+ conversationMemberIds = new Map();
53
+ pendingMembershipChanges = new Map();
52
54
  constructor(options) {
53
55
  this.options = {
54
56
  baseUrl: 'https://api-6m6mlelskq-uc.a.run.app',
@@ -178,6 +180,7 @@ export class CanonAgent {
178
180
  status: 'messaged',
179
181
  conversationId: result.conversationId,
180
182
  messageId: result.messageId,
183
+ selfContextId: result.selfContextId,
181
184
  }
182
185
  : result;
183
186
  }
@@ -207,6 +210,7 @@ export class CanonAgent {
207
210
  try {
208
211
  conversations = await this.apiClient.getConversations();
209
212
  this.cachedConversationIds = conversations.map((c) => c.id);
213
+ this.rememberConversationMembers(conversations);
210
214
  }
211
215
  catch {
212
216
  // Non-fatal — delivery mode will fall back to default
@@ -265,6 +269,9 @@ export class CanonAgent {
265
269
  void this.handleContactGraphEvent(this.contactRemovedHandler, payload);
266
270
  },
267
271
  });
272
+ rtm.setConversationUpdatedHandler((payload) => {
273
+ this.handleConversationUpdated(payload);
274
+ });
268
275
  rtm.setConnectionHandlers({
269
276
  onConnected: () => this.startRuntimeHeartbeat(),
270
277
  onDisconnected: () => this.stopRuntimeHeartbeat(),
@@ -437,6 +444,41 @@ export class CanonAgent {
437
444
  return;
438
445
  this.cachedConversationIds.push(conversationId);
439
446
  }
447
+ rememberConversationMembers(conversations) {
448
+ for (const conversation of conversations) {
449
+ this.conversationMemberIds.set(conversation.id, [...(conversation.memberIds ?? [])]);
450
+ }
451
+ }
452
+ handleConversationUpdated(payload) {
453
+ const rawMemberIds = payload.changes.memberIds;
454
+ if (!Array.isArray(rawMemberIds))
455
+ return;
456
+ const memberIds = rawMemberIds.filter((id) => typeof id === 'string');
457
+ const hadPreviousMemberIds = this.conversationMemberIds.has(payload.conversationId);
458
+ const previousMemberIds = this.conversationMemberIds.get(payload.conversationId) ?? [];
459
+ const membershipChange = payload.membershipChange
460
+ ?? (hadPreviousMemberIds ? diffCanonMemberIds(previousMemberIds, memberIds) : null);
461
+ this.conversationMemberIds.set(payload.conversationId, memberIds);
462
+ if (membershipChange) {
463
+ this.pendingMembershipChanges.set(payload.conversationId, membershipChange);
464
+ }
465
+ if (this.agentId && !memberIds.includes(this.agentId)) {
466
+ this.cachedConversationIds = this.cachedConversationIds.filter((id) => id !== payload.conversationId);
467
+ }
468
+ else {
469
+ this.rememberConversationId(payload.conversationId);
470
+ }
471
+ }
472
+ buildGroupContext(input) {
473
+ return buildCanonGroupContext({
474
+ conversation: input.conversation,
475
+ messages: [...input.history, ...input.messages],
476
+ agentId: input.agent.agentId,
477
+ ownerId: input.agent.ownerId,
478
+ ownerName: input.agent.ownerName,
479
+ membershipChange: input.membershipChange,
480
+ });
481
+ }
440
482
  async baselineRuntimeControlSignals(conversationIds) {
441
483
  if (!this.agentId || !this.hasRuntimeSignalSupport())
442
484
  return;
@@ -655,6 +697,7 @@ export class CanonAgent {
655
697
  }
656
698
  // Get conversation info
657
699
  const conversations = await this.apiClient.getConversations();
700
+ this.rememberConversationMembers(conversations);
658
701
  const conversation = conversations.find((c) => c.id === conversationId);
659
702
  if (!conversation)
660
703
  return;
@@ -664,13 +707,11 @@ export class CanonAgent {
664
707
  await this.apiClient.setTyping(conversationId, true, 'typing');
665
708
  }
666
709
  catch { }
710
+ const sendOptions = withActiveSelfContext(options);
667
711
  const result = await this.apiClient.sendMessage(conversationId, text, {
668
- ...(options ?? {}),
669
- ...(options?.selfContextId === undefined && activeSelfContextId
670
- ? { selfContextId: activeSelfContextId }
671
- : {}),
712
+ ...sendOptions,
672
713
  metadata: {
673
- ...(options?.metadata ?? {}),
714
+ ...(sendOptions.metadata ?? {}),
674
715
  turnId,
675
716
  turnSemantics: 'turn_complete',
676
717
  turnComplete: true,
@@ -689,10 +730,11 @@ export class CanonAgent {
689
730
  return { turnId, durable: false, messageId: null };
690
731
  }
691
732
  const { durable: _durable, ...sendOptions } = options;
733
+ const sendOptionsWithContext = withActiveSelfContext(sendOptions);
692
734
  const result = await this.apiClient.sendMessage(conversationId, text, {
693
- ...sendOptions,
735
+ ...sendOptionsWithContext,
694
736
  metadata: {
695
- ...(sendOptions.metadata ?? {}),
737
+ ...(sendOptionsWithContext.metadata ?? {}),
696
738
  turnId,
697
739
  turnSemantics: 'progress',
698
740
  turnComplete: false,
@@ -707,8 +749,19 @@ export class CanonAgent {
707
749
  m.isOwner = m.senderId === ownerId;
708
750
  }
709
751
  }
710
- const selfContexts = page.selfContexts ?? [];
711
- const activeSelfContextId = selfContexts[0]?.id;
752
+ const latestMessage = hydratedMessages[hydratedMessages.length - 1] ?? null;
753
+ const resolvedActiveSelfContextId = resolveMessageActiveSelfContextId({
754
+ messageId: latestMessage?.id,
755
+ activeSelfContextIdByMessageId: page.activeSelfContextIdByMessageId,
756
+ });
757
+ const selfContexts = selectActiveSelfContexts(page.selfContexts, resolvedActiveSelfContextId);
758
+ const activeSelfContextId = selfContexts.length > 0 ? resolvedActiveSelfContextId : null;
759
+ const withActiveSelfContext = (options) => {
760
+ const base = { ...(options ?? {}) };
761
+ if (base.selfContextId !== undefined)
762
+ return base;
763
+ return activeSelfContextId ? { ...base, selfContextId: activeSelfContextId } : base;
764
+ };
712
765
  // Build agent context (fallback to minimal if not yet received)
713
766
  const agent = this.agentContext ?? {
714
767
  agentId: this.agentId,
@@ -718,6 +771,15 @@ export class CanonAgent {
718
771
  inboundPolicy: 'approval-required',
719
772
  groupJoinPolicy: 'approval-required',
720
773
  };
774
+ const membershipChange = this.pendingMembershipChanges.get(conversationId) ?? null;
775
+ this.pendingMembershipChanges.delete(conversationId);
776
+ const groupContext = this.buildGroupContext({
777
+ conversation,
778
+ history,
779
+ messages: hydratedMessages,
780
+ agent,
781
+ membershipChange,
782
+ });
721
783
  // Build context methods bound to this conversation
722
784
  const deleteMessage = (messageId) => this.apiClient.deleteMessage(conversationId, messageId);
723
785
  const markAsRead = () => this.apiClient.markAsRead(conversationId);
@@ -740,6 +802,10 @@ export class CanonAgent {
740
802
  },
741
803
  },
742
804
  });
805
+ const reachOut = (card, options) => this.reachOut(card, {
806
+ ...(options ?? {}),
807
+ sourceConversationId: conversationId,
808
+ });
743
809
  const uploadFile = (filePath, options) => uploadMediaFile(this.apiClient, conversationId, filePath, options);
744
810
  const replyWithFile = async (filePath, text = '', options) => {
745
811
  try {
@@ -753,9 +819,9 @@ export class CanonAgent {
753
819
  ? { replyToPosition: options.replyToPosition }
754
820
  : {}),
755
821
  ...(options?.mentions ? { mentions: options.mentions } : {}),
756
- ...(options?.selfContextId === undefined && activeSelfContextId
757
- ? { selfContextId: activeSelfContextId }
758
- : {}),
822
+ ...withActiveSelfContext(options?.selfContextId !== undefined
823
+ ? { selfContextId: options.selfContextId }
824
+ : undefined),
759
825
  metadata: {
760
826
  ...(options?.metadata ?? {}),
761
827
  turnId,
@@ -782,6 +848,7 @@ export class CanonAgent {
782
848
  history,
783
849
  conversationId,
784
850
  conversation,
851
+ ...(groupContext ? { groupContext } : {}),
785
852
  replyFinal,
786
853
  replyProgress,
787
854
  deleteMessage,
@@ -791,7 +858,9 @@ export class CanonAgent {
791
858
  addMember,
792
859
  removeMember,
793
860
  sendContextualMessage,
861
+ reachOut,
794
862
  agent,
863
+ activeSelfContextId,
795
864
  selfContexts,
796
865
  abortSignal: abortController.signal,
797
866
  media: {
package/dist/index.d.ts CHANGED
@@ -6,5 +6,5 @@ export { SessionManager } from './session-manager.js';
6
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
- export type { AgentContext, CanonContactRequest, CanonMessage, CanonConversation, CanonSelfContext, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, CreateConversationOptions, } from '@canonmsg/core';
9
+ export type { AgentContext, CanonGroupContext, CanonKnownRecentParticipant, CanonMembershipChange, CanonContactRequest, CanonMessage, CanonConversation, CanonSelfContext, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, CreateConversationOptions, } from '@canonmsg/core';
10
10
  export type { CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, ContactRequestHandler, MessageHandler, MessageHandlerContext, ProgressMessageOptions, ProgressMessageResult, ReachOutOptions, ReachOutResult, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
@@ -1,4 +1,4 @@
1
- import { type AgentContext, type CanonClient, type ContactAddedPayload, type ContactApprovedPayload, type ContactRemovedPayload, type ContactRequestPayload } from '@canonmsg/core';
1
+ import { type AgentContext, type CanonClient, type ContactAddedPayload, type ContactApprovedPayload, type ContactRemovedPayload, type ContactRequestPayload, type ConversationUpdatedPayload } from '@canonmsg/core';
2
2
  import { Debouncer } from './debouncer.js';
3
3
  /**
4
4
  * Wraps @canonmsg/core's CanonStream with SDK-specific features:
@@ -15,6 +15,7 @@ export declare class RealtimeManager {
15
15
  private onContactApproved;
16
16
  private onContactAdded;
17
17
  private onContactRemoved;
18
+ private onConversationUpdated;
18
19
  private onConnected;
19
20
  private onDisconnected;
20
21
  constructor(apiKey: string, debouncer: Debouncer, agentId: string, streamUrl?: string, apiClient?: CanonClient);
@@ -27,6 +28,7 @@ export declare class RealtimeManager {
27
28
  onContactAdded?: (payload: ContactAddedPayload) => void;
28
29
  onContactRemoved?: (payload: ContactRemovedPayload) => void;
29
30
  }): void;
31
+ setConversationUpdatedHandler(cb: (payload: ConversationUpdatedPayload) => void): void;
30
32
  setConnectionHandlers(handlers: {
31
33
  onConnected?: () => void;
32
34
  onDisconnected?: () => void;
package/dist/realtime.js CHANGED
@@ -14,6 +14,7 @@ export class RealtimeManager {
14
14
  onContactApproved = null;
15
15
  onContactAdded = null;
16
16
  onContactRemoved = null;
17
+ onConversationUpdated = null;
17
18
  onConnected = null;
18
19
  onDisconnected = null;
19
20
  constructor(apiKey, debouncer, agentId, streamUrl, apiClient) {
@@ -30,6 +31,7 @@ export class RealtimeManager {
30
31
  const message = {
31
32
  id: m.id,
32
33
  senderId: m.senderId,
34
+ ...(m.senderName ? { senderName: m.senderName } : {}),
33
35
  senderType: m.senderType ?? 'human',
34
36
  isOwner: m.isOwner ?? false,
35
37
  contentType: m.contentType ?? 'text',
@@ -67,6 +69,9 @@ export class RealtimeManager {
67
69
  onContactRemoved: (payload) => {
68
70
  this.onContactRemoved?.(payload);
69
71
  },
72
+ onConversationUpdated: (payload) => {
73
+ this.onConversationUpdated?.(payload);
74
+ },
70
75
  onConnected: () => {
71
76
  // Reset backoff is handled internally by CanonStream
72
77
  this.onConnected?.();
@@ -91,6 +96,9 @@ export class RealtimeManager {
91
96
  this.onContactAdded = handlers.onContactAdded ?? null;
92
97
  this.onContactRemoved = handlers.onContactRemoved ?? null;
93
98
  }
99
+ setConversationUpdatedHandler(cb) {
100
+ this.onConversationUpdated = cb;
101
+ }
94
102
  setConnectionHandlers(handlers) {
95
103
  this.onConnected = handlers.onConnected ?? null;
96
104
  this.onDisconnected = handlers.onDisconnected ?? null;
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export type { AddMemberResult, AgentClientType, CanonRuntimeDescriptor, CanonMessage, CanonConversation, CanonContact, CanonContactRequest, CanonResolveAdmissionResult, ContactAddedPayload, ContactRemovedPayload, ContactSource, AgentContext, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonSelfContext, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, SessionConfig, CreateConversationOptions, TurnLifecycleState, } from '@canonmsg/core';
2
- import type { AddMemberResult, CanonMessage, CanonConversation, CanonRuntimeActionDispatch, SendMessageOptions, SendContextualSelfContextInput, SessionConfig } from '@canonmsg/core';
1
+ export type { AddMemberResult, AgentClientType, CanonGroupContext, CanonRuntimeDescriptor, CanonMessage, CanonConversation, CanonContact, CanonContactRequest, CanonResolveAdmissionResult, ContactAddedPayload, ContactRemovedPayload, ContactSource, AgentContext, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonSelfContext, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, SessionConfig, CreateConversationOptions, TurnLifecycleState, } from '@canonmsg/core';
2
+ import type { AddMemberResult, CanonGroupContext, CanonMessage, CanonConversation, ContactCardPayload, CanonRuntimeActionDispatch, SendMessageOptions, SendContextualSelfContextInput, SessionConfig } from '@canonmsg/core';
3
3
  import type { MaterializeMediaOptions, MaterializedCanonAttachment, ReplyWithFileOptions, UploadMediaFileOptions } from './media.js';
4
4
  export interface ProgressMessageOptions extends SendMessageOptions {
5
5
  /**
@@ -39,6 +39,8 @@ export interface MessageHandlerContext {
39
39
  history: CanonMessage[];
40
40
  conversationId: string;
41
41
  conversation: CanonConversation;
42
+ /** Lightweight group awareness, present for group conversations. */
43
+ groupContext?: CanonGroupContext;
42
44
  replyFinal: (text: string, options?: SendMessageOptions) => Promise<{
43
45
  messageId: string;
44
46
  }>;
@@ -67,8 +69,12 @@ export interface MessageHandlerContext {
67
69
  } | {
68
70
  targetUserId: string;
69
71
  }, text: string, options: Omit<import('@canonmsg/core').SendContextualMessageOptions, 'sourceConversationId' | 'targetConversationId' | 'targetUserId' | 'text'>) => Promise<import('@canonmsg/core').SendContextualMessageResult>;
72
+ /** Reach a contact card from this conversation; contextual reach-outs use this conversation as source. */
73
+ reachOut: (card: ContactCardPayload, options?: Omit<ReachOutOptions, 'sourceConversationId'>) => Promise<ReachOutResult>;
70
74
  /** Trusted agent identity & access context */
71
75
  agent: import('@canonmsg/core').AgentContext;
76
+ /** Active private self-context to continue for this turn, if Canon supplied one. */
77
+ activeSelfContextId: string | null;
72
78
  /** Canon-provided private context explaining this agent's cross-session actions. */
73
79
  selfContexts?: import('@canonmsg/core').CanonSelfContext[];
74
80
  /** Canon-managed local media access for the current conversation. */
@@ -156,12 +162,15 @@ export type ReachOutResult = {
156
162
  status: 'messaged';
157
163
  conversationId: string;
158
164
  messageId?: string;
165
+ selfContextId?: string;
159
166
  } | {
160
167
  status: 'requested';
161
168
  requestId: string | null;
169
+ deferredIntentId?: string | null;
162
170
  } | {
163
171
  status: 'pending';
164
172
  requestId: string | null;
173
+ deferredIntentId?: string | null;
165
174
  } | {
166
175
  status: 'setup_required';
167
176
  reason: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "1.2.1",
3
+ "version": "1.3.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.16.0"
31
+ "@canonmsg/core": "^0.17.0"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"