@tangle-network/agent-app 0.43.50 → 0.43.52
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/dist/assistant/index.d.ts +1 -1
- package/dist/assistant/index.js +2 -2
- package/dist/attachment-validation-DX2KIzMC.d.ts +219 -0
- package/dist/chat-routes/index.d.ts +166 -20
- package/dist/chat-routes/index.js +362 -169
- package/dist/chat-routes/index.js.map +1 -1
- package/dist/chunk-3EKOSBYL.js +239 -0
- package/dist/chunk-3EKOSBYL.js.map +1 -0
- package/dist/{chunk-O6H2WD3I.js → chunk-EIG7ZQW2.js} +854 -369
- package/dist/chunk-EIG7ZQW2.js.map +1 -0
- package/dist/web-react/index.d.ts +175 -5
- package/dist/web-react/index.js +15 -2
- package/package.json +1 -1
- package/dist/chunk-LCNY3DCM.js +0 -84
- package/dist/chunk-LCNY3DCM.js.map +0 -1
- package/dist/chunk-O6H2WD3I.js.map +0 -1
- package/dist/file-index-b26ee-_R.d.ts +0 -114
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
ALLOWED_ATTACHMENT_SNIFFED_MIMES,
|
|
3
|
+
ATTACHMENT_ACCEPT,
|
|
4
|
+
ATTACHMENT_MAX_COUNT,
|
|
5
|
+
MAX_ATTACHMENT_TOTAL_BYTES,
|
|
6
|
+
MAX_BINARY_ATTACHMENT_BYTES,
|
|
7
|
+
MAX_TEXT_ATTACHMENT_BYTES,
|
|
8
|
+
attachmentSizeErrorMessage,
|
|
9
|
+
attachmentTotalSizeErrorMessage,
|
|
10
|
+
checkAttachmentType,
|
|
11
|
+
createSandboxFileIndexRoute,
|
|
12
|
+
sanitizeAttachmentFileName,
|
|
13
|
+
sniffBinary
|
|
14
|
+
} from "../chunk-3EKOSBYL.js";
|
|
4
15
|
import {
|
|
5
16
|
DEFAULT_STALE_TURN_LOCK_GRACE_MS,
|
|
6
17
|
DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS,
|
|
@@ -759,6 +770,59 @@ function createSandboxChatProducer(options) {
|
|
|
759
770
|
};
|
|
760
771
|
}
|
|
761
772
|
|
|
773
|
+
// src/chat-routes/detached-turn.ts
|
|
774
|
+
var TERMINAL_ERROR_TYPES = /* @__PURE__ */ new Set(["error", "session.run.failed"]);
|
|
775
|
+
function errorMessageOf(ev) {
|
|
776
|
+
const rec = ev;
|
|
777
|
+
const raw = rec?.data?.message ?? rec?.data?.reason ?? rec?.message;
|
|
778
|
+
return typeof raw === "string" && raw ? raw : "run failed";
|
|
779
|
+
}
|
|
780
|
+
function hasUsage(usage) {
|
|
781
|
+
return typeof usage.inputTokens === "number" && usage.inputTokens > 0;
|
|
782
|
+
}
|
|
783
|
+
async function runDetachedTurn(opts) {
|
|
784
|
+
const { store, turnId, scopeId } = opts;
|
|
785
|
+
const prior = await store.getStatus(turnId).catch(() => null);
|
|
786
|
+
if (prior === "complete") {
|
|
787
|
+
const final = opts.completedResult ? await opts.completedResult().catch(() => null) : null;
|
|
788
|
+
return { state: "completed", text: final?.text ?? "", usage: final?.usage ?? {}, cached: true };
|
|
789
|
+
}
|
|
790
|
+
const tap = createBufferedTurnTap({
|
|
791
|
+
store,
|
|
792
|
+
turnId,
|
|
793
|
+
scopeId,
|
|
794
|
+
coalesce: opts.coalesce ?? coalesceDeltas
|
|
795
|
+
});
|
|
796
|
+
await tap.onEvent({ type: "turn", turnId });
|
|
797
|
+
const producer = createSandboxChatProducer({
|
|
798
|
+
events: opts.events,
|
|
799
|
+
model: opts.model,
|
|
800
|
+
log: opts.log
|
|
801
|
+
});
|
|
802
|
+
let runError;
|
|
803
|
+
try {
|
|
804
|
+
for await (const ev of producer.stream) {
|
|
805
|
+
const type = ev.type;
|
|
806
|
+
if (typeof type === "string" && TERMINAL_ERROR_TYPES.has(type)) runError = errorMessageOf(ev);
|
|
807
|
+
await tap.onEvent(ev);
|
|
808
|
+
}
|
|
809
|
+
await tap.done(runError ? "error" : "complete");
|
|
810
|
+
} catch (err) {
|
|
811
|
+
await tap.done("error").catch(() => {
|
|
812
|
+
});
|
|
813
|
+
throw err;
|
|
814
|
+
}
|
|
815
|
+
const text = producer.finalText?.() ?? "";
|
|
816
|
+
let usage = producer.usage?.() ?? {};
|
|
817
|
+
if (!runError && !hasUsage(usage)) {
|
|
818
|
+
const final = opts.completedResult ? await opts.completedResult().catch(() => null) : null;
|
|
819
|
+
if (final?.usage) usage = { ...usage, ...final.usage };
|
|
820
|
+
if (!text && final?.text) return { state: "completed", text: final.text, usage, cached: false };
|
|
821
|
+
}
|
|
822
|
+
if (runError) return { state: "failed", text, usage, error: runError, cached: false };
|
|
823
|
+
return { state: "completed", text, usage, cached: false };
|
|
824
|
+
}
|
|
825
|
+
|
|
762
826
|
// src/chat-routes/durable-projection.ts
|
|
763
827
|
function errorMessage(error) {
|
|
764
828
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -896,12 +960,7 @@ function createUploadRoute(options) {
|
|
|
896
960
|
}
|
|
897
961
|
|
|
898
962
|
// src/chat-routes/resolve-attachments.ts
|
|
899
|
-
var ATTACHMENT_MAX_COUNT = 10;
|
|
900
|
-
var MAX_ATTACHMENT_TOTAL_BYTES = 25 * 1024 * 1024;
|
|
901
963
|
var MAX_ATTACHMENT_NAME_LENGTH = 256;
|
|
902
|
-
function attachmentTotalSizeErrorMessage(totalBytes, limitBytes) {
|
|
903
|
-
return `Attachments total ${formatBytes(totalBytes)}; each message is limited to ${formatBytes(limitBytes)}`;
|
|
904
|
-
}
|
|
905
964
|
function isAttachmentKind(value) {
|
|
906
965
|
return value === "image" || value === "file";
|
|
907
966
|
}
|
|
@@ -995,164 +1054,6 @@ async function resolveChatAttachments(value, options) {
|
|
|
995
1054
|
return { succeeded: true, value: inputs.map(attachmentInputToPart) };
|
|
996
1055
|
}
|
|
997
1056
|
|
|
998
|
-
// src/chat-routes/dispatch-parts.ts
|
|
999
|
-
function byteLen(value) {
|
|
1000
|
-
return new TextEncoder().encode(value).length;
|
|
1001
|
-
}
|
|
1002
|
-
async function readSandboxMention(box, absolutePath, options) {
|
|
1003
|
-
const stat = await statSandboxFileSize(box, absolutePath);
|
|
1004
|
-
if (!stat.succeeded) {
|
|
1005
|
-
return { succeeded: false, error: `mentioned sandbox file missing or unreadable: ${absolutePath} \u2014 ${stat.error}` };
|
|
1006
|
-
}
|
|
1007
|
-
if (!options.readBytes) return { succeeded: true, value: { size: stat.value } };
|
|
1008
|
-
const read = await readSandboxBinaryBytes(box, absolutePath, stat.value);
|
|
1009
|
-
if (!read.succeeded) {
|
|
1010
|
-
return { succeeded: false, error: `mentioned sandbox file read failed: ${absolutePath} \u2014 ${read.error}` };
|
|
1011
|
-
}
|
|
1012
|
-
return { succeeded: true, value: { size: stat.value, base64: bytesToBase64(read.value.bytes) } };
|
|
1013
|
-
}
|
|
1014
|
-
function violatesUrlPathXor(part) {
|
|
1015
|
-
if (part.type === "text") return false;
|
|
1016
|
-
const hasUrl = typeof part.url === "string" && part.url.startsWith("data:");
|
|
1017
|
-
const hasPath = typeof part.path === "string" && part.path.startsWith("/");
|
|
1018
|
-
return hasUrl === hasPath;
|
|
1019
|
-
}
|
|
1020
|
-
function readResultToBase64(read) {
|
|
1021
|
-
if (typeof read.base64 === "string") return read.base64;
|
|
1022
|
-
if (read.bytes) return bytesToBase64(read.bytes);
|
|
1023
|
-
return void 0;
|
|
1024
|
-
}
|
|
1025
|
-
async function buildDispatchParts(input) {
|
|
1026
|
-
const readMention = input.readSandboxMention ?? readSandboxMention;
|
|
1027
|
-
const resolveMentionPath = input.resolveMentionPath ?? input.resolveAttachmentPath;
|
|
1028
|
-
const forcePath = input.forcePath ?? false;
|
|
1029
|
-
const mentions = input.mentions ?? [];
|
|
1030
|
-
const requestMaxBytes = input.requestMaxBytes ?? DISPATCH_REQUEST_MAX_BYTES;
|
|
1031
|
-
const structuralReserveBytes = input.structuralReserveBytes ?? DISPATCH_STRUCTURAL_RESERVE_BYTES;
|
|
1032
|
-
const maxParts = input.maxParts ?? DISPATCH_MAX_PARTS;
|
|
1033
|
-
const parts = [{ type: "text", text: input.text }];
|
|
1034
|
-
const emittedAbsPaths = /* @__PURE__ */ new Set();
|
|
1035
|
-
const flattenedForSizing = flattenHistory(input.text, input.history);
|
|
1036
|
-
const inlineBudget = requestMaxBytes - base64WireLen(byteLen(flattenedForSizing)) - byteLen(JSON.stringify(input.systemPrompt)) - input.profileWireBytes - structuralReserveBytes;
|
|
1037
|
-
let runningInline = 0;
|
|
1038
|
-
for (const attachment of input.attachments) {
|
|
1039
|
-
if (!attachment.path) {
|
|
1040
|
-
return { succeeded: false, error: `attachment path must be non-empty: ${attachment.name}` };
|
|
1041
|
-
}
|
|
1042
|
-
let read;
|
|
1043
|
-
try {
|
|
1044
|
-
read = await input.readAttachment(input.scopeId, attachment.path);
|
|
1045
|
-
} catch (err) {
|
|
1046
|
-
return {
|
|
1047
|
-
succeeded: false,
|
|
1048
|
-
error: `attachment store read failed: ${attachment.path} \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
1049
|
-
};
|
|
1050
|
-
}
|
|
1051
|
-
if (!read.ok) return { succeeded: false, error: read.reason };
|
|
1052
|
-
const base64 = readResultToBase64(read);
|
|
1053
|
-
if (base64 === void 0) {
|
|
1054
|
-
return { succeeded: false, error: `attachment store read produced no content: ${attachment.path}` };
|
|
1055
|
-
}
|
|
1056
|
-
const mediaType = attachment.mediaType ?? read.mediaType;
|
|
1057
|
-
if (attachment.type === "image" && !mediaType) {
|
|
1058
|
-
return { succeeded: false, error: `attachment is missing a mediaType required for an image data URI: ${attachment.path}` };
|
|
1059
|
-
}
|
|
1060
|
-
const absPath = input.resolveAttachmentPath(attachment.path);
|
|
1061
|
-
emittedAbsPaths.add(absPath);
|
|
1062
|
-
if (attachment.type === "image") {
|
|
1063
|
-
const inlinePart2 = {
|
|
1064
|
-
type: "image",
|
|
1065
|
-
filename: attachment.name,
|
|
1066
|
-
mediaType,
|
|
1067
|
-
url: `data:${mediaType};base64,${base64}`
|
|
1068
|
-
};
|
|
1069
|
-
const cost2 = byteLen(JSON.stringify(inlinePart2));
|
|
1070
|
-
if (!forcePath && runningInline + cost2 <= inlineBudget) {
|
|
1071
|
-
parts.push(inlinePart2);
|
|
1072
|
-
runningInline += cost2;
|
|
1073
|
-
} else {
|
|
1074
|
-
parts.push({ type: "image", filename: attachment.name, mediaType, path: absPath });
|
|
1075
|
-
}
|
|
1076
|
-
continue;
|
|
1077
|
-
}
|
|
1078
|
-
const fileMediaType = mediaType ?? "application/octet-stream";
|
|
1079
|
-
const inlinePart = {
|
|
1080
|
-
type: "file",
|
|
1081
|
-
filename: attachment.name,
|
|
1082
|
-
mediaType: fileMediaType,
|
|
1083
|
-
url: `data:${fileMediaType};base64,${base64}`
|
|
1084
|
-
};
|
|
1085
|
-
const cost = byteLen(JSON.stringify(inlinePart));
|
|
1086
|
-
if (!forcePath && runningInline + cost <= inlineBudget) {
|
|
1087
|
-
parts.push(inlinePart);
|
|
1088
|
-
runningInline += cost;
|
|
1089
|
-
} else {
|
|
1090
|
-
parts.push({ type: "file", path: absPath });
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
if (mentions.length > 0 && !input.box) {
|
|
1094
|
-
return { succeeded: false, error: "internal error: sandbox mentions require a box to read from" };
|
|
1095
|
-
}
|
|
1096
|
-
for (const mention of mentions) {
|
|
1097
|
-
if (!mention.path) {
|
|
1098
|
-
return { succeeded: false, error: `mention path must be non-empty: ${mention.name}` };
|
|
1099
|
-
}
|
|
1100
|
-
const absPath = resolveMentionPath(mention.path);
|
|
1101
|
-
if (emittedAbsPaths.has(absPath)) continue;
|
|
1102
|
-
emittedAbsPaths.add(absPath);
|
|
1103
|
-
const isImage = mention.mentionKind === "image";
|
|
1104
|
-
const mediaType = isImage ? mediaTypeForMentionPath(mention.path) : void 0;
|
|
1105
|
-
let stat;
|
|
1106
|
-
try {
|
|
1107
|
-
stat = await readMention(input.box, absPath, { readBytes: false });
|
|
1108
|
-
} catch (err) {
|
|
1109
|
-
return { succeeded: false, error: `mention read failed: ${absPath} \u2014 ${err instanceof Error ? err.message : String(err)}` };
|
|
1110
|
-
}
|
|
1111
|
-
if (!stat.succeeded) return { succeeded: false, error: stat.error };
|
|
1112
|
-
const projectedInlineCost = base64WireLen(stat.value.size) + byteLen(JSON.stringify({ type: "image", filename: mention.name, mediaType: mediaType ?? "", url: "" }));
|
|
1113
|
-
if (isImage && mediaType && !forcePath && runningInline + projectedInlineCost <= inlineBudget) {
|
|
1114
|
-
let read;
|
|
1115
|
-
try {
|
|
1116
|
-
read = await readMention(input.box, absPath, { readBytes: true });
|
|
1117
|
-
} catch (err) {
|
|
1118
|
-
return { succeeded: false, error: `mention read failed: ${absPath} \u2014 ${err instanceof Error ? err.message : String(err)}` };
|
|
1119
|
-
}
|
|
1120
|
-
if (!read.succeeded) return { succeeded: false, error: read.error };
|
|
1121
|
-
if (!read.value.base64) return { succeeded: false, error: `mentioned image produced no bytes: ${absPath}` };
|
|
1122
|
-
const inlinePart = {
|
|
1123
|
-
type: "image",
|
|
1124
|
-
filename: mention.name,
|
|
1125
|
-
mediaType,
|
|
1126
|
-
url: `data:${mediaType};base64,${read.value.base64}`
|
|
1127
|
-
};
|
|
1128
|
-
const cost = byteLen(JSON.stringify(inlinePart));
|
|
1129
|
-
if (runningInline + cost <= inlineBudget) {
|
|
1130
|
-
parts.push(inlinePart);
|
|
1131
|
-
runningInline += cost;
|
|
1132
|
-
continue;
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
parts.push(
|
|
1136
|
-
isImage && mediaType ? { type: "image", filename: mention.name, mediaType, path: absPath } : { type: "file", path: absPath }
|
|
1137
|
-
);
|
|
1138
|
-
}
|
|
1139
|
-
for (const part of parts) {
|
|
1140
|
-
if (violatesUrlPathXor(part)) {
|
|
1141
|
-
return { succeeded: false, error: "internal error: emitted media part violates the url/path exclusivity invariant" };
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
const textPartSize = base64WireLen(byteLen(flattenedForSizing));
|
|
1145
|
-
const mediaPartsSize = parts.slice(1).reduce((total, part) => total + byteLen(JSON.stringify(part)), 0);
|
|
1146
|
-
const systemPromptSize = byteLen(JSON.stringify(input.systemPrompt));
|
|
1147
|
-
if (textPartSize + mediaPartsSize + systemPromptSize + input.profileWireBytes + structuralReserveBytes > requestMaxBytes) {
|
|
1148
|
-
return { succeeded: false, error: "dispatch parts exceed the sandbox proxy request cap even after path demotion" };
|
|
1149
|
-
}
|
|
1150
|
-
if (parts.length > maxParts) {
|
|
1151
|
-
return { succeeded: false, error: `dispatch parts exceed the sidecar per-request cap of ${maxParts}` };
|
|
1152
|
-
}
|
|
1153
|
-
return { succeeded: true, value: parts };
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
1057
|
// src/chat-routes/promote-file-part.ts
|
|
1157
1058
|
var PROMOTE_MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
1158
1059
|
var EXT_TO_MIME = {
|
|
@@ -1192,10 +1093,6 @@ function sniffMimeFromName(filename) {
|
|
|
1192
1093
|
if (!ext) return "text/plain";
|
|
1193
1094
|
return EXT_TO_MIME[ext] ?? "text/plain";
|
|
1194
1095
|
}
|
|
1195
|
-
function sanitizeAttachmentFileName(name) {
|
|
1196
|
-
const sanitized = name.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[.-]+/, "");
|
|
1197
|
-
return sanitized || "file";
|
|
1198
|
-
}
|
|
1199
1096
|
function base64ToBytes(base64) {
|
|
1200
1097
|
const binary = atob(base64);
|
|
1201
1098
|
const bytes = new Uint8Array(binary.length);
|
|
@@ -1328,7 +1225,295 @@ async function promoteAgentFilePart(options) {
|
|
|
1328
1225
|
}
|
|
1329
1226
|
};
|
|
1330
1227
|
}
|
|
1228
|
+
|
|
1229
|
+
// src/chat-routes/attachment-upload.ts
|
|
1230
|
+
function attachmentUploadError(status, code, message, path) {
|
|
1231
|
+
return Response.json(
|
|
1232
|
+
{ error: path === void 0 ? { code, message } : { code, message, path } },
|
|
1233
|
+
{ status }
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
function createAttachmentUploadRoute(options) {
|
|
1237
|
+
const maxCount = options.limits?.maxCount ?? ATTACHMENT_MAX_COUNT;
|
|
1238
|
+
const maxBinaryBytes = options.limits?.maxBinaryBytes ?? MAX_BINARY_ATTACHMENT_BYTES;
|
|
1239
|
+
const maxTextBytes = options.limits?.maxTextBytes ?? MAX_TEXT_ATTACHMENT_BYTES;
|
|
1240
|
+
const maxTotalBytes = options.limits?.maxTotalBytes ?? MAX_ATTACHMENT_TOTAL_BYTES;
|
|
1241
|
+
const allowedKinds = options.allowedKinds ?? ["image", "file"];
|
|
1242
|
+
const allowedSniffedMimes = options.allowedSniffedMimes ?? ALLOWED_ATTACHMENT_SNIFFED_MIMES;
|
|
1243
|
+
const pathFor = options.pathFor ?? ((name) => name);
|
|
1244
|
+
const validatePath = options.validatePath ?? defaultValidateAttachmentPath;
|
|
1245
|
+
const sniffMime = options.sniffMime ?? sniffMimeFromName;
|
|
1246
|
+
return async function attachmentUpload(request) {
|
|
1247
|
+
const auth = await options.authorize({ request });
|
|
1248
|
+
if (!auth.ok) return auth.response;
|
|
1249
|
+
const write = auth.writeAttachment ?? options.writeAttachment;
|
|
1250
|
+
let form;
|
|
1251
|
+
try {
|
|
1252
|
+
form = await request.formData();
|
|
1253
|
+
} catch {
|
|
1254
|
+
return attachmentUploadError(400, "invalid_upload", "Expected a multipart/form-data body with file fields");
|
|
1255
|
+
}
|
|
1256
|
+
const files = [];
|
|
1257
|
+
form.forEach((value) => {
|
|
1258
|
+
if (value instanceof File) files.push(value);
|
|
1259
|
+
});
|
|
1260
|
+
if (files.length === 0) {
|
|
1261
|
+
return attachmentUploadError(400, "invalid_upload", "No files in the upload body");
|
|
1262
|
+
}
|
|
1263
|
+
if (files.length > maxCount) {
|
|
1264
|
+
return attachmentUploadError(
|
|
1265
|
+
400,
|
|
1266
|
+
"attachment_count_exceeded",
|
|
1267
|
+
`Too many files \u2014 the ${maxCount}-file limit was exceeded`
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
const advisoryTotal = files.reduce((sum, file) => sum + file.size, 0);
|
|
1271
|
+
if (advisoryTotal > maxTotalBytes) {
|
|
1272
|
+
return attachmentUploadError(
|
|
1273
|
+
413,
|
|
1274
|
+
"attachments_total_too_large",
|
|
1275
|
+
attachmentTotalSizeErrorMessage(advisoryTotal, maxTotalBytes)
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
const prepared = [];
|
|
1279
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
1280
|
+
let totalBytes = 0;
|
|
1281
|
+
for (const file of files) {
|
|
1282
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
1283
|
+
const sniff = sniffBinary(bytes);
|
|
1284
|
+
const name = sanitizeAttachmentFileName(file.name);
|
|
1285
|
+
const typeCheck = checkAttachmentType(name, sniff, allowedSniffedMimes);
|
|
1286
|
+
if (!typeCheck.succeeded) {
|
|
1287
|
+
return attachmentUploadError(
|
|
1288
|
+
typeCheck.code === "attachment_type_mismatch" ? 400 : 415,
|
|
1289
|
+
typeCheck.code,
|
|
1290
|
+
typeCheck.message
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
const kind = attachmentKindForMime(sniff.mime ?? "");
|
|
1294
|
+
if (!allowedKinds.includes(kind)) {
|
|
1295
|
+
return attachmentUploadError(
|
|
1296
|
+
415,
|
|
1297
|
+
"attachment_kind_not_allowed",
|
|
1298
|
+
`${name} is a "${kind}" attachment, which this upload route does not accept`
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
const limit = sniff.binary ? maxBinaryBytes : maxTextBytes;
|
|
1302
|
+
if (bytes.length > limit) {
|
|
1303
|
+
return attachmentUploadError(
|
|
1304
|
+
413,
|
|
1305
|
+
"attachment_too_large",
|
|
1306
|
+
attachmentSizeErrorMessage(name, bytes.length, limit)
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
const path = pathFor(name);
|
|
1310
|
+
const pathCheck = validatePath(path);
|
|
1311
|
+
if (!pathCheck.succeeded) {
|
|
1312
|
+
return attachmentUploadError(400, "invalid_attachment_path", pathCheck.error, path);
|
|
1313
|
+
}
|
|
1314
|
+
if (seenPaths.has(path)) {
|
|
1315
|
+
return attachmentUploadError(
|
|
1316
|
+
400,
|
|
1317
|
+
"attachment_duplicate_path",
|
|
1318
|
+
`attachments must not repeat a path within one upload: ${path}`,
|
|
1319
|
+
path
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
seenPaths.add(path);
|
|
1323
|
+
totalBytes += bytes.length;
|
|
1324
|
+
if (totalBytes > maxTotalBytes) {
|
|
1325
|
+
return attachmentUploadError(
|
|
1326
|
+
413,
|
|
1327
|
+
"attachments_total_too_large",
|
|
1328
|
+
attachmentTotalSizeErrorMessage(totalBytes, maxTotalBytes)
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
const mediaType = sniff.mime ?? sniffMime(name);
|
|
1332
|
+
prepared.push({ path, name, bytes, originalName: file.name, size: bytes.length, mediaType, kind });
|
|
1333
|
+
}
|
|
1334
|
+
const uploaded = [];
|
|
1335
|
+
for (const input of prepared) {
|
|
1336
|
+
const written = await write(auth.scopeId, input.path, input.bytes, {
|
|
1337
|
+
mediaType: input.mediaType,
|
|
1338
|
+
name: input.name,
|
|
1339
|
+
originalName: input.originalName,
|
|
1340
|
+
size: input.size
|
|
1341
|
+
});
|
|
1342
|
+
if (!written.ok) {
|
|
1343
|
+
return attachmentUploadError(413, "attachment_write_failed", written.reason, input.path);
|
|
1344
|
+
}
|
|
1345
|
+
uploaded.push({
|
|
1346
|
+
path: input.path,
|
|
1347
|
+
name: input.name,
|
|
1348
|
+
size: input.size,
|
|
1349
|
+
mediaType: input.mediaType,
|
|
1350
|
+
kind: input.kind
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
return Response.json({ files: uploaded });
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// src/chat-routes/dispatch-parts.ts
|
|
1358
|
+
function byteLen(value) {
|
|
1359
|
+
return new TextEncoder().encode(value).length;
|
|
1360
|
+
}
|
|
1361
|
+
async function readSandboxMention(box, absolutePath, options) {
|
|
1362
|
+
const stat = await statSandboxFileSize(box, absolutePath);
|
|
1363
|
+
if (!stat.succeeded) {
|
|
1364
|
+
return { succeeded: false, error: `mentioned sandbox file missing or unreadable: ${absolutePath} \u2014 ${stat.error}` };
|
|
1365
|
+
}
|
|
1366
|
+
if (!options.readBytes) return { succeeded: true, value: { size: stat.value } };
|
|
1367
|
+
const read = await readSandboxBinaryBytes(box, absolutePath, stat.value);
|
|
1368
|
+
if (!read.succeeded) {
|
|
1369
|
+
return { succeeded: false, error: `mentioned sandbox file read failed: ${absolutePath} \u2014 ${read.error}` };
|
|
1370
|
+
}
|
|
1371
|
+
return { succeeded: true, value: { size: stat.value, base64: bytesToBase64(read.value.bytes) } };
|
|
1372
|
+
}
|
|
1373
|
+
function violatesUrlPathXor(part) {
|
|
1374
|
+
if (part.type === "text") return false;
|
|
1375
|
+
const hasUrl = typeof part.url === "string" && part.url.startsWith("data:");
|
|
1376
|
+
const hasPath = typeof part.path === "string" && part.path.startsWith("/");
|
|
1377
|
+
return hasUrl === hasPath;
|
|
1378
|
+
}
|
|
1379
|
+
function readResultToBase64(read) {
|
|
1380
|
+
if (typeof read.base64 === "string") return read.base64;
|
|
1381
|
+
if (read.bytes) return bytesToBase64(read.bytes);
|
|
1382
|
+
return void 0;
|
|
1383
|
+
}
|
|
1384
|
+
async function buildDispatchParts(input) {
|
|
1385
|
+
const readMention = input.readSandboxMention ?? readSandboxMention;
|
|
1386
|
+
const resolveMentionPath = input.resolveMentionPath ?? input.resolveAttachmentPath;
|
|
1387
|
+
const forcePath = input.forcePath ?? false;
|
|
1388
|
+
const mentions = input.mentions ?? [];
|
|
1389
|
+
const requestMaxBytes = input.requestMaxBytes ?? DISPATCH_REQUEST_MAX_BYTES;
|
|
1390
|
+
const structuralReserveBytes = input.structuralReserveBytes ?? DISPATCH_STRUCTURAL_RESERVE_BYTES;
|
|
1391
|
+
const maxParts = input.maxParts ?? DISPATCH_MAX_PARTS;
|
|
1392
|
+
const parts = [{ type: "text", text: input.text }];
|
|
1393
|
+
const emittedAbsPaths = /* @__PURE__ */ new Set();
|
|
1394
|
+
const flattenedForSizing = flattenHistory(input.text, input.history);
|
|
1395
|
+
const inlineBudget = requestMaxBytes - base64WireLen(byteLen(flattenedForSizing)) - byteLen(JSON.stringify(input.systemPrompt)) - input.profileWireBytes - structuralReserveBytes;
|
|
1396
|
+
let runningInline = 0;
|
|
1397
|
+
for (const attachment of input.attachments) {
|
|
1398
|
+
if (!attachment.path) {
|
|
1399
|
+
return { succeeded: false, error: `attachment path must be non-empty: ${attachment.name}` };
|
|
1400
|
+
}
|
|
1401
|
+
let read;
|
|
1402
|
+
try {
|
|
1403
|
+
read = await input.readAttachment(input.scopeId, attachment.path);
|
|
1404
|
+
} catch (err) {
|
|
1405
|
+
return {
|
|
1406
|
+
succeeded: false,
|
|
1407
|
+
error: `attachment store read failed: ${attachment.path} \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
if (!read.ok) return { succeeded: false, error: read.reason };
|
|
1411
|
+
const base64 = readResultToBase64(read);
|
|
1412
|
+
if (base64 === void 0) {
|
|
1413
|
+
return { succeeded: false, error: `attachment store read produced no content: ${attachment.path}` };
|
|
1414
|
+
}
|
|
1415
|
+
const mediaType = attachment.mediaType ?? read.mediaType;
|
|
1416
|
+
if (attachment.type === "image" && !mediaType) {
|
|
1417
|
+
return { succeeded: false, error: `attachment is missing a mediaType required for an image data URI: ${attachment.path}` };
|
|
1418
|
+
}
|
|
1419
|
+
const absPath = input.resolveAttachmentPath(attachment.path);
|
|
1420
|
+
emittedAbsPaths.add(absPath);
|
|
1421
|
+
if (attachment.type === "image") {
|
|
1422
|
+
const inlinePart2 = {
|
|
1423
|
+
type: "image",
|
|
1424
|
+
filename: attachment.name,
|
|
1425
|
+
mediaType,
|
|
1426
|
+
url: `data:${mediaType};base64,${base64}`
|
|
1427
|
+
};
|
|
1428
|
+
const cost2 = byteLen(JSON.stringify(inlinePart2));
|
|
1429
|
+
if (!forcePath && runningInline + cost2 <= inlineBudget) {
|
|
1430
|
+
parts.push(inlinePart2);
|
|
1431
|
+
runningInline += cost2;
|
|
1432
|
+
} else {
|
|
1433
|
+
parts.push({ type: "image", filename: attachment.name, mediaType, path: absPath });
|
|
1434
|
+
}
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
const fileMediaType = mediaType ?? "application/octet-stream";
|
|
1438
|
+
const inlinePart = {
|
|
1439
|
+
type: "file",
|
|
1440
|
+
filename: attachment.name,
|
|
1441
|
+
mediaType: fileMediaType,
|
|
1442
|
+
url: `data:${fileMediaType};base64,${base64}`
|
|
1443
|
+
};
|
|
1444
|
+
const cost = byteLen(JSON.stringify(inlinePart));
|
|
1445
|
+
if (!forcePath && runningInline + cost <= inlineBudget) {
|
|
1446
|
+
parts.push(inlinePart);
|
|
1447
|
+
runningInline += cost;
|
|
1448
|
+
} else {
|
|
1449
|
+
parts.push({ type: "file", path: absPath });
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
if (mentions.length > 0 && !input.box) {
|
|
1453
|
+
return { succeeded: false, error: "internal error: sandbox mentions require a box to read from" };
|
|
1454
|
+
}
|
|
1455
|
+
for (const mention of mentions) {
|
|
1456
|
+
if (!mention.path) {
|
|
1457
|
+
return { succeeded: false, error: `mention path must be non-empty: ${mention.name}` };
|
|
1458
|
+
}
|
|
1459
|
+
const absPath = resolveMentionPath(mention.path);
|
|
1460
|
+
if (emittedAbsPaths.has(absPath)) continue;
|
|
1461
|
+
emittedAbsPaths.add(absPath);
|
|
1462
|
+
const isImage = mention.mentionKind === "image";
|
|
1463
|
+
const mediaType = isImage ? mediaTypeForMentionPath(mention.path) : void 0;
|
|
1464
|
+
let stat;
|
|
1465
|
+
try {
|
|
1466
|
+
stat = await readMention(input.box, absPath, { readBytes: false });
|
|
1467
|
+
} catch (err) {
|
|
1468
|
+
return { succeeded: false, error: `mention read failed: ${absPath} \u2014 ${err instanceof Error ? err.message : String(err)}` };
|
|
1469
|
+
}
|
|
1470
|
+
if (!stat.succeeded) return { succeeded: false, error: stat.error };
|
|
1471
|
+
const projectedInlineCost = base64WireLen(stat.value.size) + byteLen(JSON.stringify({ type: "image", filename: mention.name, mediaType: mediaType ?? "", url: "" }));
|
|
1472
|
+
if (isImage && mediaType && !forcePath && runningInline + projectedInlineCost <= inlineBudget) {
|
|
1473
|
+
let read;
|
|
1474
|
+
try {
|
|
1475
|
+
read = await readMention(input.box, absPath, { readBytes: true });
|
|
1476
|
+
} catch (err) {
|
|
1477
|
+
return { succeeded: false, error: `mention read failed: ${absPath} \u2014 ${err instanceof Error ? err.message : String(err)}` };
|
|
1478
|
+
}
|
|
1479
|
+
if (!read.succeeded) return { succeeded: false, error: read.error };
|
|
1480
|
+
if (!read.value.base64) return { succeeded: false, error: `mentioned image produced no bytes: ${absPath}` };
|
|
1481
|
+
const inlinePart = {
|
|
1482
|
+
type: "image",
|
|
1483
|
+
filename: mention.name,
|
|
1484
|
+
mediaType,
|
|
1485
|
+
url: `data:${mediaType};base64,${read.value.base64}`
|
|
1486
|
+
};
|
|
1487
|
+
const cost = byteLen(JSON.stringify(inlinePart));
|
|
1488
|
+
if (runningInline + cost <= inlineBudget) {
|
|
1489
|
+
parts.push(inlinePart);
|
|
1490
|
+
runningInline += cost;
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
parts.push(
|
|
1495
|
+
isImage && mediaType ? { type: "image", filename: mention.name, mediaType, path: absPath } : { type: "file", path: absPath }
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
for (const part of parts) {
|
|
1499
|
+
if (violatesUrlPathXor(part)) {
|
|
1500
|
+
return { succeeded: false, error: "internal error: emitted media part violates the url/path exclusivity invariant" };
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
const textPartSize = base64WireLen(byteLen(flattenedForSizing));
|
|
1504
|
+
const mediaPartsSize = parts.slice(1).reduce((total, part) => total + byteLen(JSON.stringify(part)), 0);
|
|
1505
|
+
const systemPromptSize = byteLen(JSON.stringify(input.systemPrompt));
|
|
1506
|
+
if (textPartSize + mediaPartsSize + systemPromptSize + input.profileWireBytes + structuralReserveBytes > requestMaxBytes) {
|
|
1507
|
+
return { succeeded: false, error: "dispatch parts exceed the sandbox proxy request cap even after path demotion" };
|
|
1508
|
+
}
|
|
1509
|
+
if (parts.length > maxParts) {
|
|
1510
|
+
return { succeeded: false, error: `dispatch parts exceed the sidecar per-request cap of ${maxParts}` };
|
|
1511
|
+
}
|
|
1512
|
+
return { succeeded: true, value: parts };
|
|
1513
|
+
}
|
|
1331
1514
|
export {
|
|
1515
|
+
ALLOWED_ATTACHMENT_SNIFFED_MIMES,
|
|
1516
|
+
ATTACHMENT_ACCEPT,
|
|
1332
1517
|
ATTACHMENT_MAX_COUNT,
|
|
1333
1518
|
ChatTurnInputError,
|
|
1334
1519
|
DEFAULT_STALE_TURN_LOCK_GRACE_MS,
|
|
@@ -1339,17 +1524,22 @@ export {
|
|
|
1339
1524
|
DISPATCH_STRUCTURAL_RESERVE_BYTES,
|
|
1340
1525
|
INLINE_PARTS_MAX_BYTES,
|
|
1341
1526
|
MAX_ATTACHMENT_TOTAL_BYTES,
|
|
1527
|
+
MAX_BINARY_ATTACHMENT_BYTES,
|
|
1528
|
+
MAX_TEXT_ATTACHMENT_BYTES,
|
|
1342
1529
|
MENTION_MAX_COUNT,
|
|
1343
1530
|
PROMOTE_MAX_FILE_BYTES,
|
|
1344
1531
|
UPLOAD_INLINE_MAX_BYTES,
|
|
1345
1532
|
UPLOAD_MAX_FILE_BYTES,
|
|
1346
1533
|
assertPromptPartsWithinCap,
|
|
1534
|
+
attachmentSizeErrorMessage,
|
|
1347
1535
|
attachmentTotalSizeErrorMessage,
|
|
1348
1536
|
base64WireLen,
|
|
1349
1537
|
buildDispatchParts,
|
|
1350
1538
|
buildMentionPromptBlock,
|
|
1351
1539
|
bytesToBase64,
|
|
1352
1540
|
chatTurnRequestInit,
|
|
1541
|
+
checkAttachmentType,
|
|
1542
|
+
createAttachmentUploadRoute,
|
|
1353
1543
|
createChatTurnRoutes,
|
|
1354
1544
|
createSandboxChatProducer,
|
|
1355
1545
|
createSandboxFileIndexRoute,
|
|
@@ -1365,7 +1555,10 @@ export {
|
|
|
1365
1555
|
promptPartsByteSize,
|
|
1366
1556
|
reconcileStaleTurnLock,
|
|
1367
1557
|
resolveChatAttachments,
|
|
1558
|
+
runDetachedTurn,
|
|
1559
|
+
sanitizeAttachmentFileName,
|
|
1368
1560
|
sanitizeUploadFilename,
|
|
1561
|
+
sniffBinary,
|
|
1369
1562
|
sniffMimeFromName,
|
|
1370
1563
|
validateSandboxMentionPath,
|
|
1371
1564
|
withDurableChatProjection
|