@probelabs/probe 0.6.0-rc278 → 0.6.0-rc279

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.
@@ -125,6 +125,27 @@ const MAX_HISTORY_MESSAGES = 100;
125
125
  // Maximum image file size (20MB) to prevent OOM attacks
126
126
  const MAX_IMAGE_FILE_SIZE = 20 * 1024 * 1024;
127
127
 
128
+ /**
129
+ * Truncate a string for debug logging, showing first and last portion.
130
+ */
131
+ export function debugTruncate(s, limit = 200) {
132
+ if (s.length <= limit) return s;
133
+ const half = Math.floor(limit / 2);
134
+ return s.substring(0, half) + ` ... [${s.length} chars] ... ` + s.substring(s.length - half);
135
+ }
136
+
137
+ /**
138
+ * Log tool results details for debug output.
139
+ */
140
+ export function debugLogToolResults(toolResults) {
141
+ if (!toolResults || toolResults.length === 0) return;
142
+ for (const tr of toolResults) {
143
+ const argsStr = JSON.stringify(tr.args || {});
144
+ const resultStr = typeof tr.result === 'string' ? tr.result : JSON.stringify(tr.result || '');
145
+ console.log(`[DEBUG] tool: ${tr.toolName} | args: ${debugTruncate(argsStr)} | result: ${debugTruncate(resultStr)}`);
146
+ }
147
+ }
148
+
128
149
  /**
129
150
  * ProbeAgent class to handle AI interactions with code search capabilities
130
151
  */
