deepagentsdk 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/package.json +95 -0
- package/src/agent.ts +1230 -0
- package/src/backends/composite.ts +273 -0
- package/src/backends/filesystem.ts +692 -0
- package/src/backends/index.ts +22 -0
- package/src/backends/local-sandbox.ts +175 -0
- package/src/backends/persistent.ts +593 -0
- package/src/backends/sandbox.ts +510 -0
- package/src/backends/state.ts +244 -0
- package/src/backends/utils.ts +287 -0
- package/src/checkpointer/file-saver.ts +98 -0
- package/src/checkpointer/index.ts +5 -0
- package/src/checkpointer/kv-saver.ts +82 -0
- package/src/checkpointer/memory-saver.ts +82 -0
- package/src/checkpointer/types.ts +125 -0
- package/src/cli/components/ApiKeyInput.tsx +300 -0
- package/src/cli/components/FilePreview.tsx +237 -0
- package/src/cli/components/Input.tsx +277 -0
- package/src/cli/components/Message.tsx +93 -0
- package/src/cli/components/ModelSelection.tsx +338 -0
- package/src/cli/components/SlashMenu.tsx +101 -0
- package/src/cli/components/StatusBar.tsx +89 -0
- package/src/cli/components/Subagent.tsx +91 -0
- package/src/cli/components/TodoList.tsx +133 -0
- package/src/cli/components/ToolApproval.tsx +70 -0
- package/src/cli/components/ToolCall.tsx +144 -0
- package/src/cli/components/ToolCallSummary.tsx +175 -0
- package/src/cli/components/Welcome.tsx +75 -0
- package/src/cli/components/index.ts +24 -0
- package/src/cli/hooks/index.ts +12 -0
- package/src/cli/hooks/useAgent.ts +933 -0
- package/src/cli/index.tsx +1066 -0
- package/src/cli/theme.ts +205 -0
- package/src/cli/utils/model-list.ts +365 -0
- package/src/constants/errors.ts +29 -0
- package/src/constants/limits.ts +195 -0
- package/src/index.ts +176 -0
- package/src/middleware/agent-memory.ts +330 -0
- package/src/prompts.ts +196 -0
- package/src/skills/index.ts +2 -0
- package/src/skills/load.ts +191 -0
- package/src/skills/types.ts +53 -0
- package/src/tools/execute.ts +167 -0
- package/src/tools/filesystem.ts +418 -0
- package/src/tools/index.ts +39 -0
- package/src/tools/subagent.ts +443 -0
- package/src/tools/todos.ts +101 -0
- package/src/tools/web.ts +567 -0
- package/src/types/backend.ts +177 -0
- package/src/types/core.ts +220 -0
- package/src/types/events.ts +429 -0
- package/src/types/index.ts +94 -0
- package/src/types/structured-output.ts +43 -0
- package/src/types/subagent.ts +96 -0
- package/src/types.ts +22 -0
- package/src/utils/approval.ts +213 -0
- package/src/utils/events.ts +416 -0
- package/src/utils/eviction.ts +181 -0
- package/src/utils/index.ts +34 -0
- package/src/utils/model-parser.ts +38 -0
- package/src/utils/patch-tool-calls.ts +233 -0
- package/src/utils/project-detection.ts +32 -0
- package/src/utils/summarization.ts +254 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StateBackend: Store files in shared state (ephemeral, in-memory).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
BackendProtocol,
|
|
7
|
+
EditResult,
|
|
8
|
+
FileData,
|
|
9
|
+
FileInfo,
|
|
10
|
+
GrepMatch,
|
|
11
|
+
WriteResult,
|
|
12
|
+
DeepAgentState,
|
|
13
|
+
} from "../types";
|
|
14
|
+
import {
|
|
15
|
+
createFileData,
|
|
16
|
+
fileDataToString,
|
|
17
|
+
formatReadResponse,
|
|
18
|
+
globSearchFiles,
|
|
19
|
+
grepMatchesFromFiles,
|
|
20
|
+
performStringReplacement,
|
|
21
|
+
updateFileData,
|
|
22
|
+
} from "./utils";
|
|
23
|
+
import {
|
|
24
|
+
FILE_NOT_FOUND,
|
|
25
|
+
FILE_ALREADY_EXISTS,
|
|
26
|
+
} from "../constants/errors";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Backend that stores files in shared state (ephemeral).
|
|
30
|
+
*
|
|
31
|
+
* Files persist within a single agent invocation but not across invocations.
|
|
32
|
+
* This is the default backend for Deep Agent when no backend is specified.
|
|
33
|
+
*
|
|
34
|
+
* Files are stored in memory as part of the `DeepAgentState`, making this backend
|
|
35
|
+
* fast but non-persistent. Use `FilesystemBackend` or `PersistentBackend` for
|
|
36
|
+
* cross-session persistence.
|
|
37
|
+
*
|
|
38
|
+
* @example Default usage (no backend specified)
|
|
39
|
+
* ```typescript
|
|
40
|
+
* const agent = createDeepAgent({
|
|
41
|
+
* model: anthropic('claude-sonnet-4-20250514'),
|
|
42
|
+
* // StateBackend is used by default
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @example Explicit usage
|
|
47
|
+
* ```typescript
|
|
48
|
+
* const state: DeepAgentState = { todos: [], files: {} };
|
|
49
|
+
* const backend = new StateBackend(state);
|
|
50
|
+
* const agent = createDeepAgent({
|
|
51
|
+
* model: anthropic('claude-sonnet-4-20250514'),
|
|
52
|
+
* backend,
|
|
53
|
+
* });
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export class StateBackend implements BackendProtocol {
|
|
57
|
+
private state: DeepAgentState;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create a new StateBackend instance.
|
|
61
|
+
*
|
|
62
|
+
* @param state - The DeepAgentState object that will store the files.
|
|
63
|
+
* Files are stored in `state.files` as a Record<string, FileData>.
|
|
64
|
+
*/
|
|
65
|
+
constructor(state: DeepAgentState) {
|
|
66
|
+
this.state = state;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get files from current state.
|
|
71
|
+
*/
|
|
72
|
+
private getFiles(): Record<string, FileData> {
|
|
73
|
+
return this.state.files || {};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* List files and directories in the specified directory (non-recursive).
|
|
78
|
+
*/
|
|
79
|
+
lsInfo(path: string): FileInfo[] {
|
|
80
|
+
const files = this.getFiles();
|
|
81
|
+
const infos: FileInfo[] = [];
|
|
82
|
+
const subdirs = new Set<string>();
|
|
83
|
+
|
|
84
|
+
const normalizedPath = path.endsWith("/") ? path : path + "/";
|
|
85
|
+
|
|
86
|
+
for (const [k, fd] of Object.entries(files)) {
|
|
87
|
+
if (!k.startsWith(normalizedPath)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const relative = k.substring(normalizedPath.length);
|
|
92
|
+
|
|
93
|
+
if (relative.includes("/")) {
|
|
94
|
+
const subdirName = relative.split("/")[0];
|
|
95
|
+
subdirs.add(normalizedPath + subdirName + "/");
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const size = fd.content.join("\n").length;
|
|
100
|
+
infos.push({
|
|
101
|
+
path: k,
|
|
102
|
+
is_dir: false,
|
|
103
|
+
size: size,
|
|
104
|
+
modified_at: fd.modified_at,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const subdir of Array.from(subdirs).sort()) {
|
|
109
|
+
infos.push({
|
|
110
|
+
path: subdir,
|
|
111
|
+
is_dir: true,
|
|
112
|
+
size: 0,
|
|
113
|
+
modified_at: "",
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
infos.sort((a, b) => a.path.localeCompare(b.path));
|
|
118
|
+
return infos;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Read file content with line numbers.
|
|
123
|
+
*/
|
|
124
|
+
read(filePath: string, offset: number = 0, limit: number = 2000): string {
|
|
125
|
+
const files = this.getFiles();
|
|
126
|
+
const fileData = files[filePath];
|
|
127
|
+
|
|
128
|
+
if (!fileData) {
|
|
129
|
+
return FILE_NOT_FOUND(filePath);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return formatReadResponse(fileData, offset, limit);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Read file content as raw FileData.
|
|
137
|
+
*/
|
|
138
|
+
readRaw(filePath: string): FileData {
|
|
139
|
+
const files = this.getFiles();
|
|
140
|
+
const fileData = files[filePath];
|
|
141
|
+
|
|
142
|
+
if (!fileData) throw new Error(`File '${filePath}' not found`);
|
|
143
|
+
return fileData;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Create a new file with content.
|
|
148
|
+
*/
|
|
149
|
+
write(filePath: string, content: string): WriteResult {
|
|
150
|
+
const files = this.getFiles();
|
|
151
|
+
|
|
152
|
+
// Validate file path
|
|
153
|
+
if (!filePath || filePath.trim() === "") {
|
|
154
|
+
return {
|
|
155
|
+
success: false,
|
|
156
|
+
error: "File path cannot be empty",
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (filePath in files) {
|
|
161
|
+
return {
|
|
162
|
+
success: false,
|
|
163
|
+
error: FILE_ALREADY_EXISTS(filePath),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const newFileData = createFileData(content);
|
|
168
|
+
this.state.files[filePath] = newFileData;
|
|
169
|
+
return { success: true, path: filePath };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Edit a file by replacing string occurrences.
|
|
174
|
+
*/
|
|
175
|
+
edit(
|
|
176
|
+
filePath: string,
|
|
177
|
+
oldString: string,
|
|
178
|
+
newString: string,
|
|
179
|
+
replaceAll: boolean = false
|
|
180
|
+
): EditResult {
|
|
181
|
+
const files = this.getFiles();
|
|
182
|
+
const fileData = files[filePath];
|
|
183
|
+
|
|
184
|
+
if (!fileData) {
|
|
185
|
+
return { success: false, error: FILE_NOT_FOUND(filePath) };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const content = fileDataToString(fileData);
|
|
189
|
+
const result = performStringReplacement(
|
|
190
|
+
content,
|
|
191
|
+
oldString,
|
|
192
|
+
newString,
|
|
193
|
+
replaceAll
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
if (typeof result === "string") {
|
|
197
|
+
return { success: false, error: result };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const [newContent, occurrences] = result;
|
|
201
|
+
const newFileData = updateFileData(fileData, newContent);
|
|
202
|
+
this.state.files[filePath] = newFileData;
|
|
203
|
+
return { success: true, path: filePath, occurrences };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Structured search results or error string for invalid input.
|
|
208
|
+
*/
|
|
209
|
+
grepRaw(
|
|
210
|
+
pattern: string,
|
|
211
|
+
path: string = "/",
|
|
212
|
+
glob: string | null = null
|
|
213
|
+
): GrepMatch[] | string {
|
|
214
|
+
const files = this.getFiles();
|
|
215
|
+
return grepMatchesFromFiles(files, pattern, path, glob);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Structured glob matching returning FileInfo objects.
|
|
220
|
+
*/
|
|
221
|
+
globInfo(pattern: string, path: string = "/"): FileInfo[] {
|
|
222
|
+
const files = this.getFiles();
|
|
223
|
+
const result = globSearchFiles(files, pattern, path);
|
|
224
|
+
|
|
225
|
+
if (result === "No files found") {
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const paths = result.split("\n");
|
|
230
|
+
const infos: FileInfo[] = [];
|
|
231
|
+
for (const p of paths) {
|
|
232
|
+
const fd = files[p];
|
|
233
|
+
const size = fd ? fd.content.join("\n").length : 0;
|
|
234
|
+
infos.push({
|
|
235
|
+
path: p,
|
|
236
|
+
is_dir: false,
|
|
237
|
+
size: size,
|
|
238
|
+
modified_at: fd?.modified_at || "",
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return infos;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utility functions for memory backend implementations.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import micromatch from "micromatch";
|
|
6
|
+
import { basename } from "path";
|
|
7
|
+
import type { FileData, GrepMatch } from "../types";
|
|
8
|
+
import { SYSTEM_REMINDER_FILE_EMPTY, INVALID_REGEX } from "../constants/errors";
|
|
9
|
+
import {
|
|
10
|
+
MAX_LINE_LENGTH,
|
|
11
|
+
LINE_NUMBER_WIDTH,
|
|
12
|
+
DEFAULT_EVICTION_TOKEN_LIMIT,
|
|
13
|
+
} from "../constants/limits";
|
|
14
|
+
|
|
15
|
+
// Constants
|
|
16
|
+
export const EMPTY_CONTENT_WARNING = SYSTEM_REMINDER_FILE_EMPTY;
|
|
17
|
+
// Re-export from limits for backward compatibility
|
|
18
|
+
export { MAX_LINE_LENGTH, LINE_NUMBER_WIDTH };
|
|
19
|
+
export const TOOL_RESULT_TOKEN_LIMIT = DEFAULT_EVICTION_TOKEN_LIMIT;
|
|
20
|
+
export const TRUNCATION_GUIDANCE =
|
|
21
|
+
"... [results truncated, try being more specific with your parameters]";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Format file content with line numbers (cat -n style).
|
|
25
|
+
*/
|
|
26
|
+
export function formatContentWithLineNumbers(
|
|
27
|
+
content: string | string[],
|
|
28
|
+
startLine: number = 1
|
|
29
|
+
): string {
|
|
30
|
+
let lines: string[];
|
|
31
|
+
if (typeof content === "string") {
|
|
32
|
+
lines = content.split("\n");
|
|
33
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
34
|
+
lines = lines.slice(0, -1);
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
lines = content;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const resultLines: string[] = [];
|
|
41
|
+
for (let i = 0; i < lines.length; i++) {
|
|
42
|
+
const line = lines[i];
|
|
43
|
+
const lineNum = i + startLine;
|
|
44
|
+
|
|
45
|
+
if (line && line.length <= MAX_LINE_LENGTH) {
|
|
46
|
+
resultLines.push(
|
|
47
|
+
`${lineNum.toString().padStart(LINE_NUMBER_WIDTH)}\t${line}`
|
|
48
|
+
);
|
|
49
|
+
} else if (line) {
|
|
50
|
+
// Split long line into chunks with continuation markers
|
|
51
|
+
const numChunks = Math.ceil(line.length / MAX_LINE_LENGTH);
|
|
52
|
+
for (let chunkIdx = 0; chunkIdx < numChunks; chunkIdx++) {
|
|
53
|
+
const start = chunkIdx * MAX_LINE_LENGTH;
|
|
54
|
+
const end = Math.min(start + MAX_LINE_LENGTH, line.length);
|
|
55
|
+
const chunk = line.substring(start, end);
|
|
56
|
+
if (chunkIdx === 0) {
|
|
57
|
+
resultLines.push(
|
|
58
|
+
`${lineNum.toString().padStart(LINE_NUMBER_WIDTH)}\t${chunk}`
|
|
59
|
+
);
|
|
60
|
+
} else {
|
|
61
|
+
const continuationMarker = `${lineNum}.${chunkIdx}`;
|
|
62
|
+
resultLines.push(
|
|
63
|
+
`${continuationMarker.padStart(LINE_NUMBER_WIDTH)}\t${chunk}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
resultLines.push(
|
|
69
|
+
`${lineNum.toString().padStart(LINE_NUMBER_WIDTH)}\t`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return resultLines.join("\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if content is empty and return warning message.
|
|
79
|
+
*/
|
|
80
|
+
export function checkEmptyContent(content: string): string | null {
|
|
81
|
+
if (!content || content.trim() === "") {
|
|
82
|
+
return EMPTY_CONTENT_WARNING;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Convert FileData to plain string content.
|
|
89
|
+
*/
|
|
90
|
+
export function fileDataToString(fileData: FileData): string {
|
|
91
|
+
return fileData.content.join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create a FileData object with timestamps.
|
|
96
|
+
*/
|
|
97
|
+
export function createFileData(content: string, createdAt?: string): FileData {
|
|
98
|
+
const lines = typeof content === "string" ? content.split("\n") : content;
|
|
99
|
+
const now = new Date().toISOString();
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
content: lines,
|
|
103
|
+
created_at: createdAt || now,
|
|
104
|
+
modified_at: now,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Update FileData with new content, preserving creation timestamp.
|
|
110
|
+
*/
|
|
111
|
+
export function updateFileData(fileData: FileData, content: string): FileData {
|
|
112
|
+
const lines = typeof content === "string" ? content.split("\n") : content;
|
|
113
|
+
const now = new Date().toISOString();
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
content: lines,
|
|
117
|
+
created_at: fileData.created_at,
|
|
118
|
+
modified_at: now,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Format file data for read response with line numbers.
|
|
124
|
+
*/
|
|
125
|
+
export function formatReadResponse(
|
|
126
|
+
fileData: FileData,
|
|
127
|
+
offset: number,
|
|
128
|
+
limit: number
|
|
129
|
+
): string {
|
|
130
|
+
const content = fileDataToString(fileData);
|
|
131
|
+
const emptyMsg = checkEmptyContent(content);
|
|
132
|
+
if (emptyMsg) {
|
|
133
|
+
return emptyMsg;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const lines = content.split("\n");
|
|
137
|
+
const startIdx = offset;
|
|
138
|
+
const endIdx = Math.min(startIdx + limit, lines.length);
|
|
139
|
+
|
|
140
|
+
if (startIdx >= lines.length) {
|
|
141
|
+
return `Error: Line offset ${offset} exceeds file length (${lines.length} lines)`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const selectedLines = lines.slice(startIdx, endIdx);
|
|
145
|
+
return formatContentWithLineNumbers(selectedLines, startIdx + 1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Perform string replacement with occurrence validation.
|
|
150
|
+
*/
|
|
151
|
+
export function performStringReplacement(
|
|
152
|
+
content: string,
|
|
153
|
+
oldString: string,
|
|
154
|
+
newString: string,
|
|
155
|
+
replaceAll: boolean
|
|
156
|
+
): [string, number] | string {
|
|
157
|
+
const occurrences = content.split(oldString).length - 1;
|
|
158
|
+
|
|
159
|
+
if (occurrences === 0) {
|
|
160
|
+
return `Error: String not found in file: '${oldString}'`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (occurrences > 1 && !replaceAll) {
|
|
164
|
+
return `Error: String '${oldString}' appears ${occurrences} times in file. Use replace_all=true to replace all instances, or provide a more specific string with surrounding context.`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const newContent = content.split(oldString).join(newString);
|
|
168
|
+
return [newContent, occurrences];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Validate and normalize a path.
|
|
173
|
+
*/
|
|
174
|
+
export function validatePath(path: string | null | undefined): string {
|
|
175
|
+
const pathStr = path || "/";
|
|
176
|
+
if (!pathStr || pathStr.trim() === "") {
|
|
177
|
+
throw new Error("Path cannot be empty");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let normalized = pathStr.startsWith("/") ? pathStr : "/" + pathStr;
|
|
181
|
+
|
|
182
|
+
if (!normalized.endsWith("/")) {
|
|
183
|
+
normalized += "/";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return normalized;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Search files dict for paths matching glob pattern.
|
|
191
|
+
*/
|
|
192
|
+
export function globSearchFiles(
|
|
193
|
+
files: Record<string, FileData>,
|
|
194
|
+
pattern: string,
|
|
195
|
+
path: string = "/"
|
|
196
|
+
): string {
|
|
197
|
+
let normalizedPath: string;
|
|
198
|
+
try {
|
|
199
|
+
normalizedPath = validatePath(path);
|
|
200
|
+
} catch {
|
|
201
|
+
return "No files found";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const filtered = Object.fromEntries(
|
|
205
|
+
Object.entries(files).filter(([fp]) => fp.startsWith(normalizedPath))
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const matches: Array<[string, string]> = [];
|
|
209
|
+
for (const [filePath, fileData] of Object.entries(filtered)) {
|
|
210
|
+
let relative = filePath.substring(normalizedPath.length);
|
|
211
|
+
if (relative.startsWith("/")) {
|
|
212
|
+
relative = relative.substring(1);
|
|
213
|
+
}
|
|
214
|
+
if (!relative) {
|
|
215
|
+
const parts = filePath.split("/");
|
|
216
|
+
relative = parts[parts.length - 1] || "";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (
|
|
220
|
+
micromatch.isMatch(relative, pattern, {
|
|
221
|
+
dot: true,
|
|
222
|
+
nobrace: false,
|
|
223
|
+
})
|
|
224
|
+
) {
|
|
225
|
+
matches.push([filePath, fileData.modified_at]);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
matches.sort((a, b) => b[1].localeCompare(a[1]));
|
|
230
|
+
|
|
231
|
+
if (matches.length === 0) {
|
|
232
|
+
return "No files found";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return matches.map(([fp]) => fp).join("\n");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Return structured grep matches from an in-memory files mapping.
|
|
240
|
+
*/
|
|
241
|
+
export function grepMatchesFromFiles(
|
|
242
|
+
files: Record<string, FileData>,
|
|
243
|
+
pattern: string,
|
|
244
|
+
path: string | null = null,
|
|
245
|
+
glob: string | null = null
|
|
246
|
+
): GrepMatch[] | string {
|
|
247
|
+
let regex: RegExp;
|
|
248
|
+
try {
|
|
249
|
+
regex = new RegExp(pattern);
|
|
250
|
+
} catch (e: unknown) {
|
|
251
|
+
const error = e as Error;
|
|
252
|
+
return INVALID_REGEX(error.message);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let normalizedPath: string;
|
|
256
|
+
try {
|
|
257
|
+
normalizedPath = validatePath(path);
|
|
258
|
+
} catch {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let filtered = Object.fromEntries(
|
|
263
|
+
Object.entries(files).filter(([fp]) => fp.startsWith(normalizedPath))
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
if (glob) {
|
|
267
|
+
filtered = Object.fromEntries(
|
|
268
|
+
Object.entries(filtered).filter(([fp]) =>
|
|
269
|
+
micromatch.isMatch(basename(fp), glob, { dot: true, nobrace: false })
|
|
270
|
+
)
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const matches: GrepMatch[] = [];
|
|
275
|
+
for (const [filePath, fileData] of Object.entries(filtered)) {
|
|
276
|
+
for (let i = 0; i < fileData.content.length; i++) {
|
|
277
|
+
const line = fileData.content[i];
|
|
278
|
+
const lineNum = i + 1;
|
|
279
|
+
if (line && regex.test(line)) {
|
|
280
|
+
matches.push({ path: filePath, line: lineNum, text: line });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return matches;
|
|
286
|
+
}
|
|
287
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-based checkpoint saver for local development.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, readdirSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import type { Checkpoint, BaseCheckpointSaver } from "./types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Options for FileSaver.
|
|
11
|
+
*/
|
|
12
|
+
export interface FileSaverOptions {
|
|
13
|
+
/** Directory to store checkpoint files */
|
|
14
|
+
dir: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* File-based checkpoint saver.
|
|
19
|
+
*
|
|
20
|
+
* Stores checkpoints as JSON files in a directory. Each thread gets
|
|
21
|
+
* its own file named `{threadId}.json`.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* const saver = new FileSaver({ dir: './.checkpoints' });
|
|
26
|
+
* const agent = createDeepAgent({
|
|
27
|
+
* model: anthropic('claude-sonnet-4-20250514'),
|
|
28
|
+
* checkpointer: saver,
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export class FileSaver implements BaseCheckpointSaver {
|
|
33
|
+
private dir: string;
|
|
34
|
+
|
|
35
|
+
constructor(options: FileSaverOptions) {
|
|
36
|
+
this.dir = options.dir;
|
|
37
|
+
|
|
38
|
+
// Ensure directory exists
|
|
39
|
+
if (!existsSync(this.dir)) {
|
|
40
|
+
mkdirSync(this.dir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private getFilePath(threadId: string): string {
|
|
45
|
+
// Sanitize threadId to be safe for filenames
|
|
46
|
+
const safeId = threadId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
47
|
+
return join(this.dir, `${safeId}.json`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async save(checkpoint: Checkpoint): Promise<void> {
|
|
51
|
+
const filePath = this.getFilePath(checkpoint.threadId);
|
|
52
|
+
const data = {
|
|
53
|
+
...checkpoint,
|
|
54
|
+
updatedAt: new Date().toISOString(),
|
|
55
|
+
};
|
|
56
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async load(threadId: string): Promise<Checkpoint | undefined> {
|
|
60
|
+
const filePath = this.getFilePath(threadId);
|
|
61
|
+
|
|
62
|
+
if (!existsSync(filePath)) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
68
|
+
return JSON.parse(content) as Checkpoint;
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async list(): Promise<string[]> {
|
|
75
|
+
if (!existsSync(this.dir)) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const files = readdirSync(this.dir);
|
|
80
|
+
return files
|
|
81
|
+
.filter(f => f.endsWith('.json'))
|
|
82
|
+
.map(f => f.replace('.json', ''));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async delete(threadId: string): Promise<void> {
|
|
86
|
+
const filePath = this.getFilePath(threadId);
|
|
87
|
+
|
|
88
|
+
if (existsSync(filePath)) {
|
|
89
|
+
unlinkSync(filePath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async exists(threadId: string): Promise<boolean> {
|
|
94
|
+
const filePath = this.getFilePath(threadId);
|
|
95
|
+
return existsSync(filePath);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkpoint saver using KeyValueStore interface.
|
|
3
|
+
*
|
|
4
|
+
* Allows using existing KeyValueStore implementations (Redis, etc.)
|
|
5
|
+
* for checkpoint storage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { KeyValueStore } from "../backends/persistent";
|
|
9
|
+
import type { Checkpoint, BaseCheckpointSaver, CheckpointSaverOptions } from "./types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Options for KeyValueStoreSaver.
|
|
13
|
+
*/
|
|
14
|
+
export interface KeyValueStoreSaverOptions extends CheckpointSaverOptions {
|
|
15
|
+
/** The KeyValueStore implementation to use */
|
|
16
|
+
store: KeyValueStore;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Checkpoint saver using KeyValueStore interface.
|
|
21
|
+
*
|
|
22
|
+
* This adapter allows using any KeyValueStore implementation (Redis,
|
|
23
|
+
* database, cloud storage, etc.) for checkpoint storage.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* import { InMemoryStore } from 'deepagentsdk';
|
|
28
|
+
*
|
|
29
|
+
* const store = new InMemoryStore();
|
|
30
|
+
* const saver = new KeyValueStoreSaver({ store });
|
|
31
|
+
* const agent = createDeepAgent({
|
|
32
|
+
* model: anthropic('claude-sonnet-4-20250514'),
|
|
33
|
+
* checkpointer: saver,
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @example With Redis
|
|
38
|
+
* ```typescript
|
|
39
|
+
* const redisStore = new RedisStore(redisClient); // Your implementation
|
|
40
|
+
* const saver = new KeyValueStoreSaver({ store: redisStore });
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export class KeyValueStoreSaver implements BaseCheckpointSaver {
|
|
44
|
+
private store: KeyValueStore;
|
|
45
|
+
private namespace: string[];
|
|
46
|
+
|
|
47
|
+
constructor(options: KeyValueStoreSaverOptions) {
|
|
48
|
+
this.store = options.store;
|
|
49
|
+
this.namespace = [options.namespace || "default", "checkpoints"];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async save(checkpoint: Checkpoint): Promise<void> {
|
|
53
|
+
const data = {
|
|
54
|
+
...checkpoint,
|
|
55
|
+
updatedAt: new Date().toISOString(),
|
|
56
|
+
};
|
|
57
|
+
await this.store.put(this.namespace, checkpoint.threadId, data as unknown as Record<string, unknown>);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async load(threadId: string): Promise<Checkpoint | undefined> {
|
|
61
|
+
const data = await this.store.get(this.namespace, threadId);
|
|
62
|
+
if (!data) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
return data as unknown as Checkpoint;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async list(): Promise<string[]> {
|
|
69
|
+
const items = await this.store.list(this.namespace);
|
|
70
|
+
return items.map(item => item.key);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async delete(threadId: string): Promise<void> {
|
|
74
|
+
await this.store.delete(this.namespace, threadId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async exists(threadId: string): Promise<boolean> {
|
|
78
|
+
const data = await this.store.get(this.namespace, threadId);
|
|
79
|
+
return data !== undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|