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.
@@ -1,16 +1,38 @@
1
1
  import { AIMessage, HumanMessage, SystemMessage, ToolMessage, anthropicPromptCachingMiddleware, bedrockPromptCachingMiddleware, context, countTokensApproximately, createAgent, createMiddleware, humanInTheLoopMiddleware, todoListMiddleware, tool } from "langchain";
2
- import { Command, REMOVE_ALL_MESSAGES, ReducedValue, StateSchema, getConfig, getCurrentTaskInput, getStore, isCommand } from "@langchain/langgraph";
2
+ import { ContextOverflowError } from "@langchain/core/errors";
3
+ import { Command, Overwrite, REMOVE_ALL_MESSAGES, ReducedValue, StateSchema, getConfig, getCurrentTaskInput, getStore, isCommand } from "@langchain/langgraph";
3
4
  import { z } from "zod/v4";
4
5
  import micromatch from "micromatch";
6
+ import { createHash } from "node:crypto";
5
7
  import { AIMessage as AIMessage$1, HumanMessage as HumanMessage$1, RemoveMessage, SystemMessage as SystemMessage$1, ToolMessage as ToolMessage$1, getBufferString } from "@langchain/core/messages";
6
8
  import * as z$2 from "zod";
7
9
  import { z as z$1 } from "zod";
8
- import { ContextOverflowError } from "@langchain/core/errors";
9
10
  import { initChatModel } from "langchain/chat_models/universal";
10
11
  import yaml from "yaml";
12
+ import { Runnable, RunnableBinding } from "@langchain/core/runnables";
13
+ import { isLangChainTool } from "@langchain/core/tools";
14
+ import { isOpenAITool } from "@langchain/core/language_models/base";
15
+ import { convertToOpenAIFunction } from "@langchain/core/utils/function_calling";
16
+ import { toJsonSchema } from "@langchain/core/utils/json_schema";
17
+ import { isInteropZodSchema } from "@langchain/core/utils/types";
11
18
  import { Client } from "@langchain/langgraph-sdk";
12
19
  import { Client as Client$1 } from "langsmith";
13
20
  import { LangSmithResourceNotFoundError, LangSmithSandboxError, SandboxClient } from "langsmith/experimental/sandbox";
21
+ //#region src/backends/realpath.ts
22
+ const REALPATH = Symbol.for("deepagents.realpath");
23
+ function getRealpath(backend) {
24
+ if (typeof backend !== "object" || backend === null) return;
25
+ const fn = backend[REALPATH];
26
+ return typeof fn === "function" ? fn : void 0;
27
+ }
28
+ function setRealpath(target, fn) {
29
+ Object.defineProperty(target, REALPATH, {
30
+ value: fn,
31
+ enumerable: false,
32
+ configurable: true
33
+ });
34
+ }
35
+ //#endregion
14
36
  //#region src/backends/utils.ts
15
37
  /**
16
38
  * Shared utility functions for memory backend implementations.
@@ -633,9 +655,20 @@ function adaptBackendProtocol(backend) {
633
655
  return result;
634
656
  }
635
657
  };
636
- const routePrefixes = backend.routePrefixes;
637
- if (Array.isArray(routePrefixes)) Object.defineProperty(adapted, "routePrefixes", {
638
- value: routePrefixes,
658
+ const identityMarkers = [["routePrefixes", Array.isArray], ["backendKind", (value) => typeof value === "string"]];
659
+ for (const [key, isValid] of identityMarkers) {
660
+ const value = backend[key];
661
+ if (isValid(value)) Object.defineProperty(adapted, key, {
662
+ value,
663
+ enumerable: true,
664
+ configurable: true
665
+ });
666
+ }
667
+ const realpath = getRealpath(backend);
668
+ if (realpath !== void 0) setRealpath(adapted, realpath);
669
+ const resolveBackendForPath = backend.resolveBackendForPath;
670
+ if (typeof resolveBackendForPath === "function") Object.defineProperty(adapted, "resolveBackendForPath", {
671
+ value: (path) => resolveBackendForPath.call(backend, path),
639
672
  enumerable: true,
640
673
  configurable: true
641
674
  });
@@ -786,8 +819,14 @@ const PREGEL_READ_KEY = "__pregel_read";
786
819
  * for the middleware to apply via Command.
787
820
  */
