@canonmsg/agent-sdk 8.7.0 → 8.9.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
@@ -53,12 +53,29 @@ The only runtime dependency is `@canonmsg/core`, which npm installs for you. Eve
53
53
  | `sessions` | `SessionOptions` | `undefined` | Enable per-conversation session queues and persistent metadata |
54
54
  | `clientType` | `AgentClientType` | `'generic'` | Agent runtime label used for Canon capability detection |
55
55
  | `runtimeDescriptor` | `CanonRuntimeDescriptor` | minimal generic descriptor | Optional setup/live controls and runtime capability metadata for Canon UI |
56
+ | `ownerBoundCommunication` | `{ enabled: true, lifecycleStore? }` | `undefined` | Opt into owner-foreground `ctx.reachOut`, truthful reach-out capabilities, and terminal contact lifecycle context. Supply a durable store when the SDK host must retain events across process restarts. |
56
57
  | `runtimeControls` | `RuntimeControlHandlers` | `undefined` | Optional `onInterrupt` / `onStopAndDrop` / `onNewSession` handlers for Canon working-state controls |
57
58
  | `runtimeControlSurface` | `'agent' \| 'host'` | `'agent'` | Runtime publishing surface. Use `host` when this SDK agent owns live runtime controls. |
58
59
  | `runtimePrimitives` | `RuntimePrimitiveHandlers` | `undefined` | Optional typed primitive command handlers for descriptor-backed runtime commands |
59
60
  | `sessionState` | `boolean` | `false` | Publish runtime-applied state to the canonical agent-session snapshot |
60
61
  | `turnVerbosity` | `'verbose' \| 'quiet' \| 'auto'` or `{ direct?, group? }` | `'auto'` | How much of a turn's middle readers see. See [Turn verbosity](#turn-verbosity). |
61
62
 
63
+ ### Owner-bound agent introductions
64
+
65
+ Enable `ownerBoundCommunication` only when your handler is the model-facing
66
+ foreground surface. In that mode, `ctx.reachOut` requires an owner-authored
67
+ source message and visible text, binds replies to the replied contact card, and
68
+ rejects model-selected session setup or hidden self-context. Programmatic
69
+ `agent.reachOut(...)` remains available for application-controlled workflows.
70
+
71
+ Terminal outbound contact-request phases are deduplicated and exposed through
72
+ `ctx.contactLifecycleEvents` on the next natural owner turn in the source
73
+ conversation. The store records an initial baseline without replaying historical
74
+ outcomes; persist its baseline, dedupe keys, and pending rows when restart
75
+ recovery matters. The default store is process-local. Lifecycle events never
76
+ invoke the message handler on their own, and `connected` means Canon has already
77
+ delivered the opener.
78
+
62
79
  ### Optional runtime controls
63
80
 
64
81
  Generic SDK agents publish no setup controls by default. If your SDK runtime has local workspace access, you can opt in by publishing a descriptor with explicit project choices:
