ak-gemini 2.1.3 → 2.2.1

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.
Files changed (4) hide show
  1. package/code-agent.js +234 -43
  2. package/index.cjs +216 -44
  3. package/package.json +1 -1
  4. package/types.d.ts +29 -2
package/code-agent.js CHANGED
@@ -8,7 +8,7 @@ import BaseGemini from './base.js';
8
8
  import log from './logger.js';
9
9
  import { execFile } from 'node:child_process';
10
10
  import { writeFile, unlink, readdir, readFile, mkdir } from 'node:fs/promises';
11
- import { join, sep, basename } from 'node:path';
11
+ import { join, sep, basename, isAbsolute } from 'node:path';
12
12
  import { randomUUID } from 'node:crypto';
13
13
 
14
14
  /**
@@ -19,7 +19,54 @@ import { randomUUID } from 'node:crypto';
19
19
 
20
20
  const MAX_OUTPUT_CHARS = 50_000;
21
21
  const MAX_FILE_TREE_LINES = 500;
22
- const IGNORE_DIRS = new Set(['node_modules', '.git', 'dist', 'coverage', '.next', 'build', '__pycache__']);
22
+ const IGNORE_DIRS = new Set(['node_modules', '.git', 'dist', 'coverage', '.next', 'build', '__pycache__', '.venv']);
23
+
24
+ const LANG_CONFIG = {
25
+ javascript: {
26
+ toolDescExecute: 'Execute a given piece of JavaScript code in a Node.js child process. Use this when you already have code to run — e.g., running code from a previous write_code call, re-running a snippet, or executing code the user provided. Use console.log() for output.',
27
+ toolDescWriteAndRun: 'Write a fresh solution from scratch and execute it in one step. Use this when you need to figure out the code AND run it — the autonomous, end-to-end tool for solving problems with code.',
28
+ codeParamDesc: 'JavaScript code to execute. Use console.log() for output. Use import syntax (ES modules).',
29
+ codeRules: `- Your code runs in a Node.js child process with access to all built-in modules
30
+ - IMPORTANT: Your code runs as an ES module (.mjs). Use import syntax, NOT require():
31
+ - import fs from 'fs';
32
+ - import path from 'path';
33
+ - import { execSync } from 'child_process';
34
+ - Use console.log() to produce output — that's how results are returned to you
35
+ - Write efficient scripts that do multiple things per execution when possible
36
+ - For parallel async operations, use Promise.all()
37
+ - Handle errors in your scripts with try/catch so you get useful error messages
38
+ - Top-level await is supported`,
39
+ commentsEnabled: `- 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`,
40
+ commentsDisabled: `- Do NOT write any comments in your code — save tokens. The code should be self-explanatory.`,
41
+ packageLabel: 'Available Packages',
42
+ packageIntro: 'These npm packages are installed and can be imported',
43
+ bashExamples: 'ls, grep, curl, git, npm, cat',
44
+ codeBlockLang: 'javascript',
45
+ execToolSummary: 'Run a given piece of JavaScript code. Use when you already have code to run — e.g., from a previous write_code call, re-running a snippet, or executing user-provided code.',
46
+ writeRunToolSummary: 'Write a fresh solution from scratch and execute it in one step. The autonomous, end-to-end tool for solving problems with code.',
47
+ },
48
+ python: {
49
+ toolDescExecute: 'Execute a given piece of Python code in a child process. Use this when you already have code to run — e.g., running code from a previous write_code call, re-running a snippet, or executing code the user provided. Use print() for output.',
50
+ toolDescWriteAndRun: 'Write a fresh Python solution from scratch and execute it in one step. Use this when you need to figure out the code AND run it — the autonomous, end-to-end tool for solving problems with code.',
51
+ codeParamDesc: 'Python code to execute. Use print() for output.',
52
+ codeRules: `- Your code runs in a Python 3 child process
53
+ - Use print() to produce output — that's how results are returned to you
54
+ - Use standard Python imports (import os, import json, from pathlib import Path, etc.)
55
+ - Write efficient scripts; prefer list comprehensions and built-in functions
56
+ - For async operations, use asyncio
57
+ - Handle errors with try/except so you get useful error messages
58
+ - A virtual environment is active — you can install packages using run_bash with: pip install <package>
59
+ - Installed packages persist across executions in this session`,
60
+ commentsEnabled: `- Add a module-level docstring at the top of each script explaining what it does\n- Add brief docstrings for any functions you define`,
61
+ commentsDisabled: `- Do NOT write any comments in your code — save tokens. The code should be self-explanatory.`,
62
+ packageLabel: 'Available Packages',
63
+ packageIntro: 'These Python packages are available for import',
64
+ bashExamples: 'ls, grep, curl, git, pip, cat',
65
+ codeBlockLang: 'python',
66
+ execToolSummary: 'Run a given piece of Python code. Use when you already have code to run — e.g., from a previous write_code call, re-running a snippet, or executing user-provided code.',
67
+ writeRunToolSummary: 'Write a fresh Python solution from scratch and execute it in one step. The autonomous, end-to-end tool for solving problems with code.',
68
+ }
69
+ };
23
70
 
24
71
  /** Tools that execute code/commands and can fail */
25
72
  const EXECUTING_TOOLS = new Set(['execute_code', 'write_and_run_code', 'run_bash']);
