@canonmsg/agent-sdk 8.5.0 → 8.7.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 +2 -2
- package/dist/bounded-file-read.d.ts +14 -0
- package/dist/bounded-file-read.js +34 -0
- package/dist/canon-agent.js +7 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/media.d.ts +41 -6
- package/dist/media.js +333 -20
- package/dist/types.d.ts +3 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -356,8 +356,8 @@ agent.on('message', async ({ messages, media }) => {
|
|
|
356
356
|
});
|
|
357
357
|
```
|
|
358
358
|
|
|
359
|
-
- `media.materialize(message?)`
|
|
360
|
-
- `media.uploadFile(path, options?)`
|
|
359
|
+
- `media.materialize(message?, options?)` streams attachments on demand into `~/.canon/media-cache`. Automatic materialization is capped at 10 MiB per attachment before and during the download. A runtime that deliberately needs a larger input can pass `maxBytes`, up to 100 MiB. Failed or oversized attachments are reported through optional `onError`; valid siblings are still returned. Cancellation still aborts the complete operation. Cache files are published through an atomic rename. The cache is persistent and currently has no aggregate quota or automatic eviction, so hosted-agent operators should monitor or periodically clear it.
|
|
360
|
+
- `media.uploadFile(path, options?)` streams a local file (up to 100 MiB) through Canon's private resumable-capable upload lane and returns `{ uploadId, url, attachment }`. The attachment carries the same server-issued `uploadId`, allowing a subsequent durable message/card write to retain the temporary upload. File bytes are never base64-buffered in the SDK. The current helper uses one streamed PUT; a failed application-level attempt starts a new Canon session rather than resuming an acknowledged byte range in the old session.
|
|
361
361
|
- `media.replyWithFile(path, text?, options?)` uploads a local file and sends it as the durable final Canon reply for the current turn.
|
|
362
362
|
|
|
363
363
|
GIFs are regular image attachments. Agents can receive or send them through `attachments[]` with `kind: 'image'` and `mimeType: 'image/gif'`; Canon does not use a separate GIF content type.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FileHandle } from 'node:fs/promises';
|
|
2
|
+
export type BoundedFileReadFailure = 'not-file' | 'too-large';
|
|
3
|
+
export declare class BoundedFileReadError extends Error {
|
|
4
|
+
readonly failure: BoundedFileReadFailure;
|
|
5
|
+
constructor(failure: BoundedFileReadFailure);
|
|
6
|
+
}
|
|
7
|
+
type ReadableFileHandle = Pick<FileHandle, 'stat' | 'read'>;
|
|
8
|
+
/**
|
|
9
|
+
* Read a regular file through an already-open handle without ever requesting
|
|
10
|
+
* more than maxBytes + 1. The extra byte detects a file that grew after stat
|
|
11
|
+
* without allowing an unbounded path-based read.
|
|
12
|
+
*/
|
|
13
|
+
export declare function readBoundedRegularFileHandle(handle: ReadableFileHandle, maxBytes: number): Promise<Buffer>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export class BoundedFileReadError extends Error {
|
|
2
|
+
failure;
|
|
3
|
+
constructor(failure) {
|
|
4
|
+
super(failure);
|
|
5
|
+
this.failure = failure;
|
|
6
|
+
this.name = 'BoundedFileReadError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Read a regular file through an already-open handle without ever requesting
|
|
11
|
+
* more than maxBytes + 1. The extra byte detects a file that grew after stat
|
|
12
|
+
* without allowing an unbounded path-based read.
|
|
13
|
+
*/
|
|
14
|
+
export async function readBoundedRegularFileHandle(handle, maxBytes) {
|
|
15
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
|
|
16
|
+
throw new Error('maxBytes must be a positive safe integer');
|
|
17
|
+
}
|
|
18
|
+
const fileStat = await handle.stat();
|
|
19
|
+
if (!fileStat.isFile())
|
|
20
|
+
throw new BoundedFileReadError('not-file');
|
|
21
|
+
if (fileStat.size > maxBytes)
|
|
22
|
+
throw new BoundedFileReadError('too-large');
|
|
23
|
+
const buffer = Buffer.allocUnsafe(maxBytes + 1);
|
|
24
|
+
let totalBytes = 0;
|
|
25
|
+
while (totalBytes < buffer.byteLength) {
|
|
26
|
+
const { bytesRead } = await handle.read(buffer, totalBytes, buffer.byteLength - totalBytes, totalBytes);
|
|
27
|
+
if (bytesRead === 0)
|
|
28
|
+
break;
|
|
29
|
+
totalBytes += bytesRead;
|
|
30
|
+
}
|
|
31
|
+
if (totalBytes > maxBytes)
|
|
32
|
+
throw new BoundedFileReadError('too-large');
|
|
33
|
+
return buffer.subarray(0, totalBytes);
|
|
34
|
+
}
|
package/dist/canon-agent.js
CHANGED
|
@@ -2028,7 +2028,10 @@ export class CanonAgent {
|
|
|
2028
2028
|
return result;
|
|
2029
2029
|
}
|
|
2030
2030
|
};
|
|
2031
|
-
const uploadFile = (filePath, options) => uploadMediaFile(this.apiClient, conversationId, filePath,
|
|
2031
|
+
const uploadFile = (filePath, options) => uploadMediaFile(this.apiClient, conversationId, filePath, {
|
|
2032
|
+
...(options ?? {}),
|
|
2033
|
+
signal: options?.signal ?? abortController.signal,
|
|
2034
|
+
});
|
|
2032
2035
|
const replyWithFile = async (filePath, text = '', options) => {
|
|
2033
2036
|
throwIfAborted();
|
|
2034
2037
|
try {
|
|
@@ -2057,6 +2060,8 @@ export class CanonAgent {
|
|
|
2057
2060
|
...(options?.fileName ? { fileName: options.fileName } : {}),
|
|
2058
2061
|
...(options?.mimeType ? { mimeType: options.mimeType } : {}),
|
|
2059
2062
|
...(options?.durationMs != null ? { durationMs: options.durationMs } : {}),
|
|
2063
|
+
...(options?.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
|
|
2064
|
+
signal: options?.signal ?? abortController.signal,
|
|
2060
2065
|
});
|
|
2061
2066
|
durableMessageSequence += 1;
|
|
2062
2067
|
await sleep(FINAL_MESSAGE_HANDOFF_MS);
|
|
@@ -2108,6 +2113,7 @@ export class CanonAgent {
|
|
|
2108
2113
|
agentId: agent.agentId,
|
|
2109
2114
|
conversationId,
|
|
2110
2115
|
...(options ?? {}),
|
|
2116
|
+
signal: options?.signal ?? abortController.signal,
|
|
2111
2117
|
});
|
|
2112
2118
|
},
|
|
2113
2119
|
uploadFile,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export { CanonAgent } from './canon-agent.js';
|
|
2
2
|
export type { AgentContactsAPI, AgentConversationsAPI, AgentUsersAPI } from './canon-agent.js';
|
|
3
3
|
export { ApprovalManager, buildApprovalOutcome, buildApprovalReply, buildApprovalRequest, CanonApiError, DEFAULT_APPROVAL_CONFIG, generateApprovalId, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, parseApprovalReplyMetadata, parseApprovalRequestMetadata, parseSessionRule, redactSecrets, } from '@canonmsg/core';
|
|
4
|
-
export type { ApprovalConfig, ApprovalNativeRequestMetadata, ApprovalOutcomeMetadata, ApprovalReplyMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalResult, ApprovalRisk, CanonContact, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeTurnModeActivation, CanonRuntimeTurnModeDescriptor, CanonRuntimeTurnModeScope, CanonRuntimeFact, CanonRuntimeFactGroup, CanonResolveAdmissionResult, ContactAddedPayload, ContactCardPayload, ContactRemovedPayload, ContactSource, HostAdmissionActionCapabilities, ParticipationSuppressedPayload, ResolveAdmissionTargetInput, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, RuntimeInputAnswers, RuntimeInputChoice, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, SessionRule, } from '@canonmsg/core';
|
|
4
|
+
export type { ApprovalConfig, ApprovalNativeRequestMetadata, ApprovalOutcomeMetadata, ApprovalReplyMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalResult, ApprovalRisk, CanonContact, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeTurnModeActivation, CanonRuntimeTurnModeDescriptor, CanonRuntimeTurnModeScope, CanonRuntimeFact, CanonRuntimeFactGroup, CanonResolveAdmissionResult, ContactAddedPayload, ContactCardPayload, ContactRemovedPayload, ContactSource, HostAdmissionActionCapabilities, ParticipationSuppressedPayload, ResolveAdmissionTargetInput, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, ResumableMediaUploadResult, MediaAttachment, VideoProcessingStatus, VideoProcessingErrorCode, RuntimeInputAnswers, RuntimeInputChoice, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, SessionRule, } from '@canonmsg/core';
|
|
5
5
|
export { SessionManager } from './session-manager.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';
|
|
6
|
+
export { DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES, DEFAULT_MEDIA_CACHE_DIR, DEFAULT_MEDIA_MATERIALIZATION_BYTES, MAX_ANTHROPIC_IMAGE_RAW_BYTES, MAX_ANTHROPIC_REQUEST_BYTES, MAX_CANON_MEDIA_BYTES, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, materializeReplyContextMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, toAnthropicImageBlocksWithinBudget, uploadMediaFile, } from './media.js';
|
|
7
|
+
export type { AnthropicImageBlock, AnthropicImageBudgetOptions, AnthropicImageMimeType, MaterializeMediaOptions, MaterializedCanonAttachment, MaterializedCanonReplyContext, ReplyWithFileOptions, UploadMediaFileOptions, } from './media.js';
|
|
8
8
|
export type { SessionConfig, Session } from './session-manager.js';
|
|
9
9
|
export type { CanonAgentTurnVerbosityOption } from './turn-verbosity-option.js';
|
|
10
10
|
export type { AgentContext, CanonGroupContext, CanonKnownRecentParticipant, CanonMembershipChange, CanonContactRequest, GroupInviteRequirements, CanonMessage, CanonConversation, CanonConversationsPage, CanonConversationsPageOptions, CanonReplyContext, CanonSelfContext, CanonTurnContextV2, CanonRuntimeDescriptor, MessageUpdatedPayload, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, CreateConversationOptions, CreateConversationResult, DirectSessionSelection, TurnVerbosity, TurnVerbosityConfig, } from '@canonmsg/core';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { CanonAgent } from './canon-agent.js';
|
|
2
2
|
export { ApprovalManager, buildApprovalOutcome, buildApprovalReply, buildApprovalRequest, CanonApiError, DEFAULT_APPROVAL_CONFIG, generateApprovalId, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, parseApprovalReplyMetadata, parseApprovalRequestMetadata, parseSessionRule, redactSecrets, } from '@canonmsg/core';
|
|
3
3
|
export { SessionManager } from './session-manager.js';
|
|
4
|
-
export { DEFAULT_MEDIA_CACHE_DIR, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, materializeReplyContextMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
|
|
4
|
+
export { DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES, DEFAULT_MEDIA_CACHE_DIR, DEFAULT_MEDIA_MATERIALIZATION_BYTES, MAX_ANTHROPIC_IMAGE_RAW_BYTES, MAX_ANTHROPIC_REQUEST_BYTES, MAX_CANON_MEDIA_BYTES, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, materializeReplyContextMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, toAnthropicImageBlocksWithinBudget, uploadMediaFile, } from './media.js';
|
package/dist/media.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CanonClient, type CanonReplyContext, type CanonMessage, type MediaAttachment, type SendMessageOptions } from '@canonmsg/core';
|
|
1
|
+
import { CanonClient, type CanonReplyContext, type CanonMessage, type MediaAttachment, type ResumableMediaUploadResult, type SendMessageOptions } from '@canonmsg/core';
|
|
2
2
|
export interface MaterializeMediaOptions {
|
|
3
3
|
agentId: string;
|
|
4
4
|
conversationId: string;
|
|
@@ -6,11 +6,20 @@ export interface MaterializeMediaOptions {
|
|
|
6
6
|
rootDir?: string;
|
|
7
7
|
fetchImpl?: typeof fetch;
|
|
8
8
|
signal?: AbortSignal;
|
|
9
|
+
/**
|
|
10
|
+
* Maximum bytes written to disk per attachment. Automatic materialization
|
|
11
|
+
* defaults to 10 MiB; runtimes may opt in up to Canon's 100 MiB upload cap.
|
|
12
|
+
*/
|
|
13
|
+
maxBytes?: number;
|
|
14
|
+
/** Called for each attachment that could not be materialized. */
|
|
15
|
+
onError?: (error: unknown, attachment: MediaAttachment, index: number) => void;
|
|
9
16
|
}
|
|
10
17
|
export interface UploadMediaFileOptions {
|
|
11
18
|
fileName?: string;
|
|
12
19
|
mimeType?: string;
|
|
13
20
|
durationMs?: number;
|
|
21
|
+
fetchImpl?: typeof fetch;
|
|
22
|
+
signal?: AbortSignal;
|
|
14
23
|
}
|
|
15
24
|
export interface ReplyWithFileOptions extends Omit<SendMessageOptions, 'attachments' | 'contentType'>, UploadMediaFileOptions {
|
|
16
25
|
}
|
|
@@ -44,7 +53,27 @@ export interface AnthropicImageBlock {
|
|
|
44
53
|
data: string;
|
|
45
54
|
};
|
|
46
55
|
}
|
|
56
|
+
export interface AnthropicImageBudgetOptions {
|
|
57
|
+
/** Text content that shares the Anthropic request with the image blocks. */
|
|
58
|
+
promptText: string;
|
|
59
|
+
/** Optional lower per-image limit for providers with smaller image caps. */
|
|
60
|
+
maxImageRawBytes?: number;
|
|
61
|
+
/** Optional lower total request ceiling. Defaults to Anthropic's 32 MB limit. */
|
|
62
|
+
maxRequestBytes?: number;
|
|
63
|
+
/** Bytes reserved for SDK/request framing outside these content blocks. */
|
|
64
|
+
requestHeadroomBytes?: number;
|
|
65
|
+
}
|
|
47
66
|
export declare const DEFAULT_MEDIA_CACHE_DIR: string;
|
|
67
|
+
export declare const DEFAULT_MEDIA_MATERIALIZATION_BYTES: number;
|
|
68
|
+
export declare const MAX_CANON_MEDIA_BYTES: number;
|
|
69
|
+
/**
|
|
70
|
+
* Seven MiB expands to just under 10 MB when base64-encoded. This is suitable
|
|
71
|
+
* for the direct Anthropic transport used by the Claude Code host. Providers
|
|
72
|
+
* with smaller encoded-image limits must pass a lower maxImageRawBytes value.
|
|
73
|
+
*/
|
|
74
|
+
export declare const MAX_ANTHROPIC_IMAGE_RAW_BYTES: number;
|
|
75
|
+
export declare const MAX_ANTHROPIC_REQUEST_BYTES = 32000000;
|
|
76
|
+
export declare const DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES: number;
|
|
48
77
|
export declare function getMessageAttachments(message: Pick<CanonMessage, 'attachments'>): MediaAttachment[];
|
|
49
78
|
export declare function materializeAttachment(attachment: MediaAttachment, options: MaterializeMediaOptions & {
|
|
50
79
|
index?: number;
|
|
@@ -52,10 +81,7 @@ export declare function materializeAttachment(attachment: MediaAttachment, optio
|
|
|
52
81
|
export declare function materializeMessageMedia(message: Pick<CanonMessage, 'id' | 'attachments'>, options: Omit<MaterializeMediaOptions, 'messageId'>): Promise<MaterializedCanonAttachment[]>;
|
|
53
82
|
export declare function materializeReplyContextMedia(replyContext: CanonReplyContext | null, options: Omit<MaterializeMediaOptions, 'messageId'>): Promise<MaterializedCanonReplyContext>;
|
|
54
83
|
export declare function inferUploadMimeType(filePath: string, overrideMimeType?: string): string;
|
|
55
|
-
export declare function uploadMediaFile(client: CanonClient, conversationId: string, filePath: string, options?: UploadMediaFileOptions): Promise<
|
|
56
|
-
url: string;
|
|
57
|
-
attachment: MediaAttachment;
|
|
58
|
-
}>;
|
|
84
|
+
export declare function uploadMediaFile(client: CanonClient, conversationId: string, filePath: string, options?: UploadMediaFileOptions): Promise<ResumableMediaUploadResult>;
|
|
59
85
|
export declare function sendMediaFileMessage(client: CanonClient, conversationId: string, filePath: string, text?: string, options?: ReplyWithFileOptions): Promise<{
|
|
60
86
|
messageId: string;
|
|
61
87
|
}>;
|
|
@@ -75,7 +101,16 @@ export declare function isAnthropicImageAttachment(attachment: MaterializedCanon
|
|
|
75
101
|
* Callers should first check `isAnthropicImageAttachment`; this function
|
|
76
102
|
* throws if the MIME type is not supported.
|
|
77
103
|
*/
|
|
78
|
-
export declare function toAnthropicImageBlock(attachment: MaterializedCanonAttachment
|
|
104
|
+
export declare function toAnthropicImageBlock(attachment: MaterializedCanonAttachment, options?: {
|
|
105
|
+
maxRawBytes?: number;
|
|
106
|
+
}): Promise<AnthropicImageBlock>;
|
|
107
|
+
/**
|
|
108
|
+
* Convert only the supported images that fit both the direct-image limit and
|
|
109
|
+
* the aggregate request budget. Skipped images remain represented by the
|
|
110
|
+
* paths already rendered into promptText; one unavailable image never drops
|
|
111
|
+
* valid siblings.
|
|
112
|
+
*/
|
|
113
|
+
export declare function toAnthropicImageBlocksWithinBudget(attachments: ReadonlyArray<MaterializedCanonAttachment>, options: AnthropicImageBudgetOptions): Promise<AnthropicImageBlock[]>;
|
|
79
114
|
/**
|
|
80
115
|
* Return the local path to pass to Codex via its `-i/--image` flag when the
|
|
81
116
|
* attachment is an image Codex can consume. Codex accepts the same MIME types
|
package/dist/media.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createReadStream, createWriteStream } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { mkdir, open, rename, stat, unlink } from 'node:fs/promises';
|
|
2
4
|
import { basename, dirname, extname, join } from 'node:path';
|
|
5
|
+
import { Readable, Transform } from 'node:stream';
|
|
6
|
+
import { pipeline } from 'node:stream/promises';
|
|
3
7
|
import { CANON_DIR, renderCanonHostInboundContent, } from '@canonmsg/core';
|
|
8
|
+
import { BoundedFileReadError, readBoundedRegularFileHandle, } from './bounded-file-read.js';
|
|
4
9
|
const ANTHROPIC_IMAGE_MIME_TYPES = new Set([
|
|
5
10
|
'image/jpeg',
|
|
6
11
|
'image/png',
|
|
@@ -8,6 +13,16 @@ const ANTHROPIC_IMAGE_MIME_TYPES = new Set([
|
|
|
8
13
|
'image/webp',
|
|
9
14
|
]);
|
|
10
15
|
export const DEFAULT_MEDIA_CACHE_DIR = join(CANON_DIR, 'media-cache');
|
|
16
|
+
export const DEFAULT_MEDIA_MATERIALIZATION_BYTES = 10 * 1024 * 1024;
|
|
17
|
+
export const MAX_CANON_MEDIA_BYTES = 100 * 1024 * 1024;
|
|
18
|
+
/**
|
|
19
|
+
* Seven MiB expands to just under 10 MB when base64-encoded. This is suitable
|
|
20
|
+
* for the direct Anthropic transport used by the Claude Code host. Providers
|
|
21
|
+
* with smaller encoded-image limits must pass a lower maxImageRawBytes value.
|
|
22
|
+
*/
|
|
23
|
+
export const MAX_ANTHROPIC_IMAGE_RAW_BYTES = 7 * 1024 * 1024;
|
|
24
|
+
export const MAX_ANTHROPIC_REQUEST_BYTES = 32_000_000;
|
|
25
|
+
export const DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES = 4 * 1024 * 1024;
|
|
11
26
|
const EXTENSION_BY_MIME = {
|
|
12
27
|
'application/json': 'json',
|
|
13
28
|
'application/pdf': 'pdf',
|
|
@@ -100,9 +115,75 @@ async function fileExists(path) {
|
|
|
100
115
|
return false;
|
|
101
116
|
}
|
|
102
117
|
}
|
|
118
|
+
function resolveMaxBytes(value) {
|
|
119
|
+
if (value === undefined)
|
|
120
|
+
return DEFAULT_MEDIA_MATERIALIZATION_BYTES;
|
|
121
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > MAX_CANON_MEDIA_BYTES) {
|
|
122
|
+
throw new Error(`maxBytes must be an integer between 1 and ${MAX_CANON_MEDIA_BYTES}`);
|
|
123
|
+
}
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
function reportMaterializationError(options, error, attachment, index) {
|
|
127
|
+
try {
|
|
128
|
+
options.onError?.(error, attachment, index);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Diagnostics must never turn a recoverable sibling failure into a failed
|
|
132
|
+
// message materialization.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function materializeAvailableAttachments(inputs, options) {
|
|
136
|
+
const settled = await Promise.allSettled(inputs.map((input) => input.run()));
|
|
137
|
+
options.signal?.throwIfAborted();
|
|
138
|
+
const materialized = [];
|
|
139
|
+
let firstError;
|
|
140
|
+
let hasFailure = false;
|
|
141
|
+
settled.forEach((result, resultIndex) => {
|
|
142
|
+
if (result.status === 'fulfilled') {
|
|
143
|
+
materialized.push(result.value);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const input = inputs[resultIndex];
|
|
147
|
+
if (!hasFailure) {
|
|
148
|
+
firstError = result.reason;
|
|
149
|
+
hasFailure = true;
|
|
150
|
+
}
|
|
151
|
+
reportMaterializationError(options, result.reason, input.attachment, input.index);
|
|
152
|
+
});
|
|
153
|
+
// Preserve the strict all-failed behavior so callers still learn when no
|
|
154
|
+
// bytes were available, while allowing valid siblings through unchanged.
|
|
155
|
+
if (materialized.length === 0 && hasFailure) {
|
|
156
|
+
throw firstError;
|
|
157
|
+
}
|
|
158
|
+
return materialized;
|
|
159
|
+
}
|
|
160
|
+
function byteLimitTransform(maxBytes) {
|
|
161
|
+
let total = 0;
|
|
162
|
+
return {
|
|
163
|
+
stream: new Transform({
|
|
164
|
+
transform(chunk, _encoding, callback) {
|
|
165
|
+
total += chunk.length;
|
|
166
|
+
if (total > maxBytes) {
|
|
167
|
+
callback(new Error(`Canon media exceeds the ${maxBytes}-byte materialization limit`));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
callback(null, chunk);
|
|
171
|
+
},
|
|
172
|
+
}),
|
|
173
|
+
bytesRead: () => total,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
103
176
|
export function getMessageAttachments(message) {
|
|
104
177
|
return Array.isArray(message.attachments) ? message.attachments : [];
|
|
105
178
|
}
|
|
179
|
+
function isMaterializableAttachment(attachment) {
|
|
180
|
+
// Missing status is the legacy-ready contract. Processing URLs are reserved
|
|
181
|
+
// future capabilities and failed videos intentionally have no playable
|
|
182
|
+
// object, so neither should trigger a network request from an agent runtime.
|
|
183
|
+
return attachment.kind !== 'video'
|
|
184
|
+
|| attachment.processingStatus === undefined
|
|
185
|
+
|| attachment.processingStatus === 'ready';
|
|
186
|
+
}
|
|
106
187
|
export async function materializeAttachment(attachment, options) {
|
|
107
188
|
const path = buildCachePath({
|
|
108
189
|
agentId: options.agentId,
|
|
@@ -113,7 +194,25 @@ export async function materializeAttachment(attachment, options) {
|
|
|
113
194
|
rootDir: options.rootDir,
|
|
114
195
|
});
|
|
115
196
|
await mkdir(dirname(path), { recursive: true });
|
|
197
|
+
const maxBytes = resolveMaxBytes(options.maxBytes);
|
|
198
|
+
const expectedBytes = typeof attachment.sizeBytes === 'number'
|
|
199
|
+
&& Number.isSafeInteger(attachment.sizeBytes)
|
|
200
|
+
&& attachment.sizeBytes >= 0
|
|
201
|
+
? attachment.sizeBytes
|
|
202
|
+
: null;
|
|
203
|
+
if (expectedBytes !== null && expectedBytes > maxBytes) {
|
|
204
|
+
throw new Error(`Canon media exceeds the ${maxBytes}-byte materialization limit`);
|
|
205
|
+
}
|
|
116
206
|
let responseMimeType = null;
|
|
207
|
+
if (await fileExists(path)) {
|
|
208
|
+
const info = await stat(path);
|
|
209
|
+
if (info.size > maxBytes) {
|
|
210
|
+
throw new Error(`Cached Canon media exceeds the ${maxBytes}-byte materialization limit`);
|
|
211
|
+
}
|
|
212
|
+
if (expectedBytes !== null && info.size !== expectedBytes) {
|
|
213
|
+
await unlink(path).catch(() => { });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
117
216
|
if (!(await fileExists(path))) {
|
|
118
217
|
const fetchImpl = ensureFetch(options.fetchImpl);
|
|
119
218
|
const response = await fetchImpl(attachment.url, {
|
|
@@ -123,8 +222,33 @@ export async function materializeAttachment(attachment, options) {
|
|
|
123
222
|
throw new Error(`Failed to download Canon media (${response.status} ${response.statusText})`);
|
|
124
223
|
}
|
|
125
224
|
responseMimeType = response.headers.get('content-type');
|
|
126
|
-
const
|
|
127
|
-
|
|
225
|
+
const contentLength = response.headers.get('content-length');
|
|
226
|
+
const declaredLength = contentLength === null ? null : Number(contentLength);
|
|
227
|
+
if (declaredLength !== null && Number.isFinite(declaredLength) && declaredLength > maxBytes) {
|
|
228
|
+
throw new Error(`Canon media exceeds the ${maxBytes}-byte materialization limit`);
|
|
229
|
+
}
|
|
230
|
+
if (expectedBytes !== null
|
|
231
|
+
&& declaredLength !== null
|
|
232
|
+
&& Number.isFinite(declaredLength)
|
|
233
|
+
&& declaredLength !== expectedBytes) {
|
|
234
|
+
throw new Error('Canon media response length does not match attachment metadata');
|
|
235
|
+
}
|
|
236
|
+
if (!response.body) {
|
|
237
|
+
throw new Error('Canon media response had no body');
|
|
238
|
+
}
|
|
239
|
+
const tempPath = `${path}.${process.pid}.${randomUUID()}.part`;
|
|
240
|
+
const limiter = byteLimitTransform(maxBytes);
|
|
241
|
+
try {
|
|
242
|
+
await pipeline(Readable.fromWeb(response.body), limiter.stream, createWriteStream(tempPath, { mode: 0o600, flags: 'wx' }));
|
|
243
|
+
if (expectedBytes !== null && limiter.bytesRead() !== expectedBytes) {
|
|
244
|
+
throw new Error('Canon media response length does not match attachment metadata');
|
|
245
|
+
}
|
|
246
|
+
await rename(tempPath, path);
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
await unlink(tempPath).catch(() => { });
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
128
252
|
}
|
|
129
253
|
return {
|
|
130
254
|
...attachment,
|
|
@@ -142,23 +266,34 @@ export async function materializeAttachment(attachment, options) {
|
|
|
142
266
|
}
|
|
143
267
|
export async function materializeMessageMedia(message, options) {
|
|
144
268
|
const attachments = getMessageAttachments(message);
|
|
145
|
-
return
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
269
|
+
return materializeAvailableAttachments(attachments.flatMap((attachment, index) => (isMaterializableAttachment(attachment)
|
|
270
|
+
? [{
|
|
271
|
+
attachment,
|
|
272
|
+
index,
|
|
273
|
+
run: () => materializeAttachment(attachment, {
|
|
274
|
+
...options,
|
|
275
|
+
messageId: message.id,
|
|
276
|
+
index,
|
|
277
|
+
}),
|
|
278
|
+
}]
|
|
279
|
+
: [])), options);
|
|
150
280
|
}
|
|
151
281
|
export async function materializeReplyContextMedia(replyContext, options) {
|
|
152
282
|
if (!replyContext?.found || !replyContext.attachments?.length) {
|
|
153
283
|
return { replyContext, materialized: [] };
|
|
154
284
|
}
|
|
155
|
-
const
|
|
156
|
-
?
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
285
|
+
const materializable = replyContext.attachments.flatMap((attachment, index) => attachment.url && isMaterializableAttachment(attachment)
|
|
286
|
+
? [{
|
|
287
|
+
attachment,
|
|
288
|
+
index,
|
|
289
|
+
run: () => materializeAttachment(attachment, {
|
|
290
|
+
...options,
|
|
291
|
+
messageId: replyContext.messageId,
|
|
292
|
+
index,
|
|
293
|
+
}),
|
|
294
|
+
}]
|
|
295
|
+
: []);
|
|
296
|
+
const materialized = await materializeAvailableAttachments(materializable, options);
|
|
162
297
|
return {
|
|
163
298
|
replyContext: {
|
|
164
299
|
...replyContext,
|
|
@@ -179,10 +314,77 @@ export function inferUploadMimeType(filePath, overrideMimeType) {
|
|
|
179
314
|
return MIME_BY_EXTENSION[extname(filePath).toLowerCase()] ?? 'application/octet-stream';
|
|
180
315
|
}
|
|
181
316
|
export async function uploadMediaFile(client, conversationId, filePath, options) {
|
|
182
|
-
|
|
317
|
+
options?.signal?.throwIfAborted();
|
|
318
|
+
const fileStat = await stat(filePath);
|
|
319
|
+
if (!fileStat.isFile())
|
|
320
|
+
throw new Error(`Canon media path is not a file: ${filePath}`);
|
|
321
|
+
if (fileStat.size <= 0)
|
|
322
|
+
throw new Error('Canon media file is empty');
|
|
323
|
+
if (fileStat.size > MAX_CANON_MEDIA_BYTES) {
|
|
324
|
+
throw new Error(`Canon media exceeds the ${MAX_CANON_MEDIA_BYTES}-byte upload limit`);
|
|
325
|
+
}
|
|
183
326
|
const mimeType = inferUploadMimeType(filePath, options?.mimeType);
|
|
184
327
|
const fileName = options?.fileName ?? basename(filePath);
|
|
185
|
-
const
|
|
328
|
+
const session = await client.createResumableMediaUpload(conversationId, fileStat.size, mimeType, fileName);
|
|
329
|
+
options?.signal?.throwIfAborted();
|
|
330
|
+
if (typeof session.uploadId !== 'string' || session.uploadId.length === 0) {
|
|
331
|
+
throw new Error('Canon returned an invalid resumable upload ID');
|
|
332
|
+
}
|
|
333
|
+
if (session.sizeBytes !== fileStat.size) {
|
|
334
|
+
throw new Error('Canon resumable upload size does not match the local file');
|
|
335
|
+
}
|
|
336
|
+
if (typeof session.mimeType !== 'string' || session.mimeType.length === 0) {
|
|
337
|
+
throw new Error('Canon returned an invalid resumable upload MIME type');
|
|
338
|
+
}
|
|
339
|
+
let uploadUrl;
|
|
340
|
+
try {
|
|
341
|
+
uploadUrl = new URL(session.uploadUrl);
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
throw new Error('Canon returned an invalid resumable upload URL');
|
|
345
|
+
}
|
|
346
|
+
if (uploadUrl.protocol !== 'https:') {
|
|
347
|
+
throw new Error('Canon returned a non-HTTPS resumable upload URL');
|
|
348
|
+
}
|
|
349
|
+
if (uploadUrl.hostname !== 'storage.googleapis.com'
|
|
350
|
+
|| uploadUrl.username.length > 0
|
|
351
|
+
|| uploadUrl.password.length > 0) {
|
|
352
|
+
throw new Error('Canon returned an untrusted resumable upload URL');
|
|
353
|
+
}
|
|
354
|
+
const fetchImpl = ensureFetch(options?.fetchImpl);
|
|
355
|
+
const uploadResponse = await fetchImpl(uploadUrl, {
|
|
356
|
+
method: 'PUT',
|
|
357
|
+
headers: {
|
|
358
|
+
'Content-Type': session.mimeType,
|
|
359
|
+
'Content-Length': String(fileStat.size),
|
|
360
|
+
'Content-Range': `bytes 0-${fileStat.size - 1}/${fileStat.size}`,
|
|
361
|
+
},
|
|
362
|
+
body: createReadStream(filePath),
|
|
363
|
+
signal: options?.signal,
|
|
364
|
+
// Node requires this for streaming request bodies. It is intentionally
|
|
365
|
+
// outside the DOM RequestInit type but supported by the built-in fetch.
|
|
366
|
+
duplex: 'half',
|
|
367
|
+
});
|
|
368
|
+
if (!uploadResponse.ok) {
|
|
369
|
+
throw new Error(`Failed to upload Canon media (${uploadResponse.status} ${uploadResponse.statusText})`);
|
|
370
|
+
}
|
|
371
|
+
options?.signal?.throwIfAborted();
|
|
372
|
+
const finalized = await client.finalizeResumableMediaUpload(session.uploadId);
|
|
373
|
+
options?.signal?.throwIfAborted();
|
|
374
|
+
if (finalized.uploadId !== session.uploadId) {
|
|
375
|
+
throw new Error('Canon resumable upload result does not match its session');
|
|
376
|
+
}
|
|
377
|
+
if (finalized.attachment.uploadId !== undefined
|
|
378
|
+
&& finalized.attachment.uploadId !== session.uploadId) {
|
|
379
|
+
throw new Error('Canon resumable upload attachment does not match its session');
|
|
380
|
+
}
|
|
381
|
+
const uploaded = {
|
|
382
|
+
...finalized,
|
|
383
|
+
attachment: {
|
|
384
|
+
...finalized.attachment,
|
|
385
|
+
uploadId: session.uploadId,
|
|
386
|
+
},
|
|
387
|
+
};
|
|
186
388
|
if (uploaded.attachment.kind === 'audio'
|
|
187
389
|
&& typeof options?.durationMs === 'number'
|
|
188
390
|
&& Number.isFinite(options.durationMs)
|
|
@@ -198,12 +400,15 @@ export async function uploadMediaFile(client, conversationId, filePath, options)
|
|
|
198
400
|
return uploaded;
|
|
199
401
|
}
|
|
200
402
|
export async function sendMediaFileMessage(client, conversationId, filePath, text = '', options) {
|
|
201
|
-
const { fileName, mimeType, durationMs, ...sendOptions } = options ?? {};
|
|
403
|
+
const { fileName, mimeType, durationMs, fetchImpl, signal, ...sendOptions } = options ?? {};
|
|
202
404
|
const uploaded = await uploadMediaFile(client, conversationId, filePath, {
|
|
203
405
|
...(fileName ? { fileName } : {}),
|
|
204
406
|
...(mimeType ? { mimeType } : {}),
|
|
205
407
|
...(durationMs != null ? { durationMs } : {}),
|
|
408
|
+
...(fetchImpl ? { fetchImpl } : {}),
|
|
409
|
+
...(signal ? { signal } : {}),
|
|
206
410
|
});
|
|
411
|
+
signal?.throwIfAborted();
|
|
207
412
|
return client.sendMessage(conversationId, text, {
|
|
208
413
|
...sendOptions,
|
|
209
414
|
contentType: uploaded.attachment.kind,
|
|
@@ -247,12 +452,39 @@ export function isAnthropicImageAttachment(attachment) {
|
|
|
247
452
|
* Callers should first check `isAnthropicImageAttachment`; this function
|
|
248
453
|
* throws if the MIME type is not supported.
|
|
249
454
|
*/
|
|
250
|
-
export async function toAnthropicImageBlock(attachment) {
|
|
455
|
+
export async function toAnthropicImageBlock(attachment, options) {
|
|
251
456
|
if (!isAnthropicImageAttachment(attachment)) {
|
|
252
457
|
throw new Error(`Canon attachment ${attachment.index} is not a supported Anthropic image (kind=${attachment.kind}, mime=${attachment.mimeType ?? 'unknown'})`);
|
|
253
458
|
}
|
|
459
|
+
const maxRawBytes = options?.maxRawBytes ?? MAX_ANTHROPIC_IMAGE_RAW_BYTES;
|
|
460
|
+
if (!Number.isSafeInteger(maxRawBytes)
|
|
461
|
+
|| maxRawBytes <= 0
|
|
462
|
+
|| maxRawBytes > MAX_ANTHROPIC_IMAGE_RAW_BYTES) {
|
|
463
|
+
throw new Error(`maxRawBytes must be an integer between 1 and ${MAX_ANTHROPIC_IMAGE_RAW_BYTES}`);
|
|
464
|
+
}
|
|
254
465
|
const mediaType = resolveAttachmentMimeType(attachment);
|
|
255
|
-
const buffer = await
|
|
466
|
+
const buffer = await readAnthropicImageBytes(attachment, maxRawBytes);
|
|
467
|
+
return buildAnthropicImageBlock(buffer, mediaType);
|
|
468
|
+
}
|
|
469
|
+
async function readAnthropicImageBytes(attachment, maxRawBytes) {
|
|
470
|
+
const handle = await open(attachment.path, 'r');
|
|
471
|
+
try {
|
|
472
|
+
return await readBoundedRegularFileHandle(handle, maxRawBytes);
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
if (error instanceof BoundedFileReadError) {
|
|
476
|
+
if (error.failure === 'not-file') {
|
|
477
|
+
throw new Error(`Canon attachment ${attachment.index} is not a file`);
|
|
478
|
+
}
|
|
479
|
+
throw new Error(`Canon attachment ${attachment.index} exceeds the ${maxRawBytes}-byte Anthropic image limit`);
|
|
480
|
+
}
|
|
481
|
+
throw error;
|
|
482
|
+
}
|
|
483
|
+
finally {
|
|
484
|
+
await handle.close();
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function buildAnthropicImageBlock(buffer, mediaType) {
|
|
256
488
|
return {
|
|
257
489
|
type: 'image',
|
|
258
490
|
source: {
|
|
@@ -262,6 +494,87 @@ export async function toAnthropicImageBlock(attachment) {
|
|
|
262
494
|
},
|
|
263
495
|
};
|
|
264
496
|
}
|
|
497
|
+
function resolveBoundedInteger(input) {
|
|
498
|
+
const value = input.value ?? input.fallback;
|
|
499
|
+
const minimum = input.allowZero ? 0 : 1;
|
|
500
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > input.maximum) {
|
|
501
|
+
throw new Error(`${input.name} must be an integer between ${minimum} and ${input.maximum}`);
|
|
502
|
+
}
|
|
503
|
+
return value;
|
|
504
|
+
}
|
|
505
|
+
function encodedBase64Length(rawBytes) {
|
|
506
|
+
return Math.ceil(rawBytes / 3) * 4;
|
|
507
|
+
}
|
|
508
|
+
function serializedTextBlockBytes(promptText) {
|
|
509
|
+
return Buffer.byteLength(JSON.stringify({ type: 'text', text: promptText }), 'utf8');
|
|
510
|
+
}
|
|
511
|
+
function serializedImageBlockBytes(rawBytes, mediaType) {
|
|
512
|
+
const framingBytes = Buffer.byteLength(JSON.stringify({
|
|
513
|
+
type: 'image',
|
|
514
|
+
source: {
|
|
515
|
+
type: 'base64',
|
|
516
|
+
media_type: mediaType,
|
|
517
|
+
data: '',
|
|
518
|
+
},
|
|
519
|
+
}), 'utf8');
|
|
520
|
+
return framingBytes + encodedBase64Length(rawBytes);
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Convert only the supported images that fit both the direct-image limit and
|
|
524
|
+
* the aggregate request budget. Skipped images remain represented by the
|
|
525
|
+
* paths already rendered into promptText; one unavailable image never drops
|
|
526
|
+
* valid siblings.
|
|
527
|
+
*/
|
|
528
|
+
export async function toAnthropicImageBlocksWithinBudget(attachments, options) {
|
|
529
|
+
const maxImageRawBytes = resolveBoundedInteger({
|
|
530
|
+
value: options.maxImageRawBytes,
|
|
531
|
+
fallback: MAX_ANTHROPIC_IMAGE_RAW_BYTES,
|
|
532
|
+
maximum: MAX_ANTHROPIC_IMAGE_RAW_BYTES,
|
|
533
|
+
name: 'maxImageRawBytes',
|
|
534
|
+
});
|
|
535
|
+
const maxRequestBytes = resolveBoundedInteger({
|
|
536
|
+
value: options.maxRequestBytes,
|
|
537
|
+
fallback: MAX_ANTHROPIC_REQUEST_BYTES,
|
|
538
|
+
maximum: MAX_ANTHROPIC_REQUEST_BYTES,
|
|
539
|
+
name: 'maxRequestBytes',
|
|
540
|
+
});
|
|
541
|
+
const requestHeadroomBytes = resolveBoundedInteger({
|
|
542
|
+
value: options.requestHeadroomBytes,
|
|
543
|
+
fallback: DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES,
|
|
544
|
+
maximum: maxRequestBytes,
|
|
545
|
+
name: 'requestHeadroomBytes',
|
|
546
|
+
allowZero: true,
|
|
547
|
+
});
|
|
548
|
+
const contentBudgetBytes = maxRequestBytes - requestHeadroomBytes;
|
|
549
|
+
// Exact JSON content-array framing: opening/closing brackets around the
|
|
550
|
+
// text block, then one comma before every native image block.
|
|
551
|
+
let usedBytes = 2 + serializedTextBlockBytes(options.promptText);
|
|
552
|
+
if (usedBytes > contentBudgetBytes)
|
|
553
|
+
return [];
|
|
554
|
+
const blocks = [];
|
|
555
|
+
for (const attachment of attachments) {
|
|
556
|
+
if (!isAnthropicImageAttachment(attachment))
|
|
557
|
+
continue;
|
|
558
|
+
try {
|
|
559
|
+
const mediaType = resolveAttachmentMimeType(attachment);
|
|
560
|
+
const buffer = await readAnthropicImageBytes(attachment, maxImageRawBytes);
|
|
561
|
+
const nextBlockBytes = 1 + serializedImageBlockBytes(buffer.byteLength, mediaType);
|
|
562
|
+
if (usedBytes + nextBlockBytes > contentBudgetBytes)
|
|
563
|
+
continue;
|
|
564
|
+
const block = buildAnthropicImageBlock(buffer, mediaType);
|
|
565
|
+
const actualBlockBytes = 1 + Buffer.byteLength(JSON.stringify(block), 'utf8');
|
|
566
|
+
if (usedBytes + actualBlockBytes > contentBudgetBytes)
|
|
567
|
+
continue;
|
|
568
|
+
blocks.push(block);
|
|
569
|
+
usedBytes += actualBlockBytes;
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
// A missing, changed, or unreadable file stays as its prompt path. Keep
|
|
573
|
+
// inspecting siblings instead of failing the whole model request.
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return blocks;
|
|
577
|
+
}
|
|
265
578
|
/**
|
|
266
579
|
* Return the local path to pass to Codex via its `-i/--image` flag when the
|
|
267
580
|
* attachment is an image Codex can consume. Codex accepts the same MIME types
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type { AddMemberResult, GroupInviteRequirements, AgentClientType, CanonGroupContext, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeDescriptor, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimePrimitiveId, CanonRuntimeProvenance, CanonTurnContextV2, CanonMessage, CanonConversation, CanonReplyContext, CanonContact, CanonContactRequest, CanonResolveAdmissionResult, ContactAddedPayload, ContactRemovedPayload, ContactSource, AgentContext, ResolveAdmissionTargetInput, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonSelfContext, CreateConversationResult, DirectSessionSelection, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, SessionConfig, VerbSessionConfig, CreateConversationOptions, TurnLifecycleState, TurnOutputBlock, TurnOutputBlockInput, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalRisk, ApprovalReplyMetadata, ApprovalOutcomeMetadata, RuntimeInputChoice, RuntimeInputAnswers, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, RuntimeCardNativeMetadata, RuntimeCardV1, SessionRule, ApprovalResult, } from '@canonmsg/core';
|
|
2
|
-
import type { AddMemberResult, CanonGroupContext, CanonMessage, CanonConversation, CanonReplyContext, ContactCardPayload, CanonRuntimeActionDispatch, CanonRuntimePrimitiveId, CanonRuntimeProvenance, CanonTurnContextV2, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalResult, ApprovalRisk, RuntimeInputChoice, RuntimeInputAnswers, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, RuntimeCardNativeMetadata, RuntimeCardV1, SendMessageOptions, SendContextualSelfContextInput, VerbSessionConfig, DirectSessionSelection, TurnOutputBlock, TurnOutputBlockInput } from '@canonmsg/core';
|
|
1
|
+
export type { AddMemberResult, GroupInviteRequirements, AgentClientType, CanonGroupContext, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeDescriptor, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimePrimitiveId, CanonRuntimeProvenance, CanonTurnContextV2, CanonMessage, CanonConversation, CanonReplyContext, CanonContact, CanonContactRequest, CanonResolveAdmissionResult, ContactAddedPayload, ContactRemovedPayload, ContactSource, AgentContext, ResolveAdmissionTargetInput, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonSelfContext, CreateConversationResult, DirectSessionSelection, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, SessionConfig, VerbSessionConfig, CreateConversationOptions, TurnLifecycleState, TurnOutputBlock, TurnOutputBlockInput, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalRisk, ApprovalReplyMetadata, ApprovalOutcomeMetadata, RuntimeInputChoice, RuntimeInputAnswers, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, RuntimeCardNativeMetadata, RuntimeCardV1, ResumableMediaUploadResult, MediaAttachment, VideoProcessingStatus, VideoProcessingErrorCode, SessionRule, ApprovalResult, } from '@canonmsg/core';
|
|
2
|
+
import type { AddMemberResult, CanonGroupContext, CanonMessage, CanonConversation, CanonReplyContext, ContactCardPayload, CanonRuntimeActionDispatch, CanonRuntimePrimitiveId, CanonRuntimeProvenance, CanonTurnContextV2, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalResult, ApprovalRisk, RuntimeInputChoice, RuntimeInputAnswers, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, RuntimeCardNativeMetadata, RuntimeCardV1, ResumableMediaUploadResult, SendMessageOptions, SendContextualSelfContextInput, VerbSessionConfig, DirectSessionSelection, TurnOutputBlock, TurnOutputBlockInput } from '@canonmsg/core';
|
|
3
3
|
import type { MaterializeMediaOptions, MaterializedCanonAttachment, ReplyWithFileOptions, UploadMediaFileOptions } from './media.js';
|
|
4
4
|
export interface ProgressMessageOptions extends SendMessageOptions {
|
|
5
5
|
/**
|
|
@@ -230,10 +230,7 @@ export interface MessageHandlerContext {
|
|
|
230
230
|
/** Canon-managed local media access for the current conversation. */
|
|
231
231
|
media: {
|
|
232
232
|
materialize: (message?: CanonMessage, options?: Omit<MaterializeMediaOptions, 'agentId' | 'conversationId' | 'messageId'>) => Promise<MaterializedCanonAttachment[]>;
|
|
233
|
-
uploadFile: (filePath: string, options?: UploadMediaFileOptions) => Promise<
|
|
234
|
-
url: string;
|
|
235
|
-
attachment: import('@canonmsg/core').MediaAttachment;
|
|
236
|
-
}>;
|
|
233
|
+
uploadFile: (filePath: string, options?: UploadMediaFileOptions) => Promise<ResumableMediaUploadResult>;
|
|
237
234
|
replyWithFile: (filePath: string, text?: string, options?: ReplyWithFileOptions) => Promise<{
|
|
238
235
|
messageId: string;
|
|
239
236
|
}>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/agent-sdk",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.7.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": "^10.
|
|
31
|
+
"@canonmsg/core": "^10.5.0"
|
|
32
32
|
},
|
|
33
33
|
"publishConfig": {
|
|
34
34
|
"access": "public"
|