@canonmsg/agent-sdk 3.4.3 → 4.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.
@@ -1,15 +0,0 @@
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
- }
package/dist/debouncer.js DELETED
@@ -1,98 +0,0 @@
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
- }
@@ -1,10 +0,0 @@
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>;
@@ -1,11 +0,0 @@
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
- }
@@ -1,77 +0,0 @@
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
- * - REST catch-up when the SSE replay window expires (replay.expired)
8
- */
9
- export declare class RealtimeManager {
10
- private debouncer;
11
- private agentId;
12
- private apiClient;
13
- private stream;
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;
22
- private hasConnectedOnce;
23
- private lastSseErrorKey;
24
- private lastSseErrorAt;
25
- private suppressedSseErrorCount;
26
- private onAgentContext;
27
- private onContactRequest;
28
- private onContactApproved;
29
- private onContactAdded;
30
- private onContactRemoved;
31
- private onConversationUpdated;
32
- private onMessageUpdated;
33
- private onMessageDeleted;
34
- private onConnected;
35
- private onDisconnected;
36
- constructor(apiKey: string, debouncer: Debouncer, agentId: string, streamUrl?: string, apiClient?: CanonClient);
37
- private queueReplayCatchup;
38
- private hasSeenInboundMessage;
39
- private recordSeenInboundMessage;
40
- private pruneRecentInboundMessageIds;
41
- /**
42
- * REST catch-up after `replay.expired`: the stream service evicted our
43
- * cursor, so messages in the gap were silently dropped. Fetch the newest
44
- * page per conversation and feed unseen inbound messages through the normal
45
- * debouncer path (same entry point as SSE delivery, same id dedupe).
46
- *
47
- * Lower bound per conversation: the in-memory last-seen inbound timestamp,
48
- * falling back to this manager's construction time for conversations with
49
- * no prior inbound traffic — anything older predates this process and may
50
- * already have been handled by a previous run. For the same reason the
51
- * catch-up is NOT wired on initial connect: with no durable cursor, a fresh
52
- * process would re-fire turns for messages an earlier run already answered.
53
- */
54
- private runReplayCatchup;
55
- private logSseError;
56
- setOnAgentContext(cb: (ctx: AgentContext) => void): void;
57
- setContactRequestHandlers(handlers: {
58
- onContactRequest?: (payload: ContactRequestPayload) => void;
59
- onContactApproved?: (payload: ContactApprovedPayload) => void;
60
- }): void;
61
- setContactGraphHandlers(handlers: {
62
- onContactAdded?: (payload: ContactAddedPayload) => void;
63
- onContactRemoved?: (payload: ContactRemovedPayload) => void;
64
- }): void;
65
- setConversationUpdatedHandler(cb: (payload: ConversationUpdatedPayload) => void): void;
66
- setMessageUpdatedHandler(cb: (payload: MessageUpdatedPayload) => void): void;
67
- setMessageDeletedHandler(cb: (payload: {
68
- conversationId: string;
69
- messageId: string;
70
- }) => void): void;
71
- setConnectionHandlers(handlers: {
72
- onConnected?: () => void;
73
- onDisconnected?: () => void;
74
- }): void;
75
- start(): Promise<void>;
76
- stop(): void;
77
- }
package/dist/realtime.js DELETED
@@ -1,295 +0,0 @@
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
- }
18
- /**
19
- * Wraps @canonmsg/core's CanonStream with SDK-specific features:
20
- * - Debouncer integration (message batching)
21
- * - Agent context callback
22
- * - REST catch-up when the SSE replay window expires (replay.expired)
23
- */
24
- export class RealtimeManager {
25
- debouncer;
26
- agentId;
27
- apiClient;
28
- stream;
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;
37
- hasConnectedOnce = false;
38
- lastSseErrorKey = null;
39
- lastSseErrorAt = 0;
40
- suppressedSseErrorCount = 0;
41
- onAgentContext = null;
42
- onContactRequest = null;
43
- onContactApproved = null;
44
- onContactAdded = null;
45
- onContactRemoved = null;
46
- onConversationUpdated = null;
47
- onMessageUpdated = null;
48
- onMessageDeleted = null;
49
- onConnected = null;
50
- onDisconnected = null;
51
- constructor(apiKey, debouncer, agentId, streamUrl, apiClient) {
52
- this.debouncer = debouncer;
53
- this.agentId = agentId;
54
- this.apiClient = apiClient ?? null;
55
- this.stream = new CanonStream({
56
- apiKey,
57
- agentId,
58
- streamUrl,
59
- handler: {
60
- onMessage: (payload) => {
61
- // Cross-flush id dedupe: replay overlap or a concurrent REST
62
- // catch-up must never double-fire a turn for the same message.
63
- if (this.hasSeenInboundMessage(payload.conversationId, payload.message.id)) {
64
- return;
65
- }
66
- this.recordSeenInboundMessage(payload.conversationId, payload.message.id, messageCreatedAtMs(payload.message.createdAt));
67
- if (payload.turnDispatch && payload.turnDispatch.kind !== 'run_turn') {
68
- console.error(`[canon-sdk] Ignoring server-dispatched observe-only message in ${payload.conversationId}: ${payload.turnDispatch.reason}`);
69
- return;
70
- }
71
- const m = payload.message;
72
- const message = {
73
- id: m.id,
74
- senderId: m.senderId,
75
- ...(m.senderName ? { senderName: m.senderName } : {}),
76
- senderType: m.senderType ?? 'human',
77
- isOwner: m.isOwner ?? false,
78
- contentType: m.contentType ?? 'text',
79
- text: m.text ?? null,
80
- attachments: m.attachments ?? [],
81
- mentions: m.mentions ?? [],
82
- ...(m.reactions ? { reactions: m.reactions } : {}),
83
- replyTo: m.replyTo ?? null,
84
- replyToPosition: m.replyToPosition ?? null,
85
- ...(m.forwarded === true || m.forwardedFrom
86
- ? { forwarded: true }
87
- : {}),
88
- ...(m.forwardedFrom
89
- ? { forwardedFrom: m.forwardedFrom }
90
- : {}),
91
- status: 'sent',
92
- deleted: false,
93
- createdAt: m.createdAt ?? new Date().toISOString(),
94
- ...(m.contactCard ? { contactCard: m.contactCard } : {}),
95
- ...(m.metadata ? { metadata: m.metadata } : {}),
96
- };
97
- this.debouncer.add(payload.conversationId, message, payload.provenance ?? null);
98
- },
99
- onMessageDeleted: (payload) => {
100
- this.debouncer.removeMessage(payload.conversationId, payload.messageId);
101
- this.onMessageDeleted?.(payload);
102
- },
103
- onMessageUpdated: (payload) => {
104
- this.onMessageUpdated?.(payload);
105
- },
106
- onAgentContext: (ctx) => {
107
- this.onAgentContext?.(ctx);
108
- },
109
- onContactRequest: (payload) => {
110
- this.onContactRequest?.(payload);
111
- },
112
- onContactApproved: (payload) => {
113
- this.onContactApproved?.(payload);
114
- },
115
- onContactAdded: (payload) => {
116
- this.onContactAdded?.(payload);
117
- },
118
- onContactRemoved: (payload) => {
119
- this.onContactRemoved?.(payload);
120
- },
121
- onConversationUpdated: (payload) => {
122
- this.onConversationUpdated?.(payload);
123
- },
124
- onConnected: () => {
125
- // Reset backoff is handled internally by CanonStream
126
- const shouldCatchUp = this.hasConnectedOnce;
127
- this.hasConnectedOnce = true;
128
- this.onConnected?.();
129
- if (shouldCatchUp) {
130
- this.queueReplayCatchup();
131
- }
132
- },
133
- onDisconnected: () => {
134
- this.onDisconnected?.();
135
- },
136
- onReplayExpired: (payload) => {
137
- console.error(`[canon-sdk] SSE replay window expired${payload.lastAvailableId ? ` (oldest available event: ${payload.lastAvailableId})` : ''} — catching up over REST`);
138
- this.queueReplayCatchup();
139
- },
140
- onError: (err) => {
141
- this.logSseError(err);
142
- },
143
- },
144
- });
145
- }
146
- queueReplayCatchup() {
147
- this.replayCatchupInFlight ??= this.runReplayCatchup().finally(() => {
148
- this.replayCatchupInFlight = null;
149
- });
150
- }
151
- hasSeenInboundMessage(conversationId, messageId) {
152
- return this.recentInboundMessageIds.has(`${conversationId}:${messageId}`);
153
- }
154
- recordSeenInboundMessage(conversationId, messageId, createdAtMs) {
155
- const now = Date.now();
156
- this.recentInboundMessageIds.set(`${conversationId}:${messageId}`, now);
157
- const effectiveTimestamp = createdAtMs > 0 ? createdAtMs : now;
158
- const previous = this.lastInboundMessageAtByConversation.get(conversationId) ?? 0;
159
- if (effectiveTimestamp > previous) {
160
- this.lastInboundMessageAtByConversation.set(conversationId, effectiveTimestamp);
161
- }
162
- this.pruneRecentInboundMessageIds(now);
163
- }
164
- pruneRecentInboundMessageIds(now = Date.now()) {
165
- const cutoff = now - RECENT_INBOUND_TTL_MS;
166
- for (const [key, seenAt] of this.recentInboundMessageIds) {
167
- if (seenAt < cutoff) {
168
- this.recentInboundMessageIds.delete(key);
169
- }
170
- }
171
- while (this.recentInboundMessageIds.size > MAX_RECENT_INBOUND_MESSAGE_IDS) {
172
- const oldestKey = this.recentInboundMessageIds.keys().next().value;
173
- if (!oldestKey)
174
- break;
175
- this.recentInboundMessageIds.delete(oldestKey);
176
- }
177
- }
178
- /**
179
- * REST catch-up after `replay.expired`: the stream service evicted our
180
- * cursor, so messages in the gap were silently dropped. Fetch the newest
181
- * page per conversation and feed unseen inbound messages through the normal
182
- * debouncer path (same entry point as SSE delivery, same id dedupe).
183
- *
184
- * Lower bound per conversation: the in-memory last-seen inbound timestamp,
185
- * falling back to this manager's construction time for conversations with
186
- * no prior inbound traffic — anything older predates this process and may
187
- * already have been handled by a previous run. For the same reason the
188
- * catch-up is NOT wired on initial connect: with no durable cursor, a fresh
189
- * process would re-fire turns for messages an earlier run already answered.
190
- */
191
- async runReplayCatchup() {
192
- const apiClient = this.apiClient;
193
- if (!apiClient) {
194
- console.error('[canon-sdk] Replay catch-up skipped — no API client available');
195
- return;
196
- }
197
- try {
198
- const conversations = await apiClient.getConversations();
199
- let recovered = 0;
200
- await Promise.all(conversations.map(async (conversation) => {
201
- try {
202
- const page = await apiClient.getMessagesPage(conversation.id, REPLAY_CATCHUP_PAGE_LIMIT);
203
- const lowerBoundMs = this.lastInboundMessageAtByConversation.get(conversation.id)
204
- ?? this.replaySyncStartedAt;
205
- const candidates = [...(page.messages ?? [])]
206
- .filter((message) => !message.deleted)
207
- .sort((a, b) => messageCreatedAtMs(a.createdAt) - messageCreatedAtMs(b.createdAt));
208
- for (const message of candidates) {
209
- if (!this.running)
210
- return;
211
- if (message.senderId === this.agentId)
212
- continue;
213
- const createdAtMs = messageCreatedAtMs(message.createdAt);
214
- // Use a strict lower bound of `< lowerBoundMs` (not `<=`): a
215
- // gap-dropped message can share the same createdAt millisecond as
216
- // the last message seen over SSE (server timestamps collide under
217
- // bursts — exactly the scenario catch-up targets). The id-dedupe on
218
- // the next line suppresses the already-delivered boundary message,
219
- // so excluding by `<=` would only drop never-seen same-ms peers.
220
- if (!createdAtMs || createdAtMs < lowerBoundMs)
221
- continue;
222
- if (this.hasSeenInboundMessage(conversation.id, message.id))
223
- continue;
224
- this.recordSeenInboundMessage(conversation.id, message.id, createdAtMs);
225
- const dispatch = await shouldDispatchInboundMessage(conversation.id, this.agentId, message, {
226
- conversationType: conversation.type,
227
- behavior: page.behavior ?? null,
228
- });
229
- if (!dispatch)
230
- continue;
231
- this.debouncer.add(conversation.id, message, null);
232
- recovered += 1;
233
- }
234
- }
235
- catch (err) {
236
- console.error(`[canon-sdk] Replay catch-up failed for ${conversation.id}:`, err instanceof Error ? err.message : err);
237
- }
238
- }));
239
- if (recovered > 0) {
240
- console.error(`[canon-sdk] Replay catch-up recovered ${recovered} missed message(s)`);
241
- }
242
- }
243
- catch (err) {
244
- console.error('[canon-sdk] Replay catch-up failed:', err instanceof Error ? err.message : err);
245
- }
246
- }
247
- logSseError(err) {
248
- const code = err.code;
249
- const key = `${typeof code === 'string' ? code : 'generic'}:${err.message}`;
250
- const now = Date.now();
251
- if (this.lastSseErrorKey === key && now - this.lastSseErrorAt < 60_000) {
252
- this.suppressedSseErrorCount += 1;
253
- return;
254
- }
255
- if (this.suppressedSseErrorCount > 0) {
256
- console.error(`[canon-sdk] SSE error repeated ${this.suppressedSseErrorCount} more time${this.suppressedSseErrorCount === 1 ? '' : 's'}`);
257
- this.suppressedSseErrorCount = 0;
258
- }
259
- this.lastSseErrorKey = key;
260
- this.lastSseErrorAt = now;
261
- console.error('[canon-sdk] SSE error:', err.message);
262
- }
263
- setOnAgentContext(cb) {
264
- this.onAgentContext = cb;
265
- }
266
- setContactRequestHandlers(handlers) {
267
- this.onContactRequest = handlers.onContactRequest ?? null;
268
- this.onContactApproved = handlers.onContactApproved ?? null;
269
- }
270
- setContactGraphHandlers(handlers) {
271
- this.onContactAdded = handlers.onContactAdded ?? null;
272
- this.onContactRemoved = handlers.onContactRemoved ?? null;
273
- }
274
- setConversationUpdatedHandler(cb) {
275
- this.onConversationUpdated = cb;
276
- }
277
- setMessageUpdatedHandler(cb) {
278
- this.onMessageUpdated = cb;
279
- }
280
- setMessageDeletedHandler(cb) {
281
- this.onMessageDeleted = cb;
282
- }
283
- setConnectionHandlers(handlers) {
284
- this.onConnected = handlers.onConnected ?? null;
285
- this.onDisconnected = handlers.onDisconnected ?? null;
286
- }
287
- async start() {
288
- this.running = true;
289
- await this.stream.start();
290
- }
291
- stop() {
292
- this.running = false;
293
- this.stream.stop();
294
- }
295
- }
@@ -1,29 +0,0 @@
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;
@@ -1,22 +0,0 @@
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
- }
@@ -1,9 +0,0 @@
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>;
@@ -1,25 +0,0 @@
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
- }