deepagents 1.8.5 → 1.9.0-alpha.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.cjs +1372 -229
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1649 -1171
- package/dist/index.d.ts +1646 -1166
- package/dist/index.js +1405 -269
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.cjs
CHANGED
|
@@ -28,10 +28,12 @@ let zod_v4 = require("zod/v4");
|
|
|
28
28
|
let micromatch = require("micromatch");
|
|
29
29
|
micromatch = __toESM(micromatch);
|
|
30
30
|
let path = require("path");
|
|
31
|
+
path = __toESM(path);
|
|
31
32
|
let _langchain_core_messages = require("@langchain/core/messages");
|
|
32
33
|
let zod = require("zod");
|
|
33
34
|
let yaml = require("yaml");
|
|
34
35
|
yaml = __toESM(yaml);
|
|
36
|
+
let _langchain_langgraph_sdk = require("@langchain/langgraph-sdk");
|
|
35
37
|
let _langchain_core_errors = require("@langchain/core/errors");
|
|
36
38
|
let langchain_chat_models_universal = require("langchain/chat_models/universal");
|
|
37
39
|
let node_fs_promises = require("node:fs/promises");
|
|
@@ -52,10 +54,22 @@ node_os = __toESM(node_os);
|
|
|
52
54
|
* Type guard to check if a backend supports execution.
|
|
53
55
|
*
|
|
54
56
|
* @param backend - Backend instance to check
|
|
55
|
-
* @returns True if the backend implements
|
|
57
|
+
* @returns True if the backend implements SandboxBackendProtocolV2
|
|
56
58
|
*/
|
|
57
59
|
function isSandboxBackend(backend) {
|
|
58
|
-
return typeof backend.execute === "function" && typeof backend.id === "string" && backend.id !== "";
|
|
60
|
+
return backend != null && typeof backend === "object" && typeof backend.execute === "function" && typeof backend.id === "string" && backend.id !== "";
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Type guard to check if a backend is a sandbox protocol (v1 or v2).
|
|
64
|
+
*
|
|
65
|
+
* Checks for the presence of `execute` function and `id` string,
|
|
66
|
+
* which are the defining features of sandbox protocols.
|
|
67
|
+
*
|
|
68
|
+
* @param backend - Backend instance to check
|
|
69
|
+
* @returns True if the backend implements sandbox protocol (v1 or v2)
|
|
70
|
+
*/
|
|
71
|
+
function isSandboxProtocol(backend) {
|
|
72
|
+
return backend != null && typeof backend === "object" && typeof backend.execute === "function" && typeof backend.id === "string" && backend.id !== "";
|
|
59
73
|
}
|
|
60
74
|
const SANDBOX_ERROR_SYMBOL = Symbol.for("sandbox.error");
|
|
61
75
|
/**
|
|
@@ -119,6 +133,19 @@ const EMPTY_CONTENT_WARNING = "System reminder: File exists but has empty conten
|
|
|
119
133
|
const MAX_LINE_LENGTH = 1e4;
|
|
120
134
|
const TOOL_RESULT_TOKEN_LIMIT = 2e4;
|
|
121
135
|
const TRUNCATION_GUIDANCE = "... [results truncated, try being more specific with your parameters]";
|
|
136
|
+
const MIME_TYPES = {
|
|
137
|
+
".png": "image/png",
|
|
138
|
+
".jpg": "image/jpeg",
|
|
139
|
+
".jpeg": "image/jpeg",
|
|
140
|
+
".gif": "image/gif",
|
|
141
|
+
".webp": "image/webp",
|
|
142
|
+
".svg": "image/svg+xml",
|
|
143
|
+
".mp3": "audio/mpeg",
|
|
144
|
+
".wav": "audio/wav",
|
|
145
|
+
".mp4": "video/mp4",
|
|
146
|
+
".webm": "video/webm",
|
|
147
|
+
".pdf": "application/pdf"
|
|
148
|
+
};
|
|
122
149
|
/**
|
|
123
150
|
* Sanitize tool_call_id to prevent path traversal and separator issues.
|
|
124
151
|
*
|
|
@@ -180,20 +207,50 @@ function checkEmptyContent(content) {
|
|
|
180
207
|
* @returns Content as string with lines joined by newlines
|
|
181
208
|
*/
|
|
182
209
|
function fileDataToString(fileData) {
|
|
183
|
-
return fileData.content.join("\n");
|
|
210
|
+
if (Array.isArray(fileData.content)) return fileData.content.join("\n");
|
|
211
|
+
if (typeof fileData.content === "string") return fileData.content;
|
|
212
|
+
throw new Error("Cannot convert binary FileData to string");
|
|
184
213
|
}
|
|
185
214
|
/**
|
|
186
|
-
*
|
|
215
|
+
* Type guard to check if FileData contains binary content (Uint8Array).
|
|
187
216
|
*
|
|
188
|
-
* @param
|
|
189
|
-
* @
|
|
190
|
-
* @returns FileData object with content and timestamps
|
|
217
|
+
* @param data - FileData to check
|
|
218
|
+
* @returns True if the content is a Uint8Array (binary)
|
|
191
219
|
*/
|
|
192
|
-
function
|
|
193
|
-
|
|
220
|
+
function isFileDataBinary(data) {
|
|
221
|
+
return ArrayBuffer.isView(data.content);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Create a FileData object.
|
|
225
|
+
*
|
|
226
|
+
* Defaults to v2 format (content as single string). Pass `fileFormat: "v1"` for
|
|
227
|
+
* backward compatibility with older readers during a rolling deployment.
|
|
228
|
+
* Binary content (Uint8Array) is only supported with v2.
|
|
229
|
+
*
|
|
230
|
+
* @param content - File content as a string or binary Uint8Array (v2 only)
|
|
231
|
+
* @param createdAt - Optional creation timestamp (ISO format), defaults to now
|
|
232
|
+
* @param fileFormat - Storage format: "v2" (default) or "v1" (legacy line array)
|
|
233
|
+
* @returns FileData in the requested format
|
|
234
|
+
*/
|
|
235
|
+
function createFileData(content, createdAt, fileFormat = "v2", mimeType) {
|
|
194
236
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
237
|
+
if (fileFormat === "v1" && ArrayBuffer.isView(content)) throw new Error("Binary data is not supported with v1 file formats. Please use v2 file format");
|
|
238
|
+
if (fileFormat === "v2") {
|
|
239
|
+
if (ArrayBuffer.isView(content)) return {
|
|
240
|
+
content: new Uint8Array(content.buffer, content.byteOffset, content.byteLength),
|
|
241
|
+
mimeType: mimeType ?? "application/octet-stream",
|
|
242
|
+
created_at: createdAt || now,
|
|
243
|
+
modified_at: now
|
|
244
|
+
};
|
|
245
|
+
return {
|
|
246
|
+
content,
|
|
247
|
+
mimeType: mimeType ?? "text/plain",
|
|
248
|
+
created_at: createdAt || now,
|
|
249
|
+
modified_at: now
|
|
250
|
+
};
|
|
251
|
+
}
|
|
195
252
|
return {
|
|
196
|
-
content:
|
|
253
|
+
content: typeof content === "string" ? content.split("\n") : content,
|
|
197
254
|
created_at: createdAt || now,
|
|
198
255
|
modified_at: now
|
|
199
256
|
};
|
|
@@ -206,33 +263,20 @@ function createFileData(content, createdAt) {
|
|
|
206
263
|
* @returns Updated FileData object
|
|
207
264
|
*/
|
|
208
265
|
function updateFileData(fileData, content) {
|
|
209
|
-
const lines = typeof content === "string" ? content.split("\n") : content;
|
|
210
266
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
267
|
+
if (isFileDataV1(fileData)) return {
|
|
268
|
+
content: typeof content === "string" ? content.split("\n") : content,
|
|
269
|
+
created_at: fileData.created_at,
|
|
270
|
+
modified_at: now
|
|
271
|
+
};
|
|
211
272
|
return {
|
|
212
|
-
content
|
|
273
|
+
content,
|
|
274
|
+
mimeType: fileData.mimeType,
|
|
213
275
|
created_at: fileData.created_at,
|
|
214
276
|
modified_at: now
|
|
215
277
|
};
|
|
216
278
|
}
|
|
217
279
|
/**
|
|
218
|
-
* Format file data for read response with line numbers.
|
|
219
|
-
*
|
|
220
|
-
* @param fileData - FileData object
|
|
221
|
-
* @param offset - Line offset (0-indexed)
|
|
222
|
-
* @param limit - Maximum number of lines
|
|
223
|
-
* @returns Formatted content or error message
|
|
224
|
-
*/
|
|
225
|
-
function formatReadResponse(fileData, offset, limit) {
|
|
226
|
-
const content = fileDataToString(fileData);
|
|
227
|
-
const emptyMsg = checkEmptyContent(content);
|
|
228
|
-
if (emptyMsg) return emptyMsg;
|
|
229
|
-
const lines = content.split("\n");
|
|
230
|
-
const startIdx = offset;
|
|
231
|
-
const endIdx = Math.min(startIdx + limit, lines.length);
|
|
232
|
-
if (startIdx >= lines.length) return `Error: Line offset ${offset} exceeds file length (${lines.length} lines)`;
|
|
233
|
-
return formatContentWithLineNumbers(lines.slice(startIdx, endIdx), startIdx + 1);
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
280
|
* Perform string replacement with occurrence validation.
|
|
237
281
|
*
|
|
238
282
|
* @param content - Original content
|
|
@@ -291,8 +335,8 @@ function truncateIfTooLong(result) {
|
|
|
291
335
|
* validatePath("C:\\Users\\file") // Throws: Windows absolute paths not supported
|
|
292
336
|
* ```
|
|
293
337
|
*/
|
|
294
|
-
function validatePath(path$
|
|
295
|
-
const pathStr = path$
|
|
338
|
+
function validatePath(path$6) {
|
|
339
|
+
const pathStr = path$6 || "/";
|
|
296
340
|
if (!pathStr || pathStr.trim() === "") throw new Error("Path cannot be empty");
|
|
297
341
|
let normalized = pathStr.startsWith("/") ? pathStr : "/" + pathStr;
|
|
298
342
|
if (!normalized.endsWith("/")) normalized += "/";
|
|
@@ -314,10 +358,10 @@ function validatePath(path$5) {
|
|
|
314
358
|
* // Returns: "/test.py\n/src/main.py" (sorted by modified_at)
|
|
315
359
|
* ```
|
|
316
360
|
*/
|
|
317
|
-
function globSearchFiles(files, pattern, path$
|
|
361
|
+
function globSearchFiles(files, pattern, path$8 = "/") {
|
|
318
362
|
let normalizedPath;
|
|
319
363
|
try {
|
|
320
|
-
normalizedPath = validatePath(path$
|
|
364
|
+
normalizedPath = validatePath(path$8);
|
|
321
365
|
} catch {
|
|
322
366
|
return "No files found";
|
|
323
367
|
}
|
|
@@ -343,16 +387,13 @@ function globSearchFiles(files, pattern, path$7 = "/") {
|
|
|
343
387
|
/**
|
|
344
388
|
* Return structured grep matches from an in-memory files mapping.
|
|
345
389
|
*
|
|
346
|
-
* Performs literal text search (not regex).
|
|
347
|
-
*
|
|
348
|
-
* Returns a list of GrepMatch on success, or a string for invalid inputs.
|
|
349
|
-
* We deliberately do not raise here to keep backends non-throwing in tool
|
|
350
|
-
* contexts and preserve user-facing error messages.
|
|
390
|
+
* Performs literal text search (not regex). Binary files are skipped.
|
|
391
|
+
* Returns an empty array when no matches are found or on invalid input.
|
|
351
392
|
*/
|
|
352
|
-
function grepMatchesFromFiles(files, pattern, path$
|
|
393
|
+
function grepMatchesFromFiles(files, pattern, path$10 = null, glob = null) {
|
|
353
394
|
let normalizedPath;
|
|
354
395
|
try {
|
|
355
|
-
normalizedPath = validatePath(path$
|
|
396
|
+
normalizedPath = validatePath(path$10);
|
|
356
397
|
} catch {
|
|
357
398
|
return [];
|
|
358
399
|
}
|
|
@@ -362,17 +403,140 @@ function grepMatchesFromFiles(files, pattern, path$9 = null, glob = null) {
|
|
|
362
403
|
nobrace: false
|
|
363
404
|
})));
|
|
364
405
|
const matches = [];
|
|
365
|
-
for (const [filePath, fileData] of Object.entries(filtered))
|
|
366
|
-
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
406
|
+
for (const [filePath, fileData] of Object.entries(filtered)) {
|
|
407
|
+
if (!isTextMimeType(migrateToFileDataV2(fileData, filePath).mimeType)) continue;
|
|
408
|
+
const lines = fileDataToString(fileData).split("\n");
|
|
409
|
+
for (let i = 0; i < lines.length; i++) {
|
|
410
|
+
const line = lines[i];
|
|
411
|
+
const lineNum = i + 1;
|
|
412
|
+
if (line.includes(pattern)) matches.push({
|
|
413
|
+
path: filePath,
|
|
414
|
+
line: lineNum,
|
|
415
|
+
text: line
|
|
416
|
+
});
|
|
417
|
+
}
|
|
373
418
|
}
|
|
374
419
|
return matches;
|
|
375
420
|
}
|
|
421
|
+
/**
|
|
422
|
+
* Determine MIME type from a file path's extension.
|
|
423
|
+
*
|
|
424
|
+
* Returns "text/plain" for unknown extensions.
|
|
425
|
+
*
|
|
426
|
+
* @param filePath - File path to inspect
|
|
427
|
+
* @returns MIME type string (e.g., "image/png", "text/plain")
|
|
428
|
+
*/
|
|
429
|
+
function getMimeType(filePath) {
|
|
430
|
+
return MIME_TYPES[path.default.extname(filePath).toLocaleLowerCase()] || "text/plain";
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Check whether a MIME type represents text content.
|
|
434
|
+
*
|
|
435
|
+
* @param mimeType - MIME type string to check
|
|
436
|
+
* @returns True if the MIME type is text-based
|
|
437
|
+
*/
|
|
438
|
+
function isTextMimeType(mimeType) {
|
|
439
|
+
return mimeType.startsWith("text/") || mimeType === "application/json" || mimeType === "application/javascript" || mimeType === "image/svg+xml";
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Type guard to check if FileData is v1 format (content as line array).
|
|
443
|
+
*
|
|
444
|
+
* @param data - FileData to check
|
|
445
|
+
* @returns True if data is FileDataV1
|
|
446
|
+
*/
|
|
447
|
+
function isFileDataV1(data) {
|
|
448
|
+
return Array.isArray(data.content);
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Convert FileData to v2 format, joining v1 line arrays into a single string.
|
|
452
|
+
*
|
|
453
|
+
* If the data is already v2, returns it unchanged.
|
|
454
|
+
*
|
|
455
|
+
* @param data - FileData in either format
|
|
456
|
+
* @returns FileDataV2 with content as string (text) or Uint8Array (binary)
|
|
457
|
+
*/
|
|
458
|
+
function migrateToFileDataV2(data, filePath) {
|
|
459
|
+
if (isFileDataV1(data)) return {
|
|
460
|
+
content: data.content.join("\n"),
|
|
461
|
+
mimeType: getMimeType(filePath),
|
|
462
|
+
created_at: data.created_at,
|
|
463
|
+
modified_at: data.modified_at
|
|
464
|
+
};
|
|
465
|
+
if (!("mimeType" in data) || !data.mimeType) return {
|
|
466
|
+
...data,
|
|
467
|
+
mimeType: getMimeType(filePath)
|
|
468
|
+
};
|
|
469
|
+
return data;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Adapt a v1 {@link BackendProtocol} to {@link BackendProtocolV2}.
|
|
473
|
+
*
|
|
474
|
+
* If the backend already implements v2, it is returned as-is.
|
|
475
|
+
* For v1 backends, wraps returns in Result types:
|
|
476
|
+
* - `read()` string returns wrapped in {@link ReadResult}
|
|
477
|
+
* - `readRaw()` FileData returns wrapped in {@link ReadRawResult}
|
|
478
|
+
* - `grep()` returns wrapped in {@link GrepResult}
|
|
479
|
+
* - `ls()` FileInfo[] returns wrapped in {@link LsResult}
|
|
480
|
+
* - `glob()` FileInfo[] returns wrapped in {@link GlobResult}
|
|
481
|
+
*
|
|
482
|
+
* Note: For sandbox instances, use {@link adaptSandboxProtocol} instead.
|
|
483
|
+
*
|
|
484
|
+
* @param backend - Backend instance (v1 or v2)
|
|
485
|
+
* @returns BackendProtocolV2-compatible backend
|
|
486
|
+
*/
|
|
487
|
+
function adaptBackendProtocol(backend) {
|
|
488
|
+
return {
|
|
489
|
+
async ls(path$11) {
|
|
490
|
+
const result = await ("ls" in backend ? backend.ls(path$11) : backend.lsInfo(path$11));
|
|
491
|
+
if (Array.isArray(result)) return { files: result };
|
|
492
|
+
return result;
|
|
493
|
+
},
|
|
494
|
+
async readRaw(filePath) {
|
|
495
|
+
const result = await backend.readRaw(filePath);
|
|
496
|
+
if ("data" in result || "error" in result) return result;
|
|
497
|
+
return { data: migrateToFileDataV2(result, filePath) };
|
|
498
|
+
},
|
|
499
|
+
async glob(pattern, path$12) {
|
|
500
|
+
const result = await ("glob" in backend ? backend.glob(pattern, path$12) : backend.globInfo(pattern, path$12));
|
|
501
|
+
if (Array.isArray(result)) return { files: result };
|
|
502
|
+
return result;
|
|
503
|
+
},
|
|
504
|
+
write: (filePath, content) => backend.write(filePath, content),
|
|
505
|
+
edit: (filePath, oldString, newString, replaceAll) => backend.edit(filePath, oldString, newString, replaceAll),
|
|
506
|
+
uploadFiles: backend.uploadFiles ? (files) => backend.uploadFiles(files) : void 0,
|
|
507
|
+
downloadFiles: backend.downloadFiles ? (paths) => backend.downloadFiles(paths) : void 0,
|
|
508
|
+
async read(filePath, offset, limit) {
|
|
509
|
+
const result = await backend.read(filePath, offset, limit);
|
|
510
|
+
if (typeof result === "string") return { content: result };
|
|
511
|
+
return result;
|
|
512
|
+
},
|
|
513
|
+
async grep(pattern, path$13, glob) {
|
|
514
|
+
const result = await ("grep" in backend ? backend.grep(pattern, path$13, glob) : backend.grepRaw(pattern, path$13, glob));
|
|
515
|
+
if (Array.isArray(result)) return { matches: result };
|
|
516
|
+
if (typeof result === "string") return { error: result };
|
|
517
|
+
return result;
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Adapt a sandbox backend from v1 to v2 interface.
|
|
523
|
+
*
|
|
524
|
+
* This extends {@link adaptBackendProtocol} to also preserve sandbox-specific
|
|
525
|
+
* properties from {@link SandboxBackendProtocol}: `execute` and `id`.
|
|
526
|
+
*
|
|
527
|
+
* @param sandbox - Sandbox backend (v1 or v2)
|
|
528
|
+
* @returns SandboxBackendProtocolV2-compatible sandbox
|
|
529
|
+
*/
|
|
530
|
+
function adaptSandboxProtocol(sandbox) {
|
|
531
|
+
const adapted = adaptBackendProtocol(sandbox);
|
|
532
|
+
adapted.execute = (cmd) => sandbox.execute(cmd);
|
|
533
|
+
Object.defineProperty(adapted, "id", {
|
|
534
|
+
value: sandbox.id,
|
|
535
|
+
enumerable: true,
|
|
536
|
+
configurable: true
|
|
537
|
+
});
|
|
538
|
+
return adapted;
|
|
539
|
+
}
|
|
376
540
|
//#endregion
|
|
377
541
|
//#region src/backends/state.ts
|
|
378
542
|
/**
|
|
@@ -388,8 +552,10 @@ function grepMatchesFromFiles(files, pattern, path$9 = null, glob = null) {
|
|
|
388
552
|
*/
|
|
389
553
|
var StateBackend = class {
|
|
390
554
|
stateAndStore;
|
|
391
|
-
|
|
555
|
+
fileFormat;
|
|
556
|
+
constructor(stateAndStore, options) {
|
|
392
557
|
this.stateAndStore = stateAndStore;
|
|
558
|
+
this.fileFormat = options?.fileFormat ?? "v2";
|
|
393
559
|
}
|
|
394
560
|
/**
|
|
395
561
|
* Get files from current state.
|
|
@@ -401,10 +567,10 @@ var StateBackend = class {
|
|
|
401
567
|
* List files and directories in the specified directory (non-recursive).
|
|
402
568
|
*
|
|
403
569
|
* @param path - Absolute path to directory
|
|
404
|
-
* @returns
|
|
570
|
+
* @returns LsResult with list of FileInfo objects on success or error on failure.
|
|
405
571
|
* Directories have a trailing / in their path and is_dir=true.
|
|
406
572
|
*/
|
|
407
|
-
|
|
573
|
+
ls(path) {
|
|
408
574
|
const files = this.getFiles();
|
|
409
575
|
const infos = [];
|
|
410
576
|
const subdirs = /* @__PURE__ */ new Set();
|
|
@@ -417,7 +583,7 @@ var StateBackend = class {
|
|
|
417
583
|
subdirs.add(normalizedPath + subdirName + "/");
|
|
418
584
|
continue;
|
|
419
585
|
}
|
|
420
|
-
const size = fd.content.join("\n").length;
|
|
586
|
+
const size = isFileDataV1(fd) ? fd.content.join("\n").length : isFileDataBinary(fd) ? fd.content.byteLength : fd.content.length;
|
|
421
587
|
infos.push({
|
|
422
588
|
path: k,
|
|
423
589
|
is_dir: false,
|
|
@@ -432,31 +598,43 @@ var StateBackend = class {
|
|
|
432
598
|
modified_at: ""
|
|
433
599
|
});
|
|
434
600
|
infos.sort((a, b) => a.path.localeCompare(b.path));
|
|
435
|
-
return infos;
|
|
601
|
+
return { files: infos };
|
|
436
602
|
}
|
|
437
603
|
/**
|
|
438
|
-
* Read file content
|
|
604
|
+
* Read file content.
|
|
605
|
+
*
|
|
606
|
+
* Text files are paginated by line offset/limit.
|
|
607
|
+
* Binary files return full Uint8Array content (offset/limit ignored).
|
|
439
608
|
*
|
|
440
609
|
* @param filePath - Absolute file path
|
|
441
610
|
* @param offset - Line offset to start reading from (0-indexed)
|
|
442
611
|
* @param limit - Maximum number of lines to read
|
|
443
|
-
* @returns
|
|
612
|
+
* @returns ReadResult with content on success or error on failure
|
|
444
613
|
*/
|
|
445
614
|
read(filePath, offset = 0, limit = 500) {
|
|
446
615
|
const fileData = this.getFiles()[filePath];
|
|
447
|
-
if (!fileData) return
|
|
448
|
-
|
|
616
|
+
if (!fileData) return { error: `File '${filePath}' not found` };
|
|
617
|
+
const fileDataV2 = migrateToFileDataV2(fileData, filePath);
|
|
618
|
+
if (!isTextMimeType(fileDataV2.mimeType)) return {
|
|
619
|
+
content: fileDataV2.content,
|
|
620
|
+
mimeType: fileDataV2.mimeType
|
|
621
|
+
};
|
|
622
|
+
if (typeof fileDataV2.content !== "string") return { error: `File '${filePath}' has binary content but text MIME type` };
|
|
623
|
+
return {
|
|
624
|
+
content: fileDataV2.content.split("\n").slice(offset, offset + limit).join("\n"),
|
|
625
|
+
mimeType: fileDataV2.mimeType
|
|
626
|
+
};
|
|
449
627
|
}
|
|
450
628
|
/**
|
|
451
629
|
* Read file content as raw FileData.
|
|
452
630
|
*
|
|
453
631
|
* @param filePath - Absolute file path
|
|
454
|
-
* @returns
|
|
632
|
+
* @returns ReadRawResult with raw file data on success or error on failure
|
|
455
633
|
*/
|
|
456
634
|
readRaw(filePath) {
|
|
457
635
|
const fileData = this.getFiles()[filePath];
|
|
458
|
-
if (!fileData)
|
|
459
|
-
return fileData;
|
|
636
|
+
if (!fileData) return { error: `File '${filePath}' not found` };
|
|
637
|
+
return { data: fileData };
|
|
460
638
|
}
|
|
461
639
|
/**
|
|
462
640
|
* Create a new file with content.
|
|
@@ -464,7 +642,8 @@ var StateBackend = class {
|
|
|
464
642
|
*/
|
|
465
643
|
write(filePath, content) {
|
|
466
644
|
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.` };
|
|
467
|
-
const
|
|
645
|
+
const mimeType = getMimeType(filePath);
|
|
646
|
+
const newFileData = createFileData(content, void 0, this.fileFormat, mimeType);
|
|
468
647
|
return {
|
|
469
648
|
path: filePath,
|
|
470
649
|
filesUpdate: { [filePath]: newFileData }
|
|
@@ -488,23 +667,24 @@ var StateBackend = class {
|
|
|
488
667
|
};
|
|
489
668
|
}
|
|
490
669
|
/**
|
|
491
|
-
*
|
|
670
|
+
* Search file contents for a literal text pattern.
|
|
671
|
+
* Binary files are skipped.
|
|
492
672
|
*/
|
|
493
|
-
|
|
494
|
-
return grepMatchesFromFiles(this.getFiles(), pattern, path, glob);
|
|
673
|
+
grep(pattern, path = "/", glob = null) {
|
|
674
|
+
return { matches: grepMatchesFromFiles(this.getFiles(), pattern, path, glob) };
|
|
495
675
|
}
|
|
496
676
|
/**
|
|
497
677
|
* Structured glob matching returning FileInfo objects.
|
|
498
678
|
*/
|
|
499
|
-
|
|
679
|
+
glob(pattern, path = "/") {
|
|
500
680
|
const files = this.getFiles();
|
|
501
681
|
const result = globSearchFiles(files, pattern, path);
|
|
502
|
-
if (result === "No files found") return [];
|
|
682
|
+
if (result === "No files found") return { files: [] };
|
|
503
683
|
const paths = result.split("\n");
|
|
504
684
|
const infos = [];
|
|
505
685
|
for (const p of paths) {
|
|
506
686
|
const fd = files[p];
|
|
507
|
-
const size = fd ? fd.content.join("\n").length : 0;
|
|
687
|
+
const size = fd ? isFileDataV1(fd) ? fd.content.join("\n").length : isFileDataBinary(fd) ? fd.content.byteLength : fd.content.length : 0;
|
|
508
688
|
infos.push({
|
|
509
689
|
path: p,
|
|
510
690
|
is_dir: false,
|
|
@@ -512,7 +692,7 @@ var StateBackend = class {
|
|
|
512
692
|
modified_at: fd?.modified_at || ""
|
|
513
693
|
});
|
|
514
694
|
}
|
|
515
|
-
return infos;
|
|
695
|
+
return { files: infos };
|
|
516
696
|
}
|
|
517
697
|
/**
|
|
518
698
|
* Upload multiple files.
|
|
@@ -527,7 +707,9 @@ var StateBackend = class {
|
|
|
527
707
|
const responses = [];
|
|
528
708
|
const updates = {};
|
|
529
709
|
for (const [path, content] of files) try {
|
|
530
|
-
|
|
710
|
+
const mimeType = getMimeType(path);
|
|
711
|
+
if (this.fileFormat === "v2" && !isTextMimeType(mimeType)) updates[path] = createFileData(content, void 0, "v2", mimeType);
|
|
712
|
+
else updates[path] = createFileData(new TextDecoder().decode(content), void 0, this.fileFormat, mimeType);
|
|
531
713
|
responses.push({
|
|
532
714
|
path,
|
|
533
715
|
error: null
|
|
@@ -561,11 +743,17 @@ var StateBackend = class {
|
|
|
561
743
|
});
|
|
562
744
|
continue;
|
|
563
745
|
}
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
746
|
+
const fileDataV2 = migrateToFileDataV2(fileData, path);
|
|
747
|
+
if (typeof fileDataV2.content === "string") {
|
|
748
|
+
const content = new TextEncoder().encode(fileDataV2.content);
|
|
749
|
+
responses.push({
|
|
750
|
+
path,
|
|
751
|
+
content,
|
|
752
|
+
error: null
|
|
753
|
+
});
|
|
754
|
+
} else responses.push({
|
|
567
755
|
path,
|
|
568
|
-
content,
|
|
756
|
+
content: fileDataV2.content,
|
|
569
757
|
error: null
|
|
570
758
|
});
|
|
571
759
|
}
|
|
@@ -627,6 +815,12 @@ const TOOLS_EXCLUDED_FROM_EVICTION = [
|
|
|
627
815
|
"write_file"
|
|
628
816
|
];
|
|
629
817
|
/**
|
|
818
|
+
* Maximum size for binary (non-text) files read via read_file, in bytes.
|
|
819
|
+
* Base64-encoded content is ~33% larger, so 10MB raw ≈ 13.3MB in context.
|
|
820
|
+
* This keeps inline multimodal payloads within all major provider limits.
|
|
821
|
+
*/
|
|
822
|
+
const MAX_BINARY_READ_SIZE_BYTES = 10 * 1024 * 1024;
|
|
823
|
+
/**
|
|
630
824
|
* Template for truncation message in read_file.
|
|
631
825
|
* {file_path} will be filled in at runtime.
|
|
632
826
|
*/
|
|
@@ -665,14 +859,27 @@ function createContentPreview(contentStr, headLines = 5, tailLines = 5) {
|
|
|
665
859
|
return headSample + truncationNotice + tailSample;
|
|
666
860
|
}
|
|
667
861
|
/**
|
|
668
|
-
* Zod
|
|
862
|
+
* Zod schema for legacy FileDataV1 (content as line array).
|
|
669
863
|
*/
|
|
670
|
-
const
|
|
864
|
+
const FileDataV1Schema = zod_v4.z.object({
|
|
671
865
|
content: zod_v4.z.array(zod_v4.z.string()),
|
|
672
866
|
created_at: zod_v4.z.string(),
|
|
673
867
|
modified_at: zod_v4.z.string()
|
|
674
868
|
});
|
|
675
869
|
/**
|
|
870
|
+
* Zod schema for FileDataV2 (content as string for text or Uint8Array for binary).
|
|
871
|
+
*/
|
|
872
|
+
const FileDataV2Schema = zod_v4.z.object({
|
|
873
|
+
content: zod_v4.z.union([zod_v4.z.string(), zod_v4.z.instanceof(Uint8Array)]),
|
|
874
|
+
mimeType: zod_v4.z.string(),
|
|
875
|
+
created_at: zod_v4.z.string(),
|
|
876
|
+
modified_at: zod_v4.z.string()
|
|
877
|
+
});
|
|
878
|
+
/**
|
|
879
|
+
* Zod v3 schema for FileData (re-export from backends)
|
|
880
|
+
*/
|
|
881
|
+
const FileDataSchema = zod_v4.z.union([FileDataV1Schema, FileDataV2Schema]);
|
|
882
|
+
/**
|
|
676
883
|
* Reducer for files state that merges file updates with support for deletions.
|
|
677
884
|
* When a file value is null, the file is deleted from state.
|
|
678
885
|
* When a file value is non-null, it is added or updated in state.
|
|
@@ -715,8 +922,8 @@ const FilesystemStateSchema = new _langchain_langgraph.StateSchema({ files: new
|
|
|
715
922
|
* @param stateAndStore - State and store container for backend initialization
|
|
716
923
|
*/
|
|
717
924
|
function getBackend(backend, stateAndStore) {
|
|
718
|
-
|
|
719
|
-
return
|
|
925
|
+
const actualBackend = typeof backend === "function" ? backend(stateAndStore) : backend;
|
|
926
|
+
return isSandboxProtocol(actualBackend) ? adaptSandboxProtocol(actualBackend) : adaptBackendProtocol(actualBackend);
|
|
720
927
|
}
|
|
721
928
|
const FILESYSTEM_SYSTEM_PROMPT = `## Filesystem Tools \`ls\`, \`read_file\`, \`write_file\`, \`edit_file\`, \`glob\`, \`grep\`
|
|
722
929
|
|
|
@@ -841,7 +1048,9 @@ function createLsTool(backend, options) {
|
|
|
841
1048
|
store: config.store
|
|
842
1049
|
});
|
|
843
1050
|
const path = input.path || "/";
|
|
844
|
-
const
|
|
1051
|
+
const lsResult = await resolvedBackend.ls(path);
|
|
1052
|
+
if (lsResult.error) return `Error listing files: ${lsResult.error}`;
|
|
1053
|
+
const infos = lsResult.files || [];
|
|
845
1054
|
if (infos.length === 0) return `No files found in ${path}`;
|
|
846
1055
|
const lines = [];
|
|
847
1056
|
for (const info of infos) if (info.is_dir) lines.push(`${info.path} (directory)`);
|
|
@@ -869,15 +1078,64 @@ function createReadFileTool(backend, options) {
|
|
|
869
1078
|
store: config.store
|
|
870
1079
|
});
|
|
871
1080
|
const { file_path, offset = 0, limit = 100 } = input;
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1081
|
+
const readResult = await resolvedBackend.read(file_path, offset, limit);
|
|
1082
|
+
if (readResult.error) return [{
|
|
1083
|
+
type: "text",
|
|
1084
|
+
text: `Error: ${readResult.error}`
|
|
1085
|
+
}];
|
|
1086
|
+
const mimeType = readResult.mimeType ?? getMimeType(file_path);
|
|
1087
|
+
if (!isTextMimeType(mimeType)) {
|
|
1088
|
+
const binaryContent = readResult.content;
|
|
1089
|
+
if (!binaryContent) return [{
|
|
1090
|
+
type: "text",
|
|
1091
|
+
text: `Error: expected binary content for '${file_path}'`
|
|
1092
|
+
}];
|
|
1093
|
+
let base64Data;
|
|
1094
|
+
if (typeof binaryContent === "string") base64Data = binaryContent;
|
|
1095
|
+
else if (ArrayBuffer.isView(binaryContent)) base64Data = Buffer.from(binaryContent).toString("base64");
|
|
1096
|
+
else {
|
|
1097
|
+
const values = Object.values(binaryContent);
|
|
1098
|
+
base64Data = Buffer.from(new Uint8Array(values)).toString("base64");
|
|
1099
|
+
}
|
|
1100
|
+
const sizeBytes = Math.ceil(base64Data.length * 3 / 4);
|
|
1101
|
+
if (sizeBytes > 10485760) return [{
|
|
1102
|
+
type: "text",
|
|
1103
|
+
text: `Error: file too large to read (${Math.round(sizeBytes / (1024 * 1024))}MB exceeds ${MAX_BINARY_READ_SIZE_BYTES / (1024 * 1024)}MB limit for binary files)`
|
|
1104
|
+
}];
|
|
1105
|
+
if (mimeType.startsWith("image/")) return [{
|
|
1106
|
+
type: "image",
|
|
1107
|
+
mimeType,
|
|
1108
|
+
data: base64Data
|
|
1109
|
+
}];
|
|
1110
|
+
if (mimeType.startsWith("audio/")) return [{
|
|
1111
|
+
type: "audio",
|
|
1112
|
+
mimeType,
|
|
1113
|
+
data: base64Data
|
|
1114
|
+
}];
|
|
1115
|
+
if (mimeType.startsWith("video/")) return [{
|
|
1116
|
+
type: "video",
|
|
1117
|
+
mimeType,
|
|
1118
|
+
data: base64Data
|
|
1119
|
+
}];
|
|
1120
|
+
return [{
|
|
1121
|
+
type: "file",
|
|
1122
|
+
mimeType,
|
|
1123
|
+
data: base64Data
|
|
1124
|
+
}];
|
|
1125
|
+
}
|
|
1126
|
+
let content = typeof readResult.content === "string" ? readResult.content : "";
|
|
1127
|
+
const lines = content.split("\n");
|
|
1128
|
+
if (lines.length > limit) content = lines.slice(0, limit).join("\n");
|
|
1129
|
+
let formatted = formatContentWithLineNumbers(content, offset + 1);
|
|
1130
|
+
if (toolTokenLimitBeforeEvict && formatted.length >= 4 * toolTokenLimitBeforeEvict) {
|
|
876
1131
|
const truncationMsg = READ_FILE_TRUNCATION_MSG.replace("{file_path}", file_path);
|
|
877
1132
|
const maxContentLength = 4 * toolTokenLimitBeforeEvict - truncationMsg.length;
|
|
878
|
-
|
|
1133
|
+
formatted = formatted.substring(0, maxContentLength) + truncationMsg;
|
|
879
1134
|
}
|
|
880
|
-
return
|
|
1135
|
+
return [{
|
|
1136
|
+
type: "text",
|
|
1137
|
+
text: formatted
|
|
1138
|
+
}];
|
|
881
1139
|
}, {
|
|
882
1140
|
name: "read_file",
|
|
883
1141
|
description: customDescription || READ_FILE_TOOL_DESCRIPTION,
|
|
@@ -967,7 +1225,9 @@ function createGlobTool(backend, options) {
|
|
|
967
1225
|
store: config.store
|
|
968
1226
|
});
|
|
969
1227
|
const { pattern, path = "/" } = input;
|
|
970
|
-
const
|
|
1228
|
+
const globResult = await resolvedBackend.glob(pattern, path);
|
|
1229
|
+
if (globResult.error) return `Error finding files: ${globResult.error}`;
|
|
1230
|
+
const infos = globResult.files || [];
|
|
971
1231
|
if (infos.length === 0) return `No files found matching pattern '${pattern}'`;
|
|
972
1232
|
const result = truncateIfTooLong(infos.map((info) => info.path));
|
|
973
1233
|
if (Array.isArray(result)) return result.join("\n");
|
|
@@ -992,12 +1252,13 @@ function createGrepTool(backend, options) {
|
|
|
992
1252
|
store: config.store
|
|
993
1253
|
});
|
|
994
1254
|
const { pattern, path = "/", glob = null } = input;
|
|
995
|
-
const result = await resolvedBackend.
|
|
996
|
-
if (
|
|
997
|
-
|
|
1255
|
+
const result = await resolvedBackend.grep(pattern, path, glob);
|
|
1256
|
+
if (result.error) return result.error;
|
|
1257
|
+
const matches = result.matches ?? [];
|
|
1258
|
+
if (matches.length === 0) return `No matches found for pattern '${pattern}'`;
|
|
998
1259
|
const lines = [];
|
|
999
1260
|
let currentFile = null;
|
|
1000
|
-
for (const match of
|
|
1261
|
+
for (const match of matches) {
|
|
1001
1262
|
if (match.path !== currentFile) {
|
|
1002
1263
|
currentFile = match.path;
|
|
1003
1264
|
lines.push(`\n${currentFile}:`);
|
|
@@ -1820,12 +2081,14 @@ function formatMemoryContents(contents, sources) {
|
|
|
1820
2081
|
* @returns File content if found, null otherwise.
|
|
1821
2082
|
*/
|
|
1822
2083
|
async function loadMemoryFromBackend(backend, path) {
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
return
|
|
1827
|
-
|
|
1828
|
-
|
|
2084
|
+
const adaptedBackend = adaptBackendProtocol(backend);
|
|
2085
|
+
if (!adaptedBackend.downloadFiles) {
|
|
2086
|
+
const content = await adaptedBackend.read(path);
|
|
2087
|
+
if (content.error) return null;
|
|
2088
|
+
if (typeof content.content !== "string") return null;
|
|
2089
|
+
return content.content;
|
|
2090
|
+
}
|
|
2091
|
+
const results = await adaptedBackend.downloadFiles([path]);
|
|
1829
2092
|
if (results.length !== 1) throw new Error(`Expected 1 response for path ${path}, got ${results.length}`);
|
|
1830
2093
|
const response = results[0];
|
|
1831
2094
|
if (response.error != null) {
|
|
@@ -1861,8 +2124,8 @@ function createMemoryMiddleware(options) {
|
|
|
1861
2124
|
* Resolve backend from instance or factory.
|
|
1862
2125
|
*/
|
|
1863
2126
|
function getBackend(state) {
|
|
1864
|
-
if (typeof backend === "function") return backend({ state });
|
|
1865
|
-
return backend;
|
|
2127
|
+
if (typeof backend === "function") return adaptBackendProtocol(backend({ state }));
|
|
2128
|
+
return adaptBackendProtocol(backend);
|
|
1866
2129
|
}
|
|
1867
2130
|
return (0, langchain.createMiddleware)({
|
|
1868
2131
|
name: "MemoryMiddleware",
|
|
@@ -2184,12 +2447,15 @@ function parseSkillMetadataFromContent(content, skillPath, directoryName) {
|
|
|
2184
2447
|
* List all skills from a backend source.
|
|
2185
2448
|
*/
|
|
2186
2449
|
async function listSkillsFromBackend(backend, sourcePath) {
|
|
2450
|
+
const adaptedBackend = adaptBackendProtocol(backend);
|
|
2187
2451
|
const skills = [];
|
|
2188
2452
|
const pathSep = sourcePath.includes("\\") ? "\\" : "/";
|
|
2189
2453
|
const normalizedPath = sourcePath.endsWith("/") || sourcePath.endsWith("\\") ? sourcePath : `${sourcePath}${pathSep}`;
|
|
2190
2454
|
let fileInfos;
|
|
2191
2455
|
try {
|
|
2192
|
-
|
|
2456
|
+
const lsResult = await adaptedBackend.ls(normalizedPath);
|
|
2457
|
+
if (lsResult.error || !lsResult.files) return [];
|
|
2458
|
+
fileInfos = lsResult.files;
|
|
2193
2459
|
} catch {
|
|
2194
2460
|
return [];
|
|
2195
2461
|
}
|
|
@@ -2201,16 +2467,17 @@ async function listSkillsFromBackend(backend, sourcePath) {
|
|
|
2201
2467
|
if (entry.type !== "directory") continue;
|
|
2202
2468
|
const skillMdPath = `${normalizedPath}${entry.name}${pathSep}SKILL.md`;
|
|
2203
2469
|
let content;
|
|
2204
|
-
if (
|
|
2205
|
-
const results = await
|
|
2470
|
+
if (adaptedBackend.downloadFiles) {
|
|
2471
|
+
const results = await adaptedBackend.downloadFiles([skillMdPath]);
|
|
2206
2472
|
if (results.length !== 1) continue;
|
|
2207
2473
|
const response = results[0];
|
|
2208
2474
|
if (response.error != null || response.content == null) continue;
|
|
2209
2475
|
content = new TextDecoder().decode(response.content);
|
|
2210
2476
|
} else {
|
|
2211
|
-
const readResult = await
|
|
2212
|
-
if (readResult.
|
|
2213
|
-
content
|
|
2477
|
+
const readResult = await adaptedBackend.read(skillMdPath);
|
|
2478
|
+
if (readResult.error) continue;
|
|
2479
|
+
if (typeof readResult.content !== "string") continue;
|
|
2480
|
+
content = readResult.content;
|
|
2214
2481
|
}
|
|
2215
2482
|
const metadata = parseSkillMetadataFromContent(content, skillMdPath, entry.name);
|
|
2216
2483
|
if (metadata) skills.push(metadata);
|
|
@@ -2275,8 +2542,8 @@ function createSkillsMiddleware(options) {
|
|
|
2275
2542
|
* Resolve backend from instance or factory.
|
|
2276
2543
|
*/
|
|
2277
2544
|
function getBackend(state) {
|
|
2278
|
-
if (typeof backend === "function") return backend({ state });
|
|
2279
|
-
return backend;
|
|
2545
|
+
if (typeof backend === "function") return adaptBackendProtocol(backend({ state }));
|
|
2546
|
+
return adaptBackendProtocol(backend);
|
|
2280
2547
|
}
|
|
2281
2548
|
return (0, langchain.createMiddleware)({
|
|
2282
2549
|
name: "SkillsMiddleware",
|
|
@@ -2319,6 +2586,226 @@ function createSkillsMiddleware(options) {
|
|
|
2319
2586
|
* This module provides shared helpers used across middleware implementations.
|
|
2320
2587
|
*/
|
|
2321
2588
|
//#endregion
|
|
2589
|
+
//#region src/middleware/completion_notifier.ts
|
|
2590
|
+
/**
|
|
2591
|
+
* Completion notifier middleware for async subagents.
|
|
2592
|
+
*
|
|
2593
|
+
* **Experimental** — this middleware is experimental and may change in future releases.
|
|
2594
|
+
*
|
|
2595
|
+
* When an async subagent finishes (success or error), this middleware sends a
|
|
2596
|
+
* message back to the **supervisor's** thread so the supervisor wakes up and can
|
|
2597
|
+
* proactively relay results to the user — without the user having to poll via
|
|
2598
|
+
* `check_async_task`.
|
|
2599
|
+
*
|
|
2600
|
+
* ## Architecture
|
|
2601
|
+
*
|
|
2602
|
+
* The async subagent protocol is inherently fire-and-forget: the supervisor
|
|
2603
|
+
* launches a job via `start_async_task` and only learns about completion
|
|
2604
|
+
* when someone calls `check_async_task`. This middleware closes that gap.
|
|
2605
|
+
*
|
|
2606
|
+
* ```
|
|
2607
|
+
* Supervisor Subagent
|
|
2608
|
+
* | |
|
|
2609
|
+
* |--- start_async_task -----> |
|
|
2610
|
+
* |<-- task_id (immediately) - |
|
|
2611
|
+
* | | (working...)
|
|
2612
|
+
* | | (done!)
|
|
2613
|
+
* | |
|
|
2614
|
+
* |<-- runs.create( |
|
|
2615
|
+
* | supervisor_thread, |
|
|
2616
|
+
* | "completed: ...") |
|
|
2617
|
+
* | |
|
|
2618
|
+
* | (wakes up, sees result) |
|
|
2619
|
+
* ```
|
|
2620
|
+
*
|
|
2621
|
+
* The notifier calls `runs.create()` on the supervisor's thread, which
|
|
2622
|
+
* queues a new run. From the supervisor's perspective, it looks like a new
|
|
2623
|
+
* user message arrived — except the content is a structured notification
|
|
2624
|
+
* from the subagent.
|
|
2625
|
+
*
|
|
2626
|
+
* ## How parent context is propagated
|
|
2627
|
+
*
|
|
2628
|
+
* - `parentGraphId` is passed as a **constructor argument** to the middleware.
|
|
2629
|
+
* This is the supervisor's graph ID (or assistant ID), which the subagent
|
|
2630
|
+
* developer knows at configuration time.
|
|
2631
|
+
* - `url` is the URL of the LangGraph server where the supervisor is deployed.
|
|
2632
|
+
* This is required since JS does not support in-process ASGI transport.
|
|
2633
|
+
* - `headers` are optional additional headers for authenticating with the
|
|
2634
|
+
* supervisor's server.
|
|
2635
|
+
* - `parent_thread_id` is injected into the subagent's input state by the
|
|
2636
|
+
* supervisor's `start_async_task` tool. It survives thread interrupts and
|
|
2637
|
+
* updates because it lives in state, not config.
|
|
2638
|
+
* - If `parent_thread_id` is not present in state, the notifier silently no-ops.
|
|
2639
|
+
*
|
|
2640
|
+
* ## Usage
|
|
2641
|
+
*
|
|
2642
|
+
* ```typescript
|
|
2643
|
+
* import { createCompletionNotifierMiddleware } from "deepagents";
|
|
2644
|
+
*
|
|
2645
|
+
* const notifier = createCompletionNotifierMiddleware({
|
|
2646
|
+
* parentGraphId: "supervisor",
|
|
2647
|
+
* url: "https://my-deployment.langsmith.dev",
|
|
2648
|
+
* });
|
|
2649
|
+
*
|
|
2650
|
+
* const agent = createDeepAgent({
|
|
2651
|
+
* model: "claude-sonnet-4-5-20250929",
|
|
2652
|
+
* middleware: [notifier],
|
|
2653
|
+
* });
|
|
2654
|
+
* ```
|
|
2655
|
+
*
|
|
2656
|
+
* The middleware will read `parent_thread_id` from the agent's state at the
|
|
2657
|
+
* end of execution. This is injected automatically by the supervisor's
|
|
2658
|
+
* `start_async_task` tool when it creates the run.
|
|
2659
|
+
*
|
|
2660
|
+
* @module
|
|
2661
|
+
*/
|
|
2662
|
+
/** State key where the supervisor's launch tool stores the parent thread ID. */
|
|
2663
|
+
const PARENT_THREAD_ID_KEY = "parent_thread_id";
|
|
2664
|
+
/** Maximum characters to include from the last message in notifications. */
|
|
2665
|
+
const MAX_SUMMARY_LENGTH = 500;
|
|
2666
|
+
/**
|
|
2667
|
+
* State extension for subagents that use the completion notifier.
|
|
2668
|
+
*
|
|
2669
|
+
* These fields are injected by the supervisor's `start_async_task`
|
|
2670
|
+
* tool and read by the completion notifier middleware to send notifications
|
|
2671
|
+
* back to the supervisor's thread.
|
|
2672
|
+
*/
|
|
2673
|
+
const CompletionNotifierStateSchema = zod_v4.z.object({ parent_thread_id: zod_v4.z.string().nullish() });
|
|
2674
|
+
/**
|
|
2675
|
+
* Build headers for the supervisor's LangGraph server.
|
|
2676
|
+
*
|
|
2677
|
+
* Ensures `x-auth-scheme: langsmith` is present unless explicitly overridden.
|
|
2678
|
+
*/
|
|
2679
|
+
function resolveHeaders(headers) {
|
|
2680
|
+
const resolved = { ...headers };
|
|
2681
|
+
if (!("x-auth-scheme" in resolved)) resolved["x-auth-scheme"] = "langsmith";
|
|
2682
|
+
return resolved;
|
|
2683
|
+
}
|
|
2684
|
+
/**
|
|
2685
|
+
* Send a notification run to the parent supervisor's thread.
|
|
2686
|
+
*/
|
|
2687
|
+
async function notifyParent(parentThreadId, parentGraphId, notification, options) {
|
|
2688
|
+
try {
|
|
2689
|
+
await new _langchain_langgraph_sdk.Client({
|
|
2690
|
+
apiUrl: options.url,
|
|
2691
|
+
apiKey: null,
|
|
2692
|
+
defaultHeaders: resolveHeaders(options.headers)
|
|
2693
|
+
}).runs.create(parentThreadId, parentGraphId, { input: { messages: [{
|
|
2694
|
+
role: "user",
|
|
2695
|
+
content: notification
|
|
2696
|
+
}] } });
|
|
2697
|
+
} catch (e) {
|
|
2698
|
+
console.warn(`[CompletionNotifierMiddleware] Failed to notify parent thread ${parentThreadId}:`, e);
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
/**
|
|
2702
|
+
* Extract a summary from the subagent's final message.
|
|
2703
|
+
*
|
|
2704
|
+
* Returns at most 500 characters from the last message's content.
|
|
2705
|
+
*/
|
|
2706
|
+
function extractLastMessage(state) {
|
|
2707
|
+
const messages = state.messages;
|
|
2708
|
+
if (!messages || messages.length === 0) return "(no output)";
|
|
2709
|
+
const last = messages[messages.length - 1];
|
|
2710
|
+
if (last && typeof last === "object" && "content" in last) {
|
|
2711
|
+
const content = last.content;
|
|
2712
|
+
if (typeof content === "string") return content.slice(0, MAX_SUMMARY_LENGTH);
|
|
2713
|
+
return JSON.stringify(content).slice(0, MAX_SUMMARY_LENGTH);
|
|
2714
|
+
}
|
|
2715
|
+
return String(last).slice(0, MAX_SUMMARY_LENGTH);
|
|
2716
|
+
}
|
|
2717
|
+
/**
|
|
2718
|
+
* Create a completion notifier middleware for async subagents.
|
|
2719
|
+
*
|
|
2720
|
+
* **Experimental** — this middleware is experimental and may change.
|
|
2721
|
+
*
|
|
2722
|
+
* This middleware is added to the **subagent's** middleware stack (not the
|
|
2723
|
+
* supervisor's). When the subagent finishes, it sends a message to the
|
|
2724
|
+
* supervisor's thread via `runs.create()`, waking the supervisor so it can
|
|
2725
|
+
* proactively relay results.
|
|
2726
|
+
*
|
|
2727
|
+
* The supervisor's `parent_thread_id` is read from the subagent's own state
|
|
2728
|
+
* (injected by the supervisor's `start_async_task` tool at launch time).
|
|
2729
|
+
* The `parentGraphId` is provided as a constructor argument since it's static
|
|
2730
|
+
* configuration known at deployment time.
|
|
2731
|
+
*
|
|
2732
|
+
* If `parent_thread_id` is not present in state (e.g., the subagent was
|
|
2733
|
+
* launched manually without a supervisor), the middleware silently does
|
|
2734
|
+
* nothing.
|
|
2735
|
+
*
|
|
2736
|
+
* @param options - Configuration options.
|
|
2737
|
+
* @returns An `AgentMiddleware` instance.
|
|
2738
|
+
*
|
|
2739
|
+
* @example
|
|
2740
|
+
* ```typescript
|
|
2741
|
+
* import { createCompletionNotifierMiddleware } from "deepagents";
|
|
2742
|
+
*
|
|
2743
|
+
* const notifier = createCompletionNotifierMiddleware({
|
|
2744
|
+
* parentGraphId: "supervisor",
|
|
2745
|
+
* url: "https://my-deployment.langsmith.dev",
|
|
2746
|
+
* });
|
|
2747
|
+
*
|
|
2748
|
+
* const agent = createDeepAgent({
|
|
2749
|
+
* model: "claude-sonnet-4-5-20250929",
|
|
2750
|
+
* middleware: [notifier],
|
|
2751
|
+
* });
|
|
2752
|
+
* ```
|
|
2753
|
+
*/
|
|
2754
|
+
function createCompletionNotifierMiddleware(options) {
|
|
2755
|
+
const { parentGraphId, url, headers } = options;
|
|
2756
|
+
let notified = false;
|
|
2757
|
+
/**
|
|
2758
|
+
* Check whether we should send a notification.
|
|
2759
|
+
*/
|
|
2760
|
+
function shouldNotify(state) {
|
|
2761
|
+
if (notified) return false;
|
|
2762
|
+
return Boolean(state[PARENT_THREAD_ID_KEY]);
|
|
2763
|
+
}
|
|
2764
|
+
/**
|
|
2765
|
+
* Send a notification to the parent if conditions are met.
|
|
2766
|
+
*/
|
|
2767
|
+
async function sendNotification(state, message) {
|
|
2768
|
+
if (!shouldNotify(state)) return;
|
|
2769
|
+
notified = true;
|
|
2770
|
+
await notifyParent(state[PARENT_THREAD_ID_KEY], parentGraphId, message, {
|
|
2771
|
+
url,
|
|
2772
|
+
headers
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
2775
|
+
/**
|
|
2776
|
+
* Read the subagent's own thread_id from runtime config.
|
|
2777
|
+
*
|
|
2778
|
+
* The subagent's `thread_id` is the same as the `task_id` from the
|
|
2779
|
+
* supervisor's perspective.
|
|
2780
|
+
*/
|
|
2781
|
+
function getTaskId(runtime) {
|
|
2782
|
+
return runtime?.configurable?.thread_id;
|
|
2783
|
+
}
|
|
2784
|
+
/**
|
|
2785
|
+
* Build a notification string with task_id prefix.
|
|
2786
|
+
*/
|
|
2787
|
+
function formatNotification(body, runtime) {
|
|
2788
|
+
const taskId = getTaskId(runtime);
|
|
2789
|
+
return `${taskId ? `[task_id=${taskId}]` : ""}${body}`;
|
|
2790
|
+
}
|
|
2791
|
+
return (0, langchain.createMiddleware)({
|
|
2792
|
+
name: "CompletionNotifierMiddleware",
|
|
2793
|
+
stateSchema: CompletionNotifierStateSchema,
|
|
2794
|
+
async afterAgent(state, runtime) {
|
|
2795
|
+
await sendNotification(state, formatNotification(`Completed. Result: ${extractLastMessage(state)}`, runtime));
|
|
2796
|
+
},
|
|
2797
|
+
async wrapModelCall(request, handler) {
|
|
2798
|
+
try {
|
|
2799
|
+
return await handler(request);
|
|
2800
|
+
} catch (e) {
|
|
2801
|
+
const notification = formatNotification(`Error: ${e instanceof Error ? e.message : String(e)}`, request.runtime);
|
|
2802
|
+
await sendNotification(request.state, notification);
|
|
2803
|
+
throw e;
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
});
|
|
2807
|
+
}
|
|
2808
|
+
//#endregion
|
|
2322
2809
|
//#region src/middleware/summarization.ts
|
|
2323
2810
|
/**
|
|
2324
2811
|
* Summarization middleware with backend support for conversation history offloading.
|
|
@@ -2514,8 +3001,8 @@ function createSummarizationMiddleware(options) {
|
|
|
2514
3001
|
* Resolve backend from instance or factory.
|
|
2515
3002
|
*/
|
|
2516
3003
|
function getBackend(state) {
|
|
2517
|
-
if (typeof backend === "function") return backend({ state });
|
|
2518
|
-
return backend;
|
|
3004
|
+
if (typeof backend === "function") return adaptBackendProtocol(backend({ state }));
|
|
3005
|
+
return adaptBackendProtocol(backend);
|
|
2519
3006
|
}
|
|
2520
3007
|
/**
|
|
2521
3008
|
* Get or create session ID for history file naming.
|
|
@@ -3008,6 +3495,532 @@ ${summary}
|
|
|
3008
3495
|
});
|
|
3009
3496
|
}
|
|
3010
3497
|
//#endregion
|
|
3498
|
+
//#region src/middleware/async_subagents.ts
|
|
3499
|
+
function toolCallIdFromRuntime(runtime) {
|
|
3500
|
+
return runtime.toolCall?.id ?? runtime.toolCallId ?? "";
|
|
3501
|
+
}
|
|
3502
|
+
/**
|
|
3503
|
+
* Zod schema for {@link AsyncTask}.
|
|
3504
|
+
*
|
|
3505
|
+
* Used by the {@link ReducedValue} in the state schema so that LangGraph
|
|
3506
|
+
* can validate and serialize task records stored in `asyncTasks`.
|
|
3507
|
+
*/
|
|
3508
|
+
const AsyncTaskSchema = zod_v4.z.object({
|
|
3509
|
+
taskId: zod_v4.z.string(),
|
|
3510
|
+
agentName: zod_v4.z.string(),
|
|
3511
|
+
threadId: zod_v4.z.string(),
|
|
3512
|
+
runId: zod_v4.z.string(),
|
|
3513
|
+
status: zod_v4.z.string(),
|
|
3514
|
+
createdAt: zod_v4.z.string(),
|
|
3515
|
+
updatedAt: zod_v4.z.string().optional(),
|
|
3516
|
+
checkedAt: zod_v4.z.string().optional()
|
|
3517
|
+
});
|
|
3518
|
+
/**
|
|
3519
|
+
* State schema for the async subagent middleware.
|
|
3520
|
+
*
|
|
3521
|
+
* Declares `asyncTasks` as a reduced state channel so that individual
|
|
3522
|
+
* tool updates (launch, check, update, cancel, list) merge into the existing
|
|
3523
|
+
* tasks dict rather than replacing it wholesale.
|
|
3524
|
+
*/
|
|
3525
|
+
const AsyncTaskStateSchema = new _langchain_langgraph.StateSchema({ asyncTasks: new _langchain_langgraph.ReducedValue(zod_v4.z.record(zod_v4.z.string(), AsyncTaskSchema).default(() => ({})), {
|
|
3526
|
+
inputSchema: zod_v4.z.record(zod_v4.z.string(), AsyncTaskSchema).optional(),
|
|
3527
|
+
reducer: asyncTasksReducer
|
|
3528
|
+
}) });
|
|
3529
|
+
/**
|
|
3530
|
+
* Reducer for the `asyncTasks` state channel.
|
|
3531
|
+
*
|
|
3532
|
+
* Merges task updates into the existing tasks dict using shallow spread.
|
|
3533
|
+
* This allows individual tools to update a single task without overwriting
|
|
3534
|
+
* the full map — only the keys present in `update` are replaced.
|
|
3535
|
+
*
|
|
3536
|
+
* @param existing - The current tasks dict from state (may be undefined on first write).
|
|
3537
|
+
* @param update - New or updated task entries to merge in.
|
|
3538
|
+
* @returns Merged tasks dict.
|
|
3539
|
+
*/
|
|
3540
|
+
function asyncTasksReducer(existing, update) {
|
|
3541
|
+
return {
|
|
3542
|
+
...existing || {},
|
|
3543
|
+
...update || {}
|
|
3544
|
+
};
|
|
3545
|
+
}
|
|
3546
|
+
/**
|
|
3547
|
+
* Description template for the `start_async_task` tool.
|
|
3548
|
+
*
|
|
3549
|
+
* The `{available_agents}` placeholder is replaced at middleware creation
|
|
3550
|
+
* time with a formatted list of configured async subagent names and descriptions.
|
|
3551
|
+
*/
|
|
3552
|
+
const ASYNC_TASK_TOOL_DESCRIPTION = `Launch an async subagent on a remote LangGraph server. The subagent runs in the background and returns a task ID immediately.
|
|
3553
|
+
|
|
3554
|
+
Available async agent types:
|
|
3555
|
+
{available_agents}
|
|
3556
|
+
|
|
3557
|
+
## Usage notes:
|
|
3558
|
+
1. This tool launches a background task and returns immediately with a task ID. Report the task ID to the user and stop — do NOT immediately check status.
|
|
3559
|
+
2. Use \`check_async_task\` only when the user asks for a status update or result.
|
|
3560
|
+
3. Use \`update_async_task\` to send new instructions to a running task.
|
|
3561
|
+
4. Multiple async subagents can run concurrently — launch several and let them run in the background.
|
|
3562
|
+
5. The subagent runs on a remote LangGraph server, so it has its own tools and capabilities.`;
|
|
3563
|
+
/**
|
|
3564
|
+
* Default system prompt appended to the main agent's system message when
|
|
3565
|
+
* async subagent middleware is active.
|
|
3566
|
+
*
|
|
3567
|
+
* Provides the agent with instructions on how to use the five async subagent
|
|
3568
|
+
* tools (launch, check, update, cancel, list) including workflow ordering,
|
|
3569
|
+
* critical rules about polling behavior, and guidance on when to use async
|
|
3570
|
+
* subagents vs. synchronous delegation.
|
|
3571
|
+
*/
|
|
3572
|
+
const ASYNC_TASK_SYSTEM_PROMPT = `## Async subagents (remote LangGraph servers)
|
|
3573
|
+
|
|
3574
|
+
You have access to async subagent tools that launch background tasks on remote LangGraph servers.
|
|
3575
|
+
|
|
3576
|
+
### Tools:
|
|
3577
|
+
- \`start_async_task\`: Start a new background task. Returns a task ID immediately.
|
|
3578
|
+
- \`check_async_task\`: Check the status of a running task. Returns status and result if complete.
|
|
3579
|
+
- \`update_async_task\`: Send an update or new instructions to a running task.
|
|
3580
|
+
- \`cancel_async_task\`: Cancel a running task that is no longer needed.
|
|
3581
|
+
- \`list_async_tasks\`: List all tracked tasks with live statuses. Use this to check all tasks at once.
|
|
3582
|
+
|
|
3583
|
+
### Workflow:
|
|
3584
|
+
1. **Launch** — Use \`start_async_task\` to start a task. Report the task ID to the user and stop.
|
|
3585
|
+
Do NOT immediately check the status — the task runs in the background while you and the user continue other work.
|
|
3586
|
+
2. **Check (on request)** — Only use \`check_async_task\` when the user explicitly asks for a status update or
|
|
3587
|
+
result. If the status is "running", report that and stop — do not poll in a loop.
|
|
3588
|
+
3. **Update** (optional) — Use \`update_async_task\` to send new instructions to a running task. This interrupts
|
|
3589
|
+
the current run and starts a fresh one on the same thread. The task_id stays the same.
|
|
3590
|
+
4. **Cancel** (optional) — Use \`cancel_async_task\` to stop a task that is no longer needed.
|
|
3591
|
+
5. **Collect** — When \`check_async_task\` returns status "success", the result is included in the response.
|
|
3592
|
+
6. **List** — Use \`list_async_tasks\` to see live statuses for all tasks at once, or to recall task IDs after context compaction.
|
|
3593
|
+
|
|
3594
|
+
### Critical rules:
|
|
3595
|
+
- After launching, ALWAYS return control to the user immediately. Never auto-check after launching.
|
|
3596
|
+
- Never poll \`check_async_task\` in a loop. Check once per user request, then stop.
|
|
3597
|
+
- If a check returns "running", tell the user and wait for them to ask again.
|
|
3598
|
+
- Task statuses in conversation history are ALWAYS stale — a task that was "running" may now be done.
|
|
3599
|
+
NEVER report a status from a previous tool result. ALWAYS call a tool to get the current status:
|
|
3600
|
+
use \`list_async_tasks\` when the user asks about multiple tasks or "all tasks",
|
|
3601
|
+
use \`check_async_task\` when the user asks about a specific task.
|
|
3602
|
+
- Always show the full task_id — never truncate or abbreviate it.
|
|
3603
|
+
|
|
3604
|
+
### When to use async subagents:
|
|
3605
|
+
- Long-running tasks that would block the main agent
|
|
3606
|
+
- Tasks that benefit from running on specialized remote deployments
|
|
3607
|
+
- When you want to run multiple tasks concurrently and collect results later`;
|
|
3608
|
+
/**
|
|
3609
|
+
* Task statuses that will never change.
|
|
3610
|
+
*
|
|
3611
|
+
* When listing tasks, live-status fetches are skipped for tasks whose
|
|
3612
|
+
* cached status is in this set, since they are guaranteed to be final.
|
|
3613
|
+
*/
|
|
3614
|
+
const TERMINAL_STATUSES = new Set([
|
|
3615
|
+
"cancelled",
|
|
3616
|
+
"success",
|
|
3617
|
+
"error",
|
|
3618
|
+
"timeout",
|
|
3619
|
+
"interrupted"
|
|
3620
|
+
]);
|
|
3621
|
+
/**
|
|
3622
|
+
* Look up a tracked task from state by its `taskId`.
|
|
3623
|
+
*
|
|
3624
|
+
* @param taskId - The task ID to look up (will be trimmed).
|
|
3625
|
+
* @param state - The current agent state containing `asyncTasks`.
|
|
3626
|
+
* @returns The tracked task on success, or an error string.
|
|
3627
|
+
*/
|
|
3628
|
+
function resolveTrackedTask(taskId, state) {
|
|
3629
|
+
const tracked = (state.asyncTasks ?? {})[taskId.trim()];
|
|
3630
|
+
if (!tracked) return `No tracked task found for taskId: '${taskId}'`;
|
|
3631
|
+
return tracked;
|
|
3632
|
+
}
|
|
3633
|
+
/**
|
|
3634
|
+
* Build a check result from a run's current status and thread state values.
|
|
3635
|
+
*
|
|
3636
|
+
* For successful runs, extracts the last message's content from the remote
|
|
3637
|
+
* thread's state values. For errored runs, includes a generic error message.
|
|
3638
|
+
*
|
|
3639
|
+
* @param run - The run object from the SDK.
|
|
3640
|
+
* @param threadId - The thread ID for the run.
|
|
3641
|
+
* @param threadValues - The `values` from `ThreadState` (the remote subagent's state).
|
|
3642
|
+
*/
|
|
3643
|
+
function buildCheckResult(run, threadId, threadValues) {
|
|
3644
|
+
const checkResult = {
|
|
3645
|
+
status: run.status,
|
|
3646
|
+
threadId
|
|
3647
|
+
};
|
|
3648
|
+
if (run.status === "success") {
|
|
3649
|
+
const messages = (Array.isArray(threadValues) ? {} : threadValues)?.messages ?? [];
|
|
3650
|
+
if (messages.length > 0) {
|
|
3651
|
+
const last = messages[messages.length - 1];
|
|
3652
|
+
const rawContent = typeof last === "object" && last !== null && "content" in last ? last.content : last;
|
|
3653
|
+
checkResult.result = typeof rawContent === "string" ? rawContent : JSON.stringify(rawContent);
|
|
3654
|
+
} else checkResult.result = "Completed with no output messages.";
|
|
3655
|
+
} else if (run.status === "error") checkResult.error = "The async subagent encountered an error.";
|
|
3656
|
+
return checkResult;
|
|
3657
|
+
}
|
|
3658
|
+
/**
|
|
3659
|
+
* Filter tasks by cached status from agent state.
|
|
3660
|
+
*
|
|
3661
|
+
* Filtering uses the cached status, not live server status. Live statuses
|
|
3662
|
+
* are fetched after filtering by the calling tool.
|
|
3663
|
+
*
|
|
3664
|
+
* @param tasks - All tracked tasks from state.
|
|
3665
|
+
* @param statusFilter - If nullish or `'all'`, return all tasks.
|
|
3666
|
+
* Otherwise return only tasks whose cached status matches.
|
|
3667
|
+
*/
|
|
3668
|
+
function filterTasks(tasks, statusFilter) {
|
|
3669
|
+
if (!statusFilter || statusFilter === "all") return Object.values(tasks);
|
|
3670
|
+
return Object.values(tasks).filter((task) => task.status === statusFilter);
|
|
3671
|
+
}
|
|
3672
|
+
/**
|
|
3673
|
+
* Fetch the current run status from the server.
|
|
3674
|
+
*
|
|
3675
|
+
* Returns the cached status immediately for terminal tasks (avoiding
|
|
3676
|
+
* unnecessary API calls). Falls back to the cached status on SDK errors.
|
|
3677
|
+
*/
|
|
3678
|
+
async function fetchLiveTaskStatus(clients, task) {
|
|
3679
|
+
if (TERMINAL_STATUSES.has(task.status)) return task.status;
|
|
3680
|
+
try {
|
|
3681
|
+
return (await clients.getClient(task.agentName).runs.get(task.threadId, task.runId)).status;
|
|
3682
|
+
} catch {
|
|
3683
|
+
return task.status;
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
/**
|
|
3687
|
+
* Format a single task as a display string for list output.
|
|
3688
|
+
*/
|
|
3689
|
+
function formatTaskEntry(task, status) {
|
|
3690
|
+
return `- taskId: ${task.taskId} agent: ${task.agentName} status: ${status}`;
|
|
3691
|
+
}
|
|
3692
|
+
/**
|
|
3693
|
+
* Lazily-created, cached LangGraph SDK clients keyed by (url, headers).
|
|
3694
|
+
*
|
|
3695
|
+
* Agents that share the same URL and headers will reuse a single `Client`
|
|
3696
|
+
* instance, avoiding unnecessary connections.
|
|
3697
|
+
*/
|
|
3698
|
+
var ClientCache = class {
|
|
3699
|
+
agents;
|
|
3700
|
+
clients = /* @__PURE__ */ new Map();
|
|
3701
|
+
constructor(agents) {
|
|
3702
|
+
this.agents = agents;
|
|
3703
|
+
}
|
|
3704
|
+
/**
|
|
3705
|
+
* Build headers for a remote LangGraph server, adding the default
|
|
3706
|
+
* `x-auth-scheme: langsmith` header if not already present.
|
|
3707
|
+
*/
|
|
3708
|
+
resolveHeaders(spec) {
|
|
3709
|
+
const headers = { ...spec.headers || {} };
|
|
3710
|
+
if (!("x-auth-scheme" in headers)) headers["x-auth-scheme"] = "langsmith";
|
|
3711
|
+
return headers;
|
|
3712
|
+
}
|
|
3713
|
+
/**
|
|
3714
|
+
* Build a stable cache key from a spec's url and resolved headers.
|
|
3715
|
+
*/
|
|
3716
|
+
cacheKey(spec) {
|
|
3717
|
+
const headers = this.resolveHeaders(spec);
|
|
3718
|
+
const headerStr = Object.entries(headers).sort().flat().join(":");
|
|
3719
|
+
return `${spec.url ?? ""}|${headerStr}`;
|
|
3720
|
+
}
|
|
3721
|
+
/**
|
|
3722
|
+
* Get or create a `Client` for the named agent.
|
|
3723
|
+
*/
|
|
3724
|
+
getClient(name) {
|
|
3725
|
+
const spec = this.agents[name];
|
|
3726
|
+
const key = this.cacheKey(spec);
|
|
3727
|
+
const existing = this.clients.get(key);
|
|
3728
|
+
if (existing) return existing;
|
|
3729
|
+
const headers = this.resolveHeaders(spec);
|
|
3730
|
+
const client = new _langchain_langgraph_sdk.Client({
|
|
3731
|
+
apiUrl: spec.url,
|
|
3732
|
+
defaultHeaders: headers
|
|
3733
|
+
});
|
|
3734
|
+
this.clients.set(key, client);
|
|
3735
|
+
return client;
|
|
3736
|
+
}
|
|
3737
|
+
};
|
|
3738
|
+
/**
|
|
3739
|
+
* Build the `start_async_task` tool.
|
|
3740
|
+
*
|
|
3741
|
+
* Creates a thread on the remote server, starts a run, and returns a
|
|
3742
|
+
* `Command` that persists the new task in state.
|
|
3743
|
+
*/
|
|
3744
|
+
function buildStartTool(agentMap, clients, toolDescription) {
|
|
3745
|
+
return (0, langchain.tool)(async (input, runtime) => {
|
|
3746
|
+
if (!(input.agentName in agentMap)) {
|
|
3747
|
+
const allowed = Object.keys(agentMap).map((k) => `\`${k}\``).join(", ");
|
|
3748
|
+
return `Unknown async subagent type \`${input.agentName}\`. Available types: ${allowed}`;
|
|
3749
|
+
}
|
|
3750
|
+
const spec = agentMap[input.agentName];
|
|
3751
|
+
try {
|
|
3752
|
+
const client = clients.getClient(input.agentName);
|
|
3753
|
+
const thread = await client.threads.create();
|
|
3754
|
+
const run = await client.runs.create(thread.thread_id, spec.graphId, { input: { messages: [{
|
|
3755
|
+
role: "user",
|
|
3756
|
+
content: input.description
|
|
3757
|
+
}] } });
|
|
3758
|
+
const taskId = thread.thread_id;
|
|
3759
|
+
const task = {
|
|
3760
|
+
taskId,
|
|
3761
|
+
agentName: input.agentName,
|
|
3762
|
+
threadId: taskId,
|
|
3763
|
+
runId: run.run_id,
|
|
3764
|
+
status: "running",
|
|
3765
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3766
|
+
};
|
|
3767
|
+
return new _langchain_langgraph.Command({ update: {
|
|
3768
|
+
messages: [new langchain.ToolMessage({
|
|
3769
|
+
content: `Launched async subagent. taskId: ${taskId}`,
|
|
3770
|
+
tool_call_id: toolCallIdFromRuntime(runtime)
|
|
3771
|
+
})],
|
|
3772
|
+
asyncTasks: { [taskId]: task }
|
|
3773
|
+
} });
|
|
3774
|
+
} catch (e) {
|
|
3775
|
+
return `Failed to launch async subagent '${input.agentName}': ${e}`;
|
|
3776
|
+
}
|
|
3777
|
+
}, {
|
|
3778
|
+
name: "start_async_task",
|
|
3779
|
+
description: toolDescription,
|
|
3780
|
+
schema: zod_v4.z.object({
|
|
3781
|
+
description: zod_v4.z.string().describe("A detailed description of the task for the async subagent to perform."),
|
|
3782
|
+
agentName: zod_v4.z.string().describe("The type of async subagent to use. Must be one of the available types listed in the tool description.")
|
|
3783
|
+
})
|
|
3784
|
+
});
|
|
3785
|
+
}
|
|
3786
|
+
/**
|
|
3787
|
+
* Build the `check_async_task` tool.
|
|
3788
|
+
*
|
|
3789
|
+
* Fetches the current run status from the remote server and, if the run
|
|
3790
|
+
* succeeded, retrieves the thread state to extract the result.
|
|
3791
|
+
*/
|
|
3792
|
+
function buildCheckTool(clients) {
|
|
3793
|
+
return (0, langchain.tool)(async (input, runtime) => {
|
|
3794
|
+
const task = resolveTrackedTask(input.taskId, runtime.state);
|
|
3795
|
+
if (typeof task === "string") return task;
|
|
3796
|
+
const client = clients.getClient(task.agentName);
|
|
3797
|
+
let run;
|
|
3798
|
+
try {
|
|
3799
|
+
run = await client.runs.get(task.threadId, task.runId);
|
|
3800
|
+
} catch (e) {
|
|
3801
|
+
return `Failed to get run status: ${e}`;
|
|
3802
|
+
}
|
|
3803
|
+
let threadValues = {};
|
|
3804
|
+
if (run.status === "success") try {
|
|
3805
|
+
threadValues = (await client.threads.getState(task.threadId)).values || {};
|
|
3806
|
+
} catch {}
|
|
3807
|
+
const result = buildCheckResult(run, task.threadId, threadValues);
|
|
3808
|
+
const updatedTask = {
|
|
3809
|
+
taskId: task.taskId,
|
|
3810
|
+
agentName: task.agentName,
|
|
3811
|
+
threadId: task.threadId,
|
|
3812
|
+
runId: task.runId,
|
|
3813
|
+
status: result.status,
|
|
3814
|
+
createdAt: task.createdAt,
|
|
3815
|
+
updatedAt: task.updatedAt,
|
|
3816
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3817
|
+
};
|
|
3818
|
+
return new _langchain_langgraph.Command({ update: {
|
|
3819
|
+
messages: [new langchain.ToolMessage({
|
|
3820
|
+
content: JSON.stringify(result),
|
|
3821
|
+
tool_call_id: toolCallIdFromRuntime(runtime)
|
|
3822
|
+
})],
|
|
3823
|
+
asyncTasks: { [task.taskId]: updatedTask }
|
|
3824
|
+
} });
|
|
3825
|
+
}, {
|
|
3826
|
+
name: "check_async_task",
|
|
3827
|
+
description: "Check the status of an async subagent task. Returns the current status and, if complete, the result.",
|
|
3828
|
+
schema: zod_v4.z.object({ taskId: zod_v4.z.string().describe("The exact taskId string returned by start_async_task. Pass it verbatim.") })
|
|
3829
|
+
});
|
|
3830
|
+
}
|
|
3831
|
+
/**
|
|
3832
|
+
* Build the `update_async_task` tool.
|
|
3833
|
+
*
|
|
3834
|
+
* Sends a follow-up message to a running async subagent by creating a new
|
|
3835
|
+
* run on the same thread with `multitaskStrategy: "interrupt"`. The subagent
|
|
3836
|
+
* sees the full conversation history plus the new message. The `taskId`
|
|
3837
|
+
* remains the same; only the internal `runId` is updated.
|
|
3838
|
+
*/
|
|
3839
|
+
function buildUpdateTool(agentMap, clients) {
|
|
3840
|
+
return (0, langchain.tool)(async (input, runtime) => {
|
|
3841
|
+
const tracked = resolveTrackedTask(input.taskId, runtime.state);
|
|
3842
|
+
if (typeof tracked === "string") return tracked;
|
|
3843
|
+
const spec = agentMap[tracked.agentName];
|
|
3844
|
+
try {
|
|
3845
|
+
const run = await clients.getClient(tracked.agentName).runs.create(tracked.threadId, spec.graphId, {
|
|
3846
|
+
input: { messages: [{
|
|
3847
|
+
role: "user",
|
|
3848
|
+
content: input.message
|
|
3849
|
+
}] },
|
|
3850
|
+
multitaskStrategy: "interrupt"
|
|
3851
|
+
});
|
|
3852
|
+
const task = {
|
|
3853
|
+
taskId: tracked.taskId,
|
|
3854
|
+
agentName: tracked.agentName,
|
|
3855
|
+
threadId: tracked.threadId,
|
|
3856
|
+
runId: run.run_id,
|
|
3857
|
+
status: "running",
|
|
3858
|
+
createdAt: tracked.createdAt,
|
|
3859
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3860
|
+
checkedAt: tracked.checkedAt
|
|
3861
|
+
};
|
|
3862
|
+
return new _langchain_langgraph.Command({ update: {
|
|
3863
|
+
messages: [new langchain.ToolMessage({
|
|
3864
|
+
content: `Updated async subagent. taskId: ${tracked.taskId}`,
|
|
3865
|
+
tool_call_id: toolCallIdFromRuntime(runtime)
|
|
3866
|
+
})],
|
|
3867
|
+
asyncTasks: { [tracked.taskId]: task }
|
|
3868
|
+
} });
|
|
3869
|
+
} catch (e) {
|
|
3870
|
+
return `Failed to update async subagent: ${e}`;
|
|
3871
|
+
}
|
|
3872
|
+
}, {
|
|
3873
|
+
name: "update_async_task",
|
|
3874
|
+
description: "send updated instructions to an async subagent. Interrupts the current run and starts a new one on the same thread so the subagent sees the full conversation history plus your new message. The taskId remains the same.",
|
|
3875
|
+
schema: zod_v4.z.object({
|
|
3876
|
+
taskId: zod_v4.z.string().describe("The exact taskId string returned by start_async_task. Pass it verbatim."),
|
|
3877
|
+
message: zod_v4.z.string().describe("Follow-up instructions or context to send to the subagent")
|
|
3878
|
+
})
|
|
3879
|
+
});
|
|
3880
|
+
}
|
|
3881
|
+
/**
|
|
3882
|
+
* Build the `cancel_async_task` tool.
|
|
3883
|
+
*
|
|
3884
|
+
* Cancels the current run on the remote server and updates the task's
|
|
3885
|
+
* cached status to `"cancelled"`.
|
|
3886
|
+
*/
|
|
3887
|
+
function buildCancelTool(clients) {
|
|
3888
|
+
return (0, langchain.tool)(async (input, runtime) => {
|
|
3889
|
+
const tracked = resolveTrackedTask(input.taskId, runtime.state);
|
|
3890
|
+
if (typeof tracked === "string") return tracked;
|
|
3891
|
+
const client = clients.getClient(tracked.agentName);
|
|
3892
|
+
try {
|
|
3893
|
+
await client.runs.cancel(tracked.threadId, tracked.runId);
|
|
3894
|
+
} catch (e) {
|
|
3895
|
+
return `Failed to cancel run: ${e}`;
|
|
3896
|
+
}
|
|
3897
|
+
const updated = {
|
|
3898
|
+
taskId: tracked.taskId,
|
|
3899
|
+
agentName: tracked.agentName,
|
|
3900
|
+
threadId: tracked.threadId,
|
|
3901
|
+
runId: tracked.runId,
|
|
3902
|
+
status: "cancelled",
|
|
3903
|
+
createdAt: tracked.createdAt,
|
|
3904
|
+
updatedAt: tracked.updatedAt,
|
|
3905
|
+
checkedAt: tracked.checkedAt
|
|
3906
|
+
};
|
|
3907
|
+
return new _langchain_langgraph.Command({ update: {
|
|
3908
|
+
messages: [new langchain.ToolMessage({
|
|
3909
|
+
content: `Cancelled async subagent task: ${tracked.taskId}`,
|
|
3910
|
+
tool_call_id: toolCallIdFromRuntime(runtime)
|
|
3911
|
+
})],
|
|
3912
|
+
asyncTasks: { [tracked.taskId]: updated }
|
|
3913
|
+
} });
|
|
3914
|
+
}, {
|
|
3915
|
+
name: "cancel_async_task",
|
|
3916
|
+
description: "Cancel a running async subagent task. Use this to stop a task that is no longer needed.",
|
|
3917
|
+
schema: zod_v4.z.object({ taskId: zod_v4.z.string().describe("The exact taskId string returned by start_async_task. Pass it verbatim.") })
|
|
3918
|
+
});
|
|
3919
|
+
}
|
|
3920
|
+
/**
|
|
3921
|
+
* Build the `list_async_tasks` tool.
|
|
3922
|
+
*
|
|
3923
|
+
* Lists all tracked tasks with their live statuses fetched in parallel.
|
|
3924
|
+
* Supports optional filtering by cached status.
|
|
3925
|
+
*/
|
|
3926
|
+
function buildListTool(clients) {
|
|
3927
|
+
return (0, langchain.tool)(async (input, runtime) => {
|
|
3928
|
+
const filtered = filterTasks(runtime.state.asyncTasks ?? {}, input.statusFilter ?? void 0);
|
|
3929
|
+
if (filtered.length === 0) return "No async subagent tasks tracked";
|
|
3930
|
+
const statuses = await Promise.all(filtered.map((task) => fetchLiveTaskStatus(clients, task)));
|
|
3931
|
+
const updatedTasks = {};
|
|
3932
|
+
const entries = [];
|
|
3933
|
+
for (let idx = 0; idx < filtered.length; idx++) {
|
|
3934
|
+
const task = filtered[idx];
|
|
3935
|
+
const status = statuses[idx];
|
|
3936
|
+
const taskEntry = formatTaskEntry(task, status);
|
|
3937
|
+
entries.push(taskEntry);
|
|
3938
|
+
updatedTasks[task.taskId] = {
|
|
3939
|
+
taskId: task.taskId,
|
|
3940
|
+
agentName: task.agentName,
|
|
3941
|
+
threadId: task.threadId,
|
|
3942
|
+
runId: task.runId,
|
|
3943
|
+
status,
|
|
3944
|
+
createdAt: task.createdAt,
|
|
3945
|
+
updatedAt: task.updatedAt,
|
|
3946
|
+
checkedAt: task.checkedAt
|
|
3947
|
+
};
|
|
3948
|
+
}
|
|
3949
|
+
return new _langchain_langgraph.Command({ update: {
|
|
3950
|
+
messages: [new langchain.ToolMessage({
|
|
3951
|
+
content: `${entries.length} tracked task(s):\n${entries.join("\n")}`,
|
|
3952
|
+
tool_call_id: toolCallIdFromRuntime(runtime)
|
|
3953
|
+
})],
|
|
3954
|
+
asyncTasks: updatedTasks
|
|
3955
|
+
} });
|
|
3956
|
+
}, {
|
|
3957
|
+
name: "list_async_tasks",
|
|
3958
|
+
description: "List tracked async subagent tasks with their current live statuses. Be default shows all tasks. Use `statusFilter` to narrow by status (e.g., 'running', 'success', 'error', 'cancelled'). Use `check_async_task` to get the full result of a specific completed task.",
|
|
3959
|
+
schema: zod_v4.z.object({ statusFilter: zod_v4.z.string().nullish().describe("Filter tasks by status. One of: 'running', 'success', 'error', 'cancelled', 'all'. Defaults to 'all'.") })
|
|
3960
|
+
});
|
|
3961
|
+
}
|
|
3962
|
+
/**
|
|
3963
|
+
* Create middleware that adds async subagent tools to an agent.
|
|
3964
|
+
*
|
|
3965
|
+
* Provides five tools for launching, checking, updating, cancelling, and
|
|
3966
|
+
* listing background tasks on remote LangGraph deployments. Task state is
|
|
3967
|
+
* persisted in the `asyncTasks` state channel so it survives
|
|
3968
|
+
* context compaction.
|
|
3969
|
+
*
|
|
3970
|
+
* @throws {Error} If no async subagents are provided or names are duplicated.
|
|
3971
|
+
*
|
|
3972
|
+
* @example
|
|
3973
|
+
* ```ts
|
|
3974
|
+
* const middleware = createAsyncSubAgentMiddleware({
|
|
3975
|
+
* asyncSubAgents: [{
|
|
3976
|
+
* name: "researcher",
|
|
3977
|
+
* description: "Research agent for deep analysis",
|
|
3978
|
+
* url: "https://my-deployment.langsmith.dev",
|
|
3979
|
+
* graphId: "research_agent",
|
|
3980
|
+
* }],
|
|
3981
|
+
* });
|
|
3982
|
+
* ```
|
|
3983
|
+
*/
|
|
3984
|
+
/**
|
|
3985
|
+
* Type guard to distinguish async SubAgents from sync SubAgents/CompiledSubAgents.
|
|
3986
|
+
*
|
|
3987
|
+
* Uses the presence of the `graphId` field as the runtime discriminant —
|
|
3988
|
+
* `AsyncSubAgent` requires it, while `SubAgent` and `CompiledSubAgent` do not have it.
|
|
3989
|
+
*/
|
|
3990
|
+
function isAsyncSubAgent(subAgent) {
|
|
3991
|
+
return "graphId" in subAgent;
|
|
3992
|
+
}
|
|
3993
|
+
function createAsyncSubAgentMiddleware(options) {
|
|
3994
|
+
const { asyncSubAgents, systemPrompt = ASYNC_TASK_SYSTEM_PROMPT } = options;
|
|
3995
|
+
if (!asyncSubAgents || asyncSubAgents.length === 0) throw new Error("At least one async subagent must be specified");
|
|
3996
|
+
const names = asyncSubAgents.map((a) => a.name);
|
|
3997
|
+
const duplicates = names.filter((n, i) => names.indexOf(n) !== i);
|
|
3998
|
+
if (duplicates.length > 0) throw new Error(`Duplicate async subagent names: ${[...new Set(duplicates)].join(", ")}`);
|
|
3999
|
+
const agentMap = Object.fromEntries(asyncSubAgents.map((a) => [a.name, a]));
|
|
4000
|
+
const clients = new ClientCache(agentMap);
|
|
4001
|
+
const agentsDescription = asyncSubAgents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
|
|
4002
|
+
const tools = [
|
|
4003
|
+
buildStartTool(agentMap, clients, ASYNC_TASK_TOOL_DESCRIPTION.replace("{available_agents}", agentsDescription)),
|
|
4004
|
+
buildCheckTool(clients),
|
|
4005
|
+
buildUpdateTool(agentMap, clients),
|
|
4006
|
+
buildCancelTool(clients),
|
|
4007
|
+
buildListTool(clients)
|
|
4008
|
+
];
|
|
4009
|
+
const fullSystemPrompt = systemPrompt ? `${systemPrompt}\n\nAvailable async subagent types:\n${agentsDescription}` : null;
|
|
4010
|
+
return (0, langchain.createMiddleware)({
|
|
4011
|
+
name: "asyncSubAgentMiddleware",
|
|
4012
|
+
stateSchema: AsyncTaskStateSchema,
|
|
4013
|
+
tools,
|
|
4014
|
+
wrapModelCall: async (request, handler) => {
|
|
4015
|
+
if (fullSystemPrompt !== null) return handler({
|
|
4016
|
+
...request,
|
|
4017
|
+
systemMessage: request.systemMessage.concat(new langchain.SystemMessage({ content: fullSystemPrompt }))
|
|
4018
|
+
});
|
|
4019
|
+
return handler(request);
|
|
4020
|
+
}
|
|
4021
|
+
});
|
|
4022
|
+
}
|
|
4023
|
+
//#endregion
|
|
3011
4024
|
//#region src/backends/store.ts
|
|
3012
4025
|
const NAMESPACE_COMPONENT_RE = /^[A-Za-z0-9\-_.@+:~]+$/;
|
|
3013
4026
|
/**
|
|
@@ -3043,9 +4056,11 @@ function validateNamespace(namespace) {
|
|
|
3043
4056
|
var StoreBackend = class {
|
|
3044
4057
|
stateAndStore;
|
|
3045
4058
|
_namespace;
|
|
4059
|
+
fileFormat;
|
|
3046
4060
|
constructor(stateAndStore, options) {
|
|
3047
4061
|
this.stateAndStore = stateAndStore;
|
|
3048
4062
|
if (options?.namespace) this._namespace = validateNamespace(options.namespace);
|
|
4063
|
+
this.fileFormat = options?.fileFormat ?? "v2";
|
|
3049
4064
|
}
|
|
3050
4065
|
/**
|
|
3051
4066
|
* Get the store instance.
|
|
@@ -3082,9 +4097,10 @@ var StoreBackend = class {
|
|
|
3082
4097
|
*/
|
|
3083
4098
|
convertStoreItemToFileData(storeItem) {
|
|
3084
4099
|
const value = storeItem.value;
|
|
3085
|
-
if (!value.content
|
|
4100
|
+
if (!(value.content !== void 0 && (Array.isArray(value.content) || typeof value.content === "string" || ArrayBuffer.isView(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(", ")}`);
|
|
3086
4101
|
return {
|
|
3087
4102
|
content: value.content,
|
|
4103
|
+
...value.mimeType ? { mimeType: value.mimeType } : {},
|
|
3088
4104
|
created_at: value.created_at,
|
|
3089
4105
|
modified_at: value.modified_at
|
|
3090
4106
|
};
|
|
@@ -3093,11 +4109,12 @@ var StoreBackend = class {
|
|
|
3093
4109
|
* Convert FileData to a value suitable for store.put().
|
|
3094
4110
|
*
|
|
3095
4111
|
* @param fileData - The FileData to convert
|
|
3096
|
-
* @returns Object with content, created_at, and modified_at fields
|
|
4112
|
+
* @returns Object with content, mimeType, created_at, and modified_at fields
|
|
3097
4113
|
*/
|
|
3098
4114
|
convertFileDataToStoreValue(fileData) {
|
|
3099
4115
|
return {
|
|
3100
4116
|
content: fileData.content,
|
|
4117
|
+
..."mimeType" in fileData ? { mimeType: fileData.mimeType } : {},
|
|
3101
4118
|
created_at: fileData.created_at,
|
|
3102
4119
|
modified_at: fileData.modified_at
|
|
3103
4120
|
};
|
|
@@ -3132,10 +4149,10 @@ var StoreBackend = class {
|
|
|
3132
4149
|
* List files and directories in the specified directory (non-recursive).
|
|
3133
4150
|
*
|
|
3134
4151
|
* @param path - Absolute path to directory
|
|
3135
|
-
* @returns
|
|
4152
|
+
* @returns LsResult with list of FileInfo objects on success or error on failure.
|
|
3136
4153
|
* Directories have a trailing / in their path and is_dir=true.
|
|
3137
4154
|
*/
|
|
3138
|
-
async
|
|
4155
|
+
async ls(path) {
|
|
3139
4156
|
const store = this.getStore();
|
|
3140
4157
|
const namespace = this.getNamespace();
|
|
3141
4158
|
const items = await this.searchStorePaginated(store, namespace);
|
|
@@ -3153,7 +4170,7 @@ var StoreBackend = class {
|
|
|
3153
4170
|
}
|
|
3154
4171
|
try {
|
|
3155
4172
|
const fd = this.convertStoreItemToFileData(item);
|
|
3156
|
-
const size = fd.content.join("\n").length;
|
|
4173
|
+
const size = isFileDataV1(fd) ? fd.content.join("\n").length : isFileDataBinary(fd) ? fd.content.byteLength : fd.content.length;
|
|
3157
4174
|
infos.push({
|
|
3158
4175
|
path: itemKey,
|
|
3159
4176
|
is_dir: false,
|
|
@@ -3171,35 +4188,49 @@ var StoreBackend = class {
|
|
|
3171
4188
|
modified_at: ""
|
|
3172
4189
|
});
|
|
3173
4190
|
infos.sort((a, b) => a.path.localeCompare(b.path));
|
|
3174
|
-
return infos;
|
|
4191
|
+
return { files: infos };
|
|
3175
4192
|
}
|
|
3176
4193
|
/**
|
|
3177
|
-
* Read file content
|
|
4194
|
+
* Read file content.
|
|
4195
|
+
*
|
|
4196
|
+
* Text files are paginated by line offset/limit.
|
|
4197
|
+
* Binary files return full Uint8Array content (offset/limit ignored).
|
|
3178
4198
|
*
|
|
3179
4199
|
* @param filePath - Absolute file path
|
|
3180
4200
|
* @param offset - Line offset to start reading from (0-indexed)
|
|
3181
4201
|
* @param limit - Maximum number of lines to read
|
|
3182
|
-
* @returns
|
|
4202
|
+
* @returns ReadResult with content on success or error on failure
|
|
3183
4203
|
*/
|
|
3184
4204
|
async read(filePath, offset = 0, limit = 500) {
|
|
3185
4205
|
try {
|
|
3186
|
-
|
|
4206
|
+
const readRawResult = await this.readRaw(filePath);
|
|
4207
|
+
if (readRawResult.error || !readRawResult.data) return { error: readRawResult.error || "File data not found" };
|
|
4208
|
+
const fileDataV2 = migrateToFileDataV2(readRawResult.data, filePath);
|
|
4209
|
+
if (!isTextMimeType(fileDataV2.mimeType)) return {
|
|
4210
|
+
content: fileDataV2.content,
|
|
4211
|
+
mimeType: fileDataV2.mimeType
|
|
4212
|
+
};
|
|
4213
|
+
if (typeof fileDataV2.content !== "string") return { error: `File '${filePath}' has binary content but text MIME type` };
|
|
4214
|
+
return {
|
|
4215
|
+
content: fileDataV2.content.split("\n").slice(offset, offset + limit).join("\n"),
|
|
4216
|
+
mimeType: fileDataV2.mimeType
|
|
4217
|
+
};
|
|
3187
4218
|
} catch (e) {
|
|
3188
|
-
return
|
|
4219
|
+
return { error: e.message };
|
|
3189
4220
|
}
|
|
3190
4221
|
}
|
|
3191
4222
|
/**
|
|
3192
4223
|
* Read file content as raw FileData.
|
|
3193
4224
|
*
|
|
3194
4225
|
* @param filePath - Absolute file path
|
|
3195
|
-
* @returns
|
|
4226
|
+
* @returns ReadRawResult with raw file data on success or error on failure
|
|
3196
4227
|
*/
|
|
3197
4228
|
async readRaw(filePath) {
|
|
3198
4229
|
const store = this.getStore();
|
|
3199
4230
|
const namespace = this.getNamespace();
|
|
3200
4231
|
const item = await store.get(namespace, filePath);
|
|
3201
|
-
if (!item)
|
|
3202
|
-
return this.convertStoreItemToFileData(item);
|
|
4232
|
+
if (!item) return { error: `File '${filePath}' not found` };
|
|
4233
|
+
return { data: this.convertStoreItemToFileData(item) };
|
|
3203
4234
|
}
|
|
3204
4235
|
/**
|
|
3205
4236
|
* Create a new file with content.
|
|
@@ -3209,7 +4240,8 @@ var StoreBackend = class {
|
|
|
3209
4240
|
const store = this.getStore();
|
|
3210
4241
|
const namespace = this.getNamespace();
|
|
3211
4242
|
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.` };
|
|
3212
|
-
const
|
|
4243
|
+
const mimeType = getMimeType(filePath);
|
|
4244
|
+
const fileData = createFileData(content, void 0, this.fileFormat, mimeType);
|
|
3213
4245
|
const storeValue = this.convertFileDataToStoreValue(fileData);
|
|
3214
4246
|
await store.put(namespace, filePath, storeValue);
|
|
3215
4247
|
return {
|
|
@@ -3244,9 +4276,10 @@ var StoreBackend = class {
|
|
|
3244
4276
|
}
|
|
3245
4277
|
}
|
|
3246
4278
|
/**
|
|
3247
|
-
*
|
|
4279
|
+
* Search file contents for a literal text pattern.
|
|
4280
|
+
* Binary files are skipped.
|
|
3248
4281
|
*/
|
|
3249
|
-
async
|
|
4282
|
+
async grep(pattern, path = "/", glob = null) {
|
|
3250
4283
|
const store = this.getStore();
|
|
3251
4284
|
const namespace = this.getNamespace();
|
|
3252
4285
|
const items = await this.searchStorePaginated(store, namespace);
|
|
@@ -3256,12 +4289,12 @@ var StoreBackend = class {
|
|
|
3256
4289
|
} catch {
|
|
3257
4290
|
continue;
|
|
3258
4291
|
}
|
|
3259
|
-
return grepMatchesFromFiles(files, pattern, path, glob);
|
|
4292
|
+
return { matches: grepMatchesFromFiles(files, pattern, path, glob) };
|
|
3260
4293
|
}
|
|
3261
4294
|
/**
|
|
3262
4295
|
* Structured glob matching returning FileInfo objects.
|
|
3263
4296
|
*/
|
|
3264
|
-
async
|
|
4297
|
+
async glob(pattern, path = "/") {
|
|
3265
4298
|
const store = this.getStore();
|
|
3266
4299
|
const namespace = this.getNamespace();
|
|
3267
4300
|
const items = await this.searchStorePaginated(store, namespace);
|
|
@@ -3272,12 +4305,12 @@ var StoreBackend = class {
|
|
|
3272
4305
|
continue;
|
|
3273
4306
|
}
|
|
3274
4307
|
const result = globSearchFiles(files, pattern, path);
|
|
3275
|
-
if (result === "No files found") return [];
|
|
4308
|
+
if (result === "No files found") return { files: [] };
|
|
3276
4309
|
const paths = result.split("\n");
|
|
3277
4310
|
const infos = [];
|
|
3278
4311
|
for (const p of paths) {
|
|
3279
4312
|
const fd = files[p];
|
|
3280
|
-
const size = fd ? fd.content.join("\n").length : 0;
|
|
4313
|
+
const size = fd ? isFileDataV1(fd) ? fd.content.join("\n").length : isFileDataBinary(fd) ? fd.content.byteLength : fd.content.length : 0;
|
|
3281
4314
|
infos.push({
|
|
3282
4315
|
path: p,
|
|
3283
4316
|
is_dir: false,
|
|
@@ -3285,7 +4318,7 @@ var StoreBackend = class {
|
|
|
3285
4318
|
modified_at: fd?.modified_at || ""
|
|
3286
4319
|
});
|
|
3287
4320
|
}
|
|
3288
|
-
return infos;
|
|
4321
|
+
return { files: infos };
|
|
3289
4322
|
}
|
|
3290
4323
|
/**
|
|
3291
4324
|
* Upload multiple files.
|
|
@@ -3298,7 +4331,11 @@ var StoreBackend = class {
|
|
|
3298
4331
|
const namespace = this.getNamespace();
|
|
3299
4332
|
const responses = [];
|
|
3300
4333
|
for (const [path, content] of files) try {
|
|
3301
|
-
const
|
|
4334
|
+
const mimeType = getMimeType(path);
|
|
4335
|
+
const isBinary = this.fileFormat === "v2" && !isTextMimeType(mimeType);
|
|
4336
|
+
let fileData;
|
|
4337
|
+
if (isBinary) fileData = createFileData(content, void 0, "v2", mimeType);
|
|
4338
|
+
else fileData = createFileData(new TextDecoder().decode(content), void 0, this.fileFormat, mimeType);
|
|
3302
4339
|
const storeValue = this.convertFileDataToStoreValue(fileData);
|
|
3303
4340
|
await store.put(namespace, path, storeValue);
|
|
3304
4341
|
responses.push({
|
|
@@ -3333,11 +4370,17 @@ var StoreBackend = class {
|
|
|
3333
4370
|
});
|
|
3334
4371
|
continue;
|
|
3335
4372
|
}
|
|
3336
|
-
const
|
|
3337
|
-
|
|
3338
|
-
|
|
4373
|
+
const fileDataV2 = migrateToFileDataV2(this.convertStoreItemToFileData(item), path);
|
|
4374
|
+
if (typeof fileDataV2.content === "string") {
|
|
4375
|
+
const content = new TextEncoder().encode(fileDataV2.content);
|
|
4376
|
+
responses.push({
|
|
4377
|
+
path,
|
|
4378
|
+
content,
|
|
4379
|
+
error: null
|
|
4380
|
+
});
|
|
4381
|
+
} else responses.push({
|
|
3339
4382
|
path,
|
|
3340
|
-
content,
|
|
4383
|
+
content: fileDataV2.content,
|
|
3341
4384
|
error: null
|
|
3342
4385
|
});
|
|
3343
4386
|
} catch {
|
|
@@ -3410,10 +4453,10 @@ var FilesystemBackend = class {
|
|
|
3410
4453
|
* @returns List of FileInfo objects for files and directories directly in the directory.
|
|
3411
4454
|
* Directories have a trailing / in their path and is_dir=true.
|
|
3412
4455
|
*/
|
|
3413
|
-
async
|
|
4456
|
+
async ls(dirPath) {
|
|
3414
4457
|
try {
|
|
3415
4458
|
const resolvedPath = this.resolvePath(dirPath);
|
|
3416
|
-
if (!(await node_fs_promises.default.stat(resolvedPath)).isDirectory()) return [];
|
|
4459
|
+
if (!(await node_fs_promises.default.stat(resolvedPath)).isDirectory()) return { files: [] };
|
|
3417
4460
|
const entries = await node_fs_promises.default.readdir(resolvedPath, { withFileTypes: true });
|
|
3418
4461
|
const results = [];
|
|
3419
4462
|
const cwdStr = this.cwd.endsWith(node_path.default.sep) ? this.cwd : this.cwd + node_path.default.sep;
|
|
@@ -3461,9 +4504,9 @@ var FilesystemBackend = class {
|
|
|
3461
4504
|
}
|
|
3462
4505
|
}
|
|
3463
4506
|
results.sort((a, b) => a.path.localeCompare(b.path));
|
|
3464
|
-
return results;
|
|
4507
|
+
return { files: results };
|
|
3465
4508
|
} catch {
|
|
3466
|
-
return [];
|
|
4509
|
+
return { files: [] };
|
|
3467
4510
|
}
|
|
3468
4511
|
}
|
|
3469
4512
|
/**
|
|
@@ -3477,62 +4520,105 @@ var FilesystemBackend = class {
|
|
|
3477
4520
|
async read(filePath, offset = 0, limit = 500) {
|
|
3478
4521
|
try {
|
|
3479
4522
|
const resolvedPath = this.resolvePath(filePath);
|
|
4523
|
+
const mimeType = getMimeType(filePath);
|
|
4524
|
+
const isBinary = !isTextMimeType(mimeType);
|
|
3480
4525
|
let content;
|
|
3481
4526
|
if (SUPPORTS_NOFOLLOW) {
|
|
3482
|
-
if (!(await node_fs_promises.default.stat(resolvedPath)).isFile()) return
|
|
4527
|
+
if (!(await node_fs_promises.default.stat(resolvedPath)).isFile()) return { error: `File '${filePath}' not found` };
|
|
3483
4528
|
const fd = await node_fs_promises.default.open(resolvedPath, node_fs.default.constants.O_RDONLY | node_fs.default.constants.O_NOFOLLOW);
|
|
3484
4529
|
try {
|
|
4530
|
+
if (isBinary) {
|
|
4531
|
+
const buffer = await fd.readFile();
|
|
4532
|
+
return {
|
|
4533
|
+
content: new Uint8Array(buffer),
|
|
4534
|
+
mimeType
|
|
4535
|
+
};
|
|
4536
|
+
}
|
|
3485
4537
|
content = await fd.readFile({ encoding: "utf-8" });
|
|
3486
4538
|
} finally {
|
|
3487
4539
|
await fd.close();
|
|
3488
4540
|
}
|
|
3489
4541
|
} else {
|
|
3490
4542
|
const stat = await node_fs_promises.default.lstat(resolvedPath);
|
|
3491
|
-
if (stat.isSymbolicLink()) return
|
|
3492
|
-
if (!stat.isFile()) return
|
|
4543
|
+
if (stat.isSymbolicLink()) return { error: `Symlinks are not allowed: ${filePath}` };
|
|
4544
|
+
if (!stat.isFile()) return { error: `File '${filePath}' not found` };
|
|
4545
|
+
if (isBinary) {
|
|
4546
|
+
const buffer = await node_fs_promises.default.readFile(resolvedPath);
|
|
4547
|
+
return {
|
|
4548
|
+
content: new Uint8Array(buffer),
|
|
4549
|
+
mimeType
|
|
4550
|
+
};
|
|
4551
|
+
}
|
|
3493
4552
|
content = await node_fs_promises.default.readFile(resolvedPath, "utf-8");
|
|
3494
4553
|
}
|
|
3495
4554
|
const emptyMsg = checkEmptyContent(content);
|
|
3496
|
-
if (emptyMsg) return
|
|
4555
|
+
if (emptyMsg) return {
|
|
4556
|
+
content: emptyMsg,
|
|
4557
|
+
mimeType
|
|
4558
|
+
};
|
|
3497
4559
|
const lines = content.split("\n");
|
|
3498
4560
|
const startIdx = offset;
|
|
3499
4561
|
const endIdx = Math.min(startIdx + limit, lines.length);
|
|
3500
|
-
if (startIdx >= lines.length) return
|
|
3501
|
-
return
|
|
4562
|
+
if (startIdx >= lines.length) return { error: `Line offset ${offset} exceeds file length (${lines.length} lines)` };
|
|
4563
|
+
return {
|
|
4564
|
+
content: lines.slice(startIdx, endIdx).join("\n"),
|
|
4565
|
+
mimeType
|
|
4566
|
+
};
|
|
3502
4567
|
} catch (e) {
|
|
3503
|
-
return `Error reading file '${filePath}': ${e.message}
|
|
4568
|
+
return { error: `Error reading file '${filePath}': ${e.message}` };
|
|
3504
4569
|
}
|
|
3505
4570
|
}
|
|
3506
4571
|
/**
|
|
3507
4572
|
* Read file content as raw FileData.
|
|
3508
4573
|
*
|
|
3509
4574
|
* @param filePath - Absolute file path
|
|
3510
|
-
* @returns
|
|
4575
|
+
* @returns ReadRawResult with raw file data on success or error on failure
|
|
3511
4576
|
*/
|
|
3512
4577
|
async readRaw(filePath) {
|
|
3513
4578
|
const resolvedPath = this.resolvePath(filePath);
|
|
4579
|
+
const mimeType = getMimeType(filePath);
|
|
4580
|
+
const isBinary = !isTextMimeType(mimeType);
|
|
3514
4581
|
let content;
|
|
3515
4582
|
let stat;
|
|
3516
4583
|
if (SUPPORTS_NOFOLLOW) {
|
|
3517
4584
|
stat = await node_fs_promises.default.stat(resolvedPath);
|
|
3518
|
-
if (!stat.isFile())
|
|
4585
|
+
if (!stat.isFile()) return { error: `File '${filePath}' not found` };
|
|
3519
4586
|
const fd = await node_fs_promises.default.open(resolvedPath, node_fs.default.constants.O_RDONLY | node_fs.default.constants.O_NOFOLLOW);
|
|
3520
4587
|
try {
|
|
4588
|
+
if (isBinary) {
|
|
4589
|
+
const buffer = await fd.readFile();
|
|
4590
|
+
return { data: {
|
|
4591
|
+
content: new Uint8Array(buffer),
|
|
4592
|
+
mimeType,
|
|
4593
|
+
created_at: stat.ctime.toISOString(),
|
|
4594
|
+
modified_at: stat.mtime.toISOString()
|
|
4595
|
+
} };
|
|
4596
|
+
}
|
|
3521
4597
|
content = await fd.readFile({ encoding: "utf-8" });
|
|
3522
4598
|
} finally {
|
|
3523
4599
|
await fd.close();
|
|
3524
4600
|
}
|
|
3525
4601
|
} else {
|
|
3526
4602
|
stat = await node_fs_promises.default.lstat(resolvedPath);
|
|
3527
|
-
if (stat.isSymbolicLink())
|
|
3528
|
-
if (!stat.isFile())
|
|
4603
|
+
if (stat.isSymbolicLink()) return { error: `Symlinks are not allowed: ${filePath}` };
|
|
4604
|
+
if (!stat.isFile()) return { error: `File '${filePath}' not found` };
|
|
4605
|
+
if (isBinary) {
|
|
4606
|
+
const buffer = await node_fs_promises.default.readFile(resolvedPath);
|
|
4607
|
+
return { data: {
|
|
4608
|
+
content: new Uint8Array(buffer),
|
|
4609
|
+
mimeType,
|
|
4610
|
+
created_at: stat.ctime.toISOString(),
|
|
4611
|
+
modified_at: stat.mtime.toISOString()
|
|
4612
|
+
} };
|
|
4613
|
+
}
|
|
3529
4614
|
content = await node_fs_promises.default.readFile(resolvedPath, "utf-8");
|
|
3530
4615
|
}
|
|
3531
|
-
return {
|
|
3532
|
-
content
|
|
4616
|
+
return { data: {
|
|
4617
|
+
content,
|
|
4618
|
+
mimeType,
|
|
3533
4619
|
created_at: stat.ctime.toISOString(),
|
|
3534
4620
|
modified_at: stat.mtime.toISOString()
|
|
3535
|
-
};
|
|
4621
|
+
} };
|
|
3536
4622
|
}
|
|
3537
4623
|
/**
|
|
3538
4624
|
* Create a new file with content.
|
|
@@ -3541,6 +4627,7 @@ var FilesystemBackend = class {
|
|
|
3541
4627
|
async write(filePath, content) {
|
|
3542
4628
|
try {
|
|
3543
4629
|
const resolvedPath = this.resolvePath(filePath);
|
|
4630
|
+
const isBinary = !isTextMimeType(getMimeType(filePath));
|
|
3544
4631
|
try {
|
|
3545
4632
|
if ((await node_fs_promises.default.lstat(resolvedPath)).isSymbolicLink()) return { error: `Cannot write to ${filePath} because it is a symlink. Symlinks are not allowed.` };
|
|
3546
4633
|
return { error: `Cannot write to ${filePath} because it already exists. Read and then make an edit, or write to a new path.` };
|
|
@@ -3550,10 +4637,16 @@ var FilesystemBackend = class {
|
|
|
3550
4637
|
const flags = node_fs.default.constants.O_WRONLY | node_fs.default.constants.O_CREAT | node_fs.default.constants.O_TRUNC | node_fs.default.constants.O_NOFOLLOW;
|
|
3551
4638
|
const fd = await node_fs_promises.default.open(resolvedPath, flags, 420);
|
|
3552
4639
|
try {
|
|
3553
|
-
|
|
4640
|
+
if (isBinary) {
|
|
4641
|
+
const buffer = Buffer.from(content, "base64");
|
|
4642
|
+
await fd.writeFile(buffer);
|
|
4643
|
+
} else await fd.writeFile(content, "utf-8");
|
|
3554
4644
|
} finally {
|
|
3555
4645
|
await fd.close();
|
|
3556
4646
|
}
|
|
4647
|
+
} else if (isBinary) {
|
|
4648
|
+
const buffer = Buffer.from(content, "base64");
|
|
4649
|
+
await node_fs_promises.default.writeFile(resolvedPath, buffer);
|
|
3557
4650
|
} else await node_fs_promises.default.writeFile(resolvedPath, content, "utf-8");
|
|
3558
4651
|
return {
|
|
3559
4652
|
path: filePath,
|
|
@@ -3616,17 +4709,17 @@ var FilesystemBackend = class {
|
|
|
3616
4709
|
* @param glob - Optional glob pattern to filter which files to search.
|
|
3617
4710
|
* @returns List of GrepMatch dicts containing path, line number, and matched text.
|
|
3618
4711
|
*/
|
|
3619
|
-
async
|
|
4712
|
+
async grep(pattern, dirPath = "/", glob = null) {
|
|
3620
4713
|
let baseFull;
|
|
3621
4714
|
try {
|
|
3622
4715
|
baseFull = this.resolvePath(dirPath || ".");
|
|
3623
4716
|
} catch {
|
|
3624
|
-
return [];
|
|
4717
|
+
return { matches: [] };
|
|
3625
4718
|
}
|
|
3626
4719
|
try {
|
|
3627
4720
|
await node_fs_promises.default.stat(baseFull);
|
|
3628
4721
|
} catch {
|
|
3629
|
-
return [];
|
|
4722
|
+
return { matches: [] };
|
|
3630
4723
|
}
|
|
3631
4724
|
let results = await this.ripgrepSearch(pattern, baseFull, glob);
|
|
3632
4725
|
if (results === null) results = await this.literalSearch(pattern, baseFull, glob);
|
|
@@ -3636,7 +4729,7 @@ var FilesystemBackend = class {
|
|
|
3636
4729
|
line: lineNum,
|
|
3637
4730
|
text: lineText
|
|
3638
4731
|
});
|
|
3639
|
-
return matches;
|
|
4732
|
+
return { matches };
|
|
3640
4733
|
}
|
|
3641
4734
|
/**
|
|
3642
4735
|
* Search using ripgrep with fixed-string (literal) mode.
|
|
@@ -3716,6 +4809,7 @@ var FilesystemBackend = class {
|
|
|
3716
4809
|
dot: true
|
|
3717
4810
|
});
|
|
3718
4811
|
for (const fp of files) try {
|
|
4812
|
+
if (!isTextMimeType(getMimeType(fp))) continue;
|
|
3719
4813
|
if (includeGlob && !micromatch.default.isMatch(node_path.default.basename(fp), includeGlob)) continue;
|
|
3720
4814
|
if ((await node_fs_promises.default.stat(fp)).size > this.maxFileSizeBytes) continue;
|
|
3721
4815
|
const lines = (await node_fs_promises.default.readFile(fp, "utf-8")).split("\n");
|
|
@@ -3743,13 +4837,13 @@ var FilesystemBackend = class {
|
|
|
3743
4837
|
/**
|
|
3744
4838
|
* Structured glob matching returning FileInfo objects.
|
|
3745
4839
|
*/
|
|
3746
|
-
async
|
|
4840
|
+
async glob(pattern, searchPath = "/") {
|
|
3747
4841
|
if (pattern.startsWith("/")) pattern = pattern.substring(1);
|
|
3748
4842
|
const resolvedSearchPath = searchPath === "/" ? this.cwd : this.resolvePath(searchPath);
|
|
3749
4843
|
try {
|
|
3750
|
-
if (!(await node_fs_promises.default.stat(resolvedSearchPath)).isDirectory()) return [];
|
|
4844
|
+
if (!(await node_fs_promises.default.stat(resolvedSearchPath)).isDirectory()) return { files: [] };
|
|
3751
4845
|
} catch {
|
|
3752
|
-
return [];
|
|
4846
|
+
return { files: [] };
|
|
3753
4847
|
}
|
|
3754
4848
|
const results = [];
|
|
3755
4849
|
try {
|
|
@@ -3789,7 +4883,7 @@ var FilesystemBackend = class {
|
|
|
3789
4883
|
}
|
|
3790
4884
|
} catch {}
|
|
3791
4885
|
results.sort((a, b) => a.path.localeCompare(b.path));
|
|
3792
|
-
return results;
|
|
4886
|
+
return { files: results };
|
|
3793
4887
|
}
|
|
3794
4888
|
/**
|
|
3795
4889
|
* Upload multiple files to the filesystem.
|
|
@@ -3884,9 +4978,9 @@ var CompositeBackend = class {
|
|
|
3884
4978
|
routes;
|
|
3885
4979
|
sortedRoutes;
|
|
3886
4980
|
constructor(defaultBackend, routes) {
|
|
3887
|
-
this.default = defaultBackend;
|
|
3888
|
-
this.routes = routes;
|
|
3889
|
-
this.sortedRoutes = Object.entries(routes).sort((a, b) => b[0].length - a[0].length);
|
|
4981
|
+
this.default = isSandboxProtocol(defaultBackend) ? adaptSandboxProtocol(defaultBackend) : adaptBackendProtocol(defaultBackend);
|
|
4982
|
+
this.routes = Object.fromEntries(Object.entries(routes).map(([k, v]) => [k, isSandboxProtocol(v) ? adaptSandboxProtocol(v) : adaptBackendProtocol(v)]));
|
|
4983
|
+
this.sortedRoutes = Object.entries(this.routes).sort((a, b) => b[0].length - a[0].length);
|
|
3890
4984
|
}
|
|
3891
4985
|
/** Delegates to default backend's id if it is a sandbox, otherwise empty string. */
|
|
3892
4986
|
get id() {
|
|
@@ -3910,25 +5004,27 @@ var CompositeBackend = class {
|
|
|
3910
5004
|
* List files and directories in the specified directory (non-recursive).
|
|
3911
5005
|
*
|
|
3912
5006
|
* @param path - Absolute path to directory
|
|
3913
|
-
* @returns
|
|
3914
|
-
*
|
|
5007
|
+
* @returns LsResult with list of FileInfo objects (with route prefixes added) on success or error on failure.
|
|
5008
|
+
* Directories have a trailing / in their path and is_dir=true.
|
|
3915
5009
|
*/
|
|
3916
|
-
async
|
|
5010
|
+
async ls(path) {
|
|
3917
5011
|
for (const [routePrefix, backend] of this.sortedRoutes) if (path.startsWith(routePrefix.replace(/\/$/, ""))) {
|
|
3918
5012
|
const suffix = path.substring(routePrefix.length);
|
|
3919
5013
|
const searchPath = suffix ? "/" + suffix : "/";
|
|
3920
|
-
const
|
|
5014
|
+
const result = await backend.ls(searchPath);
|
|
5015
|
+
if (result.error) return result;
|
|
3921
5016
|
const prefixed = [];
|
|
3922
|
-
for (const fi of
|
|
5017
|
+
for (const fi of result.files || []) prefixed.push({
|
|
3923
5018
|
...fi,
|
|
3924
5019
|
path: routePrefix.slice(0, -1) + fi.path
|
|
3925
5020
|
});
|
|
3926
|
-
return prefixed;
|
|
5021
|
+
return { files: prefixed };
|
|
3927
5022
|
}
|
|
3928
5023
|
if (path === "/") {
|
|
3929
5024
|
const results = [];
|
|
3930
|
-
const
|
|
3931
|
-
|
|
5025
|
+
const defaultResult = await this.default.ls(path);
|
|
5026
|
+
if (defaultResult.error) return defaultResult;
|
|
5027
|
+
results.push(...defaultResult.files || []);
|
|
3932
5028
|
for (const [routePrefix] of this.sortedRoutes) results.push({
|
|
3933
5029
|
path: routePrefix,
|
|
3934
5030
|
is_dir: true,
|
|
@@ -3936,9 +5032,9 @@ var CompositeBackend = class {
|
|
|
3936
5032
|
modified_at: ""
|
|
3937
5033
|
});
|
|
3938
5034
|
results.sort((a, b) => a.path.localeCompare(b.path));
|
|
3939
|
-
return results;
|
|
5035
|
+
return { files: results };
|
|
3940
5036
|
}
|
|
3941
|
-
return await this.default.
|
|
5037
|
+
return await this.default.ls(path);
|
|
3942
5038
|
}
|
|
3943
5039
|
/**
|
|
3944
5040
|
* Read file content, routing to appropriate backend.
|
|
@@ -3956,7 +5052,7 @@ var CompositeBackend = class {
|
|
|
3956
5052
|
* Read file content as raw FileData.
|
|
3957
5053
|
*
|
|
3958
5054
|
* @param filePath - Absolute file path
|
|
3959
|
-
* @returns
|
|
5055
|
+
* @returns ReadRawResult with raw file data on success or error on failure
|
|
3960
5056
|
*/
|
|
3961
5057
|
async readRaw(filePath) {
|
|
3962
5058
|
const [backend, strippedKey] = this.getBackendAndKey(filePath);
|
|
@@ -3965,53 +5061,59 @@ var CompositeBackend = class {
|
|
|
3965
5061
|
/**
|
|
3966
5062
|
* Structured search results or error string for invalid input.
|
|
3967
5063
|
*/
|
|
3968
|
-
async
|
|
5064
|
+
async grep(pattern, path = "/", glob = null) {
|
|
3969
5065
|
for (const [routePrefix, backend] of this.sortedRoutes) if (path.startsWith(routePrefix.replace(/\/$/, ""))) {
|
|
3970
5066
|
const searchPath = path.substring(routePrefix.length - 1);
|
|
3971
|
-
const raw = await backend.
|
|
3972
|
-
if (
|
|
3973
|
-
return raw.map((m) => ({
|
|
5067
|
+
const raw = await backend.grep(pattern, searchPath || "/", glob);
|
|
5068
|
+
if (raw.error) return raw;
|
|
5069
|
+
return { matches: (raw.matches || []).map((m) => ({
|
|
3974
5070
|
...m,
|
|
3975
5071
|
path: routePrefix.slice(0, -1) + m.path
|
|
3976
|
-
}));
|
|
5072
|
+
})) };
|
|
3977
5073
|
}
|
|
3978
5074
|
const allMatches = [];
|
|
3979
|
-
const rawDefault = await this.default.
|
|
3980
|
-
if (
|
|
3981
|
-
allMatches.push(...rawDefault);
|
|
5075
|
+
const rawDefault = await this.default.grep(pattern, path, glob);
|
|
5076
|
+
if (rawDefault.error) return rawDefault;
|
|
5077
|
+
allMatches.push(...rawDefault.matches || []);
|
|
3982
5078
|
for (const [routePrefix, backend] of Object.entries(this.routes)) {
|
|
3983
|
-
const raw = await backend.
|
|
3984
|
-
if (
|
|
3985
|
-
|
|
5079
|
+
const raw = await backend.grep(pattern, "/", glob);
|
|
5080
|
+
if (raw.error) return raw;
|
|
5081
|
+
const matches = (raw.matches || []).map((m) => ({
|
|
3986
5082
|
...m,
|
|
3987
5083
|
path: routePrefix.slice(0, -1) + m.path
|
|
3988
|
-
}))
|
|
5084
|
+
}));
|
|
5085
|
+
allMatches.push(...matches);
|
|
3989
5086
|
}
|
|
3990
|
-
return allMatches;
|
|
5087
|
+
return { matches: allMatches };
|
|
3991
5088
|
}
|
|
3992
5089
|
/**
|
|
3993
5090
|
* Structured glob matching returning FileInfo objects.
|
|
3994
5091
|
*/
|
|
3995
|
-
async
|
|
5092
|
+
async glob(pattern, path = "/") {
|
|
3996
5093
|
const results = [];
|
|
3997
5094
|
for (const [routePrefix, backend] of this.sortedRoutes) if (path.startsWith(routePrefix.replace(/\/$/, ""))) {
|
|
3998
5095
|
const searchPath = path.substring(routePrefix.length - 1);
|
|
3999
|
-
|
|
5096
|
+
const result = await backend.glob(pattern, searchPath || "/");
|
|
5097
|
+
if (result.error) return result;
|
|
5098
|
+
return { files: (result.files || []).map((fi) => ({
|
|
4000
5099
|
...fi,
|
|
4001
5100
|
path: routePrefix.slice(0, -1) + fi.path
|
|
4002
|
-
}));
|
|
5101
|
+
})) };
|
|
4003
5102
|
}
|
|
4004
|
-
const
|
|
4005
|
-
|
|
5103
|
+
const defaultResult = await this.default.glob(pattern, path);
|
|
5104
|
+
if (defaultResult.error) return defaultResult;
|
|
5105
|
+
results.push(...defaultResult.files || []);
|
|
4006
5106
|
for (const [routePrefix, backend] of Object.entries(this.routes)) {
|
|
4007
|
-
const
|
|
4008
|
-
|
|
5107
|
+
const result = await backend.glob(pattern, "/");
|
|
5108
|
+
if (result.error) continue;
|
|
5109
|
+
const files = (result.files || []).map((fi) => ({
|
|
4009
5110
|
...fi,
|
|
4010
5111
|
path: routePrefix.slice(0, -1) + fi.path
|
|
4011
|
-
}))
|
|
5112
|
+
}));
|
|
5113
|
+
results.push(...files);
|
|
4012
5114
|
}
|
|
4013
5115
|
results.sort((a, b) => a.path.localeCompare(b.path));
|
|
4014
|
-
return results;
|
|
5116
|
+
return { files: results };
|
|
4015
5117
|
}
|
|
4016
5118
|
/**
|
|
4017
5119
|
* Create a new file, routing to appropriate backend.
|
|
@@ -4239,7 +5341,7 @@ var LocalShellBackend = class LocalShellBackend extends FilesystemBackend {
|
|
|
4239
5341
|
*/
|
|
4240
5342
|
async read(filePath, offset = 0, limit = 500) {
|
|
4241
5343
|
const result = await super.read(filePath, offset, limit);
|
|
4242
|
-
if (
|
|
5344
|
+
if (result.error?.includes("ENOENT")) return { error: `File '${filePath}' not found` };
|
|
4243
5345
|
return result;
|
|
4244
5346
|
}
|
|
4245
5347
|
/**
|
|
@@ -4256,25 +5358,26 @@ var LocalShellBackend = class LocalShellBackend extends FilesystemBackend {
|
|
|
4256
5358
|
/**
|
|
4257
5359
|
* List directory contents, returning paths relative to rootDir.
|
|
4258
5360
|
*/
|
|
4259
|
-
async
|
|
4260
|
-
const
|
|
4261
|
-
if (
|
|
5361
|
+
async ls(dirPath) {
|
|
5362
|
+
const result = await super.ls(dirPath);
|
|
5363
|
+
if (result.error) return result;
|
|
5364
|
+
if (this.virtualMode) return result;
|
|
4262
5365
|
const cwdPrefix = this.cwd.endsWith(node_path.default.sep) ? this.cwd : this.cwd + node_path.default.sep;
|
|
4263
|
-
return
|
|
5366
|
+
return { files: (result.files || []).map((info) => ({
|
|
4264
5367
|
...info,
|
|
4265
5368
|
path: info.path.startsWith(cwdPrefix) ? info.path.slice(cwdPrefix.length) : info.path
|
|
4266
|
-
}));
|
|
5369
|
+
})) };
|
|
4267
5370
|
}
|
|
4268
5371
|
/**
|
|
4269
5372
|
* Glob matching that returns relative paths and includes directories.
|
|
4270
5373
|
*/
|
|
4271
|
-
async
|
|
5374
|
+
async glob(pattern, searchPath = "/") {
|
|
4272
5375
|
if (pattern.startsWith("/")) pattern = pattern.substring(1);
|
|
4273
5376
|
const resolvedSearchPath = searchPath === "/" || searchPath === "" ? this.cwd : this.virtualMode ? node_path.default.resolve(this.cwd, searchPath.replace(/^\//, "")) : node_path.default.resolve(this.cwd, searchPath);
|
|
4274
5377
|
try {
|
|
4275
|
-
if (!(await node_fs_promises.default.stat(resolvedSearchPath)).isDirectory()) return [];
|
|
5378
|
+
if (!(await node_fs_promises.default.stat(resolvedSearchPath)).isDirectory()) return { files: [] };
|
|
4276
5379
|
} catch {
|
|
4277
|
-
return [];
|
|
5380
|
+
return { files: [] };
|
|
4278
5381
|
}
|
|
4279
5382
|
const formatPath = (rel) => this.virtualMode ? `/${rel}` : rel;
|
|
4280
5383
|
const globOpts = {
|
|
@@ -4316,7 +5419,7 @@ var LocalShellBackend = class LocalShellBackend extends FilesystemBackend {
|
|
|
4316
5419
|
const [fileInfos, dirInfos] = await Promise.all([Promise.all(fileMatches.map(statFile)), Promise.all(dirMatches.map(statDir))]);
|
|
4317
5420
|
const results = [...fileInfos, ...dirInfos].filter((info) => info !== null);
|
|
4318
5421
|
results.sort((a, b) => a.path.localeCompare(b.path));
|
|
4319
|
-
return results;
|
|
5422
|
+
return { files: results };
|
|
4320
5423
|
}
|
|
4321
5424
|
/**
|
|
4322
5425
|
* Execute a shell command directly on the host system.
|
|
@@ -4591,9 +5694,9 @@ var BaseSandbox = class {
|
|
|
4591
5694
|
* including Alpine. No Python or Node.js needed.
|
|
4592
5695
|
*
|
|
4593
5696
|
* @param path - Absolute path to directory
|
|
4594
|
-
* @returns
|
|
5697
|
+
* @returns LsResult with list of FileInfo objects on success or error on failure.
|
|
4595
5698
|
*/
|
|
4596
|
-
async
|
|
5699
|
+
async ls(path) {
|
|
4597
5700
|
const command = buildLsCommand(path);
|
|
4598
5701
|
const result = await this.execute(command);
|
|
4599
5702
|
const infos = [];
|
|
@@ -4608,7 +5711,7 @@ var BaseSandbox = class {
|
|
|
4608
5711
|
modified_at: (/* @__PURE__ */ new Date(parsed.mtime * 1e3)).toISOString()
|
|
4609
5712
|
});
|
|
4610
5713
|
}
|
|
4611
|
-
return infos;
|
|
5714
|
+
return { files: infos };
|
|
4612
5715
|
}
|
|
4613
5716
|
/**
|
|
4614
5717
|
* Read file content with line numbers.
|
|
@@ -4623,11 +5726,26 @@ var BaseSandbox = class {
|
|
|
4623
5726
|
* @returns Formatted file content with line numbers, or error message
|
|
4624
5727
|
*/
|
|
4625
5728
|
async read(filePath, offset = 0, limit = 500) {
|
|
4626
|
-
|
|
5729
|
+
const mimeType = getMimeType(filePath);
|
|
5730
|
+
if (!isTextMimeType(mimeType)) {
|
|
5731
|
+
const results = await this.downloadFiles([filePath]);
|
|
5732
|
+
if (results[0].error || !results[0].content) return { error: `File '${filePath}' not found` };
|
|
5733
|
+
return {
|
|
5734
|
+
content: results[0].content,
|
|
5735
|
+
mimeType
|
|
5736
|
+
};
|
|
5737
|
+
}
|
|
5738
|
+
if (limit === 0) return {
|
|
5739
|
+
content: "",
|
|
5740
|
+
mimeType
|
|
5741
|
+
};
|
|
4627
5742
|
const command = buildReadCommand(filePath, offset, limit);
|
|
4628
5743
|
const result = await this.execute(command);
|
|
4629
|
-
if (result.exitCode !== 0) return
|
|
4630
|
-
return
|
|
5744
|
+
if (result.exitCode !== 0) return { error: `File '${filePath}' not found` };
|
|
5745
|
+
return {
|
|
5746
|
+
content: result.output,
|
|
5747
|
+
mimeType
|
|
5748
|
+
};
|
|
4631
5749
|
}
|
|
4632
5750
|
/**
|
|
4633
5751
|
* Read file content as raw FileData.
|
|
@@ -4635,18 +5753,25 @@ var BaseSandbox = class {
|
|
|
4635
5753
|
* Uses downloadFiles() directly — no runtime needed on the sandbox host.
|
|
4636
5754
|
*
|
|
4637
5755
|
* @param filePath - Absolute file path
|
|
4638
|
-
* @returns
|
|
5756
|
+
* @returns ReadRawResult with raw file data on success or error on failure
|
|
4639
5757
|
*/
|
|
4640
5758
|
async readRaw(filePath) {
|
|
4641
5759
|
const results = await this.downloadFiles([filePath]);
|
|
4642
|
-
if (results[0].error || !results[0].content)
|
|
4643
|
-
const lines = new TextDecoder().decode(results[0].content).split("\n");
|
|
5760
|
+
if (results[0].error || !results[0].content) return { error: `File '${filePath}' not found` };
|
|
4644
5761
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4645
|
-
|
|
4646
|
-
|
|
5762
|
+
const mimeType = getMimeType(filePath);
|
|
5763
|
+
if (!isTextMimeType(mimeType)) return { data: {
|
|
5764
|
+
content: results[0].content,
|
|
5765
|
+
mimeType,
|
|
4647
5766
|
created_at: now,
|
|
4648
5767
|
modified_at: now
|
|
4649
|
-
};
|
|
5768
|
+
} };
|
|
5769
|
+
return { data: {
|
|
5770
|
+
content: new TextDecoder().decode(results[0].content),
|
|
5771
|
+
mimeType,
|
|
5772
|
+
created_at: now,
|
|
5773
|
+
modified_at: now
|
|
5774
|
+
} };
|
|
4650
5775
|
}
|
|
4651
5776
|
/**
|
|
4652
5777
|
* Search for a literal text pattern in files using grep.
|
|
@@ -4656,23 +5781,25 @@ var BaseSandbox = class {
|
|
|
4656
5781
|
* @param glob - Optional glob pattern to filter which files to search.
|
|
4657
5782
|
* @returns List of GrepMatch dicts containing path, line number, and matched text.
|
|
4658
5783
|
*/
|
|
4659
|
-
async
|
|
5784
|
+
async grep(pattern, path = "/", glob = null) {
|
|
4660
5785
|
const command = buildGrepCommand(pattern, path, glob);
|
|
4661
5786
|
const output = (await this.execute(command)).output.trim();
|
|
4662
|
-
if (!output) return [];
|
|
5787
|
+
if (!output) return { matches: [] };
|
|
4663
5788
|
const matches = [];
|
|
4664
5789
|
for (const line of output.split("\n")) {
|
|
4665
5790
|
const parts = line.split(":");
|
|
4666
5791
|
if (parts.length >= 3) {
|
|
5792
|
+
const filePath = parts[0];
|
|
5793
|
+
if (!isTextMimeType(getMimeType(filePath))) continue;
|
|
4667
5794
|
const lineNum = parseInt(parts[1], 10);
|
|
4668
5795
|
if (!isNaN(lineNum)) matches.push({
|
|
4669
|
-
path:
|
|
5796
|
+
path: filePath,
|
|
4670
5797
|
line: lineNum,
|
|
4671
5798
|
text: parts.slice(2).join(":")
|
|
4672
5799
|
});
|
|
4673
5800
|
}
|
|
4674
5801
|
}
|
|
4675
|
-
return matches;
|
|
5802
|
+
return { matches };
|
|
4676
5803
|
}
|
|
4677
5804
|
/**
|
|
4678
5805
|
* Structured glob matching returning FileInfo objects.
|
|
@@ -4687,7 +5814,7 @@ var BaseSandbox = class {
|
|
|
4687
5814
|
* - `?` matches a single character except `/`
|
|
4688
5815
|
* - `[...]` character classes
|
|
4689
5816
|
*/
|
|
4690
|
-
async
|
|
5817
|
+
async glob(pattern, path = "/") {
|
|
4691
5818
|
const command = buildFindCommand(path);
|
|
4692
5819
|
const result = await this.execute(command);
|
|
4693
5820
|
const regex = globToPathRegex(pattern);
|
|
@@ -4705,7 +5832,7 @@ var BaseSandbox = class {
|
|
|
4705
5832
|
modified_at: (/* @__PURE__ */ new Date(parsed.mtime * 1e3)).toISOString()
|
|
4706
5833
|
});
|
|
4707
5834
|
}
|
|
4708
|
-
return infos;
|
|
5835
|
+
return { files: infos };
|
|
4709
5836
|
}
|
|
4710
5837
|
/**
|
|
4711
5838
|
* Create a new file with content.
|
|
@@ -4718,8 +5845,11 @@ var BaseSandbox = class {
|
|
|
4718
5845
|
const existCheck = await this.downloadFiles([filePath]);
|
|
4719
5846
|
if (existCheck[0].content !== null && existCheck[0].error === null) return { error: `Cannot write to ${filePath} because it already exists. Read and then make an edit, or write to a new path.` };
|
|
4720
5847
|
} catch {}
|
|
4721
|
-
const
|
|
4722
|
-
|
|
5848
|
+
const mimeType = getMimeType(filePath);
|
|
5849
|
+
let fileContent;
|
|
5850
|
+
if (isTextMimeType(mimeType)) fileContent = new TextEncoder().encode(content);
|
|
5851
|
+
else fileContent = Buffer.from(content, "base64");
|
|
5852
|
+
const results = await this.uploadFiles([[filePath, fileContent]]);
|
|
4723
5853
|
if (results[0].error) return { error: `Failed to write to ${filePath}: ${results[0].error}` };
|
|
4724
5854
|
return {
|
|
4725
5855
|
path: filePath,
|
|
@@ -5142,13 +6272,19 @@ function createDeepAgent(params = {}) {
|
|
|
5142
6272
|
addCacheControl: anthropicModel
|
|
5143
6273
|
})] : [];
|
|
5144
6274
|
/**
|
|
6275
|
+
* Split the unified subagents array into sync and async subagents.
|
|
6276
|
+
* AsyncSubAgents are identified by the presence of a `graphId` field.
|
|
6277
|
+
*/
|
|
6278
|
+
const syncSubAgents = subagents.filter((a) => !isAsyncSubAgent(a));
|
|
6279
|
+
const asyncSubAgents = subagents.filter((a) => isAsyncSubAgent(a));
|
|
6280
|
+
/**
|
|
5145
6281
|
* Process subagents to add SkillsMiddleware for those with their own skills.
|
|
5146
6282
|
*
|
|
5147
6283
|
* Custom subagents do NOT inherit skills from the main agent by default.
|
|
5148
6284
|
* Only the general-purpose subagent inherits the main agent's skills (via defaultMiddleware).
|
|
5149
6285
|
* If a custom subagent needs skills, it must specify its own `skills` array.
|
|
5150
6286
|
*/
|
|
5151
|
-
const processedSubagents =
|
|
6287
|
+
const processedSubagents = syncSubAgents.map((subagent) => {
|
|
5152
6288
|
/**
|
|
5153
6289
|
* CompiledSubAgent - use as-is (already has its own middleware baked in)
|
|
5154
6290
|
*/
|
|
@@ -5239,7 +6375,8 @@ function createDeepAgent(params = {}) {
|
|
|
5239
6375
|
minMessagesToCache: 1
|
|
5240
6376
|
}), createCacheBreakpointMiddleware()] : [],
|
|
5241
6377
|
...memoryMiddlewareArray,
|
|
5242
|
-
...interruptOn ? [(0, langchain.humanInTheLoopMiddleware)({ interruptOn })] : []
|
|
6378
|
+
...interruptOn ? [(0, langchain.humanInTheLoopMiddleware)({ interruptOn })] : [],
|
|
6379
|
+
...asyncSubAgents && asyncSubAgents.length > 0 ? [createAsyncSubAgentMiddleware({ asyncSubAgents })] : []
|
|
5243
6380
|
],
|
|
5244
6381
|
...responseFormat != null && { responseFormat },
|
|
5245
6382
|
contextSchema,
|
|
@@ -5778,8 +6915,12 @@ exports.SandboxError = SandboxError;
|
|
|
5778
6915
|
exports.StateBackend = StateBackend;
|
|
5779
6916
|
exports.StoreBackend = StoreBackend;
|
|
5780
6917
|
exports.TASK_SYSTEM_PROMPT = TASK_SYSTEM_PROMPT;
|
|
6918
|
+
exports.adaptBackendProtocol = adaptBackendProtocol;
|
|
6919
|
+
exports.adaptSandboxProtocol = adaptSandboxProtocol;
|
|
5781
6920
|
exports.computeSummarizationDefaults = computeSummarizationDefaults;
|
|
5782
6921
|
exports.createAgentMemoryMiddleware = createAgentMemoryMiddleware;
|
|
6922
|
+
exports.createAsyncSubAgentMiddleware = createAsyncSubAgentMiddleware;
|
|
6923
|
+
exports.createCompletionNotifierMiddleware = createCompletionNotifierMiddleware;
|
|
5783
6924
|
exports.createDeepAgent = createDeepAgent;
|
|
5784
6925
|
exports.createFilesystemMiddleware = createFilesystemMiddleware;
|
|
5785
6926
|
exports.createMemoryMiddleware = createMemoryMiddleware;
|
|
@@ -5790,7 +6931,9 @@ exports.createSubAgentMiddleware = createSubAgentMiddleware;
|
|
|
5790
6931
|
exports.createSummarizationMiddleware = createSummarizationMiddleware;
|
|
5791
6932
|
exports.filesValue = filesValue;
|
|
5792
6933
|
exports.findProjectRoot = findProjectRoot;
|
|
6934
|
+
exports.isAsyncSubAgent = isAsyncSubAgent;
|
|
5793
6935
|
exports.isSandboxBackend = isSandboxBackend;
|
|
6936
|
+
exports.isSandboxProtocol = isSandboxProtocol;
|
|
5794
6937
|
exports.listSkills = listSkills;
|
|
5795
6938
|
exports.parseSkillMetadata = parseSkillMetadata;
|
|
5796
6939
|
|