@canonmsg/agent-sdk 3.1.0 → 3.2.2
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/dist/canon-agent.d.ts +29 -12
- package/dist/canon-agent.js +117 -130
- package/dist/index.d.ts +2 -2
- package/dist/realtime.d.ts +26 -0
- package/dist/realtime.js +128 -1
- package/dist/types.d.ts +10 -2
- package/package.json +2 -2
package/dist/canon-agent.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AddMemberResult, type CanonContact, type CanonRuntimeActivityItem, type CanonRuntimeCommandDescriptor, type CanonRuntimeFact, type CanonRuntimePrimitiveId, type ContactCardPayload, type ClearRuntimeActivityOptions, type CreateContactRequestResult } from '@canonmsg/core';
|
|
1
|
+
import { type AddMemberResult, type CanonContact, type CanonConversation, type CreateConversationResult, type CanonRuntimeActivityItem, type CanonRuntimeCommandDescriptor, type CanonRuntimeFact, type CanonRuntimePrimitiveId, type ContactCardPayload, type ClearRuntimeActivityOptions, type CreateContactRequestResult } from '@canonmsg/core';
|
|
2
2
|
import type { CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, CreateConversationOptions, MessageHandler, MessageUpdatedHandler, ReachOutOptions, ReachOutResult, ContactRequestHandler, RuntimeSignalHandler, RuntimePrimitiveHandler } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Contact-graph operations exposed under `agent.contacts`. Wraps the REST
|
|
@@ -18,6 +18,11 @@ export interface AgentUsersAPI {
|
|
|
18
18
|
block(userId: string): Promise<void>;
|
|
19
19
|
unblock(userId: string): Promise<void>;
|
|
20
20
|
}
|
|
21
|
+
export interface AgentConversationsAPI {
|
|
22
|
+
list(options?: {
|
|
23
|
+
targetUserId?: string;
|
|
24
|
+
}): Promise<CanonConversation[]>;
|
|
25
|
+
}
|
|
21
26
|
export declare class CanonAgent {
|
|
22
27
|
private options;
|
|
23
28
|
private apiClient;
|
|
@@ -40,6 +45,8 @@ export declare class CanonAgent {
|
|
|
40
45
|
readonly contacts: AgentContactsAPI;
|
|
41
46
|
/** Block/unblock operations (`agent.users.*`). Initialized in the constructor. */
|
|
42
47
|
readonly users: AgentUsersAPI;
|
|
48
|
+
/** Conversation discovery for choosing existing sessions intentionally. */
|
|
49
|
+
readonly conversations: AgentConversationsAPI;
|
|
43
50
|
private readonly reachOutInFlight;
|
|
44
51
|
private agentId;
|
|
45
52
|
private agentContext;
|
|
@@ -49,9 +56,8 @@ export declare class CanonAgent {
|
|
|
49
56
|
private cachedConversationIds;
|
|
50
57
|
private running;
|
|
51
58
|
private runtimeHeartbeatTimer;
|
|
52
|
-
private
|
|
53
|
-
private
|
|
54
|
-
private readonly primitiveRequestDedupe;
|
|
59
|
+
private rtdbHandle;
|
|
60
|
+
private controlPoller;
|
|
55
61
|
private readonly activeAbortControllers;
|
|
56
62
|
private readonly activeTurns;
|
|
57
63
|
private readonly conversationMemberIds;
|
|
@@ -85,9 +91,7 @@ export declare class CanonAgent {
|
|
|
85
91
|
reachOut(card: ContactCardPayload, options?: ReachOutOptions): Promise<ReachOutResult>;
|
|
86
92
|
private executeReachOut;
|
|
87
93
|
start(): Promise<void>;
|
|
88
|
-
createConversation(options: CreateConversationOptions): Promise<
|
|
89
|
-
conversationId: string;
|
|
90
|
-
}>;
|
|
94
|
+
createConversation(options: CreateConversationOptions): Promise<CreateConversationResult>;
|
|
91
95
|
updateTopic(conversationId: string, topic: string): Promise<void>;
|
|
92
96
|
leaveConversation(conversationId: string): Promise<void>;
|
|
93
97
|
updateConversationName(conversationId: string, name: string): Promise<void>;
|
|
@@ -132,20 +136,33 @@ export declare class CanonAgent {
|
|
|
132
136
|
private rememberConversationMembers;
|
|
133
137
|
private handleConversationUpdated;
|
|
134
138
|
private buildGroupContext;
|
|
139
|
+
/**
|
|
140
|
+
* Shared `/control` channel poller, configured to the agent-sdk host
|
|
141
|
+
* profile pinned by core's characterization tests: flat 2s single-flight
|
|
142
|
+
* cadence, parallel conversations, signal + primitive keys (no session),
|
|
143
|
+
* eager signal baseline, and TTL'd primitive dedupe released on successful
|
|
144
|
+
* consume. The poller talks only to the scoped RTDB handle captured in
|
|
145
|
+
* start() — never the module-global default client.
|
|
146
|
+
*/
|
|
147
|
+
private ensureControlPoller;
|
|
135
148
|
private baselineRuntimeControlSignals;
|
|
136
149
|
private startRuntimeControlPolling;
|
|
137
150
|
private stopRuntimeControlPolling;
|
|
138
|
-
private
|
|
139
|
-
private
|
|
140
|
-
private clearRuntimePrimitiveRequest;
|
|
141
|
-
private prunePrimitiveRequestDedupe;
|
|
142
|
-
private handleRuntimeSignal;
|
|
151
|
+
private handleRuntimePrimitiveEvent;
|
|
152
|
+
private handleRuntimeSignalEvent;
|
|
143
153
|
private firstActiveTurn;
|
|
144
154
|
private publishAcceptedRuntimeSignal;
|
|
145
155
|
private abortActiveTurns;
|
|
146
156
|
private resolveBatchDeliveryIntent;
|
|
147
157
|
private markQueuedMessagesAccepted;
|
|
148
158
|
private notifyMessageInterrupt;
|
|
159
|
+
/**
|
|
160
|
+
* Builds a runtime-state publisher bound to this agent's scoped RTDB
|
|
161
|
+
* handle (captured in start()). Threading the handle keeps every
|
|
162
|
+
* publish on this agent's own credentials — without it the publisher
|
|
163
|
+
* would fall back to core's deprecated module-global RTDB client,
|
|
164
|
+
* where the last-started agent's token wins in multi-agent processes.
|
|
165
|
+
*/
|
|
149
166
|
private createRuntimeStatePublisher;
|
|
150
167
|
private requireRuntimeStatePublisher;
|
|
151
168
|
private handleMessages;
|
package/dist/canon-agent.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApprovalManager, CanonClient, 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,
|
|
1
|
+
import { ApprovalManager, 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, normalizeTurnMetadata, reachOutToCanonContact, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, selectActiveSelfContexts, renderCanonHostInboundContent, } from '@canonmsg/core';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { AuthManager } from './auth.js';
|
|
4
4
|
import { Debouncer } from './debouncer.js';
|
|
@@ -6,6 +6,7 @@ import { buildRuntimeCardCreateArgs } from './runtime-card.js';
|
|
|
6
6
|
import { materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, uploadMediaFile, } from './media.js';
|
|
7
7
|
import { SessionManager } from './session-manager.js';
|
|
8
8
|
const AGENT_RUNTIME_HEARTBEAT_MS = 30_000;
|
|
9
|
+
const RUNTIME_CONTROL_POLL_INTERVAL_MS = 2_000;
|
|
9
10
|
const RUNTIME_PRIMITIVE_DEDUPE_TTL_MS = 5 * 60 * 1000;
|
|
10
11
|
const RUNTIME_PRIMITIVE_DEDUPE_MAX = 1_000;
|
|
11
12
|
const DEFAULT_RUNTIME_INPUT_TIMEOUT_MS = 5 * 60_000;
|
|
@@ -245,6 +246,8 @@ export class CanonAgent {
|
|
|
245
246
|
contacts;
|
|
246
247
|
/** Block/unblock operations (`agent.users.*`). Initialized in the constructor. */
|
|
247
248
|
users;
|
|
249
|
+
/** Conversation discovery for choosing existing sessions intentionally. */
|
|
250
|
+
conversations;
|
|
248
251
|
reachOutInFlight = new Map();
|
|
249
252
|
agentId = null;
|
|
250
253
|
agentContext = null;
|
|
@@ -254,9 +257,8 @@ export class CanonAgent {
|
|
|
254
257
|
cachedConversationIds = [];
|
|
255
258
|
running = false;
|
|
256
259
|
runtimeHeartbeatTimer = null;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
primitiveRequestDedupe = new Map();
|
|
260
|
+
rtdbHandle = null;
|
|
261
|
+
controlPoller = null;
|
|
260
262
|
activeAbortControllers = new Map();
|
|
261
263
|
activeTurns = new Map();
|
|
262
264
|
conversationMemberIds = new Map();
|
|
@@ -292,6 +294,14 @@ export class CanonAgent {
|
|
|
292
294
|
block: (userId) => apiClient.blockUser(userId),
|
|
293
295
|
unblock: (userId) => apiClient.unblockUser(userId),
|
|
294
296
|
};
|
|
297
|
+
this.conversations = {
|
|
298
|
+
list: async (options = {}) => {
|
|
299
|
+
const conversations = await apiClient.getConversations();
|
|
300
|
+
if (!options.targetUserId)
|
|
301
|
+
return conversations;
|
|
302
|
+
return conversations.filter((conversation) => conversation.memberIds.includes(options.targetUserId));
|
|
303
|
+
},
|
|
304
|
+
};
|
|
295
305
|
if (options.sessions?.enabled) {
|
|
296
306
|
this.sessionManager = new SessionManager({
|
|
297
307
|
contextLimit: options.sessions.contextLimit,
|
|
@@ -457,7 +467,7 @@ export class CanonAgent {
|
|
|
457
467
|
const contextualKey = options?.selfContext
|
|
458
468
|
? `${options.sourceConversationId ?? ''}\u0000${options.selfContext.type}\u0000${options.selfContext.context}`
|
|
459
469
|
: '';
|
|
460
|
-
const inFlightKey = `${targetUserId}\u0000${options?.text ?? ''}\u0000${options?.requestMessage ?? ''}\u0000${JSON.stringify(options?.sessionConfig ?? null)}\u0000${contextualKey}`;
|
|
470
|
+
const inFlightKey = `${targetUserId}\u0000${options?.text ?? ''}\u0000${options?.requestMessage ?? ''}\u0000${JSON.stringify(options?.sessionConfig ?? null)}\u0000${JSON.stringify(options?.sessionSelection ?? null)}\u0000${contextualKey}`;
|
|
461
471
|
const inFlight = this.reachOutInFlight.get(inFlightKey);
|
|
462
472
|
if (inFlight)
|
|
463
473
|
return inFlight;
|
|
@@ -482,6 +492,7 @@ export class CanonAgent {
|
|
|
482
492
|
selfContext: options.selfContext,
|
|
483
493
|
requestMessage: options.requestMessage ?? null,
|
|
484
494
|
sessionConfig: options.sessionConfig ?? null,
|
|
495
|
+
sessionSelection: options.sessionSelection,
|
|
485
496
|
});
|
|
486
497
|
return result.status === 'messaged'
|
|
487
498
|
? {
|
|
@@ -489,6 +500,9 @@ export class CanonAgent {
|
|
|
489
500
|
conversationId: result.conversationId,
|
|
490
501
|
messageId: result.messageId,
|
|
491
502
|
selfContextId: result.selfContextId,
|
|
503
|
+
created: result.created,
|
|
504
|
+
reused: result.reused,
|
|
505
|
+
sessionSelection: result.sessionSelection,
|
|
492
506
|
}
|
|
493
507
|
: result;
|
|
494
508
|
}
|
|
@@ -497,13 +511,18 @@ export class CanonAgent {
|
|
|
497
511
|
text: options?.text ?? null,
|
|
498
512
|
requestMessage: options?.requestMessage ?? null,
|
|
499
513
|
sessionConfig: options?.sessionConfig ?? null,
|
|
514
|
+
sessionSelection: options?.sessionSelection,
|
|
500
515
|
});
|
|
501
516
|
}
|
|
502
517
|
async start() {
|
|
503
518
|
if (this.running)
|
|
504
519
|
return;
|
|
505
520
|
this.running = true;
|
|
506
|
-
|
|
521
|
+
// The single scoped RTDB client for this agent. Every RTDB consumer in
|
|
522
|
+
// the SDK (control poller, runtime-state publishers) threads this handle;
|
|
523
|
+
// the SDK never reads through core's deprecated module-global default,
|
|
524
|
+
// so multiple CanonAgents in one process cannot clobber each other.
|
|
525
|
+
this.rtdbHandle = initRTDBAuth(this.apiClient);
|
|
507
526
|
// 1. Authenticate
|
|
508
527
|
const { agentId } = await this.authManager.authenticate();
|
|
509
528
|
this.agentId = agentId;
|
|
@@ -832,143 +851,104 @@ export class CanonAgent {
|
|
|
832
851
|
membershipChange: input.membershipChange,
|
|
833
852
|
});
|
|
834
853
|
}
|
|
854
|
+
/**
|
|
855
|
+
* Shared `/control` channel poller, configured to the agent-sdk host
|
|
856
|
+
* profile pinned by core's characterization tests: flat 2s single-flight
|
|
857
|
+
* cadence, parallel conversations, signal + primitive keys (no session),
|
|
858
|
+
* eager signal baseline, and TTL'd primitive dedupe released on successful
|
|
859
|
+
* consume. The poller talks only to the scoped RTDB handle captured in
|
|
860
|
+
* start() — never the module-global default client.
|
|
861
|
+
*/
|
|
862
|
+
ensureControlPoller() {
|
|
863
|
+
if (this.controlPoller)
|
|
864
|
+
return this.controlPoller;
|
|
865
|
+
if (!this.rtdbHandle)
|
|
866
|
+
return null;
|
|
867
|
+
this.controlPoller = new ControlChannelPoller({
|
|
868
|
+
rtdb: this.rtdbHandle,
|
|
869
|
+
agentId: () => this.agentId,
|
|
870
|
+
conversationIds: () => this.cachedConversationIds,
|
|
871
|
+
cadence: { kind: 'fixed', intervalMs: RUNTIME_CONTROL_POLL_INTERVAL_MS },
|
|
872
|
+
pollOnStart: false,
|
|
873
|
+
conversationConcurrency: 'parallel',
|
|
874
|
+
handlers: {
|
|
875
|
+
signal: {
|
|
876
|
+
handle: (event) => this.handleRuntimeSignalEvent(event),
|
|
877
|
+
consumeOnError: true,
|
|
878
|
+
},
|
|
879
|
+
primitive: {
|
|
880
|
+
handle: (event) => this.handleRuntimePrimitiveEvent(event),
|
|
881
|
+
consumeOnError: true,
|
|
882
|
+
ordering: 'sequential',
|
|
883
|
+
dedupeTtlMs: RUNTIME_PRIMITIVE_DEDUPE_TTL_MS,
|
|
884
|
+
dedupeMaxEntries: RUNTIME_PRIMITIVE_DEDUPE_MAX,
|
|
885
|
+
releaseDedupeOnConsume: true,
|
|
886
|
+
},
|
|
887
|
+
},
|
|
888
|
+
onError: (error) => {
|
|
889
|
+
// Read/consume failures stay silent (the legacy loop swallowed them);
|
|
890
|
+
// handler-scope errors are host dispatch bugs worth surfacing.
|
|
891
|
+
if (error.scope === 'handler') {
|
|
892
|
+
console.error(`[canon-sdk] Runtime control ${error.key ?? 'poll'} dispatch failed for ${error.conversationId}:`, error.error);
|
|
893
|
+
}
|
|
894
|
+
},
|
|
895
|
+
});
|
|
896
|
+
return this.controlPoller;
|
|
897
|
+
}
|
|
835
898
|
async baselineRuntimeControlSignals(conversationIds) {
|
|
836
|
-
if (!this.
|
|
899
|
+
if (!this.hasRuntimeSignalSupport())
|
|
837
900
|
return;
|
|
838
|
-
await
|
|
839
|
-
const raw = await Promise.resolve(rtdbRead(`/control/${conversationId}/${this.agentId}/signal`)).catch(() => null);
|
|
840
|
-
if (!raw || typeof raw !== 'object')
|
|
841
|
-
return;
|
|
842
|
-
const timestamp = Number(raw.updatedAt ?? 0);
|
|
843
|
-
if (timestamp > 0) {
|
|
844
|
-
this.lastSeenSignal.set(conversationId, timestamp);
|
|
845
|
-
}
|
|
846
|
-
}));
|
|
901
|
+
await this.ensureControlPoller()?.baseline(conversationIds);
|
|
847
902
|
}
|
|
848
903
|
startRuntimeControlPolling() {
|
|
849
|
-
if (!this.
|
|
904
|
+
if (!this.hasRuntimeControlSupport())
|
|
850
905
|
return;
|
|
851
|
-
this.
|
|
852
|
-
void this.pollRuntimeControls();
|
|
853
|
-
}, 2_000);
|
|
854
|
-
this.runtimeControlPollTimer.unref?.();
|
|
906
|
+
this.ensureControlPoller()?.start();
|
|
855
907
|
}
|
|
856
908
|
stopRuntimeControlPolling() {
|
|
857
|
-
|
|
858
|
-
return;
|
|
859
|
-
clearInterval(this.runtimeControlPollTimer);
|
|
860
|
-
this.runtimeControlPollTimer = null;
|
|
909
|
+
this.controlPoller?.stop();
|
|
861
910
|
}
|
|
862
|
-
async
|
|
863
|
-
|
|
911
|
+
async handleRuntimePrimitiveEvent(event) {
|
|
912
|
+
// Requests only belong to this runtime once primitive handlers exist —
|
|
913
|
+
// leave them untouched otherwise (the legacy loop never read the key).
|
|
914
|
+
if (!this.hasRuntimePrimitiveSupport())
|
|
915
|
+
return { consume: false };
|
|
916
|
+
const { conversationId, requestId, value } = event;
|
|
917
|
+
const primitive = value.id;
|
|
918
|
+
// Unknown primitives and unhandled ids fall through so the poller
|
|
919
|
+
// consumes the request without dispatching, matching the legacy loop.
|
|
920
|
+
if (!isRuntimePrimitiveId(primitive))
|
|
864
921
|
return;
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
const raw = await Promise.resolve(rtdbRead(`/control/${conversationId}/${this.agentId}/signal`)).catch(() => null);
|
|
868
|
-
if (raw && typeof raw === 'object') {
|
|
869
|
-
await this.handleRuntimeSignal(conversationId, raw);
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
if (this.hasRuntimePrimitiveSupport()) {
|
|
873
|
-
const raw = await Promise.resolve(rtdbRead(`/control/${conversationId}/${this.agentId}/primitive`)).catch(() => null);
|
|
874
|
-
if (raw && typeof raw === 'object') {
|
|
875
|
-
await this.handleRuntimePrimitiveRequests(conversationId, raw);
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
}));
|
|
879
|
-
}
|
|
880
|
-
async handleRuntimePrimitiveRequests(conversationId, raw) {
|
|
881
|
-
if (!this.agentId)
|
|
922
|
+
const handler = this.primitiveHandlers.get(primitive) ?? this.primitiveFallbackHandler;
|
|
923
|
+
if (!handler)
|
|
882
924
|
return;
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
continue;
|
|
895
|
-
this.primitiveRequestDedupe.set(requestKey, Date.now());
|
|
896
|
-
let cleared = false;
|
|
897
|
-
try {
|
|
898
|
-
const primitive = value.id;
|
|
899
|
-
if (!isRuntimePrimitiveId(primitive)) {
|
|
900
|
-
cleared = await this.clearRuntimePrimitiveRequest(conversationId, requestId);
|
|
901
|
-
continue;
|
|
902
|
-
}
|
|
903
|
-
const handler = this.primitiveHandlers.get(primitive) ?? this.primitiveFallbackHandler;
|
|
904
|
-
if (!handler) {
|
|
905
|
-
cleared = await this.clearRuntimePrimitiveRequest(conversationId, requestId);
|
|
906
|
-
continue;
|
|
907
|
-
}
|
|
908
|
-
const args = normalizePrimitiveArgs(value.args);
|
|
909
|
-
await Promise.resolve(handler({
|
|
910
|
-
conversationId,
|
|
911
|
-
primitive,
|
|
912
|
-
args,
|
|
913
|
-
requestId,
|
|
914
|
-
updatedAt: typeof value.updatedAt === 'number' ? value.updatedAt : undefined,
|
|
915
|
-
rawText: typeof value.rawText === 'string' ? value.rawText : undefined,
|
|
916
|
-
alias: typeof value.alias === 'string' ? value.alias : undefined,
|
|
917
|
-
})).catch((error) => {
|
|
918
|
-
console.error(`[canon-sdk] Runtime primitive ${primitive} handler failed for ${conversationId}:`, error);
|
|
919
|
-
});
|
|
920
|
-
cleared = await this.clearRuntimePrimitiveRequest(conversationId, requestId);
|
|
921
|
-
}
|
|
922
|
-
finally {
|
|
923
|
-
if (cleared) {
|
|
924
|
-
this.primitiveRequestDedupe.delete(requestKey);
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
async clearRuntimePrimitiveRequest(conversationId, requestId) {
|
|
930
|
-
if (!this.agentId)
|
|
931
|
-
return false;
|
|
932
|
-
try {
|
|
933
|
-
await Promise.resolve(rtdbWrite(`/control/${conversationId}/${this.agentId}/primitive/${requestId}`, null));
|
|
934
|
-
return true;
|
|
935
|
-
}
|
|
936
|
-
catch {
|
|
937
|
-
return false;
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
prunePrimitiveRequestDedupe(now = Date.now()) {
|
|
941
|
-
for (const [key, timestamp] of this.primitiveRequestDedupe) {
|
|
942
|
-
if (now - timestamp >= RUNTIME_PRIMITIVE_DEDUPE_TTL_MS) {
|
|
943
|
-
this.primitiveRequestDedupe.delete(key);
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
while (this.primitiveRequestDedupe.size > RUNTIME_PRIMITIVE_DEDUPE_MAX) {
|
|
947
|
-
const oldestKey = this.primitiveRequestDedupe.keys().next().value;
|
|
948
|
-
if (!oldestKey)
|
|
949
|
-
break;
|
|
950
|
-
this.primitiveRequestDedupe.delete(oldestKey);
|
|
951
|
-
}
|
|
925
|
+
await Promise.resolve(handler({
|
|
926
|
+
conversationId,
|
|
927
|
+
primitive,
|
|
928
|
+
args: normalizePrimitiveArgs(value.args),
|
|
929
|
+
requestId,
|
|
930
|
+
updatedAt: typeof value.updatedAt === 'number' ? value.updatedAt : undefined,
|
|
931
|
+
rawText: typeof value.rawText === 'string' ? value.rawText : undefined,
|
|
932
|
+
alias: typeof value.alias === 'string' ? value.alias : undefined,
|
|
933
|
+
})).catch((error) => {
|
|
934
|
+
console.error(`[canon-sdk] Runtime primitive ${primitive} handler failed for ${conversationId}:`, error);
|
|
935
|
+
});
|
|
952
936
|
}
|
|
953
|
-
async
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
const timestamp = Number(raw.updatedAt ?? 0);
|
|
960
|
-
if (timestamp <= (this.lastSeenSignal.get(conversationId) ?? 0))
|
|
961
|
-
return;
|
|
962
|
-
this.lastSeenSignal.set(conversationId, timestamp);
|
|
937
|
+
async handleRuntimeSignalEvent(event) {
|
|
938
|
+
// Signals only belong to this runtime once signal handlers exist — leave
|
|
939
|
+
// them untouched otherwise (the legacy loop never read the key).
|
|
940
|
+
if (!this.hasRuntimeSignalSupport())
|
|
941
|
+
return { consume: false };
|
|
942
|
+
const { conversationId, type: signal, updatedAt } = event;
|
|
963
943
|
const handler = signal === 'new_session'
|
|
964
944
|
? this.newSessionHandler
|
|
965
945
|
: signal === 'stop_and_drop'
|
|
966
946
|
? this.stopAndDropHandler
|
|
967
947
|
: this.interruptHandler;
|
|
968
|
-
|
|
969
|
-
|
|
948
|
+
// No handler for this specific signal: fall through so the poller
|
|
949
|
+
// consumes the node without dispatching, matching the legacy loop.
|
|
950
|
+
if (!handler)
|
|
970
951
|
return;
|
|
971
|
-
}
|
|
972
952
|
const activeTurn = this.firstActiveTurn(conversationId);
|
|
973
953
|
const abortSignal = this.abortActiveTurns(conversationId);
|
|
974
954
|
const droppedMessages = signal === 'new_session'
|
|
@@ -986,16 +966,15 @@ export class CanonAgent {
|
|
|
986
966
|
hasActiveTurn: Boolean(abortSignal),
|
|
987
967
|
droppedCount: droppedMessages.length,
|
|
988
968
|
});
|
|
989
|
-
await Promise.resolve(handler
|
|
969
|
+
await Promise.resolve(handler({
|
|
990
970
|
conversationId,
|
|
991
|
-
signal
|
|
992
|
-
updatedAt:
|
|
971
|
+
signal,
|
|
972
|
+
updatedAt: updatedAt || undefined,
|
|
993
973
|
abortSignal,
|
|
994
974
|
droppedMessageIds,
|
|
995
975
|
})).catch((error) => {
|
|
996
976
|
console.error(`[canon-sdk] Runtime ${signal} handler failed for ${conversationId}:`, error);
|
|
997
977
|
});
|
|
998
|
-
await Promise.resolve(rtdbWrite(`/control/${conversationId}/${this.agentId}/signal`, null)).catch(() => { });
|
|
999
978
|
}
|
|
1000
979
|
firstActiveTurn(conversationId) {
|
|
1001
980
|
const turns = this.activeTurns.get(conversationId);
|
|
@@ -1065,6 +1044,13 @@ export class CanonAgent {
|
|
|
1065
1044
|
console.error(`[canon-sdk] Runtime interrupt handler failed for ${conversationId}:`, error);
|
|
1066
1045
|
});
|
|
1067
1046
|
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Builds a runtime-state publisher bound to this agent's scoped RTDB
|
|
1049
|
+
* handle (captured in start()). Threading the handle keeps every
|
|
1050
|
+
* publish on this agent's own credentials — without it the publisher
|
|
1051
|
+
* would fall back to core's deprecated module-global RTDB client,
|
|
1052
|
+
* where the last-started agent's token wins in multi-agent processes.
|
|
1053
|
+
*/
|
|
1068
1054
|
createRuntimeStatePublisher() {
|
|
1069
1055
|
if (!this.agentId)
|
|
1070
1056
|
return null;
|
|
@@ -1072,6 +1058,7 @@ export class CanonAgent {
|
|
|
1072
1058
|
agentId: this.agentId,
|
|
1073
1059
|
clientType: this.options.clientType ?? 'generic',
|
|
1074
1060
|
hostMode: this.options.runtimeControlSurface === 'host',
|
|
1061
|
+
...(this.rtdbHandle ? { rtdb: this.rtdbHandle } : {}),
|
|
1075
1062
|
});
|
|
1076
1063
|
}
|
|
1077
1064
|
requireRuntimeStatePublisher() {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export { CanonAgent } from './canon-agent.js';
|
|
2
|
-
export type { AgentContactsAPI, AgentUsersAPI } from './canon-agent.js';
|
|
2
|
+
export type { AgentContactsAPI, AgentConversationsAPI, AgentUsersAPI } from './canon-agent.js';
|
|
3
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';
|
|
4
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, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, RuntimeInputAnswers, RuntimeInputChoice, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, SessionRule, } from '@canonmsg/core';
|
|
5
5
|
export { SessionManager } from './session-manager.js';
|
|
6
6
|
export { DEFAULT_MEDIA_CACHE_DIR, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, materializeReplyContextMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
|
|
7
7
|
export type { AnthropicImageBlock, AnthropicImageMimeType, MaterializeMediaOptions, MaterializedCanonAttachment, MaterializedCanonReplyContext, ReplyWithFileOptions, UploadMediaFileOptions, } from './media.js';
|
|
8
8
|
export type { SessionConfig, Session } from './session-manager.js';
|
|
9
|
-
export type { AgentContext, CanonGroupContext, CanonKnownRecentParticipant, CanonMembershipChange, CanonContactRequest, CanonMessage, CanonConversation, CanonReplyContext, CanonSelfContext, CanonTurnContextV2, CanonRuntimeDescriptor, MessageUpdatedPayload, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, CreateConversationOptions, } from '@canonmsg/core';
|
|
9
|
+
export type { AgentContext, CanonGroupContext, CanonKnownRecentParticipant, CanonMembershipChange, CanonContactRequest, CanonMessage, CanonConversation, CanonReplyContext, CanonSelfContext, CanonTurnContextV2, CanonRuntimeDescriptor, MessageUpdatedPayload, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, CreateConversationOptions, CreateConversationResult, DirectSessionSelection, } from '@canonmsg/core';
|
|
10
10
|
export type { CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, ContactRequestHandler, MessageHandler, MessageHandlerContext, MessageUpdatedHandler, ProgressMessageOptions, ProgressMessageResult, ReachOutOptions, ReachOutResult, RuntimeApprovalRequest, RuntimeInputRequest, RuntimeInputResult, RuntimeControlSurface, RuntimePrimitiveContext, RuntimePrimitiveHandler, RuntimePrimitiveHandlers, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
|
package/dist/realtime.d.ts
CHANGED
|
@@ -4,12 +4,21 @@ import { Debouncer } from './debouncer.js';
|
|
|
4
4
|
* Wraps @canonmsg/core's CanonStream with SDK-specific features:
|
|
5
5
|
* - Debouncer integration (message batching)
|
|
6
6
|
* - Agent context callback
|
|
7
|
+
* - REST catch-up when the SSE replay window expires (replay.expired)
|
|
7
8
|
*/
|
|
8
9
|
export declare class RealtimeManager {
|
|
9
10
|
private debouncer;
|
|
10
11
|
private agentId;
|
|
12
|
+
private apiClient;
|
|
11
13
|
private stream;
|
|
12
14
|
private running;
|
|
15
|
+
/** Recent inbound message IDs (`conversationId:messageId`) for cross-flush dedupe. */
|
|
16
|
+
private readonly recentInboundMessageIds;
|
|
17
|
+
/** Latest handled inbound message timestamp per conversation. */
|
|
18
|
+
private readonly lastInboundMessageAtByConversation;
|
|
19
|
+
/** Lower bound for catch-up in conversations with no in-memory cursor. */
|
|
20
|
+
private readonly replaySyncStartedAt;
|
|
21
|
+
private replayCatchupInFlight;
|
|
13
22
|
private lastSseErrorKey;
|
|
14
23
|
private lastSseErrorAt;
|
|
15
24
|
private suppressedSseErrorCount;
|
|
@@ -24,6 +33,23 @@ export declare class RealtimeManager {
|
|
|
24
33
|
private onConnected;
|
|
25
34
|
private onDisconnected;
|
|
26
35
|
constructor(apiKey: string, debouncer: Debouncer, agentId: string, streamUrl?: string, apiClient?: CanonClient);
|
|
36
|
+
private hasSeenInboundMessage;
|
|
37
|
+
private recordSeenInboundMessage;
|
|
38
|
+
private pruneRecentInboundMessageIds;
|
|
39
|
+
/**
|
|
40
|
+
* REST catch-up after `replay.expired`: the stream service evicted our
|
|
41
|
+
* cursor, so messages in the gap were silently dropped. Fetch the newest
|
|
42
|
+
* page per conversation and feed unseen inbound messages through the normal
|
|
43
|
+
* debouncer path (same entry point as SSE delivery, same id dedupe).
|
|
44
|
+
*
|
|
45
|
+
* Lower bound per conversation: the in-memory last-seen inbound timestamp,
|
|
46
|
+
* falling back to this manager's construction time for conversations with
|
|
47
|
+
* no prior inbound traffic — anything older predates this process and may
|
|
48
|
+
* already have been handled by a previous run. For the same reason the
|
|
49
|
+
* catch-up is NOT wired on initial connect: with no durable cursor, a fresh
|
|
50
|
+
* process would re-fire turns for messages an earlier run already answered.
|
|
51
|
+
*/
|
|
52
|
+
private runReplayCatchup;
|
|
27
53
|
private logSseError;
|
|
28
54
|
setOnAgentContext(cb: (ctx: AgentContext) => void): void;
|
|
29
55
|
setContactRequestHandlers(handlers: {
|
package/dist/realtime.js
CHANGED
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
import { CanonStream, } from '@canonmsg/core';
|
|
2
|
+
import { shouldDispatchInboundMessage } from './turn-filter.js';
|
|
3
|
+
const RECENT_INBOUND_TTL_MS = 30 * 60 * 1000;
|
|
4
|
+
const MAX_RECENT_INBOUND_MESSAGE_IDS = 5000;
|
|
5
|
+
/**
|
|
6
|
+
* Newest-page bound for the replay-expiry REST catch-up. The SDK has no
|
|
7
|
+
* durable per-conversation cursor (everything here is in-memory), so the
|
|
8
|
+
* catch-up only inspects the newest page per conversation and relies on the
|
|
9
|
+
* id-based dedupe below for anything that overlaps live SSE delivery.
|
|
10
|
+
*/
|
|
11
|
+
const REPLAY_CATCHUP_PAGE_LIMIT = 50;
|
|
12
|
+
function messageCreatedAtMs(createdAt) {
|
|
13
|
+
if (!createdAt)
|
|
14
|
+
return 0;
|
|
15
|
+
const parsed = new Date(createdAt).getTime();
|
|
16
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
17
|
+
}
|
|
2
18
|
/**
|
|
3
19
|
* Wraps @canonmsg/core's CanonStream with SDK-specific features:
|
|
4
20
|
* - Debouncer integration (message batching)
|
|
5
21
|
* - Agent context callback
|
|
22
|
+
* - REST catch-up when the SSE replay window expires (replay.expired)
|
|
6
23
|
*/
|
|
7
24
|
export class RealtimeManager {
|
|
8
25
|
debouncer;
|
|
9
26
|
agentId;
|
|
27
|
+
apiClient;
|
|
10
28
|
stream;
|
|
11
29
|
running = false;
|
|
30
|
+
/** Recent inbound message IDs (`conversationId:messageId`) for cross-flush dedupe. */
|
|
31
|
+
recentInboundMessageIds = new Map();
|
|
32
|
+
/** Latest handled inbound message timestamp per conversation. */
|
|
33
|
+
lastInboundMessageAtByConversation = new Map();
|
|
34
|
+
/** Lower bound for catch-up in conversations with no in-memory cursor. */
|
|
35
|
+
replaySyncStartedAt = Date.now();
|
|
36
|
+
replayCatchupInFlight = null;
|
|
12
37
|
lastSseErrorKey = null;
|
|
13
38
|
lastSseErrorAt = 0;
|
|
14
39
|
suppressedSseErrorCount = 0;
|
|
@@ -25,13 +50,19 @@ export class RealtimeManager {
|
|
|
25
50
|
constructor(apiKey, debouncer, agentId, streamUrl, apiClient) {
|
|
26
51
|
this.debouncer = debouncer;
|
|
27
52
|
this.agentId = agentId;
|
|
28
|
-
|
|
53
|
+
this.apiClient = apiClient ?? null;
|
|
29
54
|
this.stream = new CanonStream({
|
|
30
55
|
apiKey,
|
|
31
56
|
agentId,
|
|
32
57
|
streamUrl,
|
|
33
58
|
handler: {
|
|
34
59
|
onMessage: (payload) => {
|
|
60
|
+
// Cross-flush id dedupe: replay overlap or a concurrent REST
|
|
61
|
+
// catch-up must never double-fire a turn for the same message.
|
|
62
|
+
if (this.hasSeenInboundMessage(payload.conversationId, payload.message.id)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.recordSeenInboundMessage(payload.conversationId, payload.message.id, messageCreatedAtMs(payload.message.createdAt));
|
|
35
66
|
if (payload.turnDispatch && payload.turnDispatch.kind !== 'run_turn') {
|
|
36
67
|
console.error(`[canon-sdk] Ignoring server-dispatched observe-only message in ${payload.conversationId}: ${payload.turnDispatch.reason}`);
|
|
37
68
|
return;
|
|
@@ -96,12 +127,108 @@ export class RealtimeManager {
|
|
|
96
127
|
onDisconnected: () => {
|
|
97
128
|
this.onDisconnected?.();
|
|
98
129
|
},
|
|
130
|
+
onReplayExpired: (payload) => {
|
|
131
|
+
console.error(`[canon-sdk] SSE replay window expired${payload.lastAvailableId ? ` (oldest available event: ${payload.lastAvailableId})` : ''} — catching up over REST`);
|
|
132
|
+
this.replayCatchupInFlight ??= this.runReplayCatchup().finally(() => {
|
|
133
|
+
this.replayCatchupInFlight = null;
|
|
134
|
+
});
|
|
135
|
+
},
|
|
99
136
|
onError: (err) => {
|
|
100
137
|
this.logSseError(err);
|
|
101
138
|
},
|
|
102
139
|
},
|
|
103
140
|
});
|
|
104
141
|
}
|
|
142
|
+
hasSeenInboundMessage(conversationId, messageId) {
|
|
143
|
+
return this.recentInboundMessageIds.has(`${conversationId}:${messageId}`);
|
|
144
|
+
}
|
|
145
|
+
recordSeenInboundMessage(conversationId, messageId, createdAtMs) {
|
|
146
|
+
const now = Date.now();
|
|
147
|
+
this.recentInboundMessageIds.set(`${conversationId}:${messageId}`, now);
|
|
148
|
+
const effectiveTimestamp = createdAtMs > 0 ? createdAtMs : now;
|
|
149
|
+
const previous = this.lastInboundMessageAtByConversation.get(conversationId) ?? 0;
|
|
150
|
+
if (effectiveTimestamp > previous) {
|
|
151
|
+
this.lastInboundMessageAtByConversation.set(conversationId, effectiveTimestamp);
|
|
152
|
+
}
|
|
153
|
+
this.pruneRecentInboundMessageIds(now);
|
|
154
|
+
}
|
|
155
|
+
pruneRecentInboundMessageIds(now = Date.now()) {
|
|
156
|
+
const cutoff = now - RECENT_INBOUND_TTL_MS;
|
|
157
|
+
for (const [key, seenAt] of this.recentInboundMessageIds) {
|
|
158
|
+
if (seenAt < cutoff) {
|
|
159
|
+
this.recentInboundMessageIds.delete(key);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
while (this.recentInboundMessageIds.size > MAX_RECENT_INBOUND_MESSAGE_IDS) {
|
|
163
|
+
const oldestKey = this.recentInboundMessageIds.keys().next().value;
|
|
164
|
+
if (!oldestKey)
|
|
165
|
+
break;
|
|
166
|
+
this.recentInboundMessageIds.delete(oldestKey);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* REST catch-up after `replay.expired`: the stream service evicted our
|
|
171
|
+
* cursor, so messages in the gap were silently dropped. Fetch the newest
|
|
172
|
+
* page per conversation and feed unseen inbound messages through the normal
|
|
173
|
+
* debouncer path (same entry point as SSE delivery, same id dedupe).
|
|
174
|
+
*
|
|
175
|
+
* Lower bound per conversation: the in-memory last-seen inbound timestamp,
|
|
176
|
+
* falling back to this manager's construction time for conversations with
|
|
177
|
+
* no prior inbound traffic — anything older predates this process and may
|
|
178
|
+
* already have been handled by a previous run. For the same reason the
|
|
179
|
+
* catch-up is NOT wired on initial connect: with no durable cursor, a fresh
|
|
180
|
+
* process would re-fire turns for messages an earlier run already answered.
|
|
181
|
+
*/
|
|
182
|
+
async runReplayCatchup() {
|
|
183
|
+
const apiClient = this.apiClient;
|
|
184
|
+
if (!apiClient) {
|
|
185
|
+
console.error('[canon-sdk] Replay catch-up skipped — no API client available');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const conversations = await apiClient.getConversations();
|
|
190
|
+
let recovered = 0;
|
|
191
|
+
await Promise.all(conversations.map(async (conversation) => {
|
|
192
|
+
try {
|
|
193
|
+
const page = await apiClient.getMessagesPage(conversation.id, REPLAY_CATCHUP_PAGE_LIMIT);
|
|
194
|
+
const lowerBoundMs = this.lastInboundMessageAtByConversation.get(conversation.id)
|
|
195
|
+
?? this.replaySyncStartedAt;
|
|
196
|
+
const candidates = [...(page.messages ?? [])]
|
|
197
|
+
.filter((message) => !message.deleted)
|
|
198
|
+
.sort((a, b) => messageCreatedAtMs(a.createdAt) - messageCreatedAtMs(b.createdAt));
|
|
199
|
+
for (const message of candidates) {
|
|
200
|
+
if (!this.running)
|
|
201
|
+
return;
|
|
202
|
+
if (message.senderId === this.agentId)
|
|
203
|
+
continue;
|
|
204
|
+
const createdAtMs = messageCreatedAtMs(message.createdAt);
|
|
205
|
+
if (!createdAtMs || createdAtMs <= lowerBoundMs)
|
|
206
|
+
continue;
|
|
207
|
+
if (this.hasSeenInboundMessage(conversation.id, message.id))
|
|
208
|
+
continue;
|
|
209
|
+
this.recordSeenInboundMessage(conversation.id, message.id, createdAtMs);
|
|
210
|
+
const dispatch = await shouldDispatchInboundMessage(conversation.id, this.agentId, message, {
|
|
211
|
+
conversationType: conversation.type,
|
|
212
|
+
behavior: page.behavior ?? null,
|
|
213
|
+
});
|
|
214
|
+
if (!dispatch)
|
|
215
|
+
continue;
|
|
216
|
+
this.debouncer.add(conversation.id, message, null);
|
|
217
|
+
recovered += 1;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
console.error(`[canon-sdk] Replay catch-up failed for ${conversation.id}:`, err instanceof Error ? err.message : err);
|
|
222
|
+
}
|
|
223
|
+
}));
|
|
224
|
+
if (recovered > 0) {
|
|
225
|
+
console.error(`[canon-sdk] Replay catch-up recovered ${recovered} missed message(s)`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
console.error('[canon-sdk] Replay catch-up failed:', err instanceof Error ? err.message : err);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
105
232
|
logSseError(err) {
|
|
106
233
|
const code = err.code;
|
|
107
234
|
const key = `${typeof code === 'string' ? code : 'generic'}:${err.message}`;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type { AddMemberResult, AgentClientType, CanonGroupContext, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeDescriptor, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimePrimitiveId, CanonRuntimeProvenance, CanonTurnContextV2, CanonMessage, CanonConversation, CanonReplyContext, CanonContact, CanonContactRequest, CanonResolveAdmissionResult, ContactAddedPayload, ContactRemovedPayload, ContactSource, AgentContext, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonSelfContext, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, SessionConfig, CreateConversationOptions, TurnLifecycleState, TurnOutputBlock, TurnOutputBlockInput, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalRisk, ApprovalReplyMetadata, ApprovalOutcomeMetadata, RuntimeInputChoice, RuntimeInputAnswers, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, RuntimeCardNativeMetadata, RuntimeCardV1, SessionRule, ApprovalResult, } from '@canonmsg/core';
|
|
2
|
-
import type { AddMemberResult, CanonGroupContext, CanonMessage, CanonConversation, CanonReplyContext, ContactCardPayload, CanonRuntimeActionDispatch, CanonRuntimePrimitiveId, CanonRuntimeProvenance, CanonTurnContextV2, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalResult, ApprovalRisk, RuntimeInputChoice, RuntimeInputAnswers, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, RuntimeCardNativeMetadata, RuntimeCardV1, SendMessageOptions, SendContextualSelfContextInput, SessionConfig, TurnOutputBlock, TurnOutputBlockInput } from '@canonmsg/core';
|
|
1
|
+
export type { AddMemberResult, AgentClientType, CanonGroupContext, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeDescriptor, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimePrimitiveId, CanonRuntimeProvenance, CanonTurnContextV2, CanonMessage, CanonConversation, CanonReplyContext, CanonContact, CanonContactRequest, CanonResolveAdmissionResult, ContactAddedPayload, ContactRemovedPayload, ContactSource, AgentContext, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonSelfContext, CreateConversationResult, DirectSessionSelection, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, SessionConfig, CreateConversationOptions, TurnLifecycleState, TurnOutputBlock, TurnOutputBlockInput, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalRisk, ApprovalReplyMetadata, ApprovalOutcomeMetadata, RuntimeInputChoice, RuntimeInputAnswers, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, RuntimeCardNativeMetadata, RuntimeCardV1, SessionRule, ApprovalResult, } from '@canonmsg/core';
|
|
2
|
+
import type { AddMemberResult, CanonGroupContext, CanonMessage, CanonConversation, CanonReplyContext, ContactCardPayload, CanonRuntimeActionDispatch, CanonRuntimePrimitiveId, CanonRuntimeProvenance, CanonTurnContextV2, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalResult, ApprovalRisk, RuntimeInputChoice, RuntimeInputAnswers, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, RuntimeCardNativeMetadata, RuntimeCardV1, SendMessageOptions, SendContextualSelfContextInput, SessionConfig, DirectSessionSelection, TurnOutputBlock, TurnOutputBlockInput } from '@canonmsg/core';
|
|
3
3
|
import type { MaterializeMediaOptions, MaterializedCanonAttachment, ReplyWithFileOptions, UploadMediaFileOptions } from './media.js';
|
|
4
4
|
export interface ProgressMessageOptions extends SendMessageOptions {
|
|
5
5
|
/**
|
|
@@ -282,6 +282,9 @@ export type ReachOutResult = {
|
|
|
282
282
|
conversationId: string;
|
|
283
283
|
messageId?: string;
|
|
284
284
|
selfContextId?: string;
|
|
285
|
+
created?: boolean;
|
|
286
|
+
reused?: boolean;
|
|
287
|
+
sessionSelection?: DirectSessionSelection['mode'];
|
|
285
288
|
} | {
|
|
286
289
|
status: 'requested';
|
|
287
290
|
requestId: string | null;
|
|
@@ -293,6 +296,9 @@ export type ReachOutResult = {
|
|
|
293
296
|
} | {
|
|
294
297
|
status: 'setup_required';
|
|
295
298
|
reason: string;
|
|
299
|
+
} | {
|
|
300
|
+
status: 'no_session';
|
|
301
|
+
reason: string;
|
|
296
302
|
} | {
|
|
297
303
|
status: 'blocked' | 'unavailable';
|
|
298
304
|
reason: string;
|
|
@@ -304,6 +310,8 @@ export interface ReachOutOptions {
|
|
|
304
310
|
requestMessage?: string;
|
|
305
311
|
/** Explicit session setup to use when the contact-card target is an agent. */
|
|
306
312
|
sessionConfig?: SessionConfig | null;
|
|
313
|
+
/** Whether to continue an existing direct agent session or start a fresh one. */
|
|
314
|
+
sessionSelection?: DirectSessionSelection;
|
|
307
315
|
/** Source conversation for contextual cross-session reach-outs. */
|
|
308
316
|
sourceConversationId?: string;
|
|
309
317
|
/** Private context for the agent when this reach-out sends a cross-session message. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/agent-sdk",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.2",
|
|
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": "^2.1
|
|
31
|
+
"@canonmsg/core": "^2.4.1"
|
|
32
32
|
},
|
|
33
33
|
"publishConfig": {
|
|
34
34
|
"access": "public"
|