Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions libs/langchain/src/agents/middleware/anthropicTools/CommandHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { FileSystem } from "./FileSystem.js";

/**
* Executes text editor and memory tool commands against a FileSystem.
*
* This class provides the command execution layer for Anthropic's text_editor and
* memory tools, translating high-level commands (view, create, str_replace, etc.)
* into FileSystem operations. It serves as an abstraction between the tool schemas
* and the underlying storage implementation.
*
* The CommandHandler is used by both state-based (LangGraph) and filesystem-based
* middleware implementations, enabling consistent command handling across different
* storage backends.
*
* ## Supported Commands
*
* ### Text Editor and Memory Tools
* - **view**: Display file contents with line numbers or list directory contents
* - **create**: Create a new file or overwrite an existing file
* - **str_replace**: Replace a string occurrence within a file
* - **insert**: Insert text at a specific line number
*
* ### Memory Tool Only
* - **delete**: Delete a file or directory
* - **rename**: Rename or move a file/directory to a new path
*
* @example
* ```ts
* const fileSystem = new StateFileSystem(files, allowedPrefixes, onUpdate);
* const handler = new CommandHandler(fileSystem);
*
* // View file contents
* const contents = await handler.handleViewCommand("/path/to/file.txt");
*
* // Replace string in file
* await handler.handleStrReplaceCommand(
* "/path/to/file.txt",
* "old text",
* "new text"
* );
* ```
*
* @see {@link FileSystem} for the underlying storage interface
* @see {@link TextEditorCommandSchema} for text editor command schemas
* @see {@link MemoryCommandSchema} for memory command schemas
*/
export class CommandHandler {
/**
* Creates a new CommandHandler instance.
* @param fileSystem - The FileSystem implementation to execute commands against
*/
constructor(private fileSystem: FileSystem) {}

/**
* Handle view command - shows file contents or directory listing.
*/
async handleViewCommand(path: string): Promise<string> {
const normalizedPath = this.fileSystem.validatePath(path);
const fileData = await this.fileSystem.readFile(normalizedPath);

if (!fileData) {
// Try listing as directory
const matching = await this.fileSystem.listDirectory(normalizedPath);
if (matching.length > 0) {
return matching.join("\n");
}
throw new Error(`File not found: ${path}`);
}

const lines = fileData.content.split("\n");
const formattedLines = lines.map((line, i) => `${i + 1}|${line}`);
return formattedLines.join("\n");
}

/**
* Handle create command - creates or overwrites a file.
*/
async handleCreateCommand(path: string, fileText: string): Promise<string> {
const normalizedPath = this.fileSystem.validatePath(path);
const existing = await this.fileSystem.readFile(normalizedPath);
const now = new Date().toISOString();

await this.fileSystem.writeFile(normalizedPath, {
content: fileText,
created_at: existing ? existing.created_at : now,
modified_at: now,
});

return `File created: ${path}`;
}

/**
* Handle str_replace command - replaces a string in a file.
*/
async handleStrReplaceCommand(
path: string,
oldStr: string,
newStr: string
): Promise<string> {
const normalizedPath = this.fileSystem.validatePath(path);
const fileData = await this.fileSystem.readFile(normalizedPath);
if (!fileData) throw new Error(`File not found: ${path}`);

if (!fileData.content.includes(oldStr)) {
throw new Error(`String not found in file: ${oldStr}`);
}

const newContent = fileData.content.replace(oldStr, newStr);
await this.fileSystem.writeFile(normalizedPath, {
content: newContent,
created_at: fileData.created_at,
modified_at: new Date().toISOString(),
});

return `String replaced in file: ${path}`;
}

/**
* Handle insert command - inserts text at a specific line.
*/
async handleInsertCommand(
path: string,
insertLine: number,
textToInsert: string
): Promise<string> {
const normalizedPath = this.fileSystem.validatePath(path);
const fileData = await this.fileSystem.readFile(normalizedPath);
if (!fileData) throw new Error(`File not found: ${path}`);

const lines = fileData.content.split("\n");
const newLines = textToInsert.split("\n");
const updatedLines = [
...lines.slice(0, insertLine),
...newLines,
...lines.slice(insertLine),
];

await this.fileSystem.writeFile(normalizedPath, {
content: updatedLines.join("\n"),
created_at: fileData.created_at,
modified_at: new Date().toISOString(),
});

return `Text inserted in file: ${path}`;
}

/**
* Handle delete command - deletes a file or directory.
*/
async handleDeleteCommand(path: string): Promise<string> {
const normalizedPath = this.fileSystem.validatePath(path);
await this.fileSystem.deleteFile(normalizedPath);
return `File deleted: ${path}`;
}

/**
* Handle rename command - renames/moves a file or directory.
*/
async handleRenameCommand(oldPath: string, newPath: string): Promise<string> {
const normalizedOld = this.fileSystem.validatePath(oldPath);
const normalizedNew = this.fileSystem.validatePath(newPath);
const fileData = await this.fileSystem.readFile(normalizedOld);
if (!fileData) throw new Error(`File not found: ${oldPath}`);

await this.fileSystem.renameFile(normalizedOld, normalizedNew, fileData);
return `File renamed: ${oldPath} -> ${newPath}`;
}
}
12 changes: 12 additions & 0 deletions libs/langchain/src/agents/middleware/anthropicTools/FileData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import z from "zod";

/**
* Zod schema for file data.
*/
export const FileDataSchema = z.object({
content: z.string(),
created_at: z.string(),
modified_at: z.string(),
});

export type FileData = z.infer<typeof FileDataSchema>;
57 changes: 57 additions & 0 deletions libs/langchain/src/agents/middleware/anthropicTools/FileSystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { FileData } from "./FileData.js";

/**
* Abstract interface for file system operations.
* Supports both state-based (LangGraph) and physical filesystem implementations.
*/
export interface FileSystem {
/**
* Read a file's contents and metadata.
* @param path - Normalized path to the file
* @returns FileData if file exists, null if not found or is a directory
*/
readFile(path: string): Promise<FileData | null>;

/**
* List files in a directory.
* @param path - Normalized path to the directory
* @returns Array of file paths in the directory
*/
listDirectory(path: string): Promise<string[]>;

/**
* Write a file with content and metadata.
* @param path - Normalized path to write
* @param data - File data to write
* @returns Result with message and optional state updates
*/
writeFile(path: string, data: FileData): Promise<void>;

/**
* Delete a file or directory.
* @param path - Normalized path to delete
* @returns Result with message and optional state updates
*/
deleteFile(path: string): Promise<void>;

/**
* Rename/move a file or directory.
* @param oldPath - Normalized source path
* @param newPath - Normalized destination path
* @param existingData - File data to move
* @returns Result with message and optional state updates
*/
renameFile(
oldPath: string,
newPath: string,
existingData: FileData
): Promise<void>;

/**
* Validate and normalize a file path.
* @param path - Path to validate
* @returns Normalized path
* @throws Error if path is invalid
*/
validatePath(path: string): string;
}
Loading
Loading