Skip to content

Commit a797ff4

Browse files
committed
feat(langchain/agents): add Anthropic tools middleware
Implements client-side Anthropic text editor, memory, and file search tools middleware for agent workflows. Features: - StateClaudeTextEditorMiddleware and StateClaudeMemoryMiddleware for state-based file operations - FilesystemClaudeTextEditorMiddleware and FilesystemClaudeMemoryMiddleware for filesystem operations - Path validation with traversal protection and prefix enforcement - File operations: view, create, str_replace, insert, delete, rename - Comprehensive test coverage (79 tests total) Ports Python implementation langchain-ai/langchain#33384
1 parent fd44c11 commit a797ff4

File tree

10 files changed

+2797
-0
lines changed

10 files changed

+2797
-0
lines changed
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { FileSystem } from "./FileSystem.js";
2+
3+
/**
4+
* Executes text editor and memory tool commands against a FileSystem.
5+
*
6+
* This class provides the command execution layer for Anthropic's text_editor and
7+
* memory tools, translating high-level commands (view, create, str_replace, etc.)
8+
* into FileSystem operations. It serves as an abstraction between the tool schemas
9+
* and the underlying storage implementation.
10+
*
11+
* The CommandHandler is used by both state-based (LangGraph) and filesystem-based
12+
* middleware implementations, enabling consistent command handling across different
13+
* storage backends.
14+
*
15+
* ## Supported Commands
16+
*
17+
* ### Text Editor and Memory Tools
18+
* - **view**: Display file contents with line numbers or list directory contents
19+
* - **create**: Create a new file or overwrite an existing file
20+
* - **str_replace**: Replace a string occurrence within a file
21+
* - **insert**: Insert text at a specific line number
22+
*
23+
* ### Memory Tool Only
24+
* - **delete**: Delete a file or directory
25+
* - **rename**: Rename or move a file/directory to a new path
26+
*
27+
* @example
28+
* ```ts
29+
* const fileSystem = new StateFileSystem(files, allowedPrefixes, onUpdate);
30+
* const handler = new CommandHandler(fileSystem);
31+
*
32+
* // View file contents
33+
* const contents = await handler.handleViewCommand("/path/to/file.txt");
34+
*
35+
* // Replace string in file
36+
* await handler.handleStrReplaceCommand(
37+
* "/path/to/file.txt",
38+
* "old text",
39+
* "new text"
40+
* );
41+
* ```
42+
*
43+
* @see {@link FileSystem} for the underlying storage interface
44+
* @see {@link TextEditorCommandSchema} for text editor command schemas
45+
* @see {@link MemoryCommandSchema} for memory command schemas
46+
*/
47+
export class CommandHandler {
48+
/**
49+
* Creates a new CommandHandler instance.
50+
* @param fileSystem - The FileSystem implementation to execute commands against
51+
*/
52+
constructor(private fileSystem: FileSystem) {}
53+
54+
/**
55+
* Handle view command - shows file contents or directory listing.
56+
*/
57+
async handleViewCommand(path: string): Promise<string> {
58+
const normalizedPath = this.fileSystem.validatePath(path);
59+
const fileData = await this.fileSystem.readFile(normalizedPath);
60+
61+
if (!fileData) {
62+
// Try listing as directory
63+
const matching = await this.fileSystem.listDirectory(normalizedPath);
64+
if (matching.length > 0) {
65+
return matching.join("\n");
66+
}
67+
throw new Error(`File not found: ${path}`);
68+
}
69+
70+
const lines = fileData.content.split("\n");
71+
const formattedLines = lines.map((line, i) => `${i + 1}|${line}`);
72+
return formattedLines.join("\n");
73+
}
74+
75+
/**
76+
* Handle create command - creates or overwrites a file.
77+
*/
78+
async handleCreateCommand(path: string, fileText: string): Promise<string> {
79+
const normalizedPath = this.fileSystem.validatePath(path);
80+
const existing = await this.fileSystem.readFile(normalizedPath);
81+
const now = new Date().toISOString();
82+
83+
await this.fileSystem.writeFile(normalizedPath, {
84+
content: fileText,
85+
created_at: existing ? existing.created_at : now,
86+
modified_at: now,
87+
});
88+
89+
return `File created: ${path}`;
90+
}
91+
92+
/**
93+
* Handle str_replace command - replaces a string in a file.
94+
*/
95+
async handleStrReplaceCommand(
96+
path: string,
97+
oldStr: string,
98+
newStr: string
99+
): Promise<string> {
100+
const normalizedPath = this.fileSystem.validatePath(path);
101+
const fileData = await this.fileSystem.readFile(normalizedPath);
102+
if (!fileData) throw new Error(`File not found: ${path}`);
103+
104+
if (!fileData.content.includes(oldStr)) {
105+
throw new Error(`String not found in file: ${oldStr}`);
106+
}
107+
108+
const newContent = fileData.content.replace(oldStr, newStr);
109+
await this.fileSystem.writeFile(normalizedPath, {
110+
content: newContent,
111+
created_at: fileData.created_at,
112+
modified_at: new Date().toISOString(),
113+
});
114+
115+
return `String replaced in file: ${path}`;
116+
}
117+
118+
/**
119+
* Handle insert command - inserts text at a specific line.
120+
*/
121+
async handleInsertCommand(
122+
path: string,
123+
insertLine: number,
124+
textToInsert: string
125+
): Promise<string> {
126+
const normalizedPath = this.fileSystem.validatePath(path);
127+
const fileData = await this.fileSystem.readFile(normalizedPath);
128+
if (!fileData) throw new Error(`File not found: ${path}`);
129+
130+
const lines = fileData.content.split("\n");
131+
const newLines = textToInsert.split("\n");
132+
const updatedLines = [
133+
...lines.slice(0, insertLine),
134+
...newLines,
135+
...lines.slice(insertLine),
136+
];
137+
138+
await this.fileSystem.writeFile(normalizedPath, {
139+
content: updatedLines.join("\n"),
140+
created_at: fileData.created_at,
141+
modified_at: new Date().toISOString(),
142+
});
143+
144+
return `Text inserted in file: ${path}`;
145+
}
146+
147+
/**
148+
* Handle delete command - deletes a file or directory.
149+
*/
150+
async handleDeleteCommand(path: string): Promise<string> {
151+
const normalizedPath = this.fileSystem.validatePath(path);
152+
await this.fileSystem.deleteFile(normalizedPath);
153+
return `File deleted: ${path}`;
154+
}
155+
156+
/**
157+
* Handle rename command - renames/moves a file or directory.
158+
*/
159+
async handleRenameCommand(oldPath: string, newPath: string): Promise<string> {
160+
const normalizedOld = this.fileSystem.validatePath(oldPath);
161+
const normalizedNew = this.fileSystem.validatePath(newPath);
162+
const fileData = await this.fileSystem.readFile(normalizedOld);
163+
if (!fileData) throw new Error(`File not found: ${oldPath}`);
164+
165+
await this.fileSystem.renameFile(normalizedOld, normalizedNew, fileData);
166+
return `File renamed: ${oldPath} -> ${newPath}`;
167+
}
168+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import z from "zod";
2+
3+
/**
4+
* Zod schema for file data.
5+
*/
6+
export const FileDataSchema = z.object({
7+
content: z.string(),
8+
created_at: z.string(),
9+
modified_at: z.string(),
10+
});
11+
12+
export type FileData = z.infer<typeof FileDataSchema>;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { FileData } from "./FileData.js";
2+
3+
/**
4+
* Abstract interface for file system operations.
5+
* Supports both state-based (LangGraph) and physical filesystem implementations.
6+
*/
7+
export interface FileSystem {
8+
/**
9+
* Read a file's contents and metadata.
10+
* @param path - Normalized path to the file
11+
* @returns FileData if file exists, null if not found or is a directory
12+
*/
13+
readFile(path: string): Promise<FileData | null>;
14+
15+
/**
16+
* List files in a directory.
17+
* @param path - Normalized path to the directory
18+
* @returns Array of file paths in the directory
19+
*/
20+
listDirectory(path: string): Promise<string[]>;
21+
22+
/**
23+
* Write a file with content and metadata.
24+
* @param path - Normalized path to write
25+
* @param data - File data to write
26+
* @returns Result with message and optional state updates
27+
*/
28+
writeFile(path: string, data: FileData): Promise<void>;
29+
30+
/**
31+
* Delete a file or directory.
32+
* @param path - Normalized path to delete
33+
* @returns Result with message and optional state updates
34+
*/
35+
deleteFile(path: string): Promise<void>;
36+
37+
/**
38+
* Rename/move a file or directory.
39+
* @param oldPath - Normalized source path
40+
* @param newPath - Normalized destination path
41+
* @param existingData - File data to move
42+
* @returns Result with message and optional state updates
43+
*/
44+
renameFile(
45+
oldPath: string,
46+
newPath: string,
47+
existingData: FileData
48+
): Promise<void>;
49+
50+
/**
51+
* Validate and normalize a file path.
52+
* @param path - Path to validate
53+
* @returns Normalized path
54+
* @throws Error if path is invalid
55+
*/
56+
validatePath(path: string): string;
57+
}

0 commit comments

Comments
 (0)