Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changeset/tidy-buckets-delete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"deepagents": minor
---

feat(backends): add delete protocol support

Adds a `DeleteResult` type and optional backend `delete` method, preserves delete through backend protocol adaptation, and implements StateBackend deletion through Pregel file-state updates.
1 change: 1 addition & 0 deletions libs/deepagents/src/backends/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type {
GlobResult,
WriteResult,
EditResult,
DeleteResult,
StateAndStore,
// Sandbox execution types
ExecuteResponse,
Expand Down
10 changes: 10 additions & 0 deletions libs/deepagents/src/backends/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,16 @@ export interface EditResult {
metadata?: Record<string, unknown>;
}

/**
* Result from backend delete operations.
*/
export interface DeleteResult {
/** Error message on failure, undefined on success */
error?: string;
/** File path of deleted file, undefined on failure */
path?: string;
}

/**
* Result of code execution.
* Simplified schema optimized for LLM consumption.
Expand Down
77 changes: 69 additions & 8 deletions libs/deepagents/src/backends/state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,33 @@ vi.mock("@langchain/langgraph", async (importOriginal) => {

function makeConfig(files: Record<string, FileData> = {}) {
const state = { messages: [], files: { ...files } };
const pendingFilesSends: Record<string, FileData>[] = [];
const pendingFilesSends: Record<string, FileData | null>[] = [];

const sendSpy = vi
.fn()
.mockImplementation((sends: [string, Record<string, FileData>][]) => {
for (const [channel, update] of sends) {
if (channel === "files") pendingFilesSends.push(update);
}
});
.mockImplementation(
(sends: [string, Record<string, FileData | null>][]) => {
for (const [channel, update] of sends) {
if (channel === "files") pendingFilesSends.push(update);
}
},
);

const readSpy = vi
.fn()
.mockImplementation((channel: string, fresh?: boolean) => {
if (channel !== "files") return undefined;
if (!fresh || pendingFilesSends.length === 0) return state.files;
const merged = { ...state.files };
for (const update of pendingFilesSends) Object.assign(merged, update);
for (const update of pendingFilesSends) {
for (const [path, fileData] of Object.entries(update)) {
if (fileData === null) {
delete merged[path];
} else {
merged[path] = fileData;
}
}
}
return merged;
});

Expand All @@ -40,7 +50,15 @@ function makeConfig(files: Record<string, FileData> = {}) {

// Simulate Pregel committing task.writes to state and clearing the buffer.
function commitSends() {
for (const update of pendingFilesSends) Object.assign(state.files, update);
for (const update of pendingFilesSends) {
for (const [path, fileData] of Object.entries(update)) {
if (fileData === null) {
delete state.files[path];
} else {
state.files[path] = fileData;
}
}
}
pendingFilesSends.length = 0;
}

Expand Down Expand Up @@ -109,6 +127,49 @@ describe("StateBackend", () => {
expect(infos.files!.some((i) => i.path === "/notes.txt")).toBe(true);
});

it("should delete files through Pregel send in zero-arg mode", () => {
const { sendSpy, commitSends } = makeConfig();
const backend = new StateBackend();

const writeRes = backend.write("/drop.txt", "bye");
expect(writeRes.error).toBeUndefined();
commitSends();
expect(backend.read("/drop.txt").error).toBeUndefined();

const deleteRes = backend.delete("/drop.txt");
expect(deleteRes.error).toBeUndefined();
expect(deleteRes.path).toBe("/drop.txt");
expect(sendSpy).toHaveBeenLastCalledWith([
["files", { "/drop.txt": null }],
]);

commitSends();
expect(backend.read("/drop.txt").error).toContain("not found");
});

it("should return an error when deleting a missing file", () => {
makeConfig();
const backend = new StateBackend();

const result = backend.delete("/missing.txt");

expect(result.path).toBeUndefined();
expect(result.error).toContain("not found");
});

it("should return an explicit error for delete in legacy mode", () => {
const { state, runtime } = makeConfig();
const backend = new StateBackend(runtime);
const writeRes = backend.write("/drop.txt", "bye");
Object.assign(state.files, writeRes.filesUpdate);

const result = backend.delete("/drop.txt");

expect(result.path).toBeUndefined();
expect(result.error).toContain("zero-argument StateBackend");
expect(backend.read("/drop.txt").error).toBeUndefined();
});

it("should handle errors correctly", () => {
const { state, runtime } = makeConfig();
const backend = new StateBackend(runtime);
Expand Down
27 changes: 25 additions & 2 deletions libs/deepagents/src/backends/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/

import type {
DeleteResult,
EditResult,
FileData,
FileDownloadResponse,
Expand Down Expand Up @@ -118,9 +119,10 @@ export class StateBackend implements BackendProtocolV2 {
* In legacy mode, this is a no-op — the caller uses `filesUpdate`
* from the return value instead.
*
* @param update - Map of file paths to their updated {@link FileData}
* @param update - Map of file paths to their updated {@link FileData},
* or null deletion markers.
*/
private sendFilesUpdate(update: Record<string, FileData>): void {
private sendFilesUpdate(update: Record<string, FileData | null>): void {
if (this.isLegacy) {
return;
}
Expand Down Expand Up @@ -325,6 +327,27 @@ export class StateBackend implements BackendProtocolV2 {
};
}

/**
* Delete a file from state by sending a null deletion marker through Pregel.
*/
delete(filePath: string): DeleteResult {
const files = this.files;

if (!(filePath in files)) {
return { error: `Error: File '${filePath}' not found` };
}

if (this.isLegacy) {
return {
error:
"StateBackend.delete requires a zero-argument StateBackend in a LangGraph execution context.",
};
}

this.sendFilesUpdate({ [filePath]: null });
return { path: filePath };
}

/**
* Search file contents for a literal text pattern.
* Binary files are skipped.
Expand Down
1 change: 1 addition & 0 deletions libs/deepagents/src/backends/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,7 @@ export function adaptBackendProtocol(
write: (filePath, content) => backend.write(filePath, content),
edit: (filePath, oldString, newString, replaceAll) =>
backend.edit(filePath, oldString, newString, replaceAll),
delete: backend.delete?.bind(backend),
uploadFiles: backend.uploadFiles
? (files) => backend.uploadFiles!(files)
: undefined,
Expand Down
9 changes: 9 additions & 0 deletions libs/deepagents/src/backends/v1/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import type {
DeleteResult,
EditResult,
ExecuteResponse,
FileData,
Expand Down Expand Up @@ -111,6 +112,14 @@ export interface BackendProtocolV1 {
replaceAll?: boolean,
): MaybePromise<EditResult>;

/**
* Delete a single file.
*
* @param filePath - Absolute path to the file to delete
* @returns DeleteResult with path on success or error on failure
*/
delete?(filePath: string): MaybePromise<DeleteResult>;

/**
* Upload multiple files.
* Optional - backends that don't support file upload can omit this.
Expand Down
10 changes: 10 additions & 0 deletions libs/deepagents/src/backends/v2/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import type { BackendProtocolV1 } from "../v1/protocol.js";
import type {
DeleteResult,
ExecuteResponse,
GlobResult,
GrepResult,
Expand Down Expand Up @@ -93,6 +94,15 @@ export interface BackendProtocolV2 extends Omit<
* @returns GlobResult with list of FileInfo objects matching the pattern on success or error on failure
*/
glob(pattern: string, path?: string): MaybePromise<GlobResult>;

/**
* Delete a single file.
* Optional - backends that don't support file deletion can omit this.
*
* @param filePath - Absolute path to the file to delete
* @returns DeleteResult with path on success or error on failure
*/
delete?(filePath: string): MaybePromise<DeleteResult>;
}

/**
Expand Down
1 change: 1 addition & 0 deletions libs/deepagents/src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export type {
GlobResult,
WriteResult,
EditResult,
DeleteResult,
StateAndStore,
// Sandbox execution types
ExecuteResponse,
Expand Down
1 change: 1 addition & 0 deletions libs/deepagents/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export {
type ReadRawResult,
type WriteResult,
type EditResult,
type DeleteResult,
Comment thread
open-swe[bot] marked this conversation as resolved.
// Sandbox execution types
type ExecuteResponse,
type FileData,
Expand Down
Loading