@canonmsg/agent-sdk 0.4.0 → 0.6.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, mergeWorkSessionContexts, 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.`);
@@ -237,8 +274,9 @@ export class CanonAgent {
237
274
  }
238
275
  }, 3500);
239
276
  try {
240
- // Fetch history from API
241
- const history = await this.apiClient.getMessages(conversationId, this.options.historyLimit);
277
+ // Fetch hydrated history/context from API
278
+ const page = await this.apiClient.getMessagesPage(conversationId, this.options.historyLimit);
279
+ const history = page.messages;
242
280
  // If sessions enabled, seed the session with fetched history
243
281
  if (this.sessionManager && session) {
244
282
  this.sessionManager.seedHistory(conversationId, history);
@@ -294,6 +332,11 @@ export class CanonAgent {
294
332
  m.isOwner = m.senderId === ownerId;
295
333
  }
296
334
  }
335
+ const explicitWorkSession = messages.find((message) => message.workSession)?.workSession
336
+ ?? history.find((message) => message.workSession)?.workSession
337
+ ?? null;
338
+ const activeWorkSessions = mergeWorkSessionContexts(explicitWorkSession, page.workSessions ?? []);
339
+ const workSession = explicitWorkSession;
297
340
  // Build agent context (fallback to minimal if not yet received)
298
341
  const agent = this.agentContext ?? {
299
342
  agentId: this.agentId,
@@ -308,6 +351,23 @@ export class CanonAgent {
308
351
  const react = (messageId, emoji) => this.apiClient.react(conversationId, messageId, emoji);
309
352
  const addMember = (userId) => this.apiClient.addMember(conversationId, userId);
310
353
  const removeMember = (userId) => this.apiClient.removeMember(conversationId, userId);
354
+ const createWorkSession = (options) => this.apiClient.createWorkSession({
355
+ conversationId,
356
+ ...(options ?? {}),
357
+ });
358
+ const getWorkSession = (workSessionId, targetConversationId = conversationId) => this.apiClient.getWorkSession(workSessionId, targetConversationId);
359
+ const updateWorkSessionContext = (workSessionId, options) => this.apiClient.upsertWorkSessionConversation(workSessionId, conversationId, options);
360
+ const sendLinkedMessage = (targetConversationId, text, options) => {
361
+ if (!options?.workSessionId && !options?.createWorkSession) {
362
+ throw new Error('sendLinkedMessage requires workSessionId or createWorkSession');
363
+ }
364
+ return this.apiClient.sendLinkedMessage({
365
+ sourceConversationId: conversationId,
366
+ targetConversationId,
367
+ text,
368
+ ...options,
369
+ });
370
+ };
311
371
  // Invoke handler
312
372
  await this.handler({
313
373
  messages,
@@ -323,7 +383,13 @@ export class CanonAgent {
323
383
  react,
324
384
  addMember,
325
385
  removeMember,
386
+ createWorkSession,
387
+ getWorkSession,
388
+ updateWorkSessionContext,
389
+ sendLinkedMessage,
326
390
  agent,
391
+ workSession,
392
+ activeWorkSessions,
327
393
  session: session
328
394
  ? {
329
395
  id: session.id,
package/dist/index.d.ts CHANGED
@@ -2,5 +2,5 @@ export { CanonAgent } from './canon-agent.js';
2
2
  export { CanonApiError } from '@canonmsg/core';
3
3
  export { SessionManager } from './session-manager.js';
4
4
  export type { SessionConfig, Session } from './session-manager.js';
5
- export type { AgentContext, CanonMessage, CanonConversation, SendMessageOptions, CreateConversationOptions, } from '@canonmsg/core';
5
+ export type { AgentContext, CanonMessage, CanonConversation, CanonResolvedWorkSession, CanonWorkSession, CanonWorkSessionContext, CanonWorkSessionConversationRole, CanonWorkSessionDisclosureMode, CanonWorkSessionParticipant, CanonWorkSessionStatus, CreateWorkSessionOptions, SendLinkedMessageOptions, SendLinkedMessageResult, SendMessageOptions, CreateConversationOptions, UpdateWorkSessionConversationOptions, } from '@canonmsg/core';
6
6
  export type { SDKMessage, SDKConversation, CanonAgentOptions, MessageHandler, MessageHandlerContext, ProgressMessageOptions, ProgressMessageResult, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
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/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export type { AgentClientType, CanonMessage, CanonConversation, AgentContext, SendMessageOptions, CreateConversationOptions, TurnLifecycleState, } from '@canonmsg/core';
2
- import type { CanonMessage, CanonConversation, SendMessageOptions } from '@canonmsg/core';
1
+ export type { AgentClientType, CanonMessage, CanonConversation, AgentContext, CanonResolvedWorkSession, CanonWorkSession, CanonWorkSessionContext, CanonWorkSessionConversationRole, CanonWorkSessionDisclosureMode, CanonWorkSessionParticipant, CanonWorkSessionStatus, CreateWorkSessionOptions, SendLinkedMessageOptions, SendLinkedMessageResult, SendMessageOptions, CreateConversationOptions, TurnLifecycleState, UpdateWorkSessionConversationOptions, } from '@canonmsg/core';
2
+ import type { CanonMessage, CanonConversation, CreateWorkSessionOptions, SendMessageOptions, UpdateWorkSessionConversationOptions } from '@canonmsg/core';
3
3
  export type SDKMessage = CanonMessage;
4
4
  export type SDKConversation = CanonConversation;
5
5
  export interface ProgressMessageOptions extends SendMessageOptions {
@@ -59,8 +59,20 @@ export interface MessageHandlerContext {
59
59
  addMember: (userId: string) => Promise<void>;
60
60
  /** Remove a member from this conversation (requires owner/admin role) */
61
61
  removeMember: (userId: string) => Promise<void>;
62
+ /** Create a Canon work session rooted in this conversation. */
63
+ createWorkSession: (options?: Omit<CreateWorkSessionOptions, 'conversationId'>) => Promise<import('@canonmsg/core').CanonResolvedWorkSession>;
64
+ /** Load this conversation's scoped view of a Canon work session. */
65
+ getWorkSession: (workSessionId: string, conversationId?: string) => Promise<import('@canonmsg/core').CanonResolvedWorkSession>;
66
+ /** Update or attach this conversation's scoped work-session context. */
67
+ updateWorkSessionContext: (workSessionId: string, options?: UpdateWorkSessionConversationOptions) => Promise<import('@canonmsg/core').CanonResolvedWorkSession>;
68
+ /** Send into another conversation under an existing or lazily created Canon work session. */
69
+ sendLinkedMessage: (targetConversationId: string, text: string, options?: Omit<import('@canonmsg/core').SendLinkedMessageOptions, 'sourceConversationId' | 'targetConversationId' | 'text'>) => Promise<import('@canonmsg/core').SendLinkedMessageResult>;
62
70
  /** Trusted agent identity & access context */
63
71
  agent: import('@canonmsg/core').AgentContext;
72
+ /** Canon-provided shared task context for this turn, when attached to inbound messages. */
73
+ workSession?: import('@canonmsg/core').CanonWorkSessionContext | null;
74
+ /** All active Canon work sessions currently linked to this conversation. */
75
+ activeWorkSessions?: import('@canonmsg/core').CanonWorkSessionContext[];
64
76
  /** Per-conversation session state. Present when sessions are enabled. */
65
77
  session?: SessionInfo;
66
78
  /** Turn lifecycle helpers for live-work rendering and progress reporting. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "0.4.0",
3
+ "version": "0.6.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.6.0"
27
27
  },
28
28
  "publishConfig": {
29
29
  "access": "public"