@canonmsg/agent-sdk 1.4.0 → 1.5.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 +1 -0
- package/dist/canon-agent.d.ts +8 -0
- package/dist/canon-agent.js +230 -8
- package/dist/debouncer.d.ts +4 -3
- package/dist/debouncer.js +15 -2
- package/dist/index.d.ts +6 -6
- package/dist/index.js +2 -2
- package/dist/media.d.ts +6 -1
- package/dist/media.js +26 -1
- package/dist/realtime.js +1 -1
- package/dist/types.d.ts +34 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -130,6 +130,7 @@ Current rules of thumb:
|
|
|
130
130
|
- `selectionPolicy: 'required_explicit'` means Canon should require the user to make a choice instead of silently inheriting a default
|
|
131
131
|
- `workspaceRoots` and `writableRoots` document allowed roots and let Canon group project choices. Canon still stores the selected concrete `workspaceId`; it does not send arbitrary root-relative paths to generic SDK agents.
|
|
132
132
|
- Publishing a descriptor does not automatically make your SDK agent enforce those controls. If you advertise model, workspace, execution mode, or runtime-native controls, your runtime must actually read and apply the stored config.
|
|
133
|
+
- Message handlers receive `ctx.provenance`, a Canon-computed sender/conversation context for the latest inbound message in the batch. Use it when your runtime wants owner-only tools, group mention policy, or self-context-aware behavior; Canon does not impose a sandbox on SDK agents.
|
|
133
134
|
|
|
134
135
|
## Delivery Modes
|
|
135
136
|
|
package/dist/canon-agent.d.ts
CHANGED
|
@@ -42,6 +42,9 @@ export declare class CanonAgent {
|
|
|
42
42
|
private readonly reachOutInFlight;
|
|
43
43
|
private agentId;
|
|
44
44
|
private agentContext;
|
|
45
|
+
private approvalManager;
|
|
46
|
+
private approvalManagerAgentId;
|
|
47
|
+
private approvalManagerOwnerId;
|
|
45
48
|
private cachedConversationIds;
|
|
46
49
|
private running;
|
|
47
50
|
private runtimeHeartbeatTimer;
|
|
@@ -49,9 +52,12 @@ export declare class CanonAgent {
|
|
|
49
52
|
private readonly lastSeenSignal;
|
|
50
53
|
private readonly primitiveRequestDedupe;
|
|
51
54
|
private readonly activeAbortControllers;
|
|
55
|
+
private readonly activeTurns;
|
|
52
56
|
private readonly conversationMemberIds;
|
|
53
57
|
private readonly pendingMembershipChanges;
|
|
54
58
|
constructor(options: CanonAgentOptions);
|
|
59
|
+
private ensureApprovalManager;
|
|
60
|
+
private filterApprovalReplyMessages;
|
|
55
61
|
on(event: 'message', handler: MessageHandler): void;
|
|
56
62
|
on(event: 'contactRequest', handler: ContactRequestHandler): void;
|
|
57
63
|
on(event: 'contactApproved', handler: ContactRequestHandler): void;
|
|
@@ -128,6 +134,8 @@ export declare class CanonAgent {
|
|
|
128
134
|
private clearRuntimePrimitiveRequest;
|
|
129
135
|
private prunePrimitiveRequestDedupe;
|
|
130
136
|
private handleRuntimeSignal;
|
|
137
|
+
private firstActiveTurn;
|
|
138
|
+
private publishAcceptedRuntimeSignal;
|
|
131
139
|
private abortActiveTurns;
|
|
132
140
|
private resolveBatchDeliveryIntent;
|
|
133
141
|
private notifyMessageInterrupt;
|
package/dist/canon-agent.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { CanonClient, buildCanonGroupContext, createRuntimeStatePublisher, diffCanonMemberIds, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, initRTDBAuth, rtdbRead, rtdbWrite, normalizeTurnMetadata, reachOutToCanonContact, resolveMessageActiveSelfContextId, selectActiveSelfContexts, } from '@canonmsg/core';
|
|
1
|
+
import { ApprovalManager, CanonClient, buildCanonGroupContext, createRuntimeStatePublisher, diffCanonMemberIds, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, initRTDBAuth, rtdbRead, rtdbWrite, normalizeTurnMetadata, reachOutToCanonContact, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, selectActiveSelfContexts, } from '@canonmsg/core';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { AuthManager } from './auth.js';
|
|
4
4
|
import { Debouncer } from './debouncer.js';
|
|
5
|
-
import { materializeMessageMedia, sendMediaFileMessage, uploadMediaFile, } from './media.js';
|
|
5
|
+
import { materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, uploadMediaFile, } from './media.js';
|
|
6
6
|
import { SessionManager } from './session-manager.js';
|
|
7
7
|
const AGENT_RUNTIME_HEARTBEAT_MS = 30_000;
|
|
8
8
|
const RUNTIME_PRIMITIVE_DEDUPE_TTL_MS = 5 * 60 * 1000;
|
|
@@ -168,6 +168,19 @@ function normalizeRuntimeActivityItem(item) {
|
|
|
168
168
|
updatedAt: item.updatedAt || Date.now(),
|
|
169
169
|
};
|
|
170
170
|
}
|
|
171
|
+
function createTurnAbortError() {
|
|
172
|
+
const error = new Error('Canon turn was interrupted before reply delivery.');
|
|
173
|
+
error.name = 'AbortError';
|
|
174
|
+
return error;
|
|
175
|
+
}
|
|
176
|
+
function isAbortLikeError(error) {
|
|
177
|
+
if (!error || typeof error !== 'object')
|
|
178
|
+
return false;
|
|
179
|
+
const record = error;
|
|
180
|
+
if (record.name === 'AbortError' || record.code === 'ABORT_ERR')
|
|
181
|
+
return true;
|
|
182
|
+
return typeof record.message === 'string' && /\babort(?:ed)?\b/i.test(record.message);
|
|
183
|
+
}
|
|
171
184
|
export class CanonAgent {
|
|
172
185
|
options;
|
|
173
186
|
apiClient;
|
|
@@ -192,6 +205,9 @@ export class CanonAgent {
|
|
|
192
205
|
reachOutInFlight = new Map();
|
|
193
206
|
agentId = null;
|
|
194
207
|
agentContext = null;
|
|
208
|
+
approvalManager = null;
|
|
209
|
+
approvalManagerAgentId = null;
|
|
210
|
+
approvalManagerOwnerId = null;
|
|
195
211
|
cachedConversationIds = [];
|
|
196
212
|
running = false;
|
|
197
213
|
runtimeHeartbeatTimer = null;
|
|
@@ -199,6 +215,7 @@ export class CanonAgent {
|
|
|
199
215
|
lastSeenSignal = new Map();
|
|
200
216
|
primitiveRequestDedupe = new Map();
|
|
201
217
|
activeAbortControllers = new Map();
|
|
218
|
+
activeTurns = new Map();
|
|
202
219
|
conversationMemberIds = new Map();
|
|
203
220
|
pendingMembershipChanges = new Map();
|
|
204
221
|
constructor(options) {
|
|
@@ -243,6 +260,47 @@ export class CanonAgent {
|
|
|
243
260
|
}
|
|
244
261
|
}
|
|
245
262
|
}
|
|
263
|
+
ensureApprovalManager(agentContext = this.agentContext) {
|
|
264
|
+
const agentId = agentContext?.agentId || this.agentId;
|
|
265
|
+
const ownerId = agentContext?.ownerId;
|
|
266
|
+
if (!agentId || !ownerId)
|
|
267
|
+
return null;
|
|
268
|
+
if (this.approvalManager
|
|
269
|
+
&& this.approvalManagerAgentId === agentId
|
|
270
|
+
&& this.approvalManagerOwnerId === ownerId) {
|
|
271
|
+
return this.approvalManager;
|
|
272
|
+
}
|
|
273
|
+
if (this.approvalManager?.pendingCount) {
|
|
274
|
+
return this.approvalManager;
|
|
275
|
+
}
|
|
276
|
+
this.approvalManager?.dispose();
|
|
277
|
+
this.approvalManager = new ApprovalManager(this.apiClient, agentId, ownerId);
|
|
278
|
+
this.approvalManagerAgentId = agentId;
|
|
279
|
+
this.approvalManagerOwnerId = ownerId;
|
|
280
|
+
return this.approvalManager;
|
|
281
|
+
}
|
|
282
|
+
filterApprovalReplyMessages(conversationId, messages) {
|
|
283
|
+
const manager = this.ensureApprovalManager();
|
|
284
|
+
if (!manager) {
|
|
285
|
+
return messages.filter((message) => {
|
|
286
|
+
const metadata = message.metadata;
|
|
287
|
+
return !(metadata && typeof metadata === 'object' && !Array.isArray(metadata)
|
|
288
|
+
&& metadata.type === 'approval_reply');
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return messages.filter((message) => {
|
|
292
|
+
const metadata = message.metadata;
|
|
293
|
+
const metadataRecord = metadata && typeof metadata === 'object' && !Array.isArray(metadata)
|
|
294
|
+
? metadata
|
|
295
|
+
: null;
|
|
296
|
+
const consumed = manager.handleMessage(conversationId, {
|
|
297
|
+
senderId: message.senderId,
|
|
298
|
+
...(typeof message.text === 'string' ? { text: message.text } : {}),
|
|
299
|
+
...(metadataRecord ? { metadata: metadataRecord } : {}),
|
|
300
|
+
});
|
|
301
|
+
return !consumed && metadataRecord?.type !== 'approval_reply';
|
|
302
|
+
});
|
|
303
|
+
}
|
|
246
304
|
on(event, handler) {
|
|
247
305
|
if (event === 'message') {
|
|
248
306
|
this.handler = handler;
|
|
@@ -397,9 +455,9 @@ export class CanonAgent {
|
|
|
397
455
|
this.agentId = agentId;
|
|
398
456
|
console.log(`[canon-sdk] Authenticated as ${agentId}`);
|
|
399
457
|
// 2. Wire debouncer to handler
|
|
400
|
-
this.debouncer.setCallback(async (conversationId, messages) => {
|
|
458
|
+
this.debouncer.setCallback(async (conversationId, messages, provenanceByMessageId) => {
|
|
401
459
|
this.rememberConversationId(conversationId);
|
|
402
|
-
await this.handleMessages(conversationId, messages);
|
|
460
|
+
await this.handleMessages(conversationId, messages, provenanceByMessageId);
|
|
403
461
|
});
|
|
404
462
|
// 3. Fetch conversations (used for delivery mode + session state)
|
|
405
463
|
let conversations = [];
|
|
@@ -423,6 +481,7 @@ export class CanonAgent {
|
|
|
423
481
|
// 3b. Fetch agent context (identity, owner, access level)
|
|
424
482
|
try {
|
|
425
483
|
this.agentContext = await this.apiClient.getAgentMe();
|
|
484
|
+
this.ensureApprovalManager(this.agentContext);
|
|
426
485
|
}
|
|
427
486
|
catch {
|
|
428
487
|
console.warn('[canon-sdk] Failed to fetch agent context — owner/access info unavailable');
|
|
@@ -448,6 +507,7 @@ export class CanonAgent {
|
|
|
448
507
|
const rtm = new RealtimeManager(this.options.apiKey, this.debouncer, agentId, this.options.streamUrl, this.apiClient);
|
|
449
508
|
rtm.setOnAgentContext((ctx) => {
|
|
450
509
|
this.agentContext = ctx;
|
|
510
|
+
this.ensureApprovalManager(ctx);
|
|
451
511
|
});
|
|
452
512
|
rtm.setContactRequestHandlers({
|
|
453
513
|
onContactRequest: (request) => {
|
|
@@ -832,6 +892,7 @@ export class CanonAgent {
|
|
|
832
892
|
await Promise.resolve(rtdbWrite(`/control/${conversationId}/${this.agentId}/signal`, null)).catch(() => { });
|
|
833
893
|
return;
|
|
834
894
|
}
|
|
895
|
+
const activeTurn = this.firstActiveTurn(conversationId);
|
|
835
896
|
const abortSignal = this.abortActiveTurns(conversationId);
|
|
836
897
|
const droppedMessages = signal === 'new_session'
|
|
837
898
|
? this.sessionManager?.resetSession(conversationId) ?? []
|
|
@@ -844,6 +905,10 @@ export class CanonAgent {
|
|
|
844
905
|
return Promise.resolve();
|
|
845
906
|
return this.apiClient.updateMessageDisposition(conversationId, message.id, 'rejected').catch(() => { });
|
|
846
907
|
}));
|
|
908
|
+
await this.publishAcceptedRuntimeSignal(conversationId, signal, activeTurn, {
|
|
909
|
+
hasActiveTurn: Boolean(abortSignal),
|
|
910
|
+
droppedCount: droppedMessages.length,
|
|
911
|
+
});
|
|
847
912
|
await Promise.resolve(handler?.({
|
|
848
913
|
conversationId,
|
|
849
914
|
signal: signal,
|
|
@@ -855,6 +920,39 @@ export class CanonAgent {
|
|
|
855
920
|
});
|
|
856
921
|
await Promise.resolve(rtdbWrite(`/control/${conversationId}/${this.agentId}/signal`, null)).catch(() => { });
|
|
857
922
|
}
|
|
923
|
+
firstActiveTurn(conversationId) {
|
|
924
|
+
const turns = this.activeTurns.get(conversationId);
|
|
925
|
+
if (!turns || turns.size === 0)
|
|
926
|
+
return null;
|
|
927
|
+
return turns.values().next().value ?? null;
|
|
928
|
+
}
|
|
929
|
+
async publishAcceptedRuntimeSignal(conversationId, signal, activeTurn, outcome) {
|
|
930
|
+
if (!this.agentId)
|
|
931
|
+
return;
|
|
932
|
+
const shouldPublishInterrupted = signal === 'interrupt'
|
|
933
|
+
|| signal === 'stop_and_drop'
|
|
934
|
+
|| outcome.hasActiveTurn
|
|
935
|
+
|| outcome.droppedCount > 0;
|
|
936
|
+
const runtimeState = shouldPublishInterrupted
|
|
937
|
+
? this.createRuntimeStatePublisher()
|
|
938
|
+
: null;
|
|
939
|
+
if (runtimeState) {
|
|
940
|
+
await Promise.resolve(runtimeState.writeTurnState(conversationId, {
|
|
941
|
+
turnId: activeTurn?.turnId ?? null,
|
|
942
|
+
state: 'interrupted',
|
|
943
|
+
queueDepth: this.sessionManager?.getQueueDepth(conversationId) ?? 0,
|
|
944
|
+
currentSpeakerId: this.agentId,
|
|
945
|
+
activeMessageIds: activeTurn?.activeMessageIds ?? [],
|
|
946
|
+
capabilities: this.buildRuntimeCapabilities(),
|
|
947
|
+
...(activeTurn?.openedAt ? { openedAt: activeTurn.openedAt } : {}),
|
|
948
|
+
completedAt: { '.sv': 'timestamp' },
|
|
949
|
+
})).catch(() => { });
|
|
950
|
+
}
|
|
951
|
+
await Promise.all([
|
|
952
|
+
this.apiClient.clearStreaming(conversationId).catch(() => { }),
|
|
953
|
+
this.apiClient.setTyping(conversationId, false).catch(() => { }),
|
|
954
|
+
]);
|
|
955
|
+
}
|
|
858
956
|
abortActiveTurns(conversationId) {
|
|
859
957
|
const controllers = this.activeAbortControllers.get(conversationId);
|
|
860
958
|
if (!controllers || controllers.size === 0)
|
|
@@ -898,7 +996,12 @@ export class CanonAgent {
|
|
|
898
996
|
}
|
|
899
997
|
return publisher;
|
|
900
998
|
}
|
|
901
|
-
async handleMessages(conversationId, messages) {
|
|
999
|
+
async handleMessages(conversationId, messages, provenanceByMessageId) {
|
|
1000
|
+
const actionableMessages = this.filterApprovalReplyMessages(conversationId, messages);
|
|
1001
|
+
if (actionableMessages.length === 0) {
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
messages = actionableMessages;
|
|
902
1005
|
if (!this.handler) {
|
|
903
1006
|
console.warn(`[canon-sdk] No message handler registered — messages for ${conversationId} dropped. Call agent.on('message', handler) before starting.`);
|
|
904
1007
|
return;
|
|
@@ -909,14 +1012,14 @@ export class CanonAgent {
|
|
|
909
1012
|
await this.notifyMessageInterrupt(conversationId, abortSignal);
|
|
910
1013
|
if (this.sessionManager) {
|
|
911
1014
|
await this.sessionManager.enqueue(conversationId, messages, async (session, newMessages) => {
|
|
912
|
-
await this.executeHandler(conversationId, newMessages, session);
|
|
1015
|
+
await this.executeHandler(conversationId, newMessages, session, provenanceByMessageId);
|
|
913
1016
|
}, { toFront: shouldInterrupt });
|
|
914
1017
|
}
|
|
915
1018
|
else {
|
|
916
|
-
await this.executeHandler(conversationId, messages);
|
|
1019
|
+
await this.executeHandler(conversationId, messages, undefined, provenanceByMessageId);
|
|
917
1020
|
}
|
|
918
1021
|
}
|
|
919
|
-
async executeHandler(conversationId, messages, session) {
|
|
1022
|
+
async executeHandler(conversationId, messages, session, provenanceByMessageId) {
|
|
920
1023
|
if (!this.handler)
|
|
921
1024
|
return;
|
|
922
1025
|
const turnId = randomUUID();
|
|
@@ -927,9 +1030,21 @@ export class CanonAgent {
|
|
|
927
1030
|
const runtimeState = this.createRuntimeStatePublisher();
|
|
928
1031
|
const queueDepth = () => this.sessionManager?.getQueueDepth(conversationId) ?? 0;
|
|
929
1032
|
const abortController = new AbortController();
|
|
1033
|
+
const throwIfAborted = () => {
|
|
1034
|
+
if (abortController.signal.aborted) {
|
|
1035
|
+
throw createTurnAbortError();
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
930
1038
|
const activeControllers = this.activeAbortControllers.get(conversationId) ?? new Set();
|
|
931
1039
|
activeControllers.add(abortController);
|
|
932
1040
|
this.activeAbortControllers.set(conversationId, activeControllers);
|
|
1041
|
+
const activeTurns = this.activeTurns.get(conversationId) ?? new Map();
|
|
1042
|
+
activeTurns.set(abortController, {
|
|
1043
|
+
turnId,
|
|
1044
|
+
openedAt: turnOpenedAt,
|
|
1045
|
+
activeMessageIds: messages.map((message) => message.id).filter(Boolean),
|
|
1046
|
+
});
|
|
1047
|
+
this.activeTurns.set(conversationId, activeTurns);
|
|
933
1048
|
const writeTurn = async (state) => {
|
|
934
1049
|
if (!runtimeState || !agentId)
|
|
935
1050
|
return;
|
|
@@ -947,6 +1062,7 @@ export class CanonAgent {
|
|
|
947
1062
|
})).catch(() => { });
|
|
948
1063
|
};
|
|
949
1064
|
const setLiveState = async (state, text, streamingStatus) => {
|
|
1065
|
+
throwIfAborted();
|
|
950
1066
|
await writeTurn(state);
|
|
951
1067
|
if (streamingStatus) {
|
|
952
1068
|
try {
|
|
@@ -1007,10 +1123,12 @@ export class CanonAgent {
|
|
|
1007
1123
|
return;
|
|
1008
1124
|
// Build reply functions
|
|
1009
1125
|
const replyFinal = async (text, options) => {
|
|
1126
|
+
throwIfAborted();
|
|
1010
1127
|
try {
|
|
1011
1128
|
await this.apiClient.setTyping(conversationId, true, 'typing');
|
|
1012
1129
|
}
|
|
1013
1130
|
catch { }
|
|
1131
|
+
throwIfAborted();
|
|
1014
1132
|
const sendOptions = withActiveSelfContext(options);
|
|
1015
1133
|
const result = await this.apiClient.sendMessage(conversationId, text, {
|
|
1016
1134
|
...sendOptions,
|
|
@@ -1029,10 +1147,12 @@ export class CanonAgent {
|
|
|
1029
1147
|
return result;
|
|
1030
1148
|
};
|
|
1031
1149
|
const replyProgress = async (text, options) => {
|
|
1150
|
+
throwIfAborted();
|
|
1032
1151
|
await setLiveState('streaming', text, 'streaming');
|
|
1033
1152
|
if (!options?.durable) {
|
|
1034
1153
|
return { turnId, durable: false, messageId: null };
|
|
1035
1154
|
}
|
|
1155
|
+
throwIfAborted();
|
|
1036
1156
|
const { durable: _durable, ...sendOptions } = options;
|
|
1037
1157
|
const sendOptionsWithContext = withActiveSelfContext(sendOptions);
|
|
1038
1158
|
const result = await this.apiClient.sendMessage(conversationId, text, {
|
|
@@ -1054,6 +1174,9 @@ export class CanonAgent {
|
|
|
1054
1174
|
}
|
|
1055
1175
|
}
|
|
1056
1176
|
const latestMessage = hydratedMessages[hydratedMessages.length - 1] ?? null;
|
|
1177
|
+
let replyContext = latestMessage
|
|
1178
|
+
? resolveCanonReplyContext({ message: latestMessage, messages: history })
|
|
1179
|
+
: null;
|
|
1057
1180
|
const resolvedActiveSelfContextId = resolveMessageActiveSelfContextId({
|
|
1058
1181
|
messageId: latestMessage?.id,
|
|
1059
1182
|
activeSelfContextIdByMessageId: page.activeSelfContextIdByMessageId,
|
|
@@ -1075,6 +1198,44 @@ export class CanonAgent {
|
|
|
1075
1198
|
inboundPolicy: 'approval-required',
|
|
1076
1199
|
groupJoinPolicy: 'approval-required',
|
|
1077
1200
|
};
|
|
1201
|
+
const provenance = latestMessage
|
|
1202
|
+
? resolveRuntimeProvenance({
|
|
1203
|
+
provenance: provenanceByMessageId?.get(latestMessage.id) ?? null,
|
|
1204
|
+
conversationId,
|
|
1205
|
+
conversationType: conversation.type,
|
|
1206
|
+
memberCount: conversation.memberIds.length,
|
|
1207
|
+
senderId: latestMessage.senderId,
|
|
1208
|
+
senderName: latestMessage.senderName ?? latestMessage.senderId,
|
|
1209
|
+
senderType: latestMessage.senderType,
|
|
1210
|
+
isOwner: latestMessage.isOwner,
|
|
1211
|
+
agentId: agent.agentId,
|
|
1212
|
+
mentions: latestMessage.mentions,
|
|
1213
|
+
activeSelfContextId,
|
|
1214
|
+
selfContexts,
|
|
1215
|
+
})
|
|
1216
|
+
: resolveRuntimeProvenance({
|
|
1217
|
+
conversationId,
|
|
1218
|
+
conversationType: conversation.type,
|
|
1219
|
+
memberCount: conversation.memberIds.length,
|
|
1220
|
+
senderId: '',
|
|
1221
|
+
senderType: 'human',
|
|
1222
|
+
isOwner: false,
|
|
1223
|
+
agentId: agent.agentId,
|
|
1224
|
+
activeSelfContextId,
|
|
1225
|
+
selfContexts,
|
|
1226
|
+
});
|
|
1227
|
+
if (replyContext?.found && replyContext.attachments?.length) {
|
|
1228
|
+
try {
|
|
1229
|
+
const materializedReply = await materializeReplyContextMedia(replyContext, {
|
|
1230
|
+
agentId: agent.agentId,
|
|
1231
|
+
conversationId,
|
|
1232
|
+
});
|
|
1233
|
+
replyContext = materializedReply.replyContext;
|
|
1234
|
+
}
|
|
1235
|
+
catch (error) {
|
|
1236
|
+
console.error(`[canon-sdk] Failed to materialize reply context media for ${conversationId}:`, error instanceof Error ? error.message : error);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1078
1239
|
const membershipChange = this.pendingMembershipChanges.get(conversationId) ?? null;
|
|
1079
1240
|
this.pendingMembershipChanges.delete(conversationId);
|
|
1080
1241
|
const groupContext = this.buildGroupContext({
|
|
@@ -1110,12 +1271,60 @@ export class CanonAgent {
|
|
|
1110
1271
|
...(options ?? {}),
|
|
1111
1272
|
sourceConversationId: conversationId,
|
|
1112
1273
|
});
|
|
1274
|
+
const requestApproval = async (request) => {
|
|
1275
|
+
throwIfAborted();
|
|
1276
|
+
const manager = this.ensureApprovalManager(agent);
|
|
1277
|
+
if (!manager) {
|
|
1278
|
+
return { decision: 'deny' };
|
|
1279
|
+
}
|
|
1280
|
+
shouldPersistTurnState = true;
|
|
1281
|
+
try {
|
|
1282
|
+
try {
|
|
1283
|
+
await this.apiClient.clearStreaming(conversationId);
|
|
1284
|
+
}
|
|
1285
|
+
catch { }
|
|
1286
|
+
await writeTurn('waiting_input');
|
|
1287
|
+
try {
|
|
1288
|
+
await this.apiClient.setTyping(conversationId, false);
|
|
1289
|
+
}
|
|
1290
|
+
catch { }
|
|
1291
|
+
const result = await manager.requestApproval(conversationId, request.toolName, request.toolInput ?? {}, {
|
|
1292
|
+
riskLevel: request.riskLevel,
|
|
1293
|
+
risk: request.risk,
|
|
1294
|
+
category: request.category,
|
|
1295
|
+
runtimeId: request.runtimeId,
|
|
1296
|
+
turnId: request.turnId ?? turnId,
|
|
1297
|
+
native: request.native,
|
|
1298
|
+
toolSummary: request.toolSummary,
|
|
1299
|
+
details: request.details,
|
|
1300
|
+
ignoreSessionRules: request.ignoreSessionRules,
|
|
1301
|
+
allowSessionRule: request.allowSessionRule,
|
|
1302
|
+
});
|
|
1303
|
+
throwIfAborted();
|
|
1304
|
+
shouldPersistTurnState = false;
|
|
1305
|
+
try {
|
|
1306
|
+
await this.apiClient.setTyping(conversationId, true, 'thinking');
|
|
1307
|
+
}
|
|
1308
|
+
catch { }
|
|
1309
|
+
await setLiveState('thinking', 'Thinking...', 'thinking');
|
|
1310
|
+
return result;
|
|
1311
|
+
}
|
|
1312
|
+
catch (error) {
|
|
1313
|
+
if (abortController.signal.aborted || isAbortLikeError(error)) {
|
|
1314
|
+
throw error;
|
|
1315
|
+
}
|
|
1316
|
+
shouldPersistTurnState = false;
|
|
1317
|
+
return { decision: 'deny' };
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1113
1320
|
const uploadFile = (filePath, options) => uploadMediaFile(this.apiClient, conversationId, filePath, options);
|
|
1114
1321
|
const replyWithFile = async (filePath, text = '', options) => {
|
|
1322
|
+
throwIfAborted();
|
|
1115
1323
|
try {
|
|
1116
1324
|
await this.apiClient.setTyping(conversationId, true, 'typing');
|
|
1117
1325
|
}
|
|
1118
1326
|
catch { }
|
|
1327
|
+
throwIfAborted();
|
|
1119
1328
|
try {
|
|
1120
1329
|
const result = await sendMediaFileMessage(this.apiClient, conversationId, filePath, text, {
|
|
1121
1330
|
...(options?.replyTo ? { replyTo: options.replyTo } : {}),
|
|
@@ -1147,9 +1356,11 @@ export class CanonAgent {
|
|
|
1147
1356
|
}
|
|
1148
1357
|
};
|
|
1149
1358
|
// Invoke handler
|
|
1359
|
+
throwIfAborted();
|
|
1150
1360
|
await this.handler({
|
|
1151
1361
|
messages: hydratedMessages,
|
|
1152
1362
|
history,
|
|
1363
|
+
replyContext,
|
|
1153
1364
|
conversationId,
|
|
1154
1365
|
conversation,
|
|
1155
1366
|
...(groupContext ? { groupContext } : {}),
|
|
@@ -1166,6 +1377,8 @@ export class CanonAgent {
|
|
|
1166
1377
|
agent,
|
|
1167
1378
|
activeSelfContextId,
|
|
1168
1379
|
selfContexts,
|
|
1380
|
+
provenance,
|
|
1381
|
+
requestApproval,
|
|
1169
1382
|
abortSignal: abortController.signal,
|
|
1170
1383
|
media: {
|
|
1171
1384
|
materialize: (message = hydratedMessages[hydratedMessages.length - 1], options) => {
|
|
@@ -1236,6 +1449,10 @@ export class CanonAgent {
|
|
|
1236
1449
|
}
|
|
1237
1450
|
}
|
|
1238
1451
|
catch (err) {
|
|
1452
|
+
if (abortController.signal.aborted || isAbortLikeError(err)) {
|
|
1453
|
+
await writeTurn('interrupted');
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1239
1456
|
console.error(`[canon-sdk] Handler error for ${conversationId}:`, err);
|
|
1240
1457
|
await writeTurn('interrupted');
|
|
1241
1458
|
}
|
|
@@ -1245,6 +1462,11 @@ export class CanonAgent {
|
|
|
1245
1462
|
if (activeControllers?.size === 0) {
|
|
1246
1463
|
this.activeAbortControllers.delete(conversationId);
|
|
1247
1464
|
}
|
|
1465
|
+
const activeTurns = this.activeTurns.get(conversationId);
|
|
1466
|
+
activeTurns?.delete(abortController);
|
|
1467
|
+
if (activeTurns?.size === 0) {
|
|
1468
|
+
this.activeTurns.delete(conversationId);
|
|
1469
|
+
}
|
|
1248
1470
|
clearInterval(thinkingKeepalive);
|
|
1249
1471
|
// Always clear typing when done
|
|
1250
1472
|
try {
|
package/dist/debouncer.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import type { CanonMessage } from '@canonmsg/core';
|
|
1
|
+
import type { CanonMessage, CanonRuntimeProvenance } from '@canonmsg/core';
|
|
2
2
|
export declare class Debouncer {
|
|
3
3
|
private debounceMs;
|
|
4
4
|
private pending;
|
|
5
|
+
private provenanceByMessageId;
|
|
5
6
|
private timers;
|
|
6
7
|
private orderedFlags;
|
|
7
8
|
private callback;
|
|
8
9
|
constructor(debounceMs: number);
|
|
9
|
-
setCallback(cb: (conversationId: string, messages: CanonMessage[]) => void): void;
|
|
10
|
-
add(conversationId: string, message: CanonMessage): void;
|
|
10
|
+
setCallback(cb: (conversationId: string, messages: CanonMessage[], provenanceByMessageId: ReadonlyMap<string, CanonRuntimeProvenance>) => void): void;
|
|
11
|
+
add(conversationId: string, message: CanonMessage, provenance?: CanonRuntimeProvenance | null): void;
|
|
11
12
|
private flush;
|
|
12
13
|
destroy(): void;
|
|
13
14
|
}
|
package/dist/debouncer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export class Debouncer {
|
|
2
2
|
debounceMs;
|
|
3
3
|
pending = new Map();
|
|
4
|
+
provenanceByMessageId = new Map();
|
|
4
5
|
timers = new Map();
|
|
5
6
|
// Track whether each conversation's pending messages are already in sorted
|
|
6
7
|
// order so we can skip the sort on flush when possible (#10)
|
|
@@ -12,8 +13,11 @@ export class Debouncer {
|
|
|
12
13
|
setCallback(cb) {
|
|
13
14
|
this.callback = cb;
|
|
14
15
|
}
|
|
15
|
-
add(conversationId, message) {
|
|
16
|
+
add(conversationId, message, provenance) {
|
|
16
17
|
const existing = this.pending.get(conversationId) || [];
|
|
18
|
+
if (provenance) {
|
|
19
|
+
this.provenanceByMessageId.set(message.id, provenance);
|
|
20
|
+
}
|
|
17
21
|
// Deduplicate by message ID
|
|
18
22
|
if (!existing.some((m) => m.id === message.id)) {
|
|
19
23
|
if (existing.length === 0) {
|
|
@@ -50,7 +54,15 @@ export class Debouncer {
|
|
|
50
54
|
if (!isOrdered) {
|
|
51
55
|
messages.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
52
56
|
}
|
|
53
|
-
|
|
57
|
+
const provenanceByMessageId = new Map();
|
|
58
|
+
for (const message of messages) {
|
|
59
|
+
const provenance = this.provenanceByMessageId.get(message.id);
|
|
60
|
+
if (provenance) {
|
|
61
|
+
provenanceByMessageId.set(message.id, provenance);
|
|
62
|
+
this.provenanceByMessageId.delete(message.id);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
this.callback(conversationId, messages, provenanceByMessageId);
|
|
54
66
|
}
|
|
55
67
|
}
|
|
56
68
|
destroy() {
|
|
@@ -59,6 +71,7 @@ export class Debouncer {
|
|
|
59
71
|
}
|
|
60
72
|
this.timers.clear();
|
|
61
73
|
this.pending.clear();
|
|
74
|
+
this.provenanceByMessageId.clear();
|
|
62
75
|
this.orderedFlags.clear();
|
|
63
76
|
}
|
|
64
77
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export { CanonAgent } from './canon-agent.js';
|
|
2
2
|
export type { AgentContactsAPI, AgentUsersAPI } from './canon-agent.js';
|
|
3
|
-
export { CanonApiError, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, } from '@canonmsg/core';
|
|
4
|
-
export type { CanonContact, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeFact, CanonRuntimeFactGroup, CanonResolveAdmissionResult, ContactAddedPayload, ContactCardPayload, ContactRemovedPayload, ContactSource, HostAdmissionActionCapabilities, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, } from '@canonmsg/core';
|
|
3
|
+
export { ApprovalManager, buildApprovalOutcome, buildApprovalReply, buildApprovalRequest, CanonApiError, DEFAULT_APPROVAL_CONFIG, generateApprovalId, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, parseApprovalReplyMetadata, parseApprovalRequestMetadata, parseSessionRule, parseTextApprovalReply, redactSecrets, } from '@canonmsg/core';
|
|
4
|
+
export type { ApprovalConfig, ApprovalNativeRequestMetadata, ApprovalOutcomeMetadata, ApprovalReplyMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalResult, ApprovalRisk, CanonContact, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeFact, CanonRuntimeFactGroup, CanonResolveAdmissionResult, ContactAddedPayload, ContactCardPayload, ContactRemovedPayload, ContactSource, HostAdmissionActionCapabilities, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, SessionRule, } from '@canonmsg/core';
|
|
5
5
|
export { SessionManager } from './session-manager.js';
|
|
6
|
-
export { DEFAULT_MEDIA_CACHE_DIR, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
|
|
7
|
-
export type { AnthropicImageBlock, AnthropicImageMimeType, MaterializeMediaOptions, MaterializedCanonAttachment, ReplyWithFileOptions, UploadMediaFileOptions, } from './media.js';
|
|
6
|
+
export { DEFAULT_MEDIA_CACHE_DIR, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, materializeReplyContextMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
|
|
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, CanonSelfContext, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, CreateConversationOptions, } from '@canonmsg/core';
|
|
10
|
-
export type { CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, ContactRequestHandler, MessageHandler, MessageHandlerContext, ProgressMessageOptions, ProgressMessageResult, ReachOutOptions, ReachOutResult, RuntimePrimitiveContext, RuntimePrimitiveHandler, RuntimePrimitiveHandlers, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
|
|
9
|
+
export type { AgentContext, CanonGroupContext, CanonKnownRecentParticipant, CanonMembershipChange, CanonContactRequest, CanonMessage, CanonConversation, CanonReplyContext, CanonSelfContext, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, CreateConversationOptions, } from '@canonmsg/core';
|
|
10
|
+
export type { CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, ContactRequestHandler, MessageHandler, MessageHandlerContext, ProgressMessageOptions, ProgressMessageResult, ReachOutOptions, ReachOutResult, RuntimeApprovalRequest, RuntimePrimitiveContext, RuntimePrimitiveHandler, RuntimePrimitiveHandlers, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { CanonAgent } from './canon-agent.js';
|
|
2
|
-
export { CanonApiError, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, } from '@canonmsg/core';
|
|
2
|
+
export { ApprovalManager, buildApprovalOutcome, buildApprovalReply, buildApprovalRequest, CanonApiError, DEFAULT_APPROVAL_CONFIG, generateApprovalId, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, parseApprovalReplyMetadata, parseApprovalRequestMetadata, parseSessionRule, parseTextApprovalReply, redactSecrets, } from '@canonmsg/core';
|
|
3
3
|
export { SessionManager } from './session-manager.js';
|
|
4
|
-
export { DEFAULT_MEDIA_CACHE_DIR, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
|
|
4
|
+
export { DEFAULT_MEDIA_CACHE_DIR, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, materializeReplyContextMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
|
package/dist/media.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CanonClient, type CanonMessage, type MediaAttachment, type SendMessageOptions } from '@canonmsg/core';
|
|
1
|
+
import { CanonClient, type CanonReplyContext, type CanonMessage, type MediaAttachment, type SendMessageOptions } from '@canonmsg/core';
|
|
2
2
|
export interface MaterializeMediaOptions {
|
|
3
3
|
agentId: string;
|
|
4
4
|
conversationId: string;
|
|
@@ -21,6 +21,10 @@ export interface MaterializedCanonAttachment extends MediaAttachment {
|
|
|
21
21
|
conversationId: string;
|
|
22
22
|
messageId: string;
|
|
23
23
|
}
|
|
24
|
+
export interface MaterializedCanonReplyContext {
|
|
25
|
+
replyContext: CanonReplyContext | null;
|
|
26
|
+
materialized: MaterializedCanonAttachment[];
|
|
27
|
+
}
|
|
24
28
|
/**
|
|
25
29
|
* Anthropic `image` content blocks only accept these MIME types for
|
|
26
30
|
* base64 sources. Anything outside this set must either be re-encoded or
|
|
@@ -46,6 +50,7 @@ export declare function materializeAttachment(attachment: MediaAttachment, optio
|
|
|
46
50
|
index?: number;
|
|
47
51
|
}): Promise<MaterializedCanonAttachment>;
|
|
48
52
|
export declare function materializeMessageMedia(message: Pick<CanonMessage, 'id' | 'attachments'>, options: Omit<MaterializeMediaOptions, 'messageId'>): Promise<MaterializedCanonAttachment[]>;
|
|
53
|
+
export declare function materializeReplyContextMedia(replyContext: CanonReplyContext | null, options: Omit<MaterializeMediaOptions, 'messageId'>): Promise<MaterializedCanonReplyContext>;
|
|
49
54
|
export declare function inferUploadMimeType(filePath: string, overrideMimeType?: string): string;
|
|
50
55
|
export declare function uploadMediaFile(client: CanonClient, conversationId: string, filePath: string, options?: UploadMediaFileOptions): Promise<{
|
|
51
56
|
url: string;
|
package/dist/media.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { basename, dirname, extname, join } from 'node:path';
|
|
3
|
-
import { CANON_DIR, } from '@canonmsg/core';
|
|
3
|
+
import { CANON_DIR, renderCanonHostInboundContent, } from '@canonmsg/core';
|
|
4
4
|
const ANTHROPIC_IMAGE_MIME_TYPES = new Set([
|
|
5
5
|
'image/jpeg',
|
|
6
6
|
'image/png',
|
|
@@ -141,6 +141,31 @@ export async function materializeMessageMedia(message, options) {
|
|
|
141
141
|
index,
|
|
142
142
|
})));
|
|
143
143
|
}
|
|
144
|
+
export async function materializeReplyContextMedia(replyContext, options) {
|
|
145
|
+
if (!replyContext?.found || !replyContext.attachments?.length) {
|
|
146
|
+
return { replyContext, materialized: [] };
|
|
147
|
+
}
|
|
148
|
+
const materialized = (await Promise.all(replyContext.attachments.map((attachment, index) => attachment.url
|
|
149
|
+
? materializeAttachment(attachment, {
|
|
150
|
+
...options,
|
|
151
|
+
messageId: replyContext.messageId,
|
|
152
|
+
index,
|
|
153
|
+
})
|
|
154
|
+
: Promise.resolve(null)))).filter((attachment) => attachment !== null);
|
|
155
|
+
return {
|
|
156
|
+
replyContext: {
|
|
157
|
+
...replyContext,
|
|
158
|
+
body: renderCanonHostInboundContent({
|
|
159
|
+
text: replyContext.text,
|
|
160
|
+
contentType: replyContext.contentType,
|
|
161
|
+
attachments: replyContext.attachments,
|
|
162
|
+
contactCard: replyContext.contactCard,
|
|
163
|
+
senderType: replyContext.senderType ?? undefined,
|
|
164
|
+
}, materialized),
|
|
165
|
+
},
|
|
166
|
+
materialized,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
144
169
|
export function inferUploadMimeType(filePath, overrideMimeType) {
|
|
145
170
|
if (overrideMimeType)
|
|
146
171
|
return overrideMimeType;
|
package/dist/realtime.js
CHANGED
|
@@ -52,7 +52,7 @@ export class RealtimeManager {
|
|
|
52
52
|
...(m.contactCard ? { contactCard: m.contactCard } : {}),
|
|
53
53
|
...(m.metadata ? { metadata: m.metadata } : {}),
|
|
54
54
|
};
|
|
55
|
-
this.debouncer.add(payload.conversationId, message);
|
|
55
|
+
this.debouncer.add(payload.conversationId, message, payload.provenance ?? null);
|
|
56
56
|
},
|
|
57
57
|
onAgentContext: (ctx) => {
|
|
58
58
|
this.onAgentContext?.(ctx);
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type { AddMemberResult, AgentClientType, CanonGroupContext, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeDescriptor, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimePrimitiveId, CanonMessage, CanonConversation, CanonContact, CanonContactRequest, CanonResolveAdmissionResult, ContactAddedPayload, ContactRemovedPayload, ContactSource, AgentContext, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonSelfContext, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, SessionConfig, CreateConversationOptions, TurnLifecycleState, } from '@canonmsg/core';
|
|
2
|
-
import type { AddMemberResult, CanonGroupContext, CanonMessage, CanonConversation, ContactCardPayload, CanonRuntimeActionDispatch, CanonRuntimePrimitiveId, SendMessageOptions, SendContextualSelfContextInput, SessionConfig } from '@canonmsg/core';
|
|
1
|
+
export type { AddMemberResult, AgentClientType, CanonGroupContext, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeDescriptor, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimePrimitiveId, CanonRuntimeProvenance, CanonMessage, CanonConversation, CanonReplyContext, CanonContact, CanonContactRequest, CanonResolveAdmissionResult, ContactAddedPayload, ContactRemovedPayload, ContactSource, AgentContext, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonSelfContext, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, SessionConfig, CreateConversationOptions, TurnLifecycleState, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalRisk, ApprovalReplyMetadata, ApprovalOutcomeMetadata, SessionRule, ApprovalResult, } from '@canonmsg/core';
|
|
2
|
+
import type { AddMemberResult, CanonGroupContext, CanonMessage, CanonConversation, CanonReplyContext, ContactCardPayload, CanonRuntimeActionDispatch, CanonRuntimePrimitiveId, CanonRuntimeProvenance, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalResult, ApprovalRisk, SendMessageOptions, SendContextualSelfContextInput, SessionConfig } from '@canonmsg/core';
|
|
3
3
|
import type { MaterializeMediaOptions, MaterializedCanonAttachment, ReplyWithFileOptions, UploadMediaFileOptions } from './media.js';
|
|
4
4
|
export interface ProgressMessageOptions extends SendMessageOptions {
|
|
5
5
|
/**
|
|
@@ -34,9 +34,33 @@ export interface TurnController {
|
|
|
34
34
|
setTool: (text: string) => Promise<void>;
|
|
35
35
|
setWaitingInput: (text?: string) => Promise<void>;
|
|
36
36
|
}
|
|
37
|
+
export interface RuntimeApprovalRequest {
|
|
38
|
+
/** Native runtime/tool/action name that is asking for permission. */
|
|
39
|
+
toolName: string;
|
|
40
|
+
/** Structured native input. Raw values are summarized/redacted before storage. */
|
|
41
|
+
toolInput?: Record<string, unknown>;
|
|
42
|
+
/** Optional pre-redacted summary when a runtime already has user-facing copy. */
|
|
43
|
+
toolSummary?: string;
|
|
44
|
+
category?: ApprovalRequestCategory;
|
|
45
|
+
risk?: ApprovalRisk;
|
|
46
|
+
riskLevel?: 'normal' | 'destructive';
|
|
47
|
+
runtimeId?: string;
|
|
48
|
+
turnId?: string;
|
|
49
|
+
native?: ApprovalNativeRequestMetadata;
|
|
50
|
+
details?: ApprovalRequestDetail[];
|
|
51
|
+
/**
|
|
52
|
+
* Ignore in-memory session rules for this approval request. Useful when the
|
|
53
|
+
* action was triggered by someone other than the agent owner.
|
|
54
|
+
*/
|
|
55
|
+
ignoreSessionRules?: boolean;
|
|
56
|
+
/** Whether an approval reply may set an in-memory session rule. */
|
|
57
|
+
allowSessionRule?: boolean;
|
|
58
|
+
}
|
|
37
59
|
export interface MessageHandlerContext {
|
|
38
60
|
messages: CanonMessage[];
|
|
39
61
|
history: CanonMessage[];
|
|
62
|
+
/** Resolved message/media content for the latest swipe-reply target, if any. */
|
|
63
|
+
replyContext: CanonReplyContext | null;
|
|
40
64
|
conversationId: string;
|
|
41
65
|
conversation: CanonConversation;
|
|
42
66
|
/** Lightweight group awareness, present for group conversations. */
|
|
@@ -77,6 +101,14 @@ export interface MessageHandlerContext {
|
|
|
77
101
|
activeSelfContextId: string | null;
|
|
78
102
|
/** Canon-provided private context explaining this agent's cross-session actions. */
|
|
79
103
|
selfContexts?: import('@canonmsg/core').CanonSelfContext[];
|
|
104
|
+
/** Trusted Canon provenance for the latest inbound message in this handler batch. */
|
|
105
|
+
provenance: CanonRuntimeProvenance;
|
|
106
|
+
/**
|
|
107
|
+
* Ask the conversation owner to approve a native runtime action. This only
|
|
108
|
+
* renders Canon's inline approval card; runtimes must explicitly wait for
|
|
109
|
+
* and enforce the returned decision in their own approval hook.
|
|
110
|
+
*/
|
|
111
|
+
requestApproval: (request: RuntimeApprovalRequest) => Promise<ApprovalResult>;
|
|
80
112
|
/** Canon-managed local media access for the current conversation. */
|
|
81
113
|
media: {
|
|
82
114
|
materialize: (message?: CanonMessage, options?: Omit<MaterializeMediaOptions, 'agentId' | 'conversationId' | 'messageId'>) => Promise<MaterializedCanonAttachment[]>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/agent-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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": "^0.
|
|
31
|
+
"@canonmsg/core": "^0.19.0"
|
|
32
32
|
},
|
|
33
33
|
"publishConfig": {
|
|
34
34
|
"access": "public"
|