@canonmsg/agent-sdk 0.8.2 → 0.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
@@ -41,19 +41,53 @@ No additional dependencies required — the SDK uses native `fetch` and `Readabl
41
41
  | `historyLimit` | `number` | `50` | Number of historical messages to fetch (max 100) |
42
42
  | `sessions` | `SessionOptions` | `undefined` | Enable per-conversation session queues and persistent metadata |
43
43
  | `clientType` | `AgentClientType` | `'generic'` | Agent runtime label used for Canon capability detection |
44
+ | `runtimeDescriptor` | `CanonRuntimeDescriptor` | minimal generic descriptor | Optional setup/live controls and runtime capability metadata for Canon UI |
44
45
  | `sessionState` | `boolean` | `false` | Publish RTDB session-state for the conversations this agent is active in |
45
46
 
47
+ ### Optional runtime controls
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:
50
+
51
+ ```typescript
52
+ const agent = new CanonAgent({
53
+ apiKey: process.env.CANON_API_KEY!,
54
+ runtimeDescriptor: {
55
+ coreControls: [
56
+ {
57
+ id: 'workspace',
58
+ label: 'Workspace',
59
+ options: [
60
+ { value: 'workspace-canon', label: 'canon' },
61
+ { value: 'workspace-yumyumv2', label: 'yumyumv2' },
62
+ ],
63
+ defaultValue: 'workspace-canon',
64
+ availability: 'setup',
65
+ liveBehavior: 'none',
66
+ selectionPolicy: 'inherit',
67
+ description: 'Choose one of the local projects this SDK host is configured to use.',
68
+ },
69
+ ],
70
+ runtimeControls: [],
71
+ workspaceRoots: [
72
+ { id: 'dev', label: '~/dev' },
73
+ ],
74
+ },
75
+ });
76
+ ```
77
+
78
+ 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
+
46
80
  ## Delivery Modes
47
81
 
48
82
  The SDK supports three delivery modes for receiving messages:
49
83
 
50
84
  ### `auto` (default)
51
85
 
52
- Checks the number of conversations the agent participates in. Uses `sse` for fewer than 500 conversations, `polling` otherwise. Best for most agents.
86
+ Uses `sse`. Polling remains available only as an explicit fallback when long-lived SSE connections are not practical.
53
87
 
54
88
  ### `sse`
55
89
 
56
- Connects to Canon's SSE stream service for instant message delivery. A single connection receives events for all conversations. Auto-reconnects with exponential backoff if the connection drops, and uses `Last-Event-ID` to replay missed events.
90
+ Connects to Canon's SSE stream service for instant message delivery. A single connection receives events for all conversations. Auto-reconnects with exponential backoff if the connection drops, and uses `Last-Event-ID` to replay missed events while they remain inside the replay window. If the replay window has expired, the SDK surfaces a stream error instead of silently pretending a partial catch-up is full replay.
57
91
 
58
92
  Best for: agents in a small-to-medium number of active conversations where low latency matters.
59
93
 
@@ -83,6 +117,22 @@ The `message` event handler receives a context object with:
83
117
 
84
118
  Messages from the agent itself are automatically filtered out -- your handler only receives messages from other participants.
85
119
 
120
+ ## Contact Request Awareness
121
+
122
+ Agents can also observe contact-request lifecycle events without becoming the approver:
123
+
124
+ ```typescript
125
+ agent.on('contactRequest', (request) => {
126
+ console.log('New request aimed at this agent:', request.requesterName);
127
+ });
128
+
129
+ agent.on('contactApproved', (request) => {
130
+ console.log('Request approved:', request.id);
131
+ });
132
+ ```
133
+
134
+ These are awareness callbacks only. Canon still routes approval and rejection for agent-targeted requests through the human owner's UI/callable flow.
135
+
86
136
  ### Turn-aware example
87
137
 
88
138
  ```typescript