@@ -193,6 +214,7 @@ export class ProbeAgent {
193
214
  this.enableExecutePlan = !!options.enableExecutePlan;
194
215
  this.debug = options.debug || process.env.DEBUG === '1';
195
216
  this.cancelled = false;
217
+ this._abortController = new AbortController();
196
218
  this.tracer = options.tracer || null;
197
219
  this.outline = !!options.outline;
198
220
  this.searchDelegate = options.searchDelegate !== undefined ? !!options.searchDelegate : true;
@@ -792,6 +814,7 @@ export class ProbeAgent {
792
814
  searchDelegateProvider: this.searchDelegateProvider,
793
815
  searchDelegateModel: this.searchDelegateModel,
794
816
  delegationManager: this.delegationManager, // Per-instance delegation limits
817
+ parentAbortSignal: this._abortController.signal, // Propagate cancellation to delegations
795
818
  outputBuffer: this._outputBuffer,
796
819
  concurrencyLimiter: this.concurrencyLimiter, // Global AI concurrency limiter
797
820
  isToolAllowed,
@@ -1362,6 +1385,19 @@ export class ProbeAgent {
1362
1385
  const controller = new AbortController();
1363
1386
  const timeoutState = { timeoutId: null };
1364
1387
 
1388
+ // Link agent-level abort to this operation's controller
1389
+ // so that cancel() / cleanup() stops the current streamText call
1390
+ if (this._abortController.signal.aborted) {
1391
+ controller.abort();
1392
+ } else {
1393
+ const onAgentAbort = () => controller.abort();
1394
+ this._abortController.signal.addEventListener('abort', onAgentAbort, { once: true });
1395
+ // Clean up listener when this controller aborts (from any source)
1396
+ controller.signal.addEventListener('abort', () => {
1397
+ this._abortController.signal.removeEventListener('abort', onAgentAbort);
1398
+ }, { once: true });
1399
+ }
1400
+
1365
1401
  // Set up overall operation timeout (default 5 minutes)
1366
1402
  if (this.maxOperationTimeout && this.maxOperationTimeout > 0) {
1367
1403
  timeoutState.timeoutId = setTimeout(() => {
@@ -1729,7 +1765,8 @@ export class ProbeAgent {
1729
1765
  allowEdit: this.allowEdit,
1730
1766
  allowedTools: allowedToolsForDelegate,
1731
1767
  debug: this.debug,
1732
- tracer: this.tracer
1768
+ tracer: this.tracer,
1769
+ parentAbortSignal: this._abortController.signal
1733
1770
  };
1734
1771
 
1735
1772
  if (this.debug) {
@@ -3369,6 +3406,11 @@ Follow these instructions carefully:
3369
3406
  completionAttempted = true;
3370
3407
  }, toolContext);
3371
3408
 
3409
+ if (this.debug) {
3410
+ const toolNames = Object.keys(tools);
3411
+ console.log(`[DEBUG] Agent tools registered (${toolNames.length}): ${toolNames.join(', ')}`);
3412
+ }
3413
+
3372
3414
  let maxResponseTokens = this.maxResponseTokens;
3373
3415
  if (!maxResponseTokens) {
3374
3416
  maxResponseTokens = 4000;
@@ -3418,6 +3460,7 @@ Follow these instructions carefully:
3418
3460
 
3419
3461
  if (this.debug) {
3420
3462
  console.log(`[DEBUG] Step ${currentIteration}/${maxIterations} finished (reason: ${finishReason}, tools: ${toolResults?.length || 0})`);
3463
+ debugLogToolResults(toolResults);
3421
3464
  }
3422
3465
  }
3423
3466
  };
@@ -3629,6 +3672,7 @@ Double-check your response based on the criteria above. If everything looks good
3629
3672
  }
3630
3673
  if (this.debug) {
3631
3674
  console.log(`[DEBUG] Completion prompt step finished (reason: ${finishReason}, tools: ${toolResults?.length || 0})`);
3675
+ debugLogToolResults(toolResults);
3632
3676
  }
3633
3677
  }
3634
3678
  };
@@ -4545,6 +4589,11 @@ Convert your previous response content into actual JSON data that follows this s
4545
4589
  * Clean up resources (including MCP connections)
4546
4590
  */
4547
4591
  async cleanup() {
4592
+ // Abort any in-flight operations (delegations, streaming, etc.)
4593
+ if (!this._abortController.signal.aborted) {
4594
+ this._abortController.abort();
4595
+ }
4596
+
4548
4597
  // Clean up MCP bridge
4549
4598
  if (this.mcpBridge) {
4550
4599
  try {
@@ -4574,12 +4623,24 @@ Convert your previous response content into actual JSON data that follows this s
4574
4623
  }
4575
4624
 
4576
4625
  /**
4577
- * Cancel the current request
4626
+ * Cancel the current request and all in-flight delegations.
4627
+ * Aborts the internal AbortController so streamText, subagents,
4628
+ * and any code checking the signal will stop.
4578
4629
  */
4579
4630
  cancel() {
4580
4631
  this.cancelled = true;
4632
+ this._abortController.abort();
4581
4633
  if (this.debug) {
4582
4634
  console.log(`[DEBUG] Agent cancelled for session ${this.sessionId}`);
4583
4635
  }
4584
4636
  }
4637
+
4638
+ /**
4639
+ * Get the abort signal for this agent.
4640
+ * Delegations and subagents should check this signal.
4641
+ * @returns {AbortSignal}
4642
+ */
4643
+ get abortSignal() {
4644
+ return this._abortController.signal;
4645
+ }
4585
4646
  }
@@ -3862,12 +3862,17 @@ async function delegate({
3862
3862
  mcpConfigPath = null,
3863
3863
  delegationManager = null,
3864
3864
  // Optional per-instance manager, falls back to default singleton
3865
- concurrencyLimiter = null
3865
+ concurrencyLimiter = null,
3866
3866
  // Optional global AI concurrency limiter
3867
+ parentAbortSignal = null
3868
+ // Optional AbortSignal from parent to cancel this delegation
3867
3869
  }) {
3868
3870
  if (!task || typeof task !== "string") {
3869
3871
  throw new Error("Task parameter is required and must be a string");
3870
3872
  }
3873
+ if (parentAbortSignal?.aborted) {
3874
+ throw new Error("Delegation cancelled: parent operation was aborted");
3875
+ }
3871
3876
  const hasExplicitTimeout = Object.prototype.hasOwnProperty.call(arguments?.[0] ?? {}, "timeout");
3872
3877
  if (!hasExplicitTimeout) {
3873
3878
  const envTimeoutMs = parseInt(process.env.DELEGATION_TIMEOUT_MS || "", 10);
@@ -3952,12 +3957,37 @@ async function delegate({
3952
3957
  }
3953
3958
  const timeoutPromise = new Promise((_, reject2) => {
3954
3959
  timeoutId = setTimeout(() => {
3960
+ subagent.cancel();
3955
3961
  reject2(new Error(`Delegation timed out after ${timeout} seconds`));
3956
3962
  }, timeout * 1e3);
3957
3963
  });
3964
+ let parentAbortHandler;
3965
+ const parentAbortPromise = new Promise((_, reject2) => {
3966
+ if (parentAbortSignal) {
3967
+ if (parentAbortSignal.aborted) {
3968
+ subagent.cancel();
3969
+ reject2(new Error("Delegation cancelled: parent operation was aborted"));
3970
+ return;
3971
+ }
3972
+ parentAbortHandler = () => {
3973
+ subagent.cancel();
3974
+ reject2(new Error("Delegation cancelled: parent operation was aborted"));
3975
+ };
3976
+ parentAbortSignal.addEventListener("abort", parentAbortHandler, { once: true });
3977
+ }
3978
+ });
3958
3979
  const answerOptions = schema ? { schema } : void 0;
3959
3980
  const answerPromise = answerOptions ? subagent.answer(task, [], answerOptions) : subagent.answer(task);
3960
- const response = await Promise.race([answerPromise, timeoutPromise]);
3981
+ const racers = [answerPromise, timeoutPromise];
3982
+ if (parentAbortSignal) racers.push(parentAbortPromise);
3983
+ let response;
3984
+ try {
3985
+ response = await Promise.race(racers);
3986
+ } finally {
3987
+ if (parentAbortHandler && parentAbortSignal) {
3988
+ parentAbortSignal.removeEventListener("abort", parentAbortHandler);
3989
+ }
3990
+ }
3961
3991
  if (timeoutId !== null) {
3962
3992
  clearTimeout(timeoutId);
3963
3993
  timeoutId = null;
@@ -4369,8 +4399,9 @@ Instructions:
4369
4399
  promptType: "code-researcher",
4370
4400
  allowedTools: ["extract"],
4371
4401
  maxIterations: 5,
4372
- delegationManager: options.delegationManager
4402
+ delegationManager: options.delegationManager,
4373
4403
  // Per-instance delegation limits
4404
+ parentAbortSignal: options.parentAbortSignal || null
4374
4405
  // timeout removed - inherit default from delegate (300s)
4375
4406
  });
4376
4407
  return { chunk, result };
@@ -4469,8 +4500,9 @@ Organize all findings into clear categories with items listed under each.${compl
4469
4500
  promptType: "code-researcher",
4470
4501
  allowedTools: [],
4471
4502
  maxIterations: 5,
4472
- delegationManager: options.delegationManager
4503
+ delegationManager: options.delegationManager,
4473
4504
  // Per-instance delegation limits
4505
+ parentAbortSignal: options.parentAbortSignal || null
4474
4506
  // timeout removed - inherit default from delegate (300s)
4475
4507
  });
4476
4508
  return result;
@@ -4534,8 +4566,9 @@ CRITICAL: Do NOT guess keywords. Actually run searches and see what returns resu
4534
4566
  promptType: "code-researcher",
4535
4567
  // Full tool access for exploration and experimentation
4536
4568
  maxIterations: 15,
4537
- delegationManager: options.delegationManager
4569
+ delegationManager: options.delegationManager,
4538
4570
  // Per-instance delegation limits
4571
+ parentAbortSignal: options.parentAbortSignal || null
4539
4572
  // timeout removed - inherit default from delegate (300s)
4540
4573
  });
4541
4574
  const plan = parsePlanningResult(stripResultTags(result));
@@ -4592,8 +4625,9 @@ When done, use the attempt_completion tool with your answer as the result.`;
4592
4625
  promptType: "code-researcher",
4593
4626
  allowedTools: [],
4594
4627
  maxIterations: 5,
4595
- delegationManager: options.delegationManager
4628
+ delegationManager: options.delegationManager,
4596
4629
  // Per-instance delegation limits
4630
+ parentAbortSignal: options.parentAbortSignal || null
4597
4631
  // timeout removed - inherit default from delegate (300s)
4598
4632
  });
4599
4633
  return stripResultTags(result);
@@ -9277,7 +9311,8 @@ var init_vercel = __esm({
9277
9311
  promptType: "code-searcher",
9278
9312
  allowedTools: ["search", "extract", "listFiles", "attempt_completion"],
9279
9313
  searchDelegate: false,
9280
- schema: CODE_SEARCH_SCHEMA
9314
+ schema: CODE_SEARCH_SCHEMA,
9315
+ parentAbortSignal: options.parentAbortSignal || null
9281
9316
  });
9282
9317
  const delegateResult = options.tracer?.withSpan ? await options.tracer.withSpan("search.delegate", runDelegation, {
9283
9318
  "search.query": searchQuery,
@@ -9491,7 +9526,7 @@ var init_vercel = __esm({
9491
9526
  name: "delegate",
9492
9527
  description: delegateDescription,
9493
9528
  inputSchema: delegateSchema,
9494
- execute: async ({ task, currentIteration, maxIterations, parentSessionId, path: path9, provider, model, tracer, searchDelegate }) => {
9529
+ execute: async ({ task, currentIteration, maxIterations, parentSessionId, path: path9, provider, model, tracer, searchDelegate, parentAbortSignal }) => {
9495
9530
  if (!task || typeof task !== "string") {
9496
9531
  throw new Error("Task parameter is required and must be a non-empty string");
9497
9532
  }
@@ -9549,8 +9584,9 @@ var init_vercel = __esm({
9549
9584
  enableMcp,
9550
9585
  mcpConfig,
9551
9586
  mcpConfigPath,
9552
- delegationManager
9587
+ delegationManager,
9553
9588
  // Per-instance delegation limits
9589
+ parentAbortSignal
9554
9590
  });
9555
9591
  return result;
9556
9592
  }
@@ -9588,8 +9624,9 @@ var init_vercel = __esm({
9588
9624
  provider: options.provider,
9589
9625
  model: options.model,
9590
9626
  tracer: options.tracer,
9591
- delegationManager
9627
+ delegationManager,
9592
9628
  // Per-instance delegation limits
9629
+ parentAbortSignal: options.parentAbortSignal || null
9593
9630
  });
9594
9631
  return result;
9595
9632
  } catch (error) {
@@ -81768,7 +81805,9 @@ __export(ProbeAgent_exports, {
81768
81805
  ENGINE_ACTIVITY_TIMEOUT_DEFAULT: () => ENGINE_ACTIVITY_TIMEOUT_DEFAULT,
81769
81806
  ENGINE_ACTIVITY_TIMEOUT_MAX: () => ENGINE_ACTIVITY_TIMEOUT_MAX,
81770
81807
  ENGINE_ACTIVITY_TIMEOUT_MIN: () => ENGINE_ACTIVITY_TIMEOUT_MIN,
81771
- ProbeAgent: () => ProbeAgent
81808
+ ProbeAgent: () => ProbeAgent,
81809
+ debugLogToolResults: () => debugLogToolResults,
81810
+ debugTruncate: () => debugTruncate
81772
81811
  });
81773
81812
  import dotenv2 from "dotenv";
81774
81813
  import { createAnthropic as createAnthropic2 } from "@ai-sdk/anthropic";
@@ -81781,6 +81820,19 @@ import { EventEmitter as EventEmitter5 } from "events";
81781
81820
  import { existsSync as existsSync7 } from "fs";
81782
81821
  import { readFile as readFile3, stat, readdir as readdir3 } from "fs/promises";
81783
81822
  import { resolve as resolve7, isAbsolute as isAbsolute6, dirname as dirname5, basename, normalize as normalize2, sep as sep5 } from "path";
81823
+ function debugTruncate(s, limit = 200) {
81824
+ if (s.length <= limit) return s;
81825
+ const half = Math.floor(limit / 2);
81826
+ return s.substring(0, half) + ` ... [${s.length} chars] ... ` + s.substring(s.length - half);
81827
+ }
81828
+ function debugLogToolResults(toolResults) {
81829
+ if (!toolResults || toolResults.length === 0) return;
81830
+ for (const tr of toolResults) {
81831
+ const argsStr = JSON.stringify(tr.args || {});
81832
+ const resultStr = typeof tr.result === "string" ? tr.result : JSON.stringify(tr.result || "");
81833
+ console.log(`[DEBUG] tool: ${tr.toolName} | args: ${debugTruncate(argsStr)} | result: ${debugTruncate(resultStr)}`);
81834
+ }
81835
+ }
81784
81836
  var ENGINE_ACTIVITY_TIMEOUT_DEFAULT, ENGINE_ACTIVITY_TIMEOUT_MIN, ENGINE_ACTIVITY_TIMEOUT_MAX, MAX_TOOL_ITERATIONS, MAX_HISTORY_MESSAGES, MAX_IMAGE_FILE_SIZE, ProbeAgent;
81785
81837
  var init_ProbeAgent = __esm({
81786
81838
  "src/agent/ProbeAgent.js"() {
@@ -81889,6 +81941,7 @@ var init_ProbeAgent = __esm({
81889
81941
  this.enableExecutePlan = !!options.enableExecutePlan;
81890
81942
  this.debug = options.debug || process.env.DEBUG === "1";
81891
81943
  this.cancelled = false;
81944
+ this._abortController = new AbortController();
81892
81945
  this.tracer = options.tracer || null;
81893
81946
  this.outline = !!options.outline;
81894
81947
  this.searchDelegate = options.searchDelegate !== void 0 ? !!options.searchDelegate : true;
@@ -82334,6 +82387,8 @@ var init_ProbeAgent = __esm({
82334
82387
  searchDelegateModel: this.searchDelegateModel,
82335
82388
  delegationManager: this.delegationManager,
82336
82389
  // Per-instance delegation limits
82390
+ parentAbortSignal: this._abortController.signal,
82391
+ // Propagate cancellation to delegations
82337
82392
  outputBuffer: this._outputBuffer,
82338
82393
  concurrencyLimiter: this.concurrencyLimiter,
82339
82394
  // Global AI concurrency limiter
@@ -82781,6 +82836,15 @@ var init_ProbeAgent = __esm({
82781
82836
  }
82782
82837
  const controller = new AbortController();
82783
82838
  const timeoutState = { timeoutId: null };
82839
+ if (this._abortController.signal.aborted) {
82840
+ controller.abort();
82841
+ } else {
82842
+ const onAgentAbort = () => controller.abort();
82843
+ this._abortController.signal.addEventListener("abort", onAgentAbort, { once: true });
82844
+ controller.signal.addEventListener("abort", () => {
82845
+ this._abortController.signal.removeEventListener("abort", onAgentAbort);
82846
+ }, { once: true });
82847
+ }
82784
82848
  if (this.maxOperationTimeout && this.maxOperationTimeout > 0) {
82785
82849
  timeoutState.timeoutId = setTimeout(() => {
82786
82850
  controller.abort();
@@ -83084,7 +83148,8 @@ var init_ProbeAgent = __esm({
83084
83148
  allowEdit: this.allowEdit,
83085
83149
  allowedTools: allowedToolsForDelegate,
83086
83150
  debug: this.debug,
83087
- tracer: this.tracer
83151
+ tracer: this.tracer,
83152
+ parentAbortSignal: this._abortController.signal
83088
83153
  };
83089
83154
  if (this.debug) {
83090
83155
  console.log(`[DEBUG] Executing delegate tool`);
@@ -84401,6 +84466,10 @@ You are working with a workspace. Available paths: ${workspaceDesc}
84401
84466
  completionResult = result;
84402
84467
  completionAttempted = true;
84403
84468
  }, toolContext);
84469
+ if (this.debug) {
84470
+ const toolNames = Object.keys(tools2);
84471
+ console.log(`[DEBUG] Agent tools registered (${toolNames.length}): ${toolNames.join(", ")}`);
84472
+ }
84404
84473
  let maxResponseTokens = this.maxResponseTokens;
84405
84474
  if (!maxResponseTokens) {
84406
84475
  maxResponseTokens = 4e3;
@@ -84440,6 +84509,7 @@ You are working with a workspace. Available paths: ${workspaceDesc}
84440
84509
  }
84441
84510
  if (this.debug) {
84442
84511
  console.log(`[DEBUG] Step ${currentIteration}/${maxIterations} finished (reason: ${finishReason}, tools: ${toolResults?.length || 0})`);
84512
+ debugLogToolResults(toolResults);
84443
84513
  }
84444
84514
  }
84445
84515
  };
@@ -84596,6 +84666,7 @@ Double-check your response based on the criteria above. If everything looks good
84596
84666
  }
84597
84667
  if (this.debug) {
84598
84668
  console.log(`[DEBUG] Completion prompt step finished (reason: ${finishReason}, tools: ${toolResults?.length || 0})`);
84669
+ debugLogToolResults(toolResults);
84599
84670
  }
84600
84671
  }
84601
84672
  };
@@ -85306,6 +85377,9 @@ Convert your previous response content into actual JSON data that follows this s
85306
85377
  * Clean up resources (including MCP connections)
85307
85378
  */
85308
85379
  async cleanup() {
85380
+ if (!this._abortController.signal.aborted) {
85381
+ this._abortController.abort();
85382
+ }
85309
85383
  if (this.mcpBridge) {
85310
85384
  try {
85311
85385
  await this.mcpBridge.cleanup();
@@ -85329,14 +85403,25 @@ Convert your previous response content into actual JSON data that follows this s
85329
85403
  this.clearHistory();
85330
85404
  }
85331
85405
  /**
85332
- * Cancel the current request
85406
+ * Cancel the current request and all in-flight delegations.
85407
+ * Aborts the internal AbortController so streamText, subagents,
85408
+ * and any code checking the signal will stop.
85333
85409
  */
85334
85410
  cancel() {
85335
85411
  this.cancelled = true;
85412
+ this._abortController.abort();
85336
85413
  if (this.debug) {
85337
85414
  console.log(`[DEBUG] Agent cancelled for session ${this.sessionId}`);
85338
85415
  }
85339
85416
  }
85417
+ /**
85418
+ * Get the abort signal for this agent.
85419
+ * Delegations and subagents should check this signal.
85420
+ * @returns {AbortSignal}
85421
+ */
85422
+ get abortSignal() {
85423
+ return this._abortController.signal;
85424
+ }
85340
85425
  };
85341
85426
  }
85342
85427
  });
package/build/delegate.js CHANGED
@@ -386,12 +386,18 @@ export async function delegate({
386
386
  mcpConfig = null,
387
387
  mcpConfigPath = null,
388
388
  delegationManager = null, // Optional per-instance manager, falls back to default singleton
389
- concurrencyLimiter = null // Optional global AI concurrency limiter
389
+ concurrencyLimiter = null, // Optional global AI concurrency limiter
390
+ parentAbortSignal = null // Optional AbortSignal from parent to cancel this delegation
390
391
  }) {
391
392
  if (!task || typeof task !== 'string') {
392
393
  throw new Error('Task parameter is required and must be a string');
393
394
  }
394
395
 
396
+ // Check if parent has already been cancelled
397
+ if (parentAbortSignal?.aborted) {
398
+ throw new Error('Delegation cancelled: parent operation was aborted');
399
+ }
400
+
395
401
  // Support runtime timeout override via environment variables when timeout not explicitly passed
396
402
  // This allows operators to configure delegation timeouts without code changes
397
403
  // Priority: DELEGATION_TIMEOUT_MS (milliseconds) > DELEGATION_TIMEOUT_SECONDS > DELEGATION_TIMEOUT (seconds)
@@ -481,24 +487,47 @@ export async function delegate({
481
487
  console.error(`[DELEGATE] Subagent config: promptType=${promptType}, enableDelegate=false, maxIterations=${remainingIterations}`);
482
488
  }
483
489
 
484
- // Set up timeout with proper cleanup
485
- // TODO: Implement AbortController support in ProbeAgent.answer() for proper cancellation
486
- // Current limitation: When timeout occurs, subagent.answer() continues running in background
487
- // This is acceptable since:
488
- // 1. The promise will eventually resolve/reject and be garbage collected
489
- // 2. The delegation slot is properly released on timeout
490
- // 3. The parent receives timeout error and can handle it
491
- // Future improvement: Add signal parameter to ProbeAgent.answer(task, [], { signal })
490
+ // Set up timeout and parent abort handling.
491
+ // When timeout fires or parent aborts, we cancel the subagent so it
492
+ // stops making API calls and releases resources promptly.
492
493
  const timeoutPromise = new Promise((_, reject) => {
493
494
  timeoutId = setTimeout(() => {
495
+ subagent.cancel();
494
496
  reject(new Error(`Delegation timed out after ${timeout} seconds`));
495
497
  }, timeout * 1000);
496
498
  });
497
499
 
498
- // Execute the task with timeout
500
+ // Listen for parent abort signal
501
+ let parentAbortHandler;
502
+ const parentAbortPromise = new Promise((_, reject) => {
503
+ if (parentAbortSignal) {
504
+ if (parentAbortSignal.aborted) {
505
+ subagent.cancel();
506
+ reject(new Error('Delegation cancelled: parent operation was aborted'));
507
+ return;
508
+ }
509
+ parentAbortHandler = () => {
510
+ subagent.cancel();
511
+ reject(new Error('Delegation cancelled: parent operation was aborted'));
512
+ };
513
+ parentAbortSignal.addEventListener('abort', parentAbortHandler, { once: true });
514
+ }
515
+ });
516
+
517
+ // Execute the task with timeout and parent abort
499
518
  const answerOptions = schema ? { schema } : undefined;
500
519
  const answerPromise = answerOptions ? subagent.answer(task, [], answerOptions) : subagent.answer(task);
501
- const response = await Promise.race([answerPromise, timeoutPromise]);
520
+ const racers = [answerPromise, timeoutPromise];
521
+ if (parentAbortSignal) racers.push(parentAbortPromise);
522
+ let response;
523
+ try {
524
+ response = await Promise.race(racers);
525
+ } finally {
526
+ // Clean up parent abort listener to prevent memory leaks
527
+ if (parentAbortHandler && parentAbortSignal) {
528
+ parentAbortSignal.removeEventListener('abort', parentAbortHandler);
529
+ }
530
+ }
502
531
 
503
532
  // Clear timeout immediately after race completes to prevent memory leak
504
533
  // Note: timeoutId is always set by this point (synchronous in Promise constructor)
@@ -192,7 +192,8 @@ Instructions:
192
192
  promptType: 'code-researcher',
193
193
  allowedTools: ['extract'],
194
194
  maxIterations: 5,
195
- delegationManager: options.delegationManager // Per-instance delegation limits
195
+ delegationManager: options.delegationManager, // Per-instance delegation limits
196
+ parentAbortSignal: options.parentAbortSignal || null
196
197
  // timeout removed - inherit default from delegate (300s)
197
198
  });
198
199
 
@@ -328,7 +329,8 @@ Organize all findings into clear categories with items listed under each.${compl
328
329
  promptType: 'code-researcher',
329
330
  allowedTools: [],
330
331
  maxIterations: 5,
331
- delegationManager: options.delegationManager // Per-instance delegation limits
332
+ delegationManager: options.delegationManager, // Per-instance delegation limits
333
+ parentAbortSignal: options.parentAbortSignal || null
332
334
  // timeout removed - inherit default from delegate (300s)
333
335
  });
334
336
 
@@ -404,7 +406,8 @@ CRITICAL: Do NOT guess keywords. Actually run searches and see what returns resu
404
406
  promptType: 'code-researcher',
405
407
  // Full tool access for exploration and experimentation
406
408
  maxIterations: 15,
407
- delegationManager: options.delegationManager // Per-instance delegation limits
409
+ delegationManager: options.delegationManager, // Per-instance delegation limits
410
+ parentAbortSignal: options.parentAbortSignal || null
408
411
  // timeout removed - inherit default from delegate (300s)
409
412
  });
410
413
 
@@ -475,7 +478,8 @@ When done, use the attempt_completion tool with your answer as the result.`;
475
478
  promptType: 'code-researcher',
476
479
  allowedTools: [],
477
480
  maxIterations: 5,
478
- delegationManager: options.delegationManager // Per-instance delegation limits
481
+ delegationManager: options.delegationManager, // Per-instance delegation limits
482
+ parentAbortSignal: options.parentAbortSignal || null
479
483
  // timeout removed - inherit default from delegate (300s)
480
484
  });
