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 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
- const lastMessage = messages?.[messages.length - 1];
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: lastMessage?.content || "Task completed",
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 path = getHistoryPath(state);
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
- 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;
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 (existingContent) result = await resolvedBackend.edit(path, existingContent, combinedContent);
2722
- else result = await resolvedBackend.write(path, combinedContent);
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 ${path}: ${result.error}`);
2755
+ console.warn(`Failed to offload conversation history to ${filePath}: ${result.error}`);
2725
2756
  return null;
2726
2757
  }
2727
- return path;
2758
+ return filePath;
2728
2759
  } catch (e) {
2729
- console.warn(`Exception offloading conversation history to ${path}:`, e);
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 include an optional assistant_id for multi-agent isolation.
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
- constructor(stateAndStore) {
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 an assistant_id is available in stateAndStore, return
2947
- * [assistant_id, "filesystem"] to provide per-assistant isolation.
2948
- * Otherwise return ["filesystem"].
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
- const namespace = "filesystem";
3009
+ if (this._namespace) return this._namespace;
2952
3010
  const assistantId = this.stateAndStore.assistantId;
2953
- if (assistantId) return [assistantId, namespace];
2954
- return [namespace];
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
- 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)]]);
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,