deepagents 1.8.0 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  <div align="center">
2
2
  <a href="https://docs.langchain.com/oss/python/deepagents/overview#deep-agents-overview">
3
3
  <picture>
4
- <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/langchain-ai/deepagentsjs/refs/heads/main/.github/images/logo-dark.svg">
5
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/langchain-ai/deepagentsjs/refs/heads/main/.github/images/logo-light.svg">
6
- <img alt="Deep Agents Logo" src="https://raw.githubusercontent.com/langchain-ai/deepagentsjs/refs/heads/main/.github/images/logo-dark.svg" width="80%">
4
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/langchain-ai/deepagentsjs/refs/heads/main/.github/images/logo-light.svg">
5
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/langchain-ai/deepagentsjs/refs/heads/main/.github/images/logo-dark.svg">
6
+ <img alt="Deep Agents Logo" src="https://raw.githubusercontent.com/langchain-ai/deepagentsjs/refs/heads/main/.github/images/logo-dark.svg" width="50%">
7
7
  </picture>
8
8
  </a>
9
9
  </div>
@@ -132,6 +132,9 @@ const researchInstructions = `You are an expert researcher. Your job is to condu
132
132
 
133
133
  You have access to an internet search tool as your primary means of gathering information.
134
134
 
135
+ > [!TIP]
136
+ > For developing, debugging, and deploying AI agents and LLM applications, see [LangSmith](https://docs.langchain.com/langsmith/home).
137
+
135
138
  ## \`internet_search\`
136
139
 
137
140
  Use this to run an internet search for a given query. You can specify the max number of results to return, the topic, and whether raw content should be included.
@@ -713,3 +716,52 @@ const agent = createAgent({
713
716
  ],
714
717
  });
715
718
  ```