788
821
  var StateBackend = class {
822
+ /** Class identity discriminant for {@link StateBackend.isInstance}. */
823
+ backendKind = "state";
789
824
  runtime;
790
825
  fileFormat;
826
+ /** Duck-typed so this survives the plain-object copies `adaptBackendProtocol` makes — a class check wouldn't. */
827
+ static isInstance(backend) {
828
+ return typeof backend === "object" && backend !== null && backend.backendKind === "state";
829
+ }
791
830
  constructor(runtimeOrOptions, options) {
792
831
  if (runtimeOrOptions != null && typeof runtimeOrOptions === "object" && "state" in runtimeOrOptions) {
793
832
  this.runtime = runtimeOrOptions;
@@ -1087,6 +1126,466 @@ var StateBackend = class {
1087
1126
  }
1088
1127
  };
1089
1128
  //#endregion
1129
+ //#region src/middleware/blobOffload.ts
1130
+ /**
1131
+ * Content-addressed offload of binary `read_file` and inline `HumanMessage`
1132
+ * blocks to the backend.
1133
+ *
1134
+ * Binary blocks are written to `/blobs/<sha256>` and replaced in state with a
1135
+ * `deepagents_blob` reference. Model requests are rehydrated from the backend
1136
+ * (or a per-run cache), so checkpoints never carry the base64 payload.
1137
+ */
1138
+ /** Content block key holding the SHA-256 digest of an offloaded payload. */
1139
+ const BLOB_REF_KEY = "deepagents_blob";
1140
+ 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.]";
1141
+ const DIGEST_RE = /^[0-9a-f]{64}$/;
1142
+ const BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
1143
+ /** Whether `reference` is a well-formed digest string. */
1144
+ function isDigest(reference) {
1145
+ return typeof reference === "string" && DIGEST_RE.test(reference);
1146
+ }
1147
+ function isValidBase64(value) {
1148
+ return value.length > 0 && value.length % 4 === 0 && BASE64_RE.test(value);
1149
+ }
1150
+ function sha256Hex(bytes) {
1151
+ return createHash("sha256").update(bytes).digest("hex");
1152
+ }
1153
+ const DEFAULT_BLOB_CACHE_BYTES = 268435456;
1154
+ /**
1155
+ * Content-addressed cache of base64 payloads, bounded by total payload size
1156
+ * (LRU-evicted past the bound). Safe to share across threads — every key is
1157
+ * a SHA-256 digest of its own value, so a hit is only reachable by a caller
1158
+ * who already has that exact reference, and an eviction just forces a
1159
+ * re-fetch from the backend, never a wrong result.
1160
+ */
1161
+ var BlobCache = class extends Map {
1162
+ maxBytes;
1163
+ totalBytes = 0;
1164
+ constructor(maxBytes = DEFAULT_BLOB_CACHE_BYTES) {
1165
+ super();
1166
+ this.maxBytes = maxBytes;
1167
+ }
1168
+ /** Returns the cached payload for `digest`, marking it most recently used. */
1169
+ get(digest) {
1170
+ const payload = super.get(digest);
1171
+ if (payload === void 0) return void 0;
1172
+ super.delete(digest);
1173
+ super.set(digest, payload);
1174
+ return payload;
1175
+ }
1176
+ /** Caches `payload`, evicting least-recently-used entries past the size bound. */
1177
+ set(digest, payload) {
1178
+ if (payload.length > this.maxBytes) return this;
1179
+ const previous = super.get(digest);
1180
+ if (previous !== void 0) {
1181
+ this.totalBytes -= previous.length;
1182
+ super.delete(digest);
1183
+ }
1184
+ super.set(digest, payload);
1185
+ this.totalBytes += payload.length;
1186
+ while (this.totalBytes > this.maxBytes) {
1187
+ const oldestDigest = this.keys().next().value;
1188
+ if (oldestDigest === void 0) break;
1189
+ const oldest = super.get(oldestDigest);
1190
+ super.delete(oldestDigest);
1191
+ this.totalBytes -= oldest?.length ?? 0;
1192
+ }
1193
+ return this;
1194
+ }
1195
+ };
1196
+ function blobPath(prefix, digest) {
1197
+ return `${prefix}/${digest}`;
1198
+ }
1199
+ function contentBlocks(message) {
1200
+ const content = message?.content;
1201
+ return Array.isArray(content) ? content : null;
1202
+ }
1203
+ /** Map each inline base64 payload found in `messages` to its digest and decoded bytes. */
1204
+ function pendingBlobs(messages) {
1205
+ const pending = /* @__PURE__ */ new Map();
1206
+ for (const message of messages) {
1207
+ const blocks = contentBlocks(message);
1208
+ if (!blocks) continue;
1209
+ for (const block of blocks) {
1210
+ if (block == null || typeof block !== "object") continue;
1211
+ const payload = block.data;
1212
+ if (typeof payload !== "string" || pending.has(payload) || !isValidBase64(payload)) continue;
1213
+ let raw;
1214
+ try {
1215
+ raw = new Uint8Array(Buffer.from(payload, "base64"));
1216
+ } catch {
1217
+ continue;
1218
+ }
1219
+ pending.set(payload, {
1220
+ digest: sha256Hex(raw),
1221
+ raw
1222
+ });
1223
+ }
1224
+ }
1225
+ return pending;
1226
+ }
1227
+ /** Replace payloads found in `digests` with blob references; returns `messages` unchanged if nothing matched. */
1228
+ function stubMessages(messages, digests) {
1229
+ return messages.map((message) => {
1230
+ const blocks = contentBlocks(message);
1231
+ if (!blocks) return message;
1232
+ let changed = false;
1233
+ const newContent = blocks.map((block) => {
1234
+ const digest = typeof block.data === "string" ? digests.get(block.data) : void 0;
1235
+ if (digest === void 0) return block;
1236
+ changed = true;
1237
+ const { data: _data, ...rest } = block;
1238
+ return {
1239
+ ...rest,
1240
+ [BLOB_REF_KEY]: digest
1241
+ };
1242
+ });
1243
+ if (!changed) return message;
1244
+ return withContent(message, newContent);
1245
+ });
1246
+ }
1247
+ /**
1248
+ * Upload inline binary payloads found in `messages` and return messages carrying blob
1249
+ * references instead. Payloads that fail to upload stay inline — offload is best-effort.
1250
+ */
1251
+ async function offloadMessages(messages, backend, prefix, cache) {
1252
+ const pending = pendingBlobs(messages);
1253
+ if (pending.size === 0) return [...messages];
1254
+ if (typeof backend.uploadFiles !== "function") {
1255
+ console.warn("Backend does not support uploadFiles; binary read_file content stays inline");
1256
+ return [...messages];
1257
+ }
1258
+ const entries = [...pending.entries()];
1259
+ let responses;
1260
+ try {
1261
+ responses = await backend.uploadFiles(entries.map(([, { digest, raw }]) => [blobPath(prefix, digest), raw]));
1262
+ } catch (error) {
1263
+ console.warn("Failed to offload binary read_file content; keeping it inline", error);
1264
+ return [...messages];
1265
+ }
1266
+ const digests = /* @__PURE__ */ new Map();
1267
+ entries.forEach(([payload, { digest }], index) => {
1268
+ if (responses[index]?.error == null) {
1269
+ digests.set(payload, digest);
1270
+ cache.set(digest, payload);
1271
+ }
1272
+ });
1273
+ return stubMessages(messages, digests);
1274
+ }
1275
+ /** Apply `offloadMessages` to a tool result (a `ToolMessage`, or a `Command`-shaped update). */
1276
+ async function offloadToolResult(result, backend, prefix, cache) {
1277
+ if (ToolMessage.isInstance(result)) return (await offloadMessages([result], backend, prefix, cache))[0];
1278
+ const update = result?.update;
1279
+ if (update && Array.isArray(update.messages)) {
1280
+ const messages = await offloadMessages(update.messages, backend, prefix, cache);
1281
+ return new Command({
1282
+ ...result,
1283
+ update: {
1284
+ ...update,
1285
+ messages
1286
+ }
1287
+ });
1288
+ }
1289
+ return result;
1290
+ }
1291
+ /** Return `HumanMessage`s with ids after the last `AIMessage`, preferring a version already queued in `pending`. */
1292
+ function humanCandidates({ stateMessages, pending }) {
1293
+ let lastAiIndex = -1;
1294
+ stateMessages.forEach((message, index) => {
1295
+ if (AIMessage.isInstance(message)) lastAiIndex = index;
1296
+ });
1297
+ const queued = /* @__PURE__ */ new Map();
1298
+ for (const message of pending) {
1299
+ const id = message?.id;
1300
+ if (typeof id === "string") queued.set(id, message);
1301
+ }
1302
+ const candidates = [];
1303
+ for (const message of stateMessages.slice(lastAiIndex + 1)) {
1304
+ if (!HumanMessage.isInstance(message) || typeof message.id !== "string") continue;
1305
+ candidates.push(queued.get(message.id) ?? message);
1306
+ }
1307
+ return candidates;
1308
+ }
1309
+ function mergeReplacements({ pending, offloaded, candidates }) {
1310
+ const replaced = offloaded.filter((next, index) => next !== candidates[index]);
1311
+ const replacedIds = new Set(replaced.map((message) => message?.id));
1312
+ return [...pending.filter((message) => !replacedIds.has(message?.id)), ...replaced];
1313
+ }
1314
+ /** Return `pending` plus stubbed replacements for `HumanMessage`s in state carrying inline media. */
1315
+ async function offloadHumanMessages({ stateMessages, pending }, backend, prefix, cache) {
1316
+ const candidates = humanCandidates({
1317
+ stateMessages,
1318
+ pending
1319
+ });
1320
+ if (candidates.length === 0) return [...pending];
1321
+ return mergeReplacements({
1322
+ pending,
1323
+ offloaded: await offloadMessages(candidates, backend, prefix, cache),
1324
+ candidates
1325
+ });
1326
+ }
1327
+ /** Collect every well-formed `deepagents_blob` digest referenced in `messages`, deduplicated. */
1328
+ function referencedDigests(messages) {
1329
+ const digests = /* @__PURE__ */ new Set();
1330
+ for (const message of messages) {
1331
+ const blocks = contentBlocks(message);
1332
+ if (!blocks) continue;
1333
+ for (const block of blocks) {
1334
+ const ref = block?.[BLOB_REF_KEY];
1335
+ if (isDigest(ref)) digests.add(ref);
1336
+ }
1337
+ }
1338
+ return [...digests];
1339
+ }
1340
+ function cachedPayloads(digests, cache) {
1341
+ const payloads = /* @__PURE__ */ new Map();
1342
+ const missing = [];
1343
+ for (const digest of digests) {
1344
+ const payload = cache.get(digest);
1345
+ if (payload !== void 0) payloads.set(digest, payload);
1346
+ else missing.push(digest);
1347
+ }
1348
+ return {
1349
+ payloads,
1350
+ missing
1351
+ };
1352
+ }
1353
+ /** Verify each downloaded blob's integrity before trusting it — blobs live on an agent-writable backend. */
1354
+ function acceptDownloads(missing, responses, payloads, cache) {
1355
+ missing.forEach((digest, index) => {
1356
+ const response = responses[index];
1357
+ if (!response || response.error != null || response.content == null) return;
1358
+ if (sha256Hex(response.content) !== digest) return;
1359
+ const payload = Buffer.from(response.content).toString("base64");
1360
+ cache.set(digest, payload);
1361
+ payloads.set(digest, payload);
1362
+ });
1363
+ }
1364
+ function messageHasRefs(message) {
1365
+ const blocks = contentBlocks(message);
1366
+ return blocks != null && blocks.some((block) => block != null && typeof block === "object" && "deepagents_blob" in block);
1367
+ }
1368
+ function withContent(message, content) {
1369
+ if (ToolMessage.isInstance(message)) return new ToolMessage({
1370
+ content,
1371
+ tool_call_id: message.tool_call_id,
1372
+ name: message.name,
1373
+ id: message.id,
1374
+ artifact: message.artifact,
1375
+ status: message.status,
1376
+ additional_kwargs: message.additional_kwargs,
1377
+ response_metadata: message.response_metadata,
1378
+ metadata: message.metadata
1379
+ });
1380
+ if (HumanMessage.isInstance(message)) return new HumanMessage({
1381
+ content,
1382
+ id: message.id,
1383
+ additional_kwargs: message.additional_kwargs,
1384
+ response_metadata: message.response_metadata
1385
+ });
1386
+ if (AIMessage.isInstance(message)) return new AIMessage({
1387
+ content,
1388
+ id: message.id,
1389
+ tool_calls: message.tool_calls,
1390
+ invalid_tool_calls: message.invalid_tool_calls,
1391
+ usage_metadata: message.usage_metadata,
1392
+ additional_kwargs: message.additional_kwargs,
1393
+ response_metadata: message.response_metadata
1394
+ });
1395
+ if (SystemMessage.isInstance(message)) return new SystemMessage({
1396
+ content,
1397
+ id: message.id,
1398
+ additional_kwargs: message.additional_kwargs,
1399
+ response_metadata: message.response_metadata
1400
+ });
1401
+ console.warn(`blobOffload: left a blob reference unresolved on an unsupported message type (${message?.constructor?.name})`);
1402
+ return message;
1403
+ }
1404
+ /** Restore base64 payloads for blob references; a reference with no matching payload becomes a text notice. */
1405
+ function restorePayloads(messages, payloads) {
1406
+ const hydrate = (block) => {
1407
+ const ref = block[BLOB_REF_KEY];
1408
+ if (ref === void 0) return block;
1409
+ const payload = isDigest(ref) ? payloads.get(ref) : void 0;
1410
+ if (payload === void 0) return {
1411
+ type: "text",
1412
+ text: MISSING_BLOB_TEXT
1413
+ };
1414
+ const { [BLOB_REF_KEY]: _ref, ...rest } = block;
1415
+ return {
1416
+ ...rest,
1417
+ data: payload
1418
+ };
1419
+ };
1420
+ return messages.map((message) => {
1421
+ if (!messageHasRefs(message)) return message;
1422
+ return withContent(message, (contentBlocks(message) ?? []).map(hydrate));
1423
+ });
1424
+ }
1425
+ /**
1426
+ * Restore base64 payloads for blob references ahead of a model call.
1427
+ *
1428
+ * Only the returned array is used for the request — callers must not persist
1429
+ * the result back into state, or the checkpoint-size benefit of offloading is
1430
+ * lost the next time state is saved.
1431
+ */
1432
+ async function hydrateMessages(messages, backend, prefix, cache) {
1433
+ if (!messages.some(messageHasRefs)) return [...messages];
1434
+ const { payloads, missing } = cachedPayloads(referencedDigests(messages), cache);
1435
+ if (missing.length > 0) if (typeof backend.downloadFiles !== "function") console.warn("Backend does not support downloadFiles; offloaded read_file content is unavailable");
1436
+ else {
1437
+ let responses = [];
1438
+ try {
1439
+ responses = await backend.downloadFiles(missing.map((digest) => blobPath(prefix, digest)));
1440
+ } catch (error) {
1441
+ console.warn("Failed to load offloaded read_file content", error);
1442
+ }
1443
+ acceptDownloads(missing, responses, payloads, cache);
1444
+ }
1445
+ return restorePayloads(messages, payloads);
1446
+ }
1447
+ //#endregion
1448
+ //#region src/middleware/unsupportedContent.ts
1449
+ const PDF_MIME_TYPE = "application/pdf";
1450
+ /** Content block types `read_file` may emit that require multimodal model support. */
1451
+ const MULTIMODAL_BLOCK_TYPES = /* @__PURE__ */ new Set([
1452
+ "image",
1453
+ "audio",
1454
+ "video",
1455
+ "file"
1456
+ ]);
1457
+ const PROFILE_FIELD_BY_BLOCK_TYPE = {
1458
+ image: "imageInputs",
1459
+ audio: "audioInputs",
1460
+ video: "videoInputs"
1461
+ };
1462
+ const TOOL_MESSAGE_FIELD_BY_BLOCK_TYPE = { image: "imageToolMessage" };
1463
+ function hasInlineData(block) {
1464
+ return block.data != null || block.source_type === "base64";
1465
+ }
1466
+ function blockMimeType(block) {
1467
+ const mimeType = block.mimeType ?? block.mime_type;
1468
+ return typeof mimeType === "string" ? mimeType : void 0;
1469
+ }
1470
+ /**
1471
+ * Unlike the other block types, a missing `fileMimeTypes` defaults to
1472
+ * unsupported rather than supported: generic file support varies far more
1473
+ * across models than image/audio/video, so there's no safe assumption to
1474
+ * fall back on absent real profile data.
1475
+ */
1476
+ function fileBlockSupported(block, profile, inToolMessage) {
1477
+ if (!hasInlineData(block)) return true;
1478
+ const mimeType = blockMimeType(block);
1479
+ if (mimeType === PDF_MIME_TYPE) {
1480
+ if (inToolMessage && profile.pdfToolMessage !== void 0) return profile.pdfToolMessage !== false;
1481
+ return profile.pdfInputs !== false;
1482
+ }
1483
+ if (mimeType == null) return false;
1484
+ const { fileMimeTypes } = profile;
1485
+ return Array.isArray(fileMimeTypes) && fileMimeTypes.includes(mimeType);
1486
+ }
1487
+ /**
1488
+ * Whether the model accepts a multimodal block.
1489
+ *
1490
+ * Missing profile fields default to supported, since profile coverage is
1491
+ * incomplete — except `file` blocks, where a missing `fileMimeTypes`
1492
+ * defaults to unsupported (see `fileBlockSupported`).
1493
+ */
1494
+ function multimodalBlockSupported(block, profile, inToolMessage) {
1495
+ if (block.type === "file") return fileBlockSupported(block, profile, inToolMessage);
1496
+ const field = PROFILE_FIELD_BY_BLOCK_TYPE[block.type];
1497
+ if (field == null) return true;
1498
+ if (inToolMessage) {
1499
+ const toolField = TOOL_MESSAGE_FIELD_BY_BLOCK_TYPE[block.type];
1500
+ if (toolField != null && profile[toolField] !== void 0) return profile[toolField] !== false;
1501
+ }
1502
+ return profile[field] !== false;
1503
+ }
1504
+ function placeholder(block, path) {
1505
+ return {
1506
+ type: "text",
1507
+ text: `[read_file: ${path ?? "the requested file"} was not attached because this model does not support ${block.type} content (${blockMimeType(block) ?? "unknown"}).]`
1508
+ };
1509
+ }
1510
+ function readFilePaths(messages) {
1511
+ const paths = /* @__PURE__ */ new Map();
1512
+ for (const message of messages) {
1513
+ if (!AIMessage$1.isInstance(message)) continue;
1514
+ for (const toolCall of message.tool_calls ?? []) {
1515
+ const path = toolCall.args?.file_path;
1516
+ if (toolCall.id != null && typeof path === "string") paths.set(toolCall.id, path);
1517
+ }
1518
+ }
1519
+ return paths;
1520
+ }
1521
+ /**
1522
+ * Without this, a request carrying content the model can't accept (e.g. a
1523
+ * `.zip` file sent to a model without file support) fails with a
1524
+ * non-retryable 400, and since the content stays in history, every later
1525
+ * turn fails the same way. Only the request is changed; messages in state
1526
+ * are left untouched, so switching to a model that accepts the content
1527
+ * sends it again.
1528
+ */
1529
+ function scrubUnsupportedMultimodalContent(messages, model) {
1530
+ const rawProfile = model?.profile;
1531
+ const profile = rawProfile != null && typeof rawProfile === "object" ? rawProfile : {};
1532
+ let paths;
1533
+ let changed = false;
1534
+ const result = messages.map((message) => {
1535
+ const isTool = ToolMessage$1.isInstance(message);
1536
+ if (!isTool && !HumanMessage$1.isInstance(message)) return message;
1537
+ let messageChanged = false;
1538
+ const content = message.contentBlocks.map((block) => {
1539
+ if (block == null || typeof block !== "object" || !MULTIMODAL_BLOCK_TYPES.has(block.type) || multimodalBlockSupported(block, profile, isTool)) return block;
1540
+ messageChanged = true;
1541
+ paths ??= readFilePaths(messages);
1542
+ return placeholder(block, isTool ? paths.get(message.tool_call_id) : void 0);
1543
+ });
1544
+ if (!messageChanged) return message;
1545
+ changed = true;
1546
+ if (isTool) return new ToolMessage$1({
1547
+ content,
1548
+ tool_call_id: message.tool_call_id,
1549
+ name: message.name,
1550
+ id: message.id,
1551
+ artifact: message.artifact,
1552
+ status: message.status,
1553
+ metadata: message.metadata,
1554
+ additional_kwargs: message.additional_kwargs,
1555
+ response_metadata: message.response_metadata
1556
+ });
1557
+ return new HumanMessage$1({
1558
+ content,
1559
+ name: message.name,
1560
+ id: message.id,
1561
+ additional_kwargs: message.additional_kwargs,
1562
+ response_metadata: message.response_metadata
1563
+ });
1564
+ });
1565
+ return changed ? result : messages;
1566
+ }
1567
+ /**
1568
+ * Support is read from `model.profile` (see
1569
+ * https://docs.langchain.com/oss/javascript/langchain/models#model-profiles).
1570
+ *
1571
+ * Place this middleware last in the agent's `middleware` list, so that if
1572
+ * some other middleware changes `request.model`, this one sees the model
1573
+ * that will actually receive the request.
1574
+ */
1575
+ function createUnsupportedContentMiddleware() {
1576
+ return createMiddleware({
1577
+ name: "UnsupportedContentMiddleware",
1578
+ wrapModelCall: async (request, handler) => {
1579
+ const messages = scrubUnsupportedMultimodalContent(request.messages, request.model);
1580
+ if (messages === request.messages) return handler(request);
1581
+ return handler({
1582
+ ...request,
1583
+ messages
1584
+ });
1585
+ }
1586
+ });
1587
+ }
1588
+ //#endregion
1090
1589
  //#region src/permissions/enforce.ts
1091
1590
  /**
1092
1591
  * Validate permission rule paths at setup time. Throws if any path is
@@ -1103,11 +1602,13 @@ function validatePermissionPaths(permissions) {
1103
1602
  * - Non-absolute paths (must start with `/`)
1104
1603
  * - Paths containing `..`
1105
1604
  * - Paths containing `~`
1605
+ *
1606
+ * Empty and `.` segments are dropped (`/a/./b` and `/a//b` become `/a/b`).
1106
1607
  */
1107
1608
  function validatePath(raw) {
1108
1609
  if (typeof raw !== "string" || raw.length === 0) throw new Error("path must be a non-empty string");
1109
1610
  if (!raw.startsWith("/")) throw new Error(`path must be absolute: ${JSON.stringify(raw)}`);
1110
- const segments = raw.split("/").filter((s) => s.length > 0);
1611
+ const segments = raw.split("/").filter((s) => s.length > 0 && s !== ".");
1111
1612
  if (segments.includes("..")) throw new Error(`path must not contain "..": ${JSON.stringify(raw)}`);
1112
1613
  if (segments.includes("~")) throw new Error(`path must not contain "~": ${JSON.stringify(raw)}`);
1113
1614
  return `/${segments.join("/")}`;
@@ -1157,14 +1658,15 @@ var CompositeBackend = class {
1157
1658
  sortedRoutes;
1158
1659
  constructor(defaultBackend, routes) {
1159
1660
  this.default = isSandboxProtocol(defaultBackend) ? adaptSandboxProtocol(defaultBackend) : adaptBackendProtocol(defaultBackend);
1160
- this.routes = Object.fromEntries(Object.entries(routes).map(([k, v]) => [k, isSandboxProtocol(v) ? adaptSandboxProtocol(v) : adaptBackendProtocol(v)]));
1661
+ this.routes = Object.fromEntries(Object.entries(routes).map(([k, v]) => [k.endsWith("/") ? k : `${k}/`, isSandboxProtocol(v) ? adaptSandboxProtocol(v) : adaptBackendProtocol(v)]));
1161
1662
  this.sortedRoutes = Object.entries(this.routes).sort((a, b) => b[0].length - a[0].length);
1663
+ setRealpath(this, (path) => this.resolveRealPath(path));
1162
1664
  }
1163
1665
  /** Delegates to default backend's id if it is a sandbox, otherwise empty string. */
1164
1666
  get id() {
1165
1667
  return isSandboxBackend(this.default) ? this.default.id : "";
1166
1668
  }
1167
- /** Route prefixes registered on this backend (e.g. `["/workspace"]`). */
1669
+ /** Route prefixes registered on this backend, normalized to a trailing slash (e.g. `["/workspace/"]`). */
1168
1670
  get routePrefixes() {
1169
1671
  return Object.keys(this.routes);
1170
1672
  }
@@ -1178,6 +1680,28 @@ var CompositeBackend = class {
1178
1680
  return typeof backend === "object" && backend !== null && Array.isArray(backend.routePrefixes);
1179
1681
  }
1180
1682
  /**
1683
+ * Returns the backend for `path`, with the matched prefix stripped (`/foo` → `/`).
1684
+ * Routes match on path-segment boundaries: `/foo` matches `/foo/bar` but not `/foobar`.
1685
+ */
1686
+ resolveBackendForPath(path) {
1687
+ return this.getBackendAndKey(path);
1688
+ }
1689
+ /**
1690
+ * Resolve symlinks via the backend that owns `path`, re-adding the route
1691
+ * prefix. Returns `path` unchanged when that backend has no realpath hook.
1692
+ */
1693
+ async resolveRealPath(path) {
1694
+ for (const [routePrefix, backend] of this.sortedRoutes) if (this.isPathWithinRoute(path, routePrefix)) {
1695
+ const realpath = getRealpath(backend);
1696
+ if (realpath === void 0) return path;
1697
+ const suffix = path.substring(routePrefix.length);
1698
+ const resolved = await realpath(suffix ? "/" + suffix : "/");
1699
+ return routePrefix.slice(0, -1) + resolved;
1700
+ }
1701
+ const realpath = getRealpath(this.default);
1702
+ return realpath === void 0 ? path : await realpath(path);
1703
+ }
1704
+ /**
1181
1705
  * Determine which backend handles this key and strip prefix.
1182
1706
  *
1183
1707
  * @param key - Original file path
@@ -1781,6 +2305,8 @@ const GREP_TRUNCATION_NOTE = "Note: the search stopped early because it hit the
1781
2305
  * Set to null to disable the cap.
1782
2306
  */
1783
2307
  const DEFAULT_GREP_MAX_COUNT = 1e3;
2308
+ /** Path prefix offloaded binary content blobs are written under. */
2309
+ const BLOBS_PREFIX = "/blobs";
1784
2310
  /**
1785
2311
  * Message template for evicted tool results.
1786
2312
  */
@@ -1942,25 +2468,42 @@ function getErrorMessage$1(error) {
1942
2468
  return String(error);
1943
2469
  }
1944
2470
  /**
1945
- * Check whether `path` is permitted under `rules` for `operation`, returning an
1946
- * error string to surface to the model (or `undefined` when allowed).
2471
+ * Resolve `canonical` through the backend's realpath hook, when it has one, and
2472
+ * re-validate the result so it is canonical for rule matching. Returns
2473
+ * `canonical` unchanged for backends without path aliasing.
2474
+ */
2475
+ async function resolveRealPath(backend, canonical) {
2476
+ const realpath = getRealpath(backend);
2477
+ return realpath ? validatePath(await realpath(canonical)) : canonical;
2478
+ }
2479
+ /**
2480
+ * Check whether `path` is permitted under `rules` for `operation`.
2481
+ *
2482
+ * Returns the canonical path to hand the backend, or an error string to
2483
+ * surface to the model. The decision covers every form of the target: the
2484
+ * lexical canonical path (dot and empty segments dropped) and, when the
2485
+ * backend has a realpath hook, the symlink-resolved path it would actually
2486
+ * touch. A deny on either blocks the call.
1947
2487
  *
1948
- * Never throws: an invalid path (non-absolute, or containing `..` or `~`) or a
1949
- * denied path is a recoverable tool error, not a fatal run-ending one. Such
1950
- * paths are rejected, never normalized, so they cannot bypass a deny rule or
1951
- * reach the backend.
2488
+ * Never throws: an invalid path (non-absolute, or containing `..` or `~`), one
2489
+ * the backend cannot resolve, or a denied one is a recoverable tool error, not
2490
+ * a fatal run-ending one. With no rules configured, `path` is returned as-is.
1952
2491
  *
1953
2492
  * @internal
1954
2493
  */
1955
- function checkPermission(rules, operation, path) {
1956
- if (rules.length === 0) return;
2494
+ async function checkPermission(backend, rules, operation, path) {
2495
+ if (rules.length === 0) return { path };
1957
2496
  let canonical;
2497
+ let real;
1958
2498
  try {
1959
2499
  canonical = validatePath(path);
2500
+ if (decidePathAccess(rules, operation, canonical) === "deny") return { error: `Error: permission denied for ${operation} on ${canonical}` };
2501
+ real = await resolveRealPath(backend, canonical);
1960
2502
  } catch (error) {
1961
- return `Error: ${getErrorMessage$1(error)}`;
2503
+ return { error: `Error: ${getErrorMessage$1(error)}` };
1962
2504
  }
1963
- if (decidePathAccess(rules, operation, canonical) === "deny") return `Error: permission denied for ${operation} on ${canonical}`;
2505
+ if (real !== canonical && decidePathAccess(rules, operation, real) === "deny") return { error: `Error: permission denied for ${operation} on ${canonical} (resolves to ${real})` };
2506
+ return { path: canonical };
1964
2507
  }
1965
2508
  /**
1966
2509
  * Build an error {@link ToolMessage} for a rejected or denied path. Returning a
@@ -2133,27 +2676,56 @@ function parentPath(path) {
2133
2676
  if (parts.length <= 1) return "/";
2134
2677
  return `/${parts.slice(0, -1).join("/")}`;
2135
2678
  }
2679
+ /**
2680
+ * Resolve the path a delete of `target` would actually remove. Backends unlink
2681
+ * a symlink leaf rather than following it, so only the parent is resolved.
2682
+ */
2683
+ async function resolveDeleteRealPath(backend, target) {
2684
+ const parts = posixParts(target);
2685
+ if (parts.length === 0) return resolveRealPath(backend, target);
2686
+ return validatePath(`${await resolveRealPath(backend, parentPath(target))}/${parts[parts.length - 1]}`);
2687
+ }
2136
2688
  function supportsDelete(backend) {
2137
2689
  return typeof backend.delete === "function";
2138
2690
  }
2139
2691
  /**
2140
2692
  * Filter a list of filesystem entries to those the rules permit.
2141
2693
  *
2142
- * `getPath` extracts the absolute path from each entry. Entries with
2143
- * unparsable paths are included (not silently dropped). Returns the
2144
- * original array unchanged when `rules` is empty.
2694
+ * `getPath` extracts the absolute path from each entry. Each entry is checked
2695
+ * on its canonical path and, when the backend has a realpath hook, on its
2696
+ * symlink-resolved path. Entries with unparsable paths are included; entries
2697
+ * whose path cannot be resolved are dropped.
2698
+ * Returns the original array unchanged when `rules` is empty.
2145
2699
  *
2146
2700
  * @internal
2147
2701
  */
2148
- function filterByPermissions(entries, rules, operation, getPath) {
2702
+ async function filterByPermissions(backend, entries, rules, operation, getPath) {
2149
2703
  if (rules.length === 0) return entries;
2150
- return entries.filter((entry) => {
2704
+ const decisions = /* @__PURE__ */ new Map();
2705
+ const isPermitted = async (rawPath) => {
2706
+ let canonical;
2151
2707
  try {
2152
- return decidePathAccess(rules, operation, validatePath(getPath(entry))) !== "deny";
2708
+ canonical = validatePath(rawPath);
2153
2709
  } catch {
2154
2710
  return true;
2155
2711
  }
2156
- });
2712
+ if (decidePathAccess(rules, operation, canonical) === "deny") return false;
2713
+ try {
2714
+ return decidePathAccess(rules, operation, await resolveRealPath(backend, canonical)) !== "deny";
2715
+ } catch {
2716
+ return false;
2717
+ }
2718
+ };
2719
+ const permitted = await Promise.all(entries.map((entry) => {
2720
+ const rawPath = getPath(entry);
2721
+ let decision = decisions.get(rawPath);
2722
+ if (decision === void 0) {
2723
+ decision = isPermitted(rawPath);
2724
+ decisions.set(rawPath, decision);
2725
+ }
2726
+ return decision;
2727
+ }));
2728
+ return entries.filter((_, index) => permitted[index]);
2157
2729
  }
2158
2730
  const LS_TOOL_DESCRIPTION = context`
2159
2731
  Lists all files in a directory.
@@ -2241,13 +2813,13 @@ function getExecuteToolDescription(hasGrep, hasGlob) {
2241
2813
  function createLsTool(backend, options) {
2242
2814
  const { customDescription, permissions } = options;
2243
2815
  return tool(async (input, runtime) => {
2244
- const permissionError = checkPermission(permissions, "read", input.path ?? "/");
2245
- if (permissionError !== void 0) return toolError(runtime, "ls", permissionError);
2246
2816
  const resolvedBackend = await resolveBackend(backend, runtime);
2247
2817
  const path = input.path || "/";
2248
- const lsResult = await resolvedBackend.ls(path);
2818
+ const permission = await checkPermission(resolvedBackend, permissions, "read", path);
2819
+ if (permission.error !== void 0) return toolError(runtime, "ls", permission.error);
2820
+ const lsResult = await resolvedBackend.ls(permission.path);
2249
2821
  if (lsResult.error) return `Error listing files: ${lsResult.error}`;
2250
- const infos = filterByPermissions(lsResult.files ?? [], permissions, "read", (info) => info.path);
2822
+ const infos = await filterByPermissions(resolvedBackend, lsResult.files ?? [], permissions, "read", (info) => info.path);
2251
2823
  if (infos.length === 0) return `No files found in ${path}`;
2252
2824
  const lines = [];
2253
2825
  for (const info of infos) if (info.is_dir) lines.push(`${info.path} (directory)`);
@@ -2270,23 +2842,17 @@ function createLsTool(backend, options) {
2270
2842
  function createReadFileTool(backend, options) {
2271
2843
  const { customDescription, toolTokenLimitBeforeEvict, permissions } = options;
2272
2844
  return tool(async (input, runtime) => {
2273
- const permissionError = checkPermission(permissions, "read", input.file_path);
2274
- if (permissionError !== void 0) return toolError(runtime, "read_file", permissionError);
2275
2845
  const resolvedBackend = await resolveBackend(backend, runtime);
2846
+ const permission = await checkPermission(resolvedBackend, permissions, "read", input.file_path);
2847
+ if (permission.error !== void 0) return toolError(runtime, "read_file", permission.error);
2276
2848
  const { file_path, offset: requestedOffset = 0, limit: requestedLimit = 100 } = input;
2277
2849
  const { offset, limit } = normalizeReadPagination(requestedOffset, requestedLimit);
2278
- const readResult = await resolvedBackend.read(file_path, offset, limit);
2279
- if (readResult.error) return [{
2280
- type: "text",
2281
- text: `Error: ${readResult.error}`
2282
- }];
2850
+ const readResult = await resolvedBackend.read(permission.path, offset, limit);
2851
+ if (readResult.error) return toolError(runtime, "read_file", `Error: ${readResult.error}`);
2283
2852
  const mimeType = readResult.mimeType ?? getMimeType(file_path);
2284
2853
  if (!isTextMimeType(mimeType)) {
2285
2854
  const binaryContent = readResult.content;
2286
- if (!binaryContent) return [{
2287
- type: "text",
2288
- text: `Error: expected binary content for '${file_path}'`
2289
- }];
2855
+ if (!binaryContent) return toolError(runtime, "read_file", `Error: expected binary content for '${file_path}'`);
2290
2856
  let base64Data;
2291
2857
  if (typeof binaryContent === "string") base64Data = binaryContent;
2292
2858
  else if (ArrayBuffer.isView(binaryContent)) base64Data = Buffer.from(binaryContent).toString("base64");
@@ -2295,10 +2861,7 @@ function createReadFileTool(backend, options) {
2295
2861
  base64Data = Buffer.from(new Uint8Array(values)).toString("base64");
2296
2862
  }
2297
2863
  const sizeBytes = Math.ceil(base64Data.length * 3 / 4);
2298
- if (sizeBytes > 10485760) return [{
2299
- type: "text",
2300
- text: `Error: file too large to read (${Math.round(sizeBytes / 1048576)}MB exceeds ${MAX_BINARY_READ_SIZE_BYTES / 1048576}MB limit for binary files)`
2301
- }];
2864
+ 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)`);
2302
2865
  if (mimeType.startsWith("image/")) return [{
2303
2866
  type: "image",
2304
2867
  mimeType,
@@ -2364,11 +2927,11 @@ function createReadFileTool(backend, options) {
2364
2927
  function createWriteFileTool(backend, options) {
2365
2928
  const { customDescription, permissions } = options;
2366
2929
  return tool(async (input, runtime) => {
2367
- const permissionError = checkPermission(permissions, "write", input.file_path);
2368
- if (permissionError !== void 0) return toolError(runtime, "write_file", permissionError);
2369
2930
  const resolvedBackend = await resolveBackend(backend, runtime);
2931
+ const permission = await checkPermission(resolvedBackend, permissions, "write", input.file_path);
2932
+ if (permission.error !== void 0) return toolError(runtime, "write_file", permission.error);
2370
2933
  const { file_path, content } = input;
2371
- const result = await resolvedBackend.write(file_path, content);
2934
+ const result = await resolvedBackend.write(permission.path, content);
2372
2935
  if (result.error) return result.error;
2373
2936
  const message = new ToolMessage({
2374
2937
  content: `Successfully wrote to '${file_path}'`,
@@ -2396,11 +2959,11 @@ function createWriteFileTool(backend, options) {
2396
2959
  function createEditFileTool(backend, options) {
2397
2960
  const { customDescription, permissions } = options;
2398
2961
  return tool(async (input, runtime) => {
2399
- const permissionError = checkPermission(permissions, "write", input.file_path);
2400
- if (permissionError !== void 0) return toolError(runtime, "edit_file", permissionError);
2401
2962
  const resolvedBackend = await resolveBackend(backend, runtime);
2963
+ const permission = await checkPermission(resolvedBackend, permissions, "write", input.file_path);
2964
+ if (permission.error !== void 0) return toolError(runtime, "edit_file", permission.error);
2402
2965
  const { file_path, old_string, new_string, replace_all = false } = input;
2403
- const result = await resolvedBackend.edit(file_path, old_string, new_string, replace_all);
2966
+ const result = await resolvedBackend.edit(permission.path, old_string, new_string, replace_all);
2404
2967
  if (result.error) return result.error;
2405
2968
  const message = new ToolMessage({
2406
2969
  content: `Successfully replaced ${result.occurrences} occurrence(s) in '${file_path}'`,
@@ -2440,6 +3003,17 @@ function createDeleteTool(backend, options) {
2440
3003
  const hasDescendants = await deleteTargetMayHaveDescendants(resolvedBackend, validatedPath, permissions.length > 0);
2441
3004
  const denyingPatterns = findDeleteDenyPatterns(permissions, validatedPath, hasDescendants);
2442
3005
  if (denyingPatterns.length > 0) return toolError(runtime, "delete", `Error: permission denied for write on ${validatedPath} (matches deny rule(s): ${denyingPatterns.join(", ")})`);
3006
+ if (permissions.length > 0) {
3007
+ let realTarget;
3008
+ try {
3009
+ realTarget = await resolveDeleteRealPath(resolvedBackend, validatedPath);
3010
+ } catch (error) {
3011
+ return toolError(runtime, "delete", `Error: ${getErrorMessage$1(error)}`);
3012
+ }
3013
+ if (realTarget !== validatedPath) {
3014
+ if (findDeleteDenyPatterns(permissions, realTarget, hasDescendants).length > 0) return toolError(runtime, "delete", `Error: permission denied for write on ${validatedPath} (resolves to ${realTarget})`);
3015
+ }
3016
+ }
2443
3017
  if (!supportsDelete(resolvedBackend)) return toolError(runtime, "delete", `Error: deletion is not available for '${validatedPath}'.`);
2444
3018
  const result = await resolvedBackend.delete(validatedPath);
2445
3019
  if (result.error) return toolError(runtime, "delete", result.error);
@@ -2466,13 +3040,13 @@ function createDeleteTool(backend, options) {
2466
3040
  function createGlobTool(backend, options) {
2467
3041
  const { customDescription, permissions } = options;
2468
3042
  return tool(async (input, runtime) => {
2469
- const permissionError = checkPermission(permissions, "read", input.path ?? "/");
2470
- if (permissionError !== void 0) return toolError(runtime, "glob", permissionError);
2471
3043
  const resolvedBackend = await resolveBackend(backend, runtime);
2472
- const { pattern, path } = input;
2473
- const globResult = await resolvedBackend.glob(pattern, path);
3044
+ const { pattern } = input;
3045
+ const permission = await checkPermission(resolvedBackend, permissions, "read", input.path ?? "/");
3046
+ if (permission.error !== void 0) return toolError(runtime, "glob", permission.error);
3047
+ const globResult = await resolvedBackend.glob(pattern, input.path === void 0 ? void 0 : permission.path);
2474
3048
  if (globResult.error) return `Error finding files: ${globResult.error}`;
2475
- const infos = filterByPermissions(globResult.files ?? [], permissions, "read", (info) => info.path);
3049
+ const infos = await filterByPermissions(resolvedBackend, globResult.files ?? [], permissions, "read", (info) => info.path);
2476
3050
  if (infos.length === 0) return `No files found matching pattern '${pattern}'`;
2477
3051
  const result = truncateIfTooLong(infos.map((info) => info.path));
2478
3052
  if (Array.isArray(result)) return result.join("\n");
@@ -2492,14 +3066,14 @@ function createGlobTool(backend, options) {
2492
3066
  function createGrepTool(backend, options) {
2493
3067
  const { customDescription, permissions, includeExecution, grepMaxCount } = options;
2494
3068
  return tool(async (input, runtime) => {
2495
- const permissionError = checkPermission(permissions, "read", input.path ?? "/");
2496
- if (permissionError !== void 0) return toolError(runtime, "grep", permissionError);
2497
3069
  const resolvedBackend = await resolveBackend(backend, runtime);
2498
3070
  const { pattern, path = "/", glob = null, output_mode = "content" } = input;
3071
+ const permission = await checkPermission(resolvedBackend, permissions, "read", path);
3072
+ if (permission.error !== void 0) return toolError(runtime, "grep", permission.error);
2499
3073
  const maxCount = input.max_count ?? grepMaxCount;
2500
- const result = await resolvedBackend.grep(pattern, path, glob, maxCount);
3074
+ const result = await resolvedBackend.grep(pattern, permission.path, glob, maxCount);
2501
3075
  if (result.error) return result.error;
2502
- const matches = filterByPermissions(result.matches ?? [], permissions, "read", (m) => m.path);
3076
+ const matches = await filterByPermissions(resolvedBackend, result.matches ?? [], permissions, "read", (m) => m.path);
2503
3077
  if (matches.length === 0) return `No matches found for pattern '${pattern}'`;
2504
3078
  const truncated = truncateIfTooLong(formatGrepMatches(matches, output_mode));
2505
3079
  let content = typeof truncated === "string" ? truncated : truncated.join("\n");
@@ -2563,6 +3137,68 @@ function allPathsScopedToRoutes(permissions, backend) {
2563
3137
  return path === normalizedRoute.slice(0, -1) || path.startsWith(normalizedRoute);
2564
3138
  })));
2565
3139
  }
3140
+ /** Whether `path` resolves to a `StateBackend`, recursing through composite routing. */
3141
+ function routesToStateBackend(backend, path) {
3142
+ if (CompositeBackend.isInstance(backend)) {
3143
+ const [routed, routedPath] = backend.resolveBackendForPath(path);
3144
+ return routesToStateBackend(routed, routedPath);
3145
+ }
3146
+ return StateBackend.isInstance(backend);
3147
+ }
3148
+ /**
3149
+ * Whether `response` is the `{ structuredResponse, messages }` shape a
3150
+ * provider-strategy structured-output call can return instead of a plain
3151
+ * `AIMessage`. The agent framework only recognizes this shape at the
3152
+ * outermost `wrapModelCall` return value — returning a `Command` instead
3153
+ * discards `structuredResponse` with no way to carry it through the Command.
3154
+ */
3155
+ function hasStructuredResponse(response) {
3156
+ return typeof response === "object" && response !== null && "structuredResponse" in response && "messages" in response;
3157
+ }
3158
+ const REJECTED_FILE_CONTENT_TEXT = "Unsupported content. The file may be invalid, too large, or of an unsupported mime-type.";
3159
+ /**
3160
+ * No LangChain error class captures "the provider rejected this request"
3161
+ * generically, so this checks the raw HTTP status providers' client SDKs set
3162
+ * on their errors instead. `ContextOverflowError` is excluded because it's a
3163
+ * different, already-handled problem, and retrying it with stripped file
3164
+ * content wouldn't fix an oversized prompt.
3165
+ */
3166
+ function isRejectedRequestError(error) {
3167
+ if (ContextOverflowError.isInstance(error)) return false;
3168
+ return typeof error === "object" && error !== null && "status" in error && error.status === 400;
3169
+ }
3170
+ /**
3171
+ * Only messages carrying multimodal blocks after the last `AIMessage` are
3172
+ * candidates for replacement, since anything earlier was already accepted by
3173
+ * the model in a prior turn. Returns `messages` unchanged if nothing
3174
+ * matched, so the caller can tell there was nothing to blame for the
3175
+ * rejection.
3176
+ */
3177
+ function replaceRejectedFileContent(messages) {
3178
+ let lastResponseIndex = -1;
3179
+ for (let i = messages.length - 1; i >= 0; i--) if (AIMessage.isInstance(messages[i])) {
3180
+ lastResponseIndex = i;
3181
+ break;
3182
+ }
3183
+ let changed = false;
3184
+ const result = messages.map((message, index) => {
3185
+ if (index <= lastResponseIndex || !ToolMessage.isInstance(message) || message.name !== "read_file") return message;
3186
+ if (!message.contentBlocks.some((block) => MULTIMODAL_BLOCK_TYPES.has(block.type))) return message;
3187
+ changed = true;
3188
+ return new ToolMessage({
3189
+ content: REJECTED_FILE_CONTENT_TEXT,
3190
+ tool_call_id: message.tool_call_id,
3191
+ name: message.name,
3192
+ id: message.id,
3193
+ artifact: message.artifact,
3194
+ status: message.status,
3195
+ metadata: message.metadata,
3196
+ additional_kwargs: message.additional_kwargs,
3197
+ response_metadata: message.response_metadata
3198
+ });
3199
+ });
3200
+ return changed ? result : messages;
3201
+ }
2566
3202
  /**
2567
3203
  * Create middleware that provides built-in filesystem tools and optional custom
2568
3204
  * prompt guidance.
@@ -2589,7 +3225,10 @@ function allPathsScopedToRoutes(permissions, backend) {
2589
3225
  * ```
2590
3226
  */
2591
3227
  function createFilesystemMiddleware(options = {}) {
2592
- const { backend = (runtime) => new StateBackend(runtime), systemPrompt: customSystemPrompt = null, customToolDescriptions = null, toolTokenLimitBeforeEvict = 2e4, humanMessageTokenLimitBeforeEvict = 5e4, permissions = [], tools: filesystemTools = null, grepMaxCount = DEFAULT_GREP_MAX_COUNT } = options;
3228
+ 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;
3229
+ const backendIsStateBackend = StateBackend.isInstance(backend);
3230
+ if (offloadBinaryContent && backendIsStateBackend) console.warn("offloadBinaryContent has no effect: the backend is a StateBackend, which keeps files as part of checkpointed state.");
3231
+ const blobCache = offloadBinaryContent && !backendIsStateBackend ? new BlobCache() : null;
2593
3232
  const enabledFilesystemTools = normalizeFilesystemTools(filesystemTools);
2594
3233
  const executeToolEnabled = enabledFilesystemTools == null || enabledFilesystemTools.has("execute");
2595
3234
  if (permissions.length > 0) validatePermissionPaths(permissions);
@@ -2726,18 +3365,59 @@ function createFilesystemMiddleware(options = {}) {
2726
3365
  return msg;
2727
3366
  });
2728
3367
  }
2729
- return handler({
3368
+ let offloadedHuman = [];
3369
+ if (blobCache && !routesToStateBackend(resolvedBackend, `${BLOBS_PREFIX}/`)) {
3370
+ offloadedHuman = await offloadHumanMessages({
3371
+ stateMessages: request.state.messages ?? [],
3372
+ pending: []
3373
+ }, resolvedBackend, BLOBS_PREFIX, blobCache);
3374
+ messages = await hydrateMessages(messages, resolvedBackend, BLOBS_PREFIX, blobCache);
3375
+ }
3376
+ const finalRequest = {
2730
3377
  ...request,
2731
3378
  tools,
2732
3379
  messages,
2733
3380
  systemMessage: newSystemMessage
2734
- });
3381
+ };
3382
+ const foldOffload = (response, extra = []) => {
3383
+ if (offloadedHuman.length === 0 && extra.length === 0) return response;
3384
+ if (hasStructuredResponse(response)) return {
3385
+ ...response,
3386
+ messages: [
3387
+ ...extra,
3388
+ ...offloadedHuman,
3389
+ ...response.messages
3390
+ ]
3391
+ };
3392
+ return new Command({ update: { messages: extra.length === 0 ? offloadedHuman : [
3393
+ ...extra,
3394
+ ...offloadedHuman,
3395
+ response
3396
+ ] } });
3397
+ };
3398
+ try {
3399
+ return foldOffload(await handler(finalRequest));
3400
+ } catch (error) {
3401
+ if (!isRejectedRequestError(error)) throw error;
3402
+ const replaced = replaceRejectedFileContent(finalRequest.messages);
3403
+ if (replaced === finalRequest.messages) throw error;
3404
+ return foldOffload(await handler({
3405
+ ...finalRequest,
3406
+ messages: replaced
3407
+ }), replaced.filter((msg, i) => msg !== finalRequest.messages[i]));
3408
+ }
2735
3409
  },
2736
3410
  wrapToolCall: async (request, handler) => {
2737
- if (!toolTokenLimitBeforeEvict) return handler(request);
2738
3411
  const toolName = request.toolCall?.name;
2739
- if (toolName && TOOLS_EXCLUDED_FROM_EVICTION.includes(toolName)) return handler(request);
2740
- const result = await handler(request);
3412
+ let result = await handler(request);
3413
+ if (blobCache && toolName === "read_file") {
3414
+ const resolvedBackend = await resolveBackend(backend, {
3415
+ ...request.runtime,
3416
+ state: request.state
3417
+ });
3418
+ if (!routesToStateBackend(resolvedBackend, `${BLOBS_PREFIX}/`)) result = await offloadToolResult(result, resolvedBackend, BLOBS_PREFIX, /* @__PURE__ */ new Map());
3419
+ }
3420
+ if (!toolTokenLimitBeforeEvict || toolName && TOOLS_EXCLUDED_FROM_EVICTION.includes(toolName)) return result;
2741
3421
  if (ToolMessage.isInstance(result)) {
2742
3422
  const processed = await processToolMessage(result, request.runtime, request.state, request.toolCall?.id);
2743
3423
  if (processed.filesUpdate) return new Command({ update: {
@@ -3585,10 +4265,12 @@ const EXCLUDED_STATE_KEYS = [
3585
4265
  "todos",
3586
4266
  "structuredResponse",
3587
4267
  "skillsMetadata",
4268
+ "pinnedSkills",
3588
4269
  "memoryContents",
3589
4270
  ...CALL_COUNT_STATE_KEYS,
3590
4271
  "_summarizationEvent",
3591
4272
  "_summarizationSessionId",
4273
+ "_skillToolsDisclosed",
3592
4274
  FORKED_CONTEXT_KEY
3593
4275
  ];
3594
4276
  /**
@@ -3607,9 +4289,11 @@ const EXCLUDED_STATE_KEYS = [
3607
4289
  */
3608
4290
  const FORK_EXCLUDED_STATE_KEYS = [
3609
4291
  "structuredResponse",
4292
+ "pinnedSkills",
3610
4293
  ...CALL_COUNT_STATE_KEYS,
3611
4294
  "_summarizationEvent",
3612
- "_summarizationSessionId"
4295
+ "_summarizationSessionId",
4296
+ "_skillToolsDisclosed"
3613
4297
  ];
3614
4298
  /**
3615
4299
  * Default description for the general-purpose subagent.
@@ -3781,6 +4465,7 @@ function createSubAgent(spec, options) {
3781
4465
  if (!spec.tools) throw new Error(`SubAgent '${spec.name}' must specify 'tools'`);
3782
4466
  const middleware = [...spec.middleware ?? []];
3783
4467
  if (spec.interruptOn) middleware.push(humanInTheLoopMiddleware({ interruptOn: spec.interruptOn }));
4468
+ if (!middleware.some((m) => m.name === "UnsupportedContentMiddleware")) middleware.push(createUnsupportedContentMiddleware());
3784
4469
  const selectedResponseFormat = options?.responseFormat ?? spec.responseFormat;
3785
4470
  return createAgent({
3786
4471
  model: spec.model,
@@ -4051,7 +4736,8 @@ function patchDanglingToolCalls(messages) {
4051
4736
  patchedMessages.push(new ToolMessage({
4052
4737
  content: toolMsg,
4053
4738
  name: toolCall.name,
4054
- tool_call_id: toolCall.id
4739
+ tool_call_id: toolCall.id,
4740
+ status: "error"
4055
4741
  }));
4056
4742
  }
4057
4743
  }
@@ -4493,6 +5179,46 @@ function createMemoryMiddleware(options) {
4493
5179
  });
4494
5180
  }
4495
5181
  //#endregion
5182
+ //#region src/errors.ts
5183
+ const CONFIGURATION_ERROR_SYMBOL = Symbol.for("deepagents.configuration_error");
5184
+ /**
5185
+ * Thrown when `createDeepAgent` receives invalid configuration.
5186
+ *
5187
+ * Follows the same pattern as {@link SandboxError}: a human-readable
5188
+ * `message`, a structured `code` for programmatic handling, and a
5189
+ * static `isInstance` guard that works across realms.
5190
+ *
5191
+ * @example
5192
+ * ```typescript
5193
+ * try {
5194
+ * createDeepAgent({ tools: [myTool] });
5195
+ * } catch (error) {
5196
+ * if (ConfigurationError.isInstance(error)) {
5197
+ * switch (error.code) {
5198
+ * case "TOOL_NAME_COLLISION":
5199
+ * console.error("Rename your tool:", error.message);
5200
+ * break;
5201
+ * }
5202
+ * }
5203
+ * }
5204
+ * ```
5205
+ */
5206
+ var ConfigurationError = class ConfigurationError extends Error {
5207
+ code;
5208
+ cause;
5209
+ [CONFIGURATION_ERROR_SYMBOL] = true;
5210
+ name = "ConfigurationError";
5211
+ constructor(message, code, cause) {
5212
+ super(message);
5213
+ this.code = code;
5214
+ this.cause = cause;
5215
+ Object.setPrototypeOf(this, ConfigurationError.prototype);
5216
+ }
5217
+ static isInstance(error) {
5218
+ return typeof error === "object" && error !== null && error[CONFIGURATION_ERROR_SYMBOL] === true;
5219
+ }
5220
+ };
5221
+ //#endregion
4496
5222
  //#region src/middleware/skills.ts
4497
5223
  /**
4498
5224
  * Backend-agnostic skills middleware for loading agent skills from any backend.
@@ -4552,6 +5278,10 @@ const SKILL_MODULE_EXTENSIONS = [
4552
5278
  ".jsx",
4553
5279
  ".tsx"
4554
5280
  ];
5281
+ /** `SKILL.md` frontmatter `metadata` key listing the skill's include names, space-separated. */
5282
+ const INCLUDE_TOOLS_KEY = "include_tools";
5283
+ /** YAML frontmatter between `---` delimiters at the start of a `SKILL.md`. */
5284
+ const FRONTMATTER_PATTERN = /^---\s*\n([\s\S]*?)\n---\s*\n/;
4555
5285
  /**
4556
5286
  * Zod schema for a single skill metadata entry.
4557
5287
  */
@@ -4593,11 +5323,52 @@ const SkillMetadataEntrySchema = z$1.object({
4593
5323
  */
4594
5324
  const skillsMetadataValue = z$1.array(SkillMetadataEntrySchema).nullish();
4595
5325
  /**
5326
+ * State value for a middleware's `pinnedSkills` field: the names of skills to
5327
+ * pin.
5328
+ *
5329
+ * The next time the skills middleware's `beforeModel` runs, before a model
5330
+ * call, each named skill's `SKILL.md`, without its frontmatter, is appended to
5331
+ * the conversation as its own `HumanMessage`, in the order named. The message
5332
+ * is a snapshot: editing the file later doesn't change it, and naming the
5333
+ * skill again appends a new copy.
5334
+ *
5335
+ * Names only, matched exactly against the loaded skills. A name that isn't
5336
+ * loaded, or whose `SKILL.md` can't be read, is skipped without an error. The
5337
+ * key is cleared once consumed, so a later run pins nothing unless it names
5338
+ * skills again. Writes in the same step add up, so parallel tool calls can
5339
+ * each pin skills by returning a `Command`.
5340
+ *
5341
+ * Like {@link skillsMetadataValue}, treat the value as opaque. A middleware
5342
+ * declares it on its state schema to set `pinnedSkills` from its own hooks.
5343
+ *
5344
+ * @example
5345
+ * ```typescript
5346
+ * import { createMiddleware } from "langchain";
5347
+ * import { StateSchema } from "@langchain/langgraph";
5348
+ * import { pinnedSkillsValue } from "deepagents";
5349
+ *
5350
+ * const routeSkills = createMiddleware({
5351
+ * name: "RouteSkills",
5352
+ * stateSchema: new StateSchema({ pinnedSkills: pinnedSkillsValue }),
5353
+ * beforeAgent: (state) => ({ pinnedSkills: skillsFor(state.messages) }),
5354
+ * });
5355
+ * ```
5356
+ */
5357
+ const pinnedSkillsValue = new ReducedValue(z$1.array(z$1.string()).default(() => []), { reducer: (stored, written) => [...stored ?? [], ...written ?? []] });
5358
+ /**
4596
5359
  * State schema for skills middleware.
4597
5360
  */
4598
5361
  const SkillsStateSchema = new StateSchema({
4599
5362
  skillsMetadata: skillsMetadataValue,
4600
- files: filesValue
5363
+ pinnedSkills: pinnedSkillsValue,
5364
+ files: filesValue,
5365
+ /**
5366
+ * The skill tools disclosed to the latest model call, each mapped to the
5367
+ * include name that produced it, for the tool-time gate. Written on every
5368
+ * model call of a skills middleware with skill tools. The last write wins,
5369
+ * so a model call retried within one step can write it again.
5370
+ */
5371
+ _skillToolsDisclosed: new ReducedValue(z$1.record(z$1.string(), z$1.string()).optional(), { reducer: (_previous, next) => next })
4601
5372
  });
4602
5373
  /**
4603
5374
  * Skills System Documentation prompt template.
@@ -4699,7 +5470,8 @@ function validateSkillName(name, directoryName) {
4699
5470
  *
4700
5471
  * YAML parsing can return any type for the `metadata` key. This ensures the
4701
5472
  * value in {@link SkillMetadata} is always a `Record<string, string>` by
4702
- * coercing via `String()` and rejecting non-object inputs.
5473
+ * coercing via `String()` and rejecting non-object inputs. It also warns when
5474
+ * `include_tools` isn't a space-separated string of tool names.
4703
5475
  *
4704
5476
  * @param raw - Raw value from `frontmatterData.metadata`.
4705
5477
  * @param skillPath - Path to the `SKILL.md` file (for warning messages).
@@ -4710,6 +5482,8 @@ function validateMetadata(raw, skillPath) {
4710
5482
  if (raw) console.warn(`Ignoring non-object metadata in ${skillPath} (got ${typeof raw})`);
4711
5483
  return {};
4712
5484
  }
5485
+ const includeTools = raw[INCLUDE_TOOLS_KEY];
5486
+ 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)}`);
4713
5487
  const result = {};
4714
5488
  for (const [k, v] of Object.entries(raw)) result[String(k)] = String(v);
4715
5489
  return result;
@@ -4747,7 +5521,7 @@ function parseSkillMetadataFromContent(content, skillPath, directoryName) {
4747
5521
  console.warn(`Skipping ${skillPath}: content too large (${content.length} bytes)`);
4748
5522
  return null;
4749
5523
  }
4750
- const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
5524
+ const match = content.match(FRONTMATTER_PATTERN);
4751
5525
  if (!match) {
4752
5526
  console.warn(`Skipping ${skillPath}: no valid YAML frontmatter found`);
4753
5527
  return null;
@@ -4866,6 +5640,61 @@ async function listSkillsFromBackend(backend, sourcePath) {
4866
5640
  return skills;
4867
5641
  }
4868
5642
  /**
5643
+ * Load the skills in every source, in order. A later source's skill replaces
5644
+ * an earlier one of the same name.
5645
+ */
5646
+ async function loadSkills(backend, sources) {
5647
+ const allSkills = /* @__PURE__ */ new Map();
5648
+ for (const sourcePath of sources) try {
5649
+ const skills = await listSkillsFromBackend(backend, sourcePath);
5650
+ for (const skill of skills) allSkills.set(skill.name, skill);
5651
+ } catch (error) {
5652
+ console.debug(`[BackendSkillsMiddleware] Failed to load skills from ${sourcePath}:`, error);
5653
+ }
5654
+ return Array.from(allSkills.values());
5655
+ }
5656
+ /**
5657
+ * Return a message pinning each skill `names` refers to, in first-seen order.
5658
+ *
5659
+ * Skips a name that isn't in `skills`, and a skill whose `SKILL.md` is
5660
+ * missing, unreadable, empty, or larger than {@link MAX_SKILL_FILE_SIZE}.
5661
+ */
5662
+ async function buildPinnedSkillMessages(backend, names, skills) {
5663
+ const adaptedBackend = adaptBackendProtocol(backend);
5664
+ const skillsByName = new Map(skills.map((skill) => [skill.name, skill]));
5665
+ const skillsToPin = [...new Set(names)].flatMap((name) => {
5666
+ const skill = skillsByName.get(name);
5667
+ return skill === void 0 ? [] : [skill];
5668
+ });
5669
+ const contents = await Promise.all(skillsToPin.map((skill) => readFileFromBackend(adaptedBackend, skill.path).catch(() => null)));
5670
+ return skillsToPin.flatMap((skill, i) => {
5671
+ const content = contents[i];
5672
+ if (!content || content.length > 10485760) return [];
5673
+ return [buildPinnedSkillMessage(skill, content)];
5674
+ });
5675
+ }
5676
+ /** Return the message pinning `skill`, carrying its `SKILL.md` `content` without frontmatter. */
5677
+ function buildPinnedSkillMessage(skill, content) {
5678
+ const body = content.replace(FRONTMATTER_PATTERN, "").trimEnd();
5679
+ const name = escapeXmlAttribute(skill.name);
5680
+ const path = escapeXmlAttribute(skill.path);
5681
+ return new HumanMessage$1({
5682
+ content: `<skill name="${name}" path="${path}">\n${body}\n</skill>`,
5683
+ additional_kwargs: {
5684
+ lc_source: "pinned_skill",
5685
+ skill: {
5686
+ name: skill.name,
5687
+ path: skill.path,
5688
+ description: skill.description
5689
+ }
5690
+ }
5691
+ });
5692
+ }
5693
+ /** Escape `value` for a double-quoted XML attribute. */
5694
+ function escapeXmlAttribute(value) {
5695
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
5696
+ }
5697
+ /**
4869
5698
  * Format skills locations for display in system prompt.
4870
5699
  * Shows priority indicator for the last source (highest priority).
4871
5700
  */
@@ -4925,6 +5754,336 @@ function validateModulePath(raw) {
4925
5754
  if (!endsWithModuleExtension(normalized)) return;
4926
5755
  return normalized;
4927
5756
  }
5757
+ /** Tool `extras` key (and provider field) that withholds a tool's schema until searched for. */
5758
+ const DEFER_LOADING = "defer_loading";
5759
+ /**
5760
+ * Model ID prefixes that accept an inline `tool_definition` mid-conversation.
5761
+ *
5762
+ * `@langchain/anthropic` sends any mid-conversation system message in place,
5763
+ * without gating on the model, so this list is the only guard.
5764
+ */
5765
+ const ANTHROPIC_INLINE_TOOL_MODELS = [
5766
+ "claude-opus-5",
5767
+ "claude-fable-5",
5768
+ "claude-mythos-5",
5769
+ "claude-opus-4-8"
5770
+ ];
5771
+ /** Models whose Responses API accepts an `additional_tools` input item, matched with or without a `-` suffix such as a snapshot date. */
5772
+ const OPENAI_INLINE_TOOL_MODELS = ["gpt-6", "gpt-5.6"];
5773
+ /** Root `input_schema` keys the Anthropic API rejects, failing the whole request. */
5774
+ const ANTHROPIC_ROOT_COMBINATORS = [
5775
+ "oneOf",
5776
+ "anyOf",
5777
+ "allOf"
5778
+ ];
5779
+ /** `extras` keys `ChatAnthropic` copies into a tool's definition, bar the ones a disclosure must never carry. */
5780
+ const ANTHROPIC_DEFINITION_EXTRAS = [
5781
+ "input_examples",
5782
+ "allowed_callers",
5783
+ "strict"
5784
+ ];
5785
+ /**
5786
+ * Return the resolver the skills middleware's `tools` option stands for.
5787
+ *
5788
+ * A list is validated, then resolved by exact name.
5789
+ *
5790
+ * @throws {ConfigurationError} If `tools` is neither a list nor a function,
5791
+ * or a list entry isn't a client tool or repeats a name.
5792
+ */
5793
+ function toSkillToolResolver(tools) {
5794
+ if (tools != null && !Array.isArray(tools)) {
5795
+ 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");
5796
+ return tools;
5797
+ }
5798
+ const entries = tools ?? [];
5799
+ if (!entries.every(isClientTool)) throw new ConfigurationError("tools entries must be client tools; provider-native tool objects are not supported", "SKILL_TOOLS_UNSUPPORTED_TYPE");
5800
+ const names = entries.map((t) => t.name);
5801
+ const duplicates = [...new Set(names.filter((n, i) => names.indexOf(n) !== i))].sort();
5802
+ if (duplicates.length > 0) throw new ConfigurationError(`tools contains duplicate tool name(s): ${duplicates.join(", ")}`, "SKILL_TOOLS_DUPLICATE_NAME");
5803
+ const byName = new Map(entries.map((t) => [t.name, t]));
5804
+ return (name) => byName.has(name) ? [byName.get(name)] : [];
5805
+ }
5806
+ /** Whether `value` is a runnable client tool rather than a provider-native or plain object. */
5807
+ function isClientTool(value) {
5808
+ return Runnable.isRunnable(value) && typeof value.name === "string";
5809
+ }
5810
+ /**
5811
+ * Call `resolver` for one include name, keeping the first of each tool name.
5812
+ *
5813
+ * @throws {TypeError} If the resolver returns something other than an array
5814
+ * of client tools.
5815
+ */
5816
+ async function callSkillToolResolver(resolver, includeName, runtime) {
5817
+ const result = await resolver(includeName, runtime);
5818
+ if (!Array.isArray(result)) throw new TypeError(`skill tool resolver must return an array of tools for '${includeName}', got ${typeof result}`);
5819
+ const tools = /* @__PURE__ */ new Map();
5820
+ for (const item of result) {
5821
+ if (!isClientTool(item)) throw new TypeError(`skill tool resolver returned a non-tool ${typeof item} for '${includeName}'; expected tool instances`);
5822
+ if (!tools.has(item.name)) tools.set(item.name, item);
5823
+ }
5824
+ return [...tools.values()];
5825
+ }
5826
+ /**
5827
+ * Return every successful `read_file` or pin of a `SKILL.md` that lists
5828
+ * tools, in message order.
5829
+ *
5830
+ * The read's or pin's path must equal the skill's path exactly, as the skills
5831
+ * listing shows it. Any `offset` or `limit` counts, and so does a result whose
5832
+ * content was later truncated or compacted, since only the call and the
5833
+ * result's status are read.
5834
+ */
5835
+ function findSkillReads(messages, skills) {
5836
+ const skillsByPath = /* @__PURE__ */ new Map();
5837
+ for (const skill of skills) {
5838
+ const value = skill.metadata?.[INCLUDE_TOOLS_KEY];
5839
+ const includeNames = typeof value === "string" ? value.split(/\s+/).filter(Boolean) : [];
5840
+ if (includeNames.length > 0) skillsByPath.set(skill.path, { includeNames });
5841
+ }
5842
+ if (skillsByPath.size === 0) return [];
5843
+ const readPaths = /* @__PURE__ */ new Map();
5844
+ for (const message of messages) {
5845
+ if (!AIMessage$1.isInstance(message)) continue;
5846
+ 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);
5847
+ }
5848
+ const reads = [];
5849
+ messages.forEach((message, index) => {
5850
+ let path;
5851
+ if (ToolMessage$1.isInstance(message) && message.status !== "error") path = readPaths.get(message.tool_call_id);
5852
+ else if (HumanMessage$1.isInstance(message) && message.additional_kwargs?.lc_source === "pinned_skill") path = message.additional_kwargs.skill?.path;
5853
+ else return;
5854
+ const skill = typeof path === "string" ? skillsByPath.get(path) : void 0;
5855
+ if (skill !== void 0) reads.push({
5856
+ index,
5857
+ ...skill
5858
+ });
5859
+ });
5860
+ return reads;
5861
+ }
5862
+ /** Return a request tool's name, whether it is a tool instance or a provider object. */
5863
+ function toolName(tool) {
5864
+ if (isOpenAITool(tool)) return tool.function.name;
5865
+ return typeof tool.name === "string" ? tool.name : void 0;
5866
+ }
5867
+ /**
5868
+ * Disclose the tools the read skills' include names produce to one model call.
5869
+ *
5870
+ * Returns the request to send, and the record of the skill tools it
5871
+ * discloses, for the tool-time gate.
5872
+ */
5873
+ async function discloseSkillTools(request, reads, resolver) {
5874
+ const requestTools = /* @__PURE__ */ new Map();
5875
+ for (const tool of request.tools) {
5876
+ const name = toolName(tool);
5877
+ if (name !== void 0) requestTools.set(name, tool);
5878
+ }
5879
+ const names = [...new Set(reads.flatMap((read) => read.includeNames.filter((name) => !requestTools.has(name))))];
5880
+ const tools = await Promise.all(names.map((name) => callSkillToolResolver(resolver, name, request.runtime)));
5881
+ const disclosed = planDisclosure(reads, requestTools, new Map(names.map((name, i) => [name, tools[i]])));
5882
+ if (disclosed.size === 0) return {
5883
+ request,
5884
+ record: {}
5885
+ };
5886
+ const chatModel = await resolveChatModel(request.model, request.runtime);
5887
+ if (llmType(chatModel) === "anthropic") withholdRootCombinatorTools(disclosed);
5888
+ const gatedNames = [...disclosed.keys()].filter((name) => disclosed.get(name).includeName !== void 0).sort();
5889
+ const record = Object.fromEntries(gatedNames.map((name) => [name, disclosed.get(name).includeName]));
5890
+ if (disclosed.size === 0) return {
5891
+ request,
5892
+ record
5893
+ };
5894
+ const build = inlineBlockBuilder(chatModel);
5895
+ if (build !== void 0) return {
5896
+ request: {
5897
+ ...request,
5898
+ messages: discloseInMessages(request.messages, disclosed, build)
5899
+ },
5900
+ record
5901
+ };
5902
+ return {
5903
+ request: {
5904
+ ...request,
5905
+ tools: discloseInTools(request.tools, disclosed, gatedNames)
5906
+ },
5907
+ record
5908
+ };
5909
+ }
5910
+ /**
5911
+ * Classify what each read skill's include names produce, anchoring each tool
5912
+ * at the earliest read producing it.
5913
+ *
5914
+ * An include name that matches a request tool's name produces that tool; any
5915
+ * other produces what the resolver returned for it. A produced tool whose name
5916
+ * a request tool has wins as that request tool, because it's the one that
5917
+ * runs: a deferred one is disclosed early, and a bound one adds nothing, since
5918
+ * the model already sees it. Tools are matched by name, never by identity,
5919
+ * because outer middleware such as tool search replaces the request's tools
5920
+ * with copies.
5921
+ */
5922
+ function planDisclosure(reads, requestTools, resolved) {
5923
+ const disclosed = /* @__PURE__ */ new Map();
5924
+ const discloseIfDeferred = (name, anchor) => {
5925
+ const entry = requestTools.get(name);
5926
+ if (!disclosed.has(name) && isLangChainTool(entry) && entry.extras?.[DEFER_LOADING] === true) disclosed.set(name, {
5927
+ tool: entry,
5928
+ anchor
5929
+ });
5930
+ };
5931
+ for (const { index, includeNames } of reads) for (const includeName of includeNames) {
5932
+ if (requestTools.has(includeName)) {
5933
+ discloseIfDeferred(includeName, index);
5934
+ continue;
5935
+ }
5936
+ for (const tool of resolved.get(includeName) ?? []) {
5937
+ if (disclosed.has(tool.name)) continue;
5938
+ if (requestTools.has(tool.name)) discloseIfDeferred(tool.name, index);
5939
+ else disclosed.set(tool.name, {
5940
+ tool,
5941
+ anchor: index,
5942
+ includeName
5943
+ });
5944
+ }
5945
+ }
5946
+ return disclosed;
5947
+ }
5948
+ /**
5949
+ * Withhold each disclosed tool whose root input schema uses `oneOf`, `anyOf`
5950
+ * or `allOf`, which the Anthropic API rejects, failing the whole request, in
5951
+ * `tools` and inline alike. Withholding it on every path keeps the record
5952
+ * equal to what the model was shown.
5953
+ */
5954
+ function withholdRootCombinatorTools(disclosed) {
5955
+ for (const [name, { tool }] of disclosed) {
5956
+ const schema = anthropicDefinition(tool).input_schema;
5957
+ const keys = ANTHROPIC_ROOT_COMBINATORS.filter((key) => typeof schema === "object" && schema !== null && Object.prototype.hasOwnProperty.call(schema, key));
5958
+ if (keys.length > 0) {
5959
+ console.warn(`Not disclosing tool '${name}': its input_schema has a top-level ${keys.join("/")}, which the Anthropic API does not support`);
5960
+ disclosed.delete(name);
5961
+ }
5962
+ }
5963
+ }
5964
+ /**
5965
+ * Insert one `SystemMessage` per insertion point, carrying the tools anchored
5966
+ * there, for models that accept tool definitions mid-conversation.
5967
+ *
5968
+ * Each message lands at the same index with the same bytes on every call while
5969
+ * its anchor stays visible, so the provider's cached prefix survives.
5970
+ */
5971
+ function discloseInMessages(messages, disclosed, build) {
5972
+ const namesByIndex = /* @__PURE__ */ new Map();
5973
+ for (const [name, { anchor }] of disclosed) {
5974
+ let index = anchor + 1;
5975
+ while (index < messages.length) {
5976
+ const message = messages[index];
5977
+ const emptyReply = AIMessage$1.isInstance(message) && message.content.length === 0 && !message.tool_calls?.length;
5978
+ if (!ToolMessage$1.isInstance(message) && !HumanMessage$1.isInstance(message) && !emptyReply) break;
5979
+ index += 1;
5980
+ }
5981
+ namesByIndex.set(index, [...namesByIndex.get(index) ?? [], name]);
5982
+ }
5983
+ const result = [...messages];
5984
+ for (const index of [...namesByIndex.keys()].sort((a, b) => b - a)) {
5985
+ const blocks = namesByIndex.get(index).sort().map((name) => build(disclosed.get(name).tool));
5986
+ result.splice(index, 0, new SystemMessage$1({ content: blocks }));
5987
+ }
5988
+ return result;
5989
+ }
5990
+ /**
5991
+ * Return `requestTools` with deferred disclosures undeferred and skill tools
5992
+ * appended, for models that can't take tool definitions mid-conversation.
5993
+ *
5994
+ * A deferred tool is replaced by a plain spec rather than a copy: `createAgent`
5995
+ * rejects a request that swaps a registered tool for another instance of the
5996
+ * same name, and doesn't validate plain objects.
5997
+ */
5998
+ function discloseInTools(requestTools, disclosed, gatedNames) {
5999
+ return [...requestTools.map((tool) => {
6000
+ const name = toolName(tool);
6001
+ const entry = name === void 0 ? void 0 : disclosed.get(name);
6002
+ if (entry?.tool !== tool || entry.includeName !== void 0) return tool;
6003
+ const { description, schema, extras = {} } = entry.tool;
6004
+ const { [DEFER_LOADING]: _deferLoading, ...rest } = extras;
6005
+ return {
6006
+ name,
6007
+ description,
6008
+ schema,
6009
+ extras: rest
6010
+ };
6011
+ }), ...gatedNames.map((name) => disclosed.get(name).tool)];
6012
+ }
6013
+ /** Return the model inside a `bind()` / `withConfig()` wrapper; chained calls merge into one. */
6014
+ function unwrapBinding(model) {
6015
+ return RunnableBinding.isRunnableBinding(model) ? model.bound : model;
6016
+ }
6017
+ /**
6018
+ * Return the concrete chat model a request will call.
6019
+ *
6020
+ * Resolves a `ConfigurableModel`, which a string `model` always becomes, to
6021
+ * the instance it will call with this run's `configurable`. Without
6022
+ * configurable overrides that's the instance cached at creation: the instance
6023
+ * cache is keyed on the whole config, so passing `configurable`
6024
+ * unconditionally would build one per thread.
6025
+ */
6026
+ async function resolveChatModel(model, runtime) {
6027
+ const current = unwrapBinding(model);
6028
+ if (current?._queuedMethodOperations === void 0 || typeof current._getModelInstance !== "function") return current;
6029
+ const configurable = runtime.configurable ?? {};
6030
+ const overrides = current._modelParams?.({ configurable }) ?? {};
6031
+ return unwrapBinding(Object.keys(overrides).length > 0 ? await current._getModelInstance({ configurable }) : await current._getModelInstance());
6032
+ }
6033
+ /** Return a chat model's `_llmType()`, which tells providers apart. */
6034
+ function llmType(model) {
6035
+ const fn = model?._llmType;
6036
+ return typeof fn === "function" ? fn.call(model) : void 0;
6037
+ }
6038
+ /**
6039
+ * Return how `chatModel` is given a tool mid-conversation, or `undefined` if
6040
+ * it can't be.
6041
+ *
6042
+ * The one place that decides support, so a model-profile capability can
6043
+ * replace the allowlists later.
6044
+ */
6045
+ function inlineBlockBuilder(chatModel) {
6046
+ const { model: name, useResponsesApi } = chatModel;
6047
+ if (typeof name !== "string") return void 0;
6048
+ const type = llmType(chatModel);
6049
+ if (type === "anthropic" && ANTHROPIC_INLINE_TOOL_MODELS.some((prefix) => name.startsWith(prefix))) return anthropicToolAddition;
6050
+ if (type === "openai" && useResponsesApi === true && OPENAI_INLINE_TOOL_MODELS.some((model) => name === model || name.startsWith(`${model}-`))) return openaiAdditionalTools;
6051
+ }
6052
+ /**
6053
+ * Return the definition `ChatAnthropic` sends in `tools` for `tool`, without
6054
+ * `defer_loading` (a deferred definition would stay withheld) or
6055
+ * `cache_control` (a stray marker uses up a breakpoint).
6056
+ */
6057
+ function anthropicDefinition(tool) {
6058
+ const definition = {
6059
+ name: tool.name,
6060
+ description: tool.description,
6061
+ input_schema: isInteropZodSchema(tool.schema) ? toJsonSchema(tool.schema) : tool.schema
6062
+ };
6063
+ for (const key of ANTHROPIC_DEFINITION_EXTRAS) if (tool.extras?.[key] !== void 0) definition[key] = tool.extras[key];
6064
+ return definition;
6065
+ }
6066
+ /** Build the Anthropic `tool_addition` block carrying `tool`'s full definition. */
6067
+ function anthropicToolAddition(tool) {
6068
+ return {
6069
+ type: "tool_addition",
6070
+ tool: {
6071
+ type: "tool_definition",
6072
+ definition: anthropicDefinition(tool)
6073
+ }
6074
+ };
6075
+ }
6076
+ /** Build the OpenAI Responses `additional_tools` item carrying `tool`'s function schema. */
6077
+ function openaiAdditionalTools(tool) {
6078
+ return {
6079
+ type: "additional_tools",
6080
+ role: "developer",
6081
+ tools: [{
6082
+ type: "function",
6083
+ ...convertToOpenAIFunction(tool)
6084
+ }]
6085
+ };
6086
+ }
4928
6087
  /**
4929
6088
  * Create backend-agnostic middleware for loading and exposing agent skills.
4930
6089
  *
@@ -4948,8 +6107,62 @@ function validateModulePath(raw) {
4948
6107
  * invalidate from any hook — including mid-run, from `afterModel` — and see
4949
6108
  * the fresh list on the following call. See {@link skillsMetadataValue}.
4950
6109
  *
6110
+ * To guarantee the model gets a skill's instructions, for example when the
6111
+ * user names it, pass its name in `pinnedSkills`. Before the next model call,
6112
+ * each named skill's `SKILL.md`, without its frontmatter, is appended to the
6113
+ * conversation as its own `HumanMessage`, with `additional_kwargs.lc_source`
6114
+ * set to `"pinned_skill"` and the skill's `name`, `path` and `description`
6115
+ * under `additional_kwargs.skill`:
6116
+ *
6117
+ * ```ts
6118
+ * await agent.invoke(
6119
+ * {
6120
+ * messages: [new HumanMessage("/write-tests /house-style for auth.py")],
6121
+ * pinnedSkills: ["write-tests", "house-style"],
6122
+ * },
6123
+ * config,
6124
+ * );
6125
+ * ```
6126
+ *
6127
+ * Pins resolve against the skills loaded for the thread, so reset
6128
+ * `skillsMetadata` in the same input to pin a skill added since. See
6129
+ * {@link pinnedSkillsValue}.
6130
+ *
6131
+ * ## Skill tools
6132
+ *
6133
+ * A skill can list the tools its instructions use, separated by spaces, under
6134
+ * `metadata.include_tools` in its `SKILL.md` frontmatter:
6135
+ *
6136
+ * ```yaml
6137
+ * metadata:
6138
+ * include_tools: create_customer_request list_customer_requests
6139
+ * ```
6140
+ *
6141
+ * Pass those tools as `tools`, either as an array or as a
6142
+ * {@link SkillToolResolver} that looks them up by name when a skill is read
6143
+ * or pinned, so one name can stand for a family of tools whose real names are
6144
+ * only known at runtime. The model sees a skill tool only after it reads, with
6145
+ * `read_file`, a skill that lists it, or the skill is pinned, and only while
6146
+ * that read or pin stays in the conversation it is sent; compaction that drops
6147
+ * the read or pin withdraws the tool.
6148
+ * Until then, calling the tool fails with the standard invalid-tool error and
6149
+ * the tool doesn't run. This controls what is in the model's context; it is
6150
+ * not a security boundary, since the model can read any `SKILL.md` at any
6151
+ * time.
6152
+ *
6153
+ * `include_tools` can also name a tool passed to the agent rather than to this
6154
+ * middleware. That tool wins over a skill tool of the same name. If it is
6155
+ * deferred (`extras: { defer_loading: true }`), reading or pinning the skill
6156
+ * discloses it early, and it stays deferred and searchable; if it is bound,
6157
+ * nothing changes.
6158
+ *
6159
+ *
4951
6160
  * @param options - Configuration options
4952
6161
  * @returns AgentMiddleware for skills loading and injection
6162
+ * @throws {ConfigurationError} If `tools` is neither an array nor a
6163
+ * function (`SKILL_TOOLS_UNSUPPORTED_TYPE`), an entry isn't a client tool
6164
+ * (`SKILL_TOOLS_UNSUPPORTED_TYPE`), or two entries share a name
6165
+ * (`SKILL_TOOLS_DUPLICATE_NAME`).
4953
6166
  *
4954
6167
  * @example
4955
6168
  * ```typescript
@@ -4961,30 +6174,60 @@ function validateModulePath(raw) {
4961
6174
  */
4962
6175
  function createSkillsMiddleware(options) {
4963
6176
  const { backend, sources } = options;
6177
+ const skillToolResolver = toSkillToolResolver(options.tools);
6178
+ const hasSkillTools = typeof options.tools === "function" || Array.isArray(options.tools) && options.tools.length > 0;
4964
6179
  return createMiddleware({
4965
6180
  name: "SkillsMiddleware",
4966
6181
  stateSchema: SkillsStateSchema,
4967
6182
  async beforeModel(state) {
4968
- if (state.skillsMetadata !== null && state.skillsMetadata !== void 0) return;
6183
+ const needsLoad = state.skillsMetadata === null || state.skillsMetadata === void 0;
6184
+ const pinnedNames = state.pinnedSkills ?? [];
6185
+ if (!needsLoad && pinnedNames.length === 0) return;
4969
6186
  const resolvedBackend = await resolveBackend(backend, { state });
4970
- const allSkills = /* @__PURE__ */ new Map();
4971
- for (const sourcePath of sources) try {
4972
- const skills = await listSkillsFromBackend(resolvedBackend, sourcePath);
4973
- for (const skill of skills) allSkills.set(skill.name, skill);
4974
- } catch (error) {
4975
- console.debug(`[BackendSkillsMiddleware] Failed to load skills from ${sourcePath}:`, error);
4976
- }
4977
- return { skillsMetadata: Array.from(allSkills.values()) };
6187
+ const skillsMetadata = state.skillsMetadata ?? await loadSkills(resolvedBackend, sources);
6188
+ if (pinnedNames.length === 0) return { skillsMetadata };
6189
+ const messages = await buildPinnedSkillMessages(resolvedBackend, pinnedNames, skillsMetadata);
6190
+ return {
6191
+ ...needsLoad && { skillsMetadata },
6192
+ ...messages.length > 0 && { messages },
6193
+ pinnedSkills: new Overwrite([])
6194
+ };
4978
6195
  },
4979
- wrapModelCall(request, handler) {
6196
+ async wrapModelCall(request, handler) {
4980
6197
  const skillsMetadata = request.state?.skillsMetadata || [];
4981
6198
  const skillsLocations = formatSkillsLocations(sources);
4982
6199
  const skillsList = formatSkillsList(skillsMetadata, sources);
4983
6200
  const skillsSection = SKILLS_SYSTEM_PROMPT.replace("{skills_locations}", skillsLocations).replace("{skills_list}", skillsList);
4984
6201
  const newSystemMessage = request.systemMessage.concat(skillsSection);
4985
- return handler({
6202
+ const prompted = {
4986
6203
  ...request,
4987
6204
  systemMessage: newSystemMessage
6205
+ };
6206
+ const reads = findSkillReads(request.messages, skillsMetadata);
6207
+ const { request: disclosed, record } = reads.length === 0 ? {
6208
+ request: prompted,
6209
+ record: {}
6210
+ } : await discloseSkillTools(prompted, reads, skillToolResolver);
6211
+ const response = await handler(disclosed);
6212
+ const stored = request.state._skillToolsDisclosed;
6213
+ if (!hasSkillTools && Object.keys(stored ?? {}).length === 0) return response;
6214
+ if (typeof response === "object" && response !== null && "structuredResponse" in response) return response;
6215
+ return new Command({ update: { _skillToolsDisclosed: record } });
6216
+ },
6217
+ async wrapToolCall(request, handler) {
6218
+ if (request.tool !== void 0) return handler(request);
6219
+ const name = request.toolCall.name;
6220
+ const record = request.state._skillToolsDisclosed ?? {};
6221
+ const includeName = Object.hasOwn(record, name) ? record[name] : void 0;
6222
+ if (includeName === void 0) return handler(request);
6223
+ const tool = (await callSkillToolResolver(skillToolResolver, includeName, request.runtime)).find((candidate) => candidate.name === name);
6224
+ if (tool === void 0) {
6225
+ console.warn(`Skill tool '${name}' was disclosed via '${includeName}', but the resolver no longer returns it`);
6226
+ return handler(request);
6227
+ }
6228
+ return handler({
6229
+ ...request,
6230
+ tool
4988
6231
  });
4989
6232
  }
4990
6233
  });
@@ -5754,46 +6997,6 @@ function createAsyncSubAgentMiddleware(options) {
5754
6997
  });
5755
6998
  }
5756
6999
  //#endregion
5757
- //#region src/errors.ts
5758
- const CONFIGURATION_ERROR_SYMBOL = Symbol.for("deepagents.configuration_error");
5759
- /**
5760
- * Thrown when `createDeepAgent` receives invalid configuration.
5761
- *
5762
- * Follows the same pattern as {@link SandboxError}: a human-readable
5763
- * `message`, a structured `code` for programmatic handling, and a
5764
- * static `isInstance` guard that works across realms.
5765
- *
5766
- * @example
5767
- * ```typescript
5768
- * try {
5769
- * createDeepAgent({ tools: [myTool] });
5770
- * } catch (error) {
5771
- * if (ConfigurationError.isInstance(error)) {
5772
- * switch (error.code) {
5773
- * case "TOOL_NAME_COLLISION":
5774
- * console.error("Rename your tool:", error.message);
5775
- * break;
5776
- * }
5777
- * }
5778
- * }
5779
- * ```
5780
- */
5781
- var ConfigurationError = class ConfigurationError extends Error {
5782
- code;
5783
- cause;
5784
- [CONFIGURATION_ERROR_SYMBOL] = true;
5785
- name = "ConfigurationError";
5786
- constructor(message, code, cause) {
5787
- super(message);
5788
- this.code = code;
5789
- this.cause = cause;
5790
- Object.setPrototypeOf(this, ConfigurationError.prototype);
5791
- }
5792
- static isInstance(error) {
5793
- return typeof error === "object" && error !== null && error[CONFIGURATION_ERROR_SYMBOL] === true;
5794
- }
5795
- };
5796
- //#endregion
5797
7000
  //#region src/middleware/cache.ts
5798
7001
  /**
5799
7002
  * Creates a middleware that places a cache breakpoint at the end of the static
@@ -6659,13 +7862,10 @@ function createDeepAgent(params = {}) {
6659
7862
  harnessProfile.systemPromptSuffix
6660
7863
  ]);
6661
7864
  /**
6662
- * Process subagents to add SkillsMiddleware for those with their own skills.
6663
- *
6664
- * Custom subagents do NOT inherit skills from the main agent by default.
6665
- * Only the general-purpose subagent inherits the main agent's skills.
6666
- * If a custom subagent needs skills, it must specify its own `skills` array.
7865
+ * Build the default head of a declarative subagent's middleware stack.
7866
+ * Skills go in the tail instead (see buildSubagentMiddleware).
6667
7867
  */
6668
- const createSubagentDefaultMiddleware = (input, subagentProfile, forked) => {
7868
+ const createSubagentDefaultMiddleware = (input, subagentProfile) => {
6669
7869
  const effectivePermissions = input.permissions ?? permissions;
6670
7870
  return [
6671
7871
  createFilesystemMiddleware({
@@ -6674,23 +7874,33 @@ function createDeepAgent(params = {}) {
6674
7874
  tools: computeProfileFilesystemTools(subagentProfile)
6675
7875
  }),
6676
7876
  createSummarizationMiddleware({ backend }),
6677
- createPatchToolCallsMiddleware(),
6678
- ...!forked && input.skills != null && input.skills.length > 0 ? [createSkillsMiddleware({
6679
- backend,
6680
- sources: input.skills
6681
- })] : []
7877
+ createPatchToolCallsMiddleware()
6682
7878
  ];
6683
7879
  };
6684
- const buildSubagentMiddleware = (input) => {
7880
+ /**
7881
+ * The skills middleware for a stack: the last `SkillsMiddleware` among its
7882
+ * custom middleware, else one built from `sources`, else none. A custom one
7883
+ * is picked here rather than left to replace the default by name, because
7884
+ * without `sources` there is no default to replace and it would run with
7885
+ * the novel middleware instead.
7886
+ */
7887
+ const resolveSkillsMiddleware = (sources, custom) => {
7888
+ const passed = custom.findLast((entry) => entry.name === "SkillsMiddleware");
7889
+ if (passed !== void 0) return [passed];
7890
+ return sources != null && sources.length > 0 ? [createSkillsMiddleware({
7891
+ backend,
7892
+ sources
7893
+ })] : [];
7894
+ };
7895
+ const buildSubagentMiddleware = (input, generalPurpose = false) => {
6685
7896
  const subagentProfile = resolveSubagentProfile(input.model);
6686
7897
  const forked = isForkedSubAgent(input);
6687
- const subagentDefaultMiddleware = createSubagentDefaultMiddleware(input, subagentProfile, forked);
6688
- if (forked && skills != null && skills.length > 0) subagentDefaultMiddleware.unshift(createSkillsMiddleware({
6689
- backend,
6690
- sources: skills
6691
- }));
6692
- let subagentMiddleware = mergeMiddlewareStack(subagentDefaultMiddleware, forked && customMiddleware.length > 0 ? mergeMiddleware$1(customMiddleware, input.middleware ?? []) : input.middleware ?? [], [
7898
+ const subagentDefaultMiddleware = createSubagentDefaultMiddleware(input, subagentProfile);
7899
+ const inputMiddleware = forked && customMiddleware.length > 0 ? mergeMiddleware$1(customMiddleware, input.middleware ?? []) : input.middleware ?? [];
7900
+ const subagentSkillsMiddleware = generalPurpose ? resolveSkillsMiddleware(skills, customMiddleware) : resolveSkillsMiddleware(forked ? skills : input.skills, inputMiddleware);
7901
+ let subagentMiddleware = mergeMiddlewareStack(subagentDefaultMiddleware, inputMiddleware, [
6693
7902
  ...resolveMiddleware(subagentProfile.extraMiddleware),
7903
+ ...subagentSkillsMiddleware,
6694
7904
  ...cacheMiddleware,
6695
7905
  ...forked && memory != null && memory.length > 0 ? [createMemoryMiddleware({
6696
7906
  backend,
@@ -6700,11 +7910,12 @@ function createDeepAgent(params = {}) {
6700
7910
  ]);
6701
7911
  if (subagentProfile.excludedMiddleware.size > 0) subagentMiddleware = subagentMiddleware.filter((middleware) => !subagentProfile.excludedMiddleware.has(middleware.name));
6702
7912
  if (subagentProfile.excludedTools.size > 0) subagentMiddleware.push(createToolExclusionMiddleware(subagentProfile.excludedTools));
7913
+ if (!subagentProfile.excludedMiddleware.has("UnsupportedContentMiddleware") && !subagentMiddleware.some((middleware) => middleware.name === "UnsupportedContentMiddleware")) subagentMiddleware.push(createUnsupportedContentMiddleware());
6703
7914
  return subagentMiddleware;
6704
7915
  };
6705
- const normalizeSubagentSpec = (input) => ({
7916
+ const normalizeSubagentSpec = (input, generalPurpose = false) => ({
6706
7917
  ...input,
6707
- middleware: buildSubagentMiddleware(input)
7918
+ middleware: buildSubagentMiddleware(input, generalPurpose)
6708
7919
  });
6709
7920
  const allSubagents = subagents;
6710
7921
  const asyncSubAgents = allSubagents.filter((item) => isAsyncSubAgent(item));
@@ -6717,16 +7928,12 @@ function createDeepAgent(params = {}) {
6717
7928
  description: gpConfig?.description ?? GENERAL_PURPOSE_SUBAGENT.description,
6718
7929
  systemPrompt: gpSystemPrompt,
6719
7930
  model,
6720
- skills,
6721
7931
  tools: effectiveTools
6722
- });
7932
+ }, true);
6723
7933
  generalPurposeSpec.middleware = mergeMiddlewareStack(generalPurposeSpec.middleware ?? [], customMiddleware, [], { appendNew: false });
6724
7934
  inlineSubagents.unshift(generalPurposeSpec);
6725
7935
  }
6726
- const skillsMiddleware = skills != null && skills.length > 0 ? [createSkillsMiddleware({
6727
- backend,
6728
- sources: skills
6729
- })] : [];
7936
+ const skillsMiddleware = resolveSkillsMiddleware(skills, customMiddleware);
6730
7937
  const [fsMiddleware, subagentMiddleware, summarizationMiddleware, patchToolCallsMiddleware] = [
6731
7938
  createFilesystemMiddleware({
6732
7939
  backend,
@@ -6745,7 +7952,6 @@ function createDeepAgent(params = {}) {
6745
7952
  createPatchToolCallsMiddleware()
6746
7953
  ];
6747
7954
  let middleware = mergeMiddlewareStack([
6748
- ...skillsMiddleware,
6749
7955
  fsMiddleware,
6750
7956
  subagentMiddleware,
6751
7957
  summarizationMiddleware,
@@ -6753,6 +7959,7 @@ function createDeepAgent(params = {}) {
6753
7959
  ...asyncSubAgents.length > 0 ? [createAsyncSubAgentMiddleware({ asyncSubAgents })] : []
6754
7960
  ], customMiddleware, [
6755
7961
  ...resolveMiddleware(harnessProfile.extraMiddleware),
7962
+ ...skillsMiddleware,
6756
7963
  ...cacheMiddleware,
6757
7964
  ...memoryMiddleware,
6758
7965
  ...interruptOn ? [humanInTheLoopMiddleware({ interruptOn })] : []
@@ -6762,6 +7969,7 @@ function createDeepAgent(params = {}) {
6762
7969
  middleware = middleware.filter((entry) => !excluded.has(entry.name));
6763
7970
  }
6764
7971
  if (harnessProfile.excludedTools.size > 0) middleware.push(createToolExclusionMiddleware(harnessProfile.excludedTools));
7972
+ if (!harnessProfile.excludedMiddleware.has("UnsupportedContentMiddleware") && !middleware.some((m) => m.name === "UnsupportedContentMiddleware")) middleware.push(createUnsupportedContentMiddleware());
6765
7973
  /**
6766
7974
  * Return as DeepAgent with proper DeepAgentTypeConfig
6767
7975
  * - Response: InferStructuredResponse<TResponse> (unwraps ToolStrategy<T>/ProviderStrategy<T> → T)
@@ -8879,6 +10087,6 @@ var LangSmithSandbox = class LangSmithSandbox extends BaseSandbox {
8879
10087
  }
8880
10088
  };
8881
10089
  //#endregion
8882
- export { performStringReplacement as $, createPatchToolCallsMiddleware as A, CompositeBackend as B, MAX_SKILL_DESCRIPTION_LENGTH as C, skillsMetadataValue as D, createSkillsMiddleware as E, createSubAgent as F, isSandboxProtocol as G, SandboxError as H, createSubAgentMiddleware as I, adaptSandboxProtocol as J, resolveBackend as K, computeSummarizationDefaults as L, DEFAULT_SUBAGENT_PROMPT as M, GENERAL_PURPOSE_SUBAGENT as N, createMemoryMiddleware as O, SUBAGENT_RESPONSE_FORMAT_CONFIG_KEY as P, normalizeReadPagination as Q, createSummarizationMiddleware as R, createCompletionCallbackMiddleware as S, MAX_SKILL_NAME_LENGTH as T, applyGrepMaxCount as U, StateBackend as V, isSandboxBackend as W, getMimeType as X, checkEmptyContent as Y, isTextMimeType as Z, createHarnessProfile as _, ASYNC_TASK_SYSTEM_PROMPT as a, createAsyncSubAgentMiddleware as b, TASK_SYSTEM_PROMPT as c, registerHarnessProfile as d, generalPurposeSubagentConfigSchema as f, EMPTY_HARNESS_PROFILE as g, serializeProfile as h, StoreBackend as i, DEFAULT_GENERAL_PURPOSE_DESCRIPTION as j, filesValue as k, createDeepAgent as l, parseHarnessProfileConfig as m, BaseSandbox as n, BASE_AGENT_PROMPT as o, harnessProfileConfigSchema as p, adaptBackendProtocol as q, ContextHubBackend as r, EXECUTION_SYSTEM_PROMPT as s, LangSmithSandbox as t, getHarnessProfile as u, REQUIRED_MIDDLEWARE_NAMES as v, MAX_SKILL_FILE_SIZE as w, isAsyncSubAgent as x, ConfigurationError as y, createFilesystemMiddleware as z };
10090
+ export { adaptSandboxProtocol as $, filesValue as A, createFilesystemMiddleware as B, MAX_SKILL_FILE_SIZE as C, skillsMetadataValue as D, pinnedSkillsValue as E, SUBAGENT_RESPONSE_FORMAT_CONFIG_KEY as F, scrubUnsupportedMultimodalContent as G, MULTIMODAL_BLOCK_TYPES as H, createSubAgent as I, applyGrepMaxCount as J, StateBackend as K, createSubAgentMiddleware as L, DEFAULT_GENERAL_PURPOSE_DESCRIPTION as M, DEFAULT_SUBAGENT_PROMPT as N, ConfigurationError as O, GENERAL_PURPOSE_SUBAGENT as P, adaptBackendProtocol as Q, computeSummarizationDefaults as R, MAX_SKILL_DESCRIPTION_LENGTH as S, createSkillsMiddleware as T, createUnsupportedContentMiddleware as U, CompositeBackend as V, multimodalBlockSupported as W, isSandboxProtocol as X, isSandboxBackend as Y, resolveBackend as Z, createHarnessProfile as _, ASYNC_TASK_SYSTEM_PROMPT as a, setRealpath as at, isAsyncSubAgent as b, TASK_SYSTEM_PROMPT as c, registerHarnessProfile as d, checkEmptyContent as et, generalPurposeSubagentConfigSchema as f, EMPTY_HARNESS_PROFILE as g, serializeProfile as h, StoreBackend as i, performStringReplacement as it, createPatchToolCallsMiddleware as j, createMemoryMiddleware as k, createDeepAgent as l, parseHarnessProfileConfig as m, BaseSandbox as n, isTextMimeType as nt, BASE_AGENT_PROMPT as o, harnessProfileConfigSchema as p, SandboxError as q, ContextHubBackend as r, normalizeReadPagination as rt, EXECUTION_SYSTEM_PROMPT as s, LangSmithSandbox as t, getMimeType as tt, getHarnessProfile as u, REQUIRED_MIDDLEWARE_NAMES as v, MAX_SKILL_NAME_LENGTH as w, createCompletionCallbackMiddleware as x, createAsyncSubAgentMiddleware as y, createSummarizationMiddleware as z };
8883
10091
 
8884
- //# sourceMappingURL=langsmith-Bgi_Ta-k.js.map
10092
+ //# sourceMappingURL=langsmith-v_uskil7.js.map