ak-claude 0.0.2 → 0.0.5

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/GUIDE.md CHANGED
@@ -430,6 +430,22 @@ const agent = new ToolAgent({
430
430
  });
431
431
  ```
432
432
 
433
+ ### Parallel Tool Execution
434
+
435
+ Control whether tool calls within a round execute in parallel or sequentially:
436
+
437
+ ```javascript
438
+ const agent = new ToolAgent({
439
+ tools: [...],
440
+ toolExecutor: myExecutor,
441
+ parallelToolCalls: true, // default: unlimited parallel execution
442
+ // parallelToolCalls: false, // sequential execution
443
+ // parallelToolCalls: 3, // max 3 concurrent tool executions
444
+ });
445
+ ```
446
+
447
+ When the model returns multiple tool calls in a single response, parallel execution runs them concurrently — significantly faster for I/O-bound tools (HTTP requests, database queries, etc.).
448
+
433
449
  ### Streaming
434
450
 
435
451
  Stream the agent's output in real-time --- useful for showing progress in a UI:
package/README.md CHANGED
@@ -561,6 +561,7 @@ All classes (except AgentQuery) accept `BaseClaudeOptions`:
561
561
  | `onBeforeExecution` | function | — | `async (toolName, args) => boolean` — gate execution |
562
562
  | `toolChoice` | object | — | Tool choice config (`auto`, `any`, `tool`, `none`) |
563
563
  | `disableParallelToolUse` | boolean | `false` | Force sequential tool calls |
564
+ | `parallelToolCalls` | boolean \| number | `true` | Parallel tool execution: `false` = sequential, `true` = unlimited, number = concurrency limit |
564
565
 
565
566
  ### CodeAgent-Specific
566
567
 
package/index.cjs CHANGED
@@ -1280,7 +1280,7 @@ No markdown code blocks, no preamble text.`;
1280
1280
  };