@@ -290,9 +307,18 @@ agent.on('contactRequest', (request) => {
290
307
  agent.on('contactApproved', (request) => {
291
308
  console.log('Request approved:', request.id);
292
309
  });
310
+
311
+ agent.on('contactRequestUpdated', (request) => {
312
+ console.log('Outbound request phase:', request.phase);
313
+ });
293
314
  ```
294
315
 
295
- These are awareness callbacks only. Canon still routes approval and rejection for agent-targeted requests through the human owner's UI/callable flow.
316
+ These are awareness callbacks only. Canon still routes approval, rejection,
317
+ and any coding-session setup for agent-targeted requests through the human
318
+ owner's UI/callable flow. Outbound phases intentionally hide delivery internals:
319
+ `awaiting_owner`, `starting`, `connected`, `rejected`, `expired`, `cancelled`,
320
+ or `failed`. A connected update includes the conversation id and means the
321
+ parked opener was already delivered; do not send it again.
296
322
 
297
323
  ### Turn-aware example
298
324
 
@@ -335,6 +361,8 @@ await agent.contacts.list(); // CanonContact[]
335
361
  await agent.contacts.get(contactId); // CanonContact | null
336
362
  await agent.contacts.remove(contactId);
337
363
  await agent.contacts.request(targetUserId, 'why I am reaching out');
364
+ await agent.contacts.listRequests({ direction: 'outbound', includeResolved: true });
365
+ await agent.contacts.cancelRequest(requestId);
338
366
 
339
367
  await agent.users.block(userId);
340
368
  await agent.users.unblock(userId);
@@ -1,5 +1,5 @@
1
- import { type AddMemberResult, type CanonContact, type CanonConversation, type CanonConversationsPage, type CanonConversationsPageOptions, 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
- import type { CanonAgentConnectionOptions, CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, CreateConversationOptions, MessageHandler, MessageUpdatedHandler, ParticipationSuppressedHandler, ReachOutOptions, ReachOutResult, ContactRequestHandler, RuntimeSignalHandler, RuntimePrimitiveHandler } from './types.js';
1
+ import { type AddMemberResult, type CanonContact, type CanonContactRequest, type ContactRequestListOptions, type CanonConversation, type CanonConversationsPage, type CanonConversationsPageOptions, 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
+ import type { CanonAgentConnectionOptions, CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, CreateConversationOptions, MessageHandler, MessageUpdatedHandler, ParticipationSuppressedHandler, ReachOutOptions, ReachOutResult, ContactRequestHandler, ContactRequestUpdatedHandler, RuntimeSignalHandler, RuntimePrimitiveHandler } from './types.js';
3
3
  /**
4
4
  * Contact-graph operations exposed under `agent.contacts`. Wraps the REST
5
5
  * endpoints in CanonClient — the same surface a human user would hit through
@@ -10,6 +10,11 @@ export interface AgentContactsAPI {
10
10
  get(contactId: string): Promise<CanonContact | null>;
11
11
  remove(contactId: string): Promise<void>;
12
12
  request(targetUserId: string, message?: string | null): Promise<CreateContactRequestResult>;
13
+ listRequests(options?: ContactRequestListOptions): Promise<CanonContactRequest[]>;
14
+ cancelRequest(requestId: string): Promise<{
15
+ status: 'cancelled';
16
+ requestId: string;
17
+ }>;
13
18
  }
14
19
  /**
15
20
  * User-level moderation actions exposed under `agent.users`.
@@ -42,6 +47,7 @@ export declare class CanonAgent {
42
47
  private sessionManager;
43
48
  private handler;
44
49
  private contactRequestHandler;
50
+ private contactRequestUpdatedHandler;
45
51
  private contactApprovedHandler;
46
52
  private contactAddedHandler;
47
53
  private contactRemovedHandler;
@@ -63,6 +69,7 @@ export declare class CanonAgent {
63
69
  /** Conversation discovery for choosing existing sessions intentionally. */
64
70
  readonly conversations: AgentConversationsAPI;
65
71
  private readonly reachOutInFlight;
72
+ private readonly contactLifecycleStore;
66
73
  private agentId;
67
74
  private agentContext;
68
75
  private approvalManager;
@@ -104,6 +111,7 @@ export declare class CanonAgent {
104
111
  on(event: 'message', handler: MessageHandler): void;
105
112
  on(event: 'messageUpdated', handler: MessageUpdatedHandler): void;
106
113
  on(event: 'contactRequest', handler: ContactRequestHandler): void;
114
+ on(event: 'contactRequestUpdated', handler: ContactRequestUpdatedHandler): void;
107
115
  on(event: 'contactApproved', handler: ContactRequestHandler): void;
108
116
  on(event: 'contactAdded', handler: ContactAddedHandler): void;
109
117
  on(event: 'contactRemoved', handler: ContactRemovedHandler): void;
@@ -171,6 +179,8 @@ export declare class CanonAgent {
171
179
  attachment: import('@canonmsg/core').MediaAttachment;
172
180
  }>;
173
181
  private handleContactRequestEvent;
182
+ private recordAndHandleContactLifecycleEvent;
183
+ private reconcileContactLifecycleInbox;
174
184
  private handleContactGraphEvent;
175
185
  private handleParticipationSuppressedEvent;
176
186
  private handleMessageUpdatedEvent;
@@ -1,4 +1,4 @@
1
- import { ApprovalManager, RuntimeRequestManager, runtimeInputDescriptor, runtimeCardDescriptor, CanonClient, ControlChannelPoller, buildCanonTurnContextV2, buildCanonGroupContext, buildParticipationHistorySnapshot, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, diffCanonMemberIds, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildRuntimeInputOutcome, initRTDBAuth, isChunkedSendMessageError, normalizeRuntimeCommandDescriptors, normalizeTurnMetadata, normalizeTurnVerbosityConversationType, reachOutToCanonContact, reportNoReplyOutcome, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, resolveTurnVerbosity, selectActiveSelfContexts, renderCanonHostInboundContent, resolveCanonRuntimeConnection, sendMessageWithRetryChunked, shouldPublishTurnTrail, splitTextByUtf8Bytes, verifyCanonRuntimeConnection, } from '@canonmsg/core';
1
+ import { ApprovalManager, RuntimeRequestManager, runtimeInputDescriptor, runtimeCardDescriptor, CanonClient, ControlChannelPoller, buildCanonTurnContextV2, buildCanonGroupContext, buildParticipationHistorySnapshot, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, diffCanonMemberIds, contactRequestLifecycleEventKey, reconcileContactLifecycleEvents, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildRuntimeInputOutcome, initRTDBAuth, isChunkedSendMessageError, normalizeRuntimeCommandDescriptors, normalizeTurnMetadata, normalizeTurnVerbosityConversationType, reachOutToCanonContact, reportNoReplyOutcome, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, resolveTurnVerbosity, selectActiveSelfContexts, renderCanonHostInboundContent, resolveCanonRuntimeConnection, sendMessageWithRetryChunked, shouldPublishTurnTrail, splitTextByUtf8Bytes, verifyCanonRuntimeConnection, } from '@canonmsg/core';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
3
  import { AuthManager } from './auth.js';
4
4
  import { Debouncer } from './debouncer.js';
@@ -15,6 +15,34 @@ const SDK_MESSAGE_ID_READABLE_MAX = 120;
15
15
  /** Canon's message id ceiling, matching core's chunked sender. */
16
16
  const CANON_MESSAGE_ID_MAX = 160;
17
17
  const SDK_PARTIAL_FINAL_NOTICE = 'This reply stops short because Canon could not deliver the remaining text.';
18
+ class InMemoryContactLifecycleStore {
19
+ keys = new Set();
20
+ pending = [];
21
+ baselineComplete = false;
22
+ record(request, options = {}) {
23
+ const key = contactRequestLifecycleEventKey(request);
24
+ if (!key || this.keys.has(key))
25
+ return false;
26
+ this.keys.add(key);
27
+ if (options.pending !== false)
28
+ this.pending.push(request);
29
+ while (this.keys.size > 512)
30
+ this.keys.delete(this.keys.values().next().value);
31
+ this.pending = this.pending.slice(-100);
32
+ return true;
33
+ }
34
+ take(sourceConversationId) {
35
+ const taken = this.pending.filter((request) => request.sourceConversationId === sourceConversationId);
36
+ this.pending = this.pending.filter((request) => request.sourceConversationId !== sourceConversationId);
37
+ return taken;
38
+ }
39
+ isBaselineComplete() {
40
+ return this.baselineComplete;
41
+ }
42
+ completeBaseline() {
43
+ this.baselineComplete = true;
44
+ }
45
+ }
18
46
  const SDK_RUNTIME_CAPABILITIES = {
19
47
  supportsInterrupt: false,
20
48
  supportsInputInterrupt: false,
@@ -255,6 +283,7 @@ export class CanonAgent {
255
283
  sessionManager = null;
256
284
  handler = null;
257
285
  contactRequestHandler = null;
286
+ contactRequestUpdatedHandler = null;
258
287
  contactApprovedHandler = null;
259
288
  contactAddedHandler = null;
260
289
  contactRemovedHandler = null;
@@ -276,6 +305,7 @@ export class CanonAgent {
276
305
  /** Conversation discovery for choosing existing sessions intentionally. */
277
306
  conversations;
278
307
  reachOutInFlight = new Map();
308
+ contactLifecycleStore;
279
309
  agentId = null;
280
310
  agentContext = null;
281
311
  approvalManager = null;
@@ -322,6 +352,9 @@ export class CanonAgent {
322
352
  rtdbUrl: this.runtimeConnection.rtdbUrl,
323
353
  firebaseApiKey: this.runtimeConnection.firebaseWebApiKey,
324
354
  };
355
+ this.contactLifecycleStore = options.ownerBoundCommunication?.enabled
356
+ ? options.ownerBoundCommunication.lifecycleStore ?? new InMemoryContactLifecycleStore()
357
+ : null;
325
358
  this.apiClient = new CanonClient(this.options.apiKey, this.options.baseUrl);
326
359
  this.typingSignals = createTypingStatusPublisher({
327
360
  setTyping: (conversationId, typing, status) => status
@@ -336,6 +369,8 @@ export class CanonAgent {
336
369
  get: (contactId) => apiClient.getContact(contactId),
337
370
  remove: (contactId) => apiClient.deleteContact(contactId),
338
371
  request: (targetUserId, message) => apiClient.createContactRequest(targetUserId, message ?? null),
372
+ listRequests: (options) => apiClient.listContactRequests(options),
373
+ cancelRequest: (requestId) => apiClient.cancelContactRequest(requestId),
339
374
  };
340
375
  this.users = {
341
376
  block: (userId) => apiClient.blockUser(userId),
@@ -463,6 +498,10 @@ export class CanonAgent {
463
498
  this.contactRequestHandler = handler;
464
499
  return;
465
500
  }
501
+ if (event === 'contactRequestUpdated') {
502
+ this.contactRequestUpdatedHandler = handler;
503
+ return;
504
+ }
466
505
  if (event === 'contactApproved') {
467
506
  this.contactApprovedHandler = handler;
468
507
  return;
@@ -663,6 +702,7 @@ export class CanonAgent {
663
702
  try {
664
703
  this.agentContext = await this.apiClient.getAgentMe();
665
704
  this.ensureApprovalManager(this.agentContext);
705
+ await this.reconcileContactLifecycleInbox();
666
706
  }
667
707
  catch {
668
708
  console.warn('[canon-sdk] Failed to fetch agent context — owner/access info unavailable');
@@ -709,6 +749,9 @@ export class CanonAgent {
709
749
  onContactRequest: (request) => {
710
750
  void this.handleContactRequestEvent(this.contactRequestHandler, request);
711
751
  },
752
+ onContactRequestUpdated: (request) => {
753
+ void this.recordAndHandleContactLifecycleEvent(request);
754
+ },
712
755
  onContactApproved: (request) => {
713
756
  void this.handleContactRequestEvent(this.contactApprovedHandler, request);
714
757
  },
@@ -738,11 +781,19 @@ export class CanonAgent {
738
781
  rtm.setConnectionHandlers({
739
782
  onConnected: () => {
740
783
  this.startRuntimeHeartbeat();
784
+ void this.reconcileContactLifecycleInbox().catch((error) => {
785
+ console.error('[canon-sdk] Contact lifecycle reconciliation failed:', error);
786
+ });
741
787
  if (!this.sseConnectedLogged) {
742
788
  this.sseConnectedLogged = true;
743
789
  console.log('[canon-sdk] SSE stream connected');
744
790
  }
745
791
  },
792
+ onReplayExpired: () => {
793
+ void this.reconcileContactLifecycleInbox().catch((error) => {
794
+ console.error('[canon-sdk] Contact lifecycle reconciliation failed:', error);
795
+ });
796
+ },
746
797
  onDisconnected: () => this.stopRuntimeHeartbeat(),
747
798
  });
748
799
  this.realtimeManager = rtm;
@@ -818,6 +869,37 @@ export class CanonAgent {
818
869
  console.error('[canon-sdk] Contact-request handler failed:', error instanceof Error ? error.message : error);
819
870
  }
820
871
  }
872
+ async recordAndHandleContactLifecycleEvent(request) {
873
+ if (this.contactLifecycleStore
874
+ && request.requesterId === this.agentId
875
+ && request.sourceConversationId) {
876
+ await this.contactLifecycleStore.record(request);
877
+ }
878
+ await this.handleContactRequestEvent(this.contactRequestUpdatedHandler, request);
879
+ }
880
+ async reconcileContactLifecycleInbox() {
881
+ if (!this.contactLifecycleStore || !this.agentId)
882
+ return;
883
+ const requests = await this.apiClient.listContactRequests({
884
+ direction: 'outbound',
885
+ includeResolved: true,
886
+ limit: 100,
887
+ });
888
+ if (!await this.contactLifecycleStore.isBaselineComplete()) {
889
+ await reconcileContactLifecycleEvents({
890
+ requests,
891
+ requesterId: this.agentId,
892
+ record: (request) => this.contactLifecycleStore.record(request, { pending: false }),
893
+ });
894
+ await this.contactLifecycleStore.completeBaseline();
895
+ return;
896
+ }
897
+ await reconcileContactLifecycleEvents({
898
+ requests,
899
+ requesterId: this.agentId,
900
+ record: (request) => this.contactLifecycleStore.record(request),
901
+ });
902
+ }
821
903
  async handleContactGraphEvent(handler, payload) {
822
904
  if (!handler)
823
905
  return;
@@ -934,6 +1016,13 @@ export class CanonAgent {
934
1016
  }
935
1017
  return {
936
1018
  ...source,
1019
+ admissionActions: {
1020
+ blockUser: source.admissionActions?.blockUser === true,
1021
+ unblockUser: source.admissionActions?.unblockUser === true,
1022
+ removeContact: source.admissionActions?.removeContact === true,
1023
+ requestContact: this.options.ownerBoundCommunication?.enabled === true,
1024
+ reachOut: this.options.ownerBoundCommunication?.enabled === true,
1025
+ },
937
1026
  supportsInterrupt: hasInterrupt,
938
1027
  supportsInputInterrupt: source.supportsInputInterrupt === false ? false : hasInterrupt,
939
1028
  commands: normalizeRuntimeCommandDescriptors(commands),
@@ -1542,6 +1631,9 @@ export class CanonAgent {
1542
1631
  for (const m of history) {
1543
1632
  m.isOwner = m.senderId === ownerId;
1544
1633
  }
1634
+ for (const m of hydratedMessages) {
1635
+ m.isOwner = m.senderId === ownerId;
1636
+ }
1545
1637
  }
1546
1638
  const latestMessage = hydratedMessages[hydratedMessages.length - 1] ?? null;
1547
1639
  const triggeringHumanId = latestMessage?.senderType === 'human'
@@ -1550,6 +1642,9 @@ export class CanonAgent {
1550
1642
  let replyContext = latestMessage
1551
1643
  ? resolveCanonReplyContext({ message: latestMessage, messages: history })
1552
1644
  : null;
1645
+ const contactLifecycleEvents = latestMessage?.isOwner && this.contactLifecycleStore
1646
+ ? await this.contactLifecycleStore.take(conversationId)
1647
+ : [];
1553
1648
  const resolvedActiveSelfContextId = resolveMessageActiveSelfContextId({
1554
1649
  messageId: latestMessage?.id,
1555
1650
  activeSelfContextIdByMessageId: page.activeSelfContextIdByMessageId,
@@ -1716,10 +1811,44 @@ export class CanonAgent {
1716
1811
  },
1717
1812
  },
1718
1813
  });
1719
- const reachOut = (card, options) => this.reachOut(card, {
1720
- ...(options ?? {}),
1721
- sourceConversationId: conversationId,
1722
- });
1814
+ const reachOut = (card, options) => {
1815
+ if (!this.options.ownerBoundCommunication?.enabled) {
1816
+ return this.reachOut(card, {
1817
+ ...(options ?? {}),
1818
+ sourceConversationId: conversationId,
1819
+ });
1820
+ }
1821
+ if (!latestMessage?.isOwner || !latestMessage.id) {
1822
+ return Promise.reject(new Error('Owner-bound reachOut requires an owner-authored foreground message.'));
1823
+ }
1824
+ if (options?.sessionConfig != null
1825
+ || options?.sessionSelection != null
1826
+ || options?.selfContext != null) {
1827
+ return Promise.reject(new Error('Owner-bound reachOut does not accept model-selected session configuration or hidden context.'));
1828
+ }
1829
+ const boundCard = replyContext?.found && replyContext.contactCard
1830
+ ? replyContext.contactCard
1831
+ : card;
1832
+ const text = options?.text?.trim();
1833
+ if (!text)
1834
+ return Promise.reject(new Error('Owner-bound reachOut requires visible text.'));
1835
+ return this.apiClient.sendTo({
1836
+ ...(boundCard.canonContactId
1837
+ ? { canonContactId: boundCard.canonContactId }
1838
+ : { targetUserId: boundCard.userId }),
1839
+ sourceConversationId: conversationId,
1840
+ text,
1841
+ ...(options?.requestMessage ? { requestMessage: options.requestMessage } : {}),
1842
+ messageOptions: {
1843
+ metadata: {
1844
+ sourceConversationId: conversationId,
1845
+ sourceMessageId: latestMessage.id,
1846
+ turnId,
1847
+ turnSemantics: 'turn_complete',
1848
+ },
1849
+ },
1850
+ });
1851
+ };
1723
1852
  const requestApproval = async (request) => {
1724
1853
  throwIfAborted();
1725
1854
  const manager = this.ensureApprovalManager(agent);
@@ -1761,7 +1890,7 @@ export class CanonAgent {
1761
1890
  ...(responseUserId ? { responseUserId } : {}),
1762
1891
  ignoreSessionRules: mustIsolateSessionRules ? true : request.ignoreSessionRules,
1763
1892
  allowSessionRule: mustIsolateSessionRules ? false : request.allowSessionRule,
1764
- });
1893
+ }, request.signal ? { signal: request.signal } : undefined);
1765
1894
  throwIfAborted();
1766
1895
  shouldPersistTurnState = false;
1767
1896
  try {
@@ -2080,6 +2209,7 @@ export class CanonAgent {
2080
2209
  messages: hydratedMessages,
2081
2210
  history,
2082
2211
  replyContext,
2212
+ contactLifecycleEvents,
2083
2213
  conversationId,
2084
2214
  conversation,
2085
2215
  ...(groupContext ? { groupContext } : {}),
package/dist/index.d.ts CHANGED
@@ -7,5 +7,5 @@ export { DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES, DEFAULT_MEDIA_CACHE_DIR, DEFA
7
7
  export type { AnthropicImageBlock, AnthropicImageBudgetOptions, AnthropicImageMimeType, MaterializeMediaOptions, MaterializedCanonAttachment, MaterializedCanonReplyContext, ReplyWithFileOptions, UploadMediaFileOptions, } from './media.js';
8
8
  export type { SessionConfig, Session } from './session-manager.js';
9
9
  export type { CanonAgentTurnVerbosityOption } from './turn-verbosity-option.js';
10
- export type { AgentContext, CanonGroupContext, CanonKnownRecentParticipant, CanonMembershipChange, CanonContactRequest, GroupInviteRequirements, CanonMessage, CanonConversation, CanonConversationsPage, CanonConversationsPageOptions, CanonReplyContext, CanonSelfContext, CanonTurnContextV2, CanonRuntimeDescriptor, MessageUpdatedPayload, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, CreateConversationOptions, CreateConversationResult, DirectSessionSelection, TurnVerbosity, TurnVerbosityConfig, } from '@canonmsg/core';
11
- export type { CanonAgentConnectionOptions, CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, ContactRequestHandler, FinalMessageResult, MessageHandler, MessageHandlerContext, MessageUpdatedHandler, ParticipationSuppressedHandler, ProgressMessageOptions, ProgressMessageResult, ReachOutOptions, ReachOutResult, RuntimeApprovalRequest, RuntimeInputRequest, RuntimeInputResult, RuntimeControlSurface, RuntimePrimitiveContext, RuntimePrimitiveHandler, RuntimePrimitiveHandlers, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
10
+ export type { AgentContext, CanonGroupContext, CanonKnownRecentParticipant, CanonMembershipChange, CanonContactRequest, ContactRequestUpdatedPayload, ContactRequestListOptions, ContactRequestRequirements, ContactRequestLifecyclePhase, GroupInviteRequirements, CanonMessage, CanonConversation, CanonConversationsPage, CanonConversationsPageOptions, CanonReplyContext, CanonSelfContext, CanonTurnContextV2, CanonRuntimeDescriptor, MessageUpdatedPayload, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, CreateConversationOptions, CreateConversationResult, DirectSessionSelection, TurnVerbosity, TurnVerbosityConfig, } from '@canonmsg/core';
11
+ export type { CanonAgentConnectionOptions, CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, ContactRequestHandler, ContactRequestUpdatedHandler, ContactLifecycleStore, FinalMessageResult, MessageHandler, MessageHandlerContext, MessageUpdatedHandler, ParticipationSuppressedHandler, ProgressMessageOptions, ProgressMessageResult, ReachOutOptions, ReachOutResult, RuntimeApprovalRequest, RuntimeInputRequest, RuntimeInputResult, RuntimeControlSurface, RuntimePrimitiveContext, RuntimePrimitiveHandler, RuntimePrimitiveHandlers, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
@@ -1,4 +1,4 @@
1
- import { type AgentContext, type ContactAddedPayload, type ContactApprovedPayload, type ContactRemovedPayload, type ContactRequestPayload, type ConversationUpdatedPayload, type MessageUpdatedPayload, type ParticipationSuppressedPayload, type VoiceSessionEventPayload } from '@canonmsg/core';
1
+ import { type AgentContext, type ContactAddedPayload, type ContactApprovedPayload, type ContactRemovedPayload, type ContactRequestPayload, type ContactRequestUpdatedPayload, type ConversationUpdatedPayload, type MessageUpdatedPayload, type ParticipationSuppressedPayload, 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:
@@ -16,6 +16,7 @@ export declare class RealtimeManager {
16
16
  private suppressedSseErrorCount;
17
17
  private onAgentContext;
18
18
  private onContactRequest;
19
+ private onContactRequestUpdated;
19
20
  private onContactApproved;
20
21
  private onContactAdded;
21
22
  private onContactRemoved;
@@ -25,6 +26,7 @@ export declare class RealtimeManager {
25
26
  private onMessageDeleted;
26
27
  private onConnected;
27
28
  private onDisconnected;
29
+ private onReplayExpired;
28
30
  private onCallStarted;
29
31
  private onCallEnded;
30
32
  constructor(apiKey: string, debouncer: Debouncer, agentId: string, streamUrl?: string, options?: {
@@ -37,6 +39,7 @@ export declare class RealtimeManager {
37
39
  setOnAgentContext(cb: (ctx: AgentContext) => void): void;
38
40
  setContactRequestHandlers(handlers: {
39
41
  onContactRequest?: (payload: ContactRequestPayload) => void;
42
+ onContactRequestUpdated?: (payload: ContactRequestUpdatedPayload) => void;
40
43
  onContactApproved?: (payload: ContactApprovedPayload) => void;
41
44
  }): void;
42
45
  setContactGraphHandlers(handlers: {
@@ -58,6 +61,7 @@ export declare class RealtimeManager {
58
61
  setConnectionHandlers(handlers: {
59
62
  onConnected?: () => void;
60
63
  onDisconnected?: () => void;
64
+ onReplayExpired?: () => void;
61
65
  }): void;
62
66
  setCallHandlers(handlers: {
63
67
  onCallStarted?: (payload: VoiceSessionEventPayload) => void;
package/dist/realtime.js CHANGED
@@ -17,6 +17,7 @@ export class RealtimeManager {
17
17
  suppressedSseErrorCount = 0;
18
18
  onAgentContext = null;
19
19
  onContactRequest = null;
20
+ onContactRequestUpdated = null;
20
21
  onContactApproved = null;
21
22
  onContactAdded = null;
22
23
  onContactRemoved = null;
@@ -26,6 +27,7 @@ export class RealtimeManager {
26
27
  onMessageDeleted = null;
27
28
  onConnected = null;
28
29
  onDisconnected = null;
30
+ onReplayExpired = null;
29
31
  onCallStarted = null;
30
32
  onCallEnded = null;
31
33
  constructor(apiKey, debouncer, agentId, streamUrl, options) {
@@ -102,6 +104,9 @@ export class RealtimeManager {
102
104
  onContactRequest: (payload) => {
103
105
  this.onContactRequest?.(payload);
104
106
  },
107
+ onContactRequestUpdated: (payload) => {
108
+ this.onContactRequestUpdated?.(payload);
109
+ },
105
110
  onContactApproved: (payload) => {
106
111
  this.onContactApproved?.(payload);
107
112
  },
@@ -125,6 +130,7 @@ export class RealtimeManager {
125
130
  },
126
131
  onReplayExpired: (payload) => {
127
132
  console.error(`[canon-sdk] SSE replay window expired${payload.lastAvailableId ? ` (oldest available event: ${payload.lastAvailableId})` : ''}; missed history is available via explicit REST fetch`);
133
+ this.onReplayExpired?.();
128
134
  },
129
135
  onError: (err) => {
130
136
  this.logSseError(err);
@@ -175,6 +181,7 @@ export class RealtimeManager {
175
181
  }
176
182
  setContactRequestHandlers(handlers) {
177
183
  this.onContactRequest = handlers.onContactRequest ?? null;
184
+ this.onContactRequestUpdated = handlers.onContactRequestUpdated ?? null;
178
185
  this.onContactApproved = handlers.onContactApproved ?? null;
179
186
  }
180
187
  setContactGraphHandlers(handlers) {
@@ -201,6 +208,7 @@ export class RealtimeManager {
201
208
  setConnectionHandlers(handlers) {
202
209
  this.onConnected = handlers.onConnected ?? null;
203
210
  this.onDisconnected = handlers.onDisconnected ?? null;
211
+ this.onReplayExpired = handlers.onReplayExpired ?? null;
204
212
  }
205
213
  setCallHandlers(handlers) {
206
214
  this.onCallStarted = handlers.onCallStarted ?? null;
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type { AddMemberResult, GroupInviteRequirements, AgentClientType, CanonGroupContext, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeDescriptor, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimePrimitiveId, CanonRuntimeProvenance, CanonTurnContextV2, CanonMessage, CanonConversation, CanonReplyContext, CanonContact, CanonContactRequest, CanonResolveAdmissionResult, ContactAddedPayload, ContactRemovedPayload, ContactSource, AgentContext, ResolveAdmissionTargetInput, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonSelfContext, CreateConversationResult, DirectSessionSelection, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, SessionConfig, VerbSessionConfig, CreateConversationOptions, TurnLifecycleState, TurnOutputBlock, TurnOutputBlockInput, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalRisk, ApprovalReplyMetadata, ApprovalOutcomeMetadata, RuntimeInputChoice, RuntimeInputAnswers, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, RuntimeCardNativeMetadata, RuntimeCardV1, ResumableMediaUploadResult, MediaAttachment, VideoProcessingStatus, VideoProcessingErrorCode, SessionRule, ApprovalResult, } from '@canonmsg/core';
1
+ export type { AddMemberResult, ContactRequestListOptions, ContactRequestRequirements, ContactRequestLifecyclePhase, GroupInviteRequirements, AgentClientType, CanonGroupContext, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeDescriptor, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimePrimitiveId, CanonRuntimeProvenance, CanonTurnContextV2, CanonMessage, CanonConversation, CanonReplyContext, CanonContact, CanonContactRequest, ContactRequestUpdatedPayload, CanonResolveAdmissionResult, ContactAddedPayload, ContactRemovedPayload, ContactSource, AgentContext, ResolveAdmissionTargetInput, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonSelfContext, CreateConversationResult, DirectSessionSelection, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, SessionConfig, VerbSessionConfig, CreateConversationOptions, TurnLifecycleState, TurnOutputBlock, TurnOutputBlockInput, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalRisk, ApprovalReplyMetadata, ApprovalOutcomeMetadata, RuntimeInputChoice, RuntimeInputAnswers, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, RuntimeCardNativeMetadata, RuntimeCardV1, ResumableMediaUploadResult, MediaAttachment, VideoProcessingStatus, VideoProcessingErrorCode, SessionRule, ApprovalResult, } from '@canonmsg/core';
2
2
  import type { AddMemberResult, CanonGroupContext, CanonMessage, CanonConversation, CanonReplyContext, ContactCardPayload, CanonRuntimeActionDispatch, CanonRuntimePrimitiveId, CanonRuntimeProvenance, CanonTurnContextV2, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalResult, ApprovalRisk, RuntimeInputChoice, RuntimeInputAnswers, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, RuntimeCardNativeMetadata, RuntimeCardV1, ResumableMediaUploadResult, SendMessageOptions, SendContextualSelfContextInput, VerbSessionConfig, DirectSessionSelection, TurnOutputBlock, TurnOutputBlockInput } from '@canonmsg/core';
3
3
  import type { MaterializeMediaOptions, MaterializedCanonAttachment, ReplyWithFileOptions, UploadMediaFileOptions } from './media.js';
4
4
  export interface ProgressMessageOptions extends SendMessageOptions {
@@ -96,6 +96,8 @@ export interface RuntimeApprovalRequest {
96
96
  details?: ApprovalRequestDetail[];
97
97
  /** Human conversation member who should answer. Defaults to the triggering human. */
98
98
  responseUserId?: string;
99
+ /** Aborts the Canon approval request and withdraws its pending card. */
100
+ signal?: AbortSignal;
99
101
  /**
100
102
  * Ignore in-memory session rules for this approval request. Useful when the
101
103
  * action was triggered by someone other than the agent owner.
@@ -150,6 +152,8 @@ export interface MessageHandlerContext {
150
152
  history: CanonMessage[];
151
153
  /** Resolved message/media content for the latest swipe-reply target, if any. */
152
154
  replyContext: CanonReplyContext | null;
155
+ /** Terminal introduction events retained until this natural owner turn. */
156
+ contactLifecycleEvents: import('@canonmsg/core').CanonContactRequest[];
153
157
  conversationId: string;
154
158
  conversation: CanonConversation;
155
159
  /** Lightweight group awareness, present for group conversations. */
@@ -308,6 +312,15 @@ export interface CanonAgentOptions extends CanonAgentConnectionOptions {
308
312
  clientType?: import('@canonmsg/core').AgentClientType;
309
313
  /** Optional runtime descriptor published to Canon for setup/live UI rendering. */
310
314
  runtimeDescriptor?: import('@canonmsg/core').CanonRuntimeDescriptor;
315
+ /**
316
+ * Opt into the owner-authorized introduction surface advertised to Canon.
317
+ * Lifecycle events never start a model turn; they are passed to the next
318
+ * natural owner handler invocation and to onContactRequestUpdated.
319
+ */
320
+ ownerBoundCommunication?: {
321
+ enabled: true;
322
+ lifecycleStore?: ContactLifecycleStore;
323
+ };
311
324
  /** Optional Canon runtime signal handlers. Enables interrupt controls when provided. */
312
325
  runtimeControls?: RuntimeControlHandlers;
313
326
  /** Runtime publishing surface. Use `host` when this agent owns live runtime controls. */
@@ -334,6 +347,15 @@ export interface CanonAgentOptions extends CanonAgentConnectionOptions {
334
347
  turnVerbosity?: import('./turn-verbosity-option.js').CanonAgentTurnVerbosityOption;
335
348
  }
336
349
  export type ContactRequestHandler = (request: import('@canonmsg/core').CanonContactRequest) => void | Promise<void>;
350
+ export type ContactRequestUpdatedHandler = ContactRequestHandler;
351
+ export interface ContactLifecycleStore {
352
+ record(request: import('@canonmsg/core').CanonContactRequest, options?: {
353
+ pending?: boolean;
354
+ }): boolean | Promise<boolean>;
355
+ take(sourceConversationId: string): import('@canonmsg/core').CanonContactRequest[] | Promise<import('@canonmsg/core').CanonContactRequest[]>;
356
+ isBaselineComplete(): boolean | Promise<boolean>;
357
+ completeBaseline(): void | Promise<void>;
358
+ }
337
359
  export type ContactAddedHandler = (contact: import('@canonmsg/core').ContactAddedPayload) => void | Promise<void>;
338
360
  export type ContactRemovedHandler = (payload: import('@canonmsg/core').ContactRemovedPayload) => void | Promise<void>;
339
361
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "8.7.0",
3
+ "version": "8.9.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": "^10.5.0"
31
+ "@canonmsg/core": "^10.7.0"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"