deepagents 1.14.1 → 1.14.2

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.
@@ -1,11 +1,12 @@
1
1
  import { AIMessage, HumanMessage, SystemMessage, ToolMessage, anthropicPromptCachingMiddleware, bedrockPromptCachingMiddleware, context, countTokensApproximately, createAgent, createMiddleware, humanInTheLoopMiddleware, todoListMiddleware, tool } from "langchain";
2
+ import { ContextOverflowError } from "@langchain/core/errors";
2
3
  import { Command, REMOVE_ALL_MESSAGES, ReducedValue, StateSchema, getConfig, getCurrentTaskInput, getStore, isCommand } from "@langchain/langgraph";
3
4
  import { z } from "zod/v4";
4
5
  import micromatch from "micromatch";
6
+ import { createHash } from "node:crypto";
5
7
  import { AIMessage as AIMessage$1, HumanMessage as HumanMessage$1, RemoveMessage, SystemMessage as SystemMessage$1, ToolMessage as ToolMessage$1, getBufferString } from "@langchain/core/messages";
6
8
  import * as z$2 from "zod";
7
9
  import { z as z$1 } from "zod";
8
- import { ContextOverflowError } from "@langchain/core/errors";
9
10
  import { initChatModel } from "langchain/chat_models/universal";
10
11
  import yaml from "yaml";
11
12
  import { Client } from "@langchain/langgraph-sdk";
@@ -633,9 +634,18 @@ function adaptBackendProtocol(backend) {
633
634
  return result;
634
635
  }
635
636
  };
