@tangle-network/agent-app 0.43.46 → 0.43.48
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 +3 -2
- package/dist/assistant/index.js.map +1 -1
- package/dist/chat-routes/index.d.ts +407 -160
- package/dist/chat-routes/index.js +512 -102
- 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-4AUQIAYU.js → chunk-5MG74GVQ.js} +2 -281
- package/dist/chunk-5MG74GVQ.js.map +1 -0
- package/dist/chunk-BQV42AFK.js +99 -0
- package/dist/chunk-BQV42AFK.js.map +1 -0
- package/dist/{chunk-6E2XJSCT.js → chunk-JGYOYY5D.js} +65 -2
- package/dist/chunk-JGYOYY5D.js.map +1 -0
- package/dist/{chunk-7VMUOD3G.js → chunk-PHIXZC6Y.js} +1 -1
- package/dist/chunk-PHIXZC6Y.js.map +1 -0
- package/dist/{file-index-Bn6sitKb.d.ts → file-index-b26ee-_R.d.ts} +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +38 -22
- package/dist/{parts-1_3y2JmR.d.ts → parts-IB-Kbb7z.d.ts} +146 -2
- package/dist/sandbox/index.d.ts +1 -1
- package/dist/stale-turn-lock-C8Na1cFZ.d.ts +149 -0
- package/dist/stream/index.d.ts +5 -192
- package/dist/stream/index.js +14 -12
- package/dist/stream-normalizer-DWvtmY6F.d.ts +48 -0
- package/dist/teams/index.js +9 -9
- package/dist/teams/invitations-api.js +7 -7
- package/dist/turn-buffer-C9mEgoop.d.ts +146 -0
- package/dist/turn-stream/index.d.ts +551 -0
- package/dist/turn-stream/index.js +837 -0
- package/dist/turn-stream/index.js.map +1 -0
- package/dist/web-react/index.d.ts +4 -3
- package/dist/web-react/index.js +24 -2
- package/package.json +6 -1
- package/dist/chunk-4AUQIAYU.js.map +0 -1
- package/dist/chunk-6E2XJSCT.js.map +0 -1
- package/dist/chunk-7VMUOD3G.js.map +0 -1
|
@@ -2,19 +2,17 @@ import {
|
|
|
2
2
|
createSandboxFileIndexRoute
|
|
3
3
|
} from "../chunk-LCNY3DCM.js";
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
DEFAULT_STALE_TURN_LOCK_GRACE_MS,
|
|
6
|
+
DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS,
|
|
7
|
+
reconcileStaleTurnLock
|
|
8
|
+
} from "../chunk-BQV42AFK.js";
|
|
9
|
+
import {
|
|
7
10
|
coalesceDeltas,
|
|
8
11
|
createBufferedTurnTap,
|
|
9
|
-
finalizeAssistantParts,
|
|
10
|
-
getPartKey,
|
|
11
|
-
mergePersistedPart,
|
|
12
12
|
normalizeClientTurnId,
|
|
13
|
-
normalizePersistedPart,
|
|
14
|
-
normalizeToolEvent,
|
|
15
13
|
replayTurnEvents,
|
|
16
14
|
resolveChatTurn
|
|
17
|
-
} from "../chunk-
|
|
15
|
+
} from "../chunk-23LUHHHN.js";
|
|
18
16
|
import {
|
|
19
17
|
createInteractionAnswerRoute
|
|
20
18
|
} from "../chunk-6KVH5SP5.js";
|
|
@@ -23,12 +21,20 @@ import {
|
|
|
23
21
|
} from "../chunk-HFC4BTWJ.js";
|
|
24
22
|
import {
|
|
25
23
|
ChatTurnInputError,
|
|
24
|
+
DISPATCH_MAX_MEDIA_PARTS,
|
|
25
|
+
DISPATCH_MAX_PARTS,
|
|
26
|
+
DISPATCH_REQUEST_MAX_BYTES,
|
|
27
|
+
DISPATCH_STRUCTURAL_RESERVE_BYTES,
|
|
26
28
|
INLINE_PARTS_MAX_BYTES,
|
|
27
29
|
MENTION_MAX_COUNT,
|
|
28
30
|
assertPromptPartsWithinCap,
|
|
31
|
+
attachmentInputToPart,
|
|
32
|
+
attachmentKindForMime,
|
|
33
|
+
base64WireLen,
|
|
29
34
|
buildMentionPromptBlock,
|
|
30
35
|
chatTurnRequestInit,
|
|
31
36
|
fileMentionsToParts,
|
|
37
|
+
formatBytes,
|
|
32
38
|
mediaTypeForMentionPath,
|
|
33
39
|
mentionInputToPart,
|
|
34
40
|
mentionKindForPath,
|
|
@@ -37,7 +43,16 @@ import {
|
|
|
37
43
|
promptPartsByteSize,
|
|
38
44
|
toChatMessageParts,
|
|
39
45
|
validateSandboxMentionPath
|
|
40
|
-
} 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";
|
|
41
56
|
import {
|
|
42
57
|
cancelStatusFor,
|
|
43
58
|
interactionPartKey,
|
|
@@ -50,6 +65,17 @@ import {
|
|
|
50
65
|
parsePlanSubmittedEvent,
|
|
51
66
|
planToPersistedPart
|
|
52
67
|
} from "../chunk-SIXYZ2FB.js";
|
|
68
|
+
import {
|
|
69
|
+
flattenHistory,
|
|
70
|
+
readSandboxBinaryBytes,
|
|
71
|
+
statSandboxFileSize
|
|
72
|
+
} from "../chunk-5RJNEEO2.js";
|
|
73
|
+
import "../chunk-E7QYOOON.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";
|
|
53
79
|
|
|
54
80
|
// src/chat-routes/turn-routes.ts
|
|
55
81
|
import { deriveExecutionId, handleChatTurn } from "@tangle-network/agent-runtime";
|
|
@@ -499,99 +525,6 @@ function concatStreams(streams) {
|
|
|
499
525
|
});
|
|
500
526
|
}
|
|
501
527
|
|
|
502
|
-
// src/chat-routes/stale-turn-lock.ts
|
|
503
|
-
var DEFAULT_STALE_TURN_LOCK_GRACE_MS = 5 * 60 * 1e3;
|
|
504
|
-
var DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS = 60 * 1e3;
|
|
505
|
-
function messageOf(err) {
|
|
506
|
-
return err instanceof Error ? err.message : String(err);
|
|
507
|
-
}
|
|
508
|
-
async function reconcileStaleTurnLock(options) {
|
|
509
|
-
let sandbox;
|
|
510
|
-
try {
|
|
511
|
-
sandbox = await options.probeSandbox();
|
|
512
|
-
} catch (err) {
|
|
513
|
-
return forceReleaseUnreachable(options, {
|
|
514
|
-
unreachableReason: "SANDBOX_PROBE_FAILED",
|
|
515
|
-
unreachableDetail: messageOf(err)
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
if (sandbox.status !== "running") {
|
|
519
|
-
return forceReleaseUnreachable(options, {
|
|
520
|
-
unreachableReason: sandbox.status === "absent" ? "SANDBOX_ABSENT" : "SANDBOX_NOT_RUNNING",
|
|
521
|
-
...sandbox.status === "not-running" && sandbox.state !== void 0 ? { sandboxState: sandbox.state } : {}
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
const observedAt = (options.now ?? Date.now)();
|
|
525
|
-
let session;
|
|
526
|
-
try {
|
|
527
|
-
session = await options.probeSession();
|
|
528
|
-
} catch (err) {
|
|
529
|
-
return forceReleaseUnreachable(options, {
|
|
530
|
-
unreachableReason: "SESSION_PROBE_FAILED",
|
|
531
|
-
unreachableDetail: messageOf(err)
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
if (!session.reachable) {
|
|
535
|
-
return forceReleaseUnreachable(options, {
|
|
536
|
-
unreachableReason: "SESSION_UNREACHABLE",
|
|
537
|
-
...session.reason !== void 0 ? { sessionProbeError: session.reason } : {}
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
const diagnostics = {
|
|
541
|
-
sandboxReachable: true,
|
|
542
|
-
sessionTerminal: session.terminal,
|
|
543
|
-
...session.diagnostics ?? {}
|
|
544
|
-
};
|
|
545
|
-
if (!session.terminal) return { released: false, diagnostics };
|
|
546
|
-
const terminalGraceMs = options.terminalGraceMs ?? DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS;
|
|
547
|
-
const lockAgeMs = observedAt - options.lockStartedAt;
|
|
548
|
-
if (lockAgeMs < terminalGraceMs) {
|
|
549
|
-
const log = options.log ?? ((message, meta) => console.warn(message, meta));
|
|
550
|
-
const withheld = {
|
|
551
|
-
...diagnostics,
|
|
552
|
-
lockAgeMs,
|
|
553
|
-
terminalReleaseGraceMs: terminalGraceMs,
|
|
554
|
-
terminalReleaseWithheld: "LOCK_WITHIN_TERMINAL_GRACE_PERIOD"
|
|
555
|
-
};
|
|
556
|
-
log("[chat-routes] stale turn lock held: terminal verdict but lock is younger than the registration window", {
|
|
557
|
-
...options.context ?? {},
|
|
558
|
-
...withheld
|
|
559
|
-
});
|
|
560
|
-
return { released: false, diagnostics: withheld };
|
|
561
|
-
}
|
|
562
|
-
const released = await options.release({ observedAt });
|
|
563
|
-
return {
|
|
564
|
-
released,
|
|
565
|
-
diagnostics: { ...diagnostics, lockAgeMs, terminalReleaseGraceMs: terminalGraceMs, released, observedAt }
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
async function forceReleaseUnreachable(options, reason) {
|
|
569
|
-
const log = options.log ?? ((message, meta) => console.warn(message, meta));
|
|
570
|
-
const graceMs = options.graceMs ?? DEFAULT_STALE_TURN_LOCK_GRACE_MS;
|
|
571
|
-
const at = (options.now ?? Date.now)();
|
|
572
|
-
const lockAgeMs = at - options.lockStartedAt;
|
|
573
|
-
const diagnostics = {
|
|
574
|
-
...reason,
|
|
575
|
-
sandboxReachable: false,
|
|
576
|
-
lockAgeMs,
|
|
577
|
-
forceReleaseGraceMs: graceMs
|
|
578
|
-
};
|
|
579
|
-
if (lockAgeMs < graceMs) {
|
|
580
|
-
log("[chat-routes] stale turn lock held: sandbox unreachable but lock is inside the grace period", {
|
|
581
|
-
...options.context ?? {},
|
|
582
|
-
...diagnostics
|
|
583
|
-
});
|
|
584
|
-
return { released: false, diagnostics: { ...diagnostics, forceReleaseWithheld: "LOCK_WITHIN_GRACE_PERIOD" } };
|
|
585
|
-
}
|
|
586
|
-
const released = await options.release({ observedAt: at });
|
|
587
|
-
log("[chat-routes] force-released stale turn lock: sandbox unreachable, no turn can be executing", {
|
|
588
|
-
...options.context ?? {},
|
|
589
|
-
...diagnostics,
|
|
590
|
-
released
|
|
591
|
-
});
|
|
592
|
-
return { released, diagnostics: { ...diagnostics, forceReleased: released } };
|
|
593
|
-
}
|
|
594
|
-
|
|
595
528
|
// src/chat-routes/sandbox-producer.ts
|
|
596
529
|
function textDelta(tracker, key, part, rawDelta) {
|
|
597
530
|
const explicit = typeof rawDelta === "string" ? rawDelta : void 0;
|
|
@@ -640,6 +573,7 @@ function createSandboxChatProducer(options) {
|
|
|
640
573
|
const announcedTools = /* @__PURE__ */ new Set();
|
|
641
574
|
const settledTools = /* @__PURE__ */ new Set();
|
|
642
575
|
let stepCounter = 0;
|
|
576
|
+
const promotedFileParts = /* @__PURE__ */ new Map();
|
|
643
577
|
function recordPersistedPart(part, delta, keyOverride) {
|
|
644
578
|
const persisted = normalizePersistedPart(part);
|
|
645
579
|
if (!persisted) return;
|
|
@@ -711,6 +645,33 @@ function createSandboxChatProducer(options) {
|
|
|
711
645
|
recordPersistedPart(part, void 0, `step-start:#${stepCounter}`);
|
|
712
646
|
continue;
|
|
713
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
|
+
}
|
|
714
675
|
recordPersistedPart(part, void 0);
|
|
715
676
|
continue;
|
|
716
677
|
}
|
|
@@ -933,15 +894,459 @@ function createUploadRoute(options) {
|
|
|
933
894
|
return Response.json({ files: uploaded });
|
|
934
895
|
};
|
|
935
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
|
+
}
|
|
936
1331
|
export {
|
|
1332
|
+
ATTACHMENT_MAX_COUNT,
|
|
937
1333
|
ChatTurnInputError,
|
|
938
1334
|
DEFAULT_STALE_TURN_LOCK_GRACE_MS,
|
|
939
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,
|
|
940
1340
|
INLINE_PARTS_MAX_BYTES,
|
|
1341
|
+
MAX_ATTACHMENT_TOTAL_BYTES,
|
|
941
1342
|
MENTION_MAX_COUNT,
|
|
1343
|
+
PROMOTE_MAX_FILE_BYTES,
|
|
942
1344
|
UPLOAD_INLINE_MAX_BYTES,
|
|
943
1345
|
UPLOAD_MAX_FILE_BYTES,
|
|
944
1346
|
assertPromptPartsWithinCap,
|
|
1347
|
+
attachmentTotalSizeErrorMessage,
|
|
1348
|
+
base64WireLen,
|
|
1349
|
+
buildDispatchParts,
|
|
945
1350
|
buildMentionPromptBlock,
|
|
946
1351
|
bytesToBase64,
|
|
947
1352
|
chatTurnRequestInit,
|
|
@@ -949,14 +1354,19 @@ export {
|
|
|
949
1354
|
createSandboxChatProducer,
|
|
950
1355
|
createSandboxFileIndexRoute,
|
|
951
1356
|
createUploadRoute,
|
|
1357
|
+
defaultValidateAttachmentPath,
|
|
952
1358
|
fileMentionsToParts,
|
|
1359
|
+
formatBytes,
|
|
953
1360
|
mediaTypeForMentionPath,
|
|
954
1361
|
mentionKindForPath,
|
|
955
1362
|
parseChatTurnParts,
|
|
956
1363
|
parseFileMentions,
|
|
1364
|
+
promoteAgentFilePart,
|
|
957
1365
|
promptPartsByteSize,
|
|
958
1366
|
reconcileStaleTurnLock,
|
|
1367
|
+
resolveChatAttachments,
|
|
959
1368
|
sanitizeUploadFilename,
|
|
1369
|
+
sniffMimeFromName,
|
|
960
1370
|
validateSandboxMentionPath,
|
|
961
1371
|
withDurableChatProjection
|
|
962
1372
|
};
|