@canonmsg/agent-sdk 0.4.0 → 0.5.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.
@@ -12,6 +12,7 @@ export declare class CanonAgent {
12
12
  private agentContext;
13
13
  private cachedConversationIds;
14
14
  private running;
15
+ private runtimeHeartbeatTimer;
15
16
  constructor(options: CanonAgentOptions);
16
17
  on(event: 'message', handler: MessageHandler): void;
17
18
  start(): Promise<void>;
@@ -28,6 +29,10 @@ export declare class CanonAgent {
28
29
  attachment: import('@canonmsg/core').MediaAttachment;
29
30
  }>;
30
31
  stop(): Promise<void>;
32
+ private publishAgentRuntime;
33
+ private startRuntimeHeartbeat;
34
+ private stopRuntimeHeartbeat;
35
+ private clearAgentRuntime;
31
36
  private handleMessages;
32
37
  private executeHandler;
33
38
  static register(options: {
@@ -1,10 +1,11 @@
1
- import { CanonClient, FINAL_MESSAGE_HANDOFF_MS, initRTDBAuth, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from '@canonmsg/core';
1
+ import { CanonClient, FINAL_MESSAGE_HANDOFF_MS, initRTDBAuth, rtdbWrite, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from '@canonmsg/core';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { AuthManager } from './auth.js';
4
4
  import { Debouncer } from './debouncer.js';
5
5
  import { PollingManager } from './polling.js';
6
6
  import { SessionManager } from './session-manager.js';
7
7
  const AUTO_MODE_THRESHOLD = 500;
8
+ const AGENT_RUNTIME_HEARTBEAT_MS = 30_000;
8
9
  const SDK_RUNTIME_CAPABILITIES = {
9
10
  supportsInterrupt: false,
10
11
  supportsQueue: true,
@@ -28,6 +29,7 @@ export class CanonAgent {
28
29
  agentContext = null;
29
30
  cachedConversationIds = [];
30
31
  running = false;
32
+ runtimeHeartbeatTimer = null;
31
33
  constructor(options) {
32
34
  this.options = {
33
35
  baseUrl: 'https://api-6m6mlelskq-uc.a.run.app',
@@ -109,12 +111,16 @@ export class CanonAgent {
109
111
  rtm.setOnAgentContext((ctx) => {
110
112
  this.agentContext = ctx;
111
113
  });
114
+ rtm.setConnectionHandlers({
115
+ onConnected: () => this.startRuntimeHeartbeat(),
116
+ onDisconnected: () => this.stopRuntimeHeartbeat(),
117
+ });
112
118
  this.realtimeManager = rtm;
113
119
  await rtm.start();
114
120
  console.log('[canon-sdk] SSE stream started');
115
121
  }
116
122
  else {
117
- this.pollingManager = new PollingManager(this.apiClient, this.debouncer, agentId, this.options.pollingIntervalMs);
123
+ this.pollingManager = new PollingManager(this.apiClient, this.debouncer, agentId, this.options.pollingIntervalMs, () => this.startRuntimeHeartbeat(), () => this.stopRuntimeHeartbeat());
118
124
  await this.pollingManager.start();
119
125
  console.log(`[canon-sdk] Polling started (interval: ${this.options.pollingIntervalMs}ms)`);
120
126
  }
@@ -155,6 +161,7 @@ export class CanonAgent {
155
161
  clearTurnState(id, this.agentId).catch(() => { });
156
162
  }
157
163
  }
164
+ await this.clearAgentRuntime();
158
165
  this.pollingManager?.stop();
159
166
  this.realtimeManager?.stop();
160
167
  this.sessionManager?.destroy();
@@ -162,6 +169,36 @@ export class CanonAgent {
162
169
  this.debouncer.destroy();
163
170
  console.log('[canon-sdk] Stopped');
164
171
  }
172
+ async publishAgentRuntime() {
173
+ if (!this.agentId)
174
+ return;
175
+ await rtdbWrite(`/agent-runtime/${this.agentId}`, {
176
+ clientType: this.options.clientType ?? 'generic',
177
+ hostMode: false,
178
+ updatedAt: { '.sv': 'timestamp' },
179
+ });
180
+ }
181
+ startRuntimeHeartbeat() {
182
+ void this.publishAgentRuntime();
183
+ if (this.runtimeHeartbeatTimer)
184
+ return;
185
+ this.runtimeHeartbeatTimer = setInterval(() => {
186
+ void this.publishAgentRuntime();
187
+ }, AGENT_RUNTIME_HEARTBEAT_MS);
188
+ this.runtimeHeartbeatTimer.unref?.();
189
+ }
190
+ stopRuntimeHeartbeat() {
191
+ if (this.runtimeHeartbeatTimer) {
192
+ clearInterval(this.runtimeHeartbeatTimer);
193
+ this.runtimeHeartbeatTimer = null;
194
+ }
195
+ void this.clearAgentRuntime();
196
+ }
197
+ async clearAgentRuntime() {
198
+ if (!this.agentId)
199
+ return;
200
+ await rtdbWrite(`/agent-runtime/${this.agentId}`, null).catch(() => { });
201
+ }
165
202
  async handleMessages(conversationId, messages) {
166
203
  if (!this.handler) {
167
204
  console.warn(`[canon-sdk] No message handler registered — messages for ${conversationId} dropped. Call agent.on('message', handler) before starting.`);
package/dist/polling.d.ts CHANGED
@@ -5,10 +5,12 @@ export declare class PollingManager {
5
5
  private debouncer;
6
6
  private agentId;
7
7
  private pollingIntervalMs;
8
+ private onHealthy;
9
+ private onUnhealthy;
8
10
  private lastSeenTimestamps;
9
11
  private pollTimer;
10
12
  private running;
11
- constructor(apiClient: CanonClient, debouncer: Debouncer, agentId: string, pollingIntervalMs: number);
13
+ constructor(apiClient: CanonClient, debouncer: Debouncer, agentId: string, pollingIntervalMs: number, onHealthy?: () => void, onUnhealthy?: () => void);
12
14
  start(): Promise<void>;
13
15
  private poll;
14
16
  private findActiveConversations;
package/dist/polling.js CHANGED
@@ -5,14 +5,18 @@ export class PollingManager {
5
5
  debouncer;
6
6
  agentId;
7
7
  pollingIntervalMs;
8
+ onHealthy;
9
+ onUnhealthy;
8
10
  lastSeenTimestamps = new Map();
9
11
  pollTimer = null;
10
12
  running = false;
11
- constructor(apiClient, debouncer, agentId, pollingIntervalMs) {
13
+ constructor(apiClient, debouncer, agentId, pollingIntervalMs, onHealthy, onUnhealthy) {
12
14
  this.apiClient = apiClient;
13
15
  this.debouncer = debouncer;
14
16
  this.agentId = agentId;
15
17
  this.pollingIntervalMs = pollingIntervalMs;
18
+ this.onHealthy = onHealthy ?? null;
19
+ this.onUnhealthy = onUnhealthy ?? null;
16
20
  }
17
21
  async start() {
18
22
  this.running = true;
@@ -22,6 +26,7 @@ export class PollingManager {
22
26
  for (const convo of conversations) {
23
27
  this.lastSeenTimestamps.set(convo.id, now);
24
28
  }
29
+ this.onHealthy?.();
25
30
  // Start polling
26
31
  this.pollTimer = setInterval(() => this.poll(), this.pollingIntervalMs);
27
32
  }
@@ -30,6 +35,7 @@ export class PollingManager {
30
35
  return;
31
36
  try {
32
37
  const conversations = await this.apiClient.getConversations();
38
+ this.onHealthy?.();
33
39
  const activeConvos = this.findActiveConversations(conversations);
34
40
  await Promise.all(activeConvos.map(async (convo) => {
35
41
  try {
@@ -67,6 +73,7 @@ export class PollingManager {
67
73
  }));
68
74
  }
69
75
  catch (err) {
76
+ this.onUnhealthy?.();
70
77
  console.error('[canon-sdk] Polling error:', err);
71
78
  }
72
79
  }
@@ -15,8 +15,14 @@ export declare class RealtimeManager {
15
15
  private knownConversationIds;
16
16
  private discoveryTimer;
17
17
  private onAgentContext;
18
+ private onConnected;
19
+ private onDisconnected;
18
20
  constructor(apiKey: string, debouncer: Debouncer, agentId: string, streamUrl?: string, apiClient?: CanonClient);
19
21
  setOnAgentContext(cb: (ctx: AgentContext) => void): void;
22
+ setConnectionHandlers(handlers: {
23
+ onConnected?: () => void;
24
+ onDisconnected?: () => void;
25
+ }): void;
20
26
  start(): Promise<void>;
21
27
  stop(): void;
22
28
  private discoverNewConversations;
package/dist/realtime.js CHANGED
@@ -17,6 +17,8 @@ export class RealtimeManager {
17
17
  knownConversationIds = new Set();
18
18
  discoveryTimer = null;
19
19
  onAgentContext = null;
20
+ onConnected = null;
21
+ onDisconnected = null;
20
22
  constructor(apiKey, debouncer, agentId, streamUrl, apiClient) {
21
23
  this.debouncer = debouncer;
22
24
  this.agentId = agentId;
@@ -54,6 +56,10 @@ export class RealtimeManager {
54
56
  },
55
57
  onConnected: () => {
56
58
  // Reset backoff is handled internally by CanonStream
59
+ this.onConnected?.();
60
+ },
61
+ onDisconnected: () => {
62
+ this.onDisconnected?.();
57
63
  },
58
64
  onError: (err) => {
59
65
  console.error('[canon-sdk] SSE error:', err.message);
@@ -64,6 +70,10 @@ export class RealtimeManager {
64
70
  setOnAgentContext(cb) {
65
71
  this.onAgentContext = cb;
66
72
  }
73
+ setConnectionHandlers(handlers) {
74
+ this.onConnected = handlers.onConnected ?? null;
75
+ this.onDisconnected = handlers.onDisconnected ?? null;
76
+ }
67
77
  async start() {
68
78
  this.running = true;
69
79
  // Snapshot current conversations
@@ -89,6 +99,7 @@ export class RealtimeManager {
89
99
  this.discoveryTimer = null;
90
100
  }
91
101
  this.stream.stop();
102
+ this.onDisconnected?.();
92
103
  }
93
104
  // ── Conversation discovery ─────────────────────────────────────────
94
105
  async discoverNewConversations() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "0.4.0",
3
+ "version": "0.5.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",
@@ -23,7 +23,7 @@
23
23
  "node": ">=18.0.0"
24
24
  },
25
25
  "dependencies": {
26
- "@canonmsg/core": "^0.4.0"
26
+ "@canonmsg/core": "^0.5.0"
27
27
  },
28
28
  "publishConfig": {
29
29
  "access": "public"