deepagents 1.14.1 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,20 +21,42 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  }) : target, mod));
22
22
  //#endregion
23
23
  let langchain = require("langchain");
24
+ let _langchain_core_errors = require("@langchain/core/errors");
24
25
  let _langchain_langgraph = require("@langchain/langgraph");
25
26
  let zod_v4 = require("zod/v4");
26
27
  let micromatch = require("micromatch");
27
28
  micromatch = __toESM(micromatch, 1);
29
+ let node_crypto = require("node:crypto");
28
30
  let _langchain_core_messages = require("@langchain/core/messages");
29
31
  let zod = require("zod");
30
32
  zod = __toESM(zod, 1);
31
- let _langchain_core_errors = require("@langchain/core/errors");
32
33
  let langchain_chat_models_universal = require("langchain/chat_models/universal");
33
34
  let yaml = require("yaml");
34
35
  yaml = __toESM(yaml, 1);
36
+ let _langchain_core_runnables = require("@langchain/core/runnables");
37
+ let _langchain_core_tools = require("@langchain/core/tools");
38
+ let _langchain_core_language_models_base = require("@langchain/core/language_models/base");
39
+ let _langchain_core_utils_function_calling = require("@langchain/core/utils/function_calling");
40
+ let _langchain_core_utils_json_schema = require("@langchain/core/utils/json_schema");
41
+ let _langchain_core_utils_types = require("@langchain/core/utils/types");
35
42
  let _langchain_langgraph_sdk = require("@langchain/langgraph-sdk");
36
43
  let langsmith = require("langsmith");
37
44
  let langsmith_experimental_sandbox = require("langsmith/experimental/sandbox");
45
+ //#region src/backends/realpath.ts
46
+ const REALPATH = Symbol.for("deepagents.realpath");
47
+ function getRealpath(backend) {
48
+ if (typeof backend !== "object" || backend === null) return;
49
+ const fn = backend[REALPATH];
50
+ return typeof fn === "function" ? fn : void 0;
51
+ }
52
+ function setRealpath(target, fn) {
53
+ Object.defineProperty(target, REALPATH, {
54
+ value: fn,
55
+ enumerable: false,
56
+ configurable: true
57
+ });
58
+ }
59
+ //#endregion
38
60
  //#region src/backends/utils.ts
39
61
  /**
40
62
  * Shared utility functions for memory backend implementations.
@@ -657,9 +679,20 @@ function adaptBackendProtocol(backend) {
657
679
  return result;
658
680
  }
659
681
  };
660
- const routePrefixes = backend.routePrefixes;
661
- if (Array.isArray(routePrefixes)) Object.defineProperty(adapted, "routePrefixes", {
662
- value: routePrefixes,
682
+ const identityMarkers = [["routePrefixes", Array.isArray], ["backendKind", (value) => typeof value === "string"]];
683
+ for (const [key, isValid] of identityMarkers) {
684
+ const value = backend[key];
685
+ if (isValid(value)) Object.defineProperty(adapted, key, {
686
+ value,
687
+ enumerable: true,
688
+ configurable: true
689
+ });
690
+ }
691
+ const realpath = getRealpath(backend);
692
+ if (realpath !== void 0) setRealpath(adapted, realpath);
693
+ const resolveBackendForPath = backend.resolveBackendForPath;
694
+ if (typeof resolveBackendForPath === "function") Object.defineProperty(adapted, "resolveBackendForPath", {
695
+ value: (path) => resolveBackendForPath.call(backend, path),
663
696
  enumerable: true,
664
697
  configurable: true
665
698
  });
@@ -810,8 +843,14 @@ const PREGEL_READ_KEY = "__pregel_read";
810
843
  * for the middleware to apply via Command.
811
844
  */
