deepagents 1.8.8 → 1.9.0-alpha.1
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 +2155 -831
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1771 -1241
- package/dist/index.d.ts +1772 -1240
- package/dist/index.js +2196 -879
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/dist/index.cjs
CHANGED
|
@@ -22,16 +22,19 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
22
|
}) : target, mod));
|
|
23
23
|
//#endregion
|
|
24
24
|
let langchain = require("langchain");
|
|
25
|
-
let
|
|
25
|
+
let _langchain_anthropic = require("@langchain/anthropic");
|
|
26
26
|
let _langchain_langgraph = require("@langchain/langgraph");
|
|
27
27
|
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");
|
|
34
|
+
zod = __toESM(zod);
|
|
33
35
|
let yaml = require("yaml");
|
|
34
36
|
yaml = __toESM(yaml);
|
|
37
|
+
let _langchain_langgraph_sdk = require("@langchain/langgraph-sdk");
|
|
35
38
|
let _langchain_core_errors = require("@langchain/core/errors");
|
|
36
39
|
let langchain_chat_models_universal = require("langchain/chat_models/universal");
|
|
37
40
|
let node_fs_promises = require("node:fs/promises");
|
|
@@ -47,77 +50,6 @@ fast_glob = __toESM(fast_glob);
|
|
|
47
50
|
let langsmith_experimental_sandbox = require("langsmith/experimental/sandbox");
|
|
48
51
|
let node_os = require("node:os");
|
|
49
52
|
node_os = __toESM(node_os);
|
|
50
|
-
//#region src/backends/protocol.ts
|
|
51
|
-
/**
|
|
52
|
-
* Type guard to check if a backend supports execution.
|
|
53
|
-
*
|
|
54
|
-
* @param backend - Backend instance to check
|
|
55
|
-
* @returns True if the backend implements SandboxBackendProtocol
|
|
56
|
-
*/
|
|
57
|
-
function isSandboxBackend(backend) {
|
|
58
|
-
return typeof backend.execute === "function" && typeof backend.id === "string" && backend.id !== "";
|
|
59
|
-
}
|
|
60
|
-
const SANDBOX_ERROR_SYMBOL = Symbol.for("sandbox.error");
|
|
61
|
-
/**
|
|
62
|
-
* Custom error class for sandbox operations.
|
|
63
|
-
*
|
|
64
|
-
* @param message - Human-readable error description
|
|
65
|
-
* @param code - Structured error code for programmatic handling
|
|
66
|
-
* @returns SandboxError with message and code
|
|
67
|
-
*
|
|
68
|
-
* @example
|
|
69
|
-
* ```typescript
|
|
70
|
-
* try {
|
|
71
|
-
* await sandbox.execute("some command");
|
|
72
|
-
* } catch (error) {
|
|
73
|
-
* if (error instanceof SandboxError) {
|
|
74
|
-
* switch (error.code) {
|
|
75
|
-
* case "NOT_INITIALIZED":
|
|
76
|
-
* await sandbox.initialize();
|
|
77
|
-
* break;
|
|
78
|
-
* case "COMMAND_TIMEOUT":
|
|
79
|
-
* console.error("Command took too long");
|
|
80
|
-
* break;
|
|
81
|
-
* default:
|
|
82
|
-
* throw error;
|
|
83
|
-
* }
|
|
84
|
-
* }
|
|
85
|
-
* }
|
|
86
|
-
* ```
|
|
87
|
-
*/
|
|
88
|
-
var SandboxError = class SandboxError extends Error {
|
|
89
|
-
/** Symbol for identifying sandbox error instances */
|
|
90
|
-
[SANDBOX_ERROR_SYMBOL] = true;
|
|
91
|
-
/** Error name for instanceof checks and logging */
|
|
92
|
-
name = "SandboxError";
|
|
93
|
-
/**
|
|
94
|
-
* Creates a new SandboxError.
|
|
95
|
-
*
|
|
96
|
-
* @param message - Human-readable error description
|
|
97
|
-
* @param code - Structured error code for programmatic handling
|
|
98
|
-
*/
|
|
99
|
-
constructor(message, code, cause) {
|
|
100
|
-
super(message);
|
|
101
|
-
this.code = code;
|
|
102
|
-
this.cause = cause;
|
|
103
|
-
Object.setPrototypeOf(this, SandboxError.prototype);
|
|
104
|
-
}
|
|
105
|
-
static isInstance(error) {
|
|
106
|
-
return typeof error === "object" && error !== null && error[SANDBOX_ERROR_SYMBOL] === true;
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
/**
|
|
110
|
-
* Resolve a backend instance or await a {@link BackendFactory}.
|
|
111
|
-
*
|
|
112
|
-
* Accepts {@link BackendRuntime} or {@link ToolRuntime} — store typing differs
|
|
113
|
-
* between LangGraph checkpoint stores and core `ToolRuntime`; factories receive
|
|
114
|
-
* a value that is structurally compatible at runtime.
|
|
115
|
-
*/
|
|
116
|
-
async function resolveBackend(backend, runtime) {
|
|
117
|
-
if (typeof backend === "function") return await backend(runtime);
|
|
118
|
-
return backend;
|
|
119
|
-
}
|
|
120
|
-
//#endregion
|
|
121
53
|
//#region src/backends/utils.ts
|
|
122
54
|
/**
|
|
123
55
|
* Shared utility functions for memory backend implementations.
|
|
@@ -127,9 +59,37 @@ async function resolveBackend(backend, runtime) {
|
|
|
127
59
|
* enable composition without fragile string parsing.
|
|
128
60
|
*/
|
|
129
61
|
const EMPTY_CONTENT_WARNING = "System reminder: File exists but has empty contents";
|
|
130
|
-
const MAX_LINE_LENGTH =
|
|
62
|
+
const MAX_LINE_LENGTH = 5e3;
|
|
131
63
|
const TOOL_RESULT_TOKEN_LIMIT = 2e4;
|
|
132
64
|
const TRUNCATION_GUIDANCE = "... [results truncated, try being more specific with your parameters]";
|
|
65
|
+
const MIME_TYPES = {
|
|
66
|
+
".png": "image/png",
|
|
67
|
+
".jpg": "image/jpeg",
|
|
68
|
+
".jpeg": "image/jpeg",
|
|
69
|
+
".gif": "image/gif",
|
|
70
|
+
".webp": "image/webp",
|
|
71
|
+
".svg": "image/svg+xml",
|
|
72
|
+
".heic": "image/heic",
|
|
73
|
+
".heif": "image/heif",
|
|
74
|
+
".mp3": "audio/mpeg",
|
|
75
|
+
".wav": "audio/wav",
|
|
76
|
+
".aiff": "audio/aiff",
|
|
77
|
+
".aac": "audio/aac",
|
|
78
|
+
".ogg": "audio/ogg",
|
|
79
|
+
".flac": "audio/flac",
|
|
80
|
+
".mp4": "video/mp4",
|
|
81
|
+
".webm": "video/webm",
|
|
82
|
+
".mpeg": "video/mpeg",
|
|
83
|
+
".mov": "video/quicktime",
|
|
84
|
+
".avi": "video/x-msvideo",
|
|
85
|
+
".flv": "video/x-flv",
|
|
86
|
+
".mpg": "video/mpeg",
|
|
87
|
+
".wmv": "video/x-ms-wmv",
|
|
88
|
+
".3gpp": "video/3gpp",
|
|
89
|
+
".pdf": "application/pdf",
|
|
90
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
91
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
92
|
+
};
|
|
133
93
|
/**
|
|
134
94
|
* Sanitize tool_call_id to prevent path traversal and separator issues.
|
|
135
95
|
*
|
|
@@ -157,7 +117,7 @@ function formatContentWithLineNumbers(content, startLine = 1) {
|
|
|
157
117
|
for (let i = 0; i < lines.length; i++) {
|
|
158
118
|
const line = lines[i];
|
|
159
119
|
const lineNum = i + startLine;
|
|
160
|
-
if (line.length <=
|
|
120
|
+
if (line.length <= 5e3) resultLines.push(`${lineNum.toString().padStart(6)}\t${line}`);
|
|
161
121
|
else {
|
|
162
122
|
const numChunks = Math.ceil(line.length / MAX_LINE_LENGTH);
|
|
163
123
|
for (let chunkIdx = 0; chunkIdx < numChunks; chunkIdx++) {
|
|
@@ -191,20 +151,50 @@ function checkEmptyContent(content) {
|
|
|
191
151
|
* @returns Content as string with lines joined by newlines
|
|
192
152
|
*/
|
|
193
153
|
function fileDataToString(fileData) {
|
|
194
|
-
return fileData.content.join("\n");
|
|
154
|
+
if (Array.isArray(fileData.content)) return fileData.content.join("\n");
|
|
155
|
+
if (typeof fileData.content === "string") return fileData.content;
|
|
156
|
+
throw new Error("Cannot convert binary FileData to string");
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Type guard to check if FileData contains binary content (Uint8Array).
|
|
160
|
+
*
|
|
161
|
+
* @param data - FileData to check
|
|
162
|
+
* @returns True if the content is a Uint8Array (binary)
|
|
163
|
+
*/
|
|
164
|
+
function isFileDataBinary(data) {
|
|
165
|
+
return ArrayBuffer.isView(data.content);
|
|
195
166
|
}
|
|
196
167
|
/**
|
|
197
|
-
* Create a FileData object
|
|
168
|
+
* Create a FileData object.
|
|
198
169
|
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
170
|
+
* Defaults to v2 format (content as single string). Pass `fileFormat: "v1"` for
|
|
171
|
+
* backward compatibility with older readers during a rolling deployment.
|
|
172
|
+
* Binary content (Uint8Array) is only supported with v2.
|
|
173
|
+
*
|
|
174
|
+
* @param content - File content as a string or binary Uint8Array (v2 only)
|
|
175
|
+
* @param createdAt - Optional creation timestamp (ISO format), defaults to now
|
|
176
|
+
* @param fileFormat - Storage format: "v2" (default) or "v1" (legacy line array)
|
|
177
|
+
* @returns FileData in the requested format
|
|
202
178
|
*/
|
|
203
|
-
function createFileData(content, createdAt) {
|
|
204
|
-
const lines = typeof content === "string" ? content.split("\n") : content;
|
|
179
|
+
function createFileData(content, createdAt, fileFormat = "v2", mimeType) {
|
|
205
180
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
181
|
+
if (fileFormat === "v1" && ArrayBuffer.isView(content)) throw new Error("Binary data is not supported with v1 file formats. Please use v2 file format");
|
|
182
|
+
if (fileFormat === "v2") {
|
|
183
|
+
if (ArrayBuffer.isView(content)) return {
|
|
184
|
+
content: new Uint8Array(content.buffer, content.byteOffset, content.byteLength),
|
|
185
|
+
mimeType: mimeType ?? "application/octet-stream",
|
|
186
|
+
created_at: createdAt || now,
|
|
187
|
+
modified_at: now
|
|
188
|
+
};
|
|
189
|
+
return {
|
|
190
|
+
content,
|
|
191
|
+
mimeType: mimeType ?? "text/plain",
|
|
192
|
+
created_at: createdAt || now,
|
|
193
|
+
modified_at: now
|
|
194
|
+
};
|
|
195
|
+
}
|
|
206
196
|
return {
|
|
207
|
-
content:
|
|
197
|
+
content: typeof content === "string" ? content.split("\n") : content,
|
|
208
198
|
created_at: createdAt || now,
|
|
209
199
|
modified_at: now
|
|
210
200
|
};
|
|
@@ -217,33 +207,20 @@ function createFileData(content, createdAt) {
|
|
|
217
207
|
* @returns Updated FileData object
|
|
218
208
|
*/
|
|
219
209
|
function updateFileData(fileData, content) {
|
|
220
|
-
const lines = typeof content === "string" ? content.split("\n") : content;
|
|
221
210
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
211
|
+
if (isFileDataV1(fileData)) return {
|
|
212
|
+
content: typeof content === "string" ? content.split("\n") : content,
|
|
213
|
+
created_at: fileData.created_at,
|
|
214
|
+
modified_at: now
|
|
215
|
+
};
|
|
222
216
|
return {
|
|
223
|
-
content
|
|
217
|
+
content,
|
|
218
|
+
mimeType: fileData.mimeType,
|
|
224
219
|
created_at: fileData.created_at,
|
|
225
220
|
modified_at: now
|
|
226
221
|
};
|
|
227
222
|
}
|
|
228
223
|
/**
|
|
229
|
-
* Format file data for read response with line numbers.
|
|
230
|
-
*
|
|
231
|
-
* @param fileData - FileData object
|
|
232
|
-
* @param offset - Line offset (0-indexed)
|
|
233
|
-
* @param limit - Maximum number of lines
|
|
234
|
-
* @returns Formatted content or error message
|
|
235
|
-
*/
|
|
236
|
-
function formatReadResponse(fileData, offset, limit) {
|
|
237
|
-
const content = fileDataToString(fileData);
|
|
238
|
-
const emptyMsg = checkEmptyContent(content);
|
|
239
|
-
if (emptyMsg) return emptyMsg;
|
|
240
|
-
const lines = content.split("\n");
|
|
241
|
-
const startIdx = offset;
|
|
242
|
-
const endIdx = Math.min(startIdx + limit, lines.length);
|
|
243
|
-
if (startIdx >= lines.length) return `Error: Line offset ${offset} exceeds file length (${lines.length} lines)`;
|
|
244
|
-
return formatContentWithLineNumbers(lines.slice(startIdx, endIdx), startIdx + 1);
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
224
|
* Perform string replacement with occurrence validation.
|
|
248
225
|
*
|
|
249
226
|
* @param content - Original content
|
|
@@ -302,8 +279,8 @@ function truncateIfTooLong(result) {
|
|
|
302
279
|
* validatePath("C:\\Users\\file") // Throws: Windows absolute paths not supported
|
|
303
280
|
* ```
|
|
304
281
|
*/
|
|
305
|
-
function validatePath(path$
|
|
306
|
-
const pathStr = path$
|
|
282
|
+
function validatePath(path$6) {
|
|
283
|
+
const pathStr = path$6 || "/";
|
|
307
284
|
if (!pathStr || pathStr.trim() === "") throw new Error("Path cannot be empty");
|
|
308
285
|
let normalized = pathStr.startsWith("/") ? pathStr : "/" + pathStr;
|
|
309
286
|
if (!normalized.endsWith("/")) normalized += "/";
|
|
@@ -325,10 +302,10 @@ function validatePath(path$5) {
|
|
|
325
302
|
* // Returns: "/test.py\n/src/main.py" (sorted by modified_at)
|
|
326
303
|
* ```
|
|
327
304
|
*/
|
|
328
|
-
function globSearchFiles(files, pattern, path$
|
|
305
|
+
function globSearchFiles(files, pattern, path$8 = "/") {
|
|
329
306
|
let normalizedPath;
|
|
330
307
|
try {
|
|
331
|
-
normalizedPath = validatePath(path$
|
|
308
|
+
normalizedPath = validatePath(path$8);
|
|
332
309
|
} catch {
|
|
333
310
|
return "No files found";
|
|
334
311
|
}
|
|
@@ -354,16 +331,13 @@ function globSearchFiles(files, pattern, path$7 = "/") {
|
|
|
354
331
|
/**
|
|
355
332
|
* Return structured grep matches from an in-memory files mapping.
|
|
356
333
|
*
|
|
357
|
-
* Performs literal text search (not regex).
|
|
358
|
-
*
|
|
359
|
-
* Returns a list of GrepMatch on success, or a string for invalid inputs.
|
|
360
|
-
* We deliberately do not raise here to keep backends non-throwing in tool
|
|
361
|
-
* contexts and preserve user-facing error messages.
|
|
334
|
+
* Performs literal text search (not regex). Binary files are skipped.
|
|
335
|
+
* Returns an empty array when no matches are found or on invalid input.
|
|
362
336
|
*/
|
|
363
|
-
function grepMatchesFromFiles(files, pattern, path$
|
|
337
|
+
function grepMatchesFromFiles(files, pattern, path$10 = null, glob = null) {
|
|
364
338
|
let normalizedPath;
|
|
365
339
|
try {
|
|
366
|
-
normalizedPath = validatePath(path$
|
|
340
|
+
normalizedPath = validatePath(path$10);
|
|
367
341
|
} catch {
|
|
368
342
|
return [];
|
|
369
343
|
}
|
|
@@ -373,19 +347,231 @@ function grepMatchesFromFiles(files, pattern, path$9 = null, glob = null) {
|
|
|
373
347
|
nobrace: false
|
|
374
348
|
})));
|
|
375
349
|
const matches = [];
|
|
376
|
-
for (const [filePath, fileData] of Object.entries(filtered))
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
350
|
+
for (const [filePath, fileData] of Object.entries(filtered)) {
|
|
351
|
+
if (!isTextMimeType(migrateToFileDataV2(fileData, filePath).mimeType)) continue;
|
|
352
|
+
const lines = fileDataToString(fileData).split("\n");
|
|
353
|
+
for (let i = 0; i < lines.length; i++) {
|
|
354
|
+
const line = lines[i];
|
|
355
|
+
const lineNum = i + 1;
|
|
356
|
+
if (line.includes(pattern)) matches.push({
|
|
357
|
+
path: filePath,
|
|
358
|
+
line: lineNum,
|
|
359
|
+
text: line
|
|
360
|
+
});
|
|
361
|
+
}
|
|
384
362
|
}
|
|
385
363
|
return matches;
|
|
386
364
|
}
|
|
365
|
+
/**
|
|
366
|
+
* Determine MIME type from a file path's extension.
|
|
367
|
+
*
|
|
368
|
+
* Returns "text/plain" for unknown extensions.
|
|
369
|
+
*
|
|
370
|
+
* @param filePath - File path to inspect
|
|
371
|
+
* @returns MIME type string (e.g., "image/png", "text/plain")
|
|
372
|
+
*/
|
|
373
|
+
function getMimeType(filePath) {
|
|
374
|
+
return MIME_TYPES[path.default.extname(filePath).toLocaleLowerCase()] || "text/plain";
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Check whether a MIME type represents text content.
|
|
378
|
+
*
|
|
379
|
+
* @param mimeType - MIME type string to check
|
|
380
|
+
* @returns True if the MIME type is text-based
|
|
381
|
+
*/
|
|
382
|
+
function isTextMimeType(mimeType) {
|
|
383
|
+
return mimeType.startsWith("text/") || mimeType === "application/json" || mimeType === "application/javascript" || mimeType === "image/svg+xml";
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Type guard to check if FileData is v1 format (content as line array).
|
|
387
|
+
*
|
|
388
|
+
* @param data - FileData to check
|
|
389
|
+
* @returns True if data is FileDataV1
|
|
390
|
+
*/
|
|
391
|
+
function isFileDataV1(data) {
|
|
392
|
+
return Array.isArray(data.content);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Convert FileData to v2 format, joining v1 line arrays into a single string.
|
|
396
|
+
*
|
|
397
|
+
* If the data is already v2, returns it unchanged.
|
|
398
|
+
*
|
|
399
|
+
* @param data - FileData in either format
|
|
400
|
+
* @returns FileDataV2 with content as string (text) or Uint8Array (binary)
|
|
401
|
+
*/
|
|
402
|
+
function migrateToFileDataV2(data, filePath) {
|
|
403
|
+
if (isFileDataV1(data)) return {
|
|
404
|
+
content: data.content.join("\n"),
|
|
405
|
+
mimeType: getMimeType(filePath),
|
|
406
|
+
created_at: data.created_at,
|
|
407
|
+
modified_at: data.modified_at
|
|
408
|
+
};
|
|
409
|
+
if (!("mimeType" in data) || !data.mimeType) return {
|
|
410
|
+
...data,
|
|
411
|
+
mimeType: getMimeType(filePath)
|
|
412
|
+
};
|
|
413
|
+
return data;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Adapt a v1 {@link BackendProtocol} to {@link BackendProtocolV2}.
|
|
417
|
+
*
|
|
418
|
+
* If the backend already implements v2, it is returned as-is.
|
|
419
|
+
* For v1 backends, wraps returns in Result types:
|
|
420
|
+
* - `read()` string returns wrapped in {@link ReadResult}
|
|
421
|
+
* - `readRaw()` FileData returns wrapped in {@link ReadRawResult}
|
|
422
|
+
* - `grep()` returns wrapped in {@link GrepResult}
|
|
423
|
+
* - `ls()` FileInfo[] returns wrapped in {@link LsResult}
|
|
424
|
+
* - `glob()` FileInfo[] returns wrapped in {@link GlobResult}
|
|
425
|
+
*
|
|
426
|
+
* Note: For sandbox instances, use {@link adaptSandboxProtocol} instead.
|
|
427
|
+
*
|
|
428
|
+
* @param backend - Backend instance (v1 or v2)
|
|
429
|
+
* @returns BackendProtocolV2-compatible backend
|
|
430
|
+
*/
|
|
431
|
+
function adaptBackendProtocol(backend) {
|
|
432
|
+
return {
|
|
433
|
+
async ls(path$11) {
|
|
434
|
+
const result = await ("ls" in backend ? backend.ls(path$11) : backend.lsInfo(path$11));
|
|
435
|
+
if (Array.isArray(result)) return { files: result };
|
|
436
|
+
return result;
|
|
437
|
+
},
|
|
438
|
+
async readRaw(filePath) {
|
|
439
|
+
const result = await backend.readRaw(filePath);
|
|
440
|
+
if ("data" in result || "error" in result) return result;
|
|
441
|
+
return { data: migrateToFileDataV2(result, filePath) };
|
|
442
|
+
},
|
|
443
|
+
async glob(pattern, path$12) {
|
|
444
|
+
const result = await ("glob" in backend ? backend.glob(pattern, path$12) : backend.globInfo(pattern, path$12));
|
|
445
|
+
if (Array.isArray(result)) return { files: result };
|
|
446
|
+
return result;
|
|
447
|
+
},
|
|
448
|
+
write: (filePath, content) => backend.write(filePath, content),
|
|
449
|
+
edit: (filePath, oldString, newString, replaceAll) => backend.edit(filePath, oldString, newString, replaceAll),
|
|
450
|
+
uploadFiles: backend.uploadFiles ? (files) => backend.uploadFiles(files) : void 0,
|
|
451
|
+
downloadFiles: backend.downloadFiles ? (paths) => backend.downloadFiles(paths) : void 0,
|
|
452
|
+
async read(filePath, offset, limit) {
|
|
453
|
+
const result = await backend.read(filePath, offset, limit);
|
|
454
|
+
if (typeof result === "string") return { content: result };
|
|
455
|
+
return result;
|
|
456
|
+
},
|
|
457
|
+
async grep(pattern, path$13, glob) {
|
|
458
|
+
const result = await ("grep" in backend ? backend.grep(pattern, path$13, glob) : backend.grepRaw(pattern, path$13, glob));
|
|
459
|
+
if (Array.isArray(result)) return { matches: result };
|
|
460
|
+
if (typeof result === "string") return { error: result };
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Adapt a sandbox backend from v1 to v2 interface.
|
|
467
|
+
*
|
|
468
|
+
* This extends {@link adaptBackendProtocol} to also preserve sandbox-specific
|
|
469
|
+
* properties from {@link SandboxBackendProtocol}: `execute` and `id`.
|
|
470
|
+
*
|
|
471
|
+
* @param sandbox - Sandbox backend (v1 or v2)
|
|
472
|
+
* @returns SandboxBackendProtocolV2-compatible sandbox
|
|
473
|
+
*/
|
|
474
|
+
function adaptSandboxProtocol(sandbox) {
|
|
475
|
+
const adapted = adaptBackendProtocol(sandbox);
|
|
476
|
+
adapted.execute = (cmd) => sandbox.execute(cmd);
|
|
477
|
+
Object.defineProperty(adapted, "id", {
|
|
478
|
+
value: sandbox.id,
|
|
479
|
+
enumerable: true,
|
|
480
|
+
configurable: true
|
|
481
|
+
});
|
|
482
|
+
return adapted;
|
|
483
|
+
}
|
|
484
|
+
//#endregion
|
|
485
|
+
//#region src/backends/protocol.ts
|
|
486
|
+
/**
|
|
487
|
+
* Type guard to check if a backend supports execution.
|
|
488
|
+
*
|
|
489
|
+
* @param backend - Backend instance to check
|
|
490
|
+
* @returns True if the backend implements SandboxBackendProtocolV2
|
|
491
|
+
*/
|
|
492
|
+
function isSandboxBackend(backend) {
|
|
493
|
+
return backend != null && typeof backend === "object" && typeof backend.execute === "function" && typeof backend.id === "string" && backend.id !== "";
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Type guard to check if a backend is a sandbox protocol (v1 or v2).
|
|
497
|
+
*
|
|
498
|
+
* Checks for the presence of `execute` function and `id` string,
|
|
499
|
+
* which are the defining features of sandbox protocols.
|
|
500
|
+
*
|
|
501
|
+
* @param backend - Backend instance to check
|
|
502
|
+
* @returns True if the backend implements sandbox protocol (v1 or v2)
|
|
503
|
+
*/
|
|
504
|
+
function isSandboxProtocol(backend) {
|
|
505
|
+
return backend != null && typeof backend === "object" && typeof backend.execute === "function" && typeof backend.id === "string" && backend.id !== "";
|
|
506
|
+
}
|
|
507
|
+
const SANDBOX_ERROR_SYMBOL = Symbol.for("sandbox.error");
|
|
508
|
+
/**
|
|
509
|
+
* Custom error class for sandbox operations.
|
|
510
|
+
*
|
|
511
|
+
* @param message - Human-readable error description
|
|
512
|
+
* @param code - Structured error code for programmatic handling
|
|
513
|
+
* @returns SandboxError with message and code
|
|
514
|
+
*
|
|
515
|
+
* @example
|
|
516
|
+
* ```typescript
|
|
517
|
+
* try {
|
|
518
|
+
* await sandbox.execute("some command");
|
|
519
|
+
* } catch (error) {
|
|
520
|
+
* if (error instanceof SandboxError) {
|
|
521
|
+
* switch (error.code) {
|
|
522
|
+
* case "NOT_INITIALIZED":
|
|
523
|
+
* await sandbox.initialize();
|
|
524
|
+
* break;
|
|
525
|
+
* case "COMMAND_TIMEOUT":
|
|
526
|
+
* console.error("Command took too long");
|
|
527
|
+
* break;
|
|
528
|
+
* default:
|
|
529
|
+
* throw error;
|
|
530
|
+
* }
|
|
531
|
+
* }
|
|
532
|
+
* }
|
|
533
|
+
* ```
|
|
534
|
+
*/
|
|
535
|
+
var SandboxError = class SandboxError extends Error {
|
|
536
|
+
/** Symbol for identifying sandbox error instances */
|
|
537
|
+
[SANDBOX_ERROR_SYMBOL] = true;
|
|
538
|
+
/** Error name for instanceof checks and logging */
|
|
539
|
+
name = "SandboxError";
|
|
540
|
+
/**
|
|
541
|
+
* Creates a new SandboxError.
|
|
542
|
+
*
|
|
543
|
+
* @param message - Human-readable error description
|
|
544
|
+
* @param code - Structured error code for programmatic handling
|
|
545
|
+
*/
|
|
546
|
+
constructor(message, code, cause) {
|
|
547
|
+
super(message);
|
|
548
|
+
this.code = code;
|
|
549
|
+
this.cause = cause;
|
|
550
|
+
Object.setPrototypeOf(this, SandboxError.prototype);
|
|
551
|
+
}
|
|
552
|
+
static isInstance(error) {
|
|
553
|
+
return typeof error === "object" && error !== null && error[SANDBOX_ERROR_SYMBOL] === true;
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
/**
|
|
557
|
+
* Resolve a backend instance or await a {@link BackendFactory}.
|
|
558
|
+
*
|
|
559
|
+
* Accepts {@link BackendRuntime} or {@link ToolRuntime} — store typing differs
|
|
560
|
+
* between LangGraph checkpoint stores and core `ToolRuntime`; factories receive
|
|
561
|
+
* a value that is structurally compatible at runtime.
|
|
562
|
+
*
|
|
563
|
+
* @internal
|
|
564
|
+
*/
|
|
565
|
+
async function resolveBackend(backend, runtime) {
|
|
566
|
+
if (typeof backend === "function") {
|
|
567
|
+
const resolved = await backend(runtime);
|
|
568
|
+
return isSandboxProtocol(resolved) ? adaptSandboxProtocol(resolved) : adaptBackendProtocol(resolved);
|
|
569
|
+
}
|
|
570
|
+
return isSandboxProtocol(backend) ? adaptSandboxProtocol(backend) : adaptBackendProtocol(backend);
|
|
571
|
+
}
|
|
387
572
|
//#endregion
|
|
388
573
|
//#region src/backends/state.ts
|
|
574
|
+
const PREGEL_SEND_KEY = "__pregel_send";
|
|
389
575
|
/**
|
|
390
576
|
* Backend that stores files in agent state (ephemeral).
|
|
391
577
|
*
|
|
@@ -399,23 +585,60 @@ function grepMatchesFromFiles(files, pattern, path$9 = null, glob = null) {
|
|
|
399
585
|
*/
|
|
400
586
|
var StateBackend = class {
|
|
401
587
|
runtime;
|
|
402
|
-
|
|
403
|
-
|
|
588
|
+
fileFormat;
|
|
589
|
+
constructor(runtimeOrOptions, options) {
|
|
590
|
+
if (runtimeOrOptions != null && typeof runtimeOrOptions === "object" && "state" in runtimeOrOptions) {
|
|
591
|
+
this.runtime = runtimeOrOptions;
|
|
592
|
+
this.fileFormat = options?.fileFormat ?? "v2";
|
|
593
|
+
} else {
|
|
594
|
+
this.runtime = void 0;
|
|
595
|
+
this.fileFormat = runtimeOrOptions?.fileFormat ?? "v2";
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Whether this instance was constructed with the legacy factory pattern.
|
|
600
|
+
*
|
|
601
|
+
* When true, state is read from the injected `runtime` and `filesUpdate`
|
|
602
|
+
* is returned to the caller. When false, state is read from LangGraph's
|
|
603
|
+
* execution context and updates are sent via `__pregel_send`.
|
|
604
|
+
*/
|
|
605
|
+
get isLegacy() {
|
|
606
|
+
return this.runtime !== void 0;
|
|
404
607
|
}
|
|
405
608
|
/**
|
|
406
609
|
* Get files from current state.
|
|
610
|
+
*
|
|
611
|
+
* In legacy mode, reads from the injected {@link BackendRuntime}.
|
|
612
|
+
* In zero-arg mode, reads from the LangGraph execution context via
|
|
613
|
+
* {@link getCurrentTaskInput}.
|
|
407
614
|
*/
|
|
408
615
|
getFiles() {
|
|
409
|
-
return this.runtime.state.files || {};
|
|
616
|
+
if (this.runtime) return this.runtime.state.files || {};
|
|
617
|
+
return (0, _langchain_langgraph.getCurrentTaskInput)()?.files || {};
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Push a files state update through LangGraph's internal send channel.
|
|
621
|
+
*
|
|
622
|
+
* In zero-arg mode, sends the update via the `__pregel_send` function
|
|
623
|
+
* from {@link getConfig}, mirroring Python's `CONFIG_KEY_SEND`.
|
|
624
|
+
* In legacy mode, this is a no-op — the caller uses `filesUpdate`
|
|
625
|
+
* from the return value instead.
|
|
626
|
+
*
|
|
627
|
+
* @param update - Map of file paths to their updated {@link FileData}
|
|
628
|
+
*/
|
|
629
|
+
sendFilesUpdate(update) {
|
|
630
|
+
if (this.isLegacy) return;
|
|
631
|
+
const send = (0, _langchain_langgraph.getConfig)().configurable?.[PREGEL_SEND_KEY];
|
|
632
|
+
if (typeof send === "function") send([["files", update]]);
|
|
410
633
|
}
|
|
411
634
|
/**
|
|
412
635
|
* List files and directories in the specified directory (non-recursive).
|
|
413
636
|
*
|
|
414
637
|
* @param path - Absolute path to directory
|
|
415
|
-
* @returns
|
|
638
|
+
* @returns LsResult with list of FileInfo objects on success or error on failure.
|
|
416
639
|
* Directories have a trailing / in their path and is_dir=true.
|
|
417
640
|
*/
|
|
418
|
-
|
|
641
|
+
ls(path) {
|
|
419
642
|
const files = this.getFiles();
|
|
420
643
|
const infos = [];
|
|
421
644
|
const subdirs = /* @__PURE__ */ new Set();
|
|
@@ -428,7 +651,7 @@ var StateBackend = class {
|
|
|
428
651
|
subdirs.add(normalizedPath + subdirName + "/");
|
|
429
652
|
continue;
|
|
430
653
|
}
|
|
431
|
-
const size = fd.content.join("\n").length;
|
|
654
|
+
const size = isFileDataV1(fd) ? fd.content.join("\n").length : isFileDataBinary(fd) ? fd.content.byteLength : fd.content.length;
|
|
432
655
|
infos.push({
|
|
433
656
|
path: k,
|
|
434
657
|
is_dir: false,
|
|
@@ -443,31 +666,43 @@ var StateBackend = class {
|
|
|
443
666
|
modified_at: ""
|
|
444
667
|
});
|
|
445
668
|
infos.sort((a, b) => a.path.localeCompare(b.path));
|
|
446
|
-
return infos;
|
|
669
|
+
return { files: infos };
|
|
447
670
|
}
|
|
448
671
|
/**
|
|
449
|
-
* Read file content
|
|
672
|
+
* Read file content.
|
|
673
|
+
*
|
|
674
|
+
* Text files are paginated by line offset/limit.
|
|
675
|
+
* Binary files return full Uint8Array content (offset/limit ignored).
|
|
450
676
|
*
|
|
451
677
|
* @param filePath - Absolute file path
|
|
452
678
|
* @param offset - Line offset to start reading from (0-indexed)
|
|
453
679
|
* @param limit - Maximum number of lines to read
|
|
454
|
-
* @returns
|
|
680
|
+
* @returns ReadResult with content on success or error on failure
|
|
455
681
|
*/
|
|
456
682
|
read(filePath, offset = 0, limit = 500) {
|
|
457
683
|
const fileData = this.getFiles()[filePath];
|
|
458
|
-
if (!fileData) return
|
|
459
|
-
|
|
684
|
+
if (!fileData) return { error: `File '${filePath}' not found` };
|
|
685
|
+
const fileDataV2 = migrateToFileDataV2(fileData, filePath);
|
|
686
|
+
if (!isTextMimeType(fileDataV2.mimeType)) return {
|
|
687
|
+
content: fileDataV2.content,
|
|
688
|
+
mimeType: fileDataV2.mimeType
|
|
689
|
+
};
|
|
690
|
+
if (typeof fileDataV2.content !== "string") return { error: `File '${filePath}' has binary content but text MIME type` };
|
|
691
|
+
return {
|
|
692
|
+
content: fileDataV2.content.split("\n").slice(offset, offset + limit).join("\n"),
|
|
693
|
+
mimeType: fileDataV2.mimeType
|
|
694
|
+
};
|
|
460
695
|
}
|
|
461
696
|
/**
|
|
462
697
|
* Read file content as raw FileData.
|
|
463
698
|
*
|
|
464
699
|
* @param filePath - Absolute file path
|
|
465
|
-
* @returns
|
|
700
|
+
* @returns ReadRawResult with raw file data on success or error on failure
|
|
466
701
|
*/
|
|
467
702
|
readRaw(filePath) {
|
|
468
703
|
const fileData = this.getFiles()[filePath];
|
|
469
|
-
if (!fileData)
|
|
470
|
-
return fileData;
|
|
704
|
+
if (!fileData) return { error: `File '${filePath}' not found` };
|
|
705
|
+
return { data: fileData };
|
|
471
706
|
}
|
|
472
707
|
/**
|
|
473
708
|
* Create a new file with content.
|
|
@@ -475,7 +710,13 @@ var StateBackend = class {
|
|
|
475
710
|
*/
|
|
476
711
|
write(filePath, content) {
|
|
477
712
|
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.` };
|
|
478
|
-
const
|
|
713
|
+
const mimeType = getMimeType(filePath);
|
|
714
|
+
const newFileData = createFileData(content, void 0, this.fileFormat, mimeType);
|
|
715
|
+
const update = { [filePath]: newFileData };
|
|
716
|
+
if (!this.isLegacy) {
|
|
717
|
+
this.sendFilesUpdate(update);
|
|
718
|
+
return { path: filePath };
|
|
719
|
+
}
|
|
479
720
|
return {
|
|
480
721
|
path: filePath,
|
|
481
722
|
filesUpdate: { [filePath]: newFileData }
|
|
@@ -492,6 +733,14 @@ var StateBackend = class {
|
|
|
492
733
|
if (typeof result === "string") return { error: result };
|
|
493
734
|
const [newContent, occurrences] = result;
|
|
494
735
|
const newFileData = updateFileData(fileData, newContent);
|
|
736
|
+
const update = { [filePath]: newFileData };
|
|
737
|
+
if (!this.isLegacy) {
|
|
738
|
+
this.sendFilesUpdate(update);
|
|
739
|
+
return {
|
|
740
|
+
path: filePath,
|
|
741
|
+
occurrences
|
|
742
|
+
};
|
|
743
|
+
}
|
|
495
744
|
return {
|
|
496
745
|
path: filePath,
|
|
497
746
|
filesUpdate: { [filePath]: newFileData },
|
|
@@ -499,23 +748,24 @@ var StateBackend = class {
|
|
|
499
748
|
};
|
|
500
749
|
}
|
|
501
750
|
/**
|
|
502
|
-
*
|
|
751
|
+
* Search file contents for a literal text pattern.
|
|
752
|
+
* Binary files are skipped.
|
|
503
753
|
*/
|
|
504
|
-
|
|
505
|
-
return grepMatchesFromFiles(this.getFiles(), pattern, path, glob);
|
|
754
|
+
grep(pattern, path = "/", glob = null) {
|
|
755
|
+
return { matches: grepMatchesFromFiles(this.getFiles(), pattern, path, glob) };
|
|
506
756
|
}
|
|
507
757
|
/**
|
|
508
758
|
* Structured glob matching returning FileInfo objects.
|
|
509
759
|
*/
|
|
510
|
-
|
|
760
|
+
glob(pattern, path = "/") {
|
|
511
761
|
const files = this.getFiles();
|
|
512
762
|
const result = globSearchFiles(files, pattern, path);
|
|
513
|
-
if (result === "No files found") return [];
|
|
763
|
+
if (result === "No files found") return { files: [] };
|
|
514
764
|
const paths = result.split("\n");
|
|
515
765
|
const infos = [];
|
|
516
766
|
for (const p of paths) {
|
|
517
767
|
const fd = files[p];
|
|
518
|
-
const size = fd ? fd.content.join("\n").length : 0;
|
|
768
|
+
const size = fd ? isFileDataV1(fd) ? fd.content.join("\n").length : isFileDataBinary(fd) ? fd.content.byteLength : fd.content.length : 0;
|
|
519
769
|
infos.push({
|
|
520
770
|
path: p,
|
|
521
771
|
is_dir: false,
|
|
@@ -523,7 +773,7 @@ var StateBackend = class {
|
|
|
523
773
|
modified_at: fd?.modified_at || ""
|
|
524
774
|
});
|
|
525
775
|
}
|
|
526
|
-
return infos;
|
|
776
|
+
return { files: infos };
|
|
527
777
|
}
|
|
528
778
|
/**
|
|
529
779
|
* Upload multiple files.
|
|
@@ -538,7 +788,9 @@ var StateBackend = class {
|
|
|
538
788
|
const responses = [];
|
|
539
789
|
const updates = {};
|
|
540
790
|
for (const [path, content] of files) try {
|
|
541
|
-
|
|
791
|
+
const mimeType = getMimeType(path);
|
|
792
|
+
if (this.fileFormat === "v2" && !isTextMimeType(mimeType)) updates[path] = createFileData(content, void 0, "v2", mimeType);
|
|
793
|
+
else updates[path] = createFileData(new TextDecoder().decode(content), void 0, this.fileFormat, mimeType);
|
|
542
794
|
responses.push({
|
|
543
795
|
path,
|
|
544
796
|
error: null
|
|
@@ -549,6 +801,10 @@ var StateBackend = class {
|
|
|
549
801
|
error: "invalid_path"
|
|
550
802
|
});
|
|
551
803
|
}
|
|
804
|
+
if (!this.isLegacy) {
|
|
805
|
+
if (Object.keys(updates).length > 0) this.sendFilesUpdate(updates);
|
|
806
|
+
return responses;
|
|
807
|
+
}
|
|
552
808
|
const result = responses;
|
|
553
809
|
result.filesUpdate = updates;
|
|
554
810
|
return result;
|
|
@@ -572,11 +828,17 @@ var StateBackend = class {
|
|
|
572
828
|
});
|
|
573
829
|
continue;
|
|
574
830
|
}
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
|
|
831
|
+
const fileDataV2 = migrateToFileDataV2(fileData, path);
|
|
832
|
+
if (typeof fileDataV2.content === "string") {
|
|
833
|
+
const content = new TextEncoder().encode(fileDataV2.content);
|
|
834
|
+
responses.push({
|
|
835
|
+
path,
|
|
836
|
+
content,
|
|
837
|
+
error: null
|
|
838
|
+
});
|
|
839
|
+
} else responses.push({
|
|
578
840
|
path,
|
|
579
|
-
content,
|
|
841
|
+
content: fileDataV2.content,
|
|
580
842
|
error: null
|
|
581
843
|
});
|
|
582
844
|
}
|
|
@@ -592,6 +854,7 @@ var StateBackend = class {
|
|
|
592
854
|
* - Pluggable backends (StateBackend, StoreBackend, FilesystemBackend, CompositeBackend)
|
|
593
855
|
* - Tool result eviction for large outputs
|
|
594
856
|
*/
|
|
857
|
+
const INT_FORMATTER = new Intl.NumberFormat("en-US");
|
|
595
858
|
/**
|
|
596
859
|
* Tools that should be excluded from the large result eviction logic.
|
|
597
860
|
*
|
|
@@ -638,6 +901,12 @@ const TOOLS_EXCLUDED_FROM_EVICTION = [
|
|
|
638
901
|
"write_file"
|
|
639
902
|
];
|
|
640
903
|
/**
|
|
904
|
+
* Maximum size for binary (non-text) files read via read_file, in bytes.
|
|
905
|
+
* Base64-encoded content is ~33% larger, so 10MB raw ≈ 13.3MB in context.
|
|
906
|
+
* This keeps inline multimodal payloads within all major provider limits.
|
|
907
|
+
*/
|
|
908
|
+
const MAX_BINARY_READ_SIZE_BYTES = 10 * 1024 * 1024;
|
|
909
|
+
/**
|
|
641
910
|
* Template for truncation message in read_file.
|
|
642
911
|
* {file_path} will be filled in at runtime.
|
|
643
912
|
*/
|
|
@@ -647,16 +916,18 @@ const READ_FILE_TRUNCATION_MSG = `
|
|
|
647
916
|
/**
|
|
648
917
|
* Message template for evicted tool results.
|
|
649
918
|
*/
|
|
650
|
-
const TOO_LARGE_TOOL_MSG = `
|
|
651
|
-
|
|
652
|
-
You can
|
|
653
|
-
|
|
919
|
+
const TOO_LARGE_TOOL_MSG = langchain.context`
|
|
920
|
+
Tool result too large, the result of this tool call {tool_call_id} was saved in the filesystem at this path: {file_path}
|
|
921
|
+
You can read the result from the filesystem by using the read_file tool, but make sure to only read part of the result at a time.
|
|
922
|
+
You can do this by specifying an offset and limit in the read_file tool call.
|
|
923
|
+
For example, to read the first 100 lines, you can use the read_file tool with offset=0 and limit=100.
|
|
654
924
|
|
|
655
|
-
Here is a preview showing the head and tail of the result (lines of the form
|
|
656
|
-
... [N lines truncated] ...
|
|
657
|
-
indicate omitted lines in the middle of the content):
|
|
925
|
+
Here is a preview showing the head and tail of the result (lines of the form
|
|
926
|
+
... [N lines truncated] ...
|
|
927
|
+
indicate omitted lines in the middle of the content):
|
|
658
928
|
|
|
659
|
-
{content_sample}
|
|
929
|
+
{content_sample}
|
|
930
|
+
`;
|
|
660
931
|
/**
|
|
661
932
|
* Message template for evicted HumanMessages.
|
|
662
933
|
*/
|
|
@@ -732,14 +1003,27 @@ function createContentPreview(contentStr, headLines = 5, tailLines = 5) {
|
|
|
732
1003
|
return headSample + truncationNotice + tailSample;
|
|
733
1004
|
}
|
|
734
1005
|
/**
|
|
735
|
-
* Zod
|
|
1006
|
+
* Zod schema for legacy FileDataV1 (content as line array).
|
|
736
1007
|
*/
|
|
737
|
-
const
|
|
1008
|
+
const FileDataV1Schema = zod_v4.z.object({
|
|
738
1009
|
content: zod_v4.z.array(zod_v4.z.string()),
|
|
739
1010
|
created_at: zod_v4.z.string(),
|
|
740
1011
|
modified_at: zod_v4.z.string()
|
|
741
1012
|
});
|
|
742
1013
|
/**
|
|
1014
|
+
* Zod schema for FileDataV2 (content as string for text or Uint8Array for binary).
|
|
1015
|
+
*/
|
|
1016
|
+
const FileDataV2Schema = zod_v4.z.object({
|
|
1017
|
+
content: zod_v4.z.union([zod_v4.z.string(), zod_v4.z.instanceof(Uint8Array)]),
|
|
1018
|
+
mimeType: zod_v4.z.string(),
|
|
1019
|
+
created_at: zod_v4.z.string(),
|
|
1020
|
+
modified_at: zod_v4.z.string()
|
|
1021
|
+
});
|
|
1022
|
+
/**
|
|
1023
|
+
* Zod v3 schema for FileData (re-export from backends)
|
|
1024
|
+
*/
|
|
1025
|
+
const FileDataSchema = zod_v4.z.union([FileDataV1Schema, FileDataV2Schema]);
|
|
1026
|
+
/**
|
|
743
1027
|
* Reducer for files state that merges file updates with support for deletions.
|
|
744
1028
|
* When a file value is null, the file is deleted from state.
|
|
745
1029
|
* When a file value is non-null, it is added or updated in state.
|
|
@@ -775,118 +1059,141 @@ const FilesystemStateSchema = new _langchain_langgraph.StateSchema({ files: new
|
|
|
775
1059
|
inputSchema: zod_v4.z.record(zod_v4.z.string(), FileDataSchema.nullable()).optional(),
|
|
776
1060
|
reducer: fileDataReducer
|
|
777
1061
|
}) });
|
|
778
|
-
const FILESYSTEM_SYSTEM_PROMPT =
|
|
1062
|
+
const FILESYSTEM_SYSTEM_PROMPT = langchain.context`
|
|
1063
|
+
## Following Conventions
|
|
779
1064
|
|
|
780
|
-
|
|
781
|
-
|
|
1065
|
+
- Read files before editing — understand existing content before making changes
|
|
1066
|
+
- Mimic existing style, naming conventions, and patterns
|
|
782
1067
|
|
|
783
|
-
|
|
784
|
-
- read_file: read a file from the filesystem
|
|
785
|
-
- write_file: write to a file in the filesystem
|
|
786
|
-
- edit_file: edit a file in the filesystem
|
|
787
|
-
- glob: find files matching a pattern (e.g., "**/*.py")
|
|
788
|
-
- grep: search for text within files`;
|
|
789
|
-
const LS_TOOL_DESCRIPTION = `Lists all files in a directory.
|
|
1068
|
+
## Filesystem Tools \`ls\`, \`read_file\`, \`write_file\`, \`edit_file\`, \`glob\`, \`grep\`
|
|
790
1069
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
const READ_FILE_TOOL_DESCRIPTION = `Reads a file from the filesystem.
|
|
1070
|
+
You have access to a filesystem which you can interact with using these tools.
|
|
1071
|
+
All file paths must start with a /.
|
|
794
1072
|
|
|
795
|
-
|
|
1073
|
+
- ls: list files in a directory (requires absolute path)
|
|
1074
|
+
- read_file: read a file from the filesystem
|
|
1075
|
+
- write_file: write to a file in the filesystem
|
|
1076
|
+
- edit_file: edit a file in the filesystem
|
|
1077
|
+
- glob: find files matching a pattern (e.g., "**/*.py")
|
|
1078
|
+
- grep: search for text within files
|
|
1079
|
+
`;
|
|
1080
|
+
const LS_TOOL_DESCRIPTION = langchain.context`
|
|
1081
|
+
Lists all files in a directory.
|
|
1082
|
+
|
|
1083
|
+
This is useful for exploring the filesystem and finding the right file to read or edit.
|
|
1084
|
+
You should almost ALWAYS use this tool before using the read_file or edit_file tools.
|
|
1085
|
+
`;
|
|
1086
|
+
const READ_FILE_TOOL_DESCRIPTION = langchain.context`
|
|
1087
|
+
Reads a file from the filesystem.
|
|
796
1088
|
|
|
797
|
-
|
|
798
|
-
- By default, it reads up to 100 lines starting from the beginning of the file
|
|
799
|
-
- **IMPORTANT for large files and codebase exploration**: Use pagination with offset and limit parameters to avoid context overflow
|
|
800
|
-
- First scan: read_file(path, limit=100) to see file structure
|
|
801
|
-
- Read more sections: read_file(path, offset=100, limit=200) for next 200 lines
|
|
802
|
-
- Only omit limit (read full file) when necessary for editing
|
|
803
|
-
- Specify offset and limit: read_file(path, offset=0, limit=100) reads first 100 lines
|
|
804
|
-
- Results are returned using cat -n format, with line numbers starting at 1
|
|
805
|
-
- Lines longer than 10,000 characters will be split into multiple lines with continuation markers (e.g., 5.1, 5.2, etc.). When you specify a limit, these continuation lines count towards the limit.
|
|
806
|
-
- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.
|
|
807
|
-
- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.
|
|
808
|
-
- You should ALWAYS make sure a file has been read before editing it.`;
|
|
809
|
-
const WRITE_FILE_TOOL_DESCRIPTION = `Writes to a new file in the filesystem.
|
|
1089
|
+
Assume this tool is able to read all files. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
|
|
810
1090
|
|
|
811
|
-
Usage:
|
|
812
|
-
-
|
|
813
|
-
-
|
|
814
|
-
|
|
1091
|
+
Usage:
|
|
1092
|
+
- By default, it reads up to 100 lines starting from the beginning of the file
|
|
1093
|
+
- **IMPORTANT for large files and codebase exploration**: Use pagination with offset and limit parameters to avoid context overflow
|
|
1094
|
+
- First scan: read_file(path, limit=100) to see file structure
|
|
1095
|
+
- Read more sections: read_file(path, offset=100, limit=200) for next 200 lines
|
|
1096
|
+
- Only omit limit (read full file) when necessary for editing
|
|
1097
|
+
- Specify offset and limit: read_file(path, offset=0, limit=100) reads first 100 lines
|
|
1098
|
+
- Results are returned using cat -n format, with line numbers starting at 1
|
|
1099
|
+
- Lines longer than ${INT_FORMATTER.format(MAX_LINE_LENGTH)} characters will be split into multiple lines with continuation markers (e.g., 5.1, 5.2, etc.). When you specify a limit, these continuation lines count towards the limit.
|
|
1100
|
+
- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.
|
|
1101
|
+
- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.
|
|
1102
|
+
- You should ALWAYS make sure a file has been read before editing it.
|
|
1103
|
+
`;
|
|
1104
|
+
const WRITE_FILE_TOOL_DESCRIPTION = langchain.context`
|
|
1105
|
+
Writes to a new file in the filesystem.
|
|
1106
|
+
|
|
1107
|
+
Usage:
|
|
1108
|
+
- The write_file tool will create a new file.
|
|
1109
|
+
- Prefer to edit existing files (with the edit_file tool) over creating new ones when possible.
|
|
1110
|
+
`;
|
|
1111
|
+
const EDIT_FILE_TOOL_DESCRIPTION = langchain.context`
|
|
1112
|
+
Performs exact string replacements in files.
|
|
815
1113
|
|
|
816
|
-
Usage:
|
|
817
|
-
- You must read the file before editing. This tool will error if you attempt an edit without reading the file first.
|
|
818
|
-
- When editing, preserve the exact indentation (tabs/spaces) from the read output. Never include line number prefixes in old_string or new_string.
|
|
819
|
-
- ALWAYS prefer editing existing files over creating new ones.
|
|
820
|
-
- Only use emojis if the user explicitly requests it
|
|
821
|
-
|
|
1114
|
+
Usage:
|
|
1115
|
+
- You must read the file before editing. This tool will error if you attempt an edit without reading the file first.
|
|
1116
|
+
- When editing, preserve the exact indentation (tabs/spaces) from the read output. Never include line number prefixes in old_string or new_string.
|
|
1117
|
+
- ALWAYS prefer editing existing files over creating new ones.
|
|
1118
|
+
- Only use emojis if the user explicitly requests it.
|
|
1119
|
+
`;
|
|
1120
|
+
const GLOB_TOOL_DESCRIPTION = langchain.context`
|
|
1121
|
+
Find files matching a glob pattern.
|
|
822
1122
|
|
|
823
|
-
Supports standard glob patterns: \`*\` (any characters), \`**\` (any directories), \`?\` (single character).
|
|
824
|
-
Returns a list of absolute file paths that match the pattern.
|
|
1123
|
+
Supports standard glob patterns: \`*\` (any characters), \`**\` (any directories), \`?\` (single character).
|
|
1124
|
+
Returns a list of absolute file paths that match the pattern.
|
|
825
1125
|
|
|
826
|
-
Examples:
|
|
827
|
-
- \`**/*.py\` - Find all Python files
|
|
828
|
-
- \`*.txt\` - Find all text files in root
|
|
829
|
-
- \`/subdir/**/*.md\` - Find all markdown files under /subdir
|
|
830
|
-
|
|
1126
|
+
Examples:
|
|
1127
|
+
- \`**/*.py\` - Find all Python files
|
|
1128
|
+
- \`*.txt\` - Find all text files in root
|
|
1129
|
+
- \`/subdir/**/*.md\` - Find all markdown files under /subdir
|
|
1130
|
+
`;
|
|
1131
|
+
const GREP_TOOL_DESCRIPTION = langchain.context`
|
|
1132
|
+
Search for a text pattern across files.
|
|
831
1133
|
|
|
832
|
-
Searches for literal text (not regex) and returns matching files or content based on output_mode.
|
|
833
|
-
Special characters like parentheses, brackets, pipes, etc. are treated as literal characters, not regex operators.
|
|
1134
|
+
Searches for literal text (not regex) and returns matching files or content based on output_mode.
|
|
1135
|
+
Special characters like parentheses, brackets, pipes, etc. are treated as literal characters, not regex operators.
|
|
834
1136
|
|
|
835
|
-
Examples:
|
|
836
|
-
- Search all files: \`grep(pattern="TODO")\`
|
|
837
|
-
- Search Python files only: \`grep(pattern="import", glob="*.py")\`
|
|
838
|
-
- Show matching lines: \`grep(pattern="error", output_mode="content")\`
|
|
839
|
-
- Search for code with special chars: \`grep(pattern="def __init__(self):")
|
|
840
|
-
|
|
1137
|
+
Examples:
|
|
1138
|
+
- Search all files: \`grep(pattern="TODO")\`
|
|
1139
|
+
- Search Python files only: \`grep(pattern="import", glob="*.py")\`
|
|
1140
|
+
- Show matching lines: \`grep(pattern="error", output_mode="content")\`
|
|
1141
|
+
- Search for code with special chars: \`grep(pattern="def __init__(self):")\`
|
|
1142
|
+
`;
|
|
1143
|
+
const EXECUTE_TOOL_DESCRIPTION = langchain.context`
|
|
1144
|
+
Executes a shell command in an isolated sandbox environment.
|
|
841
1145
|
|
|
842
|
-
Usage:
|
|
843
|
-
Executes a given command in the sandbox environment with proper handling and security measures.
|
|
844
|
-
Before executing the command, please follow these steps:
|
|
1146
|
+
Usage:
|
|
1147
|
+
Executes a given command in the sandbox environment with proper handling and security measures.
|
|
1148
|
+
Before executing the command, please follow these steps:
|
|
845
1149
|
|
|
846
|
-
1. Directory Verification:
|
|
847
|
-
|
|
848
|
-
|
|
1150
|
+
1. Directory Verification:
|
|
1151
|
+
- If the command will create new directories or files, first use the ls tool to verify the parent directory exists and is the correct location
|
|
1152
|
+
- For example, before running "mkdir foo/bar", first use ls to check that "foo" exists and is the intended parent directory
|
|
849
1153
|
|
|
850
|
-
2. Command Execution:
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
1154
|
+
2. Command Execution:
|
|
1155
|
+
- Always quote file paths that contain spaces with double quotes (e.g., cd "path with spaces/file.txt")
|
|
1156
|
+
- Examples of proper quoting:
|
|
1157
|
+
- cd "/Users/name/My Documents" (correct)
|
|
1158
|
+
- cd /Users/name/My Documents (incorrect - will fail)
|
|
1159
|
+
- python "/path/with spaces/script.py" (correct)
|
|
1160
|
+
- python /path/with spaces/script.py (incorrect - will fail)
|
|
1161
|
+
- After ensuring proper quoting, execute the command
|
|
1162
|
+
- Capture the output of the command
|
|
859
1163
|
|
|
860
|
-
Usage notes:
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1164
|
+
Usage notes:
|
|
1165
|
+
- Commands run in an isolated sandbox environment
|
|
1166
|
+
- Returns combined stdout/stderr output with exit code
|
|
1167
|
+
- If the output is very large, it may be truncated
|
|
1168
|
+
- VERY IMPORTANT: You MUST avoid using search commands like find and grep. Instead use the grep, glob tools to search. You MUST avoid read tools like cat, head, tail, and use read_file to read files.
|
|
1169
|
+
- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings)
|
|
1170
|
+
- Use '&&' when commands depend on each other (e.g., "mkdir dir && cd dir")
|
|
1171
|
+
- Use ';' only when you need to run commands sequentially but don't care if earlier commands fail
|
|
1172
|
+
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of cd
|
|
869
1173
|
|
|
870
|
-
Examples:
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1174
|
+
Examples:
|
|
1175
|
+
Good examples:
|
|
1176
|
+
- execute(command="pytest /foo/bar/tests")
|
|
1177
|
+
- execute(command="python /path/to/script.py")
|
|
1178
|
+
- execute(command="npm install && npm test")
|
|
875
1179
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1180
|
+
Bad examples (avoid these):
|
|
1181
|
+
- execute(command="cd /foo/bar && pytest tests") # Use absolute path instead
|
|
1182
|
+
- execute(command="cat file.txt") # Use read_file tool instead
|
|
1183
|
+
- execute(command="find . -name '*.py'") # Use glob tool instead
|
|
1184
|
+
- execute(command="grep -r 'pattern' .") # Use grep tool instead
|
|
881
1185
|
|
|
882
|
-
Note: This tool is only available if the backend supports execution (SandboxBackendProtocol).
|
|
883
|
-
If execution is not supported, the tool will return an error message
|
|
884
|
-
|
|
1186
|
+
Note: This tool is only available if the backend supports execution (SandboxBackendProtocol).
|
|
1187
|
+
If execution is not supported, the tool will return an error message.
|
|
1188
|
+
`;
|
|
1189
|
+
const EXECUTION_SYSTEM_PROMPT = langchain.context`
|
|
1190
|
+
## Execute Tool \`execute\`
|
|
885
1191
|
|
|
886
|
-
You have access to an \`execute\` tool for running shell commands in a sandboxed environment.
|
|
887
|
-
Use this tool to run commands, scripts, tests, builds, and other shell operations.
|
|
1192
|
+
You have access to an \`execute\` tool for running shell commands in a sandboxed environment.
|
|
1193
|
+
Use this tool to run commands, scripts, tests, builds, and other shell operations.
|
|
888
1194
|
|
|
889
|
-
- execute: run a shell command in the sandbox (returns output and exit code)
|
|
1195
|
+
- execute: run a shell command in the sandbox (returns output and exit code)
|
|
1196
|
+
`;
|
|
890
1197
|
/**
|
|
891
1198
|
* Create ls tool using backend.
|
|
892
1199
|
*/
|
|
@@ -895,7 +1202,9 @@ function createLsTool(backend, options) {
|
|
|
895
1202
|
return (0, langchain.tool)(async (input, runtime) => {
|
|
896
1203
|
const resolvedBackend = await resolveBackend(backend, runtime);
|
|
897
1204
|
const path = input.path || "/";
|
|
898
|
-
const
|
|
1205
|
+
const lsResult = await resolvedBackend.ls(path);
|
|
1206
|
+
if (lsResult.error) return `Error listing files: ${lsResult.error}`;
|
|
1207
|
+
const infos = lsResult.files || [];
|
|
899
1208
|
if (infos.length === 0) return `No files found in ${path}`;
|
|
900
1209
|
const lines = [];
|
|
901
1210
|
for (const info of infos) if (info.is_dir) lines.push(`${info.path} (directory)`);
|
|
@@ -920,15 +1229,64 @@ function createReadFileTool(backend, options) {
|
|
|
920
1229
|
return (0, langchain.tool)(async (input, runtime) => {
|
|
921
1230
|
const resolvedBackend = await resolveBackend(backend, runtime);
|
|
922
1231
|
const { file_path, offset = 0, limit = 100 } = input;
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1232
|
+
const readResult = await resolvedBackend.read(file_path, offset, limit);
|
|
1233
|
+
if (readResult.error) return [{
|
|
1234
|
+
type: "text",
|
|
1235
|
+
text: `Error: ${readResult.error}`
|
|
1236
|
+
}];
|
|
1237
|
+
const mimeType = readResult.mimeType ?? getMimeType(file_path);
|
|
1238
|
+
if (!isTextMimeType(mimeType)) {
|
|
1239
|
+
const binaryContent = readResult.content;
|
|
1240
|
+
if (!binaryContent) return [{
|
|
1241
|
+
type: "text",
|
|
1242
|
+
text: `Error: expected binary content for '${file_path}'`
|
|
1243
|
+
}];
|
|
1244
|
+
let base64Data;
|
|
1245
|
+
if (typeof binaryContent === "string") base64Data = binaryContent;
|
|
1246
|
+
else if (ArrayBuffer.isView(binaryContent)) base64Data = Buffer.from(binaryContent).toString("base64");
|
|
1247
|
+
else {
|
|
1248
|
+
const values = Object.values(binaryContent);
|
|
1249
|
+
base64Data = Buffer.from(new Uint8Array(values)).toString("base64");
|
|
1250
|
+
}
|
|
1251
|
+
const sizeBytes = Math.ceil(base64Data.length * 3 / 4);
|
|
1252
|
+
if (sizeBytes > 10485760) return [{
|
|
1253
|
+
type: "text",
|
|
1254
|
+
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)`
|
|
1255
|
+
}];
|
|
1256
|
+
if (mimeType.startsWith("image/")) return [{
|
|
1257
|
+
type: "image",
|
|
1258
|
+
mimeType,
|
|
1259
|
+
data: base64Data
|
|
1260
|
+
}];
|
|
1261
|
+
if (mimeType.startsWith("audio/")) return [{
|
|
1262
|
+
type: "audio",
|
|
1263
|
+
mimeType,
|
|
1264
|
+
data: base64Data
|
|
1265
|
+
}];
|
|
1266
|
+
if (mimeType.startsWith("video/")) return [{
|
|
1267
|
+
type: "video",
|
|
1268
|
+
mimeType,
|
|
1269
|
+
data: base64Data
|
|
1270
|
+
}];
|
|
1271
|
+
return [{
|
|
1272
|
+
type: "file",
|
|
1273
|
+
mimeType,
|
|
1274
|
+
data: base64Data
|
|
1275
|
+
}];
|
|
1276
|
+
}
|
|
1277
|
+
let content = typeof readResult.content === "string" ? readResult.content : "";
|
|
1278
|
+
const lines = content.split("\n");
|
|
1279
|
+
if (lines.length > limit) content = lines.slice(0, limit).join("\n");
|
|
1280
|
+
let formatted = formatContentWithLineNumbers(content, offset + 1);
|
|
1281
|
+
if (toolTokenLimitBeforeEvict && formatted.length >= 4 * toolTokenLimitBeforeEvict) {
|
|
927
1282
|
const truncationMsg = READ_FILE_TRUNCATION_MSG.replace("{file_path}", file_path);
|
|
928
1283
|
const maxContentLength = 4 * toolTokenLimitBeforeEvict - truncationMsg.length;
|
|
929
|
-
|
|
1284
|
+
formatted = formatted.substring(0, maxContentLength) + truncationMsg;
|
|
930
1285
|
}
|
|
931
|
-
return
|
|
1286
|
+
return [{
|
|
1287
|
+
type: "text",
|
|
1288
|
+
text: formatted
|
|
1289
|
+
}];
|
|
932
1290
|
}, {
|
|
933
1291
|
name: "read_file",
|
|
934
1292
|
description: customDescription || READ_FILE_TOOL_DESCRIPTION,
|
|
@@ -1009,7 +1367,9 @@ function createGlobTool(backend, options) {
|
|
|
1009
1367
|
return (0, langchain.tool)(async (input, runtime) => {
|
|
1010
1368
|
const resolvedBackend = await resolveBackend(backend, runtime);
|
|
1011
1369
|
const { pattern, path = "/" } = input;
|
|
1012
|
-
const
|
|
1370
|
+
const globResult = await resolvedBackend.glob(pattern, path);
|
|
1371
|
+
if (globResult.error) return `Error finding files: ${globResult.error}`;
|
|
1372
|
+
const infos = globResult.files || [];
|
|
1013
1373
|
if (infos.length === 0) return `No files found matching pattern '${pattern}'`;
|
|
1014
1374
|
const result = truncateIfTooLong(infos.map((info) => info.path));
|
|
1015
1375
|
if (Array.isArray(result)) return result.join("\n");
|
|
@@ -1031,12 +1391,13 @@ function createGrepTool(backend, options) {
|
|
|
1031
1391
|
return (0, langchain.tool)(async (input, runtime) => {
|
|
1032
1392
|
const resolvedBackend = await resolveBackend(backend, runtime);
|
|
1033
1393
|
const { pattern, path = "/", glob = null } = input;
|
|
1034
|
-
const result = await resolvedBackend.
|
|
1035
|
-
if (
|
|
1036
|
-
|
|
1394
|
+
const result = await resolvedBackend.grep(pattern, path, glob);
|
|
1395
|
+
if (result.error) return result.error;
|
|
1396
|
+
const matches = result.matches ?? [];
|
|
1397
|
+
if (matches.length === 0) return `No matches found for pattern '${pattern}'`;
|
|
1037
1398
|
const lines = [];
|
|
1038
1399
|
let currentFile = null;
|
|
1039
|
-
for (const match of
|
|
1400
|
+
for (const match of matches) {
|
|
1040
1401
|
if (match.path !== currentFile) {
|
|
1041
1402
|
currentFile = match.path;
|
|
1042
1403
|
lines.push(`\n${currentFile}:`);
|
|
@@ -1052,7 +1413,7 @@ function createGrepTool(backend, options) {
|
|
|
1052
1413
|
schema: zod_v4.z.object({
|
|
1053
1414
|
pattern: zod_v4.z.string().describe("Regex pattern to search for"),
|
|
1054
1415
|
path: zod_v4.z.string().optional().default("/").describe("Base path to search from (default: /)"),
|
|
1055
|
-
glob: zod_v4.z.string().optional().nullable().describe("Optional glob pattern to filter files (e.g., '*.py')")
|
|
1416
|
+
glob: zod_v4.z.string().optional().nullable().default(null).describe("Optional glob pattern to filter files (e.g., '*.py')")
|
|
1056
1417
|
})
|
|
1057
1418
|
});
|
|
1058
1419
|
}
|
|
@@ -1252,117 +1613,117 @@ const EXCLUDED_STATE_KEYS = [
|
|
|
1252
1613
|
*/
|
|
1253
1614
|
const DEFAULT_GENERAL_PURPOSE_DESCRIPTION = "General-purpose agent for researching complex questions, searching for files and content, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you. This agent has access to all tools as the main agent.";
|
|
1254
1615
|
function getTaskToolDescription(subagentDescriptions) {
|
|
1255
|
-
return `
|
|
1256
|
-
Launch an ephemeral subagent to handle complex, multi-step independent tasks with isolated context windows.
|
|
1616
|
+
return langchain.context`
|
|
1617
|
+
Launch an ephemeral subagent to handle complex, multi-step independent tasks with isolated context windows.
|
|
1257
1618
|
|
|
1258
|
-
Available agent types and the tools they have access to:
|
|
1259
|
-
${subagentDescriptions.join("\n")}
|
|
1619
|
+
Available agent types and the tools they have access to:
|
|
1620
|
+
${subagentDescriptions.join("\n")}
|
|
1260
1621
|
|
|
1261
|
-
When using the Task tool, you must specify a subagent_type parameter to select which agent type to use.
|
|
1622
|
+
When using the Task tool, you must specify a subagent_type parameter to select which agent type to use.
|
|
1262
1623
|
|
|
1263
|
-
## Usage notes:
|
|
1264
|
-
1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
|
|
1265
|
-
2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
|
|
1266
|
-
3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
|
|
1267
|
-
4. The agent's outputs should generally be trusted
|
|
1268
|
-
5. Clearly tell the agent whether you expect it to create content, perform analysis, or just do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent
|
|
1269
|
-
6. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
|
|
1270
|
-
7. When only the general-purpose agent is provided, you should use it for all tasks. It is great for isolating context and token usage, and completing specific, complex tasks, as it has all the same capabilities as the main agent.
|
|
1624
|
+
## Usage notes:
|
|
1625
|
+
1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
|
|
1626
|
+
2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
|
|
1627
|
+
3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
|
|
1628
|
+
4. The agent's outputs should generally be trusted
|
|
1629
|
+
5. Clearly tell the agent whether you expect it to create content, perform analysis, or just do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent
|
|
1630
|
+
6. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
|
|
1631
|
+
7. When only the general-purpose agent is provided, you should use it for all tasks. It is great for isolating context and token usage, and completing specific, complex tasks, as it has all the same capabilities as the main agent.
|
|
1271
1632
|
|
|
1272
|
-
### Example usage of the general-purpose agent:
|
|
1633
|
+
### Example usage of the general-purpose agent:
|
|
1273
1634
|
|
|
1274
|
-
<example_agent_descriptions>
|
|
1275
|
-
"general-purpose": use this agent for general purpose tasks, it has access to all tools as the main agent.
|
|
1276
|
-
</example_agent_descriptions>
|
|
1635
|
+
<example_agent_descriptions>
|
|
1636
|
+
"general-purpose": use this agent for general purpose tasks, it has access to all tools as the main agent.
|
|
1637
|
+
</example_agent_descriptions>
|
|
1277
1638
|
|
|
1278
|
-
<example>
|
|
1279
|
-
User: "I want to conduct research on the accomplishments of Lebron James, Michael Jordan, and Kobe Bryant, and then compare them."
|
|
1280
|
-
Assistant: *Uses the task tool in parallel to conduct isolated research on each of the three players*
|
|
1281
|
-
Assistant: *Synthesizes the results of the three isolated research tasks and responds to the User*
|
|
1282
|
-
<commentary>
|
|
1283
|
-
Research is a complex, multi-step task in it of itself.
|
|
1284
|
-
The research of each individual player is not dependent on the research of the other players.
|
|
1285
|
-
The assistant uses the task tool to break down the complex objective into three isolated tasks.
|
|
1286
|
-
Each research task only needs to worry about context and tokens about one player, then returns synthesized information about each player as the Tool Result.
|
|
1287
|
-
This means each research task can dive deep and spend tokens and context deeply researching each player, but the final result is synthesized information, and saves us tokens in the long run when comparing the players to each other.
|
|
1288
|
-
</commentary>
|
|
1289
|
-
</example>
|
|
1639
|
+
<example>
|
|
1640
|
+
User: "I want to conduct research on the accomplishments of Lebron James, Michael Jordan, and Kobe Bryant, and then compare them."
|
|
1641
|
+
Assistant: *Uses the task tool in parallel to conduct isolated research on each of the three players*
|
|
1642
|
+
Assistant: *Synthesizes the results of the three isolated research tasks and responds to the User*
|
|
1643
|
+
<commentary>
|
|
1644
|
+
Research is a complex, multi-step task in it of itself.
|
|
1645
|
+
The research of each individual player is not dependent on the research of the other players.
|
|
1646
|
+
The assistant uses the task tool to break down the complex objective into three isolated tasks.
|
|
1647
|
+
Each research task only needs to worry about context and tokens about one player, then returns synthesized information about each player as the Tool Result.
|
|
1648
|
+
This means each research task can dive deep and spend tokens and context deeply researching each player, but the final result is synthesized information, and saves us tokens in the long run when comparing the players to each other.
|
|
1649
|
+
</commentary>
|
|
1650
|
+
</example>
|
|
1290
1651
|
|
|
1291
|
-
<example>
|
|
1292
|
-
User: "Analyze a single large code repository for security vulnerabilities and generate a report."
|
|
1293
|
-
Assistant: *Launches a single \`task\` subagent for the repository analysis*
|
|
1294
|
-
Assistant: *Receives report and integrates results into final summary*
|
|
1295
|
-
<commentary>
|
|
1296
|
-
Subagent is used to isolate a large, context-heavy task, even though there is only one. This prevents the main thread from being overloaded with details.
|
|
1297
|
-
If the user then asks followup questions, we have a concise report to reference instead of the entire history of analysis and tool calls, which is good and saves us time and money.
|
|
1298
|
-
</commentary>
|
|
1299
|
-
</example>
|
|
1652
|
+
<example>
|
|
1653
|
+
User: "Analyze a single large code repository for security vulnerabilities and generate a report."
|
|
1654
|
+
Assistant: *Launches a single \`task\` subagent for the repository analysis*
|
|
1655
|
+
Assistant: *Receives report and integrates results into final summary*
|
|
1656
|
+
<commentary>
|
|
1657
|
+
Subagent is used to isolate a large, context-heavy task, even though there is only one. This prevents the main thread from being overloaded with details.
|
|
1658
|
+
If the user then asks followup questions, we have a concise report to reference instead of the entire history of analysis and tool calls, which is good and saves us time and money.
|
|
1659
|
+
</commentary>
|
|
1660
|
+
</example>
|
|
1300
1661
|
|
|
1301
|
-
<example>
|
|
1302
|
-
User: "Schedule two meetings for me and prepare agendas for each."
|
|
1303
|
-
Assistant: *Calls the task tool in parallel to launch two \`task\` subagents (one per meeting) to prepare agendas*
|
|
1304
|
-
Assistant: *Returns final schedules and agendas*
|
|
1305
|
-
<commentary>
|
|
1306
|
-
Tasks are simple individually, but subagents help silo agenda preparation.
|
|
1307
|
-
Each subagent only needs to worry about the agenda for one meeting.
|
|
1308
|
-
</commentary>
|
|
1309
|
-
</example>
|
|
1662
|
+
<example>
|
|
1663
|
+
User: "Schedule two meetings for me and prepare agendas for each."
|
|
1664
|
+
Assistant: *Calls the task tool in parallel to launch two \`task\` subagents (one per meeting) to prepare agendas*
|
|
1665
|
+
Assistant: *Returns final schedules and agendas*
|
|
1666
|
+
<commentary>
|
|
1667
|
+
Tasks are simple individually, but subagents help silo agenda preparation.
|
|
1668
|
+
Each subagent only needs to worry about the agenda for one meeting.
|
|
1669
|
+
</commentary>
|
|
1670
|
+
</example>
|
|
1310
1671
|
|
|
1311
|
-
<example>
|
|
1312
|
-
User: "I want to order a pizza from Dominos, order a burger from McDonald's, and order a salad from Subway."
|
|
1313
|
-
Assistant: *Calls tools directly in parallel to order a pizza from Dominos, a burger from McDonald's, and a salad from Subway*
|
|
1314
|
-
<commentary>
|
|
1315
|
-
The assistant did not use the task tool because the objective is super simple and clear and only requires a few trivial tool calls.
|
|
1316
|
-
It is better to just complete the task directly and NOT use the \`task\`tool.
|
|
1317
|
-
</commentary>
|
|
1318
|
-
</example>
|
|
1672
|
+
<example>
|
|
1673
|
+
User: "I want to order a pizza from Dominos, order a burger from McDonald's, and order a salad from Subway."
|
|
1674
|
+
Assistant: *Calls tools directly in parallel to order a pizza from Dominos, a burger from McDonald's, and a salad from Subway*
|
|
1675
|
+
<commentary>
|
|
1676
|
+
The assistant did not use the task tool because the objective is super simple and clear and only requires a few trivial tool calls.
|
|
1677
|
+
It is better to just complete the task directly and NOT use the \`task\`tool.
|
|
1678
|
+
</commentary>
|
|
1679
|
+
</example>
|
|
1319
1680
|
|
|
1320
|
-
### Example usage with custom agents:
|
|
1681
|
+
### Example usage with custom agents:
|
|
1321
1682
|
|
|
1322
|
-
<example_agent_descriptions>
|
|
1323
|
-
"content-reviewer": use this agent after you are done creating significant content or documents
|
|
1324
|
-
"greeting-responder": use this agent when to respond to user greetings with a friendly joke
|
|
1325
|
-
"research-analyst": use this agent to conduct thorough research on complex topics
|
|
1326
|
-
</example_agent_description>
|
|
1683
|
+
<example_agent_descriptions>
|
|
1684
|
+
"content-reviewer": use this agent after you are done creating significant content or documents
|
|
1685
|
+
"greeting-responder": use this agent when to respond to user greetings with a friendly joke
|
|
1686
|
+
"research-analyst": use this agent to conduct thorough research on complex topics
|
|
1687
|
+
</example_agent_description>
|
|
1327
1688
|
|
|
1328
|
-
<example>
|
|
1329
|
-
user: "Please write a function that checks if a number is prime"
|
|
1330
|
-
assistant: Sure let me write a function that checks if a number is prime
|
|
1331
|
-
assistant: First let me use the Write tool to write a function that checks if a number is prime
|
|
1332
|
-
assistant: I'm going to use the Write tool to write the following code:
|
|
1333
|
-
<code>
|
|
1334
|
-
function isPrime(n) {
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
}
|
|
1341
|
-
</code>
|
|
1342
|
-
<commentary>
|
|
1343
|
-
Since significant content was created and the task was completed, now use the content-reviewer agent to review the work
|
|
1344
|
-
</commentary>
|
|
1345
|
-
assistant: Now let me use the content-reviewer agent to review the code
|
|
1346
|
-
assistant: Uses the Task tool to launch with the content-reviewer agent
|
|
1347
|
-
</example>
|
|
1689
|
+
<example>
|
|
1690
|
+
user: "Please write a function that checks if a number is prime"
|
|
1691
|
+
assistant: Sure let me write a function that checks if a number is prime
|
|
1692
|
+
assistant: First let me use the Write tool to write a function that checks if a number is prime
|
|
1693
|
+
assistant: I'm going to use the Write tool to write the following code:
|
|
1694
|
+
<code>
|
|
1695
|
+
function isPrime(n) {{
|
|
1696
|
+
if (n <= 1) return false
|
|
1697
|
+
for (let i = 2; i * i <= n; i++) {{
|
|
1698
|
+
if (n % i === 0) return false
|
|
1699
|
+
}}
|
|
1700
|
+
return true
|
|
1701
|
+
}}
|
|
1702
|
+
</code>
|
|
1703
|
+
<commentary>
|
|
1704
|
+
Since significant content was created and the task was completed, now use the content-reviewer agent to review the work
|
|
1705
|
+
</commentary>
|
|
1706
|
+
assistant: Now let me use the content-reviewer agent to review the code
|
|
1707
|
+
assistant: Uses the Task tool to launch with the content-reviewer agent
|
|
1708
|
+
</example>
|
|
1348
1709
|
|
|
1349
|
-
<example>
|
|
1350
|
-
user: "Can you help me research the environmental impact of different renewable energy sources and create a comprehensive report?"
|
|
1351
|
-
<commentary>
|
|
1352
|
-
This is a complex research task that would benefit from using the research-analyst agent to conduct thorough analysis
|
|
1353
|
-
</commentary>
|
|
1354
|
-
assistant: I'll help you research the environmental impact of renewable energy sources. Let me use the research-analyst agent to conduct comprehensive research on this topic.
|
|
1355
|
-
assistant: Uses the Task tool to launch with the research-analyst agent, providing detailed instructions about what research to conduct and what format the report should take
|
|
1356
|
-
</example>
|
|
1710
|
+
<example>
|
|
1711
|
+
user: "Can you help me research the environmental impact of different renewable energy sources and create a comprehensive report?"
|
|
1712
|
+
<commentary>
|
|
1713
|
+
This is a complex research task that would benefit from using the research-analyst agent to conduct thorough analysis
|
|
1714
|
+
</commentary>
|
|
1715
|
+
assistant: I'll help you research the environmental impact of renewable energy sources. Let me use the research-analyst agent to conduct comprehensive research on this topic.
|
|
1716
|
+
assistant: Uses the Task tool to launch with the research-analyst agent, providing detailed instructions about what research to conduct and what format the report should take
|
|
1717
|
+
</example>
|
|
1357
1718
|
|
|
1358
|
-
<example>
|
|
1359
|
-
user: "Hello"
|
|
1360
|
-
<commentary>
|
|
1361
|
-
Since the user is greeting, use the greeting-responder agent to respond with a friendly joke
|
|
1362
|
-
</commentary>
|
|
1363
|
-
assistant: "I'm going to use the Task tool to launch with the greeting-responder agent"
|
|
1364
|
-
</example>
|
|
1365
|
-
|
|
1719
|
+
<example>
|
|
1720
|
+
user: "Hello"
|
|
1721
|
+
<commentary>
|
|
1722
|
+
Since the user is greeting, use the greeting-responder agent to respond with a friendly joke
|
|
1723
|
+
</commentary>
|
|
1724
|
+
assistant: "I'm going to use the Task tool to launch with the greeting-responder agent"
|
|
1725
|
+
</example>
|
|
1726
|
+
`;
|
|
1366
1727
|
}
|
|
1367
1728
|
/**
|
|
1368
1729
|
* System prompt section that explains how to use the task tool for spawning subagents.
|
|
@@ -1377,33 +1738,35 @@ assistant: "I'm going to use the Task tool to launch with the greeting-responder
|
|
|
1377
1738
|
* You can provide a custom `systemPrompt` to `createSubAgentMiddleware` to override
|
|
1378
1739
|
* or extend this default.
|
|
1379
1740
|
*/
|
|
1380
|
-
const TASK_SYSTEM_PROMPT =
|
|
1741
|
+
const TASK_SYSTEM_PROMPT = langchain.context`
|
|
1742
|
+
## \`task\` (subagent spawner)
|
|
1381
1743
|
|
|
1382
|
-
You have access to a \`task\` tool to launch short-lived subagents that handle isolated tasks. These agents are ephemeral — they live only for the duration of the task and return a single result.
|
|
1744
|
+
You have access to a \`task\` tool to launch short-lived subagents that handle isolated tasks. These agents are ephemeral — they live only for the duration of the task and return a single result.
|
|
1383
1745
|
|
|
1384
|
-
When to use the task tool:
|
|
1385
|
-
- When a task is complex and multi-step, and can be fully delegated in isolation
|
|
1386
|
-
- When a task is independent of other tasks and can run in parallel
|
|
1387
|
-
- When a task requires focused reasoning or heavy token/context usage that would bloat the orchestrator thread
|
|
1388
|
-
- When sandboxing improves reliability (e.g. code execution, structured searches, data formatting)
|
|
1389
|
-
- When you only care about the output of the subagent, and not the intermediate steps (ex. performing a lot of research and then returned a synthesized report, performing a series of computations or lookups to achieve a concise, relevant answer.)
|
|
1746
|
+
When to use the task tool:
|
|
1747
|
+
- When a task is complex and multi-step, and can be fully delegated in isolation
|
|
1748
|
+
- When a task is independent of other tasks and can run in parallel
|
|
1749
|
+
- When a task requires focused reasoning or heavy token/context usage that would bloat the orchestrator thread
|
|
1750
|
+
- When sandboxing improves reliability (e.g. code execution, structured searches, data formatting)
|
|
1751
|
+
- When you only care about the output of the subagent, and not the intermediate steps (ex. performing a lot of research and then returned a synthesized report, performing a series of computations or lookups to achieve a concise, relevant answer.)
|
|
1390
1752
|
|
|
1391
|
-
Subagent lifecycle:
|
|
1392
|
-
1. **Spawn** → Provide clear role, instructions, and expected output
|
|
1393
|
-
2. **Run** → The subagent completes the task autonomously
|
|
1394
|
-
3. **Return** → The subagent provides a single structured result
|
|
1395
|
-
4. **Reconcile** → Incorporate or synthesize the result into the main thread
|
|
1753
|
+
Subagent lifecycle:
|
|
1754
|
+
1. **Spawn** → Provide clear role, instructions, and expected output
|
|
1755
|
+
2. **Run** → The subagent completes the task autonomously
|
|
1756
|
+
3. **Return** → The subagent provides a single structured result
|
|
1757
|
+
4. **Reconcile** → Incorporate or synthesize the result into the main thread
|
|
1396
1758
|
|
|
1397
|
-
When NOT to use the task tool:
|
|
1398
|
-
- If you need to see the intermediate reasoning or steps after the subagent has completed (the task tool hides them)
|
|
1399
|
-
- If the task is trivial (a few tool calls or simple lookup)
|
|
1400
|
-
- If delegating does not reduce token usage, complexity, or context switching
|
|
1401
|
-
- If splitting would add latency without benefit
|
|
1759
|
+
When NOT to use the task tool:
|
|
1760
|
+
- If you need to see the intermediate reasoning or steps after the subagent has completed (the task tool hides them)
|
|
1761
|
+
- If the task is trivial (a few tool calls or simple lookup)
|
|
1762
|
+
- If delegating does not reduce token usage, complexity, or context switching
|
|
1763
|
+
- If splitting would add latency without benefit
|
|
1402
1764
|
|
|
1403
|
-
## Important Task Tool Usage Notes to Remember
|
|
1404
|
-
- Whenever possible, parallelize the work that you do. This is true for both tool_calls, and for tasks. Whenever you have independent steps to complete - make tool_calls, or kick off tasks (subagents) in parallel to accomplish them faster. This saves time for the user, which is incredibly important.
|
|
1405
|
-
- Remember to use the \`task\` tool to silo independent tasks within a multi-part objective.
|
|
1406
|
-
- You should use the \`task\` tool whenever you have a complex task that will take multiple steps, and is independent from other tasks that the agent needs to complete. These agents are highly competent and efficient
|
|
1765
|
+
## Important Task Tool Usage Notes to Remember
|
|
1766
|
+
- Whenever possible, parallelize the work that you do. This is true for both tool_calls, and for tasks. Whenever you have independent steps to complete - make tool_calls, or kick off tasks (subagents) in parallel to accomplish them faster. This saves time for the user, which is incredibly important.
|
|
1767
|
+
- Remember to use the \`task\` tool to silo independent tasks within a multi-part objective.
|
|
1768
|
+
- You should use the \`task\` tool whenever you have a complex task that will take multiple steps, and is independent from other tasks that the agent needs to complete. These agents are highly competent and efficient.
|
|
1769
|
+
`;
|
|
1407
1770
|
/**
|
|
1408
1771
|
* Base specification for the general-purpose subagent.
|
|
1409
1772
|
*
|
|
@@ -1812,65 +2175,67 @@ const MemoryStateSchema = new _langchain_langgraph.StateSchema({
|
|
|
1812
2175
|
* Default system prompt template for memory.
|
|
1813
2176
|
* Ported from Python's comprehensive memory guidelines.
|
|
1814
2177
|
*/
|
|
1815
|
-
const MEMORY_SYSTEM_PROMPT =
|
|
1816
|
-
|
|
1817
|
-
|
|
2178
|
+
const MEMORY_SYSTEM_PROMPT = langchain.context`
|
|
2179
|
+
<agent_memory>
|
|
2180
|
+
{memory_contents}
|
|
2181
|
+
</agent_memory>
|
|
1818
2182
|
|
|
1819
|
-
<memory_guidelines>
|
|
1820
|
-
|
|
2183
|
+
<memory_guidelines>
|
|
2184
|
+
The above <agent_memory> was loaded in from files in your filesystem. As you learn from your interactions with the user, you can save new knowledge by calling the \`edit_file\` tool.
|
|
1821
2185
|
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
2186
|
+
**Learning from feedback:**
|
|
2187
|
+
- One of your MAIN PRIORITIES is to learn from your interactions with the user. These learnings can be implicit or explicit. This means that in the future, you will remember this important information.
|
|
2188
|
+
- When you need to remember something, updating memory must be your FIRST, IMMEDIATE action - before responding to the user, before calling other tools, before doing anything else. Just update memory immediately.
|
|
2189
|
+
- When user says something is better/worse, capture WHY and encode it as a pattern.
|
|
2190
|
+
- Each correction is a chance to improve permanently - don't just fix the immediate issue, update your instructions.
|
|
2191
|
+
- A great opportunity to update your memories is when the user interrupts a tool call and provides feedback. You should update your memories immediately before revising the tool call.
|
|
2192
|
+
- Look for the underlying principle behind corrections, not just the specific mistake.
|
|
2193
|
+
- The user might not explicitly ask you to remember something, but if they provide information that is useful for future use, you should update your memories immediately.
|
|
1830
2194
|
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
2195
|
+
**Asking for information:**
|
|
2196
|
+
- If you lack context to perform an action (e.g. send a Slack DM, requires a user ID/email) you should explicitly ask the user for this information.
|
|
2197
|
+
- It is preferred for you to ask for information, don't assume anything that you do not know!
|
|
2198
|
+
- When the user provides information that is useful for future use, you should update your memories immediately.
|
|
1835
2199
|
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
2200
|
+
**When to update memories:**
|
|
2201
|
+
- When the user explicitly asks you to remember something (e.g., "remember my email", "save this preference")
|
|
2202
|
+
- When the user describes your role or how you should behave (e.g., "you are a web researcher", "always do X")
|
|
2203
|
+
- When the user gives feedback on your work - capture what was wrong and how to improve
|
|
2204
|
+
- When the user provides information required for tool use (e.g., slack channel ID, email addresses)
|
|
2205
|
+
- When the user provides context useful for future tasks, such as how to use tools, or which actions to take in a particular situation
|
|
2206
|
+
- When you discover new patterns or preferences (coding styles, conventions, workflows)
|
|
1843
2207
|
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
2208
|
+
**When to NOT update memories:**
|
|
2209
|
+
- When the information is temporary or transient (e.g., "I'm running late", "I'm on my phone right now")
|
|
2210
|
+
- When the information is a one-time task request (e.g., "Find me a recipe", "What's 25 * 4?")
|
|
2211
|
+
- When the information is a simple question that doesn't reveal lasting preferences (e.g., "What day is it?", "Can you explain X?")
|
|
2212
|
+
- When the information is an acknowledgment or small talk (e.g., "Sounds good!", "Hello", "Thanks for that")
|
|
2213
|
+
- When the information is stale or irrelevant in future conversations
|
|
2214
|
+
- Never store API keys, access tokens, passwords, or any other credentials in any file, memory, or system prompt.
|
|
2215
|
+
- If the user asks where to put API keys or provides an API key, do NOT echo or save it.
|
|
1852
2216
|
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
2217
|
+
**Examples:**
|
|
2218
|
+
Example 1 (remembering user information):
|
|
2219
|
+
User: Can you connect to my google account?
|
|
2220
|
+
Agent: Sure, I'll connect to your google account, what's your google account email?
|
|
2221
|
+
User: john@example.com
|
|
2222
|
+
Agent: Let me save this to my memory.
|
|
2223
|
+
Tool Call: edit_file(...) -> remembers that the user's google account email is john@example.com
|
|
1860
2224
|
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
2225
|
+
Example 2 (remembering implicit user preferences):
|
|
2226
|
+
User: Can you write me an example for creating a deep agent in LangChain?
|
|
2227
|
+
Agent: Sure, I'll write you an example for creating a deep agent in LangChain <example code in Python>
|
|
2228
|
+
User: Can you do this in JavaScript
|
|
2229
|
+
Agent: Let me save this to my memory.
|
|
2230
|
+
Tool Call: edit_file(...) -> remembers that the user prefers to get LangChain code examples in JavaScript
|
|
2231
|
+
Agent: Sure, here is the JavaScript example<example code in JavaScript>
|
|
1868
2232
|
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
</memory_guidelines
|
|
2233
|
+
Example 3 (do not remember transient information):
|
|
2234
|
+
User: I'm going to play basketball tonight so I will be offline for a few hours.
|
|
2235
|
+
Agent: Okay I'll add a block to your calendar.
|
|
2236
|
+
Tool Call: create_calendar_event(...) -> just calls a tool, does not commit anything to memory, as it is transient information
|
|
2237
|
+
</memory_guidelines>
|
|
2238
|
+
`;
|
|
1874
2239
|
/**
|
|
1875
2240
|
* Format loaded memory contents for injection into prompt.
|
|
1876
2241
|
* Pairs memory locations with their contents for clarity.
|
|
@@ -1890,12 +2255,14 @@ function formatMemoryContents(contents, sources) {
|
|
|
1890
2255
|
* @returns File content if found, null otherwise.
|
|
1891
2256
|
*/
|
|
1892
2257
|
async function loadMemoryFromBackend(backend, path) {
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
return
|
|
1897
|
-
|
|
1898
|
-
|
|
2258
|
+
const adaptedBackend = adaptBackendProtocol(backend);
|
|
2259
|
+
if (!adaptedBackend.downloadFiles) {
|
|
2260
|
+
const content = await adaptedBackend.read(path);
|
|
2261
|
+
if (content.error) return null;
|
|
2262
|
+
if (typeof content.content !== "string") return null;
|
|
2263
|
+
return content.content;
|
|
2264
|
+
}
|
|
2265
|
+
const results = await adaptedBackend.downloadFiles([path]);
|
|
1899
2266
|
if (results.length !== 1) throw new Error(`Expected 1 response for path ${path}, got ${results.length}`);
|
|
1900
2267
|
const response = results[0];
|
|
1901
2268
|
if (response.error != null) {
|
|
@@ -2066,7 +2433,7 @@ Skills follow a **progressive disclosure** pattern - you know they exist (name +
|
|
|
2066
2433
|
1. **Recognize when a skill applies**: Check if the user's task matches any skill's description
|
|
2067
2434
|
2. **Read the skill's full instructions**: The skill list above shows the exact path to use with read_file
|
|
2068
2435
|
3. **Follow the skill's instructions**: SKILL.md contains step-by-step workflows, best practices, and examples
|
|
2069
|
-
4. **Access supporting files**: Skills may include
|
|
2436
|
+
4. **Access supporting files**: Skills may include scripts, configs, or reference docs - use absolute paths
|
|
2070
2437
|
|
|
2071
2438
|
**When to Use Skills:**
|
|
2072
2439
|
- When the user's request matches a skill's domain (e.g., "research X" → web-research skill)
|
|
@@ -2078,7 +2445,7 @@ Skills follow a **progressive disclosure** pattern - you know they exist (name +
|
|
|
2078
2445
|
- The skill list above shows the full path for each skill's SKILL.md file
|
|
2079
2446
|
|
|
2080
2447
|
**Executing Skill Scripts:**
|
|
2081
|
-
Skills may contain
|
|
2448
|
+
Skills may contain scripts or other executable files. Always use absolute paths from the skill list.
|
|
2082
2449
|
|
|
2083
2450
|
**Example Workflow:**
|
|
2084
2451
|
|
|
@@ -2247,12 +2614,15 @@ function parseSkillMetadataFromContent(content, skillPath, directoryName) {
|
|
|
2247
2614
|
* List all skills from a backend source.
|
|
2248
2615
|
*/
|
|
2249
2616
|
async function listSkillsFromBackend(backend, sourcePath) {
|
|
2617
|
+
const adaptedBackend = adaptBackendProtocol(backend);
|
|
2250
2618
|
const skills = [];
|
|
2251
2619
|
const pathSep = sourcePath.includes("\\") ? "\\" : "/";
|
|
2252
2620
|
const normalizedPath = sourcePath.endsWith("/") || sourcePath.endsWith("\\") ? sourcePath : `${sourcePath}${pathSep}`;
|
|
2253
2621
|
let fileInfos;
|
|
2254
2622
|
try {
|
|
2255
|
-
|
|
2623
|
+
const lsResult = await adaptedBackend.ls(normalizedPath);
|
|
2624
|
+
if (lsResult.error || !lsResult.files) return [];
|
|
2625
|
+
fileInfos = lsResult.files;
|
|
2256
2626
|
} catch {
|
|
2257
2627
|
return [];
|
|
2258
2628
|
}
|
|
@@ -2264,16 +2634,17 @@ async function listSkillsFromBackend(backend, sourcePath) {
|
|
|
2264
2634
|
if (entry.type !== "directory") continue;
|
|
2265
2635
|
const skillMdPath = `${normalizedPath}${entry.name}${pathSep}SKILL.md`;
|
|
2266
2636
|
let content;
|
|
2267
|
-
if (
|
|
2268
|
-
const results = await
|
|
2637
|
+
if (adaptedBackend.downloadFiles) {
|
|
2638
|
+
const results = await adaptedBackend.downloadFiles([skillMdPath]);
|
|
2269
2639
|
if (results.length !== 1) continue;
|
|
2270
2640
|
const response = results[0];
|
|
2271
2641
|
if (response.error != null || response.content == null) continue;
|
|
2272
2642
|
content = new TextDecoder().decode(response.content);
|
|
2273
2643
|
} else {
|
|
2274
|
-
const readResult = await
|
|
2275
|
-
if (readResult.
|
|
2276
|
-
content
|
|
2644
|
+
const readResult = await adaptedBackend.read(skillMdPath);
|
|
2645
|
+
if (readResult.error) continue;
|
|
2646
|
+
if (typeof readResult.content !== "string") continue;
|
|
2647
|
+
content = readResult.content;
|
|
2277
2648
|
}
|
|
2278
2649
|
const metadata = parseSkillMetadataFromContent(content, skillMdPath, entry.name);
|
|
2279
2650
|
if (metadata) skills.push(metadata);
|
|
@@ -2310,71 +2681,299 @@ function formatSkillsList(skills, sources) {
|
|
|
2310
2681
|
if (skill.allowedTools && skill.allowedTools.length > 0) lines.push(` → Allowed tools: ${skill.allowedTools.join(", ")}`);
|
|
2311
2682
|
lines.push(` → Read \`${skill.path}\` for full instructions`);
|
|
2312
2683
|
}
|
|
2313
|
-
return lines.join("\n");
|
|
2684
|
+
return lines.join("\n");
|
|
2685
|
+
}
|
|
2686
|
+
/**
|
|
2687
|
+
* Create backend-agnostic middleware for loading and exposing agent skills.
|
|
2688
|
+
*
|
|
2689
|
+
* This middleware loads skills from configurable backend sources and injects
|
|
2690
|
+
* skill metadata into the system prompt. It implements the progressive disclosure
|
|
2691
|
+
* pattern: skill names and descriptions are shown in the prompt, but the agent
|
|
2692
|
+
* reads full SKILL.md content only when needed.
|
|
2693
|
+
*
|
|
2694
|
+
* @param options - Configuration options
|
|
2695
|
+
* @returns AgentMiddleware for skills loading and injection
|
|
2696
|
+
*
|
|
2697
|
+
* @example
|
|
2698
|
+
* ```typescript
|
|
2699
|
+
* const middleware = createSkillsMiddleware({
|
|
2700
|
+
* backend: new FilesystemBackend({ rootDir: "/" }),
|
|
2701
|
+
* sources: ["/skills/user/", "/skills/project/"],
|
|
2702
|
+
* });
|
|
2703
|
+
* ```
|
|
2704
|
+
*/
|
|
2705
|
+
function createSkillsMiddleware(options) {
|
|
2706
|
+
const { backend, sources } = options;
|
|
2707
|
+
let loadedSkills = [];
|
|
2708
|
+
return (0, langchain.createMiddleware)({
|
|
2709
|
+
name: "SkillsMiddleware",
|
|
2710
|
+
stateSchema: SkillsStateSchema,
|
|
2711
|
+
async beforeAgent(state) {
|
|
2712
|
+
if (loadedSkills.length > 0) return;
|
|
2713
|
+
if ("skillsMetadata" in state && Array.isArray(state.skillsMetadata) && state.skillsMetadata.length > 0) {
|
|
2714
|
+
loadedSkills = state.skillsMetadata;
|
|
2715
|
+
return;
|
|
2716
|
+
}
|
|
2717
|
+
const resolvedBackend = await resolveBackend(backend, { state });
|
|
2718
|
+
const allSkills = /* @__PURE__ */ new Map();
|
|
2719
|
+
for (const sourcePath of sources) try {
|
|
2720
|
+
const skills = await listSkillsFromBackend(resolvedBackend, sourcePath);
|
|
2721
|
+
for (const skill of skills) allSkills.set(skill.name, skill);
|
|
2722
|
+
} catch (error) {
|
|
2723
|
+
console.debug(`[BackendSkillsMiddleware] Failed to load skills from ${sourcePath}:`, error);
|
|
2724
|
+
}
|
|
2725
|
+
loadedSkills = Array.from(allSkills.values());
|
|
2726
|
+
return { skillsMetadata: loadedSkills };
|
|
2727
|
+
},
|
|
2728
|
+
wrapModelCall(request, handler) {
|
|
2729
|
+
const skillsMetadata = loadedSkills.length > 0 ? loadedSkills : request.state?.skillsMetadata || [];
|
|
2730
|
+
const skillsLocations = formatSkillsLocations(sources);
|
|
2731
|
+
const skillsList = formatSkillsList(skillsMetadata, sources);
|
|
2732
|
+
const skillsSection = SKILLS_SYSTEM_PROMPT.replace("{skills_locations}", skillsLocations).replace("{skills_list}", skillsList);
|
|
2733
|
+
const newSystemMessage = request.systemMessage.concat(skillsSection);
|
|
2734
|
+
return handler({
|
|
2735
|
+
...request,
|
|
2736
|
+
systemMessage: newSystemMessage
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2741
|
+
//#endregion
|
|
2742
|
+
//#region src/middleware/utils.ts
|
|
2743
|
+
/**
|
|
2744
|
+
* Utility functions for middleware.
|
|
2745
|
+
*
|
|
2746
|
+
* This module provides shared helpers used across middleware implementations.
|
|
2747
|
+
*/
|
|
2748
|
+
//#endregion
|
|
2749
|
+
//#region src/middleware/completion_callback.ts
|
|
2750
|
+
/**
|
|
2751
|
+
* Callback middleware for async subagents.
|
|
2752
|
+
*
|
|
2753
|
+
* @experimental - this middleware is experimental and may change in future releases.
|
|
2754
|
+
*
|
|
2755
|
+
* This middleware sends a notification to a callback thread when a subagent
|
|
2756
|
+
* completes successfully or raises an error. The callback agent can then
|
|
2757
|
+
* process that notification instead of relying only on polling via
|
|
2758
|
+
* `check_async_task`.
|
|
2759
|
+
*
|
|
2760
|
+
* ## Architecture
|
|
2761
|
+
*
|
|
2762
|
+
* A parent agent launches a subagent with `start_async_task` and can later
|
|
2763
|
+
* inspect task state with `check_async_task`. This middleware adds an optional
|
|
2764
|
+
* completion signal by creating a run on the callback thread when the subagent
|
|
2765
|
+
* finishes.
|
|
2766
|
+
*
|
|
2767
|
+
* ```
|
|
2768
|
+
* Parent Subagent
|
|
2769
|
+
* | |
|
|
2770
|
+
* |--- start_async_task -----> |
|
|
2771
|
+
* |<-- task_id (immediately) - |
|
|
2772
|
+
* | | (working...)
|
|
2773
|
+
* | | (done!)
|
|
2774
|
+
* | |
|
|
2775
|
+
* |<-- runs.create( |
|
|
2776
|
+
* | callback_thread, |
|
|
2777
|
+
* | "completed: ...") |
|
|
2778
|
+
* | |
|
|
2779
|
+
* | (processes result) |
|
|
2780
|
+
* ```
|
|
2781
|
+
*
|
|
2782
|
+
* The middleware calls `runs.create()` on the callback thread. From the
|
|
2783
|
+
* callback agent's perspective, this appears as a new user message containing
|
|
2784
|
+
* structured output from the subagent.
|
|
2785
|
+
*
|
|
2786
|
+
* ## Callback context
|
|
2787
|
+
*
|
|
2788
|
+
* - `callbackGraphId` identifies the callback graph or assistant. It is
|
|
2789
|
+
* provided when the middleware is constructed.
|
|
2790
|
+
* - `url` and `headers` optionally configure a remote callback destination.
|
|
2791
|
+
* Omit `url` for same-deployment ASGI transport.
|
|
2792
|
+
* - `callback_thread_id` is stored in the subagent state by the parent's
|
|
2793
|
+
* `start_async_task` tool. Because it is stored in state rather than config,
|
|
2794
|
+
* it survives thread updates and interrupts.
|
|
2795
|
+
* - If `callback_thread_id` is not present in state, the middleware does
|
|
2796
|
+
* nothing.
|
|
2797
|
+
*
|
|
2798
|
+
* ## Usage
|
|
2799
|
+
*
|
|
2800
|
+
* ```typescript
|
|
2801
|
+
* import { createCompletionCallbackMiddleware } from "deepagents";
|
|
2802
|
+
*
|
|
2803
|
+
* // Same deployment (callback agent and subagent share a server):
|
|
2804
|
+
* const notifier = createCompletionCallbackMiddleware({
|
|
2805
|
+
* callbackGraphId: "supervisor",
|
|
2806
|
+
* });
|
|
2807
|
+
*
|
|
2808
|
+
* // Remote deployment (callback destination on a different server):
|
|
2809
|
+
* const notifier = createCompletionCallbackMiddleware({
|
|
2810
|
+
* callbackGraphId: "supervisor",
|
|
2811
|
+
* url: "https://my-deployment.langsmith.dev",
|
|
2812
|
+
* });
|
|
2813
|
+
*
|
|
2814
|
+
* const agent = createDeepAgent({
|
|
2815
|
+
* model,
|
|
2816
|
+
* middleware: [notifier],
|
|
2817
|
+
* });
|
|
2818
|
+
* ```
|
|
2819
|
+
*
|
|
2820
|
+
* The middleware reads `callbackThreadId` from the agent state at the end of
|
|
2821
|
+
* execution. This value is injected by the parent's `start_async_task` tool
|
|
2822
|
+
* when it creates the run.
|
|
2823
|
+
*
|
|
2824
|
+
* @module
|
|
2825
|
+
*/
|
|
2826
|
+
/** Maximum characters to include from the last message in notifications. */
|
|
2827
|
+
const MAX_MESSAGE_LENGTH = 500;
|
|
2828
|
+
/** Suffix appended when truncating long messages. */
|
|
2829
|
+
const TRUNCATION_SUFFIX = "... [full result truncated]";
|
|
2830
|
+
/** State key for the callback thread ID. */
|
|
2831
|
+
const CALLBACK_THREAD_ID_KEY = "callbackThreadId";
|
|
2832
|
+
/**
|
|
2833
|
+
* State extension for subagents that use completion callbacks.
|
|
2834
|
+
*
|
|
2835
|
+
* @experimental - this state schema is experimental and may change in future releases.
|
|
2836
|
+
*
|
|
2837
|
+
* `callbackThreadId` is written by the parent's `start_async_task` tool
|
|
2838
|
+
* and read by `CompletionCallbackMiddleware` when sending callback
|
|
2839
|
+
* notifications.
|
|
2840
|
+
*/
|
|
2841
|
+
const CompletionCallbackStateSchema = zod.object({ [CALLBACK_THREAD_ID_KEY]: zod.string().optional() });
|
|
2842
|
+
/**
|
|
2843
|
+
* Build headers for the callback LangGraph server.
|
|
2844
|
+
*
|
|
2845
|
+
* Ensures `x-auth-scheme: langsmith` is present unless explicitly overridden.
|
|
2846
|
+
*/
|
|
2847
|
+
function resolveHeaders(headers) {
|
|
2848
|
+
const resolved = { ...headers };
|
|
2849
|
+
if (!("x-auth-scheme" in resolved)) resolved["x-auth-scheme"] = "langsmith";
|
|
2850
|
+
return resolved;
|
|
2851
|
+
}
|
|
2852
|
+
/**
|
|
2853
|
+
* Send a notification run to the callback thread.
|
|
2854
|
+
*
|
|
2855
|
+
* @param callbackGraphId - The callback graph ID used as `assistant_id`
|
|
2856
|
+
* in the `runs.create` call.
|
|
2857
|
+
* @param callbackThreadId - The callback thread ID.
|
|
2858
|
+
* @param message - The message content to send.
|
|
2859
|
+
* @param options - Optional url and headers for the callback server.
|
|
2860
|
+
*/
|
|
2861
|
+
async function notifyParent(callbackGraphId, callbackThreadId, message, options) {
|
|
2862
|
+
try {
|
|
2863
|
+
await new _langchain_langgraph_sdk.Client({
|
|
2864
|
+
apiUrl: options?.url ?? void 0,
|
|
2865
|
+
apiKey: null,
|
|
2866
|
+
defaultHeaders: resolveHeaders(options?.headers)
|
|
2867
|
+
}).runs.create(callbackThreadId, callbackGraphId, { input: { messages: [{
|
|
2868
|
+
role: "user",
|
|
2869
|
+
content: message
|
|
2870
|
+
}] } });
|
|
2871
|
+
} catch (e) {
|
|
2872
|
+
console.warn(`[CompletionCallbackMiddleware] Failed to notify callback thread ${callbackThreadId}:`, e);
|
|
2873
|
+
}
|
|
2314
2874
|
}
|
|
2315
2875
|
/**
|
|
2316
|
-
*
|
|
2876
|
+
* Extract a summary from the subagent's final message.
|
|
2317
2877
|
*
|
|
2318
|
-
*
|
|
2319
|
-
*
|
|
2320
|
-
* pattern: skill names and descriptions are shown in the prompt, but the agent
|
|
2321
|
-
* reads full SKILL.md content only when needed.
|
|
2878
|
+
* Returns at most 500 characters from the last message's content.
|
|
2879
|
+
* Throws if no messages exist or if the last message is not an AIMessage.
|
|
2322
2880
|
*
|
|
2323
|
-
* @param
|
|
2324
|
-
* @
|
|
2881
|
+
* @param state - The agent state dict.
|
|
2882
|
+
* @param taskId - Optional task ID to include in truncation hint.
|
|
2883
|
+
*/
|
|
2884
|
+
function extractLastMessage(state, taskId) {
|
|
2885
|
+
const messages = state.messages;
|
|
2886
|
+
if (!messages || messages.length === 0) throw new Error(`Expected at least one message in state ${JSON.stringify(state)}`);
|
|
2887
|
+
const last = messages[messages.length - 1];
|
|
2888
|
+
if (!_langchain_core_messages.AIMessage.isInstance(last)) throw new TypeError(`Expected an AIMessage, got ${typeof last === "object" && last !== null ? last.constructor?.name ?? typeof last : typeof last} instead`);
|
|
2889
|
+
let textContent = last.text;
|
|
2890
|
+
if (textContent.length > MAX_MESSAGE_LENGTH) {
|
|
2891
|
+
textContent = textContent.slice(0, MAX_MESSAGE_LENGTH) + TRUNCATION_SUFFIX;
|
|
2892
|
+
if (taskId) textContent += ` Result truncated. Use \`check_async_task(task_id='${taskId}')\` to retrieve the full result if needed.`;
|
|
2893
|
+
}
|
|
2894
|
+
return textContent;
|
|
2895
|
+
}
|
|
2896
|
+
/**
|
|
2897
|
+
* Create a completion callback middleware for async subagents.
|
|
2898
|
+
*
|
|
2899
|
+
* **Experimental** — this middleware is experimental and may change.
|
|
2900
|
+
*
|
|
2901
|
+
* This middleware is added to a subagent's middleware stack. On success or
|
|
2902
|
+
* model-call error, it sends a notification to the configured callback
|
|
2903
|
+
* thread by calling `runs.create()`.
|
|
2904
|
+
*
|
|
2905
|
+
* The callback destination is configured with `callbackGraphId` and
|
|
2906
|
+
* optional `url` and `headers`. The target thread is read from
|
|
2907
|
+
* `callbackThreadId` in the subagent state.
|
|
2908
|
+
*
|
|
2909
|
+
* If `callbackThreadId` is not present in state, the middleware does
|
|
2910
|
+
* nothing.
|
|
2911
|
+
*
|
|
2912
|
+
* @param options - Configuration options.
|
|
2913
|
+
* @returns An `AgentMiddleware` instance.
|
|
2325
2914
|
*
|
|
2326
2915
|
* @example
|
|
2327
2916
|
* ```typescript
|
|
2328
|
-
*
|
|
2329
|
-
*
|
|
2330
|
-
*
|
|
2917
|
+
* import { createCompletionCallbackMiddleware } from "deepagents";
|
|
2918
|
+
*
|
|
2919
|
+
* const notifier = createCompletionCallbackMiddleware({
|
|
2920
|
+
* callbackGraphId: "supervisor",
|
|
2921
|
+
* });
|
|
2922
|
+
*
|
|
2923
|
+
* const agent = createDeepAgent({
|
|
2924
|
+
* model: "claude-sonnet-4-5-20250929",
|
|
2925
|
+
* middleware: [notifier],
|
|
2331
2926
|
* });
|
|
2332
2927
|
* ```
|
|
2333
2928
|
*/
|
|
2334
|
-
function
|
|
2335
|
-
const {
|
|
2336
|
-
|
|
2929
|
+
function createCompletionCallbackMiddleware(options) {
|
|
2930
|
+
const { callbackGraphId, url, headers } = options;
|
|
2931
|
+
/**
|
|
2932
|
+
* Send a notification to the callback destination.
|
|
2933
|
+
*/
|
|
2934
|
+
async function sendNotification(callbackThreadId, message) {
|
|
2935
|
+
await notifyParent(callbackGraphId, callbackThreadId, message, {
|
|
2936
|
+
url,
|
|
2937
|
+
headers
|
|
2938
|
+
});
|
|
2939
|
+
}
|
|
2940
|
+
/**
|
|
2941
|
+
* Read the subagent's own thread_id from runtime config.
|
|
2942
|
+
*
|
|
2943
|
+
* The subagent's `thread_id` is the same as the `task_id` from the
|
|
2944
|
+
* parent's perspective.
|
|
2945
|
+
*/
|
|
2946
|
+
function getTaskId(runtime) {
|
|
2947
|
+
return runtime?.configurable?.thread_id;
|
|
2948
|
+
}
|
|
2949
|
+
/**
|
|
2950
|
+
* Build a notification string with task_id prefix.
|
|
2951
|
+
*/
|
|
2952
|
+
function formatNotification(body, runtime) {
|
|
2953
|
+
const taskId = getTaskId(runtime);
|
|
2954
|
+
return `${taskId ? `[task_id=${taskId}]` : ""}${body}`;
|
|
2955
|
+
}
|
|
2337
2956
|
return (0, langchain.createMiddleware)({
|
|
2338
|
-
name: "
|
|
2339
|
-
stateSchema:
|
|
2340
|
-
async
|
|
2341
|
-
|
|
2342
|
-
if (
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
}
|
|
2346
|
-
const resolvedBackend = await resolveBackend(backend, { state });
|
|
2347
|
-
const allSkills = /* @__PURE__ */ new Map();
|
|
2348
|
-
for (const sourcePath of sources) try {
|
|
2349
|
-
const skills = await listSkillsFromBackend(resolvedBackend, sourcePath);
|
|
2350
|
-
for (const skill of skills) allSkills.set(skill.name, skill);
|
|
2351
|
-
} catch (error) {
|
|
2352
|
-
console.debug(`[BackendSkillsMiddleware] Failed to load skills from ${sourcePath}:`, error);
|
|
2353
|
-
}
|
|
2354
|
-
loadedSkills = Array.from(allSkills.values());
|
|
2355
|
-
return { skillsMetadata: loadedSkills };
|
|
2957
|
+
name: "CompletionCallbackMiddleware",
|
|
2958
|
+
stateSchema: CompletionCallbackStateSchema,
|
|
2959
|
+
async afterAgent(state, runtime) {
|
|
2960
|
+
const callbackThreadId = state[CALLBACK_THREAD_ID_KEY];
|
|
2961
|
+
if (callbackThreadId == null) throw new Error(`Missing required state key '${CALLBACK_THREAD_ID_KEY}'`);
|
|
2962
|
+
const taskId = getTaskId(runtime);
|
|
2963
|
+
await sendNotification(callbackThreadId, formatNotification(`Completed. Result: ${extractLastMessage(state, typeof taskId === "string" ? taskId : void 0)}`, runtime));
|
|
2356
2964
|
},
|
|
2357
|
-
wrapModelCall(request, handler) {
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
systemMessage: newSystemMessage
|
|
2366
|
-
});
|
|
2965
|
+
async wrapModelCall(request, handler) {
|
|
2966
|
+
try {
|
|
2967
|
+
return await handler(request);
|
|
2968
|
+
} catch (e) {
|
|
2969
|
+
const callbackThreadId = request.state[CALLBACK_THREAD_ID_KEY];
|
|
2970
|
+
if (typeof callbackThreadId === "string") await sendNotification(callbackThreadId, formatNotification("The agent encountered an error while calling the model.", request.runtime));
|
|
2971
|
+
throw e;
|
|
2972
|
+
}
|
|
2367
2973
|
}
|
|
2368
2974
|
});
|
|
2369
2975
|
}
|
|
2370
2976
|
//#endregion
|
|
2371
|
-
//#region src/middleware/utils.ts
|
|
2372
|
-
/**
|
|
2373
|
-
* Utility functions for middleware.
|
|
2374
|
-
*
|
|
2375
|
-
* This module provides shared helpers used across middleware implementations.
|
|
2376
|
-
*/
|
|
2377
|
-
//#endregion
|
|
2378
2977
|
//#region src/middleware/summarization.ts
|
|
2379
2978
|
/**
|
|
2380
2979
|
* Summarization middleware with backend support for conversation history offloading.
|
|
@@ -2898,15 +3497,17 @@ function createSummarizationMiddleware(options) {
|
|
|
2898
3497
|
*/
|
|
2899
3498
|
function buildSummaryMessage(summary, filePath) {
|
|
2900
3499
|
let content;
|
|
2901
|
-
if (filePath) content = `
|
|
3500
|
+
if (filePath) content = langchain.context`
|
|
3501
|
+
You are in the middle of a conversation that has been summarized.
|
|
2902
3502
|
|
|
2903
|
-
The full conversation history has been saved to ${filePath} should you need to refer back to it for details.
|
|
3503
|
+
The full conversation history has been saved to ${filePath} should you need to refer back to it for details.
|
|
2904
3504
|
|
|
2905
|
-
A condensed summary follows:
|
|
3505
|
+
A condensed summary follows:
|
|
2906
3506
|
|
|
2907
|
-
<summary>
|
|
2908
|
-
${summary}
|
|
2909
|
-
</summary
|
|
3507
|
+
<summary>
|
|
3508
|
+
${summary}
|
|
3509
|
+
</summary>
|
|
3510
|
+
`;
|
|
2910
3511
|
else content = `Here is a summary of the conversation to date:\n\n${summary}`;
|
|
2911
3512
|
return new langchain.HumanMessage({
|
|
2912
3513
|
content,
|
|
@@ -2972,92 +3573,661 @@ ${summary}
|
|
|
2972
3573
|
if (!isContextOverflow(err)) throw err;
|
|
2973
3574
|
}
|
|
2974
3575
|
}
|
|
2975
|
-
const previousEvent = request.state._summarizationEvent;
|
|
2976
|
-
const previousCutoffIndex = previousEvent != null ? previousEvent.cutoffIndex : void 0;
|
|
2977
|
-
const { summaryMessage, filePath, stateCutoffIndex } = await summarizeMessages(messagesToSummarize, resolvedModel, request.state, previousCutoffIndex, cutoffIndex);
|
|
2978
|
-
let modifiedMessages = [summaryMessage, ...preservedMessages];
|
|
2979
|
-
const modifiedTokens = countTotalTokens(modifiedMessages, request.systemMessage, request.tools);
|
|
2980
|
-
let finalStateCutoffIndex = stateCutoffIndex;
|
|
2981
|
-
let finalSummaryMessage = summaryMessage;
|
|
2982
|
-
let finalFilePath = filePath;
|
|
3576
|
+
const previousEvent = request.state._summarizationEvent;
|
|
3577
|
+
const previousCutoffIndex = previousEvent != null ? previousEvent.cutoffIndex : void 0;
|
|
3578
|
+
const { summaryMessage, filePath, stateCutoffIndex } = await summarizeMessages(messagesToSummarize, resolvedModel, request.state, previousCutoffIndex, cutoffIndex);
|
|
3579
|
+
let modifiedMessages = [summaryMessage, ...preservedMessages];
|
|
3580
|
+
const modifiedTokens = countTotalTokens(modifiedMessages, request.systemMessage, request.tools);
|
|
3581
|
+
let finalStateCutoffIndex = stateCutoffIndex;
|
|
3582
|
+
let finalSummaryMessage = summaryMessage;
|
|
3583
|
+
let finalFilePath = filePath;
|
|
3584
|
+
try {
|
|
3585
|
+
await handler({
|
|
3586
|
+
...request,
|
|
3587
|
+
messages: modifiedMessages
|
|
3588
|
+
});
|
|
3589
|
+
} catch (err) {
|
|
3590
|
+
if (!isContextOverflow(err)) throw err;
|
|
3591
|
+
if (maxInputTokens && modifiedTokens > 0) {
|
|
3592
|
+
const observedRatio = maxInputTokens / modifiedTokens;
|
|
3593
|
+
if (observedRatio > tokenEstimationMultiplier) tokenEstimationMultiplier = observedRatio * 1.1;
|
|
3594
|
+
}
|
|
3595
|
+
const reSumResult = await summarizeMessages([...messagesToSummarize, ...preservedMessages], resolvedModel, request.state, previousCutoffIndex, truncatedMessages.length);
|
|
3596
|
+
finalSummaryMessage = reSumResult.summaryMessage;
|
|
3597
|
+
finalFilePath = reSumResult.filePath;
|
|
3598
|
+
finalStateCutoffIndex = reSumResult.stateCutoffIndex;
|
|
3599
|
+
modifiedMessages = [reSumResult.summaryMessage];
|
|
3600
|
+
await handler({
|
|
3601
|
+
...request,
|
|
3602
|
+
messages: modifiedMessages
|
|
3603
|
+
});
|
|
3604
|
+
}
|
|
3605
|
+
return new _langchain_langgraph.Command({ update: {
|
|
3606
|
+
_summarizationEvent: {
|
|
3607
|
+
cutoffIndex: finalStateCutoffIndex,
|
|
3608
|
+
summaryMessage: finalSummaryMessage,
|
|
3609
|
+
filePath: finalFilePath
|
|
3610
|
+
},
|
|
3611
|
+
_summarizationSessionId: getSessionId(request.state)
|
|
3612
|
+
} });
|
|
3613
|
+
}
|
|
3614
|
+
return (0, langchain.createMiddleware)({
|
|
3615
|
+
name: "SummarizationMiddleware",
|
|
3616
|
+
stateSchema: SummarizationStateSchema,
|
|
3617
|
+
async wrapModelCall(request, handler) {
|
|
3618
|
+
const effectiveMessages = getEffectiveMessages(request.messages ?? [], request.state);
|
|
3619
|
+
if (effectiveMessages.length === 0) return handler(request);
|
|
3620
|
+
/**
|
|
3621
|
+
* Resolve the chat model and get max input tokens from its profile.
|
|
3622
|
+
*/
|
|
3623
|
+
const resolvedModel = await getChatModel();
|
|
3624
|
+
const maxInputTokens = getMaxInputTokens(resolvedModel);
|
|
3625
|
+
applyModelDefaults(resolvedModel);
|
|
3626
|
+
/**
|
|
3627
|
+
* Step 1: Truncate args if configured
|
|
3628
|
+
*/
|
|
3629
|
+
const { messages: truncatedMessages } = truncateArgs(effectiveMessages, maxInputTokens, request.systemMessage, request.tools);
|
|
3630
|
+
/**
|
|
3631
|
+
* Step 2: Check if summarization should happen.
|
|
3632
|
+
* Count tokens including system message and tools to match what's
|
|
3633
|
+
* actually sent to the model (matching Python implementation).
|
|
3634
|
+
*/
|
|
3635
|
+
const totalTokens = countTotalTokens(truncatedMessages, request.systemMessage, request.tools);
|
|
3636
|
+
/**
|
|
3637
|
+
* If no summarization needed, try passing through.
|
|
3638
|
+
* If the handler throws a ContextOverflowError, fall back to
|
|
3639
|
+
* emergency summarization (matching Python's behavior).
|
|
3640
|
+
*/
|
|
3641
|
+
if (!shouldSummarize(truncatedMessages, totalTokens, maxInputTokens)) try {
|
|
3642
|
+
return await handler({
|
|
3643
|
+
...request,
|
|
3644
|
+
messages: truncatedMessages
|
|
3645
|
+
});
|
|
3646
|
+
} catch (err) {
|
|
3647
|
+
if (!isContextOverflow(err)) throw err;
|
|
3648
|
+
if (maxInputTokens && totalTokens > 0) {
|
|
3649
|
+
const observedRatio = maxInputTokens / totalTokens;
|
|
3650
|
+
if (observedRatio > tokenEstimationMultiplier) tokenEstimationMultiplier = observedRatio * 1.1;
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
/**
|
|
3654
|
+
* Step 3: Perform summarization
|
|
3655
|
+
*/
|
|
3656
|
+
return performSummarization(request, handler, truncatedMessages, resolvedModel, maxInputTokens);
|
|
3657
|
+
}
|
|
3658
|
+
});
|
|
3659
|
+
}
|
|
3660
|
+
//#endregion
|
|
3661
|
+
//#region src/middleware/async_subagents.ts
|
|
3662
|
+
function toolCallIdFromRuntime(runtime) {
|
|
3663
|
+
return runtime.toolCall?.id ?? runtime.toolCallId ?? "";
|
|
3664
|
+
}
|
|
3665
|
+
/**
|
|
3666
|
+
* Zod schema for {@link AsyncTask}.
|
|
3667
|
+
*
|
|
3668
|
+
* Used by the {@link ReducedValue} in the state schema so that LangGraph
|
|
3669
|
+
* can validate and serialize task records stored in `asyncTasks`.
|
|
3670
|
+
*/
|
|
3671
|
+
const AsyncTaskSchema = zod_v4.z.object({
|
|
3672
|
+
taskId: zod_v4.z.string(),
|
|
3673
|
+
agentName: zod_v4.z.string(),
|
|
3674
|
+
threadId: zod_v4.z.string(),
|
|
3675
|
+
runId: zod_v4.z.string(),
|
|
3676
|
+
status: zod_v4.z.string(),
|
|
3677
|
+
createdAt: zod_v4.z.string(),
|
|
3678
|
+
description: zod_v4.z.string().optional(),
|
|
3679
|
+
updatedAt: zod_v4.z.string().optional(),
|
|
3680
|
+
checkedAt: zod_v4.z.string().optional()
|
|
3681
|
+
});
|
|
3682
|
+
/**
|
|
3683
|
+
* State schema for the async subagent middleware.
|
|
3684
|
+
*
|
|
3685
|
+
* Declares `asyncTasks` as a reduced state channel so that individual
|
|
3686
|
+
* tool updates (launch, check, update, cancel, list) merge into the existing
|
|
3687
|
+
* tasks dict rather than replacing it wholesale.
|
|
3688
|
+
*/
|
|
3689
|
+
const AsyncTaskStateSchema = new _langchain_langgraph.StateSchema({ asyncTasks: new _langchain_langgraph.ReducedValue(zod_v4.z.record(zod_v4.z.string(), AsyncTaskSchema).default(() => ({})), {
|
|
3690
|
+
inputSchema: zod_v4.z.record(zod_v4.z.string(), AsyncTaskSchema).optional(),
|
|
3691
|
+
reducer: asyncTasksReducer
|
|
3692
|
+
}) });
|
|
3693
|
+
/**
|
|
3694
|
+
* Reducer for the `asyncTasks` state channel.
|
|
3695
|
+
*
|
|
3696
|
+
* Merges task updates into the existing tasks dict using shallow spread.
|
|
3697
|
+
* This allows individual tools to update a single task without overwriting
|
|
3698
|
+
* the full map — only the keys present in `update` are replaced.
|
|
3699
|
+
*
|
|
3700
|
+
* @param existing - The current tasks dict from state (may be undefined on first write).
|
|
3701
|
+
* @param update - New or updated task entries to merge in.
|
|
3702
|
+
* @returns Merged tasks dict.
|
|
3703
|
+
*/
|
|
3704
|
+
function asyncTasksReducer(existing, update) {
|
|
3705
|
+
return {
|
|
3706
|
+
...existing || {},
|
|
3707
|
+
...update || {}
|
|
3708
|
+
};
|
|
3709
|
+
}
|
|
3710
|
+
/**
|
|
3711
|
+
* Description template for the `start_async_task` tool.
|
|
3712
|
+
*
|
|
3713
|
+
* The `{available_agents}` placeholder is replaced at middleware creation
|
|
3714
|
+
* time with a formatted list of configured async subagent names and descriptions.
|
|
3715
|
+
*/
|
|
3716
|
+
const ASYNC_TASK_TOOL_DESCRIPTION = `Launch an async subagent on a remote server. The subagent runs in the background and returns a task ID immediately.
|
|
3717
|
+
|
|
3718
|
+
Available async agent types:
|
|
3719
|
+
{available_agents}
|
|
3720
|
+
|
|
3721
|
+
## Usage notes:
|
|
3722
|
+
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.
|
|
3723
|
+
2. Use \`check_async_task\` only when the user asks for a status update or result.
|
|
3724
|
+
3. Use \`update_async_task\` to send new instructions to a running task.
|
|
3725
|
+
4. Multiple async subagents can run concurrently — launch several and let them run in the background.
|
|
3726
|
+
5. The subagent runs on a remote server, so it has its own tools and capabilities.`;
|
|
3727
|
+
/**
|
|
3728
|
+
* Default system prompt appended to the main agent's system message when
|
|
3729
|
+
* async subagent middleware is active.
|
|
3730
|
+
*
|
|
3731
|
+
* Provides the agent with instructions on how to use the five async subagent
|
|
3732
|
+
* tools (launch, check, update, cancel, list) including workflow ordering,
|
|
3733
|
+
* critical rules about polling behavior, and guidance on when to use async
|
|
3734
|
+
* subagents vs. synchronous delegation.
|
|
3735
|
+
*/
|
|
3736
|
+
const ASYNC_TASK_SYSTEM_PROMPT = `## Async subagents (remote servers)
|
|
3737
|
+
|
|
3738
|
+
You have access to async subagent tools that launch background tasks on remote servers.
|
|
3739
|
+
|
|
3740
|
+
### Tools:
|
|
3741
|
+
- \`start_async_task\`: Start a new background task. Returns a task ID immediately.
|
|
3742
|
+
- \`check_async_task\`: Check the status of a running task. Returns status and result if complete.
|
|
3743
|
+
- \`update_async_task\`: Send an update or new instructions to a running task.
|
|
3744
|
+
- \`cancel_async_task\`: Cancel a running task that is no longer needed.
|
|
3745
|
+
- \`list_async_tasks\`: List all tracked tasks with live statuses. Use this to check all tasks at once.
|
|
3746
|
+
|
|
3747
|
+
### Workflow:
|
|
3748
|
+
1. **Launch** — Use \`start_async_task\` to start a task. Report the task ID to the user and stop.
|
|
3749
|
+
Do NOT immediately check the status — the task runs in the background while you and the user continue other work.
|
|
3750
|
+
2. **Check (on request)** — Only use \`check_async_task\` when the user explicitly asks for a status update or
|
|
3751
|
+
result. If the status is "running", report that and stop — do not poll in a loop.
|
|
3752
|
+
3. **Update** (optional) — Use \`update_async_task\` to send new instructions to a running task. This interrupts
|
|
3753
|
+
the current run and starts a fresh one on the same thread. The task_id stays the same.
|
|
3754
|
+
4. **Cancel** (optional) — Use \`cancel_async_task\` to stop a task that is no longer needed.
|
|
3755
|
+
5. **Collect** — When \`check_async_task\` returns status "success", the result is included in the response.
|
|
3756
|
+
6. **List** — Use \`list_async_tasks\` to see live statuses for all tasks at once, or to recall task IDs after context compaction.
|
|
3757
|
+
|
|
3758
|
+
### Critical rules:
|
|
3759
|
+
- After launching, ALWAYS return control to the user immediately. Never auto-check after launching.
|
|
3760
|
+
- Never poll \`check_async_task\` in a loop. Check once per user request, then stop.
|
|
3761
|
+
- If a check returns "running", tell the user and wait for them to ask again.
|
|
3762
|
+
- Task statuses in conversation history are ALWAYS stale — a task that was "running" may now be done.
|
|
3763
|
+
NEVER report a status from a previous tool result. ALWAYS call a tool to get the current status:
|
|
3764
|
+
use \`list_async_tasks\` when the user asks about multiple tasks or "all tasks",
|
|
3765
|
+
use \`check_async_task\` when the user asks about a specific task.
|
|
3766
|
+
- Always show the full task_id — never truncate or abbreviate it.
|
|
3767
|
+
|
|
3768
|
+
### When to use async subagents:
|
|
3769
|
+
- Long-running tasks that would block the main agent
|
|
3770
|
+
- Tasks that benefit from running on specialized remote deployments
|
|
3771
|
+
- When you want to run multiple tasks concurrently and collect results later`;
|
|
3772
|
+
/**
|
|
3773
|
+
* Task statuses that will never change.
|
|
3774
|
+
*
|
|
3775
|
+
* When listing tasks, live-status fetches are skipped for tasks whose
|
|
3776
|
+
* cached status is in this set, since they are guaranteed to be final.
|
|
3777
|
+
*/
|
|
3778
|
+
/**
|
|
3779
|
+
* Names of the tools added by the async subagent middleware.
|
|
3780
|
+
*
|
|
3781
|
+
* Exported so `agent.ts` can include them in `BUILTIN_TOOL_NAMES` and
|
|
3782
|
+
* surface a `ConfigurationError` if a user-provided tool collides.
|
|
3783
|
+
*/
|
|
3784
|
+
const ASYNC_TASK_TOOL_NAMES = [
|
|
3785
|
+
"start_async_task",
|
|
3786
|
+
"check_async_task",
|
|
3787
|
+
"update_async_task",
|
|
3788
|
+
"cancel_async_task",
|
|
3789
|
+
"list_async_tasks"
|
|
3790
|
+
];
|
|
3791
|
+
const TERMINAL_STATUSES = new Set([
|
|
3792
|
+
"cancelled",
|
|
3793
|
+
"success",
|
|
3794
|
+
"error",
|
|
3795
|
+
"timeout",
|
|
3796
|
+
"interrupted"
|
|
3797
|
+
]);
|
|
3798
|
+
/**
|
|
3799
|
+
* Look up a tracked task from state by its `taskId`.
|
|
3800
|
+
*
|
|
3801
|
+
* @param taskId - The task ID to look up (will be trimmed).
|
|
3802
|
+
* @param state - The current agent state containing `asyncTasks`.
|
|
3803
|
+
* @returns The tracked task on success, or an error string.
|
|
3804
|
+
*/
|
|
3805
|
+
function resolveTrackedTask(taskId, state) {
|
|
3806
|
+
const tracked = (state.asyncTasks ?? {})[taskId.trim()];
|
|
3807
|
+
if (!tracked) return `No tracked task found for taskId: '${taskId}'`;
|
|
3808
|
+
return tracked;
|
|
3809
|
+
}
|
|
3810
|
+
/**
|
|
3811
|
+
* Build a check result from a run's current status and thread state values.
|
|
3812
|
+
*
|
|
3813
|
+
* For successful runs, extracts the last message's content from the remote
|
|
3814
|
+
* thread's state values. For errored runs, includes a generic error message.
|
|
3815
|
+
*
|
|
3816
|
+
* @param run - The run object from the SDK.
|
|
3817
|
+
* @param threadId - The thread ID for the run.
|
|
3818
|
+
* @param threadValues - The `values` from `ThreadState` (the remote subagent's state).
|
|
3819
|
+
*/
|
|
3820
|
+
function buildCheckResult(run, threadId, threadValues) {
|
|
3821
|
+
const checkResult = {
|
|
3822
|
+
status: run.status,
|
|
3823
|
+
threadId
|
|
3824
|
+
};
|
|
3825
|
+
if (run.status === "success") {
|
|
3826
|
+
const messages = (Array.isArray(threadValues) ? {} : threadValues)?.messages ?? [];
|
|
3827
|
+
if (messages.length > 0) {
|
|
3828
|
+
const last = messages[messages.length - 1];
|
|
3829
|
+
const rawContent = typeof last === "object" && last !== null && "content" in last ? last.content : last;
|
|
3830
|
+
checkResult.result = typeof rawContent === "string" ? rawContent : JSON.stringify(rawContent);
|
|
3831
|
+
} else checkResult.result = "Completed with no output messages.";
|
|
3832
|
+
} else if (run.status === "error") checkResult.error = "The async subagent encountered an error.";
|
|
3833
|
+
return checkResult;
|
|
3834
|
+
}
|
|
3835
|
+
/**
|
|
3836
|
+
* Filter tasks by cached status from agent state.
|
|
3837
|
+
*
|
|
3838
|
+
* Filtering uses the cached status, not live server status. Live statuses
|
|
3839
|
+
* are fetched after filtering by the calling tool.
|
|
3840
|
+
*
|
|
3841
|
+
* @param tasks - All tracked tasks from state.
|
|
3842
|
+
* @param statusFilter - If nullish or `'all'`, return all tasks.
|
|
3843
|
+
* Otherwise return only tasks whose cached status matches.
|
|
3844
|
+
*/
|
|
3845
|
+
function filterTasks(tasks, statusFilter) {
|
|
3846
|
+
if (!statusFilter || statusFilter === "all") return Object.values(tasks);
|
|
3847
|
+
return Object.values(tasks).filter((task) => task.status === statusFilter);
|
|
3848
|
+
}
|
|
3849
|
+
/**
|
|
3850
|
+
* Fetch the current run status from the server.
|
|
3851
|
+
*
|
|
3852
|
+
* Returns the cached status immediately for terminal tasks (avoiding
|
|
3853
|
+
* unnecessary API calls). Falls back to the cached status on SDK errors.
|
|
3854
|
+
*/
|
|
3855
|
+
async function fetchLiveTaskStatus(clients, task) {
|
|
3856
|
+
if (TERMINAL_STATUSES.has(task.status)) return task.status;
|
|
3857
|
+
try {
|
|
3858
|
+
return (await clients.getClient(task.agentName).runs.get(task.threadId, task.runId)).status;
|
|
3859
|
+
} catch {
|
|
3860
|
+
return task.status;
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
/**
|
|
3864
|
+
* Format a single task as a display string for list output.
|
|
3865
|
+
*/
|
|
3866
|
+
function formatTaskEntry(task, status) {
|
|
3867
|
+
return `- taskId: ${task.taskId} agent: ${task.agentName} status: ${status}`;
|
|
3868
|
+
}
|
|
3869
|
+
/**
|
|
3870
|
+
* Lazily-created, cached LangGraph SDK clients keyed by (url, headers).
|
|
3871
|
+
*
|
|
3872
|
+
* Agents that share the same URL and headers will reuse a single `Client`
|
|
3873
|
+
* instance, avoiding unnecessary connections.
|
|
3874
|
+
*/
|
|
3875
|
+
var ClientCache = class {
|
|
3876
|
+
agents;
|
|
3877
|
+
clients = /* @__PURE__ */ new Map();
|
|
3878
|
+
constructor(agents) {
|
|
3879
|
+
this.agents = agents;
|
|
3880
|
+
}
|
|
3881
|
+
/**
|
|
3882
|
+
* Build headers for a remote Agent Protocol server.
|
|
3883
|
+
*
|
|
3884
|
+
* Adds `x-auth-scheme: langsmith` by default unless already provided.
|
|
3885
|
+
* For self-hosted servers that don't require this header, it is typically
|
|
3886
|
+
* ignored. Override via the `headers` field on the AsyncSubAgent config.
|
|
3887
|
+
*/
|
|
3888
|
+
resolveHeaders(spec) {
|
|
3889
|
+
const headers = { ...spec.headers || {} };
|
|
3890
|
+
if (!("x-auth-scheme" in headers)) headers["x-auth-scheme"] = "langsmith";
|
|
3891
|
+
return headers;
|
|
3892
|
+
}
|
|
3893
|
+
/**
|
|
3894
|
+
* Build a stable cache key from a spec's url and resolved headers.
|
|
3895
|
+
*/
|
|
3896
|
+
cacheKey(spec) {
|
|
3897
|
+
const headers = this.resolveHeaders(spec);
|
|
3898
|
+
const headerStr = Object.entries(headers).sort().flat().join(":");
|
|
3899
|
+
return `${spec.url ?? ""}|${headerStr}`;
|
|
3900
|
+
}
|
|
3901
|
+
/**
|
|
3902
|
+
* Get or create a `Client` for the named agent.
|
|
3903
|
+
*/
|
|
3904
|
+
getClient(name) {
|
|
3905
|
+
const spec = this.agents[name];
|
|
3906
|
+
const key = this.cacheKey(spec);
|
|
3907
|
+
const existing = this.clients.get(key);
|
|
3908
|
+
if (existing) return existing;
|
|
3909
|
+
const headers = this.resolveHeaders(spec);
|
|
3910
|
+
const client = new _langchain_langgraph_sdk.Client({
|
|
3911
|
+
apiUrl: spec.url,
|
|
3912
|
+
defaultHeaders: headers
|
|
3913
|
+
});
|
|
3914
|
+
this.clients.set(key, client);
|
|
3915
|
+
return client;
|
|
3916
|
+
}
|
|
3917
|
+
};
|
|
3918
|
+
/**
|
|
3919
|
+
* Extract the callback thread ID from the tool runtime.
|
|
3920
|
+
*
|
|
3921
|
+
* The thread ID is included in the subagent's input state so the subagent
|
|
3922
|
+
* can notify the parent when it completes (via
|
|
3923
|
+
* `CompletionCallbackMiddleware`).
|
|
3924
|
+
*
|
|
3925
|
+
* @returns Object with `callbackThreadId` if available. Empty object otherwise.
|
|
3926
|
+
*/
|
|
3927
|
+
function extractCallbackContext(runtime) {
|
|
3928
|
+
const threadId = (runtime.config?.configurable)?.thread_id;
|
|
3929
|
+
if (typeof threadId === "string" && threadId) return { callbackThreadId: threadId };
|
|
3930
|
+
return {};
|
|
3931
|
+
}
|
|
3932
|
+
/**
|
|
3933
|
+
* Build the `start_async_task` tool.
|
|
3934
|
+
*
|
|
3935
|
+
* Creates a thread on the remote server, starts a run, and returns a
|
|
3936
|
+
* `Command` that persists the new task in state.
|
|
3937
|
+
*/
|
|
3938
|
+
function buildStartTool(agentMap, clients, toolDescription) {
|
|
3939
|
+
return (0, langchain.tool)(async (input, runtime) => {
|
|
3940
|
+
if (!(input.agentName in agentMap)) {
|
|
3941
|
+
const allowed = Object.keys(agentMap).map((k) => `\`${k}\``).join(", ");
|
|
3942
|
+
return `Unknown async subagent type \`${input.agentName}\`. Available types: ${allowed}`;
|
|
3943
|
+
}
|
|
3944
|
+
const spec = agentMap[input.agentName];
|
|
3945
|
+
const callbackContext = extractCallbackContext(runtime);
|
|
2983
3946
|
try {
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
const
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3947
|
+
const client = clients.getClient(input.agentName);
|
|
3948
|
+
const thread = await client.threads.create();
|
|
3949
|
+
const run = await client.runs.create(thread.thread_id, spec.graphId, { input: {
|
|
3950
|
+
messages: [{
|
|
3951
|
+
role: "user",
|
|
3952
|
+
content: input.description
|
|
3953
|
+
}],
|
|
3954
|
+
...callbackContext
|
|
3955
|
+
} });
|
|
3956
|
+
const taskId = thread.thread_id;
|
|
3957
|
+
const task = {
|
|
3958
|
+
taskId,
|
|
3959
|
+
agentName: input.agentName,
|
|
3960
|
+
threadId: taskId,
|
|
3961
|
+
runId: run.run_id,
|
|
3962
|
+
status: "running",
|
|
3963
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3964
|
+
description: input.description
|
|
3965
|
+
};
|
|
3966
|
+
return new _langchain_langgraph.Command({ update: {
|
|
3967
|
+
messages: [new langchain.ToolMessage({
|
|
3968
|
+
content: `Launched async subagent. taskId: ${taskId}`,
|
|
3969
|
+
tool_call_id: toolCallIdFromRuntime(runtime)
|
|
3970
|
+
})],
|
|
3971
|
+
asyncTasks: { [taskId]: task }
|
|
3972
|
+
} });
|
|
3973
|
+
} catch (e) {
|
|
3974
|
+
return `Failed to launch async subagent '${input.agentName}': ${e}`;
|
|
3975
|
+
}
|
|
3976
|
+
}, {
|
|
3977
|
+
name: "start_async_task",
|
|
3978
|
+
description: toolDescription,
|
|
3979
|
+
schema: zod_v4.z.object({
|
|
3980
|
+
description: zod_v4.z.string().describe("A detailed description of the task for the async subagent to perform."),
|
|
3981
|
+
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.")
|
|
3982
|
+
})
|
|
3983
|
+
});
|
|
3984
|
+
}
|
|
3985
|
+
/**
|
|
3986
|
+
* Build the `check_async_task` tool.
|
|
3987
|
+
*
|
|
3988
|
+
* Fetches the current run status from the remote server and, if the run
|
|
3989
|
+
* succeeded, retrieves the thread state to extract the result.
|
|
3990
|
+
*/
|
|
3991
|
+
function buildCheckTool(clients) {
|
|
3992
|
+
return (0, langchain.tool)(async (input, runtime) => {
|
|
3993
|
+
const task = resolveTrackedTask(input.taskId, runtime.state);
|
|
3994
|
+
if (typeof task === "string") return task;
|
|
3995
|
+
const client = clients.getClient(task.agentName);
|
|
3996
|
+
let run;
|
|
3997
|
+
try {
|
|
3998
|
+
run = await client.runs.get(task.threadId, task.runId);
|
|
3999
|
+
} catch (e) {
|
|
4000
|
+
return `Failed to get run status: ${e}`;
|
|
4001
|
+
}
|
|
4002
|
+
let threadValues = {};
|
|
4003
|
+
if (run.status === "success") try {
|
|
4004
|
+
threadValues = (await client.threads.getState(task.threadId)).values || {};
|
|
4005
|
+
} catch {}
|
|
4006
|
+
const result = buildCheckResult(run, task.threadId, threadValues);
|
|
4007
|
+
const updatedTask = {
|
|
4008
|
+
taskId: task.taskId,
|
|
4009
|
+
agentName: task.agentName,
|
|
4010
|
+
threadId: task.threadId,
|
|
4011
|
+
runId: task.runId,
|
|
4012
|
+
status: result.status,
|
|
4013
|
+
createdAt: task.createdAt,
|
|
4014
|
+
updatedAt: result.status !== task.status ? (/* @__PURE__ */ new Date()).toISOString() : task.updatedAt,
|
|
4015
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4016
|
+
};
|
|
4017
|
+
return new _langchain_langgraph.Command({ update: {
|
|
4018
|
+
messages: [new langchain.ToolMessage({
|
|
4019
|
+
content: JSON.stringify(result),
|
|
4020
|
+
tool_call_id: toolCallIdFromRuntime(runtime)
|
|
4021
|
+
})],
|
|
4022
|
+
asyncTasks: { [task.taskId]: updatedTask }
|
|
4023
|
+
} });
|
|
4024
|
+
}, {
|
|
4025
|
+
name: "check_async_task",
|
|
4026
|
+
description: "Check the status of an async subagent task. Returns the current status and, if complete, the result.",
|
|
4027
|
+
schema: zod_v4.z.object({ taskId: zod_v4.z.string().describe("The exact taskId string returned by start_async_task. Pass it verbatim.") })
|
|
4028
|
+
});
|
|
4029
|
+
}
|
|
4030
|
+
/**
|
|
4031
|
+
* Build the `update_async_task` tool.
|
|
4032
|
+
*
|
|
4033
|
+
* Sends a follow-up message to a running async subagent by creating a new
|
|
4034
|
+
* run on the same thread with `multitaskStrategy: "interrupt"`. The subagent
|
|
4035
|
+
* sees the full conversation history plus the new message. The `taskId`
|
|
4036
|
+
* remains the same; only the internal `runId` is updated.
|
|
4037
|
+
*/
|
|
4038
|
+
function buildUpdateTool(agentMap, clients) {
|
|
4039
|
+
return (0, langchain.tool)(async (input, runtime) => {
|
|
4040
|
+
const tracked = resolveTrackedTask(input.taskId, runtime.state);
|
|
4041
|
+
if (typeof tracked === "string") return tracked;
|
|
4042
|
+
const spec = agentMap[tracked.agentName];
|
|
4043
|
+
try {
|
|
4044
|
+
const run = await clients.getClient(tracked.agentName).runs.create(tracked.threadId, spec.graphId, {
|
|
4045
|
+
input: { messages: [{
|
|
4046
|
+
role: "user",
|
|
4047
|
+
content: input.message
|
|
4048
|
+
}] },
|
|
4049
|
+
multitaskStrategy: "interrupt"
|
|
3002
4050
|
});
|
|
4051
|
+
const task = {
|
|
4052
|
+
taskId: tracked.taskId,
|
|
4053
|
+
agentName: tracked.agentName,
|
|
4054
|
+
threadId: tracked.threadId,
|
|
4055
|
+
runId: run.run_id,
|
|
4056
|
+
status: "running",
|
|
4057
|
+
createdAt: tracked.createdAt,
|
|
4058
|
+
description: input.message,
|
|
4059
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4060
|
+
checkedAt: tracked.checkedAt
|
|
4061
|
+
};
|
|
4062
|
+
return new _langchain_langgraph.Command({ update: {
|
|
4063
|
+
messages: [new langchain.ToolMessage({
|
|
4064
|
+
content: `Updated async subagent. taskId: ${tracked.taskId}`,
|
|
4065
|
+
tool_call_id: toolCallIdFromRuntime(runtime)
|
|
4066
|
+
})],
|
|
4067
|
+
asyncTasks: { [tracked.taskId]: task }
|
|
4068
|
+
} });
|
|
4069
|
+
} catch (e) {
|
|
4070
|
+
return `Failed to update async subagent: ${e}`;
|
|
4071
|
+
}
|
|
4072
|
+
}, {
|
|
4073
|
+
name: "update_async_task",
|
|
4074
|
+
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.",
|
|
4075
|
+
schema: zod_v4.z.object({
|
|
4076
|
+
taskId: zod_v4.z.string().describe("The exact taskId string returned by start_async_task. Pass it verbatim."),
|
|
4077
|
+
message: zod_v4.z.string().describe("Follow-up instructions or context to send to the subagent")
|
|
4078
|
+
})
|
|
4079
|
+
});
|
|
4080
|
+
}
|
|
4081
|
+
/**
|
|
4082
|
+
* Build the `cancel_async_task` tool.
|
|
4083
|
+
*
|
|
4084
|
+
* Cancels the current run on the remote server and updates the task's
|
|
4085
|
+
* cached status to `"cancelled"`.
|
|
4086
|
+
*/
|
|
4087
|
+
function buildCancelTool(clients) {
|
|
4088
|
+
return (0, langchain.tool)(async (input, runtime) => {
|
|
4089
|
+
const tracked = resolveTrackedTask(input.taskId, runtime.state);
|
|
4090
|
+
if (typeof tracked === "string") return tracked;
|
|
4091
|
+
const client = clients.getClient(tracked.agentName);
|
|
4092
|
+
try {
|
|
4093
|
+
await client.runs.cancel(tracked.threadId, tracked.runId);
|
|
4094
|
+
} catch (e) {
|
|
4095
|
+
return `Failed to cancel run: ${e}`;
|
|
3003
4096
|
}
|
|
4097
|
+
const updated = {
|
|
4098
|
+
taskId: tracked.taskId,
|
|
4099
|
+
agentName: tracked.agentName,
|
|
4100
|
+
threadId: tracked.threadId,
|
|
4101
|
+
runId: tracked.runId,
|
|
4102
|
+
status: "cancelled",
|
|
4103
|
+
createdAt: tracked.createdAt,
|
|
4104
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4105
|
+
checkedAt: tracked.checkedAt
|
|
4106
|
+
};
|
|
3004
4107
|
return new _langchain_langgraph.Command({ update: {
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
}
|
|
3010
|
-
_summarizationSessionId: getSessionId(request.state)
|
|
4108
|
+
messages: [new langchain.ToolMessage({
|
|
4109
|
+
content: `Cancelled async subagent task: ${tracked.taskId}`,
|
|
4110
|
+
tool_call_id: toolCallIdFromRuntime(runtime)
|
|
4111
|
+
})],
|
|
4112
|
+
asyncTasks: { [tracked.taskId]: updated }
|
|
3011
4113
|
} });
|
|
3012
|
-
}
|
|
4114
|
+
}, {
|
|
4115
|
+
name: "cancel_async_task",
|
|
4116
|
+
description: "Cancel a running async subagent task. Use this to stop a task that is no longer needed.",
|
|
4117
|
+
schema: zod_v4.z.object({ taskId: zod_v4.z.string().describe("The exact taskId string returned by start_async_task. Pass it verbatim.") })
|
|
4118
|
+
});
|
|
4119
|
+
}
|
|
4120
|
+
/**
|
|
4121
|
+
* Build the `list_async_tasks` tool.
|
|
4122
|
+
*
|
|
4123
|
+
* Lists all tracked tasks with their live statuses fetched in parallel.
|
|
4124
|
+
* Supports optional filtering by cached status.
|
|
4125
|
+
*/
|
|
4126
|
+
function buildListTool(clients) {
|
|
4127
|
+
return (0, langchain.tool)(async (input, runtime) => {
|
|
4128
|
+
const filtered = filterTasks(runtime.state.asyncTasks ?? {}, input.statusFilter ?? void 0);
|
|
4129
|
+
if (filtered.length === 0) return "No async subagent tasks tracked";
|
|
4130
|
+
const statuses = await Promise.all(filtered.map((task) => fetchLiveTaskStatus(clients, task)));
|
|
4131
|
+
const updatedTasks = {};
|
|
4132
|
+
const entries = [];
|
|
4133
|
+
for (let idx = 0; idx < filtered.length; idx++) {
|
|
4134
|
+
const task = filtered[idx];
|
|
4135
|
+
const status = statuses[idx];
|
|
4136
|
+
const taskEntry = formatTaskEntry(task, status);
|
|
4137
|
+
entries.push(taskEntry);
|
|
4138
|
+
updatedTasks[task.taskId] = {
|
|
4139
|
+
taskId: task.taskId,
|
|
4140
|
+
agentName: task.agentName,
|
|
4141
|
+
threadId: task.threadId,
|
|
4142
|
+
runId: task.runId,
|
|
4143
|
+
status,
|
|
4144
|
+
createdAt: task.createdAt,
|
|
4145
|
+
updatedAt: status !== task.status ? (/* @__PURE__ */ new Date()).toISOString() : task.updatedAt,
|
|
4146
|
+
checkedAt: task.checkedAt
|
|
4147
|
+
};
|
|
4148
|
+
}
|
|
4149
|
+
return new _langchain_langgraph.Command({ update: {
|
|
4150
|
+
messages: [new langchain.ToolMessage({
|
|
4151
|
+
content: `${entries.length} tracked task(s):\n${entries.join("\n")}`,
|
|
4152
|
+
tool_call_id: toolCallIdFromRuntime(runtime)
|
|
4153
|
+
})],
|
|
4154
|
+
asyncTasks: updatedTasks
|
|
4155
|
+
} });
|
|
4156
|
+
}, {
|
|
4157
|
+
name: "list_async_tasks",
|
|
4158
|
+
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.",
|
|
4159
|
+
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'.") })
|
|
4160
|
+
});
|
|
4161
|
+
}
|
|
4162
|
+
/**
|
|
4163
|
+
* Create middleware that adds async subagent tools to an agent.
|
|
4164
|
+
*
|
|
4165
|
+
* Provides five tools for launching, checking, updating, cancelling, and
|
|
4166
|
+
* listing background tasks on remote Agent Protocol servers. Task state is
|
|
4167
|
+
* persisted in the `asyncTasks` state channel so it survives
|
|
4168
|
+
* context compaction.
|
|
4169
|
+
*
|
|
4170
|
+
* Works with any Agent Protocol-compliant server — LangGraph Platform (managed)
|
|
4171
|
+
* or self-hosted (e.g. a Hono/Express server implementing the Agent Protocol spec).
|
|
4172
|
+
*
|
|
4173
|
+
* @throws {Error} If no async subagents are provided or names are duplicated.
|
|
4174
|
+
*
|
|
4175
|
+
* @example
|
|
4176
|
+
* ```ts
|
|
4177
|
+
* const middleware = createAsyncSubAgentMiddleware({
|
|
4178
|
+
* asyncSubAgents: [{
|
|
4179
|
+
* name: "researcher",
|
|
4180
|
+
* description: "Research agent for deep analysis",
|
|
4181
|
+
* url: "https://my-agent-protocol-server.example.com",
|
|
4182
|
+
* graphId: "research_agent",
|
|
4183
|
+
* }],
|
|
4184
|
+
* });
|
|
4185
|
+
* ```
|
|
4186
|
+
*/
|
|
4187
|
+
/**
|
|
4188
|
+
* Type guard to distinguish async SubAgents from sync SubAgents/CompiledSubAgents.
|
|
4189
|
+
*
|
|
4190
|
+
* Uses the presence of the `graphId` field as the runtime discriminant —
|
|
4191
|
+
* `AsyncSubAgent` requires it, while `SubAgent` and `CompiledSubAgent` do not have it.
|
|
4192
|
+
*/
|
|
4193
|
+
function isAsyncSubAgent(subAgent) {
|
|
4194
|
+
return "graphId" in subAgent;
|
|
4195
|
+
}
|
|
4196
|
+
function createAsyncSubAgentMiddleware(options) {
|
|
4197
|
+
const { asyncSubAgents, systemPrompt = ASYNC_TASK_SYSTEM_PROMPT } = options;
|
|
4198
|
+
if (!asyncSubAgents || asyncSubAgents.length === 0) throw new Error("At least one async subagent must be specified");
|
|
4199
|
+
const names = asyncSubAgents.map((a) => a.name);
|
|
4200
|
+
const duplicates = names.filter((n, i) => names.indexOf(n) !== i);
|
|
4201
|
+
if (duplicates.length > 0) throw new Error(`Duplicate async subagent names: ${[...new Set(duplicates)].join(", ")}`);
|
|
4202
|
+
const agentMap = Object.fromEntries(asyncSubAgents.map((a) => [a.name, a]));
|
|
4203
|
+
const clients = new ClientCache(agentMap);
|
|
4204
|
+
const agentsDescription = asyncSubAgents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
|
|
4205
|
+
const tools = [
|
|
4206
|
+
buildStartTool(agentMap, clients, ASYNC_TASK_TOOL_DESCRIPTION.replace("{available_agents}", agentsDescription)),
|
|
4207
|
+
buildCheckTool(clients),
|
|
4208
|
+
buildUpdateTool(agentMap, clients),
|
|
4209
|
+
buildCancelTool(clients),
|
|
4210
|
+
buildListTool(clients)
|
|
4211
|
+
];
|
|
4212
|
+
const fullSystemPrompt = systemPrompt ? `${systemPrompt}\n\nAvailable async subagent types:\n${agentsDescription}` : null;
|
|
3013
4213
|
return (0, langchain.createMiddleware)({
|
|
3014
|
-
name: "
|
|
3015
|
-
stateSchema:
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
if (
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
const maxInputTokens = getMaxInputTokens(resolvedModel);
|
|
3024
|
-
applyModelDefaults(resolvedModel);
|
|
3025
|
-
/**
|
|
3026
|
-
* Step 1: Truncate args if configured
|
|
3027
|
-
*/
|
|
3028
|
-
const { messages: truncatedMessages } = truncateArgs(effectiveMessages, maxInputTokens, request.systemMessage, request.tools);
|
|
3029
|
-
/**
|
|
3030
|
-
* Step 2: Check if summarization should happen.
|
|
3031
|
-
* Count tokens including system message and tools to match what's
|
|
3032
|
-
* actually sent to the model (matching Python implementation).
|
|
3033
|
-
*/
|
|
3034
|
-
const totalTokens = countTotalTokens(truncatedMessages, request.systemMessage, request.tools);
|
|
3035
|
-
/**
|
|
3036
|
-
* If no summarization needed, try passing through.
|
|
3037
|
-
* If the handler throws a ContextOverflowError, fall back to
|
|
3038
|
-
* emergency summarization (matching Python's behavior).
|
|
3039
|
-
*/
|
|
3040
|
-
if (!shouldSummarize(truncatedMessages, totalTokens, maxInputTokens)) try {
|
|
3041
|
-
return await handler({
|
|
3042
|
-
...request,
|
|
3043
|
-
messages: truncatedMessages
|
|
3044
|
-
});
|
|
3045
|
-
} catch (err) {
|
|
3046
|
-
if (!isContextOverflow(err)) throw err;
|
|
3047
|
-
if (maxInputTokens && totalTokens > 0) {
|
|
3048
|
-
const observedRatio = maxInputTokens / totalTokens;
|
|
3049
|
-
if (observedRatio > tokenEstimationMultiplier) tokenEstimationMultiplier = observedRatio * 1.1;
|
|
3050
|
-
}
|
|
3051
|
-
}
|
|
3052
|
-
/**
|
|
3053
|
-
* Step 3: Perform summarization
|
|
3054
|
-
*/
|
|
3055
|
-
return performSummarization(request, handler, truncatedMessages, resolvedModel, maxInputTokens);
|
|
4214
|
+
name: "asyncSubAgentMiddleware",
|
|
4215
|
+
stateSchema: AsyncTaskStateSchema,
|
|
4216
|
+
tools,
|
|
4217
|
+
wrapModelCall: async (request, handler) => {
|
|
4218
|
+
if (fullSystemPrompt !== null) return handler({
|
|
4219
|
+
...request,
|
|
4220
|
+
systemMessage: request.systemMessage.concat(new langchain.SystemMessage({ content: fullSystemPrompt }))
|
|
4221
|
+
});
|
|
4222
|
+
return handler(request);
|
|
3056
4223
|
}
|
|
3057
4224
|
});
|
|
3058
4225
|
}
|
|
3059
4226
|
//#endregion
|
|
3060
4227
|
//#region src/backends/store.ts
|
|
4228
|
+
/**
|
|
4229
|
+
* StoreBackend: Adapter for LangGraph's BaseStore (persistent, cross-thread).
|
|
4230
|
+
*/
|
|
3061
4231
|
const NAMESPACE_COMPONENT_RE = /^[A-Za-z0-9\-_.@+:~]+$/;
|
|
3062
4232
|
/**
|
|
3063
4233
|
* Validate a namespace array.
|
|
@@ -3092,34 +4262,55 @@ function validateNamespace(namespace) {
|
|
|
3092
4262
|
var StoreBackend = class {
|
|
3093
4263
|
stateAndStore;
|
|
3094
4264
|
_namespace;
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
4265
|
+
fileFormat;
|
|
4266
|
+
constructor(stateAndStoreOrOptions, options) {
|
|
4267
|
+
let opts;
|
|
4268
|
+
if (stateAndStoreOrOptions != null && typeof stateAndStoreOrOptions === "object" && "state" in stateAndStoreOrOptions) {
|
|
4269
|
+
this.stateAndStore = stateAndStoreOrOptions;
|
|
4270
|
+
opts = options;
|
|
4271
|
+
} else {
|
|
4272
|
+
this.stateAndStore = void 0;
|
|
4273
|
+
opts = stateAndStoreOrOptions;
|
|
4274
|
+
}
|
|
4275
|
+
if (opts?.namespace) this._namespace = validateNamespace(opts.namespace);
|
|
4276
|
+
this.fileFormat = opts?.fileFormat ?? "v2";
|
|
3098
4277
|
}
|
|
3099
4278
|
/**
|
|
3100
|
-
* Get the
|
|
4279
|
+
* Get the BaseStore instance for persistent storage operations.
|
|
4280
|
+
*
|
|
4281
|
+
* In legacy mode, reads from the injected {@link StateAndStore}.
|
|
4282
|
+
* In zero-arg mode, retrieves the store from the LangGraph execution
|
|
4283
|
+
* context via {@link getLangGraphStore}.
|
|
3101
4284
|
*
|
|
3102
4285
|
* @returns BaseStore instance
|
|
3103
|
-
* @throws Error if no store is available
|
|
4286
|
+
* @throws Error if no store is available in either mode
|
|
3104
4287
|
*/
|
|
3105
4288
|
getStore() {
|
|
3106
|
-
|
|
3107
|
-
|
|
4289
|
+
if (this.stateAndStore) {
|
|
4290
|
+
const store = this.stateAndStore.store;
|
|
4291
|
+
if (!store) throw new Error("Store is required but not available in runtime");
|
|
4292
|
+
return store;
|
|
4293
|
+
}
|
|
4294
|
+
const store = (0, _langchain_langgraph.getStore)();
|
|
4295
|
+
if (!store) throw new Error("Store is required but not available in LangGraph execution context. Ensure the graph was configured with a store.");
|
|
3108
4296
|
return store;
|
|
3109
4297
|
}
|
|
3110
4298
|
/**
|
|
3111
4299
|
* Get the namespace for store operations.
|
|
3112
4300
|
*
|
|
3113
|
-
*
|
|
3114
|
-
*
|
|
3115
|
-
*
|
|
3116
|
-
* -
|
|
3117
|
-
*
|
|
4301
|
+
* Resolution order:
|
|
4302
|
+
* 1. Explicit namespace from constructor options (both modes)
|
|
4303
|
+
* 2. Legacy mode: `[assistantId, "filesystem"]` fallback from {@link StateAndStore}
|
|
4304
|
+
* 3. Zero-arg mode without namespace: `["filesystem"]` with a deprecation warning
|
|
4305
|
+
* nudging callers to pass an explicit namespace
|
|
4306
|
+
* 4. Legacy mode without assistantId: `["filesystem"]`
|
|
3118
4307
|
*/
|
|
3119
4308
|
getNamespace() {
|
|
3120
4309
|
if (this._namespace) return this._namespace;
|
|
3121
|
-
|
|
3122
|
-
|
|
4310
|
+
if (this.stateAndStore) {
|
|
4311
|
+
const assistantId = this.stateAndStore.assistantId;
|
|
4312
|
+
if (assistantId) return [assistantId, "filesystem"];
|
|
4313
|
+
}
|
|
3123
4314
|
return ["filesystem"];
|
|
3124
4315
|
}
|
|
3125
4316
|
/**
|
|
@@ -3131,9 +4322,10 @@ var StoreBackend = class {
|
|
|
3131
4322
|
*/
|
|
3132
4323
|
convertStoreItemToFileData(storeItem) {
|
|
3133
4324
|
const value = storeItem.value;
|
|
3134
|
-
if (!value.content
|
|
4325
|
+
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(", ")}`);
|
|
3135
4326
|
return {
|
|
3136
4327
|
content: value.content,
|
|
4328
|
+
...value.mimeType ? { mimeType: value.mimeType } : {},
|
|
3137
4329
|
created_at: value.created_at,
|
|
3138
4330
|
modified_at: value.modified_at
|
|
3139
4331
|
};
|
|
@@ -3142,11 +4334,12 @@ var StoreBackend = class {
|
|
|
3142
4334
|
* Convert FileData to a value suitable for store.put().
|
|
3143
4335
|
*
|
|
3144
4336
|
* @param fileData - The FileData to convert
|
|
3145
|
-
* @returns Object with content, created_at, and modified_at fields
|
|
4337
|
+
* @returns Object with content, mimeType, created_at, and modified_at fields
|
|
3146
4338
|
*/
|
|
3147
4339
|
convertFileDataToStoreValue(fileData) {
|
|
3148
4340
|
return {
|
|
3149
4341
|
content: fileData.content,
|
|
4342
|
+
..."mimeType" in fileData ? { mimeType: fileData.mimeType } : {},
|
|
3150
4343
|
created_at: fileData.created_at,
|
|
3151
4344
|
modified_at: fileData.modified_at
|
|
3152
4345
|
};
|
|
@@ -3181,10 +4374,10 @@ var StoreBackend = class {
|
|
|
3181
4374
|
* List files and directories in the specified directory (non-recursive).
|
|
3182
4375
|
*
|
|
3183
4376
|
* @param path - Absolute path to directory
|
|
3184
|
-
* @returns
|
|
4377
|
+
* @returns LsResult with list of FileInfo objects on success or error on failure.
|
|
3185
4378
|
* Directories have a trailing / in their path and is_dir=true.
|
|
3186
4379
|
*/
|
|
3187
|
-
async
|
|
4380
|
+
async ls(path) {
|
|
3188
4381
|
const store = this.getStore();
|
|
3189
4382
|
const namespace = this.getNamespace();
|
|
3190
4383
|
const items = await this.searchStorePaginated(store, namespace);
|
|
@@ -3202,7 +4395,7 @@ var StoreBackend = class {
|
|
|
3202
4395
|
}
|
|
3203
4396
|
try {
|
|
3204
4397
|
const fd = this.convertStoreItemToFileData(item);
|
|
3205
|
-
const size = fd.content.join("\n").length;
|
|
4398
|
+
const size = isFileDataV1(fd) ? fd.content.join("\n").length : isFileDataBinary(fd) ? fd.content.byteLength : fd.content.length;
|
|
3206
4399
|
infos.push({
|
|
3207
4400
|
path: itemKey,
|
|
3208
4401
|
is_dir: false,
|
|
@@ -3220,35 +4413,49 @@ var StoreBackend = class {
|
|
|
3220
4413
|
modified_at: ""
|
|
3221
4414
|
});
|
|
3222
4415
|
infos.sort((a, b) => a.path.localeCompare(b.path));
|
|
3223
|
-
return infos;
|
|
4416
|
+
return { files: infos };
|
|
3224
4417
|
}
|
|
3225
4418
|
/**
|
|
3226
|
-
* Read file content
|
|
4419
|
+
* Read file content.
|
|
4420
|
+
*
|
|
4421
|
+
* Text files are paginated by line offset/limit.
|
|
4422
|
+
* Binary files return full Uint8Array content (offset/limit ignored).
|
|
3227
4423
|
*
|
|
3228
4424
|
* @param filePath - Absolute file path
|
|
3229
4425
|
* @param offset - Line offset to start reading from (0-indexed)
|
|
3230
4426
|
* @param limit - Maximum number of lines to read
|
|
3231
|
-
* @returns
|
|
4427
|
+
* @returns ReadResult with content on success or error on failure
|
|
3232
4428
|
*/
|
|
3233
4429
|
async read(filePath, offset = 0, limit = 500) {
|
|
3234
4430
|
try {
|
|
3235
|
-
|
|
4431
|
+
const readRawResult = await this.readRaw(filePath);
|
|
4432
|
+
if (readRawResult.error || !readRawResult.data) return { error: readRawResult.error || "File data not found" };
|
|
4433
|
+
const fileDataV2 = migrateToFileDataV2(readRawResult.data, filePath);
|
|
4434
|
+
if (!isTextMimeType(fileDataV2.mimeType)) return {
|
|
4435
|
+
content: fileDataV2.content,
|
|
4436
|
+
mimeType: fileDataV2.mimeType
|
|
4437
|
+
};
|
|
4438
|
+
if (typeof fileDataV2.content !== "string") return { error: `File '${filePath}' has binary content but text MIME type` };
|
|
4439
|
+
return {
|
|
4440
|
+
content: fileDataV2.content.split("\n").slice(offset, offset + limit).join("\n"),
|
|
4441
|
+
mimeType: fileDataV2.mimeType
|
|
4442
|
+
};
|
|
3236
4443
|
} catch (e) {
|
|
3237
|
-
return
|
|
4444
|
+
return { error: e.message };
|
|
3238
4445
|
}
|
|
3239
4446
|
}
|
|
3240
4447
|
/**
|
|
3241
4448
|
* Read file content as raw FileData.
|
|
3242
4449
|
*
|
|
3243
4450
|
* @param filePath - Absolute file path
|
|
3244
|
-
* @returns
|
|
4451
|
+
* @returns ReadRawResult with raw file data on success or error on failure
|
|
3245
4452
|
*/
|
|
3246
4453
|
async readRaw(filePath) {
|
|
3247
4454
|
const store = this.getStore();
|
|
3248
4455
|
const namespace = this.getNamespace();
|
|
3249
4456
|
const item = await store.get(namespace, filePath);
|
|
3250
|
-
if (!item)
|
|
3251
|
-
return this.convertStoreItemToFileData(item);
|
|
4457
|
+
if (!item) return { error: `File '${filePath}' not found` };
|
|
4458
|
+
return { data: this.convertStoreItemToFileData(item) };
|
|
3252
4459
|
}
|
|
3253
4460
|
/**
|
|
3254
4461
|
* Create a new file with content.
|
|
@@ -3258,7 +4465,8 @@ var StoreBackend = class {
|
|
|
3258
4465
|
const store = this.getStore();
|
|
3259
4466
|
const namespace = this.getNamespace();
|
|
3260
4467
|
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.` };
|
|
3261
|
-
const
|
|
4468
|
+
const mimeType = getMimeType(filePath);
|
|
4469
|
+
const fileData = createFileData(content, void 0, this.fileFormat, mimeType);
|
|
3262
4470
|
const storeValue = this.convertFileDataToStoreValue(fileData);
|
|
3263
4471
|
await store.put(namespace, filePath, storeValue);
|
|
3264
4472
|
return {
|
|
@@ -3293,9 +4501,10 @@ var StoreBackend = class {
|
|
|
3293
4501
|
}
|
|
3294
4502
|
}
|
|
3295
4503
|
/**
|
|
3296
|
-
*
|
|
4504
|
+
* Search file contents for a literal text pattern.
|
|
4505
|
+
* Binary files are skipped.
|
|
3297
4506
|
*/
|
|
3298
|
-
async
|
|
4507
|
+
async grep(pattern, path = "/", glob = null) {
|
|
3299
4508
|
const store = this.getStore();
|
|
3300
4509
|
const namespace = this.getNamespace();
|
|
3301
4510
|
const items = await this.searchStorePaginated(store, namespace);
|
|
@@ -3305,12 +4514,12 @@ var StoreBackend = class {
|
|
|
3305
4514
|
} catch {
|
|
3306
4515
|
continue;
|
|
3307
4516
|
}
|
|
3308
|
-
return grepMatchesFromFiles(files, pattern, path, glob);
|
|
4517
|
+
return { matches: grepMatchesFromFiles(files, pattern, path, glob) };
|
|
3309
4518
|
}
|
|
3310
4519
|
/**
|
|
3311
4520
|
* Structured glob matching returning FileInfo objects.
|
|
3312
4521
|
*/
|
|
3313
|
-
async
|
|
4522
|
+
async glob(pattern, path = "/") {
|
|
3314
4523
|
const store = this.getStore();
|
|
3315
4524
|
const namespace = this.getNamespace();
|
|
3316
4525
|
const items = await this.searchStorePaginated(store, namespace);
|
|
@@ -3321,12 +4530,12 @@ var StoreBackend = class {
|
|
|
3321
4530
|
continue;
|
|
3322
4531
|
}
|
|
3323
4532
|
const result = globSearchFiles(files, pattern, path);
|
|
3324
|
-
if (result === "No files found") return [];
|
|
4533
|
+
if (result === "No files found") return { files: [] };
|
|
3325
4534
|
const paths = result.split("\n");
|
|
3326
4535
|
const infos = [];
|
|
3327
4536
|
for (const p of paths) {
|
|
3328
4537
|
const fd = files[p];
|
|
3329
|
-
const size = fd ? fd.content.join("\n").length : 0;
|
|
4538
|
+
const size = fd ? isFileDataV1(fd) ? fd.content.join("\n").length : isFileDataBinary(fd) ? fd.content.byteLength : fd.content.length : 0;
|
|
3330
4539
|
infos.push({
|
|
3331
4540
|
path: p,
|
|
3332
4541
|
is_dir: false,
|
|
@@ -3334,7 +4543,7 @@ var StoreBackend = class {
|
|
|
3334
4543
|
modified_at: fd?.modified_at || ""
|
|
3335
4544
|
});
|
|
3336
4545
|
}
|
|
3337
|
-
return infos;
|
|
4546
|
+
return { files: infos };
|
|
3338
4547
|
}
|
|
3339
4548
|
/**
|
|
3340
4549
|
* Upload multiple files.
|
|
@@ -3347,7 +4556,11 @@ var StoreBackend = class {
|
|
|
3347
4556
|
const namespace = this.getNamespace();
|
|
3348
4557
|
const responses = [];
|
|
3349
4558
|
for (const [path, content] of files) try {
|
|
3350
|
-
const
|
|
4559
|
+
const mimeType = getMimeType(path);
|
|
4560
|
+
const isBinary = this.fileFormat === "v2" && !isTextMimeType(mimeType);
|
|
4561
|
+
let fileData;
|
|
4562
|
+
if (isBinary) fileData = createFileData(content, void 0, "v2", mimeType);
|
|
4563
|
+
else fileData = createFileData(new TextDecoder().decode(content), void 0, this.fileFormat, mimeType);
|
|
3351
4564
|
const storeValue = this.convertFileDataToStoreValue(fileData);
|
|
3352
4565
|
await store.put(namespace, path, storeValue);
|
|
3353
4566
|
responses.push({
|
|
@@ -3382,11 +4595,17 @@ var StoreBackend = class {
|
|
|
3382
4595
|
});
|
|
3383
4596
|
continue;
|
|
3384
4597
|
}
|
|
3385
|
-
const
|
|
3386
|
-
|
|
3387
|
-
|
|
4598
|
+
const fileDataV2 = migrateToFileDataV2(this.convertStoreItemToFileData(item), path);
|
|
4599
|
+
if (typeof fileDataV2.content === "string") {
|
|
4600
|
+
const content = new TextEncoder().encode(fileDataV2.content);
|
|
4601
|
+
responses.push({
|
|
4602
|
+
path,
|
|
4603
|
+
content,
|
|
4604
|
+
error: null
|
|
4605
|
+
});
|
|
4606
|
+
} else responses.push({
|
|
3388
4607
|
path,
|
|
3389
|
-
content,
|
|
4608
|
+
content: fileDataV2.content,
|
|
3390
4609
|
error: null
|
|
3391
4610
|
});
|
|
3392
4611
|
} catch {
|
|
@@ -3459,10 +4678,10 @@ var FilesystemBackend = class {
|
|
|
3459
4678
|
* @returns List of FileInfo objects for files and directories directly in the directory.
|
|
3460
4679
|
* Directories have a trailing / in their path and is_dir=true.
|
|
3461
4680
|
*/
|
|
3462
|
-
async
|
|
4681
|
+
async ls(dirPath) {
|
|
3463
4682
|
try {
|
|
3464
4683
|
const resolvedPath = this.resolvePath(dirPath);
|
|
3465
|
-
if (!(await node_fs_promises.default.stat(resolvedPath)).isDirectory()) return [];
|
|
4684
|
+
if (!(await node_fs_promises.default.stat(resolvedPath)).isDirectory()) return { files: [] };
|
|
3466
4685
|
const entries = await node_fs_promises.default.readdir(resolvedPath, { withFileTypes: true });
|
|
3467
4686
|
const results = [];
|
|
3468
4687
|
const cwdStr = this.cwd.endsWith(node_path.default.sep) ? this.cwd : this.cwd + node_path.default.sep;
|
|
@@ -3510,9 +4729,9 @@ var FilesystemBackend = class {
|
|
|
3510
4729
|
}
|
|
3511
4730
|
}
|
|
3512
4731
|
results.sort((a, b) => a.path.localeCompare(b.path));
|
|
3513
|
-
return results;
|
|
4732
|
+
return { files: results };
|
|
3514
4733
|
} catch {
|
|
3515
|
-
return [];
|
|
4734
|
+
return { files: [] };
|
|
3516
4735
|
}
|
|
3517
4736
|
}
|
|
3518
4737
|
/**
|
|
@@ -3526,62 +4745,105 @@ var FilesystemBackend = class {
|
|
|
3526
4745
|
async read(filePath, offset = 0, limit = 500) {
|
|
3527
4746
|
try {
|
|
3528
4747
|
const resolvedPath = this.resolvePath(filePath);
|
|
4748
|
+
const mimeType = getMimeType(filePath);
|
|
4749
|
+
const isBinary = !isTextMimeType(mimeType);
|
|
3529
4750
|
let content;
|
|
3530
4751
|
if (SUPPORTS_NOFOLLOW) {
|
|
3531
|
-
if (!(await node_fs_promises.default.stat(resolvedPath)).isFile()) return
|
|
4752
|
+
if (!(await node_fs_promises.default.stat(resolvedPath)).isFile()) return { error: `File '${filePath}' not found` };
|
|
3532
4753
|
const fd = await node_fs_promises.default.open(resolvedPath, node_fs.default.constants.O_RDONLY | node_fs.default.constants.O_NOFOLLOW);
|
|
3533
4754
|
try {
|
|
4755
|
+
if (isBinary) {
|
|
4756
|
+
const buffer = await fd.readFile();
|
|
4757
|
+
return {
|
|
4758
|
+
content: new Uint8Array(buffer),
|
|
4759
|
+
mimeType
|
|
4760
|
+
};
|
|
4761
|
+
}
|
|
3534
4762
|
content = await fd.readFile({ encoding: "utf-8" });
|
|
3535
4763
|
} finally {
|
|
3536
4764
|
await fd.close();
|
|
3537
4765
|
}
|
|
3538
4766
|
} else {
|
|
3539
4767
|
const stat = await node_fs_promises.default.lstat(resolvedPath);
|
|
3540
|
-
if (stat.isSymbolicLink()) return
|
|
3541
|
-
if (!stat.isFile()) return
|
|
4768
|
+
if (stat.isSymbolicLink()) return { error: `Symlinks are not allowed: ${filePath}` };
|
|
4769
|
+
if (!stat.isFile()) return { error: `File '${filePath}' not found` };
|
|
4770
|
+
if (isBinary) {
|
|
4771
|
+
const buffer = await node_fs_promises.default.readFile(resolvedPath);
|
|
4772
|
+
return {
|
|
4773
|
+
content: new Uint8Array(buffer),
|
|
4774
|
+
mimeType
|
|
4775
|
+
};
|
|
4776
|
+
}
|
|
3542
4777
|
content = await node_fs_promises.default.readFile(resolvedPath, "utf-8");
|
|
3543
4778
|
}
|
|
3544
4779
|
const emptyMsg = checkEmptyContent(content);
|
|
3545
|
-
if (emptyMsg) return
|
|
4780
|
+
if (emptyMsg) return {
|
|
4781
|
+
content: emptyMsg,
|
|
4782
|
+
mimeType
|
|
4783
|
+
};
|
|
3546
4784
|
const lines = content.split("\n");
|
|
3547
4785
|
const startIdx = offset;
|
|
3548
4786
|
const endIdx = Math.min(startIdx + limit, lines.length);
|
|
3549
|
-
if (startIdx >= lines.length) return
|
|
3550
|
-
return
|
|
4787
|
+
if (startIdx >= lines.length) return { error: `Line offset ${offset} exceeds file length (${lines.length} lines)` };
|
|
4788
|
+
return {
|
|
4789
|
+
content: lines.slice(startIdx, endIdx).join("\n"),
|
|
4790
|
+
mimeType
|
|
4791
|
+
};
|
|
3551
4792
|
} catch (e) {
|
|
3552
|
-
return `Error reading file '${filePath}': ${e.message}
|
|
4793
|
+
return { error: `Error reading file '${filePath}': ${e.message}` };
|
|
3553
4794
|
}
|
|
3554
4795
|
}
|
|
3555
4796
|
/**
|
|
3556
4797
|
* Read file content as raw FileData.
|
|
3557
4798
|
*
|
|
3558
4799
|
* @param filePath - Absolute file path
|
|
3559
|
-
* @returns
|
|
4800
|
+
* @returns ReadRawResult with raw file data on success or error on failure
|
|
3560
4801
|
*/
|
|
3561
4802
|
async readRaw(filePath) {
|
|
3562
4803
|
const resolvedPath = this.resolvePath(filePath);
|
|
4804
|
+
const mimeType = getMimeType(filePath);
|
|
4805
|
+
const isBinary = !isTextMimeType(mimeType);
|
|
3563
4806
|
let content;
|
|
3564
4807
|
let stat;
|
|
3565
4808
|
if (SUPPORTS_NOFOLLOW) {
|
|
3566
4809
|
stat = await node_fs_promises.default.stat(resolvedPath);
|
|
3567
|
-
if (!stat.isFile())
|
|
4810
|
+
if (!stat.isFile()) return { error: `File '${filePath}' not found` };
|
|
3568
4811
|
const fd = await node_fs_promises.default.open(resolvedPath, node_fs.default.constants.O_RDONLY | node_fs.default.constants.O_NOFOLLOW);
|
|
3569
4812
|
try {
|
|
4813
|
+
if (isBinary) {
|
|
4814
|
+
const buffer = await fd.readFile();
|
|
4815
|
+
return { data: {
|
|
4816
|
+
content: new Uint8Array(buffer),
|
|
4817
|
+
mimeType,
|
|
4818
|
+
created_at: stat.ctime.toISOString(),
|
|
4819
|
+
modified_at: stat.mtime.toISOString()
|
|
4820
|
+
} };
|
|
4821
|
+
}
|
|
3570
4822
|
content = await fd.readFile({ encoding: "utf-8" });
|
|
3571
4823
|
} finally {
|
|
3572
4824
|
await fd.close();
|
|
3573
4825
|
}
|
|
3574
4826
|
} else {
|
|
3575
4827
|
stat = await node_fs_promises.default.lstat(resolvedPath);
|
|
3576
|
-
if (stat.isSymbolicLink())
|
|
3577
|
-
if (!stat.isFile())
|
|
4828
|
+
if (stat.isSymbolicLink()) return { error: `Symlinks are not allowed: ${filePath}` };
|
|
4829
|
+
if (!stat.isFile()) return { error: `File '${filePath}' not found` };
|
|
4830
|
+
if (isBinary) {
|
|
4831
|
+
const buffer = await node_fs_promises.default.readFile(resolvedPath);
|
|
4832
|
+
return { data: {
|
|
4833
|
+
content: new Uint8Array(buffer),
|
|
4834
|
+
mimeType,
|
|
4835
|
+
created_at: stat.ctime.toISOString(),
|
|
4836
|
+
modified_at: stat.mtime.toISOString()
|
|
4837
|
+
} };
|
|
4838
|
+
}
|
|
3578
4839
|
content = await node_fs_promises.default.readFile(resolvedPath, "utf-8");
|
|
3579
4840
|
}
|
|
3580
|
-
return {
|
|
3581
|
-
content
|
|
4841
|
+
return { data: {
|
|
4842
|
+
content,
|
|
4843
|
+
mimeType,
|
|
3582
4844
|
created_at: stat.ctime.toISOString(),
|
|
3583
4845
|
modified_at: stat.mtime.toISOString()
|
|
3584
|
-
};
|
|
4846
|
+
} };
|
|
3585
4847
|
}
|
|
3586
4848
|
/**
|
|
3587
4849
|
* Create a new file with content.
|
|
@@ -3590,6 +4852,7 @@ var FilesystemBackend = class {
|
|
|
3590
4852
|
async write(filePath, content) {
|
|
3591
4853
|
try {
|
|
3592
4854
|
const resolvedPath = this.resolvePath(filePath);
|
|
4855
|
+
const isBinary = !isTextMimeType(getMimeType(filePath));
|
|
3593
4856
|
try {
|
|
3594
4857
|
if ((await node_fs_promises.default.lstat(resolvedPath)).isSymbolicLink()) return { error: `Cannot write to ${filePath} because it is a symlink. Symlinks are not allowed.` };
|
|
3595
4858
|
return { error: `Cannot write to ${filePath} because it already exists. Read and then make an edit, or write to a new path.` };
|
|
@@ -3599,10 +4862,16 @@ var FilesystemBackend = class {
|
|
|
3599
4862
|
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;
|
|
3600
4863
|
const fd = await node_fs_promises.default.open(resolvedPath, flags, 420);
|
|
3601
4864
|
try {
|
|
3602
|
-
|
|
4865
|
+
if (isBinary) {
|
|
4866
|
+
const buffer = Buffer.from(content, "base64");
|
|
4867
|
+
await fd.writeFile(buffer);
|
|
4868
|
+
} else await fd.writeFile(content, "utf-8");
|
|
3603
4869
|
} finally {
|
|
3604
4870
|
await fd.close();
|
|
3605
4871
|
}
|
|
4872
|
+
} else if (isBinary) {
|
|
4873
|
+
const buffer = Buffer.from(content, "base64");
|
|
4874
|
+
await node_fs_promises.default.writeFile(resolvedPath, buffer);
|
|
3606
4875
|
} else await node_fs_promises.default.writeFile(resolvedPath, content, "utf-8");
|
|
3607
4876
|
return {
|
|
3608
4877
|
path: filePath,
|
|
@@ -3665,17 +4934,17 @@ var FilesystemBackend = class {
|
|
|
3665
4934
|
* @param glob - Optional glob pattern to filter which files to search.
|
|
3666
4935
|
* @returns List of GrepMatch dicts containing path, line number, and matched text.
|
|
3667
4936
|
*/
|
|
3668
|
-
async
|
|
4937
|
+
async grep(pattern, dirPath = "/", glob = null) {
|
|
3669
4938
|
let baseFull;
|
|
3670
4939
|
try {
|
|
3671
4940
|
baseFull = this.resolvePath(dirPath || ".");
|
|
3672
4941
|
} catch {
|
|
3673
|
-
return [];
|
|
4942
|
+
return { matches: [] };
|
|
3674
4943
|
}
|
|
3675
4944
|
try {
|
|
3676
4945
|
await node_fs_promises.default.stat(baseFull);
|
|
3677
4946
|
} catch {
|
|
3678
|
-
return [];
|
|
4947
|
+
return { matches: [] };
|
|
3679
4948
|
}
|
|
3680
4949
|
let results = await this.ripgrepSearch(pattern, baseFull, glob);
|
|
3681
4950
|
if (results === null) results = await this.literalSearch(pattern, baseFull, glob);
|
|
@@ -3685,7 +4954,7 @@ var FilesystemBackend = class {
|
|
|
3685
4954
|
line: lineNum,
|
|
3686
4955
|
text: lineText
|
|
3687
4956
|
});
|
|
3688
|
-
return matches;
|
|
4957
|
+
return { matches };
|
|
3689
4958
|
}
|
|
3690
4959
|
/**
|
|
3691
4960
|
* Search using ripgrep with fixed-string (literal) mode.
|
|
@@ -3765,6 +5034,7 @@ var FilesystemBackend = class {
|
|
|
3765
5034
|
dot: true
|
|
3766
5035
|
});
|
|
3767
5036
|
for (const fp of files) try {
|
|
5037
|
+
if (!isTextMimeType(getMimeType(fp))) continue;
|
|
3768
5038
|
if (includeGlob && !micromatch.default.isMatch(node_path.default.basename(fp), includeGlob)) continue;
|
|
3769
5039
|
if ((await node_fs_promises.default.stat(fp)).size > this.maxFileSizeBytes) continue;
|
|
3770
5040
|
const lines = (await node_fs_promises.default.readFile(fp, "utf-8")).split("\n");
|
|
@@ -3792,13 +5062,13 @@ var FilesystemBackend = class {
|
|
|
3792
5062
|
/**
|
|
3793
5063
|
* Structured glob matching returning FileInfo objects.
|
|
3794
5064
|
*/
|
|
3795
|
-
async
|
|
5065
|
+
async glob(pattern, searchPath = "/") {
|
|
3796
5066
|
if (pattern.startsWith("/")) pattern = pattern.substring(1);
|
|
3797
5067
|
const resolvedSearchPath = searchPath === "/" ? this.cwd : this.resolvePath(searchPath);
|
|
3798
5068
|
try {
|
|
3799
|
-
if (!(await node_fs_promises.default.stat(resolvedSearchPath)).isDirectory()) return [];
|
|
5069
|
+
if (!(await node_fs_promises.default.stat(resolvedSearchPath)).isDirectory()) return { files: [] };
|
|
3800
5070
|
} catch {
|
|
3801
|
-
return [];
|
|
5071
|
+
return { files: [] };
|
|
3802
5072
|
}
|
|
3803
5073
|
const results = [];
|
|
3804
5074
|
try {
|
|
@@ -3838,7 +5108,7 @@ var FilesystemBackend = class {
|
|
|
3838
5108
|
}
|
|
3839
5109
|
} catch {}
|
|
3840
5110
|
results.sort((a, b) => a.path.localeCompare(b.path));
|
|
3841
|
-
return results;
|
|
5111
|
+
return { files: results };
|
|
3842
5112
|
}
|
|
3843
5113
|
/**
|
|
3844
5114
|
* Upload multiple files to the filesystem.
|
|
@@ -3933,9 +5203,9 @@ var CompositeBackend = class {
|
|
|
3933
5203
|
routes;
|
|
3934
5204
|
sortedRoutes;
|
|
3935
5205
|
constructor(defaultBackend, routes) {
|
|
3936
|
-
this.default = defaultBackend;
|
|
3937
|
-
this.routes = routes;
|
|
3938
|
-
this.sortedRoutes = Object.entries(routes).sort((a, b) => b[0].length - a[0].length);
|
|
5206
|
+
this.default = isSandboxProtocol(defaultBackend) ? adaptSandboxProtocol(defaultBackend) : adaptBackendProtocol(defaultBackend);
|
|
5207
|
+
this.routes = Object.fromEntries(Object.entries(routes).map(([k, v]) => [k, isSandboxProtocol(v) ? adaptSandboxProtocol(v) : adaptBackendProtocol(v)]));
|
|
5208
|
+
this.sortedRoutes = Object.entries(this.routes).sort((a, b) => b[0].length - a[0].length);
|
|
3939
5209
|
}
|
|
3940
5210
|
/** Delegates to default backend's id if it is a sandbox, otherwise empty string. */
|
|
3941
5211
|
get id() {
|
|
@@ -3959,25 +5229,27 @@ var CompositeBackend = class {
|
|
|
3959
5229
|
* List files and directories in the specified directory (non-recursive).
|
|
3960
5230
|
*
|
|
3961
5231
|
* @param path - Absolute path to directory
|
|
3962
|
-
* @returns
|
|
3963
|
-
*
|
|
5232
|
+
* @returns LsResult with list of FileInfo objects (with route prefixes added) on success or error on failure.
|
|
5233
|
+
* Directories have a trailing / in their path and is_dir=true.
|
|
3964
5234
|
*/
|
|
3965
|
-
async
|
|
5235
|
+
async ls(path) {
|
|
3966
5236
|
for (const [routePrefix, backend] of this.sortedRoutes) if (path.startsWith(routePrefix.replace(/\/$/, ""))) {
|
|
3967
5237
|
const suffix = path.substring(routePrefix.length);
|
|
3968
5238
|
const searchPath = suffix ? "/" + suffix : "/";
|
|
3969
|
-
const
|
|
5239
|
+
const result = await backend.ls(searchPath);
|
|
5240
|
+
if (result.error) return result;
|
|
3970
5241
|
const prefixed = [];
|
|
3971
|
-
for (const fi of
|
|
5242
|
+
for (const fi of result.files || []) prefixed.push({
|
|
3972
5243
|
...fi,
|
|
3973
5244
|
path: routePrefix.slice(0, -1) + fi.path
|
|
3974
5245
|
});
|
|
3975
|
-
return prefixed;
|
|
5246
|
+
return { files: prefixed };
|
|
3976
5247
|
}
|
|
3977
5248
|
if (path === "/") {
|
|
3978
5249
|
const results = [];
|
|
3979
|
-
const
|
|
3980
|
-
|
|
5250
|
+
const defaultResult = await this.default.ls(path);
|
|
5251
|
+
if (defaultResult.error) return defaultResult;
|
|
5252
|
+
results.push(...defaultResult.files || []);
|
|
3981
5253
|
for (const [routePrefix] of this.sortedRoutes) results.push({
|
|
3982
5254
|
path: routePrefix,
|
|
3983
5255
|
is_dir: true,
|
|
@@ -3985,9 +5257,9 @@ var CompositeBackend = class {
|
|
|
3985
5257
|
modified_at: ""
|
|
3986
5258
|
});
|
|
3987
5259
|
results.sort((a, b) => a.path.localeCompare(b.path));
|
|
3988
|
-
return results;
|
|
5260
|
+
return { files: results };
|
|
3989
5261
|
}
|
|
3990
|
-
return await this.default.
|
|
5262
|
+
return await this.default.ls(path);
|
|
3991
5263
|
}
|
|
3992
5264
|
/**
|
|
3993
5265
|
* Read file content, routing to appropriate backend.
|
|
@@ -4005,7 +5277,7 @@ var CompositeBackend = class {
|
|
|
4005
5277
|
* Read file content as raw FileData.
|
|
4006
5278
|
*
|
|
4007
5279
|
* @param filePath - Absolute file path
|
|
4008
|
-
* @returns
|
|
5280
|
+
* @returns ReadRawResult with raw file data on success or error on failure
|
|
4009
5281
|
*/
|
|
4010
5282
|
async readRaw(filePath) {
|
|
4011
5283
|
const [backend, strippedKey] = this.getBackendAndKey(filePath);
|
|
@@ -4014,53 +5286,59 @@ var CompositeBackend = class {
|
|
|
4014
5286
|
/**
|
|
4015
5287
|
* Structured search results or error string for invalid input.
|
|
4016
5288
|
*/
|
|
4017
|
-
async
|
|
5289
|
+
async grep(pattern, path = "/", glob = null) {
|
|
4018
5290
|
for (const [routePrefix, backend] of this.sortedRoutes) if (path.startsWith(routePrefix.replace(/\/$/, ""))) {
|
|
4019
5291
|
const searchPath = path.substring(routePrefix.length - 1);
|
|
4020
|
-
const raw = await backend.
|
|
4021
|
-
if (
|
|
4022
|
-
return raw.map((m) => ({
|
|
5292
|
+
const raw = await backend.grep(pattern, searchPath || "/", glob);
|
|
5293
|
+
if (raw.error) return raw;
|
|
5294
|
+
return { matches: (raw.matches || []).map((m) => ({
|
|
4023
5295
|
...m,
|
|
4024
5296
|
path: routePrefix.slice(0, -1) + m.path
|
|
4025
|
-
}));
|
|
5297
|
+
})) };
|
|
4026
5298
|
}
|
|
4027
5299
|
const allMatches = [];
|
|
4028
|
-
const rawDefault = await this.default.
|
|
4029
|
-
if (
|
|
4030
|
-
allMatches.push(...rawDefault);
|
|
5300
|
+
const rawDefault = await this.default.grep(pattern, path, glob);
|
|
5301
|
+
if (rawDefault.error) return rawDefault;
|
|
5302
|
+
allMatches.push(...rawDefault.matches || []);
|
|
4031
5303
|
for (const [routePrefix, backend] of Object.entries(this.routes)) {
|
|
4032
|
-
const raw = await backend.
|
|
4033
|
-
if (
|
|
4034
|
-
|
|
5304
|
+
const raw = await backend.grep(pattern, "/", glob);
|
|
5305
|
+
if (raw.error) return raw;
|
|
5306
|
+
const matches = (raw.matches || []).map((m) => ({
|
|
4035
5307
|
...m,
|
|
4036
5308
|
path: routePrefix.slice(0, -1) + m.path
|
|
4037
|
-
}))
|
|
5309
|
+
}));
|
|
5310
|
+
allMatches.push(...matches);
|
|
4038
5311
|
}
|
|
4039
|
-
return allMatches;
|
|
5312
|
+
return { matches: allMatches };
|
|
4040
5313
|
}
|
|
4041
5314
|
/**
|
|
4042
5315
|
* Structured glob matching returning FileInfo objects.
|
|
4043
5316
|
*/
|
|
4044
|
-
async
|
|
5317
|
+
async glob(pattern, path = "/") {
|
|
4045
5318
|
const results = [];
|
|
4046
5319
|
for (const [routePrefix, backend] of this.sortedRoutes) if (path.startsWith(routePrefix.replace(/\/$/, ""))) {
|
|
4047
5320
|
const searchPath = path.substring(routePrefix.length - 1);
|
|
4048
|
-
|
|
5321
|
+
const result = await backend.glob(pattern, searchPath || "/");
|
|
5322
|
+
if (result.error) return result;
|
|
5323
|
+
return { files: (result.files || []).map((fi) => ({
|
|
4049
5324
|
...fi,
|
|
4050
5325
|
path: routePrefix.slice(0, -1) + fi.path
|
|
4051
|
-
}));
|
|
5326
|
+
})) };
|
|
4052
5327
|
}
|
|
4053
|
-
const
|
|
4054
|
-
|
|
5328
|
+
const defaultResult = await this.default.glob(pattern, path);
|
|
5329
|
+
if (defaultResult.error) return defaultResult;
|
|
5330
|
+
results.push(...defaultResult.files || []);
|
|
4055
5331
|
for (const [routePrefix, backend] of Object.entries(this.routes)) {
|
|
4056
|
-
const
|
|
4057
|
-
|
|
5332
|
+
const result = await backend.glob(pattern, "/");
|
|
5333
|
+
if (result.error) continue;
|
|
5334
|
+
const files = (result.files || []).map((fi) => ({
|
|
4058
5335
|
...fi,
|
|
4059
5336
|
path: routePrefix.slice(0, -1) + fi.path
|
|
4060
|
-
}))
|
|
5337
|
+
}));
|
|
5338
|
+
results.push(...files);
|
|
4061
5339
|
}
|
|
4062
5340
|
results.sort((a, b) => a.path.localeCompare(b.path));
|
|
4063
|
-
return results;
|
|
5341
|
+
return { files: results };
|
|
4064
5342
|
}
|
|
4065
5343
|
/**
|
|
4066
5344
|
* Create a new file, routing to appropriate backend.
|
|
@@ -4288,7 +5566,7 @@ var LocalShellBackend = class LocalShellBackend extends FilesystemBackend {
|
|
|
4288
5566
|
*/
|
|
4289
5567
|
async read(filePath, offset = 0, limit = 500) {
|
|
4290
5568
|
const result = await super.read(filePath, offset, limit);
|
|
4291
|
-
if (
|
|
5569
|
+
if (result.error?.includes("ENOENT")) return { error: `File '${filePath}' not found` };
|
|
4292
5570
|
return result;
|
|
4293
5571
|
}
|
|
4294
5572
|
/**
|
|
@@ -4305,25 +5583,26 @@ var LocalShellBackend = class LocalShellBackend extends FilesystemBackend {
|
|
|
4305
5583
|
/**
|
|
4306
5584
|
* List directory contents, returning paths relative to rootDir.
|
|
4307
5585
|
*/
|
|
4308
|
-
async
|
|
4309
|
-
const
|
|
4310
|
-
if (
|
|
5586
|
+
async ls(dirPath) {
|
|
5587
|
+
const result = await super.ls(dirPath);
|
|
5588
|
+
if (result.error) return result;
|
|
5589
|
+
if (this.virtualMode) return result;
|
|
4311
5590
|
const cwdPrefix = this.cwd.endsWith(node_path.default.sep) ? this.cwd : this.cwd + node_path.default.sep;
|
|
4312
|
-
return
|
|
5591
|
+
return { files: (result.files || []).map((info) => ({
|
|
4313
5592
|
...info,
|
|
4314
5593
|
path: info.path.startsWith(cwdPrefix) ? info.path.slice(cwdPrefix.length) : info.path
|
|
4315
|
-
}));
|
|
5594
|
+
})) };
|
|
4316
5595
|
}
|
|
4317
5596
|
/**
|
|
4318
5597
|
* Glob matching that returns relative paths and includes directories.
|
|
4319
5598
|
*/
|
|
4320
|
-
async
|
|
5599
|
+
async glob(pattern, searchPath = "/") {
|
|
4321
5600
|
if (pattern.startsWith("/")) pattern = pattern.substring(1);
|
|
4322
5601
|
const resolvedSearchPath = searchPath === "/" || searchPath === "" ? this.cwd : this.virtualMode ? node_path.default.resolve(this.cwd, searchPath.replace(/^\//, "")) : node_path.default.resolve(this.cwd, searchPath);
|
|
4323
5602
|
try {
|
|
4324
|
-
if (!(await node_fs_promises.default.stat(resolvedSearchPath)).isDirectory()) return [];
|
|
5603
|
+
if (!(await node_fs_promises.default.stat(resolvedSearchPath)).isDirectory()) return { files: [] };
|
|
4325
5604
|
} catch {
|
|
4326
|
-
return [];
|
|
5605
|
+
return { files: [] };
|
|
4327
5606
|
}
|
|
4328
5607
|
const formatPath = (rel) => this.virtualMode ? `/${rel}` : rel;
|
|
4329
5608
|
const globOpts = {
|
|
@@ -4365,7 +5644,7 @@ var LocalShellBackend = class LocalShellBackend extends FilesystemBackend {
|
|
|
4365
5644
|
const [fileInfos, dirInfos] = await Promise.all([Promise.all(fileMatches.map(statFile)), Promise.all(dirMatches.map(statDir))]);
|
|
4366
5645
|
const results = [...fileInfos, ...dirInfos].filter((info) => info !== null);
|
|
4367
5646
|
results.sort((a, b) => a.path.localeCompare(b.path));
|
|
4368
|
-
return results;
|
|
5647
|
+
return { files: results };
|
|
4369
5648
|
}
|
|
4370
5649
|
/**
|
|
4371
5650
|
* Execute a shell command directly on the host system.
|
|
@@ -4640,9 +5919,9 @@ var BaseSandbox = class {
|
|
|
4640
5919
|
* including Alpine. No Python or Node.js needed.
|
|
4641
5920
|
*
|
|
4642
5921
|
* @param path - Absolute path to directory
|
|
4643
|
-
* @returns
|
|
5922
|
+
* @returns LsResult with list of FileInfo objects on success or error on failure.
|
|
4644
5923
|
*/
|
|
4645
|
-
async
|
|
5924
|
+
async ls(path) {
|
|
4646
5925
|
const command = buildLsCommand(path);
|
|
4647
5926
|
const result = await this.execute(command);
|
|
4648
5927
|
const infos = [];
|
|
@@ -4657,7 +5936,7 @@ var BaseSandbox = class {
|
|
|
4657
5936
|
modified_at: (/* @__PURE__ */ new Date(parsed.mtime * 1e3)).toISOString()
|
|
4658
5937
|
});
|
|
4659
5938
|
}
|
|
4660
|
-
return infos;
|
|
5939
|
+
return { files: infos };
|
|
4661
5940
|
}
|
|
4662
5941
|
/**
|
|
4663
5942
|
* Read file content with line numbers.
|
|
@@ -4672,11 +5951,26 @@ var BaseSandbox = class {
|
|
|
4672
5951
|
* @returns Formatted file content with line numbers, or error message
|
|
4673
5952
|
*/
|
|
4674
5953
|
async read(filePath, offset = 0, limit = 500) {
|
|
4675
|
-
|
|
5954
|
+
const mimeType = getMimeType(filePath);
|
|
5955
|
+
if (!isTextMimeType(mimeType)) {
|
|
5956
|
+
const results = await this.downloadFiles([filePath]);
|
|
5957
|
+
if (results[0].error || !results[0].content) return { error: `File '${filePath}' not found` };
|
|
5958
|
+
return {
|
|
5959
|
+
content: results[0].content,
|
|
5960
|
+
mimeType
|
|
5961
|
+
};
|
|
5962
|
+
}
|
|
5963
|
+
if (limit === 0) return {
|
|
5964
|
+
content: "",
|
|
5965
|
+
mimeType
|
|
5966
|
+
};
|
|
4676
5967
|
const command = buildReadCommand(filePath, offset, limit);
|
|
4677
5968
|
const result = await this.execute(command);
|
|
4678
|
-
if (result.exitCode !== 0) return
|
|
4679
|
-
return
|
|
5969
|
+
if (result.exitCode !== 0) return { error: `File '${filePath}' not found` };
|
|
5970
|
+
return {
|
|
5971
|
+
content: result.output,
|
|
5972
|
+
mimeType
|
|
5973
|
+
};
|
|
4680
5974
|
}
|
|
4681
5975
|
/**
|
|
4682
5976
|
* Read file content as raw FileData.
|
|
@@ -4684,18 +5978,25 @@ var BaseSandbox = class {
|
|
|
4684
5978
|
* Uses downloadFiles() directly — no runtime needed on the sandbox host.
|
|
4685
5979
|
*
|
|
4686
5980
|
* @param filePath - Absolute file path
|
|
4687
|
-
* @returns
|
|
5981
|
+
* @returns ReadRawResult with raw file data on success or error on failure
|
|
4688
5982
|
*/
|
|
4689
5983
|
async readRaw(filePath) {
|
|
4690
5984
|
const results = await this.downloadFiles([filePath]);
|
|
4691
|
-
if (results[0].error || !results[0].content)
|
|
4692
|
-
const lines = new TextDecoder().decode(results[0].content).split("\n");
|
|
5985
|
+
if (results[0].error || !results[0].content) return { error: `File '${filePath}' not found` };
|
|
4693
5986
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4694
|
-
|
|
4695
|
-
|
|
5987
|
+
const mimeType = getMimeType(filePath);
|
|
5988
|
+
if (!isTextMimeType(mimeType)) return { data: {
|
|
5989
|
+
content: results[0].content,
|
|
5990
|
+
mimeType,
|
|
4696
5991
|
created_at: now,
|
|
4697
5992
|
modified_at: now
|
|
4698
|
-
};
|
|
5993
|
+
} };
|
|
5994
|
+
return { data: {
|
|
5995
|
+
content: new TextDecoder().decode(results[0].content),
|
|
5996
|
+
mimeType,
|
|
5997
|
+
created_at: now,
|
|
5998
|
+
modified_at: now
|
|
5999
|
+
} };
|
|
4699
6000
|
}
|
|
4700
6001
|
/**
|
|
4701
6002
|
* Search for a literal text pattern in files using grep.
|
|
@@ -4705,23 +6006,25 @@ var BaseSandbox = class {
|
|
|
4705
6006
|
* @param glob - Optional glob pattern to filter which files to search.
|
|
4706
6007
|
* @returns List of GrepMatch dicts containing path, line number, and matched text.
|
|
4707
6008
|
*/
|
|
4708
|
-
async
|
|
6009
|
+
async grep(pattern, path = "/", glob = null) {
|
|
4709
6010
|
const command = buildGrepCommand(pattern, path, glob);
|
|
4710
6011
|
const output = (await this.execute(command)).output.trim();
|
|
4711
|
-
if (!output) return [];
|
|
6012
|
+
if (!output) return { matches: [] };
|
|
4712
6013
|
const matches = [];
|
|
4713
6014
|
for (const line of output.split("\n")) {
|
|
4714
6015
|
const parts = line.split(":");
|
|
4715
6016
|
if (parts.length >= 3) {
|
|
6017
|
+
const filePath = parts[0];
|
|
6018
|
+
if (!isTextMimeType(getMimeType(filePath))) continue;
|
|
4716
6019
|
const lineNum = parseInt(parts[1], 10);
|
|
4717
6020
|
if (!isNaN(lineNum)) matches.push({
|
|
4718
|
-
path:
|
|
6021
|
+
path: filePath,
|
|
4719
6022
|
line: lineNum,
|
|
4720
6023
|
text: parts.slice(2).join(":")
|
|
4721
6024
|
});
|
|
4722
6025
|
}
|
|
4723
6026
|
}
|
|
4724
|
-
return matches;
|
|
6027
|
+
return { matches };
|
|
4725
6028
|
}
|
|
4726
6029
|
/**
|
|
4727
6030
|
* Structured glob matching returning FileInfo objects.
|
|
@@ -4736,7 +6039,7 @@ var BaseSandbox = class {
|
|
|
4736
6039
|
* - `?` matches a single character except `/`
|
|
4737
6040
|
* - `[...]` character classes
|
|
4738
6041
|
*/
|
|
4739
|
-
async
|
|
6042
|
+
async glob(pattern, path = "/") {
|
|
4740
6043
|
const command = buildFindCommand(path);
|
|
4741
6044
|
const result = await this.execute(command);
|
|
4742
6045
|
const regex = globToPathRegex(pattern);
|
|
@@ -4754,7 +6057,7 @@ var BaseSandbox = class {
|
|
|
4754
6057
|
modified_at: (/* @__PURE__ */ new Date(parsed.mtime * 1e3)).toISOString()
|
|
4755
6058
|
});
|
|
4756
6059
|
}
|
|
4757
|
-
return infos;
|
|
6060
|
+
return { files: infos };
|
|
4758
6061
|
}
|
|
4759
6062
|
/**
|
|
4760
6063
|
* Create a new file with content.
|
|
@@ -4767,8 +6070,11 @@ var BaseSandbox = class {
|
|
|
4767
6070
|
const existCheck = await this.downloadFiles([filePath]);
|
|
4768
6071
|
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.` };
|
|
4769
6072
|
} catch {}
|
|
4770
|
-
const
|
|
4771
|
-
|
|
6073
|
+
const mimeType = getMimeType(filePath);
|
|
6074
|
+
let fileContent;
|
|
6075
|
+
if (isTextMimeType(mimeType)) fileContent = new TextEncoder().encode(content);
|
|
6076
|
+
else fileContent = Buffer.from(content, "base64");
|
|
6077
|
+
const results = await this.uploadFiles([[filePath, fileContent]]);
|
|
4772
6078
|
if (results[0].error) return { error: `Failed to write to ${filePath}: ${results[0].error}` };
|
|
4773
6079
|
return {
|
|
4774
6080
|
path: filePath,
|
|
@@ -5103,9 +6409,44 @@ function createCacheBreakpointMiddleware() {
|
|
|
5103
6409
|
}
|
|
5104
6410
|
//#endregion
|
|
5105
6411
|
//#region src/agent.ts
|
|
5106
|
-
const
|
|
6412
|
+
const BASE_AGENT_PROMPT = langchain.context`
|
|
6413
|
+
You are a Deep Agent, an AI assistant that helps users accomplish tasks using tools. You respond with text and tool calls. The user can see your responses and tool outputs in real time.
|
|
6414
|
+
|
|
6415
|
+
## Core Behavior
|
|
6416
|
+
|
|
6417
|
+
- Be concise and direct. Don't over-explain unless asked.
|
|
6418
|
+
- NEVER add unnecessary preamble (\"Sure!\", \"Great question!\", \"I'll now...\").
|
|
6419
|
+
- Don't say \"I'll now do X\" — just do it.
|
|
6420
|
+
- If the request is ambiguous, ask questions before acting.
|
|
6421
|
+
- If asked how to approach something, explain first, then act.
|
|
6422
|
+
|
|
6423
|
+
## Professional Objectivity
|
|
6424
|
+
|
|
6425
|
+
- Prioritize accuracy over validating the user's beliefs
|
|
6426
|
+
- Disagree respectfully when the user is incorrect
|
|
6427
|
+
- Avoid unnecessary superlatives, praise, or emotional validation
|
|
6428
|
+
|
|
6429
|
+
## Doing Tasks
|
|
6430
|
+
|
|
6431
|
+
When the user asks you to do something:
|
|
6432
|
+
|
|
6433
|
+
1. **Understand first** — read relevant files, check existing patterns. Quick but thorough — gather enough evidence to start, then iterate.
|
|
6434
|
+
2. **Act** — implement the solution. Work quickly but accurately.
|
|
6435
|
+
3. **Verify** — check your work against what was asked, not against your own output. Your first attempt is rarely correct — iterate.
|
|
6436
|
+
|
|
6437
|
+
Keep working until the task is fully complete. Don't stop partway and explain what you would do — just do it. Only yield back to the user when the task is done or you're genuinely blocked.
|
|
6438
|
+
|
|
6439
|
+
**When things go wrong:**
|
|
6440
|
+
- If something fails repeatedly, stop and analyze *why* — don't keep retrying the same approach.
|
|
6441
|
+
- If you're blocked, tell the user what's wrong and ask for guidance.
|
|
6442
|
+
|
|
6443
|
+
## Progress Updates
|
|
6444
|
+
|
|
6445
|
+
For longer tasks, provide brief progress updates at reasonable intervals — a concise sentence recapping what you've done and what's next.
|
|
6446
|
+
`;
|
|
5107
6447
|
const BUILTIN_TOOL_NAMES = new Set([
|
|
5108
6448
|
...FILESYSTEM_TOOL_NAMES,
|
|
6449
|
+
...ASYNC_TASK_TOOL_NAMES,
|
|
5109
6450
|
"task",
|
|
5110
6451
|
"write_todos"
|
|
5111
6452
|
]);
|
|
@@ -5122,19 +6463,18 @@ function isAnthropicModel(model) {
|
|
|
5122
6463
|
return model.getName() === "ChatAnthropic";
|
|
5123
6464
|
}
|
|
5124
6465
|
/**
|
|
5125
|
-
* Create a Deep Agent
|
|
6466
|
+
* Create a Deep Agent.
|
|
5126
6467
|
*
|
|
5127
|
-
*
|
|
5128
|
-
*
|
|
5129
|
-
*
|
|
5130
|
-
* -
|
|
5131
|
-
*
|
|
5132
|
-
*
|
|
5133
|
-
*
|
|
5134
|
-
* - Human-in-the-loop (humanInTheLoopMiddleware) - optional
|
|
6468
|
+
* This is the main entry point for building a production-style agent with
|
|
6469
|
+
* deepagents. It gives you a strong default runtime (filesystem, tasks,
|
|
6470
|
+
* subagents, summarization) and lets you opt into skills, memory,
|
|
6471
|
+
* human-in-the-loop interrupts, async subagents, and custom middleware.
|
|
6472
|
+
*
|
|
6473
|
+
* The runtime is intentionally opinionated: defaults work out of the box, and
|
|
6474
|
+
* when you customize behavior, the middleware ordering stays deterministic.
|
|
5135
6475
|
*
|
|
5136
6476
|
* @param params Configuration parameters for the agent
|
|
5137
|
-
* @returns
|
|
6477
|
+
* @returns Deep Agent instance with inferred state/response types
|
|
5138
6478
|
*
|
|
5139
6479
|
* @example
|
|
5140
6480
|
* ```typescript
|
|
@@ -5153,92 +6493,92 @@ function isAnthropicModel(model) {
|
|
|
5153
6493
|
* ```
|
|
5154
6494
|
*/
|
|
5155
6495
|
function createDeepAgent(params = {}) {
|
|
5156
|
-
const { model = "claude-sonnet-4-
|
|
6496
|
+
const { model = new _langchain_anthropic.ChatAnthropic("claude-sonnet-4-6"), tools = [], systemPrompt, middleware: customMiddleware = [], subagents = [], responseFormat, contextSchema, checkpointer, store, backend = (config) => new StateBackend(config), interruptOn, name, memory, skills } = params;
|
|
5157
6497
|
const collidingTools = tools.map((t) => t.name).filter((n) => typeof n === "string" && BUILTIN_TOOL_NAMES.has(n));
|
|
5158
6498
|
if (collidingTools.length > 0) throw new ConfigurationError(`Tool name(s) [${collidingTools.join(", ")}] conflict with built-in tools. Rename your custom tools to avoid this.`, "TOOL_NAME_COLLISION");
|
|
5159
6499
|
const anthropicModel = isAnthropicModel(model);
|
|
5160
|
-
const
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
}] : [
|
|
5164
|
-
type: "text",
|
|
5165
|
-
text: BASE_PROMPT
|
|
5166
|
-
}, ...typeof systemPrompt.content === "string" ? [{
|
|
5167
|
-
type: "text",
|
|
5168
|
-
text: systemPrompt.content
|
|
5169
|
-
}] : systemPrompt.content] : [{
|
|
5170
|
-
type: "text",
|
|
5171
|
-
text: BASE_PROMPT
|
|
5172
|
-
}] });
|
|
5173
|
-
/**
|
|
5174
|
-
* Create backend configuration for filesystem middleware
|
|
5175
|
-
* If no backend is provided, use a factory that creates a StateBackend
|
|
5176
|
-
*/
|
|
5177
|
-
const filesystemBackend = backend ? backend : (runtime) => new StateBackend(runtime);
|
|
5178
|
-
/**
|
|
5179
|
-
* Skills middleware (created conditionally for runtime use)
|
|
5180
|
-
*/
|
|
5181
|
-
const skillsMiddlewareArray = skills != null && skills.length > 0 ? [createSkillsMiddleware({
|
|
5182
|
-
backend: filesystemBackend,
|
|
5183
|
-
sources: skills
|
|
5184
|
-
})] : [];
|
|
5185
|
-
/**
|
|
5186
|
-
* Memory middleware (created conditionally for runtime use)
|
|
5187
|
-
*/
|
|
5188
|
-
const memoryMiddlewareArray = memory != null && memory.length > 0 ? [createMemoryMiddleware({
|
|
5189
|
-
backend: filesystemBackend,
|
|
5190
|
-
sources: memory,
|
|
5191
|
-
addCacheControl: anthropicModel
|
|
5192
|
-
})] : [];
|
|
6500
|
+
const cacheMiddleware = anthropicModel ? [(0, langchain.anthropicPromptCachingMiddleware)({
|
|
6501
|
+
unsupportedModelBehavior: "ignore",
|
|
6502
|
+
minMessagesToCache: 1
|
|
6503
|
+
}), createCacheBreakpointMiddleware()] : [];
|
|
5193
6504
|
/**
|
|
5194
6505
|
* Process subagents to add SkillsMiddleware for those with their own skills.
|
|
5195
6506
|
*
|
|
5196
6507
|
* Custom subagents do NOT inherit skills from the main agent by default.
|
|
5197
|
-
* Only the general-purpose subagent inherits the main agent's skills
|
|
6508
|
+
* Only the general-purpose subagent inherits the main agent's skills.
|
|
5198
6509
|
* If a custom subagent needs skills, it must specify its own `skills` array.
|
|
5199
6510
|
*/
|
|
5200
|
-
const
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
sources: subagent.skills ?? []
|
|
5217
|
-
});
|
|
6511
|
+
const normalizeSubagentSpec = (input) => {
|
|
6512
|
+
const subagentMiddleware = [
|
|
6513
|
+
(0, langchain.todoListMiddleware)(),
|
|
6514
|
+
createFilesystemMiddleware({ backend }),
|
|
6515
|
+
createSummarizationMiddleware({
|
|
6516
|
+
backend,
|
|
6517
|
+
model
|
|
6518
|
+
}),
|
|
6519
|
+
createPatchToolCallsMiddleware(),
|
|
6520
|
+
...input.skills != null && input.skills.length > 0 ? [createSkillsMiddleware({
|
|
6521
|
+
backend,
|
|
6522
|
+
sources: input.skills
|
|
6523
|
+
})] : [],
|
|
6524
|
+
...input.middleware ?? [],
|
|
6525
|
+
...cacheMiddleware
|
|
6526
|
+
];
|
|
5218
6527
|
return {
|
|
5219
|
-
...
|
|
5220
|
-
|
|
6528
|
+
...input,
|
|
6529
|
+
tools: input.tools ?? [],
|
|
6530
|
+
middleware: subagentMiddleware
|
|
5221
6531
|
};
|
|
5222
|
-
}
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
6532
|
+
};
|
|
6533
|
+
const allSubagents = subagents;
|
|
6534
|
+
const asyncSubAgents = allSubagents.filter((item) => isAsyncSubAgent(item));
|
|
6535
|
+
const inlineSubagents = allSubagents.filter((item) => !isAsyncSubAgent(item)).map((item) => "runnable" in item ? item : normalizeSubagentSpec(item));
|
|
6536
|
+
if (!inlineSubagents.some((item) => item.name === GENERAL_PURPOSE_SUBAGENT["name"])) {
|
|
6537
|
+
const generalPurposeSpec = normalizeSubagentSpec({
|
|
6538
|
+
...GENERAL_PURPOSE_SUBAGENT,
|
|
6539
|
+
model,
|
|
6540
|
+
skills,
|
|
6541
|
+
tools
|
|
6542
|
+
});
|
|
6543
|
+
inlineSubagents.unshift(generalPurposeSpec);
|
|
6544
|
+
}
|
|
6545
|
+
const skillsMiddleware = skills != null && skills.length > 0 ? [createSkillsMiddleware({
|
|
6546
|
+
backend,
|
|
6547
|
+
sources: skills
|
|
6548
|
+
})] : [];
|
|
6549
|
+
const [todoMiddleware, fsMiddleware, subagentMiddleware, summarizationMiddleware, patchToolCallsMiddleware] = [
|
|
5234
6550
|
(0, langchain.todoListMiddleware)(),
|
|
5235
|
-
createFilesystemMiddleware({ backend
|
|
6551
|
+
createFilesystemMiddleware({ backend }),
|
|
6552
|
+
createSubAgentMiddleware({
|
|
6553
|
+
defaultModel: model,
|
|
6554
|
+
defaultTools: tools,
|
|
6555
|
+
defaultInterruptOn: interruptOn,
|
|
6556
|
+
subagents: inlineSubagents,
|
|
6557
|
+
generalPurposeAgent: false
|
|
6558
|
+
}),
|
|
5236
6559
|
createSummarizationMiddleware({
|
|
5237
6560
|
model,
|
|
5238
|
-
backend
|
|
6561
|
+
backend
|
|
5239
6562
|
}),
|
|
5240
6563
|
createPatchToolCallsMiddleware()
|
|
5241
6564
|
];
|
|
6565
|
+
const middleware = [
|
|
6566
|
+
todoMiddleware,
|
|
6567
|
+
...skillsMiddleware,
|
|
6568
|
+
fsMiddleware,
|
|
6569
|
+
subagentMiddleware,
|
|
6570
|
+
summarizationMiddleware,
|
|
6571
|
+
patchToolCallsMiddleware,
|
|
6572
|
+
...asyncSubAgents.length > 0 ? [createAsyncSubAgentMiddleware({ asyncSubAgents })] : [],
|
|
6573
|
+
...customMiddleware,
|
|
6574
|
+
...cacheMiddleware,
|
|
6575
|
+
...memory && memory.length > 0 ? [createMemoryMiddleware({
|
|
6576
|
+
backend,
|
|
6577
|
+
sources: memory,
|
|
6578
|
+
addCacheControl: anthropicModel
|
|
6579
|
+
})] : [],
|
|
6580
|
+
...interruptOn ? [(0, langchain.humanInTheLoopMiddleware)({ interruptOn })] : []
|
|
6581
|
+
];
|
|
5242
6582
|
/**
|
|
5243
6583
|
* Return as DeepAgent with proper DeepAgentTypeConfig
|
|
5244
6584
|
* - Response: InferStructuredResponse<TResponse> (unwraps ToolStrategy<T>/ProviderStrategy<T> → T)
|
|
@@ -5250,54 +6590,32 @@ function createDeepAgent(params = {}) {
|
|
|
5250
6590
|
*/
|
|
5251
6591
|
return (0, langchain.createAgent)({
|
|
5252
6592
|
model,
|
|
5253
|
-
systemPrompt:
|
|
6593
|
+
systemPrompt: typeof systemPrompt === "string" ? new langchain.SystemMessage({ contentBlocks: [{
|
|
6594
|
+
type: "text",
|
|
6595
|
+
text: systemPrompt
|
|
6596
|
+
}, {
|
|
6597
|
+
type: "text",
|
|
6598
|
+
text: BASE_AGENT_PROMPT
|
|
6599
|
+
}] }) : langchain.SystemMessage.isInstance(systemPrompt) ? new langchain.SystemMessage({ contentBlocks: [...systemPrompt.contentBlocks, {
|
|
6600
|
+
type: "text",
|
|
6601
|
+
text: BASE_AGENT_PROMPT
|
|
6602
|
+
}] }) : new langchain.SystemMessage({ contentBlocks: [{
|
|
6603
|
+
type: "text",
|
|
6604
|
+
text: BASE_AGENT_PROMPT
|
|
6605
|
+
}] }),
|
|
5254
6606
|
tools,
|
|
5255
|
-
middleware
|
|
5256
|
-
|
|
5257
|
-
(0, langchain.todoListMiddleware)(),
|
|
5258
|
-
createFilesystemMiddleware({ backend: filesystemBackend }),
|
|
5259
|
-
createSubAgentMiddleware({
|
|
5260
|
-
defaultModel: model,
|
|
5261
|
-
defaultTools: tools,
|
|
5262
|
-
defaultMiddleware: [...subagentMiddleware, ...anthropicModel ? [(0, langchain.anthropicPromptCachingMiddleware)({
|
|
5263
|
-
unsupportedModelBehavior: "ignore",
|
|
5264
|
-
minMessagesToCache: 1
|
|
5265
|
-
}), createCacheBreakpointMiddleware()] : []],
|
|
5266
|
-
generalPurposeMiddleware: [
|
|
5267
|
-
...subagentMiddleware,
|
|
5268
|
-
...skillsMiddlewareArray,
|
|
5269
|
-
...anthropicModel ? [(0, langchain.anthropicPromptCachingMiddleware)({
|
|
5270
|
-
unsupportedModelBehavior: "ignore",
|
|
5271
|
-
minMessagesToCache: 1
|
|
5272
|
-
}), createCacheBreakpointMiddleware()] : []
|
|
5273
|
-
],
|
|
5274
|
-
defaultInterruptOn: interruptOn,
|
|
5275
|
-
subagents: processedSubagents,
|
|
5276
|
-
generalPurposeAgent: true
|
|
5277
|
-
}),
|
|
5278
|
-
createSummarizationMiddleware({
|
|
5279
|
-
model,
|
|
5280
|
-
backend: filesystemBackend
|
|
5281
|
-
}),
|
|
5282
|
-
createPatchToolCallsMiddleware()
|
|
5283
|
-
],
|
|
5284
|
-
...skillsMiddlewareArray,
|
|
5285
|
-
...customMiddleware,
|
|
5286
|
-
...anthropicModel ? [(0, langchain.anthropicPromptCachingMiddleware)({
|
|
5287
|
-
unsupportedModelBehavior: "ignore",
|
|
5288
|
-
minMessagesToCache: 1
|
|
5289
|
-
}), createCacheBreakpointMiddleware()] : [],
|
|
5290
|
-
...memoryMiddlewareArray,
|
|
5291
|
-
...interruptOn ? [(0, langchain.humanInTheLoopMiddleware)({ interruptOn })] : []
|
|
5292
|
-
],
|
|
5293
|
-
...responseFormat != null && { responseFormat },
|
|
6607
|
+
middleware,
|
|
6608
|
+
...responseFormat !== null && { responseFormat },
|
|
5294
6609
|
contextSchema,
|
|
5295
6610
|
checkpointer,
|
|
5296
6611
|
store,
|
|
5297
6612
|
name
|
|
5298
6613
|
}).withConfig({
|
|
5299
6614
|
recursionLimit: 1e4,
|
|
5300
|
-
metadata: {
|
|
6615
|
+
metadata: {
|
|
6616
|
+
ls_integration: "deepagents",
|
|
6617
|
+
lc_agent_name: name
|
|
6618
|
+
}
|
|
5301
6619
|
});
|
|
5302
6620
|
}
|
|
5303
6621
|
//#endregion
|
|
@@ -5827,8 +7145,12 @@ exports.SandboxError = SandboxError;
|
|
|
5827
7145
|
exports.StateBackend = StateBackend;
|
|
5828
7146
|
exports.StoreBackend = StoreBackend;
|
|
5829
7147
|
exports.TASK_SYSTEM_PROMPT = TASK_SYSTEM_PROMPT;
|
|
7148
|
+
exports.adaptBackendProtocol = adaptBackendProtocol;
|
|
7149
|
+
exports.adaptSandboxProtocol = adaptSandboxProtocol;
|
|
5830
7150
|
exports.computeSummarizationDefaults = computeSummarizationDefaults;
|
|
5831
7151
|
exports.createAgentMemoryMiddleware = createAgentMemoryMiddleware;
|
|
7152
|
+
exports.createAsyncSubAgentMiddleware = createAsyncSubAgentMiddleware;
|
|
7153
|
+
exports.createCompletionCallbackMiddleware = createCompletionCallbackMiddleware;
|
|
5832
7154
|
exports.createDeepAgent = createDeepAgent;
|
|
5833
7155
|
exports.createFilesystemMiddleware = createFilesystemMiddleware;
|
|
5834
7156
|
exports.createMemoryMiddleware = createMemoryMiddleware;
|
|
@@ -5839,7 +7161,9 @@ exports.createSubAgentMiddleware = createSubAgentMiddleware;
|
|
|
5839
7161
|
exports.createSummarizationMiddleware = createSummarizationMiddleware;
|
|
5840
7162
|
exports.filesValue = filesValue;
|
|
5841
7163
|
exports.findProjectRoot = findProjectRoot;
|
|
7164
|
+
exports.isAsyncSubAgent = isAsyncSubAgent;
|
|
5842
7165
|
exports.isSandboxBackend = isSandboxBackend;
|
|
7166
|
+
exports.isSandboxProtocol = isSandboxProtocol;
|
|
5843
7167
|
exports.listSkills = listSkills;
|
|
5844
7168
|
exports.parseSkillMetadata = parseSkillMetadata;
|
|
5845
7169
|
exports.resolveBackend = resolveBackend;
|