@canonmsg/agent-sdk 1.5.1 → 1.5.3

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.
@@ -55,6 +55,7 @@ export declare class CanonAgent {
55
55
  private readonly activeTurns;
56
56
  private readonly conversationMemberIds;
57
57
  private readonly pendingMembershipChanges;
58
+ private sseConnectedLogged;
58
59
  constructor(options: CanonAgentOptions);
59
60
  private ensureApprovalManager;
60
61
  private filterApprovalReplyMessages;
@@ -116,6 +117,7 @@ export declare class CanonAgent {
116
117
  private hasRuntimeSignalSupport;
117
118
  private hasRuntimePrimitiveSupport;
118
119
  private hasRuntimeControlSupport;
120
+ private supportsInputInterrupt;
119
121
  private buildRuntimeDescriptor;
120
122
  private buildRuntimeCapabilities;
121
123
  private publishAgentRuntime;
@@ -9,6 +9,7 @@ const RUNTIME_PRIMITIVE_DEDUPE_TTL_MS = 5 * 60 * 1000;
9
9
  const RUNTIME_PRIMITIVE_DEDUPE_MAX = 1_000;
10
10
  const SDK_RUNTIME_CAPABILITIES = {
11
11
  supportsInterrupt: false,
12
+ supportsInputInterrupt: false,
12
13
  supportsQueue: true,
13
14
  supportsInterleave: false,
14
15
  supportsRequiresAction: true,
@@ -218,6 +219,7 @@ export class CanonAgent {
218
219
  activeTurns = new Map();
219
220
  conversationMemberIds = new Map();
220
221
  pendingMembershipChanges = new Map();
222
+ sseConnectedLogged = false;
221
223
  constructor(options) {
222
224
  this.options = {
223
225
  baseUrl: 'https://api-6m6mlelskq-uc.a.run.app',
@@ -528,13 +530,21 @@ export class CanonAgent {
528
530
  rtm.setConversationUpdatedHandler((payload) => {
529
531
  this.handleConversationUpdated(payload);
530
532
  });
533
+ rtm.setMessageDeletedHandler((payload) => {
534
+ this.sessionManager?.dropQueuedMessage(payload.conversationId, payload.messageId);
535
+ });
531
536
  rtm.setConnectionHandlers({
532
- onConnected: () => this.startRuntimeHeartbeat(),
537
+ onConnected: () => {
538
+ this.startRuntimeHeartbeat();
539
+ if (!this.sseConnectedLogged) {
540
+ this.sseConnectedLogged = true;
541
+ console.log('[canon-sdk] SSE stream connected');
542
+ }
543
+ },
533
544
  onDisconnected: () => this.stopRuntimeHeartbeat(),
534
545
  });
535
546
  this.realtimeManager = rtm;
536
547
  await rtm.start();
537
- console.log('[canon-sdk] SSE stream started');
538
548
  }
539
549
  async createConversation(options) {
540
550
  return this.apiClient.createConversation(options);
@@ -633,6 +643,9 @@ export class CanonAgent {
633
643
  hasRuntimeControlSupport() {
634
644
  return this.hasRuntimeSignalSupport() || this.hasRuntimePrimitiveSupport();
635
645
  }
646
+ supportsInputInterrupt() {
647
+ return this.hasInterruptSupport() && this.options.runtimeDescriptor?.supportsInputInterrupt !== false;
648
+ }
636
649
  buildRuntimeDescriptor() {
637
650
  const source = this.options.runtimeDescriptor ?? DEFAULT_SDK_RUNTIME_DESCRIPTOR;
638
651
  const hasInterrupt = this.hasInterruptSupport();
@@ -677,6 +690,7 @@ export class CanonAgent {
677
690
  return {
678
691
  ...source,
679
692
  supportsInterrupt: hasInterrupt,
693
+ supportsInputInterrupt: source.supportsInputInterrupt === false ? false : hasInterrupt,
680
694
  commands,
681
695
  actions,
682
696
  };
@@ -685,6 +699,7 @@ export class CanonAgent {
685
699
  return {
686
700
  ...SDK_RUNTIME_CAPABILITIES,
687
701
  supportsInterrupt: this.hasInterruptSupport(),
702
+ supportsInputInterrupt: this.supportsInputInterrupt(),
688
703
  supportsQueue: Boolean(this.sessionManager),
689
704
  };
690
705
  }
@@ -1049,6 +1064,10 @@ export class CanonAgent {
1049
1064
  if (!runtimeState || !agentId)
1050
1065
  return;
1051
1066
  turnState = state;
1067
+ const isOpenTurn = state === 'thinking'
1068
+ || state === 'streaming'
1069
+ || state === 'tool'
1070
+ || state === 'waiting_input';
1052
1071
  await Promise.resolve(runtimeState.writeTurnState(conversationId, {
1053
1072
  turnId,
1054
1073
  state,
@@ -1056,6 +1075,7 @@ export class CanonAgent {
1056
1075
  currentSpeakerId: agentId,
1057
1076
  capabilities: this.buildRuntimeCapabilities(),
1058
1077
  openedAt: turnOpenedAt,
1078
+ ...(isOpenTurn ? { turnUpdatedAt: Date.now() } : {}),
1059
1079
  ...(state === 'completed' || state === 'interrupted' || state === 'idle'
1060
1080
  ? { completedAt: { '.sv': 'timestamp' } }
1061
1081
  : {}),
@@ -9,6 +9,7 @@ export declare class Debouncer {
9
9
  constructor(debounceMs: number);
10
10
  setCallback(cb: (conversationId: string, messages: CanonMessage[], provenanceByMessageId: ReadonlyMap<string, CanonRuntimeProvenance>) => void): void;
11
11
  add(conversationId: string, message: CanonMessage, provenance?: CanonRuntimeProvenance | null): void;
12
+ removeMessage(conversationId: string, messageId: string): boolean;
12
13
  private flush;
13
14
  destroy(): void;
14
15
  }
package/dist/debouncer.js CHANGED
@@ -43,6 +43,27 @@ export class Debouncer {
43
43
  this.flush(conversationId);
44
44
  }, this.debounceMs));
45
45
  }
46
+ removeMessage(conversationId, messageId) {
47
+ const existing = this.pending.get(conversationId);
48
+ if (!existing || existing.length === 0)
49
+ return false;
50
+ const next = existing.filter((message) => message.id !== messageId);
51
+ if (next.length === existing.length)
52
+ return false;
53
+ this.provenanceByMessageId.delete(messageId);
54
+ if (next.length === 0) {
55
+ this.pending.delete(conversationId);
56
+ this.orderedFlags.delete(conversationId);
57
+ const timer = this.timers.get(conversationId);
58
+ if (timer)
59
+ clearTimeout(timer);
60
+ this.timers.delete(conversationId);
61
+ }
62
+ else {
63
+ this.pending.set(conversationId, next);
64
+ }
65
+ return true;
66
+ }
46
67
  flush(conversationId) {
47
68
  const messages = this.pending.get(conversationId);
48
69
  const isOrdered = this.orderedFlags.get(conversationId) ?? false;
@@ -10,15 +10,20 @@ export declare class RealtimeManager {
10
10
  private agentId;
11
11
  private stream;
12
12
  private running;
13
+ private lastSseErrorKey;
14
+ private lastSseErrorAt;
15
+ private suppressedSseErrorCount;
13
16
  private onAgentContext;
14
17
  private onContactRequest;
15
18
  private onContactApproved;
16
19
  private onContactAdded;
17
20
  private onContactRemoved;
18
21
  private onConversationUpdated;
22
+ private onMessageDeleted;
19
23
  private onConnected;
20
24
  private onDisconnected;
21
25
  constructor(apiKey: string, debouncer: Debouncer, agentId: string, streamUrl?: string, apiClient?: CanonClient);
26
+ private logSseError;
22
27
  setOnAgentContext(cb: (ctx: AgentContext) => void): void;
23
28
  setContactRequestHandlers(handlers: {
24
29
  onContactRequest?: (payload: ContactRequestPayload) => void;
@@ -29,6 +34,10 @@ export declare class RealtimeManager {
29
34
  onContactRemoved?: (payload: ContactRemovedPayload) => void;
30
35
  }): void;
31
36
  setConversationUpdatedHandler(cb: (payload: ConversationUpdatedPayload) => void): void;
37
+ setMessageDeletedHandler(cb: (payload: {
38
+ conversationId: string;
39
+ messageId: string;
40
+ }) => void): void;
32
41
  setConnectionHandlers(handlers: {
33
42
  onConnected?: () => void;
34
43
  onDisconnected?: () => void;
package/dist/realtime.js CHANGED
@@ -9,12 +9,16 @@ export class RealtimeManager {
9
9
  agentId;
10
10
  stream;
11
11
  running = false;
12
+ lastSseErrorKey = null;
13
+ lastSseErrorAt = 0;
14
+ suppressedSseErrorCount = 0;
12
15
  onAgentContext = null;
13
16
  onContactRequest = null;
14
17
  onContactApproved = null;
15
18
  onContactAdded = null;
16
19
  onContactRemoved = null;
17
20
  onConversationUpdated = null;
21
+ onMessageDeleted = null;
18
22
  onConnected = null;
19
23
  onDisconnected = null;
20
24
  constructor(apiKey, debouncer, agentId, streamUrl, apiClient) {
@@ -54,6 +58,10 @@ export class RealtimeManager {
54
58
  };
55
59
  this.debouncer.add(payload.conversationId, message, payload.provenance ?? null);
56
60
  },
61
+ onMessageDeleted: (payload) => {
62
+ this.debouncer.removeMessage(payload.conversationId, payload.messageId);
63
+ this.onMessageDeleted?.(payload);
64
+ },
57
65
  onAgentContext: (ctx) => {
58
66
  this.onAgentContext?.(ctx);
59
67
  },
@@ -80,11 +88,27 @@ export class RealtimeManager {
80
88
  this.onDisconnected?.();
81
89
  },
82
90
  onError: (err) => {
83
- console.error('[canon-sdk] SSE error:', err.message);
91
+ this.logSseError(err);
84
92
  },
85
93
  },
86
94
  });
87
95
  }
96
+ logSseError(err) {
97
+ const code = err.code;
98
+ const key = `${typeof code === 'string' ? code : 'generic'}:${err.message}`;
99
+ const now = Date.now();
100
+ if (this.lastSseErrorKey === key && now - this.lastSseErrorAt < 60_000) {
101
+ this.suppressedSseErrorCount += 1;
102
+ return;
103
+ }
104
+ if (this.suppressedSseErrorCount > 0) {
105
+ console.error(`[canon-sdk] SSE error repeated ${this.suppressedSseErrorCount} more time${this.suppressedSseErrorCount === 1 ? '' : 's'}`);
106
+ this.suppressedSseErrorCount = 0;
107
+ }
108
+ this.lastSseErrorKey = key;
109
+ this.lastSseErrorAt = now;
110
+ console.error('[canon-sdk] SSE error:', err.message);
111
+ }
88
112
  setOnAgentContext(cb) {
89
113
  this.onAgentContext = cb;
90
114
  }
@@ -99,6 +123,9 @@ export class RealtimeManager {
99
123
  setConversationUpdatedHandler(cb) {
100
124
  this.onConversationUpdated = cb;
101
125
  }
126
+ setMessageDeletedHandler(cb) {
127
+ this.onMessageDeleted = cb;
128
+ }
102
129
  setConnectionHandlers(handlers) {
103
130
  this.onConnected = handlers.onConnected ?? null;
104
131
  this.onDisconnected = handlers.onDisconnected ?? null;
@@ -110,6 +137,5 @@ export class RealtimeManager {
110
137
  stop() {
111
138
  this.running = false;
112
139
  this.stream.stop();
113
- this.onDisconnected?.();
114
140
  }
115
141
  }
@@ -63,6 +63,8 @@ export declare class SessionManager {
63
63
  get sessionCount(): number;
64
64
  /** Number of queued batches waiting behind the active turn for this conversation. */
65
65
  getQueueDepth(conversationId: string): number;
66
+ /** Drop one not-yet-running queued message for a conversation. */
67
+ dropQueuedMessage(conversationId: string, messageId: string): CanonMessage[];
66
68
  /** Drop queued, not-yet-running batches for a conversation. */
67
69
  dropQueued(conversationId: string): CanonMessage[];
68
70
  /** Drop queued work and clear retained context for a conversation. */
@@ -199,6 +199,42 @@ export class SessionManager {
199
199
  getQueueDepth(conversationId) {
200
200
  return this.queues.get(conversationId)?.length ?? 0;
201
201
  }
202
+ /** Drop one not-yet-running queued message for a conversation. */
203
+ dropQueuedMessage(conversationId, messageId) {
204
+ const queue = this.queues.get(conversationId);
205
+ if (!queue || queue.length === 0)
206
+ return [];
207
+ const droppedMessages = [];
208
+ const nextQueue = [];
209
+ for (const item of queue) {
210
+ const keptMessages = item.messages.filter((message) => {
211
+ const drop = message.id === messageId;
212
+ if (drop)
213
+ droppedMessages.push(message);
214
+ return !drop;
215
+ });
216
+ if (keptMessages.length === 0) {
217
+ item.resolve();
218
+ }
219
+ else if (keptMessages.length !== item.messages.length) {
220
+ nextQueue.push({ ...item, messages: keptMessages });
221
+ }
222
+ else {
223
+ nextQueue.push(item);
224
+ }
225
+ }
226
+ if (droppedMessages.length === 0)
227
+ return [];
228
+ if (nextQueue.length === 0) {
229
+ this.queues.delete(conversationId);
230
+ this.pending.delete(conversationId);
231
+ }
232
+ else {
233
+ this.queues.set(conversationId, nextQueue);
234
+ }
235
+ this.removeMessagesFromSession(conversationId, droppedMessages);
236
+ return droppedMessages;
237
+ }
202
238
  /** Drop queued, not-yet-running batches for a conversation. */
203
239
  dropQueued(conversationId) {
204
240
  const queue = this.queues.get(conversationId);
@@ -2,7 +2,12 @@ import { evaluateParticipationPolicy, normalizeTurnState, rtdbRead, shouldTrigge
2
2
  function normalizeRuntimeTurnState(value) {
3
3
  const turnState = normalizeTurnState(value);
4
4
  if (turnState) {
5
- return { state: turnState.state };
5
+ return {
6
+ state: turnState.state,
7
+ ...(turnState.openedAt !== undefined ? { openedAt: turnState.openedAt } : {}),
8
+ ...(turnState.updatedAt !== undefined ? { updatedAt: turnState.updatedAt } : {}),
9
+ ...(turnState.turnUpdatedAt !== undefined ? { turnUpdatedAt: turnState.turnUpdatedAt } : {}),
10
+ };
6
11
  }
7
12
  return null;
8
13
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
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.19.2"
31
+ "@canonmsg/core": "^0.20.0"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"