codemaxxing 0.1.8 → 0.1.9
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/README.md +43 -0
- package/dist/agent.d.ts +31 -1
- package/dist/agent.js +357 -5
- package/dist/config.d.ts +3 -5
- package/dist/config.js +23 -0
- package/dist/index.js +114 -9
- package/dist/tools/files.d.ts +8 -0
- package/dist/tools/files.js +137 -0
- package/dist/utils/sessions.d.ts +7 -0
- package/dist/utils/sessions.js +26 -1
- package/package.json +2 -1
- package/src/agent.ts +380 -6
- package/src/config.ts +28 -1
- package/src/index.tsx +152 -6
- package/src/tools/files.ts +127 -0
- package/src/utils/sessions.ts +33 -1
package/src/agent.ts
CHANGED
|
@@ -1,18 +1,63 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
3
|
import type {
|
|
3
4
|
ChatCompletionMessageParam,
|
|
4
5
|
ChatCompletionTool,
|
|
5
6
|
ChatCompletionChunk,
|
|
6
7
|
} from "openai/resources/chat/completions";
|
|
7
|
-
import { FILE_TOOLS, executeTool } from "./tools/files.js";
|
|
8
|
+
import { FILE_TOOLS, executeTool, generateDiff, getExistingContent } from "./tools/files.js";
|
|
8
9
|
import { buildProjectContext, getSystemPrompt } from "./utils/context.js";
|
|
9
10
|
import { isGitRepo, autoCommit } from "./utils/git.js";
|
|
10
|
-
import { createSession, saveMessage, updateTokenEstimate, loadMessages } from "./utils/sessions.js";
|
|
11
|
+
import { createSession, saveMessage, updateTokenEstimate, updateSessionCost, loadMessages } from "./utils/sessions.js";
|
|
11
12
|
import type { ProviderConfig } from "./config.js";
|
|
12
13
|
|
|
13
14
|
// Tools that can modify your project — require approval
|
|
14
15
|
const DANGEROUS_TOOLS = new Set(["write_file", "run_command"]);
|
|
15
16
|
|
|
17
|
+
// Cost per 1M tokens (input/output) for common models
|
|
18
|
+
const MODEL_COSTS: Record<string, { input: number; output: number }> = {
|
|
19
|
+
// OpenAI
|
|
20
|
+
"gpt-4o": { input: 2.5, output: 10 },
|
|
21
|
+
"gpt-4o-mini": { input: 0.15, output: 0.6 },
|
|
22
|
+
"gpt-4-turbo": { input: 10, output: 30 },
|
|
23
|
+
"gpt-4": { input: 30, output: 60 },
|
|
24
|
+
"gpt-3.5-turbo": { input: 0.5, output: 1.5 },
|
|
25
|
+
"o1": { input: 15, output: 60 },
|
|
26
|
+
"o1-mini": { input: 3, output: 12 },
|
|
27
|
+
"o3-mini": { input: 1.1, output: 4.4 },
|
|
28
|
+
// Anthropic
|
|
29
|
+
"claude-3-5-sonnet-20241022": { input: 3, output: 15 },
|
|
30
|
+
"claude-3-5-sonnet": { input: 3, output: 15 },
|
|
31
|
+
"claude-sonnet-4-20250514": { input: 3, output: 15 },
|
|
32
|
+
"claude-3-5-haiku-20241022": { input: 0.8, output: 4 },
|
|
33
|
+
"claude-3-opus-20240229": { input: 15, output: 75 },
|
|
34
|
+
"claude-3-haiku-20240307": { input: 0.25, output: 1.25 },
|
|
35
|
+
// Qwen (typically free/cheap on local, but OpenRouter pricing)
|
|
36
|
+
"qwen/qwen-2.5-coder-32b-instruct": { input: 0.2, output: 0.2 },
|
|
37
|
+
"qwen/qwen-2.5-72b-instruct": { input: 0.35, output: 0.4 },
|
|
38
|
+
// DeepSeek
|
|
39
|
+
"deepseek/deepseek-chat": { input: 0.14, output: 0.28 },
|
|
40
|
+
"deepseek/deepseek-coder": { input: 0.14, output: 0.28 },
|
|
41
|
+
// Llama
|
|
42
|
+
"meta-llama/llama-3.1-70b-instruct": { input: 0.52, output: 0.75 },
|
|
43
|
+
"meta-llama/llama-3.1-8b-instruct": { input: 0.055, output: 0.055 },
|
|
44
|
+
// Google
|
|
45
|
+
"google/gemini-pro-1.5": { input: 1.25, output: 5 },
|
|
46
|
+
"google/gemini-flash-1.5": { input: 0.075, output: 0.3 },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function getModelCost(model: string): { input: number; output: number } {
|
|
50
|
+
// Direct match
|
|
51
|
+
if (MODEL_COSTS[model]) return MODEL_COSTS[model];
|
|
52
|
+
// Partial match (model name contains a known key)
|
|
53
|
+
const lower = model.toLowerCase();
|
|
54
|
+
for (const [key, cost] of Object.entries(MODEL_COSTS)) {
|
|
55
|
+
if (lower.includes(key) || key.includes(lower)) return cost;
|
|
56
|
+
}
|
|
57
|
+
// Default: $0 (local/unknown models)
|
|
58
|
+
return { input: 0, output: 0 };
|
|
59
|
+
}
|
|
60
|
+
|
|
16
61
|
export interface AgentOptions {
|
|
17
62
|
provider: ProviderConfig;
|
|
18
63
|
cwd: string;
|
|
@@ -22,8 +67,10 @@ export interface AgentOptions {
|
|
|
22
67
|
onToolCall?: (name: string, args: Record<string, unknown>) => void;
|
|
23
68
|
onToolResult?: (name: string, result: string) => void;
|
|
24
69
|
onThinking?: (text: string) => void;
|
|
25
|
-
onToolApproval?: (name: string, args: Record<string, unknown
|
|
70
|
+
onToolApproval?: (name: string, args: Record<string, unknown>, diff?: string) => Promise<"yes" | "no" | "always">;
|
|
26
71
|
onGitCommit?: (message: string) => void;
|
|
72
|
+
onContextCompressed?: (oldTokens: number, newTokens: number) => void;
|
|
73
|
+
contextCompressionThreshold?: number;
|
|
27
74
|
}
|
|
28
75
|
|
|
29
76
|
interface AssembledToolCall {
|
|
@@ -34,6 +81,8 @@ interface AssembledToolCall {
|
|
|
34
81
|
|
|
35
82
|
export class CodingAgent {
|
|
36
83
|
private client: OpenAI;
|
|
84
|
+
private anthropicClient: Anthropic | null = null;
|
|
85
|
+
private providerType: "openai" | "anthropic";
|
|
37
86
|
private messages: ChatCompletionMessageParam[] = [];
|
|
38
87
|
private tools: ChatCompletionTool[] = FILE_TOOLS;
|
|
39
88
|
private cwd: string;
|
|
@@ -45,17 +94,33 @@ export class CodingAgent {
|
|
|
45
94
|
private autoCommitEnabled: boolean = false;
|
|
46
95
|
private repoMap: string = "";
|
|
47
96
|
private sessionId: string = "";
|
|
97
|
+
private totalPromptTokens: number = 0;
|
|
98
|
+
private totalCompletionTokens: number = 0;
|
|
99
|
+
private totalCost: number = 0;
|
|
100
|
+
private systemPrompt: string = "";
|
|
101
|
+
private compressionThreshold: number;
|
|
48
102
|
|
|
49
103
|
constructor(private options: AgentOptions) {
|
|
104
|
+
this.providerType = options.provider.type || "openai";
|
|
50
105
|
this.client = new OpenAI({
|
|
51
106
|
baseURL: options.provider.baseUrl,
|
|
52
107
|
apiKey: options.provider.apiKey,
|
|
53
108
|
});
|
|
109
|
+
if (this.providerType === "anthropic") {
|
|
110
|
+
this.anthropicClient = new Anthropic({
|
|
111
|
+
apiKey: options.provider.apiKey,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
54
114
|
this.cwd = options.cwd;
|
|
55
115
|
this.maxTokens = options.maxTokens;
|
|
56
116
|
this.autoApprove = options.autoApprove;
|
|
57
117
|
this.model = options.provider.model;
|
|
118
|
+
// Default model for Anthropic
|
|
119
|
+
if (this.providerType === "anthropic" && (this.model === "auto" || !this.model)) {
|
|
120
|
+
this.model = "claude-sonnet-4-20250514";
|
|
121
|
+
}
|
|
58
122
|
this.gitEnabled = isGitRepo(this.cwd);
|
|
123
|
+
this.compressionThreshold = options.contextCompressionThreshold ?? 80000;
|
|
59
124
|
}
|
|
60
125
|
|
|
61
126
|
/**
|
|
@@ -63,10 +128,10 @@ export class CodingAgent {
|
|
|
63
128
|
*/
|
|
64
129
|
async init(): Promise<void> {
|
|
65
130
|
const context = await buildProjectContext(this.cwd);
|
|
66
|
-
|
|
131
|
+
this.systemPrompt = await getSystemPrompt(context);
|
|
67
132
|
|
|
68
133
|
this.messages = [
|
|
69
|
-
{ role: "system", content: systemPrompt },
|
|
134
|
+
{ role: "system", content: this.systemPrompt },
|
|
70
135
|
];
|
|
71
136
|
|
|
72
137
|
// Create a new session
|
|
@@ -116,6 +181,13 @@ export class CodingAgent {
|
|
|
116
181
|
this.messages.push(userMsg);
|
|
117
182
|
saveMessage(this.sessionId, userMsg);
|
|
118
183
|
|
|
184
|
+
// Check if context needs compression before sending
|
|
185
|
+
await this.maybeCompressContext();
|
|
186
|
+
|
|
187
|
+
if (this.providerType === "anthropic" && this.anthropicClient) {
|
|
188
|
+
return this.chatAnthropic(userMessage);
|
|
189
|
+
}
|
|
190
|
+
|
|
119
191
|
let iterations = 0;
|
|
120
192
|
const MAX_ITERATIONS = 20;
|
|
121
193
|
|
|
@@ -128,6 +200,7 @@ export class CodingAgent {
|
|
|
128
200
|
tools: this.tools,
|
|
129
201
|
max_tokens: this.maxTokens,
|
|
130
202
|
stream: true,
|
|
203
|
+
stream_options: { include_usage: true },
|
|
131
204
|
});
|
|
132
205
|
|
|
133
206
|
// Accumulate the streamed response
|
|
@@ -135,8 +208,15 @@ export class CodingAgent {
|
|
|
135
208
|
let thinkingText = "";
|
|
136
209
|
let inThinking = false;
|
|
137
210
|
const toolCalls: Map<number, AssembledToolCall> = new Map();
|
|
211
|
+
let chunkPromptTokens = 0;
|
|
212
|
+
let chunkCompletionTokens = 0;
|
|
138
213
|
|
|
139
214
|
for await (const chunk of stream) {
|
|
215
|
+
// Capture usage from the final chunk
|
|
216
|
+
if ((chunk as any).usage) {
|
|
217
|
+
chunkPromptTokens = (chunk as any).usage.prompt_tokens ?? 0;
|
|
218
|
+
chunkCompletionTokens = (chunk as any).usage.completion_tokens ?? 0;
|
|
219
|
+
}
|
|
140
220
|
const delta = chunk.choices?.[0]?.delta;
|
|
141
221
|
if (!delta) continue;
|
|
142
222
|
|
|
@@ -195,6 +275,16 @@ export class CodingAgent {
|
|
|
195
275
|
this.messages.push(assistantMessage);
|
|
196
276
|
saveMessage(this.sessionId, assistantMessage);
|
|
197
277
|
|
|
278
|
+
// Track token usage and cost
|
|
279
|
+
if (chunkPromptTokens > 0 || chunkCompletionTokens > 0) {
|
|
280
|
+
this.totalPromptTokens += chunkPromptTokens;
|
|
281
|
+
this.totalCompletionTokens += chunkCompletionTokens;
|
|
282
|
+
const costs = getModelCost(this.model);
|
|
283
|
+
this.totalCost = (this.totalPromptTokens / 1_000_000) * costs.input +
|
|
284
|
+
(this.totalCompletionTokens / 1_000_000) * costs.output;
|
|
285
|
+
updateSessionCost(this.sessionId, this.totalPromptTokens, this.totalCompletionTokens, this.totalCost);
|
|
286
|
+
}
|
|
287
|
+
|
|
198
288
|
// If no tool calls, we're done — return the text
|
|
199
289
|
if (toolCalls.size === 0) {
|
|
200
290
|
updateTokenEstimate(this.sessionId, this.estimateTokens());
|
|
@@ -215,7 +305,15 @@ export class CodingAgent {
|
|
|
215
305
|
// Check approval for dangerous tools
|
|
216
306
|
if (DANGEROUS_TOOLS.has(toolCall.name) && !this.autoApprove && !this.alwaysApproved.has(toolCall.name)) {
|
|
217
307
|
if (this.options.onToolApproval) {
|
|
218
|
-
|
|
308
|
+
// Generate diff for write_file if file already exists
|
|
309
|
+
let diff: string | undefined;
|
|
310
|
+
if (toolCall.name === "write_file" && args.path && args.content) {
|
|
311
|
+
const existing = getExistingContent(String(args.path), this.cwd);
|
|
312
|
+
if (existing !== null) {
|
|
313
|
+
diff = generateDiff(existing, String(args.content), String(args.path));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const decision = await this.options.onToolApproval(toolCall.name, args, diff);
|
|
219
317
|
if (decision === "no") {
|
|
220
318
|
const denied = `Tool call "${toolCall.name}" was denied by the user.`;
|
|
221
319
|
this.options.onToolResult?.(toolCall.name, denied);
|
|
@@ -262,6 +360,198 @@ export class CodingAgent {
|
|
|
262
360
|
return "Max iterations reached. The agent may be stuck in a loop.";
|
|
263
361
|
}
|
|
264
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Convert OpenAI-format tools to Anthropic tool format
|
|
365
|
+
*/
|
|
366
|
+
private getAnthropicTools(): Anthropic.Tool[] {
|
|
367
|
+
return this.tools.map((t) => ({
|
|
368
|
+
name: t.function.name,
|
|
369
|
+
description: t.function.description ?? "",
|
|
370
|
+
input_schema: (t.function.parameters as Anthropic.Tool.InputSchema) ?? { type: "object" as const, properties: {} },
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Convert messages to Anthropic format (separate system from conversation)
|
|
376
|
+
*/
|
|
377
|
+
private getAnthropicMessages(): Anthropic.MessageParam[] {
|
|
378
|
+
const msgs: Anthropic.MessageParam[] = [];
|
|
379
|
+
for (const msg of this.messages) {
|
|
380
|
+
if (msg.role === "system") continue; // system handled separately
|
|
381
|
+
if (msg.role === "user") {
|
|
382
|
+
msgs.push({ role: "user", content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content) });
|
|
383
|
+
} else if (msg.role === "assistant") {
|
|
384
|
+
const content: Anthropic.ContentBlockParam[] = [];
|
|
385
|
+
if (msg.content) {
|
|
386
|
+
content.push({ type: "text", text: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content) });
|
|
387
|
+
}
|
|
388
|
+
if ("tool_calls" in msg && Array.isArray((msg as any).tool_calls)) {
|
|
389
|
+
for (const tc of (msg as any).tool_calls) {
|
|
390
|
+
let input: Record<string, unknown> = {};
|
|
391
|
+
try { input = JSON.parse(tc.function.arguments); } catch {}
|
|
392
|
+
content.push({
|
|
393
|
+
type: "tool_use",
|
|
394
|
+
id: tc.id,
|
|
395
|
+
name: tc.function.name,
|
|
396
|
+
input,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (content.length > 0) {
|
|
401
|
+
msgs.push({ role: "assistant", content });
|
|
402
|
+
}
|
|
403
|
+
} else if (msg.role === "tool") {
|
|
404
|
+
const toolCallId = (msg as any).tool_call_id;
|
|
405
|
+
const resultContent = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
|
|
406
|
+
// Anthropic expects tool results as user messages with tool_result content
|
|
407
|
+
msgs.push({
|
|
408
|
+
role: "user",
|
|
409
|
+
content: [{
|
|
410
|
+
type: "tool_result",
|
|
411
|
+
tool_use_id: toolCallId,
|
|
412
|
+
content: resultContent,
|
|
413
|
+
}],
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return msgs;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Anthropic-native streaming chat
|
|
422
|
+
*/
|
|
423
|
+
private async chatAnthropic(_userMessage: string): Promise<string> {
|
|
424
|
+
const client = this.anthropicClient!;
|
|
425
|
+
let iterations = 0;
|
|
426
|
+
const MAX_ITERATIONS = 20;
|
|
427
|
+
|
|
428
|
+
while (iterations < MAX_ITERATIONS) {
|
|
429
|
+
iterations++;
|
|
430
|
+
|
|
431
|
+
const anthropicMessages = this.getAnthropicMessages();
|
|
432
|
+
const anthropicTools = this.getAnthropicTools();
|
|
433
|
+
|
|
434
|
+
const stream = client.messages.stream({
|
|
435
|
+
model: this.model,
|
|
436
|
+
max_tokens: this.maxTokens,
|
|
437
|
+
system: this.systemPrompt,
|
|
438
|
+
messages: anthropicMessages,
|
|
439
|
+
tools: anthropicTools,
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
let contentText = "";
|
|
443
|
+
const toolCalls: Array<{ id: string; name: string; input: Record<string, unknown> }> = [];
|
|
444
|
+
let currentToolId = "";
|
|
445
|
+
let currentToolName = "";
|
|
446
|
+
let currentToolInput = "";
|
|
447
|
+
|
|
448
|
+
stream.on("text", (text) => {
|
|
449
|
+
contentText += text;
|
|
450
|
+
this.options.onToken?.(text);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const finalMessage = await stream.finalMessage();
|
|
454
|
+
|
|
455
|
+
// Track usage
|
|
456
|
+
if (finalMessage.usage) {
|
|
457
|
+
const promptTokens = finalMessage.usage.input_tokens;
|
|
458
|
+
const completionTokens = finalMessage.usage.output_tokens;
|
|
459
|
+
this.totalPromptTokens += promptTokens;
|
|
460
|
+
this.totalCompletionTokens += completionTokens;
|
|
461
|
+
const costs = getModelCost(this.model);
|
|
462
|
+
this.totalCost = (this.totalPromptTokens / 1_000_000) * costs.input +
|
|
463
|
+
(this.totalCompletionTokens / 1_000_000) * costs.output;
|
|
464
|
+
updateSessionCost(this.sessionId, this.totalPromptTokens, this.totalCompletionTokens, this.totalCost);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Extract tool uses from content blocks
|
|
468
|
+
for (const block of finalMessage.content) {
|
|
469
|
+
if (block.type === "tool_use") {
|
|
470
|
+
toolCalls.push({
|
|
471
|
+
id: block.id,
|
|
472
|
+
name: block.name,
|
|
473
|
+
input: block.input as Record<string, unknown>,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Build OpenAI-format assistant message for session storage
|
|
479
|
+
const assistantMessage: any = { role: "assistant", content: contentText || null };
|
|
480
|
+
if (toolCalls.length > 0) {
|
|
481
|
+
assistantMessage.tool_calls = toolCalls.map((tc) => ({
|
|
482
|
+
id: tc.id,
|
|
483
|
+
type: "function" as const,
|
|
484
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.input) },
|
|
485
|
+
}));
|
|
486
|
+
}
|
|
487
|
+
this.messages.push(assistantMessage);
|
|
488
|
+
saveMessage(this.sessionId, assistantMessage);
|
|
489
|
+
|
|
490
|
+
// If no tool calls, we're done
|
|
491
|
+
if (toolCalls.length === 0) {
|
|
492
|
+
updateTokenEstimate(this.sessionId, this.estimateTokens());
|
|
493
|
+
return contentText || "(empty response)";
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Process tool calls
|
|
497
|
+
for (const toolCall of toolCalls) {
|
|
498
|
+
const args = toolCall.input;
|
|
499
|
+
this.options.onToolCall?.(toolCall.name, args);
|
|
500
|
+
|
|
501
|
+
// Check approval for dangerous tools
|
|
502
|
+
if (DANGEROUS_TOOLS.has(toolCall.name) && !this.autoApprove && !this.alwaysApproved.has(toolCall.name)) {
|
|
503
|
+
if (this.options.onToolApproval) {
|
|
504
|
+
let diff: string | undefined;
|
|
505
|
+
if (toolCall.name === "write_file" && args.path && args.content) {
|
|
506
|
+
const existing = getExistingContent(String(args.path), this.cwd);
|
|
507
|
+
if (existing !== null) {
|
|
508
|
+
diff = generateDiff(existing, String(args.content), String(args.path));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const decision = await this.options.onToolApproval(toolCall.name, args, diff);
|
|
512
|
+
if (decision === "no") {
|
|
513
|
+
const denied = `Tool call "${toolCall.name}" was denied by the user.`;
|
|
514
|
+
this.options.onToolResult?.(toolCall.name, denied);
|
|
515
|
+
const deniedMsg: ChatCompletionMessageParam = {
|
|
516
|
+
role: "tool",
|
|
517
|
+
tool_call_id: toolCall.id,
|
|
518
|
+
content: denied,
|
|
519
|
+
};
|
|
520
|
+
this.messages.push(deniedMsg);
|
|
521
|
+
saveMessage(this.sessionId, deniedMsg);
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (decision === "always") {
|
|
525
|
+
this.alwaysApproved.add(toolCall.name);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const result = await executeTool(toolCall.name, args, this.cwd);
|
|
531
|
+
this.options.onToolResult?.(toolCall.name, result);
|
|
532
|
+
|
|
533
|
+
// Auto-commit after successful write_file
|
|
534
|
+
if (this.gitEnabled && this.autoCommitEnabled && toolCall.name === "write_file" && result.startsWith("✅")) {
|
|
535
|
+
const path = String(args.path ?? "unknown");
|
|
536
|
+
const committed = autoCommit(this.cwd, path, "write");
|
|
537
|
+
if (committed) {
|
|
538
|
+
this.options.onGitCommit?.(`write ${path}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const toolMsg: ChatCompletionMessageParam = {
|
|
543
|
+
role: "tool",
|
|
544
|
+
tool_call_id: toolCall.id,
|
|
545
|
+
content: result,
|
|
546
|
+
};
|
|
547
|
+
this.messages.push(toolMsg);
|
|
548
|
+
saveMessage(this.sessionId, toolMsg);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return "Max iterations reached. The agent may be stuck in a loop.";
|
|
553
|
+
}
|
|
554
|
+
|
|
265
555
|
/**
|
|
266
556
|
* Switch to a different model mid-session
|
|
267
557
|
*/
|
|
@@ -315,6 +605,90 @@ export class CodingAgent {
|
|
|
315
605
|
return Math.ceil(chars / 4);
|
|
316
606
|
}
|
|
317
607
|
|
|
608
|
+
/**
|
|
609
|
+
* Check if context needs compression and compress if threshold exceeded
|
|
610
|
+
*/
|
|
611
|
+
private async maybeCompressContext(): Promise<void> {
|
|
612
|
+
const currentTokens = this.estimateTokens();
|
|
613
|
+
if (currentTokens < this.compressionThreshold) return;
|
|
614
|
+
|
|
615
|
+
// Keep: system prompt (index 0) + last 10 messages
|
|
616
|
+
const keepCount = 10;
|
|
617
|
+
if (this.messages.length <= keepCount + 1) return; // Not enough to compress
|
|
618
|
+
|
|
619
|
+
const systemMsg = this.messages[0];
|
|
620
|
+
const middleMessages = this.messages.slice(1, this.messages.length - keepCount);
|
|
621
|
+
const recentMessages = this.messages.slice(this.messages.length - keepCount);
|
|
622
|
+
|
|
623
|
+
if (middleMessages.length === 0) return;
|
|
624
|
+
|
|
625
|
+
// Build a summary of the middle messages
|
|
626
|
+
const summaryParts: string[] = [];
|
|
627
|
+
for (const msg of middleMessages) {
|
|
628
|
+
if (msg.role === "user" && typeof msg.content === "string") {
|
|
629
|
+
summaryParts.push(`User: ${msg.content.slice(0, 200)}`);
|
|
630
|
+
} else if (msg.role === "assistant" && typeof msg.content === "string" && msg.content) {
|
|
631
|
+
summaryParts.push(`Assistant: ${msg.content.slice(0, 200)}`);
|
|
632
|
+
} else if (msg.role === "tool") {
|
|
633
|
+
// Skip tool messages in summary to save tokens
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Use the active model to summarize
|
|
638
|
+
const summaryPrompt = `Summarize this conversation history in 2-3 concise paragraphs. Focus on: what was discussed, what files were modified, what decisions were made, and any important context for continuing the conversation.\n\n${summaryParts.join("\n")}`;
|
|
639
|
+
|
|
640
|
+
try {
|
|
641
|
+
let summary: string;
|
|
642
|
+
if (this.providerType === "anthropic" && this.anthropicClient) {
|
|
643
|
+
const response = await this.anthropicClient.messages.create({
|
|
644
|
+
model: this.model,
|
|
645
|
+
max_tokens: 500,
|
|
646
|
+
messages: [{ role: "user", content: summaryPrompt }],
|
|
647
|
+
});
|
|
648
|
+
summary = response.content
|
|
649
|
+
.filter((b): b is Anthropic.TextBlock => b.type === "text")
|
|
650
|
+
.map((b) => b.text)
|
|
651
|
+
.join("");
|
|
652
|
+
} else {
|
|
653
|
+
const response = await this.client.chat.completions.create({
|
|
654
|
+
model: this.model,
|
|
655
|
+
max_tokens: 500,
|
|
656
|
+
messages: [{ role: "user", content: summaryPrompt }],
|
|
657
|
+
});
|
|
658
|
+
summary = response.choices[0]?.message?.content ?? "Previous conversation context.";
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const compressedMsg: ChatCompletionMessageParam = {
|
|
662
|
+
role: "assistant",
|
|
663
|
+
content: `[Context compressed: ${summary}]`,
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
const oldTokens = currentTokens;
|
|
667
|
+
this.messages = [systemMsg, compressedMsg, ...recentMessages];
|
|
668
|
+
const newTokens = this.estimateTokens();
|
|
669
|
+
|
|
670
|
+
this.options.onContextCompressed?.(oldTokens, newTokens);
|
|
671
|
+
} catch {
|
|
672
|
+
// If summarization fails, just truncate without summary
|
|
673
|
+
const compressedMsg: ChatCompletionMessageParam = {
|
|
674
|
+
role: "assistant",
|
|
675
|
+
content: "[Context compressed: Earlier conversation history was removed to stay within token limits.]",
|
|
676
|
+
};
|
|
677
|
+
const oldTokens = currentTokens;
|
|
678
|
+
this.messages = [systemMsg, compressedMsg, ...recentMessages];
|
|
679
|
+
const newTokens = this.estimateTokens();
|
|
680
|
+
this.options.onContextCompressed?.(oldTokens, newTokens);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
getCostInfo(): { promptTokens: number; completionTokens: number; totalCost: number } {
|
|
685
|
+
return {
|
|
686
|
+
promptTokens: this.totalPromptTokens,
|
|
687
|
+
completionTokens: this.totalCompletionTokens,
|
|
688
|
+
totalCost: this.totalCost,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
318
692
|
reset(): void {
|
|
319
693
|
const systemMsg = this.messages[0];
|
|
320
694
|
this.messages = [systemMsg];
|
package/src/config.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface ProviderConfig {
|
|
|
7
7
|
baseUrl: string;
|
|
8
8
|
apiKey: string;
|
|
9
9
|
model: string;
|
|
10
|
+
type?: "openai" | "anthropic";
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export interface ProviderProfile extends ProviderConfig {
|
|
@@ -20,6 +21,7 @@ export interface CodemaxxingConfig {
|
|
|
20
21
|
autoApprove: boolean;
|
|
21
22
|
contextFiles: number;
|
|
22
23
|
maxTokens: number;
|
|
24
|
+
contextCompressionThreshold?: number;
|
|
23
25
|
};
|
|
24
26
|
}
|
|
25
27
|
|
|
@@ -135,6 +137,14 @@ export function applyOverrides(config: CodemaxxingConfig, args: CLIArgs): Codema
|
|
|
135
137
|
if (args.provider && config.providers?.[args.provider]) {
|
|
136
138
|
const profile = config.providers[args.provider];
|
|
137
139
|
result.provider = { ...profile };
|
|
140
|
+
// Also check auth store for this provider
|
|
141
|
+
const authCred = getCredential(args.provider);
|
|
142
|
+
if (authCred) {
|
|
143
|
+
result.provider.baseUrl = authCred.baseUrl;
|
|
144
|
+
result.provider.apiKey = authCred.apiKey;
|
|
145
|
+
}
|
|
146
|
+
// Detect provider type
|
|
147
|
+
result.provider.type = detectProviderType(args.provider, result.provider.baseUrl);
|
|
138
148
|
}
|
|
139
149
|
|
|
140
150
|
// CLI flags override everything
|
|
@@ -142,6 +152,11 @@ export function applyOverrides(config: CodemaxxingConfig, args: CLIArgs): Codema
|
|
|
142
152
|
if (args.apiKey) result.provider.apiKey = args.apiKey;
|
|
143
153
|
if (args.baseUrl) result.provider.baseUrl = args.baseUrl;
|
|
144
154
|
|
|
155
|
+
// Auto-detect type from baseUrl if not set
|
|
156
|
+
if (!result.provider.type && result.provider.baseUrl) {
|
|
157
|
+
result.provider.type = detectProviderType(args.provider || "", result.provider.baseUrl);
|
|
158
|
+
}
|
|
159
|
+
|
|
145
160
|
return result;
|
|
146
161
|
}
|
|
147
162
|
|
|
@@ -218,7 +233,7 @@ export async function listModels(baseUrl: string, apiKey: string): Promise<strin
|
|
|
218
233
|
export function resolveProvider(
|
|
219
234
|
providerId: string,
|
|
220
235
|
cliArgs: CLIArgs
|
|
221
|
-
):
|
|
236
|
+
): ProviderConfig | null {
|
|
222
237
|
// Check auth store first
|
|
223
238
|
const authCred = getCredential(providerId);
|
|
224
239
|
if (authCred) {
|
|
@@ -226,6 +241,7 @@ export function resolveProvider(
|
|
|
226
241
|
baseUrl: authCred.baseUrl,
|
|
227
242
|
apiKey: authCred.apiKey,
|
|
228
243
|
model: cliArgs.model || "auto",
|
|
244
|
+
type: detectProviderType(providerId, authCred.baseUrl),
|
|
229
245
|
};
|
|
230
246
|
}
|
|
231
247
|
|
|
@@ -237,8 +253,19 @@ export function resolveProvider(
|
|
|
237
253
|
baseUrl: provider.baseUrl,
|
|
238
254
|
apiKey: cliArgs.apiKey || provider.apiKey,
|
|
239
255
|
model: cliArgs.model || provider.model,
|
|
256
|
+
type: detectProviderType(providerId, provider.baseUrl),
|
|
240
257
|
};
|
|
241
258
|
}
|
|
242
259
|
|
|
243
260
|
return null;
|
|
244
261
|
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Detect provider type from ID or base URL
|
|
265
|
+
*/
|
|
266
|
+
function detectProviderType(providerId: string, baseUrl: string): "openai" | "anthropic" {
|
|
267
|
+
if (providerId === "anthropic" || baseUrl.includes("anthropic.com")) {
|
|
268
|
+
return "anthropic";
|
|
269
|
+
}
|
|
270
|
+
return "openai";
|
|
271
|
+
}
|