@canonmsg/agent-sdk 11.2.0 → 12.0.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.
Files changed (188) hide show
  1. package/README.md +171 -536
  2. package/dist/chunks/chunk-6VK4UZHJ.js +2806 -0
  3. package/dist/chunks/chunk-FPQFTTND.js +4899 -0
  4. package/dist/chunks/chunk-OLJ7H4E2.js +14705 -0
  5. package/dist/chunks/mls-crypto-NZDNSPAO.js +10 -0
  6. package/dist/cli.js +198 -0
  7. package/dist/drivers/claude.d.ts +36 -0
  8. package/dist/drivers/code.d.ts +18 -0
  9. package/dist/drivers/codex.d.ts +37 -0
  10. package/dist/index.d.ts +12 -25
  11. package/dist/index.js +69 -13
  12. package/dist/internal/backend-contracts/agentBehaviorPolicy.d.ts +122 -0
  13. package/dist/internal/backend-contracts/agentDirectory.d.ts +32 -0
  14. package/dist/internal/backend-contracts/agentOrigin.d.ts +23 -0
  15. package/dist/internal/backend-contracts/agentSearch.d.ts +17 -0
  16. package/dist/internal/backend-contracts/api-origins.generated.d.ts +4 -0
  17. package/dist/internal/backend-contracts/apiErrors.d.ts +1 -0
  18. package/dist/internal/backend-contracts/artifactActions.d.ts +159 -0
  19. package/dist/internal/backend-contracts/artifacts.d.ts +386 -0
  20. package/dist/internal/backend-contracts/communication.d.ts +131 -0
  21. package/dist/internal/backend-contracts/conversationMembership.d.ts +28 -0
  22. package/dist/internal/backend-contracts/deliveryObservation.d.ts +23 -0
  23. package/dist/internal/backend-contracts/diffRedaction.d.ts +6 -0
  24. package/dist/internal/backend-contracts/endpoint-unread.d.ts +4 -0
  25. package/dist/internal/backend-contracts/endpoint.d.ts +103 -0
  26. package/dist/internal/backend-contracts/endpointContentPolicy.d.ts +37 -0
  27. package/dist/internal/backend-contracts/environment.d.ts +20 -0
  28. package/dist/internal/backend-contracts/externalInbound.d.ts +24 -0
  29. package/dist/internal/backend-contracts/firestoreValues.d.ts +9 -0
  30. package/dist/internal/backend-contracts/index.d.ts +48 -0
  31. package/dist/internal/backend-contracts/media-url.d.ts +6 -0
  32. package/dist/internal/backend-contracts/media.d.ts +51 -0
  33. package/dist/internal/backend-contracts/mediaDelivery.d.ts +46 -0
  34. package/dist/internal/backend-contracts/mediaTransferProtocol.d.ts +17 -0
  35. package/dist/internal/backend-contracts/membershipChange.d.ts +21 -0
  36. package/dist/internal/backend-contracts/message.d.ts +86 -0
  37. package/dist/internal/backend-contracts/messageText.d.ts +6 -0
  38. package/dist/internal/backend-contracts/mlsDevApplication.d.ts +73 -0
  39. package/dist/internal/backend-contracts/mlsDevCredential.d.ts +75 -0
  40. package/dist/internal/backend-contracts/mlsDevCredentialReconciliation.d.ts +45 -0
  41. package/dist/internal/backend-contracts/mlsDevDeviceCustody.d.ts +51 -0
  42. package/dist/internal/backend-contracts/mlsDevEnrollment.d.ts +74 -0
  43. package/dist/internal/backend-contracts/mlsDevGroup.d.ts +197 -0
  44. package/dist/internal/backend-contracts/mlsDevGroupFeed.d.ts +47 -0
  45. package/dist/internal/backend-contracts/mlsDevKeyPackage.d.ts +42 -0
  46. package/dist/internal/backend-contracts/mlsDevKeyPackages.d.ts +55 -0
  47. package/dist/internal/backend-contracts/moderation.d.ts +7 -0
  48. package/dist/internal/backend-contracts/plaintextProcessors.d.ts +63 -0
  49. package/dist/internal/backend-contracts/preparedContent.d.ts +54 -0
  50. package/dist/internal/backend-contracts/replyAuthority.d.ts +14 -0
  51. package/dist/internal/backend-contracts/runtimeCardFields.d.ts +66 -0
  52. package/dist/internal/backend-contracts/runtimeCardStorage.d.ts +3 -0
  53. package/dist/internal/backend-contracts/runtimeDescriptor.d.ts +88 -0
  54. package/dist/internal/backend-contracts/selfContext.d.ts +9 -0
  55. package/dist/internal/backend-contracts/turnProtocol.d.ts +43 -0
  56. package/dist/internal/backend-contracts/verbContract.d.ts +848 -0
  57. package/dist/internal/backend-contracts/verbSchemas.d.ts +1707 -0
  58. package/dist/internal/backend-contracts/verbWire.d.ts +493 -0
  59. package/dist/internal/backend-contracts/webAuth.d.ts +8 -0
  60. package/dist/internal/backend-contracts/workSessions.d.ts +205 -0
  61. package/dist/internal/core/agent-profiles.d.ts +69 -0
  62. package/dist/internal/core/agent-resolver.d.ts +49 -0
  63. package/dist/internal/core/agent-session.d.ts +12 -0
  64. package/dist/internal/core/approval-format.d.ts +26 -0
  65. package/dist/internal/core/approval-manager.d.ts +43 -0
  66. package/dist/internal/core/approval-types.d.ts +74 -0
  67. package/dist/internal/core/artifacts.d.ts +262 -0
  68. package/dist/internal/core/attached-native-session.d.ts +174 -0
  69. package/dist/internal/core/attached-session-file-store.d.ts +3 -0
  70. package/dist/internal/core/base-url.d.ts +1 -0
  71. package/dist/internal/core/cli-metadata.d.ts +13 -0
  72. package/dist/internal/core/client.d.ts +486 -0
  73. package/dist/internal/core/completed-output-journal.d.ts +39 -0
  74. package/dist/internal/core/constants.d.ts +4 -0
  75. package/dist/internal/core/control-poller.d.ts +221 -0
  76. package/dist/internal/core/endpoint-client.d.ts +43 -0
  77. package/dist/internal/core/exclusive-json-file-store.d.ts +7 -0
  78. package/dist/internal/core/execution-environment-mode.d.ts +8 -0
  79. package/dist/internal/core/execution-environment.d.ts +55 -0
  80. package/dist/internal/core/gifs.d.ts +35 -0
  81. package/dist/internal/core/group-context.d.ts +25 -0
  82. package/dist/internal/core/host-runtime.d.ts +257 -0
  83. package/dist/internal/core/inbound-frame.d.ts +64 -0
  84. package/dist/internal/core/index.d.ts +107 -0
  85. package/dist/internal/core/local-runtime-catalog.d.ts +131 -0
  86. package/dist/internal/core/message-format.d.ts +13 -0
  87. package/dist/internal/core/native-session-interaction.d.ts +44 -0
  88. package/dist/internal/core/node-media.d.ts +6 -0
  89. package/dist/internal/core/policy.d.ts +2 -0
  90. package/dist/internal/core/provenance.d.ts +31 -0
  91. package/dist/internal/core/registration-cli.d.ts +40 -0
  92. package/dist/internal/core/registration-profile.d.ts +10 -0
  93. package/dist/internal/core/registration-session.d.ts +55 -0
  94. package/dist/internal/core/registration.d.ts +21 -0
  95. package/dist/internal/core/retry.d.ts +122 -0
  96. package/dist/internal/core/rtdb-rest.d.ts +145 -0
  97. package/dist/internal/core/runtime-cards.d.ts +331 -0
  98. package/dist/internal/core/runtime-descriptor.d.ts +75 -0
  99. package/dist/internal/core/runtime-endpoints.d.ts +26 -0
  100. package/dist/internal/core/runtime-heartbeat.d.ts +29 -0
  101. package/dist/internal/core/runtime-presentation.d.ts +35 -0
  102. package/dist/internal/core/runtime-request-manager.d.ts +284 -0
  103. package/dist/internal/core/runtime-state-publisher.d.ts +42 -0
  104. package/dist/internal/core/self-context.d.ts +61 -0
  105. package/dist/internal/core/silent-turn.d.ts +86 -0
  106. package/dist/internal/core/stream.d.ts +77 -0
  107. package/dist/internal/core/turn-output-controller.d.ts +69 -0
  108. package/dist/internal/core/turn-protocol.d.ts +33 -0
  109. package/dist/internal/core/turn-verbosity.d.ts +97 -0
  110. package/dist/internal/core/types.d.ts +1075 -0
  111. package/dist/internal/core/typing-status-publisher.d.ts +14 -0
  112. package/dist/internal/core/unified-diff.d.ts +35 -0
  113. package/dist/internal/core/work-session-provider.d.ts +29 -0
  114. package/dist/internal/core/workspace-discovery.d.ts +17 -0
  115. package/dist/internal/endpoint/artifact-images.d.ts +60 -0
  116. package/dist/internal/endpoint/artifact-reducer.d.ts +35 -0
  117. package/dist/internal/endpoint/artifact-views.d.ts +113 -0
  118. package/dist/internal/endpoint/artifacts.d.ts +75 -0
  119. package/dist/internal/endpoint/delivery.d.ts +47 -0
  120. package/dist/internal/endpoint/engine.d.ts +219 -0
  121. package/dist/internal/endpoint/ephemeral.d.ts +8 -0
  122. package/dist/internal/endpoint/events.d.ts +32 -0
  123. package/dist/internal/endpoint/external-inbound.d.ts +36 -0
  124. package/dist/internal/endpoint/handoff.d.ts +50 -0
  125. package/dist/internal/endpoint/history.d.ts +22 -0
  126. package/dist/internal/endpoint/image-generation-errors.d.ts +2 -0
  127. package/dist/internal/endpoint/inbound.d.ts +29 -0
  128. package/dist/internal/endpoint/index.d.ts +32 -0
  129. package/dist/internal/endpoint/media.d.ts +172 -0
  130. package/dist/internal/endpoint/migration.d.ts +50 -0
  131. package/dist/internal/endpoint/mls-crypto.d.ts +25 -0
  132. package/dist/internal/endpoint/mls-dev-client.d.ts +44 -0
  133. package/dist/internal/endpoint/mls-dev-device.d.ts +67 -0
  134. package/dist/internal/endpoint/mls-dev-groups.d.ts +114 -0
  135. package/dist/internal/endpoint/mls-dev-key-package-service.d.ts +33 -0
  136. package/dist/internal/endpoint/mls-dev-key-packages.d.ts +42 -0
  137. package/dist/internal/endpoint/mls-dev-transport.d.ts +37 -0
  138. package/dist/internal/endpoint/node-mls-custody-protocol.d.ts +24 -0
  139. package/dist/internal/endpoint/node-mls-custody.d.ts +15 -0
  140. package/dist/internal/endpoint/node.d.ts +7 -0
  141. package/dist/internal/endpoint/observe-query.d.ts +17 -0
  142. package/dist/internal/endpoint/portable-node-mls-custody.d.ts +23 -0
  143. package/dist/internal/endpoint/queue.d.ts +22 -0
  144. package/dist/internal/endpoint/recovery.d.ts +27 -0
  145. package/dist/internal/endpoint/retention.d.ts +7 -0
  146. package/dist/internal/endpoint/store.d.ts +22 -0
  147. package/dist/internal/endpoint/sync.d.ts +33 -0
  148. package/dist/internal/endpoint/transient-publisher.d.ts +8 -0
  149. package/dist/internal/endpoint/transport.d.ts +58 -0
  150. package/dist/media.d.ts +1 -1
  151. package/dist/media.js +42 -541
  152. package/dist/runner/runner.d.ts +37 -0
  153. package/dist/runner/types.d.ts +168 -0
  154. package/package.json +18 -5
  155. package/dist/attached-session.d.ts +0 -58
  156. package/dist/attached-session.js +0 -638
  157. package/dist/bounded-file-read.d.ts +0 -14
  158. package/dist/bounded-file-read.js +0 -34
  159. package/dist/canon-agent.d.ts +0 -238
  160. package/dist/canon-agent.js +0 -2534
  161. package/dist/debouncer.d.ts +0 -18
  162. package/dist/debouncer.js +0 -133
  163. package/dist/inbound-handoff.d.ts +0 -6
  164. package/dist/inbound-handoff.js +0 -17
  165. package/dist/managed-session-host.d.ts +0 -27
  166. package/dist/managed-session-host.js +0 -195
  167. package/dist/managed-session-setup.d.ts +0 -99
  168. package/dist/managed-session-setup.js +0 -234
  169. package/dist/realtime.d.ts +0 -62
  170. package/dist/realtime.js +0 -193
  171. package/dist/runtime-card.d.ts +0 -48
  172. package/dist/runtime-card.js +0 -64
  173. package/dist/session-manager.d.ts +0 -77
  174. package/dist/session-manager.js +0 -285
  175. package/dist/turn-streaming-request.d.ts +0 -17
  176. package/dist/turn-streaming-request.js +0 -22
  177. package/dist/turn-verbosity-option.d.ts +0 -33
  178. package/dist/turn-verbosity-option.js +0 -23
  179. package/dist/types.d.ts +0 -387
  180. package/dist/types.js +0 -1
  181. package/dist/work-session-coordination.d.ts +0 -21
  182. package/dist/work-session-coordination.js +0 -53
  183. package/dist/work-session-host.d.ts +0 -31
  184. package/dist/work-session-host.js +0 -617
  185. package/dist/work-session-interactions.d.ts +0 -12
  186. package/dist/work-session-interactions.js +0 -173
  187. package/dist/work-session-state.d.ts +0 -36
  188. package/dist/work-session-state.js +0 -62
