@canonmsg/agent-sdk 4.0.0 → 5.0.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 +343 -41
- package/dist/auth.d.ts +22 -0
- package/dist/auth.js +73 -0
- package/dist/canon-agent.d.ts +195 -0
- package/dist/canon-agent.js +2052 -0
- package/dist/debouncer.d.ts +15 -0
- package/dist/debouncer.js +98 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +2 -6
- package/dist/policy-history.d.ts +10 -0
- package/dist/policy-history.js +11 -0
- package/dist/realtime.d.ts +55 -0
- package/dist/realtime.js +194 -0
- package/dist/runtime-card.d.ts +29 -0
- package/dist/runtime-card.js +22 -0
- package/dist/turn-filter.d.ts +9 -0
- package/dist/turn-filter.js +25 -0
- package/dist/types.d.ts +321 -0
- package/dist/types.js +1 -0
- package/package.json +7 -7
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CanonMessage, CanonRuntimeProvenance } from '@canonmsg/core';
|
|
2
|
+
export declare class Debouncer {
|
|
3
|
+
private debounceMs;
|
|
4
|
+
private pending;
|
|
5
|
+
private provenanceByMessageId;
|
|
6
|
+
private timers;
|
|
7
|
+
private orderedFlags;
|
|
8
|
+
private callback;
|
|
9
|
+
constructor(debounceMs: number);
|
|
10
|
+
setCallback(cb: (conversationId: string, messages: CanonMessage[], provenanceByMessageId: ReadonlyMap<string, CanonRuntimeProvenance>) => void): void;
|
|
11
|
+
add(conversationId: string, message: CanonMessage, provenance?: CanonRuntimeProvenance | null): void;
|
|
12
|
+
removeMessage(conversationId: string, messageId: string): boolean;
|
|
13
|
+
private flush;
|
|
14
|
+
destroy(): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export class Debouncer {
|
|
2
|
+
debounceMs;
|
|
3
|
+
pending = new Map();
|
|
4
|
+
provenanceByMessageId = new Map();
|
|
5
|
+
timers = new Map();
|
|
6
|
+
// Track whether each conversation's pending messages are already in sorted
|
|
7
|
+
// order so we can skip the sort on flush when possible (#10)
|
|
8
|
+
orderedFlags = new Map();
|
|
9
|
+
callback = null;
|
|
10
|
+
constructor(debounceMs) {
|
|
11
|
+
this.debounceMs = debounceMs;
|
|
12
|
+
}
|
|
13
|
+
setCallback(cb) {
|
|
14
|
+
this.callback = cb;
|
|
15
|
+
}
|
|
16
|
+
add(conversationId, message, provenance) {
|
|
17
|
+
const existing = this.pending.get(conversationId) || [];
|
|
18
|
+
if (provenance) {
|
|
19
|
+
this.provenanceByMessageId.set(message.id, provenance);
|
|
20
|
+
}
|
|
21
|
+
// Deduplicate by message ID
|
|
22
|
+
if (!existing.some((m) => m.id === message.id)) {
|
|
23
|
+
if (existing.length === 0) {
|
|
24
|
+
// First message for this conversation — trivially ordered
|
|
25
|
+
this.orderedFlags.set(conversationId, true);
|
|
26
|
+
}
|
|
27
|
+
else if (this.orderedFlags.get(conversationId) !== false) {
|
|
28
|
+
// Still potentially ordered — check if new message maintains sort order
|
|
29
|
+
const lastTime = new Date(existing[existing.length - 1].createdAt).getTime();
|
|
30
|
+
const newTime = new Date(message.createdAt).getTime();
|
|
31
|
+
if (newTime < lastTime) {
|
|
32
|
+
this.orderedFlags.set(conversationId, false);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
existing.push(message);
|
|
36
|
+
this.pending.set(conversationId, existing);
|
|
37
|
+
}
|
|
38
|
+
// Reset timer for this conversation
|
|
39
|
+
const existingTimer = this.timers.get(conversationId);
|
|
40
|
+
if (existingTimer)
|
|
41
|
+
clearTimeout(existingTimer);
|
|
42
|
+
this.timers.set(conversationId, setTimeout(() => {
|
|
43
|
+
this.flush(conversationId);
|
|
44
|
+
}, this.debounceMs));
|
|
45
|
+
}
|
|
46
|
+
removeMessage(conversationId, messageId) {
|
|
47
|
+
const existing = this.pending.get(conversationId);
|
|
48
|
+
if (!existing || existing.length === 0)
|
|
49
|
+
return false;
|
|
50
|
+
const next = existing.filter((message) => message.id !== messageId);
|
|
51
|
+
if (next.length === existing.length)
|
|
52
|
+
return false;
|
|
53
|
+
this.provenanceByMessageId.delete(messageId);
|
|
54
|
+
if (next.length === 0) {
|
|
55
|
+
this.pending.delete(conversationId);
|
|
56
|
+
this.orderedFlags.delete(conversationId);
|
|
57
|
+
const timer = this.timers.get(conversationId);
|
|
58
|
+
if (timer)
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
this.timers.delete(conversationId);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
this.pending.set(conversationId, next);
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
flush(conversationId) {
|
|
68
|
+
const messages = this.pending.get(conversationId);
|
|
69
|
+
const isOrdered = this.orderedFlags.get(conversationId) ?? false;
|
|
70
|
+
this.pending.delete(conversationId);
|
|
71
|
+
this.timers.delete(conversationId);
|
|
72
|
+
this.orderedFlags.delete(conversationId);
|
|
73
|
+
if (messages && messages.length > 0 && this.callback) {
|
|
74
|
+
// Only sort if insertions arrived out of order (#10)
|
|
75
|
+
if (!isOrdered) {
|
|
76
|
+
messages.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
77
|
+
}
|
|
78
|
+
const provenanceByMessageId = new Map();
|
|
79
|
+
for (const message of messages) {
|
|
80
|
+
const provenance = this.provenanceByMessageId.get(message.id);
|
|
81
|
+
if (provenance) {
|
|
82
|
+
provenanceByMessageId.set(message.id, provenance);
|
|
83
|
+
this.provenanceByMessageId.delete(message.id);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
this.callback(conversationId, messages, provenanceByMessageId);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
destroy() {
|
|
90
|
+
for (const timer of this.timers.values()) {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
}
|
|
93
|
+
this.timers.clear();
|
|
94
|
+
this.pending.clear();
|
|
95
|
+
this.provenanceByMessageId.clear();
|
|
96
|
+
this.orderedFlags.clear();
|
|
97
|
+
}
|
|
98
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
export { CanonAgent } from './canon-agent.js';
|
|
2
|
+
export type { AgentContactsAPI, AgentConversationsAPI, AgentUsersAPI } from './canon-agent.js';
|
|
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
|
+
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, ResolveAdmissionTargetInput, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, RuntimeInputAnswers, RuntimeInputChoice, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, SessionRule, } from '@canonmsg/core';
|
|
1
5
|
export { SessionManager } from './session-manager.js';
|
|
2
|
-
export type { EnqueueOptions, Session, SessionConfig } from './session-manager.js';
|
|
3
6
|
export { DEFAULT_MEDIA_CACHE_DIR, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, materializeReplyContextMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
|
|
4
7
|
export type { AnthropicImageBlock, AnthropicImageMimeType, MaterializeMediaOptions, MaterializedCanonAttachment, MaterializedCanonReplyContext, ReplyWithFileOptions, UploadMediaFileOptions, } from './media.js';
|
|
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, CreateConversationResult, DirectSessionSelection, } from '@canonmsg/core';
|
|
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/index.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// The pre-bridge CanonAgent runtime (SSE loop, RTDB control polling, HITL
|
|
4
|
-
// custody) was retired in the housecleaning campaign (W2-A1). Standalone
|
|
5
|
-
// agents speak Canon through @canonmsg/framework + the canon-bridge daemon;
|
|
6
|
-
// the published 3.x line keeps the old runtime for anyone pinned to it.
|
|
1
|
+
export { CanonAgent } from './canon-agent.js';
|
|
2
|
+
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';
|
|
7
3
|
export { SessionManager } from './session-manager.js';
|
|
8
4
|
export { DEFAULT_MEDIA_CACHE_DIR, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, materializeReplyContextMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type CanonMessage, type ParticipationHistorySnapshot } from '@canonmsg/core';
|
|
2
|
+
export type { ParticipationHistorySnapshot } from '@canonmsg/core';
|
|
3
|
+
/**
|
|
4
|
+
* Builds message-specific participation history snapshots for backlog delivery.
|
|
5
|
+
*
|
|
6
|
+
* `messages` must be ordered newest-first, matching Canon's `getMessages()`
|
|
7
|
+
* API. Each snapshot is computed from older history only, never from the
|
|
8
|
+
* target message itself or newer messages that had not occurred yet.
|
|
9
|
+
*/
|
|
10
|
+
export declare function buildParticipationHistorySnapshots(messages: CanonMessage[], agentId: string): Map<string, ParticipationHistorySnapshot>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { buildParticipationHistorySnapshots as buildSharedParticipationHistorySnapshots, } from '@canonmsg/core';
|
|
2
|
+
/**
|
|
3
|
+
* Builds message-specific participation history snapshots for backlog delivery.
|
|
4
|
+
*
|
|
5
|
+
* `messages` must be ordered newest-first, matching Canon's `getMessages()`
|
|
6
|
+
* API. Each snapshot is computed from older history only, never from the
|
|
7
|
+
* target message itself or newer messages that had not occurred yet.
|
|
8
|
+
*/
|
|
9
|
+
export function buildParticipationHistorySnapshots(messages, agentId) {
|
|
10
|
+
return buildSharedParticipationHistorySnapshots(messages, agentId);
|
|
11
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type AgentContext, type CanonClient, type ContactAddedPayload, type ContactApprovedPayload, type ContactRemovedPayload, type ContactRequestPayload, type ConversationUpdatedPayload, type MessageUpdatedPayload } from '@canonmsg/core';
|
|
2
|
+
import { Debouncer } from './debouncer.js';
|
|
3
|
+
/**
|
|
4
|
+
* Wraps @canonmsg/core's CanonStream with SDK-specific features:
|
|
5
|
+
* - Debouncer integration (message batching)
|
|
6
|
+
* - Agent context callback
|
|
7
|
+
* - Connection/status callbacks
|
|
8
|
+
*/
|
|
9
|
+
export declare class RealtimeManager {
|
|
10
|
+
private debouncer;
|
|
11
|
+
private agentId;
|
|
12
|
+
private stream;
|
|
13
|
+
private running;
|
|
14
|
+
/** Recent inbound message IDs (`conversationId:messageId`) for cross-flush dedupe. */
|
|
15
|
+
private readonly recentInboundMessageIds;
|
|
16
|
+
private lastSseErrorKey;
|
|
17
|
+
private lastSseErrorAt;
|
|
18
|
+
private suppressedSseErrorCount;
|
|
19
|
+
private onAgentContext;
|
|
20
|
+
private onContactRequest;
|
|
21
|
+
private onContactApproved;
|
|
22
|
+
private onContactAdded;
|
|
23
|
+
private onContactRemoved;
|
|
24
|
+
private onConversationUpdated;
|
|
25
|
+
private onMessageUpdated;
|
|
26
|
+
private onMessageDeleted;
|
|
27
|
+
private onConnected;
|
|
28
|
+
private onDisconnected;
|
|
29
|
+
constructor(apiKey: string, debouncer: Debouncer, agentId: string, streamUrl?: string, apiClient?: CanonClient);
|
|
30
|
+
private hasSeenInboundMessage;
|
|
31
|
+
private recordSeenInboundMessage;
|
|
32
|
+
private pruneRecentInboundMessageIds;
|
|
33
|
+
private logSseError;
|
|
34
|
+
setOnAgentContext(cb: (ctx: AgentContext) => void): void;
|
|
35
|
+
setContactRequestHandlers(handlers: {
|
|
36
|
+
onContactRequest?: (payload: ContactRequestPayload) => void;
|
|
37
|
+
onContactApproved?: (payload: ContactApprovedPayload) => void;
|
|
38
|
+
}): void;
|
|
39
|
+
setContactGraphHandlers(handlers: {
|
|
40
|
+
onContactAdded?: (payload: ContactAddedPayload) => void;
|
|
41
|
+
onContactRemoved?: (payload: ContactRemovedPayload) => void;
|
|
42
|
+
}): void;
|
|
43
|
+
setConversationUpdatedHandler(cb: (payload: ConversationUpdatedPayload) => void): void;
|
|
44
|
+
setMessageUpdatedHandler(cb: (payload: MessageUpdatedPayload) => void): void;
|
|
45
|
+
setMessageDeletedHandler(cb: (payload: {
|
|
46
|
+
conversationId: string;
|
|
47
|
+
messageId: string;
|
|
48
|
+
}) => void): void;
|
|
49
|
+
setConnectionHandlers(handlers: {
|
|
50
|
+
onConnected?: () => void;
|
|
51
|
+
onDisconnected?: () => void;
|
|
52
|
+
}): void;
|
|
53
|
+
start(): Promise<void>;
|
|
54
|
+
stop(): void;
|
|
55
|
+
}
|
package/dist/realtime.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { CanonStream, } from '@canonmsg/core';
|
|
2
|
+
const RECENT_INBOUND_TTL_MS = 30 * 60 * 1000;
|
|
3
|
+
const MAX_RECENT_INBOUND_MESSAGE_IDS = 5000;
|
|
4
|
+
function messageCreatedAtMs(createdAt) {
|
|
5
|
+
if (!createdAt)
|
|
6
|
+
return 0;
|
|
7
|
+
const parsed = new Date(createdAt).getTime();
|
|
8
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Wraps @canonmsg/core's CanonStream with SDK-specific features:
|
|
12
|
+
* - Debouncer integration (message batching)
|
|
13
|
+
* - Agent context callback
|
|
14
|
+
* - Connection/status callbacks
|
|
15
|
+
*/
|
|
16
|
+
export class RealtimeManager {
|
|
17
|
+
debouncer;
|
|
18
|
+
agentId;
|
|
19
|
+
stream;
|
|
20
|
+
running = false;
|
|
21
|
+
/** Recent inbound message IDs (`conversationId:messageId`) for cross-flush dedupe. */
|
|
22
|
+
recentInboundMessageIds = new Map();
|
|
23
|
+
lastSseErrorKey = null;
|
|
24
|
+
lastSseErrorAt = 0;
|
|
25
|
+
suppressedSseErrorCount = 0;
|
|
26
|
+
onAgentContext = null;
|
|
27
|
+
onContactRequest = null;
|
|
28
|
+
onContactApproved = null;
|
|
29
|
+
onContactAdded = null;
|
|
30
|
+
onContactRemoved = null;
|
|
31
|
+
onConversationUpdated = null;
|
|
32
|
+
onMessageUpdated = null;
|
|
33
|
+
onMessageDeleted = null;
|
|
34
|
+
onConnected = null;
|
|
35
|
+
onDisconnected = null;
|
|
36
|
+
constructor(apiKey, debouncer, agentId, streamUrl, apiClient) {
|
|
37
|
+
this.debouncer = debouncer;
|
|
38
|
+
this.agentId = agentId;
|
|
39
|
+
this.stream = new CanonStream({
|
|
40
|
+
apiKey,
|
|
41
|
+
agentId,
|
|
42
|
+
streamUrl,
|
|
43
|
+
handler: {
|
|
44
|
+
onMessage: (payload) => {
|
|
45
|
+
// Cross-flush id dedupe: SSE replay overlap must never double-fire
|
|
46
|
+
// a turn for the same message.
|
|
47
|
+
if (this.hasSeenInboundMessage(payload.conversationId, payload.message.id)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this.recordSeenInboundMessage(payload.conversationId, payload.message.id, messageCreatedAtMs(payload.message.createdAt));
|
|
51
|
+
if (payload.turnDispatch && payload.turnDispatch.kind !== 'run_turn') {
|
|
52
|
+
console.error(`[canon-sdk] Ignoring server-dispatched observe-only message in ${payload.conversationId}: ${payload.turnDispatch.reason}`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const m = payload.message;
|
|
56
|
+
const message = {
|
|
57
|
+
id: m.id,
|
|
58
|
+
senderId: m.senderId,
|
|
59
|
+
...(m.senderName ? { senderName: m.senderName } : {}),
|
|
60
|
+
senderType: m.senderType ?? 'human',
|
|
61
|
+
isOwner: m.isOwner ?? false,
|
|
62
|
+
contentType: m.contentType ?? 'text',
|
|
63
|
+
text: m.text ?? null,
|
|
64
|
+
attachments: m.attachments ?? [],
|
|
65
|
+
mentions: m.mentions ?? [],
|
|
66
|
+
...(m.reactions ? { reactions: m.reactions } : {}),
|
|
67
|
+
replyTo: m.replyTo ?? null,
|
|
68
|
+
replyToPosition: m.replyToPosition ?? null,
|
|
69
|
+
...(m.forwarded === true || m.forwardedFrom
|
|
70
|
+
? { forwarded: true }
|
|
71
|
+
: {}),
|
|
72
|
+
...(m.forwardedFrom
|
|
73
|
+
? { forwardedFrom: m.forwardedFrom }
|
|
74
|
+
: {}),
|
|
75
|
+
status: 'sent',
|
|
76
|
+
deleted: false,
|
|
77
|
+
createdAt: m.createdAt ?? new Date().toISOString(),
|
|
78
|
+
...(m.contactCard ? { contactCard: m.contactCard } : {}),
|
|
79
|
+
...(m.metadata ? { metadata: m.metadata } : {}),
|
|
80
|
+
};
|
|
81
|
+
this.debouncer.add(payload.conversationId, message, payload.provenance ?? null);
|
|
82
|
+
},
|
|
83
|
+
onMessageDeleted: (payload) => {
|
|
84
|
+
this.debouncer.removeMessage(payload.conversationId, payload.messageId);
|
|
85
|
+
this.onMessageDeleted?.(payload);
|
|
86
|
+
},
|
|
87
|
+
onMessageUpdated: (payload) => {
|
|
88
|
+
this.onMessageUpdated?.(payload);
|
|
89
|
+
},
|
|
90
|
+
onAgentContext: (ctx) => {
|
|
91
|
+
this.onAgentContext?.(ctx);
|
|
92
|
+
},
|
|
93
|
+
onContactRequest: (payload) => {
|
|
94
|
+
this.onContactRequest?.(payload);
|
|
95
|
+
},
|
|
96
|
+
onContactApproved: (payload) => {
|
|
97
|
+
this.onContactApproved?.(payload);
|
|
98
|
+
},
|
|
99
|
+
onContactAdded: (payload) => {
|
|
100
|
+
this.onContactAdded?.(payload);
|
|
101
|
+
},
|
|
102
|
+
onContactRemoved: (payload) => {
|
|
103
|
+
this.onContactRemoved?.(payload);
|
|
104
|
+
},
|
|
105
|
+
onConversationUpdated: (payload) => {
|
|
106
|
+
this.onConversationUpdated?.(payload);
|
|
107
|
+
},
|
|
108
|
+
onConnected: () => {
|
|
109
|
+
this.onConnected?.();
|
|
110
|
+
},
|
|
111
|
+
onDisconnected: () => {
|
|
112
|
+
this.onDisconnected?.();
|
|
113
|
+
},
|
|
114
|
+
onReplayExpired: (payload) => {
|
|
115
|
+
console.error(`[canon-sdk] SSE replay window expired${payload.lastAvailableId ? ` (oldest available event: ${payload.lastAvailableId})` : ''}; missed history is available via explicit REST fetch`);
|
|
116
|
+
},
|
|
117
|
+
onError: (err) => {
|
|
118
|
+
this.logSseError(err);
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
hasSeenInboundMessage(conversationId, messageId) {
|
|
124
|
+
return this.recentInboundMessageIds.has(`${conversationId}:${messageId}`);
|
|
125
|
+
}
|
|
126
|
+
recordSeenInboundMessage(conversationId, messageId, createdAtMs) {
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
this.recentInboundMessageIds.set(`${conversationId}:${messageId}`, now);
|
|
129
|
+
void createdAtMs;
|
|
130
|
+
this.pruneRecentInboundMessageIds(now);
|
|
131
|
+
}
|
|
132
|
+
pruneRecentInboundMessageIds(now = Date.now()) {
|
|
133
|
+
const cutoff = now - RECENT_INBOUND_TTL_MS;
|
|
134
|
+
for (const [key, seenAt] of this.recentInboundMessageIds) {
|
|
135
|
+
if (seenAt < cutoff) {
|
|
136
|
+
this.recentInboundMessageIds.delete(key);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
while (this.recentInboundMessageIds.size > MAX_RECENT_INBOUND_MESSAGE_IDS) {
|
|
140
|
+
const oldestKey = this.recentInboundMessageIds.keys().next().value;
|
|
141
|
+
if (!oldestKey)
|
|
142
|
+
break;
|
|
143
|
+
this.recentInboundMessageIds.delete(oldestKey);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
logSseError(err) {
|
|
147
|
+
const code = err.code;
|
|
148
|
+
const key = `${typeof code === 'string' ? code : 'generic'}:${err.message}`;
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
if (this.lastSseErrorKey === key && now - this.lastSseErrorAt < 60_000) {
|
|
151
|
+
this.suppressedSseErrorCount += 1;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (this.suppressedSseErrorCount > 0) {
|
|
155
|
+
console.error(`[canon-sdk] SSE error repeated ${this.suppressedSseErrorCount} more time${this.suppressedSseErrorCount === 1 ? '' : 's'}`);
|
|
156
|
+
this.suppressedSseErrorCount = 0;
|
|
157
|
+
}
|
|
158
|
+
this.lastSseErrorKey = key;
|
|
159
|
+
this.lastSseErrorAt = now;
|
|
160
|
+
console.error('[canon-sdk] SSE error:', err.message);
|
|
161
|
+
}
|
|
162
|
+
setOnAgentContext(cb) {
|
|
163
|
+
this.onAgentContext = cb;
|
|
164
|
+
}
|
|
165
|
+
setContactRequestHandlers(handlers) {
|
|
166
|
+
this.onContactRequest = handlers.onContactRequest ?? null;
|
|
167
|
+
this.onContactApproved = handlers.onContactApproved ?? null;
|
|
168
|
+
}
|
|
169
|
+
setContactGraphHandlers(handlers) {
|
|
170
|
+
this.onContactAdded = handlers.onContactAdded ?? null;
|
|
171
|
+
this.onContactRemoved = handlers.onContactRemoved ?? null;
|
|
172
|
+
}
|
|
173
|
+
setConversationUpdatedHandler(cb) {
|
|
174
|
+
this.onConversationUpdated = cb;
|
|
175
|
+
}
|
|
176
|
+
setMessageUpdatedHandler(cb) {
|
|
177
|
+
this.onMessageUpdated = cb;
|
|
178
|
+
}
|
|
179
|
+
setMessageDeletedHandler(cb) {
|
|
180
|
+
this.onMessageDeleted = cb;
|
|
181
|
+
}
|
|
182
|
+
setConnectionHandlers(handlers) {
|
|
183
|
+
this.onConnected = handlers.onConnected ?? null;
|
|
184
|
+
this.onDisconnected = handlers.onDisconnected ?? null;
|
|
185
|
+
}
|
|
186
|
+
async start() {
|
|
187
|
+
this.running = true;
|
|
188
|
+
await this.stream.start();
|
|
189
|
+
}
|
|
190
|
+
stop() {
|
|
191
|
+
this.running = false;
|
|
192
|
+
this.stream.stop();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RuntimeCardNativeMetadata, RuntimeCardV1 } from '@canonmsg/core';
|
|
2
|
+
import type { RuntimeCardRequest } from './types';
|
|
3
|
+
/** Arguments passed to `CanonClient.createRuntimeCardRequest`. */
|
|
4
|
+
export interface RuntimeCardCreateArgs {
|
|
5
|
+
conversationId: string;
|
|
6
|
+
card: RuntimeCardV1;
|
|
7
|
+
cardId: string;
|
|
8
|
+
expiresAt: number;
|
|
9
|
+
responseUserId?: string;
|
|
10
|
+
runtimeId?: string;
|
|
11
|
+
turnId?: string;
|
|
12
|
+
native?: RuntimeCardNativeMetadata;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Build the `createRuntimeCardRequest` payload shared by `sendCard` (display)
|
|
16
|
+
* and `requestCard` (interactive).
|
|
17
|
+
*
|
|
18
|
+
* `responseUserId` is OMITTED unless the developer supplied one, so the Canon
|
|
19
|
+
* backend infers a reachable responder (the owner when they are a conversation
|
|
20
|
+
* member, otherwise the sole other member). Pre-filling the owner here would
|
|
21
|
+
* break agent-to-user DMs where the owner is not a member of the conversation.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildRuntimeCardCreateArgs(input: {
|
|
24
|
+
request: RuntimeCardRequest;
|
|
25
|
+
conversationId: string;
|
|
26
|
+
cardId: string;
|
|
27
|
+
fallbackTurnId?: string;
|
|
28
|
+
expiresAtMs: number;
|
|
29
|
+
}): RuntimeCardCreateArgs;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the `createRuntimeCardRequest` payload shared by `sendCard` (display)
|
|
3
|
+
* and `requestCard` (interactive).
|
|
4
|
+
*
|
|
5
|
+
* `responseUserId` is OMITTED unless the developer supplied one, so the Canon
|
|
6
|
+
* backend infers a reachable responder (the owner when they are a conversation
|
|
7
|
+
* member, otherwise the sole other member). Pre-filling the owner here would
|
|
8
|
+
* break agent-to-user DMs where the owner is not a member of the conversation.
|
|
9
|
+
*/
|
|
10
|
+
export function buildRuntimeCardCreateArgs(input) {
|
|
11
|
+
const { request, conversationId, cardId, fallbackTurnId, expiresAtMs } = input;
|
|
12
|
+
return {
|
|
13
|
+
conversationId,
|
|
14
|
+
cardId,
|
|
15
|
+
card: { ...request.card, cardId },
|
|
16
|
+
expiresAt: expiresAtMs,
|
|
17
|
+
...(request.responseUserId ? { responseUserId: request.responseUserId } : {}),
|
|
18
|
+
...(request.runtimeId ? { runtimeId: request.runtimeId } : {}),
|
|
19
|
+
turnId: request.turnId ?? fallbackTurnId,
|
|
20
|
+
...(request.native ? { native: request.native } : {}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ResolvedAgentBehaviorPolicy, type CanonMessage, type MessageCreatedPayload } from '@canonmsg/core';
|
|
2
|
+
export declare function shouldDispatchInboundMessage(_conversationId: string, agentId: string, message: CanonMessage, options?: {
|
|
3
|
+
conversationType?: 'direct' | 'group' | 'unknown';
|
|
4
|
+
behavior?: ResolvedAgentBehaviorPolicy | null;
|
|
5
|
+
recentHumanCount?: number;
|
|
6
|
+
consecutiveAgentTurns?: number;
|
|
7
|
+
currentAgentStreakStartedByHuman?: boolean;
|
|
8
|
+
turnDispatch?: MessageCreatedPayload['turnDispatch'];
|
|
9
|
+
}): Promise<boolean>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { evaluateParticipationPolicy, shouldTriggerAgentTurn, } from '@canonmsg/core';
|
|
2
|
+
export async function shouldDispatchInboundMessage(_conversationId, agentId, message, options) {
|
|
3
|
+
if (message.senderId === agentId)
|
|
4
|
+
return false;
|
|
5
|
+
if (options?.turnDispatch) {
|
|
6
|
+
return options.turnDispatch.kind === 'run_turn';
|
|
7
|
+
}
|
|
8
|
+
const triggerDecision = shouldTriggerAgentTurn({
|
|
9
|
+
senderType: message.senderType,
|
|
10
|
+
metadata: message.metadata,
|
|
11
|
+
});
|
|
12
|
+
if (!triggerDecision.allow)
|
|
13
|
+
return false;
|
|
14
|
+
if (!options?.behavior)
|
|
15
|
+
return true;
|
|
16
|
+
return evaluateParticipationPolicy(options.behavior, {
|
|
17
|
+
conversationType: options.conversationType ?? 'unknown',
|
|
18
|
+
senderType: message.senderType,
|
|
19
|
+
isOwner: message.isOwner,
|
|
20
|
+
mentionedAgent: Array.isArray(message.mentions) && message.mentions.includes(agentId),
|
|
21
|
+
recentHumanCount: options.recentHumanCount,
|
|
22
|
+
consecutiveAgentTurns: options.consecutiveAgentTurns,
|
|
23
|
+
currentAgentStreakStartedByHuman: options.currentAgentStreakStartedByHuman,
|
|
24
|
+
}).allow;
|
|
25
|
+
}
|