Skip to content

Commit 0fa85f6

Browse files
chore(deepagents): update deps and re-establish old interfaces (#133)
* chore(deepagents): update deps * add changeset * re-estebalish old interfaces * format * skip new integration test for now
1 parent de2b4f1 commit 0fa85f6

File tree

15 files changed

+1138
-59
lines changed

15 files changed

+1138
-59
lines changed

.changeset/some-otters-own.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"deepagents": patch
3+
---
4+
5+
chore(deepagents): update deps

libs/deepagents/package.json

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,31 +38,31 @@
3838
},
3939
"homepage": "https://github.com/langchain-ai/deepagentsjs#readme",
4040
"dependencies": {
41-
"@langchain/anthropic": "^1.3.7",
42-
"@langchain/core": "^1.1.12",
43-
"@langchain/langgraph": "^1.0.14",
41+
"@langchain/anthropic": "^1.3.11",
42+
"@langchain/core": "^1.1.16",
43+
"@langchain/langgraph": "^1.1.1",
4444
"fast-glob": "^3.3.3",
45-
"langchain": "^1.2.7",
45+
"langchain": "^1.2.12",
4646
"micromatch": "^4.0.8",
4747
"yaml": "^2.8.2",
4848
"zod": "^4.3.5"
4949
},
5050
"devDependencies": {
5151
"@langchain/langgraph-checkpoint": "^1.0.0",
52-
"@langchain/openai": "^1.2.1",
52+
"@langchain/openai": "^1.2.3",
5353
"@langchain/tavily": "^1.2.0",
5454
"@tsconfig/recommended": "^1.0.13",
5555
"@types/micromatch": "^4.0.10",
56-
"@types/node": "^25.0.3",
56+
"@types/node": "^25.0.9",
5757
"@types/uuid": "^11.0.0",
58-
"@vitest/coverage-v8": "^4.0.16",
59-
"@vitest/ui": "^4.0.16",
58+
"@vitest/coverage-v8": "^4.0.17",
59+
"@vitest/ui": "^4.0.17",
6060
"dotenv": "^17.2.3",
6161
"tsdown": "^0.19.0",
6262
"tsx": "^4.21.0",
6363
"typescript": "^5.9.3",
6464
"uuid": "^13.0.0",
65-
"vitest": "^4.0.16"
65+
"vitest": "^4.0.17"
6666
},
6767
"exports": {
6868
".": {

libs/deepagents/src/backends/composite.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
BackendProtocol,
77
EditResult,
88
ExecuteResponse,
9+
FileData,
910
FileDownloadResponse,
1011
FileInfo,
1112
FileUploadResponse,
@@ -132,6 +133,17 @@ export class CompositeBackend implements BackendProtocol {
132133
return await backend.read(strippedKey, offset, limit);
133134
}
134135

136+
/**
137+
* Read file content as raw FileData.
138+
*
139+
* @param filePath - Absolute file path
140+
* @returns Raw file content as FileData
141+
*/
142+
async readRaw(filePath: string): Promise<FileData> {
143+
const [backend, strippedKey] = this.getBackendAndKey(filePath);
144+
return await backend.readRaw(strippedKey);
145+
}
146+
135147
/**
136148
* Structured search results or error string for invalid input.
137149
*/
@@ -304,6 +316,10 @@ export class CompositeBackend implements BackendProtocol {
304316
}
305317

306318
for (const [backend, batch] of batchesByBackend) {
319+
if (!backend.uploadFiles) {
320+
throw new Error("Backend does not support uploadFiles");
321+
}
322+
307323
const batchFiles = batch.map(
308324
(b) => [b.path, b.content] as [string, Uint8Array],
309325
);
@@ -347,6 +363,10 @@ export class CompositeBackend implements BackendProtocol {
347363
}
348364

349365
for (const [backend, batch] of batchesByBackend) {
366+
if (!backend.downloadFiles) {
367+
throw new Error("Backend does not support downloadFiles");
368+
}
369+
350370
const batchPaths = batch.map((b) => b.path);
351371
const batchResponses = await backend.downloadFiles(batchPaths);
352372

libs/deepagents/src/backends/filesystem.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import micromatch from "micromatch";
1818
import type {
1919
BackendProtocol,
2020
EditResult,
21+
FileData,
2122
FileDownloadResponse,
2223
FileInfo,
2324
FileUploadResponse,
@@ -244,6 +245,46 @@ export class FilesystemBackend implements BackendProtocol {
244245
}
245246
}
246247

248+
/**
249+
* Read file content as raw FileData.
250+
*
251+
* @param filePath - Absolute file path
252+
* @returns Raw file content as FileData
253+
*/
254+
async readRaw(filePath: string): Promise<FileData> {
255+
const resolvedPath = this.resolvePath(filePath);
256+
257+
let content: string;
258+
let stat: fsSync.Stats;
259+
260+
if (SUPPORTS_NOFOLLOW) {
261+
stat = await fs.stat(resolvedPath);
262+
if (!stat.isFile()) throw new Error(`File '${filePath}' not found`);
263+
const fd = await fs.open(
264+
resolvedPath,
265+
fsSync.constants.O_RDONLY | fsSync.constants.O_NOFOLLOW,
266+
);
267+
try {
268+
content = await fd.readFile({ encoding: "utf-8" });
269+
} finally {
270+
await fd.close();
271+
}
272+
} else {
273+
stat = await fs.lstat(resolvedPath);
274+
if (stat.isSymbolicLink()) {
275+
throw new Error(`Symlinks are not allowed: ${filePath}`);
276+
}
277+
if (!stat.isFile()) throw new Error(`File '${filePath}' not found`);
278+
content = await fs.readFile(resolvedPath, "utf-8");
279+
}
280+
281+
return {
282+
content: content.split("\n"),
283+
created_at: stat.ctime.toISOString(),
284+
modified_at: stat.mtime.toISOString(),
285+
};
286+
}
287+
247288
/**
248289
* Create a new file with content.
249290
* Returns WriteResult. External storage sets filesUpdate=null.

libs/deepagents/src/backends/protocol.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,14 @@ export interface BackendProtocol {
174174
*/
175175
read(filePath: string, offset?: number, limit?: number): MaybePromise<string>;
176176

177+
/**
178+
* Read file content as raw FileData.
179+
*
180+
* @param filePath - Absolute file path
181+
* @returns Raw file content as FileData
182+
*/
183+
readRaw(filePath: string): MaybePromise<FileData>;
184+
177185
/**
178186
* Structured search results or error string for invalid input.
179187
*
@@ -226,21 +234,23 @@ export interface BackendProtocol {
226234

227235
/**
228236
* Upload multiple files.
237+
* Optional - backends that don't support file upload can omit this.
229238
*
230239
* @param files - List of [path, content] tuples to upload
231240
* @returns List of FileUploadResponse objects, one per input file
232241
*/
233-
uploadFiles(
242+
uploadFiles?(
234243
files: Array<[string, Uint8Array]>,
235244
): MaybePromise<FileUploadResponse[]>;
236245

237246
/**
238247
* Download multiple files.
248+
* Optional - backends that don't support file download can omit this.
239249
*
240250
* @param paths - List of file paths to download
241251
* @returns List of FileDownloadResponse objects, one per input path
242252
*/
243-
downloadFiles(paths: string[]): MaybePromise<FileDownloadResponse[]>;
253+
downloadFiles?(paths: string[]): MaybePromise<FileDownloadResponse[]>;
244254
}
245255

246256
/**

libs/deepagents/src/backends/sandbox.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import type {
1212
EditResult,
1313
ExecuteResponse,
14+
FileData,
1415
FileDownloadResponse,
1516
FileInfo,
1617
FileUploadResponse,
@@ -394,6 +395,38 @@ export abstract class BaseSandbox implements SandboxBackendProtocol {
394395
return result.output;
395396
}
396397

398+
/**
399+
* Read file content as raw FileData.
400+
*
401+
* @param filePath - Absolute file path
402+
* @returns Raw file content as FileData
403+
*/
404+
async readRaw(filePath: string): Promise<FileData> {
405+
const command = buildReadCommand(filePath, 0, Number.MAX_SAFE_INTEGER);
406+
const result = await this.execute(command);
407+
408+
if (result.exitCode !== 0) {
409+
throw new Error(`File '${filePath}' not found`);
410+
}
411+
412+
// Parse the line-numbered output back to content
413+
const lines: string[] = [];
414+
for (const line of result.output.split("\n")) {
415+
// Format is " 123\tContent"
416+
const tabIndex = line.indexOf("\t");
417+
if (tabIndex !== -1) {
418+
lines.push(line.substring(tabIndex + 1));
419+
}
420+
}
421+
422+
const now = new Date().toISOString();
423+
return {
424+
content: lines,
425+
created_at: now,
426+
modified_at: now,
427+
};
428+
}
429+
397430
/**
398431
* Structured search results or error string for invalid input.
399432
*/

libs/deepagents/src/backends/state.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,20 @@ export class StateBackend implements BackendProtocol {
126126
return formatReadResponse(fileData, offset, limit);
127127
}
128128

129+
/**
130+
* Read file content as raw FileData.
131+
*
132+
* @param filePath - Absolute file path
133+
* @returns Raw file content as FileData
134+
*/
135+
readRaw(filePath: string): FileData {
136+
const files = this.getFiles();
137+
const fileData = files[filePath];
138+
139+
if (!fileData) throw new Error(`File '${filePath}' not found`);
140+
return fileData;
141+
}
142+
129143
/**
130144
* Create a new file with content.
131145
* Returns WriteResult with filesUpdate to update LangGraph state.

libs/deepagents/src/backends/store.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -241,21 +241,28 @@ export class StoreBackend implements BackendProtocol {
241241
limit: number = 500,
242242
): Promise<string> {
243243
try {
244-
const store = this.getStore();
245-
const namespace = this.getNamespace();
246-
const item = await store.get(namespace, filePath);
247-
248-
if (!item) {
249-
return `Error: File '${filePath}' not found`;
250-
}
251-
252-
const fileData = this.convertStoreItemToFileData(item);
244+
const fileData = await this.readRaw(filePath);
253245
return formatReadResponse(fileData, offset, limit);
254246
} catch (e: any) {
255247
return `Error: ${e.message}`;
256248
}
257249
}
258250

251+
/**
252+
* Read file content as raw FileData.
253+
*
254+
* @param filePath - Absolute file path
255+
* @returns Raw file content as FileData
256+
*/
257+
async readRaw(filePath: string): Promise<FileData> {
258+
const store = this.getStore();
259+
const namespace = this.getNamespace();
260+
const item = await store.get(namespace, filePath);
261+
262+
if (!item) throw new Error(`File '${filePath}' not found`);
263+
return this.convertStoreItemToFileData(item);
264+
}
265+
259266
/**
260267
* Create a new file with content.
261268
* Returns WriteResult. External storage sets filesUpdate=null.

libs/deepagents/src/middleware/hitl.int.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,4 +254,78 @@ describe("Human-in-the-Loop (HITL) Integration Tests", () => {
254254
expect(result.messages.length).toBeGreaterThan(0);
255255
},
256256
);
257+
258+
it.concurrent(
259+
"should properly propagate HITL interrupts from subagents without TypeError",
260+
{ timeout: 120000 },
261+
async () => {
262+
// This test specifically verifies the fix for the issue where
263+
// GraphInterrupt.interrupts was undefined when propagating from subagents,
264+
// causing "Cannot read properties of undefined (reading 'length')" error
265+
266+
const checkpointer = new MemorySaver();
267+
const agent = createDeepAgent({
268+
tools: [sampleTool],
269+
interruptOn: { sample_tool: true },
270+
checkpointer,
271+
});
272+
273+
const config = { configurable: { thread_id: uuidv4() } };
274+
275+
// Invoke with a task that will use the subagent which has HITL
276+
// The subagent should interrupt, and this interrupt should propagate
277+
// properly to the parent graph without causing a TypeError
278+
const result = await agent.invoke(
279+
{
280+
messages: [
281+
new HumanMessage(
282+
"Use the task tool with the general-purpose subagent to call the sample_tool",
283+
),
284+
],
285+
},
286+
config,
287+
);
288+
289+
// Verify the agent called the task tool
290+
const aiMessages = result.messages.filter((msg: any) =>
291+
AIMessage.isInstance(msg),
292+
);
293+
const toolCalls = aiMessages.flatMap((msg: any) => msg.tool_calls || []);
294+
expect(toolCalls.some((tc: any) => tc.name === "task")).toBe(true);
295+
296+
// Verify interrupt was properly propagated from the subagent
297+
expect(result.__interrupt__).toBeDefined();
298+
expect(result.__interrupt__).toHaveLength(1);
299+
300+
// Verify the interrupt has the correct HITL structure
301+
const interrupt = result.__interrupt__?.[0];
302+
expect(interrupt).toBeDefined();
303+
expect(interrupt!.value).toBeDefined();
304+
305+
const hitlRequest = interrupt!.value as HITLRequest;
306+
expect(hitlRequest.actionRequests).toBeDefined();
307+
expect(hitlRequest.actionRequests.length).toBeGreaterThan(0);
308+
expect(hitlRequest.reviewConfigs).toBeDefined();
309+
expect(hitlRequest.reviewConfigs.length).toBeGreaterThan(0);
310+
311+
// Verify we can resume successfully
312+
const resumeResult = await agent.invoke(
313+
new Command({
314+
resume: {
315+
decisions: [{ type: "approve" }],
316+
},
317+
}),
318+
config,
319+
);
320+
321+
// After resume, there should be no more interrupts
322+
expect(resumeResult.__interrupt__).toBeUndefined();
323+
324+
// The tool should have been executed
325+
const toolMessages = resumeResult.messages.filter(
326+
(msg: any) => msg._getType() === "tool",
327+
);
328+
expect(toolMessages.length).toBeGreaterThan(0);
329+
},
330+
);
257331
});

libs/deepagents/src/middleware/memory.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,15 @@ async function loadMemoryFromBackend(
194194
backend: BackendProtocol,
195195
path: string,
196196
): Promise<string | null> {
197+
// Use downloadFiles if available, otherwise fall back to read
198+
if (!backend.downloadFiles) {
199+
const content = await backend.read(path);
200+
if (content.startsWith("Error:")) {
201+
return null;
202+
}
203+
return content;
204+
}
205+
197206
const results = await backend.downloadFiles([path]);
198207

199208
// Should get exactly one response for one path

0 commit comments

Comments
 (0)