@canonmsg/agent-sdk 3.4.4 → 5.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/dist/canon-agent.js +4 -8
- package/dist/realtime.d.ts +1 -23
- package/dist/realtime.js +5 -106
- package/package.json +6 -6
package/dist/canon-agent.js
CHANGED
|
@@ -1077,13 +1077,13 @@ export class CanonAgent {
|
|
|
1077
1077
|
* where the last-started agent's token wins in multi-agent processes.
|
|
1078
1078
|
*/
|
|
1079
1079
|
createRuntimeStatePublisher() {
|
|
1080
|
-
if (!this.agentId
|
|
1080
|
+
if (!this.agentId)
|
|
1081
1081
|
return null;
|
|
1082
1082
|
return createRuntimeStatePublisher({
|
|
1083
|
-
rtdb: this.rtdbHandle,
|
|
1084
1083
|
agentId: this.agentId,
|
|
1085
1084
|
clientType: this.options.clientType ?? 'generic',
|
|
1086
1085
|
hostMode: this.options.runtimeControlSurface === 'host',
|
|
1086
|
+
...(this.rtdbHandle ? { rtdb: this.rtdbHandle } : {}),
|
|
1087
1087
|
});
|
|
1088
1088
|
}
|
|
1089
1089
|
requireRuntimeStatePublisher() {
|
|
@@ -1393,11 +1393,7 @@ export class CanonAgent {
|
|
|
1393
1393
|
const deleteMessage = (messageId) => this.apiClient.deleteMessage(conversationId, messageId);
|
|
1394
1394
|
const markAsRead = () => this.apiClient.markAsRead(conversationId);
|
|
1395
1395
|
const leave = () => this.apiClient.leaveConversation(conversationId);
|
|
1396
|
-
|
|
1397
|
-
// (added/removed + reactions) is a bridge-era addition it doesn't use.
|
|
1398
|
-
const react = async (messageId, emoji) => {
|
|
1399
|
-
await this.apiClient.react(conversationId, messageId, emoji);
|
|
1400
|
-
};
|
|
1396
|
+
const react = (messageId, emoji) => this.apiClient.react(conversationId, messageId, emoji);
|
|
1401
1397
|
const addMember = (userId) => this.apiClient.addMember(conversationId, userId);
|
|
1402
1398
|
const removeMember = (userId) => this.apiClient.removeMember(conversationId, userId);
|
|
1403
1399
|
const sendContextualMessage = (target, text, options) => this.apiClient.sendContextualMessage({
|
|
@@ -1959,7 +1955,7 @@ export class CanonAgent {
|
|
|
1959
1955
|
await turnOutput.flush();
|
|
1960
1956
|
},
|
|
1961
1957
|
clear: async () => {
|
|
1962
|
-
await turnOutput.clear()
|
|
1958
|
+
await turnOutput.clear();
|
|
1963
1959
|
},
|
|
1964
1960
|
setTool: async (text) => {
|
|
1965
1961
|
await writeTurn('tool');
|
package/dist/realtime.d.ts
CHANGED
|
@@ -4,22 +4,15 @@ import { Debouncer } from './debouncer.js';
|
|
|
4
4
|
* Wraps @canonmsg/core's CanonStream with SDK-specific features:
|
|
5
5
|
* - Debouncer integration (message batching)
|
|
6
6
|
* - Agent context callback
|
|
7
|
-
* -
|
|
7
|
+
* - Connection/status callbacks
|
|
8
8
|
*/
|
|
9
9
|
export declare class RealtimeManager {
|
|
10
10
|
private debouncer;
|
|
11
11
|
private agentId;
|
|
12
|
-
private apiClient;
|
|
13
12
|
private stream;
|
|
14
13
|
private running;
|
|
15
14
|
/** Recent inbound message IDs (`conversationId:messageId`) for cross-flush dedupe. */
|
|
16
15
|
private readonly recentInboundMessageIds;
|
|
17
|
-
/** Latest handled inbound message timestamp per conversation. */
|
|
18
|
-
private readonly lastInboundMessageAtByConversation;
|
|
19
|
-
/** Lower bound for catch-up in conversations with no in-memory cursor. */
|
|
20
|
-
private readonly replaySyncStartedAt;
|
|
21
|
-
private replayCatchupInFlight;
|
|
22
|
-
private hasConnectedOnce;
|
|
23
16
|
private lastSseErrorKey;
|
|
24
17
|
private lastSseErrorAt;
|
|
25
18
|
private suppressedSseErrorCount;
|
|
@@ -34,24 +27,9 @@ export declare class RealtimeManager {
|
|
|
34
27
|
private onConnected;
|
|
35
28
|
private onDisconnected;
|
|
36
29
|
constructor(apiKey: string, debouncer: Debouncer, agentId: string, streamUrl?: string, apiClient?: CanonClient);
|
|
37
|
-
private queueReplayCatchup;
|
|
38
30
|
private hasSeenInboundMessage;
|
|
39
31
|
private recordSeenInboundMessage;
|
|
40
32
|
private pruneRecentInboundMessageIds;
|
|
41
|
-
/**
|
|
42
|
-
* REST catch-up after `replay.expired`: the stream service evicted our
|
|
43
|
-
* cursor, so messages in the gap were silently dropped. Fetch the newest
|
|
44
|
-
* page per conversation and feed unseen inbound messages through the normal
|
|
45
|
-
* debouncer path (same entry point as SSE delivery, same id dedupe).
|
|
46
|
-
*
|
|
47
|
-
* Lower bound per conversation: the in-memory last-seen inbound timestamp,
|
|
48
|
-
* falling back to this manager's construction time for conversations with
|
|
49
|
-
* no prior inbound traffic — anything older predates this process and may
|
|
50
|
-
* already have been handled by a previous run. For the same reason the
|
|
51
|
-
* catch-up is NOT wired on initial connect: with no durable cursor, a fresh
|
|
52
|
-
* process would re-fire turns for messages an earlier run already answered.
|
|
53
|
-
*/
|
|
54
|
-
private runReplayCatchup;
|
|
55
33
|
private logSseError;
|
|
56
34
|
setOnAgentContext(cb: (ctx: AgentContext) => void): void;
|
|
57
35
|
setContactRequestHandlers(handlers: {
|
package/dist/realtime.js
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import { CanonStream, } from '@canonmsg/core';
|
|
2
|
-
import { shouldDispatchInboundMessage } from './turn-filter.js';
|
|
3
2
|
const RECENT_INBOUND_TTL_MS = 30 * 60 * 1000;
|
|
4
3
|
const MAX_RECENT_INBOUND_MESSAGE_IDS = 5000;
|
|
5
|
-
/**
|
|
6
|
-
* Newest-page bound for the replay-expiry REST catch-up. The SDK has no
|
|
7
|
-
* durable per-conversation cursor (everything here is in-memory), so the
|
|
8
|
-
* catch-up only inspects the newest page per conversation and relies on the
|
|
9
|
-
* id-based dedupe below for anything that overlaps live SSE delivery.
|
|
10
|
-
*/
|
|
11
|
-
const REPLAY_CATCHUP_PAGE_LIMIT = 50;
|
|
12
4
|
function messageCreatedAtMs(createdAt) {
|
|
13
5
|
if (!createdAt)
|
|
14
6
|
return 0;
|
|
@@ -19,22 +11,15 @@ function messageCreatedAtMs(createdAt) {
|
|
|
19
11
|
* Wraps @canonmsg/core's CanonStream with SDK-specific features:
|
|
20
12
|
* - Debouncer integration (message batching)
|
|
21
13
|
* - Agent context callback
|
|
22
|
-
* -
|
|
14
|
+
* - Connection/status callbacks
|
|
23
15
|
*/
|
|
24
16
|
export class RealtimeManager {
|
|
25
17
|
debouncer;
|
|
26
18
|
agentId;
|
|
27
|
-
apiClient;
|
|
28
19
|
stream;
|
|
29
20
|
running = false;
|
|
30
21
|
/** Recent inbound message IDs (`conversationId:messageId`) for cross-flush dedupe. */
|
|
31
22
|
recentInboundMessageIds = new Map();
|
|
32
|
-
/** Latest handled inbound message timestamp per conversation. */
|
|
33
|
-
lastInboundMessageAtByConversation = new Map();
|
|
34
|
-
/** Lower bound for catch-up in conversations with no in-memory cursor. */
|
|
35
|
-
replaySyncStartedAt = Date.now();
|
|
36
|
-
replayCatchupInFlight = null;
|
|
37
|
-
hasConnectedOnce = false;
|
|
38
23
|
lastSseErrorKey = null;
|
|
39
24
|
lastSseErrorAt = 0;
|
|
40
25
|
suppressedSseErrorCount = 0;
|
|
@@ -51,15 +36,14 @@ export class RealtimeManager {
|
|
|
51
36
|
constructor(apiKey, debouncer, agentId, streamUrl, apiClient) {
|
|
52
37
|
this.debouncer = debouncer;
|
|
53
38
|
this.agentId = agentId;
|
|
54
|
-
this.apiClient = apiClient ?? null;
|
|
55
39
|
this.stream = new CanonStream({
|
|
56
40
|
apiKey,
|
|
57
41
|
agentId,
|
|
58
42
|
streamUrl,
|
|
59
43
|
handler: {
|
|
60
44
|
onMessage: (payload) => {
|
|
61
|
-
// Cross-flush id dedupe: replay overlap
|
|
62
|
-
//
|
|
45
|
+
// Cross-flush id dedupe: SSE replay overlap must never double-fire
|
|
46
|
+
// a turn for the same message.
|
|
63
47
|
if (this.hasSeenInboundMessage(payload.conversationId, payload.message.id)) {
|
|
64
48
|
return;
|
|
65
49
|
}
|
|
@@ -122,20 +106,13 @@ export class RealtimeManager {
|
|
|
122
106
|
this.onConversationUpdated?.(payload);
|
|
123
107
|
},
|
|
124
108
|
onConnected: () => {
|
|
125
|
-
// Reset backoff is handled internally by CanonStream
|
|
126
|
-
const shouldCatchUp = this.hasConnectedOnce;
|
|
127
|
-
this.hasConnectedOnce = true;
|
|
128
109
|
this.onConnected?.();
|
|
129
|
-
if (shouldCatchUp) {
|
|
130
|
-
this.queueReplayCatchup();
|
|
131
|
-
}
|
|
132
110
|
},
|
|
133
111
|
onDisconnected: () => {
|
|
134
112
|
this.onDisconnected?.();
|
|
135
113
|
},
|
|
136
114
|
onReplayExpired: (payload) => {
|
|
137
|
-
console.error(`[canon-sdk] SSE replay window expired${payload.lastAvailableId ? ` (oldest available event: ${payload.lastAvailableId})` : ''}
|
|
138
|
-
this.queueReplayCatchup();
|
|
115
|
+
console.error(`[canon-sdk] SSE replay window expired${payload.lastAvailableId ? ` (oldest available event: ${payload.lastAvailableId})` : ''}; missed history is available via explicit REST fetch`);
|
|
139
116
|
},
|
|
140
117
|
onError: (err) => {
|
|
141
118
|
this.logSseError(err);
|
|
@@ -143,22 +120,13 @@ export class RealtimeManager {
|
|
|
143
120
|
},
|
|
144
121
|
});
|
|
145
122
|
}
|
|
146
|
-
queueReplayCatchup() {
|
|
147
|
-
this.replayCatchupInFlight ??= this.runReplayCatchup().finally(() => {
|
|
148
|
-
this.replayCatchupInFlight = null;
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
123
|
hasSeenInboundMessage(conversationId, messageId) {
|
|
152
124
|
return this.recentInboundMessageIds.has(`${conversationId}:${messageId}`);
|
|
153
125
|
}
|
|
154
126
|
recordSeenInboundMessage(conversationId, messageId, createdAtMs) {
|
|
155
127
|
const now = Date.now();
|
|
156
128
|
this.recentInboundMessageIds.set(`${conversationId}:${messageId}`, now);
|
|
157
|
-
|
|
158
|
-
const previous = this.lastInboundMessageAtByConversation.get(conversationId) ?? 0;
|
|
159
|
-
if (effectiveTimestamp > previous) {
|
|
160
|
-
this.lastInboundMessageAtByConversation.set(conversationId, effectiveTimestamp);
|
|
161
|
-
}
|
|
129
|
+
void createdAtMs;
|
|
162
130
|
this.pruneRecentInboundMessageIds(now);
|
|
163
131
|
}
|
|
164
132
|
pruneRecentInboundMessageIds(now = Date.now()) {
|
|
@@ -175,75 +143,6 @@ export class RealtimeManager {
|
|
|
175
143
|
this.recentInboundMessageIds.delete(oldestKey);
|
|
176
144
|
}
|
|
177
145
|
}
|
|
178
|
-
/**
|
|
179
|
-
* REST catch-up after `replay.expired`: the stream service evicted our
|
|
180
|
-
* cursor, so messages in the gap were silently dropped. Fetch the newest
|
|
181
|
-
* page per conversation and feed unseen inbound messages through the normal
|
|
182
|
-
* debouncer path (same entry point as SSE delivery, same id dedupe).
|
|
183
|
-
*
|
|
184
|
-
* Lower bound per conversation: the in-memory last-seen inbound timestamp,
|
|
185
|
-
* falling back to this manager's construction time for conversations with
|
|
186
|
-
* no prior inbound traffic — anything older predates this process and may
|
|
187
|
-
* already have been handled by a previous run. For the same reason the
|
|
188
|
-
* catch-up is NOT wired on initial connect: with no durable cursor, a fresh
|
|
189
|
-
* process would re-fire turns for messages an earlier run already answered.
|
|
190
|
-
*/
|
|
191
|
-
async runReplayCatchup() {
|
|
192
|
-
const apiClient = this.apiClient;
|
|
193
|
-
if (!apiClient) {
|
|
194
|
-
console.error('[canon-sdk] Replay catch-up skipped — no API client available');
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
try {
|
|
198
|
-
const conversations = await apiClient.getConversations();
|
|
199
|
-
let recovered = 0;
|
|
200
|
-
await Promise.all(conversations.map(async (conversation) => {
|
|
201
|
-
try {
|
|
202
|
-
const page = await apiClient.getMessagesPage(conversation.id, REPLAY_CATCHUP_PAGE_LIMIT);
|
|
203
|
-
const lowerBoundMs = this.lastInboundMessageAtByConversation.get(conversation.id)
|
|
204
|
-
?? this.replaySyncStartedAt;
|
|
205
|
-
const candidates = [...(page.messages ?? [])]
|
|
206
|
-
.filter((message) => !message.deleted)
|
|
207
|
-
.sort((a, b) => messageCreatedAtMs(a.createdAt) - messageCreatedAtMs(b.createdAt));
|
|
208
|
-
for (const message of candidates) {
|
|
209
|
-
if (!this.running)
|
|
210
|
-
return;
|
|
211
|
-
if (message.senderId === this.agentId)
|
|
212
|
-
continue;
|
|
213
|
-
const createdAtMs = messageCreatedAtMs(message.createdAt);
|
|
214
|
-
// Use a strict lower bound of `< lowerBoundMs` (not `<=`): a
|
|
215
|
-
// gap-dropped message can share the same createdAt millisecond as
|
|
216
|
-
// the last message seen over SSE (server timestamps collide under
|
|
217
|
-
// bursts — exactly the scenario catch-up targets). The id-dedupe on
|
|
218
|
-
// the next line suppresses the already-delivered boundary message,
|
|
219
|
-
// so excluding by `<=` would only drop never-seen same-ms peers.
|
|
220
|
-
if (!createdAtMs || createdAtMs < lowerBoundMs)
|
|
221
|
-
continue;
|
|
222
|
-
if (this.hasSeenInboundMessage(conversation.id, message.id))
|
|
223
|
-
continue;
|
|
224
|
-
this.recordSeenInboundMessage(conversation.id, message.id, createdAtMs);
|
|
225
|
-
const dispatch = await shouldDispatchInboundMessage(conversation.id, this.agentId, message, {
|
|
226
|
-
conversationType: conversation.type,
|
|
227
|
-
behavior: page.behavior ?? null,
|
|
228
|
-
});
|
|
229
|
-
if (!dispatch)
|
|
230
|
-
continue;
|
|
231
|
-
this.debouncer.add(conversation.id, message, null);
|
|
232
|
-
recovered += 1;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
catch (err) {
|
|
236
|
-
console.error(`[canon-sdk] Replay catch-up failed for ${conversation.id}:`, err instanceof Error ? err.message : err);
|
|
237
|
-
}
|
|
238
|
-
}));
|
|
239
|
-
if (recovered > 0) {
|
|
240
|
-
console.error(`[canon-sdk] Replay catch-up recovered ${recovered} missed message(s)`);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
catch (err) {
|
|
244
|
-
console.error('[canon-sdk] Replay catch-up failed:', err instanceof Error ? err.message : err);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
146
|
logSseError(err) {
|
|
248
147
|
const code = err.code;
|
|
249
148
|
const key = `${typeof code === 'string' ? code : 'generic'}:${err.message}`;
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/agent-sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.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",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"
|
|
11
|
-
"
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
12
|
},
|
|
13
13
|
"./media": {
|
|
14
|
-
"
|
|
15
|
-
"
|
|
14
|
+
"import": "./dist/media.js",
|
|
15
|
+
"types": "./dist/media.d.ts"
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"node": ">=18.0.0"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@canonmsg/core": "^
|
|
31
|
+
"@canonmsg/core": "^4.0.0"
|
|
32
32
|
},
|
|
33
33
|
"publishConfig": {
|
|
34
34
|
"access": "public"
|