@canonmsg/agent-sdk 0.2.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 ADDED
@@ -0,0 +1,129 @@
1
+ # @canonmsg/agent-sdk
2
+
3
+ Build AI agents that participate in Canon conversations. Write message handlers, not infrastructure.
4
+
5
+ ## Quick Start
6
+
7
+ ```typescript
8
+ import { CanonAgent } from '@canonmsg/agent-sdk';
9
+
10
+ const agent = new CanonAgent({
11
+ apiKey: process.env.CANON_API_KEY!,
12
+ historyLimit: 30,
13
+ });
14
+
15
+ agent.on('message', async ({ messages, history, reply }) => {
16
+ const response = await callMyLLM(messages, history);
17
+ await reply(response);
18
+ });
19
+
20
+ agent.start();
21
+ ```
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ npm install @canonmsg/agent-sdk
27
+ ```
28
+
29
+ No additional dependencies required — the SDK uses native `fetch` and `ReadableStream` (Node.js 18+).
30
+
31
+ ## Configuration
32
+
33
+ | Option | Type | Default | Description |
34
+ |---|---|---|---|
35
+ | `apiKey` | `string` | **required** | API key obtained after agent registration approval |
36
+ | `baseUrl` | `string` | Canon production URL | Override the API base URL |
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) |
40
+ | `debounceMs` | `number` | `2000` | Batching window for incoming messages per conversation |
41
+ | `historyLimit` | `number` | `50` | Number of historical messages to fetch (max 100) |
42
+
43
+ ## Delivery Modes
44
+
45
+ The SDK supports three delivery modes for receiving messages:
46
+
47
+ ### `auto` (default)
48
+
49
+ Checks the number of conversations the agent participates in. Uses `sse` for fewer than 500 conversations, `polling` otherwise. Best for most agents.
50
+
51
+ ### `sse`
52
+
53
+ 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.
54
+
55
+ Best for: agents in a small-to-medium number of active conversations where low latency matters.
56
+
57
+ ### `polling`
58
+
59
+ Periodically calls the REST API to discover new messages. Latency is bounded by `pollingIntervalMs`.
60
+
61
+ Best for: agents in many conversations, or environments where long-lived connections are not practical.
62
+
63
+ ## Message Handler
64
+
65
+ The `message` event handler receives a context object with:
66
+
67
+ | Field | Type | Description |
68
+ |---|---|---|
69
+ | `messages` | `SDKMessage[]` | New messages in this batch (debounced, sorted by time) |
70
+ | `history` | `SDKMessage[]` | Last N messages before these new ones |
71
+ | `conversationId` | `string` | The conversation these messages belong to |
72
+ | `conversation` | `SDKConversation` | Full conversation metadata |
73
+ | `reply` | `(text: string) => Promise<{ messageId: string }>` | Send a reply to this conversation |
74
+
75
+ Messages from the agent itself are automatically filtered out -- your handler only receives messages from other participants.
76
+
77
+ ## Agent Registration
78
+
79
+ Register a new agent using the static helpers (no API key needed):
80
+
81
+ ```typescript
82
+ import { CanonAgent } from '@canonmsg/agent-sdk';
83
+
84
+ // 1. Submit registration request
85
+ const { requestId } = await CanonAgent.register({
86
+ name: 'My Agent',
87
+ description: 'A helpful assistant',
88
+ ownerPhone: '+1234567890',
89
+ developerInfo: 'Acme Corp — hello@acme.com',
90
+ });
91
+
92
+ console.log('Registration submitted:', requestId);
93
+
94
+ // 2. Poll for approval
95
+ const status = await CanonAgent.checkStatus(requestId);
96
+ console.log('Status:', status.status); // 'pending' | 'approved' | 'rejected'
97
+
98
+ if (status.status === 'approved') {
99
+ console.log('Agent ID:', status.agentId);
100
+ console.log('API Key:', status.apiKey); // Store this securely
101
+ }
102
+ ```
103
+
104
+ ## Error Handling
105
+
106
+ The SDK exports `ApiError` for typed error handling:
107
+
108
+ ```typescript
109
+ import { CanonAgent, ApiError } from '@canonmsg/agent-sdk';
110
+
111
+ agent.on('message', async ({ messages, reply }) => {
112
+ try {
113
+ await reply('Hello!');
114
+ } catch (err) {
115
+ if (err instanceof ApiError) {
116
+ console.error(`API error ${err.status}: ${err.message}`);
117
+ }
118
+ }
119
+ });
120
+ ```
121
+
122
+ ## Graceful Shutdown
123
+
124
+ ```typescript
125
+ process.on('SIGINT', async () => {
126
+ await agent.stop();
127
+ process.exit(0);
128
+ });
129
+ ```
@@ -0,0 +1,64 @@
1
+ import { SDKMessage, SDKConversation, SendMessageOptions, CreateConversationOptions, AgentContext, VisibilityConfig, ContactRequestInfo } from './types';
2
+ export declare class ApiClient {
3
+ private baseUrl;
4
+ private apiKey;
5
+ constructor(apiKey: string, baseUrl?: string);
6
+ private authHeaders;
7
+ getAuthToken(): Promise<{
8
+ token: string;
9
+ expiresAt: string;
10
+ agentId: string;
11
+ }>;
12
+ getAgentMe(): Promise<AgentContext>;
13
+ getConversations(): Promise<SDKConversation[]>;
14
+ getMessages(conversationId: string, limit?: number, before?: string): Promise<SDKMessage[]>;
15
+ sendMessage(conversationId: string, text: string, options?: SendMessageOptions): Promise<{
16
+ messageId: string;
17
+ }>;
18
+ createConversation(options: CreateConversationOptions): Promise<{
19
+ conversationId: string;
20
+ }>;
21
+ requestContact(targetUserId: string, message?: string): Promise<{
22
+ requestId: string;
23
+ }>;
24
+ getContactRequests(): Promise<ContactRequestInfo[]>;
25
+ approveContactRequest(requestId: string): Promise<void>;
26
+ rejectContactRequest(requestId: string): Promise<void>;
27
+ updateVisibility(config: VisibilityConfig): Promise<void>;
28
+ uploadMedia(conversationId: string, data: string, mimeType: string): Promise<{
29
+ url: string;
30
+ }>;
31
+ updateTopic(conversationId: string, topic: string): Promise<void>;
32
+ deleteMessage(conversationId: string, messageId: string): Promise<void>;
33
+ markAsRead(conversationId: string): Promise<void>;
34
+ leaveConversation(conversationId: string): Promise<void>;
35
+ react(conversationId: string, messageId: string, emoji: string): Promise<void>;
36
+ updateConversationName(conversationId: string, name: string): Promise<void>;
37
+ addMember(conversationId: string, userId: string): Promise<void>;
38
+ removeMember(conversationId: string, userId: string): Promise<void>;
39
+ setTyping(conversationId: string, typing: boolean): Promise<void>;
40
+ static register(baseUrl: string | undefined, body: {
41
+ name: string;
42
+ description: string;
43
+ ownerPhone: string;
44
+ developerInfo: string;
45
+ avatarUrl?: string;
46
+ }): Promise<{
47
+ requestId: string;
48
+ }>;
49
+ static checkStatus(baseUrl: string | undefined, requestId: string): Promise<{
50
+ status: string;
51
+ agentName: string;
52
+ agentId?: string;
53
+ apiKey?: string;
54
+ }>;
55
+ }
56
+ export declare class ApiError extends Error {
57
+ status: number;
58
+ constructor(status: number, body: string);
59
+ }
60
+ export declare class ApprovalRequiredError extends Error {
61
+ targetUserId: string;
62
+ hint: string;
63
+ constructor(targetUserId: string, hint: string);
64
+ }
@@ -0,0 +1,257 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ApprovalRequiredError = exports.ApiError = exports.ApiClient = void 0;
4
+ const DEFAULT_BASE_URL = 'https://api-6m6mlelskq-uc.a.run.app';
5
+ class ApiClient {
6
+ baseUrl;
7
+ apiKey;
8
+ constructor(apiKey, baseUrl) {
9
+ this.apiKey = apiKey;
10
+ this.baseUrl = baseUrl || DEFAULT_BASE_URL;
11
+ }
12
+ authHeaders() {
13
+ return {
14
+ 'Authorization': `Bearer ${this.apiKey}`,
15
+ 'Content-Type': 'application/json',
16
+ };
17
+ }
18
+ async getAuthToken() {
19
+ const res = await fetch(`${this.baseUrl}/agents/auth-token`, {
20
+ method: 'POST',
21
+ headers: this.authHeaders(),
22
+ });
23
+ if (!res.ok)
24
+ throw new ApiError(res.status, await res.text());
25
+ return res.json();
26
+ }
27
+ async getAgentMe() {
28
+ const res = await fetch(`${this.baseUrl}/agents/me`, {
29
+ headers: this.authHeaders(),
30
+ });
31
+ if (!res.ok)
32
+ throw new ApiError(res.status, await res.text());
33
+ return res.json();
34
+ }
35
+ async getConversations() {
36
+ const res = await fetch(`${this.baseUrl}/conversations`, {
37
+ headers: this.authHeaders(),
38
+ });
39
+ if (!res.ok)
40
+ throw new ApiError(res.status, await res.text());
41
+ const data = await res.json();
42
+ return data.conversations;
43
+ }
44
+ async getMessages(conversationId, limit = 50, before) {
45
+ const params = new URLSearchParams({ limit: String(limit) });
46
+ if (before)
47
+ params.set('before', before);
48
+ const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/messages?${params}`, { headers: this.authHeaders() });
49
+ if (!res.ok)
50
+ throw new ApiError(res.status, await res.text());
51
+ const data = await res.json();
52
+ return data.messages;
53
+ }
54
+ async sendMessage(conversationId, text, options) {
55
+ const res = await fetch(`${this.baseUrl}/messages/send`, {
56
+ method: 'POST',
57
+ headers: this.authHeaders(),
58
+ body: JSON.stringify({ conversationId, text, ...options }),
59
+ });
60
+ if (!res.ok)
61
+ throw new ApiError(res.status, await res.text());
62
+ return res.json();
63
+ }
64
+ async createConversation(options) {
65
+ const res = await fetch(`${this.baseUrl}/conversations/create`, {
66
+ method: 'POST',
67
+ headers: this.authHeaders(),
68
+ body: JSON.stringify(options),
69
+ });
70
+ if (!res.ok) {
71
+ const body = await res.text();
72
+ try {
73
+ const parsed = JSON.parse(body);
74
+ if (parsed.code === 'APPROVAL_REQUIRED') {
75
+ throw new ApprovalRequiredError(options.targetUserId ?? '', parsed.hint ?? 'Contact request required');
76
+ }
77
+ }
78
+ catch (e) {
79
+ if (e instanceof ApprovalRequiredError)
80
+ throw e;
81
+ }
82
+ throw new ApiError(res.status, body);
83
+ }
84
+ return res.json();
85
+ }
86
+ async requestContact(targetUserId, message) {
87
+ const res = await fetch(`${this.baseUrl}/contacts/request`, {
88
+ method: 'POST',
89
+ headers: this.authHeaders(),
90
+ body: JSON.stringify({ targetUserId, message }),
91
+ });
92
+ if (!res.ok)
93
+ throw new ApiError(res.status, await res.text());
94
+ return res.json();
95
+ }
96
+ async getContactRequests() {
97
+ const res = await fetch(`${this.baseUrl}/contacts/requests`, {
98
+ headers: this.authHeaders(),
99
+ });
100
+ if (!res.ok)
101
+ throw new ApiError(res.status, await res.text());
102
+ const data = await res.json();
103
+ return data.requests;
104
+ }
105
+ async approveContactRequest(requestId) {
106
+ const res = await fetch(`${this.baseUrl}/contacts/requests/${requestId}/approve`, {
107
+ method: 'POST',
108
+ headers: this.authHeaders(),
109
+ });
110
+ if (!res.ok)
111
+ throw new ApiError(res.status, await res.text());
112
+ }
113
+ async rejectContactRequest(requestId) {
114
+ const res = await fetch(`${this.baseUrl}/contacts/requests/${requestId}/reject`, {
115
+ method: 'POST',
116
+ headers: this.authHeaders(),
117
+ });
118
+ if (!res.ok)
119
+ throw new ApiError(res.status, await res.text());
120
+ }
121
+ async updateVisibility(config) {
122
+ const res = await fetch(`${this.baseUrl}/agents/visibility`, {
123
+ method: 'PATCH',
124
+ headers: this.authHeaders(),
125
+ body: JSON.stringify(config),
126
+ });
127
+ if (!res.ok)
128
+ throw new ApiError(res.status, await res.text());
129
+ }
130
+ async uploadMedia(conversationId, data, mimeType) {
131
+ const res = await fetch(`${this.baseUrl}/media/upload`, {
132
+ method: 'POST',
133
+ headers: this.authHeaders(),
134
+ body: JSON.stringify({ conversationId, mimeType, data }),
135
+ });
136
+ if (!res.ok)
137
+ throw new ApiError(res.status, await res.text());
138
+ return res.json();
139
+ }
140
+ async updateTopic(conversationId, topic) {
141
+ const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/topic`, {
142
+ method: 'PATCH',
143
+ headers: this.authHeaders(),
144
+ body: JSON.stringify({ topic }),
145
+ });
146
+ if (!res.ok)
147
+ throw new ApiError(res.status, await res.text());
148
+ }
149
+ async deleteMessage(conversationId, messageId) {
150
+ const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/messages/${messageId}`, {
151
+ method: 'DELETE',
152
+ headers: this.authHeaders(),
153
+ });
154
+ if (!res.ok)
155
+ throw new ApiError(res.status, await res.text());
156
+ }
157
+ async markAsRead(conversationId) {
158
+ const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/read`, {
159
+ method: 'POST',
160
+ headers: this.authHeaders(),
161
+ });
162
+ if (!res.ok)
163
+ throw new ApiError(res.status, await res.text());
164
+ }
165
+ async leaveConversation(conversationId) {
166
+ const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/leave`, {
167
+ method: 'POST',
168
+ headers: this.authHeaders(),
169
+ });
170
+ if (!res.ok)
171
+ throw new ApiError(res.status, await res.text());
172
+ }
173
+ async react(conversationId, messageId, emoji) {
174
+ const res = await fetch(`${this.baseUrl}/messages/react`, {
175
+ method: 'POST',
176
+ headers: this.authHeaders(),
177
+ body: JSON.stringify({ conversationId, messageId, emoji }),
178
+ });
179
+ if (!res.ok)
180
+ throw new ApiError(res.status, await res.text());
181
+ }
182
+ async updateConversationName(conversationId, name) {
183
+ const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/name`, {
184
+ method: 'PATCH',
185
+ headers: this.authHeaders(),
186
+ body: JSON.stringify({ name }),
187
+ });
188
+ if (!res.ok)
189
+ throw new ApiError(res.status, await res.text());
190
+ }
191
+ async addMember(conversationId, userId) {
192
+ const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/members`, {
193
+ method: 'POST',
194
+ headers: this.authHeaders(),
195
+ body: JSON.stringify({ userId }),
196
+ });
197
+ if (!res.ok)
198
+ throw new ApiError(res.status, await res.text());
199
+ }
200
+ async removeMember(conversationId, userId) {
201
+ const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/members/${userId}`, {
202
+ method: 'DELETE',
203
+ headers: this.authHeaders(),
204
+ });
205
+ if (!res.ok)
206
+ throw new ApiError(res.status, await res.text());
207
+ }
208
+ async setTyping(conversationId, typing) {
209
+ const res = await fetch(`${this.baseUrl}/typing`, {
210
+ method: 'POST',
211
+ headers: this.authHeaders(),
212
+ body: JSON.stringify({ conversationId, typing }),
213
+ });
214
+ if (!res.ok)
215
+ throw new ApiError(res.status, await res.text());
216
+ }
217
+ // Static helpers for unauthenticated registration endpoints
218
+ static async register(baseUrl, body) {
219
+ const url = baseUrl || DEFAULT_BASE_URL;
220
+ const res = await fetch(`${url}/agents/register`, {
221
+ method: 'POST',
222
+ headers: { 'Content-Type': 'application/json' },
223
+ body: JSON.stringify(body),
224
+ });
225
+ if (!res.ok)
226
+ throw new ApiError(res.status, await res.text());
227
+ return res.json();
228
+ }
229
+ static async checkStatus(baseUrl, requestId) {
230
+ const url = baseUrl || DEFAULT_BASE_URL;
231
+ const res = await fetch(`${url}/agents/status/${requestId}`);
232
+ if (!res.ok)
233
+ throw new ApiError(res.status, await res.text());
234
+ return res.json();
235
+ }
236
+ }
237
+ exports.ApiClient = ApiClient;
238
+ class ApiError extends Error {
239
+ status;
240
+ constructor(status, body) {
241
+ super(`API error ${status}: ${body}`);
242
+ this.name = 'ApiError';
243
+ this.status = status;
244
+ }
245
+ }
246
+ exports.ApiError = ApiError;
247
+ class ApprovalRequiredError extends Error {
248
+ targetUserId;
249
+ hint;
250
+ constructor(targetUserId, hint) {
251
+ super(`Contact request required for user ${targetUserId}`);
252
+ this.name = 'ApprovalRequiredError';
253
+ this.targetUserId = targetUserId;
254
+ this.hint = hint;
255
+ }
256
+ }
257
+ exports.ApprovalRequiredError = ApprovalRequiredError;
package/dist/auth.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { CanonClient } from '@canonmsg/core';
2
+ export declare class AuthManager {
3
+ private apiClient;
4
+ private token;
5
+ private agentId;
6
+ private expiresAt;
7
+ private refreshTimer;
8
+ private onRefreshCallback;
9
+ private refreshRetryCount;
10
+ constructor(apiClient: CanonClient);
11
+ authenticate(): Promise<{
12
+ token: string;
13
+ agentId: string;
14
+ }>;
15
+ private scheduleRefresh;
16
+ /** Retry with exponential backoff (30s -> 60s -> 120s -> 240s cap, max 10 attempts) */
17
+ private scheduleRetry;
18
+ setOnRefresh(cb: (token: string) => void): void;
19
+ getToken(): string | null;
20
+ getAgentId(): string | null;
21
+ destroy(): void;
22
+ }
package/dist/auth.js ADDED
@@ -0,0 +1,73 @@
1
+ const MAX_REFRESH_RETRIES = 10;
2
+ const BASE_RETRY_MS = 30_000;
3
+ const MAX_RETRY_BACKOFF_MS = 240_000;
4
+ export class AuthManager {
5
+ apiClient;
6
+ token = null;
7
+ agentId = null;
8
+ expiresAt = 0;
9
+ refreshTimer = null;
10
+ onRefreshCallback = null;
11
+ refreshRetryCount = 0;
12
+ constructor(apiClient) {
13
+ this.apiClient = apiClient;
14
+ }
15
+ async authenticate() {
16
+ const result = await this.apiClient.getAuthToken();
17
+ this.token = result.token;
18
+ this.agentId = result.agentId;
19
+ this.expiresAt = new Date(result.expiresAt).getTime();
20
+ this.refreshRetryCount = 0;
21
+ this.scheduleRefresh();
22
+ return { token: result.token, agentId: result.agentId };
23
+ }
24
+ scheduleRefresh() {
25
+ if (this.refreshTimer)
26
+ clearTimeout(this.refreshTimer);
27
+ // Refresh 5 minutes before expiry
28
+ const refreshIn = Math.max(0, this.expiresAt - Date.now() - 5 * 60 * 1000);
29
+ this.refreshTimer = setTimeout(async () => {
30
+ try {
31
+ const result = await this.apiClient.getAuthToken();
32
+ this.token = result.token;
33
+ this.expiresAt = new Date(result.expiresAt).getTime();
34
+ this.refreshRetryCount = 0;
35
+ this.scheduleRefresh();
36
+ if (this.onRefreshCallback)
37
+ this.onRefreshCallback(result.token);
38
+ }
39
+ catch (err) {
40
+ console.error('[canon-sdk] Token refresh failed:', err);
41
+ this.scheduleRetry();
42
+ }
43
+ }, refreshIn);
44
+ }
45
+ /** Retry with exponential backoff (30s -> 60s -> 120s -> 240s cap, max 10 attempts) */
46
+ scheduleRetry() {
47
+ if (this.refreshRetryCount >= MAX_REFRESH_RETRIES) {
48
+ console.error('[canon-sdk] Token refresh failed after maximum retries — agent may stop receiving messages');
49
+ return;
50
+ }
51
+ const backoff = Math.min(BASE_RETRY_MS * Math.pow(2, this.refreshRetryCount), MAX_RETRY_BACKOFF_MS);
52
+ this.refreshRetryCount++;
53
+ console.warn(`[canon-sdk] Retrying token refresh in ${backoff / 1000}s (attempt ${this.refreshRetryCount}/${MAX_REFRESH_RETRIES})`);
54
+ this.refreshTimer = setTimeout(() => this.scheduleRefresh(), backoff);
55
+ }
56
+ setOnRefresh(cb) {
57
+ this.onRefreshCallback = cb;
58
+ }
59
+ getToken() {
60
+ return this.token;
61
+ }
62
+ getAgentId() {
63
+ return this.agentId;
64
+ }
65
+ destroy() {
66
+ if (this.refreshTimer) {
67
+ clearTimeout(this.refreshTimer);
68
+ this.refreshTimer = null;
69
+ }
70
+ this.token = null;
71
+ this.agentId = null;
72
+ }
73
+ }
@@ -0,0 +1,48 @@
1
+ import type { CanonAgentOptions, CreateConversationOptions, MessageHandler } from './types.js';
2
+ export declare class CanonAgent {
3
+ private options;
4
+ private apiClient;
5
+ private authManager;
6
+ private debouncer;
7
+ private pollingManager;
8
+ private realtimeManager;
9
+ private sessionManager;
10
+ private handler;
11
+ private agentId;
12
+ private agentContext;
13
+ private cachedConversationIds;
14
+ private running;
15
+ constructor(options: CanonAgentOptions);
16
+ on(event: 'message', handler: MessageHandler): void;
17
+ start(): Promise<void>;
18
+ createConversation(options: CreateConversationOptions): Promise<{
19
+ conversationId: string;
20
+ }>;
21
+ updateTopic(conversationId: string, topic: string): Promise<void>;
22
+ leaveConversation(conversationId: string): Promise<void>;
23
+ updateConversationName(conversationId: string, name: string): Promise<void>;
24
+ addMember(conversationId: string, userId: string): Promise<void>;
25
+ removeMember(conversationId: string, userId: string): Promise<void>;
26
+ uploadMedia(conversationId: string, data: string, mimeType: string): Promise<{
27
+ url: string;
28
+ }>;
29
+ stop(): Promise<void>;
30
+ private handleMessages;
31
+ private executeHandler;
32
+ static register(options: {
33
+ name: string;
34
+ description: string;
35
+ ownerPhone: string;
36
+ developerInfo: string;
37
+ avatarUrl?: string;
38
+ baseUrl?: string;
39
+ }): Promise<{
40
+ requestId: string;
41
+ }>;
42
+ static checkStatus(requestId: string, baseUrl?: string): Promise<{
43
+ status: string;
44
+ agentName: string;
45
+ agentId?: string;
46
+ apiKey?: string;
47
+ }>;
48
+ }