deepagents 0.0.2 → 1.0.0-beta.2
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/README.md +43 -2
- package/dist/index.d.ts +192 -10
- package/dist/index.js +898 -11
- package/package.json +29 -29
- package/dist/graph.d.ts +0 -128
- package/dist/graph.js +0 -104
- package/dist/interrupt.d.ts +0 -2
- package/dist/interrupt.js +0 -103
- package/dist/model.d.ts +0 -17
- package/dist/model.js +0 -22
- package/dist/prompts.d.ts +0 -31
- package/dist/prompts.js +0 -279
- package/dist/state.d.ts +0 -37
- package/dist/state.js +0 -53
- package/dist/subAgent.d.ts +0 -41
- package/dist/subAgent.js +0 -128
- package/dist/tools.d.ts +0 -142
- package/dist/tools.js +0 -205
- package/dist/types.d.ts +0 -53
- package/dist/types.js +0 -9
package/dist/index.js
CHANGED
|
@@ -1,12 +1,899 @@
|
|
|
1
|
+
import { AIMessage, ToolMessage, anthropicPromptCachingMiddleware, createAgent, createMiddleware, humanInTheLoopMiddleware, summarizationMiddleware, todoListMiddleware, tool } from "langchain";
|
|
2
|
+
import { Command, REMOVE_ALL_MESSAGES, getConfig, getCurrentTaskInput, isCommand } from "@langchain/langgraph";
|
|
3
|
+
import { z } from "zod/v3";
|
|
4
|
+
import { withLangGraph } from "@langchain/langgraph/zod";
|
|
5
|
+
import { HumanMessage, RemoveMessage } from "@langchain/core/messages";
|
|
6
|
+
|
|
7
|
+
//#region src/middleware/fs.ts
|
|
8
|
+
const MEMORIES_PREFIX = "/memories/";
|
|
9
|
+
const EMPTY_CONTENT_WARNING = "System reminder: File exists but has empty contents";
|
|
10
|
+
const MAX_LINE_LENGTH = 2e3;
|
|
11
|
+
const LINE_NUMBER_WIDTH = 6;
|
|
12
|
+
const DEFAULT_READ_OFFSET = 0;
|
|
13
|
+
const DEFAULT_READ_LIMIT = 2e3;
|
|
1
14
|
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
* Zod v3 schema for FileData
|
|
16
|
+
*/
|
|
17
|
+
const FileDataSchema = z.object({
|
|
18
|
+
content: z.array(z.string()),
|
|
19
|
+
created_at: z.string(),
|
|
20
|
+
modified_at: z.string()
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Merge file updates with support for deletions.
|
|
24
|
+
*
|
|
25
|
+
* This reducer enables file deletion by treating `null` values in the right
|
|
26
|
+
* dictionary as deletion markers. It's designed to work with LangGraph's
|
|
27
|
+
* state management where annotated reducers control how state updates merge.
|
|
28
|
+
*/
|
|
29
|
+
function fileDataReducer(left, right) {
|
|
30
|
+
if (left === void 0) {
|
|
31
|
+
const result$1 = {};
|
|
32
|
+
for (const [key, value] of Object.entries(right)) if (value !== null) result$1[key] = value;
|
|
33
|
+
return result$1;
|
|
34
|
+
}
|
|
35
|
+
const result = { ...left };
|
|
36
|
+
for (const [key, value] of Object.entries(right)) if (value === null) delete result[key];
|
|
37
|
+
else result[key] = value;
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Validate and normalize file path for security.
|
|
42
|
+
*
|
|
43
|
+
* Ensures paths are safe to use by preventing directory traversal attacks
|
|
44
|
+
* and enforcing consistent formatting. All paths are normalized to use
|
|
45
|
+
* forward slashes and start with a leading slash.
|
|
46
|
+
*/
|
|
47
|
+
function validatePath(path, options) {
|
|
48
|
+
if (path.includes("..") || path.startsWith("~")) throw new Error(`Path traversal not allowed: ${path}`);
|
|
49
|
+
let normalized = path.replace(/\/+/g, "/").replace(/\/\./g, "/").replace(/\\/g, "/");
|
|
50
|
+
if (!normalized.startsWith("/")) normalized = `/${normalized}`;
|
|
51
|
+
if (options?.allowedPrefixes) {
|
|
52
|
+
if (!options.allowedPrefixes.some((prefix) => normalized.startsWith(prefix))) throw new Error(`Path must start with one of ${options.allowedPrefixes.join(", ")}: ${path}`);
|
|
53
|
+
}
|
|
54
|
+
return normalized;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Format file content with line numbers for display.
|
|
58
|
+
*
|
|
59
|
+
* Converts file content to a numbered format similar to `cat -n` output,
|
|
60
|
+
* with support for two different formatting styles.
|
|
61
|
+
*/
|
|
62
|
+
function formatContentWithLineNumbers(content, options = {}) {
|
|
63
|
+
const { formatStyle = "pipe", startLine = 1 } = options;
|
|
64
|
+
let lines;
|
|
65
|
+
if (typeof content === "string") {
|
|
66
|
+
lines = content.split("\n");
|
|
67
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines = lines.slice(0, -1);
|
|
68
|
+
} else lines = content;
|
|
69
|
+
if (formatStyle === "pipe") return lines.map((line, i) => `${i + startLine}|${line}`).join("\n");
|
|
70
|
+
return lines.map((line, i) => `${(i + startLine).toString().padStart(LINE_NUMBER_WIDTH)}\t${line.slice(0, MAX_LINE_LENGTH)}`).join("\n");
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Create a FileData object with automatic timestamp generation.
|
|
74
|
+
*/
|
|
75
|
+
function createFileData(content, options) {
|
|
76
|
+
const lines = typeof content === "string" ? content.split("\n") : content;
|
|
77
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
78
|
+
return {
|
|
79
|
+
content: lines,
|
|
80
|
+
created_at: options?.createdAt || now,
|
|
81
|
+
modified_at: now
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Update FileData with new content while preserving creation timestamp.
|
|
86
|
+
*/
|
|
87
|
+
function updateFileData(fileData, content) {
|
|
88
|
+
const lines = typeof content === "string" ? content.split("\n") : content;
|
|
89
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
90
|
+
return {
|
|
91
|
+
content: lines,
|
|
92
|
+
created_at: fileData.created_at,
|
|
93
|
+
modified_at: now
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Convert FileData to plain string content.
|
|
98
|
+
*/
|
|
99
|
+
function fileDataToString(fileData) {
|
|
100
|
+
return fileData.content.join("\n");
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Check if file content is empty and return a warning message.
|
|
104
|
+
*/
|
|
105
|
+
function checkEmptyContent(content) {
|
|
106
|
+
if (!content || content.trim() === "") return EMPTY_CONTENT_WARNING;
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Check if a file path is in the longterm memory filesystem.
|
|
111
|
+
*/
|
|
112
|
+
function hasMemoriesPrefix(filePath) {
|
|
113
|
+
return filePath.startsWith(MEMORIES_PREFIX);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Add the longterm memory prefix to a file path.
|
|
117
|
+
*/
|
|
118
|
+
function appendMemoriesPrefix(filePath) {
|
|
119
|
+
return `/memories${filePath}`;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Remove the longterm memory prefix from a file path.
|
|
123
|
+
*/
|
|
124
|
+
function stripMemoriesPrefix(filePath) {
|
|
125
|
+
if (filePath.startsWith(MEMORIES_PREFIX)) return filePath.slice(9);
|
|
126
|
+
return filePath;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Get the namespace for longterm filesystem storage.
|
|
130
|
+
*
|
|
131
|
+
* Returns a tuple for organizing files in the store. If an assistant_id is available
|
|
132
|
+
* in the config metadata, returns a 2-tuple of (assistant_id, "filesystem") to provide
|
|
133
|
+
* per-assistant isolation. Otherwise, returns a 1-tuple of ("filesystem",) for shared storage.
|
|
134
|
+
*
|
|
135
|
+
* @returns Namespace tuple for store operations, either [assistant_id, "filesystem"] or ["filesystem"]
|
|
136
|
+
*/
|
|
137
|
+
function getNamespace() {
|
|
138
|
+
const namespace = "filesystem";
|
|
139
|
+
try {
|
|
140
|
+
const config = getConfig();
|
|
141
|
+
if (!config) return [namespace];
|
|
142
|
+
const assistantId = config.metadata?.assistant_id;
|
|
143
|
+
if (!assistantId) return [namespace];
|
|
144
|
+
return [assistantId, namespace];
|
|
145
|
+
} catch {
|
|
146
|
+
return [namespace];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Convert a store Item to FileData format.
|
|
151
|
+
*/
|
|
152
|
+
function convertStoreItemToFileData(storeItem) {
|
|
153
|
+
const value = storeItem.value;
|
|
154
|
+
if (!Array.isArray(value.content)) throw new Error(`Store item does not contain valid content field. Got: ${Object.keys(value).join(", ")}`);
|
|
155
|
+
if (typeof value.created_at !== "string") throw new Error(`Store item does not contain valid created_at field. Got: ${Object.keys(value).join(", ")}`);
|
|
156
|
+
if (typeof value.modified_at !== "string") throw new Error(`Store item does not contain valid modified_at field. Got: ${Object.keys(value).join(", ")}`);
|
|
157
|
+
return {
|
|
158
|
+
content: value.content,
|
|
159
|
+
created_at: value.created_at,
|
|
160
|
+
modified_at: value.modified_at
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Convert FileData to a dict suitable for store.put().
|
|
165
|
+
*/
|
|
166
|
+
function convertFileDataToStoreItem(fileData) {
|
|
167
|
+
return {
|
|
168
|
+
content: fileData.content,
|
|
169
|
+
created_at: fileData.created_at,
|
|
170
|
+
modified_at: fileData.modified_at
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const stateSchema = z.object({ files: withLangGraph(z.record(z.string(), FileDataSchema).default(() => ({})), { reducer: {
|
|
174
|
+
fn: fileDataReducer,
|
|
175
|
+
schema: z.record(z.string(), FileDataSchema.nullable())
|
|
176
|
+
} }) });
|
|
177
|
+
const LIST_FILES_TOOL_DESCRIPTION = `Lists all files in the filesystem, optionally filtering by directory.
|
|
178
|
+
|
|
179
|
+
Usage:
|
|
180
|
+
- The ls tool will return a list of all files in the filesystem.
|
|
181
|
+
- You can optionally provide a path parameter to list files in a specific directory.
|
|
182
|
+
- This is very useful for exploring the file system and finding the right file to read or edit.
|
|
183
|
+
- You should almost ALWAYS use this tool before using the read_file or edit_file tools.`;
|
|
184
|
+
const LIST_FILES_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT = `\n- Files from the longterm filesystem will be prefixed with the ${MEMORIES_PREFIX} path.`;
|
|
185
|
+
const READ_FILE_TOOL_DESCRIPTION = `Reads a file from the filesystem. You can access any file directly by using this tool.
|
|
186
|
+
Assume this tool is able to read all files on the machine. 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.
|
|
187
|
+
|
|
188
|
+
Usage:
|
|
189
|
+
- The file_path parameter must be an absolute path, not a relative path
|
|
190
|
+
- By default, it reads up to 2000 lines starting from the beginning of the file
|
|
191
|
+
- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters
|
|
192
|
+
- Any lines longer than 2000 characters will be truncated
|
|
193
|
+
- Results are returned using cat -n format, with line numbers starting at 1
|
|
194
|
+
- 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.
|
|
195
|
+
- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.
|
|
196
|
+
- You should ALWAYS make sure a file has been read before editing it.`;
|
|
197
|
+
const READ_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT = `\n- file_paths prefixed with the ${MEMORIES_PREFIX} path will be read from the longterm filesystem.`;
|
|
198
|
+
const EDIT_FILE_TOOL_DESCRIPTION = `Performs exact string replacements in files.
|
|
199
|
+
|
|
200
|
+
Usage:
|
|
201
|
+
- You must use your read_file tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
|
|
202
|
+
- When editing text from read_file tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.
|
|
203
|
+
- ALWAYS prefer editing existing files. NEVER write new files unless explicitly required.
|
|
204
|
+
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
|
|
205
|
+
- The edit will FAIL if \`old_string\` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use \`replace_all\` to change every instance of \`old_string\`.
|
|
206
|
+
- Use \`replace_all\` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.`;
|
|
207
|
+
const EDIT_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT = `\n- You can edit files in the longterm filesystem by prefixing the filename with the ${MEMORIES_PREFIX} path.`;
|
|
208
|
+
const WRITE_FILE_TOOL_DESCRIPTION = `Writes to a new file in the filesystem.
|
|
209
|
+
|
|
210
|
+
Usage:
|
|
211
|
+
- The file_path parameter must be an absolute path, not a relative path
|
|
212
|
+
- The content parameter must be a string
|
|
213
|
+
- The write_file tool will create a new file.
|
|
214
|
+
- Prefer to edit existing files over creating new ones when possible.`;
|
|
215
|
+
const WRITE_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT = `\n- file_paths prefixed with the ${MEMORIES_PREFIX} path will be written to the longterm filesystem.`;
|
|
216
|
+
const FILESYSTEM_SYSTEM_PROMPT = `## Filesystem Tools \`ls\`, \`read_file\`, \`write_file\`, \`edit_file\`
|
|
217
|
+
|
|
218
|
+
You have access to a filesystem which you can interact with using these tools.
|
|
219
|
+
All file paths must start with a /.
|
|
220
|
+
|
|
221
|
+
- ls: list all files in the filesystem
|
|
222
|
+
- read_file: read a file from the filesystem
|
|
223
|
+
- write_file: write to a file in the filesystem
|
|
224
|
+
- edit_file: edit a file in the filesystem`;
|
|
225
|
+
const FILESYSTEM_SYSTEM_PROMPT_LONGTERM_SUPPLEMENT = `
|
|
226
|
+
|
|
227
|
+
You also have access to a longterm filesystem in which you can store files that you want to keep around for longer than the current conversation.
|
|
228
|
+
In order to interact with the longterm filesystem, you can use those same tools, but filenames must be prefixed with the ${MEMORIES_PREFIX} path.
|
|
229
|
+
Remember, to interact with the longterm filesystem, you must prefix the filename with the ${MEMORIES_PREFIX} path.`;
|
|
230
|
+
const TOO_LARGE_TOOL_MSG = `Tool result too large, the result of this tool call {tool_call_id} was saved in the filesystem at this path: {file_path}
|
|
231
|
+
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.
|
|
232
|
+
You can do this by specifying an offset and limit in the read_file tool call.
|
|
233
|
+
For example, to read the first 100 lines, you can use the read_file tool with offset=0 and limit=100.
|
|
234
|
+
|
|
235
|
+
Here are the first 10 lines of the result:
|
|
236
|
+
{content_sample}
|
|
237
|
+
`;
|
|
238
|
+
function assertStore(config) {
|
|
239
|
+
if (!("store" in config) || config.store == null) throw new Error("Missing store when long term memory is enabled");
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Generate the ls (list files) tool.
|
|
243
|
+
*/
|
|
244
|
+
function createLsTool(customDescription, longTermMemory) {
|
|
245
|
+
let toolDescription = LIST_FILES_TOOL_DESCRIPTION;
|
|
246
|
+
if (customDescription) toolDescription = customDescription;
|
|
247
|
+
else if (longTermMemory) toolDescription += LIST_FILES_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT;
|
|
248
|
+
return tool(async (input, config) => {
|
|
249
|
+
const filesDict = getCurrentTaskInput(config).files || {};
|
|
250
|
+
let files = Object.keys(filesDict);
|
|
251
|
+
if (longTermMemory) {
|
|
252
|
+
assertStore(config);
|
|
253
|
+
const namespace = getNamespace();
|
|
254
|
+
const longtermFilesPrefixed = (await config.store.search(namespace)).map((f) => appendMemoriesPrefix(f.key));
|
|
255
|
+
files = files.concat(longtermFilesPrefixed);
|
|
256
|
+
}
|
|
257
|
+
if (input.path) {
|
|
258
|
+
const normalizedPath = validatePath(input.path);
|
|
259
|
+
files = files.filter((f) => f.startsWith(normalizedPath));
|
|
260
|
+
}
|
|
261
|
+
return files;
|
|
262
|
+
}, {
|
|
263
|
+
name: "ls",
|
|
264
|
+
description: toolDescription,
|
|
265
|
+
schema: z.object({ path: z.string().optional().describe("Optional path to filter by") })
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Generate the read_file tool.
|
|
270
|
+
*/
|
|
271
|
+
function createReadFileTool(customDescription, longTermMemory) {
|
|
272
|
+
let toolDescription = READ_FILE_TOOL_DESCRIPTION;
|
|
273
|
+
if (customDescription) toolDescription = customDescription;
|
|
274
|
+
else if (longTermMemory) toolDescription += READ_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT;
|
|
275
|
+
function readFileDataContent(fileData, offset, limit) {
|
|
276
|
+
const content = fileDataToString(fileData);
|
|
277
|
+
const emptyMsg = checkEmptyContent(content);
|
|
278
|
+
if (emptyMsg) return emptyMsg;
|
|
279
|
+
const lines = content.split("\n");
|
|
280
|
+
const startIdx = offset;
|
|
281
|
+
const endIdx = Math.min(startIdx + limit, lines.length);
|
|
282
|
+
if (startIdx >= lines.length) return `Error: Line offset ${offset} exceeds file length (${lines.length} lines)`;
|
|
283
|
+
return formatContentWithLineNumbers(lines.slice(startIdx, endIdx), {
|
|
284
|
+
formatStyle: "tab",
|
|
285
|
+
startLine: startIdx + 1
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return tool(async (input, config) => {
|
|
289
|
+
const filePath = validatePath(input.file_path);
|
|
290
|
+
const offset = input.offset ?? DEFAULT_READ_OFFSET;
|
|
291
|
+
const limit = input.limit ?? DEFAULT_READ_LIMIT;
|
|
292
|
+
if (longTermMemory) {
|
|
293
|
+
assertStore(config);
|
|
294
|
+
if (hasMemoriesPrefix(filePath)) {
|
|
295
|
+
const strippedFilePath = stripMemoriesPrefix(filePath);
|
|
296
|
+
const namespace = getNamespace();
|
|
297
|
+
const item = await config.store?.get(namespace, strippedFilePath);
|
|
298
|
+
if (!item) return `Error: File '${filePath}' not found`;
|
|
299
|
+
return readFileDataContent(convertStoreItemToFileData(item), offset, limit);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const mockFilesystem = getCurrentTaskInput(config).files || {};
|
|
303
|
+
if (!(filePath in mockFilesystem)) return `Error: File '${filePath}' not found`;
|
|
304
|
+
const fileData = mockFilesystem[filePath];
|
|
305
|
+
return readFileDataContent(fileData, offset, limit);
|
|
306
|
+
}, {
|
|
307
|
+
name: "read_file",
|
|
308
|
+
description: toolDescription,
|
|
309
|
+
schema: z.object({
|
|
310
|
+
file_path: z.string().describe("Absolute path to the file to read"),
|
|
311
|
+
offset: z.number({ coerce: true }).optional().default(DEFAULT_READ_OFFSET).describe("Line offset to start reading from"),
|
|
312
|
+
limit: z.number({ coerce: true }).optional().default(DEFAULT_READ_LIMIT).describe("Maximum number of lines to read")
|
|
313
|
+
})
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Generate the write_file tool.
|
|
318
|
+
*/
|
|
319
|
+
function createWriteFileTool(customDescription, longTermMemory) {
|
|
320
|
+
let toolDescription = WRITE_FILE_TOOL_DESCRIPTION;
|
|
321
|
+
if (customDescription) toolDescription = customDescription;
|
|
322
|
+
else if (longTermMemory) toolDescription += WRITE_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT;
|
|
323
|
+
return tool(async (input, config) => {
|
|
324
|
+
const filePath = validatePath(input.file_path);
|
|
325
|
+
if (longTermMemory) {
|
|
326
|
+
assertStore(config);
|
|
327
|
+
if (hasMemoriesPrefix(filePath)) {
|
|
328
|
+
const strippedFilePath = stripMemoriesPrefix(filePath);
|
|
329
|
+
const namespace = getNamespace();
|
|
330
|
+
if (await config.store.get(namespace, strippedFilePath)) return `Cannot write to ${filePath} because it already exists. Read and then make an edit, or write to a new path.`;
|
|
331
|
+
const newFileData$1 = createFileData(input.content);
|
|
332
|
+
await config.store.put(namespace, strippedFilePath, convertFileDataToStoreItem(newFileData$1));
|
|
333
|
+
return `Updated longterm memories file ${filePath}`;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (filePath in (getCurrentTaskInput(config).files || {})) return `Cannot write to ${filePath} because it already exists. Read and then make an edit, or write to a new path.`;
|
|
337
|
+
const newFileData = createFileData(input.content);
|
|
338
|
+
return new Command({ update: {
|
|
339
|
+
files: { [filePath]: newFileData },
|
|
340
|
+
messages: [new ToolMessage({
|
|
341
|
+
content: `Updated file ${filePath}`,
|
|
342
|
+
tool_call_id: config.toolCall?.id,
|
|
343
|
+
name: "write_file"
|
|
344
|
+
})]
|
|
345
|
+
} });
|
|
346
|
+
}, {
|
|
347
|
+
name: "write_file",
|
|
348
|
+
description: toolDescription,
|
|
349
|
+
schema: z.object({
|
|
350
|
+
file_path: z.string().describe("Absolute path to the file to write"),
|
|
351
|
+
content: z.string().describe("Content to write to the file")
|
|
352
|
+
})
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Generate the edit_file tool.
|
|
357
|
+
*/
|
|
358
|
+
function createEditFileTool(customDescription, longTermMemory) {
|
|
359
|
+
let toolDescription = EDIT_FILE_TOOL_DESCRIPTION;
|
|
360
|
+
if (customDescription) toolDescription = customDescription;
|
|
361
|
+
else if (longTermMemory) toolDescription += EDIT_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT;
|
|
362
|
+
function performFileEdit(fileData, oldString, newString, replaceAll) {
|
|
363
|
+
const content = fileDataToString(fileData);
|
|
364
|
+
const occurrences = (content.match(new RegExp(oldString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g")) || []).length;
|
|
365
|
+
if (occurrences === 0) return `Error: String not found in file: '${oldString}'`;
|
|
366
|
+
if (occurrences > 1 && !replaceAll) return `Error: String '${oldString}' appears ${occurrences} times in file. Use replace_all=True to replace all instances, or provide a more specific string with surrounding context.`;
|
|
367
|
+
return {
|
|
368
|
+
fileData: updateFileData(fileData, content.replace(new RegExp(oldString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), replaceAll ? "g" : ""), newString)),
|
|
369
|
+
message: `Successfully replaced ${occurrences} instance(s) of the string`
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
return tool(async (input, config) => {
|
|
373
|
+
const filePath = validatePath(input.file_path);
|
|
374
|
+
const replaceAll = input.replace_all ?? false;
|
|
375
|
+
const fileData = await (async () => {
|
|
376
|
+
if (longTermMemory) {
|
|
377
|
+
assertStore(config);
|
|
378
|
+
if (hasMemoriesPrefix(filePath)) {
|
|
379
|
+
const strippedFilePath = stripMemoriesPrefix(filePath);
|
|
380
|
+
const namespace = getNamespace();
|
|
381
|
+
const item = await config.store.get(namespace, strippedFilePath);
|
|
382
|
+
if (!item) return `Error: File '${filePath}' not found`;
|
|
383
|
+
return convertStoreItemToFileData(item);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const mockFilesystem = getCurrentTaskInput(config).files || {};
|
|
387
|
+
if (!(filePath in mockFilesystem)) return `Error: File '${filePath}' not found`;
|
|
388
|
+
return mockFilesystem[filePath];
|
|
389
|
+
})();
|
|
390
|
+
if (typeof fileData === "string") return fileData;
|
|
391
|
+
const result = performFileEdit(fileData, input.old_string, input.new_string, replaceAll);
|
|
392
|
+
if (typeof result === "string") return result;
|
|
393
|
+
const { fileData: newFileData, message: resultMsg } = result;
|
|
394
|
+
const fullMsg = `${resultMsg} in '${filePath}'`;
|
|
395
|
+
if (longTermMemory) {
|
|
396
|
+
assertStore(config);
|
|
397
|
+
if (hasMemoriesPrefix(filePath)) {
|
|
398
|
+
const strippedFilePath = stripMemoriesPrefix(filePath);
|
|
399
|
+
const namespace = getNamespace();
|
|
400
|
+
await config.store.put(namespace, strippedFilePath, convertFileDataToStoreItem(newFileData));
|
|
401
|
+
return fullMsg;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return new Command({ update: {
|
|
405
|
+
files: { [filePath]: newFileData },
|
|
406
|
+
messages: [new ToolMessage({
|
|
407
|
+
content: fullMsg,
|
|
408
|
+
tool_call_id: config.toolCall?.id,
|
|
409
|
+
name: "edit_file"
|
|
410
|
+
})]
|
|
411
|
+
} });
|
|
412
|
+
}, {
|
|
413
|
+
name: "edit_file",
|
|
414
|
+
description: toolDescription,
|
|
415
|
+
schema: z.object({
|
|
416
|
+
file_path: z.string().describe("Absolute path to the file to edit"),
|
|
417
|
+
old_string: z.string().describe("String to be replaced (must match exactly)"),
|
|
418
|
+
new_string: z.string().describe("String to replace with"),
|
|
419
|
+
replace_all: z.boolean().optional().default(false).describe("Whether to replace all occurrences")
|
|
420
|
+
})
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Create filesystem middleware with all tools and features.
|
|
425
|
+
*/
|
|
426
|
+
function createFilesystemMiddleware(options = {}) {
|
|
427
|
+
const { longTermMemory = false, systemPrompt: customSystemPrompt = null, customToolDescriptions = null, toolTokenLimitBeforeEvict = 2e4 } = options;
|
|
428
|
+
let systemPrompt = FILESYSTEM_SYSTEM_PROMPT;
|
|
429
|
+
if (customSystemPrompt !== null) systemPrompt = customSystemPrompt;
|
|
430
|
+
else if (longTermMemory) systemPrompt += FILESYSTEM_SYSTEM_PROMPT_LONGTERM_SUPPLEMENT;
|
|
431
|
+
return createMiddleware({
|
|
432
|
+
name: "fsMiddleware",
|
|
433
|
+
stateSchema,
|
|
434
|
+
tools: [
|
|
435
|
+
createLsTool(customToolDescriptions?.ls ?? null, longTermMemory),
|
|
436
|
+
createReadFileTool(customToolDescriptions?.read_file ?? null, longTermMemory),
|
|
437
|
+
createWriteFileTool(customToolDescriptions?.write_file ?? null, longTermMemory),
|
|
438
|
+
createEditFileTool(customToolDescriptions?.edit_file ?? null, longTermMemory)
|
|
439
|
+
],
|
|
440
|
+
wrapModelCall: async (request, handler) => {
|
|
441
|
+
const currentSystemPrompt = request.systemPrompt || "";
|
|
442
|
+
const newSystemPrompt = currentSystemPrompt ? `${currentSystemPrompt}\n\n${systemPrompt}` : systemPrompt;
|
|
443
|
+
return handler({
|
|
444
|
+
...request,
|
|
445
|
+
systemPrompt: newSystemPrompt
|
|
446
|
+
});
|
|
447
|
+
},
|
|
448
|
+
wrapToolCall: async (request, handler) => {
|
|
449
|
+
if (!toolTokenLimitBeforeEvict || [
|
|
450
|
+
"ls",
|
|
451
|
+
"read_file",
|
|
452
|
+
"write_file",
|
|
453
|
+
"edit_file"
|
|
454
|
+
].includes(request.tool.name)) return handler(request);
|
|
455
|
+
const result = await handler(request);
|
|
456
|
+
if (ToolMessage.isInstance(result) && typeof result.content === "string") {
|
|
457
|
+
if (result.content.length > 4 * toolTokenLimitBeforeEvict) {
|
|
458
|
+
const filePath = `/large_tool_results/${request.toolCall.id}`;
|
|
459
|
+
const fileData = createFileData(result.content);
|
|
460
|
+
const contentSample = formatContentWithLineNumbers(fileData.content.slice(0, 10), {
|
|
461
|
+
formatStyle: "tab",
|
|
462
|
+
startLine: 1
|
|
463
|
+
});
|
|
464
|
+
return new Command({ update: {
|
|
465
|
+
messages: [new ToolMessage({
|
|
466
|
+
content: TOO_LARGE_TOOL_MSG.replace("{tool_call_id}", request.toolCall.id || "").replace("{file_path}", filePath).replace("{content_sample}", contentSample),
|
|
467
|
+
tool_call_id: request.toolCall.id || "",
|
|
468
|
+
name: request.tool.name
|
|
469
|
+
})],
|
|
470
|
+
files: { [filePath]: fileData }
|
|
471
|
+
} });
|
|
472
|
+
}
|
|
473
|
+
} else if (isCommand(result) && result.update) {
|
|
474
|
+
const update = result.update;
|
|
475
|
+
const messageUpdates = update.messages || [];
|
|
476
|
+
const fileUpdates = update.files || {};
|
|
477
|
+
const editedMessageUpdates = [];
|
|
478
|
+
for (const message of messageUpdates) {
|
|
479
|
+
if (toolTokenLimitBeforeEvict && message instanceof ToolMessage && typeof message.content === "string") {
|
|
480
|
+
if (message.content.length > 4 * toolTokenLimitBeforeEvict) {
|
|
481
|
+
const filePath = `/large_tool_results/${message.tool_call_id}`;
|
|
482
|
+
const fileData = createFileData(message.content);
|
|
483
|
+
const contentSample = formatContentWithLineNumbers(fileData.content.slice(0, 10), {
|
|
484
|
+
formatStyle: "tab",
|
|
485
|
+
startLine: 1
|
|
486
|
+
});
|
|
487
|
+
const evictedMessage = TOO_LARGE_TOOL_MSG.replace("{tool_call_id}", message.tool_call_id || "").replace("{file_path}", filePath).replace("{content_sample}", contentSample);
|
|
488
|
+
editedMessageUpdates.push(new ToolMessage({
|
|
489
|
+
content: evictedMessage,
|
|
490
|
+
tool_call_id: message.tool_call_id || "",
|
|
491
|
+
name: message.name || ""
|
|
492
|
+
}));
|
|
493
|
+
fileUpdates[filePath] = fileData;
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
editedMessageUpdates.push(message);
|
|
498
|
+
}
|
|
499
|
+
return new Command({ update: {
|
|
500
|
+
...update,
|
|
501
|
+
messages: editedMessageUpdates,
|
|
502
|
+
files: fileUpdates
|
|
503
|
+
} });
|
|
504
|
+
}
|
|
505
|
+
return result;
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
const fsMiddleware = createFilesystemMiddleware();
|
|
510
|
+
|
|
511
|
+
//#endregion
|
|
512
|
+
//#region src/middleware/subagents.ts
|
|
513
|
+
const DEFAULT_SUBAGENT_PROMPT = "In order to complete the objective that the user asks of you, you have access to a number of standard tools.";
|
|
514
|
+
const EXCLUDED_STATE_KEYS = [
|
|
515
|
+
"messages",
|
|
516
|
+
"todos",
|
|
517
|
+
"jumpTo"
|
|
518
|
+
];
|
|
519
|
+
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.";
|
|
520
|
+
function getTaskToolDescription(subagentDescriptions) {
|
|
521
|
+
return `
|
|
522
|
+
Launch an ephemeral subagent to handle complex, multi-step independent tasks with isolated context windows.
|
|
523
|
+
|
|
524
|
+
Available agent types and the tools they have access to:
|
|
525
|
+
${subagentDescriptions.join("\n")}
|
|
526
|
+
|
|
527
|
+
When using the Task tool, you must specify a subagent_type parameter to select which agent type to use.
|
|
528
|
+
|
|
529
|
+
## Usage notes:
|
|
530
|
+
1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
|
|
531
|
+
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.
|
|
532
|
+
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.
|
|
533
|
+
4. The agent's outputs should generally be trusted
|
|
534
|
+
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
|
|
535
|
+
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.
|
|
536
|
+
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.
|
|
537
|
+
|
|
538
|
+
### Example usage of the general-purpose agent:
|
|
539
|
+
|
|
540
|
+
<example_agent_descriptions>
|
|
541
|
+
"general-purpose": use this agent for general purpose tasks, it has access to all tools as the main agent.
|
|
542
|
+
</example_agent_descriptions>
|
|
543
|
+
|
|
544
|
+
<example>
|
|
545
|
+
User: "I want to conduct research on the accomplishments of Lebron James, Michael Jordan, and Kobe Bryant, and then compare them."
|
|
546
|
+
Assistant: *Uses the task tool in parallel to conduct isolated research on each of the three players*
|
|
547
|
+
Assistant: *Synthesizes the results of the three isolated research tasks and responds to the User*
|
|
548
|
+
<commentary>
|
|
549
|
+
Research is a complex, multi-step task in it of itself.
|
|
550
|
+
The research of each individual player is not dependent on the research of the other players.
|
|
551
|
+
The assistant uses the task tool to break down the complex objective into three isolated tasks.
|
|
552
|
+
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.
|
|
553
|
+
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.
|
|
554
|
+
</commentary>
|
|
555
|
+
</example>
|
|
556
|
+
|
|
557
|
+
<example>
|
|
558
|
+
User: "Analyze a single large code repository for security vulnerabilities and generate a report."
|
|
559
|
+
Assistant: *Launches a single \`task\` subagent for the repository analysis*
|
|
560
|
+
Assistant: *Receives report and integrates results into final summary*
|
|
561
|
+
<commentary>
|
|
562
|
+
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.
|
|
563
|
+
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.
|
|
564
|
+
</commentary>
|
|
565
|
+
</example>
|
|
566
|
+
|
|
567
|
+
<example>
|
|
568
|
+
User: "Schedule two meetings for me and prepare agendas for each."
|
|
569
|
+
Assistant: *Calls the task tool in parallel to launch two \`task\` subagents (one per meeting) to prepare agendas*
|
|
570
|
+
Assistant: *Returns final schedules and agendas*
|
|
571
|
+
<commentary>
|
|
572
|
+
Tasks are simple individually, but subagents help silo agenda preparation.
|
|
573
|
+
Each subagent only needs to worry about the agenda for one meeting.
|
|
574
|
+
</commentary>
|
|
575
|
+
</example>
|
|
576
|
+
|
|
577
|
+
<example>
|
|
578
|
+
User: "I want to order a pizza from Dominos, order a burger from McDonald's, and order a salad from Subway."
|
|
579
|
+
Assistant: *Calls tools directly in parallel to order a pizza from Dominos, a burger from McDonald's, and a salad from Subway*
|
|
580
|
+
<commentary>
|
|
581
|
+
The assistant did not use the task tool because the objective is super simple and clear and only requires a few trivial tool calls.
|
|
582
|
+
It is better to just complete the task directly and NOT use the \`task\`tool.
|
|
583
|
+
</commentary>
|
|
584
|
+
</example>
|
|
585
|
+
|
|
586
|
+
### Example usage with custom agents:
|
|
587
|
+
|
|
588
|
+
<example_agent_descriptions>
|
|
589
|
+
"content-reviewer": use this agent after you are done creating significant content or documents
|
|
590
|
+
"greeting-responder": use this agent when to respond to user greetings with a friendly joke
|
|
591
|
+
"research-analyst": use this agent to conduct thorough research on complex topics
|
|
592
|
+
</example_agent_description>
|
|
593
|
+
|
|
594
|
+
<example>
|
|
595
|
+
user: "Please write a function that checks if a number is prime"
|
|
596
|
+
assistant: Sure let me write a function that checks if a number is prime
|
|
597
|
+
assistant: First let me use the Write tool to write a function that checks if a number is prime
|
|
598
|
+
assistant: I'm going to use the Write tool to write the following code:
|
|
599
|
+
<code>
|
|
600
|
+
function isPrime(n) {
|
|
601
|
+
if (n <= 1) return false
|
|
602
|
+
for (let i = 2; i * i <= n; i++) {
|
|
603
|
+
if (n % i === 0) return false
|
|
604
|
+
}
|
|
605
|
+
return true
|
|
606
|
+
}
|
|
607
|
+
</code>
|
|
608
|
+
<commentary>
|
|
609
|
+
Since significant content was created and the task was completed, now use the content-reviewer agent to review the work
|
|
610
|
+
</commentary>
|
|
611
|
+
assistant: Now let me use the content-reviewer agent to review the code
|
|
612
|
+
assistant: Uses the Task tool to launch with the content-reviewer agent
|
|
613
|
+
</example>
|
|
614
|
+
|
|
615
|
+
<example>
|
|
616
|
+
user: "Can you help me research the environmental impact of different renewable energy sources and create a comprehensive report?"
|
|
617
|
+
<commentary>
|
|
618
|
+
This is a complex research task that would benefit from using the research-analyst agent to conduct thorough analysis
|
|
619
|
+
</commentary>
|
|
620
|
+
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.
|
|
621
|
+
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
|
|
622
|
+
</example>
|
|
623
|
+
|
|
624
|
+
<example>
|
|
625
|
+
user: "Hello"
|
|
626
|
+
<commentary>
|
|
627
|
+
Since the user is greeting, use the greeting-responder agent to respond with a friendly joke
|
|
628
|
+
</commentary>
|
|
629
|
+
assistant: "I'm going to use the Task tool to launch with the greeting-responder agent"
|
|
630
|
+
</example>
|
|
631
|
+
`.trim();
|
|
632
|
+
}
|
|
633
|
+
const TASK_SYSTEM_PROMPT = `## \`task\` (subagent spawner)
|
|
634
|
+
|
|
635
|
+
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.
|
|
636
|
+
|
|
637
|
+
When to use the task tool:
|
|
638
|
+
- When a task is complex and multi-step, and can be fully delegated in isolation
|
|
639
|
+
- When a task is independent of other tasks and can run in parallel
|
|
640
|
+
- When a task requires focused reasoning or heavy token/context usage that would bloat the orchestrator thread
|
|
641
|
+
- When sandboxing improves reliability (e.g. code execution, structured searches, data formatting)
|
|
642
|
+
- 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.)
|
|
643
|
+
|
|
644
|
+
Subagent lifecycle:
|
|
645
|
+
1. **Spawn** → Provide clear role, instructions, and expected output
|
|
646
|
+
2. **Run** → The subagent completes the task autonomously
|
|
647
|
+
3. **Return** → The subagent provides a single structured result
|
|
648
|
+
4. **Reconcile** → Incorporate or synthesize the result into the main thread
|
|
649
|
+
|
|
650
|
+
When NOT to use the task tool:
|
|
651
|
+
- If you need to see the intermediate reasoning or steps after the subagent has completed (the task tool hides them)
|
|
652
|
+
- If the task is trivial (a few tool calls or simple lookup)
|
|
653
|
+
- If delegating does not reduce token usage, complexity, or context switching
|
|
654
|
+
- If splitting would add latency without benefit
|
|
655
|
+
|
|
656
|
+
## Important Task Tool Usage Notes to Remember
|
|
657
|
+
- 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.
|
|
658
|
+
- Remember to use the \`task\` tool to silo independent tasks within a multi-part objective.
|
|
659
|
+
- 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.`;
|
|
660
|
+
/**
|
|
661
|
+
* Filter state to exclude certain keys when passing to subagents
|
|
662
|
+
*/
|
|
663
|
+
function filterStateForSubagent(state) {
|
|
664
|
+
const filtered = {};
|
|
665
|
+
for (const [key, value] of Object.entries(state)) if (!EXCLUDED_STATE_KEYS.includes(key)) filtered[key] = value;
|
|
666
|
+
return filtered;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Create Command with filtered state update from subagent result
|
|
670
|
+
*/
|
|
671
|
+
function returnCommandWithStateUpdate(result, toolCallId) {
|
|
672
|
+
const stateUpdate = filterStateForSubagent(result);
|
|
673
|
+
const messages = result.messages;
|
|
674
|
+
const lastMessage = messages?.[messages.length - 1];
|
|
675
|
+
return new Command({ update: {
|
|
676
|
+
...stateUpdate,
|
|
677
|
+
messages: [new ToolMessage({
|
|
678
|
+
content: lastMessage?.content || "Task completed",
|
|
679
|
+
tool_call_id: toolCallId,
|
|
680
|
+
name: "task"
|
|
681
|
+
})]
|
|
682
|
+
} });
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Create subagent instances from specifications
|
|
686
|
+
*/
|
|
687
|
+
function getSubagents(options) {
|
|
688
|
+
const { defaultModel, defaultTools, defaultMiddleware, defaultInterruptOn, subagents, generalPurposeAgent } = options;
|
|
689
|
+
const defaultSubagentMiddleware = defaultMiddleware || [];
|
|
690
|
+
const agents = {};
|
|
691
|
+
const subagentDescriptions = [];
|
|
692
|
+
if (generalPurposeAgent) {
|
|
693
|
+
const generalPurposeMiddleware = [...defaultSubagentMiddleware];
|
|
694
|
+
if (defaultInterruptOn) generalPurposeMiddleware.push(humanInTheLoopMiddleware({ interruptOn: defaultInterruptOn }));
|
|
695
|
+
agents["general-purpose"] = createAgent({
|
|
696
|
+
model: defaultModel,
|
|
697
|
+
systemPrompt: DEFAULT_SUBAGENT_PROMPT,
|
|
698
|
+
tools: defaultTools,
|
|
699
|
+
middleware: generalPurposeMiddleware
|
|
700
|
+
});
|
|
701
|
+
subagentDescriptions.push(`- general-purpose: ${DEFAULT_GENERAL_PURPOSE_DESCRIPTION}`);
|
|
702
|
+
}
|
|
703
|
+
for (const agentParams of subagents) {
|
|
704
|
+
subagentDescriptions.push(`- ${agentParams.name}: ${agentParams.description}`);
|
|
705
|
+
const middleware = agentParams.middleware ? [...defaultSubagentMiddleware, ...agentParams.middleware] : [...defaultSubagentMiddleware];
|
|
706
|
+
const interruptOn = agentParams.interruptOn || defaultInterruptOn;
|
|
707
|
+
if (interruptOn) middleware.push(humanInTheLoopMiddleware({ interruptOn }));
|
|
708
|
+
agents[agentParams.name] = createAgent({
|
|
709
|
+
model: agentParams.model ?? defaultModel,
|
|
710
|
+
systemPrompt: agentParams.systemPrompt,
|
|
711
|
+
tools: agentParams.tools ?? defaultTools,
|
|
712
|
+
middleware,
|
|
713
|
+
checkpointer: false
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
return {
|
|
717
|
+
agents,
|
|
718
|
+
descriptions: subagentDescriptions
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Create the task tool for invoking subagents
|
|
723
|
+
*/
|
|
724
|
+
function createTaskTool(options) {
|
|
725
|
+
const { defaultModel, defaultTools, defaultMiddleware, defaultInterruptOn, subagents, generalPurposeAgent, taskDescription } = options;
|
|
726
|
+
const { agents: subagentGraphs, descriptions: subagentDescriptions } = getSubagents({
|
|
727
|
+
defaultModel,
|
|
728
|
+
defaultTools,
|
|
729
|
+
defaultMiddleware,
|
|
730
|
+
defaultInterruptOn,
|
|
731
|
+
subagents,
|
|
732
|
+
generalPurposeAgent
|
|
733
|
+
});
|
|
734
|
+
return tool(async (input, config) => {
|
|
735
|
+
const { description, subagent_type } = input;
|
|
736
|
+
if (!(subagent_type in subagentGraphs)) {
|
|
737
|
+
const allowedTypes = Object.keys(subagentGraphs).map((k) => `\`${k}\``).join(", ");
|
|
738
|
+
throw new Error(`Error: invoked agent of type ${subagent_type}, the only allowed types are ${allowedTypes}`);
|
|
739
|
+
}
|
|
740
|
+
const subagent = subagentGraphs[subagent_type];
|
|
741
|
+
const subagentState = filterStateForSubagent(getCurrentTaskInput());
|
|
742
|
+
subagentState.messages = [new HumanMessage({ content: description })];
|
|
743
|
+
const result = await subagent.invoke(subagentState);
|
|
744
|
+
if (!config.toolCall?.id) throw new Error("Tool call ID is required for subagent invocation");
|
|
745
|
+
return returnCommandWithStateUpdate(result, config.toolCall.id);
|
|
746
|
+
}, {
|
|
747
|
+
name: "task",
|
|
748
|
+
description: taskDescription ? taskDescription : getTaskToolDescription(subagentDescriptions),
|
|
749
|
+
schema: z.object({
|
|
750
|
+
description: z.string().describe("The task to execute with the selected agent"),
|
|
751
|
+
subagent_type: z.string().describe(`Name of the agent to use. Available: ${Object.keys(subagentGraphs).join(", ")}`)
|
|
752
|
+
})
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Create subagent middleware with task tool
|
|
757
|
+
*/
|
|
758
|
+
function createSubAgentMiddleware(options) {
|
|
759
|
+
const { defaultModel, defaultTools = [], defaultMiddleware = null, defaultInterruptOn = null, subagents = [], systemPrompt = TASK_SYSTEM_PROMPT, generalPurposeAgent = true, taskDescription = null } = options;
|
|
760
|
+
return createMiddleware({
|
|
761
|
+
name: "subAgentMiddleware",
|
|
762
|
+
tools: [createTaskTool({
|
|
763
|
+
defaultModel,
|
|
764
|
+
defaultTools,
|
|
765
|
+
defaultMiddleware,
|
|
766
|
+
defaultInterruptOn,
|
|
767
|
+
subagents,
|
|
768
|
+
generalPurposeAgent,
|
|
769
|
+
taskDescription
|
|
770
|
+
})],
|
|
771
|
+
wrapModelCall: async (request, handler) => {
|
|
772
|
+
if (systemPrompt !== null) {
|
|
773
|
+
const currentPrompt = request.systemPrompt || "";
|
|
774
|
+
const newPrompt = currentPrompt ? `${currentPrompt}\n\n${systemPrompt}` : systemPrompt;
|
|
775
|
+
return handler({
|
|
776
|
+
...request,
|
|
777
|
+
systemPrompt: newPrompt
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
return handler(request);
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
//#endregion
|
|
786
|
+
//#region src/middleware/patch_tool_calls.ts
|
|
787
|
+
/**
|
|
788
|
+
* Create middleware that patches dangling tool calls in the messages history.
|
|
789
|
+
*
|
|
790
|
+
* When an AI message contains tool_calls but subsequent messages don't include
|
|
791
|
+
* the corresponding ToolMessage responses, this middleware adds synthetic
|
|
792
|
+
* ToolMessages saying the tool call was cancelled.
|
|
793
|
+
*
|
|
794
|
+
* @returns AgentMiddleware that patches dangling tool calls
|
|
795
|
+
*
|
|
796
|
+
* @example
|
|
797
|
+
* ```typescript
|
|
798
|
+
* import { createAgent } from "langchain";
|
|
799
|
+
* import { createPatchToolCallsMiddleware } from "./middleware/patch_tool_calls";
|
|
800
|
+
*
|
|
801
|
+
* const agent = createAgent({
|
|
802
|
+
* model: "claude-sonnet-4-5-20250929",
|
|
803
|
+
* middleware: [createPatchToolCallsMiddleware()],
|
|
804
|
+
* });
|
|
805
|
+
* ```
|
|
806
|
+
*/
|
|
807
|
+
function createPatchToolCallsMiddleware() {
|
|
808
|
+
return createMiddleware({
|
|
809
|
+
name: "patchToolCallsMiddleware",
|
|
810
|
+
beforeAgent: async (state) => {
|
|
811
|
+
const messages = state.messages;
|
|
812
|
+
if (!messages || messages.length === 0) return;
|
|
813
|
+
const patchedMessages = [];
|
|
814
|
+
for (let i = 0; i < messages.length; i++) {
|
|
815
|
+
const msg = messages[i];
|
|
816
|
+
patchedMessages.push(msg);
|
|
817
|
+
if (AIMessage.isInstance(msg) && msg.tool_calls != null) {
|
|
818
|
+
for (const toolCall of msg.tool_calls) if (!messages.slice(i).find((m) => ToolMessage.isInstance(m) && m.tool_call_id === toolCall.id)) {
|
|
819
|
+
const toolMsg = `Tool call ${toolCall.name} with id ${toolCall.id} was cancelled - another message came in before it could be completed.`;
|
|
820
|
+
patchedMessages.push(new ToolMessage({
|
|
821
|
+
content: toolMsg,
|
|
822
|
+
name: toolCall.name,
|
|
823
|
+
tool_call_id: toolCall.id
|
|
824
|
+
}));
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return { messages: [new RemoveMessage({ id: REMOVE_ALL_MESSAGES }), ...patchedMessages] };
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
//#endregion
|
|
834
|
+
//#region src/agent.ts
|
|
835
|
+
const BASE_PROMPT = `In order to complete the objective that the user asks of you, you have access to a number of standard tools.`;
|
|
836
|
+
/**
|
|
837
|
+
* Create a Deep Agent with middleware-based architecture.
|
|
838
|
+
*
|
|
839
|
+
* Matches Python's create_deep_agent function, using middleware for all features:
|
|
840
|
+
* - Todo management (todoListMiddleware)
|
|
841
|
+
* - Filesystem tools (createFilesystemMiddleware)
|
|
842
|
+
* - Subagent delegation (createSubAgentMiddleware)
|
|
843
|
+
* - Conversation summarization (summarizationMiddleware)
|
|
844
|
+
* - Prompt caching (anthropicPromptCachingMiddleware)
|
|
845
|
+
* - Tool call patching (createPatchToolCallsMiddleware)
|
|
846
|
+
* - Human-in-the-loop (humanInTheLoopMiddleware) - optional
|
|
847
|
+
*
|
|
848
|
+
* @param params Configuration parameters for the agent
|
|
849
|
+
* @returns ReactAgent instance ready for invocation
|
|
850
|
+
*/
|
|
851
|
+
function createDeepAgent(params = {}) {
|
|
852
|
+
const { model = "claude-sonnet-4-5-20250929", tools = [], systemPrompt, middleware: customMiddleware = [], subagents = [], responseFormat, contextSchema, checkpointer, store, useLongtermMemory = false, interruptOn, name } = params;
|
|
853
|
+
const finalSystemPrompt = systemPrompt ? `${systemPrompt}\n\n${BASE_PROMPT}` : BASE_PROMPT;
|
|
854
|
+
const middleware = [
|
|
855
|
+
todoListMiddleware(),
|
|
856
|
+
createFilesystemMiddleware({ longTermMemory: useLongtermMemory }),
|
|
857
|
+
createSubAgentMiddleware({
|
|
858
|
+
defaultModel: model,
|
|
859
|
+
defaultTools: tools,
|
|
860
|
+
defaultMiddleware: [
|
|
861
|
+
todoListMiddleware(),
|
|
862
|
+
createFilesystemMiddleware({ longTermMemory: useLongtermMemory }),
|
|
863
|
+
summarizationMiddleware({
|
|
864
|
+
model,
|
|
865
|
+
maxTokensBeforeSummary: 17e4,
|
|
866
|
+
messagesToKeep: 6
|
|
867
|
+
}),
|
|
868
|
+
anthropicPromptCachingMiddleware({ unsupportedModelBehavior: "ignore" }),
|
|
869
|
+
createPatchToolCallsMiddleware()
|
|
870
|
+
],
|
|
871
|
+
defaultInterruptOn: interruptOn,
|
|
872
|
+
subagents,
|
|
873
|
+
generalPurposeAgent: true
|
|
874
|
+
}),
|
|
875
|
+
summarizationMiddleware({
|
|
876
|
+
model,
|
|
877
|
+
maxTokensBeforeSummary: 17e4,
|
|
878
|
+
messagesToKeep: 6
|
|
879
|
+
}),
|
|
880
|
+
anthropicPromptCachingMiddleware({ unsupportedModelBehavior: "ignore" }),
|
|
881
|
+
createPatchToolCallsMiddleware()
|
|
882
|
+
];
|
|
883
|
+
if (interruptOn) middleware.push(humanInTheLoopMiddleware({ interruptOn }));
|
|
884
|
+
middleware.push(...customMiddleware);
|
|
885
|
+
return createAgent({
|
|
886
|
+
model,
|
|
887
|
+
systemPrompt: finalSystemPrompt,
|
|
888
|
+
tools,
|
|
889
|
+
middleware,
|
|
890
|
+
responseFormat,
|
|
891
|
+
contextSchema,
|
|
892
|
+
checkpointer,
|
|
893
|
+
store,
|
|
894
|
+
name
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
//#endregion
|
|
899
|
+
export { createFilesystemMiddleware as FilesystemMiddleware, createFilesystemMiddleware, createDeepAgent, createPatchToolCallsMiddleware, createSubAgentMiddleware };
|