@@ -37,6 +84,8 @@ class CodeAgent extends BaseGemini {
37
84
 
38
85
  // ── Agent config ──
39
86
  this.workingDirectory = options.workingDirectory || process.cwd();
87
+ this.language = options.language || 'javascript';
88
+ this.pythonPath = options.pythonPath || null;
40
89
  this.maxRounds = options.maxRounds || 10;
41
90
  this.timeout = options.timeout || 30_000;
42
91
  this.onBeforeExecution = options.onBeforeExecution || null;
@@ -49,6 +98,22 @@ class CodeAgent extends BaseGemini {
49
98
  this.skills = options.skills || [];
50
99
  this.envOverview = options.envOverview || '';
51
100
 
101
+ // ── Python state (resolved during init) ──
102
+ this._pythonBinary = null;
103
+ this._venvPath = null;
104
+ this._venvEnv = null;
105
+
106
+ // ── Custom tools ──
107
+ this.customTools = (options.tools || []).map(t => ({
108
+ name: t.name,
109
+ description: t.description,
110
+ parametersJsonSchema: t.parametersJsonSchema || t.parameters || t.input_schema || t.inputSchema
111
+ }));
112
+ this.toolExecutor = options.toolExecutor || null;
113
+ if (this.customTools.length > 0 && !this.toolExecutor) {
114
+ throw new Error('CodeAgent: tools provided without a toolExecutor.');
115
+ }
116
+
52
117
  // ── Internal state ──
53
118
  this._codebaseContext = null;
54
119
  this._contextGathered = false;
@@ -74,6 +139,8 @@ class CodeAgent extends BaseGemini {
74
139
  * @returns {{ functionDeclarations: Array<Object> }}
75
140
  */
76
141
  _buildToolDefinitions() {
142
+ const lang = LANG_CONFIG[this.language] || LANG_CONFIG.javascript;
143
+
77
144
  /** @type {Array<Object>} */
78
145
  const declarations = [
79
146
  {
@@ -91,11 +158,11 @@ class CodeAgent extends BaseGemini {
91
158
  },
92
159
  {
93
160
  name: 'execute_code',
94
- description: 'Execute a given piece of JavaScript code in a Node.js child process. Use this when you already have code to run — e.g., running code from a previous write_code call, re-running a snippet, or executing code the user provided. Use console.log() for output.',
161
+ description: lang.toolDescExecute,
95
162
  parametersJsonSchema: {
96
163
  type: 'object',
97
164
  properties: {
98
- code: { type: 'string', description: 'JavaScript code to execute. Use console.log() for output. Use import syntax (ES modules).' },
165
+ code: { type: 'string', description: lang.codeParamDesc },
99
166
  purpose: { type: 'string', description: 'A short 2-4 word slug describing what this script does (e.g., "read-config", "parse-logs").' }
100
167
  },
101
168
  required: ['code']
@@ -103,11 +170,11 @@ class CodeAgent extends BaseGemini {
103
170
  },
104
171
  {
105
172
  name: 'write_and_run_code',
106
- description: 'Write a fresh solution from scratch and execute it in one step. Use this when you need to figure out the code AND run it — the autonomous, end-to-end tool for solving problems with code.',
173
+ description: lang.toolDescWriteAndRun,
107
174
  parametersJsonSchema: {
108
175
  type: 'object',
109
176
  properties: {
110
- code: { type: 'string', description: 'JavaScript code to write and execute. Use console.log() for output. Use import syntax (ES modules).' },
177
+ code: { type: 'string', description: lang.codeParamDesc },
111
178
  purpose: { type: 'string', description: 'A short 2-4 word slug describing what this script does (e.g., "fetch-api-data", "generate-report").' }
112
179
  },
113
180
  required: ['code']
@@ -129,7 +196,7 @@ class CodeAgent extends BaseGemini {
129
196
  },
130
197
  {
131
198
  name: 'run_bash',
132
- description: 'Execute a shell command in the working directory. Use this for file operations, git commands, installing packages, or any shell task. Prefer this over execute_code for simple shell operations.',
199
+ description: `Execute a shell command in the working directory. Use this for file operations, git commands, installing packages, or any shell task (e.g., ${lang.bashExamples}). Prefer this over execute_code for simple shell operations.`,
133
200
  parametersJsonSchema: {
134
201
  type: 'object',
135
202
  properties: {
@@ -156,6 +223,11 @@ class CodeAgent extends BaseGemini {
156
223
  });
157
224
  }
158
225
 
226
+ // Append custom tools
227
+ for (const t of this.customTools) {
228
+ declarations.push({ name: t.name, description: t.description, parametersJsonSchema: t.parametersJsonSchema });
229
+ }
230
+
159
231
  return { functionDeclarations: declarations };
160
232
  }
161
233
 
@@ -173,6 +245,12 @@ class CodeAgent extends BaseGemini {
173
245
  await this._loadSkills();
174
246
  }
175
247
 
248
+ // Resolve Python and set up venv if needed
249
+ if (this.language === 'python' && (!this._pythonBinary || force)) {
250
+ await this._resolvePython();
251
+ await this._setupVenv();
252
+ }
253
+
176
254
  // Rebuild tools (use_skill may now be included)
177
255
  this.chatConfig.tools = [this._buildToolDefinitions()];
178
256
 
@@ -211,6 +289,73 @@ class CodeAgent extends BaseGemini {
211
289
  }
212
290
  }
213
291
 
292
+ // ── Python Resolution ───────────────────────────────────────────────────
293
+
294
+ /**
295
+ * Resolve the Python 3 binary path.
296
+ * @private
297
+ */
298
+ async _resolvePython() {
299
+ const tryBinary = (bin) => new Promise((resolve) => {
300
+ execFile(bin, ['--version'], { timeout: 5000 }, (err, stdout, stderr) => {
301
+ if (err) return resolve(null);
302
+ const output = (stdout || '') + (stderr || '');
303
+ if (output.includes('Python 3.')) return resolve(bin);
304
+ resolve(null);
305
+ });
306
+ });
307
+
308
+ if (this.pythonPath) {
309
+ const result = await tryBinary(this.pythonPath);
310
+ if (result) { this._pythonBinary = result; return; }
311
+ throw new Error(`CodeAgent: pythonPath "${this.pythonPath}" is not a valid Python 3 binary.`);
312
+ }
313
+
314
+ const python3 = await tryBinary('python3');
315
+ if (python3) { this._pythonBinary = python3; return; }
316
+
317
+ const python = await tryBinary('python');
318
+ if (python) { this._pythonBinary = python; return; }
319
+
320
+ throw new Error('CodeAgent: language is "python" but python3 was not found on PATH. Install Python 3 or provide the pythonPath option.');
321
+ }
322
+
323
+ /**
324
+ * Create a virtual environment for Python execution.
325
+ * @private
326
+ */
327
+ async _setupVenv() {
328
+ await mkdir(this.writeDir, { recursive: true });
329
+ this._venvPath = join(this.writeDir, '.venv');
330
+ const isWin = process.platform === 'win32';
331
+ const venvBin = isWin ? join(this._venvPath, 'Scripts') : join(this._venvPath, 'bin');
332
+ const venvPython = join(venvBin, isWin ? 'python.exe' : 'python');
333
+
334
+ // Create venv if it doesn't exist
335
+ try {
336
+ await readFile(venvPython);
337
+ } catch {
338
+ log.debug(`Creating Python venv at ${this._venvPath}`);
339
+ await new Promise((resolve, reject) => {
340
+ execFile(this._pythonBinary, ['-m', 'venv', this._venvPath], {
341
+ timeout: 30_000
342
+ }, (err) => {
343
+ if (err) return reject(new Error(`CodeAgent: failed to create venv: ${err.message}`));
344
+ resolve();
345
+ });
346
+ });
347
+ }
348
+
349
+ // Build env with venv activated
350
+ const env = Object.assign({}, process.env);
351
+ env.VIRTUAL_ENV = this._venvPath;
352
+ env.PATH = venvBin + (isWin ? ';' : ':') + (process.env.PATH || '');
353
+ this._venvEnv = env;
354
+ this._pythonBinary = venvPython;
355
+
356
+ log.debug(`Python venv ready at ${this._venvPath}`);
357
+ }
358
+
214
359
  // ── Context Gathering ────────────────────────────────────────────────────
215
360
 
216
361
  /**
@@ -232,15 +377,37 @@ class CodeAgent extends BaseGemini {
232
377
  fileTree = `${truncated}\n... (${lines.length - MAX_FILE_TREE_LINES} more files)`;
233
378
  }
234
379
 
235
- let npmPackages = [];
236
- try {
237
- const pkgPath = join(this.workingDirectory, 'package.json');
238
- const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
239
- npmPackages = [
240
- ...Object.keys(pkg.dependencies || {}),
241
- ...Object.keys(pkg.devDependencies || {})
242
- ];
243
- } catch { /* no package.json */ }
380
+ let packages = [];
381
+ if (this.language === 'python') {
382
+ try {
383
+ const reqPath = join(this.workingDirectory, 'requirements.txt');
384
+ const content = await readFile(reqPath, 'utf-8');
385
+ packages = content.split('\n')
386
+ .map(l => l.trim())
387
+ .filter(l => l && !l.startsWith('#') && !l.startsWith('-'))
388
+ .map(l => l.split(/[>=<!\[;\s]/)[0]);
389
+ } catch { /* no requirements.txt */ }
390
+ if (packages.length === 0) {
391
+ try {
392
+ const ppPath = join(this.workingDirectory, 'pyproject.toml');
393
+ const content = await readFile(ppPath, 'utf-8');
394
+ const depMatch = content.match(/dependencies\s*=\s*\[([\s\S]*?)\]/);
395
+ if (depMatch) {
396
+ packages = (depMatch[1].match(/"([^"]+)"/g) || [])
397
+ .map(s => s.replace(/"/g, '').split(/[>=<!\[;\s]/)[0]);
398
+ }
399
+ } catch { /* no pyproject.toml */ }
400
+ }
401
+ } else {
402
+ try {
403
+ const pkgPath = join(this.workingDirectory, 'package.json');
404
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
405
+ packages = [
406
+ ...Object.keys(pkg.dependencies || {}),
407
+ ...Object.keys(pkg.devDependencies || {})
408
+ ];
409
+ } catch { /* no package.json */ }
410
+ }
244
411
 
245
412
  const importantFileContents = [];
246
413
  if (this.importantFiles.length > 0) {
@@ -252,7 +419,7 @@ class CodeAgent extends BaseGemini {
252
419
  continue;
253
420
  }
254
421
  try {
255
- const fullPath = join(this.workingDirectory, resolved);
422
+ const fullPath = isAbsolute(resolved) ? resolved : join(this.workingDirectory, resolved);
256
423
  const content = await readFile(fullPath, 'utf-8');
257
424
  importantFileContents.push({ path: resolved, content });
258
425
  } catch (e) {
@@ -261,7 +428,7 @@ class CodeAgent extends BaseGemini {
261
428
  }
262
429
  }
263
430
 
264
- this._codebaseContext = { fileTree, npmPackages, importantFileContents };
431
+ this._codebaseContext = { fileTree, npmPackages: packages, packages, importantFileContents };
265
432
  this._contextGathered = true;
266
433
  }
267
434
 
@@ -269,6 +436,8 @@ class CodeAgent extends BaseGemini {
269
436
  * @private
270
437
  */
271
438
  _resolveImportantFile(filename, fileTreeLines) {
439
+ if (isAbsolute(filename)) return filename;
440
+
272
441
  const exact = fileTreeLines.find(line => line === filename);
273
442
  if (exact) return exact;
274
443
 
@@ -325,7 +494,8 @@ class CodeAgent extends BaseGemini {
325
494
  * @private
326
495
  */
327
496
  _buildSystemPrompt() {
328
- const { fileTree, npmPackages, importantFileContents } = this._codebaseContext || { fileTree: '', npmPackages: [], importantFileContents: [] };
497
+ const { fileTree, packages, importantFileContents } = this._codebaseContext || { fileTree: '', packages: [], importantFileContents: [] };
498
+ const lang = LANG_CONFIG[this.language] || LANG_CONFIG.javascript;
329
499
 
330
500
  let prompt = `You are a coding agent working in ${this.workingDirectory}.
331
501
 
@@ -335,16 +505,16 @@ class CodeAgent extends BaseGemini {
335
505
  Output code without executing it. Use when showing, proposing, or presenting code to the user.
336
506
 
337
507
  ### execute_code
338
- Run a given piece of JavaScript code. Use when you already have code to run — e.g., from a previous write_code call, re-running a snippet, or executing user-provided code.
508
+ ${lang.execToolSummary}
339
509
 
340
510
  ### write_and_run_code
341
- Write a fresh solution from scratch and execute it in one step. The autonomous, end-to-end tool for solving problems with code.
511
+ ${lang.writeRunToolSummary}
342
512
 
343
513
  ### fix_code
344
514
  Fix broken code by providing original and fixed versions. Set execute=true to verify the fix works.
345
515
 
346
516
  ### run_bash
347
- Run shell commands directly (e.g., ls, grep, curl, git, npm, cat). Prefer this over execute_code for simple shell operations.`;
517
+ Run shell commands directly (e.g., ${lang.bashExamples}). Prefer this over execute_code for simple shell operations.`;
348
518
 
349
519
  if (this._skillRegistry.size > 0) {
350
520
  prompt += `
@@ -358,36 +528,27 @@ Load a skill by name to get detailed instructions and templates. Available skill
358
528
  ## Code Execution Rules
359
529
  These rules apply when using execute_code, write_and_run_code, or fix_code (with execute=true):
360
530
  - Always provide a short descriptive \`purpose\` parameter (2-4 word slug like "read-config")
361
- - Your code runs in a Node.js child process with access to all built-in modules
362
- - IMPORTANT: Your code runs as an ES module (.mjs). Use import syntax, NOT require():
363
- - import fs from 'fs';
364
- - import path from 'path';
365
- - import { execSync } from 'child_process';
366
- - Use console.log() to produce output — that's how results are returned to you
367
- - Write efficient scripts that do multiple things per execution when possible
368
- - For parallel async operations, use Promise.all()
369
- - Handle errors in your scripts with try/catch so you get useful error messages
370
- - Top-level await is supported
531
+ ${lang.codeRules}
371
532
  - The working directory is: ${this.workingDirectory}`;
372
533
 
373
534
  if (this.comments) {
374
- 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`;
535
+ prompt += `\n${lang.commentsEnabled}`;
375
536
  } else {
376
- prompt += `\n- Do NOT write any comments in your code — save tokens. The code should be self-explanatory.`;
537
+ prompt += `\n${lang.commentsDisabled}`;
377
538
  }
378
539
 
379
540
  if (fileTree) {
380
541
  prompt += `\n\n## File Tree\n\`\`\`\n${fileTree}\n\`\`\``;
381
542
  }
382
543
 
383
- if (npmPackages.length > 0) {
384
- prompt += `\n\n## Available Packages\nThese npm packages are installed and can be imported: ${npmPackages.join(', ')}`;
544
+ if (packages && packages.length > 0) {
545
+ prompt += `\n\n## ${lang.packageLabel}\n${lang.packageIntro}: ${packages.join(', ')}`;
385
546
  }
386
547
 
387
548
  if (importantFileContents && importantFileContents.length > 0) {
388
549
  prompt += `\n\n## Key Files`;
389
550
  for (const { path: filePath, content } of importantFileContents) {
390
- prompt += `\n\n### ${filePath}\n\`\`\`javascript\n${content}\n\`\`\``;
551
+ prompt += `\n\n### ${filePath}\n\`\`\`${lang.codeBlockLang}\n${content}\n\`\`\``;
391
552
  }
392
553
  }
393
554
 
@@ -434,16 +595,20 @@ These rules apply when using execute_code, write_and_run_code, or fix_code (with
434
595
  await mkdir(this.writeDir, { recursive: true });
435
596
 
436
597
  const slug = this._slugify(purpose);
437
- const tempFile = join(this.writeDir, `agent-${slug}-${Date.now()}.mjs`);
598
+ const ext = this.language === 'python' ? '.py' : '.mjs';
599
+ const tempFile = join(this.writeDir, `agent-${slug}-${Date.now()}${ext}`);
438
600
 
439
601
  try {
440
602
  await writeFile(tempFile, code, 'utf-8');
441
603
 
604
+ const binary = this.language === 'python' ? this._pythonBinary : 'node';
605
+ const execEnv = this.language === 'python' && this._venvEnv ? this._venvEnv : process.env;
606
+
442
607
  const result = await new Promise((resolve) => {
443
- const child = execFile('node', [tempFile], {
608
+ const child = execFile(binary, [tempFile], {
444
609
  cwd: this.workingDirectory,
445
610
  timeout: this.timeout,
446
- env: process.env,
611
+ env: execEnv,
447
612
  maxBuffer: 10 * 1024 * 1024
448
613
  }, (err, stdout, stderr) => {
449
614
  this._activeProcess = null;
@@ -513,11 +678,13 @@ These rules apply when using execute_code, write_and_run_code, or fix_code (with
513
678
  }
514
679
  }
515
680
 
681
+ const execEnv = this.language === 'python' && this._venvEnv ? this._venvEnv : process.env;
682
+
516
683
  const result = await new Promise((resolve) => {
517
684
  const child = execFile('bash', ['-c', command], {
518
685
  cwd: this.workingDirectory,
519
686
  timeout: this.timeout,
520
- env: process.env,
687
+ env: execEnv,
521
688
  maxBuffer: 10 * 1024 * 1024
522
689
  }, (err, stdout, stderr) => {
523
690
  this._activeProcess = null;
@@ -651,12 +818,30 @@ These rules apply when using execute_code, write_and_run_code, or fix_code (with
651
818
  data: { tool: 'use_skill', skillName: skill.name, content: skill.content, found: true }
652
819
  };
653
820
  }
654
- default:
821
+ default: {
822
+ if (this.toolExecutor) {
823
+ try {
824
+ const result = await this.toolExecutor(name, input);
825
+ const resultStr = typeof result === 'string' ? result : JSON.stringify(result);
826
+ return {
827
+ output: resultStr,
828
+ type: 'tool',
829
+ data: { tool: name, args: input, result }
830
+ };
831
+ } catch (err) {
832
+ return {
833
+ output: `Tool "${name}" failed: ${err.message}`,
834
+ type: 'tool',
835
+ data: { tool: name, args: input, error: err.message }
836
+ };
837
+ }
838
+ }
655
839
  return {
656
840
  output: `Unknown tool: ${name}`,
657
841
  type: 'unknown',
658
842
  data: { tool: name }
659
843
  };
844
+ }
660
845
  }
661
846
  }
662
847
 
@@ -859,6 +1044,11 @@ These rules apply when using execute_code, write_and_run_code, or fix_code (with
859
1044
  yield { type: 'skill', skillName: data.skillName, content: data.content, found: data.found };
860
1045
  }
861
1046
 
1047
+ // Emit custom tool event
1048
+ if (type === 'tool') {
1049
+ yield { type: 'tool', toolName, args: data.args, result: data.result, error: data.error };
1050
+ }
1051
+
862
1052
  // Track consecutive failures
863
1053
  const isExecutingTool = EXECUTING_TOOLS.has(toolName) || (toolName === 'fix_code' && toolInput.execute);
864
1054
  if (isExecutingTool) {
@@ -922,8 +1112,9 @@ These rules apply when using execute_code, write_and_run_code, or fix_code (with
922
1112
  * @returns {Array<{fileName: string, purpose: string|null, script: string, filePath: string|null, tool: string}>}
923
1113
  */
924
1114
  dump() {
1115
+ const ext = this.language === 'python' ? '.py' : '.mjs';
925
1116
  return this._allExecutions.map((exec, i) => ({
926
- fileName: exec.purpose ? `agent-${exec.purpose}.mjs` : `script-${i + 1}.mjs`,
1117
+ fileName: exec.purpose ? `agent-${exec.purpose}${ext}` : `script-${i + 1}${ext}`,
927
1118
  purpose: exec.purpose || null,
928
1119
  script: exec.code,
929
1120
  filePath: exec.filePath || null,
package/index.cjs CHANGED
@@ -1626,7 +1626,55 @@ var import_node_path = require("node:path");
1626
1626
  var import_node_crypto = require("node:crypto");
1627
1627
  var MAX_OUTPUT_CHARS = 5e4;
1628
1628
  var MAX_FILE_TREE_LINES = 500;
1629
- var IGNORE_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "coverage", ".next", "build", "__pycache__"]);
1629
+ var IGNORE_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "coverage", ".next", "build", "__pycache__", ".venv"]);
1630
+ var LANG_CONFIG = {
1631
+ javascript: {
1632
+ toolDescExecute: "Execute a given piece of JavaScript code in a Node.js child process. Use this when you already have code to run \u2014 e.g., running code from a previous write_code call, re-running a snippet, or executing code the user provided. Use console.log() for output.",
1633
+ toolDescWriteAndRun: "Write a fresh solution from scratch and execute it in one step. Use this when you need to figure out the code AND run it \u2014 the autonomous, end-to-end tool for solving problems with code.",
1634
+ codeParamDesc: "JavaScript code to execute. Use console.log() for output. Use import syntax (ES modules).",
1635
+ codeRules: `- Your code runs in a Node.js child process with access to all built-in modules
1636
+ - IMPORTANT: Your code runs as an ES module (.mjs). Use import syntax, NOT require():
1637
+ - import fs from 'fs';
1638
+ - import path from 'path';
1639
+ - import { execSync } from 'child_process';
1640
+ - Use console.log() to produce output \u2014 that's how results are returned to you
1641
+ - Write efficient scripts that do multiple things per execution when possible
1642
+ - For parallel async operations, use Promise.all()
1643
+ - Handle errors in your scripts with try/catch so you get useful error messages
1644
+ - Top-level await is supported`,
1645
+ commentsEnabled: `- Add a JSDoc @fileoverview comment at the top of each script explaining what it does
1646
+ - Add brief JSDoc @param comments for any functions you define`,
1647
+ commentsDisabled: `- Do NOT write any comments in your code \u2014 save tokens. The code should be self-explanatory.`,
1648
+ packageLabel: "Available Packages",
1649
+ packageIntro: "These npm packages are installed and can be imported",
1650
+ bashExamples: "ls, grep, curl, git, npm, cat",
1651
+ codeBlockLang: "javascript",
1652
+ execToolSummary: "Run a given piece of JavaScript code. Use when you already have code to run \u2014 e.g., from a previous write_code call, re-running a snippet, or executing user-provided code.",
1653
+ writeRunToolSummary: "Write a fresh solution from scratch and execute it in one step. The autonomous, end-to-end tool for solving problems with code."
1654
+ },
1655
+ python: {
1656
+ toolDescExecute: "Execute a given piece of Python code in a child process. Use this when you already have code to run \u2014 e.g., running code from a previous write_code call, re-running a snippet, or executing code the user provided. Use print() for output.",
1657
+ toolDescWriteAndRun: "Write a fresh Python solution from scratch and execute it in one step. Use this when you need to figure out the code AND run it \u2014 the autonomous, end-to-end tool for solving problems with code.",
1658
+ codeParamDesc: "Python code to execute. Use print() for output.",
1659
+ codeRules: `- Your code runs in a Python 3 child process
1660
+ - Use print() to produce output \u2014 that's how results are returned to you
1661
+ - Use standard Python imports (import os, import json, from pathlib import Path, etc.)
1662
+ - Write efficient scripts; prefer list comprehensions and built-in functions
1663
+ - For async operations, use asyncio
1664
+ - Handle errors with try/except so you get useful error messages
1665
+ - A virtual environment is active \u2014 you can install packages using run_bash with: pip install <package>
1666
+ - Installed packages persist across executions in this session`,
1667
+ commentsEnabled: `- Add a module-level docstring at the top of each script explaining what it does
1668
+ - Add brief docstrings for any functions you define`,
1669
+ commentsDisabled: `- Do NOT write any comments in your code \u2014 save tokens. The code should be self-explanatory.`,
1670
+ packageLabel: "Available Packages",
1671
+ packageIntro: "These Python packages are available for import",
1672
+ bashExamples: "ls, grep, curl, git, pip, cat",
1673
+ codeBlockLang: "python",
1674
+ execToolSummary: "Run a given piece of Python code. Use when you already have code to run \u2014 e.g., from a previous write_code call, re-running a snippet, or executing user-provided code.",
1675
+ writeRunToolSummary: "Write a fresh Python solution from scratch and execute it in one step. The autonomous, end-to-end tool for solving problems with code."
1676
+ }
1677
+ };
1630
1678
  var EXECUTING_TOOLS = /* @__PURE__ */ new Set(["execute_code", "write_and_run_code", "run_bash"]);
1631
1679
  var CodeAgent = class extends base_default {
1632
1680
  /**
@@ -1638,6 +1686,8 @@ var CodeAgent = class extends base_default {
1638
1686
  }
1639
1687
  super(options);
1640
1688
  this.workingDirectory = options.workingDirectory || process.cwd();
1689
+ this.language = options.language || "javascript";
1690
+ this.pythonPath = options.pythonPath || null;
1641
1691
  this.maxRounds = options.maxRounds || 10;
1642
1692
  this.timeout = options.timeout || 3e4;
1643
1693
  this.onBeforeExecution = options.onBeforeExecution || null;
@@ -1649,6 +1699,18 @@ var CodeAgent = class extends base_default {
1649
1699
  this.maxRetries = options.maxRetries ?? 3;
1650
1700
  this.skills = options.skills || [];
1651
1701
  this.envOverview = options.envOverview || "";
1702
+ this._pythonBinary = null;
1703
+ this._venvPath = null;
1704
+ this._venvEnv = null;
1705
+ this.customTools = (options.tools || []).map((t) => ({
1706
+ name: t.name,
1707
+ description: t.description,
1708
+ parametersJsonSchema: t.parametersJsonSchema || t.parameters || t.input_schema || t.inputSchema
1709
+ }));
1710
+ this.toolExecutor = options.toolExecutor || null;
1711
+ if (this.customTools.length > 0 && !this.toolExecutor) {
1712
+ throw new Error("CodeAgent: tools provided without a toolExecutor.");
1713
+ }
1652
1714
  this._codebaseContext = null;
1653
1715
  this._contextGathered = false;
1654
1716
  this._stopped = false;
@@ -1668,6 +1730,7 @@ var CodeAgent = class extends base_default {
1668
1730
  * @returns {{ functionDeclarations: Array<Object> }}
1669
1731
  */
1670
1732
  _buildToolDefinitions() {
1733
+ const lang = LANG_CONFIG[this.language] || LANG_CONFIG.javascript;
1671
1734
  const declarations = [
1672
1735
  {
1673
1736
  name: "write_code",
@@ -1684,11 +1747,11 @@ var CodeAgent = class extends base_default {
1684
1747
  },
1685
1748
  {
1686
1749
  name: "execute_code",
1687
- description: "Execute a given piece of JavaScript code in a Node.js child process. Use this when you already have code to run \u2014 e.g., running code from a previous write_code call, re-running a snippet, or executing code the user provided. Use console.log() for output.",
1750
+ description: lang.toolDescExecute,
1688
1751
  parametersJsonSchema: {
1689
1752
  type: "object",
1690
1753
  properties: {
1691
- code: { type: "string", description: "JavaScript code to execute. Use console.log() for output. Use import syntax (ES modules)." },
1754
+ code: { type: "string", description: lang.codeParamDesc },
1692
1755
  purpose: { type: "string", description: 'A short 2-4 word slug describing what this script does (e.g., "read-config", "parse-logs").' }
1693
1756
  },
1694
1757
  required: ["code"]
@@ -1696,11 +1759,11 @@ var CodeAgent = class extends base_default {
1696
1759
  },
1697
1760
  {
1698
1761
  name: "write_and_run_code",
1699
- description: "Write a fresh solution from scratch and execute it in one step. Use this when you need to figure out the code AND run it \u2014 the autonomous, end-to-end tool for solving problems with code.",
1762
+ description: lang.toolDescWriteAndRun,
1700
1763
  parametersJsonSchema: {
1701
1764
  type: "object",
1702
1765
  properties: {
1703
- code: { type: "string", description: "JavaScript code to write and execute. Use console.log() for output. Use import syntax (ES modules)." },
1766
+ code: { type: "string", description: lang.codeParamDesc },
1704
1767
  purpose: { type: "string", description: 'A short 2-4 word slug describing what this script does (e.g., "fetch-api-data", "generate-report").' }
1705
1768
  },
1706
1769
  required: ["code"]
@@ -1722,7 +1785,7 @@ var CodeAgent = class extends base_default {
1722
1785
  },
1723
1786
  {
1724
1787
  name: "run_bash",
1725
- description: "Execute a shell command in the working directory. Use this for file operations, git commands, installing packages, or any shell task. Prefer this over execute_code for simple shell operations.",
1788
+ description: `Execute a shell command in the working directory. Use this for file operations, git commands, installing packages, or any shell task (e.g., ${lang.bashExamples}). Prefer this over execute_code for simple shell operations.`,
1726
1789
  parametersJsonSchema: {
1727
1790
  type: "object",
1728
1791
  properties: {
@@ -1746,6 +1809,9 @@ var CodeAgent = class extends base_default {
1746
1809
  }
1747
1810
  });
1748
1811
  }
1812
+ for (const t of this.customTools) {
1813
+ declarations.push({ name: t.name, description: t.description, parametersJsonSchema: t.parametersJsonSchema });
1814
+ }
1749
1815
  return { functionDeclarations: declarations };
1750
1816
  }
1751
1817
  // ── Init ─────────────────────────────────────────────────────────────────
@@ -1758,6 +1824,10 @@ var CodeAgent = class extends base_default {
1758
1824
  if (this.skills.length > 0 && (this._skillRegistry.size === 0 || force)) {
1759
1825
  await this._loadSkills();
1760
1826
  }
1827
+ if (this.language === "python" && (!this._pythonBinary || force)) {
1828
+ await this._resolvePython();
1829
+ await this._setupVenv();
1830
+ }
1761
1831
  this.chatConfig.tools = [this._buildToolDefinitions()];
1762
1832
  if (!this._contextGathered || force) {
1763
1833
  await this._gatherCodebaseContext();
@@ -1785,6 +1855,70 @@ var CodeAgent = class extends base_default {
1785
1855
  }
1786
1856
  }
1787
1857
  }
1858
+ // ── Python Resolution ───────────────────────────────────────────────────
1859
+ /**
1860
+ * Resolve the Python 3 binary path.
1861
+ * @private
1862
+ */
1863
+ async _resolvePython() {
1864
+ const tryBinary = (bin) => new Promise((resolve2) => {
1865
+ (0, import_node_child_process.execFile)(bin, ["--version"], { timeout: 5e3 }, (err, stdout, stderr) => {
1866
+ if (err) return resolve2(null);
1867
+ const output = (stdout || "") + (stderr || "");
1868
+ if (output.includes("Python 3.")) return resolve2(bin);
1869
+ resolve2(null);
1870
+ });
1871
+ });
1872
+ if (this.pythonPath) {
1873
+ const result = await tryBinary(this.pythonPath);
1874
+ if (result) {
1875
+ this._pythonBinary = result;
1876
+ return;
1877
+ }
1878
+ throw new Error(`CodeAgent: pythonPath "${this.pythonPath}" is not a valid Python 3 binary.`);
1879
+ }
1880
+ const python3 = await tryBinary("python3");
1881
+ if (python3) {
1882
+ this._pythonBinary = python3;
1883
+ return;
1884
+ }
1885
+ const python = await tryBinary("python");
1886
+ if (python) {
1887
+ this._pythonBinary = python;
1888
+ return;
1889
+ }
1890
+ throw new Error('CodeAgent: language is "python" but python3 was not found on PATH. Install Python 3 or provide the pythonPath option.');
1891
+ }
1892
+ /**
1893
+ * Create a virtual environment for Python execution.
1894
+ * @private
1895
+ */
1896
+ async _setupVenv() {
1897
+ await (0, import_promises2.mkdir)(this.writeDir, { recursive: true });
1898
+ this._venvPath = (0, import_node_path.join)(this.writeDir, ".venv");
1899
+ const isWin = process.platform === "win32";
1900
+ const venvBin = isWin ? (0, import_node_path.join)(this._venvPath, "Scripts") : (0, import_node_path.join)(this._venvPath, "bin");
1901
+ const venvPython = (0, import_node_path.join)(venvBin, isWin ? "python.exe" : "python");
1902
+ try {
1903
+ await (0, import_promises2.readFile)(venvPython);
1904
+ } catch {
1905
+ logger_default.debug(`Creating Python venv at ${this._venvPath}`);
1906
+ await new Promise((resolve2, reject) => {
1907
+ (0, import_node_child_process.execFile)(this._pythonBinary, ["-m", "venv", this._venvPath], {
1908
+ timeout: 3e4
1909
+ }, (err) => {
1910
+ if (err) return reject(new Error(`CodeAgent: failed to create venv: ${err.message}`));
1911
+ resolve2();
1912
+ });
1913
+ });
1914
+ }
1915
+ const env = Object.assign({}, process.env);
1916
+ env.VIRTUAL_ENV = this._venvPath;
1917
+ env.PATH = venvBin + (isWin ? ";" : ":") + (process.env.PATH || "");
1918
+ this._venvEnv = env;
1919
+ this._pythonBinary = venvPython;
1920
+ logger_default.debug(`Python venv ready at ${this._venvPath}`);
1921
+ }
1788
1922
  // ── Context Gathering ────────────────────────────────────────────────────
1789
1923
  /**
1790
1924
  * @private
@@ -1803,15 +1937,35 @@ var CodeAgent = class extends base_default {
1803
1937
  fileTree = `${truncated}
1804
1938
  ... (${lines.length - MAX_FILE_TREE_LINES} more files)`;
1805
1939
  }
1806
- let npmPackages = [];
1807
- try {
1808
- const pkgPath = (0, import_node_path.join)(this.workingDirectory, "package.json");
1809
- const pkg = JSON.parse(await (0, import_promises2.readFile)(pkgPath, "utf-8"));
1810
- npmPackages = [
1811
- ...Object.keys(pkg.dependencies || {}),
1812
- ...Object.keys(pkg.devDependencies || {})
1813
- ];
1814
- } catch {
1940
+ let packages = [];
1941
+ if (this.language === "python") {
1942
+ try {
1943
+ const reqPath = (0, import_node_path.join)(this.workingDirectory, "requirements.txt");
1944
+ const content = await (0, import_promises2.readFile)(reqPath, "utf-8");
1945
+ packages = content.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#") && !l.startsWith("-")).map((l) => l.split(/[>=<!\[;\s]/)[0]);
1946
+ } catch {
1947
+ }
1948
+ if (packages.length === 0) {
1949
+ try {
1950
+ const ppPath = (0, import_node_path.join)(this.workingDirectory, "pyproject.toml");
1951
+ const content = await (0, import_promises2.readFile)(ppPath, "utf-8");
1952
+ const depMatch = content.match(/dependencies\s*=\s*\[([\s\S]*?)\]/);
1953
+ if (depMatch) {
1954
+ packages = (depMatch[1].match(/"([^"]+)"/g) || []).map((s) => s.replace(/"/g, "").split(/[>=<!\[;\s]/)[0]);
1955
+ }
1956
+ } catch {
1957
+ }
1958
+ }
1959
+ } else {
1960
+ try {
1961
+ const pkgPath = (0, import_node_path.join)(this.workingDirectory, "package.json");
1962
+ const pkg = JSON.parse(await (0, import_promises2.readFile)(pkgPath, "utf-8"));
1963
+ packages = [
1964
+ ...Object.keys(pkg.dependencies || {}),
1965
+ ...Object.keys(pkg.devDependencies || {})
1966
+ ];
1967
+ } catch {
1968
+ }
1815
1969
  }
1816
1970
  const importantFileContents = [];
1817
1971
  if (this.importantFiles.length > 0) {
@@ -1823,7 +1977,7 @@ var CodeAgent = class extends base_default {
1823
1977
  continue;
1824
1978
  }
1825
1979
  try {
1826
- const fullPath = (0, import_node_path.join)(this.workingDirectory, resolved);
1980
+ const fullPath = (0, import_node_path.isAbsolute)(resolved) ? resolved : (0, import_node_path.join)(this.workingDirectory, resolved);
1827
1981
  const content = await (0, import_promises2.readFile)(fullPath, "utf-8");
1828
1982
  importantFileContents.push({ path: resolved, content });
1829
1983
  } catch (e) {
@@ -1831,13 +1985,14 @@ var CodeAgent = class extends base_default {
1831
1985
  }
1832
1986
  }
1833
1987
  }
1834
- this._codebaseContext = { fileTree, npmPackages, importantFileContents };
1988
+ this._codebaseContext = { fileTree, npmPackages: packages, packages, importantFileContents };
1835
1989
  this._contextGathered = true;
1836
1990
  }
1837
1991
  /**
1838
1992
  * @private
1839
1993
  */
1840
1994
  _resolveImportantFile(filename, fileTreeLines) {
1995
+ if ((0, import_node_path.isAbsolute)(filename)) return filename;
1841
1996
  const exact = fileTreeLines.find((line) => line === filename);
1842
1997
  if (exact) return exact;
1843
1998
  const partial = fileTreeLines.find(
@@ -1888,7 +2043,8 @@ var CodeAgent = class extends base_default {
1888
2043
  * @private
1889
2044
  */
1890
2045
  _buildSystemPrompt() {
1891
- const { fileTree, npmPackages, importantFileContents } = this._codebaseContext || { fileTree: "", npmPackages: [], importantFileContents: [] };
2046
+ const { fileTree, packages, importantFileContents } = this._codebaseContext || { fileTree: "", packages: [], importantFileContents: [] };
2047
+ const lang = LANG_CONFIG[this.language] || LANG_CONFIG.javascript;
1892
2048
  let prompt = `You are a coding agent working in ${this.workingDirectory}.
1893
2049
 
1894
2050
  ## Available Tools
@@ -1897,16 +2053,16 @@ var CodeAgent = class extends base_default {
1897
2053
  Output code without executing it. Use when showing, proposing, or presenting code to the user.
1898
2054
 
1899
2055
  ### execute_code
1900
- Run a given piece of JavaScript code. Use when you already have code to run \u2014 e.g., from a previous write_code call, re-running a snippet, or executing user-provided code.
2056
+ ${lang.execToolSummary}
1901
2057
 
1902
2058
  ### write_and_run_code
1903
- Write a fresh solution from scratch and execute it in one step. The autonomous, end-to-end tool for solving problems with code.
2059
+ ${lang.writeRunToolSummary}
1904
2060
 
1905
2061
  ### fix_code
1906
2062
  Fix broken code by providing original and fixed versions. Set execute=true to verify the fix works.
1907
2063
 
1908
2064
  ### run_bash
1909
- Run shell commands directly (e.g., ls, grep, curl, git, npm, cat). Prefer this over execute_code for simple shell operations.`;
2065
+ Run shell commands directly (e.g., ${lang.bashExamples}). Prefer this over execute_code for simple shell operations.`;
1910
2066
  if (this._skillRegistry.size > 0) {
1911
2067
  prompt += `
1912
2068
 
@@ -1918,24 +2074,14 @@ Load a skill by name to get detailed instructions and templates. Available skill
1918
2074
  ## Code Execution Rules
1919
2075
  These rules apply when using execute_code, write_and_run_code, or fix_code (with execute=true):
1920
2076
  - Always provide a short descriptive \`purpose\` parameter (2-4 word slug like "read-config")
1921
- - Your code runs in a Node.js child process with access to all built-in modules
1922
- - IMPORTANT: Your code runs as an ES module (.mjs). Use import syntax, NOT require():
1923
- - import fs from 'fs';
1924
- - import path from 'path';
1925
- - import { execSync } from 'child_process';
1926
- - Use console.log() to produce output \u2014 that's how results are returned to you
1927
- - Write efficient scripts that do multiple things per execution when possible
1928
- - For parallel async operations, use Promise.all()
1929
- - Handle errors in your scripts with try/catch so you get useful error messages
1930
- - Top-level await is supported
2077
+ ${lang.codeRules}
1931
2078
  - The working directory is: ${this.workingDirectory}`;
1932
2079
  if (this.comments) {
1933
2080
  prompt += `
1934
- - Add a JSDoc @fileoverview comment at the top of each script explaining what it does
1935
- - Add brief JSDoc @param comments for any functions you define`;
2081
+ ${lang.commentsEnabled}`;
1936
2082
  } else {
1937
2083
  prompt += `
1938
- - Do NOT write any comments in your code \u2014 save tokens. The code should be self-explanatory.`;
2084
+ ${lang.commentsDisabled}`;
1939
2085
  }
1940
2086
  if (fileTree) {
1941
2087
  prompt += `
@@ -1945,11 +2091,11 @@ These rules apply when using execute_code, write_and_run_code, or fix_code (with
1945
2091
  ${fileTree}
1946
2092
  \`\`\``;
1947
2093
  }
1948
- if (npmPackages.length > 0) {
2094
+ if (packages && packages.length > 0) {
1949
2095
  prompt += `
1950
2096
 
1951
- ## Available Packages
1952
- These npm packages are installed and can be imported: ${npmPackages.join(", ")}`;
2097
+ ## ${lang.packageLabel}
2098
+ ${lang.packageIntro}: ${packages.join(", ")}`;
1953
2099
  }
1954
2100
  if (importantFileContents && importantFileContents.length > 0) {
1955
2101
  prompt += `
@@ -1959,7 +2105,7 @@ These npm packages are installed and can be imported: ${npmPackages.join(", ")}`
1959
2105
  prompt += `
1960
2106
 
1961
2107
  ### ${filePath}
1962
- \`\`\`javascript
2108
+ \`\`\`${lang.codeBlockLang}
1963
2109
  ${content}
1964
2110
  \`\`\``;
1965
2111
  }
@@ -2005,14 +2151,17 @@ ${this.envOverview}`;
2005
2151
  }
2006
2152
  await (0, import_promises2.mkdir)(this.writeDir, { recursive: true });
2007
2153
  const slug = this._slugify(purpose);
2008
- const tempFile = (0, import_node_path.join)(this.writeDir, `agent-${slug}-${Date.now()}.mjs`);
2154
+ const ext = this.language === "python" ? ".py" : ".mjs";
2155
+ const tempFile = (0, import_node_path.join)(this.writeDir, `agent-${slug}-${Date.now()}${ext}`);
2009
2156
  try {
2010
2157
  await (0, import_promises2.writeFile)(tempFile, code, "utf-8");
2158
+ const binary = this.language === "python" ? this._pythonBinary : "node";
2159
+ const execEnv = this.language === "python" && this._venvEnv ? this._venvEnv : process.env;
2011
2160
  const result = await new Promise((resolve2) => {
2012
- const child = (0, import_node_child_process.execFile)("node", [tempFile], {
2161
+ const child = (0, import_node_child_process.execFile)(binary, [tempFile], {
2013
2162
  cwd: this.workingDirectory,
2014
2163
  timeout: this.timeout,
2015
- env: process.env,
2164
+ env: execEnv,
2016
2165
  maxBuffer: 10 * 1024 * 1024
2017
2166
  }, (err, stdout, stderr) => {
2018
2167
  this._activeProcess = null;
@@ -2083,11 +2232,12 @@ ${this.envOverview}`;
2083
2232
  logger_default.warn(`onBeforeExecution callback error: ${e.message}`);
2084
2233
  }
2085
2234
  }
2235
+ const execEnv = this.language === "python" && this._venvEnv ? this._venvEnv : process.env;
2086
2236
  const result = await new Promise((resolve2) => {
2087
2237
  const child = (0, import_node_child_process.execFile)("bash", ["-c", command], {
2088
2238
  cwd: this.workingDirectory,
2089
2239
  timeout: this.timeout,
2090
- env: process.env,
2240
+ env: execEnv,
2091
2241
  maxBuffer: 10 * 1024 * 1024
2092
2242
  }, (err, stdout, stderr) => {
2093
2243
  this._activeProcess = null;
@@ -2234,12 +2384,30 @@ ${this.envOverview}`;
2234
2384
  data: { tool: "use_skill", skillName: skill.name, content: skill.content, found: true }
2235
2385
  };
2236
2386
  }
2237
- default:
2387
+ default: {
2388
+ if (this.toolExecutor) {
2389
+ try {
2390
+ const result = await this.toolExecutor(name, input);
2391
+ const resultStr = typeof result === "string" ? result : JSON.stringify(result);
2392
+ return {
2393
+ output: resultStr,
2394
+ type: "tool",
2395
+ data: { tool: name, args: input, result }
2396
+ };
2397
+ } catch (err) {
2398
+ return {
2399
+ output: `Tool "${name}" failed: ${err.message}`,
2400
+ type: "tool",
2401
+ data: { tool: name, args: input, error: err.message }
2402
+ };
2403
+ }
2404
+ }
2238
2405
  return {
2239
2406
  output: `Unknown tool: ${name}`,
2240
2407
  type: "unknown",
2241
2408
  data: { tool: name }
2242
2409
  };
2410
+ }
2243
2411
  }
2244
2412
  }
2245
2413
  // ── Non-Streaming Chat ───────────────────────────────────────────────────
@@ -2396,6 +2564,9 @@ ${this.envOverview}`;
2396
2564
  if (toolName === "use_skill") {
2397
2565
  yield { type: "skill", skillName: data.skillName, content: data.content, found: data.found };
2398
2566
  }
2567
+ if (type === "tool") {
2568
+ yield { type: "tool", toolName, args: data.args, result: data.result, error: data.error };
2569
+ }
2399
2570
  const isExecutingTool = EXECUTING_TOOLS.has(toolName) || toolName === "fix_code" && toolInput.execute;
2400
2571
  if (isExecutingTool) {
2401
2572
  if (data.exitCode !== 0 && !data.denied) {
@@ -2446,8 +2617,9 @@ ${this.envOverview}`;
2446
2617
  * @returns {Array<{fileName: string, purpose: string|null, script: string, filePath: string|null, tool: string}>}
2447
2618
  */
2448
2619
  dump() {
2620
+ const ext = this.language === "python" ? ".py" : ".mjs";
2449
2621
  return this._allExecutions.map((exec, i) => ({
2450
- fileName: exec.purpose ? `agent-${exec.purpose}.mjs` : `script-${i + 1}.mjs`,
2622
+ fileName: exec.purpose ? `agent-${exec.purpose}${ext}` : `script-${i + 1}${ext}`,
2451
2623
  purpose: exec.purpose || null,
2452
2624
  script: exec.code,
2453
2625
  filePath: exec.filePath || null,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "ak-gemini",
3
3
  "author": "ak@mixpanel.com",
4
4
  "description": "AK's Generative AI Helper for doing... everything",
5
- "version": "2.1.3",
5
+ "version": "2.2.1",
6
6
  "main": "index.js",
7
7
  "files": [
8
8
  "index.js",
package/types.d.ts CHANGED
@@ -302,6 +302,10 @@ export interface RagAgentOptions extends BaseGeminiOptions {
302
302
  export interface CodeAgentOptions extends BaseGeminiOptions {
303
303
  /** Working directory for code execution (default: process.cwd()) */
304
304
  workingDirectory?: string;
305
+ /** Programming language for code execution: 'javascript' (default) or 'python' */
306
+ language?: 'javascript' | 'python';
307
+ /** Path to the Python binary (only used when language is 'python'; default: auto-detect python3/python) */
308
+ pythonPath?: string;
305
309
  /** Max code execution loop iterations (default: 10) */
306
310
  maxRounds?: number;
307
311
  /** Per-execution timeout in milliseconds (default: 30000) */
@@ -324,6 +328,17 @@ export interface CodeAgentOptions extends BaseGeminiOptions {
324
328
  skills?: string[];
325
329
  /** Plain text environment overview appended to the system prompt — describe the project, stack, conventions, etc. */
326
330
  envOverview?: string;
331
+ /** Custom tool declarations to add alongside built-in CodeAgent tools. Accepts Gemini, Claude, or OpenAI tool formats (auto-mapped). */
332
+ tools?: Array<{
333
+ name: string;
334
+ description: string;
335
+ parametersJsonSchema?: any;
336
+ parameters?: any;
337
+ input_schema?: any;
338
+ inputSchema?: any;
339
+ }>;
340
+ /** Function to execute custom tool calls: (toolName, args) => result */
341
+ toolExecutor?: (toolName: string, args: Record<string, any>) => Promise<any>;
327
342
  }
328
343
 
329
344
  export interface CodeExecution {
@@ -340,7 +355,7 @@ export interface CodeExecution {
340
355
  }
341
356
 
342
357
  export interface ToolCallResult {
343
- tool: 'write_code' | 'execute_code' | 'write_and_run_code' | 'fix_code' | 'run_bash' | 'use_skill';
358
+ tool: 'write_code' | 'execute_code' | 'write_and_run_code' | 'fix_code' | 'run_bash' | 'use_skill' | string;
344
359
  code?: string;
345
360
  purpose?: string;
346
361
  language?: string;
@@ -370,7 +385,7 @@ export interface CodeAgentResponse {
370
385
  }
371
386
 
372
387
  export interface CodeAgentStreamEvent {
373
- type: 'text' | 'code' | 'output' | 'write' | 'fix' | 'bash' | 'skill' | 'done';
388
+ type: 'text' | 'code' | 'output' | 'write' | 'fix' | 'bash' | 'skill' | 'tool' | 'done';
374
389
  text?: string;
375
390
  code?: string;
376
391
  stdout?: string;
@@ -390,6 +405,14 @@ export interface CodeAgentStreamEvent {
390
405
  skillName?: string;
391
406
  content?: string;
392
407
  found?: boolean;
408
+ /** custom tool: tool name */
409
+ toolName?: string;
410
+ /** custom tool: arguments passed */
411
+ args?: Record<string, any>;
412
+ /** custom tool: result returned */
413
+ result?: any;
414
+ /** custom tool: error message (if failed) */
415
+ error?: string;
393
416
  }
394
417
 
395
418
  // ── Per-Message Options ──────────────────────────────────────────────────────
@@ -614,6 +637,8 @@ export declare class CodeAgent extends BaseGemini {
614
637
  constructor(options?: CodeAgentOptions);
615
638
 
616
639
  workingDirectory: string;
640
+ language: 'javascript' | 'python';
641
+ pythonPath: string | null;
617
642
  maxRounds: number;
618
643
  timeout: number;
619
644
  onBeforeExecution: ((content: string, toolName: string) => Promise<boolean> | boolean) | null;
@@ -625,6 +650,8 @@ export declare class CodeAgent extends BaseGemini {
625
650
  maxRetries: number;
626
651
  skills: string[];
627
652
  envOverview: string;
653
+ customTools: Array<{ name: string; description: string; parametersJsonSchema: any }>;
654
+ toolExecutor: ((toolName: string, args: Record<string, any>) => Promise<any>) | null;
628
655
 
629
656
  init(force?: boolean): Promise<void>;
630
657
  chat(message: string, opts?: { labels?: Record<string, string> }): Promise<CodeAgentResponse>;