636
- const routePrefixes = backend.routePrefixes;
637
- if (Array.isArray(routePrefixes)) Object.defineProperty(adapted, "routePrefixes", {
638
- value: routePrefixes,
637
+ const identityMarkers = [["routePrefixes", Array.isArray], ["backendKind", (value) => typeof value === "string"]];
638
+ for (const [key, isValid] of identityMarkers) {
639
+ const value = backend[key];
640
+ if (isValid(value)) Object.defineProperty(adapted, key, {
641
+ value,
642
+ enumerable: true,
643
+ configurable: true
644
+ });
645
+ }
646
+ const resolveBackendForPath = backend.resolveBackendForPath;
647
+ if (typeof resolveBackendForPath === "function") Object.defineProperty(adapted, "resolveBackendForPath", {
648
+ value: (path) => resolveBackendForPath.call(backend, path),
639
649
  enumerable: true,
640
650
  configurable: true
641
651
  });
@@ -786,8 +796,14 @@ const PREGEL_READ_KEY = "__pregel_read";
786
796
  * for the middleware to apply via Command.
787
797
  */
788
798
  var StateBackend = class {
799
+ /** Class identity discriminant for {@link StateBackend.isInstance}. */
800
+ backendKind = "state";
789
801
  runtime;
790
802
  fileFormat;
803
+ /** Duck-typed so this survives the plain-object copies `adaptBackendProtocol` makes — a class check wouldn't. */
804
+ static isInstance(backend) {
805
+ return typeof backend === "object" && backend !== null && backend.backendKind === "state";
806
+ }
791
807
  constructor(runtimeOrOptions, options) {
792
808
  if (runtimeOrOptions != null && typeof runtimeOrOptions === "object" && "state" in runtimeOrOptions) {
793
809
  this.runtime = runtimeOrOptions;
@@ -1087,6 +1103,466 @@ var StateBackend = class {
1087
1103
  }
1088
1104
  };
1089
1105
  //#endregion
1106
+ //#region src/middleware/blobOffload.ts
1107
+ /**
1108
+ * Content-addressed offload of binary `read_file` and inline `HumanMessage`
1109
+ * blocks to the backend.
1110
+ *
1111
+ * Binary blocks are written to `/blobs/<sha256>` and replaced in state with a
1112
+ * `deepagents_blob` reference. Model requests are rehydrated from the backend
1113
+ * (or a per-run cache), so checkpoints never carry the base64 payload.
1114
+ */
1115
+ /** Content block key holding the SHA-256 digest of an offloaded payload. */
1116
+ const BLOB_REF_KEY = "deepagents_blob";
1117
+ const MISSING_BLOB_TEXT = "[Binary content from an earlier read_file call is no longer available. Re-read the file if you still need it.]";
1118
+ const DIGEST_RE = /^[0-9a-f]{64}$/;
1119
+ const BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
1120
+ /** Whether `reference` is a well-formed digest string. */
1121
+ function isDigest(reference) {
1122
+ return typeof reference === "string" && DIGEST_RE.test(reference);
1123
+ }
1124
+ function isValidBase64(value) {
1125
+ return value.length > 0 && value.length % 4 === 0 && BASE64_RE.test(value);
1126
+ }
1127
+ function sha256Hex(bytes) {
1128
+ return createHash("sha256").update(bytes).digest("hex");
1129
+ }
1130
+ const DEFAULT_BLOB_CACHE_BYTES = 268435456;
1131
+ /**
1132
+ * Content-addressed cache of base64 payloads, bounded by total payload size
1133
+ * (LRU-evicted past the bound). Safe to share across threads — every key is
1134
+ * a SHA-256 digest of its own value, so a hit is only reachable by a caller
1135
+ * who already has that exact reference, and an eviction just forces a
1136
+ * re-fetch from the backend, never a wrong result.
1137
+ */
1138
+ var BlobCache = class extends Map {
1139
+ maxBytes;
1140
+ totalBytes = 0;
1141
+ constructor(maxBytes = DEFAULT_BLOB_CACHE_BYTES) {
1142
+ super();
1143
+ this.maxBytes = maxBytes;
1144
+ }
1145
+ /** Returns the cached payload for `digest`, marking it most recently used. */
1146
+ get(digest) {
1147
+ const payload = super.get(digest);
1148
+ if (payload === void 0) return void 0;
1149
+ super.delete(digest);
1150
+ super.set(digest, payload);
1151
+ return payload;
1152
+ }
1153
+ /** Caches `payload`, evicting least-recently-used entries past the size bound. */
1154
+ set(digest, payload) {
1155
+ if (payload.length > this.maxBytes) return this;
1156
+ const previous = super.get(digest);
1157
+ if (previous !== void 0) {
1158
+ this.totalBytes -= previous.length;
1159
+ super.delete(digest);
1160
+ }
1161
+ super.set(digest, payload);
1162
+ this.totalBytes += payload.length;
1163
+ while (this.totalBytes > this.maxBytes) {
1164
+ const oldestDigest = this.keys().next().value;
1165
+ if (oldestDigest === void 0) break;
1166
+ const oldest = super.get(oldestDigest);
1167
+ super.delete(oldestDigest);
1168
+ this.totalBytes -= oldest?.length ?? 0;
1169
+ }
1170
+ return this;
1171
+ }
1172
+ };
1173
+ function blobPath(prefix, digest) {
1174
+ return `${prefix}/${digest}`;
1175
+ }
1176
+ function contentBlocks(message) {
1177
+ const content = message?.content;
1178
+ return Array.isArray(content) ? content : null;
1179
+ }
1180
+ /** Map each inline base64 payload found in `messages` to its digest and decoded bytes. */
1181
+ function pendingBlobs(messages) {
1182
+ const pending = /* @__PURE__ */ new Map();
1183
+ for (const message of messages) {
1184
+ const blocks = contentBlocks(message);
1185
+ if (!blocks) continue;
1186
+ for (const block of blocks) {
1187
+ if (block == null || typeof block !== "object") continue;
1188
+ const payload = block.data;
1189
+ if (typeof payload !== "string" || pending.has(payload) || !isValidBase64(payload)) continue;
1190
+ let raw;
1191
+ try {
1192
+ raw = new Uint8Array(Buffer.from(payload, "base64"));
1193
+ } catch {
1194
+ continue;
1195
+ }
1196
+ pending.set(payload, {
1197
+ digest: sha256Hex(raw),
1198
+ raw
1199
+ });
1200
+ }
1201
+ }
1202
+ return pending;
1203
+ }
1204
+ /** Replace payloads found in `digests` with blob references; returns `messages` unchanged if nothing matched. */
1205
+ function stubMessages(messages, digests) {
1206
+ return messages.map((message) => {
1207
+ const blocks = contentBlocks(message);
1208
+ if (!blocks) return message;
1209
+ let changed = false;
1210
+ const newContent = blocks.map((block) => {
1211
+ const digest = typeof block.data === "string" ? digests.get(block.data) : void 0;
1212
+ if (digest === void 0) return block;
1213
+ changed = true;
1214
+ const { data: _data, ...rest } = block;
1215
+ return {
1216
+ ...rest,
1217
+ [BLOB_REF_KEY]: digest
1218
+ };
1219
+ });
1220
+ if (!changed) return message;
1221
+ return withContent(message, newContent);
1222
+ });
1223
+ }
1224
+ /**
1225
+ * Upload inline binary payloads found in `messages` and return messages carrying blob
1226
+ * references instead. Payloads that fail to upload stay inline — offload is best-effort.
1227
+ */
1228
+ async function offloadMessages(messages, backend, prefix, cache) {
1229
+ const pending = pendingBlobs(messages);
1230
+ if (pending.size === 0) return [...messages];
1231
+ if (typeof backend.uploadFiles !== "function") {
1232
+ console.warn("Backend does not support uploadFiles; binary read_file content stays inline");
1233
+ return [...messages];
1234
+ }
1235
+ const entries = [...pending.entries()];
1236
+ let responses;
1237
+ try {
1238
+ responses = await backend.uploadFiles(entries.map(([, { digest, raw }]) => [blobPath(prefix, digest), raw]));
1239
+ } catch (error) {
1240
+ console.warn("Failed to offload binary read_file content; keeping it inline", error);
1241
+ return [...messages];
1242
+ }
1243
+ const digests = /* @__PURE__ */ new Map();
1244
+ entries.forEach(([payload, { digest }], index) => {
1245
+ if (responses[index]?.error == null) {
1246
+ digests.set(payload, digest);
1247
+ cache.set(digest, payload);
1248
+ }
1249
+ });
1250
+ return stubMessages(messages, digests);
1251
+ }
1252
+ /** Apply `offloadMessages` to a tool result (a `ToolMessage`, or a `Command`-shaped update). */
1253
+ async function offloadToolResult(result, backend, prefix, cache) {
1254
+ if (ToolMessage.isInstance(result)) return (await offloadMessages([result], backend, prefix, cache))[0];
1255
+ const update = result?.update;
1256
+ if (update && Array.isArray(update.messages)) {
1257
+ const messages = await offloadMessages(update.messages, backend, prefix, cache);
1258
+ return new Command({
1259
+ ...result,
1260
+ update: {
1261
+ ...update,
1262
+ messages
1263
+ }
1264
+ });
1265
+ }
1266
+ return result;
1267
+ }
1268
+ /** Return `HumanMessage`s with ids after the last `AIMessage`, preferring a version already queued in `pending`. */
1269
+ function humanCandidates({ stateMessages, pending }) {
1270
+ let lastAiIndex = -1;
1271
+ stateMessages.forEach((message, index) => {
1272
+ if (AIMessage.isInstance(message)) lastAiIndex = index;
1273
+ });
1274
+ const queued = /* @__PURE__ */ new Map();
1275
+ for (const message of pending) {
1276
+ const id = message?.id;
1277
+ if (typeof id === "string") queued.set(id, message);
1278
+ }
1279
+ const candidates = [];
1280
+ for (const message of stateMessages.slice(lastAiIndex + 1)) {
1281
+ if (!HumanMessage.isInstance(message) || typeof message.id !== "string") continue;
1282
+ candidates.push(queued.get(message.id) ?? message);
1283
+ }
1284
+ return candidates;
1285
+ }
1286
+ function mergeReplacements({ pending, offloaded, candidates }) {
1287
+ const replaced = offloaded.filter((next, index) => next !== candidates[index]);
1288
+ const replacedIds = new Set(replaced.map((message) => message?.id));
1289
+ return [...pending.filter((message) => !replacedIds.has(message?.id)), ...replaced];
1290
+ }
1291
+ /** Return `pending` plus stubbed replacements for `HumanMessage`s in state carrying inline media. */
1292
+ async function offloadHumanMessages({ stateMessages, pending }, backend, prefix, cache) {
1293
+ const candidates = humanCandidates({
1294
+ stateMessages,
1295
+ pending
1296
+ });
1297
+ if (candidates.length === 0) return [...pending];
1298
+ return mergeReplacements({
1299
+ pending,
1300
+ offloaded: await offloadMessages(candidates, backend, prefix, cache),
1301
+ candidates
1302
+ });
1303
+ }
1304
+ /** Collect every well-formed `deepagents_blob` digest referenced in `messages`, deduplicated. */
1305
+ function referencedDigests(messages) {
1306
+ const digests = /* @__PURE__ */ new Set();
1307
+ for (const message of messages) {
1308
+ const blocks = contentBlocks(message);
1309
+ if (!blocks) continue;
1310
+ for (const block of blocks) {
1311
+ const ref = block?.[BLOB_REF_KEY];
1312
+ if (isDigest(ref)) digests.add(ref);
1313
+ }
1314
+ }
1315
+ return [...digests];
1316
+ }
1317
+ function cachedPayloads(digests, cache) {
1318
+ const payloads = /* @__PURE__ */ new Map();
1319
+ const missing = [];
1320
+ for (const digest of digests) {
1321
+ const payload = cache.get(digest);
1322
+ if (payload !== void 0) payloads.set(digest, payload);
1323
+ else missing.push(digest);
1324
+ }
1325
+ return {
1326
+ payloads,
1327
+ missing
1328
+ };
1329
+ }
1330
+ /** Verify each downloaded blob's integrity before trusting it — blobs live on an agent-writable backend. */
1331
+ function acceptDownloads(missing, responses, payloads, cache) {
1332
+ missing.forEach((digest, index) => {
1333
+ const response = responses[index];
1334
+ if (!response || response.error != null || response.content == null) return;
1335
+ if (sha256Hex(response.content) !== digest) return;
1336
+ const payload = Buffer.from(response.content).toString("base64");
1337
+ cache.set(digest, payload);
1338
+ payloads.set(digest, payload);
1339
+ });
1340
+ }
1341
+ function messageHasRefs(message) {
1342
+ const blocks = contentBlocks(message);
1343
+ return blocks != null && blocks.some((block) => block != null && typeof block === "object" && "deepagents_blob" in block);
1344
+ }
1345
+ function withContent(message, content) {
1346
+ if (ToolMessage.isInstance(message)) return new ToolMessage({
1347
+ content,
1348
+ tool_call_id: message.tool_call_id,
1349
+ name: message.name,
1350
+ id: message.id,
1351
+ artifact: message.artifact,
1352
+ status: message.status,
1353
+ additional_kwargs: message.additional_kwargs,
1354
+ response_metadata: message.response_metadata,
1355
+ metadata: message.metadata
1356
+ });
1357
+ if (HumanMessage.isInstance(message)) return new HumanMessage({
1358
+ content,
1359
+ id: message.id,
1360
+ additional_kwargs: message.additional_kwargs,
1361
+ response_metadata: message.response_metadata
1362
+ });
1363
+ if (AIMessage.isInstance(message)) return new AIMessage({
1364
+ content,
1365
+ id: message.id,
1366
+ tool_calls: message.tool_calls,
1367
+ invalid_tool_calls: message.invalid_tool_calls,
1368
+ usage_metadata: message.usage_metadata,
1369
+ additional_kwargs: message.additional_kwargs,
1370
+ response_metadata: message.response_metadata
1371
+ });
1372
+ if (SystemMessage.isInstance(message)) return new SystemMessage({
1373
+ content,
1374
+ id: message.id,
1375
+ additional_kwargs: message.additional_kwargs,
1376
+ response_metadata: message.response_metadata
1377
+ });
1378
+ console.warn(`blobOffload: left a blob reference unresolved on an unsupported message type (${message?.constructor?.name})`);
1379
+ return message;
1380
+ }
1381
+ /** Restore base64 payloads for blob references; a reference with no matching payload becomes a text notice. */
1382
+ function restorePayloads(messages, payloads) {
1383
+ const hydrate = (block) => {
1384
+ const ref = block[BLOB_REF_KEY];
1385
+ if (ref === void 0) return block;
1386
+ const payload = isDigest(ref) ? payloads.get(ref) : void 0;
1387
+ if (payload === void 0) return {
1388
+ type: "text",
1389
+ text: MISSING_BLOB_TEXT
1390
+ };
1391
+ const { [BLOB_REF_KEY]: _ref, ...rest } = block;
1392
+ return {
1393
+ ...rest,
1394
+ data: payload
1395
+ };
1396
+ };
1397
+ return messages.map((message) => {
1398
+ if (!messageHasRefs(message)) return message;
1399
+ return withContent(message, (contentBlocks(message) ?? []).map(hydrate));
1400
+ });
1401
+ }
1402
+ /**
1403
+ * Restore base64 payloads for blob references ahead of a model call.
1404
+ *
1405
+ * Only the returned array is used for the request — callers must not persist
1406
+ * the result back into state, or the checkpoint-size benefit of offloading is
1407
+ * lost the next time state is saved.
1408
+ */
1409
+ async function hydrateMessages(messages, backend, prefix, cache) {
1410
+ if (!messages.some(messageHasRefs)) return [...messages];
1411
+ const { payloads, missing } = cachedPayloads(referencedDigests(messages), cache);
1412
+ if (missing.length > 0) if (typeof backend.downloadFiles !== "function") console.warn("Backend does not support downloadFiles; offloaded read_file content is unavailable");
1413
+ else {
1414
+ let responses = [];
1415
+ try {
1416
+ responses = await backend.downloadFiles(missing.map((digest) => blobPath(prefix, digest)));
1417
+ } catch (error) {
1418
+ console.warn("Failed to load offloaded read_file content", error);
1419
+ }
1420
+ acceptDownloads(missing, responses, payloads, cache);
1421
+ }
1422
+ return restorePayloads(messages, payloads);
1423
+ }
1424
+ //#endregion
1425
+ //#region src/middleware/unsupportedContent.ts
1426
+ const PDF_MIME_TYPE = "application/pdf";
1427
+ /** Content block types `read_file` may emit that require multimodal model support. */
1428
+ const MULTIMODAL_BLOCK_TYPES = /* @__PURE__ */ new Set([
1429
+ "image",
1430
+ "audio",
1431
+ "video",
1432
+ "file"
1433
+ ]);
1434
+ const PROFILE_FIELD_BY_BLOCK_TYPE = {
1435
+ image: "imageInputs",
1436
+ audio: "audioInputs",
1437
+ video: "videoInputs"
1438
+ };
1439
+ const TOOL_MESSAGE_FIELD_BY_BLOCK_TYPE = { image: "imageToolMessage" };
1440
+ function hasInlineData(block) {
1441
+ return block.data != null || block.source_type === "base64";
1442
+ }
1443
+ function blockMimeType(block) {
1444
+ const mimeType = block.mimeType ?? block.mime_type;
1445
+ return typeof mimeType === "string" ? mimeType : void 0;
1446
+ }
1447
+ /**
1448
+ * Unlike the other block types, a missing `fileMimeTypes` defaults to
1449
+ * unsupported rather than supported: generic file support varies far more
1450
+ * across models than image/audio/video, so there's no safe assumption to
1451
+ * fall back on absent real profile data.
1452
+ */
1453
+ function fileBlockSupported(block, profile, inToolMessage) {
1454
+ if (!hasInlineData(block)) return true;
1455
+ const mimeType = blockMimeType(block);
1456
+ if (mimeType === PDF_MIME_TYPE) {
1457
+ if (inToolMessage && profile.pdfToolMessage !== void 0) return profile.pdfToolMessage !== false;
1458
+ return profile.pdfInputs !== false;
1459
+ }
1460
+ if (mimeType == null) return false;
1461
+ const { fileMimeTypes } = profile;
1462
+ return Array.isArray(fileMimeTypes) && fileMimeTypes.includes(mimeType);
1463
+ }
1464
+ /**
1465
+ * Whether the model accepts a multimodal block.
1466
+ *
1467
+ * Missing profile fields default to supported, since profile coverage is
1468
+ * incomplete — except `file` blocks, where a missing `fileMimeTypes`
1469
+ * defaults to unsupported (see `fileBlockSupported`).
1470
+ */
1471
+ function multimodalBlockSupported(block, profile, inToolMessage) {
1472
+ if (block.type === "file") return fileBlockSupported(block, profile, inToolMessage);
1473
+ const field = PROFILE_FIELD_BY_BLOCK_TYPE[block.type];
1474
+ if (field == null) return true;
1475
+ if (inToolMessage) {
1476
+ const toolField = TOOL_MESSAGE_FIELD_BY_BLOCK_TYPE[block.type];
1477
+ if (toolField != null && profile[toolField] !== void 0) return profile[toolField] !== false;
1478
+ }
1479
+ return profile[field] !== false;
1480
+ }
1481
+ function placeholder(block, path) {
1482
+ return {
1483
+ type: "text",
1484
+ text: `[read_file: ${path ?? "the requested file"} was not attached because this model does not support ${block.type} content (${blockMimeType(block) ?? "unknown"}).]`
1485
+ };
1486
+ }
1487
+ function readFilePaths(messages) {
1488
+ const paths = /* @__PURE__ */ new Map();
1489
+ for (const message of messages) {
1490
+ if (!AIMessage$1.isInstance(message)) continue;
1491
+ for (const toolCall of message.tool_calls ?? []) {
1492
+ const path = toolCall.args?.file_path;
1493
+ if (toolCall.id != null && typeof path === "string") paths.set(toolCall.id, path);
1494
+ }
1495
+ }
1496
+ return paths;
1497
+ }
1498
+ /**
1499
+ * Without this, a request carrying content the model can't accept (e.g. a
1500
+ * `.zip` file sent to a model without file support) fails with a
1501
+ * non-retryable 400, and since the content stays in history, every later
1502
+ * turn fails the same way. Only the request is changed; messages in state
1503
+ * are left untouched, so switching to a model that accepts the content
1504
+ * sends it again.
1505
+ */
1506
+ function scrubUnsupportedMultimodalContent(messages, model) {
1507
+ const rawProfile = model?.profile;
1508
+ const profile = rawProfile != null && typeof rawProfile === "object" ? rawProfile : {};
1509
+ let paths;
1510
+ let changed = false;
1511
+ const result = messages.map((message) => {
1512
+ const isTool = ToolMessage$1.isInstance(message);
1513
+ if (!isTool && !HumanMessage$1.isInstance(message)) return message;
1514
+ let messageChanged = false;
1515
+ const content = message.contentBlocks.map((block) => {
1516
+ if (block == null || typeof block !== "object" || !MULTIMODAL_BLOCK_TYPES.has(block.type) || multimodalBlockSupported(block, profile, isTool)) return block;
1517
+ messageChanged = true;
1518
+ paths ??= readFilePaths(messages);
1519
+ return placeholder(block, isTool ? paths.get(message.tool_call_id) : void 0);
1520
+ });
1521
+ if (!messageChanged) return message;
1522
+ changed = true;
1523
+ if (isTool) return new ToolMessage$1({
1524
+ content,
1525
+ tool_call_id: message.tool_call_id,
1526
+ name: message.name,
1527
+ id: message.id,
1528
+ artifact: message.artifact,
1529
+ status: message.status,
1530
+ metadata: message.metadata,
1531
+ additional_kwargs: message.additional_kwargs,
1532
+ response_metadata: message.response_metadata
1533
+ });
1534
+ return new HumanMessage$1({
1535
+ content,
1536
+ name: message.name,
1537
+ id: message.id,
1538
+ additional_kwargs: message.additional_kwargs,
1539
+ response_metadata: message.response_metadata
1540
+ });
1541
+ });
1542
+ return changed ? result : messages;
1543
+ }
1544
+ /**
1545
+ * Support is read from `model.profile` (see
1546
+ * https://docs.langchain.com/oss/javascript/langchain/models#model-profiles).
1547
+ *
1548
+ * Place this middleware last in the agent's `middleware` list, so that if
1549
+ * some other middleware changes `request.model`, this one sees the model
1550
+ * that will actually receive the request.
1551
+ */
1552
+ function createUnsupportedContentMiddleware() {
1553
+ return createMiddleware({
1554
+ name: "UnsupportedContentMiddleware",
1555
+ wrapModelCall: async (request, handler) => {
1556
+ const messages = scrubUnsupportedMultimodalContent(request.messages, request.model);
1557
+ if (messages === request.messages) return handler(request);
1558
+ return handler({
1559
+ ...request,
1560
+ messages
1561
+ });
1562
+ }
1563
+ });
1564
+ }
1565
+ //#endregion
1090
1566
  //#region src/permissions/enforce.ts
1091
1567
  /**
1092
1568
  * Validate permission rule paths at setup time. Throws if any path is
@@ -1177,6 +1653,10 @@ var CompositeBackend = class {
1177
1653
  static isInstance(backend) {
1178
1654
  return typeof backend === "object" && backend !== null && Array.isArray(backend.routePrefixes);
1179
1655
  }
1656
+ /** Returns the backend for `path`, with the matched prefix stripped from the returned path. */
1657
+ resolveBackendForPath(path) {
1658
+ return this.getBackendAndKey(path);
1659
+ }
1180
1660
  /**
1181
1661
  * Determine which backend handles this key and strip prefix.
1182
1662
  *
@@ -1781,6 +2261,8 @@ const GREP_TRUNCATION_NOTE = "Note: the search stopped early because it hit the
1781
2261
  * Set to null to disable the cap.
1782
2262
  */
1783
2263
  const DEFAULT_GREP_MAX_COUNT = 1e3;
2264
+ /** Path prefix offloaded binary content blobs are written under. */
2265
+ const BLOBS_PREFIX = "/blobs";
1784
2266
  /**
1785
2267
  * Message template for evicted tool results.
1786
2268
  */
@@ -2563,6 +3045,68 @@ function allPathsScopedToRoutes(permissions, backend) {
2563
3045
  return path === normalizedRoute.slice(0, -1) || path.startsWith(normalizedRoute);
2564
3046
  })));
2565
3047
  }
