ak-gemini 2.0.0 → 2.0.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/README.md CHANGED
@@ -136,7 +136,7 @@ for await (const event of agent.stream('Fetch the data')) {
136
136
 
137
137
  ### CodeAgent — Agent That Writes and Executes Code
138
138
 
139
- Instead of calling tools one by one, the model writes JavaScript that can do everything — read files, write files, run commands — in a single script. Inspired by the [code mode](https://blog.cloudflare.com/how-we-built-mcp-code-mode/) philosophy.
139
+ Instead of calling tools one by one, the model writes JavaScript that can do everything — read files, write files, run commands — in a single script. Inspired by the [code mode](https://blog.cloudflare.com/code-mode/) philosophy.
140
140
 
141
141
  ```javascript
142
142
  const agent = new CodeAgent({
package/code-agent.js CHANGED
@@ -12,8 +12,8 @@
12
12
  import BaseGemini from './base.js';
13
13
  import log from './logger.js';
14
14
  import { execFile } from 'node:child_process';
15
- import { writeFile, unlink, readdir, readFile } from 'node:fs/promises';
16
- import { join } from 'node:path';
15
+ import { writeFile, unlink, readdir, readFile, mkdir } from 'node:fs/promises';
16
+ import { join, sep } from 'node:path';
17
17
  import { randomUUID } from 'node:crypto';
18
18
 
19
19
  /**
@@ -27,7 +27,7 @@ const MAX_FILE_TREE_LINES = 500;
27
27
  const IGNORE_DIRS = new Set(['node_modules', '.git', 'dist', 'coverage', '.next', 'build', '__pycache__']);
28
28
 
29
29
  /**
30
- * AI agent that writes and executes JavaScript code autonomously.
30
+ * AI agent that writes and executes JavaScript code autonomously. ... what could possibly go wrong, right?
31
31
  *
32
32
  * During init, gathers codebase context (file tree + key files) and injects it
33
33
  * into the system prompt. The model uses the `execute_code` tool to run scripts
@@ -67,6 +67,11 @@ class CodeAgent extends BaseGemini {
67
67
  this.timeout = options.timeout || 30_000;
68
68
  this.onBeforeExecution = options.onBeforeExecution || null;
69
69
  this.onCodeExecution = options.onCodeExecution || null;
70
+ this.importantFiles = options.importantFiles || [];
71
+ this.writeDir = options.writeDir || join(this.workingDirectory, 'tmp');
72
+ this.keepArtifacts = options.keepArtifacts ?? false;
73
+ this.comments = options.comments ?? false;
74
+ this.maxRetries = options.maxRetries ?? 3;
70
75
 
71
76
  // ── Internal state ──
72
77
  this._codebaseContext = null;
@@ -87,6 +92,10 @@ class CodeAgent extends BaseGemini {
87
92
  code: {
88
93
  type: 'string',
89
94
  description: 'JavaScript code to execute. Use console.log() for output. You can import any built-in Node.js module.'
95
+ },
96
+ purpose: {
97
+ type: 'string',
98
+ description: 'A short 2-4 word slug describing what this script does (e.g., "read-config", "parse-logs", "fetch-api-data"). Used for naming the script file.'
90
99
  }
91
100
  },
92
101
  required: ['code']
@@ -155,10 +164,50 @@ class CodeAgent extends BaseGemini {
155
164
  ];
156
165
  } catch { /* no package.json */ }
157
166
 
158
- this._codebaseContext = { fileTree, npmPackages };
167
+ // Resolve and read important files
168
+ const importantFileContents = [];
169
+ if (this.importantFiles.length > 0) {
170
+ const fileTreeLines = fileTree.split('\n').map(l => l.trim()).filter(Boolean);
171
+ for (const requested of this.importantFiles) {
172
+ const resolved = this._resolveImportantFile(requested, fileTreeLines);
173
+ if (!resolved) {
174
+ log.warn(`importantFiles: could not locate "${requested}"`);
175
+ continue;
176
+ }
177
+ try {
178
+ const fullPath = join(this.workingDirectory, resolved);
179
+ const content = await readFile(fullPath, 'utf-8');
180
+ importantFileContents.push({ path: resolved, content });
181
+ } catch (e) {
182
+ log.warn(`importantFiles: could not read "${resolved}": ${e.message}`);
183
+ }
184
+ }
185
+ }
186
+
187
+ this._codebaseContext = { fileTree, npmPackages, importantFileContents };
159
188
  this._contextGathered = true;
160
189
  }
161
190
 
191
+ /**
192
+ * Resolve an importantFiles entry against the file tree.
193
+ * Supports exact matches and partial (basename/suffix) matches.
194
+ * @private
195
+ * @param {string} filename
196
+ * @param {string[]} fileTreeLines
197
+ * @returns {string|null}
198
+ */
199
+ _resolveImportantFile(filename, fileTreeLines) {
200
+ // Exact match
201
+ const exact = fileTreeLines.find(line => line === filename);
202
+ if (exact) return exact;
203
+
204
+ // Partial match — filename matches end of path
205
+ const partial = fileTreeLines.find(line =>
206
+ line.endsWith('/' + filename) || line.endsWith(sep + filename)
207
+ );
208
+ return partial || null;
209
+ }
210
+
162
211
  /**
163
212
  * Get file tree using git ls-files.
164
213
  * @private
@@ -215,12 +264,13 @@ class CodeAgent extends BaseGemini {
215
264
  * @returns {string}
216
265
  */
217
266
  _buildSystemPrompt() {
218
- const { fileTree, npmPackages } = this._codebaseContext || { fileTree: '', npmPackages: [] };
267
+ const { fileTree, npmPackages, importantFileContents } = this._codebaseContext || { fileTree: '', npmPackages: [], importantFileContents: [] };
219
268
 
220
269
  let prompt = `You are a coding agent working in ${this.workingDirectory}.
221
270
 
222
271
  ## Instructions
223
272
  - Use the execute_code tool to accomplish tasks by writing JavaScript code
273
+ - Always provide a short descriptive \`purpose\` parameter (2-4 word slug like "read-config") when calling execute_code
224
274
  - Your code runs in a Node.js child process with access to all built-in modules
225
275
  - IMPORTANT: Your code runs as an ES module (.mjs). Use import syntax, NOT require():
226
276
  - import fs from 'fs';
@@ -235,6 +285,12 @@ class CodeAgent extends BaseGemini {
235
285
  - Top-level await is supported
236
286
  - The working directory is: ${this.workingDirectory}`;
237
287
 
288
+ if (this.comments) {
289
+ prompt += `\n- Add a JSDoc @fileoverview comment at the top of each script explaining what it does\n- Add brief JSDoc @param comments for any functions you define`;
290
+ } else {
291
+ prompt += `\n- Do NOT write any comments in your code — save tokens. The code should be self-explanatory.`;
292
+ }
293
+
238
294
  if (fileTree) {
239
295
  prompt += `\n\n## File Tree\n\`\`\`\n${fileTree}\n\`\`\``;
240
296
  }
@@ -243,6 +299,13 @@ class CodeAgent extends BaseGemini {
243
299
  prompt += `\n\n## Available Packages\nThese npm packages are installed and can be imported: ${npmPackages.join(', ')}`;
244
300
  }
245
301
 
302
+ if (importantFileContents && importantFileContents.length > 0) {
303
+ prompt += `\n\n## Key Files`;
304
+ for (const { path: filePath, content } of importantFileContents) {
305
+ prompt += `\n\n### ${filePath}\n\`\`\`javascript\n${content}\n\`\`\``;
306
+ }
307
+ }
308
+
246
309
  if (this._userSystemPrompt) {
247
310
  prompt += `\n\n## Additional Instructions\n${this._userSystemPrompt}`;
248
311
  }
@@ -252,13 +315,25 @@ class CodeAgent extends BaseGemini {
252
315
 
253
316
  // ── Code Execution ───────────────────────────────────────────────────────
254
317
 
318
+ /**
319
+ * Generate a sanitized slug from a purpose string.
320
+ * @private
321
+ * @param {string} [purpose]
322
+ * @returns {string}
323
+ */
324
+ _slugify(purpose) {
325
+ if (!purpose) return randomUUID().slice(0, 8);
326
+ return purpose.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40);
327
+ }
328
+
255
329
  /**
256
330
  * Execute a JavaScript code string in a child process.
257
331
  * @private
258
332
  * @param {string} code - JavaScript code to execute
333
+ * @param {string} [purpose] - Short description for file naming
259
334
  * @returns {Promise<{stdout: string, stderr: string, exitCode: number, denied?: boolean}>}
260
335
  */
261
- async _executeCode(code) {
336
+ async _executeCode(code, purpose) {
262
337
  // Check if stopped
263
338
  if (this._stopped) {
264
339
  return { stdout: '', stderr: 'Agent was stopped', exitCode: -1 };
@@ -276,7 +351,11 @@ class CodeAgent extends BaseGemini {
276
351
  }
277
352
  }
278
353
 
279
- const tempFile = join(this.workingDirectory, `.code-agent-tmp-${randomUUID()}.mjs`);
354
+ // Ensure writeDir exists
355
+ await mkdir(this.writeDir, { recursive: true });
356
+
357
+ const slug = this._slugify(purpose);
358
+ const tempFile = join(this.writeDir, `agent-${slug}-${Date.now()}.mjs`);
280
359
 
281
360
  try {
282
361
  // Write code to temp file
@@ -317,7 +396,10 @@ class CodeAgent extends BaseGemini {
317
396
  }
318
397
 
319
398
  // Track execution
320
- this._allExecutions.push({ code, output: result.stdout, stderr: result.stderr, exitCode: result.exitCode });
399
+ this._allExecutions.push({
400
+ code, purpose: purpose || null, output: result.stdout, stderr: result.stderr,
401
+ exitCode: result.exitCode, filePath: this.keepArtifacts ? tempFile : null
402
+ });
321
403
 
322
404
  // Fire notification callback
323
405
  if (this.onCodeExecution) {
@@ -327,9 +409,11 @@ class CodeAgent extends BaseGemini {
327
409
 
328
410
  return result;
329
411
  } finally {
330
- // Cleanup temp file
331
- try { await unlink(tempFile); }
332
- catch { /* file may already be gone */ }
412
+ // Cleanup temp file (unless keeping artifacts)
413
+ if (!this.keepArtifacts) {
414
+ try { await unlink(tempFile); }
415
+ catch { /* file may already be gone */ }
416
+ }
333
417
  }
334
418
  }
335
419
 
@@ -363,6 +447,7 @@ class CodeAgent extends BaseGemini {
363
447
  this._stopped = false;
364
448
 
365
449
  const codeExecutions = [];
450
+ let consecutiveFailures = 0;
366
451
 
367
452
  let response = await this.chatSession.sendMessage({ message });
368
453
 
@@ -377,19 +462,33 @@ class CodeAgent extends BaseGemini {
377
462
  if (this._stopped) break;
378
463
 
379
464
  const code = call.args?.code || '';
380
- const result = await this._executeCode(code);
465
+ const purpose = call.args?.purpose;
466
+ const result = await this._executeCode(code, purpose);
381
467
 
382
468
  codeExecutions.push({
383
469
  code,
470
+ purpose: this._slugify(purpose),
384
471
  output: result.stdout,
385
472
  stderr: result.stderr,
386
473
  exitCode: result.exitCode
387
474
  });
388
475
 
476
+ if (result.exitCode !== 0 && !result.denied) {
477
+ consecutiveFailures++;
478
+ } else {
479
+ consecutiveFailures = 0;
480
+ }
481
+
482
+ let output = this._formatOutput(result);
483
+
484
+ if (consecutiveFailures >= this.maxRetries) {
485
+ output += `\n\n[RETRY LIMIT REACHED] You have failed ${this.maxRetries} consecutive attempts. STOP trying to execute code. Instead, respond with: 1) What you were trying to do, 2) The errors you encountered, 3) Questions for the user about how to resolve it.`;
486
+ }
487
+
389
488
  results.push({
390
489
  id: call.id,
391
490
  name: call.name,
392
- result: this._formatOutput(result)
491
+ result: output
393
492
  });
394
493
  }
395
494
 
@@ -405,6 +504,8 @@ class CodeAgent extends BaseGemini {
405
504
  }
406
505
  }))
407
506
  });
507
+
508
+ if (consecutiveFailures >= this.maxRetries) break;
408
509
  }
409
510
 
410
511
  this._captureMetadata(response);
@@ -445,6 +546,7 @@ class CodeAgent extends BaseGemini {
445
546
 
446
547
  const codeExecutions = [];
447
548
  let fullText = '';
549
+ let consecutiveFailures = 0;
448
550
 
449
551
  let streamResponse = await this.chatSession.sendMessageStream({ message });
450
552
 
@@ -481,12 +583,14 @@ class CodeAgent extends BaseGemini {
481
583
  if (this._stopped) break;
482
584
 
483
585
  const code = call.args?.code || '';
586
+ const purpose = call.args?.purpose;
484
587
  yield { type: 'code', code };
485
588
 
486
- const result = await this._executeCode(code);
589
+ const result = await this._executeCode(code, purpose);
487
590
 
488
591
  codeExecutions.push({
489
592
  code,
593
+ purpose: this._slugify(purpose),
490
594
  output: result.stdout,
491
595
  stderr: result.stderr,
492
596
  exitCode: result.exitCode
@@ -500,10 +604,22 @@ class CodeAgent extends BaseGemini {
500
604
  exitCode: result.exitCode
501
605
  };
502
606
 
607
+ if (result.exitCode !== 0 && !result.denied) {
608
+ consecutiveFailures++;
609
+ } else {
610
+ consecutiveFailures = 0;
611
+ }
612
+
613
+ let output = this._formatOutput(result);
614
+
615
+ if (consecutiveFailures >= this.maxRetries) {
616
+ output += `\n\n[RETRY LIMIT REACHED] You have failed ${this.maxRetries} consecutive attempts. STOP trying to execute code. Instead, respond with: 1) What you were trying to do, 2) The errors you encountered, 3) Questions for the user about how to resolve it.`;
617
+ }
618
+
503
619
  results.push({
504
620
  id: call.id,
505
621
  name: call.name,
506
- result: this._formatOutput(result)
622
+ result: output
507
623
  });
508
624
  }
509
625
 
@@ -519,15 +635,21 @@ class CodeAgent extends BaseGemini {
519
635
  }
520
636
  }))
521
637
  });
