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,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
|
+
}
|