3048
+ /** Whether `path` resolves to a `StateBackend`, recursing through composite routing. */
3049
+ function routesToStateBackend(backend, path) {
3050
+ if (CompositeBackend.isInstance(backend)) {
3051
+ const [routed, routedPath] = backend.resolveBackendForPath(path);
3052
+ return routesToStateBackend(routed, routedPath);
3053
+ }
3054
+ return StateBackend.isInstance(backend);
3055
+ }
3056
+ /**
3057
+ * Whether `response` is the `{ structuredResponse, messages }` shape a
3058
+ * provider-strategy structured-output call can return instead of a plain
3059
+ * `AIMessage`. The agent framework only recognizes this shape at the
3060
+ * outermost `wrapModelCall` return value — returning a `Command` instead
3061
+ * discards `structuredResponse` with no way to carry it through the Command.
3062
+ */
3063
+ function hasStructuredResponse(response) {
3064
+ return typeof response === "object" && response !== null && "structuredResponse" in response && "messages" in response;
3065
+ }
3066
+ const REJECTED_FILE_CONTENT_TEXT = "Unsupported content. The file may be invalid, too large, or of an unsupported mime-type.";
3067
+ /**
3068
+ * No LangChain error class captures "the provider rejected this request"
3069
+ * generically, so this checks the raw HTTP status providers' client SDKs set
3070
+ * on their errors instead. `ContextOverflowError` is excluded because it's a
3071
+ * different, already-handled problem, and retrying it with stripped file
3072
+ * content wouldn't fix an oversized prompt.
3073
+ */
3074
+ function isRejectedRequestError(error) {
3075
+ if (ContextOverflowError.isInstance(error)) return false;
3076
+ return typeof error === "object" && error !== null && "status" in error && error.status === 400;
3077
+ }
3078
+ /**
3079
+ * Only messages carrying multimodal blocks after the last `AIMessage` are
3080
+ * candidates for replacement, since anything earlier was already accepted by
3081
+ * the model in a prior turn. Returns `messages` unchanged if nothing
3082
+ * matched, so the caller can tell there was nothing to blame for the
3083
+ * rejection.
3084
+ */
3085
+ function replaceRejectedFileContent(messages) {
3086
+ let lastResponseIndex = -1;
3087
+ for (let i = messages.length - 1; i >= 0; i--) if (AIMessage.isInstance(messages[i])) {
3088
+ lastResponseIndex = i;
3089
+ break;
3090
+ }
3091
+ let changed = false;
3092
+ const result = messages.map((message, index) => {
3093
+ if (index <= lastResponseIndex || !ToolMessage.isInstance(message) || message.name !== "read_file") return message;
3094
+ if (!message.contentBlocks.some((block) => MULTIMODAL_BLOCK_TYPES.has(block.type))) return message;
3095
+ changed = true;
3096
+ return new ToolMessage({
3097
+ content: REJECTED_FILE_CONTENT_TEXT,
3098
+ tool_call_id: message.tool_call_id,
3099
+ name: message.name,
3100
+ id: message.id,
3101
+ artifact: message.artifact,
3102
+ status: message.status,
3103
+ metadata: message.metadata,
3104
+ additional_kwargs: message.additional_kwargs,
3105
+ response_metadata: message.response_metadata
3106
+ });
3107
+ });
3108
+ return changed ? result : messages;
3109
+ }
2566
3110
  /**
2567
3111
  * Create middleware that provides built-in filesystem tools and optional custom
2568
3112
  * prompt guidance.
@@ -2589,7 +3133,10 @@ function allPathsScopedToRoutes(permissions, backend) {
2589
3133
  * ```
2590
3134
  */
