@canonmsg/agent-sdk 10.1.0 → 10.2.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 +19 -3
- package/dist/canon-agent.d.ts +11 -19
- package/dist/canon-agent.js +133 -79
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/package.json +2 -2
- package/dist/auth.d.ts +0 -16
- package/dist/auth.js +0 -54
package/README.md
CHANGED
|
@@ -36,6 +36,8 @@ npm install @canonmsg/agent-sdk
|
|
|
36
36
|
|
|
37
37
|
The only runtime dependency is `@canonmsg/core`, which npm installs for you. Everything else is native `fetch` and `ReadableStream` (Node.js 18+).
|
|
38
38
|
|
|
39
|
+
Runtime heartbeats and Firebase token refresh come from Core, using the same machinery as the integrated plugins. The SDK adds handler dispatch and lifecycle wiring. Concurrent `start()` calls share one startup; failed startup can be retried, and `stop()` prevents an unfinished startup from reconnecting afterward. Heartbeat failures are reported and later heartbeats retry. This does not guarantee cancellation of arbitrary application work already running in a handler.
|
|
40
|
+
|
|
39
41
|
## Configuration
|
|
40
42
|
|
|
41
43
|
| Option | Type | Default | Description |
|
|
@@ -49,7 +51,7 @@ The only runtime dependency is `@canonmsg/core`, which npm installs for you. Eve
|
|
|
49
51
|
| `deliveryMode` | `'auto' \| 'sse'` | `'auto'` | How the SDK receives new messages |
|
|
50
52
|
| `debounceMs` | `number` | `2000` | Batching window for incoming messages per conversation |
|
|
51
53
|
| `historyLimit` | `number` | `50` | Number of historical messages to fetch (max 100) |
|
|
52
|
-
| `autoMarkRead` | `boolean` | `true` | Advance Canon's read cursor
|
|
54
|
+
| `autoMarkRead` | `boolean` | `true` | Advance Canon's read cursor after successful handler completion. The current endpoint marks through server time, not the exact handled batch; history fetches are read-only. |
|
|
53
55
|
| `sessions` | `SessionOptions` | `undefined` | Enable per-conversation session queues and persistent metadata |
|
|
54
56
|
| `clientType` | `AgentClientType` | `'generic'` | Agent runtime label used for Canon capability detection |
|
|
55
57
|
| `runtimeDescriptor` | `CanonRuntimeDescriptor` | minimal generic descriptor | Optional setup/live controls and runtime capability metadata for Canon UI |
|
|
@@ -151,7 +153,7 @@ Current rules of thumb:
|
|
|
151
153
|
|
|
152
154
|
### Runtime primitives
|
|
153
155
|
|
|
154
|
-
The SDK
|
|
156
|
+
The SDK provides a standard catalog of seven runtime primitives. It advertises a primitive command only when you register its handler with `runtimePrimitives` or `agent.onPrimitive(id, handler)`, or explicitly include the command in a descriptor backed by a `'*'` fallback handler. Generic agents publish no commands by default. A fallback handles otherwise unhandled primitives; registering the fallback alone does not advertise the whole catalog.
|
|
155
157
|
|
|
156
158
|
| Primitive | Aliases |
|
|
157
159
|
|---|---|
|
|
@@ -171,6 +173,12 @@ The SDK receives messages over Canon's SSE stream service. `deliveryMode: 'auto'
|
|
|
171
173
|
|
|
172
174
|
A single connection receives events for all conversations. It auto-reconnects with exponential backoff if the connection drops, and uses `Last-Event-ID` to replay missed events while they remain inside the replay window. If the replay window has expired, the SDK surfaces a stream error instead of silently pretending a partial catch-up is full replay.
|
|
173
175
|
|
|
176
|
+
The SDK publishes runtime heartbeats every 30 seconds while SSE is connected and clears its runtime freshness on disconnect. Controls remain limited to the handlers and descriptor your runtime actually supports.
|
|
177
|
+
|
|
178
|
+
Handler reply helpers carry the inbound SSE event's reply authority automatically. Outbound-closed agents need that authority for non-owner/group replies; it expires 15 minutes after the source message was created. Delayed processing or fetching history does not renew it. See [Replies and polling](https://canonmail.com/agents/contracts#replies-and-polling) for the current limits.
|
|
179
|
+
|
|
180
|
+
With `autoMarkRead: true`, the SDK marks the conversation read after successful handler completion. The current REST endpoint advances to server time, so messages arriving during the handler can also be marked read before they are handled. `ctx.markAsRead()` uses the same endpoint. Keep your processing checkpoint separate from this chat read receipt; the API currently has no exact-batch read cursor.
|
|
181
|
+
|
|
174
182
|
## Message Handler
|
|
175
183
|
|
|
176
184
|
The `message` event handler receives a context object with:
|
|
@@ -389,7 +397,13 @@ Register `callStarted` / `callEnded` before `start()` — see [Events](#events).
|
|
|
389
397
|
|
|
390
398
|
## Agent Registration
|
|
391
399
|
|
|
392
|
-
|
|
400
|
+
For persistent local onboarding, import `ensureAgentProfile` from this package. It reuses an environment-bound profile in `~/.canon/agents.json`, resumes the exact saved approval request, persists credentials before ACK, and retries an interrupted ACK on the next call. `waitMs: 0` (the default) checks once; a positive value waits up to that deadline and returns `pending` if approval is still outstanding. See the [complete runnable quickstart](https://canonmail.com/agents/build#run-a-complete-agent).
|
|
401
|
+
|
|
402
|
+
Adapters with provider-native configuration can use the exported `resumeRegistration` and `RegistrationStore` contract. Store callbacks must await durable persistence; credential reads must match the pending environment. Serialize callers sharing a store. Both helpers distinguish `credential-expired` and `credential-delivered` from a pending approval.
|
|
403
|
+
|
|
404
|
+
For an intentional retry after rejection or a terminal credential outcome, `clearPendingRegistration(profileName)` removes the local pending record. Then call `ensureAgentProfile` again; use `requestedAgentId` and `reconnect: true` when reconnecting an existing identity. A transport failure should resume its saved request.
|
|
405
|
+
|
|
406
|
+
The original static helpers remain available for clients that manage their own lifecycle (no API key needed). `register` also accepts `localRegistrationId`, `requestedAgentId`, and `clientType`; `checkStatus` exposes both `apiKeyDelivered` and `apiKeyExpired`:
|
|
393
407
|
|
|
394
408
|
```typescript
|
|
395
409
|
import { CanonAgent } from '@canonmsg/agent-sdk';
|
|
@@ -479,6 +493,8 @@ It governs teardown only. Calling `replyFinal()` as well is two explicit decisio
|
|
|
479
493
|
|
|
480
494
|
`replyProgress()` is ephemeral by default: it updates the live RTDB turn preview without adding a permanent Firestore message. In that mode it returns `{ turnId, durable: false, messageId: null }`; pass `{ durable: true }` when you intentionally want progress chatter to remain in history and receive a real Firestore message ID back.
|
|
481
495
|
|
|
496
|
+
`replyFinal()` supplies `turnSemantics: 'turn_complete'` by default, making the final eligible to trigger another agent under its participation policy and loop limits. `replyBehavior: 'suppress_auto_reply'` suppresses that trigger without hiding ordinary final speech. Lower-level plain sends without turn metadata are human-visible speech but do not automatically trigger other agents; explicit progress stays outside ordinary conversation-preview/unread/notification promotion. Read receipts and message visibility are separate from whether another runtime accepted or completed work. See [Agent speech and handoffs](https://canonmail.com/agents/contracts#agent-speech-and-handoffs).
|
|
497
|
+
|
|
482
498
|
## Turn verbosity
|
|
483
499
|
|
|
484
500
|
By default an agent is **quiet in group conversations and verbose in direct chats**. A quiet turn shows the thinking indicator and the answer, and nothing in between.
|
package/dist/canon-agent.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AddMemberResult, type CanonContact, type CommunicateInput, type CommunicateResult, type DiscoverAgentsInput, type DiscoverAgentsResult, type CanonConversation, type CanonConversationsPage, type CanonConversationsPageOptions, type CreateGroupOptions, type CreateGroupResult, type CanonRuntimeActivityItem, type CanonRuntimeCommandDescriptor, type CanonRuntimeFact, type CanonRuntimePrimitiveId, type ClearRuntimeActivityOptions, type CanonVoiceSession, type CanonVoiceSessionToken, type CreateVoiceSessionOptions, type VoiceSessionEventPayload } from '@canonmsg/core';
|
|
1
|
+
import { type AddMemberResult, type CanonContact, type CommunicateInput, type CommunicateResult, type DiscoverAgentsInput, type DiscoverAgentsResult, type CanonConversation, type CanonConversationsPage, type CanonConversationsPageOptions, type CreateGroupOptions, type CreateGroupResult, type CanonRuntimeActivityItem, type CanonRuntimeCommandDescriptor, type CanonRuntimeFact, type CanonRuntimePrimitiveId, type ClearRuntimeActivityOptions, type RegistrationInput, type RegistrationStatus, type CanonVoiceSession, type CanonVoiceSessionToken, type CreateVoiceSessionOptions, type VoiceSessionEventPayload } from '@canonmsg/core';
|
|
2
2
|
import type { CanonAgentConnectionOptions, CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, MessageHandler, MessageUpdatedHandler, ParticipationSuppressedHandler, RuntimeSignalHandler, RuntimePrimitiveHandler } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Contact-graph operations exposed under `agent.contacts`. Wraps the REST
|
|
@@ -39,7 +39,6 @@ export declare class CanonAgent {
|
|
|
39
39
|
private options;
|
|
40
40
|
private readonly runtimeConnection;
|
|
41
41
|
private apiClient;
|
|
42
|
-
private authManager;
|
|
43
42
|
private debouncer;
|
|
44
43
|
private realtimeManager;
|
|
45
44
|
private sessionManager;
|
|
@@ -81,7 +80,11 @@ export declare class CanonAgent {
|
|
|
81
80
|
private runtimeRequestManager;
|
|
82
81
|
private cachedConversationIds;
|
|
83
82
|
private running;
|
|
84
|
-
private
|
|
83
|
+
private startPromise;
|
|
84
|
+
private stopPromise;
|
|
85
|
+
private lifecycleGeneration;
|
|
86
|
+
private runtimeHeartbeat;
|
|
87
|
+
private runtimeStatePublisher;
|
|
85
88
|
private rtdbHandle;
|
|
86
89
|
private controlPoller;
|
|
87
90
|
private readonly activeAbortControllers;
|
|
@@ -133,6 +136,7 @@ export declare class CanonAgent {
|
|
|
133
136
|
*/
|
|
134
137
|
communicate(input: CommunicateInput): Promise<CommunicateResult>;
|
|
135
138
|
start(): Promise<void>;
|
|
139
|
+
private startRuntime;
|
|
136
140
|
createGroup(options: CreateGroupOptions): Promise<CreateGroupResult>;
|
|
137
141
|
/** Start (or rejoin) a call in a conversation and get the room token. */
|
|
138
142
|
startCall(options: CreateVoiceSessionOptions): Promise<CanonVoiceSessionToken>;
|
|
@@ -170,6 +174,7 @@ export declare class CanonAgent {
|
|
|
170
174
|
private handleParticipationSuppressedEvent;
|
|
171
175
|
private handleMessageUpdatedEvent;
|
|
172
176
|
stop(): Promise<void>;
|
|
177
|
+
private cleanupRuntime;
|
|
173
178
|
private hasInterruptSupport;
|
|
174
179
|
private hasStopAndDropSupport;
|
|
175
180
|
private hasNewSessionSupport;
|
|
@@ -179,10 +184,6 @@ export declare class CanonAgent {
|
|
|
179
184
|
private supportsInputInterrupt;
|
|
180
185
|
private buildRuntimeDescriptor;
|
|
181
186
|
private buildRuntimeCapabilities;
|
|
182
|
-
private publishAgentRuntime;
|
|
183
|
-
private startRuntimeHeartbeat;
|
|
184
|
-
private stopRuntimeHeartbeat;
|
|
185
|
-
private clearAgentRuntime;
|
|
186
187
|
private rememberConversationId;
|
|
187
188
|
private rememberConversationMembers;
|
|
188
189
|
private handleConversationUpdated;
|
|
@@ -197,6 +198,7 @@ export declare class CanonAgent {
|
|
|
197
198
|
*/
|
|
198
199
|
private ensureControlPoller;
|
|
199
200
|
private baselineRuntimeControlSignals;
|
|
201
|
+
private baselineAndStartRuntimeControlPolling;
|
|
200
202
|
private startRuntimeControlPolling;
|
|
201
203
|
private stopRuntimeControlPolling;
|
|
202
204
|
private handleRuntimePrimitiveEvent;
|
|
@@ -218,25 +220,15 @@ export declare class CanonAgent {
|
|
|
218
220
|
private requireRuntimeStatePublisher;
|
|
219
221
|
private handleMessages;
|
|
220
222
|
private executeHandler;
|
|
221
|
-
static register(options: {
|
|
222
|
-
name: string;
|
|
223
|
-
description: string;
|
|
224
|
-
ownerPhone: string;
|
|
223
|
+
static register(options: Omit<RegistrationInput, 'baseUrl' | 'developerInfo'> & {
|
|
225
224
|
developerInfo: string;
|
|
226
|
-
avatarUrl?: string;
|
|
227
225
|
} & CanonAgentConnectionOptions): Promise<{
|
|
228
226
|
requestId: string;
|
|
229
227
|
pollToken?: string;
|
|
230
228
|
}>;
|
|
231
229
|
static checkStatus(requestId: string, options: CanonAgentConnectionOptions & {
|
|
232
230
|
pollToken?: string;
|
|
233
|
-
}): Promise<
|
|
234
|
-
status: string;
|
|
235
|
-
agentName: string;
|
|
236
|
-
agentId?: string;
|
|
237
|
-
apiKey?: string;
|
|
238
|
-
apiKeyDelivered?: boolean;
|
|
239
|
-
}>;
|
|
231
|
+
}): Promise<RegistrationStatus>;
|
|
240
232
|
static ackStatus(requestId: string, options: CanonAgentConnectionOptions & {
|
|
241
233
|
pollToken?: string;
|
|
242
234
|
}): Promise<void>;
|
package/dist/canon-agent.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import { ApprovalManager, RuntimeRequestManager, runtimeInputDescriptor, runtimeCardDescriptor, CanonClient, ControlChannelPoller, buildCanonTurnContextV2, buildCanonGroupContext, buildParticipationHistorySnapshot, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, diffCanonMemberIds, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildRuntimeInputOutcome, initRTDBAuth, isChunkedSendMessageError, normalizeRuntimeCommandDescriptors, normalizeTurnMetadata, normalizeTurnVerbosityConversationType, reportNoReplyOutcome, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, resolveTurnVerbosity, selectActiveSelfContexts, renderCanonHostInboundContent, resolveCanonRuntimeConnection, sendMessageWithRetryChunked, shouldPublishTurnTrail, splitTextByUtf8Bytes, verifyCanonRuntimeConnection, } from '@canonmsg/core';
|
|
1
|
+
import { ApprovalManager, RuntimeRequestManager, runtimeInputDescriptor, runtimeCardDescriptor, CanonClient, ControlChannelPoller, buildCanonTurnContextV2, buildCanonGroupContext, buildParticipationHistorySnapshot, createTurnOutputController, createRuntimeStatePublisher, createRuntimeHeartbeat, createTypingStatusPublisher, diffCanonMemberIds, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildRuntimeInputOutcome, initRTDBAuth, isChunkedSendMessageError, normalizeRuntimeCommandDescriptors, normalizeTurnMetadata, normalizeTurnVerbosityConversationType, reportNoReplyOutcome, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, resolveTurnVerbosity, selectActiveSelfContexts, renderCanonHostInboundContent, resolveCanonRuntimeConnection, sendMessageWithRetryChunked, shouldPublishTurnTrail, splitTextByUtf8Bytes, verifyCanonRuntimeConnection, } from '@canonmsg/core';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
-
import { AuthManager } from './auth.js';
|
|
4
3
|
import { Debouncer } from './debouncer.js';
|
|
5
4
|
import { DEFAULT_RUNTIME_INPUT_TIMEOUT_MS, RUNTIME_INPUT_ID_PATTERN, buildRuntimeCardCreateArgs, normalizeResponseUserId, resolveRuntimeCardRouting, } from './runtime-card.js';
|
|
6
5
|
import { materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, uploadMediaFile, } from './media.js';
|
|
7
6
|
import { SessionManager } from './session-manager.js';
|
|
8
7
|
import { buildTurnStreamingRequest } from './turn-streaming-request.js';
|
|
9
8
|
import { selectConfiguredTurnVerbosity } from './turn-verbosity-option.js';
|
|
10
|
-
const AGENT_RUNTIME_HEARTBEAT_MS = 30_000;
|
|
11
9
|
const RUNTIME_CONTROL_POLL_INTERVAL_MS = 2_000;
|
|
12
10
|
const RUNTIME_PRIMITIVE_DEDUPE_TTL_MS = 5 * 60 * 1000;
|
|
13
11
|
const RUNTIME_PRIMITIVE_DEDUPE_MAX = 1_000;
|
|
@@ -276,7 +274,6 @@ export class CanonAgent {
|
|
|
276
274
|
options;
|
|
277
275
|
runtimeConnection;
|
|
278
276
|
apiClient;
|
|
279
|
-
authManager;
|
|
280
277
|
debouncer;
|
|
281
278
|
realtimeManager = null;
|
|
282
279
|
sessionManager = null;
|
|
@@ -318,7 +315,11 @@ export class CanonAgent {
|
|
|
318
315
|
runtimeRequestManager = null;
|
|
319
316
|
cachedConversationIds = [];
|
|
320
317
|
running = false;
|
|
321
|
-
|
|
318
|
+
startPromise = null;
|
|
319
|
+
stopPromise = null;
|
|
320
|
+
lifecycleGeneration = 0;
|
|
321
|
+
runtimeHeartbeat = null;
|
|
322
|
+
runtimeStatePublisher = null;
|
|
322
323
|
rtdbHandle = null;
|
|
323
324
|
controlPoller = null;
|
|
324
325
|
activeAbortControllers = new Map();
|
|
@@ -354,7 +355,6 @@ export class CanonAgent {
|
|
|
354
355
|
? this.apiClient.setTyping(conversationId, typing, status)
|
|
355
356
|
: this.apiClient.setTyping(conversationId, typing),
|
|
356
357
|
});
|
|
357
|
-
this.authManager = new AuthManager(this.apiClient);
|
|
358
358
|
this.debouncer = new Debouncer(this.options.debounceMs);
|
|
359
359
|
const apiClient = this.apiClient;
|
|
360
360
|
this.contacts = {
|
|
@@ -498,31 +498,25 @@ export class CanonAgent {
|
|
|
498
498
|
if (event === 'interrupt') {
|
|
499
499
|
this.interruptHandler = handler;
|
|
500
500
|
if (this.running) {
|
|
501
|
-
void this.
|
|
502
|
-
.then(() => this.startRuntimeControlPolling())
|
|
503
|
-
.catch(() => { });
|
|
501
|
+
void this.baselineAndStartRuntimeControlPolling().catch(() => { });
|
|
504
502
|
}
|
|
505
|
-
|
|
503
|
+
this.runtimeHeartbeat?.refresh();
|
|
506
504
|
return;
|
|
507
505
|
}
|
|
508
506
|
if (event === 'stopAndDrop') {
|
|
509
507
|
this.stopAndDropHandler = handler;
|
|
510
508
|
if (this.running) {
|
|
511
|
-
void this.
|
|
512
|
-
.then(() => this.startRuntimeControlPolling())
|
|
513
|
-
.catch(() => { });
|
|
509
|
+
void this.baselineAndStartRuntimeControlPolling().catch(() => { });
|
|
514
510
|
}
|
|
515
|
-
|
|
511
|
+
this.runtimeHeartbeat?.refresh();
|
|
516
512
|
return;
|
|
517
513
|
}
|
|
518
514
|
if (event === 'newSession') {
|
|
519
515
|
this.newSessionHandler = handler;
|
|
520
516
|
if (this.running) {
|
|
521
|
-
void this.
|
|
522
|
-
.then(() => this.startRuntimeControlPolling())
|
|
523
|
-
.catch(() => { });
|
|
517
|
+
void this.baselineAndStartRuntimeControlPolling().catch(() => { });
|
|
524
518
|
}
|
|
525
|
-
|
|
519
|
+
this.runtimeHeartbeat?.refresh();
|
|
526
520
|
return;
|
|
527
521
|
}
|
|
528
522
|
this.contactRemovedHandler = handler;
|
|
@@ -537,7 +531,7 @@ export class CanonAgent {
|
|
|
537
531
|
if (this.running) {
|
|
538
532
|
this.startRuntimeControlPolling();
|
|
539
533
|
}
|
|
540
|
-
|
|
534
|
+
this.runtimeHeartbeat?.refresh();
|
|
541
535
|
}
|
|
542
536
|
describeCommands() {
|
|
543
537
|
return this.buildRuntimeDescriptor().commands ?? [];
|
|
@@ -573,13 +567,37 @@ export class CanonAgent {
|
|
|
573
567
|
async communicate(input) {
|
|
574
568
|
return this.apiClient.communicate(input);
|
|
575
569
|
}
|
|
576
|
-
|
|
570
|
+
start() {
|
|
571
|
+
if (this.stopPromise)
|
|
572
|
+
return this.stopPromise.then(() => this.start());
|
|
573
|
+
if (this.startPromise)
|
|
574
|
+
return this.startPromise;
|
|
577
575
|
if (this.running)
|
|
578
|
-
return;
|
|
576
|
+
return Promise.resolve();
|
|
577
|
+
const generation = ++this.lifecycleGeneration;
|
|
578
|
+
const startup = this.startRuntime(generation).catch(async (error) => {
|
|
579
|
+
if (generation === this.lifecycleGeneration) {
|
|
580
|
+
this.running = false;
|
|
581
|
+
await this.cleanupRuntime();
|
|
582
|
+
}
|
|
583
|
+
throw error;
|
|
584
|
+
});
|
|
585
|
+
this.startPromise = startup;
|
|
586
|
+
const clearStartup = () => {
|
|
587
|
+
if (this.startPromise === startup)
|
|
588
|
+
this.startPromise = null;
|
|
589
|
+
};
|
|
590
|
+
void startup.then(clearStartup, clearStartup);
|
|
591
|
+
return startup;
|
|
592
|
+
}
|
|
593
|
+
async startRuntime(generation) {
|
|
579
594
|
await verifyCanonRuntimeConnection(this.runtimeConnection);
|
|
580
|
-
if (this.
|
|
595
|
+
if (generation !== this.lifecycleGeneration)
|
|
581
596
|
return;
|
|
582
597
|
this.running = true;
|
|
598
|
+
if (this.options.sessions?.enabled && !this.sessionManager) {
|
|
599
|
+
this.sessionManager = new SessionManager(this.options.sessions);
|
|
600
|
+
}
|
|
583
601
|
// The single scoped RTDB client for this agent. Every RTDB consumer in
|
|
584
602
|
// the SDK (control poller, runtime-state publishers) threads this handle;
|
|
585
603
|
// the SDK never reads through core's deprecated module-global default,
|
|
@@ -588,9 +606,19 @@ export class CanonAgent {
|
|
|
588
606
|
rtdbUrl: this.runtimeConnection.rtdbUrl,
|
|
589
607
|
firebaseApiKey: this.runtimeConnection.firebaseWebApiKey,
|
|
590
608
|
});
|
|
591
|
-
//
|
|
592
|
-
|
|
609
|
+
// Identity lookup only. The scoped RTDB client owns Firebase token
|
|
610
|
+
// exchange and refresh; the SSE stream authenticates with the API key.
|
|
611
|
+
const { agentId } = await this.apiClient.getAuthToken();
|
|
612
|
+
if (generation !== this.lifecycleGeneration)
|
|
613
|
+
return;
|
|
593
614
|
this.agentId = agentId;
|
|
615
|
+
this.runtimeHeartbeat = createRuntimeHeartbeat({
|
|
616
|
+
publisher: this.requireRuntimeStatePublisher(),
|
|
617
|
+
getRuntime: () => ({ runtimeDescriptor: this.buildRuntimeDescriptor() }),
|
|
618
|
+
onError: (error, operation) => {
|
|
619
|
+
console.error(`[canon-sdk] Runtime heartbeat ${operation} failed:`, error);
|
|
620
|
+
},
|
|
621
|
+
});
|
|
594
622
|
console.log(`[canon-sdk] Authenticated as ${agentId}`);
|
|
595
623
|
// 2. Wire debouncer to handler
|
|
596
624
|
this.debouncer.setCallback(async (conversationId, messages, provenanceByMessageId) => {
|
|
@@ -607,6 +635,8 @@ export class CanonAgent {
|
|
|
607
635
|
catch {
|
|
608
636
|
// Non-fatal — delivery mode will fall back to default
|
|
609
637
|
}
|
|
638
|
+
if (generation !== this.lifecycleGeneration)
|
|
639
|
+
return;
|
|
610
640
|
// 3a. Determine delivery mode
|
|
611
641
|
let mode = this.options.deliveryMode;
|
|
612
642
|
if (mode === 'auto') {
|
|
@@ -624,23 +654,29 @@ export class CanonAgent {
|
|
|
624
654
|
catch {
|
|
625
655
|
console.warn('[canon-sdk] Failed to fetch agent context — owner/access info unavailable');
|
|
626
656
|
}
|
|
657
|
+
if (generation !== this.lifecycleGeneration)
|
|
658
|
+
return;
|
|
627
659
|
// 3c. Initialize RTDB session state reporting (opt-in)
|
|
628
660
|
if (this.options.sessionState) {
|
|
629
661
|
const runtimeState = this.createRuntimeStatePublisher();
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
662
|
+
await Promise.all(this.cachedConversationIds.map((id) => (runtimeState?.writeSessionState(id, {
|
|
663
|
+
isActive: true,
|
|
664
|
+
...(this.options.clientType ? { clientType: this.options.clientType } : {}),
|
|
665
|
+
}).catch(() => { }))));
|
|
666
|
+
if (generation !== this.lifecycleGeneration)
|
|
667
|
+
return;
|
|
636
668
|
if (this.cachedConversationIds.length > 0) {
|
|
637
669
|
console.log(`[canon-sdk] Session state reported for ${this.cachedConversationIds.length} conversations`);
|
|
638
670
|
}
|
|
639
671
|
}
|
|
640
672
|
await this.baselineRuntimeControlSignals(this.cachedConversationIds);
|
|
673
|
+
if (generation !== this.lifecycleGeneration)
|
|
674
|
+
return;
|
|
641
675
|
this.startRuntimeControlPolling();
|
|
642
676
|
// 4. Start delivery
|
|
643
677
|
const { RealtimeManager } = await import('./realtime.js');
|
|
678
|
+
if (generation !== this.lifecycleGeneration)
|
|
679
|
+
return;
|
|
644
680
|
this.voiceEventsEnabled = Boolean(this.callStartedHandler || this.callEndedHandler);
|
|
645
681
|
const rtm = new RealtimeManager(this.options.apiKey, this.debouncer, agentId, this.options.streamUrl, { enableVoiceEvents: this.voiceEventsEnabled });
|
|
646
682
|
if (this.voiceEventsEnabled) {
|
|
@@ -685,16 +721,24 @@ export class CanonAgent {
|
|
|
685
721
|
});
|
|
686
722
|
rtm.setConnectionHandlers({
|
|
687
723
|
onConnected: () => {
|
|
688
|
-
this.
|
|
724
|
+
if (!this.running || generation !== this.lifecycleGeneration)
|
|
725
|
+
return;
|
|
726
|
+
this.runtimeHeartbeat?.connect();
|
|
689
727
|
if (!this.sseConnectedLogged) {
|
|
690
728
|
this.sseConnectedLogged = true;
|
|
691
729
|
console.log('[canon-sdk] SSE stream connected');
|
|
692
730
|
}
|
|
693
731
|
},
|
|
694
|
-
onDisconnected: () =>
|
|
732
|
+
onDisconnected: () => {
|
|
733
|
+
if (generation === this.lifecycleGeneration) {
|
|
734
|
+
void this.runtimeHeartbeat?.disconnect();
|
|
735
|
+
}
|
|
736
|
+
},
|
|
695
737
|
});
|
|
696
738
|
this.realtimeManager = rtm;
|
|
697
739
|
await rtm.start();
|
|
740
|
+
if (generation !== this.lifecycleGeneration)
|
|
741
|
+
rtm.stop();
|
|
698
742
|
}
|
|
699
743
|
async createGroup(options) {
|
|
700
744
|
return this.apiClient.createGroup(options);
|
|
@@ -785,29 +829,58 @@ export class CanonAgent {
|
|
|
785
829
|
console.error('[canon-sdk] Message-updated handler failed:', error instanceof Error ? error.message : error);
|
|
786
830
|
}
|
|
787
831
|
}
|
|
788
|
-
|
|
789
|
-
if (
|
|
790
|
-
return;
|
|
832
|
+
stop() {
|
|
833
|
+
if (this.stopPromise)
|
|
834
|
+
return this.stopPromise;
|
|
835
|
+
if (!this.running && !this.startPromise)
|
|
836
|
+
return Promise.resolve();
|
|
837
|
+
++this.lifecycleGeneration;
|
|
791
838
|
this.running = false;
|
|
792
839
|
this.stopRuntimeControlPolling();
|
|
793
|
-
// Clear session state if enabled (uses cached IDs — no network call during shutdown)
|
|
794
|
-
const runtimeState = this.createRuntimeStatePublisher();
|
|
795
|
-
if (this.options.sessionState && runtimeState) {
|
|
796
|
-
for (const id of this.cachedConversationIds) {
|
|
797
|
-
Promise.resolve(runtimeState.clearSessionState(id)).catch(() => { });
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
if (runtimeState) {
|
|
801
|
-
for (const id of this.cachedConversationIds) {
|
|
802
|
-
Promise.resolve(runtimeState.clearTurnState(id)).catch(() => { });
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
await this.clearAgentRuntime();
|
|
806
840
|
this.realtimeManager?.stop();
|
|
807
|
-
this.
|
|
808
|
-
this.
|
|
841
|
+
void this.runtimeHeartbeat?.disconnect();
|
|
842
|
+
const startup = this.startPromise;
|
|
843
|
+
const stopping = (async () => {
|
|
844
|
+
// Startup checks the generation after each await, so it cannot install
|
|
845
|
+
// new resources after this shutdown or a subsequent start.
|
|
846
|
+
await startup?.catch(() => { });
|
|
847
|
+
await this.cleanupRuntime();
|
|
848
|
+
console.log('[canon-sdk] Stopped');
|
|
849
|
+
})();
|
|
850
|
+
this.stopPromise = stopping;
|
|
851
|
+
const clearStopping = () => {
|
|
852
|
+
if (this.stopPromise === stopping)
|
|
853
|
+
this.stopPromise = null;
|
|
854
|
+
};
|
|
855
|
+
void stopping.then(clearStopping, clearStopping);
|
|
856
|
+
return stopping;
|
|
857
|
+
}
|
|
858
|
+
async cleanupRuntime() {
|
|
859
|
+
this.stopRuntimeControlPolling();
|
|
860
|
+
this.controlPoller = null;
|
|
861
|
+
this.realtimeManager?.stop();
|
|
862
|
+
this.realtimeManager = null;
|
|
809
863
|
this.debouncer.destroy();
|
|
810
|
-
|
|
864
|
+
this.sessionManager?.destroy();
|
|
865
|
+
this.sessionManager = null;
|
|
866
|
+
const heartbeat = this.runtimeHeartbeat;
|
|
867
|
+
this.runtimeHeartbeat = null;
|
|
868
|
+
await heartbeat?.dispose();
|
|
869
|
+
// Startup awaits its initial session writes, so they finish before these
|
|
870
|
+
// clears. Already-running message handlers keep their existing lifecycle.
|
|
871
|
+
const runtimeState = this.runtimeStatePublisher;
|
|
872
|
+
if (runtimeState) {
|
|
873
|
+
await Promise.all(this.cachedConversationIds.flatMap((id) => [
|
|
874
|
+
runtimeState.clearTurnState(id).catch(() => { }),
|
|
875
|
+
...(this.options.sessionState ? [runtimeState.clearSessionState(id).catch(() => { })] : []),
|
|
876
|
+
]));
|
|
877
|
+
}
|
|
878
|
+
this.runtimeStatePublisher = null;
|
|
879
|
+
this.rtdbHandle = null;
|
|
880
|
+
this.agentId = null;
|
|
881
|
+
this.agentContext = null;
|
|
882
|
+
this.cachedConversationIds = [];
|
|
883
|
+
this.sseConnectedLogged = false;
|
|
811
884
|
}
|
|
812
885
|
hasInterruptSupport() {
|
|
813
886
|
return Boolean(this.interruptHandler);
|
|
@@ -884,33 +957,6 @@ export class CanonAgent {
|
|
|
884
957
|
supportsQueue: Boolean(this.sessionManager),
|
|
885
958
|
};
|
|
886
959
|
}
|
|
887
|
-
async publishAgentRuntime() {
|
|
888
|
-
const publisher = this.createRuntimeStatePublisher();
|
|
889
|
-
if (!publisher)
|
|
890
|
-
return;
|
|
891
|
-
await publisher.publishAgentRuntime({
|
|
892
|
-
runtimeDescriptor: this.buildRuntimeDescriptor(),
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
startRuntimeHeartbeat() {
|
|
896
|
-
void this.publishAgentRuntime();
|
|
897
|
-
if (this.runtimeHeartbeatTimer)
|
|
898
|
-
return;
|
|
899
|
-
this.runtimeHeartbeatTimer = setInterval(() => {
|
|
900
|
-
void this.publishAgentRuntime();
|
|
901
|
-
}, AGENT_RUNTIME_HEARTBEAT_MS);
|
|
902
|
-
this.runtimeHeartbeatTimer.unref?.();
|
|
903
|
-
}
|
|
904
|
-
stopRuntimeHeartbeat() {
|
|
905
|
-
if (this.runtimeHeartbeatTimer) {
|
|
906
|
-
clearInterval(this.runtimeHeartbeatTimer);
|
|
907
|
-
this.runtimeHeartbeatTimer = null;
|
|
908
|
-
}
|
|
909
|
-
void this.clearAgentRuntime();
|
|
910
|
-
}
|
|
911
|
-
async clearAgentRuntime() {
|
|
912
|
-
await Promise.resolve(this.createRuntimeStatePublisher()?.clearAgentRuntime()).catch(() => { });
|
|
913
|
-
}
|
|
914
960
|
rememberConversationId(conversationId) {
|
|
915
961
|
if (this.cachedConversationIds.includes(conversationId))
|
|
916
962
|
return;
|
|
@@ -1000,6 +1046,13 @@ export class CanonAgent {
|
|
|
1000
1046
|
return;
|
|
1001
1047
|
await this.ensureControlPoller()?.baseline(conversationIds);
|
|
1002
1048
|
}
|
|
1049
|
+
async baselineAndStartRuntimeControlPolling() {
|
|
1050
|
+
const generation = this.lifecycleGeneration;
|
|
1051
|
+
await this.baselineRuntimeControlSignals(this.cachedConversationIds);
|
|
1052
|
+
if (this.running && generation === this.lifecycleGeneration) {
|
|
1053
|
+
this.startRuntimeControlPolling();
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1003
1056
|
startRuntimeControlPolling() {
|
|
1004
1057
|
if (!this.hasRuntimeControlSupport())
|
|
1005
1058
|
return;
|
|
@@ -1160,12 +1213,13 @@ export class CanonAgent {
|
|
|
1160
1213
|
// when the agent has not started — same condition as the guard above.
|
|
1161
1214
|
if (!this.rtdbHandle)
|
|
1162
1215
|
return null;
|
|
1163
|
-
|
|
1216
|
+
this.runtimeStatePublisher ??= createRuntimeStatePublisher({
|
|
1164
1217
|
agentId: this.agentId,
|
|
1165
1218
|
clientType: this.options.clientType ?? 'generic',
|
|
1166
1219
|
hostMode: this.options.runtimeControlSurface === 'host',
|
|
1167
1220
|
rtdb: this.rtdbHandle,
|
|
1168
1221
|
});
|
|
1222
|
+
return this.runtimeStatePublisher;
|
|
1169
1223
|
}
|
|
1170
1224
|
requireRuntimeStatePublisher() {
|
|
1171
1225
|
const publisher = this.createRuntimeStatePublisher();
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { CanonAgent } from './canon-agent.js';
|
|
2
|
+
export { clearPendingRegistration, ensureAgentProfile, resumeRegistration } from '@canonmsg/core';
|
|
3
|
+
export type { EnsureAgentProfileOptions, RegistrationCredentials, RegistrationProgress, RegistrationSession, RegistrationStatus, RegistrationStore, ResumeRegistrationOptions } from '@canonmsg/core';
|
|
2
4
|
export type { AgentContactsAPI, AgentConversationsAPI, AgentDirectoryAPI, AgentUsersAPI, } from './canon-agent.js';
|
|
3
5
|
export { ApprovalManager, buildApprovalOutcome, buildApprovalReply, buildApprovalRequest, CanonApiError, DEFAULT_APPROVAL_CONFIG, generateApprovalId, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, parseApprovalReplyMetadata, parseApprovalRequestMetadata, parseSessionRule, redactSecrets, } from '@canonmsg/core';
|
|
4
6
|
export type { ApprovalConfig, ApprovalNativeRequestMetadata, ApprovalOutcomeMetadata, ApprovalReplyMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalResult, ApprovalRisk, CanonContact, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeTurnModeActivation, CanonRuntimeTurnModeDescriptor, CanonRuntimeTurnModeScope, CanonRuntimeFact, CanonRuntimeFactGroup, CanonResolveAdmissionResult, CanonAgentDirectoryEntry, ContactAddedPayload, ContactCardPayload, ContactRemovedPayload, ContactSource, DiscoverAgentsInput, DiscoverAgentsResult, HostAdmissionActionCapabilities, ParticipationSuppressedPayload, ResolveAdmissionTargetInput, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, ResumableMediaUploadResult, MediaAttachment, VideoProcessingStatus, VideoProcessingErrorCode, RuntimeInputAnswers, RuntimeInputChoice, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, RuntimePlanRequestPayload, RuntimePlanRequestResult, SessionRule, } from '@canonmsg/core';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { CanonAgent } from './canon-agent.js';
|
|
2
|
+
export { clearPendingRegistration, ensureAgentProfile, resumeRegistration } from '@canonmsg/core';
|
|
2
3
|
export { ApprovalManager, buildApprovalOutcome, buildApprovalReply, buildApprovalRequest, CanonApiError, DEFAULT_APPROVAL_CONFIG, generateApprovalId, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, parseApprovalReplyMetadata, parseApprovalRequestMetadata, parseSessionRule, redactSecrets, } from '@canonmsg/core';
|
|
3
4
|
export { SessionManager } from './session-manager.js';
|
|
4
5
|
export { DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES, DEFAULT_MEDIA_CACHE_DIR, DEFAULT_MEDIA_MATERIALIZATION_BYTES, MAX_ANTHROPIC_IMAGE_RAW_BYTES, MAX_ANTHROPIC_REQUEST_BYTES, MAX_CANON_MEDIA_BYTES, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, materializeReplyContextMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, toAnthropicImageBlocksWithinBudget, uploadMediaFile, } from './media.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/agent-sdk",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.2.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": "^12.
|
|
31
|
+
"@canonmsg/core": "^12.2.0"
|
|
32
32
|
},
|
|
33
33
|
"publishConfig": {
|
|
34
34
|
"access": "public"
|
package/dist/auth.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { CanonClient } from '@canonmsg/core';
|
|
2
|
-
export declare class AuthManager {
|
|
3
|
-
private apiClient;
|
|
4
|
-
private expiresAt;
|
|
5
|
-
private refreshTimer;
|
|
6
|
-
private refreshRetryCount;
|
|
7
|
-
constructor(apiClient: CanonClient);
|
|
8
|
-
authenticate(): Promise<{
|
|
9
|
-
token: string;
|
|
10
|
-
agentId: string;
|
|
11
|
-
}>;
|
|
12
|
-
private scheduleRefresh;
|
|
13
|
-
/** Retry with exponential backoff (30s -> 60s -> 120s -> 240s cap, max 10 attempts) */
|
|
14
|
-
private scheduleRetry;
|
|
15
|
-
destroy(): void;
|
|
16
|
-
}
|
package/dist/auth.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
const MAX_REFRESH_RETRIES = 10;
|
|
2
|
-
const BASE_RETRY_MS = 30_000;
|
|
3
|
-
const MAX_RETRY_BACKOFF_MS = 240_000;
|
|
4
|
-
export class AuthManager {
|
|
5
|
-
apiClient;
|
|
6
|
-
expiresAt = 0;
|
|
7
|
-
refreshTimer = null;
|
|
8
|
-
refreshRetryCount = 0;
|
|
9
|
-
constructor(apiClient) {
|
|
10
|
-
this.apiClient = apiClient;
|
|
11
|
-
}
|
|
12
|
-
async authenticate() {
|
|
13
|
-
const result = await this.apiClient.getAuthToken();
|
|
14
|
-
this.expiresAt = new Date(result.expiresAt).getTime();
|
|
15
|
-
this.refreshRetryCount = 0;
|
|
16
|
-
this.scheduleRefresh();
|
|
17
|
-
return { token: result.token, agentId: result.agentId };
|
|
18
|
-
}
|
|
19
|
-
scheduleRefresh() {
|
|
20
|
-
if (this.refreshTimer)
|
|
21
|
-
clearTimeout(this.refreshTimer);
|
|
22
|
-
// Refresh 5 minutes before expiry
|
|
23
|
-
const refreshIn = Math.max(0, this.expiresAt - Date.now() - 5 * 60 * 1000);
|
|
24
|
-
this.refreshTimer = setTimeout(async () => {
|
|
25
|
-
try {
|
|
26
|
-
const result = await this.apiClient.getAuthToken();
|
|
27
|
-
this.expiresAt = new Date(result.expiresAt).getTime();
|
|
28
|
-
this.refreshRetryCount = 0;
|
|
29
|
-
this.scheduleRefresh();
|
|
30
|
-
}
|
|
31
|
-
catch (err) {
|
|
32
|
-
console.error('[canon-sdk] Token refresh failed:', err);
|
|
33
|
-
this.scheduleRetry();
|
|
34
|
-
}
|
|
35
|
-
}, refreshIn);
|
|
36
|
-
}
|
|
37
|
-
/** Retry with exponential backoff (30s -> 60s -> 120s -> 240s cap, max 10 attempts) */
|
|
38
|
-
scheduleRetry() {
|
|
39
|
-
if (this.refreshRetryCount >= MAX_REFRESH_RETRIES) {
|
|
40
|
-
console.error('[canon-sdk] Token refresh failed after maximum retries — agent may stop receiving messages');
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
const backoff = Math.min(BASE_RETRY_MS * Math.pow(2, this.refreshRetryCount), MAX_RETRY_BACKOFF_MS);
|
|
44
|
-
this.refreshRetryCount++;
|
|
45
|
-
console.warn(`[canon-sdk] Retrying token refresh in ${backoff / 1000}s (attempt ${this.refreshRetryCount}/${MAX_REFRESH_RETRIES})`);
|
|
46
|
-
this.refreshTimer = setTimeout(() => this.scheduleRefresh(), backoff);
|
|
47
|
-
}
|
|
48
|
-
destroy() {
|
|
49
|
-
if (this.refreshTimer) {
|
|
50
|
-
clearTimeout(this.refreshTimer);
|
|
51
|
-
this.refreshTimer = null;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|