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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +159 -0
  3. package/package.json +95 -0
  4. package/src/agent.ts +1230 -0
  5. package/src/backends/composite.ts +273 -0
  6. package/src/backends/filesystem.ts +692 -0
  7. package/src/backends/index.ts +22 -0
  8. package/src/backends/local-sandbox.ts +175 -0
  9. package/src/backends/persistent.ts +593 -0
  10. package/src/backends/sandbox.ts +510 -0
  11. package/src/backends/state.ts +244 -0
  12. package/src/backends/utils.ts +287 -0
  13. package/src/checkpointer/file-saver.ts +98 -0
  14. package/src/checkpointer/index.ts +5 -0
  15. package/src/checkpointer/kv-saver.ts +82 -0
  16. package/src/checkpointer/memory-saver.ts +82 -0
  17. package/src/checkpointer/types.ts +125 -0
  18. package/src/cli/components/ApiKeyInput.tsx +300 -0
  19. package/src/cli/components/FilePreview.tsx +237 -0
  20. package/src/cli/components/Input.tsx +277 -0
  21. package/src/cli/components/Message.tsx +93 -0
  22. package/src/cli/components/ModelSelection.tsx +338 -0
  23. package/src/cli/components/SlashMenu.tsx +101 -0
  24. package/src/cli/components/StatusBar.tsx +89 -0
  25. package/src/cli/components/Subagent.tsx +91 -0
  26. package/src/cli/components/TodoList.tsx +133 -0
  27. package/src/cli/components/ToolApproval.tsx +70 -0
  28. package/src/cli/components/ToolCall.tsx +144 -0
  29. package/src/cli/components/ToolCallSummary.tsx +175 -0
  30. package/src/cli/components/Welcome.tsx +75 -0
  31. package/src/cli/components/index.ts +24 -0
  32. package/src/cli/hooks/index.ts +12 -0
  33. package/src/cli/hooks/useAgent.ts +933 -0
  34. package/src/cli/index.tsx +1066 -0
  35. package/src/cli/theme.ts +205 -0
  36. package/src/cli/utils/model-list.ts +365 -0
  37. package/src/constants/errors.ts +29 -0
  38. package/src/constants/limits.ts +195 -0
  39. package/src/index.ts +176 -0
  40. package/src/middleware/agent-memory.ts +330 -0
  41. package/src/prompts.ts +196 -0
  42. package/src/skills/index.ts +2 -0
  43. package/src/skills/load.ts +191 -0
  44. package/src/skills/types.ts +53 -0
  45. package/src/tools/execute.ts +167 -0
  46. package/src/tools/filesystem.ts +418 -0
  47. package/src/tools/index.ts +39 -0
  48. package/src/tools/subagent.ts +443 -0
  49. package/src/tools/todos.ts +101 -0
  50. package/src/tools/web.ts +567 -0
  51. package/src/types/backend.ts +177 -0
  52. package/src/types/core.ts +220 -0
  53. package/src/types/events.ts +429 -0
  54. package/src/types/index.ts +94 -0
  55. package/src/types/structured-output.ts +43 -0
  56. package/src/types/subagent.ts +96 -0
  57. package/src/types.ts +22 -0
  58. package/src/utils/approval.ts +213 -0
  59. package/src/utils/events.ts +416 -0
  60. package/src/utils/eviction.ts +181 -0
  61. package/src/utils/index.ts +34 -0
  62. package/src/utils/model-parser.ts +38 -0
  63. package/src/utils/patch-tool-calls.ts +233 -0
  64. package/src/utils/project-detection.ts +32 -0
  65. package/src/utils/summarization.ts +254 -0
