deepagents 1.6.1 → 1.6.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/dist/index.cjs CHANGED
@@ -193,8 +193,14 @@ function formatReadResponse(fileData, offset, limit) {
193
193
  * @param newString - Replacement string
194
194
  * @param replaceAll - Whether to replace all occurrences
195
195
  * @returns Tuple of [new_content, occurrences] on success, or error message string
196
+ *
197
+ * Special case: When both content and oldString are empty, this sets the initial
198
+ * content to newString. This allows editing empty files by treating empty oldString
199
+ * as "set initial content" rather than "replace nothing".
196
200
  */
197
201
  function performStringReplacement(content, oldString, newString, replaceAll) {
202
+ if (content === "" && oldString === "") return [newString, 0];
203
+ if (oldString === "") return "Error: oldString cannot be empty when file has content";
198
204
  const occurrences = content.split(oldString).length - 1;
199
205
  if (occurrences === 0) return `Error: String not found in file: '${oldString}'`;
200
206
  if (occurrences > 1 && !replaceAll) return `Error: String '${oldString}' appears ${occurrences} times in file. Use replace_all=True to replace all instances, or provide a more specific string with surrounding context.`;
@@ -557,6 +563,18 @@ const TOOLS_EXCLUDED_FROM_EVICTION = [
557
563
  */
558
564
  const NUM_CHARS_PER_TOKEN = 4;
559
565
  /**
566
+ * Default values for read_file tool pagination (in lines).
567
+ */
568
+ const DEFAULT_READ_LINE_OFFSET = 0;
569
+ const DEFAULT_READ_LINE_LIMIT = 100;
570
+ /**
571
+ * Template for truncation message in read_file.
572
+ * {file_path} will be filled in at runtime.
573
+ */
574
+ const READ_FILE_TRUNCATION_MSG = `
575
+
576
+ [Output was truncated due to size limits. The file content is very large. Consider reformatting the file to make it easier to navigate. For example, if this is JSON, use execute(command='jq . {file_path}') to pretty-print it with line breaks. For other formats, you can use appropriate formatting tools to split long lines.]`;
577
+ /**
560
578
  * Message template for evicted tool results.
561
579
  */
562
580
  const TOO_LARGE_TOOL_MSG = `Tool result too large, the result of this tool call {tool_call_id} was saved in the filesystem at this path: {file_path}
@@ -661,7 +679,7 @@ const READ_FILE_TOOL_DESCRIPTION = `Reads a file from the filesystem.
661
679
  Assume this tool is able to read all files. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
662
680
 
663
681
  Usage:
664
- - By default, it reads up to 500 lines starting from the beginning of the file
682
+ - By default, it reads up to 100 lines starting from the beginning of the file
665
683
  - **IMPORTANT for large files and codebase exploration**: Use pagination with offset and limit parameters to avoid context overflow
666
684
  - First scan: read_file(path, limit=100) to see file structure
667
685
  - Read more sections: read_file(path, offset=100, limit=200) for next 200 lines
@@ -781,21 +799,29 @@ function createLsTool(backend, options) {
781
799
  * Create read_file tool using backend.
782
800
  */
783
801
  function createReadFileTool(backend, options) {
784
- const { customDescription } = options;
802
+ const { customDescription, toolTokenLimitBeforeEvict } = options;
785
803
  return (0, langchain.tool)(async (input, config) => {
786
804
  const resolvedBackend = getBackend(backend, {
787
805
  state: (0, _langchain_langgraph.getCurrentTaskInput)(config),
788
806
  store: config.store
789
807
  });
790
- const { file_path, offset = 0, limit = 500 } = input;
791
- return await resolvedBackend.read(file_path, offset, limit);
808
+ const { file_path, offset = DEFAULT_READ_LINE_OFFSET, limit = DEFAULT_READ_LINE_LIMIT } = input;
809
+ let result = await resolvedBackend.read(file_path, offset, limit);
810
+ const lines = result.split("\n");
811
+ if (lines.length > limit) result = lines.slice(0, limit).join("\n");
812
+ if (toolTokenLimitBeforeEvict && result.length >= NUM_CHARS_PER_TOKEN * toolTokenLimitBeforeEvict) {
813
+ const truncationMsg = READ_FILE_TRUNCATION_MSG.replace("{file_path}", file_path);
814
+ const maxContentLength = NUM_CHARS_PER_TOKEN * toolTokenLimitBeforeEvict - truncationMsg.length;
815
+ result = result.substring(0, maxContentLength) + truncationMsg;
816
+ }
817
+ return result;
792
818
  }, {
793
819
  name: "read_file",
794
820
  description: customDescription || READ_FILE_TOOL_DESCRIPTION,
795
821
  schema: zod_v4.z.object({
796
822
  file_path: zod_v4.z.string().describe("Absolute path to the file to read"),
797
- offset: zod_v4.z.coerce.number().optional().default(0).describe("Line offset to start reading from (0-indexed)"),
798
- limit: zod_v4.z.coerce.number().optional().default(500).describe("Maximum number of lines to read")
823
+ offset: zod_v4.z.coerce.number().optional().default(DEFAULT_READ_LINE_OFFSET).describe("Line offset to start reading from (0-indexed)"),
824
+ limit: zod_v4.z.coerce.number().optional().default(DEFAULT_READ_LINE_LIMIT).describe("Maximum number of lines to read")
799
825
  })
800
826
  });
801
827
  }
@@ -960,7 +986,10 @@ function createFilesystemMiddleware(options = {}) {
960
986
  stateSchema: FilesystemStateSchema,
961
987
  tools: [
962
988
  createLsTool(backend, { customDescription: customToolDescriptions?.ls }),
963
- createReadFileTool(backend, { customDescription: customToolDescriptions?.read_file }),
989
+ createReadFileTool(backend, {
990
+ customDescription: customToolDescriptions?.read_file,
991
+ toolTokenLimitBeforeEvict
992
+ }),
964
993
  createWriteFileTool(backend, { customDescription: customToolDescriptions?.write_file }),
965
994
  createEditFileTool(backend, { customDescription: customToolDescriptions?.edit_file }),
966
995
  createGlobTool(backend, { customDescription: customToolDescriptions?.glob }),
@@ -1324,12 +1353,52 @@ function createSubAgentMiddleware(options) {
1324
1353
  //#endregion
1325
1354
  //#region src/middleware/patch_tool_calls.ts
1326
1355
  /**
1356
+ * Patch dangling tool calls in a messages array.
1357
+ * Returns the patched messages array and a flag indicating if patching was needed.
1358
+ *
1359
+ * @param messages - The messages array to patch
1360
+ * @returns Object with patched messages and needsPatch flag
1361
+ */
1362
+ function patchDanglingToolCalls(messages) {
1363
+ if (!messages || messages.length === 0) return {
1364
+ patchedMessages: [],
1365
+ needsPatch: false
1366
+ };
1367
+ const patchedMessages = [];
1368
+ let needsPatch = false;
1369
+ for (let i = 0; i < messages.length; i++) {
1370
+ const msg = messages[i];
1371
+ patchedMessages.push(msg);
1372
+ if (langchain.AIMessage.isInstance(msg) && msg.tool_calls != null) {
1373
+ for (const toolCall of msg.tool_calls) if (!messages.slice(i).find((m) => langchain.ToolMessage.isInstance(m) && m.tool_call_id === toolCall.id)) {
1374
+ needsPatch = true;
1375
+ const toolMsg = `Tool call ${toolCall.name} with id ${toolCall.id} was cancelled - another message came in before it could be completed.`;
1376
+ patchedMessages.push(new langchain.ToolMessage({
1377
+ content: toolMsg,
1378
+ name: toolCall.name,
1379
+ tool_call_id: toolCall.id
1380
+ }));
1381
+ }
1382
+ }
1383
+ }
1384
+ return {
1385
+ patchedMessages,
1386
+ needsPatch
1387
+ };
1388
+ }
1389
+ /**
1327
1390
  * Create middleware that patches dangling tool calls in the messages history.
1328
1391
  *
1329
1392
  * When an AI message contains tool_calls but subsequent messages don't include
1330
1393
  * the corresponding ToolMessage responses, this middleware adds synthetic
1331
1394
  * ToolMessages saying the tool call was cancelled.
1332
1395
  *
1396
+ * This middleware patches in two places:
1397
+ * 1. `beforeAgent`: Patches state at the start of the agent loop (handles most cases)
1398
+ * 2. `wrapModelCall`: Patches the request right before model invocation (handles
1399
+ * edge cases like HITL rejection during graph resume where state updates from
1400
+ * beforeAgent may not be applied in time)
1401
+ *
1333
1402
  * @returns AgentMiddleware that patches dangling tool calls
1334
1403
  *
1335
1404
  * @example
@@ -1349,22 +1418,22 @@ function createPatchToolCallsMiddleware() {
1349
1418
  beforeAgent: async (state) => {
1350
1419
  const messages = state.messages;
1351
1420
  if (!messages || messages.length === 0) return;
1352
- const patchedMessages = [];
1353
- for (let i = 0; i < messages.length; i++) {
1354
- const msg = messages[i];
1355
- patchedMessages.push(msg);
1356
- if (langchain.AIMessage.isInstance(msg) && msg.tool_calls != null) {
1357
- for (const toolCall of msg.tool_calls) if (!messages.slice(i).find((m) => langchain.ToolMessage.isInstance(m) && m.tool_call_id === toolCall.id)) {
1358
- const toolMsg = `Tool call ${toolCall.name} with id ${toolCall.id} was cancelled - another message came in before it could be completed.`;
1359
- patchedMessages.push(new langchain.ToolMessage({
1360
- content: toolMsg,
1361
- name: toolCall.name,
1362
- tool_call_id: toolCall.id
1363
- }));
1364
- }
1365
- }
1366
- }
1421
+ const { patchedMessages, needsPatch } = patchDanglingToolCalls(messages);
1422
+ /**
1423
+ * Only trigger REMOVE_ALL_MESSAGES if patching is actually needed
1424
+ */
1425
+ if (!needsPatch) return;
1367
1426
  return { messages: [new _langchain_core_messages.RemoveMessage({ id: _langchain_langgraph.REMOVE_ALL_MESSAGES }), ...patchedMessages] };
1427
+ },
1428
+ wrapModelCall: async (request, handler) => {
1429
+ const messages = request.messages;
1430
+ if (!messages || messages.length === 0) return handler(request);
1431
+ const { patchedMessages, needsPatch } = patchDanglingToolCalls(messages);
1432
+ if (!needsPatch) return handler(request);
1433
+ return handler({
1434
+ ...request,
1435
+ messages: patchedMessages
1436
+ });
1368
1437
  }
1369
1438
  });
1370
1439
  }
@@ -1423,7 +1492,13 @@ function createPatchToolCallsMiddleware() {
1423
1492
  /**
1424
1493
  * State schema for memory middleware.
1425
1494
  */
1426
- const MemoryStateSchema = zod.z.object({ memoryContents: zod.z.record(zod.z.string(), zod.z.string()).optional() });
1495
+ const MemoryStateSchema = new _langchain_langgraph.StateSchema({
1496
+ memoryContents: zod.z.record(zod.z.string(), zod.z.string()).optional(),
1497
+ files: new _langchain_langgraph.ReducedValue(zod.z.record(zod.z.string(), FileDataSchema).default(() => ({})), {
1498
+ inputSchema: zod.z.record(zod.z.string(), FileDataSchema.nullable()).optional(),
1499
+ reducer: fileDataReducer
1500
+ })
1501
+ });
1427
1502
  /**
1428
1503
  * Default system prompt template for memory.
1429
1504
  * Ported from Python's comprehensive memory guidelines.
@@ -1655,10 +1730,16 @@ function skillsMetadataReducer(current, update) {
1655
1730
  * State schema for skills middleware.
1656
1731
  * Uses ReducedValue for skillsMetadata to allow concurrent updates from parallel subagents.
1657
1732
  */
1658
- const SkillsStateSchema = new _langchain_langgraph.StateSchema({ skillsMetadata: new _langchain_langgraph.ReducedValue(zod.z.array(SkillMetadataEntrySchema).default(() => []), {
1659
- inputSchema: zod.z.array(SkillMetadataEntrySchema).optional(),
1660
- reducer: skillsMetadataReducer
1661
- }) });
1733
+ const SkillsStateSchema = new _langchain_langgraph.StateSchema({
1734
+ skillsMetadata: new _langchain_langgraph.ReducedValue(zod.z.array(SkillMetadataEntrySchema).default(() => []), {
1735
+ inputSchema: zod.z.array(SkillMetadataEntrySchema).optional(),
1736
+ reducer: skillsMetadataReducer
1737
+ }),
1738
+ files: new _langchain_langgraph.ReducedValue(zod.z.record(zod.z.string(), FileDataSchema).default(() => ({})), {
1739
+ inputSchema: zod.z.record(zod.z.string(), FileDataSchema.nullable()).optional(),
1740
+ reducer: fileDataReducer
1741
+ })
1742
+ });
1662
1743
  /**
1663
1744
  * Skills System Documentation prompt template.
1664
1745
  */
@@ -1879,7 +1960,7 @@ function createSkillsMiddleware(options) {
1879
1960
  stateSchema: SkillsStateSchema,
1880
1961
  async beforeAgent(state) {
1881
1962
  if (loadedSkills.length > 0) return;
1882
- if ("skillsMetadata" in state && state.skillsMetadata != null) {
1963
+ if ("skillsMetadata" in state && Array.isArray(state.skillsMetadata) && state.skillsMetadata.length > 0) {
1883
1964
  loadedSkills = state.skillsMetadata;
1884
1965
  return;
1885
1966
  }
@@ -3457,6 +3538,9 @@ const BASE_PROMPT = `In order to complete the objective that the user asks of yo
3457
3538
  */
3458
3539
  function createDeepAgent(params = {}) {
3459
3540
  const { model = "claude-sonnet-4-5-20250929", tools = [], systemPrompt, middleware: customMiddleware = [], subagents = [], responseFormat, contextSchema, checkpointer, store, backend, interruptOn, name, memory, skills } = params;
3541
+ /**
3542
+ * Combine system prompt with base prompt like Python implementation
3543
+ */
3460
3544
  const finalSystemPrompt = systemPrompt ? typeof systemPrompt === "string" ? `${systemPrompt}\n\n${BASE_PROMPT}` : new langchain.SystemMessage({ content: [{
3461
3545
  type: "text",
3462
3546
  text: BASE_PROMPT
@@ -3464,22 +3548,61 @@ function createDeepAgent(params = {}) {
3464
3548
  type: "text",
3465
3549
  text: systemPrompt.content
3466
3550
  }] : systemPrompt.content] }) : BASE_PROMPT;
3551
+ /**
3552
+ * Create backend configuration for filesystem middleware
3553
+ * If no backend is provided, use a factory that creates a StateBackend
3554
+ */
3467
3555
  const filesystemBackend = backend ? backend : (config) => new StateBackend(config);
3468
- const skillsMiddleware = skills != null && skills.length > 0 ? [createSkillsMiddleware({
3556
+ /**
3557
+ * Skills middleware (created conditionally for runtime use)
3558
+ */
3559
+ const skillsMiddlewareArray = skills != null && skills.length > 0 ? [createSkillsMiddleware({
3469
3560
  backend: filesystemBackend,
3470
3561
  sources: skills
3471
3562
  })] : [];
3472
- const builtInMiddleware = [
3473
- (0, langchain.todoListMiddleware)(),
3474
- ...skillsMiddleware,
3475
- createFilesystemMiddleware({ backend: filesystemBackend }),
3476
- createSubAgentMiddleware({
3477
- defaultModel: model,
3478
- defaultTools: tools,
3479
- defaultMiddleware: [
3563
+ /**
3564
+ * Memory middleware (created conditionally for runtime use)
3565
+ */
3566
+ const memoryMiddlewareArray = memory != null && memory.length > 0 ? [createMemoryMiddleware({
3567
+ backend: filesystemBackend,
3568
+ sources: memory
3569
+ })] : [];
3570
+ /**
3571
+ * Return as DeepAgent with proper DeepAgentTypeConfig
3572
+ * - Response: TResponse (from responseFormat parameter)
3573
+ * - State: undefined (state comes from middleware)
3574
+ * - Context: ContextSchema
3575
+ * - Middleware: AllMiddleware (built-in + custom + subagent middleware for state inference)
3576
+ * - Tools: TTools
3577
+ * - Subagents: TSubagents (for type-safe streaming)
3578
+ */
3579
+ return (0, langchain.createAgent)({
3580
+ model,
3581
+ systemPrompt: finalSystemPrompt,
3582
+ tools,
3583
+ middleware: [
3584
+ ...[
3480
3585
  (0, langchain.todoListMiddleware)(),
3481
- ...skillsMiddleware,
3482
3586
  createFilesystemMiddleware({ backend: filesystemBackend }),
3587
+ createSubAgentMiddleware({
3588
+ defaultModel: model,
3589
+ defaultTools: tools,
3590
+ defaultMiddleware: [
3591
+ (0, langchain.todoListMiddleware)(),
3592
+ ...skillsMiddlewareArray,
3593
+ createFilesystemMiddleware({ backend: filesystemBackend }),
3594
+ (0, langchain.summarizationMiddleware)({
3595
+ model,
3596
+ trigger: { tokens: 17e4 },
3597
+ keep: { messages: 6 }
3598
+ }),
3599
+ (0, langchain.anthropicPromptCachingMiddleware)({ unsupportedModelBehavior: "ignore" }),
3600
+ createPatchToolCallsMiddleware()
3601
+ ],
3602
+ defaultInterruptOn: interruptOn,
3603
+ subagents,
3604
+ generalPurposeAgent: true
3605
+ }),
3483
3606
  (0, langchain.summarizationMiddleware)({
3484
3607
  model,
3485
3608
  trigger: { tokens: 17e4 },
@@ -3488,28 +3611,11 @@ function createDeepAgent(params = {}) {
3488
3611
  (0, langchain.anthropicPromptCachingMiddleware)({ unsupportedModelBehavior: "ignore" }),
3489
3612
  createPatchToolCallsMiddleware()
3490
3613
  ],
3491
- defaultInterruptOn: interruptOn,
3492
- subagents,
3493
- generalPurposeAgent: true
3494
- }),
3495
- (0, langchain.summarizationMiddleware)({
3496
- model,
3497
- trigger: { tokens: 17e4 },
3498
- keep: { messages: 6 }
3499
- }),
3500
- (0, langchain.anthropicPromptCachingMiddleware)({ unsupportedModelBehavior: "ignore" }),
3501
- createPatchToolCallsMiddleware(),
3502
- ...memory != null && memory.length > 0 ? [createMemoryMiddleware({
3503
- backend: filesystemBackend,
3504
- sources: memory
3505
- })] : []
3506
- ];
3507
- if (interruptOn) builtInMiddleware.push((0, langchain.humanInTheLoopMiddleware)({ interruptOn }));
3508
- return (0, langchain.createAgent)({
3509
- model,
3510
- systemPrompt: finalSystemPrompt,
3511
- tools,
3512
- middleware: [...builtInMiddleware, ...customMiddleware],
3614
+ ...skillsMiddlewareArray,
3615
+ ...memoryMiddlewareArray,
3616
+ ...interruptOn ? [(0, langchain.humanInTheLoopMiddleware)({ interruptOn })] : [],
3617
+ ...customMiddleware
3618
+ ],
3513
3619
  responseFormat,
3514
3620
  contextSchema,
3515
3621
  checkpointer,