638
+
639
+ if (consecutiveFailures >= this.maxRetries) break;
522
640
  }
523
641
 
524
- // Max rounds reached or stopped
642
+ // Max rounds reached, stopped, or retry limit hit
643
+ let warning = 'Max tool rounds reached';
644
+ if (this._stopped) warning = 'Agent was stopped';
645
+ else if (consecutiveFailures >= this.maxRetries) warning = 'Retry limit reached';
646
+
525
647
  yield {
526
648
  type: 'done',
527
649
  fullText,
528
650
  codeExecutions,
529
651
  usage: this.getLastUsage(),
530
- warning: this._stopped ? 'Agent was stopped' : 'Max tool rounds reached'
652
+ warning
531
653
  };
532
654
  }
533
655
 
@@ -539,8 +661,10 @@ class CodeAgent extends BaseGemini {
539
661
  */
540
662
  dump() {
541
663
  return this._allExecutions.map((exec, i) => ({
542
- fileName: `script-${i + 1}.mjs`,
543
- script: exec.code
664
+ fileName: exec.purpose ? `agent-${exec.purpose}.mjs` : `script-${i + 1}.mjs`,
665
+ purpose: exec.purpose || null,
666
+ script: exec.code,
667
+ filePath: exec.filePath || null
544
668
  }));
545
669
  }
546
670