ak-gemini 2.0.7 → 2.1.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.
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.0.7",
5
+ "version": "2.1.1",
6
6
  "main": "index.js",
7
7
  "files": [
8
8
  "index.js",
package/tool-agent.js CHANGED
@@ -13,6 +13,31 @@ import log from './logger.js';
13
13
  * @typedef {import('./types').AgentStreamEvent} AgentStreamEvent
14
14
  */
15
15
 
16
+ /**
17
+ * Execute async task factories with a concurrency limit.
18
+ * @param {Array<() => Promise<any>>} tasks
19
+ * @param {number} concurrency - Infinity for unlimited, 1 for sequential
20
+ * @returns {Promise<any[]>} Results in same order as tasks
21
+ */
22
+ async function runWithConcurrency(tasks, concurrency) {
23
+ if (concurrency === Infinity) return Promise.all(tasks.map(t => t()));
24
+ if (concurrency === 1) {
25
+ const results = [];
26
+ for (const t of tasks) results.push(await t());
27
+ return results;
28
+ }
29
+ const results = new Array(tasks.length);
30
+ let next = 0;
31
+ async function worker() {
32
+ while (next < tasks.length) {
33
+ const i = next++;
34
+ results[i] = await tasks[i]();
35
+ }
36
+ }
37
+ await Promise.all(Array.from({ length: Math.min(concurrency, tasks.length) }, () => worker()));
38
+ return results;
39
+ }
40
+
16
41
  /**
17
42
  * AI agent that uses user-provided tools to accomplish tasks.
18
43
  * Automatically manages the tool-use loop: when the model decides to call
@@ -75,6 +100,13 @@ class ToolAgent extends BaseGemini {
75
100
  throw new Error("ToolAgent: toolExecutor provided without tools. Provide tool declarations so the model knows what tools are available.");
76
101
  }
77
102
 
103
+ // ── Parallel execution ──
104
+ this.parallelToolCalls = options.parallelToolCalls ?? true;
105
+ /** @private */
106
+ this._concurrency = this.parallelToolCalls === true ? Infinity
107
+ : this.parallelToolCalls === false ? 1
108
+ : this.parallelToolCalls;
109
+
78
110
  // ── Tool loop config ──
79
111
  this.maxToolRounds = options.maxToolRounds || 10;
80
112
  this.onToolCall = options.onToolCall || null;
@@ -116,41 +148,39 @@ class ToolAgent extends BaseGemini {
116
148
  const functionCalls = response.functionCalls;
117
149
  if (!functionCalls || functionCalls.length === 0) break;
118
150
 
119
- const toolResults = await Promise.all(
120
- functionCalls.map(async (call) => {
121
- // Fire onToolCall callback
122
- if (this.onToolCall) {
123
- try { this.onToolCall(call.name, call.args); }
124
- catch (e) { log.warn(`onToolCall callback error: ${e.message}`); }
125
- }
151
+ const tasks = functionCalls.map(call => async () => {
152
+ // Fire onToolCall callback
153
+ if (this.onToolCall) {
154
+ try { this.onToolCall(call.name, call.args); }
155
+ catch (e) { log.warn(`onToolCall callback error: ${e.message}`); }
156
+ }
126
157
 
127
- // Check onBeforeExecution gate
128
- if (this.onBeforeExecution) {
129
- try {
130
- const allowed = await this.onBeforeExecution(call.name, call.args);
131
- if (allowed === false) {
132
- const result = { error: 'Execution denied by onBeforeExecution callback' };
133
- allToolCalls.push({ name: call.name, args: call.args, result });
134
- return { id: call.id, name: call.name, result };
135
- }
136
- } catch (e) {
137
- log.warn(`onBeforeExecution callback error: ${e.message}`);
158
+ // Check onBeforeExecution gate
159
+ if (this.onBeforeExecution) {
160
+ try {
161
+ const allowed = await this.onBeforeExecution(call.name, call.args);
162
+ if (allowed === false) {
163
+ const result = { error: 'Execution denied by onBeforeExecution callback' };
164
+ return { id: call.id, name: call.name, args: call.args, result };
138
165
  }
166
+ } catch (e) {
167
+ log.warn(`onBeforeExecution callback error: ${e.message}`);
139
168
  }
169
+ }
140
170
 
141
- let result;
142
- try {
143
- result = await this.toolExecutor(call.name, call.args);
144
- } catch (err) {
145
- log.warn(`Tool ${call.name} failed: ${err.message}`);
146
- result = { error: err.message };
147
- }
171
+ let result;
172
+ try {
173
+ result = await this.toolExecutor(call.name, call.args);
174
+ } catch (err) {
175
+ log.warn(`Tool ${call.name} failed: ${err.message}`);
176
+ result = { error: err.message };
177
+ }
148
178
 
149
- allToolCalls.push({ name: call.name, args: call.args, result });
179
+ return { id: call.id, name: call.name, args: call.args, result };
180
+ });
150
181
 
151
- return { id: call.id, name: call.name, result };
152
- })
153
- );
182
+ const toolResults = await runWithConcurrency(tasks, this._concurrency);
183
+ for (const r of toolResults) allToolCalls.push({ name: r.name, args: r.args, result: r.result });
154
184
 
155
185
  // Send function responses back to the model
156
186
  response = await this._withRetry(() => this.chatSession.sendMessage({
@@ -234,46 +264,90 @@ class ToolAgent extends BaseGemini {
234
264
  return;
235
265
  }
236
266
 
237
- // Execute tools sequentially so we can yield events
267
+ // Execute tools and yield events
238
268
  const toolResults = [];
239
- for (const call of functionCalls) {
240
- if (this._stopped) break;
269
+ if (this._concurrency === 1) {
270
+ // Sequential: yield tool_call, execute, yield tool_result for each
271
+ for (const call of functionCalls) {
272
+ if (this._stopped) break;
241
273
 
242
- yield { type: 'tool_call', toolName: call.name, args: call.args };
274
+ yield { type: 'tool_call', toolName: call.name, args: call.args };
243
275
 
244
- // Fire onToolCall callback
245
- if (this.onToolCall) {
246
- try { this.onToolCall(call.name, call.args); }
247
- catch (e) { log.warn(`onToolCall callback error: ${e.message}`); }
248
- }
276
+ if (this.onToolCall) {
277
+ try { this.onToolCall(call.name, call.args); }
278
+ catch (e) { log.warn(`onToolCall callback error: ${e.message}`); }
279
+ }
249
280
 
250
- // Check onBeforeExecution gate
251
- let denied = false;
252
- if (this.onBeforeExecution) {
253
- try {
254
- const allowed = await this.onBeforeExecution(call.name, call.args);
255
- if (allowed === false) denied = true;
256
- } catch (e) {
257
- log.warn(`onBeforeExecution callback error: ${e.message}`);
281
+ let denied = false;
282
+ if (this.onBeforeExecution) {
283
+ try {
284
+ const allowed = await this.onBeforeExecution(call.name, call.args);
285
+ if (allowed === false) denied = true;
286
+ } catch (e) {
287
+ log.warn(`onBeforeExecution callback error: ${e.message}`);
288
+ }
258
289
  }
259
- }
260
290
 
261
- let result;
262
- if (denied) {
263
- result = { error: 'Execution denied by onBeforeExecution callback' };
264
- } else {
265
- try {
266
- result = await this.toolExecutor(call.name, call.args);
267
- } catch (err) {
268
- log.warn(`Tool ${call.name} failed: ${err.message}`);
269
- result = { error: err.message };
291
+ let result;
292
+ if (denied) {
293
+ result = { error: 'Execution denied by onBeforeExecution callback' };
294
+ } else {
295
+ try {
296
+ result = await this.toolExecutor(call.name, call.args);
297
+ } catch (err) {
298
+ log.warn(`Tool ${call.name} failed: ${err.message}`);
299
+ result = { error: err.message };
300
+ }
270
301
  }
302
+
303
+ allToolCalls.push({ name: call.name, args: call.args, result });
304
+ yield { type: 'tool_result', toolName: call.name, result };
305
+
306
+ toolResults.push({ id: call.id, name: call.name, result });
307
+ }
308
+ } else {
309
+ // Parallel: yield all tool_call events, execute all, yield all tool_result events
310
+ for (const call of functionCalls) {
311
+ yield { type: 'tool_call', toolName: call.name, args: call.args };
271
312
  }
272
313
 
273
- allToolCalls.push({ name: call.name, args: call.args, result });
274
- yield { type: 'tool_result', toolName: call.name, result };
314
+ const tasks = functionCalls.map(call => async () => {
315
+ if (this.onToolCall) {
316
+ try { this.onToolCall(call.name, call.args); }
317
+ catch (e) { log.warn(`onToolCall callback error: ${e.message}`); }
318
+ }
275
319
 
276
- toolResults.push({ id: call.id, name: call.name, result });
320
+ let denied = false;
321
+ if (this.onBeforeExecution) {
322
+ try {
323
+ const allowed = await this.onBeforeExecution(call.name, call.args);
324
+ if (allowed === false) denied = true;
325
+ } catch (e) {
326
+ log.warn(`onBeforeExecution callback error: ${e.message}`);
327
+ }
328
+ }
329
+
330
+ let result;
331
+ if (denied) {
332
+ result = { error: 'Execution denied by onBeforeExecution callback' };
333
+ } else {
334
+ try {
335
+ result = await this.toolExecutor(call.name, call.args);
336
+ } catch (err) {
337
+ log.warn(`Tool ${call.name} failed: ${err.message}`);
338
+ result = { error: err.message };
339
+ }
340
+ }
341
+
342
+ return { id: call.id, name: call.name, args: call.args, result };
343
+ });
344
+
345
+ const results = await runWithConcurrency(tasks, this._concurrency);
346
+ for (const r of results) {
347
+ allToolCalls.push({ name: r.name, args: r.args, result: r.result });
348
+ yield { type: 'tool_result', toolName: r.name, result: r.result };
349
+ toolResults.push({ id: r.id, name: r.name, result: r.result });
350
+ }
277
351
  }
278
352
 
279
353
  // Send function responses back and get next stream
package/types.d.ts CHANGED
@@ -279,6 +279,8 @@ export interface ToolAgentOptions extends BaseGeminiOptions {
279
279
  onBeforeExecution?: (toolName: string, args: Record<string, any>) => Promise<boolean>;
280
280
  /** Directory for tool-written files (pass-through for toolExecutor use) */
281
281
  writeDir?: string;
282
+ /** Parallel tool execution: false = sequential, true = unlimited parallel, number = concurrency limit (default: true) */
283
+ parallelToolCalls?: boolean | number;
282
284
  }
283
285
 
284
286
  export interface LocalDataEntry {
@@ -304,9 +306,9 @@ export interface CodeAgentOptions extends BaseGeminiOptions {
304
306
  maxRounds?: number;
305
307
  /** Per-execution timeout in milliseconds (default: 30000) */
306
308
  timeout?: number;
307
- /** Async callback before code execution; return false to deny */
308
- onBeforeExecution?: (code: string) => Promise<boolean>;
309
- /** Notification callback after code execution */
309
+ /** Async callback before code/bash execution; return false to deny. Receives (content, toolName). */
310
+ onBeforeExecution?: (content: string, toolName: string) => Promise<boolean> | boolean;
311
+ /** Notification callback after code/bash execution */
310
312
  onCodeExecution?: (code: string, output: { stdout: string; stderr: string; exitCode: number }) => void;
311
313
  /** Files whose contents are included in the system prompt for project context */
312
314
  importantFiles?: string[];
@@ -318,6 +320,10 @@ export interface CodeAgentOptions extends BaseGeminiOptions {
318
320
  comments?: boolean;
319
321
  /** Max consecutive failed executions before stopping (default: 3) */
320
322
  maxRetries?: number;
323
+ /** Paths to skill files (markdown) loaded dynamically via the use_skill tool */
324
+ skills?: string[];
325
+ /** Plain text environment overview appended to the system prompt — describe the project, stack, conventions, etc. */
326
+ envOverview?: string;
321
327
  }
322
328
 
323
329
  export interface CodeExecution {
@@ -333,35 +339,57 @@ export interface CodeExecution {
333
339
  exitCode: number;
334
340
  }
335
341
 
342
+ export interface ToolCallResult {
343
+ tool: 'write_code' | 'execute_code' | 'write_and_run_code' | 'fix_code' | 'run_bash' | 'use_skill';
344
+ code?: string;
345
+ purpose?: string;
346
+ language?: string;
347
+ originalCode?: string;
348
+ fixedCode?: string;
349
+ explanation?: string;
350
+ executed?: boolean;
351
+ command?: string;
352
+ skillName?: string;
353
+ content?: string;
354
+ found?: boolean;
355
+ stdout?: string;
356
+ stderr?: string;
357
+ exitCode?: number;
358
+ denied?: boolean;
359
+ }
360
+
336
361
  export interface CodeAgentResponse {
337
362
  /** The agent's final text response */
338
363
  text: string;
339
- /** All code executions during this interaction */
364
+ /** Backward-compatible: only code executions (execute_code, write_and_run_code, fix_code with execute) */
340
365
  codeExecutions: CodeExecution[];
366
+ /** All tool calls made during this chat turn */
367
+ toolCalls: ToolCallResult[];
341
368
  /** Token usage data */
342
369
  usage: UsageData | null;
343
370
  }
344
371
 
345
372
  export interface CodeAgentStreamEvent {
346
- type: 'text' | 'code' | 'output' | 'done';
347
- /** For 'text' events: the text chunk */
373
+ type: 'text' | 'code' | 'output' | 'write' | 'fix' | 'bash' | 'skill' | 'done';
348
374
  text?: string;
349
- /** For 'code' events: the code about to be executed */
350
375
  code?: string;
351
- /** For 'output' events: stdout from execution */
352
376
  stdout?: string;
353
- /** For 'output' events: stderr from execution */
354
377
  stderr?: string;
355
- /** For 'output' events: process exit code */
356
378
  exitCode?: number;
357
- /** For 'done' events: complete accumulated text */
358
379
  fullText?: string;
359
- /** For 'done' events: all code executions */
360
380
  codeExecutions?: CodeExecution[];
361
- /** For 'done' events: token usage */
381
+ toolCalls?: ToolCallResult[];
362
382
  usage?: UsageData | null;
363
- /** For 'done' events: e.g. "Max tool rounds reached" or "Agent was stopped" */
364
383
  warning?: string;
384
+ purpose?: string;
385
+ language?: string;
386
+ originalCode?: string;
387
+ fixedCode?: string;
388
+ explanation?: string;
389
+ command?: string;
390
+ skillName?: string;
391
+ content?: string;
392
+ found?: boolean;
365
393
  }
366
394
 
367
395
  // ── Per-Message Options ──────────────────────────────────────────────────────
@@ -539,6 +567,7 @@ export declare class ToolAgent extends BaseGemini {
539
567
  onBeforeExecution: ((toolName: string, args: Record<string, any>) => Promise<boolean>) | null;
540
568
  /** Directory for tool-written files (pass-through for toolExecutor use) */
541
569
  writeDir: string | null;
570
+ parallelToolCalls: boolean | number;
542
571
 
543
572
  chat(message: string, opts?: { labels?: Record<string, string> }): Promise<AgentResponse>;
544
573
  stream(message: string, opts?: { labels?: Record<string, string> }): AsyncGenerator<AgentStreamEvent, void, unknown>;
@@ -579,25 +608,20 @@ export declare class CodeAgent extends BaseGemini {
579
608
  workingDirectory: string;
580
609
  maxRounds: number;
581
610
  timeout: number;
582
- onBeforeExecution: ((code: string) => Promise<boolean>) | null;
611
+ onBeforeExecution: ((content: string, toolName: string) => Promise<boolean> | boolean) | null;
583
612
  onCodeExecution: ((code: string, output: { stdout: string; stderr: string; exitCode: number }) => void) | null;
584
- /** Files whose contents are included in the system prompt */
585
613
  importantFiles: string[];
586
- /** Directory for writing script files */
587
614
  writeDir: string;
588
- /** Keep script files on disk after execution */
589
615
  keepArtifacts: boolean;
590
- /** Whether the model writes comments in generated code */
591
616
  comments: boolean;
592
- /** Max consecutive failed executions before stopping */
593
617
  maxRetries: number;
618
+ skills: string[];
619
+ envOverview: string;
594
620
 
595
621
  init(force?: boolean): Promise<void>;
596
622
  chat(message: string, opts?: { labels?: Record<string, string> }): Promise<CodeAgentResponse>;
597
623
  stream(message: string, opts?: { labels?: Record<string, string> }): AsyncGenerator<CodeAgentStreamEvent, void, unknown>;
598
- /** Returns all code scripts written across all chat/stream calls. */
599
- dump(): Array<{ fileName: string; purpose: string | null; script: string; filePath: string | null }>;
600
- /** Stop the agent before the next code execution. Kills any running child process. */
624
+ dump(): Array<{ fileName: string; purpose: string | null; script: string; filePath: string | null; tool: string }>;
601
625
  stop(): void;
602
626
  }
603
627