coder-agent 2.3.2 → 2.3.4
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/dist/agent.js +77 -13
- package/dist/index.js +6 -2
- package/dist/memory.js +48 -6
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -2,7 +2,7 @@ import chalk from "chalk";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import * as fs from "fs/promises";
|
|
4
4
|
import { TOOL_DEFINITIONS, dispatchTool } from "./tools.js";
|
|
5
|
-
import { Memory } from "./memory.js";
|
|
5
|
+
import { Memory, getAgentMemoryEntrypoint } from "./memory.js";
|
|
6
6
|
// ─── Loading Spinner ──────────────────────────────────────────────────────────
|
|
7
7
|
let spinnerTimer = null;
|
|
8
8
|
let currentFrame = 0;
|
|
@@ -237,12 +237,14 @@ function extractTextToolCalls(content) {
|
|
|
237
237
|
return calls;
|
|
238
238
|
}
|
|
239
239
|
// ─── Gemini API client with Auto-Rotation Fallback ────────────────────────────
|
|
240
|
-
async function callGeminiAPIWithRotation(apiKey, params, maxRetries = 3, initialDelayMs = 1500, signal) {
|
|
240
|
+
async function callGeminiAPIWithRotation(apiKey, params, maxRetries = 3, initialDelayMs = 1500, signal, silent = false) {
|
|
241
241
|
const rotationList = [
|
|
242
|
+
"gemini-2.0-flash",
|
|
243
|
+
"gemini-2.0-pro-exp",
|
|
242
244
|
"gemini-2.5-flash",
|
|
243
245
|
"gemini-2.5-pro",
|
|
244
|
-
"gemini-
|
|
245
|
-
"gemini-
|
|
246
|
+
"gemini-3.5-flash",
|
|
247
|
+
"gemini-3.1-flash-lite"
|
|
246
248
|
];
|
|
247
249
|
let currentModel = params.model;
|
|
248
250
|
let modelIndex = rotationList.indexOf(currentModel);
|
|
@@ -282,20 +284,24 @@ async function callGeminiAPIWithRotation(apiKey, params, maxRetries = 3, initial
|
|
|
282
284
|
attempts++;
|
|
283
285
|
const status = err?.status;
|
|
284
286
|
const isRetryableError = status === 429 || status === 503 || (status >= 500 && status < 600) || !status;
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
287
|
+
const isModelError = status === 404 || status === 400;
|
|
288
|
+
if (isRetryableError || isModelError) {
|
|
289
|
+
// Rotate model immediately if rate limit or model error occurred
|
|
290
|
+
if (modelIndex + 1 < rotationList.length) {
|
|
288
291
|
modelIndex++;
|
|
289
292
|
const nextModel = rotationList[modelIndex];
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
+
if (!silent) {
|
|
294
|
+
stopSpinner();
|
|
295
|
+
const reason = isModelError ? "Model unavailable" : "Rate limited";
|
|
296
|
+
console.log(chalk.hex('#ff9f0a')('⚠') + ' ' + chalk.gray(`${reason} on ${currentModel}. Rotating to ${nextModel}`));
|
|
297
|
+
startSpinner("thinking...");
|
|
298
|
+
}
|
|
293
299
|
currentModel = nextModel;
|
|
294
300
|
attempts = 0; // reset retry counter for the fresh model
|
|
295
301
|
continue;
|
|
296
302
|
}
|
|
297
|
-
// Otherwise do standard delay retry on same model
|
|
298
|
-
if (attempts < maxRetries) {
|
|
303
|
+
// Otherwise do standard delay retry on same model ONLY if it is a transient/retryable error
|
|
304
|
+
if (isRetryableError && attempts < maxRetries) {
|
|
299
305
|
const delay = initialDelayMs * Math.pow(2, attempts - 1);
|
|
300
306
|
await new Promise((resolve, reject) => {
|
|
301
307
|
const timer = setTimeout(resolve, delay);
|
|
@@ -348,7 +354,7 @@ export class Agent {
|
|
|
348
354
|
abortErr.name = "AbortError";
|
|
349
355
|
throw abortErr;
|
|
350
356
|
}
|
|
351
|
-
await this.memory.init(this.memoryScope, "coder");
|
|
357
|
+
await this.memory.init(this.memoryScope, "coder", true);
|
|
352
358
|
if (signal?.aborted) {
|
|
353
359
|
const abortErr = new Error("The user aborted a request.");
|
|
354
360
|
abortErr.name = "AbortError";
|
|
@@ -527,9 +533,67 @@ export class Agent {
|
|
|
527
533
|
console.log(chalk.hex('#ff453a')('✕ error'));
|
|
528
534
|
console.log(chalk.dim(' Max tool iterations reached.'));
|
|
529
535
|
}
|
|
536
|
+
// Auto-update persistent memory in the background (fire and forget)
|
|
537
|
+
this.autoUpdateMemory().catch(() => { });
|
|
530
538
|
}
|
|
531
539
|
finally {
|
|
532
540
|
stopSpinner();
|
|
533
541
|
}
|
|
534
542
|
}
|
|
543
|
+
async autoUpdateMemory() {
|
|
544
|
+
try {
|
|
545
|
+
const memoryFile = getAgentMemoryEntrypoint("coder", this.memoryScope);
|
|
546
|
+
let existingMemory = "";
|
|
547
|
+
try {
|
|
548
|
+
existingMemory = await fs.readFile(memoryFile, "utf-8");
|
|
549
|
+
}
|
|
550
|
+
catch (err) {
|
|
551
|
+
if (err.code !== "ENOENT") {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const allMessages = this.memory.getAll();
|
|
556
|
+
if (allMessages.length <= 1)
|
|
557
|
+
return;
|
|
558
|
+
const prompt = `You are a memory manager for the CLI coding agent.
|
|
559
|
+
Your task is to update the persistent agent memory file based on the recent conversation history.
|
|
560
|
+
|
|
561
|
+
Here is the current memory file content:
|
|
562
|
+
---
|
|
563
|
+
${existingMemory}
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
Here is the recent conversation history:
|
|
567
|
+
---
|
|
568
|
+
${JSON.stringify(allMessages.slice(1).map(m => ({ role: m.role, content: m.content || JSON.stringify(m.tool_calls) })))}
|
|
569
|
+
---
|
|
570
|
+
|
|
571
|
+
Instructions:
|
|
572
|
+
1. Review the conversation history and extract any important new project setup details, style preferences, build/test commands, package quirks, or persistent instructions that should be remembered for future sessions.
|
|
573
|
+
2. Integrate these new learnings into the existing memory structure. Keep existing useful learnings/preferences, but clean up duplicates or obsolete info.
|
|
574
|
+
3. Keep the content concise, clean, and formatted in Markdown.
|
|
575
|
+
4. If there are no new learnings, setup details, or instructions in the conversation, output EXACTLY the existing memory content. Do not add conversational text, just output the updated/existing markdown content.`;
|
|
576
|
+
const responseObj = await callGeminiAPIWithRotation(this.apiKey, {
|
|
577
|
+
model: this.model,
|
|
578
|
+
messages: [{ role: "user", content: prompt }],
|
|
579
|
+
temperature: 0.1,
|
|
580
|
+
}, 3, 1500, undefined, true);
|
|
581
|
+
let newMemory = responseObj.data.choices[0].message.content?.trim() || "";
|
|
582
|
+
if (newMemory.startsWith("```markdown")) {
|
|
583
|
+
newMemory = newMemory.slice(11).trim();
|
|
584
|
+
}
|
|
585
|
+
if (newMemory.startsWith("```")) {
|
|
586
|
+
newMemory = newMemory.slice(3).trim();
|
|
587
|
+
}
|
|
588
|
+
if (newMemory.endsWith("```")) {
|
|
589
|
+
newMemory = newMemory.slice(0, -3).trim();
|
|
590
|
+
}
|
|
591
|
+
if (newMemory && newMemory !== existingMemory.trim()) {
|
|
592
|
+
await fs.writeFile(memoryFile, newMemory, "utf-8");
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
// Fail silently to avoid interrupting the main interaction
|
|
597
|
+
}
|
|
598
|
+
}
|
|
535
599
|
}
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,8 @@ import { getStoredApiKey, saveApiKey, getStoredModel, saveModel } from "./config
|
|
|
7
7
|
const VALID_MODELS = [
|
|
8
8
|
"gemini-2.5-flash",
|
|
9
9
|
"gemini-2.5-pro",
|
|
10
|
+
"gemini-3.5-flash",
|
|
11
|
+
"gemini-3.1-flash-lite",
|
|
10
12
|
"gemini-2.0-flash",
|
|
11
13
|
"gemini-2.0-pro-exp"
|
|
12
14
|
];
|
|
@@ -66,10 +68,12 @@ function printHelp() {
|
|
|
66
68
|
console.log(chalk.white(" --set-gemini-key <api_key> — Save your Gemini API Key globally (alias)"));
|
|
67
69
|
console.log("");
|
|
68
70
|
console.log(chalk.gray(" Popular Gemini Models:"));
|
|
71
|
+
console.log(chalk.white(" gemini-3.5-flash — Newest, highly capable & fast"));
|
|
69
72
|
console.log(chalk.white(" gemini-2.5-flash — Default, highly capable & fast"));
|
|
70
73
|
console.log(chalk.white(" gemini-2.5-pro — Reasoning model, excellent coding"));
|
|
71
|
-
console.log(chalk.white(" gemini-
|
|
72
|
-
console.log(chalk.white(" gemini-2.0-
|
|
74
|
+
console.log(chalk.white(" gemini-3.1-flash-lite — Light-weight, high volume"));
|
|
75
|
+
console.log(chalk.white(" gemini-2.0-flash — (Deprecated) Ultra-fast, lightweight"));
|
|
76
|
+
console.log(chalk.white(" gemini-2.0-pro-exp — (Deprecated) Experimental reasoning"));
|
|
73
77
|
console.log("");
|
|
74
78
|
}
|
|
75
79
|
// ─── API Key Bootstrap ────────────────────────────────────────────────────────
|
package/dist/memory.js
CHANGED
|
@@ -12,6 +12,7 @@ PRINCIPLES & SYSTEM PROTOCOLS FOR ERROR-FREE EXECUTION:
|
|
|
12
12
|
4. Auto-Verification Loop: After any code or file edit, you MUST run the appropriate compiler, type-check, build script, or test tool (e.g. npm run build, npx tsc, pytest, cargo build, etc.) to verify your changes are syntactically and logically correct. If compilation fails, diagnose the error and patch it immediately.
|
|
13
13
|
5. Autonomous Troubleshooting: If a command fails or times out, inspect the codebase or script to see why it hangs or fails. Do not blindly edit package scripts or configs.
|
|
14
14
|
6. Automated Diagnostic Parsing: When the user pastes IDE problem diagnostics (e.g., JSON blocks containing "resource", "message", "startLineNumber"), stack traces, or compiler errors, parse the diagnostic payload autonomously. Extract the file path and line number, locate the file inside the workspace (resolving drive formats like '/c:/...' to standard local paths, or searching for the filename if needed), read the target lines, and formulate a fix. Do not ask the user for clarifying questions (such as "where is this error?") if the path and error message are already present in the diagnostic block.
|
|
15
|
+
7. Resilience on Tool Failures: If a tool execution returns an error (such as "Target code not found in file" during patch_file, or any other tool failure), do NOT stop or give up. Autonomously analyze the error, adjust your arguments/parameters, or read the file to verify its exact content, and try again with a corrected tool call (or fall back to a full write_file if patching repeatedly fails) to achieve the user's goal.
|
|
15
16
|
|
|
16
17
|
Guidelines:
|
|
17
18
|
- Be concise in your explanations; let code and command output speak for itself.
|
|
@@ -218,6 +219,50 @@ ${topLevelStructure || "(empty)"}
|
|
|
218
219
|
\`\`\`
|
|
219
220
|
`;
|
|
220
221
|
}
|
|
222
|
+
function pruneMessages(messages, maxMessages) {
|
|
223
|
+
if (messages.length <= maxMessages + 1) {
|
|
224
|
+
return messages;
|
|
225
|
+
}
|
|
226
|
+
const systemPrompt = messages[0];
|
|
227
|
+
const history = messages.slice(1);
|
|
228
|
+
// Group history into turns, where each turn starts with role === "user"
|
|
229
|
+
const turns = [];
|
|
230
|
+
let currentTurn = [];
|
|
231
|
+
for (const msg of history) {
|
|
232
|
+
if (msg.role === "user") {
|
|
233
|
+
if (currentTurn.length > 0) {
|
|
234
|
+
turns.push(currentTurn);
|
|
235
|
+
}
|
|
236
|
+
currentTurn = [msg];
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
currentTurn.push(msg);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (currentTurn.length > 0) {
|
|
243
|
+
turns.push(currentTurn);
|
|
244
|
+
}
|
|
245
|
+
// Keep turns from the end (most recent) until we hit the maxMessages limit
|
|
246
|
+
const keptTurns = [];
|
|
247
|
+
let currentCount = 0;
|
|
248
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
249
|
+
const turn = turns[i];
|
|
250
|
+
if (currentCount + turn.length <= maxMessages) {
|
|
251
|
+
keptTurns.unshift(turn);
|
|
252
|
+
currentCount += turn.length;
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
// If we can't fit this turn, but we have kept nothing so far (e.g. a single giant turn),
|
|
256
|
+
// we must keep at least this turn to avoid sending an empty history.
|
|
257
|
+
if (keptTurns.length === 0) {
|
|
258
|
+
keptTurns.push(turn);
|
|
259
|
+
}
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const prunedHistory = keptTurns.flat();
|
|
264
|
+
return [systemPrompt, ...prunedHistory];
|
|
265
|
+
}
|
|
221
266
|
export class Memory {
|
|
222
267
|
messages = [];
|
|
223
268
|
maxMessages;
|
|
@@ -228,8 +273,8 @@ export class Memory {
|
|
|
228
273
|
this.maxMessages = maxMessages;
|
|
229
274
|
this.messages.push({ role: "system", content: SYSTEM_PROMPT });
|
|
230
275
|
}
|
|
231
|
-
async init(scope = "project", agentType = "coder") {
|
|
232
|
-
if (this.initialized)
|
|
276
|
+
async init(scope = "project", agentType = "coder", forceRefresh = false) {
|
|
277
|
+
if (this.initialized && !forceRefresh)
|
|
233
278
|
return;
|
|
234
279
|
this.scope = scope;
|
|
235
280
|
this.agentType = agentType;
|
|
@@ -240,10 +285,7 @@ export class Memory {
|
|
|
240
285
|
}
|
|
241
286
|
add(msg) {
|
|
242
287
|
this.messages.push(msg);
|
|
243
|
-
|
|
244
|
-
if (this.messages.length > this.maxMessages + 1) {
|
|
245
|
-
this.messages = [this.messages[0], ...this.messages.slice(-(this.maxMessages))];
|
|
246
|
-
}
|
|
288
|
+
this.messages = pruneMessages(this.messages, this.maxMessages);
|
|
247
289
|
}
|
|
248
290
|
getAll() {
|
|
249
291
|
return this.messages;
|