deepagents 1.8.0 → 1.8.1
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/dist/index.cjs +158 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -6
- package/dist/index.d.ts +41 -6
- package/dist/index.js +158 -32
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.cjs
CHANGED
|
@@ -1085,7 +1085,13 @@ function createFilesystemMiddleware(options = {}) {
|
|
|
1085
1085
|
message: new langchain.ToolMessage({
|
|
1086
1086
|
content: TOO_LARGE_TOOL_MSG.replace("{tool_call_id}", msg.tool_call_id).replace("{file_path}", evictPath).replace("{content_sample}", contentSample),
|
|
1087
1087
|
tool_call_id: msg.tool_call_id,
|
|
1088
|
-
name: msg.name
|
|
1088
|
+
name: msg.name,
|
|
1089
|
+
id: msg.id,
|
|
1090
|
+
artifact: msg.artifact,
|
|
1091
|
+
status: msg.status,
|
|
1092
|
+
metadata: msg.metadata,
|
|
1093
|
+
additional_kwargs: msg.additional_kwargs,
|
|
1094
|
+
response_metadata: msg.response_metadata
|
|
1089
1095
|
}),
|
|
1090
1096
|
filesUpdate: writeResult.filesUpdate
|
|
1091
1097
|
};
|
|
@@ -1363,16 +1369,28 @@ function filterStateForSubagent(state) {
|
|
|
1363
1369
|
return filtered;
|
|
1364
1370
|
}
|
|
1365
1371
|
/**
|
|
1372
|
+
* Invalid tool message block types
|
|
1373
|
+
*/
|
|
1374
|
+
const INVALID_TOOL_MESSAGE_BLOCK_TYPES = [
|
|
1375
|
+
"tool_use",
|
|
1376
|
+
"thinking",
|
|
1377
|
+
"redacted_thinking"
|
|
1378
|
+
];
|
|
1379
|
+
/**
|
|
1366
1380
|
* Create Command with filtered state update from subagent result
|
|
1367
1381
|
*/
|
|
1368
1382
|
function returnCommandWithStateUpdate(result, toolCallId) {
|
|
1369
1383
|
const stateUpdate = filterStateForSubagent(result);
|
|
1370
1384
|
const messages = result.messages;
|
|
1371
|
-
|
|
1385
|
+
let content = (messages?.[messages.length - 1])?.content || "Task completed";
|
|
1386
|
+
if (Array.isArray(content)) {
|
|
1387
|
+
content = content.filter((block) => !INVALID_TOOL_MESSAGE_BLOCK_TYPES.includes(block.type));
|
|
1388
|
+
if (content.length === 0) content = "Task completed";
|
|
1389
|
+
}
|
|
1372
1390
|
return new _langchain_langgraph.Command({ update: {
|
|
1373
1391
|
...stateUpdate,
|
|
1374
1392
|
messages: [new langchain.ToolMessage({
|
|
1375
|
-
content
|
|
1393
|
+
content,
|
|
1376
1394
|
tool_call_id: toolCallId,
|
|
1377
1395
|
name: "task"
|
|
1378
1396
|
})]
|
|
@@ -2702,31 +2720,44 @@ function createSummarizationMiddleware(options) {
|
|
|
2702
2720
|
return messages.filter((msg) => !isSummaryMessage(msg));
|
|
2703
2721
|
}
|
|
2704
2722
|
/**
|
|
2705
|
-
* Offload messages to backend.
|
|
2723
|
+
* Offload messages to backend by appending to the history file.
|
|
2724
|
+
*
|
|
2725
|
+
* Uses uploadFiles() directly with raw byte concatenation instead of
|
|
2726
|
+
* edit() to avoid downloading the file twice and performing a full
|
|
2727
|
+
* string search-and-replace. This keeps peak memory at ~2x file size
|
|
2728
|
+
* (existing bytes + combined bytes) instead of ~6x with the old
|
|
2729
|
+
* download → edit(oldContent, newContent) approach.
|
|
2706
2730
|
*/
|
|
2707
2731
|
async function offloadToBackend(resolvedBackend, messages, state) {
|
|
2708
|
-
const
|
|
2732
|
+
const filePath = getHistoryPath(state);
|
|
2709
2733
|
const filteredMessages = filterSummaryMessages(messages);
|
|
2710
2734
|
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;
|
|
2735
|
+
const sectionBytes = new TextEncoder().encode(newSection);
|
|
2719
2736
|
try {
|
|
2737
|
+
let existingBytes = null;
|
|
2738
|
+
if (resolvedBackend.downloadFiles) try {
|
|
2739
|
+
const responses = await resolvedBackend.downloadFiles([filePath]);
|
|
2740
|
+
if (responses.length > 0 && responses[0].content && !responses[0].error) existingBytes = responses[0].content;
|
|
2741
|
+
} catch {}
|
|
2720
2742
|
let result;
|
|
2721
|
-
if (
|
|
2722
|
-
|
|
2743
|
+
if (existingBytes && resolvedBackend.uploadFiles) {
|
|
2744
|
+
const combined = new Uint8Array(existingBytes.byteLength + sectionBytes.byteLength);
|
|
2745
|
+
combined.set(existingBytes, 0);
|
|
2746
|
+
combined.set(sectionBytes, existingBytes.byteLength);
|
|
2747
|
+
const uploadResults = await resolvedBackend.uploadFiles([[filePath, combined]]);
|
|
2748
|
+
result = uploadResults[0].error ? { error: uploadResults[0].error } : { path: filePath };
|
|
2749
|
+
} else if (!existingBytes) result = await resolvedBackend.write(filePath, newSection);
|
|
2750
|
+
else {
|
|
2751
|
+
const existingContent = new TextDecoder().decode(existingBytes);
|
|
2752
|
+
result = await resolvedBackend.edit(filePath, existingContent, existingContent + newSection);
|
|
2753
|
+
}
|
|
2723
2754
|
if (result.error) {
|
|
2724
|
-
console.warn(`Failed to offload conversation history to ${
|
|
2755
|
+
console.warn(`Failed to offload conversation history to ${filePath}: ${result.error}`);
|
|
2725
2756
|
return null;
|
|
2726
2757
|
}
|
|
2727
|
-
return
|
|
2758
|
+
return filePath;
|
|
2728
2759
|
} catch (e) {
|
|
2729
|
-
console.warn(`Exception offloading conversation history to ${
|
|
2760
|
+
console.warn(`Exception offloading conversation history to ${filePath}:`, e);
|
|
2730
2761
|
return null;
|
|
2731
2762
|
}
|
|
2732
2763
|
}
|
|
@@ -2916,18 +2947,43 @@ ${summary}
|
|
|
2916
2947
|
|
|
2917
2948
|
//#endregion
|
|
2918
2949
|
//#region src/backends/store.ts
|
|
2950
|
+
const NAMESPACE_COMPONENT_RE = /^[A-Za-z0-9\-_.@+:~]+$/;
|
|
2951
|
+
/**
|
|
2952
|
+
* Validate a namespace array.
|
|
2953
|
+
*
|
|
2954
|
+
* Each component must be a non-empty string containing only safe characters:
|
|
2955
|
+
* alphanumeric (a-z, A-Z, 0-9), hyphen (-), underscore (_), dot (.),
|
|
2956
|
+
* at sign (@), plus (+), colon (:), and tilde (~).
|
|
2957
|
+
*
|
|
2958
|
+
* Characters like *, ?, [, ], {, } etc. are rejected to prevent
|
|
2959
|
+
* wildcard or glob injection in store lookups.
|
|
2960
|
+
*/
|
|
2961
|
+
function validateNamespace(namespace) {
|
|
2962
|
+
if (namespace.length === 0) throw new Error("Namespace array must not be empty.");
|
|
2963
|
+
for (let i = 0; i < namespace.length; i++) {
|
|
2964
|
+
const component = namespace[i];
|
|
2965
|
+
if (typeof component !== "string") throw new TypeError(`Namespace component at index ${i} must be a string, got ${typeof component}.`);
|
|
2966
|
+
if (!component) throw new Error(`Namespace component at index ${i} must not be empty.`);
|
|
2967
|
+
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.`);
|
|
2968
|
+
}
|
|
2969
|
+
return namespace;
|
|
2970
|
+
}
|
|
2919
2971
|
/**
|
|
2920
2972
|
* Backend that stores files in LangGraph's BaseStore (persistent).
|
|
2921
2973
|
*
|
|
2922
2974
|
* Uses LangGraph's Store for persistent, cross-conversation storage.
|
|
2923
2975
|
* Files are organized via namespaces and persist across all threads.
|
|
2924
2976
|
*
|
|
2925
|
-
* The namespace can
|
|
2977
|
+
* The namespace can be customized via a factory function for flexible
|
|
2978
|
+
* isolation patterns (user-scoped, org-scoped, etc.), or falls back
|
|
2979
|
+
* to legacy assistant_id-based isolation.
|
|
2926
2980
|
*/
|
|
2927
2981
|
var StoreBackend = class {
|
|
2928
2982
|
stateAndStore;
|
|
2929
|
-
|
|
2983
|
+
_namespace;
|
|
2984
|
+
constructor(stateAndStore, options) {
|
|
2930
2985
|
this.stateAndStore = stateAndStore;
|
|
2986
|
+
if (options?.namespace) this._namespace = validateNamespace(options.namespace);
|
|
2931
2987
|
}
|
|
2932
2988
|
/**
|
|
2933
2989
|
* Get the store instance.
|
|
@@ -2943,15 +2999,17 @@ var StoreBackend = class {
|
|
|
2943
2999
|
/**
|
|
2944
3000
|
* Get the namespace for store operations.
|
|
2945
3001
|
*
|
|
2946
|
-
* If
|
|
2947
|
-
*
|
|
2948
|
-
* Otherwise
|
|
3002
|
+
* If a custom namespace was provided, returns it directly.
|
|
3003
|
+
*
|
|
3004
|
+
* Otherwise, falls back to legacy behavior:
|
|
3005
|
+
* - If assistantId is set: [assistantId, "filesystem"]
|
|
3006
|
+
* - Otherwise: ["filesystem"]
|
|
2949
3007
|
*/
|
|
2950
3008
|
getNamespace() {
|
|
2951
|
-
|
|
3009
|
+
if (this._namespace) return this._namespace;
|
|
2952
3010
|
const assistantId = this.stateAndStore.assistantId;
|
|
2953
|
-
if (assistantId) return [assistantId,
|
|
2954
|
-
return [
|
|
3011
|
+
if (assistantId) return [assistantId, "filesystem"];
|
|
3012
|
+
return ["filesystem"];
|
|
2955
3013
|
}
|
|
2956
3014
|
/**
|
|
2957
3015
|
* Convert a store Item to FileData format.
|
|
@@ -4611,17 +4669,85 @@ var BaseSandbox = class {
|
|
|
4611
4669
|
*
|
|
4612
4670
|
* Uses downloadFiles() to read, performs string replacement in TypeScript,
|
|
4613
4671
|
* then uploadFiles() to write back. No runtime needed on the sandbox host.
|
|
4672
|
+
*
|
|
4673
|
+
* Memory-conscious: releases intermediate references early so the GC can
|
|
4674
|
+
* reclaim buffers before the next large allocation is made.
|
|
4614
4675
|
*/
|
|
4615
4676
|
async edit(filePath, oldString, newString, replaceAll = false) {
|
|
4616
4677
|
const results = await this.downloadFiles([filePath]);
|
|
4617
4678
|
if (results[0].error || !results[0].content) return { error: `Error: File '${filePath}' not found` };
|
|
4618
4679
|
const text = new TextDecoder().decode(results[0].content);
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4680
|
+
results[0].content = null;
|
|
4681
|
+
/**
|
|
4682
|
+
* are we editing an empty file?
|
|
4683
|
+
*/
|
|
4684
|
+
if (oldString.length === 0) {
|
|
4685
|
+
/**
|
|
4686
|
+
* if the file is not empty, we cannot edit it with an empty oldString
|
|
4687
|
+
*/
|
|
4688
|
+
if (text.length !== 0) return { error: "oldString must not be empty unless the file is empty" };
|
|
4689
|
+
/**
|
|
4690
|
+
* if the newString is empty, we can just return the file as is
|
|
4691
|
+
*/
|
|
4692
|
+
if (newString.length === 0) return {
|
|
4693
|
+
path: filePath,
|
|
4694
|
+
filesUpdate: null,
|
|
4695
|
+
occurrences: 0
|
|
4696
|
+
};
|
|
4697
|
+
/**
|
|
4698
|
+
* if the newString is not empty, we can edit the file
|
|
4699
|
+
*/
|
|
4700
|
+
const encoded = new TextEncoder().encode(newString);
|
|
4701
|
+
const uploadResults = await this.uploadFiles([[filePath, encoded]]);
|
|
4702
|
+
/**
|
|
4703
|
+
* if the upload fails, we return an error
|
|
4704
|
+
*/
|
|
4705
|
+
if (uploadResults[0].error) return { error: `Failed to write edited file '${filePath}': ${uploadResults[0].error}` };
|
|
4706
|
+
return {
|
|
4707
|
+
path: filePath,
|
|
4708
|
+
filesUpdate: null,
|
|
4709
|
+
occurrences: 1
|
|
4710
|
+
};
|
|
4711
|
+
}
|
|
4712
|
+
const firstIdx = text.indexOf(oldString);
|
|
4713
|
+
if (firstIdx === -1) return { error: `String not found in file '${filePath}'` };
|
|
4714
|
+
if (oldString === newString) return {
|
|
4715
|
+
path: filePath,
|
|
4716
|
+
filesUpdate: null,
|
|
4717
|
+
occurrences: 1
|
|
4718
|
+
};
|
|
4719
|
+
let newText;
|
|
4720
|
+
let count;
|
|
4721
|
+
if (replaceAll) {
|
|
4722
|
+
newText = text.replaceAll(oldString, newString);
|
|
4723
|
+
/**
|
|
4724
|
+
* Derive count from the length delta to avoid a separate O(n) counting pass
|
|
4725
|
+
*/
|
|
4726
|
+
const lenDiff = oldString.length - newString.length;
|
|
4727
|
+
if (lenDiff !== 0) count = (text.length - newText.length) / lenDiff;
|
|
4728
|
+
else {
|
|
4729
|
+
/**
|
|
4730
|
+
* Lengths are equal — count via indexOf (we already found the first)
|
|
4731
|
+
*/
|
|
4732
|
+
count = 1;
|
|
4733
|
+
let pos = firstIdx + oldString.length;
|
|
4734
|
+
while (pos <= text.length) {
|
|
4735
|
+
const idx = text.indexOf(oldString, pos);
|
|
4736
|
+
if (idx === -1) break;
|
|
4737
|
+
count++;
|
|
4738
|
+
pos = idx + oldString.length;
|
|
4739
|
+
}
|
|
4740
|
+
}
|
|
4741
|
+
} else {
|
|
4742
|
+
if (text.indexOf(oldString, firstIdx + oldString.length) !== -1) return { error: `Multiple occurrences found in '${filePath}'. Use replaceAll=true to replace all.` };
|
|
4743
|
+
count = 1;
|
|
4744
|
+
/**
|
|
4745
|
+
* Build result from the known index — avoids a redundant search by .replace()
|
|
4746
|
+
*/
|
|
4747
|
+
newText = text.slice(0, firstIdx) + newString + text.slice(firstIdx + oldString.length);
|
|
4748
|
+
}
|
|
4749
|
+
const encoded = new TextEncoder().encode(newText);
|
|
4750
|
+
const uploadResults = await this.uploadFiles([[filePath, encoded]]);
|
|
4625
4751
|
if (uploadResults[0].error) return { error: `Failed to write edited file '${filePath}': ${uploadResults[0].error}` };
|
|
4626
4752
|
return {
|
|
4627
4753
|
path: filePath,
|