481
485
 
@@ -277,7 +277,8 @@ export const searchTool = (options = {}) => {
277
277
  promptType: 'code-searcher',
278
278
  allowedTools: ['search', 'extract', 'listFiles', 'attempt_completion'],
279
279
  searchDelegate: false,
280
- schema: CODE_SEARCH_SCHEMA
280
+ schema: CODE_SEARCH_SCHEMA,
281
+ parentAbortSignal: options.parentAbortSignal || null
281
282
  });
282
283
 
283
284
  const delegateResult = options.tracer?.withSpan
@@ -581,7 +582,7 @@ export const delegateTool = (options = {}) => {
581
582
  name: 'delegate',
582
583
  description: delegateDescription,
583
584
  inputSchema: delegateSchema,
584
- execute: async ({ task, currentIteration, maxIterations, parentSessionId, path, provider, model, tracer, searchDelegate }) => {
585
+ execute: async ({ task, currentIteration, maxIterations, parentSessionId, path, provider, model, tracer, searchDelegate, parentAbortSignal }) => {
585
586
  // Validate required parameters - throw errors for consistency
586
587
  if (!task || typeof task !== 'string') {
587
588
  throw new Error('Task parameter is required and must be a non-empty string');
@@ -673,7 +674,8 @@ export const delegateTool = (options = {}) => {
673
674
  enableMcp,
674
675
  mcpConfig,
675
676
  mcpConfigPath,
676
- delegationManager // Per-instance delegation limits
677
+ delegationManager, // Per-instance delegation limits
678
+ parentAbortSignal
677
679
  });
678
680
 
679
681
  return result;
@@ -733,7 +735,8 @@ export const analyzeAllTool = (options = {}) => {
733
735
  provider: options.provider,
734
736
  model: options.model,
735
737
  tracer: options.tracer,
736
- delegationManager // Per-instance delegation limits
738
+ delegationManager, // Per-instance delegation limits
739
+ parentAbortSignal: options.parentAbortSignal || null
737
740
  });
738
741
 
739
742
  return result;