@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 +10 -18
- package/dist/canon-agent.d.ts +0 -1
- package/dist/canon-agent.js +31 -40
- package/dist/index.d.ts +1 -1
- package/dist/turn-filter.js +0 -9
- package/dist/types.d.ts +7 -13
- package/package.json +2 -2
- package/dist/polling.d.ts +0 -18
- package/dist/polling.js +0 -99
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,
|
|
15
|
+
agent.on('message', async ({ messages, history, replyFinal }) => {
|
|
16
16
|
const response = await callMyLLM(messages, history);
|
|
17
|
-
await
|
|
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'
|
|
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
|
|
112
|
+
The SDK supports SSE-backed delivery modes for receiving messages:
|
|
114
113
|
|
|
115
114
|
### `auto` (default)
|
|
116
115
|
|
|
117
|
-
Uses `sse`.
|
|
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` | `
|
|
138
|
-
| `history` | `
|
|
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` | `
|
|
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,
|
|
250
|
+
agent.on('message', async ({ messages, replyFinal }) => {
|
|
259
251
|
try {
|
|
260
|
-
await
|
|
252
|
+
await replyFinal('Hello!');
|
|
261
253
|
} catch (err) {
|
|
262
254
|
if (err instanceof CanonApiError) {
|
|
263
255
|
console.error(`API error ${err.status}: ${err.message}`);
|
package/dist/canon-agent.d.ts
CHANGED
package/dist/canon-agent.js
CHANGED
|
@@ -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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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 {
|
|
10
|
+
export type { CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, ContactRequestHandler, MessageHandler, MessageHandlerContext, ProgressMessageOptions, ProgressMessageResult, ReachOutOptions, ReachOutResult, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
|
package/dist/turn-filter.js
CHANGED
|
@@ -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:
|
|
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:
|
|
41
|
-
history:
|
|
38
|
+
messages: CanonMessage[];
|
|
39
|
+
history: CanonMessage[];
|
|
42
40
|
conversationId: string;
|
|
43
|
-
conversation:
|
|
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?:
|
|
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'
|
|
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`
|
|
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.
|
|
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.
|
|
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
|
-
}
|