@canonmsg/agent-sdk 1.3.1 → 1.4.1

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,4 +1,4 @@
1
- import { type AddMemberResult, type CanonContact, type CanonRuntimePrimitiveId, type ContactCardPayload, type CreateContactRequestResult } from '@canonmsg/core';
1
+ import { type AddMemberResult, type CanonContact, type CanonRuntimeActivityItem, type CanonRuntimeCommandDescriptor, type CanonRuntimeFact, type CanonRuntimePrimitiveId, type ContactCardPayload, type ClearRuntimeActivityOptions, type CreateContactRequestResult } from '@canonmsg/core';
2
2
  import type { CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, CreateConversationOptions, MessageHandler, ReachOutOptions, ReachOutResult, ContactRequestHandler, RuntimeSignalHandler, RuntimePrimitiveHandler } from './types.js';
3
3
  /**
4
4
  * Contact-graph operations exposed under `agent.contacts`. Wraps the REST
@@ -49,6 +49,7 @@ export declare class CanonAgent {
49
49
  private readonly lastSeenSignal;
50
50
  private readonly primitiveRequestDedupe;
51
51
  private readonly activeAbortControllers;
52
+ private readonly activeTurns;
52
53
  private readonly conversationMemberIds;
53
54
  private readonly pendingMembershipChanges;
54
55
  constructor(options: CanonAgentOptions);
@@ -61,6 +62,10 @@ export declare class CanonAgent {
61
62
  on(event: 'stopAndDrop', handler: RuntimeSignalHandler): void;
62
63
  on(event: 'newSession', handler: RuntimeSignalHandler): void;
63
64
  onPrimitive(primitive: CanonRuntimePrimitiveId | '*', handler: RuntimePrimitiveHandler): void;
65
+ describeCommands(_provider?: string): ReadonlyArray<CanonRuntimeCommandDescriptor>;
66
+ publishRuntimeFacts(conversationId: string, facts: ReadonlyArray<CanonRuntimeFact>): Promise<void>;
67
+ publishRuntimeActivity(conversationId: string, item: CanonRuntimeActivityItem): Promise<void>;
68
+ clearRuntimeActivity(conversationId: string, options?: ClearRuntimeActivityOptions): Promise<void>;
64
69
  /**
65
70
  * Resolve admission live for a target user (typically read off a shared
66
71
  * contact card) and route into either an immediate message or a contact
@@ -124,10 +129,13 @@ export declare class CanonAgent {
124
129
  private clearRuntimePrimitiveRequest;
125
130
  private prunePrimitiveRequestDedupe;
126
131
  private handleRuntimeSignal;
132
+ private firstActiveTurn;
133
+ private publishAcceptedRuntimeSignal;
127
134
  private abortActiveTurns;
128
135
  private resolveBatchDeliveryIntent;
129
136
  private notifyMessageInterrupt;
130
137
  private createRuntimeStatePublisher;
138
+ private requireRuntimeStatePublisher;
131
139
  private handleMessages;
132
140
  private executeHandler;
133
141
  static register(options: {
@@ -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 { 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, 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;
@@ -147,6 +147,40 @@ function normalizePrimitiveArgs(value) {
147
147
  }
148
148
  return args;
149
149
  }
150
+ function normalizeRuntimeFact(fact) {
151
+ const id = fact.id.trim();
152
+ const label = fact.label.trim();
153
+ const value = fact.value.trim();
154
+ if (!id || !label || !value)
155
+ return null;
156
+ return {
157
+ ...fact,
158
+ id,
159
+ label,
160
+ value,
161
+ };
162
+ }
163
+ function normalizeRuntimeActivityItem(item) {
164
+ return {
165
+ ...item,
166
+ id: item.id.trim(),
167
+ title: item.title.trim() || item.kind,
168
+ updatedAt: item.updatedAt || Date.now(),
169
+ };
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
+ }
150
184
  export class CanonAgent {
151
185
  options;
152
186
  apiClient;
@@ -178,6 +212,7 @@ export class CanonAgent {
178
212
  lastSeenSignal = new Map();
179
213
  primitiveRequestDedupe = new Map();
180
214
  activeAbortControllers = new Map();
215
+ activeTurns = new Map();
181
216
  conversationMemberIds = new Map();
182
217
  pendingMembershipChanges = new Map();
183
218
  constructor(options) {
@@ -283,6 +318,32 @@ export class CanonAgent {
283
318
  }
284
319
  void this.publishAgentRuntime().catch(() => { });
285
320
  }
321
+ describeCommands(_provider) {
322
+ return this.buildRuntimeDescriptor().commands ?? [];
323
+ }
324
+ async publishRuntimeFacts(conversationId, facts) {
325
+ this.rememberConversationId(conversationId);
326
+ const publisher = this.requireRuntimeStatePublisher();
327
+ const normalizedFacts = facts
328
+ .map(normalizeRuntimeFact)
329
+ .filter((fact) => Boolean(fact));
330
+ await publisher.patchRuntimeInfo(conversationId, {
331
+ descriptor: this.buildRuntimeDescriptor(),
332
+ facts: normalizedFacts,
333
+ });
334
+ }
335
+ async publishRuntimeActivity(conversationId, item) {
336
+ this.rememberConversationId(conversationId);
337
+ const normalized = normalizeRuntimeActivityItem(item);
338
+ if (!normalized.id) {
339
+ throw new Error('Runtime activity item id is required.');
340
+ }
341
+ await this.requireRuntimeStatePublisher().writeRuntimeActivity(conversationId, normalized);
342
+ }
343
+ async clearRuntimeActivity(conversationId, options) {
344
+ this.rememberConversationId(conversationId);
345
+ await this.requireRuntimeStatePublisher().clearRuntimeActivity(conversationId, options);
346
+ }
286
347
  /**
287
348
  * Resolve admission live for a target user (typically read off a shared
288
349
  * contact card) and route into either an immediate message or a contact
@@ -785,6 +846,7 @@ export class CanonAgent {
785
846
  await Promise.resolve(rtdbWrite(`/control/${conversationId}/${this.agentId}/signal`, null)).catch(() => { });
786
847
  return;
787
848
  }
849
+ const activeTurn = this.firstActiveTurn(conversationId);
788
850
  const abortSignal = this.abortActiveTurns(conversationId);
789
851
  const droppedMessages = signal === 'new_session'
790
852
  ? this.sessionManager?.resetSession(conversationId) ?? []
@@ -797,6 +859,10 @@ export class CanonAgent {
797
859
  return Promise.resolve();
798
860
  return this.apiClient.updateMessageDisposition(conversationId, message.id, 'rejected').catch(() => { });
799
861
  }));
862
+ await this.publishAcceptedRuntimeSignal(conversationId, signal, activeTurn, {
863
+ hasActiveTurn: Boolean(abortSignal),
864
+ droppedCount: droppedMessages.length,
865
+ });
800
866
  await Promise.resolve(handler?.({
801
867
  conversationId,
802
868
  signal: signal,
@@ -808,6 +874,39 @@ export class CanonAgent {
808
874
  });
809
875
  await Promise.resolve(rtdbWrite(`/control/${conversationId}/${this.agentId}/signal`, null)).catch(() => { });
810
876
  }
877
+ firstActiveTurn(conversationId) {
878
+ const turns = this.activeTurns.get(conversationId);
879
+ if (!turns || turns.size === 0)
880
+ return null;
881
+ return turns.values().next().value ?? null;
882
+ }
883
+ async publishAcceptedRuntimeSignal(conversationId, signal, activeTurn, outcome) {
884
+ if (!this.agentId)
885
+ return;
886
+ const shouldPublishInterrupted = signal === 'interrupt'
887
+ || signal === 'stop_and_drop'
888
+ || outcome.hasActiveTurn
889
+ || outcome.droppedCount > 0;
890
+ const runtimeState = shouldPublishInterrupted
891
+ ? this.createRuntimeStatePublisher()
892
+ : null;
893
+ if (runtimeState) {
894
+ await Promise.resolve(runtimeState.writeTurnState(conversationId, {
895
+ turnId: activeTurn?.turnId ?? null,
896
+ state: 'interrupted',
897
+ queueDepth: this.sessionManager?.getQueueDepth(conversationId) ?? 0,
898
+ currentSpeakerId: this.agentId,
899
+ activeMessageIds: activeTurn?.activeMessageIds ?? [],
900
+ capabilities: this.buildRuntimeCapabilities(),
901
+ ...(activeTurn?.openedAt ? { openedAt: activeTurn.openedAt } : {}),
902
+ completedAt: { '.sv': 'timestamp' },
903
+ })).catch(() => { });
904
+ }
905
+ await Promise.all([
906
+ this.apiClient.clearStreaming(conversationId).catch(() => { }),
907
+ this.apiClient.setTyping(conversationId, false).catch(() => { }),
908
+ ]);
909
+ }
811
910
  abortActiveTurns(conversationId) {
812
911
  const controllers = this.activeAbortControllers.get(conversationId);
813
912
  if (!controllers || controllers.size === 0)
@@ -844,6 +943,13 @@ export class CanonAgent {
844
943
  hostMode: false,
845
944
  });
846
945
  }
946
+ requireRuntimeStatePublisher() {
947
+ const publisher = this.createRuntimeStatePublisher();
948
+ if (!publisher) {
949
+ throw new Error('Canon agent must be started before publishing runtime operations.');
950
+ }
951
+ return publisher;
952
+ }
847
953
  async handleMessages(conversationId, messages) {
848
954
  if (!this.handler) {
849
955
  console.warn(`[canon-sdk] No message handler registered — messages for ${conversationId} dropped. Call agent.on('message', handler) before starting.`);
@@ -873,9 +979,21 @@ export class CanonAgent {
873
979
  const runtimeState = this.createRuntimeStatePublisher();
874
980
  const queueDepth = () => this.sessionManager?.getQueueDepth(conversationId) ?? 0;
875
981
  const abortController = new AbortController();
982
+ const throwIfAborted = () => {
983
+ if (abortController.signal.aborted) {
984
+ throw createTurnAbortError();
985
+ }
986
+ };
876
987
  const activeControllers = this.activeAbortControllers.get(conversationId) ?? new Set();
877
988
  activeControllers.add(abortController);
878
989
  this.activeAbortControllers.set(conversationId, activeControllers);
990
+ const activeTurns = this.activeTurns.get(conversationId) ?? new Map();
991
+ activeTurns.set(abortController, {
992
+ turnId,
993
+ openedAt: turnOpenedAt,
994
+ activeMessageIds: messages.map((message) => message.id).filter(Boolean),
995
+ });
996
+ this.activeTurns.set(conversationId, activeTurns);
879
997
  const writeTurn = async (state) => {
880
998
  if (!runtimeState || !agentId)
881
999
  return;
@@ -893,6 +1011,7 @@ export class CanonAgent {
893
1011
  })).catch(() => { });
894
1012
  };
895
1013
  const setLiveState = async (state, text, streamingStatus) => {
1014
+ throwIfAborted();
896
1015
  await writeTurn(state);
897
1016
  if (streamingStatus) {
898
1017
  try {
@@ -953,10 +1072,12 @@ export class CanonAgent {
953
1072
  return;
954
1073
  // Build reply functions
955
1074
  const replyFinal = async (text, options) => {
1075
+ throwIfAborted();
956
1076
  try {
957
1077
  await this.apiClient.setTyping(conversationId, true, 'typing');
958
1078
  }
959
1079
  catch { }
1080
+ throwIfAborted();
960
1081
  const sendOptions = withActiveSelfContext(options);
961
1082
  const result = await this.apiClient.sendMessage(conversationId, text, {
962
1083
  ...sendOptions,
@@ -975,10 +1096,12 @@ export class CanonAgent {
975
1096
  return result;
976
1097
  };
977
1098
  const replyProgress = async (text, options) => {
1099
+ throwIfAborted();
978
1100
  await setLiveState('streaming', text, 'streaming');
979
1101
  if (!options?.durable) {
980
1102
  return { turnId, durable: false, messageId: null };
981
1103
  }
1104
+ throwIfAborted();
982
1105
  const { durable: _durable, ...sendOptions } = options;
983
1106
  const sendOptionsWithContext = withActiveSelfContext(sendOptions);
984
1107
  const result = await this.apiClient.sendMessage(conversationId, text, {
@@ -1000,6 +1123,9 @@ export class CanonAgent {
1000
1123
  }
1001
1124
  }
1002
1125
  const latestMessage = hydratedMessages[hydratedMessages.length - 1] ?? null;
1126
+ let replyContext = latestMessage
1127
+ ? resolveCanonReplyContext({ message: latestMessage, messages: history })
1128
+ : null;
1003
1129
  const resolvedActiveSelfContextId = resolveMessageActiveSelfContextId({
1004
1130
  messageId: latestMessage?.id,
1005
1131
  activeSelfContextIdByMessageId: page.activeSelfContextIdByMessageId,
@@ -1021,6 +1147,18 @@ export class CanonAgent {
1021
1147
  inboundPolicy: 'approval-required',
1022
1148
  groupJoinPolicy: 'approval-required',
1023
1149
  };
1150
+ if (replyContext?.found && replyContext.attachments?.length) {
1151
+ try {
1152
+ const materializedReply = await materializeReplyContextMedia(replyContext, {
1153
+ agentId: agent.agentId,
1154
+ conversationId,
1155
+ });
1156
+ replyContext = materializedReply.replyContext;
1157
+ }
1158
+ catch (error) {
1159
+ console.error(`[canon-sdk] Failed to materialize reply context media for ${conversationId}:`, error instanceof Error ? error.message : error);
1160
+ }
1161
+ }
1024
1162
  const membershipChange = this.pendingMembershipChanges.get(conversationId) ?? null;
1025
1163
  this.pendingMembershipChanges.delete(conversationId);
1026
1164
  const groupContext = this.buildGroupContext({
@@ -1058,10 +1196,12 @@ export class CanonAgent {
1058
1196
  });
1059
1197
  const uploadFile = (filePath, options) => uploadMediaFile(this.apiClient, conversationId, filePath, options);
1060
1198
  const replyWithFile = async (filePath, text = '', options) => {
1199
+ throwIfAborted();
1061
1200
  try {
1062
1201
  await this.apiClient.setTyping(conversationId, true, 'typing');
1063
1202
  }
1064
1203
  catch { }
1204
+ throwIfAborted();
1065
1205
  try {
1066
1206
  const result = await sendMediaFileMessage(this.apiClient, conversationId, filePath, text, {
1067
1207
  ...(options?.replyTo ? { replyTo: options.replyTo } : {}),
@@ -1093,9 +1233,11 @@ export class CanonAgent {
1093
1233
  }
1094
1234
  };
1095
1235
  // Invoke handler
1236
+ throwIfAborted();
1096
1237
  await this.handler({
1097
1238
  messages: hydratedMessages,
1098
1239
  history,
1240
+ replyContext,
1099
1241
  conversationId,
1100
1242
  conversation,
1101
1243
  ...(groupContext ? { groupContext } : {}),
@@ -1182,6 +1324,10 @@ export class CanonAgent {
1182
1324
  }
1183
1325
  }
1184
1326
  catch (err) {
1327
+ if (abortController.signal.aborted || isAbortLikeError(err)) {
1328
+ await writeTurn('interrupted');
1329
+ return;
1330
+ }
1185
1331
  console.error(`[canon-sdk] Handler error for ${conversationId}:`, err);
1186
1332
  await writeTurn('interrupted');
1187
1333
  }
@@ -1191,6 +1337,11 @@ export class CanonAgent {
1191
1337
  if (activeControllers?.size === 0) {
1192
1338
  this.activeAbortControllers.delete(conversationId);
1193
1339
  }
1340
+ const activeTurns = this.activeTurns.get(conversationId);
1341
+ activeTurns?.delete(abortController);
1342
+ if (activeTurns?.size === 0) {
1343
+ this.activeTurns.delete(conversationId);
1344
+ }
1194
1345
  clearInterval(thinkingKeepalive);
1195
1346
  // Always clear typing when done
1196
1347
  try {
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
3
  export { CanonApiError, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, } from '@canonmsg/core';
4
- export type { CanonContact, CanonResolveAdmissionResult, ContactAddedPayload, ContactCardPayload, ContactRemovedPayload, ContactSource, HostAdmissionActionCapabilities, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, } from '@canonmsg/core';
4
+ export type { CanonContact, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeFact, CanonRuntimeFactGroup, CanonResolveAdmissionResult, ContactAddedPayload, ContactCardPayload, ContactRemovedPayload, ContactSource, HostAdmissionActionCapabilities, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, } 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';
9
+ export type { AgentContext, CanonGroupContext, CanonKnownRecentParticipant, CanonMembershipChange, CanonContactRequest, CanonMessage, CanonConversation, CanonReplyContext, CanonSelfContext, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, CreateConversationOptions, } from '@canonmsg/core';
10
10
  export type { CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, ContactRequestHandler, MessageHandler, MessageHandlerContext, ProgressMessageOptions, ProgressMessageResult, ReachOutOptions, ReachOutResult, 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
2
  export { CanonApiError, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, } 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/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export type { AddMemberResult, AgentClientType, CanonGroupContext, CanonRuntimeDescriptor, 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, CanonMessage, CanonConversation, CanonReplyContext, 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, CanonReplyContext, ContactCardPayload, CanonRuntimeActionDispatch, CanonRuntimePrimitiveId, 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
  /**
@@ -37,6 +37,8 @@ export interface TurnController {
37
37
  export interface MessageHandlerContext {
38
38
  messages: CanonMessage[];
39
39
  history: CanonMessage[];
40
+ /** Resolved message/media content for the latest swipe-reply target, if any. */
41
+ replyContext: CanonReplyContext | null;
40
42
  conversationId: string;
41
43
  conversation: CanonConversation;
42
44
  /** Lightweight group awareness, present for group conversations. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
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.17.2"
31
+ "@canonmsg/core": "^0.18.1"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"