deepagents 1.0.0 → 1.1.0

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.js CHANGED
@@ -1,15 +1,1866 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.editFile = exports.ls = exports.readFile = exports.writeFile = exports.writeTodos = exports.getDefaultModel = exports.DeepAgentState = exports.createDeepAgent = void 0;
4
- var graph_js_1 = require("./graph.js");
5
- Object.defineProperty(exports, "createDeepAgent", { enumerable: true, get: function () { return graph_js_1.createDeepAgent; } });
6
- var state_js_1 = require("./state.js");
7
- Object.defineProperty(exports, "DeepAgentState", { enumerable: true, get: function () { return state_js_1.DeepAgentState; } });
8
- var model_js_1 = require("./model.js");
9
- Object.defineProperty(exports, "getDefaultModel", { enumerable: true, get: function () { return model_js_1.getDefaultModel; } });
10
- var tools_js_1 = require("./tools.js");
11
- Object.defineProperty(exports, "writeTodos", { enumerable: true, get: function () { return tools_js_1.writeTodos; } });
12
- Object.defineProperty(exports, "writeFile", { enumerable: true, get: function () { return tools_js_1.writeFile; } });
13
- Object.defineProperty(exports, "readFile", { enumerable: true, get: function () { return tools_js_1.readFile; } });
14
- Object.defineProperty(exports, "ls", { enumerable: true, get: function () { return tools_js_1.ls; } });
15
- Object.defineProperty(exports, "editFile", { enumerable: true, get: function () { return tools_js_1.editFile; } });
1
+ import { AIMessage, ToolMessage, anthropicPromptCachingMiddleware, createAgent, createMiddleware, humanInTheLoopMiddleware, summarizationMiddleware, todoListMiddleware, tool } from "langchain";
2
+ import { Command, REMOVE_ALL_MESSAGES, getCurrentTaskInput, isCommand } from "@langchain/langgraph";
3
+ import { z } from "zod/v3";
4
+ import { withLangGraph } from "@langchain/langgraph/zod";
5
+ import micromatch from "micromatch";
6
+ import * as path from "path";
7
+ import { basename } from "path";
8
+ import { HumanMessage, RemoveMessage } from "@langchain/core/messages";
9
+ import * as fs from "fs/promises";
10
+ import * as fsSync from "fs";
11
+ import { spawn } from "child_process";
12
+ import fg from "fast-glob";
13
+
14
+ //#region src/backends/utils.ts
15
+ const EMPTY_CONTENT_WARNING = "System reminder: File exists but has empty contents";
16
+ const MAX_LINE_LENGTH = 1e4;
17
+ const LINE_NUMBER_WIDTH = 6;
18
+ /**
19
+ * Sanitize tool_call_id to prevent path traversal and separator issues.
20
+ *
21
+ * Replaces dangerous characters (., /, \) with underscores.
22
+ */
23
+ function sanitizeToolCallId(toolCallId) {
24
+ return toolCallId.replace(/\./g, "_").replace(/\//g, "_").replace(/\\/g, "_");
25
+ }
26
+ /**
27
+ * Format file content with line numbers (cat -n style).
28
+ *
29
+ * Chunks lines longer than MAX_LINE_LENGTH with continuation markers (e.g., 5.1, 5.2).
30
+ *
31
+ * @param content - File content as string or list of lines
32
+ * @param startLine - Starting line number (default: 1)
33
+ * @returns Formatted content with line numbers and continuation markers
34
+ */
35
+ function formatContentWithLineNumbers(content, startLine = 1) {
36
+ let lines;
37
+ if (typeof content === "string") {
38
+ lines = content.split("\n");
39
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines = lines.slice(0, -1);
40
+ } else lines = content;
41
+ const resultLines = [];
42
+ for (let i = 0; i < lines.length; i++) {
43
+ const line = lines[i];
44
+ const lineNum = i + startLine;
45
+ if (line.length <= MAX_LINE_LENGTH) resultLines.push(`${lineNum.toString().padStart(LINE_NUMBER_WIDTH)}\t${line}`);
46
+ else {
47
+ const numChunks = Math.ceil(line.length / MAX_LINE_LENGTH);
48
+ for (let chunkIdx = 0; chunkIdx < numChunks; chunkIdx++) {
49
+ const start = chunkIdx * MAX_LINE_LENGTH;
50
+ const end = Math.min(start + MAX_LINE_LENGTH, line.length);
51
+ const chunk = line.substring(start, end);
52
+ if (chunkIdx === 0) resultLines.push(`${lineNum.toString().padStart(LINE_NUMBER_WIDTH)}\t${chunk}`);
53
+ else {
54
+ const continuationMarker = `${lineNum}.${chunkIdx}`;
55
+ resultLines.push(`${continuationMarker.padStart(LINE_NUMBER_WIDTH)}\t${chunk}`);
56
+ }
57
+ }
58
+ }
59
+ }
60
+ return resultLines.join("\n");
61
+ }
62
+ /**
63
+ * Check if content is empty and return warning message.
64
+ *
65
+ * @param content - Content to check
66
+ * @returns Warning message if empty, null otherwise
67
+ */
68
+ function checkEmptyContent(content) {
69
+ if (!content || content.trim() === "") return EMPTY_CONTENT_WARNING;
70
+ return null;
71
+ }
72
+ /**
73
+ * Convert FileData to plain string content.
74
+ *
75
+ * @param fileData - FileData object with 'content' key
76
+ * @returns Content as string with lines joined by newlines
77
+ */
78
+ function fileDataToString(fileData) {
79
+ return fileData.content.join("\n");
80
+ }
81
+ /**
82
+ * Create a FileData object with timestamps.
83
+ *
84
+ * @param content - File content as string
85
+ * @param createdAt - Optional creation timestamp (ISO format)
86
+ * @returns FileData object with content and timestamps
87
+ */
88
+ function createFileData(content, createdAt) {
89
+ const lines = typeof content === "string" ? content.split("\n") : content;
90
+ const now = (/* @__PURE__ */ new Date()).toISOString();
91
+ return {
92
+ content: lines,
93
+ created_at: createdAt || now,
94
+ modified_at: now
95
+ };
96
+ }
97
+ /**
98
+ * Update FileData with new content, preserving creation timestamp.
99
+ *
100
+ * @param fileData - Existing FileData object
101
+ * @param content - New content as string
102
+ * @returns Updated FileData object
103
+ */
104
+ function updateFileData(fileData, content) {
105
+ const lines = typeof content === "string" ? content.split("\n") : content;
106
+ const now = (/* @__PURE__ */ new Date()).toISOString();
107
+ return {
108
+ content: lines,
109
+ created_at: fileData.created_at,
110
+ modified_at: now
111
+ };
112
+ }
113
+ /**
114
+ * Format file data for read response with line numbers.
115
+ *
116
+ * @param fileData - FileData object
117
+ * @param offset - Line offset (0-indexed)
118
+ * @param limit - Maximum number of lines
119
+ * @returns Formatted content or error message
120
+ */
121
+ function formatReadResponse(fileData, offset, limit) {
122
+ const content = fileDataToString(fileData);
123
+ const emptyMsg = checkEmptyContent(content);
124
+ if (emptyMsg) return emptyMsg;
125
+ const lines = content.split("\n");
126
+ const startIdx = offset;
127
+ const endIdx = Math.min(startIdx + limit, lines.length);
128
+ if (startIdx >= lines.length) return `Error: Line offset ${offset} exceeds file length (${lines.length} lines)`;
129
+ return formatContentWithLineNumbers(lines.slice(startIdx, endIdx), startIdx + 1);
130
+ }
131
+ /**
132
+ * Perform string replacement with occurrence validation.
133
+ *
134
+ * @param content - Original content
135
+ * @param oldString - String to replace
136
+ * @param newString - Replacement string
137
+ * @param replaceAll - Whether to replace all occurrences
138
+ * @returns Tuple of [new_content, occurrences] on success, or error message string
139
+ */
140
+ function performStringReplacement(content, oldString, newString, replaceAll) {
141
+ const occurrences = content.split(oldString).length - 1;
142
+ if (occurrences === 0) return `Error: String not found in file: '${oldString}'`;
143
+ 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.`;
144
+ return [content.split(oldString).join(newString), occurrences];
145
+ }
146
+ /**
147
+ * Validate and normalize a path.
148
+ *
149
+ * @param path - Path to validate
150
+ * @returns Normalized path starting with / and ending with /
151
+ * @throws Error if path is invalid
152
+ */
153
+ function validatePath(path$1) {
154
+ const pathStr = path$1 || "/";
155
+ if (!pathStr || pathStr.trim() === "") throw new Error("Path cannot be empty");
156
+ let normalized = pathStr.startsWith("/") ? pathStr : "/" + pathStr;
157
+ if (!normalized.endsWith("/")) normalized += "/";
158
+ return normalized;
159
+ }
160
+ /**
161
+ * Search files dict for paths matching glob pattern.
162
+ *
163
+ * @param files - Dictionary of file paths to FileData
164
+ * @param pattern - Glob pattern (e.g., `*.py`, `**\/*.ts`)
165
+ * @param path - Base path to search from
166
+ * @returns Newline-separated file paths, sorted by modification time (most recent first).
167
+ * Returns "No files found" if no matches.
168
+ *
169
+ * @example
170
+ * ```typescript
171
+ * const files = {"/src/main.py": FileData(...), "/test.py": FileData(...)};
172
+ * globSearchFiles(files, "*.py", "/");
173
+ * // Returns: "/test.py\n/src/main.py" (sorted by modified_at)
174
+ * ```
175
+ */
176
+ function globSearchFiles(files, pattern, path$1 = "/") {
177
+ let normalizedPath;
178
+ try {
179
+ normalizedPath = validatePath(path$1);
180
+ } catch {
181
+ return "No files found";
182
+ }
183
+ const filtered = Object.fromEntries(Object.entries(files).filter(([fp]) => fp.startsWith(normalizedPath)));
184
+ const effectivePattern = pattern;
185
+ const matches = [];
186
+ for (const [filePath, fileData] of Object.entries(filtered)) {
187
+ let relative = filePath.substring(normalizedPath.length);
188
+ if (relative.startsWith("/")) relative = relative.substring(1);
189
+ if (!relative) {
190
+ const parts = filePath.split("/");
191
+ relative = parts[parts.length - 1] || "";
192
+ }
193
+ if (micromatch.isMatch(relative, effectivePattern, {
194
+ dot: true,
195
+ nobrace: false
196
+ })) matches.push([filePath, fileData.modified_at]);
197
+ }
198
+ matches.sort((a, b) => b[1].localeCompare(a[1]));
199
+ if (matches.length === 0) return "No files found";
200
+ return matches.map(([fp]) => fp).join("\n");
201
+ }
202
+ /**
203
+ * Return structured grep matches from an in-memory files mapping.
204
+ *
205
+ * Returns a list of GrepMatch on success, or a string for invalid inputs
206
+ * (e.g., invalid regex). We deliberately do not raise here to keep backends
207
+ * non-throwing in tool contexts and preserve user-facing error messages.
208
+ */
209
+ function grepMatchesFromFiles(files, pattern, path$1 = null, glob = null) {
210
+ let regex;
211
+ try {
212
+ regex = new RegExp(pattern);
213
+ } catch (e) {
214
+ return `Invalid regex pattern: ${e.message}`;
215
+ }
216
+ let normalizedPath;
217
+ try {
218
+ normalizedPath = validatePath(path$1);
219
+ } catch {
220
+ return [];
221
+ }
222
+ let filtered = Object.fromEntries(Object.entries(files).filter(([fp]) => fp.startsWith(normalizedPath)));
223
+ if (glob) filtered = Object.fromEntries(Object.entries(filtered).filter(([fp]) => micromatch.isMatch(basename(fp), glob, {
224
+ dot: true,
225
+ nobrace: false
226
+ })));
227
+ const matches = [];
228
+ for (const [filePath, fileData] of Object.entries(filtered)) for (let i = 0; i < fileData.content.length; i++) {
229
+ const line = fileData.content[i];
230
+ const lineNum = i + 1;
231
+ if (regex.test(line)) matches.push({
232
+ path: filePath,
233
+ line: lineNum,
234
+ text: line
235
+ });
236
+ }
237
+ return matches;
238
+ }
239
+
240
+ //#endregion
241
+ //#region src/backends/state.ts
242
+ /**
243
+ * Backend that stores files in agent state (ephemeral).
244
+ *
245
+ * Uses LangGraph's state management and checkpointing. Files persist within
246
+ * a conversation thread but not across threads. State is automatically
247
+ * checkpointed after each agent step.
248
+ *
249
+ * Special handling: Since LangGraph state must be updated via Command objects
250
+ * (not direct mutation), operations return filesUpdate in WriteResult/EditResult
251
+ * for the middleware to apply via Command.
252
+ */
253
+ var StateBackend = class {
254
+ stateAndStore;
255
+ constructor(stateAndStore) {
256
+ this.stateAndStore = stateAndStore;
257
+ }
258
+ /**
259
+ * Get files from current state.
260
+ */
261
+ getFiles() {
262
+ return this.stateAndStore.state.files || {};
263
+ }
264
+ /**
265
+ * List files and directories in the specified directory (non-recursive).
266
+ *
267
+ * @param path - Absolute path to directory
268
+ * @returns List of FileInfo objects for files and directories directly in the directory.
269
+ * Directories have a trailing / in their path and is_dir=true.
270
+ */
271
+ lsInfo(path$1) {
272
+ const files = this.getFiles();
273
+ const infos = [];
274
+ const subdirs = /* @__PURE__ */ new Set();
275
+ const normalizedPath = path$1.endsWith("/") ? path$1 : path$1 + "/";
276
+ for (const [k, fd] of Object.entries(files)) {
277
+ if (!k.startsWith(normalizedPath)) continue;
278
+ const relative = k.substring(normalizedPath.length);
279
+ if (relative.includes("/")) {
280
+ const subdirName = relative.split("/")[0];
281
+ subdirs.add(normalizedPath + subdirName + "/");
282
+ continue;
283
+ }
284
+ const size = fd.content.join("\n").length;
285
+ infos.push({
286
+ path: k,
287
+ is_dir: false,
288
+ size,
289
+ modified_at: fd.modified_at
290
+ });
291
+ }
292
+ for (const subdir of Array.from(subdirs).sort()) infos.push({
293
+ path: subdir,
294
+ is_dir: true,
295
+ size: 0,
296
+ modified_at: ""
297
+ });
298
+ infos.sort((a, b) => a.path.localeCompare(b.path));
299
+ return infos;
300
+ }
301
+ /**
302
+ * Read file content with line numbers.
303
+ *
304
+ * @param filePath - Absolute file path
305
+ * @param offset - Line offset to start reading from (0-indexed)
306
+ * @param limit - Maximum number of lines to read
307
+ * @returns Formatted file content with line numbers, or error message
308
+ */
309
+ read(filePath, offset = 0, limit = 2e3) {
310
+ const fileData = this.getFiles()[filePath];
311
+ if (!fileData) return `Error: File '${filePath}' not found`;
312
+ return formatReadResponse(fileData, offset, limit);
313
+ }
314
+ /**
315
+ * Create a new file with content.
316
+ * Returns WriteResult with filesUpdate to update LangGraph state.
317
+ */
318
+ write(filePath, content) {
319
+ if (filePath in this.getFiles()) return { error: `Cannot write to ${filePath} because it already exists. Read and then make an edit, or write to a new path.` };
320
+ const newFileData = createFileData(content);
321
+ return {
322
+ path: filePath,
323
+ filesUpdate: { [filePath]: newFileData }
324
+ };
325
+ }
326
+ /**
327
+ * Edit a file by replacing string occurrences.
328
+ * Returns EditResult with filesUpdate and occurrences.
329
+ */
330
+ edit(filePath, oldString, newString, replaceAll = false) {
331
+ const fileData = this.getFiles()[filePath];
332
+ if (!fileData) return { error: `Error: File '${filePath}' not found` };
333
+ const result = performStringReplacement(fileDataToString(fileData), oldString, newString, replaceAll);
334
+ if (typeof result === "string") return { error: result };
335
+ const [newContent, occurrences] = result;
336
+ const newFileData = updateFileData(fileData, newContent);
337
+ return {
338
+ path: filePath,
339
+ filesUpdate: { [filePath]: newFileData },
340
+ occurrences
341
+ };
342
+ }
343
+ /**
344
+ * Structured search results or error string for invalid input.
345
+ */
346
+ grepRaw(pattern, path$1 = "/", glob = null) {
347
+ return grepMatchesFromFiles(this.getFiles(), pattern, path$1, glob);
348
+ }
349
+ /**
350
+ * Structured glob matching returning FileInfo objects.
351
+ */
352
+ globInfo(pattern, path$1 = "/") {
353
+ const files = this.getFiles();
354
+ const result = globSearchFiles(files, pattern, path$1);
355
+ if (result === "No files found") return [];
356
+ const paths = result.split("\n");
357
+ const infos = [];
358
+ for (const p of paths) {
359
+ const fd = files[p];
360
+ const size = fd ? fd.content.join("\n").length : 0;
361
+ infos.push({
362
+ path: p,
363
+ is_dir: false,
364
+ size,
365
+ modified_at: fd?.modified_at || ""
366
+ });
367
+ }
368
+ return infos;
369
+ }
370
+ };
371
+
372
+ //#endregion
373
+ //#region src/middleware/fs.ts
374
+ /**
375
+ * Zod v3 schema for FileData (re-export from backends)
376
+ */
377
+ const FileDataSchema = z.object({
378
+ content: z.array(z.string()),
379
+ created_at: z.string(),
380
+ modified_at: z.string()
381
+ });
382
+ /**
383
+ * Merge file updates with support for deletions.
384
+ */
385
+ function fileDataReducer(left, right) {
386
+ if (left === void 0) {
387
+ const result$1 = {};
388
+ for (const [key, value] of Object.entries(right)) if (value !== null) result$1[key] = value;
389
+ return result$1;
390
+ }
391
+ const result = { ...left };
392
+ for (const [key, value] of Object.entries(right)) if (value === null) delete result[key];
393
+ else result[key] = value;
394
+ return result;
395
+ }
396
+ /**
397
+ * Resolve backend from factory or instance.
398
+ *
399
+ * @param backend - Backend instance or factory function
400
+ * @param stateAndStore - State and store container for backend initialization
401
+ */
402
+ function getBackend(backend, stateAndStore) {
403
+ if (typeof backend === "function") return backend(stateAndStore);
404
+ return backend;
405
+ }
406
+ /**
407
+ * Helper to await if Promise, otherwise return value directly.
408
+ */
409
+ async function awaitIfPromise(value) {
410
+ return value;
411
+ }
412
+ const FILESYSTEM_SYSTEM_PROMPT = `You have access to a virtual filesystem. All file paths must start with a /.
413
+
414
+ - ls: list files in a directory (requires absolute path)
415
+ - read_file: read a file from the filesystem
416
+ - write_file: write to a file in the filesystem
417
+ - edit_file: edit a file in the filesystem
418
+ - glob: find files matching a pattern (e.g., "**/*.py")
419
+ - grep: search for text within files`;
420
+ const LS_TOOL_DESCRIPTION = "List files and directories in a directory";
421
+ const READ_FILE_TOOL_DESCRIPTION = "Read the contents of a file";
422
+ const WRITE_FILE_TOOL_DESCRIPTION = "Write content to a new file. Returns an error if the file already exists";
423
+ const EDIT_FILE_TOOL_DESCRIPTION = "Edit a file by replacing a specific string with a new string";
424
+ const GLOB_TOOL_DESCRIPTION = "Find files matching a glob pattern (e.g., '**/*.py' for all Python files)";
425
+ const GREP_TOOL_DESCRIPTION = "Search for a regex pattern in files. Returns matching files and line numbers";
426
+ /**
427
+ * Create ls tool using backend.
428
+ */
429
+ function createLsTool(backend, customDescription) {
430
+ return tool(async (input, config) => {
431
+ const resolvedBackend = getBackend(backend, {
432
+ state: getCurrentTaskInput(config),
433
+ store: config.store
434
+ });
435
+ const path$1 = input.path || "/";
436
+ const infos = await awaitIfPromise(resolvedBackend.lsInfo(path$1));
437
+ if (infos.length === 0) return `No files found in ${path$1}`;
438
+ const lines = [];
439
+ for (const info of infos) if (info.is_dir) lines.push(`${info.path} (directory)`);
440
+ else {
441
+ const size = info.size ? ` (${info.size} bytes)` : "";
442
+ lines.push(`${info.path}${size}`);
443
+ }
444
+ return lines.join("\n");
445
+ }, {
446
+ name: "ls",
447
+ description: customDescription || LS_TOOL_DESCRIPTION,
448
+ schema: z.object({ path: z.string().optional().default("/").describe("Directory path to list (default: /)") })
449
+ });
450
+ }
451
+ /**
452
+ * Create read_file tool using backend.
453
+ */
454
+ function createReadFileTool(backend, customDescription) {
455
+ return tool(async (input, config) => {
456
+ const resolvedBackend = getBackend(backend, {
457
+ state: getCurrentTaskInput(config),
458
+ store: config.store
459
+ });
460
+ const { file_path, offset = 0, limit = 2e3 } = input;
461
+ return await awaitIfPromise(resolvedBackend.read(file_path, offset, limit));
462
+ }, {
463
+ name: "read_file",
464
+ description: customDescription || READ_FILE_TOOL_DESCRIPTION,
465
+ schema: z.object({
466
+ file_path: z.string().describe("Absolute path to the file to read"),
467
+ offset: z.number({ coerce: true }).optional().default(0).describe("Line offset to start reading from (0-indexed)"),
468
+ limit: z.number({ coerce: true }).optional().default(2e3).describe("Maximum number of lines to read")
469
+ })
470
+ });
471
+ }
472
+ /**
473
+ * Create write_file tool using backend.
474
+ */
475
+ function createWriteFileTool(backend, customDescription) {
476
+ return tool(async (input, config) => {
477
+ const resolvedBackend = getBackend(backend, {
478
+ state: getCurrentTaskInput(config),
479
+ store: config.store
480
+ });
481
+ const { file_path, content } = input;
482
+ const result = await awaitIfPromise(resolvedBackend.write(file_path, content));
483
+ if (result.error) return result.error;
484
+ if (result.filesUpdate) return new Command({ update: {
485
+ files: result.filesUpdate,
486
+ messages: [new ToolMessage({
487
+ content: `Successfully wrote to '${file_path}'`,
488
+ tool_call_id: config.toolCall?.id,
489
+ name: "write_file"
490
+ })]
491
+ } });
492
+ return `Successfully wrote to '${file_path}'`;
493
+ }, {
494
+ name: "write_file",
495
+ description: customDescription || WRITE_FILE_TOOL_DESCRIPTION,
496
+ schema: z.object({
497
+ file_path: z.string().describe("Absolute path to the file to write"),
498
+ content: z.string().describe("Content to write to the file")
499
+ })
500
+ });
501
+ }
502
+ /**
503
+ * Create edit_file tool using backend.
504
+ */
505
+ function createEditFileTool(backend, customDescription) {
506
+ return tool(async (input, config) => {
507
+ const resolvedBackend = getBackend(backend, {
508
+ state: getCurrentTaskInput(config),
509
+ store: config.store
510
+ });
511
+ const { file_path, old_string, new_string, replace_all = false } = input;
512
+ const result = await awaitIfPromise(resolvedBackend.edit(file_path, old_string, new_string, replace_all));
513
+ if (result.error) return result.error;
514
+ const message = `Successfully replaced ${result.occurrences} occurrence(s) in '${file_path}'`;
515
+ if (result.filesUpdate) return new Command({ update: {
516
+ files: result.filesUpdate,
517
+ messages: [new ToolMessage({
518
+ content: message,
519
+ tool_call_id: config.toolCall?.id,
520
+ name: "edit_file"
521
+ })]
522
+ } });
523
+ return message;
524
+ }, {
525
+ name: "edit_file",
526
+ description: customDescription || EDIT_FILE_TOOL_DESCRIPTION,
527
+ schema: z.object({
528
+ file_path: z.string().describe("Absolute path to the file to edit"),
529
+ old_string: z.string().describe("String to be replaced (must match exactly)"),
530
+ new_string: z.string().describe("String to replace with"),
531
+ replace_all: z.boolean().optional().default(false).describe("Whether to replace all occurrences")
532
+ })
533
+ });
534
+ }
535
+ /**
536
+ * Create glob tool using backend.
537
+ */
538
+ function createGlobTool(backend, customDescription) {
539
+ return tool(async (input, config) => {
540
+ const resolvedBackend = getBackend(backend, {
541
+ state: getCurrentTaskInput(config),
542
+ store: config.store
543
+ });
544
+ const { pattern, path: path$1 = "/" } = input;
545
+ const infos = await awaitIfPromise(resolvedBackend.globInfo(pattern, path$1));
546
+ if (infos.length === 0) return `No files found matching pattern '${pattern}'`;
547
+ return infos.map((info) => info.path).join("\n");
548
+ }, {
549
+ name: "glob",
550
+ description: customDescription || GLOB_TOOL_DESCRIPTION,
551
+ schema: z.object({
552
+ pattern: z.string().describe("Glob pattern (e.g., '*.py', '**/*.ts')"),
553
+ path: z.string().optional().default("/").describe("Base path to search from (default: /)")
554
+ })
555
+ });
556
+ }
557
+ /**
558
+ * Create grep tool using backend.
559
+ */
560
+ function createGrepTool(backend, customDescription) {
561
+ return tool(async (input, config) => {
562
+ const resolvedBackend = getBackend(backend, {
563
+ state: getCurrentTaskInput(config),
564
+ store: config.store
565
+ });
566
+ const { pattern, path: path$1 = "/", glob = null } = input;
567
+ const result = await awaitIfPromise(resolvedBackend.grepRaw(pattern, path$1, glob));
568
+ if (typeof result === "string") return result;
569
+ if (result.length === 0) return `No matches found for pattern '${pattern}'`;
570
+ const lines = [];
571
+ let currentFile = null;
572
+ for (const match of result) {
573
+ if (match.path !== currentFile) {
574
+ currentFile = match.path;
575
+ lines.push(`\n${currentFile}:`);
576
+ }
577
+ lines.push(` ${match.line}: ${match.text}`);
578
+ }
579
+ return lines.join("\n");
580
+ }, {
581
+ name: "grep",
582
+ description: customDescription || GREP_TOOL_DESCRIPTION,
583
+ schema: z.object({
584
+ pattern: z.string().describe("Regex pattern to search for"),
585
+ path: z.string().optional().default("/").describe("Base path to search from (default: /)"),
586
+ glob: z.string().optional().nullable().describe("Optional glob pattern to filter files (e.g., '*.py')")
587
+ })
588
+ });
589
+ }
590
+ /**
591
+ * Create filesystem middleware with all tools and features.
592
+ */
593
+ function createFilesystemMiddleware(options = {}) {
594
+ const { backend = (stateAndStore) => new StateBackend(stateAndStore), systemPrompt: customSystemPrompt = null, customToolDescriptions = null, toolTokenLimitBeforeEvict = 2e4 } = options;
595
+ const systemPrompt = customSystemPrompt || FILESYSTEM_SYSTEM_PROMPT;
596
+ const tools = [
597
+ createLsTool(backend, customToolDescriptions?.ls ?? null),
598
+ createReadFileTool(backend, customToolDescriptions?.read_file ?? null),
599
+ createWriteFileTool(backend, customToolDescriptions?.write_file ?? null),
600
+ createEditFileTool(backend, customToolDescriptions?.edit_file ?? null),
601
+ createGlobTool(backend, customToolDescriptions?.glob ?? null),
602
+ createGrepTool(backend, customToolDescriptions?.grep ?? null)
603
+ ];
604
+ return createMiddleware({
605
+ name: "FilesystemMiddleware",
606
+ stateSchema: z.object({ files: withLangGraph(z.record(z.string(), FileDataSchema).default({}), { reducer: {
607
+ fn: fileDataReducer,
608
+ schema: z.record(z.string(), FileDataSchema.nullable())
609
+ } }) }),
610
+ tools,
611
+ wrapModelCall: systemPrompt ? async (request, handler) => {
612
+ const currentSystemPrompt = request.systemPrompt || "";
613
+ const newSystemPrompt = currentSystemPrompt ? `${currentSystemPrompt}\n\n${systemPrompt}` : systemPrompt;
614
+ return handler({
615
+ ...request,
616
+ systemPrompt: newSystemPrompt
617
+ });
618
+ } : void 0,
619
+ wrapToolCall: toolTokenLimitBeforeEvict ? (async (request, handler) => {
620
+ const result = await handler(request);
621
+ async function processToolMessage(msg) {
622
+ if (typeof msg.content === "string" && msg.content.length > toolTokenLimitBeforeEvict * 4) {
623
+ const resolvedBackend = getBackend(backend, {
624
+ state: request.state || {},
625
+ store: request.config?.store
626
+ });
627
+ const evictPath = `/large_tool_results/${sanitizeToolCallId(request.toolCall?.id || msg.tool_call_id)}`;
628
+ const writeResult = await awaitIfPromise(resolvedBackend.write(evictPath, msg.content));
629
+ if (writeResult.error) return {
630
+ message: msg,
631
+ filesUpdate: null
632
+ };
633
+ return {
634
+ message: new ToolMessage({
635
+ content: `Tool result too large (${Math.round(msg.content.length / 4)} tokens). Content saved to ${evictPath}`,
636
+ tool_call_id: msg.tool_call_id,
637
+ name: msg.name
638
+ }),
639
+ filesUpdate: writeResult.filesUpdate
640
+ };
641
+ }
642
+ return {
643
+ message: msg,
644
+ filesUpdate: null
645
+ };
646
+ }
647
+ if (result instanceof ToolMessage) {
648
+ const processed = await processToolMessage(result);
649
+ if (processed.filesUpdate) return new Command({ update: {
650
+ files: processed.filesUpdate,
651
+ messages: [processed.message]
652
+ } });
653
+ return processed.message;
654
+ }
655
+ if (isCommand(result)) {
656
+ const update = result.update;
657
+ if (!update?.messages) return result;
658
+ let hasLargeResults = false;
659
+ const accumulatedFiles = { ...update.files || {} };
660
+ const processedMessages = [];
661
+ for (const msg of update.messages) if (msg instanceof ToolMessage) {
662
+ const processed = await processToolMessage(msg);
663
+ processedMessages.push(processed.message);
664
+ if (processed.filesUpdate) {
665
+ hasLargeResults = true;
666
+ Object.assign(accumulatedFiles, processed.filesUpdate);
667
+ }
668
+ } else processedMessages.push(msg);
669
+ if (hasLargeResults) return new Command({ update: {
670
+ ...update,
671
+ messages: processedMessages,
672
+ files: accumulatedFiles
673
+ } });
674
+ }
675
+ return result;
676
+ }) : void 0
677
+ });
678
+ }
679
+
680
+ //#endregion
681
+ //#region src/middleware/subagents.ts
682
+ const DEFAULT_SUBAGENT_PROMPT = "In order to complete the objective that the user asks of you, you have access to a number of standard tools.";
683
+ const EXCLUDED_STATE_KEYS = [
684
+ "messages",
685
+ "todos",
686
+ "jumpTo"
687
+ ];
688
+ const DEFAULT_GENERAL_PURPOSE_DESCRIPTION = "General-purpose agent for researching complex questions, searching for files and content, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you. This agent has access to all tools as the main agent.";
689
+ function getTaskToolDescription(subagentDescriptions) {
690
+ return `
691
+ Launch an ephemeral subagent to handle complex, multi-step independent tasks with isolated context windows.
692
+
693
+ Available agent types and the tools they have access to:
694
+ ${subagentDescriptions.join("\n")}
695
+
696
+ When using the Task tool, you must specify a subagent_type parameter to select which agent type to use.
697
+
698
+ ## Usage notes:
699
+ 1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
700
+ 2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
701
+ 3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
702
+ 4. The agent's outputs should generally be trusted
703
+ 5. Clearly tell the agent whether you expect it to create content, perform analysis, or just do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent
704
+ 6. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
705
+ 7. When only the general-purpose agent is provided, you should use it for all tasks. It is great for isolating context and token usage, and completing specific, complex tasks, as it has all the same capabilities as the main agent.
706
+
707
+ ### Example usage of the general-purpose agent:
708
+
709
+ <example_agent_descriptions>
710
+ "general-purpose": use this agent for general purpose tasks, it has access to all tools as the main agent.
711
+ </example_agent_descriptions>
712
+
713
+ <example>
714
+ User: "I want to conduct research on the accomplishments of Lebron James, Michael Jordan, and Kobe Bryant, and then compare them."
715
+ Assistant: *Uses the task tool in parallel to conduct isolated research on each of the three players*
716
+ Assistant: *Synthesizes the results of the three isolated research tasks and responds to the User*
717
+ <commentary>
718
+ Research is a complex, multi-step task in it of itself.
719
+ The research of each individual player is not dependent on the research of the other players.
720
+ The assistant uses the task tool to break down the complex objective into three isolated tasks.
721
+ Each research task only needs to worry about context and tokens about one player, then returns synthesized information about each player as the Tool Result.
722
+ This means each research task can dive deep and spend tokens and context deeply researching each player, but the final result is synthesized information, and saves us tokens in the long run when comparing the players to each other.
723
+ </commentary>
724
+ </example>
725
+
726
+ <example>
727
+ User: "Analyze a single large code repository for security vulnerabilities and generate a report."
728
+ Assistant: *Launches a single \`task\` subagent for the repository analysis*
729
+ Assistant: *Receives report and integrates results into final summary*
730
+ <commentary>
731
+ Subagent is used to isolate a large, context-heavy task, even though there is only one. This prevents the main thread from being overloaded with details.
732
+ If the user then asks followup questions, we have a concise report to reference instead of the entire history of analysis and tool calls, which is good and saves us time and money.
733
+ </commentary>
734
+ </example>
735
+
736
+ <example>
737
+ User: "Schedule two meetings for me and prepare agendas for each."
738
+ Assistant: *Calls the task tool in parallel to launch two \`task\` subagents (one per meeting) to prepare agendas*
739
+ Assistant: *Returns final schedules and agendas*
740
+ <commentary>
741
+ Tasks are simple individually, but subagents help silo agenda preparation.
742
+ Each subagent only needs to worry about the agenda for one meeting.
743
+ </commentary>
744
+ </example>
745
+
746
+ <example>
747
+ User: "I want to order a pizza from Dominos, order a burger from McDonald's, and order a salad from Subway."
748
+ Assistant: *Calls tools directly in parallel to order a pizza from Dominos, a burger from McDonald's, and a salad from Subway*
749
+ <commentary>
750
+ The assistant did not use the task tool because the objective is super simple and clear and only requires a few trivial tool calls.
751
+ It is better to just complete the task directly and NOT use the \`task\`tool.
752
+ </commentary>
753
+ </example>
754
+
755
+ ### Example usage with custom agents:
756
+
757
+ <example_agent_descriptions>
758
+ "content-reviewer": use this agent after you are done creating significant content or documents
759
+ "greeting-responder": use this agent when to respond to user greetings with a friendly joke
760
+ "research-analyst": use this agent to conduct thorough research on complex topics
761
+ </example_agent_description>
762
+
763
+ <example>
764
+ user: "Please write a function that checks if a number is prime"
765
+ assistant: Sure let me write a function that checks if a number is prime
766
+ assistant: First let me use the Write tool to write a function that checks if a number is prime
767
+ assistant: I'm going to use the Write tool to write the following code:
768
+ <code>
769
+ function isPrime(n) {
770
+ if (n <= 1) return false
771
+ for (let i = 2; i * i <= n; i++) {
772
+ if (n % i === 0) return false
773
+ }
774
+ return true
775
+ }
776
+ </code>
777
+ <commentary>
778
+ Since significant content was created and the task was completed, now use the content-reviewer agent to review the work
779
+ </commentary>
780
+ assistant: Now let me use the content-reviewer agent to review the code
781
+ assistant: Uses the Task tool to launch with the content-reviewer agent
782
+ </example>
783
+
784
+ <example>
785
+ user: "Can you help me research the environmental impact of different renewable energy sources and create a comprehensive report?"
786
+ <commentary>
787
+ This is a complex research task that would benefit from using the research-analyst agent to conduct thorough analysis
788
+ </commentary>
789
+ assistant: I'll help you research the environmental impact of renewable energy sources. Let me use the research-analyst agent to conduct comprehensive research on this topic.
790
+ assistant: Uses the Task tool to launch with the research-analyst agent, providing detailed instructions about what research to conduct and what format the report should take
791
+ </example>
792
+
793
+ <example>
794
+ user: "Hello"
795
+ <commentary>
796
+ Since the user is greeting, use the greeting-responder agent to respond with a friendly joke
797
+ </commentary>
798
+ assistant: "I'm going to use the Task tool to launch with the greeting-responder agent"
799
+ </example>
800
+ `.trim();
801
+ }
802
+ const TASK_SYSTEM_PROMPT = `## \`task\` (subagent spawner)
803
+
804
+ You have access to a \`task\` tool to launch short-lived subagents that handle isolated tasks. These agents are ephemeral — they live only for the duration of the task and return a single result.
805
+
806
+ When to use the task tool:
807
+ - When a task is complex and multi-step, and can be fully delegated in isolation
808
+ - When a task is independent of other tasks and can run in parallel
809
+ - When a task requires focused reasoning or heavy token/context usage that would bloat the orchestrator thread
810
+ - When sandboxing improves reliability (e.g. code execution, structured searches, data formatting)
811
+ - When you only care about the output of the subagent, and not the intermediate steps (ex. performing a lot of research and then returned a synthesized report, performing a series of computations or lookups to achieve a concise, relevant answer.)
812
+
813
+ Subagent lifecycle:
814
+ 1. **Spawn** → Provide clear role, instructions, and expected output
815
+ 2. **Run** → The subagent completes the task autonomously
816
+ 3. **Return** → The subagent provides a single structured result
817
+ 4. **Reconcile** → Incorporate or synthesize the result into the main thread
818
+
819
+ When NOT to use the task tool:
820
+ - If you need to see the intermediate reasoning or steps after the subagent has completed (the task tool hides them)
821
+ - If the task is trivial (a few tool calls or simple lookup)
822
+ - If delegating does not reduce token usage, complexity, or context switching
823
+ - If splitting would add latency without benefit
824
+
825
+ ## Important Task Tool Usage Notes to Remember
826
+ - Whenever possible, parallelize the work that you do. This is true for both tool_calls, and for tasks. Whenever you have independent steps to complete - make tool_calls, or kick off tasks (subagents) in parallel to accomplish them faster. This saves time for the user, which is incredibly important.
827
+ - Remember to use the \`task\` tool to silo independent tasks within a multi-part objective.
828
+ - You should use the \`task\` tool whenever you have a complex task that will take multiple steps, and is independent from other tasks that the agent needs to complete. These agents are highly competent and efficient.`;
829
+ /**
830
+ * Filter state to exclude certain keys when passing to subagents
831
+ */
832
+ function filterStateForSubagent(state) {
833
+ const filtered = {};
834
+ for (const [key, value] of Object.entries(state)) if (!EXCLUDED_STATE_KEYS.includes(key)) filtered[key] = value;
835
+ return filtered;
836
+ }
837
+ /**
838
+ * Create Command with filtered state update from subagent result
839
+ */
840
+ function returnCommandWithStateUpdate(result, toolCallId) {
841
+ const stateUpdate = filterStateForSubagent(result);
842
+ const messages = result.messages;
843
+ const lastMessage = messages?.[messages.length - 1];
844
+ return new Command({ update: {
845
+ ...stateUpdate,
846
+ messages: [new ToolMessage({
847
+ content: lastMessage?.content || "Task completed",
848
+ tool_call_id: toolCallId,
849
+ name: "task"
850
+ })]
851
+ } });
852
+ }
853
+ /**
854
+ * Create subagent instances from specifications
855
+ */
856
+ function getSubagents(options) {
857
+ const { defaultModel, defaultTools, defaultMiddleware, defaultInterruptOn, subagents, generalPurposeAgent } = options;
858
+ const defaultSubagentMiddleware = defaultMiddleware || [];
859
+ const agents = {};
860
+ const subagentDescriptions = [];
861
+ if (generalPurposeAgent) {
862
+ const generalPurposeMiddleware = [...defaultSubagentMiddleware];
863
+ if (defaultInterruptOn) generalPurposeMiddleware.push(humanInTheLoopMiddleware({ interruptOn: defaultInterruptOn }));
864
+ agents["general-purpose"] = createAgent({
865
+ model: defaultModel,
866
+ systemPrompt: DEFAULT_SUBAGENT_PROMPT,
867
+ tools: defaultTools,
868
+ middleware: generalPurposeMiddleware
869
+ });
870
+ subagentDescriptions.push(`- general-purpose: ${DEFAULT_GENERAL_PURPOSE_DESCRIPTION}`);
871
+ }
872
+ for (const agentParams of subagents) {
873
+ subagentDescriptions.push(`- ${agentParams.name}: ${agentParams.description}`);
874
+ const middleware = agentParams.middleware ? [...defaultSubagentMiddleware, ...agentParams.middleware] : [...defaultSubagentMiddleware];
875
+ const interruptOn = agentParams.interruptOn || defaultInterruptOn;
876
+ if (interruptOn) middleware.push(humanInTheLoopMiddleware({ interruptOn }));
877
+ agents[agentParams.name] = createAgent({
878
+ model: agentParams.model ?? defaultModel,
879
+ systemPrompt: agentParams.systemPrompt,
880
+ tools: agentParams.tools ?? defaultTools,
881
+ middleware
882
+ });
883
+ }
884
+ return {
885
+ agents,
886
+ descriptions: subagentDescriptions
887
+ };
888
+ }
889
+ /**
890
+ * Create the task tool for invoking subagents
891
+ */
892
+ function createTaskTool(options) {
893
+ const { defaultModel, defaultTools, defaultMiddleware, defaultInterruptOn, subagents, generalPurposeAgent, taskDescription } = options;
894
+ const { agents: subagentGraphs, descriptions: subagentDescriptions } = getSubagents({
895
+ defaultModel,
896
+ defaultTools,
897
+ defaultMiddleware,
898
+ defaultInterruptOn,
899
+ subagents,
900
+ generalPurposeAgent
901
+ });
902
+ return tool(async (input, config) => {
903
+ const { description, subagent_type } = input;
904
+ if (!(subagent_type in subagentGraphs)) {
905
+ const allowedTypes = Object.keys(subagentGraphs).map((k) => `\`${k}\``).join(", ");
906
+ throw new Error(`Error: invoked agent of type ${subagent_type}, the only allowed types are ${allowedTypes}`);
907
+ }
908
+ const subagent = subagentGraphs[subagent_type];
909
+ const subagentState = filterStateForSubagent(getCurrentTaskInput());
910
+ subagentState.messages = [new HumanMessage({ content: description })];
911
+ const result = await subagent.invoke(subagentState);
912
+ if (!config.toolCall?.id) throw new Error("Tool call ID is required for subagent invocation");
913
+ return returnCommandWithStateUpdate(result, config.toolCall.id);
914
+ }, {
915
+ name: "task",
916
+ description: taskDescription ? taskDescription : getTaskToolDescription(subagentDescriptions),
917
+ schema: z.object({
918
+ description: z.string().describe("The task to execute with the selected agent"),
919
+ subagent_type: z.string().describe(`Name of the agent to use. Available: ${Object.keys(subagentGraphs).join(", ")}`)
920
+ })
921
+ });
922
+ }
923
+ /**
924
+ * Create subagent middleware with task tool
925
+ */
926
+ function createSubAgentMiddleware(options) {
927
+ const { defaultModel, defaultTools = [], defaultMiddleware = null, defaultInterruptOn = null, subagents = [], systemPrompt = TASK_SYSTEM_PROMPT, generalPurposeAgent = true, taskDescription = null } = options;
928
+ return createMiddleware({
929
+ name: "subAgentMiddleware",
930
+ tools: [createTaskTool({
931
+ defaultModel,
932
+ defaultTools,
933
+ defaultMiddleware,
934
+ defaultInterruptOn,
935
+ subagents,
936
+ generalPurposeAgent,
937
+ taskDescription
938
+ })],
939
+ wrapModelCall: async (request, handler) => {
940
+ if (systemPrompt !== null) {
941
+ const currentPrompt = request.systemPrompt || "";
942
+ const newPrompt = currentPrompt ? `${currentPrompt}\n\n${systemPrompt}` : systemPrompt;
943
+ return handler({
944
+ ...request,
945
+ systemPrompt: newPrompt
946
+ });
947
+ }
948
+ return handler(request);
949
+ }
950
+ });
951
+ }
952
+
953
+ //#endregion
954
+ //#region src/middleware/patch_tool_calls.ts
955
+ /**
956
+ * Create middleware that patches dangling tool calls in the messages history.
957
+ *
958
+ * When an AI message contains tool_calls but subsequent messages don't include
959
+ * the corresponding ToolMessage responses, this middleware adds synthetic
960
+ * ToolMessages saying the tool call was cancelled.
961
+ *
962
+ * @returns AgentMiddleware that patches dangling tool calls
963
+ *
964
+ * @example
965
+ * ```typescript
966
+ * import { createAgent } from "langchain";
967
+ * import { createPatchToolCallsMiddleware } from "./middleware/patch_tool_calls";
968
+ *
969
+ * const agent = createAgent({
970
+ * model: "claude-sonnet-4-5-20250929",
971
+ * middleware: [createPatchToolCallsMiddleware()],
972
+ * });
973
+ * ```
974
+ */
975
+ function createPatchToolCallsMiddleware() {
976
+ return createMiddleware({
977
+ name: "patchToolCallsMiddleware",
978
+ beforeAgent: async (state) => {
979
+ const messages = state.messages;
980
+ if (!messages || messages.length === 0) return;
981
+ const patchedMessages = [];
982
+ for (let i = 0; i < messages.length; i++) {
983
+ const msg = messages[i];
984
+ patchedMessages.push(msg);
985
+ if (AIMessage.isInstance(msg) && msg.tool_calls != null) {
986
+ for (const toolCall of msg.tool_calls) if (!messages.slice(i).find((m) => ToolMessage.isInstance(m) && m.tool_call_id === toolCall.id)) {
987
+ const toolMsg = `Tool call ${toolCall.name} with id ${toolCall.id} was cancelled - another message came in before it could be completed.`;
988
+ patchedMessages.push(new ToolMessage({
989
+ content: toolMsg,
990
+ name: toolCall.name,
991
+ tool_call_id: toolCall.id
992
+ }));
993
+ }
994
+ }
995
+ }
996
+ return { messages: [new RemoveMessage({ id: REMOVE_ALL_MESSAGES }), ...patchedMessages] };
997
+ }
998
+ });
999
+ }
1000
+
1001
+ //#endregion
1002
+ //#region src/backends/store.ts
1003
+ /**
1004
+ * Backend that stores files in LangGraph's BaseStore (persistent).
1005
+ *
1006
+ * Uses LangGraph's Store for persistent, cross-conversation storage.
1007
+ * Files are organized via namespaces and persist across all threads.
1008
+ *
1009
+ * The namespace can include an optional assistant_id for multi-agent isolation.
1010
+ */
1011
+ var StoreBackend = class {
1012
+ stateAndStore;
1013
+ constructor(stateAndStore) {
1014
+ this.stateAndStore = stateAndStore;
1015
+ }
1016
+ /**
1017
+ * Get the store instance.
1018
+ *
1019
+ * @returns BaseStore instance
1020
+ * @throws Error if no store is available
1021
+ */
1022
+ getStore() {
1023
+ const store = this.stateAndStore.store;
1024
+ if (!store) throw new Error("Store is required but not available in StateAndStore");
1025
+ return store;
1026
+ }
1027
+ /**
1028
+ * Get the namespace for store operations.
1029
+ *
1030
+ * If an assistant_id is available in stateAndStore, return
1031
+ * [assistant_id, "filesystem"] to provide per-assistant isolation.
1032
+ * Otherwise return ["filesystem"].
1033
+ */
1034
+ getNamespace() {
1035
+ const namespace = "filesystem";
1036
+ const assistantId = this.stateAndStore.assistantId;
1037
+ if (assistantId) return [assistantId, namespace];
1038
+ return [namespace];
1039
+ }
1040
+ /**
1041
+ * Convert a store Item to FileData format.
1042
+ *
1043
+ * @param storeItem - The store Item containing file data
1044
+ * @returns FileData object
1045
+ * @throws Error if required fields are missing or have incorrect types
1046
+ */
1047
+ convertStoreItemToFileData(storeItem) {
1048
+ const value = storeItem.value;
1049
+ if (!value.content || !Array.isArray(value.content) || typeof value.created_at !== "string" || typeof value.modified_at !== "string") throw new Error(`Store item does not contain valid FileData fields. Got keys: ${Object.keys(value).join(", ")}`);
1050
+ return {
1051
+ content: value.content,
1052
+ created_at: value.created_at,
1053
+ modified_at: value.modified_at
1054
+ };
1055
+ }
1056
+ /**
1057
+ * Convert FileData to a value suitable for store.put().
1058
+ *
1059
+ * @param fileData - The FileData to convert
1060
+ * @returns Object with content, created_at, and modified_at fields
1061
+ */
1062
+ convertFileDataToStoreValue(fileData) {
1063
+ return {
1064
+ content: fileData.content,
1065
+ created_at: fileData.created_at,
1066
+ modified_at: fileData.modified_at
1067
+ };
1068
+ }
1069
+ /**
1070
+ * Search store with automatic pagination to retrieve all results.
1071
+ *
1072
+ * @param store - The store to search
1073
+ * @param namespace - Hierarchical path prefix to search within
1074
+ * @param options - Optional query, filter, and page_size
1075
+ * @returns List of all items matching the search criteria
1076
+ */
1077
+ async searchStorePaginated(store, namespace, options = {}) {
1078
+ const { query, filter, pageSize = 100 } = options;
1079
+ const allItems = [];
1080
+ let offset = 0;
1081
+ while (true) {
1082
+ const pageItems = await store.search(namespace, {
1083
+ query,
1084
+ filter,
1085
+ limit: pageSize,
1086
+ offset
1087
+ });
1088
+ if (!pageItems || pageItems.length === 0) break;
1089
+ allItems.push(...pageItems);
1090
+ if (pageItems.length < pageSize) break;
1091
+ offset += pageSize;
1092
+ }
1093
+ return allItems;
1094
+ }
1095
+ /**
1096
+ * List files and directories in the specified directory (non-recursive).
1097
+ *
1098
+ * @param path - Absolute path to directory
1099
+ * @returns List of FileInfo objects for files and directories directly in the directory.
1100
+ * Directories have a trailing / in their path and is_dir=true.
1101
+ */
1102
+ async lsInfo(path$1) {
1103
+ const store = this.getStore();
1104
+ const namespace = this.getNamespace();
1105
+ const items = await this.searchStorePaginated(store, namespace);
1106
+ const infos = [];
1107
+ const subdirs = /* @__PURE__ */ new Set();
1108
+ const normalizedPath = path$1.endsWith("/") ? path$1 : path$1 + "/";
1109
+ for (const item of items) {
1110
+ const itemKey = String(item.key);
1111
+ if (!itemKey.startsWith(normalizedPath)) continue;
1112
+ const relative = itemKey.substring(normalizedPath.length);
1113
+ if (relative.includes("/")) {
1114
+ const subdirName = relative.split("/")[0];
1115
+ subdirs.add(normalizedPath + subdirName + "/");
1116
+ continue;
1117
+ }
1118
+ try {
1119
+ const fd = this.convertStoreItemToFileData(item);
1120
+ const size = fd.content.join("\n").length;
1121
+ infos.push({
1122
+ path: itemKey,
1123
+ is_dir: false,
1124
+ size,
1125
+ modified_at: fd.modified_at
1126
+ });
1127
+ } catch {
1128
+ continue;
1129
+ }
1130
+ }
1131
+ for (const subdir of Array.from(subdirs).sort()) infos.push({
1132
+ path: subdir,
1133
+ is_dir: true,
1134
+ size: 0,
1135
+ modified_at: ""
1136
+ });
1137
+ infos.sort((a, b) => a.path.localeCompare(b.path));
1138
+ return infos;
1139
+ }
1140
+ /**
1141
+ * Read file content with line numbers.
1142
+ *
1143
+ * @param filePath - Absolute file path
1144
+ * @param offset - Line offset to start reading from (0-indexed)
1145
+ * @param limit - Maximum number of lines to read
1146
+ * @returns Formatted file content with line numbers, or error message
1147
+ */
1148
+ async read(filePath, offset = 0, limit = 2e3) {
1149
+ const store = this.getStore();
1150
+ const namespace = this.getNamespace();
1151
+ const item = await store.get(namespace, filePath);
1152
+ if (!item) return `Error: File '${filePath}' not found`;
1153
+ try {
1154
+ return formatReadResponse(this.convertStoreItemToFileData(item), offset, limit);
1155
+ } catch (e) {
1156
+ return `Error: ${e.message}`;
1157
+ }
1158
+ }
1159
+ /**
1160
+ * Create a new file with content.
1161
+ * Returns WriteResult. External storage sets filesUpdate=null.
1162
+ */
1163
+ async write(filePath, content) {
1164
+ const store = this.getStore();
1165
+ const namespace = this.getNamespace();
1166
+ if (await store.get(namespace, filePath)) return { error: `Cannot write to ${filePath} because it already exists. Read and then make an edit, or write to a new path.` };
1167
+ const fileData = createFileData(content);
1168
+ const storeValue = this.convertFileDataToStoreValue(fileData);
1169
+ await store.put(namespace, filePath, storeValue);
1170
+ return {
1171
+ path: filePath,
1172
+ filesUpdate: null
1173
+ };
1174
+ }
1175
+ /**
1176
+ * Edit a file by replacing string occurrences.
1177
+ * Returns EditResult. External storage sets filesUpdate=null.
1178
+ */
1179
+ async edit(filePath, oldString, newString, replaceAll = false) {
1180
+ const store = this.getStore();
1181
+ const namespace = this.getNamespace();
1182
+ const item = await store.get(namespace, filePath);
1183
+ if (!item) return { error: `Error: File '${filePath}' not found` };
1184
+ try {
1185
+ const fileData = this.convertStoreItemToFileData(item);
1186
+ const result = performStringReplacement(fileDataToString(fileData), oldString, newString, replaceAll);
1187
+ if (typeof result === "string") return { error: result };
1188
+ const [newContent, occurrences] = result;
1189
+ const newFileData = updateFileData(fileData, newContent);
1190
+ const storeValue = this.convertFileDataToStoreValue(newFileData);
1191
+ await store.put(namespace, filePath, storeValue);
1192
+ return {
1193
+ path: filePath,
1194
+ filesUpdate: null,
1195
+ occurrences
1196
+ };
1197
+ } catch (e) {
1198
+ return { error: `Error: ${e.message}` };
1199
+ }
1200
+ }
1201
+ /**
1202
+ * Structured search results or error string for invalid input.
1203
+ */
1204
+ async grepRaw(pattern, path$1 = "/", glob = null) {
1205
+ const store = this.getStore();
1206
+ const namespace = this.getNamespace();
1207
+ const items = await this.searchStorePaginated(store, namespace);
1208
+ const files = {};
1209
+ for (const item of items) try {
1210
+ files[item.key] = this.convertStoreItemToFileData(item);
1211
+ } catch {
1212
+ continue;
1213
+ }
1214
+ return grepMatchesFromFiles(files, pattern, path$1, glob);
1215
+ }
1216
+ /**
1217
+ * Structured glob matching returning FileInfo objects.
1218
+ */
1219
+ async globInfo(pattern, path$1 = "/") {
1220
+ const store = this.getStore();
1221
+ const namespace = this.getNamespace();
1222
+ const items = await this.searchStorePaginated(store, namespace);
1223
+ const files = {};
1224
+ for (const item of items) try {
1225
+ files[item.key] = this.convertStoreItemToFileData(item);
1226
+ } catch {
1227
+ continue;
1228
+ }
1229
+ const result = globSearchFiles(files, pattern, path$1);
1230
+ if (result === "No files found") return [];
1231
+ const paths = result.split("\n");
1232
+ const infos = [];
1233
+ for (const p of paths) {
1234
+ const fd = files[p];
1235
+ const size = fd ? fd.content.join("\n").length : 0;
1236
+ infos.push({
1237
+ path: p,
1238
+ is_dir: false,
1239
+ size,
1240
+ modified_at: fd?.modified_at || ""
1241
+ });
1242
+ }
1243
+ return infos;
1244
+ }
1245
+ };
1246
+
1247
+ //#endregion
1248
+ //#region src/backends/filesystem.ts
1249
+ const SUPPORTS_NOFOLLOW = fsSync.constants.O_NOFOLLOW !== void 0;
1250
+ /**
1251
+ * Backend that reads and writes files directly from the filesystem.
1252
+ *
1253
+ * Files are accessed using their actual filesystem paths. Relative paths are
1254
+ * resolved relative to the current working directory. Content is read/written
1255
+ * as plain text, and metadata (timestamps) are derived from filesystem stats.
1256
+ */
1257
+ var FilesystemBackend = class {
1258
+ cwd;
1259
+ virtualMode;
1260
+ maxFileSizeBytes;
1261
+ constructor(options = {}) {
1262
+ const { rootDir, virtualMode = false, maxFileSizeMb = 10 } = options;
1263
+ this.cwd = rootDir ? path.resolve(rootDir) : process.cwd();
1264
+ this.virtualMode = virtualMode;
1265
+ this.maxFileSizeBytes = maxFileSizeMb * 1024 * 1024;
1266
+ }
1267
+ /**
1268
+ * Resolve a file path with security checks.
1269
+ *
1270
+ * When virtualMode=true, treat incoming paths as virtual absolute paths under
1271
+ * this.cwd, disallow traversal (.., ~) and ensure resolved path stays within root.
1272
+ * When virtualMode=false, preserve legacy behavior: absolute paths are allowed
1273
+ * as-is; relative paths resolve under cwd.
1274
+ *
1275
+ * @param key - File path (absolute, relative, or virtual when virtualMode=true)
1276
+ * @returns Resolved absolute path string
1277
+ * @throws Error if path traversal detected or path outside root
1278
+ */
1279
+ resolvePath(key) {
1280
+ if (this.virtualMode) {
1281
+ const vpath = key.startsWith("/") ? key : "/" + key;
1282
+ if (vpath.includes("..") || vpath.startsWith("~")) throw new Error("Path traversal not allowed");
1283
+ const full = path.resolve(this.cwd, vpath.substring(1));
1284
+ const relative = path.relative(this.cwd, full);
1285
+ if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Path: ${full} outside root directory: ${this.cwd}`);
1286
+ return full;
1287
+ }
1288
+ if (path.isAbsolute(key)) return key;
1289
+ return path.resolve(this.cwd, key);
1290
+ }
1291
+ /**
1292
+ * List files and directories in the specified directory (non-recursive).
1293
+ *
1294
+ * @param dirPath - Absolute directory path to list files from
1295
+ * @returns List of FileInfo objects for files and directories directly in the directory.
1296
+ * Directories have a trailing / in their path and is_dir=true.
1297
+ */
1298
+ async lsInfo(dirPath) {
1299
+ try {
1300
+ const resolvedPath = this.resolvePath(dirPath);
1301
+ if (!(await fs.stat(resolvedPath)).isDirectory()) return [];
1302
+ const entries = await fs.readdir(resolvedPath, { withFileTypes: true });
1303
+ const results = [];
1304
+ const cwdStr = this.cwd.endsWith(path.sep) ? this.cwd : this.cwd + path.sep;
1305
+ for (const entry of entries) {
1306
+ const fullPath = path.join(resolvedPath, entry.name);
1307
+ try {
1308
+ const entryStat = await fs.stat(fullPath);
1309
+ const isFile = entryStat.isFile();
1310
+ const isDir = entryStat.isDirectory();
1311
+ if (!this.virtualMode) {
1312
+ if (isFile) results.push({
1313
+ path: fullPath,
1314
+ is_dir: false,
1315
+ size: entryStat.size,
1316
+ modified_at: entryStat.mtime.toISOString()
1317
+ });
1318
+ else if (isDir) results.push({
1319
+ path: fullPath + path.sep,
1320
+ is_dir: true,
1321
+ size: 0,
1322
+ modified_at: entryStat.mtime.toISOString()
1323
+ });
1324
+ } else {
1325
+ let relativePath;
1326
+ if (fullPath.startsWith(cwdStr)) relativePath = fullPath.substring(cwdStr.length);
1327
+ else if (fullPath.startsWith(this.cwd)) relativePath = fullPath.substring(this.cwd.length).replace(/^[/\\]/, "");
1328
+ else relativePath = fullPath;
1329
+ relativePath = relativePath.split(path.sep).join("/");
1330
+ const virtPath = "/" + relativePath;
1331
+ if (isFile) results.push({
1332
+ path: virtPath,
1333
+ is_dir: false,
1334
+ size: entryStat.size,
1335
+ modified_at: entryStat.mtime.toISOString()
1336
+ });
1337
+ else if (isDir) results.push({
1338
+ path: virtPath + "/",
1339
+ is_dir: true,
1340
+ size: 0,
1341
+ modified_at: entryStat.mtime.toISOString()
1342
+ });
1343
+ }
1344
+ } catch {
1345
+ continue;
1346
+ }
1347
+ }
1348
+ results.sort((a, b) => a.path.localeCompare(b.path));
1349
+ return results;
1350
+ } catch {
1351
+ return [];
1352
+ }
1353
+ }
1354
+ /**
1355
+ * Read file content with line numbers.
1356
+ *
1357
+ * @param filePath - Absolute or relative file path
1358
+ * @param offset - Line offset to start reading from (0-indexed)
1359
+ * @param limit - Maximum number of lines to read
1360
+ * @returns Formatted file content with line numbers, or error message
1361
+ */
1362
+ async read(filePath, offset = 0, limit = 2e3) {
1363
+ try {
1364
+ const resolvedPath = this.resolvePath(filePath);
1365
+ let content;
1366
+ if (SUPPORTS_NOFOLLOW) {
1367
+ if (!(await fs.stat(resolvedPath)).isFile()) return `Error: File '${filePath}' not found`;
1368
+ const fd = await fs.open(resolvedPath, fsSync.constants.O_RDONLY | fsSync.constants.O_NOFOLLOW);
1369
+ try {
1370
+ content = await fd.readFile({ encoding: "utf-8" });
1371
+ } finally {
1372
+ await fd.close();
1373
+ }
1374
+ } else {
1375
+ const stat = await fs.lstat(resolvedPath);
1376
+ if (stat.isSymbolicLink()) return `Error: Symlinks are not allowed: ${filePath}`;
1377
+ if (!stat.isFile()) return `Error: File '${filePath}' not found`;
1378
+ content = await fs.readFile(resolvedPath, "utf-8");
1379
+ }
1380
+ const emptyMsg = checkEmptyContent(content);
1381
+ if (emptyMsg) return emptyMsg;
1382
+ const lines = content.split("\n");
1383
+ const startIdx = offset;
1384
+ const endIdx = Math.min(startIdx + limit, lines.length);
1385
+ if (startIdx >= lines.length) return `Error: Line offset ${offset} exceeds file length (${lines.length} lines)`;
1386
+ return formatContentWithLineNumbers(lines.slice(startIdx, endIdx), startIdx + 1);
1387
+ } catch (e) {
1388
+ return `Error reading file '${filePath}': ${e.message}`;
1389
+ }
1390
+ }
1391
+ /**
1392
+ * Create a new file with content.
1393
+ * Returns WriteResult. External storage sets filesUpdate=null.
1394
+ */
1395
+ async write(filePath, content) {
1396
+ try {
1397
+ const resolvedPath = this.resolvePath(filePath);
1398
+ try {
1399
+ if ((await fs.lstat(resolvedPath)).isSymbolicLink()) return { error: `Cannot write to ${filePath} because it is a symlink. Symlinks are not allowed.` };
1400
+ return { error: `Cannot write to ${filePath} because it already exists. Read and then make an edit, or write to a new path.` };
1401
+ } catch {}
1402
+ await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
1403
+ if (SUPPORTS_NOFOLLOW) {
1404
+ const flags = fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_TRUNC | fsSync.constants.O_NOFOLLOW;
1405
+ const fd = await fs.open(resolvedPath, flags, 420);
1406
+ try {
1407
+ await fd.writeFile(content, "utf-8");
1408
+ } finally {
1409
+ await fd.close();
1410
+ }
1411
+ } else await fs.writeFile(resolvedPath, content, "utf-8");
1412
+ return {
1413
+ path: filePath,
1414
+ filesUpdate: null
1415
+ };
1416
+ } catch (e) {
1417
+ return { error: `Error writing file '${filePath}': ${e.message}` };
1418
+ }
1419
+ }
1420
+ /**
1421
+ * Edit a file by replacing string occurrences.
1422
+ * Returns EditResult. External storage sets filesUpdate=null.
1423
+ */
1424
+ async edit(filePath, oldString, newString, replaceAll = false) {
1425
+ try {
1426
+ const resolvedPath = this.resolvePath(filePath);
1427
+ let content;
1428
+ if (SUPPORTS_NOFOLLOW) {
1429
+ if (!(await fs.stat(resolvedPath)).isFile()) return { error: `Error: File '${filePath}' not found` };
1430
+ const fd = await fs.open(resolvedPath, fsSync.constants.O_RDONLY | fsSync.constants.O_NOFOLLOW);
1431
+ try {
1432
+ content = await fd.readFile({ encoding: "utf-8" });
1433
+ } finally {
1434
+ await fd.close();
1435
+ }
1436
+ } else {
1437
+ const stat = await fs.lstat(resolvedPath);
1438
+ if (stat.isSymbolicLink()) return { error: `Error: Symlinks are not allowed: ${filePath}` };
1439
+ if (!stat.isFile()) return { error: `Error: File '${filePath}' not found` };
1440
+ content = await fs.readFile(resolvedPath, "utf-8");
1441
+ }
1442
+ const result = performStringReplacement(content, oldString, newString, replaceAll);
1443
+ if (typeof result === "string") return { error: result };
1444
+ const [newContent, occurrences] = result;
1445
+ if (SUPPORTS_NOFOLLOW) {
1446
+ const flags = fsSync.constants.O_WRONLY | fsSync.constants.O_TRUNC | fsSync.constants.O_NOFOLLOW;
1447
+ const fd = await fs.open(resolvedPath, flags);
1448
+ try {
1449
+ await fd.writeFile(newContent, "utf-8");
1450
+ } finally {
1451
+ await fd.close();
1452
+ }
1453
+ } else await fs.writeFile(resolvedPath, newContent, "utf-8");
1454
+ return {
1455
+ path: filePath,
1456
+ filesUpdate: null,
1457
+ occurrences
1458
+ };
1459
+ } catch (e) {
1460
+ return { error: `Error editing file '${filePath}': ${e.message}` };
1461
+ }
1462
+ }
1463
+ /**
1464
+ * Structured search results or error string for invalid input.
1465
+ */
1466
+ async grepRaw(pattern, dirPath = "/", glob = null) {
1467
+ try {
1468
+ new RegExp(pattern);
1469
+ } catch (e) {
1470
+ return `Invalid regex pattern: ${e.message}`;
1471
+ }
1472
+ let baseFull;
1473
+ try {
1474
+ baseFull = this.resolvePath(dirPath || ".");
1475
+ } catch {
1476
+ return [];
1477
+ }
1478
+ try {
1479
+ await fs.stat(baseFull);
1480
+ } catch {
1481
+ return [];
1482
+ }
1483
+ let results = await this.ripgrepSearch(pattern, baseFull, glob);
1484
+ if (results === null) results = await this.pythonSearch(pattern, baseFull, glob);
1485
+ const matches = [];
1486
+ for (const [fpath, items] of Object.entries(results)) for (const [lineNum, lineText] of items) matches.push({
1487
+ path: fpath,
1488
+ line: lineNum,
1489
+ text: lineText
1490
+ });
1491
+ return matches;
1492
+ }
1493
+ /**
1494
+ * Try to use ripgrep for fast searching.
1495
+ * Returns null if ripgrep is not available or fails.
1496
+ */
1497
+ async ripgrepSearch(pattern, baseFull, includeGlob) {
1498
+ return new Promise((resolve) => {
1499
+ const args = ["--json"];
1500
+ if (includeGlob) args.push("--glob", includeGlob);
1501
+ args.push("--", pattern, baseFull);
1502
+ const proc = spawn("rg", args, { timeout: 3e4 });
1503
+ const results = {};
1504
+ let output = "";
1505
+ proc.stdout.on("data", (data) => {
1506
+ output += data.toString();
1507
+ });
1508
+ proc.on("close", (code) => {
1509
+ if (code !== 0 && code !== 1) {
1510
+ resolve(null);
1511
+ return;
1512
+ }
1513
+ for (const line of output.split("\n")) {
1514
+ if (!line.trim()) continue;
1515
+ try {
1516
+ const data = JSON.parse(line);
1517
+ if (data.type !== "match") continue;
1518
+ const pdata = data.data || {};
1519
+ const ftext = pdata.path?.text;
1520
+ if (!ftext) continue;
1521
+ let virtPath;
1522
+ if (this.virtualMode) try {
1523
+ const resolved = path.resolve(ftext);
1524
+ const relative = path.relative(this.cwd, resolved);
1525
+ if (relative.startsWith("..")) continue;
1526
+ virtPath = "/" + relative.split(path.sep).join("/");
1527
+ } catch {
1528
+ continue;
1529
+ }
1530
+ else virtPath = ftext;
1531
+ const ln = pdata.line_number;
1532
+ const lt = pdata.lines?.text?.replace(/\n$/, "") || "";
1533
+ if (ln === void 0) continue;
1534
+ if (!results[virtPath]) results[virtPath] = [];
1535
+ results[virtPath].push([ln, lt]);
1536
+ } catch {
1537
+ continue;
1538
+ }
1539
+ }
1540
+ resolve(results);
1541
+ });
1542
+ proc.on("error", () => {
1543
+ resolve(null);
1544
+ });
1545
+ });
1546
+ }
1547
+ /**
1548
+ * Fallback regex search implementation.
1549
+ */
1550
+ async pythonSearch(pattern, baseFull, includeGlob) {
1551
+ let regex;
1552
+ try {
1553
+ regex = new RegExp(pattern);
1554
+ } catch {
1555
+ return {};
1556
+ }
1557
+ const results = {};
1558
+ const files = await fg("**/*", {
1559
+ cwd: (await fs.stat(baseFull)).isDirectory() ? baseFull : path.dirname(baseFull),
1560
+ absolute: true,
1561
+ onlyFiles: true,
1562
+ dot: true
1563
+ });
1564
+ for (const fp of files) try {
1565
+ if (includeGlob && !micromatch.isMatch(path.basename(fp), includeGlob)) continue;
1566
+ if ((await fs.stat(fp)).size > this.maxFileSizeBytes) continue;
1567
+ const lines = (await fs.readFile(fp, "utf-8")).split("\n");
1568
+ for (let i = 0; i < lines.length; i++) {
1569
+ const line = lines[i];
1570
+ if (regex.test(line)) {
1571
+ let virtPath;
1572
+ if (this.virtualMode) try {
1573
+ const relative = path.relative(this.cwd, fp);
1574
+ if (relative.startsWith("..")) continue;
1575
+ virtPath = "/" + relative.split(path.sep).join("/");
1576
+ } catch {
1577
+ continue;
1578
+ }
1579
+ else virtPath = fp;
1580
+ if (!results[virtPath]) results[virtPath] = [];
1581
+ results[virtPath].push([i + 1, line]);
1582
+ }
1583
+ }
1584
+ } catch {
1585
+ continue;
1586
+ }
1587
+ return results;
1588
+ }
1589
+ /**
1590
+ * Structured glob matching returning FileInfo objects.
1591
+ */
1592
+ async globInfo(pattern, searchPath = "/") {
1593
+ if (pattern.startsWith("/")) pattern = pattern.substring(1);
1594
+ const resolvedSearchPath = searchPath === "/" ? this.cwd : this.resolvePath(searchPath);
1595
+ try {
1596
+ if (!(await fs.stat(resolvedSearchPath)).isDirectory()) return [];
1597
+ } catch {
1598
+ return [];
1599
+ }
1600
+ const results = [];
1601
+ try {
1602
+ const matches = await fg(pattern, {
1603
+ cwd: resolvedSearchPath,
1604
+ absolute: true,
1605
+ onlyFiles: true,
1606
+ dot: true
1607
+ });
1608
+ for (const matchedPath of matches) try {
1609
+ const stat = await fs.stat(matchedPath);
1610
+ if (!stat.isFile()) continue;
1611
+ const normalizedPath = matchedPath.split("/").join(path.sep);
1612
+ if (!this.virtualMode) results.push({
1613
+ path: normalizedPath,
1614
+ is_dir: false,
1615
+ size: stat.size,
1616
+ modified_at: stat.mtime.toISOString()
1617
+ });
1618
+ else {
1619
+ const cwdStr = this.cwd.endsWith(path.sep) ? this.cwd : this.cwd + path.sep;
1620
+ let relativePath;
1621
+ if (normalizedPath.startsWith(cwdStr)) relativePath = normalizedPath.substring(cwdStr.length);
1622
+ else if (normalizedPath.startsWith(this.cwd)) relativePath = normalizedPath.substring(this.cwd.length).replace(/^[/\\]/, "");
1623
+ else relativePath = normalizedPath;
1624
+ relativePath = relativePath.split(path.sep).join("/");
1625
+ const virt = "/" + relativePath;
1626
+ results.push({
1627
+ path: virt,
1628
+ is_dir: false,
1629
+ size: stat.size,
1630
+ modified_at: stat.mtime.toISOString()
1631
+ });
1632
+ }
1633
+ } catch {
1634
+ continue;
1635
+ }
1636
+ } catch {}
1637
+ results.sort((a, b) => a.path.localeCompare(b.path));
1638
+ return results;
1639
+ }
1640
+ };
1641
+
1642
+ //#endregion
1643
+ //#region src/backends/composite.ts
1644
+ /**
1645
+ * Backend that routes file operations to different backends based on path prefix.
1646
+ *
1647
+ * This enables hybrid storage strategies like:
1648
+ * - `/memories/` → StoreBackend (persistent, cross-thread)
1649
+ * - Everything else → StateBackend (ephemeral, per-thread)
1650
+ *
1651
+ * The CompositeBackend handles path prefix stripping/re-adding transparently.
1652
+ */
1653
+ var CompositeBackend = class {
1654
+ default;
1655
+ routes;
1656
+ sortedRoutes;
1657
+ constructor(defaultBackend, routes) {
1658
+ this.default = defaultBackend;
1659
+ this.routes = routes;
1660
+ this.sortedRoutes = Object.entries(routes).sort((a, b) => b[0].length - a[0].length);
1661
+ }
1662
+ /**
1663
+ * Determine which backend handles this key and strip prefix.
1664
+ *
1665
+ * @param key - Original file path
1666
+ * @returns Tuple of [backend, stripped_key] where stripped_key has the route
1667
+ * prefix removed (but keeps leading slash).
1668
+ */
1669
+ getBackendAndKey(key) {
1670
+ for (const [prefix, backend] of this.sortedRoutes) if (key.startsWith(prefix)) {
1671
+ const suffix = key.substring(prefix.length);
1672
+ return [backend, suffix ? "/" + suffix : "/"];
1673
+ }
1674
+ return [this.default, key];
1675
+ }
1676
+ /**
1677
+ * List files and directories in the specified directory (non-recursive).
1678
+ *
1679
+ * @param path - Absolute path to directory
1680
+ * @returns List of FileInfo objects with route prefixes added, for files and directories
1681
+ * directly in the directory. Directories have a trailing / in their path and is_dir=true.
1682
+ */
1683
+ async lsInfo(path$1) {
1684
+ for (const [routePrefix, backend] of this.sortedRoutes) if (path$1.startsWith(routePrefix.replace(/\/$/, ""))) {
1685
+ const suffix = path$1.substring(routePrefix.length);
1686
+ const searchPath = suffix ? "/" + suffix : "/";
1687
+ const infos = await backend.lsInfo(searchPath);
1688
+ const prefixed = [];
1689
+ for (const fi of infos) prefixed.push({
1690
+ ...fi,
1691
+ path: routePrefix.slice(0, -1) + fi.path
1692
+ });
1693
+ return prefixed;
1694
+ }
1695
+ if (path$1 === "/") {
1696
+ const results = [];
1697
+ const defaultInfos = await this.default.lsInfo(path$1);
1698
+ results.push(...defaultInfos);
1699
+ for (const [routePrefix] of this.sortedRoutes) results.push({
1700
+ path: routePrefix,
1701
+ is_dir: true,
1702
+ size: 0,
1703
+ modified_at: ""
1704
+ });
1705
+ results.sort((a, b) => a.path.localeCompare(b.path));
1706
+ return results;
1707
+ }
1708
+ return await this.default.lsInfo(path$1);
1709
+ }
1710
+ /**
1711
+ * Read file content, routing to appropriate backend.
1712
+ *
1713
+ * @param filePath - Absolute file path
1714
+ * @param offset - Line offset to start reading from (0-indexed)
1715
+ * @param limit - Maximum number of lines to read
1716
+ * @returns Formatted file content with line numbers, or error message
1717
+ */
1718
+ async read(filePath, offset = 0, limit = 2e3) {
1719
+ const [backend, strippedKey] = this.getBackendAndKey(filePath);
1720
+ return await backend.read(strippedKey, offset, limit);
1721
+ }
1722
+ /**
1723
+ * Structured search results or error string for invalid input.
1724
+ */
1725
+ async grepRaw(pattern, path$1 = "/", glob = null) {
1726
+ for (const [routePrefix, backend] of this.sortedRoutes) if (path$1.startsWith(routePrefix.replace(/\/$/, ""))) {
1727
+ const searchPath = path$1.substring(routePrefix.length - 1);
1728
+ const raw = await backend.grepRaw(pattern, searchPath || "/", glob);
1729
+ if (typeof raw === "string") return raw;
1730
+ return raw.map((m) => ({
1731
+ ...m,
1732
+ path: routePrefix.slice(0, -1) + m.path
1733
+ }));
1734
+ }
1735
+ const allMatches = [];
1736
+ const rawDefault = await this.default.grepRaw(pattern, path$1, glob);
1737
+ if (typeof rawDefault === "string") return rawDefault;
1738
+ allMatches.push(...rawDefault);
1739
+ for (const [routePrefix, backend] of Object.entries(this.routes)) {
1740
+ const raw = await backend.grepRaw(pattern, "/", glob);
1741
+ if (typeof raw === "string") return raw;
1742
+ allMatches.push(...raw.map((m) => ({
1743
+ ...m,
1744
+ path: routePrefix.slice(0, -1) + m.path
1745
+ })));
1746
+ }
1747
+ return allMatches;
1748
+ }
1749
+ /**
1750
+ * Structured glob matching returning FileInfo objects.
1751
+ */
1752
+ async globInfo(pattern, path$1 = "/") {
1753
+ const results = [];
1754
+ for (const [routePrefix, backend] of this.sortedRoutes) if (path$1.startsWith(routePrefix.replace(/\/$/, ""))) {
1755
+ const searchPath = path$1.substring(routePrefix.length - 1);
1756
+ return (await backend.globInfo(pattern, searchPath || "/")).map((fi) => ({
1757
+ ...fi,
1758
+ path: routePrefix.slice(0, -1) + fi.path
1759
+ }));
1760
+ }
1761
+ const defaultInfos = await this.default.globInfo(pattern, path$1);
1762
+ results.push(...defaultInfos);
1763
+ for (const [routePrefix, backend] of Object.entries(this.routes)) {
1764
+ const infos = await backend.globInfo(pattern, "/");
1765
+ results.push(...infos.map((fi) => ({
1766
+ ...fi,
1767
+ path: routePrefix.slice(0, -1) + fi.path
1768
+ })));
1769
+ }
1770
+ results.sort((a, b) => a.path.localeCompare(b.path));
1771
+ return results;
1772
+ }
1773
+ /**
1774
+ * Create a new file, routing to appropriate backend.
1775
+ *
1776
+ * @param filePath - Absolute file path
1777
+ * @param content - File content as string
1778
+ * @returns WriteResult with path or error
1779
+ */
1780
+ async write(filePath, content) {
1781
+ const [backend, strippedKey] = this.getBackendAndKey(filePath);
1782
+ return await backend.write(strippedKey, content);
1783
+ }
1784
+ /**
1785
+ * Edit a file, routing to appropriate backend.
1786
+ *
1787
+ * @param filePath - Absolute file path
1788
+ * @param oldString - String to find and replace
1789
+ * @param newString - Replacement string
1790
+ * @param replaceAll - If true, replace all occurrences
1791
+ * @returns EditResult with path, occurrences, or error
1792
+ */
1793
+ async edit(filePath, oldString, newString, replaceAll = false) {
1794
+ const [backend, strippedKey] = this.getBackendAndKey(filePath);
1795
+ return await backend.edit(strippedKey, oldString, newString, replaceAll);
1796
+ }
1797
+ };
1798
+
1799
+ //#endregion
1800
+ //#region src/agent.ts
1801
+ const BASE_PROMPT = `In order to complete the objective that the user asks of you, you have access to a number of standard tools.`;
1802
+ /**
1803
+ * Create a Deep Agent with middleware-based architecture.
1804
+ *
1805
+ * Matches Python's create_deep_agent function, using middleware for all features:
1806
+ * - Todo management (todoListMiddleware)
1807
+ * - Filesystem tools (createFilesystemMiddleware)
1808
+ * - Subagent delegation (createSubAgentMiddleware)
1809
+ * - Conversation summarization (summarizationMiddleware)
1810
+ * - Prompt caching (anthropicPromptCachingMiddleware)
1811
+ * - Tool call patching (createPatchToolCallsMiddleware)
1812
+ * - Human-in-the-loop (humanInTheLoopMiddleware) - optional
1813
+ *
1814
+ * @param params Configuration parameters for the agent
1815
+ * @returns ReactAgent instance ready for invocation
1816
+ */
1817
+ function createDeepAgent(params = {}) {
1818
+ const { model = "claude-sonnet-4-5-20250929", tools = [], systemPrompt, middleware: customMiddleware = [], subagents = [], responseFormat, contextSchema, checkpointer, store, backend, interruptOn, name } = params;
1819
+ const finalSystemPrompt = systemPrompt ? `${systemPrompt}\n\n${BASE_PROMPT}` : BASE_PROMPT;
1820
+ const filesystemBackend = backend ? backend : (config) => new StateBackend(config);
1821
+ const middleware = [
1822
+ todoListMiddleware(),
1823
+ createFilesystemMiddleware({ backend: filesystemBackend }),
1824
+ createSubAgentMiddleware({
1825
+ defaultModel: model,
1826
+ defaultTools: tools,
1827
+ defaultMiddleware: [
1828
+ todoListMiddleware(),
1829
+ createFilesystemMiddleware({ backend: filesystemBackend }),
1830
+ summarizationMiddleware({
1831
+ model,
1832
+ maxTokensBeforeSummary: 17e4,
1833
+ messagesToKeep: 6
1834
+ }),
1835
+ anthropicPromptCachingMiddleware({ unsupportedModelBehavior: "ignore" }),
1836
+ createPatchToolCallsMiddleware()
1837
+ ],
1838
+ defaultInterruptOn: interruptOn,
1839
+ subagents,
1840
+ generalPurposeAgent: true
1841
+ }),
1842
+ summarizationMiddleware({
1843
+ model,
1844
+ maxTokensBeforeSummary: 17e4,
1845
+ messagesToKeep: 6
1846
+ }),
1847
+ anthropicPromptCachingMiddleware({ unsupportedModelBehavior: "ignore" }),
1848
+ createPatchToolCallsMiddleware()
1849
+ ];
1850
+ if (interruptOn) middleware.push(humanInTheLoopMiddleware({ interruptOn }));
1851
+ middleware.push(...customMiddleware);
1852
+ return createAgent({
1853
+ model,
1854
+ systemPrompt: finalSystemPrompt,
1855
+ tools,
1856
+ middleware,
1857
+ responseFormat,
1858
+ contextSchema,
1859
+ checkpointer,
1860
+ store,
1861
+ name
1862
+ });
1863
+ }
1864
+
1865
+ //#endregion
1866
+ export { CompositeBackend, FilesystemBackend, StateBackend, StoreBackend, createDeepAgent, createFilesystemMiddleware, createPatchToolCallsMiddleware, createSubAgentMiddleware };