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,181 @@
1
+ /**
2
+ * Tool result eviction utility.
3
+ *
4
+ * When tool results exceed a certain size threshold, this utility
5
+ * writes them to the filesystem and returns a reference instead.
6
+ * This prevents context overflow from large tool outputs.
7
+ */
8
+
9
+ import type { BackendProtocol, BackendFactory, DeepAgentState } from "../types.js";
10
+ import { DEFAULT_EVICTION_TOKEN_LIMIT as CENTRALIZED_EVICTION_LIMIT } from "../constants/limits.js";
11
+
12
+ /**
13
+ * Default token limit before evicting a tool result.
14
+ * Approximately 20,000 tokens (~80KB of text).
15
+ */
16
+ export const DEFAULT_EVICTION_TOKEN_LIMIT = CENTRALIZED_EVICTION_LIMIT;
17
+
18
+ /**
19
+ * Approximate characters per token (rough estimate).
20
+ */
21
+ const CHARS_PER_TOKEN = 4;
22
+
23
+ /**
24
+ * Sanitize a tool call ID for use as a filename.
25
+ * Removes or replaces characters that are invalid in file paths.
26
+ */
27
+ export function sanitizeToolCallId(toolCallId: string): string {
28
+ return toolCallId
29
+ .replace(/[^a-zA-Z0-9_-]/g, "_")
30
+ .substring(0, 100); // Limit length
31
+ }
32
+
33
+ /**
34
+ * Estimate the number of tokens in a string.
35
+ * Uses a simple character-based approximation.
36
+ */
37
+ export function estimateTokens(text: string): number {
38
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
39
+ }
40
+
41
+ /**
42
+ * Check if a tool result should be evicted based on size.
43
+ */
44
+ export function shouldEvict(
45
+ result: string,
46
+ tokenLimit: number = DEFAULT_EVICTION_TOKEN_LIMIT
47
+ ): boolean {
48
+ return estimateTokens(result) > tokenLimit;
49
+ }
50
+
51
+ /**
52
+ * Options for evicting a tool result.
53
+ */
54
+ export interface EvictOptions {
55
+ /** The tool result content */
56
+ result: string;
57
+ /** The tool call ID (used for filename) */
58
+ toolCallId: string;
59
+ /** The tool name */
60
+ toolName: string;
61
+ /** Backend to write the evicted content to */
62
+ backend: BackendProtocol;
63
+ /** Token limit before eviction (default: 20000) */
64
+ tokenLimit?: number;
65
+ }
66
+
67
+ /**
68
+ * Result of an eviction operation.
69
+ */
70
+ export interface EvictResult {
71
+ /** Whether the result was evicted */
72
+ evicted: boolean;
73
+ /** The content to return (either original or truncated message) */
74
+ content: string;
75
+ /** Path where content was evicted to (if evicted) */
76
+ evictedPath?: string;
77
+ }
78
+
79
+ /**
80
+ * Evict a large tool result to the filesystem.
81
+ *
82
+ * If the result exceeds the token limit, writes it to a file and
83
+ * returns a truncated message with the file path.
84
+ *
85
+ * @param options - Eviction options
86
+ * @returns Eviction result with content and metadata
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * const result = await evictToolResult({
91
+ * result: veryLongString,
92
+ * toolCallId: "call_123",
93
+ * toolName: "grep",
94
+ * backend: filesystemBackend,
95
+ * });
96
+ *
97
+ * if (result.evicted) {
98
+ * console.log(`Content saved to ${result.evictedPath}`);
99
+ * }
100
+ * ```
101
+ */
102
+ export async function evictToolResult(
103
+ options: EvictOptions
104
+ ): Promise<EvictResult> {
105
+ const {
106
+ result,
107
+ toolCallId,
108
+ toolName,
109
+ backend,
110
+ tokenLimit = DEFAULT_EVICTION_TOKEN_LIMIT,
111
+ } = options;
112
+
113
+ // Check if eviction is needed
114
+ if (!shouldEvict(result, tokenLimit)) {
115
+ return {
116
+ evicted: false,
117
+ content: result,
118
+ };
119
+ }
120
+
121
+ // Generate eviction path
122
+ const sanitizedId = sanitizeToolCallId(toolCallId);
123
+ const evictPath = `/large_tool_results/${toolName}_${sanitizedId}.txt`;
124
+
125
+ // Write to backend
126
+ const writeResult = await backend.write(evictPath, result);
127
+
128
+ if (writeResult.error) {
129
+ // If write fails, return original content (may cause context issues)
130
+ console.warn(`Failed to evict tool result: ${writeResult.error}`);
131
+ return {
132
+ evicted: false,
133
+ content: result,
134
+ };
135
+ }
136
+
137
+ // Return truncated message
138
+ const estimatedTokens = estimateTokens(result);
139
+ const truncatedContent = `Tool result too large (~${estimatedTokens} tokens). Content saved to ${evictPath}. Use read_file to access the full content.`;
140
+
141
+ return {
142
+ evicted: true,
143
+ content: truncatedContent,
144
+ evictedPath: evictPath,
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Create a tool result wrapper that automatically evicts large results.
150
+ *
151
+ * @param backend - Backend or factory for filesystem operations
152
+ * @param state - Current agent state (for factory backends)
153
+ * @param tokenLimit - Token limit before eviction
154
+ * @returns Function that wraps tool results with eviction
155
+ */
156
+ export function createToolResultWrapper(
157
+ backend: BackendProtocol | BackendFactory,
158
+ state: DeepAgentState,
159
+ tokenLimit: number = DEFAULT_EVICTION_TOKEN_LIMIT
160
+ ): (result: string, toolCallId: string, toolName: string) => Promise<string> {
161
+ // Resolve backend if factory
162
+ const resolvedBackend =
163
+ typeof backend === "function" ? backend(state) : backend;
164
+
165
+ return async (
166
+ result: string,
167
+ toolCallId: string,
168
+ toolName: string
169
+ ): Promise<string> => {
170
+ const evictResult = await evictToolResult({
171
+ result,
172
+ toolCallId,
173
+ toolName,
174
+ backend: resolvedBackend,
175
+ tokenLimit,
176
+ });
177
+
178
+ return evictResult.content;
179
+ };
180
+ }
181
+
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Utility functions for AI SDK Deep Agent.
3
+ */
4
+
5
+ export { patchToolCalls, hasDanglingToolCalls } from "./patch-tool-calls.js";
6
+ export {
7
+ evictToolResult,
8
+ createToolResultWrapper,
9
+ shouldEvict,
10
+ estimateTokens,
11
+ sanitizeToolCallId,
12
+ DEFAULT_EVICTION_TOKEN_LIMIT,
13
+ type EvictOptions,
14
+ type EvictResult,
15
+ } from "./eviction.js";
16
+ export {
17
+ summarizeIfNeeded,
18
+ needsSummarization,
19
+ estimateMessagesTokens,
20
+ DEFAULT_SUMMARIZATION_THRESHOLD,
21
+ DEFAULT_KEEP_MESSAGES,
22
+ type SummarizationOptions,
23
+ type SummarizationResult,
24
+ } from "./summarization.js";
25
+ export {
26
+ parseModelString,
27
+ } from "./model-parser.js";
28
+ export {
29
+ applyInterruptConfig,
30
+ wrapToolsWithApproval,
31
+ hasApprovalTools,
32
+ type ApprovalCallback,
33
+ } from "./approval.js";
34
+
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Utility to parse model strings into LanguageModel instances.
3
+ * Provides backward compatibility for CLI and other string-based model specifications.
4
+ */
5
+
6
+ import { anthropic } from "@ai-sdk/anthropic";
7
+ import { openai } from "@ai-sdk/openai";
8
+ import type { LanguageModel } from "ai";
9
+
10
+ /**
11
+ * Parse a model string into a LanguageModel instance.
12
+ *
13
+ * Supports formats like:
14
+ * - "anthropic/claude-sonnet-4-20250514"
15
+ * - "openai/gpt-4o"
16
+ * - "claude-sonnet-4-20250514" (defaults to Anthropic)
17
+ *
18
+ * @param modelString - The model string to parse
19
+ * @returns A LanguageModel instance
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * const model = parseModelString("anthropic/claude-sonnet-4-20250514");
24
+ * const agent = createDeepAgent({ model });
25
+ * ```
26
+ */
27
+ export function parseModelString(modelString: string): LanguageModel {
28
+ const [provider, modelName] = modelString.split("/");
29
+
30
+ if (provider === "anthropic") {
31
+ return anthropic(modelName || "claude-sonnet-4-20250514");
32
+ } else if (provider === "openai") {
33
+ return openai(modelName || "gpt-5-mini") as any;
34
+ }
35
+
36
+ // Default to anthropic if no provider specified
37
+ return anthropic(modelString);
38
+ }
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Utility to patch dangling tool calls in message history.
3
+ *
4
+ * When an AI message contains tool_calls but subsequent messages don't include
5
+ * the corresponding tool result responses, this utility adds synthetic
6
+ * tool result messages saying the tool call was cancelled.
7
+ *
8
+ * This prevents errors when sending the conversation history to the model,
9
+ * as models expect every tool call to have a corresponding result.
10
+ */
11
+
12
+ import type { ModelMessage } from "ai";
13
+
14
+ /**
15
+ * Check if a message is an assistant message with tool calls.
16
+ */
17
+ function hasToolCalls(message: ModelMessage): boolean {
18
+ if (message.role !== "assistant") return false;
19
+
20
+ // Check if content contains tool calls
21
+ const content = message.content;
22
+ if (Array.isArray(content)) {
23
+ return content.some(
24
+ (part) => typeof part === "object" && part !== null && "type" in part && part.type === "tool-call"
25
+ );
26
+ }
27
+
28
+ return false;
29
+ }
30
+
31
+ /**
32
+ * Extract tool call IDs from an assistant message.
33
+ */
34
+ function getToolCallIds(message: ModelMessage): string[] {
35
+ if (message.role !== "assistant") return [];
36
+
37
+ const content = message.content;
38
+ if (!Array.isArray(content)) return [];
39
+
40
+ const ids: string[] = [];
41
+ for (const part of content) {
42
+ if (
43
+ typeof part === "object" &&
44
+ part !== null &&
45
+ "type" in part &&
46
+ part.type === "tool-call" &&
47
+ "toolCallId" in part
48
+ ) {
49
+ ids.push(part.toolCallId as string);
50
+ }
51
+ }
52
+
53
+ return ids;
54
+ }
55
+
56
+ /**
57
+ * Check if a message is a tool result for a specific tool call ID.
58
+ */
59
+ function isToolResultFor(message: ModelMessage, toolCallId: string): boolean {
60
+ if (message.role !== "tool") return false;
61
+
62
+ // Tool messages should have a toolCallId
63
+ if ("toolCallId" in message && message.toolCallId === toolCallId) {
64
+ return true;
65
+ }
66
+
67
+ // Also check content array for tool-result parts
68
+ const content = message.content;
69
+ if (Array.isArray(content)) {
70
+ return content.some(
71
+ (part) =>
72
+ typeof part === "object" &&
73
+ part !== null &&
74
+ "type" in part &&
75
+ part.type === "tool-result" &&
76
+ "toolCallId" in part &&
77
+ part.toolCallId === toolCallId
78
+ );
79
+ }
80
+
81
+ return false;
82
+ }
83
+
84
+ /**
85
+ * Create a synthetic tool result message for a cancelled tool call.
86
+ */
87
+ function createCancelledToolResult(
88
+ toolCallId: string,
89
+ toolName: string
90
+ ): ModelMessage {
91
+ const message: ModelMessage = {
92
+ role: "tool",
93
+ content: [
94
+ {
95
+ type: "tool-result" as const,
96
+ toolCallId,
97
+ toolName,
98
+ output: {
99
+ type: "text" as const,
100
+ value: `Tool call ${toolName} with id ${toolCallId} was cancelled - another message came in before it could be completed.`,
101
+ },
102
+ },
103
+ ],
104
+ };
105
+ return message;
106
+ }
107
+
108
+ /**
109
+ * Get tool name from a tool call part.
110
+ */
111
+ function getToolName(message: ModelMessage, toolCallId: string): string {
112
+ if (message.role !== "assistant") return "unknown";
113
+
114
+ const content = message.content;
115
+ if (!Array.isArray(content)) return "unknown";
116
+
117
+ for (const part of content) {
118
+ if (
119
+ typeof part === "object" &&
120
+ part !== null &&
121
+ "type" in part &&
122
+ part.type === "tool-call" &&
123
+ "toolCallId" in part &&
124
+ part.toolCallId === toolCallId &&
125
+ "toolName" in part
126
+ ) {
127
+ return part.toolName as string;
128
+ }
129
+ }
130
+
131
+ return "unknown";
132
+ }
133
+
134
+ /**
135
+ * Patch dangling tool calls in a message array.
136
+ *
137
+ * Scans for assistant messages with tool_calls that don't have corresponding
138
+ * tool result messages, and adds synthetic "cancelled" responses.
139
+ *
140
+ * @param messages - Array of messages to patch
141
+ * @returns New array with patched messages (original array is not modified)
142
+ *
143
+ * @example
144
+ * ```typescript
145
+ * const messages = [
146
+ * { role: "user", content: "Hello" },
147
+ * { role: "assistant", content: [{ type: "tool-call", toolCallId: "1", toolName: "search" }] },
148
+ * // Missing tool result for tool call "1"
149
+ * { role: "user", content: "Never mind" },
150
+ * ];
151
+ *
152
+ * const patched = patchToolCalls(messages);
153
+ * // patched now includes a synthetic tool result for the dangling call
154
+ * ```
155
+ */
156
+ export function patchToolCalls(messages: ModelMessage[]): ModelMessage[] {
157
+ if (!messages || messages.length === 0) {
158
+ return messages;
159
+ }
160
+
161
+ const result: ModelMessage[] = [];
162
+
163
+ for (let i = 0; i < messages.length; i++) {
164
+ const message = messages[i];
165
+ if (!message) continue;
166
+
167
+ result.push(message);
168
+
169
+ // Check if this is an assistant message with tool calls
170
+ if (hasToolCalls(message)) {
171
+ const toolCallIds = getToolCallIds(message);
172
+
173
+ for (const toolCallId of toolCallIds) {
174
+ // Look for a corresponding tool result in subsequent messages
175
+ let hasResult = false;
176
+ for (let j = i + 1; j < messages.length; j++) {
177
+ const subsequentMsg = messages[j];
178
+ if (subsequentMsg && isToolResultFor(subsequentMsg, toolCallId)) {
179
+ hasResult = true;
180
+ break;
181
+ }
182
+ }
183
+
184
+ // If no result found, add a synthetic cancelled result
185
+ if (!hasResult) {
186
+ const toolName = getToolName(message, toolCallId);
187
+ result.push(createCancelledToolResult(toolCallId, toolName));
188
+ }
189
+ }
190
+ }
191
+ }
192
+
193
+ return result;
194
+ }
195
+
196
+ /**
197
+ * Check if messages have any dangling tool calls.
198
+ *
199
+ * @param messages - Array of messages to check
200
+ * @returns True if there are dangling tool calls
201
+ */
202
+ export function hasDanglingToolCalls(messages: ModelMessage[]): boolean {
203
+ if (!messages || messages.length === 0) {
204
+ return false;
205
+ }
206
+
207
+ for (let i = 0; i < messages.length; i++) {
208
+ const message = messages[i];
209
+ if (!message) continue;
210
+
211
+ if (hasToolCalls(message)) {
212
+ const toolCallIds = getToolCallIds(message);
213
+
214
+ for (const toolCallId of toolCallIds) {
215
+ let hasResult = false;
216
+ for (let j = i + 1; j < messages.length; j++) {
217
+ const subsequentMsg = messages[j];
218
+ if (subsequentMsg && isToolResultFor(subsequentMsg, toolCallId)) {
219
+ hasResult = true;
220
+ break;
221
+ }
222
+ }
223
+
224
+ if (!hasResult) {
225
+ return true;
226
+ }
227
+ }
228
+ }
229
+ }
230
+
231
+ return false;
232
+ }
233
+
@@ -0,0 +1,32 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+
4
+ /**
5
+ * Find the git root by walking up the directory tree.
6
+ * Returns null if no .git directory is found.
7
+ *
8
+ * @param startPath - Starting directory (defaults to process.cwd())
9
+ * @returns Absolute path to git root, or null if not in a git repository
10
+ */
11
+ export async function findGitRoot(startPath?: string): Promise<string | null> {
12
+ let current = path.resolve(startPath || process.cwd());
13
+ const root = path.parse(current).root;
14
+
15
+ while (current !== root) {
16
+ try {
17
+ const gitPath = path.join(current, '.git');
18
+ const stat = await fs.stat(gitPath);
19
+
20
+ if (stat.isDirectory()) {
21
+ return current;
22
+ }
23
+ } catch {
24
+ // .git doesn't exist at this level, continue upward
25
+ }
26
+
27
+ // Move up one directory
28
+ current = path.dirname(current);
29
+ }
30
+
31
+ return null;
32
+ }