@canonmsg/agent-sdk 10.3.0 → 10.4.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/dist/attached-session.js +11 -2
- package/dist/canon-agent.d.ts +1 -0
- package/dist/canon-agent.js +81 -23
- package/dist/media.d.ts +6 -0
- package/dist/media.js +4 -1
- package/package.json +2 -2
package/dist/attached-session.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { buildChunkedMessagePartMetadata, CanonClient, CanonStream, createAttachedNativeSession, createRuntimeHeartbeat, createRuntimeStatePublisher, initRTDBAuth, resolveCanonRuntimeConnection, splitTextByUtf8Bytes, verifyCanonRuntimeConnection, } from '@canonmsg/core';
|
|
2
|
+
import { buildChunkedMessagePartMetadata, buildCanonGroupContext, buildCompactGroupContextLines, CanonClient, CanonStream, createAttachedNativeSession, createRuntimeHeartbeat, createRuntimeStatePublisher, initRTDBAuth, resolveCanonRuntimeConnection, splitTextByUtf8Bytes, verifyCanonRuntimeConnection, } from '@canonmsg/core';
|
|
3
3
|
/**
|
|
4
4
|
* Connect one existing native session to one existing Canon conversation.
|
|
5
5
|
* This is account-level participation, not a second group member. The caller
|
|
@@ -47,6 +47,8 @@ export function createCanonAttachedSession(options) {
|
|
|
47
47
|
const reportedHistory = new Set();
|
|
48
48
|
let publicationFailed = false;
|
|
49
49
|
let historyIncomplete = false;
|
|
50
|
+
let currentConversation = null;
|
|
51
|
+
let currentOwnerId;
|
|
50
52
|
let inputError = null;
|
|
51
53
|
let observedTurnId;
|
|
52
54
|
let turnUpdatedAt = null;
|
|
@@ -183,6 +185,7 @@ export function createCanonAttachedSession(options) {
|
|
|
183
185
|
if (stopped)
|
|
184
186
|
return false;
|
|
185
187
|
const conversation = conversations.find((entry) => entry.id === binding.conversationId);
|
|
188
|
+
currentConversation = conversation ?? null;
|
|
186
189
|
if (!conversation?.memberIds.includes(binding.agentId)) {
|
|
187
190
|
detach('conversation-membership-lost');
|
|
188
191
|
return false;
|
|
@@ -226,9 +229,11 @@ export function createCanonAttachedSession(options) {
|
|
|
226
229
|
await notice(payload, inputError);
|
|
227
230
|
return;
|
|
228
231
|
}
|
|
232
|
+
const roster = buildCanonGroupContext({ conversation: currentConversation, messages: [message], agentId: binding.agentId, ownerId: currentOwnerId });
|
|
233
|
+
const rosterLines = roster ? buildCompactGroupContextLines(roster, 'initial') : [];
|
|
229
234
|
const result = await core.submit({
|
|
230
235
|
messageId: message.id,
|
|
231
|
-
text: `Canon message from ${message.senderName ?? message.senderId} (${message.senderType}):\n\n${message.text}
|
|
236
|
+
text: [...rosterLines, `Canon message from ${message.senderName ?? message.senderId} (${message.senderType}):\n\n${message.text}`].join('\n\n'),
|
|
232
237
|
...(payload.replyAuthority ? { replyAuthority: payload.replyAuthority } : {}),
|
|
233
238
|
});
|
|
234
239
|
if (stopped)
|
|
@@ -375,6 +380,7 @@ export function createCanonAttachedSession(options) {
|
|
|
375
380
|
if (!conversation || !conversation.memberIds.includes(binding.agentId)) {
|
|
376
381
|
throw new Error('The agent is not a member of the specified Canon conversation');
|
|
377
382
|
}
|
|
383
|
+
currentConversation = conversation;
|
|
378
384
|
const initial = await client.getMessagesPage(binding.conversationId, 100);
|
|
379
385
|
if (stopped)
|
|
380
386
|
return;
|
|
@@ -414,6 +420,7 @@ export function createCanonAttachedSession(options) {
|
|
|
414
420
|
const handler = {
|
|
415
421
|
onMessage: (payload) => track(receive(payload), 'receive'),
|
|
416
422
|
onAgentContext: (context) => {
|
|
423
|
+
currentOwnerId = context.ownerId;
|
|
417
424
|
if (context.agentId !== binding.agentId) {
|
|
418
425
|
report(new Error('Canon stream context belongs to a different agent'), 'stream-identity');
|
|
419
426
|
detach('stream-identity-mismatch');
|
|
@@ -422,6 +429,8 @@ export function createCanonAttachedSession(options) {
|
|
|
422
429
|
onConversationUpdated: (payload) => {
|
|
423
430
|
if (stopped || payload.conversationId !== binding.conversationId)
|
|
424
431
|
return;
|
|
432
|
+
if (currentConversation)
|
|
433
|
+
currentConversation = { ...currentConversation, ...payload.changes };
|
|
425
434
|
const members = payload.changes.memberIds;
|
|
426
435
|
if ((Array.isArray(members) && !members.includes(binding.agentId))
|
|
427
436
|
|| payload.membershipChange?.removedMemberIds.includes(binding.agentId)
|
package/dist/canon-agent.d.ts
CHANGED
|
@@ -90,6 +90,7 @@ export declare class CanonAgent {
|
|
|
90
90
|
private readonly activeAbortControllers;
|
|
91
91
|
private readonly activeTurns;
|
|
92
92
|
private readonly conversationMemberIds;
|
|
93
|
+
private readonly conversationMembershipRevisions;
|
|
93
94
|
private readonly pendingMembershipChanges;
|
|
94
95
|
private readonly typingSignals;
|
|
95
96
|
private sseConnectedLogged;
|
package/dist/canon-agent.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApprovalManager, RuntimeRequestManager, runtimeInputDescriptor, runtimeCardDescriptor, CanonClient, ControlChannelPoller, buildCanonTurnContextV2, buildCanonGroupContext, buildParticipationHistorySnapshot, createTurnOutputController, createRuntimeStatePublisher, createRuntimeHeartbeat, createTypingStatusPublisher, diffCanonMemberIds, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildRuntimeInputOutcome, initRTDBAuth, isChunkedSendMessageError, normalizeRuntimeCommandDescriptors, normalizeTurnMetadata, normalizeTurnVerbosityConversationType, reportNoReplyOutcome, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, resolveTurnVerbosity, selectActiveSelfContexts, renderCanonHostInboundContent, resolveCanonRuntimeConnection, sendMessageWithRetryChunked, shouldPublishTurnTrail, splitTextByUtf8Bytes, verifyCanonRuntimeConnection, } from '@canonmsg/core';
|
|
1
|
+
import { ApprovalManager, RuntimeRequestManager, runtimeInputDescriptor, runtimeCardDescriptor, CanonClient, ControlChannelPoller, buildCanonTurnContextV2, buildCanonGroupContext, buildParticipationHistorySnapshot, createTurnOutputController, createRuntimeStatePublisher, createRuntimeHeartbeat, createTypingStatusPublisher, diffCanonMemberIds, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildRuntimeInputOutcome, initRTDBAuth, isChunkedSendMessageError, normalizeRuntimeCommandDescriptors, normalizeTurnMetadata, normalizeTurnVerbosityConversationType, reportNoReplyOutcome, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, resolveTurnVerbosity, resolveConversationPolicyScope, selectActiveSelfContexts, renderCanonHostInboundContent, resolveCanonRuntimeConnection, sendMessageWithRetryChunked, shouldPublishTurnTrail, splitTextByUtf8Bytes, verifyCanonRuntimeConnection, } from '@canonmsg/core';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { Debouncer } from './debouncer.js';
|
|
4
4
|
import { DEFAULT_RUNTIME_INPUT_TIMEOUT_MS, RUNTIME_INPUT_ID_PATTERN, buildRuntimeCardCreateArgs, normalizeResponseUserId, resolveRuntimeCardRouting, } from './runtime-card.js';
|
|
@@ -143,6 +143,9 @@ const STANDARD_PRIMITIVE_COMMANDS = {
|
|
|
143
143
|
function sleep(ms) {
|
|
144
144
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
145
145
|
}
|
|
146
|
+
function knownMembershipRevision(value) {
|
|
147
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
148
|
+
}
|
|
146
149
|
function sleepWithAbort(ms, signal) {
|
|
147
150
|
if (signal.aborted)
|
|
148
151
|
return Promise.reject(createTurnAbortError());
|
|
@@ -325,6 +328,7 @@ export class CanonAgent {
|
|
|
325
328
|
activeAbortControllers = new Map();
|
|
326
329
|
activeTurns = new Map();
|
|
327
330
|
conversationMemberIds = new Map();
|
|
331
|
+
conversationMembershipRevisions = new Map();
|
|
328
332
|
pendingMembershipChanges = new Map();
|
|
329
333
|
typingSignals;
|
|
330
334
|
sseConnectedLogged = false;
|
|
@@ -964,24 +968,43 @@ export class CanonAgent {
|
|
|
964
968
|
}
|
|
965
969
|
rememberConversationMembers(conversations) {
|
|
966
970
|
for (const conversation of conversations) {
|
|
971
|
+
const revision = knownMembershipRevision(conversation.membershipRevision);
|
|
972
|
+
const currentRevision = this.conversationMembershipRevisions.get(conversation.id);
|
|
973
|
+
if (revision !== undefined && currentRevision !== undefined && revision < currentRevision)
|
|
974
|
+
continue;
|
|
967
975
|
this.conversationMemberIds.set(conversation.id, [...(conversation.memberIds ?? [])]);
|
|
976
|
+
if (revision !== undefined)
|
|
977
|
+
this.conversationMembershipRevisions.set(conversation.id, revision);
|
|
978
|
+
else
|
|
979
|
+
this.conversationMembershipRevisions.delete(conversation.id);
|
|
968
980
|
}
|
|
969
981
|
}
|
|
970
982
|
handleConversationUpdated(payload) {
|
|
971
983
|
const rawMemberIds = payload.changes.memberIds;
|
|
972
984
|
if (!Array.isArray(rawMemberIds))
|
|
973
985
|
return;
|
|
986
|
+
const revision = knownMembershipRevision(payload.changes.membershipRevision);
|
|
987
|
+
const currentRevision = this.conversationMembershipRevisions.get(payload.conversationId);
|
|
988
|
+
if (revision !== undefined && currentRevision !== undefined && revision < currentRevision)
|
|
989
|
+
return;
|
|
974
990
|
const memberIds = rawMemberIds.filter((id) => typeof id === 'string');
|
|
975
991
|
const hadPreviousMemberIds = this.conversationMemberIds.has(payload.conversationId);
|
|
976
992
|
const previousMemberIds = this.conversationMemberIds.get(payload.conversationId) ?? [];
|
|
977
993
|
const membershipChange = payload.membershipChange
|
|
978
994
|
?? (hadPreviousMemberIds ? diffCanonMemberIds(previousMemberIds, memberIds) : null);
|
|
979
995
|
this.conversationMemberIds.set(payload.conversationId, memberIds);
|
|
996
|
+
if (revision !== undefined)
|
|
997
|
+
this.conversationMembershipRevisions.set(payload.conversationId, revision);
|
|
998
|
+
else
|
|
999
|
+
this.conversationMembershipRevisions.delete(payload.conversationId);
|
|
980
1000
|
if (membershipChange) {
|
|
981
1001
|
this.pendingMembershipChanges.set(payload.conversationId, membershipChange);
|
|
982
1002
|
}
|
|
983
1003
|
if (this.agentId && !memberIds.includes(this.agentId)) {
|
|
984
1004
|
this.cachedConversationIds = this.cachedConversationIds.filter((id) => id !== payload.conversationId);
|
|
1005
|
+
this.abortActiveTurns(payload.conversationId);
|
|
1006
|
+
this.sessionManager?.dropQueued(payload.conversationId);
|
|
1007
|
+
this.pendingMembershipChanges.delete(payload.conversationId);
|
|
985
1008
|
}
|
|
986
1009
|
else {
|
|
987
1010
|
this.rememberConversationId(payload.conversationId);
|
|
@@ -1254,7 +1277,53 @@ export class CanonAgent {
|
|
|
1254
1277
|
async executeHandler(conversationId, messages, session, provenanceByMessageId) {
|
|
1255
1278
|
if (!this.handler)
|
|
1256
1279
|
return;
|
|
1280
|
+
// Message rediscovery can precede the SSE roster snapshot after admission.
|
|
1281
|
+
// Refresh before trusting cached exclusion, accepting queued input, or
|
|
1282
|
+
// freezing output policy. Reuse this same snapshot for handler context.
|
|
1283
|
+
let membersBeforeRefresh = this.conversationMemberIds.get(conversationId);
|
|
1284
|
+
let conversations;
|
|
1285
|
+
try {
|
|
1286
|
+
conversations = await this.apiClient.getConversations();
|
|
1287
|
+
}
|
|
1288
|
+
catch {
|
|
1289
|
+
// Realtime delivery has already deduplicated these IDs. Retry this read
|
|
1290
|
+
// here once; throwing does not arrange replay or requeue the input.
|
|
1291
|
+
await sleep(100);
|
|
1292
|
+
membersBeforeRefresh = this.conversationMemberIds.get(conversationId);
|
|
1293
|
+
try {
|
|
1294
|
+
conversations = await this.apiClient.getConversations();
|
|
1295
|
+
}
|
|
1296
|
+
catch (error) {
|
|
1297
|
+
console.error(`[canon-sdk] Pre-turn conversation refresh failed for ${conversationId}:`, error);
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
let conversation = conversations.find((c) => c.id === conversationId);
|
|
1302
|
+
if (!conversation)
|
|
1303
|
+
return;
|
|
1304
|
+
const membersDuringRefresh = this.conversationMemberIds.get(conversationId);
|
|
1305
|
+
const currentRevision = this.conversationMembershipRevisions.get(conversationId);
|
|
1306
|
+
const refreshedRevision = knownMembershipRevision(conversation.membershipRevision);
|
|
1307
|
+
const comparableRevisions = currentRevision !== undefined && refreshedRevision !== undefined;
|
|
1308
|
+
const keepCachedRoster = comparableRevisions
|
|
1309
|
+
? currentRevision > refreshedRevision
|
|
1310
|
+
: membersDuringRefresh !== membersBeforeRefresh;
|
|
1311
|
+
if (membersDuringRefresh && keepCachedRoster) {
|
|
1312
|
+
// Prefer the higher known revision. For legacy unversioned snapshots,
|
|
1313
|
+
// retain an event received while the request was pending.
|
|
1314
|
+
conversation = { ...conversation, memberIds: membersDuringRefresh, membershipRevision: currentRevision };
|
|
1315
|
+
}
|
|
1316
|
+
else {
|
|
1317
|
+
this.rememberConversationMembers([conversation]);
|
|
1318
|
+
}
|
|
1319
|
+
const currentMembers = this.conversationMemberIds.get(conversationId);
|
|
1320
|
+
if (currentMembers && this.agentId && !currentMembers.includes(this.agentId))
|
|
1321
|
+
return;
|
|
1322
|
+
this.rememberConversationId(conversationId);
|
|
1257
1323
|
await this.markQueuedMessagesAccepted(conversationId, messages);
|
|
1324
|
+
const acceptedMembers = this.conversationMemberIds.get(conversationId);
|
|
1325
|
+
if (acceptedMembers && this.agentId && !acceptedMembers.includes(this.agentId))
|
|
1326
|
+
return;
|
|
1258
1327
|
const turnId = randomUUID();
|
|
1259
1328
|
const turnOpenedAt = Date.now();
|
|
1260
1329
|
let turnState = 'thinking';
|
|
@@ -1316,19 +1385,14 @@ export class CanonAgent {
|
|
|
1316
1385
|
: {}),
|
|
1317
1386
|
})).catch(() => { });
|
|
1318
1387
|
};
|
|
1319
|
-
//
|
|
1320
|
-
//
|
|
1321
|
-
//
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
// before the first publish of any kind.
|
|
1328
|
-
//
|
|
1329
|
-
// Re-deriving it mid-turn is what must never happen: a turn that started
|
|
1330
|
-
// quiet and finished verbose would emit a trail nothing narrated.
|
|
1331
|
-
const inboundConversationType = normalizeTurnVerbosityConversationType(provenanceByMessageId?.get(triggeringMessageId ?? '')?.conversation?.type);
|
|
1388
|
+
// Resolve once from the refreshed roster before constructing the output
|
|
1389
|
+
// controller or publishing the first thinking seed. Keep it fixed for the
|
|
1390
|
+
// turn so the final trail agrees with what was actually narrated.
|
|
1391
|
+
const inboundProvenance = provenanceByMessageId?.get(triggeringMessageId ?? '')?.conversation;
|
|
1392
|
+
const inboundRoster = this.conversationMemberIds.get(conversationId) ?? inboundProvenance?.memberCount;
|
|
1393
|
+
const inboundConversationType = inboundRoster != null
|
|
1394
|
+
? resolveConversationPolicyScope(inboundRoster)
|
|
1395
|
+
: normalizeTurnVerbosityConversationType(inboundProvenance?.type);
|
|
1332
1396
|
const turnVerbosity = resolveTurnVerbosity({
|
|
1333
1397
|
configured: selectConfiguredTurnVerbosity(this.options.turnVerbosity, inboundConversationType),
|
|
1334
1398
|
conversationType: inboundConversationType,
|
|
@@ -1412,12 +1476,6 @@ export class CanonAgent {
|
|
|
1412
1476
|
if (this.sessionManager && session) {
|
|
1413
1477
|
this.sessionManager.seedHistory(conversationId, history);
|
|
1414
1478
|
}
|
|
1415
|
-
// Get conversation info
|
|
1416
|
-
const conversations = await this.apiClient.getConversations();
|
|
1417
|
-
this.rememberConversationMembers(conversations);
|
|
1418
|
-
const conversation = conversations.find((c) => c.id === conversationId);
|
|
1419
|
-
if (!conversation)
|
|
1420
|
-
return;
|
|
1421
1479
|
// Build reply functions
|
|
1422
1480
|
const replyFinal = async (text, options) => {
|
|
1423
1481
|
throwIfAborted();
|
|
@@ -1615,7 +1673,7 @@ export class CanonAgent {
|
|
|
1615
1673
|
? resolveRuntimeProvenance({
|
|
1616
1674
|
provenance: provenanceByMessageId?.get(latestMessage.id) ?? null,
|
|
1617
1675
|
conversationId,
|
|
1618
|
-
conversationType: conversation.
|
|
1676
|
+
conversationType: resolveConversationPolicyScope(conversation.memberIds),
|
|
1619
1677
|
memberCount: conversation.memberIds.length,
|
|
1620
1678
|
senderId: latestMessage.senderId,
|
|
1621
1679
|
senderName: latestMessage.senderName ?? latestMessage.senderId,
|
|
@@ -1628,7 +1686,7 @@ export class CanonAgent {
|
|
|
1628
1686
|
})
|
|
1629
1687
|
: resolveRuntimeProvenance({
|
|
1630
1688
|
conversationId,
|
|
1631
|
-
conversationType: conversation.
|
|
1689
|
+
conversationType: resolveConversationPolicyScope(conversation.memberIds),
|
|
1632
1690
|
memberCount: conversation.memberIds.length,
|
|
1633
1691
|
senderId: '',
|
|
1634
1692
|
senderType: 'human',
|
|
@@ -1663,7 +1721,7 @@ export class CanonAgent {
|
|
|
1663
1721
|
content: latestMessage ? renderCanonHostInboundContent(latestMessage) : '[Empty message]',
|
|
1664
1722
|
conversationId,
|
|
1665
1723
|
participantContext: {
|
|
1666
|
-
conversationType: conversation.
|
|
1724
|
+
conversationType: resolveConversationPolicyScope(conversation.memberIds),
|
|
1667
1725
|
memberCount: conversation.memberIds.length,
|
|
1668
1726
|
senderType: latestMessage?.senderType ?? provenance.sender.type,
|
|
1669
1727
|
senderName: latestMessage?.senderName
|
package/dist/media.d.ts
CHANGED
|
@@ -22,6 +22,12 @@ export interface UploadMediaFileOptions {
|
|
|
22
22
|
signal?: AbortSignal;
|
|
23
23
|
}
|
|
24
24
|
export interface ReplyWithFileOptions extends Omit<SendMessageOptions, 'attachments' | 'contentType'>, UploadMediaFileOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Optional local policy check after upload/finalization, immediately before
|
|
27
|
+
* publishing the attachment. Returning false withholds the message. Automatic
|
|
28
|
+
* artifact routing uses this to recheck its current audience.
|
|
29
|
+
*/
|
|
30
|
+
canPublish?: () => boolean;
|
|
25
31
|
}
|
|
26
32
|
export interface MaterializedCanonAttachment extends MediaAttachment {
|
|
27
33
|
index: number;
|
package/dist/media.js
CHANGED
|
@@ -396,7 +396,7 @@ export async function uploadMediaFile(client, conversationId, filePath, options)
|
|
|
396
396
|
return uploaded;
|
|
397
397
|
}
|
|
398
398
|
export async function sendMediaFileMessage(client, conversationId, filePath, text = '', options) {
|
|
399
|
-
const { fileName, mimeType, durationMs, fetchImpl, signal, ...sendOptions } = options ?? {};
|
|
399
|
+
const { fileName, mimeType, durationMs, fetchImpl, signal, canPublish, ...sendOptions } = options ?? {};
|
|
400
400
|
const uploaded = await uploadMediaFile(client, conversationId, filePath, {
|
|
401
401
|
...(fileName ? { fileName } : {}),
|
|
402
402
|
...(mimeType ? { mimeType } : {}),
|
|
@@ -405,6 +405,9 @@ export async function sendMediaFileMessage(client, conversationId, filePath, tex
|
|
|
405
405
|
...(signal ? { signal } : {}),
|
|
406
406
|
});
|
|
407
407
|
signal?.throwIfAborted();
|
|
408
|
+
if (canPublish && !canPublish()) {
|
|
409
|
+
throw new Error('Canon media publication withheld by current routing policy');
|
|
410
|
+
}
|
|
408
411
|
return client.sendMessage(conversationId, text, {
|
|
409
412
|
...sendOptions,
|
|
410
413
|
contentType: uploaded.attachment.kind,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/agent-sdk",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.4.0",
|
|
4
4
|
"description": "Canon Agent SDK — build AI agents that participate in Canon conversations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"node": ">=18.0.0"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@canonmsg/core": "^12.
|
|
31
|
+
"@canonmsg/core": "^12.5.0"
|
|
32
32
|
},
|
|
33
33
|
"publishConfig": {
|
|
34
34
|
"access": "public"
|