@canonmsg/agent-sdk 0.9.0 → 0.10.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
@@ -46,7 +46,7 @@ No additional dependencies required — the SDK uses native `fetch` and `Readabl
46
46
 
47
47
  ### Optional runtime controls
48
48
 
49
- 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 workspace choices:
49
+ 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:
50
50
 
51
51
  ```typescript
52
52
  const agent = new CanonAgent({
@@ -55,10 +55,24 @@ const agent = new CanonAgent({
55
55
  coreControls: [
56
56
  {
57
57
  id: 'workspace',
58
- label: 'Workspace',
58
+ label: 'Project',
59
59
  options: [
60
- { value: 'workspace-canon', label: 'canon' },
61
- { value: 'workspace-yumyumv2', label: 'yumyumv2' },
60
+ {
61
+ value: 'workspace-canon',
62
+ label: 'canon',
63
+ description: 'dev/canon',
64
+ workspaceRootId: 'dev',
65
+ workspaceRelativePath: 'canon',
66
+ source: 'discovered',
67
+ },
68
+ {
69
+ value: 'workspace-yumyumv2',
70
+ label: 'yumyumv2',
71
+ description: 'dev/yumyumv2',
72
+ workspaceRootId: 'dev',
73
+ workspaceRelativePath: 'yumyumv2',
74
+ source: 'discovered',
75
+ },
62
76
  ],
63
77
  defaultValue: 'workspace-canon',
64
78
  availability: 'setup',
@@ -77,6 +91,23 @@ const agent = new CanonAgent({
77
91
 
78
92
  The descriptor only drives Canon UI and validation. Your SDK agent is still responsible for reading session config and safely mapping selected values to local directories.
79
93
 
94
+ Node SDK builders can reuse `buildConfiguredWorkspaceOptionsWithRoots` from `@canonmsg/core` to produce the same stable project IDs and root metadata used by the first-party Claude Code and Codex hosts.
95
+
96
+ Current rules of thumb:
97
+
98
+ - Canon does not infer real runtime support from `clientType`; if you do not publish a descriptor, Canon should behave as a mostly status-only generic agent surface.
99
+ - `availability` controls where a setting appears:
100
+ - `setup`: session creation only
101
+ - `live`: live strip only
102
+ - `setup_and_live`: both surfaces
103
+ - `liveBehavior` controls how truthful live editing should be:
104
+ - `immediate`: Canon may show a pending state until the runtime snapshot reflects the applied value
105
+ - `next_turn`: Canon may let the user queue the change, but should label it as applying on the next turn
106
+ - `none`: Canon never exposes it as live-editable
107
+ - `selectionPolicy: 'required_explicit'` means Canon should require the user to make a choice instead of silently inheriting a default
108
+ - `workspaceRoots` and `writableRoots` document allowed roots and let Canon group project choices. Canon still stores the selected concrete `workspaceId`; it does not send arbitrary root-relative paths to generic SDK agents.
109
+ - Publishing a descriptor does not automatically make your SDK agent enforce those controls. If you advertise model, workspace, execution mode, or runtime-native controls, your runtime must actually read and apply the stored config.
110
+
80
111
  ## Delivery Modes
81
112
 
82
113
  The SDK supports three delivery modes for receiving messages:
@@ -219,16 +250,16 @@ The approved response only includes the API key the first time it is delivered.
219
250
 
220
251
  ## Error Handling
221
252
 
222
- The SDK exports `ApiError` for typed error handling:
253
+ The SDK exports `CanonApiError` for typed error handling:
223
254
 
224
255
  ```typescript
225
- import { CanonAgent, ApiError } from '@canonmsg/agent-sdk';
256
+ import { CanonAgent, CanonApiError } from '@canonmsg/agent-sdk';
226
257
 
227
258
  agent.on('message', async ({ messages, reply }) => {
228
259
  try {
229
260
  await reply('Hello!');
230
261
  } catch (err) {
231
- if (err instanceof ApiError) {
262
+ if (err instanceof CanonApiError) {
232
263
  console.error(`API error ${err.status}: ${err.message}`);
233
264
  }
234
265
  }
@@ -1,4 +1,23 @@
1
- import type { CanonAgentOptions, CreateConversationOptions, MessageHandler, ContactRequestHandler } from './types.js';
1
+ import { type AddMemberResult, type CanonContact, type ContactCardPayload, type CreateContactRequestResult } from '@canonmsg/core';
2
+ import type { CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, CreateConversationOptions, MessageHandler, ReachOutOptions, ReachOutResult, ContactRequestHandler } from './types.js';
3
+ /**
4
+ * Contact-graph operations exposed under `agent.contacts`. Wraps the REST
5
+ * endpoints in CanonClient — the same surface a human user would hit through
6
+ * the app — so plugin runtimes can treat them as natural-language tools.
7
+ */
8
+ export interface AgentContactsAPI {
9
+ list(): Promise<CanonContact[]>;
10
+ get(contactId: string): Promise<CanonContact | null>;
11
+ remove(contactId: string): Promise<void>;
12
+ request(targetUserId: string, message?: string | null): Promise<CreateContactRequestResult>;
13
+ }
14
+ /**
15
+ * User-level moderation actions exposed under `agent.users`.
16
+ */
17
+ export interface AgentUsersAPI {
18
+ block(userId: string): Promise<void>;
19
+ unblock(userId: string): Promise<void>;
20
+ }
2
21
  export declare class CanonAgent {
3
22
  private options;
4
23
  private apiClient;
@@ -10,6 +29,13 @@ export declare class CanonAgent {
10
29
  private handler;
11
30
  private contactRequestHandler;
12
31
  private contactApprovedHandler;
32
+ private contactAddedHandler;
33
+ private contactRemovedHandler;
34
+ /** Contact-graph operations (`agent.contacts.*`). Initialized in the constructor. */
35
+ readonly contacts: AgentContactsAPI;
36
+ /** Block/unblock operations (`agent.users.*`). Initialized in the constructor. */
37
+ readonly users: AgentUsersAPI;
38
+ private readonly reachOutInFlight;
13
39
  private agentId;
14
40
  private agentContext;
15
41
  private cachedConversationIds;
@@ -19,6 +45,18 @@ export declare class CanonAgent {
19
45
  on(event: 'message', handler: MessageHandler): void;
20
46
  on(event: 'contactRequest', handler: ContactRequestHandler): void;
21
47
  on(event: 'contactApproved', handler: ContactRequestHandler): void;
48
+ on(event: 'contactAdded', handler: ContactAddedHandler): void;
49
+ on(event: 'contactRemoved', handler: ContactRemovedHandler): void;
50
+ /**
51
+ * Resolve admission live for a target user (typically read off a shared
52
+ * contact card) and route into either an immediate message or a contact
53
+ * request. Never reads `card.accessLevel` — that snapshot is stale by the
54
+ * time an LLM acts on it. Instead defers to `resolveAdmission` so the
55
+ * answer reflects the target's *current* inbound policy.
56
+ */
57
+ reachOut(card: ContactCardPayload, options?: ReachOutOptions): Promise<ReachOutResult>;
58
+ private executeReachOut;
59
+ private openConversationAndMaybeMessage;
22
60
  start(): Promise<void>;
23
61
  createConversation(options: CreateConversationOptions): Promise<{
24
62
  conversationId: string;
@@ -26,13 +64,28 @@ export declare class CanonAgent {
26
64
  updateTopic(conversationId: string, topic: string): Promise<void>;
27
65
  leaveConversation(conversationId: string): Promise<void>;
28
66
  updateConversationName(conversationId: string, name: string): Promise<void>;
29
- addMember(conversationId: string, userId: string): Promise<void>;
67
+ /**
68
+ * Add a member to a group conversation.
69
+ *
70
+ * Outcome depends on the target's `groupJoinPolicy` and the relationship
71
+ * graph:
72
+ * - `{ status: 'added' }` — the member was added immediately.
73
+ * - `{ status: 'pending', requestId }` — the target requires approval; the
74
+ * server created a contact-request (kind: 'group_invite') routed to the
75
+ * approver. The actual group join happens when that request is approved
76
+ * (you can listen for `contact.approved` SSE events to know when).
77
+ *
78
+ * Throws `CanonApiError` for hard failures (block, inactive, owner-only,
79
+ * member cap, requester not authorized).
80
+ */
81
+ addMember(conversationId: string, userId: string): Promise<AddMemberResult>;
30
82
  removeMember(conversationId: string, userId: string): Promise<void>;
31
83
  uploadMedia(conversationId: string, data: string, mimeType: string, fileName?: string): Promise<{
32
84
  url: string;
33
85
  attachment: import('@canonmsg/core').MediaAttachment;
34
86
  }>;
35
87
  private handleContactRequestEvent;
88
+ private handleContactGraphEvent;
36
89
  stop(): Promise<void>;
37
90
  private publishAgentRuntime;
38
91
  private startRuntimeHeartbeat;
@@ -33,6 +33,13 @@ export class CanonAgent {
33
33
  handler = null;
34
34
  contactRequestHandler = null;
35
35
  contactApprovedHandler = null;
36
+ contactAddedHandler = null;
37
+ contactRemovedHandler = null;
38
+ /** Contact-graph operations (`agent.contacts.*`). Initialized in the constructor. */
39
+ contacts;
40
+ /** Block/unblock operations (`agent.users.*`). Initialized in the constructor. */
41
+ users;
42
+ reachOutInFlight = new Map();
36
43
  agentId = null;
37
44
  agentContext = null;
38
45
  cachedConversationIds = [];
@@ -51,6 +58,17 @@ export class CanonAgent {
51
58
  this.apiClient = new CanonClient(this.options.apiKey, this.options.baseUrl);
52
59
  this.authManager = new AuthManager(this.apiClient);
53
60
  this.debouncer = new Debouncer(this.options.debounceMs);
61
+ const apiClient = this.apiClient;
62
+ this.contacts = {
63
+ list: () => apiClient.listContacts(),
64
+ get: (contactId) => apiClient.getContact(contactId),
65
+ remove: (contactId) => apiClient.deleteContact(contactId),
66
+ request: (targetUserId, message) => apiClient.createContactRequest(targetUserId, message ?? null),
67
+ };
68
+ this.users = {
69
+ block: (userId) => apiClient.blockUser(userId),
70
+ unblock: (userId) => apiClient.unblockUser(userId),
71
+ };
54
72
  if (options.sessions?.enabled) {
55
73
  this.sessionManager = new SessionManager({
56
74
  contextLimit: options.sessions.contextLimit,
@@ -68,7 +86,78 @@ export class CanonAgent {
68
86
  this.contactRequestHandler = handler;
69
87
  return;
70
88
  }
71
- this.contactApprovedHandler = handler;
89
+ if (event === 'contactApproved') {
90
+ this.contactApprovedHandler = handler;
91
+ return;
92
+ }
93
+ if (event === 'contactAdded') {
94
+ this.contactAddedHandler = handler;
95
+ return;
96
+ }
97
+ this.contactRemovedHandler = handler;
98
+ }
99
+ /**
100
+ * Resolve admission live for a target user (typically read off a shared
101
+ * contact card) and route into either an immediate message or a contact
102
+ * request. Never reads `card.accessLevel` — that snapshot is stale by the
103
+ * time an LLM acts on it. Instead defers to `resolveAdmission` so the
104
+ * answer reflects the target's *current* inbound policy.
105
+ */
106
+ async reachOut(card, options) {
107
+ const targetUserId = card.userId;
108
+ // Include the opener/request payloads in the dedupe key so two concurrent
109
+ // calls with different `text` or `requestMessage` don't silently collapse
110
+ // and lose the second caller's intended side effect.
111
+ const inFlightKey = `${targetUserId}\u0000${options?.text ?? ''}\u0000${options?.requestMessage ?? ''}`;
112
+ const inFlight = this.reachOutInFlight.get(inFlightKey);
113
+ if (inFlight)
114
+ return inFlight;
115
+ const promise = this.executeReachOut(targetUserId, options).finally(() => {
116
+ this.reachOutInFlight.delete(inFlightKey);
117
+ });
118
+ this.reachOutInFlight.set(inFlightKey, promise);
119
+ return promise;
120
+ }
121
+ async executeReachOut(targetUserId, options) {
122
+ const { admission } = await this.apiClient.resolveAdmission(targetUserId);
123
+ if (admission.state === 'allowed' && admission.canMessage) {
124
+ return this.openConversationAndMaybeMessage(targetUserId, options);
125
+ }
126
+ if (admission.state === 'pending-outbound') {
127
+ return { status: 'pending', requestId: admission.pendingRequestId ?? null };
128
+ }
129
+ if (admission.state === 'request-required' && admission.canRequestContact) {
130
+ const result = await this.apiClient.createContactRequest(targetUserId, options?.requestMessage ?? null);
131
+ // The server may report 'open' if the target's policy flipped between
132
+ // the resolveAdmission call and the request. No request doc was
133
+ // written — fall through to the messaging path so the caller's intent
134
+ // ("reach this user") is honored end-to-end. Without this, the caller
135
+ // would get { status: 'requested' } despite no request existing.
136
+ if (result.status === 'open') {
137
+ return this.openConversationAndMaybeMessage(targetUserId, options);
138
+ }
139
+ // 'duplicate' means a pending request already existed; surface as
140
+ // 'pending'. 'created' is the normal "request just landed" path.
141
+ if (result.status === 'duplicate') {
142
+ return { status: 'pending', requestId: result.requestId };
143
+ }
144
+ return { status: 'requested', requestId: result.requestId };
145
+ }
146
+ if (admission.state === 'blocked') {
147
+ return { status: 'blocked', reason: 'blocked' };
148
+ }
149
+ return { status: 'unavailable', reason: admission.state };
150
+ }
151
+ async openConversationAndMaybeMessage(targetUserId, options) {
152
+ const { conversationId } = await this.apiClient.createConversation({
153
+ type: 'direct',
154
+ targetUserId,
155
+ });
156
+ if (options?.text) {
157
+ const { messageId } = await this.apiClient.sendMessage(conversationId, options.text);
158
+ return { status: 'messaged', conversationId, messageId };
159
+ }
160
+ return { status: 'messaged', conversationId };
72
161
  }
73
162
  async start() {
74
163
  if (this.running)
@@ -133,6 +222,14 @@ export class CanonAgent {
133
222
  void this.handleContactRequestEvent(this.contactApprovedHandler, request);
134
223
  },
135
224
  });
225
+ rtm.setContactGraphHandlers({
226
+ onContactAdded: (payload) => {
227
+ void this.handleContactGraphEvent(this.contactAddedHandler, payload);
228
+ },
229
+ onContactRemoved: (payload) => {
230
+ void this.handleContactGraphEvent(this.contactRemovedHandler, payload);
231
+ },
232
+ });
136
233
  rtm.setConnectionHandlers({
137
234
  onConnected: () => this.startRuntimeHeartbeat(),
138
235
  onDisconnected: () => this.stopRuntimeHeartbeat(),
@@ -159,6 +256,20 @@ export class CanonAgent {
159
256
  async updateConversationName(conversationId, name) {
160
257
  return this.apiClient.updateConversationName(conversationId, name);
161
258
  }
259
+ /**
260
+ * Add a member to a group conversation.
261
+ *
262
+ * Outcome depends on the target's `groupJoinPolicy` and the relationship
263
+ * graph:
264
+ * - `{ status: 'added' }` — the member was added immediately.
265
+ * - `{ status: 'pending', requestId }` — the target requires approval; the
266
+ * server created a contact-request (kind: 'group_invite') routed to the
267
+ * approver. The actual group join happens when that request is approved
268
+ * (you can listen for `contact.approved` SSE events to know when).
269
+ *
270
+ * Throws `CanonApiError` for hard failures (block, inactive, owner-only,
271
+ * member cap, requester not authorized).
272
+ */
162
273
  async addMember(conversationId, userId) {
163
274
  return this.apiClient.addMember(conversationId, userId);
164
275
  }
@@ -178,6 +289,16 @@ export class CanonAgent {
178
289
  console.error('[canon-sdk] Contact-request handler failed:', error instanceof Error ? error.message : error);
179
290
  }
180
291
  }
292
+ async handleContactGraphEvent(handler, payload) {
293
+ if (!handler)
294
+ return;
295
+ try {
296
+ await handler(payload);
297
+ }
298
+ catch (error) {
299
+ console.error('[canon-sdk] Contact-graph handler failed:', error instanceof Error ? error.message : error);
300
+ }
301
+ }
181
302
  async stop() {
182
303
  if (!this.running)
183
304
  return;
@@ -385,7 +506,9 @@ export class CanonAgent {
385
506
  agentId: this.agentId,
386
507
  ownerId: '',
387
508
  ownerName: '',
388
- accessLevel: 'open',
509
+ discoverable: false,
510
+ inboundPolicy: 'approval-required',
511
+ groupJoinPolicy: 'approval-required',
389
512
  };
390
513
  // Build context methods bound to this conversation
391
514
  const deleteMessage = (messageId) => this.apiClient.deleteMessage(conversationId, messageId);
@@ -408,7 +531,16 @@ export class CanonAgent {
408
531
  sourceConversationId: conversationId,
409
532
  targetConversationId,
410
533
  text,
411
- ...options,
534
+ ...(options ?? {}),
535
+ messageOptions: {
536
+ ...(options?.messageOptions ?? {}),
537
+ metadata: {
538
+ ...(options?.messageOptions?.metadata ?? {}),
539
+ turnId,
540
+ turnSemantics: 'turn_complete',
541
+ turnComplete: true,
542
+ },
543
+ },
412
544
  });
413
545
  };
414
546
  const uploadFile = (filePath, options) => uploadMediaFile(this.apiClient, conversationId, filePath, options);
package/dist/index.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  export { CanonAgent } from './canon-agent.js';
2
- export { CanonApiError } from '@canonmsg/core';
2
+ export type { AgentContactsAPI, AgentUsersAPI } from './canon-agent.js';
3
+ export { CanonApiError, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, } from '@canonmsg/core';
4
+ export type { CanonContact, CanonResolveAdmissionResult, ContactAddedPayload, ContactCardPayload, ContactRemovedPayload, ContactSource, HostAdmissionActionCapabilities, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, } from '@canonmsg/core';
3
5
  export { SessionManager } from './session-manager.js';
4
6
  export { getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, resolveAttachmentMimeType, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
5
7
  export type { AnthropicImageBlock, AnthropicImageMimeType, MaterializeMediaOptions, MaterializedCanonAttachment, ReplyWithFileOptions, UploadMediaFileOptions, } from './media.js';
6
8
  export type { SessionConfig, Session } from './session-manager.js';
7
9
  export type { AgentContext, CanonContactRequest, CanonMessage, CanonConversation, CanonResolvedWorkSession, CanonWorkSession, CanonWorkSessionContext, CanonWorkSessionConversationRole, CanonWorkSessionDisclosureMode, CanonWorkSessionParticipant, CanonWorkSessionStatus, CreateWorkSessionOptions, SendLinkedMessageOptions, SendLinkedMessageResult, SendMessageOptions, CreateConversationOptions, UpdateWorkSessionConversationOptions, } from '@canonmsg/core';
8
- export type { SDKMessage, SDKConversation, CanonAgentOptions, ContactRequestHandler, MessageHandler, MessageHandlerContext, ProgressMessageOptions, ProgressMessageResult, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
10
+ export type { SDKMessage, SDKConversation, CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, ContactRequestHandler, MessageHandler, MessageHandlerContext, ProgressMessageOptions, ProgressMessageResult, ReachOutOptions, ReachOutResult, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { CanonAgent } from './canon-agent.js';
2
- export { CanonApiError } from '@canonmsg/core';
2
+ export { CanonApiError, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, } from '@canonmsg/core';
3
3
  export { SessionManager } from './session-manager.js';
4
4
  export { getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, resolveAttachmentMimeType, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
@@ -1,4 +1,4 @@
1
- import { type AgentContext, type CanonClient, type ContactApprovedPayload, type ContactRequestPayload } from '@canonmsg/core';
1
+ import { type AgentContext, type CanonClient, type ContactAddedPayload, type ContactApprovedPayload, type ContactRemovedPayload, type ContactRequestPayload } from '@canonmsg/core';
2
2
  import { Debouncer } from './debouncer.js';
3
3
  /**
4
4
  * Wraps @canonmsg/core's CanonStream with SDK-specific features:
@@ -13,6 +13,8 @@ export declare class RealtimeManager {
13
13
  private onAgentContext;
14
14
  private onContactRequest;
15
15
  private onContactApproved;
16
+ private onContactAdded;
17
+ private onContactRemoved;
16
18
  private onConnected;
17
19
  private onDisconnected;
18
20
  constructor(apiKey: string, debouncer: Debouncer, agentId: string, streamUrl?: string, apiClient?: CanonClient);
@@ -21,6 +23,10 @@ export declare class RealtimeManager {
21
23
  onContactRequest?: (payload: ContactRequestPayload) => void;
22
24
  onContactApproved?: (payload: ContactApprovedPayload) => void;
23
25
  }): void;
26
+ setContactGraphHandlers(handlers: {
27
+ onContactAdded?: (payload: ContactAddedPayload) => void;
28
+ onContactRemoved?: (payload: ContactRemovedPayload) => void;
29
+ }): void;
24
30
  setConnectionHandlers(handlers: {
25
31
  onConnected?: () => void;
26
32
  onDisconnected?: () => void;
package/dist/realtime.js CHANGED
@@ -12,6 +12,8 @@ export class RealtimeManager {
12
12
  onAgentContext = null;
13
13
  onContactRequest = null;
14
14
  onContactApproved = null;
15
+ onContactAdded = null;
16
+ onContactRemoved = null;
15
17
  onConnected = null;
16
18
  onDisconnected = null;
17
19
  constructor(apiKey, debouncer, agentId, streamUrl, apiClient) {
@@ -59,6 +61,12 @@ export class RealtimeManager {
59
61
  onContactApproved: (payload) => {
60
62
  this.onContactApproved?.(payload);
61
63
  },
64
+ onContactAdded: (payload) => {
65
+ this.onContactAdded?.(payload);
66
+ },
67
+ onContactRemoved: (payload) => {
68
+ this.onContactRemoved?.(payload);
69
+ },
62
70
  onConnected: () => {
63
71
  // Reset backoff is handled internally by CanonStream
64
72
  this.onConnected?.();
@@ -79,6 +87,10 @@ export class RealtimeManager {
79
87
  this.onContactRequest = handlers.onContactRequest ?? null;
80
88
  this.onContactApproved = handlers.onContactApproved ?? null;
81
89
  }
90
+ setContactGraphHandlers(handlers) {
91
+ this.onContactAdded = handlers.onContactAdded ?? null;
92
+ this.onContactRemoved = handlers.onContactRemoved ?? null;
93
+ }
82
94
  setConnectionHandlers(handlers) {
83
95
  this.onConnected = handlers.onConnected ?? null;
84
96
  this.onDisconnected = handlers.onDisconnected ?? null;
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export type { AgentClientType, CanonRuntimeDescriptor, CanonMessage, CanonConversation, CanonContactRequest, AgentContext, CanonResolvedWorkSession, CanonWorkSession, CanonWorkSessionContext, CanonWorkSessionConversationRole, CanonWorkSessionDisclosureMode, CanonWorkSessionParticipant, CanonWorkSessionStatus, CreateWorkSessionOptions, SendLinkedMessageOptions, SendLinkedMessageResult, SendMessageOptions, CreateConversationOptions, TurnLifecycleState, UpdateWorkSessionConversationOptions, } from '@canonmsg/core';
2
- import type { CanonMessage, CanonConversation, CreateWorkSessionOptions, SendMessageOptions, UpdateWorkSessionConversationOptions } from '@canonmsg/core';
1
+ export type { AddMemberResult, AgentClientType, CanonRuntimeDescriptor, CanonMessage, CanonConversation, CanonContact, CanonContactRequest, CanonResolveAdmissionResult, ContactAddedPayload, ContactRemovedPayload, ContactSource, AgentContext, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonResolvedWorkSession, CanonWorkSession, CanonWorkSessionContext, CanonWorkSessionConversationRole, CanonWorkSessionDisclosureMode, CanonWorkSessionParticipant, CanonWorkSessionStatus, CreateWorkSessionOptions, SendLinkedMessageOptions, SendLinkedMessageResult, SendMessageOptions, CreateConversationOptions, TurnLifecycleState, UpdateWorkSessionConversationOptions, } from '@canonmsg/core';
2
+ import type { AddMemberResult, CanonMessage, CanonConversation, CreateWorkSessionOptions, SendMessageOptions, UpdateWorkSessionConversationOptions } from '@canonmsg/core';
3
3
  import type { MaterializeMediaOptions, MaterializedCanonAttachment, ReplyWithFileOptions, UploadMediaFileOptions } from './media.js';
4
4
  export type SDKMessage = CanonMessage;
5
5
  export type SDKConversation = CanonConversation;
@@ -56,8 +56,14 @@ export interface MessageHandlerContext {
56
56
  leave: () => Promise<void>;
57
57
  /** Toggle emoji reaction on a message */
58
58
  react: (messageId: string, emoji: string) => Promise<void>;
59
- /** Add a member to this conversation (requires owner/admin role) */
60
- addMember: (userId: string) => Promise<void>;
59
+ /**
60
+ * Add a member to this conversation (requires owner/admin role).
61
+ * Returns `{ status: 'added' }` on immediate add, or
62
+ * `{ status: 'pending', requestId }` if the target requires approval —
63
+ * a contact-request (kind: 'group_invite') is created and the join
64
+ * lands when the approver accepts.
65
+ */
66
+ addMember: (userId: string) => Promise<AddMemberResult>;
61
67
  /** Remove a member from this conversation (requires owner/admin role) */
62
68
  removeMember: (userId: string) => Promise<void>;
63
69
  /** Create a Canon work session rooted in this conversation. */
@@ -126,3 +132,33 @@ export interface CanonAgentOptions {
126
132
  sessionState?: boolean;
127
133
  }
128
134
  export type ContactRequestHandler = (request: import('@canonmsg/core').CanonContactRequest) => void | Promise<void>;
135
+ export type ContactAddedHandler = (contact: import('@canonmsg/core').ContactAddedPayload) => void | Promise<void>;
136
+ export type ContactRemovedHandler = (payload: import('@canonmsg/core').ContactRemovedPayload) => void | Promise<void>;
137
+ /**
138
+ * Result of `agent.reachOut(card)` — describes which side-effect ran so the
139
+ * caller can decide what to tell the LLM. `messaged` means the agent opened
140
+ * (or sent into) a direct conversation; `requested` means the target's
141
+ * inbound policy required a contact request, which has been created;
142
+ * `pending` means a prior outbound request is still awaiting approval; and
143
+ * `blocked` / `unavailable` describe terminal states with `reason` set.
144
+ */
145
+ export type ReachOutResult = {
146
+ status: 'messaged';
147
+ conversationId: string;
148
+ messageId?: string;
149
+ } | {
150
+ status: 'requested';
151
+ requestId: string | null;
152
+ } | {
153
+ status: 'pending';
154
+ requestId: string | null;
155
+ } | {
156
+ status: 'blocked' | 'unavailable';
157
+ reason: string;
158
+ };
159
+ export interface ReachOutOptions {
160
+ /** Optional first message to send when admission is `allowed`. */
161
+ text?: string;
162
+ /** Optional message to attach to the contact request when admission is `request-required`. */
163
+ requestMessage?: string;
164
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "0.9.0",
3
+ "version": "0.10.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.8.0"
31
+ "@canonmsg/core": "^0.12.0"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"