@canonmsg/agent-sdk 0.10.2 → 1.0.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
@@ -12,9 +12,9 @@ const agent = new CanonAgent({
12
12
  historyLimit: 30,
13
13
  });
14
14
 
15
- agent.on('message', async ({ messages, history, reply }) => {
15
+ agent.on('message', async ({ messages, history, replyFinal }) => {
16
16
  const response = await callMyLLM(messages, history);
17
- await reply(response);
17
+ await replyFinal(response);
18
18
  });
19
19
 
20
20
  await agent.start();
@@ -35,8 +35,7 @@ No additional dependencies required — the SDK uses native `fetch` and `Readabl
35
35
  | `apiKey` | `string` | **required** | API key obtained after agent registration approval |
36
36
  | `baseUrl` | `string` | Canon production URL | Override the API base URL |
37
37
  | `streamUrl` | `string` | Canon stream service URL | Override the SSE stream URL |
38
- | `deliveryMode` | `'auto' \| 'sse' \| 'polling'` | `'auto'` | How the SDK receives new messages |
39
- | `pollingIntervalMs` | `number` | `3000` | Polling interval in milliseconds (polling mode only) |
38
+ | `deliveryMode` | `'auto' \| 'sse'` | `'auto'` | How the SDK receives new messages |
40
39
  | `debounceMs` | `number` | `2000` | Batching window for incoming messages per conversation |
41
40
  | `historyLimit` | `number` | `50` | Number of historical messages to fetch (max 100) |
42
41
  | `sessions` | `SessionOptions` | `undefined` | Enable per-conversation session queues and persistent metadata |
@@ -110,11 +109,11 @@ Current rules of thumb:
110
109
 
111
110
  ## Delivery Modes
112
111
 
113
- The SDK supports three delivery modes for receiving messages:
112
+ The SDK supports SSE-backed delivery modes for receiving messages:
114
113
 
115
114
  ### `auto` (default)
116
115
 
117
- Uses `sse`. Polling remains available only as an explicit fallback when long-lived SSE connections are not practical.
116
+ Uses `sse`.
118
117
 
119
118
  ### `sse`
120
119
 
@@ -122,23 +121,16 @@ Connects to Canon's SSE stream service for instant message delivery. A single co
122
121
 
123
122
  Best for: agents in a small-to-medium number of active conversations where low latency matters.
124
123
 
125
- ### `polling`
126
-
127
- Periodically calls the REST API to discover new messages. Latency is bounded by `pollingIntervalMs`.
128
-
129
- Best for: agents in many conversations, or environments where long-lived connections are not practical.
130
-
131
124
  ## Message Handler
132
125
 
133
126
  The `message` event handler receives a context object with:
134
127
 
135
128
  | Field | Type | Description |
136
129
  |---|---|---|
137
- | `messages` | `SDKMessage[]` | New messages in this batch (debounced, sorted by time) |
138
- | `history` | `SDKMessage[]` | Last N messages before these new ones |
130
+ | `messages` | `CanonMessage[]` | New messages in this batch (debounced, sorted by time) |
131
+ | `history` | `CanonMessage[]` | Last N messages before these new ones |
139
132
  | `conversationId` | `string` | The conversation these messages belong to |
140
- | `conversation` | `SDKConversation` | Full conversation metadata |
141
- | `reply` | `(text: string, options?) => Promise<{ messageId: string }>` | Convenience alias for `replyFinal` |
133
+ | `conversation` | `CanonConversation` | Full conversation metadata |
142
134
  | `replyFinal` | `(text: string, options?) => Promise<{ messageId: string }>` | Send the durable final reply for a turn |
143
135
  | `replyProgress` | `(text: string, options?) => Promise<{ turnId: string; durable: boolean; messageId: string \| null }>` | Update the live turn progress; add `durable: true` to also persist it |
144
136
  | `agent` | `AgentContext` | Trusted Canon agent identity and access context |
@@ -255,9 +247,9 @@ The SDK exports `CanonApiError` for typed error handling:
255
247
  ```typescript
256
248
  import { CanonAgent, CanonApiError } from '@canonmsg/agent-sdk';
257
249
 
258
- agent.on('message', async ({ messages, reply }) => {
250
+ agent.on('message', async ({ messages, replyFinal }) => {
259
251
  try {
260
- await reply('Hello!');
252
+ await replyFinal('Hello!');
261
253
  } catch (err) {
262
254
  if (err instanceof CanonApiError) {
263
255
  console.error(`API error ${err.status}: ${err.message}`);
@@ -23,7 +23,6 @@ export declare class CanonAgent {
23
23
  private apiClient;
24
24
  private authManager;
25
25
  private debouncer;
26
- private pollingManager;
27
26
  private realtimeManager;
28
27
  private sessionManager;
29
28
  private handler;
@@ -3,7 +3,6 @@ import { randomUUID } from 'node:crypto';
3
3
  import { AuthManager } from './auth.js';
4
4
  import { Debouncer } from './debouncer.js';
5
5
  import { materializeMessageMedia, uploadMediaFile, } from './media.js';
6
- import { PollingManager } from './polling.js';
7
6
  import { SessionManager } from './session-manager.js';
8
7
  const AGENT_RUNTIME_HEARTBEAT_MS = 30_000;
9
8
  const SDK_RUNTIME_CAPABILITIES = {
@@ -27,7 +26,6 @@ export class CanonAgent {
27
26
  apiClient;
28
27
  authManager;
29
28
  debouncer;
30
- pollingManager = null;
31
29
  realtimeManager = null;
32
30
  sessionManager = null;
33
31
  handler = null;
@@ -49,7 +47,6 @@ export class CanonAgent {
49
47
  this.options = {
50
48
  baseUrl: 'https://api-6m6mlelskq-uc.a.run.app',
51
49
  deliveryMode: 'auto',
52
- pollingIntervalMs: 3000,
53
50
  debounceMs: 2000,
54
51
  historyLimit: 50,
55
52
  autoMarkRead: true,
@@ -187,6 +184,9 @@ export class CanonAgent {
187
184
  mode = 'sse';
188
185
  console.log(`[canon-sdk] Auto-selected ${mode} mode (${conversations.length} conversations)`);
189
186
  }
187
+ if (mode !== 'sse') {
188
+ throw new Error(`Unsupported deliveryMode: ${mode}. Use 'auto' or 'sse'.`);
189
+ }
190
190
  // 3b. Fetch agent context (identity, owner, access level)
191
191
  try {
192
192
  this.agentContext = await this.apiClient.getAgentMe();
@@ -209,41 +209,34 @@ export class CanonAgent {
209
209
  }
210
210
  }
211
211
  // 4. Start delivery
212
- if (mode === 'sse' || mode === 'realtime') {
213
- const { RealtimeManager } = await import('./realtime.js');
214
- const rtm = new RealtimeManager(this.options.apiKey, this.debouncer, agentId, this.options.streamUrl, this.apiClient);
215
- rtm.setOnAgentContext((ctx) => {
216
- this.agentContext = ctx;
217
- });
218
- rtm.setContactRequestHandlers({
219
- onContactRequest: (request) => {
220
- void this.handleContactRequestEvent(this.contactRequestHandler, request);
221
- },
222
- onContactApproved: (request) => {
223
- void this.handleContactRequestEvent(this.contactApprovedHandler, request);
224
- },
225
- });
226
- rtm.setContactGraphHandlers({
227
- onContactAdded: (payload) => {
228
- void this.handleContactGraphEvent(this.contactAddedHandler, payload);
229
- },
230
- onContactRemoved: (payload) => {
231
- void this.handleContactGraphEvent(this.contactRemovedHandler, payload);
232
- },
233
- });
234
- rtm.setConnectionHandlers({
235
- onConnected: () => this.startRuntimeHeartbeat(),
236
- onDisconnected: () => this.stopRuntimeHeartbeat(),
237
- });
238
- this.realtimeManager = rtm;
239
- await rtm.start();
240
- console.log('[canon-sdk] SSE stream started');
241
- }
242
- else {
243
- this.pollingManager = new PollingManager(this.apiClient, this.debouncer, agentId, this.options.pollingIntervalMs, () => this.startRuntimeHeartbeat(), () => this.stopRuntimeHeartbeat());
244
- await this.pollingManager.start();
245
- console.log(`[canon-sdk] Polling started (interval: ${this.options.pollingIntervalMs}ms)`);
246
- }
212
+ const { RealtimeManager } = await import('./realtime.js');
213
+ const rtm = new RealtimeManager(this.options.apiKey, this.debouncer, agentId, this.options.streamUrl, this.apiClient);
214
+ rtm.setOnAgentContext((ctx) => {
215
+ this.agentContext = ctx;
216
+ });
217
+ rtm.setContactRequestHandlers({
218
+ onContactRequest: (request) => {
219
+ void this.handleContactRequestEvent(this.contactRequestHandler, request);
220
+ },
221
+ onContactApproved: (request) => {
222
+ void this.handleContactRequestEvent(this.contactApprovedHandler, request);
223
+ },
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
+ });
233
+ rtm.setConnectionHandlers({
234
+ onConnected: () => this.startRuntimeHeartbeat(),
235
+ onDisconnected: () => this.stopRuntimeHeartbeat(),
236
+ });
237
+ this.realtimeManager = rtm;
238
+ await rtm.start();
239
+ console.log('[canon-sdk] SSE stream started');
247
240
  }
248
241
  async createConversation(options) {
249
242
  return this.apiClient.createConversation(options);
@@ -317,7 +310,6 @@ export class CanonAgent {
317
310
  }
318
311
  }
319
312
  await this.clearAgentRuntime();
320
- this.pollingManager?.stop();
321
313
  this.realtimeManager?.stop();
322
314
  this.sessionManager?.destroy();
323
315
  this.authManager.destroy();
@@ -591,7 +583,6 @@ export class CanonAgent {
591
583
  history,
592
584
  conversationId,
593
585
  conversation,
594
- reply: replyFinal,
595
586
  replyFinal,
596
587
  replyProgress,
597
588
  deleteMessage,
package/dist/index.d.ts CHANGED
@@ -7,4 +7,4 @@ export { getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthro
7
7
  export type { AnthropicImageBlock, AnthropicImageMimeType, MaterializeMediaOptions, MaterializedCanonAttachment, ReplyWithFileOptions, UploadMediaFileOptions, } from './media.js';
8
8
  export type { SessionConfig, Session } from './session-manager.js';
9
9
  export type { AgentContext, CanonContactRequest, CanonMessage, CanonConversation, CanonResolvedWorkSession, CanonWorkSession, CanonWorkSessionContext, CanonWorkSessionConversationRole, CanonWorkSessionDisclosureMode, CanonWorkSessionParticipant, CanonWorkSessionStatus, CreateWorkSessionOptions, SendLinkedMessageOptions, SendLinkedMessageResult, SendMessageOptions, CreateConversationOptions, UpdateWorkSessionConversationOptions, } from '@canonmsg/core';
10
- export type { SDKMessage, SDKConversation, CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, ContactRequestHandler, MessageHandler, MessageHandlerContext, ProgressMessageOptions, ProgressMessageResult, ReachOutOptions, ReachOutResult, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
10
+ export type { CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, ContactRequestHandler, MessageHandler, MessageHandlerContext, ProgressMessageOptions, ProgressMessageResult, ReachOutOptions, ReachOutResult, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
@@ -4,15 +4,6 @@ function normalizeRuntimeTurnState(value) {
4
4
  if (turnState) {
5
5
  return { state: turnState.state };
6
6
  }
7
- if (!value || typeof value !== 'object')
8
- return null;
9
- const state = value.state;
10
- if (state === 'running') {
11
- return { state: 'streaming' };
12
- }
13
- if (state === 'requires_action') {
14
- return { state: 'waiting_input' };
15
- }
16
7
  return null;
17
8
  }
18
9
  export async function shouldDispatchInboundMessage(conversationId, agentId, message, options) {
package/dist/types.d.ts CHANGED
@@ -1,8 +1,6 @@
1
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
2
  import type { AddMemberResult, CanonMessage, CanonConversation, CreateWorkSessionOptions, SendMessageOptions, UpdateWorkSessionConversationOptions } from '@canonmsg/core';
3
3
  import type { MaterializeMediaOptions, MaterializedCanonAttachment, ReplyWithFileOptions, UploadMediaFileOptions } from './media.js';
4
- export type SDKMessage = CanonMessage;
5
- export type SDKConversation = CanonConversation;
6
4
  export interface ProgressMessageOptions extends SendMessageOptions {
7
5
  /**
8
6
  * Persist the progress update to Firestore.
@@ -23,7 +21,7 @@ export interface SessionInfo {
23
21
  /** Session ID (= conversationId) */
24
22
  id: string;
25
23
  /** All accumulated messages for this conversation (within the context limit), oldest first */
26
- messages: SDKMessage[];
24
+ messages: CanonMessage[];
27
25
  /** Arbitrary per-session state the agent can read/write across handler calls */
28
26
  metadata: Record<string, unknown>;
29
27
  queueDepth?: number;
@@ -37,13 +35,10 @@ export interface TurnController {
37
35
  setWaitingInput: (text?: string) => Promise<void>;
38
36
  }
39
37
  export interface MessageHandlerContext {
40
- messages: SDKMessage[];
41
- history: SDKMessage[];
38
+ messages: CanonMessage[];
39
+ history: CanonMessage[];
42
40
  conversationId: string;
43
- conversation: SDKConversation;
44
- reply: (text: string, options?: SendMessageOptions) => Promise<{
45
- messageId: string;
46
- }>;
41
+ conversation: CanonConversation;
47
42
  replyFinal: (text: string, options?: SendMessageOptions) => Promise<{
48
43
  messageId: string;
49
44
  }>;
@@ -82,7 +77,7 @@ export interface MessageHandlerContext {
82
77
  activeWorkSessions?: import('@canonmsg/core').CanonWorkSessionContext[];
83
78
  /** Canon-managed local media access for the current conversation. */
84
79
  media: {
85
- materialize: (message?: SDKMessage, options?: Omit<MaterializeMediaOptions, 'agentId' | 'conversationId' | 'messageId'>) => Promise<MaterializedCanonAttachment[]>;
80
+ materialize: (message?: CanonMessage, options?: Omit<MaterializeMediaOptions, 'agentId' | 'conversationId' | 'messageId'>) => Promise<MaterializedCanonAttachment[]>;
86
81
  uploadFile: (filePath: string, options?: UploadMediaFileOptions) => Promise<{
87
82
  url: string;
88
83
  attachment: import('@canonmsg/core').MediaAttachment;
@@ -107,14 +102,13 @@ export interface SessionOptions {
107
102
  /** Evict idle sessions after this many ms (default: 3600000 = 1h) */
108
103
  idleTimeoutMs?: number;
109
104
  }
110
- export type DeliveryMode = 'auto' | 'sse' | 'realtime' | 'polling';
105
+ export type DeliveryMode = 'auto' | 'sse';
111
106
  export interface CanonAgentOptions {
112
107
  apiKey: string;
113
108
  baseUrl?: string;
114
109
  streamUrl?: string;
115
- /** `auto` now resolves to SSE; use `polling` only as an explicit fallback. */
110
+ /** `auto` resolves to SSE. */
116
111
  deliveryMode?: DeliveryMode;
117
- pollingIntervalMs?: number;
118
112
  debounceMs?: number;
119
113
  historyLimit?: number;
120
114
  /** Automatically mark conversations as read when handling messages (default: true) */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "0.10.2",
3
+ "version": "1.0.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.14.0"
31
+ "@canonmsg/core": "^0.15.0"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"
package/dist/polling.d.ts DELETED
@@ -1,18 +0,0 @@
1
- import { CanonClient } from '@canonmsg/core';
2
- import { Debouncer } from './debouncer.js';
3
- export declare class PollingManager {
4
- private apiClient;
5
- private debouncer;
6
- private agentId;
7
- private pollingIntervalMs;
8
- private onHealthy;
9
- private onUnhealthy;
10
- private lastSeenTimestamps;
11
- private pollTimer;
12
- private running;
13
- constructor(apiClient: CanonClient, debouncer: Debouncer, agentId: string, pollingIntervalMs: number, onHealthy?: () => void, onUnhealthy?: () => void);
14
- start(): Promise<void>;
15
- private poll;
16
- private findActiveConversations;
17
- stop(): void;
18
- }
package/dist/polling.js DELETED
@@ -1,99 +0,0 @@
1
- import { buildParticipationHistorySnapshots } from './policy-history.js';
2
- import { shouldDispatchInboundMessage } from './turn-filter.js';
3
- export class PollingManager {
4
- apiClient;
5
- debouncer;
6
- agentId;
7
- pollingIntervalMs;
8
- onHealthy;
9
- onUnhealthy;
10
- lastSeenTimestamps = new Map();
11
- pollTimer = null;
12
- running = false;
13
- constructor(apiClient, debouncer, agentId, pollingIntervalMs, onHealthy, onUnhealthy) {
14
- this.apiClient = apiClient;
15
- this.debouncer = debouncer;
16
- this.agentId = agentId;
17
- this.pollingIntervalMs = pollingIntervalMs;
18
- this.onHealthy = onHealthy ?? null;
19
- this.onUnhealthy = onUnhealthy ?? null;
20
- }
21
- async start() {
22
- this.running = true;
23
- // Initialize: mark current time as baseline (only respond to messages after start)
24
- const now = Date.now();
25
- const conversations = await this.apiClient.getConversations();
26
- for (const convo of conversations) {
27
- this.lastSeenTimestamps.set(convo.id, now);
28
- }
29
- this.onHealthy?.();
30
- // Start polling
31
- this.pollTimer = setInterval(() => this.poll(), this.pollingIntervalMs);
32
- }
33
- async poll() {
34
- if (!this.running)
35
- return;
36
- try {
37
- const conversations = await this.apiClient.getConversations();
38
- this.onHealthy?.();
39
- const activeConvos = this.findActiveConversations(conversations);
40
- await Promise.all(activeConvos.map(async (convo) => {
41
- try {
42
- const page = await this.apiClient.getMessagesPage(convo.id, 50);
43
- const messages = page.messages;
44
- const participationHistory = buildParticipationHistorySnapshots(messages, this.agentId);
45
- // Filter to only new messages (after lastSeen, not from self)
46
- const lastSeen = this.lastSeenTimestamps.get(convo.id) || 0;
47
- const newMessages = messages.filter((m) => {
48
- const msgTime = new Date(m.createdAt).getTime();
49
- return msgTime > lastSeen && m.senderId !== this.agentId;
50
- });
51
- const dispatchable = await Promise.all(newMessages.map(async (message) => ({
52
- message,
53
- allow: await shouldDispatchInboundMessage(convo.id, this.agentId, message, {
54
- conversationType: convo.type,
55
- behavior: page.behavior,
56
- recentHumanCount: participationHistory.get(message.id)?.recentHumanCount,
57
- consecutiveAgentTurns: participationHistory.get(message.id)?.consecutiveAgentTurns,
58
- currentAgentStreakStartedByHuman: participationHistory.get(message.id)?.currentAgentStreakStartedByHuman,
59
- }),
60
- })));
61
- for (const msg of dispatchable.filter((entry) => entry.allow).map((entry) => entry.message)) {
62
- this.debouncer.add(convo.id, msg);
63
- }
64
- // Update lastSeen to latest message timestamp
65
- if (messages.length > 0) {
66
- const latestTime = Math.max(...messages.map((m) => new Date(m.createdAt).getTime()));
67
- this.lastSeenTimestamps.set(convo.id, latestTime);
68
- }
69
- }
70
- catch (err) {
71
- console.error(`[canon-sdk] Failed to fetch messages for ${convo.id}:`, err);
72
- }
73
- }));
74
- }
75
- catch (err) {
76
- this.onUnhealthy?.();
77
- console.error('[canon-sdk] Polling error:', err);
78
- }
79
- }
80
- findActiveConversations(conversations) {
81
- return conversations.filter((convo) => {
82
- if (!convo.lastMessage)
83
- return false;
84
- // Skip if agent was last sender
85
- if (convo.lastMessage.senderId === this.agentId)
86
- return false;
87
- const lastMsgTime = new Date(convo.lastMessage.timestamp).getTime();
88
- const lastSeen = this.lastSeenTimestamps.get(convo.id) || 0;
89
- return lastMsgTime > lastSeen;
90
- });
91
- }
92
- stop() {
93
- this.running = false;
94
- if (this.pollTimer) {
95
- clearInterval(this.pollTimer);
96
- this.pollTimer = null;
97
- }
98
- }
99
- }