2591
3135
  function createFilesystemMiddleware(options = {}) {
2592
- const { backend = (runtime) => new StateBackend(runtime), systemPrompt: customSystemPrompt = null, customToolDescriptions = null, toolTokenLimitBeforeEvict = 2e4, humanMessageTokenLimitBeforeEvict = 5e4, permissions = [], tools: filesystemTools = null, grepMaxCount = DEFAULT_GREP_MAX_COUNT } = options;
3136
+ const { backend = (runtime) => new StateBackend(runtime), systemPrompt: customSystemPrompt = null, customToolDescriptions = null, toolTokenLimitBeforeEvict = 2e4, humanMessageTokenLimitBeforeEvict = 5e4, permissions = [], tools: filesystemTools = null, grepMaxCount = DEFAULT_GREP_MAX_COUNT, offloadBinaryContent = false } = options;
3137
+ const backendIsStateBackend = StateBackend.isInstance(backend);
3138
+ if (offloadBinaryContent && backendIsStateBackend) console.warn("offloadBinaryContent has no effect: the backend is a StateBackend, which keeps files as part of checkpointed state.");
3139
+ const blobCache = offloadBinaryContent && !backendIsStateBackend ? new BlobCache() : null;
2593
3140
  const enabledFilesystemTools = normalizeFilesystemTools(filesystemTools);
2594
3141
  const executeToolEnabled = enabledFilesystemTools == null || enabledFilesystemTools.has("execute");
2595
3142
  if (permissions.length > 0) validatePermissionPaths(permissions);
@@ -2726,18 +3273,59 @@ function createFilesystemMiddleware(options = {}) {
2726
3273
  return msg;
2727
3274
  });