719
+
720
+ ## ACP (Agent Client Protocol) Support
721
+
722
+ Deep Agents can be exposed as an [Agent Client Protocol](https://agentclientprotocol.com) server, enabling integration with IDEs like [Zed](https://zed.dev), JetBrains, and other ACP-compatible clients through a standardized JSON-RPC 2.0 protocol over stdio.
723
+
724
+ The `deepagents-acp` package wraps your Deep Agent with ACP support:
725
+
726
+ ```bash
727
+ npm install deepagents-acp
728
+ ```
729
+
730
+ The quickest way to get started is via the CLI:
731
+
732
+ ```bash
733
+ npx deepagents-acp --name my-agent --workspace /path/to/project
734
+ ```
735
+
736
+ Or programmatically:
737
+
738
+ ```typescript
739
+ import { startServer } from "deepagents-acp";
740
+
741
+ await startServer({
742
+ agents: {
743
+ name: "coding-assistant",
744
+ description: "AI coding assistant with filesystem access",
745
+ skills: ["./skills/"],
746
+ },
747
+ workspaceRoot: process.cwd(),
748
+ });
749
+ ```
750
+
751
+ To use with Zed, add the following to your Zed settings:
752
+
753
+ ```json
754
+ {
755
+ "agent": {
756
+ "profiles": {
757
+ "deepagents": {
758
+ "name": "DeepAgents",
759
+ "command": "npx",
760
+ "args": ["deepagents-acp"]
761
+ }
762
+ }
763
+ }
764
+ }
765
+ ```
766
+
767
+ See the [deepagents-acp README](libs/acp/README.md) and the [ACP server example](examples/acp-server/) for full documentation and advanced configuration.
package/dist/index.cjs CHANGED
@@ -125,6 +125,8 @@ var SandboxError = class SandboxError extends Error {
125
125
  const EMPTY_CONTENT_WARNING = "System reminder: File exists but has empty contents";
126
126
  const MAX_LINE_LENGTH = 1e4;
127
127
  const LINE_NUMBER_WIDTH = 6;
128
+ const TOOL_RESULT_TOKEN_LIMIT = 2e4;
129
+ const TRUNCATION_GUIDANCE = "... [results truncated, try being more specific with your parameters]";
128
130
  /**
129
131
  * Sanitize tool_call_id to prevent path traversal and separator issues.
130
132
  *
@@ -260,6 +262,21 @@ function performStringReplacement(content, oldString, newString, replaceAll) {
260
262
  return [content.split(oldString).join(newString), occurrences];
261
263
  }
262
264
  /**
265
+ * Truncate list or string result if it exceeds token limit (rough estimate: 4 chars/token).
266
+ */
267
+ function truncateIfTooLong(result) {
268
+ if (Array.isArray(result)) {
269
+ const totalChars = result.reduce((sum, item) => sum + item.length, 0);
270
+ if (totalChars > TOOL_RESULT_TOKEN_LIMIT * 4) {
271
+ const truncateAt = Math.floor(result.length * TOOL_RESULT_TOKEN_LIMIT * 4 / totalChars);
272
+ return [...result.slice(0, truncateAt), TRUNCATION_GUIDANCE];
273
+ }
274
+ return result;
275
+ }
276
+ if (result.length > TOOL_RESULT_TOKEN_LIMIT * 4) return result.substring(0, TOOL_RESULT_TOKEN_LIMIT * 4) + "\n... [results truncated, try being more specific with your parameters]";
277
+ return result;
278
+ }
279
+ /**
263
280
  * Validate and normalize a directory path.
264
281
  *
265
282
  * Ensures paths are safe to use by preventing directory traversal attacks
@@ -839,7 +856,9 @@ function createLsTool(backend, options) {
839
856
  const size = info.size ? ` (${info.size} bytes)` : "";
840
857
  lines.push(`${info.path}${size}`);
841
858
  }
842
- return lines.join("\n");
859
+ const result = truncateIfTooLong(lines);
860
+ if (Array.isArray(result)) return result.join("\n");
861
+ return result;
843
862
  }, {
844
863
  name: "ls",
845
864
  description: customDescription || LS_TOOL_DESCRIPTION,
@@ -957,7 +976,9 @@ function createGlobTool(backend, options) {
957
976
  const { pattern, path = "/" } = input;
958
977
  const infos = await resolvedBackend.globInfo(pattern, path);
959
978
  if (infos.length === 0) return `No files found matching pattern '${pattern}'`;
960
- return infos.map((info) => info.path).join("\n");
979
+ const result = truncateIfTooLong(infos.map((info) => info.path));
980
+ if (Array.isArray(result)) return result.join("\n");
981
+ return result;
961
982
  }, {
962
983
  name: "glob",
963
984
  description: customDescription || GLOB_TOOL_DESCRIPTION,
@@ -990,7 +1011,9 @@ function createGrepTool(backend, options) {
990
1011
  }
991
1012
  lines.push(` ${match.line}: ${match.text}`);
992
1013
  }
993
- return lines.join("\n");
1014
+ const truncated = truncateIfTooLong(lines);
1015
+ if (Array.isArray(truncated)) return truncated.join("\n");
1016
+ return truncated;
994
1017
  }, {
995
1018
  name: "grep",
996
1019
  description: customDescription || GREP_TOOL_DESCRIPTION,
@@ -1085,7 +1108,13 @@ function createFilesystemMiddleware(options = {}) {
1085
1108
  message: new langchain.ToolMessage({
1086
1109
  content: TOO_LARGE_TOOL_MSG.replace("{tool_call_id}", msg.tool_call_id).replace("{file_path}", evictPath).replace("{content_sample}", contentSample),
1087
1110
  tool_call_id: msg.tool_call_id,
1088
- name: msg.name
1111
+ name: msg.name,
1112
+ id: msg.id,
1113
+ artifact: msg.artifact,
1114
+ status: msg.status,
1115
+ metadata: msg.metadata,
1116
+ additional_kwargs: msg.additional_kwargs,
1117
+ response_metadata: msg.response_metadata
1089
1118
  }),
1090
1119
  filesUpdate: writeResult.filesUpdate
1091
1120
  };
@@ -1363,16 +1392,28 @@ function filterStateForSubagent(state) {
1363
1392
  return filtered;
1364
1393
  }
1365
1394
  /**
1395
+ * Invalid tool message block types
1396
+ */
1397
+ const INVALID_TOOL_MESSAGE_BLOCK_TYPES = [
1398
+ "tool_use",
1399
+ "thinking",
1400
+ "redacted_thinking"
1401
+ ];
1402
+ /**
1366
1403
  * Create Command with filtered state update from subagent result
1367
1404
  */
1368
1405
  function returnCommandWithStateUpdate(result, toolCallId) {
1369
1406
  const stateUpdate = filterStateForSubagent(result);
1370
1407
  const messages = result.messages;
1371
- const lastMessage = messages?.[messages.length - 1];
1408
+ let content = (messages?.[messages.length - 1])?.content || "Task completed";
1409
+ if (Array.isArray(content)) {
1410
+ content = content.filter((block) => !INVALID_TOOL_MESSAGE_BLOCK_TYPES.includes(block.type));
1411
+ if (content.length === 0) content = "Task completed";
1412
+ }
1372
1413
  return new _langchain_langgraph.Command({ update: {
1373
1414
  ...stateUpdate,
1374
1415
  messages: [new langchain.ToolMessage({
1375
- content: lastMessage?.content || "Task completed",
1416
+ content,
1376
1417
  tool_call_id: toolCallId,
1377
1418
  name: "task"
1378
1419
  })]
@@ -1444,7 +1485,16 @@ function createTaskTool(options) {
1444
1485
  const subagentState = filterStateForSubagent((0, _langchain_langgraph.getCurrentTaskInput)());
1445
1486
  subagentState.messages = [new _langchain_core_messages.HumanMessage({ content: description })];
1446
1487
  const result = await subagent.invoke(subagentState, config);
1447
- if (!config.toolCall?.id) throw new Error("Tool call ID is required for subagent invocation");
1488
+ if (!config.toolCall?.id) {
1489
+ const messages = result.messages;
1490
+ let content = (messages?.[messages.length - 1])?.content || "Task completed";
1491
+ if (Array.isArray(content)) {
1492
+ content = content.filter((block) => !INVALID_TOOL_MESSAGE_BLOCK_TYPES.includes(block.type));
1493
+ if (content.length === 0) return "Task completed";
1494
+ return content.map((block) => "text" in block ? block.text : JSON.stringify(block)).join("\n");
1495
+ }
1496
+ return content;
1497
+ }
1448
1498
  return returnCommandWithStateUpdate(result, config.toolCall.id);
1449
1499
  }, {
1450
1500
  name: "task",
@@ -1781,7 +1831,7 @@ async function loadMemoryFromBackend(backend, path) {
1781
1831
  * ```
1782
1832
  */
1783
1833
  function createMemoryMiddleware(options) {
1784
- const { backend, sources } = options;
1834
+ const { backend, sources, addCacheControl = false } = options;
1785
1835
  /**
1786
1836
  * Resolve backend from instance or factory.
1787
1837
  */
@@ -1806,7 +1856,16 @@ function createMemoryMiddleware(options) {
1806
1856
  },
1807
1857
  wrapModelCall(request, handler) {
1808
1858
  const formattedContents = formatMemoryContents(request.state?.memoryContents || {}, sources);
1809
- const newSystemMessage = new langchain.SystemMessage(MEMORY_SYSTEM_PROMPT.replace("{memory_contents}", formattedContents)).concat(request.systemMessage);
1859
+ const memorySection = MEMORY_SYSTEM_PROMPT.replace("{memory_contents}", formattedContents);
1860
+ const existingContent = request.systemMessage.content;
1861
+ const newSystemMessage = new langchain.SystemMessage({ content: [...typeof existingContent === "string" ? [{
1862
+ type: "text",
1863
+ text: existingContent
1864
+ }] : Array.isArray(existingContent) ? existingContent : [], {
1865
+ type: "text",
1866
+ text: memorySection,
1867
+ ...addCacheControl && { cache_control: { type: "ephemeral" } }
1868
+ }] });
1810
1869
  return handler({
1811
1870
  ...request,
1812
1871
  systemMessage: newSystemMessage
@@ -2702,31 +2761,44 @@ function createSummarizationMiddleware(options) {
2702
2761
  return messages.filter((msg) => !isSummaryMessage(msg));
2703
2762
  }
2704
2763
  /**
2705
- * Offload messages to backend.
2764
+ * Offload messages to backend by appending to the history file.
2765
+ *
2766
+ * Uses uploadFiles() directly with raw byte concatenation instead of
2767
+ * edit() to avoid downloading the file twice and performing a full
2768
+ * string search-and-replace. This keeps peak memory at ~2x file size
2769
+ * (existing bytes + combined bytes) instead of ~6x with the old
2770
+ * download → edit(oldContent, newContent) approach.
2706
2771
  */
2707
2772
  async function offloadToBackend(resolvedBackend, messages, state) {
2708
- const path = getHistoryPath(state);
2773
+ const filePath = getHistoryPath(state);
2709
2774
  const filteredMessages = filterSummaryMessages(messages);
2710
2775
  const newSection = `## Summarized at ${(/* @__PURE__ */ new Date()).toISOString()}\n\n${(0, _langchain_core_messages.getBufferString)(filteredMessages)}\n\n`;
2711
- let existingContent = "";
2712
- try {
2713
- if (resolvedBackend.downloadFiles) {
2714
- const responses = await resolvedBackend.downloadFiles([path]);
2715
- if (responses.length > 0 && responses[0].content && !responses[0].error) existingContent = new TextDecoder().decode(responses[0].content);
2716
- }
2717
- } catch {}
2718
- const combinedContent = existingContent + newSection;
2776
+ const sectionBytes = new TextEncoder().encode(newSection);
2719
2777
  try {
2778
+ let existingBytes = null;
2779
+ if (resolvedBackend.downloadFiles) try {
2780
+ const responses = await resolvedBackend.downloadFiles([filePath]);
2781
+ if (responses.length > 0 && responses[0].content && !responses[0].error) existingBytes = responses[0].content;
2782
+ } catch {}
2720
2783
  let result;
2721
- if (existingContent) result = await resolvedBackend.edit(path, existingContent, combinedContent);
2722
- else result = await resolvedBackend.write(path, combinedContent);
2784
+ if (existingBytes && resolvedBackend.uploadFiles) {
2785
+ const combined = new Uint8Array(existingBytes.byteLength + sectionBytes.byteLength);
2786
+ combined.set(existingBytes, 0);
2787
+ combined.set(sectionBytes, existingBytes.byteLength);
2788
+ const uploadResults = await resolvedBackend.uploadFiles([[filePath, combined]]);
2789
+ result = uploadResults[0].error ? { error: uploadResults[0].error } : { path: filePath };
2790
+ } else if (!existingBytes) result = await resolvedBackend.write(filePath, newSection);
2791
+ else {
2792
+ const existingContent = new TextDecoder().decode(existingBytes);
2793
+ result = await resolvedBackend.edit(filePath, existingContent, existingContent + newSection);
2794
+ }
2723
2795
  if (result.error) {
2724
- console.warn(`Failed to offload conversation history to ${path}: ${result.error}`);
2796
+ console.warn(`Failed to offload conversation history to ${filePath}: ${result.error}`);
2725
2797
  return null;
2726
2798
  }
2727
- return path;
2799
+ return filePath;
2728
2800
  } catch (e) {
2729
- console.warn(`Exception offloading conversation history to ${path}:`, e);
2801
+ console.warn(`Exception offloading conversation history to ${filePath}:`, e);
2730
2802
  return null;
2731
2803
  }
2732
2804
  }
@@ -2804,9 +2876,10 @@ ${summary}
2804
2876
  */
2805
2877
  function isContextOverflow(err) {
2806
2878
  let cause = err;
2807
- while (cause != null) {
2879
+ for (;;) {
2880
+ if (!cause) break;
2808
2881
  if (_langchain_core_errors.ContextOverflowError.isInstance(cause)) return true;
2809
- cause = typeof cause === "object" && cause !== null && "cause" in cause ? cause.cause : void 0;
2882
+ cause = typeof cause === "object" && "cause" in cause ? cause.cause : void 0;
2810
2883
  }
2811
2884
  return false;
2812
2885
  }
@@ -2916,18 +2989,43 @@ ${summary}
2916
2989
 
2917
2990
  //#endregion
2918
2991
  //#region src/backends/store.ts
2992
+ const NAMESPACE_COMPONENT_RE = /^[A-Za-z0-9\-_.@+:~]+$/;
2993
+ /**
2994
+ * Validate a namespace array.
2995
+ *
2996
+ * Each component must be a non-empty string containing only safe characters:
2997
+ * alphanumeric (a-z, A-Z, 0-9), hyphen (-), underscore (_), dot (.),
2998
+ * at sign (@), plus (+), colon (:), and tilde (~).
2999
+ *
3000
+ * Characters like *, ?, [, ], {, } etc. are rejected to prevent
3001
+ * wildcard or glob injection in store lookups.
3002
+ */
3003
+ function validateNamespace(namespace) {
3004
+ if (namespace.length === 0) throw new Error("Namespace array must not be empty.");
3005
+ for (let i = 0; i < namespace.length; i++) {
3006
+ const component = namespace[i];
3007
+ if (typeof component !== "string") throw new TypeError(`Namespace component at index ${i} must be a string, got ${typeof component}.`);
3008
+ if (!component) throw new Error(`Namespace component at index ${i} must not be empty.`);
3009
+ if (!NAMESPACE_COMPONENT_RE.test(component)) throw new Error(`Namespace component at index ${i} contains disallowed characters: "${component}". Only alphanumeric characters, hyphens, underscores, dots, @, +, colons, and tildes are allowed.`);
3010
+ }
3011
+ return namespace;
3012
+ }
2919
3013
  /**
2920
3014
  * Backend that stores files in LangGraph's BaseStore (persistent).
2921
3015
  *
2922
3016
  * Uses LangGraph's Store for persistent, cross-conversation storage.
2923
3017
  * Files are organized via namespaces and persist across all threads.
2924
3018
  *
2925
- * The namespace can include an optional assistant_id for multi-agent isolation.
3019
+ * The namespace can be customized via a factory function for flexible
3020
+ * isolation patterns (user-scoped, org-scoped, etc.), or falls back
3021
+ * to legacy assistant_id-based isolation.
2926
3022
  */
2927
3023
  var StoreBackend = class {
2928
3024
  stateAndStore;
2929
- constructor(stateAndStore) {
3025
+ _namespace;
3026
+ constructor(stateAndStore, options) {
2930
3027
  this.stateAndStore = stateAndStore;
3028
+ if (options?.namespace) this._namespace = validateNamespace(options.namespace);
2931
3029
  }
2932
3030
  /**
2933
3031
  * Get the store instance.
@@ -2943,15 +3041,17 @@ var StoreBackend = class {
2943
3041
  /**
2944
3042
  * Get the namespace for store operations.
2945
3043
  *
2946
- * If an assistant_id is available in stateAndStore, return
2947
- * [assistant_id, "filesystem"] to provide per-assistant isolation.
2948
- * Otherwise return ["filesystem"].
3044
+ * If a custom namespace was provided, returns it directly.
3045
+ *
3046
+ * Otherwise, falls back to legacy behavior:
3047
+ * - If assistantId is set: [assistantId, "filesystem"]
3048
+ * - Otherwise: ["filesystem"]
2949
3049
  */
2950
3050
  getNamespace() {
2951
- const namespace = "filesystem";
3051
+ if (this._namespace) return this._namespace;
2952
3052
  const assistantId = this.stateAndStore.assistantId;
2953
- if (assistantId) return [assistantId, namespace];
2954
- return [namespace];
3053
+ if (assistantId) return [assistantId, "filesystem"];
3054
+ return ["filesystem"];
2955
3055
  }
2956
3056
  /**
2957
3057
  * Convert a store Item to FileData format.
@@ -3770,6 +3870,10 @@ var CompositeBackend = class {
3770
3870
  this.routes = routes;
3771
3871
  this.sortedRoutes = Object.entries(routes).sort((a, b) => b[0].length - a[0].length);
3772
3872
  }
3873
+ /** Delegates to default backend's id if it is a sandbox, otherwise empty string. */
3874
+ get id() {
3875
+ return isSandboxBackend(this.default) ? this.default.id : "";
3876
+ }
3773
3877
  /**
3774
3878
  * Determine which backend handles this key and strip prefix.
3775
3879
  *
@@ -4611,17 +4715,85 @@ var BaseSandbox = class {
4611
4715
  *
4612
4716
  * Uses downloadFiles() to read, performs string replacement in TypeScript,
4613
4717
  * then uploadFiles() to write back. No runtime needed on the sandbox host.
4718
+ *
4719
+ * Memory-conscious: releases intermediate references early so the GC can
4720
+ * reclaim buffers before the next large allocation is made.
4614
4721
  */
4615
4722
  async edit(filePath, oldString, newString, replaceAll = false) {
4616
4723
  const results = await this.downloadFiles([filePath]);
4617
4724
  if (results[0].error || !results[0].content) return { error: `Error: File '${filePath}' not found` };
4618
4725
  const text = new TextDecoder().decode(results[0].content);
4619
- const count = text.split(oldString).length - 1;
4620
- if (count === 0) return { error: `String not found in file '${filePath}'` };
4621
- if (count > 1 && !replaceAll) return { error: `Multiple occurrences found in '${filePath}'. Use replaceAll=true to replace all.` };
4622
- const newText = replaceAll ? text.split(oldString).join(newString) : text.replace(oldString, newString);
4623
- const encoder = new TextEncoder();
4624
- const uploadResults = await this.uploadFiles([[filePath, encoder.encode(newText)]]);
4726
+ results[0].content = null;
4727
+ /**
4728
+ * are we editing an empty file?
4729
+ */
4730
+ if (oldString.length === 0) {
4731
+ /**
4732
+ * if the file is not empty, we cannot edit it with an empty oldString
4733
+ */
4734
+ if (text.length !== 0) return { error: "oldString must not be empty unless the file is empty" };
4735
+ /**
4736
+ * if the newString is empty, we can just return the file as is
4737
+ */
4738
+ if (newString.length === 0) return {
4739
+ path: filePath,
4740
+ filesUpdate: null,
4741
+ occurrences: 0
4742
+ };
4743
+ /**
4744
+ * if the newString is not empty, we can edit the file
4745
+ */
4746
+ const encoded = new TextEncoder().encode(newString);
4747
+ const uploadResults = await this.uploadFiles([[filePath, encoded]]);
4748
+ /**
4749
+ * if the upload fails, we return an error
4750
+ */
4751
+ if (uploadResults[0].error) return { error: `Failed to write edited file '${filePath}': ${uploadResults[0].error}` };
4752
+ return {
4753
+ path: filePath,
4754
+ filesUpdate: null,
4755
+ occurrences: 1
4756
+ };
4757
+ }
4758
+ const firstIdx = text.indexOf(oldString);
4759
+ if (firstIdx === -1) return { error: `String not found in file '${filePath}'` };
4760
+ if (oldString === newString) return {
4761
+ path: filePath,
4762
+ filesUpdate: null,
4763
+ occurrences: 1
4764
+ };
4765
+ let newText;
4766
+ let count;
4767
+ if (replaceAll) {
4768
+ newText = text.replaceAll(oldString, newString);
4769
+ /**
4770
+ * Derive count from the length delta to avoid a separate O(n) counting pass
4771
+ */
4772
+ const lenDiff = oldString.length - newString.length;
4773
+ if (lenDiff !== 0) count = (text.length - newText.length) / lenDiff;
4774
+ else {
4775
+ /**
4776
+ * Lengths are equal — count via indexOf (we already found the first)
4777
+ */
4778
+ count = 1;
4779
+ let pos = firstIdx + oldString.length;
4780
+ while (pos <= text.length) {
4781
+ const idx = text.indexOf(oldString, pos);
4782
+ if (idx === -1) break;
4783
+ count++;
4784
+ pos = idx + oldString.length;
4785
+ }
4786
+ }
4787
+ } else {
4788
+ if (text.indexOf(oldString, firstIdx + oldString.length) !== -1) return { error: `Multiple occurrences found in '${filePath}'. Use replaceAll=true to replace all.` };
4789
+ count = 1;
4790
+ /**
4791
+ * Build result from the known index — avoids a redundant search by .replace()
4792
+ */
4793
+ newText = text.slice(0, firstIdx) + newString + text.slice(firstIdx + oldString.length);
4794
+ }
4795
+ const encoded = new TextEncoder().encode(newText);
4796
+ const uploadResults = await this.uploadFiles([[filePath, encoded]]);
4625
4797
  if (uploadResults[0].error) return { error: `Failed to write edited file '${filePath}': ${uploadResults[0].error}` };
4626
4798
  return {
4627
4799
  path: filePath,
@@ -4631,10 +4803,66 @@ var BaseSandbox = class {
4631
4803
  }
4632
4804
  };
4633
4805
 
4806
+ //#endregion
4807
+ //#region src/middleware/cache.ts
4808
+ /**
4809
+ * Creates a middleware that places a cache breakpoint at the end of the static
4810
+ * system prompt content.
4811
+ *
4812
+ * This middleware tags the last block of the system message with
4813
+ * `cache_control: { type: "ephemeral" }` at the time it runs, capturing all
4814
+ * static content injected by preceding middleware (e.g. todo list instructions,
4815
+ * filesystem tools, subagent instructions) in a single cache breakpoint.
4816
+ *
4817
+ * This should run after all static system prompt middleware and before any
4818
+ * dynamic middleware (e.g. memory) so the breakpoint sits at the boundary
4819
+ * between stable and changing content.
4820
+ *
4821
+ * When used alongside memory middleware (which adds its own breakpoint on the
4822
+ * memory block), the result is two separate cache breakpoints:
4823
+ * - One covering all static content
4824
+ * - One covering the memory block
4825
+ *
4826
+ * This is a no-op when the system message has no content blocks.
4827
+ */
4828
+ function createCacheBreakpointMiddleware() {
4829
+ return (0, langchain.createMiddleware)({
4830
+ name: "CacheBreakpointMiddleware",
4831
+ wrapModelCall(request, handler) {
4832
+ const existingContent = request.systemMessage.content;
4833
+ const existingBlocks = typeof existingContent === "string" ? [{
4834
+ type: "text",
4835
+ text: existingContent
4836
+ }] : Array.isArray(existingContent) ? [...existingContent] : [];
4837
+ if (existingBlocks.length === 0) return handler(request);
4838
+ existingBlocks[existingBlocks.length - 1] = {
4839
+ ...existingBlocks[existingBlocks.length - 1],
4840
+ cache_control: { type: "ephemeral" }
4841
+ };
4842
+ return handler({
4843
+ ...request,
4844
+ systemMessage: new langchain.SystemMessage({ content: existingBlocks })
4845
+ });
4846
+ }
4847
+ });
4848
+ }
4849
+
4634
4850
  //#endregion
4635
4851
  //#region src/agent.ts
4636
4852
  const BASE_PROMPT = `In order to complete the objective that the user asks of you, you have access to a number of standard tools.`;
4637
4853
  /**
4854
+ * Detect whether a model is an Anthropic model.
4855
+ * Used to gate Anthropic-specific prompt caching optimizations (cache_control breakpoints).
4856
+ */
4857
+ function isAnthropicModel(model) {
4858
+ if (typeof model === "string") {
4859
+ if (model.includes(":")) return model.split(":")[0] === "anthropic";
4860
+ return model.startsWith("claude");
4861
+ }
4862
+ if (model.getName() === "ConfigurableModel") return model._defaultConfig?.modelProvider === "anthropic";
4863
+ return model.getName() === "ChatAnthropic";
4864
+ }
4865
+ /**
4638
4866
  * Create a Deep Agent with middleware-based architecture.
4639
4867
  *
4640
4868
  * Matches Python's create_deep_agent function, using middleware for all features:
@@ -4667,16 +4895,20 @@ const BASE_PROMPT = `In order to complete the objective that the user asks of yo
4667
4895
  */
4668
4896
  function createDeepAgent(params = {}) {
4669
4897
  const { model = "claude-sonnet-4-5-20250929", tools = [], systemPrompt, middleware: customMiddleware = [], subagents = [], responseFormat, contextSchema, checkpointer, store, backend, interruptOn, name, memory, skills } = params;
4670
- /**
4671
- * Combine system prompt with base prompt like Python implementation
4672
- */
4673
- const finalSystemPrompt = systemPrompt ? typeof systemPrompt === "string" ? `${systemPrompt}\n\n${BASE_PROMPT}` : new langchain.SystemMessage({ content: [{
4898
+ const anthropicModel = isAnthropicModel(model);
4899
+ const finalSystemPrompt = new langchain.SystemMessage({ content: systemPrompt ? typeof systemPrompt === "string" ? [{
4900
+ type: "text",
4901
+ text: `${systemPrompt}\n\n${BASE_PROMPT}`
4902
+ }] : [{
4674
4903
  type: "text",
4675
4904
  text: BASE_PROMPT
4676
4905
  }, ...typeof systemPrompt.content === "string" ? [{
4677
4906
  type: "text",
4678
4907
  text: systemPrompt.content
4679
- }] : systemPrompt.content] }) : BASE_PROMPT;
4908
+ }] : systemPrompt.content] : [{
4909
+ type: "text",
4910
+ text: BASE_PROMPT
4911
+ }] });
4680
4912
  /**
4681
4913
  * Create backend configuration for filesystem middleware
4682
4914
  * If no backend is provided, use a factory that creates a StateBackend
@@ -4694,7 +4926,8 @@ function createDeepAgent(params = {}) {
4694
4926
  */
4695
4927
  const memoryMiddlewareArray = memory != null && memory.length > 0 ? [createMemoryMiddleware({
4696
4928
  backend: filesystemBackend,
4697
- sources: memory
4929
+ sources: memory,
4930
+ addCacheControl: anthropicModel
4698
4931
  })] : [];
4699
4932
  /**
4700
4933
  * Process subagents to add SkillsMiddleware for those with their own skills.
@@ -4743,7 +4976,10 @@ function createDeepAgent(params = {}) {
4743
4976
  model,
4744
4977
  backend: filesystemBackend
4745
4978
  }),
4746
- (0, langchain.anthropicPromptCachingMiddleware)({ unsupportedModelBehavior: "ignore" }),
4979
+ (0, langchain.anthropicPromptCachingMiddleware)({
4980
+ unsupportedModelBehavior: "ignore",
4981
+ minMessagesToCache: 1
4982
+ }),
4747
4983
  createPatchToolCallsMiddleware()
4748
4984
  ];
4749
4985
  /**
@@ -4766,8 +5002,12 @@ function createDeepAgent(params = {}) {
4766
5002
  createSubAgentMiddleware({
4767
5003
  defaultModel: model,
4768
5004
  defaultTools: tools,
4769
- defaultMiddleware: subagentMiddleware,
4770
- generalPurposeMiddleware: [...subagentMiddleware, ...skillsMiddlewareArray],
5005
+ defaultMiddleware: [...subagentMiddleware, ...anthropicModel ? [createCacheBreakpointMiddleware()] : []],
5006
+ generalPurposeMiddleware: [
5007
+ ...subagentMiddleware,
5008
+ ...skillsMiddlewareArray,
5009
+ ...anthropicModel ? [createCacheBreakpointMiddleware()] : []
5010
+ ],
4771
5011
  defaultInterruptOn: interruptOn,
4772
5012
  subagents: processedSubagents,
4773
5013
  generalPurposeAgent: true
@@ -4776,10 +5016,14 @@ function createDeepAgent(params = {}) {
4776
5016
  model,
4777
5017
  backend: filesystemBackend
4778
5018
  }),
4779
- (0, langchain.anthropicPromptCachingMiddleware)({ unsupportedModelBehavior: "ignore" }),
5019
+ (0, langchain.anthropicPromptCachingMiddleware)({
5020
+ unsupportedModelBehavior: "ignore",
5021
+ minMessagesToCache: 1
5022
+ }),
4780
5023
  createPatchToolCallsMiddleware()
4781
5024
  ],
4782
5025
  ...skillsMiddlewareArray,
5026
+ ...anthropicModel ? [createCacheBreakpointMiddleware()] : [],
4783
5027
  ...memoryMiddlewareArray,
4784
5028
  ...interruptOn ? [(0, langchain.humanInTheLoopMiddleware)({ interruptOn })] : [],
4785
5029
  ...customMiddleware