package/dist/media.js CHANGED
@@ -1,542 +1,43 @@
1
- import { createWriteStream } from 'node:fs';
2
- import { randomUUID } from 'node:crypto';
3
- import { mkdir, open, rename, stat, unlink } from 'node:fs/promises';
4
- import { basename, dirname, extname, join } from 'node:path';
5
- import { Readable, Transform } from 'node:stream';
6
- import { pipeline } from 'node:stream/promises';
7
- import { CANON_DIR, renderCanonHostInboundContent, sanitizeDownloadFileName, } from '@canonmsg/core';
8
- import { BoundedFileReadError, readBoundedRegularFileHandle, } from './bounded-file-read.js';
9
- const ANTHROPIC_IMAGE_MIME_TYPES = new Set([
10
- 'image/jpeg',
11
- 'image/png',
12
- 'image/gif',
13
- 'image/webp',
14
- ]);
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;
26
- const EXTENSION_BY_MIME = {
27
- 'application/json': 'json',
28
- 'application/pdf': 'pdf',
29
- 'application/zip': 'zip',
30
- 'audio/mp4': 'm4a',
31
- 'audio/mpeg': 'mp3',
32
- 'audio/ogg': 'ogg',
33
- 'audio/webm': 'webm',
34
- 'audio/wav': 'wav',
35
- 'video/mp4': 'mp4',
36
- 'video/quicktime': 'mov',
37
- 'video/webm': 'webm',
38
- 'video/x-m4v': 'm4v',
39
- 'image/gif': 'gif',
40
- 'image/jpeg': 'jpg',
41
- 'image/png': 'png',
42
- 'image/webp': 'webp',
43
- 'text/plain': 'txt',
1
+ import {
2
+ DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES,
3
+ DEFAULT_MEDIA_CACHE_DIR,
4
+ DEFAULT_MEDIA_MATERIALIZATION_BYTES,
5
+ MAX_ANTHROPIC_IMAGE_RAW_BYTES,
6
+ MAX_ANTHROPIC_REQUEST_BYTES,
7
+ MAX_CANON_MEDIA_BYTES,
8
+ conversationMediaCacheDir,
9
+ getCodexImagePath,
10
+ getMessageAttachments,
11
+ inferUploadMimeType,
12
+ isAnthropicImageAttachment,
13
+ materializeAttachment,
14
+ materializeMessageMedia,
15
+ materializeReplyContextMedia,
16
+ resolveAttachmentMimeType,
17
+ sendMediaFileMessage,
18
+ toAnthropicImageBlock,
19
+ toAnthropicImageBlocksWithinBudget,
20
+ uploadMediaFile
21
+ } from "./chunks/chunk-OLJ7H4E2.js";
22
+ import "./chunks/chunk-FPQFTTND.js";
23
+ export {
24
+ DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES,
25
+ DEFAULT_MEDIA_CACHE_DIR,
26
+ DEFAULT_MEDIA_MATERIALIZATION_BYTES,
27
+ MAX_ANTHROPIC_IMAGE_RAW_BYTES,
28
+ MAX_ANTHROPIC_REQUEST_BYTES,
29
+ MAX_CANON_MEDIA_BYTES,
30
+ conversationMediaCacheDir,
31
+ getCodexImagePath,
32
+ getMessageAttachments,
33
+ inferUploadMimeType,
34
+ isAnthropicImageAttachment,
35
+ materializeAttachment,
36
+ materializeMessageMedia,
37
+ materializeReplyContextMedia,
38
+ resolveAttachmentMimeType,
39
+ sendMediaFileMessage,
40
+ toAnthropicImageBlock,
41
+ toAnthropicImageBlocksWithinBudget,
42
+ uploadMediaFile
44
43
  };
45
- const MIME_BY_EXTENSION = {
46
- '.gif': 'image/gif',
47
- '.jpeg': 'image/jpeg',
48
- '.jpg': 'image/jpeg',
49
- '.json': 'application/json',
50
- '.m4a': 'audio/mp4',
51
- '.mp3': 'audio/mpeg',
52
- '.ogg': 'audio/ogg',
53
- '.pdf': 'application/pdf',
54
- '.png': 'image/png',
55
- '.txt': 'text/plain',
56
- '.wav': 'audio/wav',
57
- '.webm': 'audio/webm',
58
- '.m4v': 'video/x-m4v',
59
- '.mov': 'video/quicktime',
60
- '.mp4': 'video/mp4',
61
- '.webp': 'image/webp',
62
- '.zip': 'application/zip',
63
- };
64
- function sanitizeSegment(value, fallback) {
65
- const sanitized = value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
66
- return sanitized || fallback;
67
- }
68
- function inferExtension(input) {
69
- const explicitExtension = extname(input.attachment.fileName ?? '').toLowerCase();
70
- if (explicitExtension) {
71
- return explicitExtension.replace(/[^a-z0-9.]/g, '') || '.bin';
72
- }
73
- const mimeType = input.responseMimeType ?? input.attachment.mimeType ?? '';
74
- const byMime = EXTENSION_BY_MIME[mimeType.toLowerCase()];
75
- if (byMime) {
76
- return `.${byMime}`;
77
- }
78
- const urlPath = input.attachment.url.split('?')[0] ?? '';
79
- const urlExtension = extname(urlPath).toLowerCase();
80
- if (urlExtension) {
81
- return urlExtension.replace(/[^a-z0-9.]/g, '') || '.bin';
82
- }
83
- return '.bin';
84
- }
85
- /**
86
- * The directory every materialized attachment of one conversation lands in
87
- * (`<root>/<agent>/<conversation>/<message>/…`). A host that runs one native
88
- * session per conversation can grant its runtime read access to exactly this
89
- * directory instead of approving each downloaded file.
90
- */
91
- export function conversationMediaCacheDir(input) {
92
- return join(input.rootDir ?? DEFAULT_MEDIA_CACHE_DIR, sanitizeSegment(input.agentId, 'agent'), sanitizeSegment(input.conversationId, 'conversation'));
93
- }
94
- function buildCachePath(input) {
95
- const rootDir = input.rootDir ?? DEFAULT_MEDIA_CACHE_DIR;
96
- const extension = inferExtension({
97
- attachment: input.attachment,
98
- responseMimeType: input.responseMimeType,
99
- });
100
- const fallbackName = `${input.attachment.kind}-${input.index}${extension}`;
101
- const preferredName = input.attachment.fileName
102
- ? sanitizeDownloadFileName(input.attachment.fileName, fallbackName)
103
- : fallbackName;
104
- const fileName = extname(preferredName) ? preferredName : `${preferredName}${extension}`;
105
- return join(conversationMediaCacheDir({ agentId: input.agentId, conversationId: input.conversationId, rootDir }), sanitizeSegment(input.messageId, 'message'), `${String(input.index).padStart(2, '0')}-${fileName}`);
106
- }
107
- function ensureFetch(fetchImpl) {
108
- if (fetchImpl)
109
- return fetchImpl;
110
- if (typeof fetch === 'function')
111
- return fetch;
112
- throw new Error('Global fetch is unavailable; provide fetchImpl explicitly.');
113
- }
114
- async function fileExists(path) {
115
- try {
116
- const info = await stat(path);
117
- return info.isFile();
118
- }
119
- catch {
120
- return false;
121
- }
122
- }
123
- function resolveMaxBytes(value) {
124
- if (value === undefined)
125
- return DEFAULT_MEDIA_MATERIALIZATION_BYTES;
126
- if (!Number.isSafeInteger(value) || value <= 0 || value > MAX_CANON_MEDIA_BYTES) {
127
- throw new Error(`maxBytes must be an integer between 1 and ${MAX_CANON_MEDIA_BYTES}`);
128
- }
129
- return value;
130
- }
131
- function reportMaterializationError(options, error, attachment, index) {
132
- try {
133
- options.onError?.(error, attachment, index);
134
- }
135
- catch {
136
- // Diagnostics must never turn a recoverable sibling failure into a failed
137
- // message materialization.
138
- }
139
- }
140
- async function materializeAvailableAttachments(inputs, options) {
141
- const settled = await Promise.allSettled(inputs.map((input) => input.run()));
142
- options.signal?.throwIfAborted();
143
- const materialized = [];
144
- let firstError;
145
- let hasFailure = false;
146
- settled.forEach((result, resultIndex) => {
147
- if (result.status === 'fulfilled') {
148
- materialized.push(result.value);
149
- return;
150
- }
151
- const input = inputs[resultIndex];
152
- if (!hasFailure) {
153
- firstError = result.reason;
154
- hasFailure = true;
155
- }
156
- reportMaterializationError(options, result.reason, input.attachment, input.index);
157
- });
158
- // Preserve the strict all-failed behavior so callers still learn when no
159
- // bytes were available, while allowing valid siblings through unchanged.
160
- if (materialized.length === 0 && hasFailure) {
161
- throw firstError;
162
- }
163
- return materialized;
164
- }
165
- function byteLimitTransform(maxBytes) {
166
- let total = 0;
167
- return {
168
- stream: new Transform({
169
- transform(chunk, _encoding, callback) {
170
- total += chunk.length;
171
- if (total > maxBytes) {
172
- callback(new Error(`Canon media exceeds the ${maxBytes}-byte materialization limit`));
173
- return;
174
- }
175
- callback(null, chunk);
176
- },
177
- }),
178
- bytesRead: () => total,
179
- };
180
- }
181
- export function getMessageAttachments(message) {
182
- return Array.isArray(message.attachments) ? message.attachments : [];
183
- }
184
- function isMaterializableAttachment(attachment) {
185
- // Missing status is the legacy-ready contract. Processing URLs are reserved
186
- // future capabilities and failed videos intentionally have no playable
187
- // object, so neither should trigger a network request from an agent runtime.
188
- return attachment.kind !== 'video'
189
- || attachment.processingStatus === undefined
190
- || attachment.processingStatus === 'ready';
191
- }
192
- export async function materializeAttachment(attachment, options) {
193
- const path = buildCachePath({
194
- agentId: options.agentId,
195
- conversationId: options.conversationId,
196
- messageId: options.messageId,
197
- attachment,
198
- index: options.index ?? 0,
199
- rootDir: options.rootDir,
200
- });
201
- await mkdir(dirname(path), { recursive: true });
202
- const maxBytes = resolveMaxBytes(options.maxBytes);
203
- const expectedBytes = typeof attachment.sizeBytes === 'number'
204
- && Number.isSafeInteger(attachment.sizeBytes)
205
- && attachment.sizeBytes >= 0
206
- ? attachment.sizeBytes
207
- : null;
208
- if (expectedBytes !== null && expectedBytes > maxBytes) {
209
- throw new Error(`Canon media exceeds the ${maxBytes}-byte materialization limit`);
210
- }
211
- let responseMimeType = null;
212
- if (await fileExists(path)) {
213
- const info = await stat(path);
214
- if (info.size > maxBytes) {
215
- throw new Error(`Cached Canon media exceeds the ${maxBytes}-byte materialization limit`);
216
- }
217
- if (expectedBytes !== null && info.size !== expectedBytes) {
218
- await unlink(path).catch(() => { });
219
- }
220
- }
221
- if (!(await fileExists(path))) {
222
- const fetchImpl = ensureFetch(options.fetchImpl);
223
- const response = await fetchImpl(attachment.url, {
224
- signal: options.signal,
225
- });
226
- if (!response.ok) {
227
- throw new Error(`Failed to download Canon media (${response.status} ${response.statusText})`);
228
- }
229
- responseMimeType = response.headers.get('content-type');
230
- const contentLength = response.headers.get('content-length');
231
- const declaredLength = contentLength === null ? null : Number(contentLength);
232
- if (declaredLength !== null && Number.isFinite(declaredLength) && declaredLength > maxBytes) {
233
- throw new Error(`Canon media exceeds the ${maxBytes}-byte materialization limit`);
234
- }
235
- if (expectedBytes !== null
236
- && declaredLength !== null
237
- && Number.isFinite(declaredLength)
238
- && declaredLength !== expectedBytes) {
239
- throw new Error('Canon media response length does not match attachment metadata');
240
- }
241
- if (!response.body) {
242
- throw new Error('Canon media response had no body');
243
- }
244
- const tempPath = `${path}.${process.pid}.${randomUUID()}.part`;
245
- const limiter = byteLimitTransform(maxBytes);
246
- try {
247
- await pipeline(Readable.fromWeb(response.body), limiter.stream, createWriteStream(tempPath, { mode: 0o600, flags: 'wx' }));
248
- if (expectedBytes !== null && limiter.bytesRead() !== expectedBytes) {
249
- throw new Error('Canon media response length does not match attachment metadata');
250
- }
251
- await rename(tempPath, path);
252
- }
253
- catch (error) {
254
- await unlink(tempPath).catch(() => { });
255
- throw error;
256
- }
257
- }
258
- return {
259
- ...attachment,
260
- index: options.index ?? 0,
261
- path,
262
- sourceUrl: attachment.url,
263
- conversationId: options.conversationId,
264
- messageId: options.messageId,
265
- ...(attachment.mimeType
266
- ? {}
267
- : responseMimeType
268
- ? { mimeType: responseMimeType }
269
- : {}),
270
- };
271
- }
272
- export async function materializeMessageMedia(message, options) {
273
- const attachments = getMessageAttachments(message);
274
- return materializeAvailableAttachments(attachments.flatMap((attachment, index) => (isMaterializableAttachment(attachment)
275
- ? [{
276
- attachment,
277
- index,
278
- run: () => materializeAttachment(attachment, {
279
- ...options,
280
- messageId: message.id,
281
- index,
282
- }),
283
- }]
284
- : [])), options);
285
- }
286
- export async function materializeReplyContextMedia(replyContext, options) {
287
- if (!replyContext?.found || !replyContext.attachments?.length) {
288
- return { replyContext, materialized: [] };
289
- }
290
- const materializable = replyContext.attachments.flatMap((attachment, index) => attachment.url && isMaterializableAttachment(attachment)
291
- ? [{
292
- attachment,
293
- index,
294
- run: () => materializeAttachment(attachment, {
295
- ...options,
296
- messageId: replyContext.messageId,
297
- index,
298
- }),
299
- }]
300
- : []);
301
- const materialized = await materializeAvailableAttachments(materializable, options);
302
- return {
303
- replyContext: {
304
- ...replyContext,
305
- body: renderCanonHostInboundContent({
306
- text: replyContext.text,
307
- contentType: replyContext.contentType,
308
- attachments: replyContext.attachments,
309
- contactCard: replyContext.contactCard,
310
- senderType: replyContext.senderType ?? undefined,
311
- }, materialized),
312
- },
313
- materialized,
314
- };
315
- }
316
- export function inferUploadMimeType(filePath, overrideMimeType) {
317
- if (overrideMimeType)
318
- return overrideMimeType;
319
- return MIME_BY_EXTENSION[extname(filePath).toLowerCase()] ?? 'application/octet-stream';
320
- }
321
- export async function uploadMediaFile(client, conversationId, filePath, options) {
322
- options?.signal?.throwIfAborted();
323
- const fileStat = await stat(filePath);
324
- if (!fileStat.isFile())
325
- throw new Error(`Canon media path is not a file: ${filePath}`);
326
- if (fileStat.size <= 0)
327
- throw new Error('Canon media file is empty');
328
- if (fileStat.size > MAX_CANON_MEDIA_BYTES) {
329
- throw new Error(`Canon media exceeds the ${MAX_CANON_MEDIA_BYTES}-byte upload limit`);
330
- }
331
- const mimeType = inferUploadMimeType(filePath, options?.mimeType);
332
- const fileName = options?.fileName ?? basename(filePath);
333
- const media = await client.getFileMediaPipeline(options?.fetchImpl);
334
- const [attachment] = await media.prepare({ conversationId, attachmentId: options?.attachmentId ?? randomUUID(),
335
- files: [{ source: filePath, descriptor: { mimeType, sizeBytes: fileStat.size, fileName,
336
- ...(options?.durationMs !== undefined ? { durationMs: options.durationMs } : {}) } }],
337
- }, { signal: options?.signal });
338
- if (typeof attachment?.uploadId !== 'string' || typeof attachment.url !== 'string') {
339
- throw new Error('Canon returned an invalid prepared attachment');
340
- }
341
- return { uploadId: attachment.uploadId, url: attachment.url, attachment: attachment };
342
- }
343
- export async function sendMediaFileMessage(client, conversationId, filePath, text = '', options) {
344
- const { fileName, mimeType, durationMs, fetchImpl, signal, canPublish, attachmentId: _attachmentId, ...sendOptions } = options ?? {};
345
- signal?.throwIfAborted();
346
- const info = await stat(filePath);
347
- if (!info.isFile() || info.size <= 0 || info.size > MAX_CANON_MEDIA_BYTES) {
348
- throw new Error('Canon media must be a regular file from 1 byte to 100 MiB');
349
- }
350
- const effectiveMimeType = inferUploadMimeType(filePath, mimeType);
351
- const contentType = effectiveMimeType.startsWith('image/') ? 'image'
352
- : effectiveMimeType.startsWith('video/') ? 'video'
353
- : effectiveMimeType.startsWith('audio/') ? 'audio' : 'file';
354
- // Automatic artifacts keep their audience across upload and every later
355
- // receipt retry. A membership change may reduce, but never expand, that audience.
356
- const initialRoom = canPublish ? await client.getConversation(conversationId) : null;
357
- const originalMembers = new Set(initialRoom?.memberIds ?? []);
358
- const beforePublish = canPublish ? async () => {
359
- const current = await client.getConversation(conversationId);
360
- return !!current && originalMembers.size > 0
361
- && current.memberIds.every((id) => originalMembers.has(id)) && canPublish();
362
- } : undefined;
363
- const messageId = sendOptions.messageId ?? randomUUID();
364
- const pipeline = await client.getFileMediaPipeline(fetchImpl);
365
- const delivered = await pipeline.send({
366
- conversationId, messageId,
367
- message: { ...sendOptions, text, contentType },
368
- files: [{ source: filePath, descriptor: {
369
- mimeType: effectiveMimeType, sizeBytes: info.size, fileName: fileName ?? basename(filePath),
370
- ...(durationMs != null ? { durationMs } : {}),
371
- } }],
372
- }, { ...(signal ? { signal } : {}), ...(beforePublish ? { beforePublish } : {}) });
373
- return { messageId: delivered };
374
- }
375
- /**
376
- * Resolve the effective MIME type of a materialized attachment, falling back
377
- * to filename/URL extensions when the server didn't tell us explicitly.
378
- */
379
- export function resolveAttachmentMimeType(attachment) {
380
- if (attachment.mimeType)
381
- return attachment.mimeType.toLowerCase();
382
- const pathExt = extname(attachment.path).toLowerCase();
383
- const fromPath = MIME_BY_EXTENSION[pathExt];
384
- if (fromPath)
385
- return fromPath;
386
- const fileExt = extname(attachment.fileName ?? '').toLowerCase();
387
- const fromFileName = MIME_BY_EXTENSION[fileExt];
388
- if (fromFileName)
389
- return fromFileName;
390
- const urlExt = extname(attachment.sourceUrl.split('?')[0] ?? '').toLowerCase();
391
- const fromUrl = MIME_BY_EXTENSION[urlExt];
392
- if (fromUrl)
393
- return fromUrl;
394
- return null;
395
- }
396
- /**
397
- * True when a materialized attachment can be sent to Anthropic as a base64
398
- * `image` block (jpeg, png, gif, webp). Other images (heic, tiff, svg, …)
399
- * must be re-encoded before they can be delivered as a vision block.
400
- */
401
- export function isAnthropicImageAttachment(attachment) {
402
- if (attachment.kind !== 'image')
403
- return false;
404
- const mime = resolveAttachmentMimeType(attachment);
405
- return mime != null && ANTHROPIC_IMAGE_MIME_TYPES.has(mime);
406
- }
407
- /**
408
- * Read the materialized bytes and build an Anthropic `image` content block.
409
- * Callers should first check `isAnthropicImageAttachment`; this function
410
- * throws if the MIME type is not supported.
411
- */
412
- export async function toAnthropicImageBlock(attachment, options) {
413
- if (!isAnthropicImageAttachment(attachment)) {
414
- throw new Error(`Canon attachment ${attachment.index} is not a supported Anthropic image (kind=${attachment.kind}, mime=${attachment.mimeType ?? 'unknown'})`);
415
- }
416
- const maxRawBytes = options?.maxRawBytes ?? MAX_ANTHROPIC_IMAGE_RAW_BYTES;
417
- if (!Number.isSafeInteger(maxRawBytes)
418
- || maxRawBytes <= 0
419
- || maxRawBytes > MAX_ANTHROPIC_IMAGE_RAW_BYTES) {
420
- throw new Error(`maxRawBytes must be an integer between 1 and ${MAX_ANTHROPIC_IMAGE_RAW_BYTES}`);
421
- }
422
- const mediaType = resolveAttachmentMimeType(attachment);
423
- const buffer = await readAnthropicImageBytes(attachment, maxRawBytes);
424
- return buildAnthropicImageBlock(buffer, mediaType);
425
- }
426
- async function readAnthropicImageBytes(attachment, maxRawBytes) {
427
- const handle = await open(attachment.path, 'r');
428
- try {
429
- return await readBoundedRegularFileHandle(handle, maxRawBytes);
430
- }
431
- catch (error) {
432
- if (error instanceof BoundedFileReadError) {
433
- if (error.failure === 'not-file') {
434
- throw new Error(`Canon attachment ${attachment.index} is not a file`);
435
- }
436
- throw new Error(`Canon attachment ${attachment.index} exceeds the ${maxRawBytes}-byte Anthropic image limit`);
437
- }
438
- throw error;
439
- }
440
- finally {
441
- await handle.close();
442
- }
443
- }
444
- function buildAnthropicImageBlock(buffer, mediaType) {
445
- return {
446
- type: 'image',
447
- source: {
448
- type: 'base64',
449
- media_type: mediaType,
450
- data: buffer.toString('base64'),
451
- },
452
- };
453
- }
454
- function resolveBoundedInteger(input) {
455
- const value = input.value ?? input.fallback;
456
- const minimum = input.allowZero ? 0 : 1;
457
- if (!Number.isSafeInteger(value) || value < minimum || value > input.maximum) {
458
- throw new Error(`${input.name} must be an integer between ${minimum} and ${input.maximum}`);
459
- }
460
- return value;
461
- }
462
- function encodedBase64Length(rawBytes) {
463
- return Math.ceil(rawBytes / 3) * 4;
464
- }
465
- function serializedTextBlockBytes(promptText) {
466
- return Buffer.byteLength(JSON.stringify({ type: 'text', text: promptText }), 'utf8');
467
- }
468
- function serializedImageBlockBytes(rawBytes, mediaType) {
469
- const framingBytes = Buffer.byteLength(JSON.stringify({
470
- type: 'image',
471
- source: {
472
- type: 'base64',
473
- media_type: mediaType,
474
- data: '',
475
- },
476
- }), 'utf8');
477
- return framingBytes + encodedBase64Length(rawBytes);
478
- }
479
- /**
480
- * Convert only the supported images that fit both the direct-image limit and
481
- * the aggregate request budget. Skipped images remain represented by the
482
- * paths already rendered into promptText; one unavailable image never drops
483
- * valid siblings.
484
- */
485
- export async function toAnthropicImageBlocksWithinBudget(attachments, options) {
486
- const maxImageRawBytes = resolveBoundedInteger({
487
- value: options.maxImageRawBytes,
488
- fallback: MAX_ANTHROPIC_IMAGE_RAW_BYTES,
489
- maximum: MAX_ANTHROPIC_IMAGE_RAW_BYTES,
490
- name: 'maxImageRawBytes',
491
- });
492
- const maxRequestBytes = resolveBoundedInteger({
493
- value: options.maxRequestBytes,
494
- fallback: MAX_ANTHROPIC_REQUEST_BYTES,
495
- maximum: MAX_ANTHROPIC_REQUEST_BYTES,
496
- name: 'maxRequestBytes',
497
- });
498
- const requestHeadroomBytes = resolveBoundedInteger({
499
- value: options.requestHeadroomBytes,
500
- fallback: DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES,
501
- maximum: maxRequestBytes,
502
- name: 'requestHeadroomBytes',
503
- allowZero: true,
504
- });
505
- const contentBudgetBytes = maxRequestBytes - requestHeadroomBytes;
506
- // Exact JSON content-array framing: opening/closing brackets around the
507
- // text block, then one comma before every native image block.
508
- let usedBytes = 2 + serializedTextBlockBytes(options.promptText);
509
- if (usedBytes > contentBudgetBytes)
510
- return [];
511
- const blocks = [];
512
- for (const attachment of attachments) {
513
- if (!isAnthropicImageAttachment(attachment))
514
- continue;
515
- try {
516
- const mediaType = resolveAttachmentMimeType(attachment);
517
- const buffer = await readAnthropicImageBytes(attachment, maxImageRawBytes);
518
- const nextBlockBytes = 1 + serializedImageBlockBytes(buffer.byteLength, mediaType);
519
- if (usedBytes + nextBlockBytes > contentBudgetBytes)
520
- continue;
521
- const block = buildAnthropicImageBlock(buffer, mediaType);
522
- const actualBlockBytes = 1 + Buffer.byteLength(JSON.stringify(block), 'utf8');
523
- if (usedBytes + actualBlockBytes > contentBudgetBytes)
524
- continue;
525
- blocks.push(block);
526
- usedBytes += actualBlockBytes;
527
- }
528
- catch {
529
- // A missing, changed, or unreadable file stays as its prompt path. Keep
530
- // inspecting siblings instead of failing the whole model request.
531
- }
532
- }
533
- return blocks;
534
- }
535
- /**
536
- * Return the local path to pass to Codex via its `-i/--image` flag when the
537
- * attachment is an image Codex can consume. Codex accepts the same MIME types
538
- * as Anthropic in practice (jpeg/png/gif/webp), so we reuse the allowlist.
539
- */
540
- export function getCodexImagePath(attachment) {
541
- return isAnthropicImageAttachment(attachment) ? attachment.path : null;
542
- }
@@ -0,0 +1,37 @@
1
+ import { type CanonEndpoint, type CanonRuntimeDescriptor, type CommunicateInput, type CommunicateResult } from '../internal/core/index.js';
2
+ import { type RuntimeDriver } from './types.js';
3
+ export interface RunnerOptions {
4
+ apiKey: string;
5
+ environmentId: string;
6
+ /** CANON_HOME: a private 0700 directory, checked before anything is opened. */
7
+ home: string;
8
+ driver: RuntimeDriver;
9
+ /** Test seam: an injected endpoint engine. Production never sets it. */
10
+ endpoint?: CanonEndpoint;
11
+ /** Test seam: HTTP outside the endpoint protocol (health, identity). Production never sets it. */
12
+ fetch?: typeof fetch;
13
+ /** Test-only origin overrides (the CLI's CANON_BASE_URL and CANON_STREAM_URL). */
14
+ baseUrl?: string;
15
+ streamUrl?: string;
16
+ /** How long an aborted turn may take to settle before its session is reset (default 10 s). */
17
+ abortGraceMs?: number;
18
+ /** A turn with no text or activity for this long fails (default 30 min); the clock waits while an `ask` is pending. */
19
+ noProgressMs?: number;
20
+ /** How long an `ask` waits for its answer before it expires (default 1 h; an expired approval is a denial). */
21
+ askExpiryMs?: number;
22
+ /** How long running turns may finish on shutdown before they are aborted (default 10 s). */
23
+ shutdownGraceMs?: number;
24
+ /** Never receives prompt or reply text. */
25
+ log?: (line: string) => void;
26
+ }
27
+ export interface Runner {
28
+ /** Connects and starts taking input; resolves once listening is set up. */
29
+ start(): Promise<void>;
30
+ /** Stops intake, releases unsubmitted input, lets running turns finish within the shutdown grace. */
31
+ stop(): Promise<void>;
32
+ /** A proactive send outside any turn (cron): the `communicate` tool's handler, under the runner's identity. */
33
+ communicate(input: CommunicateInput): Promise<CommunicateResult>;
34
+ }
35
+ /** RUN-27: generated once from the driver's declarations; no static capability table. */
36
+ export declare function runtimeDescriptor(driver: RuntimeDriver): CanonRuntimeDescriptor;
37
+ export declare function createRunner(options: RunnerOptions): Promise<Runner>;