2728
3275
  }
2729
- return handler({
3276
+ let offloadedHuman = [];
3277
+ if (blobCache && !routesToStateBackend(resolvedBackend, `${BLOBS_PREFIX}/`)) {
3278
+ offloadedHuman = await offloadHumanMessages({
3279
+ stateMessages: request.state.messages ?? [],
3280
+ pending: []
3281
+ }, resolvedBackend, BLOBS_PREFIX, blobCache);
3282
+ messages = await hydrateMessages(messages, resolvedBackend, BLOBS_PREFIX, blobCache);
3283
+ }
3284
+ const finalRequest = {
2730
3285
  ...request,
2731
3286
  tools,
2732
3287
  messages,
2733
3288
  systemMessage: newSystemMessage
2734
- });
3289
+ };
3290
+ const foldOffload = (response, extra = []) => {
3291
+ if (offloadedHuman.length === 0 && extra.length === 0) return response;
3292
+ if (hasStructuredResponse(response)) return {
3293
+ ...response,
3294
+ messages: [
3295
+ ...extra,
3296
+ ...offloadedHuman,
3297
+ ...response.messages
3298
+ ]
3299
+ };
3300
+ return new Command({ update: { messages: extra.length === 0 ? offloadedHuman : [
3301
+ ...extra,
3302
+ ...offloadedHuman,
3303
+ response
3304
+ ] } });
3305
+ };
3306
+ try {
3307
+ return foldOffload(await handler(finalRequest));
3308
+ } catch (error) {
3309
+ if (!isRejectedRequestError(error)) throw error;
3310
+ const replaced = replaceRejectedFileContent(finalRequest.messages);
3311
+ if (replaced === finalRequest.messages) throw error;
3312
+ return foldOffload(await handler({
3313
+ ...finalRequest,
3314
+ messages: replaced
3315
+ }), replaced.filter((msg, i) => msg !== finalRequest.messages[i]));
3316
+ }
2735
3317
  },