1281
1281
  if (this._isStructured) {
1282
1282
  try {
1283
- if (this._responseSchema) {
1283
+ if (this._responseSchema && !this.vertexai) {
1284
1284
  result.data = JSON.parse(text);
1285
1285
  } else {
1286
1286
  result.data = extractJSON(text);
@@ -1309,6 +1309,24 @@ No markdown code blocks, no preamble text.`;
1309
1309
  var message_default = Message;
1310
1310
 
1311
1311
  // tool-agent.js
1312
+ async function runWithConcurrency(tasks, concurrency) {
1313
+ if (concurrency === Infinity) return Promise.all(tasks.map((t) => t()));
1314
+ if (concurrency === 1) {
1315
+ const results2 = [];
1316
+ for (const t of tasks) results2.push(await t());
1317
+ return results2;
1318
+ }
1319
+ const results = new Array(tasks.length);
1320
+ let next = 0;
1321
+ async function worker() {
1322
+ while (next < tasks.length) {
1323
+ const i = next++;
1324
+ results[i] = await tasks[i]();
1325
+ }
1326
+ }
1327
+ await Promise.all(Array.from({ length: Math.min(concurrency, tasks.length) }, () => worker()));
1328
+ return results;
1329
+ }
1312
1330
  var ToolAgent = class extends base_default {
1313
1331
  /**
1314
1332
  * @param {ToolAgentOptions} [options={}]
@@ -1332,6 +1350,8 @@ var ToolAgent = class extends base_default {
1332
1350
  }
1333
1351
  this.toolChoice = options.toolChoice ?? void 0;
1334
1352
  this.disableParallelToolUse = options.disableParallelToolUse ?? false;
1353
+ this.parallelToolCalls = options.parallelToolCalls ?? true;
1354
+ this._concurrency = this.parallelToolCalls === true ? Infinity : this.parallelToolCalls === false ? 1 : this.parallelToolCalls;
1335
1355
  this.maxToolRounds = options.maxToolRounds || 10;
1336
1356
  this.onToolCall = options.onToolCall || null;
1337
1357
  this.onBeforeExecution = options.onBeforeExecution || null;
@@ -1373,8 +1393,7 @@ var ToolAgent = class extends base_default {
1373
1393
  if (response.stop_reason !== "tool_use") break;
1374
1394
  const toolUseBlocks = response.content.filter((b) => b.type === "tool_use");
1375
1395
  if (toolUseBlocks.length === 0) break;
1376
- const toolResults = [];
1377
- for (const block of toolUseBlocks) {
1396
+ const tasks = toolUseBlocks.map((block) => async () => {
1378
1397
  if (this.onToolCall) {
1379
1398
  try {
1380
1399
  this.onToolCall(block.name, block.input);
@@ -1387,13 +1406,7 @@ var ToolAgent = class extends base_default {
1387
1406
  const allowed = await this.onBeforeExecution(block.name, block.input);
1388
1407
  if (allowed === false) {
1389
1408
  const result2 = { error: "Execution denied by onBeforeExecution callback" };
1390
- allToolCalls.push({ name: block.name, args: block.input, result: result2 });
1391
- toolResults.push({
1392
- type: "tool_result",
1393
- tool_use_id: block.id,
1394
- content: JSON.stringify(result2)
1395
- });
1396
- continue;
1409
+ return { toolCall: { name: block.name, args: block.input, result: result2 }, toolResult: { type: "tool_result", tool_use_id: block.id, content: JSON.stringify(result2) } };
1397
1410
  }
1398
1411
  } catch (e) {
1399
1412
  logger_default.warn(`onBeforeExecution callback error: ${e.message}`);
@@ -1406,13 +1419,14 @@ var ToolAgent = class extends base_default {
1406
1419
  logger_default.warn(`Tool ${block.name} failed: ${err.message}`);
1407
1420
  result = { error: err.message };
1408
1421
  }
1409
- allToolCalls.push({ name: block.name, args: block.input, result });
1410
- toolResults.push({
1411
- type: "tool_result",
1412
- tool_use_id: block.id,
1413
- content: typeof result === "string" ? result : JSON.stringify(result)
1414
- });
1415
- }
1422
+ return {
1423
+ toolCall: { name: block.name, args: block.input, result },
1424
+ toolResult: { type: "tool_result", tool_use_id: block.id, content: typeof result === "string" ? result : JSON.stringify(result) }
1425
+ };
1426
+ });
1427
+ const results = await runWithConcurrency(tasks, this._concurrency);
1428
+ const toolResults = results.map((r) => r.toolResult);
1429
+ for (const r of results) allToolCalls.push(r.toolCall);
1416
1430
  response = await this._sendMessage(toolResults, { tools: this.tools, ...toolChoice && { tool_choice: toolChoice } });
1417
1431
  }
1418
1432
  this._cumulativeUsage = {
@@ -1472,43 +1486,88 @@ var ToolAgent = class extends base_default {
1472
1486
  return;
1473
1487
  }
1474
1488
  const toolResults = [];
1475
- for (const block of toolUseBlocks) {
1476
- if (this._stopped) break;
1477
- yield { type: "tool_call", toolName: block.name, args: block.input };
1478
- if (this.onToolCall) {
1479
- try {
1480
- this.onToolCall(block.name, block.input);
1481
- } catch (e) {
1482
- logger_default.warn(`onToolCall callback error: ${e.message}`);
1489
+ if (this._concurrency === 1) {
1490
+ for (const block of toolUseBlocks) {
1491
+ if (this._stopped) break;
1492
+ yield { type: "tool_call", toolName: block.name, args: block.input };
1493
+ if (this.onToolCall) {
1494
+ try {
1495
+ this.onToolCall(block.name, block.input);
1496
+ } catch (e) {
1497
+ logger_default.warn(`onToolCall callback error: ${e.message}`);
1498
+ }
1483
1499
  }
1484
- }
1485
- let denied = false;
1486
- if (this.onBeforeExecution) {
1487
- try {
1488
- const allowed = await this.onBeforeExecution(block.name, block.input);
1489
- if (allowed === false) denied = true;
1490
- } catch (e) {
1491
- logger_default.warn(`onBeforeExecution callback error: ${e.message}`);
1500
+ let denied = false;
1501
+ if (this.onBeforeExecution) {
1502
+ try {
1503
+ const allowed = await this.onBeforeExecution(block.name, block.input);
1504
+ if (allowed === false) denied = true;
1505
+ } catch (e) {
1506
+ logger_default.warn(`onBeforeExecution callback error: ${e.message}`);
1507
+ }
1492
1508
  }
1493
- }
1494
- let result;
1495
- if (denied) {
1496
- result = { error: "Execution denied by onBeforeExecution callback" };
1497
- } else {
1498
- try {
1499
- result = await this.toolExecutor(block.name, block.input);
1500
- } catch (err) {
1501
- logger_default.warn(`Tool ${block.name} failed: ${err.message}`);
1502
- result = { error: err.message };
1509
+ let result;
1510
+ if (denied) {
1511
+ result = { error: "Execution denied by onBeforeExecution callback" };
1512
+ } else {
1513
+ try {
1514
+ result = await this.toolExecutor(block.name, block.input);
1515
+ } catch (err) {
1516
+ logger_default.warn(`Tool ${block.name} failed: ${err.message}`);
1517
+ result = { error: err.message };
1518
+ }
1503
1519
  }
1520
+ allToolCalls.push({ name: block.name, args: block.input, result });
1521
+ yield { type: "tool_result", toolName: block.name, result };
1522
+ toolResults.push({
1523
+ type: "tool_result",
1524
+ tool_use_id: block.id,
1525
+ content: typeof result === "string" ? result : JSON.stringify(result)
1526
+ });
1504
1527
  }
1505
- allToolCalls.push({ name: block.name, args: block.input, result });
1506
- yield { type: "tool_result", toolName: block.name, result };
1507
- toolResults.push({
1508
- type: "tool_result",
1509
- tool_use_id: block.id,
1510
- content: typeof result === "string" ? result : JSON.stringify(result)
1528
+ } else {
1529
+ for (const block of toolUseBlocks) {
1530
+ yield { type: "tool_call", toolName: block.name, args: block.input };
1531
+ }
1532
+ const tasks = toolUseBlocks.map((block) => async () => {
1533
+ if (this.onToolCall) {
1534
+ try {
1535
+ this.onToolCall(block.name, block.input);
1536
+ } catch (e) {
1537
+ logger_default.warn(`onToolCall callback error: ${e.message}`);
1538
+ }
1539
+ }
1540
+ let denied = false;
1541
+ if (this.onBeforeExecution) {
1542
+ try {
1543
+ const allowed = await this.onBeforeExecution(block.name, block.input);
1544
+ if (allowed === false) denied = true;
1545
+ } catch (e) {
1546
+ logger_default.warn(`onBeforeExecution callback error: ${e.message}`);
1547
+ }
1548
+ }
1549
+ let result;
1550
+ if (denied) {
1551
+ result = { error: "Execution denied by onBeforeExecution callback" };
1552
+ } else {
1553
+ try {
1554
+ result = await this.toolExecutor(block.name, block.input);
1555
+ } catch (err) {
1556
+ logger_default.warn(`Tool ${block.name} failed: ${err.message}`);
1557
+ result = { error: err.message };
1558
+ }
1559
+ }
1560
+ return {
1561
+ toolCall: { name: block.name, args: block.input, result },
1562
+ toolResult: { type: "tool_result", tool_use_id: block.id, content: typeof result === "string" ? result : JSON.stringify(result) }
1563
+ };
1511
1564
  });
1565
+ const results = await runWithConcurrency(tasks, this._concurrency);
1566
+ for (const r of results) {
1567
+ allToolCalls.push(r.toolCall);
1568
+ yield { type: "tool_result", toolName: r.toolCall.name, result: r.toolCall.result };
1569
+ toolResults.push(r.toolResult);
1570
+ }
1512
1571
  }
1513
1572
  stream = await this._streamMessage(toolResults, { tools: this.tools, ...toolChoice && { tool_choice: toolChoice } });
1514
1573
  }
package/message.js CHANGED
@@ -183,7 +183,7 @@ class Message extends BaseClaude {
183
183
  // Parse structured data if configured
184
184
  if (this._isStructured) {
185
185
  try {
186
- if (this._responseSchema) {
186
+ if (this._responseSchema && !this.vertexai) {
187
187
  // Native structured output — guaranteed valid JSON
188
188
  result.data = JSON.parse(text);
189
189
  } else {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "ak-claude",
3
3
  "author": "ak@mixpanel.com",
4
4
  "description": "AK's Claude AI Helper for doing... everything",
5
- "version": "0.0.2",
5
+ "version": "0.0.5",
6
6
  "main": "index.js",
7
7
  "files": [
8
8
  "index.js",
package/tool-agent.js CHANGED
@@ -19,6 +19,31 @@ import log from './logger.js';
19
19
  * @typedef {import('./types').AgentStreamEvent} AgentStreamEvent
20
20
  */
21
21
 
22
+ /**
23
+ * Execute async task factories with a concurrency limit.
24
+ * @param {Array<() => Promise<any>>} tasks
25
+ * @param {number} concurrency - Infinity for unlimited, 1 for sequential
26
+ * @returns {Promise<any[]>} Results in same order as tasks
27
+ */
28
+ async function runWithConcurrency(tasks, concurrency) {
29
+ if (concurrency === Infinity) return Promise.all(tasks.map(t => t()));
30
+ if (concurrency === 1) {
31
+ const results = [];
32
+ for (const t of tasks) results.push(await t());
33
+ return results;
34
+ }
35
+ const results = new Array(tasks.length);
36
+ let next = 0;
37
+ async function worker() {
38
+ while (next < tasks.length) {
39
+ const i = next++;
40
+ results[i] = await tasks[i]();
41
+ }
42
+ }
43
+ await Promise.all(Array.from({ length: Math.min(concurrency, tasks.length) }, () => worker()));
44
+ return results;
45
+ }
46
+
22
47
  /**
23
48
  * AI agent that uses user-provided tools to accomplish tasks.
24
49
  * Automatically manages the tool-use loop: when Claude decides to call
@@ -90,6 +115,13 @@ class ToolAgent extends BaseClaude {
90
115
  this.toolChoice = options.toolChoice ?? undefined;
91
116
  this.disableParallelToolUse = options.disableParallelToolUse ?? false;
92
117
 
118
+ // ── Parallel execution ──
119
+ this.parallelToolCalls = options.parallelToolCalls ?? true;
120
+ /** @private */
121
+ this._concurrency = this.parallelToolCalls === true ? Infinity
122
+ : this.parallelToolCalls === false ? 1
123
+ : this.parallelToolCalls;
124
+
93
125
  // ── Tool loop config ──
94
126
  this.maxToolRounds = options.maxToolRounds || 10;
95
127
  this.onToolCall = options.onToolCall || null;
@@ -147,9 +179,8 @@ class ToolAgent extends BaseClaude {
147
179
  const toolUseBlocks = response.content.filter(b => b.type === 'tool_use');
148
180
  if (toolUseBlocks.length === 0) break;
149
181
 
150
- // Execute tools and build tool_result content blocks
151
- const toolResults = [];
152
- for (const block of toolUseBlocks) {
182
+ // Execute tools (parallel or sequential based on _concurrency)
183
+ const tasks = toolUseBlocks.map(block => async () => {
153
184
  // Fire onToolCall callback
154
185
  if (this.onToolCall) {
155
186
  try { this.onToolCall(block.name, block.input); }
@@ -162,13 +193,7 @@ class ToolAgent extends BaseClaude {
162
193
  const allowed = await this.onBeforeExecution(block.name, block.input);
163
194
  if (allowed === false) {
164
195
  const result = { error: 'Execution denied by onBeforeExecution callback' };
165
- allToolCalls.push({ name: block.name, args: block.input, result });
166
- toolResults.push({
167
- type: 'tool_result',
168
- tool_use_id: block.id,
169
- content: JSON.stringify(result)
170
- });
171
- continue;
196
+ return { toolCall: { name: block.name, args: block.input, result }, toolResult: { type: 'tool_result', tool_use_id: block.id, content: JSON.stringify(result) } };
172
197
  }
173
198
  } catch (e) {
174
199
  log.warn(`onBeforeExecution callback error: ${e.message}`);
@@ -183,14 +208,15 @@ class ToolAgent extends BaseClaude {
183
208
  result = { error: err.message };
184
209
  }
185
210
 
186
- allToolCalls.push({ name: block.name, args: block.input, result });
211
+ return {
212
+ toolCall: { name: block.name, args: block.input, result },
213
+ toolResult: { type: 'tool_result', tool_use_id: block.id, content: typeof result === 'string' ? result : JSON.stringify(result) }
214
+ };
215
+ });
187
216
 
188
- toolResults.push({
189
- type: 'tool_result',
190
- tool_use_id: block.id,
191
- content: typeof result === 'string' ? result : JSON.stringify(result)
192
- });
193
- }
217
+ const results = await runWithConcurrency(tasks, this._concurrency);
218
+ const toolResults = results.map(r => r.toolResult);
219
+ for (const r of results) allToolCalls.push(r.toolCall);
194
220
 
195
221
  // Send tool results back to Claude as user message
196
222
  response = await this._sendMessage(toolResults, { tools: this.tools, ...(toolChoice && { tool_choice: toolChoice }) });
@@ -270,50 +296,97 @@ class ToolAgent extends BaseClaude {
270
296
  return;
271
297
  }
272
298
 
273
- // Execute tools sequentially so we can yield events
299
+ // Execute tools and yield events
274
300
  const toolResults = [];
275
- for (const block of toolUseBlocks) {
276
- if (this._stopped) break;
301
+ if (this._concurrency === 1) {
302
+ // Sequential: yield tool_call, execute, yield tool_result for each
303
+ for (const block of toolUseBlocks) {
304
+ if (this._stopped) break;
277
305
 
278
- yield { type: 'tool_call', toolName: block.name, args: block.input };
306
+ yield { type: 'tool_call', toolName: block.name, args: block.input };
279
307
 
280
- // Fire onToolCall callback
281
- if (this.onToolCall) {
282
- try { this.onToolCall(block.name, block.input); }
283
- catch (e) { log.warn(`onToolCall callback error: ${e.message}`); }
284
- }
308
+ if (this.onToolCall) {
309
+ try { this.onToolCall(block.name, block.input); }
310
+ catch (e) { log.warn(`onToolCall callback error: ${e.message}`); }
311
+ }
285
312
 
286
- // Check onBeforeExecution gate
287
- let denied = false;
288
- if (this.onBeforeExecution) {
289
- try {
290
- const allowed = await this.onBeforeExecution(block.name, block.input);
291
- if (allowed === false) denied = true;
292
- } catch (e) {
293
- log.warn(`onBeforeExecution callback error: ${e.message}`);
313
+ let denied = false;
314
+ if (this.onBeforeExecution) {
315
+ try {
316
+ const allowed = await this.onBeforeExecution(block.name, block.input);
317
+ if (allowed === false) denied = true;
318
+ } catch (e) {
319
+ log.warn(`onBeforeExecution callback error: ${e.message}`);
320
+ }
294
321
  }
295
- }
296
322
 
297
- let result;
298
- if (denied) {
299
- result = { error: 'Execution denied by onBeforeExecution callback' };
300
- } else {
301
- try {
302
- result = await this.toolExecutor(block.name, block.input);
303
- } catch (err) {
304
- log.warn(`Tool ${block.name} failed: ${err.message}`);
305
- result = { error: err.message };
323
+ let result;
324
+ if (denied) {
325
+ result = { error: 'Execution denied by onBeforeExecution callback' };
326
+ } else {
327
+ try {
328
+ result = await this.toolExecutor(block.name, block.input);
329
+ } catch (err) {
330
+ log.warn(`Tool ${block.name} failed: ${err.message}`);
331
+ result = { error: err.message };
332
+ }
306
333
  }
334
+
335
+ allToolCalls.push({ name: block.name, args: block.input, result });
336
+ yield { type: 'tool_result', toolName: block.name, result };
337
+
338
+ toolResults.push({
339
+ type: 'tool_result',
340
+ tool_use_id: block.id,
341
+ content: typeof result === 'string' ? result : JSON.stringify(result)
342
+ });
307
343
  }
344
+ } else {
345
+ // Parallel: yield all tool_call events, execute all, yield all tool_result events
346
+ for (const block of toolUseBlocks) {
347
+ yield { type: 'tool_call', toolName: block.name, args: block.input };
348
+ }
349
+
350
+ const tasks = toolUseBlocks.map(block => async () => {
351
+ if (this.onToolCall) {
352
+ try { this.onToolCall(block.name, block.input); }
353
+ catch (e) { log.warn(`onToolCall callback error: ${e.message}`); }
354
+ }
355
+
356
+ let denied = false;
357
+ if (this.onBeforeExecution) {
358
+ try {
359
+ const allowed = await this.onBeforeExecution(block.name, block.input);
360
+ if (allowed === false) denied = true;
361
+ } catch (e) {
362
+ log.warn(`onBeforeExecution callback error: ${e.message}`);
363
+ }
364
+ }
308
365
 
309
- allToolCalls.push({ name: block.name, args: block.input, result });
310
- yield { type: 'tool_result', toolName: block.name, result };
366
+ let result;
367
+ if (denied) {
368
+ result = { error: 'Execution denied by onBeforeExecution callback' };
369
+ } else {
370
+ try {
371
+ result = await this.toolExecutor(block.name, block.input);
372
+ } catch (err) {
373
+ log.warn(`Tool ${block.name} failed: ${err.message}`);
374
+ result = { error: err.message };
375
+ }
376
+ }
311
377
 
312
- toolResults.push({
313
- type: 'tool_result',
314
- tool_use_id: block.id,
315
- content: typeof result === 'string' ? result : JSON.stringify(result)
378
+ return {
379
+ toolCall: { name: block.name, args: block.input, result },
380
+ toolResult: { type: 'tool_result', tool_use_id: block.id, content: typeof result === 'string' ? result : JSON.stringify(result) }
381
+ };
316
382
  });
383
+
384
+ const results = await runWithConcurrency(tasks, this._concurrency);
385
+ for (const r of results) {
386
+ allToolCalls.push(r.toolCall);
387
+ yield { type: 'tool_result', toolName: r.toolCall.name, result: r.toolCall.result };
388
+ toolResults.push(r.toolResult);
389
+ }
317
390
  }
318
391
 
319
392
  // Send tool results back and get next stream
package/types.d.ts CHANGED
@@ -203,6 +203,8 @@ export interface ToolAgentOptions extends BaseClaudeOptions {
203
203
  toolChoice?: ToolChoice;
204
204
  /** Disable parallel tool use — forces sequential tool calls (default: false) */
205
205
  disableParallelToolUse?: boolean;
206
+ /** Parallel tool execution: false = sequential, true = unlimited parallel, number = concurrency limit (default: true) */
207
+ parallelToolCalls?: boolean | number;
206
208
  }
207
209
 
208
210
  export interface LocalDataEntry {
@@ -483,6 +485,7 @@ export declare class ToolAgent extends BaseClaude {
483
485
  onBeforeExecution: ((toolName: string, args: Record<string, any>) => Promise<boolean>) | null;
484
486
  toolChoice: ToolChoice | undefined;
485
487
  disableParallelToolUse: boolean;
488
+ parallelToolCalls: boolean | number;
486
489
 
487
490
  chat(message: string, opts?: Record<string, any>): Promise<AgentResponse>;
488
491
  stream(message: string, opts?: Record<string, any>): AsyncGenerator<AgentStreamEvent, void, unknown>;