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 +55 -3
- package/dist/index.cjs +294 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -6
- package/dist/index.d.ts +50 -6
- package/dist/index.js +294 -50
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
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-
|
|
5
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/langchain-ai/deepagentsjs/refs/heads/main/.github/images/logo-
|
|
6
|
-
<img alt="Deep Agents Logo" src="https://raw.githubusercontent.com/langchain-ai/deepagentsjs/refs/heads/main/.github/images/logo-dark.svg" width="
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
2722
|
-
|
|
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 ${
|
|
2796
|
+
console.warn(`Failed to offload conversation history to ${filePath}: ${result.error}`);
|
|
2725
2797
|
return null;
|
|
2726
2798
|
}
|
|
2727
|
-
return
|
|
2799
|
+
return filePath;
|
|
2728
2800
|
} catch (e) {
|
|
2729
|
-
console.warn(`Exception offloading conversation history to ${
|
|
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
|
-
|
|
2879
|
+
for (;;) {
|
|
2880
|
+
if (!cause) break;
|
|
2808
2881
|
if (_langchain_core_errors.ContextOverflowError.isInstance(cause)) return true;
|
|
2809
|
-
cause = typeof cause === "object" &&
|
|
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
|
|
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
|
-
|
|
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
|
|
2947
|
-
*
|
|
2948
|
-
* Otherwise
|
|
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
|
-
|
|
3051
|
+
if (this._namespace) return this._namespace;
|
|
2952
3052
|
const assistantId = this.stateAndStore.assistantId;
|
|
2953
|
-
if (assistantId) return [assistantId,
|
|
2954
|
-
return [
|
|
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
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
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
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
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]
|
|
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)({
|
|
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: [
|
|
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)({
|
|
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
|