2736
3318
  wrapToolCall: async (request, handler) => {
2737
- if (!toolTokenLimitBeforeEvict) return handler(request);
2738
3319
  const toolName = request.toolCall?.name;
2739
- if (toolName && TOOLS_EXCLUDED_FROM_EVICTION.includes(toolName)) return handler(request);
2740
- const result = await handler(request);
3320
+ let result = await handler(request);
3321
+ if (blobCache && toolName === "read_file") {
3322
+ const resolvedBackend = await resolveBackend(backend, {
3323
+ ...request.runtime,
3324
+ state: request.state
3325
+ });
3326
+ if (!routesToStateBackend(resolvedBackend, `${BLOBS_PREFIX}/`)) result = await offloadToolResult(result, resolvedBackend, BLOBS_PREFIX, /* @__PURE__ */ new Map());
3327
+ }
3328
+ if (!toolTokenLimitBeforeEvict || toolName && TOOLS_EXCLUDED_FROM_EVICTION.includes(toolName)) return result;
2741
3329
  if (ToolMessage.isInstance(result)) {
2742
3330
  const processed = await processToolMessage(result, request.runtime, request.state, request.toolCall?.id);
2743
3331
  if (processed.filesUpdate) return new Command({ update: {
@@ -3781,6 +4369,7 @@ function createSubAgent(spec, options) {
3781
4369
  if (!spec.tools) throw new Error(`SubAgent '${spec.name}' must specify 'tools'`);
3782
4370
  const middleware = [...spec.middleware ?? []];
3783
4371
  if (spec.interruptOn) middleware.push(humanInTheLoopMiddleware({ interruptOn: spec.interruptOn }));
4372
+ if (!middleware.some((m) => m.name === "UnsupportedContentMiddleware")) middleware.push(createUnsupportedContentMiddleware());
3784
4373
  const selectedResponseFormat = options?.responseFormat ?? spec.responseFormat;
3785
4374
  return createAgent({
3786
4375
  model: spec.model,
@@ -6700,6 +7289,7 @@ function createDeepAgent(params = {}) {
6700
7289
  ]);
6701
7290
  if (subagentProfile.excludedMiddleware.size > 0) subagentMiddleware = subagentMiddleware.filter((middleware) => !subagentProfile.excludedMiddleware.has(middleware.name));
6702
7291
  if (subagentProfile.excludedTools.size > 0) subagentMiddleware.push(createToolExclusionMiddleware(subagentProfile.excludedTools));
7292
+ if (!subagentProfile.excludedMiddleware.has("UnsupportedContentMiddleware") && !subagentMiddleware.some((middleware) => middleware.name === "UnsupportedContentMiddleware")) subagentMiddleware.push(createUnsupportedContentMiddleware());
6703
7293
  return subagentMiddleware;
6704
7294
  };
6705
7295
  const normalizeSubagentSpec = (input) => ({
@@ -6762,6 +7352,7 @@ function createDeepAgent(params = {}) {
6762
7352
  middleware = middleware.filter((entry) => !excluded.has(entry.name));
6763
7353
  }
6764
7354
  if (harnessProfile.excludedTools.size > 0) middleware.push(createToolExclusionMiddleware(harnessProfile.excludedTools));
7355
+ if (!harnessProfile.excludedMiddleware.has("UnsupportedContentMiddleware") && !middleware.some((m) => m.name === "UnsupportedContentMiddleware")) middleware.push(createUnsupportedContentMiddleware());
6765
7356
  /**
6766
7357
  * Return as DeepAgent with proper DeepAgentTypeConfig
6767
7358
  * - Response: InferStructuredResponse<TResponse> (unwraps ToolStrategy<T>/ProviderStrategy<T> → T)
@@ -8879,6 +9470,6 @@ var LangSmithSandbox = class LangSmithSandbox extends BaseSandbox {
8879
9470
  }
8880
9471
  };
8881
9472
  //#endregion
8882
- export { performStringReplacement as $, createPatchToolCallsMiddleware as A, CompositeBackend as B, MAX_SKILL_DESCRIPTION_LENGTH as C, skillsMetadataValue as D, createSkillsMiddleware as E, createSubAgent as F, isSandboxProtocol as G, SandboxError as H, createSubAgentMiddleware as I, adaptSandboxProtocol as J, resolveBackend as K, computeSummarizationDefaults as L, DEFAULT_SUBAGENT_PROMPT as M, GENERAL_PURPOSE_SUBAGENT as N, createMemoryMiddleware as O, SUBAGENT_RESPONSE_FORMAT_CONFIG_KEY as P, normalizeReadPagination as Q, createSummarizationMiddleware as R, createCompletionCallbackMiddleware as S, MAX_SKILL_NAME_LENGTH as T, applyGrepMaxCount as U, StateBackend as V, isSandboxBackend as W, getMimeType as X, checkEmptyContent as Y, isTextMimeType as Z, createHarnessProfile as _, ASYNC_TASK_SYSTEM_PROMPT as a, createAsyncSubAgentMiddleware as b, TASK_SYSTEM_PROMPT as c, registerHarnessProfile as d, generalPurposeSubagentConfigSchema as f, EMPTY_HARNESS_PROFILE as g, serializeProfile as h, StoreBackend as i, DEFAULT_GENERAL_PURPOSE_DESCRIPTION as j, filesValue as k, createDeepAgent as l, parseHarnessProfileConfig as m, BaseSandbox as n, BASE_AGENT_PROMPT as o, harnessProfileConfigSchema as p, adaptBackendProtocol as q, ContextHubBackend as r, EXECUTION_SYSTEM_PROMPT as s, LangSmithSandbox as t, getHarnessProfile as u, REQUIRED_MIDDLEWARE_NAMES as v, MAX_SKILL_FILE_SIZE as w, isAsyncSubAgent as x, ConfigurationError as y, createFilesystemMiddleware as z };
9473
+ export { checkEmptyContent as $, createPatchToolCallsMiddleware as A, CompositeBackend as B, MAX_SKILL_DESCRIPTION_LENGTH as C, skillsMetadataValue as D, createSkillsMiddleware as E, createSubAgent as F, StateBackend as G, createUnsupportedContentMiddleware as H, createSubAgentMiddleware as I, isSandboxBackend as J, SandboxError as K, computeSummarizationDefaults as L, DEFAULT_SUBAGENT_PROMPT as M, GENERAL_PURPOSE_SUBAGENT as N, createMemoryMiddleware as O, SUBAGENT_RESPONSE_FORMAT_CONFIG_KEY as P, adaptSandboxProtocol as Q, createSummarizationMiddleware as R, createCompletionCallbackMiddleware as S, MAX_SKILL_NAME_LENGTH as T, multimodalBlockSupported as U, MULTIMODAL_BLOCK_TYPES as V, scrubUnsupportedMultimodalContent as W, resolveBackend as X, isSandboxProtocol as Y, adaptBackendProtocol as Z, createHarnessProfile as _, ASYNC_TASK_SYSTEM_PROMPT as a, createAsyncSubAgentMiddleware as b, TASK_SYSTEM_PROMPT as c, registerHarnessProfile as d, getMimeType as et, generalPurposeSubagentConfigSchema as f, EMPTY_HARNESS_PROFILE as g, serializeProfile as h, StoreBackend as i, DEFAULT_GENERAL_PURPOSE_DESCRIPTION as j, filesValue as k, createDeepAgent as l, parseHarnessProfileConfig as m, BaseSandbox as n, normalizeReadPagination as nt, BASE_AGENT_PROMPT as o, harnessProfileConfigSchema as p, applyGrepMaxCount as q, ContextHubBackend as r, performStringReplacement as rt, EXECUTION_SYSTEM_PROMPT as s, LangSmithSandbox as t, isTextMimeType as tt, getHarnessProfile as u, REQUIRED_MIDDLEWARE_NAMES as v, MAX_SKILL_FILE_SIZE as w, isAsyncSubAgent as x, ConfigurationError as y, createFilesystemMiddleware as z };
8883
9474
 
8884
- //# sourceMappingURL=langsmith-Bgi_Ta-k.js.map
9475
+ //# sourceMappingURL=langsmith--RbtjNdQ.js.map