@canonmsg/agent-sdk 1.1.2 → 1.1.4

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,8 +1,8 @@
1
- import { CanonClient, createRuntimeStatePublisher, FINAL_MESSAGE_HANDOFF_MS, initRTDBAuth, rtdbRead, rtdbWrite, mergeWorkSessionContexts, normalizeTurnMetadata, reachOutToCanonContact, } from '@canonmsg/core';
1
+ import { CanonClient, createRuntimeStatePublisher, FINAL_MESSAGE_HANDOFF_MS, initRTDBAuth, rtdbRead, rtdbWrite, normalizeTurnMetadata, reachOutToCanonContact, } 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, uploadMediaFile, } from './media.js';
5
+ import { materializeMessageMedia, 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 SDK_RUNTIME_CAPABILITIES = {
@@ -152,7 +152,10 @@ export class CanonAgent {
152
152
  // Include the opener/request payloads in the dedupe key so two concurrent
153
153
  // calls with different `text`, `requestMessage`, or setup choices don't silently collapse
154
154
  // and lose the second caller's intended side effect.
155
- const inFlightKey = `${targetUserId}\u0000${options?.text ?? ''}\u0000${options?.requestMessage ?? ''}\u0000${JSON.stringify(options?.sessionConfig ?? null)}`;
155
+ const contextualKey = options?.selfContext
156
+ ? `${options.sourceConversationId ?? ''}\u0000${options.selfContext.type}\u0000${options.selfContext.context}`
157
+ : '';
158
+ const inFlightKey = `${targetUserId}\u0000${options?.text ?? ''}\u0000${options?.requestMessage ?? ''}\u0000${JSON.stringify(options?.sessionConfig ?? null)}\u0000${contextualKey}`;
156
159
  const inFlight = this.reachOutInFlight.get(inFlightKey);
157
160
  if (inFlight)
158
161
  return inFlight;
@@ -163,6 +166,29 @@ export class CanonAgent {
163
166
  return promise;
164
167
  }
165
168
  async executeReachOut(targetUserId, options) {
169
+ if (options?.selfContext) {
170
+ if (!options.sourceConversationId) {
171
+ throw new Error('sourceConversationId is required for contextual reachOut');
172
+ }
173
+ if (!options.text) {
174
+ throw new Error('text is required for contextual reachOut');
175
+ }
176
+ const result = await this.apiClient.sendContextualMessage({
177
+ sourceConversationId: options.sourceConversationId,
178
+ targetUserId,
179
+ text: options.text,
180
+ selfContext: options.selfContext,
181
+ requestMessage: options.requestMessage ?? null,
182
+ sessionConfig: options.sessionConfig ?? null,
183
+ });
184
+ return result.status === 'messaged'
185
+ ? {
186
+ status: 'messaged',
187
+ conversationId: result.conversationId,
188
+ messageId: result.messageId,
189
+ }
190
+ : result;
191
+ }
166
192
  return reachOutToCanonContact(this.apiClient, {
167
193
  targetUserId,
168
194
  text: options?.text ?? null,
@@ -634,6 +660,9 @@ export class CanonAgent {
634
660
  catch { }
635
661
  const result = await this.apiClient.sendMessage(conversationId, text, {
636
662
  ...(options ?? {}),
663
+ ...(options?.selfContextId === undefined && activeSelfContextId
664
+ ? { selfContextId: activeSelfContextId }
665
+ : {}),
637
666
  metadata: {
638
667
  ...(options?.metadata ?? {}),
639
668
  turnId,
@@ -672,11 +701,8 @@ export class CanonAgent {
672
701
  m.isOwner = m.senderId === ownerId;
673
702
  }
674
703
  }
675
- const explicitWorkSession = messages.find((message) => message.workSession)?.workSession
676
- ?? history.find((message) => message.workSession)?.workSession
677
- ?? null;
678
- const activeWorkSessions = mergeWorkSessionContexts(explicitWorkSession, page.workSessions ?? []);
679
- const workSession = explicitWorkSession;
704
+ const selfContexts = page.selfContexts ?? [];
705
+ const activeSelfContextId = selfContexts[0]?.id;
680
706
  // Build agent context (fallback to minimal if not yet received)
681
707
  const agent = this.agentContext ?? {
682
708
  agentId: this.agentId,
@@ -693,32 +719,21 @@ export class CanonAgent {
693
719
  const react = (messageId, emoji) => this.apiClient.react(conversationId, messageId, emoji);
694
720
  const addMember = (userId) => this.apiClient.addMember(conversationId, userId);
695
721
  const removeMember = (userId) => this.apiClient.removeMember(conversationId, userId);
696
- const createWorkSession = (options) => this.apiClient.createWorkSession({
697
- conversationId,
698
- ...(options ?? {}),
699
- });
700
- const getWorkSession = (workSessionId, targetConversationId = conversationId) => this.apiClient.getWorkSession(workSessionId, targetConversationId);
701
- const updateWorkSessionContext = (workSessionId, options) => this.apiClient.upsertWorkSessionConversation(workSessionId, conversationId, options);
702
- const sendLinkedMessage = (targetConversationId, text, options) => {
703
- if (!options?.workSessionId && !options?.createWorkSession) {
704
- throw new Error('sendLinkedMessage requires workSessionId or createWorkSession');
705
- }
706
- return this.apiClient.sendLinkedMessage({
707
- sourceConversationId: conversationId,
708
- targetConversationId,
709
- text,
710
- ...(options ?? {}),
711
- messageOptions: {
712
- ...(options?.messageOptions ?? {}),
713
- metadata: {
714
- ...(options?.messageOptions?.metadata ?? {}),
715
- turnId,
716
- turnSemantics: 'turn_complete',
717
- turnComplete: true,
718
- },
722
+ const sendContextualMessage = (target, text, options) => this.apiClient.sendContextualMessage({
723
+ sourceConversationId: conversationId,
724
+ ...target,
725
+ text,
726
+ ...options,
727
+ messageOptions: {
728
+ ...(options.messageOptions ?? {}),
729
+ metadata: {
730
+ ...(options.messageOptions?.metadata ?? {}),
731
+ turnId,
732
+ turnSemantics: 'turn_complete',
733
+ turnComplete: true,
719
734
  },
720
- });
721
- };
735
+ },
736
+ });
722
737
  const uploadFile = (filePath, options) => uploadMediaFile(this.apiClient, conversationId, filePath, options);
723
738
  const replyWithFile = async (filePath, text = '', options) => {
724
739
  try {
@@ -726,22 +741,24 @@ export class CanonAgent {
726
741
  }
727
742
  catch { }
728
743
  try {
729
- const uploaded = await uploadFile(filePath, options);
730
- const result = await this.apiClient.sendMessage(conversationId, text, {
744
+ const result = await sendMediaFileMessage(this.apiClient, conversationId, filePath, text, {
731
745
  ...(options?.replyTo ? { replyTo: options.replyTo } : {}),
732
746
  ...(options?.replyToPosition != null
733
747
  ? { replyToPosition: options.replyToPosition }
734
748
  : {}),
735
749
  ...(options?.mentions ? { mentions: options.mentions } : {}),
750
+ ...(options?.selfContextId === undefined && activeSelfContextId
751
+ ? { selfContextId: activeSelfContextId }
752
+ : {}),
736
753
  metadata: {
737
754
  ...(options?.metadata ?? {}),
738
755
  turnId,
739
756
  turnSemantics: 'turn_complete',
740
757
  turnComplete: true,
741
758
  },
742
- ...(options?.workSessionId ? { workSessionId: options.workSessionId } : {}),
743
- contentType: uploaded.attachment.kind,
744
- attachments: [uploaded.attachment],
759
+ ...(options?.fileName ? { fileName: options.fileName } : {}),
760
+ ...(options?.mimeType ? { mimeType: options.mimeType } : {}),
761
+ ...(options?.durationMs != null ? { durationMs: options.durationMs } : {}),
745
762
  });
746
763
  await sleep(FINAL_MESSAGE_HANDOFF_MS);
747
764
  return result;
@@ -767,13 +784,9 @@ export class CanonAgent {
767
784
  react,
768
785
  addMember,
769
786
  removeMember,
770
- createWorkSession,
771
- getWorkSession,
772
- updateWorkSessionContext,
773
- sendLinkedMessage,
787
+ sendContextualMessage,
774
788
  agent,
775
- workSession,
776
- activeWorkSessions,
789
+ selfContexts,
777
790
  abortSignal: abortController.signal,
778
791
  media: {
779
792
  materialize: (message = hydratedMessages[hydratedMessages.length - 1], options) => {
package/dist/index.d.ts CHANGED
@@ -3,8 +3,8 @@ export type { AgentContactsAPI, AgentUsersAPI } from './canon-agent.js';
3
3
  export { CanonApiError, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, } from '@canonmsg/core';
4
4
  export type { CanonContact, CanonResolveAdmissionResult, ContactAddedPayload, ContactCardPayload, ContactRemovedPayload, ContactSource, HostAdmissionActionCapabilities, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, } from '@canonmsg/core';
5
5
  export { SessionManager } from './session-manager.js';
6
- export { getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, resolveAttachmentMimeType, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
6
+ export { DEFAULT_MEDIA_CACHE_DIR, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
7
7
  export type { AnthropicImageBlock, AnthropicImageMimeType, MaterializeMediaOptions, MaterializedCanonAttachment, ReplyWithFileOptions, UploadMediaFileOptions, } from './media.js';
8
8
  export type { SessionConfig, Session } from './session-manager.js';
9
- export type { AgentContext, CanonContactRequest, CanonMessage, CanonConversation, CanonResolvedWorkSession, CanonWorkSession, CanonWorkSessionContext, CanonWorkSessionConversationRole, CanonWorkSessionDisclosureMode, CanonWorkSessionParticipant, CanonWorkSessionStatus, CreateWorkSessionOptions, SendLinkedMessageOptions, SendLinkedMessageResult, SendMessageOptions, CreateConversationOptions, UpdateWorkSessionConversationOptions, } from '@canonmsg/core';
9
+ export type { AgentContext, CanonContactRequest, CanonMessage, CanonConversation, CanonSelfContext, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, CreateConversationOptions, } from '@canonmsg/core';
10
10
  export type { CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, ContactRequestHandler, MessageHandler, MessageHandlerContext, ProgressMessageOptions, ProgressMessageResult, ReachOutOptions, ReachOutResult, 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 { getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, resolveAttachmentMimeType, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
4
+ export { DEFAULT_MEDIA_CACHE_DIR, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
package/dist/media.d.ts CHANGED
@@ -10,6 +10,7 @@ export interface MaterializeMediaOptions {
10
10
  export interface UploadMediaFileOptions {
11
11
  fileName?: string;
12
12
  mimeType?: string;
13
+ durationMs?: number;
13
14
  }
14
15
  export interface ReplyWithFileOptions extends Omit<SendMessageOptions, 'attachments' | 'contentType'>, UploadMediaFileOptions {
15
16
  }
@@ -39,6 +40,7 @@ export interface AnthropicImageBlock {
39
40
  data: string;
40
41
  };
41
42
  }
43
+ export declare const DEFAULT_MEDIA_CACHE_DIR: string;
42
44
  export declare function getMessageAttachments(message: Pick<CanonMessage, 'attachments'>): MediaAttachment[];
43
45
  export declare function materializeAttachment(attachment: MediaAttachment, options: MaterializeMediaOptions & {
44
46
  index?: number;
@@ -49,6 +51,9 @@ export declare function uploadMediaFile(client: CanonClient, conversationId: str
49
51
  url: string;
50
52
  attachment: MediaAttachment;
51
53
  }>;
54
+ export declare function sendMediaFileMessage(client: CanonClient, conversationId: string, filePath: string, text?: string, options?: ReplyWithFileOptions): Promise<{
55
+ messageId: string;
56
+ }>;
52
57
  /**
53
58
  * Resolve the effective MIME type of a materialized attachment, falling back
54
59
  * to filename/URL extensions when the server didn't tell us explicitly.
package/dist/media.js CHANGED
@@ -7,7 +7,7 @@ const ANTHROPIC_IMAGE_MIME_TYPES = new Set([
7
7
  'image/gif',
8
8
  'image/webp',
9
9
  ]);
10
- const DEFAULT_MEDIA_CACHE_DIR = join(CANON_DIR, 'media-cache');
10
+ export const DEFAULT_MEDIA_CACHE_DIR = join(CANON_DIR, 'media-cache');
11
11
  const EXTENSION_BY_MIME = {
12
12
  'application/json': 'json',
13
13
  'application/pdf': 'pdf',
@@ -117,7 +117,7 @@ export async function materializeAttachment(attachment, options) {
117
117
  }
118
118
  responseMimeType = response.headers.get('content-type');
119
119
  const body = Buffer.from(await response.arrayBuffer());
120
- await writeFile(path, body);
120
+ await writeFile(path, body, { mode: 0o644 });
121
121
  }
122
122
  return {
123
123
  ...attachment,
@@ -150,7 +150,33 @@ export async function uploadMediaFile(client, conversationId, filePath, options)
150
150
  const buffer = await readFile(filePath);
151
151
  const mimeType = inferUploadMimeType(filePath, options?.mimeType);
152
152
  const fileName = options?.fileName ?? basename(filePath);
153
- return client.uploadMedia(conversationId, buffer.toString('base64'), mimeType, fileName);
153
+ const uploaded = await client.uploadMedia(conversationId, buffer.toString('base64'), mimeType, fileName);
154
+ if (uploaded.attachment.kind === 'audio'
155
+ && typeof options?.durationMs === 'number'
156
+ && Number.isFinite(options.durationMs)
157
+ && options.durationMs > 0) {
158
+ return {
159
+ ...uploaded,
160
+ attachment: {
161
+ ...uploaded.attachment,
162
+ durationMs: Math.round(options.durationMs),
163
+ },
164
+ };
165
+ }
166
+ return uploaded;
167
+ }
168
+ export async function sendMediaFileMessage(client, conversationId, filePath, text = '', options) {
169
+ const { fileName, mimeType, durationMs, ...sendOptions } = options ?? {};
170
+ const uploaded = await uploadMediaFile(client, conversationId, filePath, {
171
+ ...(fileName ? { fileName } : {}),
172
+ ...(mimeType ? { mimeType } : {}),
173
+ ...(durationMs != null ? { durationMs } : {}),
174
+ });
175
+ return client.sendMessage(conversationId, text, {
176
+ ...sendOptions,
177
+ contentType: uploaded.attachment.kind,
178
+ attachments: [uploaded.attachment],
179
+ });
154
180
  }
155
181
  /**
156
182
  * Resolve the effective MIME type of a materialized attachment, falling back
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export type { AddMemberResult, AgentClientType, CanonRuntimeDescriptor, CanonMessage, CanonConversation, CanonContact, CanonContactRequest, CanonResolveAdmissionResult, ContactAddedPayload, ContactRemovedPayload, ContactSource, AgentContext, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonResolvedWorkSession, CanonWorkSession, CanonWorkSessionContext, CanonWorkSessionConversationRole, CanonWorkSessionDisclosureMode, CanonWorkSessionParticipant, CanonWorkSessionStatus, CreateWorkSessionOptions, SendLinkedMessageOptions, SendLinkedMessageResult, SendMessageOptions, SessionConfig, CreateConversationOptions, TurnLifecycleState, UpdateWorkSessionConversationOptions, } from '@canonmsg/core';
2
- import type { AddMemberResult, CanonMessage, CanonConversation, CanonRuntimeActionDispatch, CreateWorkSessionOptions, SendMessageOptions, SessionConfig, UpdateWorkSessionConversationOptions } from '@canonmsg/core';
1
+ export type { AddMemberResult, AgentClientType, CanonRuntimeDescriptor, 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, CanonMessage, CanonConversation, CanonRuntimeActionDispatch, 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
  /**
@@ -61,20 +61,16 @@ export interface MessageHandlerContext {
61
61
  addMember: (userId: string) => Promise<AddMemberResult>;
62
62
  /** Remove a member from this conversation (requires owner/admin role) */
63
63
  removeMember: (userId: string) => Promise<void>;
64
- /** Create a Canon work session rooted in this conversation. */
65
- createWorkSession: (options?: Omit<CreateWorkSessionOptions, 'conversationId'>) => Promise<import('@canonmsg/core').CanonResolvedWorkSession>;
66
- /** Load this conversation's scoped view of a Canon work session. */
67
- getWorkSession: (workSessionId: string, conversationId?: string) => Promise<import('@canonmsg/core').CanonResolvedWorkSession>;
68
- /** Update or attach this conversation's scoped work-session context. */
69
- updateWorkSessionContext: (workSessionId: string, options?: UpdateWorkSessionConversationOptions) => Promise<import('@canonmsg/core').CanonResolvedWorkSession>;
70
- /** Send into another conversation under an existing or lazily created Canon work session. */
71
- sendLinkedMessage: (targetConversationId: string, text: string, options?: Omit<import('@canonmsg/core').SendLinkedMessageOptions, 'sourceConversationId' | 'targetConversationId' | 'text'>) => Promise<import('@canonmsg/core').SendLinkedMessageResult>;
64
+ /** Send into another Canon conversation with private cross-session self-context. */
65
+ sendContextualMessage: (target: {
66
+ targetConversationId: string;
67
+ } | {
68
+ targetUserId: string;
69
+ }, text: string, options: Omit<import('@canonmsg/core').SendContextualMessageOptions, 'sourceConversationId' | 'targetConversationId' | 'targetUserId' | 'text'>) => Promise<import('@canonmsg/core').SendContextualMessageResult>;
72
70
  /** Trusted agent identity & access context */
73
71
  agent: import('@canonmsg/core').AgentContext;
74
- /** Canon-provided shared task context for this turn, when attached to inbound messages. */
75
- workSession?: import('@canonmsg/core').CanonWorkSessionContext | null;
76
- /** All active Canon work sessions currently linked to this conversation. */
77
- activeWorkSessions?: import('@canonmsg/core').CanonWorkSessionContext[];
72
+ /** Canon-provided private context explaining this agent's cross-session actions. */
73
+ selfContexts?: import('@canonmsg/core').CanonSelfContext[];
78
74
  /** Canon-managed local media access for the current conversation. */
79
75
  media: {
80
76
  materialize: (message?: CanonMessage, options?: Omit<MaterializeMediaOptions, 'agentId' | 'conversationId' | 'messageId'>) => Promise<MaterializedCanonAttachment[]>;
@@ -179,4 +175,8 @@ export interface ReachOutOptions {
179
175
  requestMessage?: string;
180
176
  /** Explicit session setup to use when the contact-card target is an agent. */
181
177
  sessionConfig?: SessionConfig | null;
178
+ /** Source conversation for contextual cross-session reach-outs. */
179
+ sourceConversationId?: string;
180
+ /** Private context for the agent when this reach-out sends a cross-session message. */
181
+ selfContext?: SendContextualSelfContextInput;
182
182
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
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.15.4"
31
+ "@canonmsg/core": "^0.15.5"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"