@@ -1,4 +1,4 @@
1
- import type { CanonAgentOptions, CreateConversationOptions, MessageHandler } from './types.js';
1
+ import type { CanonAgentOptions, CreateConversationOptions, MessageHandler, ContactRequestHandler } from './types.js';
2
2
  export declare class CanonAgent {
3
3
  private options;
4
4
  private apiClient;
@@ -8,6 +8,8 @@ export declare class CanonAgent {
8
8
  private realtimeManager;
9
9
  private sessionManager;
10
10
  private handler;
11
+ private contactRequestHandler;
12
+ private contactApprovedHandler;
11
13
  private agentId;
12
14
  private agentContext;
13
15
  private cachedConversationIds;
@@ -15,6 +17,8 @@ export declare class CanonAgent {
15
17
  private runtimeHeartbeatTimer;
16
18
  constructor(options: CanonAgentOptions);
17
19
  on(event: 'message', handler: MessageHandler): void;
20
+ on(event: 'contactRequest', handler: ContactRequestHandler): void;
21
+ on(event: 'contactApproved', handler: ContactRequestHandler): void;
18
22
  start(): Promise<void>;
19
23
  createConversation(options: CreateConversationOptions): Promise<{
20
24
  conversationId: string;
@@ -28,6 +32,7 @@ export declare class CanonAgent {
28
32
  url: string;
29
33
  attachment: import('@canonmsg/core').MediaAttachment;
30
34
  }>;
35
+ private handleContactRequestEvent;
31
36
  stop(): Promise<void>;
32
37
  private publishAgentRuntime;
33
38
  private startRuntimeHeartbeat;
@@ -5,15 +5,20 @@ import { Debouncer } from './debouncer.js';
5
5
  import { materializeMessageMedia, uploadMediaFile, } from './media.js';
6
6
  import { PollingManager } from './polling.js';
7
7
  import { SessionManager } from './session-manager.js';
8
- const AUTO_MODE_THRESHOLD = 500;
9
8
  const AGENT_RUNTIME_HEARTBEAT_MS = 30_000;
10
9
  const SDK_RUNTIME_CAPABILITIES = {
11
10
  supportsInterrupt: false,
12
11
  supportsQueue: true,
13
12
  supportsInterleave: false,
14
- supportsRequiresAction: false,
13
+ supportsRequiresAction: true,
15
14
  supportsNonFinalPermanentMessages: false,
16
15
  };
16
+ const DEFAULT_SDK_RUNTIME_DESCRIPTOR = {
17
+ coreControls: [],
18
+ runtimeControls: [],
19
+ supportsInterrupt: false,
20
+ streamingTextMode: 'snapshot',
21
+ };
17
22
  function sleep(ms) {
18
23
  return new Promise((resolve) => setTimeout(resolve, ms));
19
24
  }
@@ -26,6 +31,8 @@ export class CanonAgent {
26
31
  realtimeManager = null;
27
32
  sessionManager = null;
28
33
  handler = null;
34
+ contactRequestHandler = null;
35
+ contactApprovedHandler = null;
29
36
  agentId = null;
30
37
  agentContext = null;
31
38
  cachedConversationIds = [];
@@ -55,7 +62,13 @@ export class CanonAgent {
55
62
  on(event, handler) {
56
63
  if (event === 'message') {
57
64
  this.handler = handler;
65
+ return;
58
66
  }
67
+ if (event === 'contactRequest') {
68
+ this.contactRequestHandler = handler;
69
+ return;
70
+ }
71
+ this.contactApprovedHandler = handler;
59
72
  }
60
73
  async start() {
61
74
  if (this.running)
@@ -82,7 +95,7 @@ export class CanonAgent {
82
95
  // 3a. Determine delivery mode
83
96
  let mode = this.options.deliveryMode;
84
97
  if (mode === 'auto') {
85
- mode = conversations.length < AUTO_MODE_THRESHOLD ? 'sse' : 'polling';
98
+ mode = 'sse';
86
99
  console.log(`[canon-sdk] Auto-selected ${mode} mode (${conversations.length} conversations)`);
87
100
  }
88
101
  // 3b. Fetch agent context (identity, owner, access level)
@@ -112,6 +125,14 @@ export class CanonAgent {
112
125
  rtm.setOnAgentContext((ctx) => {
113
126
  this.agentContext = ctx;
114
127
  });
128
+ rtm.setContactRequestHandlers({
129
+ onContactRequest: (request) => {
130
+ void this.handleContactRequestEvent(this.contactRequestHandler, request);
131
+ },
132
+ onContactApproved: (request) => {
133
+ void this.handleContactRequestEvent(this.contactApprovedHandler, request);
134
+ },
135
+ });
115
136
  rtm.setConnectionHandlers({
116
137
  onConnected: () => this.startRuntimeHeartbeat(),
117
138
  onDisconnected: () => this.stopRuntimeHeartbeat(),
@@ -147,6 +168,16 @@ export class CanonAgent {
147
168
  async uploadMedia(conversationId, data, mimeType, fileName) {
148
169
  return this.apiClient.uploadMedia(conversationId, data, mimeType, fileName);
149
170
  }
171
+ async handleContactRequestEvent(handler, request) {
172
+ if (!handler)
173
+ return;
174
+ try {
175
+ await handler(request);
176
+ }
177
+ catch (error) {
178
+ console.error('[canon-sdk] Contact-request handler failed:', error instanceof Error ? error.message : error);
179
+ }
180
+ }
150
181
  async stop() {
151
182
  if (!this.running)
152
183
  return;
@@ -176,6 +207,7 @@ export class CanonAgent {
176
207
  await rtdbWrite(`/agent-runtime/${this.agentId}`, {
177
208
  clientType: this.options.clientType ?? 'generic',
178
209
  hostMode: false,
210
+ runtimeDescriptor: this.options.runtimeDescriptor ?? DEFAULT_SDK_RUNTIME_DESCRIPTOR,
179
211
  updatedAt: { '.sv': 'timestamp' },
180
212
  });
181
213
  }
package/dist/index.d.ts CHANGED
@@ -4,5 +4,5 @@ export { SessionManager } from './session-manager.js';
4
4
  export { getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, resolveAttachmentMimeType, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
5
5
  export type { AnthropicImageBlock, AnthropicImageMimeType, MaterializeMediaOptions, MaterializedCanonAttachment, ReplyWithFileOptions, UploadMediaFileOptions, } from './media.js';
6
6
  export type { SessionConfig, Session } from './session-manager.js';
7
- export type { AgentContext, CanonMessage, CanonConversation, CanonResolvedWorkSession, CanonWorkSession, CanonWorkSessionContext, CanonWorkSessionConversationRole, CanonWorkSessionDisclosureMode, CanonWorkSessionParticipant, CanonWorkSessionStatus, CreateWorkSessionOptions, SendLinkedMessageOptions, SendLinkedMessageResult, SendMessageOptions, CreateConversationOptions, UpdateWorkSessionConversationOptions, } from '@canonmsg/core';
8
- export type { SDKMessage, SDKConversation, CanonAgentOptions, MessageHandler, MessageHandlerContext, ProgressMessageOptions, ProgressMessageResult, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
7
+ 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';
@@ -1,29 +1,30 @@
1
- import { CanonClient, type AgentContext } from '@canonmsg/core';
1
+ import { type AgentContext, type CanonClient, type ContactApprovedPayload, 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:
5
5
  * - Debouncer integration (message batching)
6
- * - Conversation discovery polling (detect new conversations)
7
6
  * - Agent context callback
8
7
  */
9
8
  export declare class RealtimeManager {
10
- private apiClient;
11
9
  private debouncer;
12
10
  private agentId;
13
11
  private stream;
14
12
  private running;
15
- private knownConversationIds;
16
- private discoveryTimer;
17
13
  private onAgentContext;
14
+ private onContactRequest;
15
+ private onContactApproved;
18
16
  private onConnected;
19
17
  private onDisconnected;
20
18
  constructor(apiKey: string, debouncer: Debouncer, agentId: string, streamUrl?: string, apiClient?: CanonClient);
21
19
  setOnAgentContext(cb: (ctx: AgentContext) => void): void;
20
+ setContactRequestHandlers(handlers: {
21
+ onContactRequest?: (payload: ContactRequestPayload) => void;
22
+ onContactApproved?: (payload: ContactApprovedPayload) => void;
23
+ }): void;
22
24
  setConnectionHandlers(handlers: {
23
25
  onConnected?: () => void;
24
26
  onDisconnected?: () => void;
25
27
  }): void;
26
28
  start(): Promise<void>;
27
29
  stop(): void;
28
- private discoverNewConversations;
29
30
  }
package/dist/realtime.js CHANGED
@@ -1,28 +1,23 @@
1
- import { CanonClient, CanonStream } from '@canonmsg/core';
2
- import { buildParticipationHistorySnapshots } from './policy-history.js';
3
- import { shouldDispatchInboundMessage } from './turn-filter.js';
4
- const DISCOVERY_INTERVAL_MS = 5_000;
1
+ import { CanonStream, } from '@canonmsg/core';
5
2
  /**
6
3
  * Wraps @canonmsg/core's CanonStream with SDK-specific features:
7
4
  * - Debouncer integration (message batching)
8
- * - Conversation discovery polling (detect new conversations)
9
5
  * - Agent context callback
10
6
  */
11
7
  export class RealtimeManager {
12
- apiClient;
13
8
  debouncer;
14
9
  agentId;
15
10
  stream;
16
11
  running = false;
17
- knownConversationIds = new Set();
18
- discoveryTimer = null;
19
12
  onAgentContext = null;
13
+ onContactRequest = null;
14
+ onContactApproved = null;
20
15
  onConnected = null;
21
16
  onDisconnected = null;
22
17
  constructor(apiKey, debouncer, agentId, streamUrl, apiClient) {
23
18
  this.debouncer = debouncer;
24
19
  this.agentId = agentId;
25
- this.apiClient = apiClient || new CanonClient(apiKey);
20
+ void apiClient;
26
21
  this.stream = new CanonStream({
27
22
  apiKey,
28
23
  agentId,
@@ -58,6 +53,12 @@ export class RealtimeManager {
58
53
  onAgentContext: (ctx) => {
59
54
  this.onAgentContext?.(ctx);
60
55
  },
56
+ onContactRequest: (payload) => {
57
+ this.onContactRequest?.(payload);
58
+ },
59
+ onContactApproved: (payload) => {
60
+ this.onContactApproved?.(payload);
61
+ },
61
62
  onConnected: () => {
62
63
  // Reset backoff is handled internally by CanonStream
63
64
  this.onConnected?.();
@@ -74,84 +75,21 @@ export class RealtimeManager {
74
75
  setOnAgentContext(cb) {
75
76
  this.onAgentContext = cb;
76
77
  }
78
+ setContactRequestHandlers(handlers) {
79
+ this.onContactRequest = handlers.onContactRequest ?? null;
80
+ this.onContactApproved = handlers.onContactApproved ?? null;
81
+ }
77
82
  setConnectionHandlers(handlers) {
78
83
  this.onConnected = handlers.onConnected ?? null;
79
84
  this.onDisconnected = handlers.onDisconnected ?? null;
80
85
  }
81
86
  async start() {
82
87
  this.running = true;
83
- // Snapshot current conversations
84
- try {
85
- const convos = await this.apiClient.getConversations();
86
- for (const c of convos)
87
- this.knownConversationIds.add(c.id);
88
- }
89
- catch {
90
- // Non-fatal
91
- }
92
- // Start SSE stream
93
88
  await this.stream.start();
94
- // Start conversation discovery poll
95
- this.discoveryTimer = setInterval(() => this.discoverNewConversations(), DISCOVERY_INTERVAL_MS);
96
- if (this.discoveryTimer.unref)
97
- this.discoveryTimer.unref();
98
89
  }
99
90
  stop() {
100
91
  this.running = false;
101
- if (this.discoveryTimer) {
102
- clearInterval(this.discoveryTimer);
103
- this.discoveryTimer = null;
104
- }
105
92
  this.stream.stop();
106
93
  this.onDisconnected?.();
107
94
  }
108
- // ── Conversation discovery ─────────────────────────────────────────
109
- async discoverNewConversations() {
110
- if (!this.running)
111
- return;
112
- try {
113
- const convos = await this.apiClient.getConversations();
114
- const newConvoIds = [];
115
- for (const c of convos) {
116
- if (!this.knownConversationIds.has(c.id)) {
117
- this.knownConversationIds.add(c.id);
118
- newConvoIds.push(c.id);
119
- }
120
- }
121
- if (newConvoIds.length === 0)
122
- return;
123
- console.log(`[canon-sdk] Discovered ${newConvoIds.length} new conversation(s) — fetching messages`);
124
- // Fetch and deliver pending messages from new conversations
125
- for (const convoId of newConvoIds) {
126
- try {
127
- const conversation = convos.find((item) => item.id === convoId);
128
- const page = await this.apiClient.getMessagesPage(convoId, 50);
129
- const messages = page.messages;
130
- const participationHistory = buildParticipationHistorySnapshots(messages, this.agentId);
131
- const dispatchable = await Promise.all(messages.map(async (message) => ({
132
- message,
133
- allow: await shouldDispatchInboundMessage(convoId, this.agentId, message, {
134
- conversationType: conversation?.type ?? 'unknown',
135
- behavior: page.behavior,
136
- recentHumanCount: participationHistory.get(message.id)?.recentHumanCount,
137
- consecutiveAgentTurns: participationHistory.get(message.id)?.consecutiveAgentTurns,
138
- currentAgentStreakStartedByHuman: participationHistory.get(message.id)?.currentAgentStreakStartedByHuman,
139
- }),
140
- })));
141
- const newMessages = dispatchable
142
- .filter((entry) => entry.allow)
143
- .map((entry) => entry.message);
144
- for (const msg of newMessages) {
145
- this.debouncer.add(convoId, msg);
146
- }
147
- }
148
- catch {
149
- // Will be picked up after reconnect
150
- }
151
- }
152
- }
153
- catch {
154
- // Non-fatal — will retry next interval
155
- }
156
- }
157
95
  }
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type { AgentClientType, CanonMessage, CanonConversation, AgentContext, CanonResolvedWorkSession, CanonWorkSession, CanonWorkSessionContext, CanonWorkSessionConversationRole, CanonWorkSessionDisclosureMode, CanonWorkSessionParticipant, CanonWorkSessionStatus, CreateWorkSessionOptions, SendLinkedMessageOptions, SendLinkedMessageResult, SendMessageOptions, CreateConversationOptions, TurnLifecycleState, UpdateWorkSessionConversationOptions, } from '@canonmsg/core';
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
2
  import type { 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;
@@ -106,6 +106,7 @@ export interface CanonAgentOptions {
106
106
  apiKey: string;
107
107
  baseUrl?: string;
108
108
  streamUrl?: string;
109
+ /** `auto` now resolves to SSE; use `polling` only as an explicit fallback. */
109
110
  deliveryMode?: DeliveryMode;
110
111
  pollingIntervalMs?: number;
111
112
  debounceMs?: number;
@@ -116,9 +117,12 @@ export interface CanonAgentOptions {
116
117
  sessions?: SessionOptions;
117
118
  /** Agent client type for capability detection. Defaults to 'generic'. */
118
119
  clientType?: import('@canonmsg/core').AgentClientType;
120
+ /** Optional runtime descriptor published to Canon for setup/live UI rendering. */
121
+ runtimeDescriptor?: import('@canonmsg/core').CanonRuntimeDescriptor;
119
122
  /**
120
123
  * Enable RTDB session-state reporting. Off by default.
121
124
  * Turn-state reporting is automatic while handlers run.
122
125
  */
123
126
  sessionState?: boolean;
124
127
  }
128
+ export type ContactRequestHandler = (request: import('@canonmsg/core').CanonContactRequest) => void | Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "0.8.2",
3
+ "version": "0.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": "^0.7.3"
31
+ "@canonmsg/core": "^0.8.0"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"