@probelabs/probe 0.6.0-rc230 → 0.6.0-rc232

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.
@@ -357,6 +357,10 @@ export class ProbeAgent {
357
357
  // Each ProbeAgent instance has its own limits, not shared globally
358
358
  this.delegationManager = new DelegationManager();
359
359
 
360
+ // Optional global concurrency limiter shared across all ProbeAgent instances.
361
+ // When set, every AI API call acquires a slot before calling the provider.
362
+ this.concurrencyLimiter = options.concurrencyLimiter || null;
363
+
360
364
  // Request timeout configuration (default 2 minutes)
361
365
  // Validates env var to prevent NaN or unreasonable values
362
366
  this.requestTimeout = options.requestTimeout ?? (() => {
@@ -824,6 +828,7 @@ export class ProbeAgent {
824
828
  provider: this.clientApiProvider,
825
829
  model: this.clientApiModel,
826
830
  delegationManager: this.delegationManager, // Per-instance delegation limits
831
+ concurrencyLimiter: this.concurrencyLimiter, // Global AI concurrency limiter
827
832
  isToolAllowed
828
833
  };
829
834
 
@@ -1363,6 +1368,16 @@ export class ProbeAgent {
1363
1368
  * @private
1364
1369
  */
1365
1370
  async streamTextWithRetryAndFallback(options) {
1371
+ // Acquire global concurrency slot if limiter is configured
1372
+ const limiter = this.concurrencyLimiter;
1373
+ if (limiter) {
1374
+ await limiter.acquire(null);
1375
+ if (this.debug) {
1376
+ const stats = limiter.getStats();
1377
+ console.log(`[DEBUG] Acquired global AI concurrency slot (${stats.globalActive}/${stats.maxConcurrent}, queue: ${stats.queueSize})`);
1378
+ }
1379
+ }
1380
+
1366
1381
  // Create AbortController for overall operation timeout
1367
1382
  const controller = new AbortController();
1368
1383
  const timeoutState = { timeoutId: null };
@@ -1382,12 +1397,10 @@ export class ProbeAgent {
1382
1397
  const useClaudeCode = this.clientApiProvider === 'claude-code' || process.env.USE_CLAUDE_CODE === 'true';
1383
1398
  const useCodex = this.clientApiProvider === 'codex' || process.env.USE_CODEX === 'true';
1384
1399
 
1400
+ let result;
1385
1401
  if (useClaudeCode || useCodex) {
1386
1402
  try {
1387
- const result = await this._tryEngineStreamPath(options, controller, timeoutState);
1388
- if (result) {
1389
- return result;
1390
- }
1403
+ result = await this._tryEngineStreamPath(options, controller, timeoutState);
1391
1404
  } catch (error) {
1392
1405
  if (this.debug) {
1393
1406
  const engineType = useClaudeCode ? 'Claude Code' : 'Codex';
@@ -1397,8 +1410,43 @@ export class ProbeAgent {
1397
1410
  }
1398
1411
  }
1399
1412
 
1400
- // Use Vercel AI SDK with retry/fallback
1401
- return await this._executeWithVercelProvider(options, controller);
1413
+ if (!result) {
1414
+ // Use Vercel AI SDK with retry/fallback
1415
+ result = await this._executeWithVercelProvider(options, controller);
1416
+ }
1417
+
1418
+ // Wrap textStream so limiter slot is held until stream completes
1419
+ if (limiter && result.textStream) {
1420
+ const originalStream = result.textStream;
1421
+ const debug = this.debug;
1422
+ result.textStream = (async function* () {
1423
+ try {
1424
+ for await (const chunk of originalStream) {
1425
+ yield chunk;
1426
+ }
1427
+ } finally {
1428
+ limiter.release(null);
1429
+ if (debug) {
1430
+ const stats = limiter.getStats();
1431
+ console.log(`[DEBUG] Released global AI concurrency slot (${stats.globalActive}/${stats.maxConcurrent}, queue: ${stats.queueSize})`);
1432
+ }
1433
+ }
1434
+ })();
1435
+ } else if (limiter) {
1436
+ // No textStream (shouldn't happen, but release just in case)
1437
+ limiter.release(null);
1438
+ }
1439
+
1440
+ return result;
1441
+ } catch (error) {
1442
+ // Release on error if limiter was acquired
1443
+ if (limiter) {
1444
+ limiter.release(null);
1445
+ if (this.debug) {
1446
+ console.log(`[DEBUG] Released global AI concurrency slot on error`);
1447
+ }
1448
+ }
1449
+ throw error;
1402
1450
  } finally {
1403
1451
  // Clean up timeout (for non-engine paths; engine paths clean up in the generator)
1404
1452
  if (timeoutState.timeoutId) {
@@ -2496,10 +2544,9 @@ ${extractGuidance}
2496
2544
  toolDefinitions += `${taskToolDefinition}\n`;
2497
2545
  }
2498
2546
 
2499
- // Always include attempt_completion (unless explicitly disabled in raw AI mode)
2500
- if (isToolAllowed('attempt_completion')) {
2501
- toolDefinitions += `${attemptCompletionToolDefinition}\n`;
2502
- }
2547
+ // Always include attempt_completion unconditionally - it's a completion signal, not a tool
2548
+ // This ensures agents can always complete their work, regardless of tool restrictions
2549
+ toolDefinitions += `${attemptCompletionToolDefinition}\n`;
2503
2550
 
2504
2551
  // Delegate tool (require both enableDelegate flag AND allowedTools permission)
2505
2552
  // Place after attempt_completion as it's an optional tool
@@ -3304,8 +3351,9 @@ Follow these instructions carefully:
3304
3351
  if (this.enableSkills && this.allowedTools.isEnabled('listSkills')) validTools.push('listSkills');
3305
3352
  if (this.enableSkills && this.allowedTools.isEnabled('useSkill')) validTools.push('useSkill');
3306
3353
  if (this.allowedTools.isEnabled('readImage')) validTools.push('readImage');
3307
- // Always allow attempt_completion - it's a completion signal, not a tool
3354
+ // Always allow attempt_completion in validTools - it's a completion signal, not a tool
3308
3355
  // This ensures agents can complete even when disableTools: true is set (fixes #333)
3356
+ // The tool DEFINITION may be hidden in raw AI mode, but we still need to recognize it
3309
3357
  validTools.push('attempt_completion');
3310
3358
 
3311
3359
  // Edit tools (require both allowEdit flag AND allowedTools permission)
@@ -3859,8 +3859,10 @@ async function delegate({
3859
3859
  enableMcp = false,
3860
3860
  mcpConfig = null,
3861
3861
  mcpConfigPath = null,
3862
- delegationManager = null
3862
+ delegationManager = null,
3863
3863
  // Optional per-instance manager, falls back to default singleton
3864
+ concurrencyLimiter = null
3865
+ // Optional global AI concurrency limiter
3864
3866
  }) {
3865
3867
  if (!task || typeof task !== "string") {
3866
3868
  throw new Error("Task parameter is required and must be a string");
@@ -3936,8 +3938,10 @@ async function delegate({
3936
3938
  // Inherit from parent (subagent creates own MCPXmlBridge)
3937
3939
  mcpConfig,
3938
3940
  // Inherit from parent
3939
- mcpConfigPath
3941
+ mcpConfigPath,
3940
3942
  // Inherit from parent
3943
+ concurrencyLimiter
3944
+ // Inherit global AI concurrency limiter
3941
3945
  });
3942
3946
  if (debug) {
3943
3947
  console.error(`[DELEGATE] Created subagent with session ${sessionId}`);
@@ -4034,10 +4038,10 @@ var init_delegate = __esm({
4034
4038
  "use strict";
4035
4039
  init_ProbeAgent();
4036
4040
  DelegationManager = class {
4037
- constructor() {
4038
- this.maxConcurrent = parseInt(process.env.MAX_CONCURRENT_DELEGATIONS || "3", 10);
4039
- this.maxPerSession = parseInt(process.env.MAX_DELEGATIONS_PER_SESSION || "10", 10);
4040
- this.defaultQueueTimeout = parseInt(process.env.DELEGATION_QUEUE_TIMEOUT || "60000", 10);
4041
+ constructor(options = {}) {
4042
+ this.maxConcurrent = options.maxConcurrent ?? parseInt(process.env.MAX_CONCURRENT_DELEGATIONS || "3", 10);
4043
+ this.maxPerSession = options.maxPerSession ?? parseInt(process.env.MAX_DELEGATIONS_PER_SESSION || "10", 10);
4044
+ this.defaultQueueTimeout = options.queueTimeout ?? parseInt(process.env.DELEGATION_QUEUE_TIMEOUT || "60000", 10);
4041
4045
  this.sessionDelegations = /* @__PURE__ */ new Map();
4042
4046
  this.globalActive = 0;
4043
4047
  this.waitQueue = [];
@@ -9199,7 +9203,15 @@ function createTaskTool(options = {}) {
9199
9203
  });
9200
9204
  return `Error: Invalid task parameters - ${validation.error.message}`;
9201
9205
  }
9202
- const { action, tasks, id, title, description, status, priority, dependencies, after } = validation.data;
9206
+ const { action, tasks: rawTasks, id, title, description, status, priority, dependencies, after } = validation.data;
9207
+ let tasks = rawTasks;
9208
+ if (typeof rawTasks === "string") {
9209
+ try {
9210
+ tasks = JSON.parse(rawTasks);
9211
+ } catch (e) {
9212
+ return `Error: Invalid tasks JSON - ${e.message}`;
9213
+ }
9214
+ }
9203
9215
  switch (action) {
9204
9216
  case "create": {
9205
9217
  if (tasks && Array.isArray(tasks)) {
@@ -9374,7 +9386,8 @@ var init_taskTool = __esm({
9374
9386
  });
9375
9387
  taskSchema = external_exports.object({
9376
9388
  action: external_exports.enum(["create", "update", "complete", "delete", "list"]),
9377
- tasks: external_exports.array(external_exports.union([external_exports.string(), taskItemSchema])).optional(),
9389
+ // Accept both array and JSON string (AI models sometimes serialize as string)
9390
+ tasks: external_exports.union([external_exports.array(external_exports.union([external_exports.string(), taskItemSchema])), external_exports.string()]).optional(),
9378
9391
  id: external_exports.string().optional(),
9379
9392
  title: external_exports.string().optional(),
9380
9393
  description: external_exports.string().optional(),
@@ -9485,6 +9498,25 @@ SKIP TASKS for single-goal requests, even if they require multiple searches:
9485
9498
  **Key insight**: Multiple *internal steps* (search, read, analyze) are NOT the same as multiple *goals*.
9486
9499
  A single investigation with many steps is still ONE task, not many.
9487
9500
 
9501
+ ## Task Granularity
9502
+
9503
+ Tasks represent LOGICAL UNITS OF WORK, not individual files or steps:
9504
+ - "Fix 8 similar test files" \u2192 ONE task (same type of fix across files)
9505
+ - "Update API + tests + docs" \u2192 THREE tasks (different types of work)
9506
+ - "Implement feature in 5 files" \u2192 ONE task (single feature)
9507
+
9508
+ **Rule of thumb**: If you're creating more than 3-4 tasks, you're probably too granular.
9509
+
9510
+ **Anti-patterns to avoid**:
9511
+ - One task per file \u274C
9512
+ - One task per function \u274C
9513
+ - One task per repository (when same type of work) \u274C
9514
+
9515
+ **Good patterns**:
9516
+ - One task per distinct deliverable \u2713
9517
+ - One task per phase (implement, test, document) \u2713
9518
+ - One task per different type of work \u2713
9519
+
9488
9520
  MODIFY TASKS when (during execution):
9489
9521
  - You discover the problem is more complex than expected \u2192 Add new tasks
9490
9522
  - A single task covers too much scope \u2192 Split into smaller tasks
@@ -55823,6 +55855,7 @@ var require_pattern = __commonJS({
55823
55855
  "use strict";
55824
55856
  Object.defineProperty(exports2, "__esModule", { value: true });
55825
55857
  var code_1 = require_code2();
55858
+ var util_1 = require_util3();
55826
55859
  var codegen_1 = require_codegen();
55827
55860
  var error = {
55828
55861
  message: ({ schemaCode }) => (0, codegen_1.str)`must match pattern "${schemaCode}"`,
@@ -55835,10 +55868,18 @@ var require_pattern = __commonJS({
55835
55868
  $data: true,
55836
55869
  error,
55837
55870
  code(cxt) {
55838
- const { data, $data, schema, schemaCode, it } = cxt;
55871
+ const { gen, data, $data, schema, schemaCode, it } = cxt;
55839
55872
  const u = it.opts.unicodeRegExp ? "u" : "";
55840
- const regExp = $data ? (0, codegen_1._)`(new RegExp(${schemaCode}, ${u}))` : (0, code_1.usePattern)(cxt, schema);
55841
- cxt.fail$data((0, codegen_1._)`!${regExp}.test(${data})`);
55873
+ if ($data) {
55874
+ const { regExp } = it.opts.code;
55875
+ const regExpCode = regExp.code === "new RegExp" ? (0, codegen_1._)`new RegExp` : (0, util_1.useFunc)(gen, regExp);
55876
+ const valid = gen.let("valid");
55877
+ gen.try(() => gen.assign(valid, (0, codegen_1._)`${regExpCode}(${schemaCode}, ${u}).test(${data})`), () => gen.assign(valid, false));
55878
+ cxt.fail$data((0, codegen_1._)`!${valid}`);
55879
+ } else {
55880
+ const regExp = (0, code_1.usePattern)(cxt, schema);
55881
+ cxt.fail$data((0, codegen_1._)`!${regExp}.test(${data})`);
55882
+ }
55842
55883
  }
55843
55884
  };
55844
55885
  exports2.default = def;
@@ -70535,6 +70576,7 @@ var init_ProbeAgent = __esm({
70535
70576
  this.enableTasks = !!options.enableTasks;
70536
70577
  this.taskManager = null;
70537
70578
  this.delegationManager = new DelegationManager();
70579
+ this.concurrencyLimiter = options.concurrencyLimiter || null;
70538
70580
  this.requestTimeout = options.requestTimeout ?? (() => {
70539
70581
  if (process.env.REQUEST_TIMEOUT) {
70540
70582
  const parsed = parseInt(process.env.REQUEST_TIMEOUT, 10);
@@ -70911,6 +70953,8 @@ var init_ProbeAgent = __esm({
70911
70953
  model: this.clientApiModel,
70912
70954
  delegationManager: this.delegationManager,
70913
70955
  // Per-instance delegation limits
70956
+ concurrencyLimiter: this.concurrencyLimiter,
70957
+ // Global AI concurrency limiter
70914
70958
  isToolAllowed
70915
70959
  };
70916
70960
  const baseTools = createTools(configOptions);
@@ -71332,6 +71376,14 @@ var init_ProbeAgent = __esm({
71332
71376
  * @private
71333
71377
  */
71334
71378
  async streamTextWithRetryAndFallback(options) {
71379
+ const limiter = this.concurrencyLimiter;
71380
+ if (limiter) {
71381
+ await limiter.acquire(null);
71382
+ if (this.debug) {
71383
+ const stats = limiter.getStats();
71384
+ console.log(`[DEBUG] Acquired global AI concurrency slot (${stats.globalActive}/${stats.maxConcurrent}, queue: ${stats.queueSize})`);
71385
+ }
71386
+ }
71335
71387
  const controller = new AbortController();
71336
71388
  const timeoutState = { timeoutId: null };
71337
71389
  if (this.maxOperationTimeout && this.maxOperationTimeout > 0) {
@@ -71345,12 +71397,10 @@ var init_ProbeAgent = __esm({
71345
71397
  try {
71346
71398
  const useClaudeCode = this.clientApiProvider === "claude-code" || process.env.USE_CLAUDE_CODE === "true";
71347
71399
  const useCodex = this.clientApiProvider === "codex" || process.env.USE_CODEX === "true";
71400
+ let result;
71348
71401
  if (useClaudeCode || useCodex) {
71349
71402
  try {
71350
- const result = await this._tryEngineStreamPath(options, controller, timeoutState);
71351
- if (result) {
71352
- return result;
71353
- }
71403
+ result = await this._tryEngineStreamPath(options, controller, timeoutState);
71354
71404
  } catch (error) {
71355
71405
  if (this.debug) {
71356
71406
  const engineType = useClaudeCode ? "Claude Code" : "Codex";
@@ -71358,7 +71408,37 @@ var init_ProbeAgent = __esm({
71358
71408
  }
71359
71409
  }
71360
71410
  }
71361
- return await this._executeWithVercelProvider(options, controller);
71411
+ if (!result) {
71412
+ result = await this._executeWithVercelProvider(options, controller);
71413
+ }
71414
+ if (limiter && result.textStream) {
71415
+ const originalStream = result.textStream;
71416
+ const debug = this.debug;
71417
+ result.textStream = (async function* () {
71418
+ try {
71419
+ for await (const chunk of originalStream) {
71420
+ yield chunk;
71421
+ }
71422
+ } finally {
71423
+ limiter.release(null);
71424
+ if (debug) {
71425
+ const stats = limiter.getStats();
71426
+ console.log(`[DEBUG] Released global AI concurrency slot (${stats.globalActive}/${stats.maxConcurrent}, queue: ${stats.queueSize})`);
71427
+ }
71428
+ }
71429
+ })();
71430
+ } else if (limiter) {
71431
+ limiter.release(null);
71432
+ }
71433
+ return result;
71434
+ } catch (error) {
71435
+ if (limiter) {
71436
+ limiter.release(null);
71437
+ if (this.debug) {
71438
+ console.log(`[DEBUG] Released global AI concurrency slot on error`);
71439
+ }
71440
+ }
71441
+ throw error;
71362
71442
  } finally {
71363
71443
  if (timeoutState.timeoutId) {
71364
71444
  clearTimeout(timeoutState.timeoutId);
@@ -72250,10 +72330,8 @@ Workspace: ${this.allowedFolders.join(", ")}`;
72250
72330
  toolDefinitions += `${taskToolDefinition}
72251
72331
  `;
72252
72332
  }
72253
- if (isToolAllowed("attempt_completion")) {
72254
- toolDefinitions += `${attemptCompletionToolDefinition}
72333
+ toolDefinitions += `${attemptCompletionToolDefinition}
72255
72334
  `;
72256
- }
72257
72335
  if (this.enableDelegate && isToolAllowed("delegate")) {
72258
72336
  toolDefinitions += `${delegateToolDefinition}
72259
72337
  `;
@@ -23,7 +23,8 @@ export const taskItemSchema = z.object({
23
23
  */
24
24
  export const taskSchema = z.object({
25
25
  action: z.enum(['create', 'update', 'complete', 'delete', 'list']),
26
- tasks: z.array(z.union([z.string(), taskItemSchema])).optional(),
26
+ // Accept both array and JSON string (AI models sometimes serialize as string)
27
+ tasks: z.union([z.array(z.union([z.string(), taskItemSchema])), z.string()]).optional(),
27
28
  id: z.string().optional(),
28
29
  title: z.string().optional(),
29
30
  description: z.string().optional(),
@@ -142,6 +143,25 @@ SKIP TASKS for single-goal requests, even if they require multiple searches:
142
143
  **Key insight**: Multiple *internal steps* (search, read, analyze) are NOT the same as multiple *goals*.
143
144
  A single investigation with many steps is still ONE task, not many.
144
145
 
146
+ ## Task Granularity
147
+
148
+ Tasks represent LOGICAL UNITS OF WORK, not individual files or steps:
149
+ - "Fix 8 similar test files" → ONE task (same type of fix across files)
150
+ - "Update API + tests + docs" → THREE tasks (different types of work)
151
+ - "Implement feature in 5 files" → ONE task (single feature)
152
+
153
+ **Rule of thumb**: If you're creating more than 3-4 tasks, you're probably too granular.
154
+
155
+ **Anti-patterns to avoid**:
156
+ - One task per file ❌
157
+ - One task per function ❌
158
+ - One task per repository (when same type of work) ❌
159
+
160
+ **Good patterns**:
161
+ - One task per distinct deliverable ✓
162
+ - One task per phase (implement, test, document) ✓
163
+ - One task per different type of work ✓
164
+
145
165
  MODIFY TASKS when (during execution):
146
166
  - You discover the problem is more complex than expected → Add new tasks
147
167
  - A single task covers too much scope → Split into smaller tasks
@@ -314,7 +334,17 @@ export function createTaskTool(options = {}) {
314
334
  return `Error: Invalid task parameters - ${validation.error.message}`;
315
335
  }
316
336
 
317
- const { action, tasks, id, title, description, status, priority, dependencies, after } = validation.data;
337
+ const { action, tasks: rawTasks, id, title, description, status, priority, dependencies, after } = validation.data;
338
+
339
+ // Parse tasks if passed as JSON string (common AI model behavior)
340
+ let tasks = rawTasks;
341
+ if (typeof rawTasks === 'string') {
342
+ try {
343
+ tasks = JSON.parse(rawTasks);
344
+ } catch (e) {
345
+ return `Error: Invalid tasks JSON - ${e.message}`;
346
+ }
347
+ }
318
348
 
319
349
  switch (action) {
320
350
  case 'create': {
package/build/delegate.js CHANGED
@@ -19,11 +19,14 @@ import { ProbeAgent } from './agent/ProbeAgent.js';
19
19
  * - For long-running processes, periodic cleanup of stale sessions may be needed
20
20
  */
21
21
  class DelegationManager {
22
- constructor() {
23
- this.maxConcurrent = parseInt(process.env.MAX_CONCURRENT_DELEGATIONS || '3', 10);
24
- this.maxPerSession = parseInt(process.env.MAX_DELEGATIONS_PER_SESSION || '10', 10);
22
+ constructor(options = {}) {
23
+ this.maxConcurrent = options.maxConcurrent
24
+ ?? parseInt(process.env.MAX_CONCURRENT_DELEGATIONS || '3', 10);
25
+ this.maxPerSession = options.maxPerSession
26
+ ?? parseInt(process.env.MAX_DELEGATIONS_PER_SESSION || '10', 10);
25
27
  // Default queue timeout: 60 seconds. Set DELEGATION_QUEUE_TIMEOUT=0 to disable.
26
- this.defaultQueueTimeout = parseInt(process.env.DELEGATION_QUEUE_TIMEOUT || '60000', 10);
28
+ this.defaultQueueTimeout = options.queueTimeout
29
+ ?? parseInt(process.env.DELEGATION_QUEUE_TIMEOUT || '60000', 10);
27
30
 
28
31
  // Track delegations per session with timestamp for potential TTL cleanup
29
32
  // Map<string, { count: number, lastUpdated: number }>
@@ -353,6 +356,7 @@ const DEFAULT_DELEGATE_TIMEOUT = parseInt(process.env.DELEGATE_TIMEOUT, 10) || 3
353
356
  * @param {boolean} [options.enableMcp=false] - Enable MCP tool integration (inherited from parent)
354
357
  * @param {Object} [options.mcpConfig] - MCP configuration object (inherited from parent)
355
358
  * @param {string} [options.mcpConfigPath] - Path to MCP configuration file (inherited from parent)
359
+ * @param {Object} [options.concurrencyLimiter=null] - Global AI concurrency limiter (DelegationManager instance)
356
360
  * @returns {Promise<string>} The response from the delegate agent
357
361
  */
358
362
  export async function delegate({
@@ -379,7 +383,8 @@ export async function delegate({
379
383
  enableMcp = false,
380
384
  mcpConfig = null,
381
385
  mcpConfigPath = null,
382
- delegationManager = null // Optional per-instance manager, falls back to default singleton
386
+ delegationManager = null, // Optional per-instance manager, falls back to default singleton
387
+ concurrencyLimiter = null // Optional global AI concurrency limiter
383
388
  }) {
384
389
  if (!task || typeof task !== 'string') {
385
390
  throw new Error('Task parameter is required and must be a string');
@@ -464,7 +469,8 @@ export async function delegate({
464
469
  enableTasks, // Inherit from parent (subagent gets isolated TaskManager)
465
470
  enableMcp, // Inherit from parent (subagent creates own MCPXmlBridge)
466
471
  mcpConfig, // Inherit from parent
467
- mcpConfigPath // Inherit from parent
472
+ mcpConfigPath, // Inherit from parent
473
+ concurrencyLimiter // Inherit global AI concurrency limiter
468
474
  });
469
475
 
470
476
  if (debug) {
@@ -30969,8 +30969,10 @@ async function delegate({
30969
30969
  enableMcp = false,
30970
30970
  mcpConfig = null,
30971
30971
  mcpConfigPath = null,
30972
- delegationManager = null
30972
+ delegationManager = null,
30973
30973
  // Optional per-instance manager, falls back to default singleton
30974
+ concurrencyLimiter = null
30975
+ // Optional global AI concurrency limiter
30974
30976
  }) {
30975
30977
  if (!task || typeof task !== "string") {
30976
30978
  throw new Error("Task parameter is required and must be a string");
@@ -31046,8 +31048,10 @@ async function delegate({
31046
31048
  // Inherit from parent (subagent creates own MCPXmlBridge)
31047
31049
  mcpConfig,
31048
31050
  // Inherit from parent
31049
- mcpConfigPath
31051
+ mcpConfigPath,
31050
31052
  // Inherit from parent
31053
+ concurrencyLimiter
31054
+ // Inherit global AI concurrency limiter
31051
31055
  });
31052
31056
  if (debug) {
31053
31057
  console.error(`[DELEGATE] Created subagent with session ${sessionId}`);
@@ -31145,10 +31149,10 @@ var init_delegate = __esm({
31145
31149
  import_crypto2 = require("crypto");
31146
31150
  init_ProbeAgent();
31147
31151
  DelegationManager = class {
31148
- constructor() {
31149
- this.maxConcurrent = parseInt(process.env.MAX_CONCURRENT_DELEGATIONS || "3", 10);
31150
- this.maxPerSession = parseInt(process.env.MAX_DELEGATIONS_PER_SESSION || "10", 10);
31151
- this.defaultQueueTimeout = parseInt(process.env.DELEGATION_QUEUE_TIMEOUT || "60000", 10);
31152
+ constructor(options = {}) {
31153
+ this.maxConcurrent = options.maxConcurrent ?? parseInt(process.env.MAX_CONCURRENT_DELEGATIONS || "3", 10);
31154
+ this.maxPerSession = options.maxPerSession ?? parseInt(process.env.MAX_DELEGATIONS_PER_SESSION || "10", 10);
31155
+ this.defaultQueueTimeout = options.queueTimeout ?? parseInt(process.env.DELEGATION_QUEUE_TIMEOUT || "60000", 10);
31152
31156
  this.sessionDelegations = /* @__PURE__ */ new Map();
31153
31157
  this.globalActive = 0;
31154
31158
  this.waitQueue = [];
@@ -36310,7 +36314,15 @@ function createTaskTool(options = {}) {
36310
36314
  });
36311
36315
  return `Error: Invalid task parameters - ${validation.error.message}`;
36312
36316
  }
36313
- const { action, tasks, id, title, description, status, priority, dependencies, after } = validation.data;
36317
+ const { action, tasks: rawTasks, id, title, description, status, priority, dependencies, after } = validation.data;
36318
+ let tasks = rawTasks;
36319
+ if (typeof rawTasks === "string") {
36320
+ try {
36321
+ tasks = JSON.parse(rawTasks);
36322
+ } catch (e4) {
36323
+ return `Error: Invalid tasks JSON - ${e4.message}`;
36324
+ }
36325
+ }
36314
36326
  switch (action) {
36315
36327
  case "create": {
36316
36328
  if (tasks && Array.isArray(tasks)) {
@@ -36485,7 +36497,8 @@ var init_taskTool = __esm({
36485
36497
  });
36486
36498
  taskSchema = external_exports.object({
36487
36499
  action: external_exports.enum(["create", "update", "complete", "delete", "list"]),
36488
- tasks: external_exports.array(external_exports.union([external_exports.string(), taskItemSchema])).optional(),
36500
+ // Accept both array and JSON string (AI models sometimes serialize as string)
36501
+ tasks: external_exports.union([external_exports.array(external_exports.union([external_exports.string(), taskItemSchema])), external_exports.string()]).optional(),
36489
36502
  id: external_exports.string().optional(),
36490
36503
  title: external_exports.string().optional(),
36491
36504
  description: external_exports.string().optional(),
@@ -36596,6 +36609,25 @@ SKIP TASKS for single-goal requests, even if they require multiple searches:
36596
36609
  **Key insight**: Multiple *internal steps* (search, read, analyze) are NOT the same as multiple *goals*.
36597
36610
  A single investigation with many steps is still ONE task, not many.
36598
36611
 
36612
+ ## Task Granularity
36613
+
36614
+ Tasks represent LOGICAL UNITS OF WORK, not individual files or steps:
36615
+ - "Fix 8 similar test files" \u2192 ONE task (same type of fix across files)
36616
+ - "Update API + tests + docs" \u2192 THREE tasks (different types of work)
36617
+ - "Implement feature in 5 files" \u2192 ONE task (single feature)
36618
+
36619
+ **Rule of thumb**: If you're creating more than 3-4 tasks, you're probably too granular.
36620
+
36621
+ **Anti-patterns to avoid**:
36622
+ - One task per file \u274C
36623
+ - One task per function \u274C
36624
+ - One task per repository (when same type of work) \u274C
36625
+
36626
+ **Good patterns**:
36627
+ - One task per distinct deliverable \u2713
36628
+ - One task per phase (implement, test, document) \u2713
36629
+ - One task per different type of work \u2713
36630
+
36599
36631
  MODIFY TASKS when (during execution):
36600
36632
  - You discover the problem is more complex than expected \u2192 Add new tasks
36601
36633
  - A single task covers too much scope \u2192 Split into smaller tasks
@@ -82501,6 +82533,7 @@ var require_pattern = __commonJS({
82501
82533
  "use strict";
82502
82534
  Object.defineProperty(exports2, "__esModule", { value: true });
82503
82535
  var code_1 = require_code2();
82536
+ var util_1 = require_util3();
82504
82537
  var codegen_1 = require_codegen();
82505
82538
  var error2 = {
82506
82539
  message: ({ schemaCode }) => (0, codegen_1.str)`must match pattern "${schemaCode}"`,
@@ -82513,10 +82546,18 @@ var require_pattern = __commonJS({
82513
82546
  $data: true,
82514
82547
  error: error2,
82515
82548
  code(cxt) {
82516
- const { data: data2, $data, schema, schemaCode, it } = cxt;
82549
+ const { gen, data: data2, $data, schema, schemaCode, it } = cxt;
82517
82550
  const u4 = it.opts.unicodeRegExp ? "u" : "";
82518
- const regExp = $data ? (0, codegen_1._)`(new RegExp(${schemaCode}, ${u4}))` : (0, code_1.usePattern)(cxt, schema);
82519
- cxt.fail$data((0, codegen_1._)`!${regExp}.test(${data2})`);
82551
+ if ($data) {
82552
+ const { regExp } = it.opts.code;
82553
+ const regExpCode = regExp.code === "new RegExp" ? (0, codegen_1._)`new RegExp` : (0, util_1.useFunc)(gen, regExp);
82554
+ const valid = gen.let("valid");
82555
+ gen.try(() => gen.assign(valid, (0, codegen_1._)`${regExpCode}(${schemaCode}, ${u4}).test(${data2})`), () => gen.assign(valid, false));
82556
+ cxt.fail$data((0, codegen_1._)`!${valid}`);
82557
+ } else {
82558
+ const regExp = (0, code_1.usePattern)(cxt, schema);
82559
+ cxt.fail$data((0, codegen_1._)`!${regExp}.test(${data2})`);
82560
+ }
82520
82561
  }
82521
82562
  };
82522
82563
  exports2.default = def;
@@ -97212,6 +97253,7 @@ var init_ProbeAgent = __esm({
97212
97253
  this.enableTasks = !!options.enableTasks;
97213
97254
  this.taskManager = null;
97214
97255
  this.delegationManager = new DelegationManager();
97256
+ this.concurrencyLimiter = options.concurrencyLimiter || null;
97215
97257
  this.requestTimeout = options.requestTimeout ?? (() => {
97216
97258
  if (process.env.REQUEST_TIMEOUT) {
97217
97259
  const parsed = parseInt(process.env.REQUEST_TIMEOUT, 10);
@@ -97588,6 +97630,8 @@ var init_ProbeAgent = __esm({
97588
97630
  model: this.clientApiModel,
97589
97631
  delegationManager: this.delegationManager,
97590
97632
  // Per-instance delegation limits
97633
+ concurrencyLimiter: this.concurrencyLimiter,
97634
+ // Global AI concurrency limiter
97591
97635
  isToolAllowed
97592
97636
  };
97593
97637
  const baseTools = createTools(configOptions);
@@ -98009,6 +98053,14 @@ var init_ProbeAgent = __esm({
98009
98053
  * @private
98010
98054
  */
98011
98055
  async streamTextWithRetryAndFallback(options) {
98056
+ const limiter = this.concurrencyLimiter;
98057
+ if (limiter) {
98058
+ await limiter.acquire(null);
98059
+ if (this.debug) {
98060
+ const stats = limiter.getStats();
98061
+ console.log(`[DEBUG] Acquired global AI concurrency slot (${stats.globalActive}/${stats.maxConcurrent}, queue: ${stats.queueSize})`);
98062
+ }
98063
+ }
98012
98064
  const controller = new AbortController();
98013
98065
  const timeoutState = { timeoutId: null };
98014
98066
  if (this.maxOperationTimeout && this.maxOperationTimeout > 0) {
@@ -98022,12 +98074,10 @@ var init_ProbeAgent = __esm({
98022
98074
  try {
98023
98075
  const useClaudeCode = this.clientApiProvider === "claude-code" || process.env.USE_CLAUDE_CODE === "true";
98024
98076
  const useCodex = this.clientApiProvider === "codex" || process.env.USE_CODEX === "true";
98077
+ let result;
98025
98078
  if (useClaudeCode || useCodex) {
98026
98079
  try {
98027
- const result = await this._tryEngineStreamPath(options, controller, timeoutState);
98028
- if (result) {
98029
- return result;
98030
- }
98080
+ result = await this._tryEngineStreamPath(options, controller, timeoutState);
98031
98081
  } catch (error2) {
98032
98082
  if (this.debug) {
98033
98083
  const engineType = useClaudeCode ? "Claude Code" : "Codex";
@@ -98035,7 +98085,37 @@ var init_ProbeAgent = __esm({
98035
98085
  }
98036
98086
  }
98037
98087
  }
98038
- return await this._executeWithVercelProvider(options, controller);
98088
+ if (!result) {
98089
+ result = await this._executeWithVercelProvider(options, controller);
98090
+ }
98091
+ if (limiter && result.textStream) {
98092
+ const originalStream = result.textStream;
98093
+ const debug = this.debug;
98094
+ result.textStream = (async function* () {
98095
+ try {
98096
+ for await (const chunk of originalStream) {
98097
+ yield chunk;
98098
+ }
98099
+ } finally {
98100
+ limiter.release(null);
98101
+ if (debug) {
98102
+ const stats = limiter.getStats();
98103
+ console.log(`[DEBUG] Released global AI concurrency slot (${stats.globalActive}/${stats.maxConcurrent}, queue: ${stats.queueSize})`);
98104
+ }
98105
+ }
98106
+ })();
98107
+ } else if (limiter) {
98108
+ limiter.release(null);
98109
+ }
98110
+ return result;
98111
+ } catch (error2) {
98112
+ if (limiter) {
98113
+ limiter.release(null);
98114
+ if (this.debug) {
98115
+ console.log(`[DEBUG] Released global AI concurrency slot on error`);
98116
+ }
98117
+ }
98118
+ throw error2;
98039
98119
  } finally {
98040
98120
  if (timeoutState.timeoutId) {
98041
98121
  clearTimeout(timeoutState.timeoutId);
@@ -98927,10 +99007,8 @@ Workspace: ${this.allowedFolders.join(", ")}`;
98927
99007
  toolDefinitions += `${taskToolDefinition}
98928
99008
  `;
98929
99009
  }
98930
- if (isToolAllowed("attempt_completion")) {
98931
- toolDefinitions += `${attemptCompletionToolDefinition}
99010
+ toolDefinitions += `${attemptCompletionToolDefinition}
98932
99011
  `;
98933
- }
98934
99012
  if (this.enableDelegate && isToolAllowed("delegate")) {
98935
99013
  toolDefinitions += `${delegateToolDefinition}
98936
99014
  `;
package/cjs/index.cjs CHANGED
@@ -35480,7 +35480,15 @@ function createTaskTool(options = {}) {
35480
35480
  });
35481
35481
  return `Error: Invalid task parameters - ${validation.error.message}`;
35482
35482
  }
35483
- const { action, tasks, id, title, description, status, priority, dependencies, after } = validation.data;
35483
+ const { action, tasks: rawTasks, id, title, description, status, priority, dependencies, after } = validation.data;
35484
+ let tasks = rawTasks;
35485
+ if (typeof rawTasks === "string") {
35486
+ try {
35487
+ tasks = JSON.parse(rawTasks);
35488
+ } catch (e4) {
35489
+ return `Error: Invalid tasks JSON - ${e4.message}`;
35490
+ }
35491
+ }
35484
35492
  switch (action) {
35485
35493
  case "create": {
35486
35494
  if (tasks && Array.isArray(tasks)) {
@@ -35655,7 +35663,8 @@ var init_taskTool = __esm({
35655
35663
  });
35656
35664
  taskSchema = external_exports.object({
35657
35665
  action: external_exports.enum(["create", "update", "complete", "delete", "list"]),
35658
- tasks: external_exports.array(external_exports.union([external_exports.string(), taskItemSchema])).optional(),
35666
+ // Accept both array and JSON string (AI models sometimes serialize as string)
35667
+ tasks: external_exports.union([external_exports.array(external_exports.union([external_exports.string(), taskItemSchema])), external_exports.string()]).optional(),
35659
35668
  id: external_exports.string().optional(),
35660
35669
  title: external_exports.string().optional(),
35661
35670
  description: external_exports.string().optional(),
@@ -35766,6 +35775,25 @@ SKIP TASKS for single-goal requests, even if they require multiple searches:
35766
35775
  **Key insight**: Multiple *internal steps* (search, read, analyze) are NOT the same as multiple *goals*.
35767
35776
  A single investigation with many steps is still ONE task, not many.
35768
35777
 
35778
+ ## Task Granularity
35779
+
35780
+ Tasks represent LOGICAL UNITS OF WORK, not individual files or steps:
35781
+ - "Fix 8 similar test files" \u2192 ONE task (same type of fix across files)
35782
+ - "Update API + tests + docs" \u2192 THREE tasks (different types of work)
35783
+ - "Implement feature in 5 files" \u2192 ONE task (single feature)
35784
+
35785
+ **Rule of thumb**: If you're creating more than 3-4 tasks, you're probably too granular.
35786
+
35787
+ **Anti-patterns to avoid**:
35788
+ - One task per file \u274C
35789
+ - One task per function \u274C
35790
+ - One task per repository (when same type of work) \u274C
35791
+
35792
+ **Good patterns**:
35793
+ - One task per distinct deliverable \u2713
35794
+ - One task per phase (implement, test, document) \u2713
35795
+ - One task per different type of work \u2713
35796
+
35769
35797
  MODIFY TASKS when (during execution):
35770
35798
  - You discover the problem is more complex than expected \u2192 Add new tasks
35771
35799
  - A single task covers too much scope \u2192 Split into smaller tasks
@@ -79234,6 +79262,7 @@ var require_pattern = __commonJS({
79234
79262
  "use strict";
79235
79263
  Object.defineProperty(exports2, "__esModule", { value: true });
79236
79264
  var code_1 = require_code2();
79265
+ var util_1 = require_util3();
79237
79266
  var codegen_1 = require_codegen();
79238
79267
  var error2 = {
79239
79268
  message: ({ schemaCode }) => (0, codegen_1.str)`must match pattern "${schemaCode}"`,
@@ -79246,10 +79275,18 @@ var require_pattern = __commonJS({
79246
79275
  $data: true,
79247
79276
  error: error2,
79248
79277
  code(cxt) {
79249
- const { data: data2, $data, schema, schemaCode, it } = cxt;
79278
+ const { gen, data: data2, $data, schema, schemaCode, it } = cxt;
79250
79279
  const u4 = it.opts.unicodeRegExp ? "u" : "";
79251
- const regExp = $data ? (0, codegen_1._)`(new RegExp(${schemaCode}, ${u4}))` : (0, code_1.usePattern)(cxt, schema);
79252
- cxt.fail$data((0, codegen_1._)`!${regExp}.test(${data2})`);
79280
+ if ($data) {
79281
+ const { regExp } = it.opts.code;
79282
+ const regExpCode = regExp.code === "new RegExp" ? (0, codegen_1._)`new RegExp` : (0, util_1.useFunc)(gen, regExp);
79283
+ const valid = gen.let("valid");
79284
+ gen.try(() => gen.assign(valid, (0, codegen_1._)`${regExpCode}(${schemaCode}, ${u4}).test(${data2})`), () => gen.assign(valid, false));
79285
+ cxt.fail$data((0, codegen_1._)`!${valid}`);
79286
+ } else {
79287
+ const regExp = (0, code_1.usePattern)(cxt, schema);
79288
+ cxt.fail$data((0, codegen_1._)`!${regExp}.test(${data2})`);
79289
+ }
79253
79290
  }
79254
79291
  };
79255
79292
  exports2.default = def;
@@ -93945,6 +93982,7 @@ var init_ProbeAgent = __esm({
93945
93982
  this.enableTasks = !!options.enableTasks;
93946
93983
  this.taskManager = null;
93947
93984
  this.delegationManager = new DelegationManager();
93985
+ this.concurrencyLimiter = options.concurrencyLimiter || null;
93948
93986
  this.requestTimeout = options.requestTimeout ?? (() => {
93949
93987
  if (process.env.REQUEST_TIMEOUT) {
93950
93988
  const parsed = parseInt(process.env.REQUEST_TIMEOUT, 10);
@@ -94321,6 +94359,8 @@ var init_ProbeAgent = __esm({
94321
94359
  model: this.clientApiModel,
94322
94360
  delegationManager: this.delegationManager,
94323
94361
  // Per-instance delegation limits
94362
+ concurrencyLimiter: this.concurrencyLimiter,
94363
+ // Global AI concurrency limiter
94324
94364
  isToolAllowed
94325
94365
  };
94326
94366
  const baseTools = createTools(configOptions);
@@ -94742,6 +94782,14 @@ var init_ProbeAgent = __esm({
94742
94782
  * @private
94743
94783
  */
94744
94784
  async streamTextWithRetryAndFallback(options) {
94785
+ const limiter = this.concurrencyLimiter;
94786
+ if (limiter) {
94787
+ await limiter.acquire(null);
94788
+ if (this.debug) {
94789
+ const stats = limiter.getStats();
94790
+ console.log(`[DEBUG] Acquired global AI concurrency slot (${stats.globalActive}/${stats.maxConcurrent}, queue: ${stats.queueSize})`);
94791
+ }
94792
+ }
94745
94793
  const controller = new AbortController();
94746
94794
  const timeoutState = { timeoutId: null };
94747
94795
  if (this.maxOperationTimeout && this.maxOperationTimeout > 0) {
@@ -94755,12 +94803,10 @@ var init_ProbeAgent = __esm({
94755
94803
  try {
94756
94804
  const useClaudeCode = this.clientApiProvider === "claude-code" || process.env.USE_CLAUDE_CODE === "true";
94757
94805
  const useCodex = this.clientApiProvider === "codex" || process.env.USE_CODEX === "true";
94806
+ let result;
94758
94807
  if (useClaudeCode || useCodex) {
94759
94808
  try {
94760
- const result = await this._tryEngineStreamPath(options, controller, timeoutState);
94761
- if (result) {
94762
- return result;
94763
- }
94809
+ result = await this._tryEngineStreamPath(options, controller, timeoutState);
94764
94810
  } catch (error2) {
94765
94811
  if (this.debug) {
94766
94812
  const engineType = useClaudeCode ? "Claude Code" : "Codex";
@@ -94768,7 +94814,37 @@ var init_ProbeAgent = __esm({
94768
94814
  }
94769
94815
  }
94770
94816
  }
94771
- return await this._executeWithVercelProvider(options, controller);
94817
+ if (!result) {
94818
+ result = await this._executeWithVercelProvider(options, controller);
94819
+ }
94820
+ if (limiter && result.textStream) {
94821
+ const originalStream = result.textStream;
94822
+ const debug = this.debug;
94823
+ result.textStream = (async function* () {
94824
+ try {
94825
+ for await (const chunk of originalStream) {
94826
+ yield chunk;
94827
+ }
94828
+ } finally {
94829
+ limiter.release(null);
94830
+ if (debug) {
94831
+ const stats = limiter.getStats();
94832
+ console.log(`[DEBUG] Released global AI concurrency slot (${stats.globalActive}/${stats.maxConcurrent}, queue: ${stats.queueSize})`);
94833
+ }
94834
+ }
94835
+ })();
94836
+ } else if (limiter) {
94837
+ limiter.release(null);
94838
+ }
94839
+ return result;
94840
+ } catch (error2) {
94841
+ if (limiter) {
94842
+ limiter.release(null);
94843
+ if (this.debug) {
94844
+ console.log(`[DEBUG] Released global AI concurrency slot on error`);
94845
+ }
94846
+ }
94847
+ throw error2;
94772
94848
  } finally {
94773
94849
  if (timeoutState.timeoutId) {
94774
94850
  clearTimeout(timeoutState.timeoutId);
@@ -95660,10 +95736,8 @@ Workspace: ${this.allowedFolders.join(", ")}`;
95660
95736
  toolDefinitions += `${taskToolDefinition}
95661
95737
  `;
95662
95738
  }
95663
- if (isToolAllowed("attempt_completion")) {
95664
- toolDefinitions += `${attemptCompletionToolDefinition}
95739
+ toolDefinitions += `${attemptCompletionToolDefinition}
95665
95740
  `;
95666
- }
95667
95741
  if (this.enableDelegate && isToolAllowed("delegate")) {
95668
95742
  toolDefinitions += `${delegateToolDefinition}
95669
95743
  `;
@@ -97604,8 +97678,10 @@ async function delegate({
97604
97678
  enableMcp = false,
97605
97679
  mcpConfig = null,
97606
97680
  mcpConfigPath = null,
97607
- delegationManager = null
97681
+ delegationManager = null,
97608
97682
  // Optional per-instance manager, falls back to default singleton
97683
+ concurrencyLimiter = null
97684
+ // Optional global AI concurrency limiter
97609
97685
  }) {
97610
97686
  if (!task || typeof task !== "string") {
97611
97687
  throw new Error("Task parameter is required and must be a string");
@@ -97681,8 +97757,10 @@ async function delegate({
97681
97757
  // Inherit from parent (subagent creates own MCPXmlBridge)
97682
97758
  mcpConfig,
97683
97759
  // Inherit from parent
97684
- mcpConfigPath
97760
+ mcpConfigPath,
97685
97761
  // Inherit from parent
97762
+ concurrencyLimiter
97763
+ // Inherit global AI concurrency limiter
97686
97764
  });
97687
97765
  if (debug) {
97688
97766
  console.error(`[DELEGATE] Created subagent with session ${sessionId}`);
@@ -97780,10 +97858,10 @@ var init_delegate = __esm({
97780
97858
  import_crypto9 = require("crypto");
97781
97859
  init_ProbeAgent();
97782
97860
  DelegationManager = class {
97783
- constructor() {
97784
- this.maxConcurrent = parseInt(process.env.MAX_CONCURRENT_DELEGATIONS || "3", 10);
97785
- this.maxPerSession = parseInt(process.env.MAX_DELEGATIONS_PER_SESSION || "10", 10);
97786
- this.defaultQueueTimeout = parseInt(process.env.DELEGATION_QUEUE_TIMEOUT || "60000", 10);
97861
+ constructor(options = {}) {
97862
+ this.maxConcurrent = options.maxConcurrent ?? parseInt(process.env.MAX_CONCURRENT_DELEGATIONS || "3", 10);
97863
+ this.maxPerSession = options.maxPerSession ?? parseInt(process.env.MAX_DELEGATIONS_PER_SESSION || "10", 10);
97864
+ this.defaultQueueTimeout = options.queueTimeout ?? parseInt(process.env.DELEGATION_QUEUE_TIMEOUT || "60000", 10);
97787
97865
  this.sessionDelegations = /* @__PURE__ */ new Map();
97788
97866
  this.globalActive = 0;
97789
97867
  this.waitQueue = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@probelabs/probe",
3
- "version": "0.6.0-rc230",
3
+ "version": "0.6.0-rc232",
4
4
  "description": "Node.js wrapper for the probe code search tool",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -79,7 +79,7 @@
79
79
  "@ai-sdk/openai": "^2.0.10",
80
80
  "@anthropic-ai/claude-agent-sdk": "^0.1.46",
81
81
  "@modelcontextprotocol/sdk": "^1.0.0",
82
- "@probelabs/maid": "^0.0.23",
82
+ "@probelabs/maid": "^0.0.24",
83
83
  "adm-zip": "^0.5.16",
84
84
  "ai": "^5.0.0",
85
85
  "ajv": "^8.17.1",
@@ -357,6 +357,10 @@ export class ProbeAgent {
357
357
  // Each ProbeAgent instance has its own limits, not shared globally
358
358
  this.delegationManager = new DelegationManager();
359
359
 
360
+ // Optional global concurrency limiter shared across all ProbeAgent instances.
361
+ // When set, every AI API call acquires a slot before calling the provider.
362
+ this.concurrencyLimiter = options.concurrencyLimiter || null;
363
+
360
364
  // Request timeout configuration (default 2 minutes)
361
365
  // Validates env var to prevent NaN or unreasonable values
362
366
  this.requestTimeout = options.requestTimeout ?? (() => {
@@ -824,6 +828,7 @@ export class ProbeAgent {
824
828
  provider: this.clientApiProvider,
825
829
  model: this.clientApiModel,
826
830
  delegationManager: this.delegationManager, // Per-instance delegation limits
831
+ concurrencyLimiter: this.concurrencyLimiter, // Global AI concurrency limiter
827
832
  isToolAllowed
828
833
  };
829
834
 
@@ -1363,6 +1368,16 @@ export class ProbeAgent {
1363
1368
  * @private
1364
1369
  */
1365
1370
  async streamTextWithRetryAndFallback(options) {
1371
+ // Acquire global concurrency slot if limiter is configured
1372
+ const limiter = this.concurrencyLimiter;
1373
+ if (limiter) {
1374
+ await limiter.acquire(null);
1375
+ if (this.debug) {
1376
+ const stats = limiter.getStats();
1377
+ console.log(`[DEBUG] Acquired global AI concurrency slot (${stats.globalActive}/${stats.maxConcurrent}, queue: ${stats.queueSize})`);
1378
+ }
1379
+ }
1380
+
1366
1381
  // Create AbortController for overall operation timeout
1367
1382
  const controller = new AbortController();
1368
1383
  const timeoutState = { timeoutId: null };
@@ -1382,12 +1397,10 @@ export class ProbeAgent {
1382
1397
  const useClaudeCode = this.clientApiProvider === 'claude-code' || process.env.USE_CLAUDE_CODE === 'true';
1383
1398
  const useCodex = this.clientApiProvider === 'codex' || process.env.USE_CODEX === 'true';
1384
1399
 
1400
+ let result;
1385
1401
  if (useClaudeCode || useCodex) {
1386
1402
  try {
1387
- const result = await this._tryEngineStreamPath(options, controller, timeoutState);
1388
- if (result) {
1389
- return result;
1390
- }
1403
+ result = await this._tryEngineStreamPath(options, controller, timeoutState);
1391
1404
  } catch (error) {
1392
1405
  if (this.debug) {
1393
1406
  const engineType = useClaudeCode ? 'Claude Code' : 'Codex';
@@ -1397,8 +1410,43 @@ export class ProbeAgent {
1397
1410
  }
1398
1411
  }
1399
1412
 
1400
- // Use Vercel AI SDK with retry/fallback
1401
- return await this._executeWithVercelProvider(options, controller);
1413
+ if (!result) {
1414
+ // Use Vercel AI SDK with retry/fallback
1415
+ result = await this._executeWithVercelProvider(options, controller);
1416
+ }
1417
+
1418
+ // Wrap textStream so limiter slot is held until stream completes
1419
+ if (limiter && result.textStream) {
1420
+ const originalStream = result.textStream;
1421
+ const debug = this.debug;
1422
+ result.textStream = (async function* () {
1423
+ try {
1424
+ for await (const chunk of originalStream) {
1425
+ yield chunk;
1426
+ }
1427
+ } finally {
1428
+ limiter.release(null);
1429
+ if (debug) {
1430
+ const stats = limiter.getStats();
1431
+ console.log(`[DEBUG] Released global AI concurrency slot (${stats.globalActive}/${stats.maxConcurrent}, queue: ${stats.queueSize})`);
1432
+ }
1433
+ }
1434
+ })();
1435
+ } else if (limiter) {
1436
+ // No textStream (shouldn't happen, but release just in case)
1437
+ limiter.release(null);
1438
+ }
1439
+
1440
+ return result;
1441
+ } catch (error) {
1442
+ // Release on error if limiter was acquired
1443
+ if (limiter) {
1444
+ limiter.release(null);
1445
+ if (this.debug) {
1446
+ console.log(`[DEBUG] Released global AI concurrency slot on error`);
1447
+ }
1448
+ }
1449
+ throw error;
1402
1450
  } finally {
1403
1451
  // Clean up timeout (for non-engine paths; engine paths clean up in the generator)
1404
1452
  if (timeoutState.timeoutId) {
@@ -2496,10 +2544,9 @@ ${extractGuidance}
2496
2544
  toolDefinitions += `${taskToolDefinition}\n`;
2497
2545
  }
2498
2546
 
2499
- // Always include attempt_completion (unless explicitly disabled in raw AI mode)
2500
- if (isToolAllowed('attempt_completion')) {
2501
- toolDefinitions += `${attemptCompletionToolDefinition}\n`;
2502
- }
2547
+ // Always include attempt_completion unconditionally - it's a completion signal, not a tool
2548
+ // This ensures agents can always complete their work, regardless of tool restrictions
2549
+ toolDefinitions += `${attemptCompletionToolDefinition}\n`;
2503
2550
 
2504
2551
  // Delegate tool (require both enableDelegate flag AND allowedTools permission)
2505
2552
  // Place after attempt_completion as it's an optional tool
@@ -3304,8 +3351,9 @@ Follow these instructions carefully:
3304
3351
  if (this.enableSkills && this.allowedTools.isEnabled('listSkills')) validTools.push('listSkills');
3305
3352
  if (this.enableSkills && this.allowedTools.isEnabled('useSkill')) validTools.push('useSkill');
3306
3353
  if (this.allowedTools.isEnabled('readImage')) validTools.push('readImage');
3307
- // Always allow attempt_completion - it's a completion signal, not a tool
3354
+ // Always allow attempt_completion in validTools - it's a completion signal, not a tool
3308
3355
  // This ensures agents can complete even when disableTools: true is set (fixes #333)
3356
+ // The tool DEFINITION may be hidden in raw AI mode, but we still need to recognize it
3309
3357
  validTools.push('attempt_completion');
3310
3358
 
3311
3359
  // Edit tools (require both allowEdit flag AND allowedTools permission)
@@ -23,7 +23,8 @@ export const taskItemSchema = z.object({
23
23
  */
24
24
  export const taskSchema = z.object({
25
25
  action: z.enum(['create', 'update', 'complete', 'delete', 'list']),
26
- tasks: z.array(z.union([z.string(), taskItemSchema])).optional(),
26
+ // Accept both array and JSON string (AI models sometimes serialize as string)
27
+ tasks: z.union([z.array(z.union([z.string(), taskItemSchema])), z.string()]).optional(),
27
28
  id: z.string().optional(),
28
29
  title: z.string().optional(),
29
30
  description: z.string().optional(),
@@ -142,6 +143,25 @@ SKIP TASKS for single-goal requests, even if they require multiple searches:
142
143
  **Key insight**: Multiple *internal steps* (search, read, analyze) are NOT the same as multiple *goals*.
143
144
  A single investigation with many steps is still ONE task, not many.
144
145
 
146
+ ## Task Granularity
147
+
148
+ Tasks represent LOGICAL UNITS OF WORK, not individual files or steps:
149
+ - "Fix 8 similar test files" → ONE task (same type of fix across files)
150
+ - "Update API + tests + docs" → THREE tasks (different types of work)
151
+ - "Implement feature in 5 files" → ONE task (single feature)
152
+
153
+ **Rule of thumb**: If you're creating more than 3-4 tasks, you're probably too granular.
154
+
155
+ **Anti-patterns to avoid**:
156
+ - One task per file ❌
157
+ - One task per function ❌
158
+ - One task per repository (when same type of work) ❌
159
+
160
+ **Good patterns**:
161
+ - One task per distinct deliverable ✓
162
+ - One task per phase (implement, test, document) ✓
163
+ - One task per different type of work ✓
164
+
145
165
  MODIFY TASKS when (during execution):
146
166
  - You discover the problem is more complex than expected → Add new tasks
147
167
  - A single task covers too much scope → Split into smaller tasks
@@ -314,7 +334,17 @@ export function createTaskTool(options = {}) {
314
334
  return `Error: Invalid task parameters - ${validation.error.message}`;
315
335
  }
316
336
 
317
- const { action, tasks, id, title, description, status, priority, dependencies, after } = validation.data;
337
+ const { action, tasks: rawTasks, id, title, description, status, priority, dependencies, after } = validation.data;
338
+
339
+ // Parse tasks if passed as JSON string (common AI model behavior)
340
+ let tasks = rawTasks;
341
+ if (typeof rawTasks === 'string') {
342
+ try {
343
+ tasks = JSON.parse(rawTasks);
344
+ } catch (e) {
345
+ return `Error: Invalid tasks JSON - ${e.message}`;
346
+ }
347
+ }
318
348
 
319
349
  switch (action) {
320
350
  case 'create': {
package/src/delegate.js CHANGED
@@ -19,11 +19,14 @@ import { ProbeAgent } from './agent/ProbeAgent.js';
19
19
  * - For long-running processes, periodic cleanup of stale sessions may be needed
20
20
  */
21
21
  class DelegationManager {
22
- constructor() {
23
- this.maxConcurrent = parseInt(process.env.MAX_CONCURRENT_DELEGATIONS || '3', 10);
24
- this.maxPerSession = parseInt(process.env.MAX_DELEGATIONS_PER_SESSION || '10', 10);
22
+ constructor(options = {}) {
23
+ this.maxConcurrent = options.maxConcurrent
24
+ ?? parseInt(process.env.MAX_CONCURRENT_DELEGATIONS || '3', 10);
25
+ this.maxPerSession = options.maxPerSession
26
+ ?? parseInt(process.env.MAX_DELEGATIONS_PER_SESSION || '10', 10);
25
27
  // Default queue timeout: 60 seconds. Set DELEGATION_QUEUE_TIMEOUT=0 to disable.
26
- this.defaultQueueTimeout = parseInt(process.env.DELEGATION_QUEUE_TIMEOUT || '60000', 10);
28
+ this.defaultQueueTimeout = options.queueTimeout
29
+ ?? parseInt(process.env.DELEGATION_QUEUE_TIMEOUT || '60000', 10);
27
30
 
28
31
  // Track delegations per session with timestamp for potential TTL cleanup
29
32
  // Map<string, { count: number, lastUpdated: number }>
@@ -353,6 +356,7 @@ const DEFAULT_DELEGATE_TIMEOUT = parseInt(process.env.DELEGATE_TIMEOUT, 10) || 3
353
356
  * @param {boolean} [options.enableMcp=false] - Enable MCP tool integration (inherited from parent)
354
357
  * @param {Object} [options.mcpConfig] - MCP configuration object (inherited from parent)
355
358
  * @param {string} [options.mcpConfigPath] - Path to MCP configuration file (inherited from parent)
359
+ * @param {Object} [options.concurrencyLimiter=null] - Global AI concurrency limiter (DelegationManager instance)
356
360
  * @returns {Promise<string>} The response from the delegate agent
357
361
  */
358
362
  export async function delegate({
@@ -379,7 +383,8 @@ export async function delegate({
379
383
  enableMcp = false,
380
384
  mcpConfig = null,
381
385
  mcpConfigPath = null,
382
- delegationManager = null // Optional per-instance manager, falls back to default singleton
386
+ delegationManager = null, // Optional per-instance manager, falls back to default singleton
387
+ concurrencyLimiter = null // Optional global AI concurrency limiter
383
388
  }) {
384
389
  if (!task || typeof task !== 'string') {
385
390
  throw new Error('Task parameter is required and must be a string');
@@ -464,7 +469,8 @@ export async function delegate({
464
469
  enableTasks, // Inherit from parent (subagent gets isolated TaskManager)
465
470
  enableMcp, // Inherit from parent (subagent creates own MCPXmlBridge)
466
471
  mcpConfig, // Inherit from parent
467
- mcpConfigPath // Inherit from parent
472
+ mcpConfigPath, // Inherit from parent
473
+ concurrencyLimiter // Inherit global AI concurrency limiter
468
474
  });
469
475
 
470
476
  if (debug) {