@avasis-ai/synthcode 1.0.0
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 +292 -0
- package/dist/chunk-53ZOIXM4.js +624 -0
- package/dist/chunk-53ZOIXM4.js.map +1 -0
- package/dist/chunk-BWXHO6UJ.js +115 -0
- package/dist/chunk-BWXHO6UJ.js.map +1 -0
- package/dist/chunk-CARUMOML.js +123 -0
- package/dist/chunk-CARUMOML.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/chunk-F34HO4RA.js +487 -0
- package/dist/chunk-F34HO4RA.js.map +1 -0
- package/dist/chunk-FK7S2S7V.js +132 -0
- package/dist/chunk-FK7S2S7V.js.map +1 -0
- package/dist/chunk-MQ7XP6VT.js +174 -0
- package/dist/chunk-MQ7XP6VT.js.map +1 -0
- package/dist/chunk-TLPOO6C3.js +176 -0
- package/dist/chunk-TLPOO6C3.js.map +1 -0
- package/dist/chunk-W6OLZ2OI.js +56 -0
- package/dist/chunk-W6OLZ2OI.js.map +1 -0
- package/dist/cli/index.cjs +151 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +8 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/run.cjs +128 -0
- package/dist/cli/run.cjs.map +1 -0
- package/dist/cli/run.d.cts +1 -0
- package/dist/cli/run.d.ts +1 -0
- package/dist/cli/run.js +126 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/index-D-K6sx8s.d.cts +8 -0
- package/dist/index-D-K6sx8s.d.ts +8 -0
- package/dist/index.cjs +2909 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +274 -0
- package/dist/index.d.ts +274 -0
- package/dist/index.js +1048 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/index.cjs +531 -0
- package/dist/llm/index.cjs.map +1 -0
- package/dist/llm/index.d.cts +70 -0
- package/dist/llm/index.d.ts +70 -0
- package/dist/llm/index.js +24 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/mcp/index.cjs +323 -0
- package/dist/mcp/index.cjs.map +1 -0
- package/dist/mcp/index.d.cts +39 -0
- package/dist/mcp/index.d.ts +39 -0
- package/dist/mcp/index.js +11 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/memory/index.cjs +146 -0
- package/dist/memory/index.cjs.map +1 -0
- package/dist/memory/index.d.cts +51 -0
- package/dist/memory/index.d.ts +51 -0
- package/dist/memory/index.js +10 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/tools/fuzzy-edit.cjs +200 -0
- package/dist/tools/fuzzy-edit.cjs.map +1 -0
- package/dist/tools/fuzzy-edit.d.cts +9 -0
- package/dist/tools/fuzzy-edit.d.ts +9 -0
- package/dist/tools/fuzzy-edit.js +12 -0
- package/dist/tools/fuzzy-edit.js.map +1 -0
- package/dist/tools/index.cjs +1032 -0
- package/dist/tools/index.cjs.map +1 -0
- package/dist/tools/index.d.cts +4 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.js +39 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/types-C11cw5ZD.d.cts +177 -0
- package/dist/types-C11cw5ZD.d.ts +177 -0
- package/dist/utils-TF4TBXQJ.js +10 -0
- package/dist/utils-TF4TBXQJ.js.map +1 -0
- package/dist/web-fetch-B42QzYD2.d.cts +85 -0
- package/dist/web-fetch-EDdhxmEf.d.ts +85 -0
- package/package.json +134 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1048 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AnthropicProvider,
|
|
3
|
+
BaseProvider,
|
|
4
|
+
OllamaProvider,
|
|
5
|
+
OpenAIProvider,
|
|
6
|
+
RetryableError,
|
|
7
|
+
anthropic,
|
|
8
|
+
createProvider,
|
|
9
|
+
ollama,
|
|
10
|
+
openai
|
|
11
|
+
} from "./chunk-F34HO4RA.js";
|
|
12
|
+
import {
|
|
13
|
+
BashTool,
|
|
14
|
+
FileEditTool,
|
|
15
|
+
FileReadTool,
|
|
16
|
+
FileWriteTool,
|
|
17
|
+
GlobTool,
|
|
18
|
+
GrepTool,
|
|
19
|
+
ToolRegistry,
|
|
20
|
+
WebFetchTool,
|
|
21
|
+
orchestrateTools
|
|
22
|
+
} from "./chunk-53ZOIXM4.js";
|
|
23
|
+
import "./chunk-W6OLZ2OI.js";
|
|
24
|
+
import {
|
|
25
|
+
InMemoryStore,
|
|
26
|
+
SQLiteStore
|
|
27
|
+
} from "./chunk-CARUMOML.js";
|
|
28
|
+
import {
|
|
29
|
+
MCPClient,
|
|
30
|
+
loadMCPTools
|
|
31
|
+
} from "./chunk-TLPOO6C3.js";
|
|
32
|
+
import {
|
|
33
|
+
defineTool,
|
|
34
|
+
defineToolFromClass
|
|
35
|
+
} from "./chunk-FK7S2S7V.js";
|
|
36
|
+
import {
|
|
37
|
+
init
|
|
38
|
+
} from "./chunk-BWXHO6UJ.js";
|
|
39
|
+
import "./chunk-MQ7XP6VT.js";
|
|
40
|
+
import "./chunk-DGUM43GV.js";
|
|
41
|
+
|
|
42
|
+
// src/types.ts
|
|
43
|
+
var DEFAULT_CONTEXT_WINDOW = 2e5;
|
|
44
|
+
var DEFAULT_MAX_OUTPUT_TOKENS = 16384;
|
|
45
|
+
var DEFAULT_COMPACT_THRESHOLD = 0.85;
|
|
46
|
+
var DEFAULT_MAX_TURNS = 100;
|
|
47
|
+
var MAX_CONCURRENT_TOOLS = 10;
|
|
48
|
+
|
|
49
|
+
// src/hooks.ts
|
|
50
|
+
var HookRunner = class {
|
|
51
|
+
hooks;
|
|
52
|
+
constructor(hooks) {
|
|
53
|
+
this.hooks = hooks ?? {};
|
|
54
|
+
}
|
|
55
|
+
async runOnTurnStart(turn, messages) {
|
|
56
|
+
if (!this.hooks.onTurnStart) return messages;
|
|
57
|
+
const result = await this.hooks.onTurnStart(turn, messages);
|
|
58
|
+
return Array.isArray(result) ? result : messages;
|
|
59
|
+
}
|
|
60
|
+
async runOnTurnEnd(turn, messages) {
|
|
61
|
+
await this.hooks.onTurnEnd?.(turn, messages);
|
|
62
|
+
}
|
|
63
|
+
async runOnToolUse(name, input) {
|
|
64
|
+
if (!this.hooks.onToolUse) return { allow: true, input };
|
|
65
|
+
const result = await this.hooks.onToolUse(name, input);
|
|
66
|
+
return {
|
|
67
|
+
allow: result?.allow ?? true,
|
|
68
|
+
input: result?.input ?? input
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async runOnToolResult(result) {
|
|
72
|
+
if (!this.hooks.onToolResult) return result.output;
|
|
73
|
+
const modified = await this.hooks.onToolResult(result);
|
|
74
|
+
return modified ?? result.output;
|
|
75
|
+
}
|
|
76
|
+
async runOnError(error, turn) {
|
|
77
|
+
if (!this.hooks.onError) return { retry: false };
|
|
78
|
+
const result = await this.hooks.onError(error, turn);
|
|
79
|
+
return { retry: result?.retry ?? false, message: result?.message };
|
|
80
|
+
}
|
|
81
|
+
async runOnCompact(result) {
|
|
82
|
+
await this.hooks.onCompact?.(result);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// src/loop.ts
|
|
87
|
+
var DEFAULT_MAX_RETRIES = 5;
|
|
88
|
+
var INITIAL_RETRY_DELAY_MS = 1e3;
|
|
89
|
+
var MAX_RETRY_DELAY_MS = 3e4;
|
|
90
|
+
var DOOM_LOOP_THRESHOLD = 3;
|
|
91
|
+
var PRE_OVERFLOW_BUFFER = 2e4;
|
|
92
|
+
var OUTPUT_MAX_LINES = 2e3;
|
|
93
|
+
var OUTPUT_MAX_BYTES = 5e4;
|
|
94
|
+
function parseRetryAfterMs(error) {
|
|
95
|
+
const msg = error.message ?? "";
|
|
96
|
+
const retryAfterMsMatch = msg.match(/retry-after-ms:\s*(\d+)/i);
|
|
97
|
+
if (retryAfterMsMatch) return parseInt(retryAfterMsMatch[1], 10);
|
|
98
|
+
const retryAfterMatch = msg.match(/retry-after:\s*(\d+)/i);
|
|
99
|
+
if (retryAfterMatch) return parseInt(retryAfterMatch[1], 10) * 1e3;
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
function truncateOutput(output, toolName, hasSubAgent) {
|
|
103
|
+
const lines = output.split("\n");
|
|
104
|
+
if (lines.length <= OUTPUT_MAX_LINES && output.length <= OUTPUT_MAX_BYTES) return output;
|
|
105
|
+
let truncated = "";
|
|
106
|
+
let byteCount = 0;
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
if (byteCount + line.length > OUTPUT_MAX_BYTES) break;
|
|
109
|
+
truncated += line + "\n";
|
|
110
|
+
byteCount += line.length;
|
|
111
|
+
if (byteCount > OUTPUT_MAX_BYTES) break;
|
|
112
|
+
}
|
|
113
|
+
const lineCount = truncated.split("\n").filter((l) => l.length > 0).length;
|
|
114
|
+
const suffix = hasSubAgent ? `
|
|
115
|
+
|
|
116
|
+
[Output truncated: ${lineCount}/${lines.length} lines shown. Use the Task tool to have a sub-agent process the full output.]` : `
|
|
117
|
+
|
|
118
|
+
[Output truncated: ${lineCount}/${lines.length} lines shown. Use Grep to search or Read with offset/limit for the full content.]`;
|
|
119
|
+
return truncated.trimEnd() + suffix;
|
|
120
|
+
}
|
|
121
|
+
function detectDoomLoop(messages) {
|
|
122
|
+
const recentToolCalls = [];
|
|
123
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
124
|
+
const msg = messages[i];
|
|
125
|
+
if (msg.role !== "assistant") continue;
|
|
126
|
+
if (typeof msg.content === "string") continue;
|
|
127
|
+
for (const block of msg.content) {
|
|
128
|
+
if (block.type === "tool_use") recentToolCalls.push(block);
|
|
129
|
+
}
|
|
130
|
+
if (recentToolCalls.length >= DOOM_LOOP_THRESHOLD) break;
|
|
131
|
+
}
|
|
132
|
+
if (recentToolCalls.length < DOOM_LOOP_THRESHOLD) return null;
|
|
133
|
+
const last = recentToolCalls[0];
|
|
134
|
+
return recentToolCalls.every(
|
|
135
|
+
(tc) => tc.name === last.name && JSON.stringify(tc.input) === JSON.stringify(last.input)
|
|
136
|
+
) ? last : null;
|
|
137
|
+
}
|
|
138
|
+
async function* agentLoop(config) {
|
|
139
|
+
const {
|
|
140
|
+
model,
|
|
141
|
+
tools,
|
|
142
|
+
messages,
|
|
143
|
+
systemPrompt,
|
|
144
|
+
maxTurns = DEFAULT_MAX_TURNS,
|
|
145
|
+
contextManager,
|
|
146
|
+
permissionEngine,
|
|
147
|
+
cwd = process.cwd(),
|
|
148
|
+
abortSignal,
|
|
149
|
+
hooks,
|
|
150
|
+
costTracker
|
|
151
|
+
} = config;
|
|
152
|
+
const hookRunner = new HookRunner(hooks);
|
|
153
|
+
const context = { cwd, env: { ...process.env } };
|
|
154
|
+
let turns = 0;
|
|
155
|
+
let consecutiveRetries = 0;
|
|
156
|
+
const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
157
|
+
let totalUsage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
|
|
158
|
+
let inFlightText = "";
|
|
159
|
+
let inFlightReasoning = "";
|
|
160
|
+
let inFlightToolCalls = [];
|
|
161
|
+
while (turns < maxTurns) {
|
|
162
|
+
if (abortSignal?.aborted) {
|
|
163
|
+
yield finalizeLoop("Aborted");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const messagesAfterHook = await hookRunner.runOnTurnStart(turns + 1, messages);
|
|
167
|
+
if (messagesAfterHook !== messages) {
|
|
168
|
+
messages.length = 0;
|
|
169
|
+
messages.push(...messagesAfterHook);
|
|
170
|
+
}
|
|
171
|
+
const contextCheck = contextManager.check(messages);
|
|
172
|
+
if (contextCheck.needsCompact) {
|
|
173
|
+
const result = await contextManager.compact(messages);
|
|
174
|
+
messages.length = 0;
|
|
175
|
+
messages.push(...result.messages);
|
|
176
|
+
if (result.tokensSaved > 0) {
|
|
177
|
+
const thinking = `Context ${result.method}: saved ~${result.tokensSaved} tokens (${contextCheck.totalTokens} \u2192 ${contextCheck.totalTokens - result.tokensSaved})`;
|
|
178
|
+
yield { type: "thinking", thinking };
|
|
179
|
+
await hookRunner.runOnCompact(result);
|
|
180
|
+
}
|
|
181
|
+
const pruned = contextManager.pruneToolOutputs(messages);
|
|
182
|
+
if (pruned.length !== messages.length) {
|
|
183
|
+
messages.length = 0;
|
|
184
|
+
messages.push(...pruned);
|
|
185
|
+
yield { type: "thinking", thinking: "Pruned large tool outputs to free additional context" };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const estimatedTotal = totalUsage.inputTokens + totalUsage.outputTokens + (totalUsage.cacheReadTokens ?? 0) + (totalUsage.cacheWriteTokens ?? 0);
|
|
189
|
+
const maxContext = contextManager.maxTokens;
|
|
190
|
+
const maxOutput = contextManager.maxOutputTokens;
|
|
191
|
+
if (maxContext > 0 && estimatedTotal + maxOutput + PRE_OVERFLOW_BUFFER >= maxContext) {
|
|
192
|
+
yield { type: "thinking", thinking: "Approaching context limit, compacting preemptively..." };
|
|
193
|
+
const result = await contextManager.compact(messages);
|
|
194
|
+
messages.length = 0;
|
|
195
|
+
messages.push(...result.messages);
|
|
196
|
+
if (result.tokensSaved > 0) {
|
|
197
|
+
yield { type: "thinking", thinking: `Pre-overflow compact: freed ~${result.tokensSaved} tokens` };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const available = contextManager.getAvailableTokens(messages);
|
|
201
|
+
const maxTokens = Math.min(available, contextManager.maxOutputTokens);
|
|
202
|
+
if (maxTokens < 1024) {
|
|
203
|
+
yield finalizeLoop("Insufficient context window for next turn");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
inFlightText = "";
|
|
207
|
+
inFlightReasoning = "";
|
|
208
|
+
inFlightToolCalls = [];
|
|
209
|
+
let response;
|
|
210
|
+
try {
|
|
211
|
+
const mappedMessages = messages.map((m) => {
|
|
212
|
+
if (m.role === "tool") {
|
|
213
|
+
const toolCall = inFlightToolCalls.find((tc) => tc.id === m.tool_use_id);
|
|
214
|
+
if (toolCall) {
|
|
215
|
+
return {
|
|
216
|
+
role: "tool",
|
|
217
|
+
tool_use_id: m.role === "tool" ? m.tool_use_id : void 0,
|
|
218
|
+
is_error: m.role === "tool" ? m.is_error : void 0,
|
|
219
|
+
content: truncateOutput(
|
|
220
|
+
m.content,
|
|
221
|
+
toolCall.name,
|
|
222
|
+
tools.has("delegate_agent")
|
|
223
|
+
)
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
role: m.role,
|
|
229
|
+
content: m.role === "tool" ? m.content : m.role === "user" ? m.content : m.content,
|
|
230
|
+
tool_use_id: m.role === "tool" ? m.tool_use_id : void 0,
|
|
231
|
+
is_error: m.role === "tool" ? m.is_error : void 0
|
|
232
|
+
};
|
|
233
|
+
});
|
|
234
|
+
response = await model.chat({
|
|
235
|
+
messages: mappedMessages,
|
|
236
|
+
tools: tools.getAPI(),
|
|
237
|
+
systemPrompt,
|
|
238
|
+
maxOutputTokens: maxTokens,
|
|
239
|
+
abortSignal
|
|
240
|
+
});
|
|
241
|
+
} catch (err) {
|
|
242
|
+
yield* flushInFlight();
|
|
243
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
244
|
+
const hookResult = await hookRunner.runOnError(error, turns + 1);
|
|
245
|
+
if (hookResult.retry && hookResult.message) {
|
|
246
|
+
messages.push({ role: "user", content: hookResult.message });
|
|
247
|
+
turns++;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (error.message.includes("429") || error.message.includes("529") || error.message.includes("overloaded")) {
|
|
251
|
+
consecutiveRetries++;
|
|
252
|
+
if (consecutiveRetries > maxRetries) {
|
|
253
|
+
yield { type: "error", error: new Error(`Max retries (${maxRetries}) exceeded`) };
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const headerDelay = parseRetryAfterMs(error);
|
|
257
|
+
const backoffDelay = headerDelay ?? Math.min(INITIAL_RETRY_DELAY_MS * 2 ** (consecutiveRetries - 1) + Math.random() * 500, MAX_RETRY_DELAY_MS);
|
|
258
|
+
yield { type: "thinking", thinking: `Rate limited, retry ${consecutiveRetries}/${maxRetries} in ${Math.round(backoffDelay)}ms...` };
|
|
259
|
+
await new Promise((r) => setTimeout(r, backoffDelay));
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
yield { type: "error", error };
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
totalUsage.inputTokens += response.usage.inputTokens;
|
|
266
|
+
totalUsage.outputTokens += response.usage.outputTokens;
|
|
267
|
+
totalUsage.cacheReadTokens = (totalUsage.cacheReadTokens ?? 0) + (response.usage.cacheReadTokens ?? 0);
|
|
268
|
+
totalUsage.cacheWriteTokens = (totalUsage.cacheWriteTokens ?? 0) + (response.usage.cacheWriteTokens ?? 0);
|
|
269
|
+
if (costTracker) {
|
|
270
|
+
costTracker.record(model.model, response.usage, turns + 1);
|
|
271
|
+
}
|
|
272
|
+
const toolCalls = [];
|
|
273
|
+
for (const block of response.content) {
|
|
274
|
+
if (block.type === "text") {
|
|
275
|
+
inFlightText += block.text;
|
|
276
|
+
yield { type: "text", text: block.text };
|
|
277
|
+
} else if (block.type === "thinking") {
|
|
278
|
+
inFlightReasoning += block.thinking;
|
|
279
|
+
yield { type: "thinking", thinking: block.thinking };
|
|
280
|
+
} else if (block.type === "tool_use") {
|
|
281
|
+
const resolvedName = tools.has(block.name) ? block.name : tools.findCaseInsensitive(block.name);
|
|
282
|
+
if (!resolvedName) {
|
|
283
|
+
yield { type: "tool_result", id: block.id, name: block.name, output: `Unknown tool: ${block.name}. Available: ${tools.listNames().join(", ")}`, isError: true };
|
|
284
|
+
messages.push({ role: "assistant", content: response.content });
|
|
285
|
+
messages.push({ role: "tool", tool_use_id: block.id, content: `Unknown tool: ${block.name}`, is_error: true });
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const modifiedBlock = resolvedName !== block.name ? { ...block, name: resolvedName } : block;
|
|
289
|
+
const hookToolResult = await hookRunner.runOnToolUse(modifiedBlock.name, modifiedBlock.input);
|
|
290
|
+
if (!hookToolResult.allow) {
|
|
291
|
+
yield { type: "tool_result", id: modifiedBlock.id, name: modifiedBlock.name, output: "Tool denied by hook", isError: true };
|
|
292
|
+
messages.push({ role: "assistant", content: response.content });
|
|
293
|
+
messages.push({ role: "tool", tool_use_id: modifiedBlock.id, content: "Tool denied by hook", is_error: true });
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const finalBlock = { ...modifiedBlock, input: hookToolResult.input };
|
|
297
|
+
toolCalls.push(finalBlock);
|
|
298
|
+
inFlightToolCalls.push(finalBlock);
|
|
299
|
+
yield { type: "tool_use", id: finalBlock.id, name: finalBlock.name, input: finalBlock.input };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const hasNonToolContent = inFlightText.length > 0 || inFlightReasoning.length > 0;
|
|
303
|
+
if (!hasNonToolContent && toolCalls.length === 0) {
|
|
304
|
+
yield finalizeLoop("Assistant produced no content");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const assistantContent = response.content.filter((b) => {
|
|
308
|
+
if (b.type !== "tool_use") return true;
|
|
309
|
+
return toolCalls.some((tc) => tc.id === b.id);
|
|
310
|
+
});
|
|
311
|
+
messages.push({ role: "assistant", content: assistantContent });
|
|
312
|
+
turns++;
|
|
313
|
+
consecutiveRetries = 0;
|
|
314
|
+
inFlightText = "";
|
|
315
|
+
inFlightReasoning = "";
|
|
316
|
+
inFlightToolCalls = [];
|
|
317
|
+
await hookRunner.runOnTurnEnd(turns, messages);
|
|
318
|
+
if (response.stopReason !== "tool_use" || toolCalls.length === 0) {
|
|
319
|
+
yield {
|
|
320
|
+
type: "done",
|
|
321
|
+
usage: {
|
|
322
|
+
inputTokens: totalUsage.inputTokens,
|
|
323
|
+
outputTokens: totalUsage.outputTokens,
|
|
324
|
+
cacheReadTokens: totalUsage.cacheReadTokens,
|
|
325
|
+
cacheWriteTokens: totalUsage.cacheWriteTokens
|
|
326
|
+
},
|
|
327
|
+
messages: [...messages]
|
|
328
|
+
};
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const doomLoopCall = detectDoomLoop(messages);
|
|
332
|
+
if (doomLoopCall) {
|
|
333
|
+
yield { type: "thinking", thinking: `Detected repetitive tool calls: ${doomLoopCall.name} called ${DOOM_LOOP_THRESHOLD} times with identical input. Breaking the loop.` };
|
|
334
|
+
messages.push({ role: "user", content: `STOP. You have called ${doomLoopCall.name} ${DOOM_LOOP_THRESHOLD} times in a row with the same input. This indicates a loop. Try a different approach or tool.` });
|
|
335
|
+
turns++;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const results = await orchestrateTools(
|
|
339
|
+
tools,
|
|
340
|
+
toolCalls,
|
|
341
|
+
context,
|
|
342
|
+
async (name, _input) => {
|
|
343
|
+
const perm = permissionEngine.check(name);
|
|
344
|
+
return perm.allowed;
|
|
345
|
+
},
|
|
346
|
+
abortSignal
|
|
347
|
+
);
|
|
348
|
+
for (const result of results) {
|
|
349
|
+
let output = truncateOutput(result.output, result.name, tools.has("delegate_agent"));
|
|
350
|
+
const toolResult = await hookRunner.runOnToolResult({
|
|
351
|
+
id: result.id,
|
|
352
|
+
name: result.name,
|
|
353
|
+
output: result.output,
|
|
354
|
+
isError: result.isError,
|
|
355
|
+
durationMs: result.durationMs
|
|
356
|
+
});
|
|
357
|
+
output = toolResult;
|
|
358
|
+
messages.push({
|
|
359
|
+
role: "tool",
|
|
360
|
+
tool_use_id: result.id,
|
|
361
|
+
content: output,
|
|
362
|
+
is_error: result.isError
|
|
363
|
+
});
|
|
364
|
+
yield {
|
|
365
|
+
type: "tool_result",
|
|
366
|
+
id: result.id,
|
|
367
|
+
name: result.name,
|
|
368
|
+
output,
|
|
369
|
+
isError: result.isError
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
yield { type: "error", error: new Error(`Max turns (${maxTurns}) reached`) };
|
|
374
|
+
function* flushInFlight() {
|
|
375
|
+
if (inFlightText.length > 0) {
|
|
376
|
+
yield { type: "text", text: inFlightText };
|
|
377
|
+
}
|
|
378
|
+
if (inFlightReasoning.length > 0) {
|
|
379
|
+
yield { type: "thinking", thinking: inFlightReasoning };
|
|
380
|
+
}
|
|
381
|
+
for (const tc of inFlightToolCalls) {
|
|
382
|
+
yield { type: "tool_result", id: tc.id, name: tc.name, output: "[Tool execution was interrupted]", isError: true };
|
|
383
|
+
messages.push({ role: "assistant", content: [{ type: "tool_use", id: tc.id, name: tc.name, input: tc.input }] });
|
|
384
|
+
messages.push({ role: "tool", tool_use_id: tc.id, content: "[Tool execution was interrupted]", is_error: true });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function finalizeLoop(reason) {
|
|
388
|
+
if (totalUsage.inputTokens > 0 || totalUsage.outputTokens > 0) {
|
|
389
|
+
return {
|
|
390
|
+
type: "done",
|
|
391
|
+
usage: {
|
|
392
|
+
inputTokens: totalUsage.inputTokens,
|
|
393
|
+
outputTokens: totalUsage.outputTokens,
|
|
394
|
+
cacheReadTokens: totalUsage.cacheReadTokens,
|
|
395
|
+
cacheWriteTokens: totalUsage.cacheWriteTokens
|
|
396
|
+
},
|
|
397
|
+
messages: [...messages]
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
return { type: "error", error: new Error(reason) };
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/context/tokenizer.ts
|
|
405
|
+
function estimateTokens(text) {
|
|
406
|
+
const chars = text.length;
|
|
407
|
+
const words = text.split(/\s+/).filter(Boolean).length;
|
|
408
|
+
return Math.max(Math.ceil(chars / 4), Math.ceil(words * 1.3));
|
|
409
|
+
}
|
|
410
|
+
function estimateMessageTokens(message) {
|
|
411
|
+
if (message.role === "user") {
|
|
412
|
+
return estimateTokens(message.content) + 4;
|
|
413
|
+
}
|
|
414
|
+
if (message.role === "tool") {
|
|
415
|
+
return estimateTokens(message.content) + 4;
|
|
416
|
+
}
|
|
417
|
+
let tokens = 0;
|
|
418
|
+
for (const block of message.content) {
|
|
419
|
+
if (block.type === "text") {
|
|
420
|
+
tokens += estimateTokens(block.text) + 4;
|
|
421
|
+
} else if (block.type === "tool_use") {
|
|
422
|
+
tokens += estimateTokens(block.name) + estimateTokens(JSON.stringify(block.input)) + 4;
|
|
423
|
+
} else if (block.type === "thinking") {
|
|
424
|
+
tokens += estimateTokens(block.thinking) + 4;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return tokens;
|
|
428
|
+
}
|
|
429
|
+
function estimateConversationTokens(messages) {
|
|
430
|
+
const byRole = {};
|
|
431
|
+
let total = 0;
|
|
432
|
+
for (const message of messages) {
|
|
433
|
+
const msgTokens = estimateMessageTokens(message) + 10;
|
|
434
|
+
total += msgTokens;
|
|
435
|
+
byRole[message.role] = (byRole[message.role] ?? 0) + msgTokens;
|
|
436
|
+
}
|
|
437
|
+
return { total, byRole };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// src/context/manager.ts
|
|
441
|
+
var PRUNE_MINIMUM = 2e4;
|
|
442
|
+
var PRUNE_PROTECT_TOKENS = 4e4;
|
|
443
|
+
var PROTECTED_TOOL_TYPES = /* @__PURE__ */ new Set(["skill", "delegate_agent"]);
|
|
444
|
+
var RECENT_TURNS_TO_PROTECT = 2;
|
|
445
|
+
var ContextManager = class {
|
|
446
|
+
maxTokens;
|
|
447
|
+
maxOutputTokens;
|
|
448
|
+
compactThreshold;
|
|
449
|
+
constructor(config) {
|
|
450
|
+
this.maxTokens = config?.maxTokens ?? DEFAULT_CONTEXT_WINDOW;
|
|
451
|
+
this.maxOutputTokens = config?.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
|
|
452
|
+
this.compactThreshold = config?.compactThreshold ?? DEFAULT_COMPACT_THRESHOLD;
|
|
453
|
+
}
|
|
454
|
+
check(messages) {
|
|
455
|
+
const { total: totalTokens } = estimateConversationTokens(messages);
|
|
456
|
+
const availableTokens = this.maxTokens - totalTokens - this.maxOutputTokens;
|
|
457
|
+
const usagePercent = totalTokens / this.maxTokens;
|
|
458
|
+
const needsCompact = usagePercent >= this.compactThreshold;
|
|
459
|
+
let recommendedMethod;
|
|
460
|
+
if (usagePercent > 0.95) {
|
|
461
|
+
recommendedMethod = "compact";
|
|
462
|
+
} else {
|
|
463
|
+
recommendedMethod = "snip";
|
|
464
|
+
}
|
|
465
|
+
return { totalTokens, availableTokens, usagePercent, needsCompact, recommendedMethod };
|
|
466
|
+
}
|
|
467
|
+
async compact(messages, summaryFn) {
|
|
468
|
+
const contextCheck = this.check(messages);
|
|
469
|
+
if (!contextCheck.needsCompact) {
|
|
470
|
+
return { messages: [...messages], tokensSaved: 0, method: "none" };
|
|
471
|
+
}
|
|
472
|
+
const originalTokens = contextCheck.totalTokens;
|
|
473
|
+
if (summaryFn && contextCheck.recommendedMethod === "compact") {
|
|
474
|
+
const recentCount = Math.max(Math.ceil(messages.length * 0.2), 1);
|
|
475
|
+
const cutoffIndex = messages.length - recentCount;
|
|
476
|
+
const oldMessages = messages.slice(0, cutoffIndex);
|
|
477
|
+
const recentMessages = messages.slice(cutoffIndex);
|
|
478
|
+
const summary = await summaryFn(oldMessages);
|
|
479
|
+
const summaryMessage = {
|
|
480
|
+
role: "user",
|
|
481
|
+
content: `Here is a summary of our previous conversation:
|
|
482
|
+
${summary}`
|
|
483
|
+
};
|
|
484
|
+
const compacted = [summaryMessage, ...recentMessages];
|
|
485
|
+
const newTokens2 = estimateConversationTokens(compacted).total;
|
|
486
|
+
return {
|
|
487
|
+
messages: compacted,
|
|
488
|
+
tokensSaved: originalTokens - newTokens2,
|
|
489
|
+
method: "compact"
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
const keepFirst = 1;
|
|
493
|
+
const keepLast = 20;
|
|
494
|
+
const keepMin = keepFirst + keepLast;
|
|
495
|
+
if (messages.length <= keepMin) {
|
|
496
|
+
return { messages: [...messages], tokensSaved: 0, method: "none" };
|
|
497
|
+
}
|
|
498
|
+
let result = [...messages];
|
|
499
|
+
const targetUsage = 0.6;
|
|
500
|
+
while (result.length > keepMin) {
|
|
501
|
+
const { total } = estimateConversationTokens(result);
|
|
502
|
+
if (total / this.maxTokens < targetUsage) break;
|
|
503
|
+
const removeCount = Math.max(1, Math.floor((result.length - keepMin) * 0.3));
|
|
504
|
+
const removeEnd = Math.min(keepFirst + removeCount, result.length - keepLast);
|
|
505
|
+
result = [result[0], ...result.slice(removeEnd)];
|
|
506
|
+
}
|
|
507
|
+
const newTokens = estimateConversationTokens(result).total;
|
|
508
|
+
return {
|
|
509
|
+
messages: result,
|
|
510
|
+
tokensSaved: originalTokens - newTokens,
|
|
511
|
+
method: "snip"
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
pruneToolOutputs(messages) {
|
|
515
|
+
let totalPruned = 0;
|
|
516
|
+
let turnCount = 0;
|
|
517
|
+
const result = [...messages];
|
|
518
|
+
for (let i = result.length - 1; i >= 0; i--) {
|
|
519
|
+
const msg = result[i];
|
|
520
|
+
if (msg.role === "user") {
|
|
521
|
+
turnCount++;
|
|
522
|
+
if (turnCount <= RECENT_TURNS_TO_PROTECT) continue;
|
|
523
|
+
}
|
|
524
|
+
if (msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
|
|
525
|
+
for (const block of msg.content) {
|
|
526
|
+
if (block.type !== "tool_use") continue;
|
|
527
|
+
if (PROTECTED_TOOL_TYPES.has(block.name)) continue;
|
|
528
|
+
const toolResultIdx = result.findIndex(
|
|
529
|
+
(m, j) => j > i && m.role === "tool" && m.tool_use_id === block.id
|
|
530
|
+
);
|
|
531
|
+
if (toolResultIdx === -1) continue;
|
|
532
|
+
const toolResult = result[toolResultIdx];
|
|
533
|
+
const output = toolResult.content;
|
|
534
|
+
const estTokens = Math.ceil(output.length / 4);
|
|
535
|
+
if (totalPruned + estTokens >= PRUNE_MINIMUM && totalPruned >= PRUNE_PROTECT_TOKENS) {
|
|
536
|
+
return result;
|
|
537
|
+
}
|
|
538
|
+
totalPruned += estTokens;
|
|
539
|
+
result[toolResultIdx] = {
|
|
540
|
+
...toolResult,
|
|
541
|
+
content: `[Output pruned: ${estTokens} tokens saved]`,
|
|
542
|
+
is_error: false
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return result;
|
|
547
|
+
}
|
|
548
|
+
getAvailableTokens(messages) {
|
|
549
|
+
return this.maxTokens - estimateConversationTokens(messages).total - this.maxOutputTokens;
|
|
550
|
+
}
|
|
551
|
+
getUsagePercent(messages) {
|
|
552
|
+
return estimateConversationTokens(messages).total / this.maxTokens;
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
// src/permissions/engine.ts
|
|
557
|
+
function matchPattern(pattern, toolName) {
|
|
558
|
+
if (!pattern.includes("*")) {
|
|
559
|
+
return pattern === toolName;
|
|
560
|
+
}
|
|
561
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
562
|
+
const regex = new RegExp(`^${escaped}$`);
|
|
563
|
+
return regex.test(toolName);
|
|
564
|
+
}
|
|
565
|
+
var PermissionEngine = class {
|
|
566
|
+
allowedPatterns;
|
|
567
|
+
deniedPatterns;
|
|
568
|
+
askPatterns;
|
|
569
|
+
defaultAction;
|
|
570
|
+
constructor(config) {
|
|
571
|
+
this.allowedPatterns = config?.allowedTools ?? [];
|
|
572
|
+
this.deniedPatterns = config?.deniedTools ?? [];
|
|
573
|
+
this.askPatterns = config?.askTools ?? [];
|
|
574
|
+
this.defaultAction = config?.defaultAction ?? "allow";
|
|
575
|
+
}
|
|
576
|
+
check(toolName, _input) {
|
|
577
|
+
for (const pattern of this.deniedPatterns) {
|
|
578
|
+
if (matchPattern(pattern, toolName)) {
|
|
579
|
+
return { allowed: false, reason: `Tool '${toolName}' matches denied pattern '${pattern}'` };
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
for (const pattern of this.allowedPatterns) {
|
|
583
|
+
if (matchPattern(pattern, toolName)) {
|
|
584
|
+
return { allowed: true };
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
for (const pattern of this.askPatterns) {
|
|
588
|
+
if (matchPattern(pattern, toolName)) {
|
|
589
|
+
return { allowed: false, reason: "ask" };
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (this.defaultAction === "deny") {
|
|
593
|
+
return { allowed: false, reason: `Tool '${toolName}' not explicitly allowed` };
|
|
594
|
+
}
|
|
595
|
+
if (this.defaultAction === "ask") {
|
|
596
|
+
return { allowed: false, reason: "ask" };
|
|
597
|
+
}
|
|
598
|
+
return { allowed: true };
|
|
599
|
+
}
|
|
600
|
+
addAllowed(pattern) {
|
|
601
|
+
this.allowedPatterns.push(pattern);
|
|
602
|
+
}
|
|
603
|
+
addDenied(pattern) {
|
|
604
|
+
this.deniedPatterns.push(pattern);
|
|
605
|
+
}
|
|
606
|
+
addAsk(pattern) {
|
|
607
|
+
this.askPatterns.push(pattern);
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
// src/cost/tracker.ts
|
|
612
|
+
var DEFAULT_PRICING = {
|
|
613
|
+
"claude-sonnet-4-20250514": { inputCostPer1k: 3e-3, outputCostPer1k: 0.015 },
|
|
614
|
+
"claude-3-5-sonnet-20241022": { inputCostPer1k: 3e-3, outputCostPer1k: 0.015 },
|
|
615
|
+
"claude-3-5-haiku-20241022": { inputCostPer1k: 8e-4, outputCostPer1k: 4e-3 },
|
|
616
|
+
"claude-opus-4-20250514": { inputCostPer1k: 0.015, outputCostPer1k: 0.075 },
|
|
617
|
+
"gpt-4o": { inputCostPer1k: 25e-4, outputCostPer1k: 0.01 },
|
|
618
|
+
"gpt-4o-mini": { inputCostPer1k: 15e-5, outputCostPer1k: 6e-4 },
|
|
619
|
+
"gpt-4-turbo": { inputCostPer1k: 0.01, outputCostPer1k: 0.03 }
|
|
620
|
+
};
|
|
621
|
+
var CostTracker = class {
|
|
622
|
+
records = [];
|
|
623
|
+
pricing;
|
|
624
|
+
constructor(customPricing) {
|
|
625
|
+
this.pricing = { ...DEFAULT_PRICING, ...customPricing };
|
|
626
|
+
}
|
|
627
|
+
record(model, usage, turn, toolName) {
|
|
628
|
+
const pricing = this.pricing[model] ?? { inputCostPer1k: 1e-3, outputCostPer1k: 3e-3 };
|
|
629
|
+
this.records.push({
|
|
630
|
+
model,
|
|
631
|
+
inputTokens: usage.inputTokens,
|
|
632
|
+
outputTokens: usage.outputTokens,
|
|
633
|
+
cacheReadTokens: usage.cacheReadTokens ?? 0,
|
|
634
|
+
cacheWriteTokens: usage.cacheWriteTokens ?? 0,
|
|
635
|
+
inputCost: usage.inputTokens / 1e3 * pricing.inputCostPer1k,
|
|
636
|
+
outputCost: usage.outputTokens / 1e3 * pricing.outputCostPer1k,
|
|
637
|
+
timestamp: Date.now(),
|
|
638
|
+
turn,
|
|
639
|
+
toolName
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
getTotal() {
|
|
643
|
+
let cost = 0;
|
|
644
|
+
let input = 0;
|
|
645
|
+
let output = 0;
|
|
646
|
+
let cacheRead = 0;
|
|
647
|
+
let cacheWrite = 0;
|
|
648
|
+
for (const r of this.records) {
|
|
649
|
+
cost += r.inputCost + r.outputCost;
|
|
650
|
+
input += r.inputTokens;
|
|
651
|
+
output += r.outputTokens;
|
|
652
|
+
cacheRead += r.cacheReadTokens;
|
|
653
|
+
cacheWrite += r.cacheWriteTokens;
|
|
654
|
+
}
|
|
655
|
+
return { cost, inputTokens: input, outputTokens: output, cacheReadTokens: cacheRead, cacheWriteTokens: cacheWrite };
|
|
656
|
+
}
|
|
657
|
+
getByModel() {
|
|
658
|
+
const byModel = {};
|
|
659
|
+
for (const r of this.records) {
|
|
660
|
+
if (!byModel[r.model]) byModel[r.model] = { cost: 0, calls: 0 };
|
|
661
|
+
byModel[r.model].cost += r.inputCost + r.outputCost;
|
|
662
|
+
byModel[r.model].calls++;
|
|
663
|
+
}
|
|
664
|
+
return byModel;
|
|
665
|
+
}
|
|
666
|
+
getByTool() {
|
|
667
|
+
const byTool = {};
|
|
668
|
+
for (const r of this.records) {
|
|
669
|
+
const key = r.toolName ?? "llm";
|
|
670
|
+
if (!byTool[key]) byTool[key] = { cost: 0, calls: 0 };
|
|
671
|
+
byTool[key].cost += r.inputCost + r.outputCost;
|
|
672
|
+
byTool[key].calls++;
|
|
673
|
+
}
|
|
674
|
+
return byTool;
|
|
675
|
+
}
|
|
676
|
+
getRecords() {
|
|
677
|
+
return [...this.records];
|
|
678
|
+
}
|
|
679
|
+
reset() {
|
|
680
|
+
this.records = [];
|
|
681
|
+
}
|
|
682
|
+
setPricing(model, pricing) {
|
|
683
|
+
this.pricing[model] = pricing;
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
// src/agent.ts
|
|
688
|
+
var Agent = class _Agent {
|
|
689
|
+
model;
|
|
690
|
+
tools;
|
|
691
|
+
systemPrompt;
|
|
692
|
+
maxTurns;
|
|
693
|
+
contextManager;
|
|
694
|
+
permissionEngine;
|
|
695
|
+
permissionConfig;
|
|
696
|
+
cwd;
|
|
697
|
+
maxRetries;
|
|
698
|
+
messages = [];
|
|
699
|
+
hooks;
|
|
700
|
+
memory;
|
|
701
|
+
threadId;
|
|
702
|
+
costTracker;
|
|
703
|
+
_loaded = false;
|
|
704
|
+
_title;
|
|
705
|
+
_titleFetched = false;
|
|
706
|
+
_disableTitle = false;
|
|
707
|
+
constructor(config) {
|
|
708
|
+
this.model = config.model;
|
|
709
|
+
this.tools = new ToolRegistry(config.tools ?? []);
|
|
710
|
+
this.systemPrompt = config.systemPrompt ?? "";
|
|
711
|
+
this.maxTurns = config.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
712
|
+
this.contextManager = new ContextManager({
|
|
713
|
+
maxTokens: config.context?.maxTokens ?? DEFAULT_CONTEXT_WINDOW,
|
|
714
|
+
maxOutputTokens: config.context?.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS,
|
|
715
|
+
compactThreshold: config.context?.compactThreshold ?? DEFAULT_COMPACT_THRESHOLD
|
|
716
|
+
});
|
|
717
|
+
this.permissionEngine = new PermissionEngine(config.permissions);
|
|
718
|
+
this.permissionConfig = config.permissions;
|
|
719
|
+
this.cwd = config.cwd ?? process.cwd();
|
|
720
|
+
this.maxRetries = config.maxRetries ?? 5;
|
|
721
|
+
const v2 = config;
|
|
722
|
+
this.hooks = v2.hooks;
|
|
723
|
+
this.memory = v2.memory;
|
|
724
|
+
this.threadId = v2.threadId;
|
|
725
|
+
this.costTracker = v2.costTracker ?? new CostTracker();
|
|
726
|
+
this._title = v2.title;
|
|
727
|
+
this._disableTitle = v2.disableTitle ?? false;
|
|
728
|
+
}
|
|
729
|
+
addTool(tool) {
|
|
730
|
+
this.tools.add(tool);
|
|
731
|
+
}
|
|
732
|
+
addMessage(message) {
|
|
733
|
+
this.messages.push(message);
|
|
734
|
+
}
|
|
735
|
+
reset() {
|
|
736
|
+
this.messages = [];
|
|
737
|
+
this._titleFetched = false;
|
|
738
|
+
}
|
|
739
|
+
getMessages() {
|
|
740
|
+
return [...this.messages];
|
|
741
|
+
}
|
|
742
|
+
getCostTracker() {
|
|
743
|
+
return this.costTracker;
|
|
744
|
+
}
|
|
745
|
+
get title() {
|
|
746
|
+
return this._title;
|
|
747
|
+
}
|
|
748
|
+
async loadMemory() {
|
|
749
|
+
if (this._loaded || !this.memory || !this.threadId) return;
|
|
750
|
+
const stored = await this.memory.getThread(this.threadId);
|
|
751
|
+
if (stored.length > 0) {
|
|
752
|
+
this.messages = stored;
|
|
753
|
+
}
|
|
754
|
+
this._loaded = true;
|
|
755
|
+
}
|
|
756
|
+
async saveMemory() {
|
|
757
|
+
if (!this.memory || !this.threadId) return;
|
|
758
|
+
await this.memory.saveThread(this.threadId, this.messages);
|
|
759
|
+
}
|
|
760
|
+
fork(newThreadId) {
|
|
761
|
+
const forked = new _Agent({
|
|
762
|
+
model: this.model,
|
|
763
|
+
tools: this.tools.getAll(),
|
|
764
|
+
systemPrompt: this.systemPrompt,
|
|
765
|
+
maxTurns: this.maxTurns,
|
|
766
|
+
cwd: this.cwd,
|
|
767
|
+
maxRetries: this.maxRetries,
|
|
768
|
+
permissions: this.permissionConfig,
|
|
769
|
+
hooks: this.hooks,
|
|
770
|
+
memory: this.memory,
|
|
771
|
+
threadId: newThreadId,
|
|
772
|
+
costTracker: this.costTracker,
|
|
773
|
+
title: this._title,
|
|
774
|
+
context: {
|
|
775
|
+
maxTokens: this.contextManager.maxTokens,
|
|
776
|
+
maxOutputTokens: this.contextManager.maxOutputTokens,
|
|
777
|
+
compactThreshold: this.contextManager.compactThreshold
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
forked.messages = [...this.messages];
|
|
781
|
+
return forked;
|
|
782
|
+
}
|
|
783
|
+
async *run(prompt, options) {
|
|
784
|
+
await this.loadMemory();
|
|
785
|
+
const loopMessages = [...this.messages, { role: "user", content: prompt }];
|
|
786
|
+
if (!this._titleFetched && this.messages.length === 0 && !this._disableTitle) {
|
|
787
|
+
this._titleFetched = true;
|
|
788
|
+
this.generateTitle(prompt).catch(() => {
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
const loop = agentLoop({
|
|
792
|
+
model: this.model,
|
|
793
|
+
tools: this.tools,
|
|
794
|
+
messages: loopMessages,
|
|
795
|
+
systemPrompt: this.systemPrompt || void 0,
|
|
796
|
+
maxTurns: this.maxTurns,
|
|
797
|
+
contextManager: this.contextManager,
|
|
798
|
+
permissionEngine: this.permissionEngine,
|
|
799
|
+
cwd: this.cwd,
|
|
800
|
+
abortSignal: options?.abortSignal,
|
|
801
|
+
maxRetries: this.maxRetries,
|
|
802
|
+
hooks: this.hooks,
|
|
803
|
+
costTracker: this.costTracker
|
|
804
|
+
});
|
|
805
|
+
const finalMessages = [];
|
|
806
|
+
for await (const event of loop) {
|
|
807
|
+
yield event;
|
|
808
|
+
if (event.type === "done") {
|
|
809
|
+
finalMessages.length = 0;
|
|
810
|
+
finalMessages.push(...event.messages);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (finalMessages.length > 0) {
|
|
814
|
+
this.messages = finalMessages;
|
|
815
|
+
await this.saveMemory();
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
async chat(prompt, options) {
|
|
819
|
+
let text = "";
|
|
820
|
+
let usage = { inputTokens: 0, outputTokens: 0 };
|
|
821
|
+
for await (const event of this.run(prompt, options)) {
|
|
822
|
+
if (event.type === "text") {
|
|
823
|
+
text += event.text;
|
|
824
|
+
}
|
|
825
|
+
if (event.type === "done") {
|
|
826
|
+
usage = {
|
|
827
|
+
inputTokens: event.usage.inputTokens,
|
|
828
|
+
outputTokens: event.usage.outputTokens
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
const total = this.costTracker.getTotal();
|
|
833
|
+
return { text, usage, messages: this.getMessages(), cost: total.cost };
|
|
834
|
+
}
|
|
835
|
+
async structured(prompt, schema, options) {
|
|
836
|
+
const maxRetries = options?.maxRetries ?? 3;
|
|
837
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
838
|
+
const currentPrompt = attempt === 0 ? prompt : `${prompt}
|
|
839
|
+
|
|
840
|
+
Previous attempt returned invalid JSON. Please fix and respond with valid JSON only.`;
|
|
841
|
+
const agent = new _Agent({
|
|
842
|
+
model: this.model,
|
|
843
|
+
tools: [],
|
|
844
|
+
systemPrompt: "You must respond with valid JSON matching the requested schema.",
|
|
845
|
+
maxTurns: 1,
|
|
846
|
+
cwd: this.cwd,
|
|
847
|
+
permissions: this.permissionConfig,
|
|
848
|
+
context: {
|
|
849
|
+
maxTokens: this.contextManager.maxTokens,
|
|
850
|
+
maxOutputTokens: this.contextManager.maxOutputTokens,
|
|
851
|
+
compactThreshold: this.contextManager.compactThreshold
|
|
852
|
+
},
|
|
853
|
+
costTracker: this.costTracker,
|
|
854
|
+
disableTitle: true
|
|
855
|
+
});
|
|
856
|
+
const result = await agent.chat(currentPrompt, options);
|
|
857
|
+
const jsonMatch = result.text.match(/```(?:json)?\s*([\s\S]*?)```/) ?? [null, result.text];
|
|
858
|
+
const jsonStr = jsonMatch[1] ?? result.text;
|
|
859
|
+
try {
|
|
860
|
+
const parsed = JSON.parse(jsonStr);
|
|
861
|
+
const validated = schema.safeParse(parsed);
|
|
862
|
+
if (validated.success && validated.data !== void 0) {
|
|
863
|
+
return validated.data;
|
|
864
|
+
}
|
|
865
|
+
} catch {
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
throw new Error(`Failed to get valid structured output after ${maxRetries} attempts`);
|
|
870
|
+
}
|
|
871
|
+
async structuredViaTool(prompt, schema, options) {
|
|
872
|
+
const { z } = await import("zod");
|
|
873
|
+
const maxRetries = options?.maxRetries ?? 3;
|
|
874
|
+
let captured;
|
|
875
|
+
const structTool = defineTool({
|
|
876
|
+
name: "structured_output",
|
|
877
|
+
description: "Return your final response in the required structured format.",
|
|
878
|
+
inputSchema: z.object({
|
|
879
|
+
response: z.unknown().describe("The structured JSON response")
|
|
880
|
+
}),
|
|
881
|
+
isReadOnly: true,
|
|
882
|
+
isConcurrencySafe: true,
|
|
883
|
+
execute: async (input) => {
|
|
884
|
+
captured = input.response;
|
|
885
|
+
return "Structured output captured.";
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
const agent = new _Agent({
|
|
889
|
+
model: this.model,
|
|
890
|
+
tools: [structTool],
|
|
891
|
+
systemPrompt: [
|
|
892
|
+
this.systemPrompt,
|
|
893
|
+
"You MUST call the structured_output tool with your response. Do not return plain text."
|
|
894
|
+
].filter(Boolean).join("\n\n"),
|
|
895
|
+
maxTurns: 1,
|
|
896
|
+
cwd: this.cwd,
|
|
897
|
+
permissions: this.permissionConfig,
|
|
898
|
+
context: {
|
|
899
|
+
maxTokens: this.contextManager.maxTokens,
|
|
900
|
+
maxOutputTokens: this.contextManager.maxOutputTokens,
|
|
901
|
+
compactThreshold: this.contextManager.compactThreshold
|
|
902
|
+
},
|
|
903
|
+
costTracker: this.costTracker,
|
|
904
|
+
disableTitle: true
|
|
905
|
+
});
|
|
906
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
907
|
+
captured = void 0;
|
|
908
|
+
const currentPrompt = attempt === 0 ? prompt : `${prompt}
|
|
909
|
+
|
|
910
|
+
Previous attempt failed validation. Fix your structured output and call the structured_output tool again.`;
|
|
911
|
+
await agent.chat(currentPrompt, options);
|
|
912
|
+
if (captured !== void 0) {
|
|
913
|
+
const validated = schema.safeParse(captured);
|
|
914
|
+
if (validated.success && validated.data !== void 0) {
|
|
915
|
+
return validated.data;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
throw new Error(`Failed to get valid structured output via tool after ${maxRetries} attempts`);
|
|
920
|
+
}
|
|
921
|
+
async asTool(options) {
|
|
922
|
+
const { z } = await import("zod");
|
|
923
|
+
const allowSubAgents = options?.allowSubAgents ?? false;
|
|
924
|
+
return defineTool({
|
|
925
|
+
name: options?.name ?? "delegate_agent",
|
|
926
|
+
description: options?.description ?? `Delegate a task to a sub-agent with system prompt: ${this.systemPrompt?.slice(0, 100) ?? "none"}`,
|
|
927
|
+
inputSchema: z.object({
|
|
928
|
+
prompt: z.string().describe("The task to delegate")
|
|
929
|
+
}),
|
|
930
|
+
isReadOnly: true,
|
|
931
|
+
isConcurrencySafe: true,
|
|
932
|
+
execute: async (input) => {
|
|
933
|
+
const subAgent = this.fork();
|
|
934
|
+
if (!allowSubAgents) {
|
|
935
|
+
const forbiddenTools = ["delegate_agent", "task", "todowrite"];
|
|
936
|
+
const allowed = this.tools.getAll().filter((t) => !forbiddenTools.includes(t.name));
|
|
937
|
+
for (const tool of allowed) {
|
|
938
|
+
subAgent.addTool(tool);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
const result = await subAgent.chat(input.prompt);
|
|
942
|
+
return result.text;
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
async generateTitle(prompt) {
|
|
947
|
+
try {
|
|
948
|
+
const firstLine = prompt.split("\n")[0].slice(0, 200);
|
|
949
|
+
const titleAgent = new _Agent({
|
|
950
|
+
model: this.model,
|
|
951
|
+
tools: [],
|
|
952
|
+
systemPrompt: "Generate a short, descriptive title (max 10 words) for a conversation that starts with this message. Return ONLY the title, nothing else.",
|
|
953
|
+
maxTurns: 1,
|
|
954
|
+
cwd: this.cwd,
|
|
955
|
+
costTracker: this.costTracker,
|
|
956
|
+
disableTitle: true
|
|
957
|
+
});
|
|
958
|
+
const result = await titleAgent.chat(firstLine);
|
|
959
|
+
const title = result.text.trim().replace(/^["']|["']$/g, "").slice(0, 80);
|
|
960
|
+
if (title.length > 0) {
|
|
961
|
+
this._title = title;
|
|
962
|
+
}
|
|
963
|
+
} catch {
|
|
964
|
+
this._title = prompt.slice(0, 50);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
// src/stream.ts
|
|
970
|
+
function createStreamAggregator() {
|
|
971
|
+
let text = "";
|
|
972
|
+
const toolCalls = /* @__PURE__ */ new Map();
|
|
973
|
+
let usage;
|
|
974
|
+
let stopReason = "end_turn";
|
|
975
|
+
function push(event) {
|
|
976
|
+
switch (event.type) {
|
|
977
|
+
case "text_delta":
|
|
978
|
+
text += event.text ?? "";
|
|
979
|
+
break;
|
|
980
|
+
case "tool_use_start":
|
|
981
|
+
if (event.id) {
|
|
982
|
+
toolCalls.set(event.id, { id: event.id, name: event.name ?? "", input: event.input ?? "" });
|
|
983
|
+
}
|
|
984
|
+
stopReason = "tool_use";
|
|
985
|
+
break;
|
|
986
|
+
case "tool_use_delta":
|
|
987
|
+
if (event.id && toolCalls.has(event.id)) {
|
|
988
|
+
toolCalls.get(event.id).input += event.input ?? "";
|
|
989
|
+
}
|
|
990
|
+
break;
|
|
991
|
+
case "done":
|
|
992
|
+
usage = event.usage;
|
|
993
|
+
break;
|
|
994
|
+
case "tool_result":
|
|
995
|
+
break;
|
|
996
|
+
case "error":
|
|
997
|
+
break;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
function getResponse() {
|
|
1001
|
+
return { text, toolCalls: Array.from(toolCalls.values()), usage, stopReason };
|
|
1002
|
+
}
|
|
1003
|
+
return { push, getResponse };
|
|
1004
|
+
}
|
|
1005
|
+
export {
|
|
1006
|
+
Agent,
|
|
1007
|
+
AnthropicProvider,
|
|
1008
|
+
BaseProvider,
|
|
1009
|
+
BashTool,
|
|
1010
|
+
ContextManager,
|
|
1011
|
+
CostTracker,
|
|
1012
|
+
DEFAULT_COMPACT_THRESHOLD,
|
|
1013
|
+
DEFAULT_CONTEXT_WINDOW,
|
|
1014
|
+
DEFAULT_MAX_OUTPUT_TOKENS,
|
|
1015
|
+
DEFAULT_MAX_TURNS,
|
|
1016
|
+
DEFAULT_PRICING,
|
|
1017
|
+
FileEditTool,
|
|
1018
|
+
FileReadTool,
|
|
1019
|
+
FileWriteTool,
|
|
1020
|
+
GlobTool,
|
|
1021
|
+
GrepTool,
|
|
1022
|
+
HookRunner,
|
|
1023
|
+
InMemoryStore,
|
|
1024
|
+
MAX_CONCURRENT_TOOLS,
|
|
1025
|
+
MCPClient,
|
|
1026
|
+
OllamaProvider,
|
|
1027
|
+
OpenAIProvider,
|
|
1028
|
+
PermissionEngine,
|
|
1029
|
+
RetryableError,
|
|
1030
|
+
SQLiteStore,
|
|
1031
|
+
ToolRegistry,
|
|
1032
|
+
WebFetchTool,
|
|
1033
|
+
agentLoop,
|
|
1034
|
+
anthropic,
|
|
1035
|
+
createProvider,
|
|
1036
|
+
createStreamAggregator,
|
|
1037
|
+
defineTool,
|
|
1038
|
+
defineToolFromClass,
|
|
1039
|
+
estimateConversationTokens,
|
|
1040
|
+
estimateMessageTokens,
|
|
1041
|
+
estimateTokens,
|
|
1042
|
+
init,
|
|
1043
|
+
loadMCPTools,
|
|
1044
|
+
ollama,
|
|
1045
|
+
openai,
|
|
1046
|
+
orchestrateTools
|
|
1047
|
+
};
|
|
1048
|
+
//# sourceMappingURL=index.js.map
|