812
845
  var StateBackend = class {
846
+ /** Class identity discriminant for {@link StateBackend.isInstance}. */
847
+ backendKind = "state";
813
848
  runtime;
814
849
  fileFormat;
850
+ /** Duck-typed so this survives the plain-object copies `adaptBackendProtocol` makes — a class check wouldn't. */
851
+ static isInstance(backend) {
852
+ return typeof backend === "object" && backend !== null && backend.backendKind === "state";
853
+ }
815
854
  constructor(runtimeOrOptions, options) {
816
855
  if (runtimeOrOptions != null && typeof runtimeOrOptions === "object" && "state" in runtimeOrOptions) {
817
856
  this.runtime = runtimeOrOptions;
@@ -1111,6 +1150,466 @@ var StateBackend = class {
1111
1150
  }
1112
1151
  };
1113
1152
  //#endregion
1153
+ //#region src/middleware/blobOffload.ts
1154
+ /**
1155
+ * Content-addressed offload of binary `read_file` and inline `HumanMessage`
1156
+ * blocks to the backend.
1157
+ *
1158
+ * Binary blocks are written to `/blobs/<sha256>` and replaced in state with a
1159
+ * `deepagents_blob` reference. Model requests are rehydrated from the backend
1160
+ * (or a per-run cache), so checkpoints never carry the base64 payload.
1161
+ */
1162
+ /** Content block key holding the SHA-256 digest of an offloaded payload. */
1163
+ const BLOB_REF_KEY = "deepagents_blob";
1164
+ 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.]";
1165
+ const DIGEST_RE = /^[0-9a-f]{64}$/;
1166
+ const BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
1167
+ /** Whether `reference` is a well-formed digest string. */
1168
+ function isDigest(reference) {
1169
+ return typeof reference === "string" && DIGEST_RE.test(reference);
1170
+ }
1171
+ function isValidBase64(value) {
1172
+ return value.length > 0 && value.length % 4 === 0 && BASE64_RE.test(value);
1173
+ }
1174
+ function sha256Hex(bytes) {
1175
+ return (0, node_crypto.createHash)("sha256").update(bytes).digest("hex");
1176
+ }
1177
+ const DEFAULT_BLOB_CACHE_BYTES = 268435456;
1178
+ /**
1179
+ * Content-addressed cache of base64 payloads, bounded by total payload size
1180
+ * (LRU-evicted past the bound). Safe to share across threads — every key is
1181
+ * a SHA-256 digest of its own value, so a hit is only reachable by a caller
1182
+ * who already has that exact reference, and an eviction just forces a
1183
+ * re-fetch from the backend, never a wrong result.
1184
+ */
1185
+ var BlobCache = class extends Map {
1186
+ maxBytes;
1187
+ totalBytes = 0;
1188
+ constructor(maxBytes = DEFAULT_BLOB_CACHE_BYTES) {
1189
+ super();
1190
+ this.maxBytes = maxBytes;
1191
+ }
1192
+ /** Returns the cached payload for `digest`, marking it most recently used. */
1193
+ get(digest) {
1194
+ const payload = super.get(digest);
1195
+ if (payload === void 0) return void 0;
1196
+ super.delete(digest);
1197
+ super.set(digest, payload);
1198
+ return payload;
1199
+ }
1200
+ /** Caches `payload`, evicting least-recently-used entries past the size bound. */
1201
+ set(digest, payload) {
1202
+ if (payload.length > this.maxBytes) return this;
1203
+ const previous = super.get(digest);
1204
+ if (previous !== void 0) {
1205
+ this.totalBytes -= previous.length;
1206
+ super.delete(digest);
1207
+ }
1208
+ super.set(digest, payload);
1209
+ this.totalBytes += payload.length;
1210
+ while (this.totalBytes > this.maxBytes) {
1211
+ const oldestDigest = this.keys().next().value;
1212
+ if (oldestDigest === void 0) break;
1213
+ const oldest = super.get(oldestDigest);
1214
+ super.delete(oldestDigest);
1215
+ this.totalBytes -= oldest?.length ?? 0;
1216
+ }
1217
+ return this;
1218
+ }
1219
+ };
1220
+ function blobPath(prefix, digest) {
1221
+ return `${prefix}/${digest}`;
1222
+ }
1223
+ function contentBlocks(message) {
1224
+ const content = message?.content;
1225
+ return Array.isArray(content) ? content : null;
1226
+ }
1227
+ /** Map each inline base64 payload found in `messages` to its digest and decoded bytes. */
1228
+ function pendingBlobs(messages) {
1229
+ const pending = /* @__PURE__ */ new Map();
1230
+ for (const message of messages) {
1231
+ const blocks = contentBlocks(message);
1232
+ if (!blocks) continue;
1233
+ for (const block of blocks) {
1234
+ if (block == null || typeof block !== "object") continue;
1235
+ const payload = block.data;
1236
+ if (typeof payload !== "string" || pending.has(payload) || !isValidBase64(payload)) continue;
1237
+ let raw;
1238
+ try {
1239
+ raw = new Uint8Array(Buffer.from(payload, "base64"));
1240
+ } catch {
1241
+ continue;
1242
+ }
1243
+ pending.set(payload, {
1244
+ digest: sha256Hex(raw),
1245
+ raw
1246
+ });
1247
+ }
1248
+ }
1249
+ return pending;
1250
+ }
1251
+ /** Replace payloads found in `digests` with blob references; returns `messages` unchanged if nothing matched. */
1252
+ function stubMessages(messages, digests) {
1253
+ return messages.map((message) => {
1254
+ const blocks = contentBlocks(message);
1255
+ if (!blocks) return message;
1256
+ let changed = false;
1257
+ const newContent = blocks.map((block) => {
1258
+ const digest = typeof block.data === "string" ? digests.get(block.data) : void 0;
1259
+ if (digest === void 0) return block;
1260
+ changed = true;
1261
+ const { data: _data, ...rest } = block;
1262
+ return {
1263
+ ...rest,
1264
+ [BLOB_REF_KEY]: digest
1265
+ };
1266
+ });
1267
+ if (!changed) return message;
1268
+ return withContent(message, newContent);
1269
+ });
1270
+ }
1271
+ /**
1272
+ * Upload inline binary payloads found in `messages` and return messages carrying blob
1273
+ * references instead. Payloads that fail to upload stay inline — offload is best-effort.
1274
+ */
1275
+ async function offloadMessages(messages, backend, prefix, cache) {
1276
+ const pending = pendingBlobs(messages);
1277
+ if (pending.size === 0) return [...messages];
1278
+ if (typeof backend.uploadFiles !== "function") {
1279
+ console.warn("Backend does not support uploadFiles; binary read_file content stays inline");
1280
+ return [...messages];
1281
+ }
1282
+ const entries = [...pending.entries()];
1283
+ let responses;
1284
+ try {
1285
+ responses = await backend.uploadFiles(entries.map(([, { digest, raw }]) => [blobPath(prefix, digest), raw]));
1286
+ } catch (error) {
1287
+ console.warn("Failed to offload binary read_file content; keeping it inline", error);
1288
+ return [...messages];
1289
+ }
1290
+ const digests = /* @__PURE__ */ new Map();
1291
+ entries.forEach(([payload, { digest }], index) => {
1292
+ if (responses[index]?.error == null) {
1293
+ digests.set(payload, digest);
1294
+ cache.set(digest, payload);
1295
+ }
1296
+ });
1297
+ return stubMessages(messages, digests);
1298
+ }
1299
+ /** Apply `offloadMessages` to a tool result (a `ToolMessage`, or a `Command`-shaped update). */
1300
+ async function offloadToolResult(result, backend, prefix, cache) {
1301
+ if (langchain.ToolMessage.isInstance(result)) return (await offloadMessages([result], backend, prefix, cache))[0];
1302
+ const update = result?.update;
1303
+ if (update && Array.isArray(update.messages)) {
1304
+ const messages = await offloadMessages(update.messages, backend, prefix, cache);
1305
+ return new _langchain_langgraph.Command({
1306
+ ...result,
1307
+ update: {
1308
+ ...update,
1309
+ messages
1310
+ }
1311
+ });
1312
+ }
1313
+ return result;
1314
+ }
1315
+ /** Return `HumanMessage`s with ids after the last `AIMessage`, preferring a version already queued in `pending`. */
1316
+ function humanCandidates({ stateMessages, pending }) {
1317
+ let lastAiIndex = -1;
1318
+ stateMessages.forEach((message, index) => {
1319
+ if (langchain.AIMessage.isInstance(message)) lastAiIndex = index;
1320
+ });
1321
+ const queued = /* @__PURE__ */ new Map();
1322
+ for (const message of pending) {
1323
+ const id = message?.id;
1324
+ if (typeof id === "string") queued.set(id, message);
1325
+ }
1326
+ const candidates = [];
1327
+ for (const message of stateMessages.slice(lastAiIndex + 1)) {
1328
+ if (!langchain.HumanMessage.isInstance(message) || typeof message.id !== "string") continue;
1329
+ candidates.push(queued.get(message.id) ?? message);
1330
+ }
1331
+ return candidates;
1332
+ }
1333
+ function mergeReplacements({ pending, offloaded, candidates }) {
1334
+ const replaced = offloaded.filter((next, index) => next !== candidates[index]);
1335
+ const replacedIds = new Set(replaced.map((message) => message?.id));
1336
+ return [...pending.filter((message) => !replacedIds.has(message?.id)), ...replaced];
1337
+ }
1338
+ /** Return `pending` plus stubbed replacements for `HumanMessage`s in state carrying inline media. */
1339
+ async function offloadHumanMessages({ stateMessages, pending }, backend, prefix, cache) {
1340
+ const candidates = humanCandidates({
1341
+ stateMessages,
1342
+ pending
1343
+ });
1344
+ if (candidates.length === 0) return [...pending];
1345
+ return mergeReplacements({
1346
+ pending,
1347
+ offloaded: await offloadMessages(candidates, backend, prefix, cache),
1348
+ candidates
1349
+ });
1350
+ }
1351
+ /** Collect every well-formed `deepagents_blob` digest referenced in `messages`, deduplicated. */
1352
+ function referencedDigests(messages) {
1353
+ const digests = /* @__PURE__ */ new Set();
1354
+ for (const message of messages) {
1355
+ const blocks = contentBlocks(message);
1356
+ if (!blocks) continue;
1357
+ for (const block of blocks) {
1358
+ const ref = block?.[BLOB_REF_KEY];
1359
+ if (isDigest(ref)) digests.add(ref);
1360
+ }
1361
+ }
1362
+ return [...digests];
1363
+ }
1364
+ function cachedPayloads(digests, cache) {
1365
+ const payloads = /* @__PURE__ */ new Map();
1366
+ const missing = [];
1367
+ for (const digest of digests) {
1368
+ const payload = cache.get(digest);
1369
+ if (payload !== void 0) payloads.set(digest, payload);
1370
+ else missing.push(digest);
1371
+ }
1372
+ return {
1373
+ payloads,
1374
+ missing
1375
+ };
1376
+ }
1377
+ /** Verify each downloaded blob's integrity before trusting it — blobs live on an agent-writable backend. */
1378
+ function acceptDownloads(missing, responses, payloads, cache) {
1379
+ missing.forEach((digest, index) => {
1380
+ const response = responses[index];
1381
+ if (!response || response.error != null || response.content == null) return;
1382
+ if (sha256Hex(response.content) !== digest) return;
1383
+ const payload = Buffer.from(response.content).toString("base64");
1384
+ cache.set(digest, payload);
1385
+ payloads.set(digest, payload);
1386
+ });
1387
+ }
1388
+ function messageHasRefs(message) {
1389
+ const blocks = contentBlocks(message);
1390
+ return blocks != null && blocks.some((block) => block != null && typeof block === "object" && "deepagents_blob" in block);
1391
+ }
1392
+ function withContent(message, content) {
1393
+ if (langchain.ToolMessage.isInstance(message)) return new langchain.ToolMessage({
1394
+ content,
1395
+ tool_call_id: message.tool_call_id,
1396
+ name: message.name,
1397
+ id: message.id,
1398
+ artifact: message.artifact,
1399
+ status: message.status,
1400
+ additional_kwargs: message.additional_kwargs,
1401
+ response_metadata: message.response_metadata,
1402
+ metadata: message.metadata
1403
+ });
1404
+ if (langchain.HumanMessage.isInstance(message)) return new langchain.HumanMessage({
1405
+ content,
1406
+ id: message.id,
1407
+ additional_kwargs: message.additional_kwargs,
1408
+ response_metadata: message.response_metadata
1409
+ });
1410
+ if (langchain.AIMessage.isInstance(message)) return new langchain.AIMessage({
1411
+ content,
1412
+ id: message.id,
1413
+ tool_calls: message.tool_calls,
1414
+ invalid_tool_calls: message.invalid_tool_calls,
1415
+ usage_metadata: message.usage_metadata,
1416
+ additional_kwargs: message.additional_kwargs,
1417
+ response_metadata: message.response_metadata
1418
+ });
1419
+ if (langchain.SystemMessage.isInstance(message)) return new langchain.SystemMessage({
1420
+ content,
1421
+ id: message.id,
1422
+ additional_kwargs: message.additional_kwargs,
1423
+ response_metadata: message.response_metadata
1424
+ });
1425
+ console.warn(`blobOffload: left a blob reference unresolved on an unsupported message type (${message?.constructor?.name})`);
1426
+ return message;
1427
+ }
1428
+ /** Restore base64 payloads for blob references; a reference with no matching payload becomes a text notice. */
1429
+ function restorePayloads(messages, payloads) {
1430
+ const hydrate = (block) => {
1431
+ const ref = block[BLOB_REF_KEY];
1432
+ if (ref === void 0) return block;
1433
+ const payload = isDigest(ref) ? payloads.get(ref) : void 0;
1434
+ if (payload === void 0) return {
1435
+ type: "text",
1436
+ text: MISSING_BLOB_TEXT
1437
+ };
1438
+ const { [BLOB_REF_KEY]: _ref, ...rest } = block;
1439
+ return {
1440
+ ...rest,
1441
+ data: payload
1442
+ };
1443
+ };
1444
+ return messages.map((message) => {
1445
+ if (!messageHasRefs(message)) return message;
1446
+ return withContent(message, (contentBlocks(message) ?? []).map(hydrate));
1447
+ });
1448
+ }
1449
+ /**
1450
+ * Restore base64 payloads for blob references ahead of a model call.
1451
+ *
1452
+ * Only the returned array is used for the request — callers must not persist
1453
+ * the result back into state, or the checkpoint-size benefit of offloading is
1454
+ * lost the next time state is saved.
1455
+ */
1456
+ async function hydrateMessages(messages, backend, prefix, cache) {
1457
+ if (!messages.some(messageHasRefs)) return [...messages];
1458
+ const { payloads, missing } = cachedPayloads(referencedDigests(messages), cache);
1459
+ if (missing.length > 0) if (typeof backend.downloadFiles !== "function") console.warn("Backend does not support downloadFiles; offloaded read_file content is unavailable");
1460
+ else {
1461
+ let responses = [];
1462
+ try {
1463
+ responses = await backend.downloadFiles(missing.map((digest) => blobPath(prefix, digest)));
1464
+ } catch (error) {
1465
+ console.warn("Failed to load offloaded read_file content", error);
1466
+ }
1467
+ acceptDownloads(missing, responses, payloads, cache);
1468
+ }
1469
+ return restorePayloads(messages, payloads);
1470
+ }
1471
+ //#endregion
1472
+ //#region src/middleware/unsupportedContent.ts
1473
+ const PDF_MIME_TYPE = "application/pdf";
1474
+ /** Content block types `read_file` may emit that require multimodal model support. */
1475
+ const MULTIMODAL_BLOCK_TYPES = /* @__PURE__ */ new Set([
1476
+ "image",
1477
+ "audio",
1478
+ "video",
1479
+ "file"
1480
+ ]);
1481
+ const PROFILE_FIELD_BY_BLOCK_TYPE = {
1482
+ image: "imageInputs",
1483
+ audio: "audioInputs",
1484
+ video: "videoInputs"
1485
+ };
1486
+ const TOOL_MESSAGE_FIELD_BY_BLOCK_TYPE = { image: "imageToolMessage" };
1487
+ function hasInlineData(block) {
1488
+ return block.data != null || block.source_type === "base64";
1489
+ }
1490
+ function blockMimeType(block) {
1491
+ const mimeType = block.mimeType ?? block.mime_type;
1492
+ return typeof mimeType === "string" ? mimeType : void 0;
1493
+ }
1494
+ /**
1495
+ * Unlike the other block types, a missing `fileMimeTypes` defaults to
1496
+ * unsupported rather than supported: generic file support varies far more
1497
+ * across models than image/audio/video, so there's no safe assumption to
1498
+ * fall back on absent real profile data.
1499
+ */
1500
+ function fileBlockSupported(block, profile, inToolMessage) {
1501
+ if (!hasInlineData(block)) return true;
1502
+ const mimeType = blockMimeType(block);
1503
+ if (mimeType === PDF_MIME_TYPE) {
1504
+ if (inToolMessage && profile.pdfToolMessage !== void 0) return profile.pdfToolMessage !== false;
1505
+ return profile.pdfInputs !== false;
1506
+ }
1507
+ if (mimeType == null) return false;
1508
+ const { fileMimeTypes } = profile;
1509
+ return Array.isArray(fileMimeTypes) && fileMimeTypes.includes(mimeType);
1510
+ }
1511
+ /**
1512
+ * Whether the model accepts a multimodal block.
1513
+ *
1514
+ * Missing profile fields default to supported, since profile coverage is
1515
+ * incomplete — except `file` blocks, where a missing `fileMimeTypes`
1516
+ * defaults to unsupported (see `fileBlockSupported`).
1517
+ */
1518
+ function multimodalBlockSupported(block, profile, inToolMessage) {
1519
+ if (block.type === "file") return fileBlockSupported(block, profile, inToolMessage);
1520
+ const field = PROFILE_FIELD_BY_BLOCK_TYPE[block.type];
1521
+ if (field == null) return true;
1522
+ if (inToolMessage) {
1523
+ const toolField = TOOL_MESSAGE_FIELD_BY_BLOCK_TYPE[block.type];
1524
+ if (toolField != null && profile[toolField] !== void 0) return profile[toolField] !== false;
1525
+ }
1526
+ return profile[field] !== false;
1527
+ }
1528
+ function placeholder(block, path) {
1529
+ return {
1530
+ type: "text",
1531
+ text: `[read_file: ${path ?? "the requested file"} was not attached because this model does not support ${block.type} content (${blockMimeType(block) ?? "unknown"}).]`
1532
+ };
1533
+ }
1534
+ function readFilePaths(messages) {
1535
+ const paths = /* @__PURE__ */ new Map();
1536
+ for (const message of messages) {
1537
+ if (!_langchain_core_messages.AIMessage.isInstance(message)) continue;
1538
+ for (const toolCall of message.tool_calls ?? []) {
1539
+ const path = toolCall.args?.file_path;
1540
+ if (toolCall.id != null && typeof path === "string") paths.set(toolCall.id, path);
1541
+ }
1542
+ }
1543
+ return paths;
1544
+ }
1545
+ /**
1546
+ * Without this, a request carrying content the model can't accept (e.g. a
1547
+ * `.zip` file sent to a model without file support) fails with a
1548
+ * non-retryable 400, and since the content stays in history, every later
1549
+ * turn fails the same way. Only the request is changed; messages in state
1550
+ * are left untouched, so switching to a model that accepts the content
1551
+ * sends it again.
1552
+ */
1553
+ function scrubUnsupportedMultimodalContent(messages, model) {
1554
+ const rawProfile = model?.profile;
1555
+ const profile = rawProfile != null && typeof rawProfile === "object" ? rawProfile : {};
1556
+ let paths;
1557
+ let changed = false;
1558
+ const result = messages.map((message) => {
1559
+ const isTool = _langchain_core_messages.ToolMessage.isInstance(message);
1560
+ if (!isTool && !_langchain_core_messages.HumanMessage.isInstance(message)) return message;
1561
+ let messageChanged = false;
1562
+ const content = message.contentBlocks.map((block) => {
1563
+ if (block == null || typeof block !== "object" || !MULTIMODAL_BLOCK_TYPES.has(block.type) || multimodalBlockSupported(block, profile, isTool)) return block;
1564
+ messageChanged = true;
1565
+ paths ??= readFilePaths(messages);
1566
+ return placeholder(block, isTool ? paths.get(message.tool_call_id) : void 0);
1567
+ });
1568
+ if (!messageChanged) return message;
1569
+ changed = true;
1570
+ if (isTool) return new _langchain_core_messages.ToolMessage({
1571
+ content,
1572
+ tool_call_id: message.tool_call_id,
1573
+ name: message.name,
1574
+ id: message.id,
1575
+ artifact: message.artifact,
1576
+ status: message.status,
1577
+ metadata: message.metadata,
1578
+ additional_kwargs: message.additional_kwargs,
1579
+ response_metadata: message.response_metadata
1580
+ });
1581
+ return new _langchain_core_messages.HumanMessage({
1582
+ content,
1583
+ name: message.name,
1584
+ id: message.id,
1585
+ additional_kwargs: message.additional_kwargs,
1586
+ response_metadata: message.response_metadata
1587
+ });
1588
+ });
1589
+ return changed ? result : messages;
1590
+ }
1591
+ /**
1592
+ * Support is read from `model.profile` (see
1593
+ * https://docs.langchain.com/oss/javascript/langchain/models#model-profiles).
1594
+ *
1595
+ * Place this middleware last in the agent's `middleware` list, so that if
1596
+ * some other middleware changes `request.model`, this one sees the model
1597
+ * that will actually receive the request.
1598
+ */
1599
+ function createUnsupportedContentMiddleware() {
1600
+ return (0, langchain.createMiddleware)({
1601
+ name: "UnsupportedContentMiddleware",
1602
+ wrapModelCall: async (request, handler) => {
1603
+ const messages = scrubUnsupportedMultimodalContent(request.messages, request.model);
1604
+ if (messages === request.messages) return handler(request);
1605
+ return handler({
1606
+ ...request,
1607
+ messages
1608
+ });
1609
+ }
1610
+ });
1611
+ }
1612
+ //#endregion
1114
1613
  //#region src/permissions/enforce.ts
1115
1614
  /**
1116
1615
  * Validate permission rule paths at setup time. Throws if any path is
@@ -1127,11 +1626,13 @@ function validatePermissionPaths(permissions) {
1127
1626
  * - Non-absolute paths (must start with `/`)
1128
1627
  * - Paths containing `..`
1129
1628
  * - Paths containing `~`
1629
+ *
1630
+ * Empty and `.` segments are dropped (`/a/./b` and `/a//b` become `/a/b`).
1130
1631
  */
1131
1632
  function validatePath(raw) {
1132
1633
  if (typeof raw !== "string" || raw.length === 0) throw new Error("path must be a non-empty string");
1133
1634
  if (!raw.startsWith("/")) throw new Error(`path must be absolute: ${JSON.stringify(raw)}`);
1134
- const segments = raw.split("/").filter((s) => s.length > 0);
1635
+ const segments = raw.split("/").filter((s) => s.length > 0 && s !== ".");
1135
1636
  if (segments.includes("..")) throw new Error(`path must not contain "..": ${JSON.stringify(raw)}`);
1136
1637
  if (segments.includes("~")) throw new Error(`path must not contain "~": ${JSON.stringify(raw)}`);
1137
1638
  return `/${segments.join("/")}`;
@@ -1181,14 +1682,15 @@ var CompositeBackend = class {
1181
1682
  sortedRoutes;
1182
1683
  constructor(defaultBackend, routes) {
1183
1684
  this.default = isSandboxProtocol(defaultBackend) ? adaptSandboxProtocol(defaultBackend) : adaptBackendProtocol(defaultBackend);
1184
- this.routes = Object.fromEntries(Object.entries(routes).map(([k, v]) => [k, isSandboxProtocol(v) ? adaptSandboxProtocol(v) : adaptBackendProtocol(v)]));
1685
+ this.routes = Object.fromEntries(Object.entries(routes).map(([k, v]) => [k.endsWith("/") ? k : `${k}/`, isSandboxProtocol(v) ? adaptSandboxProtocol(v) : adaptBackendProtocol(v)]));
1185
1686
  this.sortedRoutes = Object.entries(this.routes).sort((a, b) => b[0].length - a[0].length);
1687
+ setRealpath(this, (path) => this.resolveRealPath(path));
1186
1688
  }
1187
1689
  /** Delegates to default backend's id if it is a sandbox, otherwise empty string. */
1188
1690
  get id() {
1189
1691
  return isSandboxBackend(this.default) ? this.default.id : "";
1190
1692
  }
1191
- /** Route prefixes registered on this backend (e.g. `["/workspace"]`). */
1693
+ /** Route prefixes registered on this backend, normalized to a trailing slash (e.g. `["/workspace/"]`). */
1192
1694
  get routePrefixes() {
1193
1695
  return Object.keys(this.routes);
1194
1696
  }
@@ -1202,6 +1704,28 @@ var CompositeBackend = class {
1202
1704
  return typeof backend === "object" && backend !== null && Array.isArray(backend.routePrefixes);
1203
1705
  }
1204
1706
  /**
1707
+ * Returns the backend for `path`, with the matched prefix stripped (`/foo` → `/`).
1708
+ * Routes match on path-segment boundaries: `/foo` matches `/foo/bar` but not `/foobar`.
1709
+ */
1710
+ resolveBackendForPath(path) {
1711
+ return this.getBackendAndKey(path);
1712
+ }
1713
+ /**
1714
+ * Resolve symlinks via the backend that owns `path`, re-adding the route
1715
+ * prefix. Returns `path` unchanged when that backend has no realpath hook.
1716
+ */
1717
+ async resolveRealPath(path) {
1718
+ for (const [routePrefix, backend] of this.sortedRoutes) if (this.isPathWithinRoute(path, routePrefix)) {
1719
+ const realpath = getRealpath(backend);
1720
+ if (realpath === void 0) return path;
1721
+ const suffix = path.substring(routePrefix.length);
1722
+ const resolved = await realpath(suffix ? "/" + suffix : "/");
1723
+ return routePrefix.slice(0, -1) + resolved;
1724
+ }
1725
+ const realpath = getRealpath(this.default);
1726
+ return realpath === void 0 ? path : await realpath(path);
1727
+ }
1728
+ /**
1205
1729
  * Determine which backend handles this key and strip prefix.
1206
1730
  *
1207
1731
  * @param key - Original file path
@@ -1805,6 +2329,8 @@ const GREP_TRUNCATION_NOTE = "Note: the search stopped early because it hit the
1805
2329
  * Set to null to disable the cap.
1806
2330
  */
1807
2331
  const DEFAULT_GREP_MAX_COUNT = 1e3;
2332
+ /** Path prefix offloaded binary content blobs are written under. */
2333
+ const BLOBS_PREFIX = "/blobs";
1808
2334
  /**
1809
2335
  * Message template for evicted tool results.
1810
2336
  */
@@ -1966,25 +2492,42 @@ function getErrorMessage$1(error) {
1966
2492
  return String(error);
1967
2493
  }
1968
2494
  /**
1969
- * Check whether `path` is permitted under `rules` for `operation`, returning an
1970
- * error string to surface to the model (or `undefined` when allowed).
2495
+ * Resolve `canonical` through the backend's realpath hook, when it has one, and
2496
+ * re-validate the result so it is canonical for rule matching. Returns
2497
+ * `canonical` unchanged for backends without path aliasing.
2498
+ */
2499
+ async function resolveRealPath(backend, canonical) {
2500
+ const realpath = getRealpath(backend);
2501
+ return realpath ? validatePath(await realpath(canonical)) : canonical;
2502
+ }
2503
+ /**
2504
+ * Check whether `path` is permitted under `rules` for `operation`.
2505
+ *
2506
+ * Returns the canonical path to hand the backend, or an error string to
2507
+ * surface to the model. The decision covers every form of the target: the
2508
+ * lexical canonical path (dot and empty segments dropped) and, when the
2509
+ * backend has a realpath hook, the symlink-resolved path it would actually
2510
+ * touch. A deny on either blocks the call.
1971
2511
  *
1972
- * Never throws: an invalid path (non-absolute, or containing `..` or `~`) or a
1973
- * denied path is a recoverable tool error, not a fatal run-ending one. Such
1974
- * paths are rejected, never normalized, so they cannot bypass a deny rule or
1975
- * reach the backend.
2512
+ * Never throws: an invalid path (non-absolute, or containing `..` or `~`), one
2513
+ * the backend cannot resolve, or a denied one is a recoverable tool error, not
2514
+ * a fatal run-ending one. With no rules configured, `path` is returned as-is.
1976
2515
  *
1977
2516
  * @internal
1978
2517
  */
1979
- function checkPermission(rules, operation, path) {
1980
- if (rules.length === 0) return;
2518
+ async function checkPermission(backend, rules, operation, path) {
2519
+ if (rules.length === 0) return { path };
1981
2520
  let canonical;
2521
+ let real;
1982
2522
  try {
1983
2523
  canonical = validatePath(path);
2524
+ if (decidePathAccess(rules, operation, canonical) === "deny") return { error: `Error: permission denied for ${operation} on ${canonical}` };
2525
+ real = await resolveRealPath(backend, canonical);
1984
2526
  } catch (error) {
1985
- return `Error: ${getErrorMessage$1(error)}`;
2527
+ return { error: `Error: ${getErrorMessage$1(error)}` };
1986
2528
  }
1987
- if (decidePathAccess(rules, operation, canonical) === "deny") return `Error: permission denied for ${operation} on ${canonical}`;
2529
+ if (real !== canonical && decidePathAccess(rules, operation, real) === "deny") return { error: `Error: permission denied for ${operation} on ${canonical} (resolves to ${real})` };
2530
+ return { path: canonical };
1988
2531
  }
1989
2532
  /**
1990
2533
  * Build an error {@link ToolMessage} for a rejected or denied path. Returning a
@@ -2157,27 +2700,56 @@ function parentPath(path) {
2157
2700
  if (parts.length <= 1) return "/";
2158
2701
  return `/${parts.slice(0, -1).join("/")}`;
2159
2702
  }
2703
+ /**
2704
+ * Resolve the path a delete of `target` would actually remove. Backends unlink
2705
+ * a symlink leaf rather than following it, so only the parent is resolved.
2706
+ */
2707
+ async function resolveDeleteRealPath(backend, target) {
2708
+ const parts = posixParts(target);
2709
+ if (parts.length === 0) return resolveRealPath(backend, target);
2710
+ return validatePath(`${await resolveRealPath(backend, parentPath(target))}/${parts[parts.length - 1]}`);
2711
+ }
2160
2712
  function supportsDelete(backend) {
2161
2713
  return typeof backend.delete === "function";
2162
2714
  }
2163
2715
  /**
2164
2716
  * Filter a list of filesystem entries to those the rules permit.
2165
2717
  *
2166
- * `getPath` extracts the absolute path from each entry. Entries with
2167
- * unparsable paths are included (not silently dropped). Returns the
2168
- * original array unchanged when `rules` is empty.
2718
+ * `getPath` extracts the absolute path from each entry. Each entry is checked
2719
+ * on its canonical path and, when the backend has a realpath hook, on its
2720
+ * symlink-resolved path. Entries with unparsable paths are included; entries
2721
+ * whose path cannot be resolved are dropped.
2722
+ * Returns the original array unchanged when `rules` is empty.
2169
2723
  *
2170
2724
  * @internal
2171
2725
  */
2172
- function filterByPermissions(entries, rules, operation, getPath) {
2726
+ async function filterByPermissions(backend, entries, rules, operation, getPath) {
2173
2727
  if (rules.length === 0) return entries;
2174
- return entries.filter((entry) => {
2728
+ const decisions = /* @__PURE__ */ new Map();
2729
+ const isPermitted = async (rawPath) => {
2730
+ let canonical;
2175
2731
  try {
2176
- return decidePathAccess(rules, operation, validatePath(getPath(entry))) !== "deny";
2732
+ canonical = validatePath(rawPath);
2177
2733
  } catch {
2178
2734
  return true;
2179
2735
  }
2180
- });
2736
+ if (decidePathAccess(rules, operation, canonical) === "deny") return false;
2737
+ try {
2738
+ return decidePathAccess(rules, operation, await resolveRealPath(backend, canonical)) !== "deny";
2739
+ } catch {
2740
+ return false;
2741
+ }
2742
+ };
2743
+ const permitted = await Promise.all(entries.map((entry) => {
2744
+ const rawPath = getPath(entry);
2745
+ let decision = decisions.get(rawPath);
2746
+ if (decision === void 0) {
2747
+ decision = isPermitted(rawPath);
2748
+ decisions.set(rawPath, decision);
2749
+ }
2750
+ return decision;
2751
+ }));
2752
+ return entries.filter((_, index) => permitted[index]);
2181
2753
  }
2182
2754
  const LS_TOOL_DESCRIPTION = langchain.context`
2183
2755
  Lists all files in a directory.
@@ -2265,13 +2837,13 @@ function getExecuteToolDescription(hasGrep, hasGlob) {
2265
2837
  function createLsTool(backend, options) {
2266
2838
  const { customDescription, permissions } = options;
2267
2839
  return (0, langchain.tool)(async (input, runtime) => {
2268
- const permissionError = checkPermission(permissions, "read", input.path ?? "/");
2269
- if (permissionError !== void 0) return toolError(runtime, "ls", permissionError);
2270
2840
  const resolvedBackend = await resolveBackend(backend, runtime);
2271
2841
  const path = input.path || "/";
2272
- const lsResult = await resolvedBackend.ls(path);
2842
+ const permission = await checkPermission(resolvedBackend, permissions, "read", path);
2843
+ if (permission.error !== void 0) return toolError(runtime, "ls", permission.error);
2844
+ const lsResult = await resolvedBackend.ls(permission.path);
2273
2845
  if (lsResult.error) return `Error listing files: ${lsResult.error}`;
2274
- const infos = filterByPermissions(lsResult.files ?? [], permissions, "read", (info) => info.path);
2846
+ const infos = await filterByPermissions(resolvedBackend, lsResult.files ?? [], permissions, "read", (info) => info.path);
2275
2847
  if (infos.length === 0) return `No files found in ${path}`;
2276
2848
  const lines = [];
2277
2849
  for (const info of infos) if (info.is_dir) lines.push(`${info.path} (directory)`);
@@ -2294,23 +2866,17 @@ function createLsTool(backend, options) {
2294
2866
  function createReadFileTool(backend, options) {
2295
2867
  const { customDescription, toolTokenLimitBeforeEvict, permissions } = options;
2296
2868
  return (0, langchain.tool)(async (input, runtime) => {
2297
- const permissionError = checkPermission(permissions, "read", input.file_path);
2298
- if (permissionError !== void 0) return toolError(runtime, "read_file", permissionError);
2299
2869
  const resolvedBackend = await resolveBackend(backend, runtime);
2870
+ const permission = await checkPermission(resolvedBackend, permissions, "read", input.file_path);
2871
+ if (permission.error !== void 0) return toolError(runtime, "read_file", permission.error);
2300
2872
  const { file_path, offset: requestedOffset = 0, limit: requestedLimit = 100 } = input;
2301
2873
  const { offset, limit } = normalizeReadPagination(requestedOffset, requestedLimit);
2302
- const readResult = await resolvedBackend.read(file_path, offset, limit);
2303
- if (readResult.error) return [{
2304
- type: "text",
2305
- text: `Error: ${readResult.error}`
2306
- }];
2874
+ const readResult = await resolvedBackend.read(permission.path, offset, limit);
2875
+ if (readResult.error) return toolError(runtime, "read_file", `Error: ${readResult.error}`);
2307
2876
  const mimeType = readResult.mimeType ?? getMimeType(file_path);
2308
2877
  if (!isTextMimeType(mimeType)) {
2309
2878
  const binaryContent = readResult.content;
2310
- if (!binaryContent) return [{
2311
- type: "text",
2312
- text: `Error: expected binary content for '${file_path}'`
2313
- }];
2879
+ if (!binaryContent) return toolError(runtime, "read_file", `Error: expected binary content for '${file_path}'`);
2314
2880
  let base64Data;
2315
2881
  if (typeof binaryContent === "string") base64Data = binaryContent;
2316
2882
  else if (ArrayBuffer.isView(binaryContent)) base64Data = Buffer.from(binaryContent).toString("base64");
@@ -2319,10 +2885,7 @@ function createReadFileTool(backend, options) {
2319
2885
  base64Data = Buffer.from(new Uint8Array(values)).toString("base64");
2320
2886
  }
2321
2887
  const sizeBytes = Math.ceil(base64Data.length * 3 / 4);
2322
- if (sizeBytes > 10485760) return [{
2323
- type: "text",
2324
- text: `Error: file too large to read (${Math.round(sizeBytes / 1048576)}MB exceeds ${MAX_BINARY_READ_SIZE_BYTES / 1048576}MB limit for binary files)`
2325
- }];
2888
+ if (sizeBytes > 10485760) return toolError(runtime, "read_file", `Error: file too large to read (${Math.round(sizeBytes / 1048576)}MB exceeds ${MAX_BINARY_READ_SIZE_BYTES / 1048576}MB limit for binary files)`);
2326
2889
  if (mimeType.startsWith("image/")) return [{
2327
2890
  type: "image",
2328
2891
  mimeType,
@@ -2388,11 +2951,11 @@ function createReadFileTool(backend, options) {
2388
2951
  function createWriteFileTool(backend, options) {
2389
2952
  const { customDescription, permissions } = options;
2390
2953
  return (0, langchain.tool)(async (input, runtime) => {
2391
- const permissionError = checkPermission(permissions, "write", input.file_path);
2392
- if (permissionError !== void 0) return toolError(runtime, "write_file", permissionError);
2393
2954
  const resolvedBackend = await resolveBackend(backend, runtime);
2955
+ const permission = await checkPermission(resolvedBackend, permissions, "write", input.file_path);
2956
+ if (permission.error !== void 0) return toolError(runtime, "write_file", permission.error);
2394
2957
  const { file_path, content } = input;
2395
- const result = await resolvedBackend.write(file_path, content);
2958
+ const result = await resolvedBackend.write(permission.path, content);
2396
2959
  if (result.error) return result.error;
2397
2960
  const message = new langchain.ToolMessage({
2398
2961
  content: `Successfully wrote to '${file_path}'`,
@@ -2420,11 +2983,11 @@ function createWriteFileTool(backend, options) {
2420
2983
  function createEditFileTool(backend, options) {
2421
2984
  const { customDescription, permissions } = options;
2422
2985
  return (0, langchain.tool)(async (input, runtime) => {
2423
- const permissionError = checkPermission(permissions, "write", input.file_path);
2424
- if (permissionError !== void 0) return toolError(runtime, "edit_file", permissionError);
2425
2986
  const resolvedBackend = await resolveBackend(backend, runtime);
2987
+ const permission = await checkPermission(resolvedBackend, permissions, "write", input.file_path);
2988
+ if (permission.error !== void 0) return toolError(runtime, "edit_file", permission.error);
2426
2989
  const { file_path, old_string, new_string, replace_all = false } = input;
2427
- const result = await resolvedBackend.edit(file_path, old_string, new_string, replace_all);
2990
+ const result = await resolvedBackend.edit(permission.path, old_string, new_string, replace_all);
2428
2991
  if (result.error) return result.error;
2429
2992
  const message = new langchain.ToolMessage({
2430
2993
  content: `Successfully replaced ${result.occurrences} occurrence(s) in '${file_path}'`,
@@ -2464,6 +3027,17 @@ function createDeleteTool(backend, options) {
2464
3027
  const hasDescendants = await deleteTargetMayHaveDescendants(resolvedBackend, validatedPath, permissions.length > 0);
2465
3028
  const denyingPatterns = findDeleteDenyPatterns(permissions, validatedPath, hasDescendants);
2466
3029
  if (denyingPatterns.length > 0) return toolError(runtime, "delete", `Error: permission denied for write on ${validatedPath} (matches deny rule(s): ${denyingPatterns.join(", ")})`);
3030
+ if (permissions.length > 0) {
3031
+ let realTarget;
3032
+ try {
3033
+ realTarget = await resolveDeleteRealPath(resolvedBackend, validatedPath);
3034
+ } catch (error) {
3035
+ return toolError(runtime, "delete", `Error: ${getErrorMessage$1(error)}`);
3036
+ }
3037
+ if (realTarget !== validatedPath) {
3038
+ if (findDeleteDenyPatterns(permissions, realTarget, hasDescendants).length > 0) return toolError(runtime, "delete", `Error: permission denied for write on ${validatedPath} (resolves to ${realTarget})`);
3039
+ }
3040
+ }
2467
3041
  if (!supportsDelete(resolvedBackend)) return toolError(runtime, "delete", `Error: deletion is not available for '${validatedPath}'.`);
2468
3042
  const result = await resolvedBackend.delete(validatedPath);
2469
3043
  if (result.error) return toolError(runtime, "delete", result.error);
@@ -2490,13 +3064,13 @@ function createDeleteTool(backend, options) {
2490
3064
  function createGlobTool(backend, options) {
2491
3065
  const { customDescription, permissions } = options;
2492
3066
  return (0, langchain.tool)(async (input, runtime) => {
2493
- const permissionError = checkPermission(permissions, "read", input.path ?? "/");
2494
- if (permissionError !== void 0) return toolError(runtime, "glob", permissionError);
2495
3067
  const resolvedBackend = await resolveBackend(backend, runtime);
2496
- const { pattern, path } = input;
2497
- const globResult = await resolvedBackend.glob(pattern, path);
3068
+ const { pattern } = input;
3069
+ const permission = await checkPermission(resolvedBackend, permissions, "read", input.path ?? "/");
3070
+ if (permission.error !== void 0) return toolError(runtime, "glob", permission.error);
3071
+ const globResult = await resolvedBackend.glob(pattern, input.path === void 0 ? void 0 : permission.path);
2498
3072
  if (globResult.error) return `Error finding files: ${globResult.error}`;
2499
- const infos = filterByPermissions(globResult.files ?? [], permissions, "read", (info) => info.path);
3073
+ const infos = await filterByPermissions(resolvedBackend, globResult.files ?? [], permissions, "read", (info) => info.path);
2500
3074
  if (infos.length === 0) return `No files found matching pattern '${pattern}'`;
2501
3075
  const result = truncateIfTooLong(infos.map((info) => info.path));
2502
3076
  if (Array.isArray(result)) return result.join("\n");
@@ -2516,14 +3090,14 @@ function createGlobTool(backend, options) {
2516
3090
  function createGrepTool(backend, options) {
2517
3091
  const { customDescription, permissions, includeExecution, grepMaxCount } = options;
2518
3092
  return (0, langchain.tool)(async (input, runtime) => {
2519
- const permissionError = checkPermission(permissions, "read", input.path ?? "/");
2520
- if (permissionError !== void 0) return toolError(runtime, "grep", permissionError);
2521
3093
  const resolvedBackend = await resolveBackend(backend, runtime);
2522
3094
  const { pattern, path = "/", glob = null, output_mode = "content" } = input;
3095
+ const permission = await checkPermission(resolvedBackend, permissions, "read", path);
3096
+ if (permission.error !== void 0) return toolError(runtime, "grep", permission.error);
2523
3097
  const maxCount = input.max_count ?? grepMaxCount;
2524
- const result = await resolvedBackend.grep(pattern, path, glob, maxCount);
3098
+ const result = await resolvedBackend.grep(pattern, permission.path, glob, maxCount);
2525
3099
  if (result.error) return result.error;
2526
- const matches = filterByPermissions(result.matches ?? [], permissions, "read", (m) => m.path);
3100
+ const matches = await filterByPermissions(resolvedBackend, result.matches ?? [], permissions, "read", (m) => m.path);
2527
3101
  if (matches.length === 0) return `No matches found for pattern '${pattern}'`;
2528
3102
  const truncated = truncateIfTooLong(formatGrepMatches(matches, output_mode));
2529
3103
  let content = typeof truncated === "string" ? truncated : truncated.join("\n");
@@ -2587,6 +3161,68 @@ function allPathsScopedToRoutes(permissions, backend) {
2587
3161
  return path === normalizedRoute.slice(0, -1) || path.startsWith(normalizedRoute);
2588
3162
  })));
2589
3163
  }
3164
+ /** Whether `path` resolves to a `StateBackend`, recursing through composite routing. */
3165
+ function routesToStateBackend(backend, path) {
3166
+ if (CompositeBackend.isInstance(backend)) {
3167
+ const [routed, routedPath] = backend.resolveBackendForPath(path);
3168
+ return routesToStateBackend(routed, routedPath);
3169
+ }
3170
+ return StateBackend.isInstance(backend);
3171
+ }
3172
+ /**
3173
+ * Whether `response` is the `{ structuredResponse, messages }` shape a
3174
+ * provider-strategy structured-output call can return instead of a plain
3175
+ * `AIMessage`. The agent framework only recognizes this shape at the
3176
+ * outermost `wrapModelCall` return value — returning a `Command` instead
3177
+ * discards `structuredResponse` with no way to carry it through the Command.
3178
+ */
3179
+ function hasStructuredResponse(response) {
3180
+ return typeof response === "object" && response !== null && "structuredResponse" in response && "messages" in response;
3181
+ }
3182
+ const REJECTED_FILE_CONTENT_TEXT = "Unsupported content. The file may be invalid, too large, or of an unsupported mime-type.";
3183
+ /**
3184
+ * No LangChain error class captures "the provider rejected this request"
3185
+ * generically, so this checks the raw HTTP status providers' client SDKs set
3186
+ * on their errors instead. `ContextOverflowError` is excluded because it's a
3187
+ * different, already-handled problem, and retrying it with stripped file
3188
+ * content wouldn't fix an oversized prompt.
3189
+ */
3190
+ function isRejectedRequestError(error) {
3191
+ if (_langchain_core_errors.ContextOverflowError.isInstance(error)) return false;
3192
+ return typeof error === "object" && error !== null && "status" in error && error.status === 400;
3193
+ }
3194
+ /**
3195
+ * Only messages carrying multimodal blocks after the last `AIMessage` are
3196
+ * candidates for replacement, since anything earlier was already accepted by
3197
+ * the model in a prior turn. Returns `messages` unchanged if nothing
3198
+ * matched, so the caller can tell there was nothing to blame for the
3199
+ * rejection.
3200
+ */
3201
+ function replaceRejectedFileContent(messages) {
3202
+ let lastResponseIndex = -1;
3203
+ for (let i = messages.length - 1; i >= 0; i--) if (langchain.AIMessage.isInstance(messages[i])) {
3204
+ lastResponseIndex = i;
3205
+ break;
3206
+ }
3207
+ let changed = false;
3208
+ const result = messages.map((message, index) => {
3209
+ if (index <= lastResponseIndex || !langchain.ToolMessage.isInstance(message) || message.name !== "read_file") return message;
3210
+ if (!message.contentBlocks.some((block) => MULTIMODAL_BLOCK_TYPES.has(block.type))) return message;
3211
+ changed = true;
3212
+ return new langchain.ToolMessage({
3213
+ content: REJECTED_FILE_CONTENT_TEXT,
3214
+ tool_call_id: message.tool_call_id,
3215
+ name: message.name,
3216
+ id: message.id,
3217
+ artifact: message.artifact,
3218
+ status: message.status,
3219
+ metadata: message.metadata,
3220
+ additional_kwargs: message.additional_kwargs,
3221
+ response_metadata: message.response_metadata
3222
+ });
3223
+ });
3224
+ return changed ? result : messages;
3225
+ }
2590
3226
  /**
2591
3227
  * Create middleware that provides built-in filesystem tools and optional custom
2592
3228
  * prompt guidance.
@@ -2613,7 +3249,10 @@ function allPathsScopedToRoutes(permissions, backend) {
2613
3249
  * ```
2614
3250
  */
2615
3251
  function createFilesystemMiddleware(options = {}) {
2616
- 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;
3252
+ 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;
3253
+ const backendIsStateBackend = StateBackend.isInstance(backend);
3254
+ if (offloadBinaryContent && backendIsStateBackend) console.warn("offloadBinaryContent has no effect: the backend is a StateBackend, which keeps files as part of checkpointed state.");
3255
+ const blobCache = offloadBinaryContent && !backendIsStateBackend ? new BlobCache() : null;
2617
3256
  const enabledFilesystemTools = normalizeFilesystemTools(filesystemTools);
2618
3257
  const executeToolEnabled = enabledFilesystemTools == null || enabledFilesystemTools.has("execute");
2619
3258
  if (permissions.length > 0) validatePermissionPaths(permissions);
@@ -2750,18 +3389,59 @@ function createFilesystemMiddleware(options = {}) {
2750
3389
  return msg;
2751
3390
  });
2752
3391
  }
2753
- return handler({
3392
+ let offloadedHuman = [];
3393
+ if (blobCache && !routesToStateBackend(resolvedBackend, `${BLOBS_PREFIX}/`)) {
3394
+ offloadedHuman = await offloadHumanMessages({
3395
+ stateMessages: request.state.messages ?? [],
3396
+ pending: []
3397
+ }, resolvedBackend, BLOBS_PREFIX, blobCache);
3398
+ messages = await hydrateMessages(messages, resolvedBackend, BLOBS_PREFIX, blobCache);
3399
+ }
3400
+ const finalRequest = {
2754
3401
  ...request,
2755
3402
  tools,
2756
3403
  messages,
2757
3404
  systemMessage: newSystemMessage
2758
- });
3405
+ };
3406
+ const foldOffload = (response, extra = []) => {
3407
+ if (offloadedHuman.length === 0 && extra.length === 0) return response;
3408
+ if (hasStructuredResponse(response)) return {
3409
+ ...response,
3410
+ messages: [
3411
+ ...extra,
3412
+ ...offloadedHuman,
3413
+ ...response.messages
3414
+ ]
3415
+ };
3416
+ return new _langchain_langgraph.Command({ update: { messages: extra.length === 0 ? offloadedHuman : [
3417
+ ...extra,
3418
+ ...offloadedHuman,
3419
+ response
3420
+ ] } });
3421
+ };
3422
+ try {
3423
+ return foldOffload(await handler(finalRequest));
3424
+ } catch (error) {
3425
+ if (!isRejectedRequestError(error)) throw error;
3426
+ const replaced = replaceRejectedFileContent(finalRequest.messages);
3427
+ if (replaced === finalRequest.messages) throw error;
3428
+ return foldOffload(await handler({
3429
+ ...finalRequest,
3430
+ messages: replaced
3431
+ }), replaced.filter((msg, i) => msg !== finalRequest.messages[i]));
3432
+ }
2759
3433
  },
2760
3434
  wrapToolCall: async (request, handler) => {
2761
- if (!toolTokenLimitBeforeEvict) return handler(request);
2762
3435
  const toolName = request.toolCall?.name;
2763
- if (toolName && TOOLS_EXCLUDED_FROM_EVICTION.includes(toolName)) return handler(request);
2764
- const result = await handler(request);
3436
+ let result = await handler(request);
3437
+ if (blobCache && toolName === "read_file") {
3438
+ const resolvedBackend = await resolveBackend(backend, {
3439
+ ...request.runtime,
3440
+ state: request.state
3441
+ });
3442
+ if (!routesToStateBackend(resolvedBackend, `${BLOBS_PREFIX}/`)) result = await offloadToolResult(result, resolvedBackend, BLOBS_PREFIX, /* @__PURE__ */ new Map());
3443
+ }
3444
+ if (!toolTokenLimitBeforeEvict || toolName && TOOLS_EXCLUDED_FROM_EVICTION.includes(toolName)) return result;
2765
3445
  if (langchain.ToolMessage.isInstance(result)) {
2766
3446
  const processed = await processToolMessage(result, request.runtime, request.state, request.toolCall?.id);
2767
3447
  if (processed.filesUpdate) return new _langchain_langgraph.Command({ update: {
@@ -3609,10 +4289,12 @@ const EXCLUDED_STATE_KEYS = [
3609
4289
  "todos",
3610
4290
  "structuredResponse",
3611
4291
  "skillsMetadata",
4292
+ "pinnedSkills",
3612
4293
  "memoryContents",
3613
4294
  ...CALL_COUNT_STATE_KEYS,
3614
4295
  "_summarizationEvent",
3615
4296
  "_summarizationSessionId",
4297
+ "_skillToolsDisclosed",
3616
4298
  FORKED_CONTEXT_KEY
3617
4299
  ];
3618
4300
  /**
@@ -3631,9 +4313,11 @@ const EXCLUDED_STATE_KEYS = [
3631
4313
  */
3632
4314
  const FORK_EXCLUDED_STATE_KEYS = [
3633
4315
  "structuredResponse",
4316
+ "pinnedSkills",
3634
4317
  ...CALL_COUNT_STATE_KEYS,
3635
4318
  "_summarizationEvent",
3636
- "_summarizationSessionId"
4319
+ "_summarizationSessionId",
4320
+ "_skillToolsDisclosed"
3637
4321
  ];
3638
4322
  /**
3639
4323
  * Default description for the general-purpose subagent.
@@ -3805,6 +4489,7 @@ function createSubAgent(spec, options) {
3805
4489
  if (!spec.tools) throw new Error(`SubAgent '${spec.name}' must specify 'tools'`);
3806
4490
  const middleware = [...spec.middleware ?? []];
3807
4491
  if (spec.interruptOn) middleware.push((0, langchain.humanInTheLoopMiddleware)({ interruptOn: spec.interruptOn }));
4492
+ if (!middleware.some((m) => m.name === "UnsupportedContentMiddleware")) middleware.push(createUnsupportedContentMiddleware());
3808
4493
  const selectedResponseFormat = options?.responseFormat ?? spec.responseFormat;
3809
4494
  return (0, langchain.createAgent)({
3810
4495
  model: spec.model,
@@ -4075,7 +4760,8 @@ function patchDanglingToolCalls(messages) {
4075
4760
  patchedMessages.push(new langchain.ToolMessage({
4076
4761
  content: toolMsg,
4077
4762
  name: toolCall.name,
4078
- tool_call_id: toolCall.id
4763
+ tool_call_id: toolCall.id,
4764
+ status: "error"
4079
4765
  }));
4080
4766
  }
4081
4767
  }
@@ -4517,6 +5203,46 @@ function createMemoryMiddleware(options) {
4517
5203
  });
4518
5204
  }
4519
5205
  //#endregion
5206
+ //#region src/errors.ts
5207
+ const CONFIGURATION_ERROR_SYMBOL = Symbol.for("deepagents.configuration_error");
5208
+ /**
5209
+ * Thrown when `createDeepAgent` receives invalid configuration.
5210
+ *
5211
+ * Follows the same pattern as {@link SandboxError}: a human-readable
5212
+ * `message`, a structured `code` for programmatic handling, and a
5213
+ * static `isInstance` guard that works across realms.
5214
+ *
5215
+ * @example
5216
+ * ```typescript
5217
+ * try {
5218
+ * createDeepAgent({ tools: [myTool] });
5219
+ * } catch (error) {
5220
+ * if (ConfigurationError.isInstance(error)) {
5221
+ * switch (error.code) {
5222
+ * case "TOOL_NAME_COLLISION":
5223
+ * console.error("Rename your tool:", error.message);
5224
+ * break;
5225
+ * }
5226
+ * }
5227
+ * }
5228
+ * ```
5229
+ */
5230
+ var ConfigurationError = class ConfigurationError extends Error {
5231
+ code;
5232
+ cause;
5233
+ [CONFIGURATION_ERROR_SYMBOL] = true;
5234
+ name = "ConfigurationError";
5235
+ constructor(message, code, cause) {
5236
+ super(message);
5237
+ this.code = code;
5238
+ this.cause = cause;
5239
+ Object.setPrototypeOf(this, ConfigurationError.prototype);
5240
+ }
5241
+ static isInstance(error) {
5242
+ return typeof error === "object" && error !== null && error[CONFIGURATION_ERROR_SYMBOL] === true;
5243
+ }
5244
+ };
5245
+ //#endregion
4520
5246
  //#region src/middleware/skills.ts
4521
5247
  /**
4522
5248
  * Backend-agnostic skills middleware for loading agent skills from any backend.
@@ -4576,6 +5302,10 @@ const SKILL_MODULE_EXTENSIONS = [
4576
5302
  ".jsx",
4577
5303
  ".tsx"
4578
5304
  ];
5305
+ /** `SKILL.md` frontmatter `metadata` key listing the skill's include names, space-separated. */
5306
+ const INCLUDE_TOOLS_KEY = "include_tools";
5307
+ /** YAML frontmatter between `---` delimiters at the start of a `SKILL.md`. */
5308
+ const FRONTMATTER_PATTERN = /^---\s*\n([\s\S]*?)\n---\s*\n/;
4579
5309
  /**
4580
5310
  * Zod schema for a single skill metadata entry.
4581
5311
  */
@@ -4617,11 +5347,52 @@ const SkillMetadataEntrySchema = zod.z.object({
4617
5347
  */
4618
5348
  const skillsMetadataValue = zod.z.array(SkillMetadataEntrySchema).nullish();
4619
5349
  /**
5350
+ * State value for a middleware's `pinnedSkills` field: the names of skills to
5351
+ * pin.
5352
+ *
5353
+ * The next time the skills middleware's `beforeModel` runs, before a model
5354
+ * call, each named skill's `SKILL.md`, without its frontmatter, is appended to
5355
+ * the conversation as its own `HumanMessage`, in the order named. The message
5356
+ * is a snapshot: editing the file later doesn't change it, and naming the
5357
+ * skill again appends a new copy.
5358
+ *
5359
+ * Names only, matched exactly against the loaded skills. A name that isn't
5360
+ * loaded, or whose `SKILL.md` can't be read, is skipped without an error. The
5361
+ * key is cleared once consumed, so a later run pins nothing unless it names
5362
+ * skills again. Writes in the same step add up, so parallel tool calls can
5363
+ * each pin skills by returning a `Command`.
5364
+ *
5365
+ * Like {@link skillsMetadataValue}, treat the value as opaque. A middleware
5366
+ * declares it on its state schema to set `pinnedSkills` from its own hooks.
5367
+ *
5368
+ * @example
5369
+ * ```typescript
5370
+ * import { createMiddleware } from "langchain";
5371
+ * import { StateSchema } from "@langchain/langgraph";
5372
+ * import { pinnedSkillsValue } from "deepagents";
5373
+ *
5374
+ * const routeSkills = createMiddleware({
5375
+ * name: "RouteSkills",
5376
+ * stateSchema: new StateSchema({ pinnedSkills: pinnedSkillsValue }),
5377
+ * beforeAgent: (state) => ({ pinnedSkills: skillsFor(state.messages) }),
5378
+ * });
5379
+ * ```
5380
+ */
5381
+ const pinnedSkillsValue = new _langchain_langgraph.ReducedValue(zod.z.array(zod.z.string()).default(() => []), { reducer: (stored, written) => [...stored ?? [], ...written ?? []] });
5382
+ /**
4620
5383
  * State schema for skills middleware.
4621
5384
  */
4622
5385
  const SkillsStateSchema = new _langchain_langgraph.StateSchema({
4623
5386
  skillsMetadata: skillsMetadataValue,
4624
- files: filesValue
5387
+ pinnedSkills: pinnedSkillsValue,
5388
+ files: filesValue,
5389
+ /**
5390
+ * The skill tools disclosed to the latest model call, each mapped to the
5391
+ * include name that produced it, for the tool-time gate. Written on every
5392
+ * model call of a skills middleware with skill tools. The last write wins,
5393
+ * so a model call retried within one step can write it again.
5394
+ */
5395
+ _skillToolsDisclosed: new _langchain_langgraph.ReducedValue(zod.z.record(zod.z.string(), zod.z.string()).optional(), { reducer: (_previous, next) => next })
4625
5396
  });
4626
5397
  /**
4627
5398
  * Skills System Documentation prompt template.
@@ -4723,7 +5494,8 @@ function validateSkillName(name, directoryName) {
4723
5494
  *
4724
5495
  * YAML parsing can return any type for the `metadata` key. This ensures the
4725
5496
  * value in {@link SkillMetadata} is always a `Record<string, string>` by
4726
- * coercing via `String()` and rejecting non-object inputs.
5497
+ * coercing via `String()` and rejecting non-object inputs. It also warns when
5498
+ * `include_tools` isn't a space-separated string of tool names.
4727
5499
  *
4728
5500
  * @param raw - Raw value from `frontmatterData.metadata`.
4729
5501
  * @param skillPath - Path to the `SKILL.md` file (for warning messages).
@@ -4734,6 +5506,8 @@ function validateMetadata(raw, skillPath) {
4734
5506
  if (raw) console.warn(`Ignoring non-object metadata in ${skillPath} (got ${typeof raw})`);
4735
5507
  return {};
4736
5508
  }
5509
+ const includeTools = raw[INCLUDE_TOOLS_KEY];
5510
+ if (Array.isArray(includeTools) || typeof includeTools === "string" && includeTools.includes(",")) console.warn(`metadata.include_tools in ${skillPath} should be a space-separated string of tool names; got ${JSON.stringify(includeTools)}`);
4737
5511
  const result = {};
4738
5512
  for (const [k, v] of Object.entries(raw)) result[String(k)] = String(v);
4739
5513
  return result;
@@ -4771,7 +5545,7 @@ function parseSkillMetadataFromContent(content, skillPath, directoryName) {
4771
5545
  console.warn(`Skipping ${skillPath}: content too large (${content.length} bytes)`);
4772
5546
  return null;
4773
5547
  }
4774
- const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
5548
+ const match = content.match(FRONTMATTER_PATTERN);
4775
5549
  if (!match) {
4776
5550
  console.warn(`Skipping ${skillPath}: no valid YAML frontmatter found`);
4777
5551
  return null;
@@ -4890,6 +5664,61 @@ async function listSkillsFromBackend(backend, sourcePath) {
4890
5664
  return skills;
4891
5665
  }
4892
5666
  /**
5667
+ * Load the skills in every source, in order. A later source's skill replaces
5668
+ * an earlier one of the same name.
5669
+ */
5670
+ async function loadSkills(backend, sources) {
5671
+ const allSkills = /* @__PURE__ */ new Map();
5672
+ for (const sourcePath of sources) try {
5673
+ const skills = await listSkillsFromBackend(backend, sourcePath);
5674
+ for (const skill of skills) allSkills.set(skill.name, skill);
5675
+ } catch (error) {
5676
+ console.debug(`[BackendSkillsMiddleware] Failed to load skills from ${sourcePath}:`, error);
5677
+ }
5678
+ return Array.from(allSkills.values());
5679
+ }
5680
+ /**
5681
+ * Return a message pinning each skill `names` refers to, in first-seen order.
5682
+ *
5683
+ * Skips a name that isn't in `skills`, and a skill whose `SKILL.md` is
5684
+ * missing, unreadable, empty, or larger than {@link MAX_SKILL_FILE_SIZE}.
5685
+ */
5686
+ async function buildPinnedSkillMessages(backend, names, skills) {
5687
+ const adaptedBackend = adaptBackendProtocol(backend);
5688
+ const skillsByName = new Map(skills.map((skill) => [skill.name, skill]));
5689
+ const skillsToPin = [...new Set(names)].flatMap((name) => {
5690
+ const skill = skillsByName.get(name);
5691
+ return skill === void 0 ? [] : [skill];
5692
+ });
5693
+ const contents = await Promise.all(skillsToPin.map((skill) => readFileFromBackend(adaptedBackend, skill.path).catch(() => null)));
5694
+ return skillsToPin.flatMap((skill, i) => {
5695
+ const content = contents[i];
5696
+ if (!content || content.length > 10485760) return [];
5697
+ return [buildPinnedSkillMessage(skill, content)];
5698
+ });
5699
+ }
5700
+ /** Return the message pinning `skill`, carrying its `SKILL.md` `content` without frontmatter. */
5701
+ function buildPinnedSkillMessage(skill, content) {
5702
+ const body = content.replace(FRONTMATTER_PATTERN, "").trimEnd();
5703
+ const name = escapeXmlAttribute(skill.name);
5704
+ const path = escapeXmlAttribute(skill.path);
5705
+ return new _langchain_core_messages.HumanMessage({
5706
+ content: `<skill name="${name}" path="${path}">\n${body}\n</skill>`,
5707
+ additional_kwargs: {
5708
+ lc_source: "pinned_skill",
5709
+ skill: {
5710
+ name: skill.name,
5711
+ path: skill.path,
5712
+ description: skill.description
5713
+ }
5714
+ }
5715
+ });
5716
+ }
5717
+ /** Escape `value` for a double-quoted XML attribute. */
5718
+ function escapeXmlAttribute(value) {
5719
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
5720
+ }
5721
+ /**
4893
5722
  * Format skills locations for display in system prompt.
4894
5723
  * Shows priority indicator for the last source (highest priority).
4895
5724
  */
@@ -4949,6 +5778,336 @@ function validateModulePath(raw) {
4949
5778
  if (!endsWithModuleExtension(normalized)) return;
4950
5779
  return normalized;
4951
5780
  }
5781
+ /** Tool `extras` key (and provider field) that withholds a tool's schema until searched for. */
5782
+ const DEFER_LOADING = "defer_loading";
5783
+ /**
5784
+ * Model ID prefixes that accept an inline `tool_definition` mid-conversation.
5785
+ *
5786
+ * `@langchain/anthropic` sends any mid-conversation system message in place,
5787
+ * without gating on the model, so this list is the only guard.
5788
+ */
5789
+ const ANTHROPIC_INLINE_TOOL_MODELS = [
5790
+ "claude-opus-5",
5791
+ "claude-fable-5",
5792
+ "claude-mythos-5",
5793
+ "claude-opus-4-8"
5794
+ ];
5795
+ /** Models whose Responses API accepts an `additional_tools` input item, matched with or without a `-` suffix such as a snapshot date. */
5796
+ const OPENAI_INLINE_TOOL_MODELS = ["gpt-6", "gpt-5.6"];
5797
+ /** Root `input_schema` keys the Anthropic API rejects, failing the whole request. */
5798
+ const ANTHROPIC_ROOT_COMBINATORS = [
5799
+ "oneOf",
5800
+ "anyOf",
5801
+ "allOf"
5802
+ ];
5803
+ /** `extras` keys `ChatAnthropic` copies into a tool's definition, bar the ones a disclosure must never carry. */
5804
+ const ANTHROPIC_DEFINITION_EXTRAS = [
5805
+ "input_examples",
5806
+ "allowed_callers",
5807
+ "strict"
5808
+ ];
5809
+ /**
5810
+ * Return the resolver the skills middleware's `tools` option stands for.
5811
+ *
5812
+ * A list is validated, then resolved by exact name.
5813
+ *
5814
+ * @throws {ConfigurationError} If `tools` is neither a list nor a function,
5815
+ * or a list entry isn't a client tool or repeats a name.
5816
+ */
5817
+ function toSkillToolResolver(tools) {
5818
+ if (tools != null && !Array.isArray(tools)) {
5819
+ if (typeof tools !== "function") throw new ConfigurationError(`tools must be an array of tools or a resolver function, got ${typeof tools}; wrap a single tool in an array`, "SKILL_TOOLS_UNSUPPORTED_TYPE");
5820
+ return tools;
5821
+ }
5822
+ const entries = tools ?? [];
5823
+ if (!entries.every(isClientTool)) throw new ConfigurationError("tools entries must be client tools; provider-native tool objects are not supported", "SKILL_TOOLS_UNSUPPORTED_TYPE");
5824
+ const names = entries.map((t) => t.name);
5825
+ const duplicates = [...new Set(names.filter((n, i) => names.indexOf(n) !== i))].sort();
5826
+ if (duplicates.length > 0) throw new ConfigurationError(`tools contains duplicate tool name(s): ${duplicates.join(", ")}`, "SKILL_TOOLS_DUPLICATE_NAME");
5827
+ const byName = new Map(entries.map((t) => [t.name, t]));
5828
+ return (name) => byName.has(name) ? [byName.get(name)] : [];
5829
+ }
5830
+ /** Whether `value` is a runnable client tool rather than a provider-native or plain object. */
5831
+ function isClientTool(value) {
5832
+ return _langchain_core_runnables.Runnable.isRunnable(value) && typeof value.name === "string";
5833
+ }
5834
+ /**
5835
+ * Call `resolver` for one include name, keeping the first of each tool name.
5836
+ *
5837
+ * @throws {TypeError} If the resolver returns something other than an array
5838
+ * of client tools.
5839
+ */
5840
+ async function callSkillToolResolver(resolver, includeName, runtime) {
5841
+ const result = await resolver(includeName, runtime);
5842
+ if (!Array.isArray(result)) throw new TypeError(`skill tool resolver must return an array of tools for '${includeName}', got ${typeof result}`);
5843
+ const tools = /* @__PURE__ */ new Map();
5844
+ for (const item of result) {
5845
+ if (!isClientTool(item)) throw new TypeError(`skill tool resolver returned a non-tool ${typeof item} for '${includeName}'; expected tool instances`);
5846
+ if (!tools.has(item.name)) tools.set(item.name, item);
5847
+ }
5848
+ return [...tools.values()];
5849
+ }
5850
+ /**
5851
+ * Return every successful `read_file` or pin of a `SKILL.md` that lists
5852
+ * tools, in message order.
5853
+ *
5854
+ * The read's or pin's path must equal the skill's path exactly, as the skills
5855
+ * listing shows it. Any `offset` or `limit` counts, and so does a result whose
5856
+ * content was later truncated or compacted, since only the call and the
5857
+ * result's status are read.
5858
+ */
5859
+ function findSkillReads(messages, skills) {
5860
+ const skillsByPath = /* @__PURE__ */ new Map();
5861
+ for (const skill of skills) {
5862
+ const value = skill.metadata?.[INCLUDE_TOOLS_KEY];
5863
+ const includeNames = typeof value === "string" ? value.split(/\s+/).filter(Boolean) : [];
5864
+ if (includeNames.length > 0) skillsByPath.set(skill.path, { includeNames });
5865
+ }
5866
+ if (skillsByPath.size === 0) return [];
5867
+ const readPaths = /* @__PURE__ */ new Map();
5868
+ for (const message of messages) {
5869
+ if (!_langchain_core_messages.AIMessage.isInstance(message)) continue;
5870
+ for (const toolCall of message.tool_calls ?? []) if (toolCall.name === "read_file" && toolCall.id) readPaths.set(toolCall.id, toolCall.args?.file_path ?? toolCall.args?.path);
5871
+ }
5872
+ const reads = [];
5873
+ messages.forEach((message, index) => {
5874
+ let path;
5875
+ if (_langchain_core_messages.ToolMessage.isInstance(message) && message.status !== "error") path = readPaths.get(message.tool_call_id);
5876
+ else if (_langchain_core_messages.HumanMessage.isInstance(message) && message.additional_kwargs?.lc_source === "pinned_skill") path = message.additional_kwargs.skill?.path;
5877
+ else return;
5878
+ const skill = typeof path === "string" ? skillsByPath.get(path) : void 0;
5879
+ if (skill !== void 0) reads.push({
5880
+ index,
5881
+ ...skill
5882
+ });
5883
+ });
5884
+ return reads;
5885
+ }
5886
+ /** Return a request tool's name, whether it is a tool instance or a provider object. */
5887
+ function toolName(tool) {
5888
+ if ((0, _langchain_core_language_models_base.isOpenAITool)(tool)) return tool.function.name;
5889
+ return typeof tool.name === "string" ? tool.name : void 0;
5890
+ }
5891
+ /**
5892
+ * Disclose the tools the read skills' include names produce to one model call.
5893
+ *
5894
+ * Returns the request to send, and the record of the skill tools it
5895
+ * discloses, for the tool-time gate.
5896
+ */
5897
+ async function discloseSkillTools(request, reads, resolver) {
5898
+ const requestTools = /* @__PURE__ */ new Map();
5899
+ for (const tool of request.tools) {
5900
+ const name = toolName(tool);
5901
+ if (name !== void 0) requestTools.set(name, tool);
5902
+ }
5903
+ const names = [...new Set(reads.flatMap((read) => read.includeNames.filter((name) => !requestTools.has(name))))];
5904
+ const tools = await Promise.all(names.map((name) => callSkillToolResolver(resolver, name, request.runtime)));
5905
+ const disclosed = planDisclosure(reads, requestTools, new Map(names.map((name, i) => [name, tools[i]])));
5906
+ if (disclosed.size === 0) return {
5907
+ request,
5908
+ record: {}
5909
+ };
5910
+ const chatModel = await resolveChatModel(request.model, request.runtime);
5911
+ if (llmType(chatModel) === "anthropic") withholdRootCombinatorTools(disclosed);
5912
+ const gatedNames = [...disclosed.keys()].filter((name) => disclosed.get(name).includeName !== void 0).sort();
5913
+ const record = Object.fromEntries(gatedNames.map((name) => [name, disclosed.get(name).includeName]));
5914
+ if (disclosed.size === 0) return {
5915
+ request,
5916
+ record
5917
+ };
5918
+ const build = inlineBlockBuilder(chatModel);
5919
+ if (build !== void 0) return {
5920
+ request: {
5921
+ ...request,
5922
+ messages: discloseInMessages(request.messages, disclosed, build)
5923
+ },
5924
+ record
5925
+ };
5926
+ return {
5927
+ request: {
5928
+ ...request,
5929
+ tools: discloseInTools(request.tools, disclosed, gatedNames)
5930
+ },
5931
+ record
5932
+ };
5933
+ }
5934
+ /**
5935
+ * Classify what each read skill's include names produce, anchoring each tool
5936
+ * at the earliest read producing it.
5937
+ *
5938
+ * An include name that matches a request tool's name produces that tool; any
5939
+ * other produces what the resolver returned for it. A produced tool whose name
5940
+ * a request tool has wins as that request tool, because it's the one that
5941
+ * runs: a deferred one is disclosed early, and a bound one adds nothing, since
5942
+ * the model already sees it. Tools are matched by name, never by identity,
5943
+ * because outer middleware such as tool search replaces the request's tools
5944
+ * with copies.
5945
+ */
5946
+ function planDisclosure(reads, requestTools, resolved) {
5947
+ const disclosed = /* @__PURE__ */ new Map();
5948
+ const discloseIfDeferred = (name, anchor) => {
5949
+ const entry = requestTools.get(name);
5950
+ if (!disclosed.has(name) && (0, _langchain_core_tools.isLangChainTool)(entry) && entry.extras?.[DEFER_LOADING] === true) disclosed.set(name, {
5951
+ tool: entry,
5952
+ anchor
5953
+ });
5954
+ };
5955
+ for (const { index, includeNames } of reads) for (const includeName of includeNames) {
5956
+ if (requestTools.has(includeName)) {
5957
+ discloseIfDeferred(includeName, index);
5958
+ continue;
5959
+ }
5960
+ for (const tool of resolved.get(includeName) ?? []) {
5961
+ if (disclosed.has(tool.name)) continue;
5962
+ if (requestTools.has(tool.name)) discloseIfDeferred(tool.name, index);
5963
+ else disclosed.set(tool.name, {
5964
+ tool,
5965
+ anchor: index,
5966
+ includeName
5967
+ });
5968
+ }
5969
+ }
5970
+ return disclosed;
5971
+ }
5972
+ /**
5973
+ * Withhold each disclosed tool whose root input schema uses `oneOf`, `anyOf`
5974
+ * or `allOf`, which the Anthropic API rejects, failing the whole request, in
5975
+ * `tools` and inline alike. Withholding it on every path keeps the record
5976
+ * equal to what the model was shown.
5977
+ */
5978
+ function withholdRootCombinatorTools(disclosed) {
5979
+ for (const [name, { tool }] of disclosed) {
5980
+ const schema = anthropicDefinition(tool).input_schema;
5981
+ const keys = ANTHROPIC_ROOT_COMBINATORS.filter((key) => typeof schema === "object" && schema !== null && Object.prototype.hasOwnProperty.call(schema, key));
5982
+ if (keys.length > 0) {
5983
+ console.warn(`Not disclosing tool '${name}': its input_schema has a top-level ${keys.join("/")}, which the Anthropic API does not support`);
5984
+ disclosed.delete(name);
5985
+ }
5986
+ }
5987
+ }
5988
+ /**
5989
+ * Insert one `SystemMessage` per insertion point, carrying the tools anchored
5990
+ * there, for models that accept tool definitions mid-conversation.
5991
+ *
5992
+ * Each message lands at the same index with the same bytes on every call while
5993
+ * its anchor stays visible, so the provider's cached prefix survives.
5994
+ */
5995
+ function discloseInMessages(messages, disclosed, build) {
5996
+ const namesByIndex = /* @__PURE__ */ new Map();
5997
+ for (const [name, { anchor }] of disclosed) {
5998
+ let index = anchor + 1;
5999
+ while (index < messages.length) {
6000
+ const message = messages[index];
6001
+ const emptyReply = _langchain_core_messages.AIMessage.isInstance(message) && message.content.length === 0 && !message.tool_calls?.length;
6002
+ if (!_langchain_core_messages.ToolMessage.isInstance(message) && !_langchain_core_messages.HumanMessage.isInstance(message) && !emptyReply) break;
6003
+ index += 1;
6004
+ }
6005
+ namesByIndex.set(index, [...namesByIndex.get(index) ?? [], name]);
6006
+ }
6007
+ const result = [...messages];
6008
+ for (const index of [...namesByIndex.keys()].sort((a, b) => b - a)) {
6009
+ const blocks = namesByIndex.get(index).sort().map((name) => build(disclosed.get(name).tool));
6010
+ result.splice(index, 0, new _langchain_core_messages.SystemMessage({ content: blocks }));
6011
+ }
6012
+ return result;
6013
+ }
6014
+ /**
6015
+ * Return `requestTools` with deferred disclosures undeferred and skill tools
6016
+ * appended, for models that can't take tool definitions mid-conversation.
6017
+ *
6018
+ * A deferred tool is replaced by a plain spec rather than a copy: `createAgent`
6019
+ * rejects a request that swaps a registered tool for another instance of the
6020
+ * same name, and doesn't validate plain objects.
6021
+ */
6022
+ function discloseInTools(requestTools, disclosed, gatedNames) {
6023
+ return [...requestTools.map((tool) => {
6024
+ const name = toolName(tool);
6025
+ const entry = name === void 0 ? void 0 : disclosed.get(name);
6026
+ if (entry?.tool !== tool || entry.includeName !== void 0) return tool;
6027
+ const { description, schema, extras = {} } = entry.tool;
6028
+ const { [DEFER_LOADING]: _deferLoading, ...rest } = extras;
6029
+ return {
6030
+ name,
6031
+ description,
6032
+ schema,
6033
+ extras: rest
6034
+ };
6035
+ }), ...gatedNames.map((name) => disclosed.get(name).tool)];
6036
+ }
6037
+ /** Return the model inside a `bind()` / `withConfig()` wrapper; chained calls merge into one. */
6038
+ function unwrapBinding(model) {
6039
+ return _langchain_core_runnables.RunnableBinding.isRunnableBinding(model) ? model.bound : model;
6040
+ }
6041
+ /**
6042
+ * Return the concrete chat model a request will call.
6043
+ *
6044
+ * Resolves a `ConfigurableModel`, which a string `model` always becomes, to
6045
+ * the instance it will call with this run's `configurable`. Without
6046
+ * configurable overrides that's the instance cached at creation: the instance
6047
+ * cache is keyed on the whole config, so passing `configurable`
6048
+ * unconditionally would build one per thread.
6049
+ */
6050
+ async function resolveChatModel(model, runtime) {
6051
+ const current = unwrapBinding(model);
6052
+ if (current?._queuedMethodOperations === void 0 || typeof current._getModelInstance !== "function") return current;
6053
+ const configurable = runtime.configurable ?? {};
6054
+ const overrides = current._modelParams?.({ configurable }) ?? {};
6055
+ return unwrapBinding(Object.keys(overrides).length > 0 ? await current._getModelInstance({ configurable }) : await current._getModelInstance());
6056
+ }
6057
+ /** Return a chat model's `_llmType()`, which tells providers apart. */
6058
+ function llmType(model) {
6059
+ const fn = model?._llmType;
6060
+ return typeof fn === "function" ? fn.call(model) : void 0;
6061
+ }
6062
+ /**
6063
+ * Return how `chatModel` is given a tool mid-conversation, or `undefined` if
6064
+ * it can't be.
6065
+ *
6066
+ * The one place that decides support, so a model-profile capability can
6067
+ * replace the allowlists later.
6068
+ */
6069
+ function inlineBlockBuilder(chatModel) {
6070
+ const { model: name, useResponsesApi } = chatModel;
6071
+ if (typeof name !== "string") return void 0;
6072
+ const type = llmType(chatModel);
6073
+ if (type === "anthropic" && ANTHROPIC_INLINE_TOOL_MODELS.some((prefix) => name.startsWith(prefix))) return anthropicToolAddition;
6074
+ if (type === "openai" && useResponsesApi === true && OPENAI_INLINE_TOOL_MODELS.some((model) => name === model || name.startsWith(`${model}-`))) return openaiAdditionalTools;
6075
+ }
6076
+ /**
6077
+ * Return the definition `ChatAnthropic` sends in `tools` for `tool`, without
6078
+ * `defer_loading` (a deferred definition would stay withheld) or
6079
+ * `cache_control` (a stray marker uses up a breakpoint).
6080
+ */
6081
+ function anthropicDefinition(tool) {
6082
+ const definition = {
6083
+ name: tool.name,
6084
+ description: tool.description,
6085
+ input_schema: (0, _langchain_core_utils_types.isInteropZodSchema)(tool.schema) ? (0, _langchain_core_utils_json_schema.toJsonSchema)(tool.schema) : tool.schema
6086
+ };
6087
+ for (const key of ANTHROPIC_DEFINITION_EXTRAS) if (tool.extras?.[key] !== void 0) definition[key] = tool.extras[key];
6088
+ return definition;
6089
+ }
6090
+ /** Build the Anthropic `tool_addition` block carrying `tool`'s full definition. */
6091
+ function anthropicToolAddition(tool) {
6092
+ return {
6093
+ type: "tool_addition",
6094
+ tool: {
6095
+ type: "tool_definition",
6096
+ definition: anthropicDefinition(tool)
6097
+ }
6098
+ };
6099
+ }
6100
+ /** Build the OpenAI Responses `additional_tools` item carrying `tool`'s function schema. */
6101
+ function openaiAdditionalTools(tool) {
6102
+ return {
6103
+ type: "additional_tools",
6104
+ role: "developer",
6105
+ tools: [{
6106
+ type: "function",
6107
+ ...(0, _langchain_core_utils_function_calling.convertToOpenAIFunction)(tool)
6108
+ }]
6109
+ };
6110
+ }
4952
6111
  /**
4953
6112
  * Create backend-agnostic middleware for loading and exposing agent skills.
4954
6113
  *
@@ -4972,8 +6131,62 @@ function validateModulePath(raw) {
4972
6131
  * invalidate from any hook — including mid-run, from `afterModel` — and see
4973
6132
  * the fresh list on the following call. See {@link skillsMetadataValue}.
4974
6133
  *
6134
+ * To guarantee the model gets a skill's instructions, for example when the
6135
+ * user names it, pass its name in `pinnedSkills`. Before the next model call,
6136
+ * each named skill's `SKILL.md`, without its frontmatter, is appended to the
6137
+ * conversation as its own `HumanMessage`, with `additional_kwargs.lc_source`
6138
+ * set to `"pinned_skill"` and the skill's `name`, `path` and `description`
6139
+ * under `additional_kwargs.skill`:
6140
+ *
6141
+ * ```ts
6142
+ * await agent.invoke(
6143
+ * {
6144
+ * messages: [new HumanMessage("/write-tests /house-style for auth.py")],
6145
+ * pinnedSkills: ["write-tests", "house-style"],
6146
+ * },
6147
+ * config,
6148
+ * );
6149
+ * ```
6150
+ *
6151
+ * Pins resolve against the skills loaded for the thread, so reset
6152
+ * `skillsMetadata` in the same input to pin a skill added since. See
6153
+ * {@link pinnedSkillsValue}.
6154
+ *
6155
+ * ## Skill tools
6156
+ *
6157
+ * A skill can list the tools its instructions use, separated by spaces, under
6158
+ * `metadata.include_tools` in its `SKILL.md` frontmatter:
6159
+ *
6160
+ * ```yaml
6161
+ * metadata:
6162
+ * include_tools: create_customer_request list_customer_requests
6163
+ * ```
6164
+ *
6165
+ * Pass those tools as `tools`, either as an array or as a
6166
+ * {@link SkillToolResolver} that looks them up by name when a skill is read
6167
+ * or pinned, so one name can stand for a family of tools whose real names are
6168
+ * only known at runtime. The model sees a skill tool only after it reads, with
6169
+ * `read_file`, a skill that lists it, or the skill is pinned, and only while
6170
+ * that read or pin stays in the conversation it is sent; compaction that drops
6171
+ * the read or pin withdraws the tool.
6172
+ * Until then, calling the tool fails with the standard invalid-tool error and
6173
+ * the tool doesn't run. This controls what is in the model's context; it is
6174
+ * not a security boundary, since the model can read any `SKILL.md` at any
6175
+ * time.
6176
+ *
6177
+ * `include_tools` can also name a tool passed to the agent rather than to this
6178
+ * middleware. That tool wins over a skill tool of the same name. If it is
6179
+ * deferred (`extras: { defer_loading: true }`), reading or pinning the skill
6180
+ * discloses it early, and it stays deferred and searchable; if it is bound,
6181
+ * nothing changes.
6182
+ *
6183
+ *
4975
6184
  * @param options - Configuration options
4976
6185
  * @returns AgentMiddleware for skills loading and injection
6186
+ * @throws {ConfigurationError} If `tools` is neither an array nor a
6187
+ * function (`SKILL_TOOLS_UNSUPPORTED_TYPE`), an entry isn't a client tool
6188
+ * (`SKILL_TOOLS_UNSUPPORTED_TYPE`), or two entries share a name
6189
+ * (`SKILL_TOOLS_DUPLICATE_NAME`).
4977
6190
  *
4978
6191
  * @example
4979
6192
  * ```typescript
@@ -4985,30 +6198,60 @@ function validateModulePath(raw) {
4985
6198
  */
4986
6199
  function createSkillsMiddleware(options) {
4987
6200
  const { backend, sources } = options;
6201
+ const skillToolResolver = toSkillToolResolver(options.tools);
6202
+ const hasSkillTools = typeof options.tools === "function" || Array.isArray(options.tools) && options.tools.length > 0;
4988
6203
  return (0, langchain.createMiddleware)({
4989
6204
  name: "SkillsMiddleware",
4990
6205
  stateSchema: SkillsStateSchema,
4991
6206
  async beforeModel(state) {
4992
- if (state.skillsMetadata !== null && state.skillsMetadata !== void 0) return;
6207
+ const needsLoad = state.skillsMetadata === null || state.skillsMetadata === void 0;
6208
+ const pinnedNames = state.pinnedSkills ?? [];
6209
+ if (!needsLoad && pinnedNames.length === 0) return;
4993
6210
  const resolvedBackend = await resolveBackend(backend, { state });
4994
- const allSkills = /* @__PURE__ */ new Map();
4995
- for (const sourcePath of sources) try {
4996
- const skills = await listSkillsFromBackend(resolvedBackend, sourcePath);
4997
- for (const skill of skills) allSkills.set(skill.name, skill);
4998
- } catch (error) {
4999
- console.debug(`[BackendSkillsMiddleware] Failed to load skills from ${sourcePath}:`, error);
5000
- }
5001
- return { skillsMetadata: Array.from(allSkills.values()) };
6211
+ const skillsMetadata = state.skillsMetadata ?? await loadSkills(resolvedBackend, sources);
6212
+ if (pinnedNames.length === 0) return { skillsMetadata };
6213
+ const messages = await buildPinnedSkillMessages(resolvedBackend, pinnedNames, skillsMetadata);
6214
+ return {
6215
+ ...needsLoad && { skillsMetadata },
6216
+ ...messages.length > 0 && { messages },
6217
+ pinnedSkills: new _langchain_langgraph.Overwrite([])
6218
+ };
5002
6219
  },
5003
- wrapModelCall(request, handler) {
6220
+ async wrapModelCall(request, handler) {
5004
6221
  const skillsMetadata = request.state?.skillsMetadata || [];
5005
6222
  const skillsLocations = formatSkillsLocations(sources);
5006
6223
  const skillsList = formatSkillsList(skillsMetadata, sources);
5007
6224
  const skillsSection = SKILLS_SYSTEM_PROMPT.replace("{skills_locations}", skillsLocations).replace("{skills_list}", skillsList);
5008
6225
  const newSystemMessage = request.systemMessage.concat(skillsSection);
5009
- return handler({
6226
+ const prompted = {
5010
6227
  ...request,
5011
6228
  systemMessage: newSystemMessage
6229
+ };
6230
+ const reads = findSkillReads(request.messages, skillsMetadata);
6231
+ const { request: disclosed, record } = reads.length === 0 ? {
6232
+ request: prompted,
6233
+ record: {}
6234
+ } : await discloseSkillTools(prompted, reads, skillToolResolver);
6235
+ const response = await handler(disclosed);
6236
+ const stored = request.state._skillToolsDisclosed;
6237
+ if (!hasSkillTools && Object.keys(stored ?? {}).length === 0) return response;
6238
+ if (typeof response === "object" && response !== null && "structuredResponse" in response) return response;
6239
+ return new _langchain_langgraph.Command({ update: { _skillToolsDisclosed: record } });
6240
+ },
6241
+ async wrapToolCall(request, handler) {
6242
+ if (request.tool !== void 0) return handler(request);
6243
+ const name = request.toolCall.name;
6244
+ const record = request.state._skillToolsDisclosed ?? {};
6245
+ const includeName = Object.hasOwn(record, name) ? record[name] : void 0;
6246
+ if (includeName === void 0) return handler(request);
6247
+ const tool = (await callSkillToolResolver(skillToolResolver, includeName, request.runtime)).find((candidate) => candidate.name === name);
6248
+ if (tool === void 0) {
6249
+ console.warn(`Skill tool '${name}' was disclosed via '${includeName}', but the resolver no longer returns it`);
6250
+ return handler(request);
6251
+ }
6252
+ return handler({
6253
+ ...request,
6254
+ tool
5012
6255
  });
5013
6256
  }
5014
6257
  });
@@ -5778,46 +7021,6 @@ function createAsyncSubAgentMiddleware(options) {
5778
7021
  });
5779
7022
  }
5780
7023
  //#endregion
5781
- //#region src/errors.ts
5782
- const CONFIGURATION_ERROR_SYMBOL = Symbol.for("deepagents.configuration_error");
5783
- /**
5784
- * Thrown when `createDeepAgent` receives invalid configuration.
5785
- *
5786
- * Follows the same pattern as {@link SandboxError}: a human-readable
5787
- * `message`, a structured `code` for programmatic handling, and a
5788
- * static `isInstance` guard that works across realms.
5789
- *
5790
- * @example
5791
- * ```typescript
5792
- * try {
5793
- * createDeepAgent({ tools: [myTool] });
5794
- * } catch (error) {
5795
- * if (ConfigurationError.isInstance(error)) {
5796
- * switch (error.code) {
5797
- * case "TOOL_NAME_COLLISION":
5798
- * console.error("Rename your tool:", error.message);
5799
- * break;
5800
- * }
5801
- * }
5802
- * }
5803
- * ```
5804
- */
5805
- var ConfigurationError = class ConfigurationError extends Error {
5806
- code;
5807
- cause;
5808
- [CONFIGURATION_ERROR_SYMBOL] = true;
5809
- name = "ConfigurationError";
5810
- constructor(message, code, cause) {
5811
- super(message);
5812
- this.code = code;
5813
- this.cause = cause;
5814
- Object.setPrototypeOf(this, ConfigurationError.prototype);
5815
- }
5816
- static isInstance(error) {
5817
- return typeof error === "object" && error !== null && error[CONFIGURATION_ERROR_SYMBOL] === true;
5818
- }
5819
- };
5820
- //#endregion
5821
7024
  //#region src/middleware/cache.ts
5822
7025
  /**
5823
7026
  * Creates a middleware that places a cache breakpoint at the end of the static
@@ -6683,13 +7886,10 @@ function createDeepAgent(params = {}) {
6683
7886
  harnessProfile.systemPromptSuffix
6684
7887
  ]);
6685
7888
  /**
6686
- * Process subagents to add SkillsMiddleware for those with their own skills.
6687
- *
6688
- * Custom subagents do NOT inherit skills from the main agent by default.
6689
- * Only the general-purpose subagent inherits the main agent's skills.
6690
- * If a custom subagent needs skills, it must specify its own `skills` array.
7889
+ * Build the default head of a declarative subagent's middleware stack.
7890
+ * Skills go in the tail instead (see buildSubagentMiddleware).
6691
7891
  */
6692
- const createSubagentDefaultMiddleware = (input, subagentProfile, forked) => {
7892
+ const createSubagentDefaultMiddleware = (input, subagentProfile) => {
6693
7893
  const effectivePermissions = input.permissions ?? permissions;
6694
7894
  return [
6695
7895
  createFilesystemMiddleware({
@@ -6698,23 +7898,33 @@ function createDeepAgent(params = {}) {
6698
7898
  tools: computeProfileFilesystemTools(subagentProfile)
6699
7899
  }),
6700
7900
  createSummarizationMiddleware({ backend }),
6701
- createPatchToolCallsMiddleware(),
6702
- ...!forked && input.skills != null && input.skills.length > 0 ? [createSkillsMiddleware({
6703
- backend,
6704
- sources: input.skills
6705
- })] : []
7901
+ createPatchToolCallsMiddleware()
6706
7902
  ];
6707
7903
  };
6708
- const buildSubagentMiddleware = (input) => {
7904
+ /**
7905
+ * The skills middleware for a stack: the last `SkillsMiddleware` among its
7906
+ * custom middleware, else one built from `sources`, else none. A custom one
7907
+ * is picked here rather than left to replace the default by name, because
7908
+ * without `sources` there is no default to replace and it would run with
7909
+ * the novel middleware instead.
7910
+ */
7911
+ const resolveSkillsMiddleware = (sources, custom) => {
7912
+ const passed = custom.findLast((entry) => entry.name === "SkillsMiddleware");
7913
+ if (passed !== void 0) return [passed];
7914
+ return sources != null && sources.length > 0 ? [createSkillsMiddleware({
7915
+ backend,
7916
+ sources
7917
+ })] : [];
7918
+ };
7919
+ const buildSubagentMiddleware = (input, generalPurpose = false) => {
6709
7920
  const subagentProfile = resolveSubagentProfile(input.model);
6710
7921
  const forked = isForkedSubAgent(input);
6711
- const subagentDefaultMiddleware = createSubagentDefaultMiddleware(input, subagentProfile, forked);
6712
- if (forked && skills != null && skills.length > 0) subagentDefaultMiddleware.unshift(createSkillsMiddleware({
6713
- backend,
6714
- sources: skills
6715
- }));
6716
- let subagentMiddleware = mergeMiddlewareStack(subagentDefaultMiddleware, forked && customMiddleware.length > 0 ? mergeMiddleware$1(customMiddleware, input.middleware ?? []) : input.middleware ?? [], [
7922
+ const subagentDefaultMiddleware = createSubagentDefaultMiddleware(input, subagentProfile);
7923
+ const inputMiddleware = forked && customMiddleware.length > 0 ? mergeMiddleware$1(customMiddleware, input.middleware ?? []) : input.middleware ?? [];
7924
+ const subagentSkillsMiddleware = generalPurpose ? resolveSkillsMiddleware(skills, customMiddleware) : resolveSkillsMiddleware(forked ? skills : input.skills, inputMiddleware);
7925
+ let subagentMiddleware = mergeMiddlewareStack(subagentDefaultMiddleware, inputMiddleware, [
6717
7926
  ...resolveMiddleware(subagentProfile.extraMiddleware),
7927
+ ...subagentSkillsMiddleware,
6718
7928
  ...cacheMiddleware,
6719
7929
  ...forked && memory != null && memory.length > 0 ? [createMemoryMiddleware({
6720
7930
  backend,
@@ -6724,11 +7934,12 @@ function createDeepAgent(params = {}) {
6724
7934
  ]);
6725
7935
  if (subagentProfile.excludedMiddleware.size > 0) subagentMiddleware = subagentMiddleware.filter((middleware) => !subagentProfile.excludedMiddleware.has(middleware.name));
6726
7936
  if (subagentProfile.excludedTools.size > 0) subagentMiddleware.push(createToolExclusionMiddleware(subagentProfile.excludedTools));
7937
+ if (!subagentProfile.excludedMiddleware.has("UnsupportedContentMiddleware") && !subagentMiddleware.some((middleware) => middleware.name === "UnsupportedContentMiddleware")) subagentMiddleware.push(createUnsupportedContentMiddleware());
6727
7938
  return subagentMiddleware;
6728
7939
  };
6729
- const normalizeSubagentSpec = (input) => ({
7940
+ const normalizeSubagentSpec = (input, generalPurpose = false) => ({
6730
7941
  ...input,
6731
- middleware: buildSubagentMiddleware(input)
7942
+ middleware: buildSubagentMiddleware(input, generalPurpose)
6732
7943
  });
6733
7944
  const allSubagents = subagents;
6734
7945
  const asyncSubAgents = allSubagents.filter((item) => isAsyncSubAgent(item));
@@ -6741,16 +7952,12 @@ function createDeepAgent(params = {}) {
6741
7952
  description: gpConfig?.description ?? GENERAL_PURPOSE_SUBAGENT.description,
6742
7953
  systemPrompt: gpSystemPrompt,
6743
7954
  model,
6744
- skills,
6745
7955
  tools: effectiveTools
6746
- });
7956
+ }, true);
6747
7957
  generalPurposeSpec.middleware = mergeMiddlewareStack(generalPurposeSpec.middleware ?? [], customMiddleware, [], { appendNew: false });
6748
7958
  inlineSubagents.unshift(generalPurposeSpec);
6749
7959
  }
6750
- const skillsMiddleware = skills != null && skills.length > 0 ? [createSkillsMiddleware({
6751
- backend,
6752
- sources: skills
6753
- })] : [];
7960
+ const skillsMiddleware = resolveSkillsMiddleware(skills, customMiddleware);
6754
7961
  const [fsMiddleware, subagentMiddleware, summarizationMiddleware, patchToolCallsMiddleware] = [
6755
7962
  createFilesystemMiddleware({
6756
7963
  backend,
@@ -6769,7 +7976,6 @@ function createDeepAgent(params = {}) {
6769
7976
  createPatchToolCallsMiddleware()
6770
7977
  ];
6771
7978
  let middleware = mergeMiddlewareStack([
6772
- ...skillsMiddleware,
6773
7979
  fsMiddleware,
6774
7980
  subagentMiddleware,
6775
7981
  summarizationMiddleware,
@@ -6777,6 +7983,7 @@ function createDeepAgent(params = {}) {
6777
7983
  ...asyncSubAgents.length > 0 ? [createAsyncSubAgentMiddleware({ asyncSubAgents })] : []
6778
7984
  ], customMiddleware, [
6779
7985
  ...resolveMiddleware(harnessProfile.extraMiddleware),
7986
+ ...skillsMiddleware,
6780
7987
  ...cacheMiddleware,
6781
7988
  ...memoryMiddleware,
6782
7989
  ...interruptOn ? [(0, langchain.humanInTheLoopMiddleware)({ interruptOn })] : []
@@ -6786,6 +7993,7 @@ function createDeepAgent(params = {}) {
6786
7993
  middleware = middleware.filter((entry) => !excluded.has(entry.name));
6787
7994
  }
6788
7995
  if (harnessProfile.excludedTools.size > 0) middleware.push(createToolExclusionMiddleware(harnessProfile.excludedTools));
7996
+ if (!harnessProfile.excludedMiddleware.has("UnsupportedContentMiddleware") && !middleware.some((m) => m.name === "UnsupportedContentMiddleware")) middleware.push(createUnsupportedContentMiddleware());
6789
7997
  /**
6790
7998
  * Return as DeepAgent with proper DeepAgentTypeConfig
6791
7999
  * - Response: InferStructuredResponse<TResponse> (unwraps ToolStrategy<T>/ProviderStrategy<T> → T)
@@ -8993,6 +10201,12 @@ Object.defineProperty(exports, "MAX_SKILL_NAME_LENGTH", {
8993
10201
  return MAX_SKILL_NAME_LENGTH;
8994
10202
  }
8995
10203
  });
10204
+ Object.defineProperty(exports, "MULTIMODAL_BLOCK_TYPES", {
10205
+ enumerable: true,
10206
+ get: function() {
10207
+ return MULTIMODAL_BLOCK_TYPES;
10208
+ }
10209
+ });
8996
10210
  Object.defineProperty(exports, "REQUIRED_MIDDLEWARE_NAMES", {
8997
10211
  enumerable: true,
8998
10212
  get: function() {
@@ -9131,6 +10345,12 @@ Object.defineProperty(exports, "createSummarizationMiddleware", {
9131
10345
  return createSummarizationMiddleware;
9132
10346
  }
9133
10347
  });
10348
+ Object.defineProperty(exports, "createUnsupportedContentMiddleware", {
10349
+ enumerable: true,
10350
+ get: function() {
10351
+ return createUnsupportedContentMiddleware;
10352
+ }
10353
+ });
9134
10354
  Object.defineProperty(exports, "filesValue", {
9135
10355
  enumerable: true,
9136
10356
  get: function() {
@@ -9185,6 +10405,12 @@ Object.defineProperty(exports, "isTextMimeType", {
9185
10405
  return isTextMimeType;
9186
10406
  }
9187
10407
  });
10408
+ Object.defineProperty(exports, "multimodalBlockSupported", {
10409
+ enumerable: true,
10410
+ get: function() {
10411
+ return multimodalBlockSupported;
10412
+ }
10413
+ });
9188
10414
  Object.defineProperty(exports, "normalizeReadPagination", {
9189
10415
  enumerable: true,
9190
10416
  get: function() {
@@ -9203,6 +10429,12 @@ Object.defineProperty(exports, "performStringReplacement", {
9203
10429
  return performStringReplacement;
9204
10430
  }
9205
10431
  });
10432
+ Object.defineProperty(exports, "pinnedSkillsValue", {
10433
+ enumerable: true,
10434
+ get: function() {
10435
+ return pinnedSkillsValue;
10436
+ }
10437
+ });
9206
10438
  Object.defineProperty(exports, "registerHarnessProfile", {
9207
10439
  enumerable: true,
9208
10440
  get: function() {
@@ -9215,12 +10447,24 @@ Object.defineProperty(exports, "resolveBackend", {
9215
10447
  return resolveBackend;
9216
10448
  }
9217
10449
  });
10450
+ Object.defineProperty(exports, "scrubUnsupportedMultimodalContent", {
10451
+ enumerable: true,
10452
+ get: function() {
10453
+ return scrubUnsupportedMultimodalContent;
10454
+ }
10455
+ });
9218
10456
  Object.defineProperty(exports, "serializeProfile", {
9219
10457
  enumerable: true,
9220
10458
  get: function() {
9221
10459
  return serializeProfile;
9222
10460
  }
9223
10461
  });
10462
+ Object.defineProperty(exports, "setRealpath", {
10463
+ enumerable: true,
10464
+ get: function() {
10465
+ return setRealpath;
10466
+ }
10467
+ });
9224
10468
  Object.defineProperty(exports, "skillsMetadataValue", {
9225
10469
  enumerable: true,
9226
10470
  get: function() {
@@ -9228,4 +10472,4 @@ Object.defineProperty(exports, "skillsMetadataValue", {
9228
10472
  }
9229
10473
  });
9230
10474
 
9231
- //# sourceMappingURL=langsmith-Bhg_Q4ZO.cjs.map
10475
+ //# sourceMappingURL=langsmith-DMOexB11.cjs.map