@tangle-network/agent-app 0.43.47 → 0.43.49
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 +3 -2
- package/dist/assistant/index.js +4 -3
- package/dist/assistant/index.js.map +1 -1
- package/dist/chat-routes/index.d.ts +394 -4
- package/dist/chat-routes/index.js +510 -12
- package/dist/chat-routes/index.js.map +1 -1
- package/dist/chat-store/index.d.ts +3 -2
- package/dist/chat-store/index.js +19 -1
- package/dist/chat-store/index.js.map +1 -1
- package/dist/chunk-23LUHHHN.js +282 -0
- package/dist/chunk-23LUHHHN.js.map +1 -0
- package/dist/{chunk-UHXQ3KNX.js → chunk-54GSK2KK.js} +4 -4
- package/dist/{chunk-4AUQIAYU.js → chunk-5MG74GVQ.js} +2 -281
- package/dist/chunk-5MG74GVQ.js.map +1 -0
- package/dist/{chunk-5RJNEEO2.js → chunk-HRJJH3RX.js} +2 -2
- package/dist/{chunk-6E2XJSCT.js → chunk-JGYOYY5D.js} +65 -2
- package/dist/chunk-JGYOYY5D.js.map +1 -0
- package/dist/{chunk-E7QYOOON.js → chunk-MCJSS6SM.js} +2 -1
- package/dist/chunk-MCJSS6SM.js.map +1 -0
- package/dist/chunk-NBSBRZ6F.js +337 -0
- package/dist/chunk-NBSBRZ6F.js.map +1 -0
- package/dist/{chunk-7VMUOD3G.js → chunk-O6H2WD3I.js} +2 -2
- package/dist/chunk-O6H2WD3I.js.map +1 -0
- package/dist/{file-index-Bn6sitKb.d.ts → file-index-b26ee-_R.d.ts} +1 -1
- package/dist/harness/index.d.ts +1 -1
- package/dist/harness/index.js +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +119 -103
- package/dist/{parts-1_3y2JmR.d.ts → parts-IB-Kbb7z.d.ts} +146 -2
- package/dist/preset-cloudflare/index.js +2 -2
- package/dist/profile/index.d.ts +13 -2
- package/dist/profile/index.js +25 -4
- package/dist/profile/index.js.map +1 -1
- package/dist/prompt/index.d.ts +2 -1
- package/dist/prompt/index.js.map +1 -1
- package/dist/run/index.js +1 -1
- package/dist/sandbox/index.d.ts +1 -1
- package/dist/sandbox/index.js +2 -2
- package/dist/skills/index.d.ts +183 -8
- package/dist/skills/index.js +21 -3
- package/dist/skills-placement/index.d.ts +52 -0
- package/dist/skills-placement/index.js +44 -0
- package/dist/skills-placement/index.js.map +1 -0
- package/dist/stream/index.d.ts +4 -47
- package/dist/stream/index.js +14 -12
- package/dist/stream-normalizer-DWvtmY6F.d.ts +48 -0
- package/dist/web-react/index.d.ts +4 -3
- package/dist/web-react/index.js +25 -3
- package/package.json +12 -2
- package/dist/chunk-4AUQIAYU.js.map +0 -1
- package/dist/chunk-6E2XJSCT.js.map +0 -1
- package/dist/chunk-7VMUOD3G.js.map +0 -1
- package/dist/chunk-E7QYOOON.js.map +0 -1
- package/dist/chunk-KOG473C4.js +0 -132
- package/dist/chunk-KOG473C4.js.map +0 -1
- /package/dist/{chunk-UHXQ3KNX.js.map → chunk-54GSK2KK.js.map} +0 -0
- /package/dist/{chunk-5RJNEEO2.js.map → chunk-HRJJH3RX.js.map} +0 -0
|
@@ -7,33 +7,34 @@ import {
|
|
|
7
7
|
reconcileStaleTurnLock
|
|
8
8
|
} from "../chunk-BQV42AFK.js";
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
parseJsonObjectBody
|
|
11
|
+
} from "../chunk-HFC4BTWJ.js";
|
|
12
|
+
import {
|
|
12
13
|
coalesceDeltas,
|
|
13
14
|
createBufferedTurnTap,
|
|
14
|
-
finalizeAssistantParts,
|
|
15
|
-
getPartKey,
|
|
16
|
-
mergePersistedPart,
|
|
17
15
|
normalizeClientTurnId,
|
|
18
|
-
normalizePersistedPart,
|
|
19
|
-
normalizeToolEvent,
|
|
20
16
|
replayTurnEvents,
|
|
21
17
|
resolveChatTurn
|
|
22
|
-
} from "../chunk-
|
|
18
|
+
} from "../chunk-23LUHHHN.js";
|
|
23
19
|
import {
|
|
24
20
|
createInteractionAnswerRoute
|
|
25
21
|
} from "../chunk-6KVH5SP5.js";
|
|
26
|
-
import {
|
|
27
|
-
parseJsonObjectBody
|
|
28
|
-
} from "../chunk-HFC4BTWJ.js";
|
|
29
22
|
import {
|
|
30
23
|
ChatTurnInputError,
|
|
24
|
+
DISPATCH_MAX_MEDIA_PARTS,
|
|
25
|
+
DISPATCH_MAX_PARTS,
|
|
26
|
+
DISPATCH_REQUEST_MAX_BYTES,
|
|
27
|
+
DISPATCH_STRUCTURAL_RESERVE_BYTES,
|
|
31
28
|
INLINE_PARTS_MAX_BYTES,
|
|
32
29
|
MENTION_MAX_COUNT,
|
|
33
30
|
assertPromptPartsWithinCap,
|
|
31
|
+
attachmentInputToPart,
|
|
32
|
+
attachmentKindForMime,
|
|
33
|
+
base64WireLen,
|
|
34
34
|
buildMentionPromptBlock,
|
|
35
35
|
chatTurnRequestInit,
|
|
36
36
|
fileMentionsToParts,
|
|
37
|
+
formatBytes,
|
|
37
38
|
mediaTypeForMentionPath,
|
|
38
39
|
mentionInputToPart,
|
|
39
40
|
mentionKindForPath,
|
|
@@ -42,7 +43,16 @@ import {
|
|
|
42
43
|
promptPartsByteSize,
|
|
43
44
|
toChatMessageParts,
|
|
44
45
|
validateSandboxMentionPath
|
|
45
|
-
} from "../chunk-
|
|
46
|
+
} from "../chunk-JGYOYY5D.js";
|
|
47
|
+
import {
|
|
48
|
+
asRecord,
|
|
49
|
+
asString,
|
|
50
|
+
finalizeAssistantParts,
|
|
51
|
+
getPartKey,
|
|
52
|
+
mergePersistedPart,
|
|
53
|
+
normalizePersistedPart,
|
|
54
|
+
normalizeToolEvent
|
|
55
|
+
} from "../chunk-5MG74GVQ.js";
|
|
46
56
|
import {
|
|
47
57
|
cancelStatusFor,
|
|
48
58
|
interactionPartKey,
|
|
@@ -55,6 +65,17 @@ import {
|
|
|
55
65
|
parsePlanSubmittedEvent,
|
|
56
66
|
planToPersistedPart
|
|
57
67
|
} from "../chunk-SIXYZ2FB.js";
|
|
68
|
+
import {
|
|
69
|
+
flattenHistory,
|
|
70
|
+
readSandboxBinaryBytes,
|
|
71
|
+
statSandboxFileSize
|
|
72
|
+
} from "../chunk-HRJJH3RX.js";
|
|
73
|
+
import "../chunk-MCJSS6SM.js";
|
|
74
|
+
import "../chunk-PJC4NXPA.js";
|
|
75
|
+
import "../chunk-7EVZUIHW.js";
|
|
76
|
+
import "../chunk-S5SRJJQG.js";
|
|
77
|
+
import "../chunk-7W5XSTUF.js";
|
|
78
|
+
import "../chunk-MFRCM32T.js";
|
|
58
79
|
|
|
59
80
|
// src/chat-routes/turn-routes.ts
|
|
60
81
|
import { deriveExecutionId, handleChatTurn } from "@tangle-network/agent-runtime";
|
|
@@ -552,6 +573,7 @@ function createSandboxChatProducer(options) {
|
|
|
552
573
|
const announcedTools = /* @__PURE__ */ new Set();
|
|
553
574
|
const settledTools = /* @__PURE__ */ new Set();
|
|
554
575
|
let stepCounter = 0;
|
|
576
|
+
const promotedFileParts = /* @__PURE__ */ new Map();
|
|
555
577
|
function recordPersistedPart(part, delta, keyOverride) {
|
|
556
578
|
const persisted = normalizePersistedPart(part);
|
|
557
579
|
if (!persisted) return;
|
|
@@ -623,6 +645,33 @@ function createSandboxChatProducer(options) {
|
|
|
623
645
|
recordPersistedPart(part, void 0, `step-start:#${stepCounter}`);
|
|
624
646
|
continue;
|
|
625
647
|
}
|
|
648
|
+
if (partType === "file" && options.promoteFilePart) {
|
|
649
|
+
const promote = options.promoteFilePart;
|
|
650
|
+
const rawId = asString(part.id);
|
|
651
|
+
const rawUrl = asString(part.url);
|
|
652
|
+
const memoKey = rawId ? `id:${rawId}` : rawUrl ? `url:${rawUrl}` : void 0;
|
|
653
|
+
const attempt = () => Promise.resolve().then(() => promote(part)).catch((err) => {
|
|
654
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
655
|
+
log("[chat-routes] file part promotion threw", { key: memoKey ?? "(keyless)", error: reason });
|
|
656
|
+
return { succeeded: false, reason };
|
|
657
|
+
});
|
|
658
|
+
let pending;
|
|
659
|
+
if (memoKey) {
|
|
660
|
+
pending = promotedFileParts.get(memoKey) ?? attempt();
|
|
661
|
+
promotedFileParts.set(memoKey, pending);
|
|
662
|
+
} else {
|
|
663
|
+
pending = attempt();
|
|
664
|
+
}
|
|
665
|
+
const outcome = await pending;
|
|
666
|
+
if (outcome.succeeded) {
|
|
667
|
+
recordPersistedPart(outcome.part, void 0, outcome.key);
|
|
668
|
+
} else if (outcome.part) {
|
|
669
|
+
recordPersistedPart(outcome.part, void 0, outcome.key);
|
|
670
|
+
} else {
|
|
671
|
+
recordPersistedPart(part, void 0);
|
|
672
|
+
}
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
626
675
|
recordPersistedPart(part, void 0);
|
|
627
676
|
continue;
|
|
628
677
|
}
|
|
@@ -845,15 +894,459 @@ function createUploadRoute(options) {
|
|
|
845
894
|
return Response.json({ files: uploaded });
|
|
846
895
|
};
|
|
847
896
|
}
|
|
897
|
+
|
|
898
|
+
// src/chat-routes/resolve-attachments.ts
|
|
899
|
+
var ATTACHMENT_MAX_COUNT = 10;
|
|
900
|
+
var MAX_ATTACHMENT_TOTAL_BYTES = 25 * 1024 * 1024;
|
|
901
|
+
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
|
+
function isAttachmentKind(value) {
|
|
906
|
+
return value === "image" || value === "file";
|
|
907
|
+
}
|
|
908
|
+
var CONTROL_CHARS = /[\x00-\x1F\x7F]/;
|
|
909
|
+
function defaultValidateAttachmentPath(path) {
|
|
910
|
+
if (path.includes("\0")) return { succeeded: false, error: "attachment path must not contain null bytes" };
|
|
911
|
+
if (path.includes("\\")) return { succeeded: false, error: "attachment path must not contain backslashes" };
|
|
912
|
+
if (CONTROL_CHARS.test(path)) return { succeeded: false, error: "attachment path must not contain control characters" };
|
|
913
|
+
if (path.startsWith("/")) return { succeeded: false, error: "attachment path must be store-relative, not absolute" };
|
|
914
|
+
const segments = path.split("/");
|
|
915
|
+
if (segments.some((segment) => segment === "..")) {
|
|
916
|
+
return { succeeded: false, error: 'attachment path must not contain ".." segments' };
|
|
917
|
+
}
|
|
918
|
+
if (segments.some((segment) => segment.startsWith("."))) {
|
|
919
|
+
return { succeeded: false, error: "attachment path must not contain a hidden (dotfile) segment" };
|
|
920
|
+
}
|
|
921
|
+
return { succeeded: true };
|
|
922
|
+
}
|
|
923
|
+
function parseAttachmentInput(value, index, validatePath) {
|
|
924
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
925
|
+
return { succeeded: false, error: `attachments[${index}] must be an object` };
|
|
926
|
+
}
|
|
927
|
+
const record = value;
|
|
928
|
+
const path = record.path;
|
|
929
|
+
if (typeof path !== "string" || !path) {
|
|
930
|
+
return { succeeded: false, error: `attachments[${index}].path must be a non-empty string` };
|
|
931
|
+
}
|
|
932
|
+
const name = record.name;
|
|
933
|
+
if (typeof name !== "string" || !name.trim()) {
|
|
934
|
+
return { succeeded: false, error: `attachments[${index}].name must be a non-empty string` };
|
|
935
|
+
}
|
|
936
|
+
if (name.length > MAX_ATTACHMENT_NAME_LENGTH) {
|
|
937
|
+
return { succeeded: false, error: `attachments[${index}].name must not exceed ${MAX_ATTACHMENT_NAME_LENGTH} characters` };
|
|
938
|
+
}
|
|
939
|
+
if (CONTROL_CHARS.test(name)) {
|
|
940
|
+
return { succeeded: false, error: `attachments[${index}].name must not contain control characters` };
|
|
941
|
+
}
|
|
942
|
+
const size = record.size;
|
|
943
|
+
if (typeof size !== "number" || !Number.isFinite(size)) {
|
|
944
|
+
return { succeeded: false, error: `attachments[${index}].size must be a finite number` };
|
|
945
|
+
}
|
|
946
|
+
if (size < 0) {
|
|
947
|
+
return { succeeded: false, error: `attachments[${index}].size must not be negative` };
|
|
948
|
+
}
|
|
949
|
+
const mediaType = record.mediaType;
|
|
950
|
+
if (typeof mediaType !== "string") {
|
|
951
|
+
return { succeeded: false, error: `attachments[${index}].mediaType must be a string` };
|
|
952
|
+
}
|
|
953
|
+
const kind = record.kind;
|
|
954
|
+
if (!isAttachmentKind(kind)) {
|
|
955
|
+
return { succeeded: false, error: `attachments[${index}].kind must be "image" or "file"` };
|
|
956
|
+
}
|
|
957
|
+
const pathCheck = validatePath(path);
|
|
958
|
+
if (!pathCheck.succeeded) return { succeeded: false, error: pathCheck.error };
|
|
959
|
+
return { succeeded: true, value: { path, name, size, mediaType, kind } };
|
|
960
|
+
}
|
|
961
|
+
async function resolveChatAttachments(value, options) {
|
|
962
|
+
const maxCount = options.maxCount ?? ATTACHMENT_MAX_COUNT;
|
|
963
|
+
const maxTotalBytes = options.maxTotalBytes ?? MAX_ATTACHMENT_TOTAL_BYTES;
|
|
964
|
+
const validatePath = options.validatePath ?? defaultValidateAttachmentPath;
|
|
965
|
+
if (value === void 0 || value === null) return { succeeded: true, value: [] };
|
|
966
|
+
if (!Array.isArray(value)) return { succeeded: false, error: "attachments must be an array" };
|
|
967
|
+
if (value.length > maxCount) {
|
|
968
|
+
return { succeeded: false, error: `attachments must not exceed ${maxCount} entries` };
|
|
969
|
+
}
|
|
970
|
+
const inputs = [];
|
|
971
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
972
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
973
|
+
const parsed = parseAttachmentInput(value[index], index, validatePath);
|
|
974
|
+
if (!parsed.succeeded) return parsed;
|
|
975
|
+
if (seenPaths.has(parsed.value.path)) {
|
|
976
|
+
return { succeeded: false, error: `attachments must not repeat a path: ${parsed.value.path}` };
|
|
977
|
+
}
|
|
978
|
+
seenPaths.add(parsed.value.path);
|
|
979
|
+
inputs.push(parsed.value);
|
|
980
|
+
}
|
|
981
|
+
const advisoryTotal = inputs.reduce((sum, input) => sum + input.size, 0);
|
|
982
|
+
if (advisoryTotal > maxTotalBytes) {
|
|
983
|
+
return { succeeded: false, error: attachmentTotalSizeErrorMessage(advisoryTotal, maxTotalBytes) };
|
|
984
|
+
}
|
|
985
|
+
let totalStoredBytes = 0;
|
|
986
|
+
for (const input of inputs) {
|
|
987
|
+
const read = await options.readAttachment(options.scopeId, input.path);
|
|
988
|
+
if (!read.ok) return { succeeded: false, error: read.reason };
|
|
989
|
+
totalStoredBytes += read.size;
|
|
990
|
+
if (totalStoredBytes > maxTotalBytes) {
|
|
991
|
+
return { succeeded: false, error: attachmentTotalSizeErrorMessage(totalStoredBytes, maxTotalBytes) };
|
|
992
|
+
}
|
|
993
|
+
input.size = read.size;
|
|
994
|
+
}
|
|
995
|
+
return { succeeded: true, value: inputs.map(attachmentInputToPart) };
|
|
996
|
+
}
|
|
997
|
+
|
|
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
|
+
// src/chat-routes/promote-file-part.ts
|
|
1157
|
+
var PROMOTE_MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
1158
|
+
var EXT_TO_MIME = {
|
|
1159
|
+
md: "text/markdown",
|
|
1160
|
+
markdown: "text/markdown",
|
|
1161
|
+
txt: "text/plain",
|
|
1162
|
+
log: "text/plain",
|
|
1163
|
+
csv: "text/csv",
|
|
1164
|
+
tsv: "text/tab-separated-values",
|
|
1165
|
+
json: "application/json",
|
|
1166
|
+
yaml: "text/yaml",
|
|
1167
|
+
yml: "text/yaml",
|
|
1168
|
+
xml: "application/xml",
|
|
1169
|
+
html: "text/html",
|
|
1170
|
+
htm: "text/html",
|
|
1171
|
+
pdf: "application/pdf",
|
|
1172
|
+
png: "image/png",
|
|
1173
|
+
jpg: "image/jpeg",
|
|
1174
|
+
jpeg: "image/jpeg",
|
|
1175
|
+
gif: "image/gif",
|
|
1176
|
+
webp: "image/webp",
|
|
1177
|
+
avif: "image/avif",
|
|
1178
|
+
heic: "image/heic",
|
|
1179
|
+
heif: "image/heif",
|
|
1180
|
+
svg: "image/svg+xml",
|
|
1181
|
+
mp4: "video/mp4",
|
|
1182
|
+
webm: "video/webm",
|
|
1183
|
+
mov: "video/quicktime",
|
|
1184
|
+
mp3: "audio/mpeg",
|
|
1185
|
+
wav: "audio/wav",
|
|
1186
|
+
ogg: "audio/ogg",
|
|
1187
|
+
m4a: "audio/mp4",
|
|
1188
|
+
aac: "audio/aac"
|
|
1189
|
+
};
|
|
1190
|
+
function sniffMimeFromName(filename) {
|
|
1191
|
+
const ext = filename.split(".").pop()?.toLowerCase();
|
|
1192
|
+
if (!ext) return "text/plain";
|
|
1193
|
+
return EXT_TO_MIME[ext] ?? "text/plain";
|
|
1194
|
+
}
|
|
1195
|
+
function sanitizeAttachmentFileName(name) {
|
|
1196
|
+
const sanitized = name.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[.-]+/, "");
|
|
1197
|
+
return sanitized || "file";
|
|
1198
|
+
}
|
|
1199
|
+
function base64ToBytes(base64) {
|
|
1200
|
+
const binary = atob(base64);
|
|
1201
|
+
const bytes = new Uint8Array(binary.length);
|
|
1202
|
+
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
|
|
1203
|
+
return bytes;
|
|
1204
|
+
}
|
|
1205
|
+
function parseDataUrl(url) {
|
|
1206
|
+
const match = /^data:[^,]*,([\s\S]*)$/.exec(url);
|
|
1207
|
+
if (!match) return null;
|
|
1208
|
+
return { base64: /;base64,/i.test(url), data: match[1] ?? "" };
|
|
1209
|
+
}
|
|
1210
|
+
function dataUrlMime(url) {
|
|
1211
|
+
if (!url) return void 0;
|
|
1212
|
+
const match = /^data:([^;,]+)[;,]/.exec(url);
|
|
1213
|
+
return match ? match[1] : void 0;
|
|
1214
|
+
}
|
|
1215
|
+
function basenameFromUrl(url) {
|
|
1216
|
+
if (!url || url.startsWith("data:")) return void 0;
|
|
1217
|
+
const withoutQuery = url.split(/[?#]/)[0] ?? url;
|
|
1218
|
+
const segments = withoutQuery.split("/").filter(Boolean);
|
|
1219
|
+
return segments[segments.length - 1] || void 0;
|
|
1220
|
+
}
|
|
1221
|
+
function resolveFileUrlPath(url) {
|
|
1222
|
+
const withoutScheme = url.startsWith("file://") ? url.slice("file://".length) : url;
|
|
1223
|
+
try {
|
|
1224
|
+
return { succeeded: true, path: decodeURIComponent(withoutScheme) };
|
|
1225
|
+
} catch (err) {
|
|
1226
|
+
return { succeeded: false, reason: `malformed file path: ${err instanceof Error ? err.message : String(err)}` };
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
function oversizeReason(filename, actual, limit) {
|
|
1230
|
+
return `${filename} is ${formatBytes(actual)}; attachments are limited to ${formatBytes(limit)}`;
|
|
1231
|
+
}
|
|
1232
|
+
function resolveDataUrlBytes(url, filename, maxBytes) {
|
|
1233
|
+
const parsed = parseDataUrl(url);
|
|
1234
|
+
if (!parsed) return { succeeded: false, reason: "malformed data URI" };
|
|
1235
|
+
let bytes;
|
|
1236
|
+
try {
|
|
1237
|
+
bytes = parsed.base64 ? base64ToBytes(parsed.data) : new TextEncoder().encode(decodeURIComponent(parsed.data));
|
|
1238
|
+
} catch (err) {
|
|
1239
|
+
return { succeeded: false, reason: `failed to decode data URI: ${err instanceof Error ? err.message : String(err)}` };
|
|
1240
|
+
}
|
|
1241
|
+
if (bytes.byteLength > maxBytes) {
|
|
1242
|
+
return { succeeded: false, reason: oversizeReason(filename, bytes.byteLength, maxBytes) };
|
|
1243
|
+
}
|
|
1244
|
+
return { succeeded: true, bytes };
|
|
1245
|
+
}
|
|
1246
|
+
async function resolveSandboxFileBytes(input) {
|
|
1247
|
+
const stat = await statSandboxFileSize(input.box, input.path, { sessionId: input.sessionId });
|
|
1248
|
+
if (!stat.succeeded) {
|
|
1249
|
+
return { succeeded: false, reason: `could not stat agent file: ${stat.error}` };
|
|
1250
|
+
}
|
|
1251
|
+
if (stat.value > input.maxBytes) {
|
|
1252
|
+
return { succeeded: false, reason: oversizeReason(input.filename, stat.value, input.maxBytes) };
|
|
1253
|
+
}
|
|
1254
|
+
const read = await readSandboxBinaryBytes(input.box, input.path, stat.value, { sessionId: input.sessionId });
|
|
1255
|
+
if (!read.succeeded) {
|
|
1256
|
+
return { succeeded: false, reason: `could not read agent file: ${read.error}` };
|
|
1257
|
+
}
|
|
1258
|
+
return { succeeded: true, bytes: read.value.bytes };
|
|
1259
|
+
}
|
|
1260
|
+
async function resolveBytes(input) {
|
|
1261
|
+
const url = input.raw.url;
|
|
1262
|
+
if (!url) return { succeeded: false, reason: "the file part carries no url" };
|
|
1263
|
+
if (url.startsWith("data:")) return resolveDataUrlBytes(url, input.filename, input.maxBytes);
|
|
1264
|
+
const isSandboxPath = url.startsWith("file://") || url.startsWith("/");
|
|
1265
|
+
if (!isSandboxPath) return { succeeded: false, reason: `unsupported file URL scheme: ${url}` };
|
|
1266
|
+
if (!input.box) return { succeeded: false, reason: "no sandbox to read agent file" };
|
|
1267
|
+
const resolvedPath = resolveFileUrlPath(url);
|
|
1268
|
+
if (!resolvedPath.succeeded) return resolvedPath;
|
|
1269
|
+
return resolveSandboxFileBytes({
|
|
1270
|
+
path: resolvedPath.path,
|
|
1271
|
+
box: input.box,
|
|
1272
|
+
sessionId: input.sessionId,
|
|
1273
|
+
filename: input.filename,
|
|
1274
|
+
maxBytes: input.maxBytes
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
async function hash8(seed) {
|
|
1278
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(seed));
|
|
1279
|
+
return Array.from(new Uint8Array(digest).slice(0, 4)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1280
|
+
}
|
|
1281
|
+
function defaultBuildAttachmentPath(args) {
|
|
1282
|
+
const extensionMatch = /\.[A-Za-z0-9]+$/.exec(args.filename);
|
|
1283
|
+
const extension = extensionMatch ? extensionMatch[0] : "";
|
|
1284
|
+
const base = extension ? args.filename.slice(0, -extension.length) : args.filename;
|
|
1285
|
+
return `uploads/agent/${args.date}/${base}-${args.hash8}${extension}`;
|
|
1286
|
+
}
|
|
1287
|
+
async function promoteAgentFilePart(options) {
|
|
1288
|
+
const maxBytes = options.maxBytes ?? PROMOTE_MAX_FILE_BYTES;
|
|
1289
|
+
const sniffMime = options.sniffMime ?? sniffMimeFromName;
|
|
1290
|
+
const buildAttachmentPath = options.buildAttachmentPath ?? defaultBuildAttachmentPath;
|
|
1291
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
1292
|
+
const filename = sanitizeAttachmentFileName(
|
|
1293
|
+
options.raw.filename ?? basenameFromUrl(options.raw.url) ?? "agent-file"
|
|
1294
|
+
);
|
|
1295
|
+
const resolved = await resolveBytes({
|
|
1296
|
+
raw: options.raw,
|
|
1297
|
+
box: options.box,
|
|
1298
|
+
sessionId: options.sessionId,
|
|
1299
|
+
filename,
|
|
1300
|
+
maxBytes
|
|
1301
|
+
});
|
|
1302
|
+
if (!resolved.succeeded) return { succeeded: false, filename, reason: resolved.reason };
|
|
1303
|
+
const mediaType = options.raw.mediaType ?? options.raw.mime ?? dataUrlMime(options.raw.url) ?? sniffMime(filename);
|
|
1304
|
+
const kind = attachmentKindForMime(mediaType);
|
|
1305
|
+
const digest = await hash8(options.raw.id ?? options.raw.url ?? filename);
|
|
1306
|
+
const date = now().toISOString().split("T")[0] ?? "";
|
|
1307
|
+
const path = buildAttachmentPath({ filename, hash8: digest, date, mediaType, kind });
|
|
1308
|
+
let written;
|
|
1309
|
+
try {
|
|
1310
|
+
written = await options.writeAttachment(options.scopeId, path, resolved.bytes, {
|
|
1311
|
+
mediaType,
|
|
1312
|
+
name: filename,
|
|
1313
|
+
originalName: options.raw.filename ?? filename,
|
|
1314
|
+
size: resolved.bytes.byteLength
|
|
1315
|
+
});
|
|
1316
|
+
} catch (err) {
|
|
1317
|
+
return { succeeded: false, filename, reason: err instanceof Error ? err.message : String(err) };
|
|
1318
|
+
}
|
|
1319
|
+
if (!written.ok) return { succeeded: false, filename, reason: written.reason };
|
|
1320
|
+
return {
|
|
1321
|
+
succeeded: true,
|
|
1322
|
+
part: {
|
|
1323
|
+
type: kind,
|
|
1324
|
+
path,
|
|
1325
|
+
name: filename,
|
|
1326
|
+
size: resolved.bytes.byteLength,
|
|
1327
|
+
mediaType
|
|
1328
|
+
}
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
848
1331
|
export {
|
|
1332
|
+
ATTACHMENT_MAX_COUNT,
|
|
849
1333
|
ChatTurnInputError,
|
|
850
1334
|
DEFAULT_STALE_TURN_LOCK_GRACE_MS,
|
|
851
1335
|
DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS,
|
|
1336
|
+
DISPATCH_MAX_MEDIA_PARTS,
|
|
1337
|
+
DISPATCH_MAX_PARTS,
|
|
1338
|
+
DISPATCH_REQUEST_MAX_BYTES,
|
|
1339
|
+
DISPATCH_STRUCTURAL_RESERVE_BYTES,
|
|
852
1340
|
INLINE_PARTS_MAX_BYTES,
|
|
1341
|
+
MAX_ATTACHMENT_TOTAL_BYTES,
|
|
853
1342
|
MENTION_MAX_COUNT,
|
|
1343
|
+
PROMOTE_MAX_FILE_BYTES,
|
|
854
1344
|
UPLOAD_INLINE_MAX_BYTES,
|
|
855
1345
|
UPLOAD_MAX_FILE_BYTES,
|
|
856
1346
|
assertPromptPartsWithinCap,
|
|
1347
|
+
attachmentTotalSizeErrorMessage,
|
|
1348
|
+
base64WireLen,
|
|
1349
|
+
buildDispatchParts,
|
|
857
1350
|
buildMentionPromptBlock,
|
|
858
1351
|
bytesToBase64,
|
|
859
1352
|
chatTurnRequestInit,
|
|
@@ -861,14 +1354,19 @@ export {
|
|
|
861
1354
|
createSandboxChatProducer,
|
|
862
1355
|
createSandboxFileIndexRoute,
|
|
863
1356
|
createUploadRoute,
|
|
1357
|
+
defaultValidateAttachmentPath,
|
|
864
1358
|
fileMentionsToParts,
|
|
1359
|
+
formatBytes,
|
|
865
1360
|
mediaTypeForMentionPath,
|
|
866
1361
|
mentionKindForPath,
|
|
867
1362
|
parseChatTurnParts,
|
|
868
1363
|
parseFileMentions,
|
|
1364
|
+
promoteAgentFilePart,
|
|
869
1365
|
promptPartsByteSize,
|
|
870
1366
|
reconcileStaleTurnLock,
|
|
1367
|
+
resolveChatAttachments,
|
|
871
1368
|
sanitizeUploadFilename,
|
|
1369
|
+
sniffMimeFromName,
|
|
872
1370
|
validateSandboxMentionPath,
|
|
873
1371
|
withDurableChatProjection
|
|
874
1372
|
};
|