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.js
CHANGED
|
@@ -1,92 +1,23 @@
|
|
|
1
|
-
import { AIMessage, HumanMessage, SystemMessage, ToolMessage, anthropicPromptCachingMiddleware, countTokensApproximately, createAgent, createMiddleware, humanInTheLoopMiddleware, todoListMiddleware, tool } from "langchain";
|
|
2
|
-
import {
|
|
3
|
-
import { Command, REMOVE_ALL_MESSAGES, ReducedValue, StateSchema, getCurrentTaskInput, isCommand } from "@langchain/langgraph";
|
|
1
|
+
import { AIMessage, HumanMessage, SystemMessage, ToolMessage, anthropicPromptCachingMiddleware, context, countTokensApproximately, createAgent, createMiddleware, humanInTheLoopMiddleware, todoListMiddleware, tool } from "langchain";
|
|
2
|
+
import { ChatAnthropic } from "@langchain/anthropic";
|
|
3
|
+
import { Command, REMOVE_ALL_MESSAGES, ReducedValue, StateSchema, getConfig, getCurrentTaskInput, getStore, isCommand } from "@langchain/langgraph";
|
|
4
4
|
import { z } from "zod/v4";
|
|
5
5
|
import micromatch from "micromatch";
|
|
6
|
-
import { basename } from "path";
|
|
7
|
-
import { HumanMessage as HumanMessage$1, RemoveMessage, getBufferString } from "@langchain/core/messages";
|
|
6
|
+
import path, { basename } from "path";
|
|
7
|
+
import { AIMessage as AIMessage$1, HumanMessage as HumanMessage$1, RemoveMessage, getBufferString } from "@langchain/core/messages";
|
|
8
|
+
import * as z$2 from "zod";
|
|
8
9
|
import { z as z$1 } from "zod";
|
|
9
10
|
import yaml from "yaml";
|
|
11
|
+
import { Client } from "@langchain/langgraph-sdk";
|
|
10
12
|
import { ContextOverflowError } from "@langchain/core/errors";
|
|
11
13
|
import { initChatModel } from "langchain/chat_models/universal";
|
|
12
14
|
import fs from "node:fs/promises";
|
|
13
15
|
import fs$1 from "node:fs";
|
|
14
|
-
import path from "node:path";
|
|
16
|
+
import path$1 from "node:path";
|
|
15
17
|
import cp, { spawn } from "node:child_process";
|
|
16
18
|
import fg from "fast-glob";
|
|
17
19
|
import { LangSmithResourceNotFoundError, LangSmithSandboxError, SandboxClient } from "langsmith/experimental/sandbox";
|
|
18
20
|
import os from "node:os";
|
|
19
|
-
//#region src/backends/protocol.ts
|
|
20
|
-
/**
|
|
21
|
-
* Type guard to check if a backend supports execution.
|
|
22
|
-
*
|
|
23
|
-
* @param backend - Backend instance to check
|
|
24
|
-
* @returns True if the backend implements SandboxBackendProtocol
|
|
25
|
-
*/
|
|
26
|
-
function isSandboxBackend(backend) {
|
|
27
|
-
return typeof backend.execute === "function" && typeof backend.id === "string" && backend.id !== "";
|
|
28
|
-
}
|
|
29
|
-
const SANDBOX_ERROR_SYMBOL = Symbol.for("sandbox.error");
|
|
30
|
-
/**
|
|
31
|
-
* Custom error class for sandbox operations.
|
|
32
|
-
*
|
|
33
|
-
* @param message - Human-readable error description
|
|
34
|
-
* @param code - Structured error code for programmatic handling
|
|
35
|
-
* @returns SandboxError with message and code
|
|
36
|
-
*
|
|
37
|
-
* @example
|
|
38
|
-
* ```typescript
|
|
39
|
-
* try {
|
|
40
|
-
* await sandbox.execute("some command");
|
|
41
|
-
* } catch (error) {
|
|
42
|
-
* if (error instanceof SandboxError) {
|
|
43
|
-
* switch (error.code) {
|
|
44
|
-
* case "NOT_INITIALIZED":
|
|
45
|
-
* await sandbox.initialize();
|
|
46
|
-
* break;
|
|
47
|
-
* case "COMMAND_TIMEOUT":
|
|
48
|
-
* console.error("Command took too long");
|
|
49
|
-
* break;
|
|
50
|
-
* default:
|
|
51
|
-
* throw error;
|
|
52
|
-
* }
|
|
53
|
-
* }
|
|
54
|
-
* }
|
|
55
|
-
* ```
|
|
56
|
-
*/
|
|
57
|
-
var SandboxError = class SandboxError extends Error {
|
|
58
|
-
/** Symbol for identifying sandbox error instances */
|
|
59
|
-
[SANDBOX_ERROR_SYMBOL] = true;
|
|
60
|
-
/** Error name for instanceof checks and logging */
|
|
61
|
-
name = "SandboxError";
|
|
62
|
-
/**
|
|
63
|
-
* Creates a new SandboxError.
|
|
64
|
-
*
|
|
65
|
-
* @param message - Human-readable error description
|
|
66
|
-
* @param code - Structured error code for programmatic handling
|
|
67
|
-
*/
|
|
68
|
-
constructor(message, code, cause) {
|
|
69
|
-
super(message);
|
|
70
|
-
this.code = code;
|
|
71
|
-
this.cause = cause;
|
|
72
|
-
Object.setPrototypeOf(this, SandboxError.prototype);
|
|
73
|
-
}
|
|
74
|
-
static isInstance(error) {
|
|
75
|
-
return typeof error === "object" && error !== null && error[SANDBOX_ERROR_SYMBOL] === true;
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
/**
|
|
79
|
-
* Resolve a backend instance or await a {@link BackendFactory}.
|
|
80
|
-
*
|
|
81
|
-
* Accepts {@link BackendRuntime} or {@link ToolRuntime} — store typing differs
|
|
82
|
-
* between LangGraph checkpoint stores and core `ToolRuntime`; factories receive
|
|
83
|
-
* a value that is structurally compatible at runtime.
|
|
84
|
-
*/
|
|
85
|
-
async function resolveBackend(backend, runtime) {
|
|
86
|
-
if (typeof backend === "function") return await backend(runtime);
|
|
87
|
-
return backend;
|
|
88
|
-
}
|
|
89
|
-
//#endregion
|
|
90
21
|
//#region src/backends/utils.ts
|
|
91
22
|
/**
|
|
92
23
|
* Shared utility functions for memory backend implementations.
|
|
@@ -96,9 +27,37 @@ async function resolveBackend(backend, runtime) {
|
|
|
96
27
|
* enable composition without fragile string parsing.
|
|
97
28
|
*/
|
|
98
29
|
const EMPTY_CONTENT_WARNING = "System reminder: File exists but has empty contents";
|
|
99
|
-
const MAX_LINE_LENGTH =
|
|
30
|
+
const MAX_LINE_LENGTH = 5e3;
|
|
100
31
|
const TOOL_RESULT_TOKEN_LIMIT = 2e4;
|
|
101
32
|
const TRUNCATION_GUIDANCE = "... [results truncated, try being more specific with your parameters]";
|
|
33
|
+
const MIME_TYPES = {
|
|
34
|
+
".png": "image/png",
|
|
35
|
+
".jpg": "image/jpeg",
|
|
36
|
+
".jpeg": "image/jpeg",
|
|
37
|
+
".gif": "image/gif",
|
|
38
|
+
".webp": "image/webp",
|
|
39
|
+
".svg": "image/svg+xml",
|
|
40
|
+
".heic": "image/heic",
|
|
41
|
+
".heif": "image/heif",
|
|
42
|
+
".mp3": "audio/mpeg",
|
|
43
|
+
".wav": "audio/wav",
|
|
44
|
+
".aiff": "audio/aiff",
|
|
45
|
+
".aac": "audio/aac",
|
|
46
|
+
".ogg": "audio/ogg",
|
|
47
|
+
".flac": "audio/flac",
|
|
48
|
+
".mp4": "video/mp4",
|
|
49
|
+
".webm": "video/webm",
|
|
50
|
+
".mpeg": "video/mpeg",
|
|
51
|
+
".mov": "video/quicktime",
|
|
52
|
+
".avi": "video/x-msvideo",
|
|
53
|
+
".flv": "video/x-flv",
|
|
54
|
+
".mpg": "video/mpeg",
|
|
55
|
+
".wmv": "video/x-ms-wmv",
|
|
56
|
+
".3gpp": "video/3gpp",
|
|
57
|
+
".pdf": "application/pdf",
|
|
58
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
59
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
60
|
+
};
|
|
102
61
|
/**
|
|
103
62
|
* Sanitize tool_call_id to prevent path traversal and separator issues.
|
|
104
63
|
*
|
|
@@ -126,7 +85,7 @@ function formatContentWithLineNumbers(content, startLine = 1) {
|
|
|
126
85
|
for (let i = 0; i < lines.length; i++) {
|
|
127
86
|
const line = lines[i];
|
|
128
87
|
const lineNum = i + startLine;
|
|
129
|
-
if (line.length <=
|
|
88
|
+
if (line.length <= 5e3) resultLines.push(`${lineNum.toString().padStart(6)}\t${line}`);
|
|
130
89
|
else {
|
|
131
90
|
const numChunks = Math.ceil(line.length / MAX_LINE_LENGTH);
|
|
132
91
|
for (let chunkIdx = 0; chunkIdx < numChunks; chunkIdx++) {
|
|
@@ -160,20 +119,50 @@ function checkEmptyContent(content) {
|
|
|
160
119
|
* @returns Content as string with lines joined by newlines
|
|
161
120
|
*/
|
|
162
121
|
function fileDataToString(fileData) {
|
|
163
|
-
return fileData.content.join("\n");
|
|
122
|
+
if (Array.isArray(fileData.content)) return fileData.content.join("\n");
|
|
123
|
+
if (typeof fileData.content === "string") return fileData.content;
|
|
124
|
+
throw new Error("Cannot convert binary FileData to string");
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Type guard to check if FileData contains binary content (Uint8Array).
|
|
128
|
+
*
|
|
129
|
+
* @param data - FileData to check
|
|
130
|
+
* @returns True if the content is a Uint8Array (binary)
|
|
131
|
+
*/
|
|
132
|
+
function isFileDataBinary(data) {
|
|
133
|
+
return ArrayBuffer.isView(data.content);
|
|
164
134
|
}
|
|
165
135
|
/**
|
|
166
|
-
* Create a FileData object
|
|
136
|
+
* Create a FileData object.
|
|
167
137
|
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
138
|
+
* Defaults to v2 format (content as single string). Pass `fileFormat: "v1"` for
|
|
139
|
+
* backward compatibility with older readers during a rolling deployment.
|
|
140
|
+
* Binary content (Uint8Array) is only supported with v2.
|
|
141
|
+
*
|
|
142
|
+
* @param content - File content as a string or binary Uint8Array (v2 only)
|
|
143
|
+
* @param createdAt - Optional creation timestamp (ISO format), defaults to now
|
|
144
|
+
* @param fileFormat - Storage format: "v2" (default) or "v1" (legacy line array)
|
|
145
|
+
* @returns FileData in the requested format
|
|
171
146
|
*/
|
|
172
|
-
function createFileData(content, createdAt) {
|
|
173
|
-
const lines = typeof content === "string" ? content.split("\n") : content;
|
|
147
|
+
function createFileData(content, createdAt, fileFormat = "v2", mimeType) {
|
|
174
148
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
149
|
+
if (fileFormat === "v1" && ArrayBuffer.isView(content)) throw new Error("Binary data is not supported with v1 file formats. Please use v2 file format");
|
|
150
|
+
if (fileFormat === "v2") {
|
|
151
|
+
if (ArrayBuffer.isView(content)) return {
|
|
152
|
+
content: new Uint8Array(content.buffer, content.byteOffset, content.byteLength),
|
|
153
|
+
mimeType: mimeType ?? "application/octet-stream",
|
|
154
|
+
created_at: createdAt || now,
|
|
155
|
+
modified_at: now
|
|
156
|
+
};
|
|
157
|
+
return {
|
|
158
|
+
content,
|
|
159
|
+
mimeType: mimeType ?? "text/plain",
|
|
160
|
+
created_at: createdAt || now,
|
|
161
|
+
modified_at: now
|
|
162
|
+
};
|
|
163
|
+
}
|
|
175
164
|
return {
|
|
176
|
-
content:
|
|
165
|
+
content: typeof content === "string" ? content.split("\n") : content,
|
|
177
166
|
created_at: createdAt || now,
|
|
178
167
|
modified_at: now
|
|
179
168
|
};
|
|
@@ -186,33 +175,20 @@ function createFileData(content, createdAt) {
|
|
|
186
175
|
* @returns Updated FileData object
|
|
187
176
|
*/
|
|
188
177
|
function updateFileData(fileData, content) {
|
|
189
|
-
const lines = typeof content === "string" ? content.split("\n") : content;
|
|
190
178
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
179
|
+
if (isFileDataV1(fileData)) return {
|
|
180
|
+
content: typeof content === "string" ? content.split("\n") : content,
|
|
181
|
+
created_at: fileData.created_at,
|
|
182
|
+
modified_at: now
|
|
183
|
+
};
|
|
191
184
|
return {
|
|
192
|
-
content
|
|
185
|
+
content,
|
|
186
|
+
mimeType: fileData.mimeType,
|
|
193
187
|
created_at: fileData.created_at,
|
|
194
188
|
modified_at: now
|
|
195
189
|
};
|
|
196
190
|
}
|
|
197
191
|
/**
|
|
198
|
-
* Format file data for read response with line numbers.
|
|
199
|
-
*
|
|
200
|
-
* @param fileData - FileData object
|
|
201
|
-
* @param offset - Line offset (0-indexed)
|
|
202
|
-
* @param limit - Maximum number of lines
|
|
203
|
-
* @returns Formatted content or error message
|
|
204
|
-
*/
|
|
205
|
-
function formatReadResponse(fileData, offset, limit) {
|
|
206
|
-
const content = fileDataToString(fileData);
|
|
207
|
-
const emptyMsg = checkEmptyContent(content);
|
|
208
|
-
if (emptyMsg) return emptyMsg;
|
|
209
|
-
const lines = content.split("\n");
|
|
210
|
-
const startIdx = offset;
|
|
211
|
-
const endIdx = Math.min(startIdx + limit, lines.length);
|
|
212
|
-
if (startIdx >= lines.length) return `Error: Line offset ${offset} exceeds file length (${lines.length} lines)`;
|
|
213
|
-
return formatContentWithLineNumbers(lines.slice(startIdx, endIdx), startIdx + 1);
|
|
214
|
-
}
|
|
215
|
-
/**
|
|
216
192
|
* Perform string replacement with occurrence validation.
|
|
217
193
|
*
|
|
218
194
|
* @param content - Original content
|
|
@@ -323,11 +299,8 @@ function globSearchFiles(files, pattern, path = "/") {
|
|
|
323
299
|
/**
|
|
324
300
|
* Return structured grep matches from an in-memory files mapping.
|
|
325
301
|
*
|
|
326
|
-
* Performs literal text search (not regex).
|
|
327
|
-
*
|
|
328
|
-
* Returns a list of GrepMatch on success, or a string for invalid inputs.
|
|
329
|
-
* We deliberately do not raise here to keep backends non-throwing in tool
|
|
330
|
-
* contexts and preserve user-facing error messages.
|
|
302
|
+
* Performs literal text search (not regex). Binary files are skipped.
|
|
303
|
+
* Returns an empty array when no matches are found or on invalid input.
|
|
331
304
|
*/
|
|
332
305
|
function grepMatchesFromFiles(files, pattern, path = null, glob = null) {
|
|
333
306
|
let normalizedPath;
|
|
@@ -342,19 +315,231 @@ function grepMatchesFromFiles(files, pattern, path = null, glob = null) {
|
|
|
342
315
|
nobrace: false
|
|
343
316
|
})));
|
|
344
317
|
const matches = [];
|
|
345
|
-
for (const [filePath, fileData] of Object.entries(filtered))
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
318
|
+
for (const [filePath, fileData] of Object.entries(filtered)) {
|
|
319
|
+
if (!isTextMimeType(migrateToFileDataV2(fileData, filePath).mimeType)) continue;
|
|
320
|
+
const lines = fileDataToString(fileData).split("\n");
|
|
321
|
+
for (let i = 0; i < lines.length; i++) {
|
|
322
|
+
const line = lines[i];
|
|
323
|
+
const lineNum = i + 1;
|
|
324
|
+
if (line.includes(pattern)) matches.push({
|
|
325
|
+
path: filePath,
|
|
326
|
+
line: lineNum,
|
|
327
|
+
text: line
|
|
328
|
+
});
|
|
329
|
+
}
|
|
353
330
|
}
|
|
354
331
|
return matches;
|
|
355
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* Determine MIME type from a file path's extension.
|
|
335
|
+
*
|
|
336
|
+
* Returns "text/plain" for unknown extensions.
|
|
337
|
+
*
|
|
338
|
+
* @param filePath - File path to inspect
|
|
339
|
+
* @returns MIME type string (e.g., "image/png", "text/plain")
|
|
340
|
+
*/
|
|
341
|
+
function getMimeType(filePath) {
|
|
342
|
+
return MIME_TYPES[path.extname(filePath).toLocaleLowerCase()] || "text/plain";
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Check whether a MIME type represents text content.
|
|
346
|
+
*
|
|
347
|
+
* @param mimeType - MIME type string to check
|
|
348
|
+
* @returns True if the MIME type is text-based
|
|
349
|
+
*/
|
|
350
|
+
function isTextMimeType(mimeType) {
|
|
351
|
+
return mimeType.startsWith("text/") || mimeType === "application/json" || mimeType === "application/javascript" || mimeType === "image/svg+xml";
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Type guard to check if FileData is v1 format (content as line array).
|
|
355
|
+
*
|
|
356
|
+
* @param data - FileData to check
|
|
357
|
+
* @returns True if data is FileDataV1
|
|
358
|
+
*/
|
|
359
|
+
function isFileDataV1(data) {
|
|
360
|
+
return Array.isArray(data.content);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Convert FileData to v2 format, joining v1 line arrays into a single string.
|
|
364
|
+
*
|
|
365
|
+
* If the data is already v2, returns it unchanged.
|
|
366
|
+
*
|
|
367
|
+
* @param data - FileData in either format
|
|
368
|
+
* @returns FileDataV2 with content as string (text) or Uint8Array (binary)
|
|
369
|
+
*/
|
|
370
|
+
function migrateToFileDataV2(data, filePath) {
|
|
371
|
+
if (isFileDataV1(data)) return {
|
|
372
|
+
content: data.content.join("\n"),
|
|
373
|
+
mimeType: getMimeType(filePath),
|
|
374
|
+
created_at: data.created_at,
|
|
375
|
+
modified_at: data.modified_at
|
|
376
|
+
};
|
|
377
|
+
if (!("mimeType" in data) || !data.mimeType) return {
|
|
378
|
+
...data,
|
|
379
|
+
mimeType: getMimeType(filePath)
|
|
380
|
+
};
|
|
381
|
+
return data;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Adapt a v1 {@link BackendProtocol} to {@link BackendProtocolV2}.
|
|
385
|
+
*
|
|
386
|
+
* If the backend already implements v2, it is returned as-is.
|
|
387
|
+
* For v1 backends, wraps returns in Result types:
|
|
388
|
+
* - `read()` string returns wrapped in {@link ReadResult}
|
|
389
|
+
* - `readRaw()` FileData returns wrapped in {@link ReadRawResult}
|
|
390
|
+
* - `grep()` returns wrapped in {@link GrepResult}
|
|
391
|
+
* - `ls()` FileInfo[] returns wrapped in {@link LsResult}
|
|
392
|
+
* - `glob()` FileInfo[] returns wrapped in {@link GlobResult}
|
|
393
|
+
*
|
|
394
|
+
* Note: For sandbox instances, use {@link adaptSandboxProtocol} instead.
|
|
395
|
+
*
|
|
396
|
+
* @param backend - Backend instance (v1 or v2)
|
|
397
|
+
* @returns BackendProtocolV2-compatible backend
|
|
398
|
+
*/
|
|
399
|
+
function adaptBackendProtocol(backend) {
|
|
400
|
+
return {
|
|
401
|
+
async ls(path) {
|
|
402
|
+
const result = await ("ls" in backend ? backend.ls(path) : backend.lsInfo(path));
|
|
403
|
+
if (Array.isArray(result)) return { files: result };
|
|
404
|
+
return result;
|
|
405
|
+
},
|
|
406
|
+
async readRaw(filePath) {
|
|
407
|
+
const result = await backend.readRaw(filePath);
|
|
408
|
+
if ("data" in result || "error" in result) return result;
|
|
409
|
+
return { data: migrateToFileDataV2(result, filePath) };
|
|
410
|
+
},
|
|
411
|
+
async glob(pattern, path) {
|
|
412
|
+
const result = await ("glob" in backend ? backend.glob(pattern, path) : backend.globInfo(pattern, path));
|
|
413
|
+
if (Array.isArray(result)) return { files: result };
|
|
414
|
+
return result;
|
|
415
|
+
},
|
|
416
|
+
write: (filePath, content) => backend.write(filePath, content),
|
|
417
|
+
edit: (filePath, oldString, newString, replaceAll) => backend.edit(filePath, oldString, newString, replaceAll),
|
|
418
|
+
uploadFiles: backend.uploadFiles ? (files) => backend.uploadFiles(files) : void 0,
|
|
419
|
+
downloadFiles: backend.downloadFiles ? (paths) => backend.downloadFiles(paths) : void 0,
|
|
420
|
+
async read(filePath, offset, limit) {
|
|
421
|
+
const result = await backend.read(filePath, offset, limit);
|
|
422
|
+
if (typeof result === "string") return { content: result };
|
|
423
|
+
return result;
|
|
424
|
+
},
|
|
425
|
+
async grep(pattern, path, glob) {
|
|
426
|
+
const result = await ("grep" in backend ? backend.grep(pattern, path, glob) : backend.grepRaw(pattern, path, glob));
|
|
427
|
+
if (Array.isArray(result)) return { matches: result };
|
|
428
|
+
if (typeof result === "string") return { error: result };
|
|
429
|
+
return result;
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Adapt a sandbox backend from v1 to v2 interface.
|
|
435
|
+
*
|
|
436
|
+
* This extends {@link adaptBackendProtocol} to also preserve sandbox-specific
|
|
437
|
+
* properties from {@link SandboxBackendProtocol}: `execute` and `id`.
|
|
438
|
+
*
|
|
439
|
+
* @param sandbox - Sandbox backend (v1 or v2)
|
|
440
|
+
* @returns SandboxBackendProtocolV2-compatible sandbox
|
|
441
|
+
*/
|
|
442
|
+
function adaptSandboxProtocol(sandbox) {
|
|
443
|
+
const adapted = adaptBackendProtocol(sandbox);
|
|
444
|
+
adapted.execute = (cmd) => sandbox.execute(cmd);
|
|
445
|
+
Object.defineProperty(adapted, "id", {
|
|
446
|
+
value: sandbox.id,
|
|
447
|
+
enumerable: true,
|
|
448
|
+
configurable: true
|
|
449
|
+
});
|
|
450
|
+
return adapted;
|
|
451
|
+
}
|
|
452
|
+
//#endregion
|
|
453
|
+
//#region src/backends/protocol.ts
|
|
454
|
+
/**
|
|
455
|
+
* Type guard to check if a backend supports execution.
|
|
456
|
+
*
|
|
457
|
+
* @param backend - Backend instance to check
|
|
458
|
+
* @returns True if the backend implements SandboxBackendProtocolV2
|
|
459
|
+
*/
|
|
460
|
+
function isSandboxBackend(backend) {
|
|
461
|
+
return backend != null && typeof backend === "object" && typeof backend.execute === "function" && typeof backend.id === "string" && backend.id !== "";
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Type guard to check if a backend is a sandbox protocol (v1 or v2).
|
|
465
|
+
*
|
|
466
|
+
* Checks for the presence of `execute` function and `id` string,
|
|
467
|
+
* which are the defining features of sandbox protocols.
|
|
468
|
+
*
|
|
469
|
+
* @param backend - Backend instance to check
|
|
470
|
+
* @returns True if the backend implements sandbox protocol (v1 or v2)
|
|
471
|
+
*/
|
|
472
|
+
function isSandboxProtocol(backend) {
|
|
473
|
+
return backend != null && typeof backend === "object" && typeof backend.execute === "function" && typeof backend.id === "string" && backend.id !== "";
|
|
474
|
+
}
|
|
475
|
+
const SANDBOX_ERROR_SYMBOL = Symbol.for("sandbox.error");
|
|
476
|
+
/**
|
|
477
|
+
* Custom error class for sandbox operations.
|
|
478
|
+
*
|
|
479
|
+
* @param message - Human-readable error description
|
|
480
|
+
* @param code - Structured error code for programmatic handling
|
|
481
|
+
* @returns SandboxError with message and code
|
|
482
|
+
*
|
|
483
|
+
* @example
|
|
484
|
+
* ```typescript
|
|
485
|
+
* try {
|
|
486
|
+
* await sandbox.execute("some command");
|
|
487
|
+
* } catch (error) {
|
|
488
|
+
* if (error instanceof SandboxError) {
|
|
489
|
+
* switch (error.code) {
|
|
490
|
+
* case "NOT_INITIALIZED":
|
|
491
|
+
* await sandbox.initialize();
|
|
492
|
+
* break;
|
|
493
|
+
* case "COMMAND_TIMEOUT":
|
|
494
|
+
* console.error("Command took too long");
|
|
495
|
+
* break;
|
|
496
|
+
* default:
|
|
497
|
+
* throw error;
|
|
498
|
+
* }
|
|
499
|
+
* }
|
|
500
|
+
* }
|
|
501
|
+
* ```
|
|
502
|
+
*/
|
|
503
|
+
var SandboxError = class SandboxError extends Error {
|
|
504
|
+
/** Symbol for identifying sandbox error instances */
|
|
505
|
+
[SANDBOX_ERROR_SYMBOL] = true;
|
|
506
|
+
/** Error name for instanceof checks and logging */
|
|
507
|
+
name = "SandboxError";
|
|
508
|
+
/**
|
|
509
|
+
* Creates a new SandboxError.
|
|
510
|
+
*
|
|
511
|
+
* @param message - Human-readable error description
|
|
512
|
+
* @param code - Structured error code for programmatic handling
|
|
513
|
+
*/
|
|
514
|
+
constructor(message, code, cause) {
|
|
515
|
+
super(message);
|
|
516
|
+
this.code = code;
|
|
517
|
+
this.cause = cause;
|
|
518
|
+
Object.setPrototypeOf(this, SandboxError.prototype);
|
|
519
|
+
}
|
|
520
|
+
static isInstance(error) {
|
|
521
|
+
return typeof error === "object" && error !== null && error[SANDBOX_ERROR_SYMBOL] === true;
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
/**
|
|
525
|
+
* Resolve a backend instance or await a {@link BackendFactory}.
|
|
526
|
+
*
|
|
527
|
+
* Accepts {@link BackendRuntime} or {@link ToolRuntime} — store typing differs
|
|
528
|
+
* between LangGraph checkpoint stores and core `ToolRuntime`; factories receive
|
|
529
|
+
* a value that is structurally compatible at runtime.
|
|
530
|
+
*
|
|
531
|
+
* @internal
|
|
532
|
+
*/
|
|
533
|
+
async function resolveBackend(backend, runtime) {
|
|
534
|
+
if (typeof backend === "function") {
|
|
535
|
+
const resolved = await backend(runtime);
|
|
536
|
+
return isSandboxProtocol(resolved) ? adaptSandboxProtocol(resolved) : adaptBackendProtocol(resolved);
|
|
537
|
+
}
|
|
538
|
+
return isSandboxProtocol(backend) ? adaptSandboxProtocol(backend) : adaptBackendProtocol(backend);
|
|
539
|
+
}
|
|
356
540
|
//#endregion
|
|
357
541
|
//#region src/backends/state.ts
|
|
542
|
+
const PREGEL_SEND_KEY = "__pregel_send";
|
|
358
543
|
/**
|
|
359
544
|
* Backend that stores files in agent state (ephemeral).
|
|
360
545
|
*
|
|
@@ -368,23 +553,60 @@ function grepMatchesFromFiles(files, pattern, path = null, glob = null) {
|
|
|
368
553
|
*/
|
|
369
554
|
var StateBackend = class {
|
|
370
555
|
runtime;
|
|
371
|
-
|
|
372
|
-
|
|
556
|
+
fileFormat;
|
|
557
|
+
constructor(runtimeOrOptions, options) {
|
|
558
|
+
if (runtimeOrOptions != null && typeof runtimeOrOptions === "object" && "state" in runtimeOrOptions) {
|
|
559
|
+
this.runtime = runtimeOrOptions;
|
|
560
|
+
this.fileFormat = options?.fileFormat ?? "v2";
|
|
561
|
+
} else {
|
|
562
|
+
this.runtime = void 0;
|
|
563
|
+
this.fileFormat = runtimeOrOptions?.fileFormat ?? "v2";
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Whether this instance was constructed with the legacy factory pattern.
|
|
568
|
+
*
|
|
569
|
+
* When true, state is read from the injected `runtime` and `filesUpdate`
|
|
570
|
+
* is returned to the caller. When false, state is read from LangGraph's
|
|
571
|
+
* execution context and updates are sent via `__pregel_send`.
|
|
572
|
+
*/
|
|
573
|
+
get isLegacy() {
|
|
574
|
+
return this.runtime !== void 0;
|
|
373
575
|
}
|
|
374
576
|
/**
|
|
375
577
|
* Get files from current state.
|
|
578
|
+
*
|
|
579
|
+
* In legacy mode, reads from the injected {@link BackendRuntime}.
|
|
580
|
+
* In zero-arg mode, reads from the LangGraph execution context via
|
|
581
|
+
* {@link getCurrentTaskInput}.
|
|
376
582
|
*/
|
|
377
583
|
getFiles() {
|
|
378
|
-
return this.runtime.state.files || {};
|
|
584
|
+
if (this.runtime) return this.runtime.state.files || {};
|
|
585
|
+
return getCurrentTaskInput()?.files || {};
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Push a files state update through LangGraph's internal send channel.
|
|
589
|
+
*
|
|
590
|
+
* In zero-arg mode, sends the update via the `__pregel_send` function
|
|
591
|
+
* from {@link getConfig}, mirroring Python's `CONFIG_KEY_SEND`.
|
|
592
|
+
* In legacy mode, this is a no-op — the caller uses `filesUpdate`
|
|
593
|
+
* from the return value instead.
|
|
594
|
+
*
|
|
595
|
+
* @param update - Map of file paths to their updated {@link FileData}
|
|
596
|
+
*/
|
|
597
|
+
sendFilesUpdate(update) {
|
|
598
|
+
if (this.isLegacy) return;
|
|
599
|
+
const send = getConfig().configurable?.[PREGEL_SEND_KEY];
|
|
600
|
+
if (typeof send === "function") send([["files", update]]);
|
|
379
601
|
}
|
|
380
602
|
/**
|
|
381
603
|
* List files and directories in the specified directory (non-recursive).
|
|
382
604
|
*
|
|
383
605
|
* @param path - Absolute path to directory
|
|
384
|
-
* @returns
|
|
606
|
+
* @returns LsResult with list of FileInfo objects on success or error on failure.
|
|
385
607
|
* Directories have a trailing / in their path and is_dir=true.
|
|
386
608
|
*/
|
|
387
|
-
|
|
609
|
+
ls(path) {
|
|
388
610
|
const files = this.getFiles();
|
|
389
611
|
const infos = [];
|
|
390
612
|
const subdirs = /* @__PURE__ */ new Set();
|
|
@@ -397,7 +619,7 @@ var StateBackend = class {
|
|
|
397
619
|
subdirs.add(normalizedPath + subdirName + "/");
|
|
398
620
|
continue;
|
|
399
621
|
}
|
|
400
|
-
const size = fd.content.join("\n").length;
|
|
622
|
+
const size = isFileDataV1(fd) ? fd.content.join("\n").length : isFileDataBinary(fd) ? fd.content.byteLength : fd.content.length;
|
|
401
623
|
infos.push({
|
|
402
624
|
path: k,
|
|
403
625
|
is_dir: false,
|
|
@@ -412,31 +634,43 @@ var StateBackend = class {
|
|
|
412
634
|
modified_at: ""
|
|
413
635
|
});
|
|
414
636
|
infos.sort((a, b) => a.path.localeCompare(b.path));
|
|
415
|
-
return infos;
|
|
637
|
+
return { files: infos };
|
|
416
638
|
}
|
|
417
639
|
/**
|
|
418
|
-
* Read file content
|
|
640
|
+
* Read file content.
|
|
641
|
+
*
|
|
642
|
+
* Text files are paginated by line offset/limit.
|
|
643
|
+
* Binary files return full Uint8Array content (offset/limit ignored).
|
|
419
644
|
*
|
|
420
645
|
* @param filePath - Absolute file path
|
|
421
646
|
* @param offset - Line offset to start reading from (0-indexed)
|
|
422
647
|
* @param limit - Maximum number of lines to read
|
|
423
|
-
* @returns
|
|
648
|
+
* @returns ReadResult with content on success or error on failure
|
|
424
649
|
*/
|
|
425
650
|
read(filePath, offset = 0, limit = 500) {
|
|
426
651
|
const fileData = this.getFiles()[filePath];
|
|
427
|
-
if (!fileData) return
|
|
428
|
-
|
|
652
|
+
if (!fileData) return { error: `File '${filePath}' not found` };
|
|
653
|
+
const fileDataV2 = migrateToFileDataV2(fileData, filePath);
|
|
654
|
+
if (!isTextMimeType(fileDataV2.mimeType)) return {
|
|
655
|
+
content: fileDataV2.content,
|
|
656
|
+
mimeType: fileDataV2.mimeType
|
|
657
|
+
};
|
|
658
|
+
if (typeof fileDataV2.content !== "string") return { error: `File '${filePath}' has binary content but text MIME type` };
|
|
659
|
+
return {
|
|
660
|
+
content: fileDataV2.content.split("\n").slice(offset, offset + limit).join("\n"),
|
|
661
|
+
mimeType: fileDataV2.mimeType
|
|
662
|
+
};
|
|
429
663
|
}
|
|
430
664
|
/**
|
|
431
665
|
* Read file content as raw FileData.
|
|
432
666
|
*
|
|
433
667
|
* @param filePath - Absolute file path
|
|
434
|
-
* @returns
|
|
668
|
+
* @returns ReadRawResult with raw file data on success or error on failure
|
|
435
669
|
*/
|
|
436
670
|
readRaw(filePath) {
|
|
437
671
|
const fileData = this.getFiles()[filePath];
|
|
438
|
-
if (!fileData)
|
|
439
|
-
return fileData;
|
|
672
|
+
if (!fileData) return { error: `File '${filePath}' not found` };
|
|
673
|
+
return { data: fileData };
|
|
440
674
|
}
|
|
441
675
|
/**
|
|
442
676
|
* Create a new file with content.
|
|
@@ -444,7 +678,13 @@ var StateBackend = class {
|
|
|
444
678
|
*/
|
|
445
679
|
write(filePath, content) {
|
|
446
680
|
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.` };
|
|
447
|
-
const
|
|
681
|
+
const mimeType = getMimeType(filePath);
|
|
682
|
+
const newFileData = createFileData(content, void 0, this.fileFormat, mimeType);
|
|
683
|
+
const update = { [filePath]: newFileData };
|
|
684
|
+
if (!this.isLegacy) {
|
|
685
|
+
this.sendFilesUpdate(update);
|
|
686
|
+
return { path: filePath };
|
|
687
|
+
}
|
|
448
688
|
return {
|
|
449
689
|
path: filePath,
|
|
450
690
|
filesUpdate: { [filePath]: newFileData }
|
|
@@ -461,6 +701,14 @@ var StateBackend = class {
|
|
|
461
701
|
if (typeof result === "string") return { error: result };
|
|
462
702
|
const [newContent, occurrences] = result;
|
|
463
703
|
const newFileData = updateFileData(fileData, newContent);
|
|
704
|
+
const update = { [filePath]: newFileData };
|
|
705
|
+
if (!this.isLegacy) {
|
|
706
|
+
this.sendFilesUpdate(update);
|
|
707
|
+
return {
|
|
708
|
+
path: filePath,
|
|
709
|
+
occurrences
|
|
710
|
+
};
|
|
711
|
+
}
|
|
464
712
|
return {
|
|
465
713
|
path: filePath,
|
|
466
714
|
filesUpdate: { [filePath]: newFileData },
|
|
@@ -468,23 +716,24 @@ var StateBackend = class {
|
|
|
468
716
|
};
|
|
469
717
|
}
|
|
470
718
|
/**
|
|
471
|
-
*
|
|
719
|
+
* Search file contents for a literal text pattern.
|
|
720
|
+
* Binary files are skipped.
|
|
472
721
|
*/
|
|
473
|
-
|
|
474
|
-
return grepMatchesFromFiles(this.getFiles(), pattern, path, glob);
|
|
722
|
+
grep(pattern, path = "/", glob = null) {
|
|
723
|
+
return { matches: grepMatchesFromFiles(this.getFiles(), pattern, path, glob) };
|
|
475
724
|
}
|
|
476
725
|
/**
|
|
477
726
|
* Structured glob matching returning FileInfo objects.
|
|
478
727
|
*/
|
|
479
|
-
|
|
728
|
+
glob(pattern, path = "/") {
|
|
480
729
|
const files = this.getFiles();
|
|
481
730
|
const result = globSearchFiles(files, pattern, path);
|
|
482
|
-
if (result === "No files found") return [];
|
|
731
|
+
if (result === "No files found") return { files: [] };
|
|
483
732
|
const paths = result.split("\n");
|
|
484
733
|
const infos = [];
|
|
485
734
|
for (const p of paths) {
|
|
486
735
|
const fd = files[p];
|
|
487
|
-
const size = fd ? fd.content.join("\n").length : 0;
|
|
736
|
+
const size = fd ? isFileDataV1(fd) ? fd.content.join("\n").length : isFileDataBinary(fd) ? fd.content.byteLength : fd.content.length : 0;
|
|
488
737
|
infos.push({
|
|
489
738
|
path: p,
|
|
490
739
|
is_dir: false,
|
|
@@ -492,7 +741,7 @@ var StateBackend = class {
|
|
|
492
741
|
modified_at: fd?.modified_at || ""
|
|
493
742
|
});
|
|
494
743
|
}
|
|
495
|
-
return infos;
|
|
744
|
+
return { files: infos };
|
|
496
745
|
}
|
|
497
746
|
/**
|
|
498
747
|
* Upload multiple files.
|
|
@@ -507,7 +756,9 @@ var StateBackend = class {
|
|
|
507
756
|
const responses = [];
|
|
508
757
|
const updates = {};
|
|
509
758
|
for (const [path, content] of files) try {
|
|
510
|
-
|
|
759
|
+
const mimeType = getMimeType(path);
|
|
760
|
+
if (this.fileFormat === "v2" && !isTextMimeType(mimeType)) updates[path] = createFileData(content, void 0, "v2", mimeType);
|
|
761
|
+
else updates[path] = createFileData(new TextDecoder().decode(content), void 0, this.fileFormat, mimeType);
|
|
511
762
|
responses.push({
|
|
512
763
|
path,
|
|
513
764
|
error: null
|
|
@@ -518,6 +769,10 @@ var StateBackend = class {
|
|
|
518
769
|
error: "invalid_path"
|
|
519
770
|
});
|
|
520
771
|
}
|
|
772
|
+
if (!this.isLegacy) {
|
|
773
|
+
if (Object.keys(updates).length > 0) this.sendFilesUpdate(updates);
|
|
774
|
+
return responses;
|
|
775
|
+
}
|
|
521
776
|
const result = responses;
|
|
522
777
|
result.filesUpdate = updates;
|
|
523
778
|
return result;
|
|
@@ -541,11 +796,17 @@ var StateBackend = class {
|
|
|
541
796
|
});
|
|
542
797
|
continue;
|
|
543
798
|
}
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
799
|
+
const fileDataV2 = migrateToFileDataV2(fileData, path);
|
|
800
|
+
if (typeof fileDataV2.content === "string") {
|
|
801
|
+
const content = new TextEncoder().encode(fileDataV2.content);
|
|
802
|
+
responses.push({
|
|
803
|
+
path,
|
|
804
|
+
content,
|
|
805
|
+
error: null
|
|
806
|
+
});
|
|
807
|
+
} else responses.push({
|
|
547
808
|
path,
|
|
548
|
-
content,
|
|
809
|
+
content: fileDataV2.content,
|
|
549
810
|
error: null
|
|
550
811
|
});
|
|
551
812
|
}
|
|
@@ -561,6 +822,7 @@ var StateBackend = class {
|
|
|
561
822
|
* - Pluggable backends (StateBackend, StoreBackend, FilesystemBackend, CompositeBackend)
|
|
562
823
|
* - Tool result eviction for large outputs
|
|
563
824
|
*/
|
|
825
|
+
const INT_FORMATTER = new Intl.NumberFormat("en-US");
|
|
564
826
|
/**
|
|
565
827
|
* Tools that should be excluded from the large result eviction logic.
|
|
566
828
|
*
|
|
@@ -607,6 +869,12 @@ const TOOLS_EXCLUDED_FROM_EVICTION = [
|
|
|
607
869
|
"write_file"
|
|
608
870
|
];
|
|
609
871
|
/**
|
|
872
|
+
* Maximum size for binary (non-text) files read via read_file, in bytes.
|
|
873
|
+
* Base64-encoded content is ~33% larger, so 10MB raw ≈ 13.3MB in context.
|
|
874
|
+
* This keeps inline multimodal payloads within all major provider limits.
|
|
875
|
+
*/
|
|
876
|
+
const MAX_BINARY_READ_SIZE_BYTES = 10 * 1024 * 1024;
|
|
877
|
+
/**
|
|
610
878
|
* Template for truncation message in read_file.
|
|
611
879
|
* {file_path} will be filled in at runtime.
|
|
612
880
|
*/
|
|
@@ -616,16 +884,18 @@ const READ_FILE_TRUNCATION_MSG = `
|
|
|
616
884
|
/**
|
|
617
885
|
* Message template for evicted tool results.
|
|
618
886
|
*/
|
|
619
|
-
const TOO_LARGE_TOOL_MSG = `
|
|
620
|
-
|
|
621
|
-
You can
|
|
622
|
-
|
|
887
|
+
const TOO_LARGE_TOOL_MSG = context`
|
|
888
|
+
Tool result too large, the result of this tool call {tool_call_id} was saved in the filesystem at this path: {file_path}
|
|
889
|
+
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.
|
|
890
|
+
You can do this by specifying an offset and limit in the read_file tool call.
|
|
891
|
+
For example, to read the first 100 lines, you can use the read_file tool with offset=0 and limit=100.
|
|
623
892
|
|
|
624
|
-
Here is a preview showing the head and tail of the result (lines of the form
|
|
625
|
-
... [N lines truncated] ...
|
|
626
|
-
indicate omitted lines in the middle of the content):
|
|
893
|
+
Here is a preview showing the head and tail of the result (lines of the form
|
|
894
|
+
... [N lines truncated] ...
|
|
895
|
+
indicate omitted lines in the middle of the content):
|
|
627
896
|
|
|
628
|
-
{content_sample}
|
|
897
|
+
{content_sample}
|
|
898
|
+
`;
|
|
629
899
|
/**
|
|
630
900
|
* Message template for evicted HumanMessages.
|
|
631
901
|
*/
|
|
@@ -701,14 +971,27 @@ function createContentPreview(contentStr, headLines = 5, tailLines = 5) {
|
|
|
701
971
|
return headSample + truncationNotice + tailSample;
|
|
702
972
|
}
|
|
703
973
|
/**
|
|
704
|
-
* Zod
|
|
974
|
+
* Zod schema for legacy FileDataV1 (content as line array).
|
|
705
975
|
*/
|
|
706
|
-
const
|
|
976
|
+
const FileDataV1Schema = z.object({
|
|
707
977
|
content: z.array(z.string()),
|
|
708
978
|
created_at: z.string(),
|
|
709
979
|
modified_at: z.string()
|
|
710
980
|
});
|
|
711
981
|
/**
|
|
982
|
+
* Zod schema for FileDataV2 (content as string for text or Uint8Array for binary).
|
|
983
|
+
*/
|
|
984
|
+
const FileDataV2Schema = z.object({
|
|
985
|
+
content: z.union([z.string(), z.instanceof(Uint8Array)]),
|
|
986
|
+
mimeType: z.string(),
|
|
987
|
+
created_at: z.string(),
|
|
988
|
+
modified_at: z.string()
|
|
989
|
+
});
|
|
990
|
+
/**
|
|
991
|
+
* Zod v3 schema for FileData (re-export from backends)
|
|
992
|
+
*/
|
|
993
|
+
const FileDataSchema = z.union([FileDataV1Schema, FileDataV2Schema]);
|
|
994
|
+
/**
|
|
712
995
|
* Reducer for files state that merges file updates with support for deletions.
|
|
713
996
|
* When a file value is null, the file is deleted from state.
|
|
714
997
|
* When a file value is non-null, it is added or updated in state.
|
|
@@ -744,118 +1027,141 @@ const FilesystemStateSchema = new StateSchema({ files: new ReducedValue(z.record
|
|
|
744
1027
|
inputSchema: z.record(z.string(), FileDataSchema.nullable()).optional(),
|
|
745
1028
|
reducer: fileDataReducer
|
|
746
1029
|
}) });
|
|
747
|
-
const FILESYSTEM_SYSTEM_PROMPT =
|
|
1030
|
+
const FILESYSTEM_SYSTEM_PROMPT = context`
|
|
1031
|
+
## Following Conventions
|
|
748
1032
|
|
|
749
|
-
|
|
750
|
-
|
|
1033
|
+
- Read files before editing — understand existing content before making changes
|
|
1034
|
+
- Mimic existing style, naming conventions, and patterns
|
|
751
1035
|
|
|
752
|
-
|
|
753
|
-
- read_file: read a file from the filesystem
|
|
754
|
-
- write_file: write to a file in the filesystem
|
|
755
|
-
- edit_file: edit a file in the filesystem
|
|
756
|
-
- glob: find files matching a pattern (e.g., "**/*.py")
|
|
757
|
-
- grep: search for text within files`;
|
|
758
|
-
const LS_TOOL_DESCRIPTION = `Lists all files in a directory.
|
|
1036
|
+
## Filesystem Tools \`ls\`, \`read_file\`, \`write_file\`, \`edit_file\`, \`glob\`, \`grep\`
|
|
759
1037
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
const READ_FILE_TOOL_DESCRIPTION = `Reads a file from the filesystem.
|
|
1038
|
+
You have access to a filesystem which you can interact with using these tools.
|
|
1039
|
+
All file paths must start with a /.
|
|
763
1040
|
|
|
764
|
-
|
|
1041
|
+
- ls: list files in a directory (requires absolute path)
|
|
1042
|
+
- read_file: read a file from the filesystem
|
|
1043
|
+
- write_file: write to a file in the filesystem
|
|
1044
|
+
- edit_file: edit a file in the filesystem
|
|
1045
|
+
- glob: find files matching a pattern (e.g., "**/*.py")
|
|
1046
|
+
- grep: search for text within files
|
|
1047
|
+
`;
|
|
1048
|
+
const LS_TOOL_DESCRIPTION = context`
|
|
1049
|
+
Lists all files in a directory.
|
|
765
1050
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
-
|
|
776
|
-
-
|
|
777
|
-
-
|
|
778
|
-
|
|
1051
|
+
This is useful for exploring the filesystem and finding the right file to read or edit.
|
|
1052
|
+
You should almost ALWAYS use this tool before using the read_file or edit_file tools.
|
|
1053
|
+
`;
|
|
1054
|
+
const READ_FILE_TOOL_DESCRIPTION = context`
|
|
1055
|
+
Reads a file from the filesystem.
|
|
1056
|
+
|
|
1057
|
+
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.
|
|
1058
|
+
|
|
1059
|
+
Usage:
|
|
1060
|
+
- By default, it reads up to 100 lines starting from the beginning of the file
|
|
1061
|
+
- **IMPORTANT for large files and codebase exploration**: Use pagination with offset and limit parameters to avoid context overflow
|
|
1062
|
+
- First scan: read_file(path, limit=100) to see file structure
|
|
1063
|
+
- Read more sections: read_file(path, offset=100, limit=200) for next 200 lines
|
|
1064
|
+
- Only omit limit (read full file) when necessary for editing
|
|
1065
|
+
- Specify offset and limit: read_file(path, offset=0, limit=100) reads first 100 lines
|
|
1066
|
+
- Results are returned using cat -n format, with line numbers starting at 1
|
|
1067
|
+
- 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.
|
|
1068
|
+
- 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.
|
|
1069
|
+
- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.
|
|
1070
|
+
- You should ALWAYS make sure a file has been read before editing it.
|
|
1071
|
+
`;
|
|
1072
|
+
const WRITE_FILE_TOOL_DESCRIPTION = context`
|
|
1073
|
+
Writes to a new file in the filesystem.
|
|
779
1074
|
|
|
780
|
-
Usage:
|
|
781
|
-
- The write_file tool will create a new file.
|
|
782
|
-
- Prefer to edit existing files (with the edit_file tool) over creating new ones when possible
|
|
783
|
-
|
|
1075
|
+
Usage:
|
|
1076
|
+
- The write_file tool will create a new file.
|
|
1077
|
+
- Prefer to edit existing files (with the edit_file tool) over creating new ones when possible.
|
|
1078
|
+
`;
|
|
1079
|
+
const EDIT_FILE_TOOL_DESCRIPTION = context`
|
|
1080
|
+
Performs exact string replacements in files.
|
|
784
1081
|
|
|
785
|
-
Usage:
|
|
786
|
-
- You must read the file before editing. This tool will error if you attempt an edit without reading the file first.
|
|
787
|
-
- When editing, preserve the exact indentation (tabs/spaces) from the read output. Never include line number prefixes in old_string or new_string.
|
|
788
|
-
- ALWAYS prefer editing existing files over creating new ones.
|
|
789
|
-
- Only use emojis if the user explicitly requests it
|
|
790
|
-
|
|
1082
|
+
Usage:
|
|
1083
|
+
- You must read the file before editing. This tool will error if you attempt an edit without reading the file first.
|
|
1084
|
+
- When editing, preserve the exact indentation (tabs/spaces) from the read output. Never include line number prefixes in old_string or new_string.
|
|
1085
|
+
- ALWAYS prefer editing existing files over creating new ones.
|
|
1086
|
+
- Only use emojis if the user explicitly requests it.
|
|
1087
|
+
`;
|
|
1088
|
+
const GLOB_TOOL_DESCRIPTION = context`
|
|
1089
|
+
Find files matching a glob pattern.
|
|
791
1090
|
|
|
792
|
-
Supports standard glob patterns: \`*\` (any characters), \`**\` (any directories), \`?\` (single character).
|
|
793
|
-
Returns a list of absolute file paths that match the pattern.
|
|
1091
|
+
Supports standard glob patterns: \`*\` (any characters), \`**\` (any directories), \`?\` (single character).
|
|
1092
|
+
Returns a list of absolute file paths that match the pattern.
|
|
794
1093
|
|
|
795
|
-
Examples:
|
|
796
|
-
- \`**/*.py\` - Find all Python files
|
|
797
|
-
- \`*.txt\` - Find all text files in root
|
|
798
|
-
- \`/subdir/**/*.md\` - Find all markdown files under /subdir
|
|
799
|
-
|
|
1094
|
+
Examples:
|
|
1095
|
+
- \`**/*.py\` - Find all Python files
|
|
1096
|
+
- \`*.txt\` - Find all text files in root
|
|
1097
|
+
- \`/subdir/**/*.md\` - Find all markdown files under /subdir
|
|
1098
|
+
`;
|
|
1099
|
+
const GREP_TOOL_DESCRIPTION = context`
|
|
1100
|
+
Search for a text pattern across files.
|
|
800
1101
|
|
|
801
|
-
Searches for literal text (not regex) and returns matching files or content based on output_mode.
|
|
802
|
-
Special characters like parentheses, brackets, pipes, etc. are treated as literal characters, not regex operators.
|
|
1102
|
+
Searches for literal text (not regex) and returns matching files or content based on output_mode.
|
|
1103
|
+
Special characters like parentheses, brackets, pipes, etc. are treated as literal characters, not regex operators.
|
|
803
1104
|
|
|
804
|
-
Examples:
|
|
805
|
-
- Search all files: \`grep(pattern="TODO")\`
|
|
806
|
-
- Search Python files only: \`grep(pattern="import", glob="*.py")\`
|
|
807
|
-
- Show matching lines: \`grep(pattern="error", output_mode="content")\`
|
|
808
|
-
- Search for code with special chars: \`grep(pattern="def __init__(self):")
|
|
809
|
-
|
|
1105
|
+
Examples:
|
|
1106
|
+
- Search all files: \`grep(pattern="TODO")\`
|
|
1107
|
+
- Search Python files only: \`grep(pattern="import", glob="*.py")\`
|
|
1108
|
+
- Show matching lines: \`grep(pattern="error", output_mode="content")\`
|
|
1109
|
+
- Search for code with special chars: \`grep(pattern="def __init__(self):")\`
|
|
1110
|
+
`;
|
|
1111
|
+
const EXECUTE_TOOL_DESCRIPTION = context`
|
|
1112
|
+
Executes a shell command in an isolated sandbox environment.
|
|
810
1113
|
|
|
811
|
-
Usage:
|
|
812
|
-
Executes a given command in the sandbox environment with proper handling and security measures.
|
|
813
|
-
Before executing the command, please follow these steps:
|
|
1114
|
+
Usage:
|
|
1115
|
+
Executes a given command in the sandbox environment with proper handling and security measures.
|
|
1116
|
+
Before executing the command, please follow these steps:
|
|
814
1117
|
|
|
815
|
-
1. Directory Verification:
|
|
816
|
-
|
|
817
|
-
|
|
1118
|
+
1. Directory Verification:
|
|
1119
|
+
- 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
|
|
1120
|
+
- For example, before running "mkdir foo/bar", first use ls to check that "foo" exists and is the intended parent directory
|
|
818
1121
|
|
|
819
|
-
2. Command Execution:
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1122
|
+
2. Command Execution:
|
|
1123
|
+
- Always quote file paths that contain spaces with double quotes (e.g., cd "path with spaces/file.txt")
|
|
1124
|
+
- Examples of proper quoting:
|
|
1125
|
+
- cd "/Users/name/My Documents" (correct)
|
|
1126
|
+
- cd /Users/name/My Documents (incorrect - will fail)
|
|
1127
|
+
- python "/path/with spaces/script.py" (correct)
|
|
1128
|
+
- python /path/with spaces/script.py (incorrect - will fail)
|
|
1129
|
+
- After ensuring proper quoting, execute the command
|
|
1130
|
+
- Capture the output of the command
|
|
828
1131
|
|
|
829
|
-
Usage notes:
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1132
|
+
Usage notes:
|
|
1133
|
+
- Commands run in an isolated sandbox environment
|
|
1134
|
+
- Returns combined stdout/stderr output with exit code
|
|
1135
|
+
- If the output is very large, it may be truncated
|
|
1136
|
+
- 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.
|
|
1137
|
+
- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings)
|
|
1138
|
+
- Use '&&' when commands depend on each other (e.g., "mkdir dir && cd dir")
|
|
1139
|
+
- Use ';' only when you need to run commands sequentially but don't care if earlier commands fail
|
|
1140
|
+
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of cd
|
|
838
1141
|
|
|
839
|
-
Examples:
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1142
|
+
Examples:
|
|
1143
|
+
Good examples:
|
|
1144
|
+
- execute(command="pytest /foo/bar/tests")
|
|
1145
|
+
- execute(command="python /path/to/script.py")
|
|
1146
|
+
- execute(command="npm install && npm test")
|
|
844
1147
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1148
|
+
Bad examples (avoid these):
|
|
1149
|
+
- execute(command="cd /foo/bar && pytest tests") # Use absolute path instead
|
|
1150
|
+
- execute(command="cat file.txt") # Use read_file tool instead
|
|
1151
|
+
- execute(command="find . -name '*.py'") # Use glob tool instead
|
|
1152
|
+
- execute(command="grep -r 'pattern' .") # Use grep tool instead
|
|
850
1153
|
|
|
851
|
-
Note: This tool is only available if the backend supports execution (SandboxBackendProtocol).
|
|
852
|
-
If execution is not supported, the tool will return an error message
|
|
853
|
-
|
|
1154
|
+
Note: This tool is only available if the backend supports execution (SandboxBackendProtocol).
|
|
1155
|
+
If execution is not supported, the tool will return an error message.
|
|
1156
|
+
`;
|
|
1157
|
+
const EXECUTION_SYSTEM_PROMPT = context`
|
|
1158
|
+
## Execute Tool \`execute\`
|
|
854
1159
|
|
|
855
|
-
You have access to an \`execute\` tool for running shell commands in a sandboxed environment.
|
|
856
|
-
Use this tool to run commands, scripts, tests, builds, and other shell operations.
|
|
1160
|
+
You have access to an \`execute\` tool for running shell commands in a sandboxed environment.
|
|
1161
|
+
Use this tool to run commands, scripts, tests, builds, and other shell operations.
|
|
857
1162
|
|
|
858
|
-
- execute: run a shell command in the sandbox (returns output and exit code)
|
|
1163
|
+
- execute: run a shell command in the sandbox (returns output and exit code)
|
|
1164
|
+
`;
|
|
859
1165
|
/**
|
|
860
1166
|
* Create ls tool using backend.
|
|
861
1167
|
*/
|
|
@@ -864,7 +1170,9 @@ function createLsTool(backend, options) {
|
|
|
864
1170
|
return tool(async (input, runtime) => {
|
|
865
1171
|
const resolvedBackend = await resolveBackend(backend, runtime);
|
|
866
1172
|
const path = input.path || "/";
|
|
867
|
-
const
|
|
1173
|
+
const lsResult = await resolvedBackend.ls(path);
|
|
1174
|
+
if (lsResult.error) return `Error listing files: ${lsResult.error}`;
|
|
1175
|
+
const infos = lsResult.files || [];
|
|
868
1176
|
if (infos.length === 0) return `No files found in ${path}`;
|
|
869
1177
|
const lines = [];
|
|
870
1178
|
for (const info of infos) if (info.is_dir) lines.push(`${info.path} (directory)`);
|
|
@@ -889,15 +1197,64 @@ function createReadFileTool(backend, options) {
|
|
|
889
1197
|
return tool(async (input, runtime) => {
|
|
890
1198
|
const resolvedBackend = await resolveBackend(backend, runtime);
|
|
891
1199
|
const { file_path, offset = 0, limit = 100 } = input;
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1200
|
+
const readResult = await resolvedBackend.read(file_path, offset, limit);
|
|
1201
|
+
if (readResult.error) return [{
|
|
1202
|
+
type: "text",
|
|
1203
|
+
text: `Error: ${readResult.error}`
|
|
1204
|
+
}];
|
|
1205
|
+
const mimeType = readResult.mimeType ?? getMimeType(file_path);
|
|
1206
|
+
if (!isTextMimeType(mimeType)) {
|
|
1207
|
+
const binaryContent = readResult.content;
|
|
1208
|
+
if (!binaryContent) return [{
|
|
1209
|
+
type: "text",
|
|
1210
|
+
text: `Error: expected binary content for '${file_path}'`
|
|
1211
|
+
}];
|
|
1212
|
+
let base64Data;
|
|
1213
|
+
if (typeof binaryContent === "string") base64Data = binaryContent;
|
|
1214
|
+
else if (ArrayBuffer.isView(binaryContent)) base64Data = Buffer.from(binaryContent).toString("base64");
|
|
1215
|
+
else {
|
|
1216
|
+
const values = Object.values(binaryContent);
|
|
1217
|
+
base64Data = Buffer.from(new Uint8Array(values)).toString("base64");
|
|
1218
|
+
}
|
|
1219
|
+
const sizeBytes = Math.ceil(base64Data.length * 3 / 4);
|
|
1220
|
+
if (sizeBytes > 10485760) return [{
|
|
1221
|
+
type: "text",
|
|
1222
|
+
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)`
|
|
1223
|
+
}];
|
|
1224
|
+
if (mimeType.startsWith("image/")) return [{
|
|
1225
|
+
type: "image",
|
|
1226
|
+
mimeType,
|
|
1227
|
+
data: base64Data
|
|
1228
|
+
}];
|
|
1229
|
+
if (mimeType.startsWith("audio/")) return [{
|
|
1230
|
+
type: "audio",
|
|
1231
|
+
mimeType,
|
|
1232
|
+
data: base64Data
|
|
1233
|
+
}];
|
|
1234
|
+
if (mimeType.startsWith("video/")) return [{
|
|
1235
|
+
type: "video",
|
|
1236
|
+
mimeType,
|
|
1237
|
+
data: base64Data
|
|
1238
|
+
}];
|
|
1239
|
+
return [{
|
|
1240
|
+
type: "file",
|
|
1241
|
+
mimeType,
|
|
1242
|
+
data: base64Data
|
|
1243
|
+
}];
|
|
1244
|
+
}
|
|
1245
|
+
let content = typeof readResult.content === "string" ? readResult.content : "";
|
|
1246
|
+
const lines = content.split("\n");
|
|
1247
|
+
if (lines.length > limit) content = lines.slice(0, limit).join("\n");
|
|
1248
|
+
let formatted = formatContentWithLineNumbers(content, offset + 1);
|
|
1249
|
+
if (toolTokenLimitBeforeEvict && formatted.length >= 4 * toolTokenLimitBeforeEvict) {
|
|
896
1250
|
const truncationMsg = READ_FILE_TRUNCATION_MSG.replace("{file_path}", file_path);
|
|
897
1251
|
const maxContentLength = 4 * toolTokenLimitBeforeEvict - truncationMsg.length;
|
|
898
|
-
|
|
1252
|
+
formatted = formatted.substring(0, maxContentLength) + truncationMsg;
|
|
899
1253
|
}
|
|
900
|
-
return
|
|
1254
|
+
return [{
|
|
1255
|
+
type: "text",
|
|
1256
|
+
text: formatted
|
|
1257
|
+
}];
|
|
901
1258
|
}, {
|
|
902
1259
|
name: "read_file",
|
|
903
1260
|
description: customDescription || READ_FILE_TOOL_DESCRIPTION,
|
|
@@ -978,7 +1335,9 @@ function createGlobTool(backend, options) {
|
|
|
978
1335
|
return tool(async (input, runtime) => {
|
|
979
1336
|
const resolvedBackend = await resolveBackend(backend, runtime);
|
|
980
1337
|
const { pattern, path = "/" } = input;
|
|
981
|
-
const
|
|
1338
|
+
const globResult = await resolvedBackend.glob(pattern, path);
|
|
1339
|
+
if (globResult.error) return `Error finding files: ${globResult.error}`;
|
|
1340
|
+
const infos = globResult.files || [];
|
|
982
1341
|
if (infos.length === 0) return `No files found matching pattern '${pattern}'`;
|
|
983
1342
|
const result = truncateIfTooLong(infos.map((info) => info.path));
|
|
984
1343
|
if (Array.isArray(result)) return result.join("\n");
|
|
@@ -1000,12 +1359,13 @@ function createGrepTool(backend, options) {
|
|
|
1000
1359
|
return tool(async (input, runtime) => {
|
|
1001
1360
|
const resolvedBackend = await resolveBackend(backend, runtime);
|
|
1002
1361
|
const { pattern, path = "/", glob = null } = input;
|
|
1003
|
-
const result = await resolvedBackend.
|
|
1004
|
-
if (
|
|
1005
|
-
|
|
1362
|
+
const result = await resolvedBackend.grep(pattern, path, glob);
|
|
1363
|
+
if (result.error) return result.error;
|
|
1364
|
+
const matches = result.matches ?? [];
|
|
1365
|
+
if (matches.length === 0) return `No matches found for pattern '${pattern}'`;
|
|
1006
1366
|
const lines = [];
|
|
1007
1367
|
let currentFile = null;
|
|
1008
|
-
for (const match of
|
|
1368
|
+
for (const match of matches) {
|
|
1009
1369
|
if (match.path !== currentFile) {
|
|
1010
1370
|
currentFile = match.path;
|
|
1011
1371
|
lines.push(`\n${currentFile}:`);
|
|
@@ -1021,7 +1381,7 @@ function createGrepTool(backend, options) {
|
|
|
1021
1381
|
schema: z.object({
|
|
1022
1382
|
pattern: z.string().describe("Regex pattern to search for"),
|
|
1023
1383
|
path: z.string().optional().default("/").describe("Base path to search from (default: /)"),
|
|
1024
|
-
glob: z.string().optional().nullable().describe("Optional glob pattern to filter files (e.g., '*.py')")
|
|
1384
|
+
glob: z.string().optional().nullable().default(null).describe("Optional glob pattern to filter files (e.g., '*.py')")
|
|
1025
1385
|
})
|
|
1026
1386
|
});
|
|
1027
1387
|
}
|
|
@@ -1221,117 +1581,117 @@ const EXCLUDED_STATE_KEYS = [
|
|
|
1221
1581
|
*/
|
|
1222
1582
|
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.";
|
|
1223
1583
|
function getTaskToolDescription(subagentDescriptions) {
|
|
1224
|
-
return `
|
|
1225
|
-
Launch an ephemeral subagent to handle complex, multi-step independent tasks with isolated context windows.
|
|
1584
|
+
return context`
|
|
1585
|
+
Launch an ephemeral subagent to handle complex, multi-step independent tasks with isolated context windows.
|
|
1226
1586
|
|
|
1227
|
-
Available agent types and the tools they have access to:
|
|
1228
|
-
${subagentDescriptions.join("\n")}
|
|
1587
|
+
Available agent types and the tools they have access to:
|
|
1588
|
+
${subagentDescriptions.join("\n")}
|
|
1229
1589
|
|
|
1230
|
-
When using the Task tool, you must specify a subagent_type parameter to select which agent type to use.
|
|
1590
|
+
When using the Task tool, you must specify a subagent_type parameter to select which agent type to use.
|
|
1231
1591
|
|
|
1232
|
-
## Usage notes:
|
|
1233
|
-
1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
|
|
1234
|
-
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.
|
|
1235
|
-
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.
|
|
1236
|
-
4. The agent's outputs should generally be trusted
|
|
1237
|
-
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
|
|
1238
|
-
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.
|
|
1239
|
-
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.
|
|
1592
|
+
## Usage notes:
|
|
1593
|
+
1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
|
|
1594
|
+
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.
|
|
1595
|
+
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.
|
|
1596
|
+
4. The agent's outputs should generally be trusted
|
|
1597
|
+
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
|
|
1598
|
+
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.
|
|
1599
|
+
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.
|
|
1240
1600
|
|
|
1241
|
-
### Example usage of the general-purpose agent:
|
|
1601
|
+
### Example usage of the general-purpose agent:
|
|
1242
1602
|
|
|
1243
|
-
<example_agent_descriptions>
|
|
1244
|
-
"general-purpose": use this agent for general purpose tasks, it has access to all tools as the main agent.
|
|
1245
|
-
</example_agent_descriptions>
|
|
1603
|
+
<example_agent_descriptions>
|
|
1604
|
+
"general-purpose": use this agent for general purpose tasks, it has access to all tools as the main agent.
|
|
1605
|
+
</example_agent_descriptions>
|
|
1246
1606
|
|
|
1247
|
-
<example>
|
|
1248
|
-
User: "I want to conduct research on the accomplishments of Lebron James, Michael Jordan, and Kobe Bryant, and then compare them."
|
|
1249
|
-
Assistant: *Uses the task tool in parallel to conduct isolated research on each of the three players*
|
|
1250
|
-
Assistant: *Synthesizes the results of the three isolated research tasks and responds to the User*
|
|
1251
|
-
<commentary>
|
|
1252
|
-
Research is a complex, multi-step task in it of itself.
|
|
1253
|
-
The research of each individual player is not dependent on the research of the other players.
|
|
1254
|
-
The assistant uses the task tool to break down the complex objective into three isolated tasks.
|
|
1255
|
-
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.
|
|
1256
|
-
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.
|
|
1257
|
-
</commentary>
|
|
1258
|
-
</example>
|
|
1607
|
+
<example>
|
|
1608
|
+
User: "I want to conduct research on the accomplishments of Lebron James, Michael Jordan, and Kobe Bryant, and then compare them."
|
|
1609
|
+
Assistant: *Uses the task tool in parallel to conduct isolated research on each of the three players*
|
|
1610
|
+
Assistant: *Synthesizes the results of the three isolated research tasks and responds to the User*
|
|
1611
|
+
<commentary>
|
|
1612
|
+
Research is a complex, multi-step task in it of itself.
|
|
1613
|
+
The research of each individual player is not dependent on the research of the other players.
|
|
1614
|
+
The assistant uses the task tool to break down the complex objective into three isolated tasks.
|
|
1615
|
+
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.
|
|
1616
|
+
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.
|
|
1617
|
+
</commentary>
|
|
1618
|
+
</example>
|
|
1259
1619
|
|
|
1260
|
-
<example>
|
|
1261
|
-
User: "Analyze a single large code repository for security vulnerabilities and generate a report."
|
|
1262
|
-
Assistant: *Launches a single \`task\` subagent for the repository analysis*
|
|
1263
|
-
Assistant: *Receives report and integrates results into final summary*
|
|
1264
|
-
<commentary>
|
|
1265
|
-
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.
|
|
1266
|
-
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.
|
|
1267
|
-
</commentary>
|
|
1268
|
-
</example>
|
|
1620
|
+
<example>
|
|
1621
|
+
User: "Analyze a single large code repository for security vulnerabilities and generate a report."
|
|
1622
|
+
Assistant: *Launches a single \`task\` subagent for the repository analysis*
|
|
1623
|
+
Assistant: *Receives report and integrates results into final summary*
|
|
1624
|
+
<commentary>
|
|
1625
|
+
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.
|
|
1626
|
+
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.
|
|
1627
|
+
</commentary>
|
|
1628
|
+
</example>
|
|
1269
1629
|
|
|
1270
|
-
<example>
|
|
1271
|
-
User: "Schedule two meetings for me and prepare agendas for each."
|
|
1272
|
-
Assistant: *Calls the task tool in parallel to launch two \`task\` subagents (one per meeting) to prepare agendas*
|
|
1273
|
-
Assistant: *Returns final schedules and agendas*
|
|
1274
|
-
<commentary>
|
|
1275
|
-
Tasks are simple individually, but subagents help silo agenda preparation.
|
|
1276
|
-
Each subagent only needs to worry about the agenda for one meeting.
|
|
1277
|
-
</commentary>
|
|
1278
|
-
</example>
|
|
1630
|
+
<example>
|
|
1631
|
+
User: "Schedule two meetings for me and prepare agendas for each."
|
|
1632
|
+
Assistant: *Calls the task tool in parallel to launch two \`task\` subagents (one per meeting) to prepare agendas*
|
|
1633
|
+
Assistant: *Returns final schedules and agendas*
|
|
1634
|
+
<commentary>
|
|
1635
|
+
Tasks are simple individually, but subagents help silo agenda preparation.
|
|
1636
|
+
Each subagent only needs to worry about the agenda for one meeting.
|
|
1637
|
+
</commentary>
|
|
1638
|
+
</example>
|
|
1279
1639
|
|
|
1280
|
-
<example>
|
|
1281
|
-
User: "I want to order a pizza from Dominos, order a burger from McDonald's, and order a salad from Subway."
|
|
1282
|
-
Assistant: *Calls tools directly in parallel to order a pizza from Dominos, a burger from McDonald's, and a salad from Subway*
|
|
1283
|
-
<commentary>
|
|
1284
|
-
The assistant did not use the task tool because the objective is super simple and clear and only requires a few trivial tool calls.
|
|
1285
|
-
It is better to just complete the task directly and NOT use the \`task\`tool.
|
|
1286
|
-
</commentary>
|
|
1287
|
-
</example>
|
|
1640
|
+
<example>
|
|
1641
|
+
User: "I want to order a pizza from Dominos, order a burger from McDonald's, and order a salad from Subway."
|
|
1642
|
+
Assistant: *Calls tools directly in parallel to order a pizza from Dominos, a burger from McDonald's, and a salad from Subway*
|
|
1643
|
+
<commentary>
|
|
1644
|
+
The assistant did not use the task tool because the objective is super simple and clear and only requires a few trivial tool calls.
|
|
1645
|
+
It is better to just complete the task directly and NOT use the \`task\`tool.
|
|
1646
|
+
</commentary>
|
|
1647
|
+
</example>
|
|
1288
1648
|
|
|
1289
|
-
### Example usage with custom agents:
|
|
1649
|
+
### Example usage with custom agents:
|
|
1290
1650
|
|
|
1291
|
-
<example_agent_descriptions>
|
|
1292
|
-
"content-reviewer": use this agent after you are done creating significant content or documents
|
|
1293
|
-
"greeting-responder": use this agent when to respond to user greetings with a friendly joke
|
|
1294
|
-
"research-analyst": use this agent to conduct thorough research on complex topics
|
|
1295
|
-
</example_agent_description>
|
|
1651
|
+
<example_agent_descriptions>
|
|
1652
|
+
"content-reviewer": use this agent after you are done creating significant content or documents
|
|
1653
|
+
"greeting-responder": use this agent when to respond to user greetings with a friendly joke
|
|
1654
|
+
"research-analyst": use this agent to conduct thorough research on complex topics
|
|
1655
|
+
</example_agent_description>
|
|
1296
1656
|
|
|
1297
|
-
<example>
|
|
1298
|
-
user: "Please write a function that checks if a number is prime"
|
|
1299
|
-
assistant: Sure let me write a function that checks if a number is prime
|
|
1300
|
-
assistant: First let me use the Write tool to write a function that checks if a number is prime
|
|
1301
|
-
assistant: I'm going to use the Write tool to write the following code:
|
|
1302
|
-
<code>
|
|
1303
|
-
function isPrime(n) {
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
}
|
|
1310
|
-
</code>
|
|
1311
|
-
<commentary>
|
|
1312
|
-
Since significant content was created and the task was completed, now use the content-reviewer agent to review the work
|
|
1313
|
-
</commentary>
|
|
1314
|
-
assistant: Now let me use the content-reviewer agent to review the code
|
|
1315
|
-
assistant: Uses the Task tool to launch with the content-reviewer agent
|
|
1316
|
-
</example>
|
|
1657
|
+
<example>
|
|
1658
|
+
user: "Please write a function that checks if a number is prime"
|
|
1659
|
+
assistant: Sure let me write a function that checks if a number is prime
|
|
1660
|
+
assistant: First let me use the Write tool to write a function that checks if a number is prime
|
|
1661
|
+
assistant: I'm going to use the Write tool to write the following code:
|
|
1662
|
+
<code>
|
|
1663
|
+
function isPrime(n) {{
|
|
1664
|
+
if (n <= 1) return false
|
|
1665
|
+
for (let i = 2; i * i <= n; i++) {{
|
|
1666
|
+
if (n % i === 0) return false
|
|
1667
|
+
}}
|
|
1668
|
+
return true
|
|
1669
|
+
}}
|
|
1670
|
+
</code>
|
|
1671
|
+
<commentary>
|
|
1672
|
+
Since significant content was created and the task was completed, now use the content-reviewer agent to review the work
|
|
1673
|
+
</commentary>
|
|
1674
|
+
assistant: Now let me use the content-reviewer agent to review the code
|
|
1675
|
+
assistant: Uses the Task tool to launch with the content-reviewer agent
|
|
1676
|
+
</example>
|
|
1317
1677
|
|
|
1318
|
-
<example>
|
|
1319
|
-
user: "Can you help me research the environmental impact of different renewable energy sources and create a comprehensive report?"
|
|
1320
|
-
<commentary>
|
|
1321
|
-
This is a complex research task that would benefit from using the research-analyst agent to conduct thorough analysis
|
|
1322
|
-
</commentary>
|
|
1323
|
-
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.
|
|
1324
|
-
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
|
|
1325
|
-
</example>
|
|
1678
|
+
<example>
|
|
1679
|
+
user: "Can you help me research the environmental impact of different renewable energy sources and create a comprehensive report?"
|
|
1680
|
+
<commentary>
|
|
1681
|
+
This is a complex research task that would benefit from using the research-analyst agent to conduct thorough analysis
|
|
1682
|
+
</commentary>
|
|
1683
|
+
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.
|
|
1684
|
+
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
|
|
1685
|
+
</example>
|
|
1326
1686
|
|
|
1327
|
-
<example>
|
|
1328
|
-
user: "Hello"
|
|
1329
|
-
<commentary>
|
|
1330
|
-
Since the user is greeting, use the greeting-responder agent to respond with a friendly joke
|
|
1331
|
-
</commentary>
|
|
1332
|
-
assistant: "I'm going to use the Task tool to launch with the greeting-responder agent"
|
|
1333
|
-
</example>
|
|
1334
|
-
|
|
1687
|
+
<example>
|
|
1688
|
+
user: "Hello"
|
|
1689
|
+
<commentary>
|
|
1690
|
+
Since the user is greeting, use the greeting-responder agent to respond with a friendly joke
|
|
1691
|
+
</commentary>
|
|
1692
|
+
assistant: "I'm going to use the Task tool to launch with the greeting-responder agent"
|
|
1693
|
+
</example>
|
|
1694
|
+
`;
|
|
1335
1695
|
}
|
|
1336
1696
|
/**
|
|
1337
1697
|
* System prompt section that explains how to use the task tool for spawning subagents.
|
|
@@ -1346,33 +1706,35 @@ assistant: "I'm going to use the Task tool to launch with the greeting-responder
|
|
|
1346
1706
|
* You can provide a custom `systemPrompt` to `createSubAgentMiddleware` to override
|
|
1347
1707
|
* or extend this default.
|
|
1348
1708
|
*/
|
|
1349
|
-
const TASK_SYSTEM_PROMPT =
|
|
1709
|
+
const TASK_SYSTEM_PROMPT = context`
|
|
1710
|
+
## \`task\` (subagent spawner)
|
|
1350
1711
|
|
|
1351
|
-
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.
|
|
1712
|
+
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.
|
|
1352
1713
|
|
|
1353
|
-
When to use the task tool:
|
|
1354
|
-
- When a task is complex and multi-step, and can be fully delegated in isolation
|
|
1355
|
-
- When a task is independent of other tasks and can run in parallel
|
|
1356
|
-
- When a task requires focused reasoning or heavy token/context usage that would bloat the orchestrator thread
|
|
1357
|
-
- When sandboxing improves reliability (e.g. code execution, structured searches, data formatting)
|
|
1358
|
-
- 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.)
|
|
1714
|
+
When to use the task tool:
|
|
1715
|
+
- When a task is complex and multi-step, and can be fully delegated in isolation
|
|
1716
|
+
- When a task is independent of other tasks and can run in parallel
|
|
1717
|
+
- When a task requires focused reasoning or heavy token/context usage that would bloat the orchestrator thread
|
|
1718
|
+
- When sandboxing improves reliability (e.g. code execution, structured searches, data formatting)
|
|
1719
|
+
- 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.)
|
|
1359
1720
|
|
|
1360
|
-
Subagent lifecycle:
|
|
1361
|
-
1. **Spawn** → Provide clear role, instructions, and expected output
|
|
1362
|
-
2. **Run** → The subagent completes the task autonomously
|
|
1363
|
-
3. **Return** → The subagent provides a single structured result
|
|
1364
|
-
4. **Reconcile** → Incorporate or synthesize the result into the main thread
|
|
1721
|
+
Subagent lifecycle:
|
|
1722
|
+
1. **Spawn** → Provide clear role, instructions, and expected output
|
|
1723
|
+
2. **Run** → The subagent completes the task autonomously
|
|
1724
|
+
3. **Return** → The subagent provides a single structured result
|
|
1725
|
+
4. **Reconcile** → Incorporate or synthesize the result into the main thread
|
|
1365
1726
|
|
|
1366
|
-
When NOT to use the task tool:
|
|
1367
|
-
- If you need to see the intermediate reasoning or steps after the subagent has completed (the task tool hides them)
|
|
1368
|
-
- If the task is trivial (a few tool calls or simple lookup)
|
|
1369
|
-
- If delegating does not reduce token usage, complexity, or context switching
|
|
1370
|
-
- If splitting would add latency without benefit
|
|
1727
|
+
When NOT to use the task tool:
|
|
1728
|
+
- If you need to see the intermediate reasoning or steps after the subagent has completed (the task tool hides them)
|
|
1729
|
+
- If the task is trivial (a few tool calls or simple lookup)
|
|
1730
|
+
- If delegating does not reduce token usage, complexity, or context switching
|
|
1731
|
+
- If splitting would add latency without benefit
|
|
1371
1732
|
|
|
1372
|
-
## Important Task Tool Usage Notes to Remember
|
|
1373
|
-
- 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.
|
|
1374
|
-
- Remember to use the \`task\` tool to silo independent tasks within a multi-part objective.
|
|
1375
|
-
- 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
|
|
1733
|
+
## Important Task Tool Usage Notes to Remember
|
|
1734
|
+
- 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.
|
|
1735
|
+
- Remember to use the \`task\` tool to silo independent tasks within a multi-part objective.
|
|
1736
|
+
- 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.
|
|
1737
|
+
`;
|
|
1376
1738
|
/**
|
|
1377
1739
|
* Base specification for the general-purpose subagent.
|
|
1378
1740
|
*
|
|
@@ -1781,65 +2143,67 @@ const MemoryStateSchema = new StateSchema({
|
|
|
1781
2143
|
* Default system prompt template for memory.
|
|
1782
2144
|
* Ported from Python's comprehensive memory guidelines.
|
|
1783
2145
|
*/
|
|
1784
|
-
const MEMORY_SYSTEM_PROMPT =
|
|
1785
|
-
|
|
1786
|
-
|
|
2146
|
+
const MEMORY_SYSTEM_PROMPT = context`
|
|
2147
|
+
<agent_memory>
|
|
2148
|
+
{memory_contents}
|
|
2149
|
+
</agent_memory>
|
|
1787
2150
|
|
|
1788
|
-
<memory_guidelines>
|
|
1789
|
-
|
|
2151
|
+
<memory_guidelines>
|
|
2152
|
+
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.
|
|
1790
2153
|
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
2154
|
+
**Learning from feedback:**
|
|
2155
|
+
- 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.
|
|
2156
|
+
- 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.
|
|
2157
|
+
- When user says something is better/worse, capture WHY and encode it as a pattern.
|
|
2158
|
+
- Each correction is a chance to improve permanently - don't just fix the immediate issue, update your instructions.
|
|
2159
|
+
- 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.
|
|
2160
|
+
- Look for the underlying principle behind corrections, not just the specific mistake.
|
|
2161
|
+
- 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.
|
|
1799
2162
|
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
2163
|
+
**Asking for information:**
|
|
2164
|
+
- 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.
|
|
2165
|
+
- It is preferred for you to ask for information, don't assume anything that you do not know!
|
|
2166
|
+
- When the user provides information that is useful for future use, you should update your memories immediately.
|
|
1804
2167
|
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
2168
|
+
**When to update memories:**
|
|
2169
|
+
- When the user explicitly asks you to remember something (e.g., "remember my email", "save this preference")
|
|
2170
|
+
- When the user describes your role or how you should behave (e.g., "you are a web researcher", "always do X")
|
|
2171
|
+
- When the user gives feedback on your work - capture what was wrong and how to improve
|
|
2172
|
+
- When the user provides information required for tool use (e.g., slack channel ID, email addresses)
|
|
2173
|
+
- When the user provides context useful for future tasks, such as how to use tools, or which actions to take in a particular situation
|
|
2174
|
+
- When you discover new patterns or preferences (coding styles, conventions, workflows)
|
|
1812
2175
|
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
2176
|
+
**When to NOT update memories:**
|
|
2177
|
+
- When the information is temporary or transient (e.g., "I'm running late", "I'm on my phone right now")
|
|
2178
|
+
- When the information is a one-time task request (e.g., "Find me a recipe", "What's 25 * 4?")
|
|
2179
|
+
- When the information is a simple question that doesn't reveal lasting preferences (e.g., "What day is it?", "Can you explain X?")
|
|
2180
|
+
- When the information is an acknowledgment or small talk (e.g., "Sounds good!", "Hello", "Thanks for that")
|
|
2181
|
+
- When the information is stale or irrelevant in future conversations
|
|
2182
|
+
- Never store API keys, access tokens, passwords, or any other credentials in any file, memory, or system prompt.
|
|
2183
|
+
- If the user asks where to put API keys or provides an API key, do NOT echo or save it.
|
|
1821
2184
|
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
2185
|
+
**Examples:**
|
|
2186
|
+
Example 1 (remembering user information):
|
|
2187
|
+
User: Can you connect to my google account?
|
|
2188
|
+
Agent: Sure, I'll connect to your google account, what's your google account email?
|
|
2189
|
+
User: john@example.com
|
|
2190
|
+
Agent: Let me save this to my memory.
|
|
2191
|
+
Tool Call: edit_file(...) -> remembers that the user's google account email is john@example.com
|
|
1829
2192
|
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
2193
|
+
Example 2 (remembering implicit user preferences):
|
|
2194
|
+
User: Can you write me an example for creating a deep agent in LangChain?
|
|
2195
|
+
Agent: Sure, I'll write you an example for creating a deep agent in LangChain <example code in Python>
|
|
2196
|
+
User: Can you do this in JavaScript
|
|
2197
|
+
Agent: Let me save this to my memory.
|
|
2198
|
+
Tool Call: edit_file(...) -> remembers that the user prefers to get LangChain code examples in JavaScript
|
|
2199
|
+
Agent: Sure, here is the JavaScript example<example code in JavaScript>
|
|
1837
2200
|
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
</memory_guidelines
|
|
2201
|
+
Example 3 (do not remember transient information):
|
|
2202
|
+
User: I'm going to play basketball tonight so I will be offline for a few hours.
|
|
2203
|
+
Agent: Okay I'll add a block to your calendar.
|
|
2204
|
+
Tool Call: create_calendar_event(...) -> just calls a tool, does not commit anything to memory, as it is transient information
|
|
2205
|
+
</memory_guidelines>
|
|
2206
|
+
`;
|
|
1843
2207
|
/**
|
|
1844
2208
|
* Format loaded memory contents for injection into prompt.
|
|
1845
2209
|
* Pairs memory locations with their contents for clarity.
|
|
@@ -1859,12 +2223,14 @@ function formatMemoryContents(contents, sources) {
|
|
|
1859
2223
|
* @returns File content if found, null otherwise.
|
|
1860
2224
|
*/
|
|
1861
2225
|
async function loadMemoryFromBackend(backend, path) {
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
return
|
|
1866
|
-
|
|
1867
|
-
|
|
2226
|
+
const adaptedBackend = adaptBackendProtocol(backend);
|
|
2227
|
+
if (!adaptedBackend.downloadFiles) {
|
|
2228
|
+
const content = await adaptedBackend.read(path);
|
|
2229
|
+
if (content.error) return null;
|
|
2230
|
+
if (typeof content.content !== "string") return null;
|
|
2231
|
+
return content.content;
|
|
2232
|
+
}
|
|
2233
|
+
const results = await adaptedBackend.downloadFiles([path]);
|
|
1868
2234
|
if (results.length !== 1) throw new Error(`Expected 1 response for path ${path}, got ${results.length}`);
|
|
1869
2235
|
const response = results[0];
|
|
1870
2236
|
if (response.error != null) {
|
|
@@ -2035,7 +2401,7 @@ Skills follow a **progressive disclosure** pattern - you know they exist (name +
|
|
|
2035
2401
|
1. **Recognize when a skill applies**: Check if the user's task matches any skill's description
|
|
2036
2402
|
2. **Read the skill's full instructions**: The skill list above shows the exact path to use with read_file
|
|
2037
2403
|
3. **Follow the skill's instructions**: SKILL.md contains step-by-step workflows, best practices, and examples
|
|
2038
|
-
4. **Access supporting files**: Skills may include
|
|
2404
|
+
4. **Access supporting files**: Skills may include scripts, configs, or reference docs - use absolute paths
|
|
2039
2405
|
|
|
2040
2406
|
**When to Use Skills:**
|
|
2041
2407
|
- When the user's request matches a skill's domain (e.g., "research X" → web-research skill)
|
|
@@ -2047,7 +2413,7 @@ Skills follow a **progressive disclosure** pattern - you know they exist (name +
|
|
|
2047
2413
|
- The skill list above shows the full path for each skill's SKILL.md file
|
|
2048
2414
|
|
|
2049
2415
|
**Executing Skill Scripts:**
|
|
2050
|
-
Skills may contain
|
|
2416
|
+
Skills may contain scripts or other executable files. Always use absolute paths from the skill list.
|
|
2051
2417
|
|
|
2052
2418
|
**Example Workflow:**
|
|
2053
2419
|
|
|
@@ -2216,12 +2582,15 @@ function parseSkillMetadataFromContent(content, skillPath, directoryName) {
|
|
|
2216
2582
|
* List all skills from a backend source.
|
|
2217
2583
|
*/
|
|
2218
2584
|
async function listSkillsFromBackend(backend, sourcePath) {
|
|
2585
|
+
const adaptedBackend = adaptBackendProtocol(backend);
|
|
2219
2586
|
const skills = [];
|
|
2220
2587
|
const pathSep = sourcePath.includes("\\") ? "\\" : "/";
|
|
2221
2588
|
const normalizedPath = sourcePath.endsWith("/") || sourcePath.endsWith("\\") ? sourcePath : `${sourcePath}${pathSep}`;
|
|
2222
2589
|
let fileInfos;
|
|
2223
2590
|
try {
|
|
2224
|
-
|
|
2591
|
+
const lsResult = await adaptedBackend.ls(normalizedPath);
|
|
2592
|
+
if (lsResult.error || !lsResult.files) return [];
|
|
2593
|
+
fileInfos = lsResult.files;
|
|
2225
2594
|
} catch {
|
|
2226
2595
|
return [];
|
|
2227
2596
|
}
|
|
@@ -2233,16 +2602,17 @@ async function listSkillsFromBackend(backend, sourcePath) {
|
|
|
2233
2602
|
if (entry.type !== "directory") continue;
|
|
2234
2603
|
const skillMdPath = `${normalizedPath}${entry.name}${pathSep}SKILL.md`;
|
|
2235
2604
|
let content;
|
|
2236
|
-
if (
|
|
2237
|
-
const results = await
|
|
2605
|
+
if (adaptedBackend.downloadFiles) {
|
|
2606
|
+
const results = await adaptedBackend.downloadFiles([skillMdPath]);
|
|
2238
2607
|
if (results.length !== 1) continue;
|
|
2239
2608
|
const response = results[0];
|
|
2240
2609
|
if (response.error != null || response.content == null) continue;
|
|
2241
2610
|
content = new TextDecoder().decode(response.content);
|
|
2242
2611
|
} else {
|
|
2243
|
-
const readResult = await
|
|
2244
|
-
if (readResult.
|
|
2245
|
-
content
|
|
2612
|
+
const readResult = await adaptedBackend.read(skillMdPath);
|
|
2613
|
+
if (readResult.error) continue;
|
|
2614
|
+
if (typeof readResult.content !== "string") continue;
|
|
2615
|
+
content = readResult.content;
|
|
2246
2616
|
}
|
|
2247
2617
|
const metadata = parseSkillMetadataFromContent(content, skillMdPath, entry.name);
|
|
2248
2618
|
if (metadata) skills.push(metadata);
|
|
@@ -2265,74 +2635,302 @@ function formatSkillsLocations(sources) {
|
|
|
2265
2635
|
return lines.join("\n");
|
|
2266
2636
|
}
|
|
2267
2637
|
/**
|
|
2268
|
-
* Format skills metadata for display in system prompt.
|
|
2269
|
-
* Shows allowed tools for each skill if specified.
|
|
2638
|
+
* Format skills metadata for display in system prompt.
|
|
2639
|
+
* Shows allowed tools for each skill if specified.
|
|
2640
|
+
*/
|
|
2641
|
+
function formatSkillsList(skills, sources) {
|
|
2642
|
+
if (skills.length === 0) return `(No skills available yet. You can create skills in ${sources.map((s) => `\`${s}\``).join(" or ")})`;
|
|
2643
|
+
const lines = [];
|
|
2644
|
+
for (const skill of skills) {
|
|
2645
|
+
const annotations = formatSkillAnnotations(skill);
|
|
2646
|
+
let descLine = `- **${skill.name}**: ${skill.description}`;
|
|
2647
|
+
if (annotations) descLine += ` (${annotations})`;
|
|
2648
|
+
lines.push(descLine);
|
|
2649
|
+
if (skill.allowedTools && skill.allowedTools.length > 0) lines.push(` → Allowed tools: ${skill.allowedTools.join(", ")}`);
|
|
2650
|
+
lines.push(` → Read \`${skill.path}\` for full instructions`);
|
|
2651
|
+
}
|
|
2652
|
+
return lines.join("\n");
|
|
2653
|
+
}
|
|
2654
|
+
/**
|
|
2655
|
+
* Create backend-agnostic middleware for loading and exposing agent skills.
|
|
2656
|
+
*
|
|
2657
|
+
* This middleware loads skills from configurable backend sources and injects
|
|
2658
|
+
* skill metadata into the system prompt. It implements the progressive disclosure
|
|
2659
|
+
* pattern: skill names and descriptions are shown in the prompt, but the agent
|
|
2660
|
+
* reads full SKILL.md content only when needed.
|
|
2661
|
+
*
|
|
2662
|
+
* @param options - Configuration options
|
|
2663
|
+
* @returns AgentMiddleware for skills loading and injection
|
|
2664
|
+
*
|
|
2665
|
+
* @example
|
|
2666
|
+
* ```typescript
|
|
2667
|
+
* const middleware = createSkillsMiddleware({
|
|
2668
|
+
* backend: new FilesystemBackend({ rootDir: "/" }),
|
|
2669
|
+
* sources: ["/skills/user/", "/skills/project/"],
|
|
2670
|
+
* });
|
|
2671
|
+
* ```
|
|
2672
|
+
*/
|
|
2673
|
+
function createSkillsMiddleware(options) {
|
|
2674
|
+
const { backend, sources } = options;
|
|
2675
|
+
let loadedSkills = [];
|
|
2676
|
+
return createMiddleware({
|
|
2677
|
+
name: "SkillsMiddleware",
|
|
2678
|
+
stateSchema: SkillsStateSchema,
|
|
2679
|
+
async beforeAgent(state) {
|
|
2680
|
+
if (loadedSkills.length > 0) return;
|
|
2681
|
+
if ("skillsMetadata" in state && Array.isArray(state.skillsMetadata) && state.skillsMetadata.length > 0) {
|
|
2682
|
+
loadedSkills = state.skillsMetadata;
|
|
2683
|
+
return;
|
|
2684
|
+
}
|
|
2685
|
+
const resolvedBackend = await resolveBackend(backend, { state });
|
|
2686
|
+
const allSkills = /* @__PURE__ */ new Map();
|
|
2687
|
+
for (const sourcePath of sources) try {
|
|
2688
|
+
const skills = await listSkillsFromBackend(resolvedBackend, sourcePath);
|
|
2689
|
+
for (const skill of skills) allSkills.set(skill.name, skill);
|
|
2690
|
+
} catch (error) {
|
|
2691
|
+
console.debug(`[BackendSkillsMiddleware] Failed to load skills from ${sourcePath}:`, error);
|
|
2692
|
+
}
|
|
2693
|
+
loadedSkills = Array.from(allSkills.values());
|
|
2694
|
+
return { skillsMetadata: loadedSkills };
|
|
2695
|
+
},
|
|
2696
|
+
wrapModelCall(request, handler) {
|
|
2697
|
+
const skillsMetadata = loadedSkills.length > 0 ? loadedSkills : request.state?.skillsMetadata || [];
|
|
2698
|
+
const skillsLocations = formatSkillsLocations(sources);
|
|
2699
|
+
const skillsList = formatSkillsList(skillsMetadata, sources);
|
|
2700
|
+
const skillsSection = SKILLS_SYSTEM_PROMPT.replace("{skills_locations}", skillsLocations).replace("{skills_list}", skillsList);
|
|
2701
|
+
const newSystemMessage = request.systemMessage.concat(skillsSection);
|
|
2702
|
+
return handler({
|
|
2703
|
+
...request,
|
|
2704
|
+
systemMessage: newSystemMessage
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2707
|
+
});
|
|
2708
|
+
}
|
|
2709
|
+
//#endregion
|
|
2710
|
+
//#region src/middleware/completion_callback.ts
|
|
2711
|
+
/**
|
|
2712
|
+
* Callback middleware for async subagents.
|
|
2713
|
+
*
|
|
2714
|
+
* @experimental - this middleware is experimental and may change in future releases.
|
|
2715
|
+
*
|
|
2716
|
+
* This middleware sends a notification to a callback thread when a subagent
|
|
2717
|
+
* completes successfully or raises an error. The callback agent can then
|
|
2718
|
+
* process that notification instead of relying only on polling via
|
|
2719
|
+
* `check_async_task`.
|
|
2720
|
+
*
|
|
2721
|
+
* ## Architecture
|
|
2722
|
+
*
|
|
2723
|
+
* A parent agent launches a subagent with `start_async_task` and can later
|
|
2724
|
+
* inspect task state with `check_async_task`. This middleware adds an optional
|
|
2725
|
+
* completion signal by creating a run on the callback thread when the subagent
|
|
2726
|
+
* finishes.
|
|
2727
|
+
*
|
|
2728
|
+
* ```
|
|
2729
|
+
* Parent Subagent
|
|
2730
|
+
* | |
|
|
2731
|
+
* |--- start_async_task -----> |
|
|
2732
|
+
* |<-- task_id (immediately) - |
|
|
2733
|
+
* | | (working...)
|
|
2734
|
+
* | | (done!)
|
|
2735
|
+
* | |
|
|
2736
|
+
* |<-- runs.create( |
|
|
2737
|
+
* | callback_thread, |
|
|
2738
|
+
* | "completed: ...") |
|
|
2739
|
+
* | |
|
|
2740
|
+
* | (processes result) |
|
|
2741
|
+
* ```
|
|
2742
|
+
*
|
|
2743
|
+
* The middleware calls `runs.create()` on the callback thread. From the
|
|
2744
|
+
* callback agent's perspective, this appears as a new user message containing
|
|
2745
|
+
* structured output from the subagent.
|
|
2746
|
+
*
|
|
2747
|
+
* ## Callback context
|
|
2748
|
+
*
|
|
2749
|
+
* - `callbackGraphId` identifies the callback graph or assistant. It is
|
|
2750
|
+
* provided when the middleware is constructed.
|
|
2751
|
+
* - `url` and `headers` optionally configure a remote callback destination.
|
|
2752
|
+
* Omit `url` for same-deployment ASGI transport.
|
|
2753
|
+
* - `callback_thread_id` is stored in the subagent state by the parent's
|
|
2754
|
+
* `start_async_task` tool. Because it is stored in state rather than config,
|
|
2755
|
+
* it survives thread updates and interrupts.
|
|
2756
|
+
* - If `callback_thread_id` is not present in state, the middleware does
|
|
2757
|
+
* nothing.
|
|
2758
|
+
*
|
|
2759
|
+
* ## Usage
|
|
2760
|
+
*
|
|
2761
|
+
* ```typescript
|
|
2762
|
+
* import { createCompletionCallbackMiddleware } from "deepagents";
|
|
2763
|
+
*
|
|
2764
|
+
* // Same deployment (callback agent and subagent share a server):
|
|
2765
|
+
* const notifier = createCompletionCallbackMiddleware({
|
|
2766
|
+
* callbackGraphId: "supervisor",
|
|
2767
|
+
* });
|
|
2768
|
+
*
|
|
2769
|
+
* // Remote deployment (callback destination on a different server):
|
|
2770
|
+
* const notifier = createCompletionCallbackMiddleware({
|
|
2771
|
+
* callbackGraphId: "supervisor",
|
|
2772
|
+
* url: "https://my-deployment.langsmith.dev",
|
|
2773
|
+
* });
|
|
2774
|
+
*
|
|
2775
|
+
* const agent = createDeepAgent({
|
|
2776
|
+
* model,
|
|
2777
|
+
* middleware: [notifier],
|
|
2778
|
+
* });
|
|
2779
|
+
* ```
|
|
2780
|
+
*
|
|
2781
|
+
* The middleware reads `callbackThreadId` from the agent state at the end of
|
|
2782
|
+
* execution. This value is injected by the parent's `start_async_task` tool
|
|
2783
|
+
* when it creates the run.
|
|
2784
|
+
*
|
|
2785
|
+
* @module
|
|
2786
|
+
*/
|
|
2787
|
+
/** Maximum characters to include from the last message in notifications. */
|
|
2788
|
+
const MAX_MESSAGE_LENGTH = 500;
|
|
2789
|
+
/** Suffix appended when truncating long messages. */
|
|
2790
|
+
const TRUNCATION_SUFFIX = "... [full result truncated]";
|
|
2791
|
+
/** State key for the callback thread ID. */
|
|
2792
|
+
const CALLBACK_THREAD_ID_KEY = "callbackThreadId";
|
|
2793
|
+
/**
|
|
2794
|
+
* State extension for subagents that use completion callbacks.
|
|
2795
|
+
*
|
|
2796
|
+
* @experimental - this state schema is experimental and may change in future releases.
|
|
2797
|
+
*
|
|
2798
|
+
* `callbackThreadId` is written by the parent's `start_async_task` tool
|
|
2799
|
+
* and read by `CompletionCallbackMiddleware` when sending callback
|
|
2800
|
+
* notifications.
|
|
2801
|
+
*/
|
|
2802
|
+
const CompletionCallbackStateSchema = z$2.object({ [CALLBACK_THREAD_ID_KEY]: z$2.string().optional() });
|
|
2803
|
+
/**
|
|
2804
|
+
* Build headers for the callback LangGraph server.
|
|
2805
|
+
*
|
|
2806
|
+
* Ensures `x-auth-scheme: langsmith` is present unless explicitly overridden.
|
|
2807
|
+
*/
|
|
2808
|
+
function resolveHeaders(headers) {
|
|
2809
|
+
const resolved = { ...headers };
|
|
2810
|
+
if (!("x-auth-scheme" in resolved)) resolved["x-auth-scheme"] = "langsmith";
|
|
2811
|
+
return resolved;
|
|
2812
|
+
}
|
|
2813
|
+
/**
|
|
2814
|
+
* Send a notification run to the callback thread.
|
|
2815
|
+
*
|
|
2816
|
+
* @param callbackGraphId - The callback graph ID used as `assistant_id`
|
|
2817
|
+
* in the `runs.create` call.
|
|
2818
|
+
* @param callbackThreadId - The callback thread ID.
|
|
2819
|
+
* @param message - The message content to send.
|
|
2820
|
+
* @param options - Optional url and headers for the callback server.
|
|
2270
2821
|
*/
|
|
2271
|
-
function
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2822
|
+
async function notifyParent(callbackGraphId, callbackThreadId, message, options) {
|
|
2823
|
+
try {
|
|
2824
|
+
await new Client({
|
|
2825
|
+
apiUrl: options?.url ?? void 0,
|
|
2826
|
+
apiKey: null,
|
|
2827
|
+
defaultHeaders: resolveHeaders(options?.headers)
|
|
2828
|
+
}).runs.create(callbackThreadId, callbackGraphId, { input: { messages: [{
|
|
2829
|
+
role: "user",
|
|
2830
|
+
content: message
|
|
2831
|
+
}] } });
|
|
2832
|
+
} catch (e) {
|
|
2833
|
+
console.warn(`[CompletionCallbackMiddleware] Failed to notify callback thread ${callbackThreadId}:`, e);
|
|
2281
2834
|
}
|
|
2282
|
-
return lines.join("\n");
|
|
2283
2835
|
}
|
|
2284
2836
|
/**
|
|
2285
|
-
*
|
|
2837
|
+
* Extract a summary from the subagent's final message.
|
|
2286
2838
|
*
|
|
2287
|
-
*
|
|
2288
|
-
*
|
|
2289
|
-
* pattern: skill names and descriptions are shown in the prompt, but the agent
|
|
2290
|
-
* reads full SKILL.md content only when needed.
|
|
2839
|
+
* Returns at most 500 characters from the last message's content.
|
|
2840
|
+
* Throws if no messages exist or if the last message is not an AIMessage.
|
|
2291
2841
|
*
|
|
2292
|
-
* @param
|
|
2293
|
-
* @
|
|
2842
|
+
* @param state - The agent state dict.
|
|
2843
|
+
* @param taskId - Optional task ID to include in truncation hint.
|
|
2844
|
+
*/
|
|
2845
|
+
function extractLastMessage(state, taskId) {
|
|
2846
|
+
const messages = state.messages;
|
|
2847
|
+
if (!messages || messages.length === 0) throw new Error(`Expected at least one message in state ${JSON.stringify(state)}`);
|
|
2848
|
+
const last = messages[messages.length - 1];
|
|
2849
|
+
if (!AIMessage$1.isInstance(last)) throw new TypeError(`Expected an AIMessage, got ${typeof last === "object" && last !== null ? last.constructor?.name ?? typeof last : typeof last} instead`);
|
|
2850
|
+
let textContent = last.text;
|
|
2851
|
+
if (textContent.length > MAX_MESSAGE_LENGTH) {
|
|
2852
|
+
textContent = textContent.slice(0, MAX_MESSAGE_LENGTH) + TRUNCATION_SUFFIX;
|
|
2853
|
+
if (taskId) textContent += ` Result truncated. Use \`check_async_task(task_id='${taskId}')\` to retrieve the full result if needed.`;
|
|
2854
|
+
}
|
|
2855
|
+
return textContent;
|
|
2856
|
+
}
|
|
2857
|
+
/**
|
|
2858
|
+
* Create a completion callback middleware for async subagents.
|
|
2859
|
+
*
|
|
2860
|
+
* **Experimental** — this middleware is experimental and may change.
|
|
2861
|
+
*
|
|
2862
|
+
* This middleware is added to a subagent's middleware stack. On success or
|
|
2863
|
+
* model-call error, it sends a notification to the configured callback
|
|
2864
|
+
* thread by calling `runs.create()`.
|
|
2865
|
+
*
|
|
2866
|
+
* The callback destination is configured with `callbackGraphId` and
|
|
2867
|
+
* optional `url` and `headers`. The target thread is read from
|
|
2868
|
+
* `callbackThreadId` in the subagent state.
|
|
2869
|
+
*
|
|
2870
|
+
* If `callbackThreadId` is not present in state, the middleware does
|
|
2871
|
+
* nothing.
|
|
2872
|
+
*
|
|
2873
|
+
* @param options - Configuration options.
|
|
2874
|
+
* @returns An `AgentMiddleware` instance.
|
|
2294
2875
|
*
|
|
2295
2876
|
* @example
|
|
2296
2877
|
* ```typescript
|
|
2297
|
-
*
|
|
2298
|
-
*
|
|
2299
|
-
*
|
|
2878
|
+
* import { createCompletionCallbackMiddleware } from "deepagents";
|
|
2879
|
+
*
|
|
2880
|
+
* const notifier = createCompletionCallbackMiddleware({
|
|
2881
|
+
* callbackGraphId: "supervisor",
|
|
2882
|
+
* });
|
|
2883
|
+
*
|
|
2884
|
+
* const agent = createDeepAgent({
|
|
2885
|
+
* model: "claude-sonnet-4-5-20250929",
|
|
2886
|
+
* middleware: [notifier],
|
|
2300
2887
|
* });
|
|
2301
2888
|
* ```
|
|
2302
2889
|
*/
|
|
2303
|
-
function
|
|
2304
|
-
const {
|
|
2305
|
-
|
|
2890
|
+
function createCompletionCallbackMiddleware(options) {
|
|
2891
|
+
const { callbackGraphId, url, headers } = options;
|
|
2892
|
+
/**
|
|
2893
|
+
* Send a notification to the callback destination.
|
|
2894
|
+
*/
|
|
2895
|
+
async function sendNotification(callbackThreadId, message) {
|
|
2896
|
+
await notifyParent(callbackGraphId, callbackThreadId, message, {
|
|
2897
|
+
url,
|
|
2898
|
+
headers
|
|
2899
|
+
});
|
|
2900
|
+
}
|
|
2901
|
+
/**
|
|
2902
|
+
* Read the subagent's own thread_id from runtime config.
|
|
2903
|
+
*
|
|
2904
|
+
* The subagent's `thread_id` is the same as the `task_id` from the
|
|
2905
|
+
* parent's perspective.
|
|
2906
|
+
*/
|
|
2907
|
+
function getTaskId(runtime) {
|
|
2908
|
+
return runtime?.configurable?.thread_id;
|
|
2909
|
+
}
|
|
2910
|
+
/**
|
|
2911
|
+
* Build a notification string with task_id prefix.
|
|
2912
|
+
*/
|
|
2913
|
+
function formatNotification(body, runtime) {
|
|
2914
|
+
const taskId = getTaskId(runtime);
|
|
2915
|
+
return `${taskId ? `[task_id=${taskId}]` : ""}${body}`;
|
|
2916
|
+
}
|
|
2306
2917
|
return createMiddleware({
|
|
2307
|
-
name: "
|
|
2308
|
-
stateSchema:
|
|
2309
|
-
async
|
|
2310
|
-
|
|
2311
|
-
if (
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
}
|
|
2315
|
-
const resolvedBackend = await resolveBackend(backend, { state });
|
|
2316
|
-
const allSkills = /* @__PURE__ */ new Map();
|
|
2317
|
-
for (const sourcePath of sources) try {
|
|
2318
|
-
const skills = await listSkillsFromBackend(resolvedBackend, sourcePath);
|
|
2319
|
-
for (const skill of skills) allSkills.set(skill.name, skill);
|
|
2320
|
-
} catch (error) {
|
|
2321
|
-
console.debug(`[BackendSkillsMiddleware] Failed to load skills from ${sourcePath}:`, error);
|
|
2322
|
-
}
|
|
2323
|
-
loadedSkills = Array.from(allSkills.values());
|
|
2324
|
-
return { skillsMetadata: loadedSkills };
|
|
2918
|
+
name: "CompletionCallbackMiddleware",
|
|
2919
|
+
stateSchema: CompletionCallbackStateSchema,
|
|
2920
|
+
async afterAgent(state, runtime) {
|
|
2921
|
+
const callbackThreadId = state[CALLBACK_THREAD_ID_KEY];
|
|
2922
|
+
if (callbackThreadId == null) throw new Error(`Missing required state key '${CALLBACK_THREAD_ID_KEY}'`);
|
|
2923
|
+
const taskId = getTaskId(runtime);
|
|
2924
|
+
await sendNotification(callbackThreadId, formatNotification(`Completed. Result: ${extractLastMessage(state, typeof taskId === "string" ? taskId : void 0)}`, runtime));
|
|
2325
2925
|
},
|
|
2326
|
-
wrapModelCall(request, handler) {
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
systemMessage: newSystemMessage
|
|
2335
|
-
});
|
|
2926
|
+
async wrapModelCall(request, handler) {
|
|
2927
|
+
try {
|
|
2928
|
+
return await handler(request);
|
|
2929
|
+
} catch (e) {
|
|
2930
|
+
const callbackThreadId = request.state[CALLBACK_THREAD_ID_KEY];
|
|
2931
|
+
if (typeof callbackThreadId === "string") await sendNotification(callbackThreadId, formatNotification("The agent encountered an error while calling the model.", request.runtime));
|
|
2932
|
+
throw e;
|
|
2933
|
+
}
|
|
2336
2934
|
}
|
|
2337
2935
|
});
|
|
2338
2936
|
}
|
|
@@ -2860,15 +3458,17 @@ function createSummarizationMiddleware(options) {
|
|
|
2860
3458
|
*/
|
|
2861
3459
|
function buildSummaryMessage(summary, filePath) {
|
|
2862
3460
|
let content;
|
|
2863
|
-
if (filePath) content = `
|
|
3461
|
+
if (filePath) content = context`
|
|
3462
|
+
You are in the middle of a conversation that has been summarized.
|
|
2864
3463
|
|
|
2865
|
-
The full conversation history has been saved to ${filePath} should you need to refer back to it for details.
|
|
3464
|
+
The full conversation history has been saved to ${filePath} should you need to refer back to it for details.
|
|
2866
3465
|
|
|
2867
|
-
A condensed summary follows:
|
|
3466
|
+
A condensed summary follows:
|
|
2868
3467
|
|
|
2869
|
-
<summary>
|
|
2870
|
-
${summary}
|
|
2871
|
-
</summary
|
|
3468
|
+
<summary>
|
|
3469
|
+
${summary}
|
|
3470
|
+
</summary>
|
|
3471
|
+
`;
|
|
2872
3472
|
else content = `Here is a summary of the conversation to date:\n\n${summary}`;
|
|
2873
3473
|
return new HumanMessage({
|
|
2874
3474
|
content,
|
|
@@ -2934,92 +3534,661 @@ ${summary}
|
|
|
2934
3534
|
if (!isContextOverflow(err)) throw err;
|
|
2935
3535
|
}
|
|
2936
3536
|
}
|
|
2937
|
-
const previousEvent = request.state._summarizationEvent;
|
|
2938
|
-
const previousCutoffIndex = previousEvent != null ? previousEvent.cutoffIndex : void 0;
|
|
2939
|
-
const { summaryMessage, filePath, stateCutoffIndex } = await summarizeMessages(messagesToSummarize, resolvedModel, request.state, previousCutoffIndex, cutoffIndex);
|
|
2940
|
-
let modifiedMessages = [summaryMessage, ...preservedMessages];
|
|
2941
|
-
const modifiedTokens = countTotalTokens(modifiedMessages, request.systemMessage, request.tools);
|
|
2942
|
-
let finalStateCutoffIndex = stateCutoffIndex;
|
|
2943
|
-
let finalSummaryMessage = summaryMessage;
|
|
2944
|
-
let finalFilePath = filePath;
|
|
3537
|
+
const previousEvent = request.state._summarizationEvent;
|
|
3538
|
+
const previousCutoffIndex = previousEvent != null ? previousEvent.cutoffIndex : void 0;
|
|
3539
|
+
const { summaryMessage, filePath, stateCutoffIndex } = await summarizeMessages(messagesToSummarize, resolvedModel, request.state, previousCutoffIndex, cutoffIndex);
|
|
3540
|
+
let modifiedMessages = [summaryMessage, ...preservedMessages];
|
|
3541
|
+
const modifiedTokens = countTotalTokens(modifiedMessages, request.systemMessage, request.tools);
|
|
3542
|
+
let finalStateCutoffIndex = stateCutoffIndex;
|
|
3543
|
+
let finalSummaryMessage = summaryMessage;
|
|
3544
|
+
let finalFilePath = filePath;
|
|
3545
|
+
try {
|
|
3546
|
+
await handler({
|
|
3547
|
+
...request,
|
|
3548
|
+
messages: modifiedMessages
|
|
3549
|
+
});
|
|
3550
|
+
} catch (err) {
|
|
3551
|
+
if (!isContextOverflow(err)) throw err;
|
|
3552
|
+
if (maxInputTokens && modifiedTokens > 0) {
|
|
3553
|
+
const observedRatio = maxInputTokens / modifiedTokens;
|
|
3554
|
+
if (observedRatio > tokenEstimationMultiplier) tokenEstimationMultiplier = observedRatio * 1.1;
|
|
3555
|
+
}
|
|
3556
|
+
const reSumResult = await summarizeMessages([...messagesToSummarize, ...preservedMessages], resolvedModel, request.state, previousCutoffIndex, truncatedMessages.length);
|
|
3557
|
+
finalSummaryMessage = reSumResult.summaryMessage;
|
|
3558
|
+
finalFilePath = reSumResult.filePath;
|
|
3559
|
+
finalStateCutoffIndex = reSumResult.stateCutoffIndex;
|
|
3560
|
+
modifiedMessages = [reSumResult.summaryMessage];
|
|
3561
|
+
await handler({
|
|
3562
|
+
...request,
|
|
3563
|
+
messages: modifiedMessages
|
|
3564
|
+
});
|
|
3565
|
+
}
|
|
3566
|
+
return new Command({ update: {
|
|
3567
|
+
_summarizationEvent: {
|
|
3568
|
+
cutoffIndex: finalStateCutoffIndex,
|
|
3569
|
+
summaryMessage: finalSummaryMessage,
|
|
3570
|
+
filePath: finalFilePath
|
|
3571
|
+
},
|
|
3572
|
+
_summarizationSessionId: getSessionId(request.state)
|
|
3573
|
+
} });
|
|
3574
|
+
}
|
|
3575
|
+
return createMiddleware({
|
|
3576
|
+
name: "SummarizationMiddleware",
|
|
3577
|
+
stateSchema: SummarizationStateSchema,
|
|
3578
|
+
async wrapModelCall(request, handler) {
|
|
3579
|
+
const effectiveMessages = getEffectiveMessages(request.messages ?? [], request.state);
|
|
3580
|
+
if (effectiveMessages.length === 0) return handler(request);
|
|
3581
|
+
/**
|
|
3582
|
+
* Resolve the chat model and get max input tokens from its profile.
|
|
3583
|
+
*/
|
|
3584
|
+
const resolvedModel = await getChatModel();
|
|
3585
|
+
const maxInputTokens = getMaxInputTokens(resolvedModel);
|
|
3586
|
+
applyModelDefaults(resolvedModel);
|
|
3587
|
+
/**
|
|
3588
|
+
* Step 1: Truncate args if configured
|
|
3589
|
+
*/
|
|
3590
|
+
const { messages: truncatedMessages } = truncateArgs(effectiveMessages, maxInputTokens, request.systemMessage, request.tools);
|
|
3591
|
+
/**
|
|
3592
|
+
* Step 2: Check if summarization should happen.
|
|
3593
|
+
* Count tokens including system message and tools to match what's
|
|
3594
|
+
* actually sent to the model (matching Python implementation).
|
|
3595
|
+
*/
|
|
3596
|
+
const totalTokens = countTotalTokens(truncatedMessages, request.systemMessage, request.tools);
|
|
3597
|
+
/**
|
|
3598
|
+
* If no summarization needed, try passing through.
|
|
3599
|
+
* If the handler throws a ContextOverflowError, fall back to
|
|
3600
|
+
* emergency summarization (matching Python's behavior).
|
|
3601
|
+
*/
|
|
3602
|
+
if (!shouldSummarize(truncatedMessages, totalTokens, maxInputTokens)) try {
|
|
3603
|
+
return await handler({
|
|
3604
|
+
...request,
|
|
3605
|
+
messages: truncatedMessages
|
|
3606
|
+
});
|
|
3607
|
+
} catch (err) {
|
|
3608
|
+
if (!isContextOverflow(err)) throw err;
|
|
3609
|
+
if (maxInputTokens && totalTokens > 0) {
|
|
3610
|
+
const observedRatio = maxInputTokens / totalTokens;
|
|
3611
|
+
if (observedRatio > tokenEstimationMultiplier) tokenEstimationMultiplier = observedRatio * 1.1;
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
/**
|
|
3615
|
+
* Step 3: Perform summarization
|
|
3616
|
+
*/
|
|
3617
|
+
return performSummarization(request, handler, truncatedMessages, resolvedModel, maxInputTokens);
|
|
3618
|
+
}
|
|
3619
|
+
});
|
|
3620
|
+
}
|
|
3621
|
+
//#endregion
|
|
3622
|
+
//#region src/middleware/async_subagents.ts
|
|
3623
|
+
function toolCallIdFromRuntime(runtime) {
|
|
3624
|
+
return runtime.toolCall?.id ?? runtime.toolCallId ?? "";
|
|
3625
|
+
}
|
|
3626
|
+
/**
|
|
3627
|
+
* Zod schema for {@link AsyncTask}.
|
|
3628
|
+
*
|
|
3629
|
+
* Used by the {@link ReducedValue} in the state schema so that LangGraph
|
|
3630
|
+
* can validate and serialize task records stored in `asyncTasks`.
|
|
3631
|
+
*/
|
|
3632
|
+
const AsyncTaskSchema = z.object({
|
|
3633
|
+
taskId: z.string(),
|
|
3634
|
+
agentName: z.string(),
|
|
3635
|
+
threadId: z.string(),
|
|
3636
|
+
runId: z.string(),
|
|
3637
|
+
status: z.string(),
|
|
3638
|
+
createdAt: z.string(),
|
|
3639
|
+
description: z.string().optional(),
|
|
3640
|
+
updatedAt: z.string().optional(),
|
|
3641
|
+
checkedAt: z.string().optional()
|
|
3642
|
+
});
|
|
3643
|
+
/**
|
|
3644
|
+
* State schema for the async subagent middleware.
|
|
3645
|
+
*
|
|
3646
|
+
* Declares `asyncTasks` as a reduced state channel so that individual
|
|
3647
|
+
* tool updates (launch, check, update, cancel, list) merge into the existing
|
|
3648
|
+
* tasks dict rather than replacing it wholesale.
|
|
3649
|
+
*/
|
|
3650
|
+
const AsyncTaskStateSchema = new StateSchema({ asyncTasks: new ReducedValue(z.record(z.string(), AsyncTaskSchema).default(() => ({})), {
|
|
3651
|
+
inputSchema: z.record(z.string(), AsyncTaskSchema).optional(),
|
|
3652
|
+
reducer: asyncTasksReducer
|
|
3653
|
+
}) });
|
|
3654
|
+
/**
|
|
3655
|
+
* Reducer for the `asyncTasks` state channel.
|
|
3656
|
+
*
|
|
3657
|
+
* Merges task updates into the existing tasks dict using shallow spread.
|
|
3658
|
+
* This allows individual tools to update a single task without overwriting
|
|
3659
|
+
* the full map — only the keys present in `update` are replaced.
|
|
3660
|
+
*
|
|
3661
|
+
* @param existing - The current tasks dict from state (may be undefined on first write).
|
|
3662
|
+
* @param update - New or updated task entries to merge in.
|
|
3663
|
+
* @returns Merged tasks dict.
|
|
3664
|
+
*/
|
|
3665
|
+
function asyncTasksReducer(existing, update) {
|
|
3666
|
+
return {
|
|
3667
|
+
...existing || {},
|
|
3668
|
+
...update || {}
|
|
3669
|
+
};
|
|
3670
|
+
}
|
|
3671
|
+
/**
|
|
3672
|
+
* Description template for the `start_async_task` tool.
|
|
3673
|
+
*
|
|
3674
|
+
* The `{available_agents}` placeholder is replaced at middleware creation
|
|
3675
|
+
* time with a formatted list of configured async subagent names and descriptions.
|
|
3676
|
+
*/
|
|
3677
|
+
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.
|
|
3678
|
+
|
|
3679
|
+
Available async agent types:
|
|
3680
|
+
{available_agents}
|
|
3681
|
+
|
|
3682
|
+
## Usage notes:
|
|
3683
|
+
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.
|
|
3684
|
+
2. Use \`check_async_task\` only when the user asks for a status update or result.
|
|
3685
|
+
3. Use \`update_async_task\` to send new instructions to a running task.
|
|
3686
|
+
4. Multiple async subagents can run concurrently — launch several and let them run in the background.
|
|
3687
|
+
5. The subagent runs on a remote server, so it has its own tools and capabilities.`;
|
|
3688
|
+
/**
|
|
3689
|
+
* Default system prompt appended to the main agent's system message when
|
|
3690
|
+
* async subagent middleware is active.
|
|
3691
|
+
*
|
|
3692
|
+
* Provides the agent with instructions on how to use the five async subagent
|
|
3693
|
+
* tools (launch, check, update, cancel, list) including workflow ordering,
|
|
3694
|
+
* critical rules about polling behavior, and guidance on when to use async
|
|
3695
|
+
* subagents vs. synchronous delegation.
|
|
3696
|
+
*/
|
|
3697
|
+
const ASYNC_TASK_SYSTEM_PROMPT = `## Async subagents (remote servers)
|
|
3698
|
+
|
|
3699
|
+
You have access to async subagent tools that launch background tasks on remote servers.
|
|
3700
|
+
|
|
3701
|
+
### Tools:
|
|
3702
|
+
- \`start_async_task\`: Start a new background task. Returns a task ID immediately.
|
|
3703
|
+
- \`check_async_task\`: Check the status of a running task. Returns status and result if complete.
|
|
3704
|
+
- \`update_async_task\`: Send an update or new instructions to a running task.
|
|
3705
|
+
- \`cancel_async_task\`: Cancel a running task that is no longer needed.
|
|
3706
|
+
- \`list_async_tasks\`: List all tracked tasks with live statuses. Use this to check all tasks at once.
|
|
3707
|
+
|
|
3708
|
+
### Workflow:
|
|
3709
|
+
1. **Launch** — Use \`start_async_task\` to start a task. Report the task ID to the user and stop.
|
|
3710
|
+
Do NOT immediately check the status — the task runs in the background while you and the user continue other work.
|
|
3711
|
+
2. **Check (on request)** — Only use \`check_async_task\` when the user explicitly asks for a status update or
|
|
3712
|
+
result. If the status is "running", report that and stop — do not poll in a loop.
|
|
3713
|
+
3. **Update** (optional) — Use \`update_async_task\` to send new instructions to a running task. This interrupts
|
|
3714
|
+
the current run and starts a fresh one on the same thread. The task_id stays the same.
|
|
3715
|
+
4. **Cancel** (optional) — Use \`cancel_async_task\` to stop a task that is no longer needed.
|
|
3716
|
+
5. **Collect** — When \`check_async_task\` returns status "success", the result is included in the response.
|
|
3717
|
+
6. **List** — Use \`list_async_tasks\` to see live statuses for all tasks at once, or to recall task IDs after context compaction.
|
|
3718
|
+
|
|
3719
|
+
### Critical rules:
|
|
3720
|
+
- After launching, ALWAYS return control to the user immediately. Never auto-check after launching.
|
|
3721
|
+
- Never poll \`check_async_task\` in a loop. Check once per user request, then stop.
|
|
3722
|
+
- If a check returns "running", tell the user and wait for them to ask again.
|
|
3723
|
+
- Task statuses in conversation history are ALWAYS stale — a task that was "running" may now be done.
|
|
3724
|
+
NEVER report a status from a previous tool result. ALWAYS call a tool to get the current status:
|
|
3725
|
+
use \`list_async_tasks\` when the user asks about multiple tasks or "all tasks",
|
|
3726
|
+
use \`check_async_task\` when the user asks about a specific task.
|
|
3727
|
+
- Always show the full task_id — never truncate or abbreviate it.
|
|
3728
|
+
|
|
3729
|
+
### When to use async subagents:
|
|
3730
|
+
- Long-running tasks that would block the main agent
|
|
3731
|
+
- Tasks that benefit from running on specialized remote deployments
|
|
3732
|
+
- When you want to run multiple tasks concurrently and collect results later`;
|
|
3733
|
+
/**
|
|
3734
|
+
* Task statuses that will never change.
|
|
3735
|
+
*
|
|
3736
|
+
* When listing tasks, live-status fetches are skipped for tasks whose
|
|
3737
|
+
* cached status is in this set, since they are guaranteed to be final.
|
|
3738
|
+
*/
|
|
3739
|
+
/**
|
|
3740
|
+
* Names of the tools added by the async subagent middleware.
|
|
3741
|
+
*
|
|
3742
|
+
* Exported so `agent.ts` can include them in `BUILTIN_TOOL_NAMES` and
|
|
3743
|
+
* surface a `ConfigurationError` if a user-provided tool collides.
|
|
3744
|
+
*/
|
|
3745
|
+
const ASYNC_TASK_TOOL_NAMES = [
|
|
3746
|
+
"start_async_task",
|
|
3747
|
+
"check_async_task",
|
|
3748
|
+
"update_async_task",
|
|
3749
|
+
"cancel_async_task",
|
|
3750
|
+
"list_async_tasks"
|
|
3751
|
+
];
|
|
3752
|
+
const TERMINAL_STATUSES = new Set([
|
|
3753
|
+
"cancelled",
|
|
3754
|
+
"success",
|
|
3755
|
+
"error",
|
|
3756
|
+
"timeout",
|
|
3757
|
+
"interrupted"
|
|
3758
|
+
]);
|
|
3759
|
+
/**
|
|
3760
|
+
* Look up a tracked task from state by its `taskId`.
|
|
3761
|
+
*
|
|
3762
|
+
* @param taskId - The task ID to look up (will be trimmed).
|
|
3763
|
+
* @param state - The current agent state containing `asyncTasks`.
|
|
3764
|
+
* @returns The tracked task on success, or an error string.
|
|
3765
|
+
*/
|
|
3766
|
+
function resolveTrackedTask(taskId, state) {
|
|
3767
|
+
const tracked = (state.asyncTasks ?? {})[taskId.trim()];
|
|
3768
|
+
if (!tracked) return `No tracked task found for taskId: '${taskId}'`;
|
|
3769
|
+
return tracked;
|
|
3770
|
+
}
|
|
3771
|
+
/**
|
|
3772
|
+
* Build a check result from a run's current status and thread state values.
|
|
3773
|
+
*
|
|
3774
|
+
* For successful runs, extracts the last message's content from the remote
|
|
3775
|
+
* thread's state values. For errored runs, includes a generic error message.
|
|
3776
|
+
*
|
|
3777
|
+
* @param run - The run object from the SDK.
|
|
3778
|
+
* @param threadId - The thread ID for the run.
|
|
3779
|
+
* @param threadValues - The `values` from `ThreadState` (the remote subagent's state).
|
|
3780
|
+
*/
|
|
3781
|
+
function buildCheckResult(run, threadId, threadValues) {
|
|
3782
|
+
const checkResult = {
|
|
3783
|
+
status: run.status,
|
|
3784
|
+
threadId
|
|
3785
|
+
};
|
|
3786
|
+
if (run.status === "success") {
|
|
3787
|
+
const messages = (Array.isArray(threadValues) ? {} : threadValues)?.messages ?? [];
|
|
3788
|
+
if (messages.length > 0) {
|
|
3789
|
+
const last = messages[messages.length - 1];
|
|
3790
|
+
const rawContent = typeof last === "object" && last !== null && "content" in last ? last.content : last;
|
|
3791
|
+
checkResult.result = typeof rawContent === "string" ? rawContent : JSON.stringify(rawContent);
|
|
3792
|
+
} else checkResult.result = "Completed with no output messages.";
|
|
3793
|
+
} else if (run.status === "error") checkResult.error = "The async subagent encountered an error.";
|
|
3794
|
+
return checkResult;
|
|
3795
|
+
}
|
|
3796
|
+
/**
|
|
3797
|
+
* Filter tasks by cached status from agent state.
|
|
3798
|
+
*
|
|
3799
|
+
* Filtering uses the cached status, not live server status. Live statuses
|
|
3800
|
+
* are fetched after filtering by the calling tool.
|
|
3801
|
+
*
|
|
3802
|
+
* @param tasks - All tracked tasks from state.
|
|
3803
|
+
* @param statusFilter - If nullish or `'all'`, return all tasks.
|
|
3804
|
+
* Otherwise return only tasks whose cached status matches.
|
|
3805
|
+
*/
|
|
3806
|
+
function filterTasks(tasks, statusFilter) {
|
|
3807
|
+
if (!statusFilter || statusFilter === "all") return Object.values(tasks);
|
|
3808
|
+
return Object.values(tasks).filter((task) => task.status === statusFilter);
|
|
3809
|
+
}
|
|
3810
|
+
/**
|
|
3811
|
+
* Fetch the current run status from the server.
|
|
3812
|
+
*
|
|
3813
|
+
* Returns the cached status immediately for terminal tasks (avoiding
|
|
3814
|
+
* unnecessary API calls). Falls back to the cached status on SDK errors.
|
|
3815
|
+
*/
|
|
3816
|
+
async function fetchLiveTaskStatus(clients, task) {
|
|
3817
|
+
if (TERMINAL_STATUSES.has(task.status)) return task.status;
|
|
3818
|
+
try {
|
|
3819
|
+
return (await clients.getClient(task.agentName).runs.get(task.threadId, task.runId)).status;
|
|
3820
|
+
} catch {
|
|
3821
|
+
return task.status;
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3824
|
+
/**
|
|
3825
|
+
* Format a single task as a display string for list output.
|
|
3826
|
+
*/
|
|
3827
|
+
function formatTaskEntry(task, status) {
|
|
3828
|
+
return `- taskId: ${task.taskId} agent: ${task.agentName} status: ${status}`;
|
|
3829
|
+
}
|
|
3830
|
+
/**
|
|
3831
|
+
* Lazily-created, cached LangGraph SDK clients keyed by (url, headers).
|
|
3832
|
+
*
|
|
3833
|
+
* Agents that share the same URL and headers will reuse a single `Client`
|
|
3834
|
+
* instance, avoiding unnecessary connections.
|
|
3835
|
+
*/
|
|
3836
|
+
var ClientCache = class {
|
|
3837
|
+
agents;
|
|
3838
|
+
clients = /* @__PURE__ */ new Map();
|
|
3839
|
+
constructor(agents) {
|
|
3840
|
+
this.agents = agents;
|
|
3841
|
+
}
|
|
3842
|
+
/**
|
|
3843
|
+
* Build headers for a remote Agent Protocol server.
|
|
3844
|
+
*
|
|
3845
|
+
* Adds `x-auth-scheme: langsmith` by default unless already provided.
|
|
3846
|
+
* For self-hosted servers that don't require this header, it is typically
|
|
3847
|
+
* ignored. Override via the `headers` field on the AsyncSubAgent config.
|
|
3848
|
+
*/
|
|
3849
|
+
resolveHeaders(spec) {
|
|
3850
|
+
const headers = { ...spec.headers || {} };
|
|
3851
|
+
if (!("x-auth-scheme" in headers)) headers["x-auth-scheme"] = "langsmith";
|
|
3852
|
+
return headers;
|
|
3853
|
+
}
|
|
3854
|
+
/**
|
|
3855
|
+
* Build a stable cache key from a spec's url and resolved headers.
|
|
3856
|
+
*/
|
|
3857
|
+
cacheKey(spec) {
|
|
3858
|
+
const headers = this.resolveHeaders(spec);
|
|
3859
|
+
const headerStr = Object.entries(headers).sort().flat().join(":");
|
|
3860
|
+
return `${spec.url ?? ""}|${headerStr}`;
|
|
3861
|
+
}
|
|
3862
|
+
/**
|
|
3863
|
+
* Get or create a `Client` for the named agent.
|
|
3864
|
+
*/
|
|
3865
|
+
getClient(name) {
|
|
3866
|
+
const spec = this.agents[name];
|
|
3867
|
+
const key = this.cacheKey(spec);
|
|
3868
|
+
const existing = this.clients.get(key);
|
|
3869
|
+
if (existing) return existing;
|
|
3870
|
+
const headers = this.resolveHeaders(spec);
|
|
3871
|
+
const client = new Client({
|
|
3872
|
+
apiUrl: spec.url,
|
|
3873
|
+
defaultHeaders: headers
|
|
3874
|
+
});
|
|
3875
|
+
this.clients.set(key, client);
|
|
3876
|
+
return client;
|
|
3877
|
+
}
|
|
3878
|
+
};
|
|
3879
|
+
/**
|
|
3880
|
+
* Extract the callback thread ID from the tool runtime.
|
|
3881
|
+
*
|
|
3882
|
+
* The thread ID is included in the subagent's input state so the subagent
|
|
3883
|
+
* can notify the parent when it completes (via
|
|
3884
|
+
* `CompletionCallbackMiddleware`).
|
|
3885
|
+
*
|
|
3886
|
+
* @returns Object with `callbackThreadId` if available. Empty object otherwise.
|
|
3887
|
+
*/
|
|
3888
|
+
function extractCallbackContext(runtime) {
|
|
3889
|
+
const threadId = (runtime.config?.configurable)?.thread_id;
|
|
3890
|
+
if (typeof threadId === "string" && threadId) return { callbackThreadId: threadId };
|
|
3891
|
+
return {};
|
|
3892
|
+
}
|
|
3893
|
+
/**
|
|
3894
|
+
* Build the `start_async_task` tool.
|
|
3895
|
+
*
|
|
3896
|
+
* Creates a thread on the remote server, starts a run, and returns a
|
|
3897
|
+
* `Command` that persists the new task in state.
|
|
3898
|
+
*/
|
|
3899
|
+
function buildStartTool(agentMap, clients, toolDescription) {
|
|
3900
|
+
return tool(async (input, runtime) => {
|
|
3901
|
+
if (!(input.agentName in agentMap)) {
|
|
3902
|
+
const allowed = Object.keys(agentMap).map((k) => `\`${k}\``).join(", ");
|
|
3903
|
+
return `Unknown async subagent type \`${input.agentName}\`. Available types: ${allowed}`;
|
|
3904
|
+
}
|
|
3905
|
+
const spec = agentMap[input.agentName];
|
|
3906
|
+
const callbackContext = extractCallbackContext(runtime);
|
|
2945
3907
|
try {
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
const
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
3908
|
+
const client = clients.getClient(input.agentName);
|
|
3909
|
+
const thread = await client.threads.create();
|
|
3910
|
+
const run = await client.runs.create(thread.thread_id, spec.graphId, { input: {
|
|
3911
|
+
messages: [{
|
|
3912
|
+
role: "user",
|
|
3913
|
+
content: input.description
|
|
3914
|
+
}],
|
|
3915
|
+
...callbackContext
|
|
3916
|
+
} });
|
|
3917
|
+
const taskId = thread.thread_id;
|
|
3918
|
+
const task = {
|
|
3919
|
+
taskId,
|
|
3920
|
+
agentName: input.agentName,
|
|
3921
|
+
threadId: taskId,
|
|
3922
|
+
runId: run.run_id,
|
|
3923
|
+
status: "running",
|
|
3924
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3925
|
+
description: input.description
|
|
3926
|
+
};
|
|
3927
|
+
return new Command({ update: {
|
|
3928
|
+
messages: [new ToolMessage({
|
|
3929
|
+
content: `Launched async subagent. taskId: ${taskId}`,
|
|
3930
|
+
tool_call_id: toolCallIdFromRuntime(runtime)
|
|
3931
|
+
})],
|
|
3932
|
+
asyncTasks: { [taskId]: task }
|
|
3933
|
+
} });
|
|
3934
|
+
} catch (e) {
|
|
3935
|
+
return `Failed to launch async subagent '${input.agentName}': ${e}`;
|
|
3936
|
+
}
|
|
3937
|
+
}, {
|
|
3938
|
+
name: "start_async_task",
|
|
3939
|
+
description: toolDescription,
|
|
3940
|
+
schema: z.object({
|
|
3941
|
+
description: z.string().describe("A detailed description of the task for the async subagent to perform."),
|
|
3942
|
+
agentName: z.string().describe("The type of async subagent to use. Must be one of the available types listed in the tool description.")
|
|
3943
|
+
})
|
|
3944
|
+
});
|
|
3945
|
+
}
|
|
3946
|
+
/**
|
|
3947
|
+
* Build the `check_async_task` tool.
|
|
3948
|
+
*
|
|
3949
|
+
* Fetches the current run status from the remote server and, if the run
|
|
3950
|
+
* succeeded, retrieves the thread state to extract the result.
|
|
3951
|
+
*/
|
|
3952
|
+
function buildCheckTool(clients) {
|
|
3953
|
+
return tool(async (input, runtime) => {
|
|
3954
|
+
const task = resolveTrackedTask(input.taskId, runtime.state);
|
|
3955
|
+
if (typeof task === "string") return task;
|
|
3956
|
+
const client = clients.getClient(task.agentName);
|
|
3957
|
+
let run;
|
|
3958
|
+
try {
|
|
3959
|
+
run = await client.runs.get(task.threadId, task.runId);
|
|
3960
|
+
} catch (e) {
|
|
3961
|
+
return `Failed to get run status: ${e}`;
|
|
3962
|
+
}
|
|
3963
|
+
let threadValues = {};
|
|
3964
|
+
if (run.status === "success") try {
|
|
3965
|
+
threadValues = (await client.threads.getState(task.threadId)).values || {};
|
|
3966
|
+
} catch {}
|
|
3967
|
+
const result = buildCheckResult(run, task.threadId, threadValues);
|
|
3968
|
+
const updatedTask = {
|
|
3969
|
+
taskId: task.taskId,
|
|
3970
|
+
agentName: task.agentName,
|
|
3971
|
+
threadId: task.threadId,
|
|
3972
|
+
runId: task.runId,
|
|
3973
|
+
status: result.status,
|
|
3974
|
+
createdAt: task.createdAt,
|
|
3975
|
+
updatedAt: result.status !== task.status ? (/* @__PURE__ */ new Date()).toISOString() : task.updatedAt,
|
|
3976
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3977
|
+
};
|
|
3978
|
+
return new Command({ update: {
|
|
3979
|
+
messages: [new ToolMessage({
|
|
3980
|
+
content: JSON.stringify(result),
|
|
3981
|
+
tool_call_id: toolCallIdFromRuntime(runtime)
|
|
3982
|
+
})],
|
|
3983
|
+
asyncTasks: { [task.taskId]: updatedTask }
|
|
3984
|
+
} });
|
|
3985
|
+
}, {
|
|
3986
|
+
name: "check_async_task",
|
|
3987
|
+
description: "Check the status of an async subagent task. Returns the current status and, if complete, the result.",
|
|
3988
|
+
schema: z.object({ taskId: z.string().describe("The exact taskId string returned by start_async_task. Pass it verbatim.") })
|
|
3989
|
+
});
|
|
3990
|
+
}
|
|
3991
|
+
/**
|
|
3992
|
+
* Build the `update_async_task` tool.
|
|
3993
|
+
*
|
|
3994
|
+
* Sends a follow-up message to a running async subagent by creating a new
|
|
3995
|
+
* run on the same thread with `multitaskStrategy: "interrupt"`. The subagent
|
|
3996
|
+
* sees the full conversation history plus the new message. The `taskId`
|
|
3997
|
+
* remains the same; only the internal `runId` is updated.
|
|
3998
|
+
*/
|
|
3999
|
+
function buildUpdateTool(agentMap, clients) {
|
|
4000
|
+
return tool(async (input, runtime) => {
|
|
4001
|
+
const tracked = resolveTrackedTask(input.taskId, runtime.state);
|
|
4002
|
+
if (typeof tracked === "string") return tracked;
|
|
4003
|
+
const spec = agentMap[tracked.agentName];
|
|
4004
|
+
try {
|
|
4005
|
+
const run = await clients.getClient(tracked.agentName).runs.create(tracked.threadId, spec.graphId, {
|
|
4006
|
+
input: { messages: [{
|
|
4007
|
+
role: "user",
|
|
4008
|
+
content: input.message
|
|
4009
|
+
}] },
|
|
4010
|
+
multitaskStrategy: "interrupt"
|
|
2964
4011
|
});
|
|
4012
|
+
const task = {
|
|
4013
|
+
taskId: tracked.taskId,
|
|
4014
|
+
agentName: tracked.agentName,
|
|
4015
|
+
threadId: tracked.threadId,
|
|
4016
|
+
runId: run.run_id,
|
|
4017
|
+
status: "running",
|
|
4018
|
+
createdAt: tracked.createdAt,
|
|
4019
|
+
description: input.message,
|
|
4020
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4021
|
+
checkedAt: tracked.checkedAt
|
|
4022
|
+
};
|
|
4023
|
+
return new Command({ update: {
|
|
4024
|
+
messages: [new ToolMessage({
|
|
4025
|
+
content: `Updated async subagent. taskId: ${tracked.taskId}`,
|
|
4026
|
+
tool_call_id: toolCallIdFromRuntime(runtime)
|
|
4027
|
+
})],
|
|
4028
|
+
asyncTasks: { [tracked.taskId]: task }
|
|
4029
|
+
} });
|
|
4030
|
+
} catch (e) {
|
|
4031
|
+
return `Failed to update async subagent: ${e}`;
|
|
4032
|
+
}
|
|
4033
|
+
}, {
|
|
4034
|
+
name: "update_async_task",
|
|
4035
|
+
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.",
|
|
4036
|
+
schema: z.object({
|
|
4037
|
+
taskId: z.string().describe("The exact taskId string returned by start_async_task. Pass it verbatim."),
|
|
4038
|
+
message: z.string().describe("Follow-up instructions or context to send to the subagent")
|
|
4039
|
+
})
|
|
4040
|
+
});
|
|
4041
|
+
}
|
|
4042
|
+
/**
|
|
4043
|
+
* Build the `cancel_async_task` tool.
|
|
4044
|
+
*
|
|
4045
|
+
* Cancels the current run on the remote server and updates the task's
|
|
4046
|
+
* cached status to `"cancelled"`.
|
|
4047
|
+
*/
|
|
4048
|
+
function buildCancelTool(clients) {
|
|
4049
|
+
return tool(async (input, runtime) => {
|
|
4050
|
+
const tracked = resolveTrackedTask(input.taskId, runtime.state);
|
|
4051
|
+
if (typeof tracked === "string") return tracked;
|
|
4052
|
+
const client = clients.getClient(tracked.agentName);
|
|
4053
|
+
try {
|
|
4054
|
+
await client.runs.cancel(tracked.threadId, tracked.runId);
|
|
4055
|
+
} catch (e) {
|
|
4056
|
+
return `Failed to cancel run: ${e}`;
|
|
2965
4057
|
}
|
|
4058
|
+
const updated = {
|
|
4059
|
+
taskId: tracked.taskId,
|
|
4060
|
+
agentName: tracked.agentName,
|
|
4061
|
+
threadId: tracked.threadId,
|
|
4062
|
+
runId: tracked.runId,
|
|
4063
|
+
status: "cancelled",
|
|
4064
|
+
createdAt: tracked.createdAt,
|
|
4065
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4066
|
+
checkedAt: tracked.checkedAt
|
|
4067
|
+
};
|
|
2966
4068
|
return new Command({ update: {
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
}
|
|
2972
|
-
_summarizationSessionId: getSessionId(request.state)
|
|
4069
|
+
messages: [new ToolMessage({
|
|
4070
|
+
content: `Cancelled async subagent task: ${tracked.taskId}`,
|
|
4071
|
+
tool_call_id: toolCallIdFromRuntime(runtime)
|
|
4072
|
+
})],
|
|
4073
|
+
asyncTasks: { [tracked.taskId]: updated }
|
|
2973
4074
|
} });
|
|
2974
|
-
}
|
|
4075
|
+
}, {
|
|
4076
|
+
name: "cancel_async_task",
|
|
4077
|
+
description: "Cancel a running async subagent task. Use this to stop a task that is no longer needed.",
|
|
4078
|
+
schema: z.object({ taskId: z.string().describe("The exact taskId string returned by start_async_task. Pass it verbatim.") })
|
|
4079
|
+
});
|
|
4080
|
+
}
|
|
4081
|
+
/**
|
|
4082
|
+
* Build the `list_async_tasks` tool.
|
|
4083
|
+
*
|
|
4084
|
+
* Lists all tracked tasks with their live statuses fetched in parallel.
|
|
4085
|
+
* Supports optional filtering by cached status.
|
|
4086
|
+
*/
|
|
4087
|
+
function buildListTool(clients) {
|
|
4088
|
+
return tool(async (input, runtime) => {
|
|
4089
|
+
const filtered = filterTasks(runtime.state.asyncTasks ?? {}, input.statusFilter ?? void 0);
|
|
4090
|
+
if (filtered.length === 0) return "No async subagent tasks tracked";
|
|
4091
|
+
const statuses = await Promise.all(filtered.map((task) => fetchLiveTaskStatus(clients, task)));
|
|
4092
|
+
const updatedTasks = {};
|
|
4093
|
+
const entries = [];
|
|
4094
|
+
for (let idx = 0; idx < filtered.length; idx++) {
|
|
4095
|
+
const task = filtered[idx];
|
|
4096
|
+
const status = statuses[idx];
|
|
4097
|
+
const taskEntry = formatTaskEntry(task, status);
|
|
4098
|
+
entries.push(taskEntry);
|
|
4099
|
+
updatedTasks[task.taskId] = {
|
|
4100
|
+
taskId: task.taskId,
|
|
4101
|
+
agentName: task.agentName,
|
|
4102
|
+
threadId: task.threadId,
|
|
4103
|
+
runId: task.runId,
|
|
4104
|
+
status,
|
|
4105
|
+
createdAt: task.createdAt,
|
|
4106
|
+
updatedAt: status !== task.status ? (/* @__PURE__ */ new Date()).toISOString() : task.updatedAt,
|
|
4107
|
+
checkedAt: task.checkedAt
|
|
4108
|
+
};
|
|
4109
|
+
}
|
|
4110
|
+
return new Command({ update: {
|
|
4111
|
+
messages: [new ToolMessage({
|
|
4112
|
+
content: `${entries.length} tracked task(s):\n${entries.join("\n")}`,
|
|
4113
|
+
tool_call_id: toolCallIdFromRuntime(runtime)
|
|
4114
|
+
})],
|
|
4115
|
+
asyncTasks: updatedTasks
|
|
4116
|
+
} });
|
|
4117
|
+
}, {
|
|
4118
|
+
name: "list_async_tasks",
|
|
4119
|
+
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.",
|
|
4120
|
+
schema: z.object({ statusFilter: z.string().nullish().describe("Filter tasks by status. One of: 'running', 'success', 'error', 'cancelled', 'all'. Defaults to 'all'.") })
|
|
4121
|
+
});
|
|
4122
|
+
}
|
|
4123
|
+
/**
|
|
4124
|
+
* Create middleware that adds async subagent tools to an agent.
|
|
4125
|
+
*
|
|
4126
|
+
* Provides five tools for launching, checking, updating, cancelling, and
|
|
4127
|
+
* listing background tasks on remote Agent Protocol servers. Task state is
|
|
4128
|
+
* persisted in the `asyncTasks` state channel so it survives
|
|
4129
|
+
* context compaction.
|
|
4130
|
+
*
|
|
4131
|
+
* Works with any Agent Protocol-compliant server — LangGraph Platform (managed)
|
|
4132
|
+
* or self-hosted (e.g. a Hono/Express server implementing the Agent Protocol spec).
|
|
4133
|
+
*
|
|
4134
|
+
* @throws {Error} If no async subagents are provided or names are duplicated.
|
|
4135
|
+
*
|
|
4136
|
+
* @example
|
|
4137
|
+
* ```ts
|
|
4138
|
+
* const middleware = createAsyncSubAgentMiddleware({
|
|
4139
|
+
* asyncSubAgents: [{
|
|
4140
|
+
* name: "researcher",
|
|
4141
|
+
* description: "Research agent for deep analysis",
|
|
4142
|
+
* url: "https://my-agent-protocol-server.example.com",
|
|
4143
|
+
* graphId: "research_agent",
|
|
4144
|
+
* }],
|
|
4145
|
+
* });
|
|
4146
|
+
* ```
|
|
4147
|
+
*/
|
|
4148
|
+
/**
|
|
4149
|
+
* Type guard to distinguish async SubAgents from sync SubAgents/CompiledSubAgents.
|
|
4150
|
+
*
|
|
4151
|
+
* Uses the presence of the `graphId` field as the runtime discriminant —
|
|
4152
|
+
* `AsyncSubAgent` requires it, while `SubAgent` and `CompiledSubAgent` do not have it.
|
|
4153
|
+
*/
|
|
4154
|
+
function isAsyncSubAgent(subAgent) {
|
|
4155
|
+
return "graphId" in subAgent;
|
|
4156
|
+
}
|
|
4157
|
+
function createAsyncSubAgentMiddleware(options) {
|
|
4158
|
+
const { asyncSubAgents, systemPrompt = ASYNC_TASK_SYSTEM_PROMPT } = options;
|
|
4159
|
+
if (!asyncSubAgents || asyncSubAgents.length === 0) throw new Error("At least one async subagent must be specified");
|
|
4160
|
+
const names = asyncSubAgents.map((a) => a.name);
|
|
4161
|
+
const duplicates = names.filter((n, i) => names.indexOf(n) !== i);
|
|
4162
|
+
if (duplicates.length > 0) throw new Error(`Duplicate async subagent names: ${[...new Set(duplicates)].join(", ")}`);
|
|
4163
|
+
const agentMap = Object.fromEntries(asyncSubAgents.map((a) => [a.name, a]));
|
|
4164
|
+
const clients = new ClientCache(agentMap);
|
|
4165
|
+
const agentsDescription = asyncSubAgents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
|
|
4166
|
+
const tools = [
|
|
4167
|
+
buildStartTool(agentMap, clients, ASYNC_TASK_TOOL_DESCRIPTION.replace("{available_agents}", agentsDescription)),
|
|
4168
|
+
buildCheckTool(clients),
|
|
4169
|
+
buildUpdateTool(agentMap, clients),
|
|
4170
|
+
buildCancelTool(clients),
|
|
4171
|
+
buildListTool(clients)
|
|
4172
|
+
];
|
|
4173
|
+
const fullSystemPrompt = systemPrompt ? `${systemPrompt}\n\nAvailable async subagent types:\n${agentsDescription}` : null;
|
|
2975
4174
|
return createMiddleware({
|
|
2976
|
-
name: "
|
|
2977
|
-
stateSchema:
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
if (
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
const maxInputTokens = getMaxInputTokens(resolvedModel);
|
|
2986
|
-
applyModelDefaults(resolvedModel);
|
|
2987
|
-
/**
|
|
2988
|
-
* Step 1: Truncate args if configured
|
|
2989
|
-
*/
|
|
2990
|
-
const { messages: truncatedMessages } = truncateArgs(effectiveMessages, maxInputTokens, request.systemMessage, request.tools);
|
|
2991
|
-
/**
|
|
2992
|
-
* Step 2: Check if summarization should happen.
|
|
2993
|
-
* Count tokens including system message and tools to match what's
|
|
2994
|
-
* actually sent to the model (matching Python implementation).
|
|
2995
|
-
*/
|
|
2996
|
-
const totalTokens = countTotalTokens(truncatedMessages, request.systemMessage, request.tools);
|
|
2997
|
-
/**
|
|
2998
|
-
* If no summarization needed, try passing through.
|
|
2999
|
-
* If the handler throws a ContextOverflowError, fall back to
|
|
3000
|
-
* emergency summarization (matching Python's behavior).
|
|
3001
|
-
*/
|
|
3002
|
-
if (!shouldSummarize(truncatedMessages, totalTokens, maxInputTokens)) try {
|
|
3003
|
-
return await handler({
|
|
3004
|
-
...request,
|
|
3005
|
-
messages: truncatedMessages
|
|
3006
|
-
});
|
|
3007
|
-
} catch (err) {
|
|
3008
|
-
if (!isContextOverflow(err)) throw err;
|
|
3009
|
-
if (maxInputTokens && totalTokens > 0) {
|
|
3010
|
-
const observedRatio = maxInputTokens / totalTokens;
|
|
3011
|
-
if (observedRatio > tokenEstimationMultiplier) tokenEstimationMultiplier = observedRatio * 1.1;
|
|
3012
|
-
}
|
|
3013
|
-
}
|
|
3014
|
-
/**
|
|
3015
|
-
* Step 3: Perform summarization
|
|
3016
|
-
*/
|
|
3017
|
-
return performSummarization(request, handler, truncatedMessages, resolvedModel, maxInputTokens);
|
|
4175
|
+
name: "asyncSubAgentMiddleware",
|
|
4176
|
+
stateSchema: AsyncTaskStateSchema,
|
|
4177
|
+
tools,
|
|
4178
|
+
wrapModelCall: async (request, handler) => {
|
|
4179
|
+
if (fullSystemPrompt !== null) return handler({
|
|
4180
|
+
...request,
|
|
4181
|
+
systemMessage: request.systemMessage.concat(new SystemMessage({ content: fullSystemPrompt }))
|
|
4182
|
+
});
|
|
4183
|
+
return handler(request);
|
|
3018
4184
|
}
|
|
3019
4185
|
});
|
|
3020
4186
|
}
|
|
3021
4187
|
//#endregion
|
|
3022
4188
|
//#region src/backends/store.ts
|
|
4189
|
+
/**
|
|
4190
|
+
* StoreBackend: Adapter for LangGraph's BaseStore (persistent, cross-thread).
|
|
4191
|
+
*/
|
|
3023
4192
|
const NAMESPACE_COMPONENT_RE = /^[A-Za-z0-9\-_.@+:~]+$/;
|
|
3024
4193
|
/**
|
|
3025
4194
|
* Validate a namespace array.
|
|
@@ -3054,34 +4223,55 @@ function validateNamespace(namespace) {
|
|
|
3054
4223
|
var StoreBackend = class {
|
|
3055
4224
|
stateAndStore;
|
|
3056
4225
|
_namespace;
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
4226
|
+
fileFormat;
|
|
4227
|
+
constructor(stateAndStoreOrOptions, options) {
|
|
4228
|
+
let opts;
|
|
4229
|
+
if (stateAndStoreOrOptions != null && typeof stateAndStoreOrOptions === "object" && "state" in stateAndStoreOrOptions) {
|
|
4230
|
+
this.stateAndStore = stateAndStoreOrOptions;
|
|
4231
|
+
opts = options;
|
|
4232
|
+
} else {
|
|
4233
|
+
this.stateAndStore = void 0;
|
|
4234
|
+
opts = stateAndStoreOrOptions;
|
|
4235
|
+
}
|
|
4236
|
+
if (opts?.namespace) this._namespace = validateNamespace(opts.namespace);
|
|
4237
|
+
this.fileFormat = opts?.fileFormat ?? "v2";
|
|
3060
4238
|
}
|
|
3061
4239
|
/**
|
|
3062
|
-
* Get the
|
|
4240
|
+
* Get the BaseStore instance for persistent storage operations.
|
|
4241
|
+
*
|
|
4242
|
+
* In legacy mode, reads from the injected {@link StateAndStore}.
|
|
4243
|
+
* In zero-arg mode, retrieves the store from the LangGraph execution
|
|
4244
|
+
* context via {@link getLangGraphStore}.
|
|
3063
4245
|
*
|
|
3064
4246
|
* @returns BaseStore instance
|
|
3065
|
-
* @throws Error if no store is available
|
|
4247
|
+
* @throws Error if no store is available in either mode
|
|
3066
4248
|
*/
|
|
3067
4249
|
getStore() {
|
|
3068
|
-
|
|
3069
|
-
|
|
4250
|
+
if (this.stateAndStore) {
|
|
4251
|
+
const store = this.stateAndStore.store;
|
|
4252
|
+
if (!store) throw new Error("Store is required but not available in runtime");
|
|
4253
|
+
return store;
|
|
4254
|
+
}
|
|
4255
|
+
const store = getStore();
|
|
4256
|
+
if (!store) throw new Error("Store is required but not available in LangGraph execution context. Ensure the graph was configured with a store.");
|
|
3070
4257
|
return store;
|
|
3071
4258
|
}
|
|
3072
4259
|
/**
|
|
3073
4260
|
* Get the namespace for store operations.
|
|
3074
4261
|
*
|
|
3075
|
-
*
|
|
3076
|
-
*
|
|
3077
|
-
*
|
|
3078
|
-
* -
|
|
3079
|
-
*
|
|
4262
|
+
* Resolution order:
|
|
4263
|
+
* 1. Explicit namespace from constructor options (both modes)
|
|
4264
|
+
* 2. Legacy mode: `[assistantId, "filesystem"]` fallback from {@link StateAndStore}
|
|
4265
|
+
* 3. Zero-arg mode without namespace: `["filesystem"]` with a deprecation warning
|
|
4266
|
+
* nudging callers to pass an explicit namespace
|
|
4267
|
+
* 4. Legacy mode without assistantId: `["filesystem"]`
|
|
3080
4268
|
*/
|
|
3081
4269
|
getNamespace() {
|
|
3082
4270
|
if (this._namespace) return this._namespace;
|
|
3083
|
-
|
|
3084
|
-
|
|
4271
|
+
if (this.stateAndStore) {
|
|
4272
|
+
const assistantId = this.stateAndStore.assistantId;
|
|
4273
|
+
if (assistantId) return [assistantId, "filesystem"];
|
|
4274
|
+
}
|
|
3085
4275
|
return ["filesystem"];
|
|
3086
4276
|
}
|
|
3087
4277
|
/**
|
|
@@ -3093,9 +4283,10 @@ var StoreBackend = class {
|
|
|
3093
4283
|
*/
|
|
3094
4284
|
convertStoreItemToFileData(storeItem) {
|
|
3095
4285
|
const value = storeItem.value;
|
|
3096
|
-
if (!value.content
|
|
4286
|
+
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(", ")}`);
|
|
3097
4287
|
return {
|
|
3098
4288
|
content: value.content,
|
|
4289
|
+
...value.mimeType ? { mimeType: value.mimeType } : {},
|
|
3099
4290
|
created_at: value.created_at,
|
|
3100
4291
|
modified_at: value.modified_at
|
|
3101
4292
|
};
|
|
@@ -3104,11 +4295,12 @@ var StoreBackend = class {
|
|
|
3104
4295
|
* Convert FileData to a value suitable for store.put().
|
|
3105
4296
|
*
|
|
3106
4297
|
* @param fileData - The FileData to convert
|
|
3107
|
-
* @returns Object with content, created_at, and modified_at fields
|
|
4298
|
+
* @returns Object with content, mimeType, created_at, and modified_at fields
|
|
3108
4299
|
*/
|
|
3109
4300
|
convertFileDataToStoreValue(fileData) {
|
|
3110
4301
|
return {
|
|
3111
4302
|
content: fileData.content,
|
|
4303
|
+
..."mimeType" in fileData ? { mimeType: fileData.mimeType } : {},
|
|
3112
4304
|
created_at: fileData.created_at,
|
|
3113
4305
|
modified_at: fileData.modified_at
|
|
3114
4306
|
};
|
|
@@ -3143,10 +4335,10 @@ var StoreBackend = class {
|
|
|
3143
4335
|
* List files and directories in the specified directory (non-recursive).
|
|
3144
4336
|
*
|
|
3145
4337
|
* @param path - Absolute path to directory
|
|
3146
|
-
* @returns
|
|
4338
|
+
* @returns LsResult with list of FileInfo objects on success or error on failure.
|
|
3147
4339
|
* Directories have a trailing / in their path and is_dir=true.
|
|
3148
4340
|
*/
|
|
3149
|
-
async
|
|
4341
|
+
async ls(path) {
|
|
3150
4342
|
const store = this.getStore();
|
|
3151
4343
|
const namespace = this.getNamespace();
|
|
3152
4344
|
const items = await this.searchStorePaginated(store, namespace);
|
|
@@ -3164,7 +4356,7 @@ var StoreBackend = class {
|
|
|
3164
4356
|
}
|
|
3165
4357
|
try {
|
|
3166
4358
|
const fd = this.convertStoreItemToFileData(item);
|
|
3167
|
-
const size = fd.content.join("\n").length;
|
|
4359
|
+
const size = isFileDataV1(fd) ? fd.content.join("\n").length : isFileDataBinary(fd) ? fd.content.byteLength : fd.content.length;
|
|
3168
4360
|
infos.push({
|
|
3169
4361
|
path: itemKey,
|
|
3170
4362
|
is_dir: false,
|
|
@@ -3182,35 +4374,49 @@ var StoreBackend = class {
|
|
|
3182
4374
|
modified_at: ""
|
|
3183
4375
|
});
|
|
3184
4376
|
infos.sort((a, b) => a.path.localeCompare(b.path));
|
|
3185
|
-
return infos;
|
|
4377
|
+
return { files: infos };
|
|
3186
4378
|
}
|
|
3187
4379
|
/**
|
|
3188
|
-
* Read file content
|
|
4380
|
+
* Read file content.
|
|
4381
|
+
*
|
|
4382
|
+
* Text files are paginated by line offset/limit.
|
|
4383
|
+
* Binary files return full Uint8Array content (offset/limit ignored).
|
|
3189
4384
|
*
|
|
3190
4385
|
* @param filePath - Absolute file path
|
|
3191
4386
|
* @param offset - Line offset to start reading from (0-indexed)
|
|
3192
4387
|
* @param limit - Maximum number of lines to read
|
|
3193
|
-
* @returns
|
|
4388
|
+
* @returns ReadResult with content on success or error on failure
|
|
3194
4389
|
*/
|
|
3195
4390
|
async read(filePath, offset = 0, limit = 500) {
|
|
3196
4391
|
try {
|
|
3197
|
-
|
|
4392
|
+
const readRawResult = await this.readRaw(filePath);
|
|
4393
|
+
if (readRawResult.error || !readRawResult.data) return { error: readRawResult.error || "File data not found" };
|
|
4394
|
+
const fileDataV2 = migrateToFileDataV2(readRawResult.data, filePath);
|
|
4395
|
+
if (!isTextMimeType(fileDataV2.mimeType)) return {
|
|
4396
|
+
content: fileDataV2.content,
|
|
4397
|
+
mimeType: fileDataV2.mimeType
|
|
4398
|
+
};
|
|
4399
|
+
if (typeof fileDataV2.content !== "string") return { error: `File '${filePath}' has binary content but text MIME type` };
|
|
4400
|
+
return {
|
|
4401
|
+
content: fileDataV2.content.split("\n").slice(offset, offset + limit).join("\n"),
|
|
4402
|
+
mimeType: fileDataV2.mimeType
|
|
4403
|
+
};
|
|
3198
4404
|
} catch (e) {
|
|
3199
|
-
return
|
|
4405
|
+
return { error: e.message };
|
|
3200
4406
|
}
|
|
3201
4407
|
}
|
|
3202
4408
|
/**
|
|
3203
4409
|
* Read file content as raw FileData.
|
|
3204
4410
|
*
|
|
3205
4411
|
* @param filePath - Absolute file path
|
|
3206
|
-
* @returns
|
|
4412
|
+
* @returns ReadRawResult with raw file data on success or error on failure
|
|
3207
4413
|
*/
|
|
3208
4414
|
async readRaw(filePath) {
|
|
3209
4415
|
const store = this.getStore();
|
|
3210
4416
|
const namespace = this.getNamespace();
|
|
3211
4417
|
const item = await store.get(namespace, filePath);
|
|
3212
|
-
if (!item)
|
|
3213
|
-
return this.convertStoreItemToFileData(item);
|
|
4418
|
+
if (!item) return { error: `File '${filePath}' not found` };
|
|
4419
|
+
return { data: this.convertStoreItemToFileData(item) };
|
|
3214
4420
|
}
|
|
3215
4421
|
/**
|
|
3216
4422
|
* Create a new file with content.
|
|
@@ -3220,7 +4426,8 @@ var StoreBackend = class {
|
|
|
3220
4426
|
const store = this.getStore();
|
|
3221
4427
|
const namespace = this.getNamespace();
|
|
3222
4428
|
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.` };
|
|
3223
|
-
const
|
|
4429
|
+
const mimeType = getMimeType(filePath);
|
|
4430
|
+
const fileData = createFileData(content, void 0, this.fileFormat, mimeType);
|
|
3224
4431
|
const storeValue = this.convertFileDataToStoreValue(fileData);
|
|
3225
4432
|
await store.put(namespace, filePath, storeValue);
|
|
3226
4433
|
return {
|
|
@@ -3255,9 +4462,10 @@ var StoreBackend = class {
|
|
|
3255
4462
|
}
|
|
3256
4463
|
}
|
|
3257
4464
|
/**
|
|
3258
|
-
*
|
|
4465
|
+
* Search file contents for a literal text pattern.
|
|
4466
|
+
* Binary files are skipped.
|
|
3259
4467
|
*/
|
|
3260
|
-
async
|
|
4468
|
+
async grep(pattern, path = "/", glob = null) {
|
|
3261
4469
|
const store = this.getStore();
|
|
3262
4470
|
const namespace = this.getNamespace();
|
|
3263
4471
|
const items = await this.searchStorePaginated(store, namespace);
|
|
@@ -3267,12 +4475,12 @@ var StoreBackend = class {
|
|
|
3267
4475
|
} catch {
|
|
3268
4476
|
continue;
|
|
3269
4477
|
}
|
|
3270
|
-
return grepMatchesFromFiles(files, pattern, path, glob);
|
|
4478
|
+
return { matches: grepMatchesFromFiles(files, pattern, path, glob) };
|
|
3271
4479
|
}
|
|
3272
4480
|
/**
|
|
3273
4481
|
* Structured glob matching returning FileInfo objects.
|
|
3274
4482
|
*/
|
|
3275
|
-
async
|
|
4483
|
+
async glob(pattern, path = "/") {
|
|
3276
4484
|
const store = this.getStore();
|
|
3277
4485
|
const namespace = this.getNamespace();
|
|
3278
4486
|
const items = await this.searchStorePaginated(store, namespace);
|
|
@@ -3283,12 +4491,12 @@ var StoreBackend = class {
|
|
|
3283
4491
|
continue;
|
|
3284
4492
|
}
|
|
3285
4493
|
const result = globSearchFiles(files, pattern, path);
|
|
3286
|
-
if (result === "No files found") return [];
|
|
4494
|
+
if (result === "No files found") return { files: [] };
|
|
3287
4495
|
const paths = result.split("\n");
|
|
3288
4496
|
const infos = [];
|
|
3289
4497
|
for (const p of paths) {
|
|
3290
4498
|
const fd = files[p];
|
|
3291
|
-
const size = fd ? fd.content.join("\n").length : 0;
|
|
4499
|
+
const size = fd ? isFileDataV1(fd) ? fd.content.join("\n").length : isFileDataBinary(fd) ? fd.content.byteLength : fd.content.length : 0;
|
|
3292
4500
|
infos.push({
|
|
3293
4501
|
path: p,
|
|
3294
4502
|
is_dir: false,
|
|
@@ -3296,7 +4504,7 @@ var StoreBackend = class {
|
|
|
3296
4504
|
modified_at: fd?.modified_at || ""
|
|
3297
4505
|
});
|
|
3298
4506
|
}
|
|
3299
|
-
return infos;
|
|
4507
|
+
return { files: infos };
|
|
3300
4508
|
}
|
|
3301
4509
|
/**
|
|
3302
4510
|
* Upload multiple files.
|
|
@@ -3309,7 +4517,11 @@ var StoreBackend = class {
|
|
|
3309
4517
|
const namespace = this.getNamespace();
|
|
3310
4518
|
const responses = [];
|
|
3311
4519
|
for (const [path, content] of files) try {
|
|
3312
|
-
const
|
|
4520
|
+
const mimeType = getMimeType(path);
|
|
4521
|
+
const isBinary = this.fileFormat === "v2" && !isTextMimeType(mimeType);
|
|
4522
|
+
let fileData;
|
|
4523
|
+
if (isBinary) fileData = createFileData(content, void 0, "v2", mimeType);
|
|
4524
|
+
else fileData = createFileData(new TextDecoder().decode(content), void 0, this.fileFormat, mimeType);
|
|
3313
4525
|
const storeValue = this.convertFileDataToStoreValue(fileData);
|
|
3314
4526
|
await store.put(namespace, path, storeValue);
|
|
3315
4527
|
responses.push({
|
|
@@ -3344,11 +4556,17 @@ var StoreBackend = class {
|
|
|
3344
4556
|
});
|
|
3345
4557
|
continue;
|
|
3346
4558
|
}
|
|
3347
|
-
const
|
|
3348
|
-
|
|
3349
|
-
|
|
4559
|
+
const fileDataV2 = migrateToFileDataV2(this.convertStoreItemToFileData(item), path);
|
|
4560
|
+
if (typeof fileDataV2.content === "string") {
|
|
4561
|
+
const content = new TextEncoder().encode(fileDataV2.content);
|
|
4562
|
+
responses.push({
|
|
4563
|
+
path,
|
|
4564
|
+
content,
|
|
4565
|
+
error: null
|
|
4566
|
+
});
|
|
4567
|
+
} else responses.push({
|
|
3350
4568
|
path,
|
|
3351
|
-
content,
|
|
4569
|
+
content: fileDataV2.content,
|
|
3352
4570
|
error: null
|
|
3353
4571
|
});
|
|
3354
4572
|
} catch {
|
|
@@ -3386,7 +4604,7 @@ var FilesystemBackend = class {
|
|
|
3386
4604
|
maxFileSizeBytes;
|
|
3387
4605
|
constructor(options = {}) {
|
|
3388
4606
|
const { rootDir, virtualMode = false, maxFileSizeMb = 10 } = options;
|
|
3389
|
-
this.cwd = rootDir ? path.resolve(rootDir) : process.cwd();
|
|
4607
|
+
this.cwd = rootDir ? path$1.resolve(rootDir) : process.cwd();
|
|
3390
4608
|
this.virtualMode = virtualMode;
|
|
3391
4609
|
this.maxFileSizeBytes = maxFileSizeMb * 1024 * 1024;
|
|
3392
4610
|
}
|
|
@@ -3406,13 +4624,13 @@ var FilesystemBackend = class {
|
|
|
3406
4624
|
if (this.virtualMode) {
|
|
3407
4625
|
const vpath = key.startsWith("/") ? key : "/" + key;
|
|
3408
4626
|
if (vpath.includes("..") || vpath.startsWith("~")) throw new Error("Path traversal not allowed");
|
|
3409
|
-
const full = path.resolve(this.cwd, vpath.substring(1));
|
|
3410
|
-
const relative = path.relative(this.cwd, full);
|
|
3411
|
-
if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Path: ${full} outside root directory: ${this.cwd}`);
|
|
4627
|
+
const full = path$1.resolve(this.cwd, vpath.substring(1));
|
|
4628
|
+
const relative = path$1.relative(this.cwd, full);
|
|
4629
|
+
if (relative.startsWith("..") || path$1.isAbsolute(relative)) throw new Error(`Path: ${full} outside root directory: ${this.cwd}`);
|
|
3412
4630
|
return full;
|
|
3413
4631
|
}
|
|
3414
|
-
if (path.isAbsolute(key)) return key;
|
|
3415
|
-
return path.resolve(this.cwd, key);
|
|
4632
|
+
if (path$1.isAbsolute(key)) return key;
|
|
4633
|
+
return path$1.resolve(this.cwd, key);
|
|
3416
4634
|
}
|
|
3417
4635
|
/**
|
|
3418
4636
|
* List files and directories in the specified directory (non-recursive).
|
|
@@ -3421,15 +4639,15 @@ var FilesystemBackend = class {
|
|
|
3421
4639
|
* @returns List of FileInfo objects for files and directories directly in the directory.
|
|
3422
4640
|
* Directories have a trailing / in their path and is_dir=true.
|
|
3423
4641
|
*/
|
|
3424
|
-
async
|
|
4642
|
+
async ls(dirPath) {
|
|
3425
4643
|
try {
|
|
3426
4644
|
const resolvedPath = this.resolvePath(dirPath);
|
|
3427
|
-
if (!(await fs.stat(resolvedPath)).isDirectory()) return [];
|
|
4645
|
+
if (!(await fs.stat(resolvedPath)).isDirectory()) return { files: [] };
|
|
3428
4646
|
const entries = await fs.readdir(resolvedPath, { withFileTypes: true });
|
|
3429
4647
|
const results = [];
|
|
3430
|
-
const cwdStr = this.cwd.endsWith(path.sep) ? this.cwd : this.cwd + path.sep;
|
|
4648
|
+
const cwdStr = this.cwd.endsWith(path$1.sep) ? this.cwd : this.cwd + path$1.sep;
|
|
3431
4649
|
for (const entry of entries) {
|
|
3432
|
-
const fullPath = path.join(resolvedPath, entry.name);
|
|
4650
|
+
const fullPath = path$1.join(resolvedPath, entry.name);
|
|
3433
4651
|
try {
|
|
3434
4652
|
const entryStat = await fs.stat(fullPath);
|
|
3435
4653
|
const isFile = entryStat.isFile();
|
|
@@ -3442,7 +4660,7 @@ var FilesystemBackend = class {
|
|
|
3442
4660
|
modified_at: entryStat.mtime.toISOString()
|
|
3443
4661
|
});
|
|
3444
4662
|
else if (isDir) results.push({
|
|
3445
|
-
path: fullPath + path.sep,
|
|
4663
|
+
path: fullPath + path$1.sep,
|
|
3446
4664
|
is_dir: true,
|
|
3447
4665
|
size: 0,
|
|
3448
4666
|
modified_at: entryStat.mtime.toISOString()
|
|
@@ -3452,7 +4670,7 @@ var FilesystemBackend = class {
|
|
|
3452
4670
|
if (fullPath.startsWith(cwdStr)) relativePath = fullPath.substring(cwdStr.length);
|
|
3453
4671
|
else if (fullPath.startsWith(this.cwd)) relativePath = fullPath.substring(this.cwd.length).replace(/^[/\\]/, "");
|
|
3454
4672
|
else relativePath = fullPath;
|
|
3455
|
-
relativePath = relativePath.split(path.sep).join("/");
|
|
4673
|
+
relativePath = relativePath.split(path$1.sep).join("/");
|
|
3456
4674
|
const virtPath = "/" + relativePath;
|
|
3457
4675
|
if (isFile) results.push({
|
|
3458
4676
|
path: virtPath,
|
|
@@ -3472,9 +4690,9 @@ var FilesystemBackend = class {
|
|
|
3472
4690
|
}
|
|
3473
4691
|
}
|
|
3474
4692
|
results.sort((a, b) => a.path.localeCompare(b.path));
|
|
3475
|
-
return results;
|
|
4693
|
+
return { files: results };
|
|
3476
4694
|
} catch {
|
|
3477
|
-
return [];
|
|
4695
|
+
return { files: [] };
|
|
3478
4696
|
}
|
|
3479
4697
|
}
|
|
3480
4698
|
/**
|
|
@@ -3488,62 +4706,105 @@ var FilesystemBackend = class {
|
|
|
3488
4706
|
async read(filePath, offset = 0, limit = 500) {
|
|
3489
4707
|
try {
|
|
3490
4708
|
const resolvedPath = this.resolvePath(filePath);
|
|
4709
|
+
const mimeType = getMimeType(filePath);
|
|
4710
|
+
const isBinary = !isTextMimeType(mimeType);
|
|
3491
4711
|
let content;
|
|
3492
4712
|
if (SUPPORTS_NOFOLLOW) {
|
|
3493
|
-
if (!(await fs.stat(resolvedPath)).isFile()) return
|
|
4713
|
+
if (!(await fs.stat(resolvedPath)).isFile()) return { error: `File '${filePath}' not found` };
|
|
3494
4714
|
const fd = await fs.open(resolvedPath, fs$1.constants.O_RDONLY | fs$1.constants.O_NOFOLLOW);
|
|
3495
4715
|
try {
|
|
4716
|
+
if (isBinary) {
|
|
4717
|
+
const buffer = await fd.readFile();
|
|
4718
|
+
return {
|
|
4719
|
+
content: new Uint8Array(buffer),
|
|
4720
|
+
mimeType
|
|
4721
|
+
};
|
|
4722
|
+
}
|
|
3496
4723
|
content = await fd.readFile({ encoding: "utf-8" });
|
|
3497
4724
|
} finally {
|
|
3498
4725
|
await fd.close();
|
|
3499
4726
|
}
|
|
3500
4727
|
} else {
|
|
3501
4728
|
const stat = await fs.lstat(resolvedPath);
|
|
3502
|
-
if (stat.isSymbolicLink()) return
|
|
3503
|
-
if (!stat.isFile()) return
|
|
4729
|
+
if (stat.isSymbolicLink()) return { error: `Symlinks are not allowed: ${filePath}` };
|
|
4730
|
+
if (!stat.isFile()) return { error: `File '${filePath}' not found` };
|
|
4731
|
+
if (isBinary) {
|
|
4732
|
+
const buffer = await fs.readFile(resolvedPath);
|
|
4733
|
+
return {
|
|
4734
|
+
content: new Uint8Array(buffer),
|
|
4735
|
+
mimeType
|
|
4736
|
+
};
|
|
4737
|
+
}
|
|
3504
4738
|
content = await fs.readFile(resolvedPath, "utf-8");
|
|
3505
4739
|
}
|
|
3506
4740
|
const emptyMsg = checkEmptyContent(content);
|
|
3507
|
-
if (emptyMsg) return
|
|
4741
|
+
if (emptyMsg) return {
|
|
4742
|
+
content: emptyMsg,
|
|
4743
|
+
mimeType
|
|
4744
|
+
};
|
|
3508
4745
|
const lines = content.split("\n");
|
|
3509
4746
|
const startIdx = offset;
|
|
3510
4747
|
const endIdx = Math.min(startIdx + limit, lines.length);
|
|
3511
|
-
if (startIdx >= lines.length) return
|
|
3512
|
-
return
|
|
4748
|
+
if (startIdx >= lines.length) return { error: `Line offset ${offset} exceeds file length (${lines.length} lines)` };
|
|
4749
|
+
return {
|
|
4750
|
+
content: lines.slice(startIdx, endIdx).join("\n"),
|
|
4751
|
+
mimeType
|
|
4752
|
+
};
|
|
3513
4753
|
} catch (e) {
|
|
3514
|
-
return `Error reading file '${filePath}': ${e.message}
|
|
4754
|
+
return { error: `Error reading file '${filePath}': ${e.message}` };
|
|
3515
4755
|
}
|
|
3516
4756
|
}
|
|
3517
4757
|
/**
|
|
3518
4758
|
* Read file content as raw FileData.
|
|
3519
4759
|
*
|
|
3520
4760
|
* @param filePath - Absolute file path
|
|
3521
|
-
* @returns
|
|
4761
|
+
* @returns ReadRawResult with raw file data on success or error on failure
|
|
3522
4762
|
*/
|
|
3523
4763
|
async readRaw(filePath) {
|
|
3524
4764
|
const resolvedPath = this.resolvePath(filePath);
|
|
4765
|
+
const mimeType = getMimeType(filePath);
|
|
4766
|
+
const isBinary = !isTextMimeType(mimeType);
|
|
3525
4767
|
let content;
|
|
3526
4768
|
let stat;
|
|
3527
4769
|
if (SUPPORTS_NOFOLLOW) {
|
|
3528
4770
|
stat = await fs.stat(resolvedPath);
|
|
3529
|
-
if (!stat.isFile())
|
|
4771
|
+
if (!stat.isFile()) return { error: `File '${filePath}' not found` };
|
|
3530
4772
|
const fd = await fs.open(resolvedPath, fs$1.constants.O_RDONLY | fs$1.constants.O_NOFOLLOW);
|
|
3531
4773
|
try {
|
|
4774
|
+
if (isBinary) {
|
|
4775
|
+
const buffer = await fd.readFile();
|
|
4776
|
+
return { data: {
|
|
4777
|
+
content: new Uint8Array(buffer),
|
|
4778
|
+
mimeType,
|
|
4779
|
+
created_at: stat.ctime.toISOString(),
|
|
4780
|
+
modified_at: stat.mtime.toISOString()
|
|
4781
|
+
} };
|
|
4782
|
+
}
|
|
3532
4783
|
content = await fd.readFile({ encoding: "utf-8" });
|
|
3533
4784
|
} finally {
|
|
3534
4785
|
await fd.close();
|
|
3535
4786
|
}
|
|
3536
4787
|
} else {
|
|
3537
4788
|
stat = await fs.lstat(resolvedPath);
|
|
3538
|
-
if (stat.isSymbolicLink())
|
|
3539
|
-
if (!stat.isFile())
|
|
4789
|
+
if (stat.isSymbolicLink()) return { error: `Symlinks are not allowed: ${filePath}` };
|
|
4790
|
+
if (!stat.isFile()) return { error: `File '${filePath}' not found` };
|
|
4791
|
+
if (isBinary) {
|
|
4792
|
+
const buffer = await fs.readFile(resolvedPath);
|
|
4793
|
+
return { data: {
|
|
4794
|
+
content: new Uint8Array(buffer),
|
|
4795
|
+
mimeType,
|
|
4796
|
+
created_at: stat.ctime.toISOString(),
|
|
4797
|
+
modified_at: stat.mtime.toISOString()
|
|
4798
|
+
} };
|
|
4799
|
+
}
|
|
3540
4800
|
content = await fs.readFile(resolvedPath, "utf-8");
|
|
3541
4801
|
}
|
|
3542
|
-
return {
|
|
3543
|
-
content
|
|
4802
|
+
return { data: {
|
|
4803
|
+
content,
|
|
4804
|
+
mimeType,
|
|
3544
4805
|
created_at: stat.ctime.toISOString(),
|
|
3545
4806
|
modified_at: stat.mtime.toISOString()
|
|
3546
|
-
};
|
|
4807
|
+
} };
|
|
3547
4808
|
}
|
|
3548
4809
|
/**
|
|
3549
4810
|
* Create a new file with content.
|
|
@@ -3552,19 +4813,26 @@ var FilesystemBackend = class {
|
|
|
3552
4813
|
async write(filePath, content) {
|
|
3553
4814
|
try {
|
|
3554
4815
|
const resolvedPath = this.resolvePath(filePath);
|
|
4816
|
+
const isBinary = !isTextMimeType(getMimeType(filePath));
|
|
3555
4817
|
try {
|
|
3556
4818
|
if ((await fs.lstat(resolvedPath)).isSymbolicLink()) return { error: `Cannot write to ${filePath} because it is a symlink. Symlinks are not allowed.` };
|
|
3557
4819
|
return { error: `Cannot write to ${filePath} because it already exists. Read and then make an edit, or write to a new path.` };
|
|
3558
4820
|
} catch {}
|
|
3559
|
-
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
4821
|
+
await fs.mkdir(path$1.dirname(resolvedPath), { recursive: true });
|
|
3560
4822
|
if (SUPPORTS_NOFOLLOW) {
|
|
3561
4823
|
const flags = fs$1.constants.O_WRONLY | fs$1.constants.O_CREAT | fs$1.constants.O_TRUNC | fs$1.constants.O_NOFOLLOW;
|
|
3562
4824
|
const fd = await fs.open(resolvedPath, flags, 420);
|
|
3563
4825
|
try {
|
|
3564
|
-
|
|
4826
|
+
if (isBinary) {
|
|
4827
|
+
const buffer = Buffer.from(content, "base64");
|
|
4828
|
+
await fd.writeFile(buffer);
|
|
4829
|
+
} else await fd.writeFile(content, "utf-8");
|
|
3565
4830
|
} finally {
|
|
3566
4831
|
await fd.close();
|
|
3567
4832
|
}
|
|
4833
|
+
} else if (isBinary) {
|
|
4834
|
+
const buffer = Buffer.from(content, "base64");
|
|
4835
|
+
await fs.writeFile(resolvedPath, buffer);
|
|
3568
4836
|
} else await fs.writeFile(resolvedPath, content, "utf-8");
|
|
3569
4837
|
return {
|
|
3570
4838
|
path: filePath,
|
|
@@ -3627,17 +4895,17 @@ var FilesystemBackend = class {
|
|
|
3627
4895
|
* @param glob - Optional glob pattern to filter which files to search.
|
|
3628
4896
|
* @returns List of GrepMatch dicts containing path, line number, and matched text.
|
|
3629
4897
|
*/
|
|
3630
|
-
async
|
|
4898
|
+
async grep(pattern, dirPath = "/", glob = null) {
|
|
3631
4899
|
let baseFull;
|
|
3632
4900
|
try {
|
|
3633
4901
|
baseFull = this.resolvePath(dirPath || ".");
|
|
3634
4902
|
} catch {
|
|
3635
|
-
return [];
|
|
4903
|
+
return { matches: [] };
|
|
3636
4904
|
}
|
|
3637
4905
|
try {
|
|
3638
4906
|
await fs.stat(baseFull);
|
|
3639
4907
|
} catch {
|
|
3640
|
-
return [];
|
|
4908
|
+
return { matches: [] };
|
|
3641
4909
|
}
|
|
3642
4910
|
let results = await this.ripgrepSearch(pattern, baseFull, glob);
|
|
3643
4911
|
if (results === null) results = await this.literalSearch(pattern, baseFull, glob);
|
|
@@ -3647,7 +4915,7 @@ var FilesystemBackend = class {
|
|
|
3647
4915
|
line: lineNum,
|
|
3648
4916
|
text: lineText
|
|
3649
4917
|
});
|
|
3650
|
-
return matches;
|
|
4918
|
+
return { matches };
|
|
3651
4919
|
}
|
|
3652
4920
|
/**
|
|
3653
4921
|
* Search using ripgrep with fixed-string (literal) mode.
|
|
@@ -3684,10 +4952,10 @@ var FilesystemBackend = class {
|
|
|
3684
4952
|
if (!ftext) continue;
|
|
3685
4953
|
let virtPath;
|
|
3686
4954
|
if (this.virtualMode) try {
|
|
3687
|
-
const resolved = path.resolve(ftext);
|
|
3688
|
-
const relative = path.relative(this.cwd, resolved);
|
|
4955
|
+
const resolved = path$1.resolve(ftext);
|
|
4956
|
+
const relative = path$1.relative(this.cwd, resolved);
|
|
3689
4957
|
if (relative.startsWith("..")) continue;
|
|
3690
|
-
virtPath = "/" + relative.split(path.sep).join("/");
|
|
4958
|
+
virtPath = "/" + relative.split(path$1.sep).join("/");
|
|
3691
4959
|
} catch {
|
|
3692
4960
|
continue;
|
|
3693
4961
|
}
|
|
@@ -3721,13 +4989,14 @@ var FilesystemBackend = class {
|
|
|
3721
4989
|
async literalSearch(pattern, baseFull, includeGlob) {
|
|
3722
4990
|
const results = {};
|
|
3723
4991
|
const files = await fg("**/*", {
|
|
3724
|
-
cwd: (await fs.stat(baseFull)).isDirectory() ? baseFull : path.dirname(baseFull),
|
|
4992
|
+
cwd: (await fs.stat(baseFull)).isDirectory() ? baseFull : path$1.dirname(baseFull),
|
|
3725
4993
|
absolute: true,
|
|
3726
4994
|
onlyFiles: true,
|
|
3727
4995
|
dot: true
|
|
3728
4996
|
});
|
|
3729
4997
|
for (const fp of files) try {
|
|
3730
|
-
if (
|
|
4998
|
+
if (!isTextMimeType(getMimeType(fp))) continue;
|
|
4999
|
+
if (includeGlob && !micromatch.isMatch(path$1.basename(fp), includeGlob)) continue;
|
|
3731
5000
|
if ((await fs.stat(fp)).size > this.maxFileSizeBytes) continue;
|
|
3732
5001
|
const lines = (await fs.readFile(fp, "utf-8")).split("\n");
|
|
3733
5002
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -3735,9 +5004,9 @@ var FilesystemBackend = class {
|
|
|
3735
5004
|
if (line.includes(pattern)) {
|
|
3736
5005
|
let virtPath;
|
|
3737
5006
|
if (this.virtualMode) try {
|
|
3738
|
-
const relative = path.relative(this.cwd, fp);
|
|
5007
|
+
const relative = path$1.relative(this.cwd, fp);
|
|
3739
5008
|
if (relative.startsWith("..")) continue;
|
|
3740
|
-
virtPath = "/" + relative.split(path.sep).join("/");
|
|
5009
|
+
virtPath = "/" + relative.split(path$1.sep).join("/");
|
|
3741
5010
|
} catch {
|
|
3742
5011
|
continue;
|
|
3743
5012
|
}
|
|
@@ -3754,13 +5023,13 @@ var FilesystemBackend = class {
|
|
|
3754
5023
|
/**
|
|
3755
5024
|
* Structured glob matching returning FileInfo objects.
|
|
3756
5025
|
*/
|
|
3757
|
-
async
|
|
5026
|
+
async glob(pattern, searchPath = "/") {
|
|
3758
5027
|
if (pattern.startsWith("/")) pattern = pattern.substring(1);
|
|
3759
5028
|
const resolvedSearchPath = searchPath === "/" ? this.cwd : this.resolvePath(searchPath);
|
|
3760
5029
|
try {
|
|
3761
|
-
if (!(await fs.stat(resolvedSearchPath)).isDirectory()) return [];
|
|
5030
|
+
if (!(await fs.stat(resolvedSearchPath)).isDirectory()) return { files: [] };
|
|
3762
5031
|
} catch {
|
|
3763
|
-
return [];
|
|
5032
|
+
return { files: [] };
|
|
3764
5033
|
}
|
|
3765
5034
|
const results = [];
|
|
3766
5035
|
try {
|
|
@@ -3773,7 +5042,7 @@ var FilesystemBackend = class {
|
|
|
3773
5042
|
for (const matchedPath of matches) try {
|
|
3774
5043
|
const stat = await fs.stat(matchedPath);
|
|
3775
5044
|
if (!stat.isFile()) continue;
|
|
3776
|
-
const normalizedPath = matchedPath.split("/").join(path.sep);
|
|
5045
|
+
const normalizedPath = matchedPath.split("/").join(path$1.sep);
|
|
3777
5046
|
if (!this.virtualMode) results.push({
|
|
3778
5047
|
path: normalizedPath,
|
|
3779
5048
|
is_dir: false,
|
|
@@ -3781,12 +5050,12 @@ var FilesystemBackend = class {
|
|
|
3781
5050
|
modified_at: stat.mtime.toISOString()
|
|
3782
5051
|
});
|
|
3783
5052
|
else {
|
|
3784
|
-
const cwdStr = this.cwd.endsWith(path.sep) ? this.cwd : this.cwd + path.sep;
|
|
5053
|
+
const cwdStr = this.cwd.endsWith(path$1.sep) ? this.cwd : this.cwd + path$1.sep;
|
|
3785
5054
|
let relativePath;
|
|
3786
5055
|
if (normalizedPath.startsWith(cwdStr)) relativePath = normalizedPath.substring(cwdStr.length);
|
|
3787
5056
|
else if (normalizedPath.startsWith(this.cwd)) relativePath = normalizedPath.substring(this.cwd.length).replace(/^[/\\]/, "");
|
|
3788
5057
|
else relativePath = normalizedPath;
|
|
3789
|
-
relativePath = relativePath.split(path.sep).join("/");
|
|
5058
|
+
relativePath = relativePath.split(path$1.sep).join("/");
|
|
3790
5059
|
const virt = "/" + relativePath;
|
|
3791
5060
|
results.push({
|
|
3792
5061
|
path: virt,
|
|
@@ -3800,7 +5069,7 @@ var FilesystemBackend = class {
|
|
|
3800
5069
|
}
|
|
3801
5070
|
} catch {}
|
|
3802
5071
|
results.sort((a, b) => a.path.localeCompare(b.path));
|
|
3803
|
-
return results;
|
|
5072
|
+
return { files: results };
|
|
3804
5073
|
}
|
|
3805
5074
|
/**
|
|
3806
5075
|
* Upload multiple files to the filesystem.
|
|
@@ -3812,7 +5081,7 @@ var FilesystemBackend = class {
|
|
|
3812
5081
|
const responses = [];
|
|
3813
5082
|
for (const [filePath, content] of files) try {
|
|
3814
5083
|
const resolvedPath = this.resolvePath(filePath);
|
|
3815
|
-
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
5084
|
+
await fs.mkdir(path$1.dirname(resolvedPath), { recursive: true });
|
|
3816
5085
|
await fs.writeFile(resolvedPath, content);
|
|
3817
5086
|
responses.push({
|
|
3818
5087
|
path: filePath,
|
|
@@ -3895,9 +5164,9 @@ var CompositeBackend = class {
|
|
|
3895
5164
|
routes;
|
|
3896
5165
|
sortedRoutes;
|
|
3897
5166
|
constructor(defaultBackend, routes) {
|
|
3898
|
-
this.default = defaultBackend;
|
|
3899
|
-
this.routes = routes;
|
|
3900
|
-
this.sortedRoutes = Object.entries(routes).sort((a, b) => b[0].length - a[0].length);
|
|
5167
|
+
this.default = isSandboxProtocol(defaultBackend) ? adaptSandboxProtocol(defaultBackend) : adaptBackendProtocol(defaultBackend);
|
|
5168
|
+
this.routes = Object.fromEntries(Object.entries(routes).map(([k, v]) => [k, isSandboxProtocol(v) ? adaptSandboxProtocol(v) : adaptBackendProtocol(v)]));
|
|
5169
|
+
this.sortedRoutes = Object.entries(this.routes).sort((a, b) => b[0].length - a[0].length);
|
|
3901
5170
|
}
|
|
3902
5171
|
/** Delegates to default backend's id if it is a sandbox, otherwise empty string. */
|
|
3903
5172
|
get id() {
|
|
@@ -3921,25 +5190,27 @@ var CompositeBackend = class {
|
|
|
3921
5190
|
* List files and directories in the specified directory (non-recursive).
|
|
3922
5191
|
*
|
|
3923
5192
|
* @param path - Absolute path to directory
|
|
3924
|
-
* @returns
|
|
3925
|
-
*
|
|
5193
|
+
* @returns LsResult with list of FileInfo objects (with route prefixes added) on success or error on failure.
|
|
5194
|
+
* Directories have a trailing / in their path and is_dir=true.
|
|
3926
5195
|
*/
|
|
3927
|
-
async
|
|
5196
|
+
async ls(path) {
|
|
3928
5197
|
for (const [routePrefix, backend] of this.sortedRoutes) if (path.startsWith(routePrefix.replace(/\/$/, ""))) {
|
|
3929
5198
|
const suffix = path.substring(routePrefix.length);
|
|
3930
5199
|
const searchPath = suffix ? "/" + suffix : "/";
|
|
3931
|
-
const
|
|
5200
|
+
const result = await backend.ls(searchPath);
|
|
5201
|
+
if (result.error) return result;
|
|
3932
5202
|
const prefixed = [];
|
|
3933
|
-
for (const fi of
|
|
5203
|
+
for (const fi of result.files || []) prefixed.push({
|
|
3934
5204
|
...fi,
|
|
3935
5205
|
path: routePrefix.slice(0, -1) + fi.path
|
|
3936
5206
|
});
|
|
3937
|
-
return prefixed;
|
|
5207
|
+
return { files: prefixed };
|
|
3938
5208
|
}
|
|
3939
5209
|
if (path === "/") {
|
|
3940
5210
|
const results = [];
|
|
3941
|
-
const
|
|
3942
|
-
|
|
5211
|
+
const defaultResult = await this.default.ls(path);
|
|
5212
|
+
if (defaultResult.error) return defaultResult;
|
|
5213
|
+
results.push(...defaultResult.files || []);
|
|
3943
5214
|
for (const [routePrefix] of this.sortedRoutes) results.push({
|
|
3944
5215
|
path: routePrefix,
|
|
3945
5216
|
is_dir: true,
|
|
@@ -3947,9 +5218,9 @@ var CompositeBackend = class {
|
|
|
3947
5218
|
modified_at: ""
|
|
3948
5219
|
});
|
|
3949
5220
|
results.sort((a, b) => a.path.localeCompare(b.path));
|
|
3950
|
-
return results;
|
|
5221
|
+
return { files: results };
|
|
3951
5222
|
}
|
|
3952
|
-
return await this.default.
|
|
5223
|
+
return await this.default.ls(path);
|
|
3953
5224
|
}
|
|
3954
5225
|
/**
|
|
3955
5226
|
* Read file content, routing to appropriate backend.
|
|
@@ -3967,7 +5238,7 @@ var CompositeBackend = class {
|
|
|
3967
5238
|
* Read file content as raw FileData.
|
|
3968
5239
|
*
|
|
3969
5240
|
* @param filePath - Absolute file path
|
|
3970
|
-
* @returns
|
|
5241
|
+
* @returns ReadRawResult with raw file data on success or error on failure
|
|
3971
5242
|
*/
|
|
3972
5243
|
async readRaw(filePath) {
|
|
3973
5244
|
const [backend, strippedKey] = this.getBackendAndKey(filePath);
|
|
@@ -3976,53 +5247,59 @@ var CompositeBackend = class {
|
|
|
3976
5247
|
/**
|
|
3977
5248
|
* Structured search results or error string for invalid input.
|
|
3978
5249
|
*/
|
|
3979
|
-
async
|
|
5250
|
+
async grep(pattern, path = "/", glob = null) {
|
|
3980
5251
|
for (const [routePrefix, backend] of this.sortedRoutes) if (path.startsWith(routePrefix.replace(/\/$/, ""))) {
|
|
3981
5252
|
const searchPath = path.substring(routePrefix.length - 1);
|
|
3982
|
-
const raw = await backend.
|
|
3983
|
-
if (
|
|
3984
|
-
return raw.map((m) => ({
|
|
5253
|
+
const raw = await backend.grep(pattern, searchPath || "/", glob);
|
|
5254
|
+
if (raw.error) return raw;
|
|
5255
|
+
return { matches: (raw.matches || []).map((m) => ({
|
|
3985
5256
|
...m,
|
|
3986
5257
|
path: routePrefix.slice(0, -1) + m.path
|
|
3987
|
-
}));
|
|
5258
|
+
})) };
|
|
3988
5259
|
}
|
|
3989
5260
|
const allMatches = [];
|
|
3990
|
-
const rawDefault = await this.default.
|
|
3991
|
-
if (
|
|
3992
|
-
allMatches.push(...rawDefault);
|
|
5261
|
+
const rawDefault = await this.default.grep(pattern, path, glob);
|
|
5262
|
+
if (rawDefault.error) return rawDefault;
|
|
5263
|
+
allMatches.push(...rawDefault.matches || []);
|
|
3993
5264
|
for (const [routePrefix, backend] of Object.entries(this.routes)) {
|
|
3994
|
-
const raw = await backend.
|
|
3995
|
-
if (
|
|
3996
|
-
|
|
5265
|
+
const raw = await backend.grep(pattern, "/", glob);
|
|
5266
|
+
if (raw.error) return raw;
|
|
5267
|
+
const matches = (raw.matches || []).map((m) => ({
|
|
3997
5268
|
...m,
|
|
3998
5269
|
path: routePrefix.slice(0, -1) + m.path
|
|
3999
|
-
}))
|
|
5270
|
+
}));
|
|
5271
|
+
allMatches.push(...matches);
|
|
4000
5272
|
}
|
|
4001
|
-
return allMatches;
|
|
5273
|
+
return { matches: allMatches };
|
|
4002
5274
|
}
|
|
4003
5275
|
/**
|
|
4004
5276
|
* Structured glob matching returning FileInfo objects.
|
|
4005
5277
|
*/
|
|
4006
|
-
async
|
|
5278
|
+
async glob(pattern, path = "/") {
|
|
4007
5279
|
const results = [];
|
|
4008
5280
|
for (const [routePrefix, backend] of this.sortedRoutes) if (path.startsWith(routePrefix.replace(/\/$/, ""))) {
|
|
4009
5281
|
const searchPath = path.substring(routePrefix.length - 1);
|
|
4010
|
-
|
|
5282
|
+
const result = await backend.glob(pattern, searchPath || "/");
|
|
5283
|
+
if (result.error) return result;
|
|
5284
|
+
return { files: (result.files || []).map((fi) => ({
|
|
4011
5285
|
...fi,
|
|
4012
5286
|
path: routePrefix.slice(0, -1) + fi.path
|
|
4013
|
-
}));
|
|
5287
|
+
})) };
|
|
4014
5288
|
}
|
|
4015
|
-
const
|
|
4016
|
-
|
|
5289
|
+
const defaultResult = await this.default.glob(pattern, path);
|
|
5290
|
+
if (defaultResult.error) return defaultResult;
|
|
5291
|
+
results.push(...defaultResult.files || []);
|
|
4017
5292
|
for (const [routePrefix, backend] of Object.entries(this.routes)) {
|
|
4018
|
-
const
|
|
4019
|
-
|
|
5293
|
+
const result = await backend.glob(pattern, "/");
|
|
5294
|
+
if (result.error) continue;
|
|
5295
|
+
const files = (result.files || []).map((fi) => ({
|
|
4020
5296
|
...fi,
|
|
4021
5297
|
path: routePrefix.slice(0, -1) + fi.path
|
|
4022
|
-
}))
|
|
5298
|
+
}));
|
|
5299
|
+
results.push(...files);
|
|
4023
5300
|
}
|
|
4024
5301
|
results.sort((a, b) => a.path.localeCompare(b.path));
|
|
4025
|
-
return results;
|
|
5302
|
+
return { files: results };
|
|
4026
5303
|
}
|
|
4027
5304
|
/**
|
|
4028
5305
|
* Create a new file, routing to appropriate backend.
|
|
@@ -4250,7 +5527,7 @@ var LocalShellBackend = class LocalShellBackend extends FilesystemBackend {
|
|
|
4250
5527
|
*/
|
|
4251
5528
|
async read(filePath, offset = 0, limit = 500) {
|
|
4252
5529
|
const result = await super.read(filePath, offset, limit);
|
|
4253
|
-
if (
|
|
5530
|
+
if (result.error?.includes("ENOENT")) return { error: `File '${filePath}' not found` };
|
|
4254
5531
|
return result;
|
|
4255
5532
|
}
|
|
4256
5533
|
/**
|
|
@@ -4267,25 +5544,26 @@ var LocalShellBackend = class LocalShellBackend extends FilesystemBackend {
|
|
|
4267
5544
|
/**
|
|
4268
5545
|
* List directory contents, returning paths relative to rootDir.
|
|
4269
5546
|
*/
|
|
4270
|
-
async
|
|
4271
|
-
const
|
|
4272
|
-
if (
|
|
4273
|
-
|
|
4274
|
-
|
|
5547
|
+
async ls(dirPath) {
|
|
5548
|
+
const result = await super.ls(dirPath);
|
|
5549
|
+
if (result.error) return result;
|
|
5550
|
+
if (this.virtualMode) return result;
|
|
5551
|
+
const cwdPrefix = this.cwd.endsWith(path$1.sep) ? this.cwd : this.cwd + path$1.sep;
|
|
5552
|
+
return { files: (result.files || []).map((info) => ({
|
|
4275
5553
|
...info,
|
|
4276
5554
|
path: info.path.startsWith(cwdPrefix) ? info.path.slice(cwdPrefix.length) : info.path
|
|
4277
|
-
}));
|
|
5555
|
+
})) };
|
|
4278
5556
|
}
|
|
4279
5557
|
/**
|
|
4280
5558
|
* Glob matching that returns relative paths and includes directories.
|
|
4281
5559
|
*/
|
|
4282
|
-
async
|
|
5560
|
+
async glob(pattern, searchPath = "/") {
|
|
4283
5561
|
if (pattern.startsWith("/")) pattern = pattern.substring(1);
|
|
4284
|
-
const resolvedSearchPath = searchPath === "/" || searchPath === "" ? this.cwd : this.virtualMode ? path.resolve(this.cwd, searchPath.replace(/^\//, "")) : path.resolve(this.cwd, searchPath);
|
|
5562
|
+
const resolvedSearchPath = searchPath === "/" || searchPath === "" ? this.cwd : this.virtualMode ? path$1.resolve(this.cwd, searchPath.replace(/^\//, "")) : path$1.resolve(this.cwd, searchPath);
|
|
4285
5563
|
try {
|
|
4286
|
-
if (!(await fs.stat(resolvedSearchPath)).isDirectory()) return [];
|
|
5564
|
+
if (!(await fs.stat(resolvedSearchPath)).isDirectory()) return { files: [] };
|
|
4287
5565
|
} catch {
|
|
4288
|
-
return [];
|
|
5566
|
+
return { files: [] };
|
|
4289
5567
|
}
|
|
4290
5568
|
const formatPath = (rel) => this.virtualMode ? `/${rel}` : rel;
|
|
4291
5569
|
const globOpts = {
|
|
@@ -4302,7 +5580,7 @@ var LocalShellBackend = class LocalShellBackend extends FilesystemBackend {
|
|
|
4302
5580
|
})]);
|
|
4303
5581
|
const statFile = async (match) => {
|
|
4304
5582
|
try {
|
|
4305
|
-
const entryStat = await fs.stat(path.join(resolvedSearchPath, match));
|
|
5583
|
+
const entryStat = await fs.stat(path$1.join(resolvedSearchPath, match));
|
|
4306
5584
|
if (entryStat.isFile()) return {
|
|
4307
5585
|
path: formatPath(match),
|
|
4308
5586
|
is_dir: false,
|
|
@@ -4314,7 +5592,7 @@ var LocalShellBackend = class LocalShellBackend extends FilesystemBackend {
|
|
|
4314
5592
|
};
|
|
4315
5593
|
const statDir = async (match) => {
|
|
4316
5594
|
try {
|
|
4317
|
-
const entryStat = await fs.stat(path.join(resolvedSearchPath, match));
|
|
5595
|
+
const entryStat = await fs.stat(path$1.join(resolvedSearchPath, match));
|
|
4318
5596
|
if (entryStat.isDirectory()) return {
|
|
4319
5597
|
path: formatPath(match),
|
|
4320
5598
|
is_dir: true,
|
|
@@ -4327,7 +5605,7 @@ var LocalShellBackend = class LocalShellBackend extends FilesystemBackend {
|
|
|
4327
5605
|
const [fileInfos, dirInfos] = await Promise.all([Promise.all(fileMatches.map(statFile)), Promise.all(dirMatches.map(statDir))]);
|
|
4328
5606
|
const results = [...fileInfos, ...dirInfos].filter((info) => info !== null);
|
|
4329
5607
|
results.sort((a, b) => a.path.localeCompare(b.path));
|
|
4330
|
-
return results;
|
|
5608
|
+
return { files: results };
|
|
4331
5609
|
}
|
|
4332
5610
|
/**
|
|
4333
5611
|
* Execute a shell command directly on the host system.
|
|
@@ -4602,9 +5880,9 @@ var BaseSandbox = class {
|
|
|
4602
5880
|
* including Alpine. No Python or Node.js needed.
|
|
4603
5881
|
*
|
|
4604
5882
|
* @param path - Absolute path to directory
|
|
4605
|
-
* @returns
|
|
5883
|
+
* @returns LsResult with list of FileInfo objects on success or error on failure.
|
|
4606
5884
|
*/
|
|
4607
|
-
async
|
|
5885
|
+
async ls(path) {
|
|
4608
5886
|
const command = buildLsCommand(path);
|
|
4609
5887
|
const result = await this.execute(command);
|
|
4610
5888
|
const infos = [];
|
|
@@ -4619,7 +5897,7 @@ var BaseSandbox = class {
|
|
|
4619
5897
|
modified_at: (/* @__PURE__ */ new Date(parsed.mtime * 1e3)).toISOString()
|
|
4620
5898
|
});
|
|
4621
5899
|
}
|
|
4622
|
-
return infos;
|
|
5900
|
+
return { files: infos };
|
|
4623
5901
|
}
|
|
4624
5902
|
/**
|
|
4625
5903
|
* Read file content with line numbers.
|
|
@@ -4634,11 +5912,26 @@ var BaseSandbox = class {
|
|
|
4634
5912
|
* @returns Formatted file content with line numbers, or error message
|
|
4635
5913
|
*/
|
|
4636
5914
|
async read(filePath, offset = 0, limit = 500) {
|
|
4637
|
-
|
|
5915
|
+
const mimeType = getMimeType(filePath);
|
|
5916
|
+
if (!isTextMimeType(mimeType)) {
|
|
5917
|
+
const results = await this.downloadFiles([filePath]);
|
|
5918
|
+
if (results[0].error || !results[0].content) return { error: `File '${filePath}' not found` };
|
|
5919
|
+
return {
|
|
5920
|
+
content: results[0].content,
|
|
5921
|
+
mimeType
|
|
5922
|
+
};
|
|
5923
|
+
}
|
|
5924
|
+
if (limit === 0) return {
|
|
5925
|
+
content: "",
|
|
5926
|
+
mimeType
|
|
5927
|
+
};
|
|
4638
5928
|
const command = buildReadCommand(filePath, offset, limit);
|
|
4639
5929
|
const result = await this.execute(command);
|
|
4640
|
-
if (result.exitCode !== 0) return
|
|
4641
|
-
return
|
|
5930
|
+
if (result.exitCode !== 0) return { error: `File '${filePath}' not found` };
|
|
5931
|
+
return {
|
|
5932
|
+
content: result.output,
|
|
5933
|
+
mimeType
|
|
5934
|
+
};
|
|
4642
5935
|
}
|
|
4643
5936
|
/**
|
|
4644
5937
|
* Read file content as raw FileData.
|
|
@@ -4646,18 +5939,25 @@ var BaseSandbox = class {
|
|
|
4646
5939
|
* Uses downloadFiles() directly — no runtime needed on the sandbox host.
|
|
4647
5940
|
*
|
|
4648
5941
|
* @param filePath - Absolute file path
|
|
4649
|
-
* @returns
|
|
5942
|
+
* @returns ReadRawResult with raw file data on success or error on failure
|
|
4650
5943
|
*/
|
|
4651
5944
|
async readRaw(filePath) {
|
|
4652
5945
|
const results = await this.downloadFiles([filePath]);
|
|
4653
|
-
if (results[0].error || !results[0].content)
|
|
4654
|
-
const lines = new TextDecoder().decode(results[0].content).split("\n");
|
|
5946
|
+
if (results[0].error || !results[0].content) return { error: `File '${filePath}' not found` };
|
|
4655
5947
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4656
|
-
|
|
4657
|
-
|
|
5948
|
+
const mimeType = getMimeType(filePath);
|
|
5949
|
+
if (!isTextMimeType(mimeType)) return { data: {
|
|
5950
|
+
content: results[0].content,
|
|
5951
|
+
mimeType,
|
|
4658
5952
|
created_at: now,
|
|
4659
5953
|
modified_at: now
|
|
4660
|
-
};
|
|
5954
|
+
} };
|
|
5955
|
+
return { data: {
|
|
5956
|
+
content: new TextDecoder().decode(results[0].content),
|
|
5957
|
+
mimeType,
|
|
5958
|
+
created_at: now,
|
|
5959
|
+
modified_at: now
|
|
5960
|
+
} };
|
|
4661
5961
|
}
|
|
4662
5962
|
/**
|
|
4663
5963
|
* Search for a literal text pattern in files using grep.
|
|
@@ -4667,23 +5967,25 @@ var BaseSandbox = class {
|
|
|
4667
5967
|
* @param glob - Optional glob pattern to filter which files to search.
|
|
4668
5968
|
* @returns List of GrepMatch dicts containing path, line number, and matched text.
|
|
4669
5969
|
*/
|
|
4670
|
-
async
|
|
5970
|
+
async grep(pattern, path = "/", glob = null) {
|
|
4671
5971
|
const command = buildGrepCommand(pattern, path, glob);
|
|
4672
5972
|
const output = (await this.execute(command)).output.trim();
|
|
4673
|
-
if (!output) return [];
|
|
5973
|
+
if (!output) return { matches: [] };
|
|
4674
5974
|
const matches = [];
|
|
4675
5975
|
for (const line of output.split("\n")) {
|
|
4676
5976
|
const parts = line.split(":");
|
|
4677
5977
|
if (parts.length >= 3) {
|
|
5978
|
+
const filePath = parts[0];
|
|
5979
|
+
if (!isTextMimeType(getMimeType(filePath))) continue;
|
|
4678
5980
|
const lineNum = parseInt(parts[1], 10);
|
|
4679
5981
|
if (!isNaN(lineNum)) matches.push({
|
|
4680
|
-
path:
|
|
5982
|
+
path: filePath,
|
|
4681
5983
|
line: lineNum,
|
|
4682
5984
|
text: parts.slice(2).join(":")
|
|
4683
5985
|
});
|
|
4684
5986
|
}
|
|
4685
5987
|
}
|
|
4686
|
-
return matches;
|
|
5988
|
+
return { matches };
|
|
4687
5989
|
}
|
|
4688
5990
|
/**
|
|
4689
5991
|
* Structured glob matching returning FileInfo objects.
|
|
@@ -4698,7 +6000,7 @@ var BaseSandbox = class {
|
|
|
4698
6000
|
* - `?` matches a single character except `/`
|
|
4699
6001
|
* - `[...]` character classes
|
|
4700
6002
|
*/
|
|
4701
|
-
async
|
|
6003
|
+
async glob(pattern, path = "/") {
|
|
4702
6004
|
const command = buildFindCommand(path);
|
|
4703
6005
|
const result = await this.execute(command);
|
|
4704
6006
|
const regex = globToPathRegex(pattern);
|
|
@@ -4716,7 +6018,7 @@ var BaseSandbox = class {
|
|
|
4716
6018
|
modified_at: (/* @__PURE__ */ new Date(parsed.mtime * 1e3)).toISOString()
|
|
4717
6019
|
});
|
|
4718
6020
|
}
|
|
4719
|
-
return infos;
|
|
6021
|
+
return { files: infos };
|
|
4720
6022
|
}
|
|
4721
6023
|
/**
|
|
4722
6024
|
* Create a new file with content.
|
|
@@ -4729,8 +6031,11 @@ var BaseSandbox = class {
|
|
|
4729
6031
|
const existCheck = await this.downloadFiles([filePath]);
|
|
4730
6032
|
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.` };
|
|
4731
6033
|
} catch {}
|
|
4732
|
-
const
|
|
4733
|
-
|
|
6034
|
+
const mimeType = getMimeType(filePath);
|
|
6035
|
+
let fileContent;
|
|
6036
|
+
if (isTextMimeType(mimeType)) fileContent = new TextEncoder().encode(content);
|
|
6037
|
+
else fileContent = Buffer.from(content, "base64");
|
|
6038
|
+
const results = await this.uploadFiles([[filePath, fileContent]]);
|
|
4734
6039
|
if (results[0].error) return { error: `Failed to write to ${filePath}: ${results[0].error}` };
|
|
4735
6040
|
return {
|
|
4736
6041
|
path: filePath,
|
|
@@ -5065,9 +6370,44 @@ function createCacheBreakpointMiddleware() {
|
|
|
5065
6370
|
}
|
|
5066
6371
|
//#endregion
|
|
5067
6372
|
//#region src/agent.ts
|
|
5068
|
-
const
|
|
6373
|
+
const BASE_AGENT_PROMPT = context`
|
|
6374
|
+
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.
|
|
6375
|
+
|
|
6376
|
+
## Core Behavior
|
|
6377
|
+
|
|
6378
|
+
- Be concise and direct. Don't over-explain unless asked.
|
|
6379
|
+
- NEVER add unnecessary preamble (\"Sure!\", \"Great question!\", \"I'll now...\").
|
|
6380
|
+
- Don't say \"I'll now do X\" — just do it.
|
|
6381
|
+
- If the request is ambiguous, ask questions before acting.
|
|
6382
|
+
- If asked how to approach something, explain first, then act.
|
|
6383
|
+
|
|
6384
|
+
## Professional Objectivity
|
|
6385
|
+
|
|
6386
|
+
- Prioritize accuracy over validating the user's beliefs
|
|
6387
|
+
- Disagree respectfully when the user is incorrect
|
|
6388
|
+
- Avoid unnecessary superlatives, praise, or emotional validation
|
|
6389
|
+
|
|
6390
|
+
## Doing Tasks
|
|
6391
|
+
|
|
6392
|
+
When the user asks you to do something:
|
|
6393
|
+
|
|
6394
|
+
1. **Understand first** — read relevant files, check existing patterns. Quick but thorough — gather enough evidence to start, then iterate.
|
|
6395
|
+
2. **Act** — implement the solution. Work quickly but accurately.
|
|
6396
|
+
3. **Verify** — check your work against what was asked, not against your own output. Your first attempt is rarely correct — iterate.
|
|
6397
|
+
|
|
6398
|
+
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.
|
|
6399
|
+
|
|
6400
|
+
**When things go wrong:**
|
|
6401
|
+
- If something fails repeatedly, stop and analyze *why* — don't keep retrying the same approach.
|
|
6402
|
+
- If you're blocked, tell the user what's wrong and ask for guidance.
|
|
6403
|
+
|
|
6404
|
+
## Progress Updates
|
|
6405
|
+
|
|
6406
|
+
For longer tasks, provide brief progress updates at reasonable intervals — a concise sentence recapping what you've done and what's next.
|
|
6407
|
+
`;
|
|
5069
6408
|
const BUILTIN_TOOL_NAMES = new Set([
|
|
5070
6409
|
...FILESYSTEM_TOOL_NAMES,
|
|
6410
|
+
...ASYNC_TASK_TOOL_NAMES,
|
|
5071
6411
|
"task",
|
|
5072
6412
|
"write_todos"
|
|
5073
6413
|
]);
|
|
@@ -5084,19 +6424,18 @@ function isAnthropicModel(model) {
|
|
|
5084
6424
|
return model.getName() === "ChatAnthropic";
|
|
5085
6425
|
}
|
|
5086
6426
|
/**
|
|
5087
|
-
* Create a Deep Agent
|
|
6427
|
+
* Create a Deep Agent.
|
|
5088
6428
|
*
|
|
5089
|
-
*
|
|
5090
|
-
*
|
|
5091
|
-
*
|
|
5092
|
-
* -
|
|
5093
|
-
*
|
|
5094
|
-
*
|
|
5095
|
-
*
|
|
5096
|
-
* - Human-in-the-loop (humanInTheLoopMiddleware) - optional
|
|
6429
|
+
* This is the main entry point for building a production-style agent with
|
|
6430
|
+
* deepagents. It gives you a strong default runtime (filesystem, tasks,
|
|
6431
|
+
* subagents, summarization) and lets you opt into skills, memory,
|
|
6432
|
+
* human-in-the-loop interrupts, async subagents, and custom middleware.
|
|
6433
|
+
*
|
|
6434
|
+
* The runtime is intentionally opinionated: defaults work out of the box, and
|
|
6435
|
+
* when you customize behavior, the middleware ordering stays deterministic.
|
|
5097
6436
|
*
|
|
5098
6437
|
* @param params Configuration parameters for the agent
|
|
5099
|
-
* @returns
|
|
6438
|
+
* @returns Deep Agent instance with inferred state/response types
|
|
5100
6439
|
*
|
|
5101
6440
|
* @example
|
|
5102
6441
|
* ```typescript
|
|
@@ -5115,92 +6454,92 @@ function isAnthropicModel(model) {
|
|
|
5115
6454
|
* ```
|
|
5116
6455
|
*/
|
|
5117
6456
|
function createDeepAgent(params = {}) {
|
|
5118
|
-
const { model = "claude-sonnet-4-
|
|
6457
|
+
const { model = new ChatAnthropic("claude-sonnet-4-6"), tools = [], systemPrompt, middleware: customMiddleware = [], subagents = [], responseFormat, contextSchema, checkpointer, store, backend = (config) => new StateBackend(config), interruptOn, name, memory, skills } = params;
|
|
5119
6458
|
const collidingTools = tools.map((t) => t.name).filter((n) => typeof n === "string" && BUILTIN_TOOL_NAMES.has(n));
|
|
5120
6459
|
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");
|
|
5121
6460
|
const anthropicModel = isAnthropicModel(model);
|
|
5122
|
-
const
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
}] : [
|
|
5126
|
-
type: "text",
|
|
5127
|
-
text: BASE_PROMPT
|
|
5128
|
-
}, ...typeof systemPrompt.content === "string" ? [{
|
|
5129
|
-
type: "text",
|
|
5130
|
-
text: systemPrompt.content
|
|
5131
|
-
}] : systemPrompt.content] : [{
|
|
5132
|
-
type: "text",
|
|
5133
|
-
text: BASE_PROMPT
|
|
5134
|
-
}] });
|
|
5135
|
-
/**
|
|
5136
|
-
* Create backend configuration for filesystem middleware
|
|
5137
|
-
* If no backend is provided, use a factory that creates a StateBackend
|
|
5138
|
-
*/
|
|
5139
|
-
const filesystemBackend = backend ? backend : (runtime) => new StateBackend(runtime);
|
|
5140
|
-
/**
|
|
5141
|
-
* Skills middleware (created conditionally for runtime use)
|
|
5142
|
-
*/
|
|
5143
|
-
const skillsMiddlewareArray = skills != null && skills.length > 0 ? [createSkillsMiddleware({
|
|
5144
|
-
backend: filesystemBackend,
|
|
5145
|
-
sources: skills
|
|
5146
|
-
})] : [];
|
|
5147
|
-
/**
|
|
5148
|
-
* Memory middleware (created conditionally for runtime use)
|
|
5149
|
-
*/
|
|
5150
|
-
const memoryMiddlewareArray = memory != null && memory.length > 0 ? [createMemoryMiddleware({
|
|
5151
|
-
backend: filesystemBackend,
|
|
5152
|
-
sources: memory,
|
|
5153
|
-
addCacheControl: anthropicModel
|
|
5154
|
-
})] : [];
|
|
6461
|
+
const cacheMiddleware = anthropicModel ? [anthropicPromptCachingMiddleware({
|
|
6462
|
+
unsupportedModelBehavior: "ignore",
|
|
6463
|
+
minMessagesToCache: 1
|
|
6464
|
+
}), createCacheBreakpointMiddleware()] : [];
|
|
5155
6465
|
/**
|
|
5156
6466
|
* Process subagents to add SkillsMiddleware for those with their own skills.
|
|
5157
6467
|
*
|
|
5158
6468
|
* Custom subagents do NOT inherit skills from the main agent by default.
|
|
5159
|
-
* Only the general-purpose subagent inherits the main agent's skills
|
|
6469
|
+
* Only the general-purpose subagent inherits the main agent's skills.
|
|
5160
6470
|
* If a custom subagent needs skills, it must specify its own `skills` array.
|
|
5161
6471
|
*/
|
|
5162
|
-
const
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
sources: subagent.skills ?? []
|
|
5179
|
-
});
|
|
6472
|
+
const normalizeSubagentSpec = (input) => {
|
|
6473
|
+
const subagentMiddleware = [
|
|
6474
|
+
todoListMiddleware(),
|
|
6475
|
+
createFilesystemMiddleware({ backend }),
|
|
6476
|
+
createSummarizationMiddleware({
|
|
6477
|
+
backend,
|
|
6478
|
+
model
|
|
6479
|
+
}),
|
|
6480
|
+
createPatchToolCallsMiddleware(),
|
|
6481
|
+
...input.skills != null && input.skills.length > 0 ? [createSkillsMiddleware({
|
|
6482
|
+
backend,
|
|
6483
|
+
sources: input.skills
|
|
6484
|
+
})] : [],
|
|
6485
|
+
...input.middleware ?? [],
|
|
6486
|
+
...cacheMiddleware
|
|
6487
|
+
];
|
|
5180
6488
|
return {
|
|
5181
|
-
...
|
|
5182
|
-
|
|
6489
|
+
...input,
|
|
6490
|
+
tools: input.tools ?? [],
|
|
6491
|
+
middleware: subagentMiddleware
|
|
5183
6492
|
};
|
|
5184
|
-
}
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
6493
|
+
};
|
|
6494
|
+
const allSubagents = subagents;
|
|
6495
|
+
const asyncSubAgents = allSubagents.filter((item) => isAsyncSubAgent(item));
|
|
6496
|
+
const inlineSubagents = allSubagents.filter((item) => !isAsyncSubAgent(item)).map((item) => "runnable" in item ? item : normalizeSubagentSpec(item));
|
|
6497
|
+
if (!inlineSubagents.some((item) => item.name === GENERAL_PURPOSE_SUBAGENT["name"])) {
|
|
6498
|
+
const generalPurposeSpec = normalizeSubagentSpec({
|
|
6499
|
+
...GENERAL_PURPOSE_SUBAGENT,
|
|
6500
|
+
model,
|
|
6501
|
+
skills,
|
|
6502
|
+
tools
|
|
6503
|
+
});
|
|
6504
|
+
inlineSubagents.unshift(generalPurposeSpec);
|
|
6505
|
+
}
|
|
6506
|
+
const skillsMiddleware = skills != null && skills.length > 0 ? [createSkillsMiddleware({
|
|
6507
|
+
backend,
|
|
6508
|
+
sources: skills
|
|
6509
|
+
})] : [];
|
|
6510
|
+
const [todoMiddleware, fsMiddleware, subagentMiddleware, summarizationMiddleware, patchToolCallsMiddleware] = [
|
|
5196
6511
|
todoListMiddleware(),
|
|
5197
|
-
createFilesystemMiddleware({ backend
|
|
6512
|
+
createFilesystemMiddleware({ backend }),
|
|
6513
|
+
createSubAgentMiddleware({
|
|
6514
|
+
defaultModel: model,
|
|
6515
|
+
defaultTools: tools,
|
|
6516
|
+
defaultInterruptOn: interruptOn,
|
|
6517
|
+
subagents: inlineSubagents,
|
|
6518
|
+
generalPurposeAgent: false
|
|
6519
|
+
}),
|
|
5198
6520
|
createSummarizationMiddleware({
|
|
5199
6521
|
model,
|
|
5200
|
-
backend
|
|
6522
|
+
backend
|
|
5201
6523
|
}),
|
|
5202
6524
|
createPatchToolCallsMiddleware()
|
|
5203
6525
|
];
|
|
6526
|
+
const middleware = [
|
|
6527
|
+
todoMiddleware,
|
|
6528
|
+
...skillsMiddleware,
|
|
6529
|
+
fsMiddleware,
|
|
6530
|
+
subagentMiddleware,
|
|
6531
|
+
summarizationMiddleware,
|
|
6532
|
+
patchToolCallsMiddleware,
|
|
6533
|
+
...asyncSubAgents.length > 0 ? [createAsyncSubAgentMiddleware({ asyncSubAgents })] : [],
|
|
6534
|
+
...customMiddleware,
|
|
6535
|
+
...cacheMiddleware,
|
|
6536
|
+
...memory && memory.length > 0 ? [createMemoryMiddleware({
|
|
6537
|
+
backend,
|
|
6538
|
+
sources: memory,
|
|
6539
|
+
addCacheControl: anthropicModel
|
|
6540
|
+
})] : [],
|
|
6541
|
+
...interruptOn ? [humanInTheLoopMiddleware({ interruptOn })] : []
|
|
6542
|
+
];
|
|
5204
6543
|
/**
|
|
5205
6544
|
* Return as DeepAgent with proper DeepAgentTypeConfig
|
|
5206
6545
|
* - Response: InferStructuredResponse<TResponse> (unwraps ToolStrategy<T>/ProviderStrategy<T> → T)
|
|
@@ -5212,54 +6551,32 @@ function createDeepAgent(params = {}) {
|
|
|
5212
6551
|
*/
|
|
5213
6552
|
return createAgent({
|
|
5214
6553
|
model,
|
|
5215
|
-
systemPrompt:
|
|
6554
|
+
systemPrompt: typeof systemPrompt === "string" ? new SystemMessage({ contentBlocks: [{
|
|
6555
|
+
type: "text",
|
|
6556
|
+
text: systemPrompt
|
|
6557
|
+
}, {
|
|
6558
|
+
type: "text",
|
|
6559
|
+
text: BASE_AGENT_PROMPT
|
|
6560
|
+
}] }) : SystemMessage.isInstance(systemPrompt) ? new SystemMessage({ contentBlocks: [...systemPrompt.contentBlocks, {
|
|
6561
|
+
type: "text",
|
|
6562
|
+
text: BASE_AGENT_PROMPT
|
|
6563
|
+
}] }) : new SystemMessage({ contentBlocks: [{
|
|
6564
|
+
type: "text",
|
|
6565
|
+
text: BASE_AGENT_PROMPT
|
|
6566
|
+
}] }),
|
|
5216
6567
|
tools,
|
|
5217
|
-
middleware
|
|
5218
|
-
|
|
5219
|
-
todoListMiddleware(),
|
|
5220
|
-
createFilesystemMiddleware({ backend: filesystemBackend }),
|
|
5221
|
-
createSubAgentMiddleware({
|
|
5222
|
-
defaultModel: model,
|
|
5223
|
-
defaultTools: tools,
|
|
5224
|
-
defaultMiddleware: [...subagentMiddleware, ...anthropicModel ? [anthropicPromptCachingMiddleware({
|
|
5225
|
-
unsupportedModelBehavior: "ignore",
|
|
5226
|
-
minMessagesToCache: 1
|
|
5227
|
-
}), createCacheBreakpointMiddleware()] : []],
|
|
5228
|
-
generalPurposeMiddleware: [
|
|
5229
|
-
...subagentMiddleware,
|
|
5230
|
-
...skillsMiddlewareArray,
|
|
5231
|
-
...anthropicModel ? [anthropicPromptCachingMiddleware({
|
|
5232
|
-
unsupportedModelBehavior: "ignore",
|
|
5233
|
-
minMessagesToCache: 1
|
|
5234
|
-
}), createCacheBreakpointMiddleware()] : []
|
|
5235
|
-
],
|
|
5236
|
-
defaultInterruptOn: interruptOn,
|
|
5237
|
-
subagents: processedSubagents,
|
|
5238
|
-
generalPurposeAgent: true
|
|
5239
|
-
}),
|
|
5240
|
-
createSummarizationMiddleware({
|
|
5241
|
-
model,
|
|
5242
|
-
backend: filesystemBackend
|
|
5243
|
-
}),
|
|
5244
|
-
createPatchToolCallsMiddleware()
|
|
5245
|
-
],
|
|
5246
|
-
...skillsMiddlewareArray,
|
|
5247
|
-
...customMiddleware,
|
|
5248
|
-
...anthropicModel ? [anthropicPromptCachingMiddleware({
|
|
5249
|
-
unsupportedModelBehavior: "ignore",
|
|
5250
|
-
minMessagesToCache: 1
|
|
5251
|
-
}), createCacheBreakpointMiddleware()] : [],
|
|
5252
|
-
...memoryMiddlewareArray,
|
|
5253
|
-
...interruptOn ? [humanInTheLoopMiddleware({ interruptOn })] : []
|
|
5254
|
-
],
|
|
5255
|
-
...responseFormat != null && { responseFormat },
|
|
6568
|
+
middleware,
|
|
6569
|
+
...responseFormat !== null && { responseFormat },
|
|
5256
6570
|
contextSchema,
|
|
5257
6571
|
checkpointer,
|
|
5258
6572
|
store,
|
|
5259
6573
|
name
|
|
5260
6574
|
}).withConfig({
|
|
5261
6575
|
recursionLimit: 1e4,
|
|
5262
|
-
metadata: {
|
|
6576
|
+
metadata: {
|
|
6577
|
+
ls_integration: "deepagents",
|
|
6578
|
+
lc_agent_name: name
|
|
6579
|
+
}
|
|
5263
6580
|
});
|
|
5264
6581
|
}
|
|
5265
6582
|
//#endregion
|
|
@@ -5280,13 +6597,13 @@ function createDeepAgent(params = {}) {
|
|
|
5280
6597
|
* @returns Path to the project root if found, null otherwise.
|
|
5281
6598
|
*/
|
|
5282
6599
|
function findProjectRoot(startPath) {
|
|
5283
|
-
let current = path.resolve(startPath || process.cwd());
|
|
5284
|
-
while (current !== path.dirname(current)) {
|
|
5285
|
-
const gitDir = path.join(current, ".git");
|
|
6600
|
+
let current = path$1.resolve(startPath || process.cwd());
|
|
6601
|
+
while (current !== path$1.dirname(current)) {
|
|
6602
|
+
const gitDir = path$1.join(current, ".git");
|
|
5286
6603
|
if (fs$1.existsSync(gitDir)) return current;
|
|
5287
|
-
current = path.dirname(current);
|
|
6604
|
+
current = path$1.dirname(current);
|
|
5288
6605
|
}
|
|
5289
|
-
const rootGitDir = path.join(current, ".git");
|
|
6606
|
+
const rootGitDir = path$1.join(current, ".git");
|
|
5290
6607
|
if (fs$1.existsSync(rootGitDir)) return current;
|
|
5291
6608
|
return null;
|
|
5292
6609
|
}
|
|
@@ -5308,14 +6625,14 @@ function isValidAgentName(agentName) {
|
|
|
5308
6625
|
*/
|
|
5309
6626
|
function createSettings(options = {}) {
|
|
5310
6627
|
const projectRoot = findProjectRoot(options.startPath);
|
|
5311
|
-
const userDeepagentsDir = path.join(os.homedir(), ".deepagents");
|
|
6628
|
+
const userDeepagentsDir = path$1.join(os.homedir(), ".deepagents");
|
|
5312
6629
|
return {
|
|
5313
6630
|
projectRoot,
|
|
5314
6631
|
userDeepagentsDir,
|
|
5315
6632
|
hasProject: projectRoot !== null,
|
|
5316
6633
|
getAgentDir(agentName) {
|
|
5317
6634
|
if (!isValidAgentName(agentName)) throw new Error(`Invalid agent name: ${JSON.stringify(agentName)}. Agent names can only contain letters, numbers, hyphens, underscores, and spaces.`);
|
|
5318
|
-
return path.join(userDeepagentsDir, agentName);
|
|
6635
|
+
return path$1.join(userDeepagentsDir, agentName);
|
|
5319
6636
|
},
|
|
5320
6637
|
ensureAgentDir(agentName) {
|
|
5321
6638
|
const agentDir = this.getAgentDir(agentName);
|
|
@@ -5323,14 +6640,14 @@ function createSettings(options = {}) {
|
|
|
5323
6640
|
return agentDir;
|
|
5324
6641
|
},
|
|
5325
6642
|
getUserAgentMdPath(agentName) {
|
|
5326
|
-
return path.join(this.getAgentDir(agentName), "agent.md");
|
|
6643
|
+
return path$1.join(this.getAgentDir(agentName), "agent.md");
|
|
5327
6644
|
},
|
|
5328
6645
|
getProjectAgentMdPath() {
|
|
5329
6646
|
if (!projectRoot) return null;
|
|
5330
|
-
return path.join(projectRoot, ".deepagents", "agent.md");
|
|
6647
|
+
return path$1.join(projectRoot, ".deepagents", "agent.md");
|
|
5331
6648
|
},
|
|
5332
6649
|
getUserSkillsDir(agentName) {
|
|
5333
|
-
return path.join(this.getAgentDir(agentName), "skills");
|
|
6650
|
+
return path$1.join(this.getAgentDir(agentName), "skills");
|
|
5334
6651
|
},
|
|
5335
6652
|
ensureUserSkillsDir(agentName) {
|
|
5336
6653
|
const skillsDir = this.getUserSkillsDir(agentName);
|
|
@@ -5339,7 +6656,7 @@ function createSettings(options = {}) {
|
|
|
5339
6656
|
},
|
|
5340
6657
|
getProjectSkillsDir() {
|
|
5341
6658
|
if (!projectRoot) return null;
|
|
5342
|
-
return path.join(projectRoot, ".deepagents", "skills");
|
|
6659
|
+
return path$1.join(projectRoot, ".deepagents", "skills");
|
|
5343
6660
|
},
|
|
5344
6661
|
ensureProjectSkillsDir() {
|
|
5345
6662
|
const skillsDir = this.getProjectSkillsDir();
|
|
@@ -5349,7 +6666,7 @@ function createSettings(options = {}) {
|
|
|
5349
6666
|
},
|
|
5350
6667
|
ensureProjectDeepagentsDir() {
|
|
5351
6668
|
if (!projectRoot) return null;
|
|
5352
|
-
const deepagentsDir = path.join(projectRoot, ".deepagents");
|
|
6669
|
+
const deepagentsDir = path$1.join(projectRoot, ".deepagents");
|
|
5353
6670
|
fs$1.mkdirSync(deepagentsDir, { recursive: true });
|
|
5354
6671
|
return deepagentsDir;
|
|
5355
6672
|
}
|
|
@@ -5601,7 +6918,7 @@ function isSafePath(targetPath, baseDir) {
|
|
|
5601
6918
|
try {
|
|
5602
6919
|
const resolvedPath = fs$1.realpathSync(targetPath);
|
|
5603
6920
|
const resolvedBase = fs$1.realpathSync(baseDir);
|
|
5604
|
-
return resolvedPath.startsWith(resolvedBase + path.sep) || resolvedPath === resolvedBase;
|
|
6921
|
+
return resolvedPath.startsWith(resolvedBase + path$1.sep) || resolvedPath === resolvedBase;
|
|
5605
6922
|
} catch {
|
|
5606
6923
|
return false;
|
|
5607
6924
|
}
|
|
@@ -5680,7 +6997,7 @@ function parseSkillMetadata(skillMdPath, source) {
|
|
|
5680
6997
|
console.warn(`Skipping ${skillMdPath}: missing required 'name' or 'description'`);
|
|
5681
6998
|
return null;
|
|
5682
6999
|
}
|
|
5683
|
-
const directoryName = path.basename(path.dirname(skillMdPath));
|
|
7000
|
+
const directoryName = path$1.basename(path$1.dirname(skillMdPath));
|
|
5684
7001
|
const validation = validateSkillName(String(name), directoryName);
|
|
5685
7002
|
if (!validation.valid) console.warn(`Skill '${name}' in ${skillMdPath} does not follow Agent Skills spec: ${validation.error}. Consider renaming to be spec-compliant.`);
|
|
5686
7003
|
let descriptionStr = String(description);
|
|
@@ -5723,7 +7040,7 @@ function parseSkillMetadata(skillMdPath, source) {
|
|
|
5723
7040
|
* @returns List of skill metadata
|
|
5724
7041
|
*/
|
|
5725
7042
|
function listSkillsFromDir(skillsDir, source) {
|
|
5726
|
-
const expandedDir = skillsDir.startsWith("~") ? path.join(process.env.HOME || process.env.USERPROFILE || "", skillsDir.slice(1)) : skillsDir;
|
|
7043
|
+
const expandedDir = skillsDir.startsWith("~") ? path$1.join(process.env.HOME || process.env.USERPROFILE || "", skillsDir.slice(1)) : skillsDir;
|
|
5727
7044
|
if (!fs$1.existsSync(expandedDir)) return [];
|
|
5728
7045
|
let resolvedBase;
|
|
5729
7046
|
try {
|
|
@@ -5739,10 +7056,10 @@ function listSkillsFromDir(skillsDir, source) {
|
|
|
5739
7056
|
return [];
|
|
5740
7057
|
}
|
|
5741
7058
|
for (const entry of entries) {
|
|
5742
|
-
const skillDir = path.join(resolvedBase, entry.name);
|
|
7059
|
+
const skillDir = path$1.join(resolvedBase, entry.name);
|
|
5743
7060
|
if (!isSafePath(skillDir, resolvedBase)) continue;
|
|
5744
7061
|
if (!entry.isDirectory()) continue;
|
|
5745
|
-
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
7062
|
+
const skillMdPath = path$1.join(skillDir, "SKILL.md");
|
|
5746
7063
|
if (!fs$1.existsSync(skillMdPath)) continue;
|
|
5747
7064
|
if (!isSafePath(skillMdPath, resolvedBase)) continue;
|
|
5748
7065
|
const metadata = parseSkillMetadata(skillMdPath, source);
|
|
@@ -5773,6 +7090,6 @@ function listSkills(options) {
|
|
|
5773
7090
|
return Array.from(allSkills.values());
|
|
5774
7091
|
}
|
|
5775
7092
|
//#endregion
|
|
5776
|
-
export { BaseSandbox, CompositeBackend, ConfigurationError, DEFAULT_GENERAL_PURPOSE_DESCRIPTION, DEFAULT_SUBAGENT_PROMPT, FilesystemBackend, GENERAL_PURPOSE_SUBAGENT, LangSmithSandbox, LocalShellBackend, MAX_SKILL_DESCRIPTION_LENGTH, MAX_SKILL_FILE_SIZE, MAX_SKILL_NAME_LENGTH, SandboxError, StateBackend, StoreBackend, TASK_SYSTEM_PROMPT, computeSummarizationDefaults, createAgentMemoryMiddleware, createDeepAgent, createFilesystemMiddleware, createMemoryMiddleware, createPatchToolCallsMiddleware, createSettings, createSkillsMiddleware, createSubAgentMiddleware, createSummarizationMiddleware, filesValue, findProjectRoot, isSandboxBackend, listSkills, parseSkillMetadata, resolveBackend };
|
|
7093
|
+
export { BaseSandbox, CompositeBackend, ConfigurationError, DEFAULT_GENERAL_PURPOSE_DESCRIPTION, DEFAULT_SUBAGENT_PROMPT, FilesystemBackend, GENERAL_PURPOSE_SUBAGENT, LangSmithSandbox, LocalShellBackend, MAX_SKILL_DESCRIPTION_LENGTH, MAX_SKILL_FILE_SIZE, MAX_SKILL_NAME_LENGTH, SandboxError, StateBackend, StoreBackend, TASK_SYSTEM_PROMPT, adaptBackendProtocol, adaptSandboxProtocol, computeSummarizationDefaults, createAgentMemoryMiddleware, createAsyncSubAgentMiddleware, createCompletionCallbackMiddleware, createDeepAgent, createFilesystemMiddleware, createMemoryMiddleware, createPatchToolCallsMiddleware, createSettings, createSkillsMiddleware, createSubAgentMiddleware, createSummarizationMiddleware, filesValue, findProjectRoot, isAsyncSubAgent, isSandboxBackend, isSandboxProtocol, listSkills, parseSkillMetadata, resolveBackend };
|
|
5777
7094
|
|
|
5778
7095
|
//# sourceMappingURL=index.js.map
|