@@ -0,0 +1,593 @@
1
+ /**
2
+ * PersistentBackend: Generic persistent storage backend.
3
+ *
4
+ * This backend provides cross-conversation file persistence using a
5
+ * pluggable key-value store interface. It can be used with various
6
+ * storage solutions like Redis, SQLite, or any custom implementation.
7
+ */
8
+
9
+ import type {
10
+ BackendProtocol,
11
+ EditResult,
12
+ FileData,
13
+ FileInfo,
14
+ GrepMatch,
15
+ WriteResult,
16
+ } from "../types.js";
17
+ import {
18
+ createFileData,
19
+ fileDataToString,
20
+ formatReadResponse,
21
+ globSearchFiles,
22
+ grepMatchesFromFiles,
23
+ performStringReplacement,
24
+ updateFileData,
25
+ } from "./utils.js";
26
+ import {
27
+ FILE_NOT_FOUND,
28
+ FILE_ALREADY_EXISTS,
29
+ } from "../constants/errors.js";
30
+
31
+ /**
32
+ * Generic key-value store interface for persistent storage.
33
+ *
34
+ * Implement this interface to use any storage backend (Redis, SQLite, cloud storage, etc.)
35
+ * with PersistentBackend. The interface uses hierarchical namespaces for organization.
36
+ *
37
+ * @example Redis implementation
38
+ * ```typescript
39
+ * class RedisStore implements KeyValueStore {
40
+ * constructor(private redis: RedisClient) {}
41
+ *
42
+ * async get(namespace: string[], key: string) {
43
+ * const redisKey = [...namespace, key].join(':');
44
+ * const data = await this.redis.get(redisKey);
45
+ * return data ? JSON.parse(data) : undefined;
46
+ * }
47
+ *
48
+ * async put(namespace: string[], key: string, value: Record<string, unknown>) {
49
+ * const redisKey = [...namespace, key].join(':');
50
+ * await this.redis.set(redisKey, JSON.stringify(value));
51
+ * }
52
+ *
53
+ * async delete(namespace: string[], key: string) {
54
+ * const redisKey = [...namespace, key].join(':');
55
+ * await this.redis.del(redisKey);
56
+ * }
57
+ *
58
+ * async list(namespace: string[]) {
59
+ * const prefix = [...namespace].join(':') + ':';
60
+ * const keys = await this.redis.keys(prefix + '*');
61
+ * const results = [];
62
+ * for (const key of keys) {
63
+ * const data = await this.redis.get(key);
64
+ * if (data) {
65
+ * const relativeKey = key.substring(prefix.length);
66
+ * results.push({ key: relativeKey, value: JSON.parse(data) });
67
+ * }
68
+ * }
69
+ * return results;
70
+ * }
71
+ * }
72
+ * ```
73
+ */
74
+ export interface KeyValueStore {
75
+ /**
76
+ * Get a value by key from the store.
77
+ * @param namespace - Hierarchical namespace array (e.g., ["project1", "filesystem"])
78
+ * @param key - The key to retrieve (file path in the case of PersistentBackend)
79
+ * @returns The stored value as a record, or undefined if not found
80
+ */
81
+ get(namespace: string[], key: string): Promise<Record<string, unknown> | undefined>;
82
+
83
+ /**
84
+ * Store a value by key in the store.
85
+ * @param namespace - Hierarchical namespace array
86
+ * @param key - The key to store (file path in the case of PersistentBackend)
87
+ * @param value - The value to store (must be serializable to JSON)
88
+ */
89
+ put(namespace: string[], key: string, value: Record<string, unknown>): Promise<void>;
90
+
91
+ /**
92
+ * Delete a value by key from the store.
93
+ * @param namespace - Hierarchical namespace array
94
+ * @param key - The key to delete (file path in the case of PersistentBackend)
95
+ */
96
+ delete(namespace: string[], key: string): Promise<void>;
97
+
98
+ /**
99
+ * List all keys and values in a namespace.
100
+ * @param namespace - Hierarchical namespace array
101
+ * @returns Array of items with key and value pairs directly in this namespace
102
+ * (not including sub-namespaces)
103
+ */
104
+ list(namespace: string[]): Promise<Array<{ key: string; value: Record<string, unknown> }>>;
105
+ }
106
+
107
+ /**
108
+ * Simple in-memory implementation of KeyValueStore.
109
+ *
110
+ * Useful for testing or single-session persistence. Data is stored in a Map
111
+ * and does not persist across application restarts.
112
+ *
113
+ * @example Basic usage
114
+ * ```typescript
115
+ * const store = new InMemoryStore();
116
+ * const backend = new PersistentBackend({ store });
117
+ * ```
118
+ *
119
+ * @example For testing
120
+ * ```typescript
121
+ * const store = new InMemoryStore();
122
+ * // ... run tests ...
123
+ * store.clear(); // Clean up after tests
124
+ * ```
125
+ */
126
+ export class InMemoryStore implements KeyValueStore {
127
+ private data = new Map<string, Record<string, unknown>>();
128
+
129
+ private makeKey(namespace: string[], key: string): string {
130
+ return [...namespace, key].join(":");
131
+ }
132
+
133
+ private parseKey(fullKey: string, namespace: string[]): string | null {
134
+ const prefix = namespace.join(":") + ":";
135
+ if (fullKey.startsWith(prefix)) {
136
+ return fullKey.substring(prefix.length);
137
+ }
138
+ return null;
139
+ }
140
+
141
+ async get(namespace: string[], key: string): Promise<Record<string, unknown> | undefined> {
142
+ return this.data.get(this.makeKey(namespace, key));
143
+ }
144
+
145
+ async put(namespace: string[], key: string, value: Record<string, unknown>): Promise<void> {
146
+ this.data.set(this.makeKey(namespace, key), value);
147
+ }
148
+
149
+ async delete(namespace: string[], key: string): Promise<void> {
150
+ this.data.delete(this.makeKey(namespace, key));
151
+ }
152
+
153
+ async list(namespace: string[]): Promise<Array<{ key: string; value: Record<string, unknown> }>> {
154
+ const results: Array<{ key: string; value: Record<string, unknown> }> = [];
155
+ const prefix = namespace.join(":") + ":";
156
+
157
+ for (const [fullKey, value] of this.data.entries()) {
158
+ if (fullKey.startsWith(prefix)) {
159
+ const key = fullKey.substring(prefix.length);
160
+ // Only include items directly in this namespace (no sub-namespaces)
161
+ if (!key.includes(":")) {
162
+ results.push({ key, value });
163
+ }
164
+ }
165
+ }
166
+
167
+ return results;
168
+ }
169
+
170
+ /**
171
+ * Clear all data (useful for testing).
172
+ */
173
+ clear(): void {
174
+ this.data.clear();
175
+ }
176
+
177
+ /**
178
+ * Get the number of stored items.
179
+ */
180
+ size(): number {
181
+ return this.data.size;
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Options for creating a PersistentBackend.
187
+ */
188
+ export interface PersistentBackendOptions {
189
+ /**
190
+ * **Required.** The key-value store implementation to use.
191
+ *
192
+ * You can use the built-in `InMemoryStore` for testing, or implement `KeyValueStore`
193
+ * for custom storage (Redis, SQLite, etc.).
194
+ *
195
+ * @see {@link KeyValueStore} for the interface definition
196
+ * @see {@link InMemoryStore} for a simple in-memory implementation
197
+ */
198
+ store: KeyValueStore;
199
+ /**
200
+ * Optional namespace prefix for isolation (e.g., project ID, user ID).
201
+ *
202
+ * This allows multiple agents or projects to share the same store without conflicts.
203
+ * Files are stored under `[namespace]/filesystem/` in the key-value store.
204
+ *
205
+ * Default: "default"
206
+ */
207
+ namespace?: string;
208
+ }
209
+
210
+ /**
211
+ * Backend that stores files in a persistent key-value store.
212
+ *
213
+ * This provides cross-conversation file persistence that survives between agent sessions.
214
+ * Files are stored in the provided key-value store, allowing you to use any storage backend
215
+ * (Redis, SQLite, cloud storage, etc.) by implementing the `KeyValueStore` interface.
216
+ *
217
+ * @example Using InMemoryStore (for testing or single-session persistence)
218
+ * ```typescript
219
+ * import { createDeepAgent } from 'deepagentsdk';
220
+ * import { PersistentBackend, InMemoryStore } from 'deepagentsdk';
221
+ * import { anthropic } from '@ai-sdk/anthropic';
222
+ *
223
+ * const store = new InMemoryStore();
224
+ * const backend = new PersistentBackend({ store });
225
+ * const agent = createDeepAgent({
226
+ * model: anthropic('claude-sonnet-4-20250514'),
227
+ * backend,
228
+ * });
229
+ * ```
230
+ *
231
+ * @example With custom namespace for project isolation
232
+ * ```typescript
233
+ * import { createDeepAgent } from 'deepagentsdk';
234
+ * import { PersistentBackend, InMemoryStore } from 'deepagentsdk';
235
+ * import { anthropic } from '@ai-sdk/anthropic';
236
+ *
237
+ * const store = new InMemoryStore();
238
+ * const backend = new PersistentBackend({
239
+ * store,
240
+ * namespace: 'project-123', // Isolate files for this project
241
+ * });
242
+ * const agent = createDeepAgent({
243
+ * model: anthropic('claude-sonnet-4-20250514'),
244
+ * backend,
245
+ * });
246
+ * ```
247
+ *
248
+ * @example Custom KeyValueStore implementation (Redis)
249
+ * ```typescript
250
+ * import { createDeepAgent } from 'deepagentsdk';
251
+ * import { PersistentBackend, type KeyValueStore } from 'deepagentsdk';
252
+ * import { anthropic } from '@ai-sdk/anthropic';
253
+ * import { createClient } from 'redis';
254
+ *
255
+ * class RedisStore implements KeyValueStore {
256
+ * constructor(private redis: ReturnType<typeof createClient>) {}
257
+ *
258
+ * async get(namespace: string[], key: string) {
259
+ * const redisKey = [...namespace, key].join(':');
260
+ * const data = await this.redis.get(redisKey);
261
+ * return data ? JSON.parse(data) : undefined;
262
+ * }
263
+ *
264
+ * async put(namespace: string[], key: string, value: Record<string, unknown>) {
265
+ * const redisKey = [...namespace, key].join(':');
266
+ * await this.redis.set(redisKey, JSON.stringify(value));
267
+ * }
268
+ *
269
+ * async delete(namespace: string[], key: string) {
270
+ * const redisKey = [...namespace, key].join(':');
271
+ * await this.redis.del(redisKey);
272
+ * }
273
+ *
274
+ * async list(namespace: string[]) {
275
+ * const prefix = [...namespace].join(':') + ':';
276
+ * const keys = await this.redis.keys(prefix + '*');
277
+ * const results = [];
278
+ * for (const key of keys) {
279
+ * const data = await this.redis.get(key);
280
+ * if (data) {
281
+ * const relativeKey = key.substring(prefix.length);
282
+ * results.push({ key: relativeKey, value: JSON.parse(data) });
283
+ * }
284
+ * }
285
+ * return results;
286
+ * }
287
+ * }
288
+ *
289
+ * const redis = createClient();
290
+ * await redis.connect();
291
+ *
292
+ * const backend = new PersistentBackend({
293
+ * store: new RedisStore(redis),
294
+ * namespace: 'production'
295
+ * });
296
+ *
297
+ * const agent = createDeepAgent({
298
+ * model: anthropic('claude-sonnet-4-20250514'),
299
+ * backend,
300
+ * });
301
+ * ```
302
+ */
303
+ export class PersistentBackend implements BackendProtocol {
304
+ private store: KeyValueStore;
305
+ private namespacePrefix: string;
306
+
307
+ /**
308
+ * Create a new PersistentBackend instance.
309
+ *
310
+ * @param options - Configuration options
311
+ * @param options.store - The key-value store implementation to use
312
+ * @param options.namespace - Optional namespace prefix for file isolation
313
+ */
314
+ constructor(options: PersistentBackendOptions) {
315
+ this.store = options.store;
316
+ this.namespacePrefix = options.namespace || "default";
317
+ }
318
+
319
+ /**
320
+ * Get the namespace for store operations.
321
+ */
322
+ protected getNamespace(): string[] {
323
+ return [this.namespacePrefix, "filesystem"];
324
+ }
325
+
326
+ /**
327
+ * Convert a store value to FileData format.
328
+ */
329
+ private convertToFileData(value: Record<string, unknown>): FileData {
330
+ if (
331
+ !value.content ||
332
+ !Array.isArray(value.content) ||
333
+ typeof value.created_at !== "string" ||
334
+ typeof value.modified_at !== "string"
335
+ ) {
336
+ throw new Error(
337
+ `Store item does not contain valid FileData fields. Got keys: ${Object.keys(value).join(", ")}`
338
+ );
339
+ }
340
+
341
+ return {
342
+ content: value.content as string[],
343
+ created_at: value.created_at,
344
+ modified_at: value.modified_at,
345
+ };
346
+ }
347
+
348
+ /**
349
+ * Convert FileData to a value suitable for store.put().
350
+ */
351
+ private convertFromFileData(fileData: FileData): Record<string, unknown> {
352
+ return {
353
+ content: fileData.content,
354
+ created_at: fileData.created_at,
355
+ modified_at: fileData.modified_at,
356
+ };
357
+ }
358
+
359
+ /**
360
+ * List files and directories in the specified directory (non-recursive).
361
+ */
362
+ async lsInfo(path: string): Promise<FileInfo[]> {
363
+ const namespace = this.getNamespace();
364
+ const items = await this.store.list(namespace);
365
+ const infos: FileInfo[] = [];
366
+ const subdirs = new Set<string>();
367
+
368
+ // Normalize path to have trailing slash for proper prefix matching
369
+ const normalizedPath = path.endsWith("/") ? path : path + "/";
370
+
371
+ for (const item of items) {
372
+ const itemKey = item.key;
373
+
374
+ // Check if file is in the specified directory or a subdirectory
375
+ if (!itemKey.startsWith(normalizedPath)) {
376
+ continue;
377
+ }
378
+
379
+ // Get the relative path after the directory
380
+ const relative = itemKey.substring(normalizedPath.length);
381
+
382
+ // If relative path contains '/', it's in a subdirectory
383
+ if (relative.includes("/")) {
384
+ // Extract the immediate subdirectory name
385
+ const subdirName = relative.split("/")[0];
386
+ subdirs.add(normalizedPath + subdirName + "/");
387
+ continue;
388
+ }
389
+
390
+ // This is a file directly in the current directory
391
+ try {
392
+ const fd = this.convertToFileData(item.value);
393
+ const size = fd.content.join("\n").length;
394
+ infos.push({
395
+ path: itemKey,
396
+ is_dir: false,
397
+ size: size,
398
+ modified_at: fd.modified_at,
399
+ });
400
+ } catch {
401
+ // Skip invalid items
402
+ continue;
403
+ }
404
+ }
405
+
406
+ // Add directories to the results
407
+ for (const subdir of Array.from(subdirs).sort()) {
408
+ infos.push({
409
+ path: subdir,
410
+ is_dir: true,
411
+ size: 0,
412
+ modified_at: "",
413
+ });
414
+ }
415
+
416
+ infos.sort((a, b) => a.path.localeCompare(b.path));
417
+ return infos;
418
+ }
419
+
420
+ /**
421
+ * Read file content with line numbers.
422
+ */
423
+ async read(
424
+ filePath: string,
425
+ offset: number = 0,
426
+ limit: number = 2000
427
+ ): Promise<string> {
428
+ try {
429
+ const fileData = await this.readRaw(filePath);
430
+ return formatReadResponse(fileData, offset, limit);
431
+ } catch (e: unknown) {
432
+ const error = e as Error;
433
+ return `Error: ${error.message}`;
434
+ }
435
+ }
436
+
437
+ /**
438
+ * Read file content as raw FileData.
439
+ */
440
+ async readRaw(filePath: string): Promise<FileData> {
441
+ const namespace = this.getNamespace();
442
+ const value = await this.store.get(namespace, filePath);
443
+
444
+ if (!value) {
445
+ throw new Error(`File '${filePath}' not found`);
446
+ }
447
+
448
+ return this.convertToFileData(value);
449
+ }
450
+
451
+ /**
452
+ * Create a new file with content.
453
+ */
454
+ async write(filePath: string, content: string): Promise<WriteResult> {
455
+ const namespace = this.getNamespace();
456
+
457
+ // Check if file exists
458
+ const existing = await this.store.get(namespace, filePath);
459
+ if (existing) {
460
+ return {
461
+ success: false,
462
+ error: FILE_ALREADY_EXISTS(filePath),
463
+ };
464
+ }
465
+
466
+ // Create new file
467
+ const fileData = createFileData(content);
468
+ const storeValue = this.convertFromFileData(fileData);
469
+ await this.store.put(namespace, filePath, storeValue);
470
+ return { success: true, path: filePath };
471
+ }
472
+
473
+ /**
474
+ * Edit a file by replacing string occurrences.
475
+ */
476
+ async edit(
477
+ filePath: string,
478
+ oldString: string,
479
+ newString: string,
480
+ replaceAll: boolean = false
481
+ ): Promise<EditResult> {
482
+ const namespace = this.getNamespace();
483
+
484
+ // Get existing file
485
+ const value = await this.store.get(namespace, filePath);
486
+ if (!value) {
487
+ return { success: false, error: FILE_NOT_FOUND(filePath) };
488
+ }
489
+
490
+ try {
491
+ const fileData = this.convertToFileData(value);
492
+ const content = fileDataToString(fileData);
493
+ const result = performStringReplacement(
494
+ content,
495
+ oldString,
496
+ newString,
497
+ replaceAll
498
+ );
499
+
500
+ if (typeof result === "string") {
501
+ return { success: false, error: result };
502
+ }
503
+
504
+ const [newContent, occurrences] = result;
505
+ const newFileData = updateFileData(fileData, newContent);
506
+
507
+ // Update file in store
508
+ const storeValue = this.convertFromFileData(newFileData);
509
+ await this.store.put(namespace, filePath, storeValue);
510
+ return { success: true, path: filePath, occurrences };
511
+ } catch (e: unknown) {
512
+ const error = e as Error;
513
+ return { success: false, error: `Error: ${error.message}` };
514
+ }
515
+ }
516
+
517
+ /**
518
+ * Structured search results or error string for invalid input.
519
+ */
520
+ async grepRaw(
521
+ pattern: string,
522
+ path: string = "/",
523
+ glob: string | null = null
524
+ ): Promise<GrepMatch[] | string> {
525
+ const namespace = this.getNamespace();
526
+ const items = await this.store.list(namespace);
527
+
528
+ const files: Record<string, FileData> = {};
529
+ for (const item of items) {
530
+ try {
531
+ files[item.key] = this.convertToFileData(item.value);
532
+ } catch {
533
+ // Skip invalid items
534
+ continue;
535
+ }
536
+ }
537
+
538
+ return grepMatchesFromFiles(files, pattern, path, glob);
539
+ }
540
+
541
+ /**
542
+ * Structured glob matching returning FileInfo objects.
543
+ */
544
+ async globInfo(pattern: string, path: string = "/"): Promise<FileInfo[]> {
545
+ const namespace = this.getNamespace();
546
+ const items = await this.store.list(namespace);
547
+
548
+ const files: Record<string, FileData> = {};
549
+ for (const item of items) {
550
+ try {
551
+ files[item.key] = this.convertToFileData(item.value);
552
+ } catch {
553
+ // Skip invalid items
554
+ continue;
555
+ }
556
+ }
557
+
558
+ const result = globSearchFiles(files, pattern, path);
559
+ if (result === "No files found") {
560
+ return [];
561
+ }
562
+
563
+ const paths = result.split("\n");
564
+ const infos: FileInfo[] = [];
565
+ for (const p of paths) {
566
+ const fd = files[p];
567
+ const size = fd ? fd.content.join("\n").length : 0;
568
+ infos.push({
569
+ path: p,
570
+ is_dir: false,
571
+ size: size,
572
+ modified_at: fd?.modified_at || "",
573
+ });
574
+ }
575
+ return infos;
576
+ }
577
+
578
+ /**
579
+ * Delete a file.
580
+ */
581
+ async deleteFile(filePath: string): Promise<{ error?: string }> {
582
+ const namespace = this.getNamespace();
583
+ const existing = await this.store.get(namespace, filePath);
584
+
585
+ if (!existing) {
586
+ return { error: `File '${filePath}' not found` };
587
+ }
588
+
589
+ await this.store.delete(namespace, filePath);
590
+ return {};
591
+ }
592
+ }
593
+