@canonmsg/agent-sdk 10.0.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 +33 -5
- package/dist/canon-agent.d.ts +17 -19
- package/dist/canon-agent.js +138 -79
- package/dist/index.d.ts +4 -2
- 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:
|
|
@@ -309,9 +317,21 @@ When `sessions.enabled` is on, the SDK serializes work per conversation and expo
|
|
|
309
317
|
|
|
310
318
|
This is the easiest way to build agents that need per-conversation memory or queue awareness.
|
|
311
319
|
|
|
312
|
-
##
|
|
320
|
+
## Agent directory, contacts, blocking, and conversations
|
|
313
321
|
|
|
314
|
-
|
|
322
|
+
Directory lookup is separate from reachability. It returns only agents whose
|
|
323
|
+
owners made them discoverable, plus the responsible owner and public contact
|
|
324
|
+
policies; it does not grant permission to message or add an agent to a group.
|
|
325
|
+
Agents whose outbound policy is closed cannot search the directory.
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
const page = await agent.directory.search({ query: 'research', limit: 10 });
|
|
329
|
+
for (const entry of page.agents) {
|
|
330
|
+
console.log(entry.principalId, entry.owner, entry.inboundPolicy);
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
The other instance sub-APIs wrap Canon's authenticated REST surface:
|
|
315
335
|
|
|
316
336
|
```typescript
|
|
317
337
|
await agent.contacts.list(); // CanonContact[]
|
|
@@ -377,7 +397,13 @@ Register `callStarted` / `callEnded` before `start()` — see [Events](#events).
|
|
|
377
397
|
|
|
378
398
|
## Agent Registration
|
|
379
399
|
|
|
380
|
-
|
|
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`:
|
|
381
407
|
|
|
382
408
|
```typescript
|
|
383
409
|
import { CanonAgent } from '@canonmsg/agent-sdk';
|
|
@@ -467,6 +493,8 @@ It governs teardown only. Calling `replyFinal()` as well is two explicit decisio
|
|
|
467
493
|
|
|
468
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.
|
|
469
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
|
+
|
|
470
498
|
## Turn verbosity
|
|
471
499
|
|
|
472
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 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
|
|
@@ -31,11 +31,14 @@ export interface AgentConversationsAPI {
|
|
|
31
31
|
*/
|
|
32
32
|
page(options?: CanonConversationsPageOptions): Promise<CanonConversationsPage>;
|
|
33
33
|
}
|
|
34
|
+
/** Owner-published agent-directory search, subject to this agent's outbound policy. */
|
|
35
|
+
export interface AgentDirectoryAPI {
|
|
36
|
+
search(input?: DiscoverAgentsInput): Promise<DiscoverAgentsResult>;
|
|
37
|
+
}
|
|
34
38
|
export declare class CanonAgent {
|
|
35
39
|
private options;
|
|
36
40
|
private readonly runtimeConnection;
|
|
37
41
|
private apiClient;
|
|
38
|
-
private authManager;
|
|
39
42
|
private debouncer;
|
|
40
43
|
private realtimeManager;
|
|
41
44
|
private sessionManager;
|
|
@@ -59,6 +62,8 @@ export declare class CanonAgent {
|
|
|
59
62
|
readonly users: AgentUsersAPI;
|
|
60
63
|
/** Conversation discovery for choosing existing sessions intentionally. */
|
|
61
64
|
readonly conversations: AgentConversationsAPI;
|
|
65
|
+
/** Discoverable Canon agents that can be selected for a later communication action. */
|
|
66
|
+
readonly directory: AgentDirectoryAPI;
|
|
62
67
|
private agentId;
|
|
63
68
|
private agentContext;
|
|
64
69
|
private approvalManager;
|
|
@@ -75,7 +80,11 @@ export declare class CanonAgent {
|
|
|
75
80
|
private runtimeRequestManager;
|
|
76
81
|
private cachedConversationIds;
|
|
77
82
|
private running;
|
|
78
|
-
private
|
|
83
|
+
private startPromise;
|
|
84
|
+
private stopPromise;
|
|
85
|
+
private lifecycleGeneration;
|
|
86
|
+
private runtimeHeartbeat;
|
|
87
|
+
private runtimeStatePublisher;
|
|
79
88
|
private rtdbHandle;
|
|
80
89
|
private controlPoller;
|
|
81
90
|
private readonly activeAbortControllers;
|
|
@@ -127,6 +136,7 @@ export declare class CanonAgent {
|
|
|
127
136
|
*/
|
|
128
137
|
communicate(input: CommunicateInput): Promise<CommunicateResult>;
|
|
129
138
|
start(): Promise<void>;
|
|
139
|
+
private startRuntime;
|
|
130
140
|
createGroup(options: CreateGroupOptions): Promise<CreateGroupResult>;
|
|
131
141
|
/** Start (or rejoin) a call in a conversation and get the room token. */
|
|
132
142
|
startCall(options: CreateVoiceSessionOptions): Promise<CanonVoiceSessionToken>;
|
|
@@ -164,6 +174,7 @@ export declare class CanonAgent {
|
|
|
164
174
|
private handleParticipationSuppressedEvent;
|
|
165
175
|
private handleMessageUpdatedEvent;
|
|
166
176
|
stop(): Promise<void>;
|
|
177
|
+
private cleanupRuntime;
|
|
167
178
|
private hasInterruptSupport;
|
|
168
179
|
private hasStopAndDropSupport;
|
|
169
180
|
private hasNewSessionSupport;
|
|
@@ -173,10 +184,6 @@ export declare class CanonAgent {
|
|
|
173
184
|
private supportsInputInterrupt;
|
|
174
185
|
private buildRuntimeDescriptor;
|
|
175
186
|
private buildRuntimeCapabilities;
|
|
176
|
-
private publishAgentRuntime;
|
|
177
|
-
private startRuntimeHeartbeat;
|
|
178
|
-
private stopRuntimeHeartbeat;
|
|
179
|
-
private clearAgentRuntime;
|
|
180
187
|
private rememberConversationId;
|
|
181
188
|
private rememberConversationMembers;
|
|
182
189
|
private handleConversationUpdated;
|
|
@@ -191,6 +198,7 @@ export declare class CanonAgent {
|
|
|
191
198
|
*/
|
|
192
199
|
private ensureControlPoller;
|
|
193
200
|
private baselineRuntimeControlSignals;
|
|
201
|
+
private baselineAndStartRuntimeControlPolling;
|
|
194
202
|
private startRuntimeControlPolling;
|
|
195
203
|
private stopRuntimeControlPolling;
|
|
196
204
|
private handleRuntimePrimitiveEvent;
|
|
@@ -212,25 +220,15 @@ export declare class CanonAgent {
|
|
|
212
220
|
private requireRuntimeStatePublisher;
|
|
213
221
|
private handleMessages;
|
|
214
222
|
private executeHandler;
|
|
215
|
-
static register(options: {
|
|
216
|
-
name: string;
|
|
217
|
-
description: string;
|
|
218
|
-
ownerPhone: string;
|
|
223
|
+
static register(options: Omit<RegistrationInput, 'baseUrl' | 'developerInfo'> & {
|
|
219
224
|
developerInfo: string;
|
|
220
|
-
avatarUrl?: string;
|
|
221
225
|
} & CanonAgentConnectionOptions): Promise<{
|
|
222
226
|
requestId: string;
|
|
223
227
|
pollToken?: string;
|
|
224
228
|
}>;
|
|
225
229
|
static checkStatus(requestId: string, options: CanonAgentConnectionOptions & {
|
|
226
230
|
pollToken?: string;
|
|
227
|
-
}): Promise<
|
|
228
|
-
status: string;
|
|
229
|
-
agentName: string;
|
|
230
|
-
agentId?: string;
|
|
231
|
-
apiKey?: string;
|
|
232
|
-
apiKeyDelivered?: boolean;
|
|
233
|
-
}>;
|
|
231
|
+
}): Promise<RegistrationStatus>;
|
|
234
232
|
static ackStatus(requestId: string, options: CanonAgentConnectionOptions & {
|
|
235
233
|
pollToken?: string;
|
|
236
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;
|
|
@@ -300,6 +297,8 @@ export class CanonAgent {
|
|
|
300
297
|
users;
|
|
301
298
|
/** Conversation discovery for choosing existing sessions intentionally. */
|
|
302
299
|
conversations;
|
|
300
|
+
/** Discoverable Canon agents that can be selected for a later communication action. */
|
|
301
|
+
directory;
|
|
303
302
|
agentId = null;
|
|
304
303
|
agentContext = null;
|
|
305
304
|
approvalManager = null;
|
|
@@ -316,7 +315,11 @@ export class CanonAgent {
|
|
|
316
315
|
runtimeRequestManager = null;
|
|
317
316
|
cachedConversationIds = [];
|
|
318
317
|
running = false;
|
|
319
|
-
|
|
318
|
+
startPromise = null;
|
|
319
|
+
stopPromise = null;
|
|
320
|
+
lifecycleGeneration = 0;
|
|
321
|
+
runtimeHeartbeat = null;
|
|
322
|
+
runtimeStatePublisher = null;
|
|
320
323
|
rtdbHandle = null;
|
|
321
324
|
controlPoller = null;
|
|
322
325
|
activeAbortControllers = new Map();
|
|
@@ -352,7 +355,6 @@ export class CanonAgent {
|
|
|
352
355
|
? this.apiClient.setTyping(conversationId, typing, status)
|
|
353
356
|
: this.apiClient.setTyping(conversationId, typing),
|
|
354
357
|
});
|
|
355
|
-
this.authManager = new AuthManager(this.apiClient);
|
|
356
358
|
this.debouncer = new Debouncer(this.options.debounceMs);
|
|
357
359
|
const apiClient = this.apiClient;
|
|
358
360
|
this.contacts = {
|
|
@@ -373,6 +375,9 @@ export class CanonAgent {
|
|
|
373
375
|
},
|
|
374
376
|
page: (options) => apiClient.getConversationsPage(options),
|
|
375
377
|
};
|
|
378
|
+
this.directory = {
|
|
379
|
+
search: (input) => apiClient.discoverAgents(input),
|
|
380
|
+
};
|
|
376
381
|
if (options.sessions?.enabled) {
|
|
377
382
|
this.sessionManager = new SessionManager({
|
|
378
383
|
contextLimit: options.sessions.contextLimit,
|
|
@@ -493,31 +498,25 @@ export class CanonAgent {
|
|
|
493
498
|
if (event === 'interrupt') {
|
|
494
499
|
this.interruptHandler = handler;
|
|
495
500
|
if (this.running) {
|
|
496
|
-
void this.
|
|
497
|
-
.then(() => this.startRuntimeControlPolling())
|
|
498
|
-
.catch(() => { });
|
|
501
|
+
void this.baselineAndStartRuntimeControlPolling().catch(() => { });
|
|
499
502
|
}
|
|
500
|
-
|
|
503
|
+
this.runtimeHeartbeat?.refresh();
|
|
501
504
|
return;
|
|
502
505
|
}
|
|
503
506
|
if (event === 'stopAndDrop') {
|
|
504
507
|
this.stopAndDropHandler = handler;
|
|
505
508
|
if (this.running) {
|
|
506
|
-
void this.
|
|
507
|
-
.then(() => this.startRuntimeControlPolling())
|
|
508
|
-
.catch(() => { });
|
|
509
|
+
void this.baselineAndStartRuntimeControlPolling().catch(() => { });
|
|
509
510
|
}
|
|
510
|
-
|
|
511
|
+
this.runtimeHeartbeat?.refresh();
|
|
511
512
|
return;
|
|
512
513
|
}
|
|
513
514
|
if (event === 'newSession') {
|
|
514
515
|
this.newSessionHandler = handler;
|
|
515
516
|
if (this.running) {
|
|
516
|
-
void this.
|
|
517
|
-
.then(() => this.startRuntimeControlPolling())
|
|
518
|
-
.catch(() => { });
|
|
517
|
+
void this.baselineAndStartRuntimeControlPolling().catch(() => { });
|
|
519
518
|
}
|
|
520
|
-
|
|
519
|
+
this.runtimeHeartbeat?.refresh();
|
|
521
520
|
return;
|
|
522
521
|
}
|
|
523
522
|
this.contactRemovedHandler = handler;
|
|
@@ -532,7 +531,7 @@ export class CanonAgent {
|
|
|
532
531
|
if (this.running) {
|
|
533
532
|
this.startRuntimeControlPolling();
|
|
534
533
|
}
|
|
535
|
-
|
|
534
|
+
this.runtimeHeartbeat?.refresh();
|
|
536
535
|
}
|
|
537
536
|
describeCommands() {
|
|
538
537
|
return this.buildRuntimeDescriptor().commands ?? [];
|
|
@@ -568,13 +567,37 @@ export class CanonAgent {
|
|
|
568
567
|
async communicate(input) {
|
|
569
568
|
return this.apiClient.communicate(input);
|
|
570
569
|
}
|
|
571
|
-
|
|
570
|
+
start() {
|
|
571
|
+
if (this.stopPromise)
|
|
572
|
+
return this.stopPromise.then(() => this.start());
|
|
573
|
+
if (this.startPromise)
|
|
574
|
+
return this.startPromise;
|
|
572
575
|
if (this.running)
|
|
573
|
-
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) {
|
|
574
594
|
await verifyCanonRuntimeConnection(this.runtimeConnection);
|
|
575
|
-
if (this.
|
|
595
|
+
if (generation !== this.lifecycleGeneration)
|
|
576
596
|
return;
|
|
577
597
|
this.running = true;
|
|
598
|
+
if (this.options.sessions?.enabled && !this.sessionManager) {
|
|
599
|
+
this.sessionManager = new SessionManager(this.options.sessions);
|
|
600
|
+
}
|
|
578
601
|
// The single scoped RTDB client for this agent. Every RTDB consumer in
|
|
579
602
|
// the SDK (control poller, runtime-state publishers) threads this handle;
|
|
580
603
|
// the SDK never reads through core's deprecated module-global default,
|
|
@@ -583,9 +606,19 @@ export class CanonAgent {
|
|
|
583
606
|
rtdbUrl: this.runtimeConnection.rtdbUrl,
|
|
584
607
|
firebaseApiKey: this.runtimeConnection.firebaseWebApiKey,
|
|
585
608
|
});
|
|
586
|
-
//
|
|
587
|
-
|
|
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;
|
|
588
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
|
+
});
|
|
589
622
|
console.log(`[canon-sdk] Authenticated as ${agentId}`);
|
|
590
623
|
// 2. Wire debouncer to handler
|
|
591
624
|
this.debouncer.setCallback(async (conversationId, messages, provenanceByMessageId) => {
|
|
@@ -602,6 +635,8 @@ export class CanonAgent {
|
|
|
602
635
|
catch {
|
|
603
636
|
// Non-fatal — delivery mode will fall back to default
|
|
604
637
|
}
|
|
638
|
+
if (generation !== this.lifecycleGeneration)
|
|
639
|
+
return;
|
|
605
640
|
// 3a. Determine delivery mode
|
|
606
641
|
let mode = this.options.deliveryMode;
|
|
607
642
|
if (mode === 'auto') {
|
|
@@ -619,23 +654,29 @@ export class CanonAgent {
|
|
|
619
654
|
catch {
|
|
620
655
|
console.warn('[canon-sdk] Failed to fetch agent context — owner/access info unavailable');
|
|
621
656
|
}
|
|
657
|
+
if (generation !== this.lifecycleGeneration)
|
|
658
|
+
return;
|
|
622
659
|
// 3c. Initialize RTDB session state reporting (opt-in)
|
|
623
660
|
if (this.options.sessionState) {
|
|
624
661
|
const runtimeState = this.createRuntimeStatePublisher();
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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;
|
|
631
668
|
if (this.cachedConversationIds.length > 0) {
|
|
632
669
|
console.log(`[canon-sdk] Session state reported for ${this.cachedConversationIds.length} conversations`);
|
|
633
670
|
}
|
|
634
671
|
}
|
|
635
672
|
await this.baselineRuntimeControlSignals(this.cachedConversationIds);
|
|
673
|
+
if (generation !== this.lifecycleGeneration)
|
|
674
|
+
return;
|
|
636
675
|
this.startRuntimeControlPolling();
|
|
637
676
|
// 4. Start delivery
|
|
638
677
|
const { RealtimeManager } = await import('./realtime.js');
|
|
678
|
+
if (generation !== this.lifecycleGeneration)
|
|
679
|
+
return;
|
|
639
680
|
this.voiceEventsEnabled = Boolean(this.callStartedHandler || this.callEndedHandler);
|
|
640
681
|
const rtm = new RealtimeManager(this.options.apiKey, this.debouncer, agentId, this.options.streamUrl, { enableVoiceEvents: this.voiceEventsEnabled });
|
|
641
682
|
if (this.voiceEventsEnabled) {
|
|
@@ -680,16 +721,24 @@ export class CanonAgent {
|
|
|
680
721
|
});
|
|
681
722
|
rtm.setConnectionHandlers({
|
|
682
723
|
onConnected: () => {
|
|
683
|
-
this.
|
|
724
|
+
if (!this.running || generation !== this.lifecycleGeneration)
|
|
725
|
+
return;
|
|
726
|
+
this.runtimeHeartbeat?.connect();
|
|
684
727
|
if (!this.sseConnectedLogged) {
|
|
685
728
|
this.sseConnectedLogged = true;
|
|
686
729
|
console.log('[canon-sdk] SSE stream connected');
|
|
687
730
|
}
|
|
688
731
|
},
|
|
689
|
-
onDisconnected: () =>
|
|
732
|
+
onDisconnected: () => {
|
|
733
|
+
if (generation === this.lifecycleGeneration) {
|
|
734
|
+
void this.runtimeHeartbeat?.disconnect();
|
|
735
|
+
}
|
|
736
|
+
},
|
|
690
737
|
});
|
|
691
738
|
this.realtimeManager = rtm;
|
|
692
739
|
await rtm.start();
|
|
740
|
+
if (generation !== this.lifecycleGeneration)
|
|
741
|
+
rtm.stop();
|
|
693
742
|
}
|
|
694
743
|
async createGroup(options) {
|
|
695
744
|
return this.apiClient.createGroup(options);
|
|
@@ -780,29 +829,58 @@ export class CanonAgent {
|
|
|
780
829
|
console.error('[canon-sdk] Message-updated handler failed:', error instanceof Error ? error.message : error);
|
|
781
830
|
}
|
|
782
831
|
}
|
|
783
|
-
|
|
784
|
-
if (
|
|
785
|
-
return;
|
|
832
|
+
stop() {
|
|
833
|
+
if (this.stopPromise)
|
|
834
|
+
return this.stopPromise;
|
|
835
|
+
if (!this.running && !this.startPromise)
|
|
836
|
+
return Promise.resolve();
|
|
837
|
+
++this.lifecycleGeneration;
|
|
786
838
|
this.running = false;
|
|
787
839
|
this.stopRuntimeControlPolling();
|
|
788
|
-
// Clear session state if enabled (uses cached IDs — no network call during shutdown)
|
|
789
|
-
const runtimeState = this.createRuntimeStatePublisher();
|
|
790
|
-
if (this.options.sessionState && runtimeState) {
|
|
791
|
-
for (const id of this.cachedConversationIds) {
|
|
792
|
-
Promise.resolve(runtimeState.clearSessionState(id)).catch(() => { });
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
if (runtimeState) {
|
|
796
|
-
for (const id of this.cachedConversationIds) {
|
|
797
|
-
Promise.resolve(runtimeState.clearTurnState(id)).catch(() => { });
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
await this.clearAgentRuntime();
|
|
801
840
|
this.realtimeManager?.stop();
|
|
802
|
-
this.
|
|
803
|
-
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;
|
|
804
863
|
this.debouncer.destroy();
|
|
805
|
-
|
|
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;
|
|
806
884
|
}
|
|
807
885
|
hasInterruptSupport() {
|
|
808
886
|
return Boolean(this.interruptHandler);
|
|
@@ -879,33 +957,6 @@ export class CanonAgent {
|
|
|
879
957
|
supportsQueue: Boolean(this.sessionManager),
|
|
880
958
|
};
|
|
881
959
|
}
|
|
882
|
-
async publishAgentRuntime() {
|
|
883
|
-
const publisher = this.createRuntimeStatePublisher();
|
|
884
|
-
if (!publisher)
|
|
885
|
-
return;
|
|
886
|
-
await publisher.publishAgentRuntime({
|
|
887
|
-
runtimeDescriptor: this.buildRuntimeDescriptor(),
|
|
888
|
-
});
|
|
889
|
-
}
|
|
890
|
-
startRuntimeHeartbeat() {
|
|
891
|
-
void this.publishAgentRuntime();
|
|
892
|
-
if (this.runtimeHeartbeatTimer)
|
|
893
|
-
return;
|
|
894
|
-
this.runtimeHeartbeatTimer = setInterval(() => {
|
|
895
|
-
void this.publishAgentRuntime();
|
|
896
|
-
}, AGENT_RUNTIME_HEARTBEAT_MS);
|
|
897
|
-
this.runtimeHeartbeatTimer.unref?.();
|
|
898
|
-
}
|
|
899
|
-
stopRuntimeHeartbeat() {
|
|
900
|
-
if (this.runtimeHeartbeatTimer) {
|
|
901
|
-
clearInterval(this.runtimeHeartbeatTimer);
|
|
902
|
-
this.runtimeHeartbeatTimer = null;
|
|
903
|
-
}
|
|
904
|
-
void this.clearAgentRuntime();
|
|
905
|
-
}
|
|
906
|
-
async clearAgentRuntime() {
|
|
907
|
-
await Promise.resolve(this.createRuntimeStatePublisher()?.clearAgentRuntime()).catch(() => { });
|
|
908
|
-
}
|
|
909
960
|
rememberConversationId(conversationId) {
|
|
910
961
|
if (this.cachedConversationIds.includes(conversationId))
|
|
911
962
|
return;
|
|
@@ -995,6 +1046,13 @@ export class CanonAgent {
|
|
|
995
1046
|
return;
|
|
996
1047
|
await this.ensureControlPoller()?.baseline(conversationIds);
|
|
997
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
|
+
}
|
|
998
1056
|
startRuntimeControlPolling() {
|
|
999
1057
|
if (!this.hasRuntimeControlSupport())
|
|
1000
1058
|
return;
|
|
@@ -1155,12 +1213,13 @@ export class CanonAgent {
|
|
|
1155
1213
|
// when the agent has not started — same condition as the guard above.
|
|
1156
1214
|
if (!this.rtdbHandle)
|
|
1157
1215
|
return null;
|
|
1158
|
-
|
|
1216
|
+
this.runtimeStatePublisher ??= createRuntimeStatePublisher({
|
|
1159
1217
|
agentId: this.agentId,
|
|
1160
1218
|
clientType: this.options.clientType ?? 'generic',
|
|
1161
1219
|
hostMode: this.options.runtimeControlSurface === 'host',
|
|
1162
1220
|
rtdb: this.rtdbHandle,
|
|
1163
1221
|
});
|
|
1222
|
+
return this.runtimeStatePublisher;
|
|
1164
1223
|
}
|
|
1165
1224
|
requireRuntimeStatePublisher() {
|
|
1166
1225
|
const publisher = this.createRuntimeStatePublisher();
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { CanonAgent } from './canon-agent.js';
|
|
2
|
-
export
|
|
2
|
+
export { clearPendingRegistration, ensureAgentProfile, resumeRegistration } from '@canonmsg/core';
|
|
3
|
+
export type { EnsureAgentProfileOptions, RegistrationCredentials, RegistrationProgress, RegistrationSession, RegistrationStatus, RegistrationStore, ResumeRegistrationOptions } from '@canonmsg/core';
|
|
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
|
-
export type { ApprovalConfig, ApprovalNativeRequestMetadata, ApprovalOutcomeMetadata, ApprovalReplyMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalResult, ApprovalRisk, CanonContact, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeTurnModeActivation, CanonRuntimeTurnModeDescriptor, CanonRuntimeTurnModeScope, CanonRuntimeFact, CanonRuntimeFactGroup, CanonResolveAdmissionResult, ContactAddedPayload, ContactCardPayload, ContactRemovedPayload, ContactSource, HostAdmissionActionCapabilities, ParticipationSuppressedPayload, ResolveAdmissionTargetInput, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, ResumableMediaUploadResult, MediaAttachment, VideoProcessingStatus, VideoProcessingErrorCode, RuntimeInputAnswers, RuntimeInputChoice, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, RuntimePlanRequestPayload, RuntimePlanRequestResult, SessionRule, } from '@canonmsg/core';
|
|
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';
|
|
5
7
|
export { SessionManager } from './session-manager.js';
|
|
6
8
|
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';
|
|
7
9
|
export type { AnthropicImageBlock, AnthropicImageBudgetOptions, AnthropicImageMimeType, MaterializeMediaOptions, MaterializedCanonAttachment, MaterializedCanonReplyContext, ReplyWithFileOptions, UploadMediaFileOptions, } from './media.js';
|
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
|
-
}
|