@probelabs/probe 0.6.0-rc277 → 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.
package/cjs/index.cjs CHANGED
@@ -36120,7 +36120,7 @@ ${taskManager.formatTasksForPrompt()}`;
36120
36120
  }
36121
36121
  };
36122
36122
  }
36123
- var taskItemSchema, taskSchema, taskSystemPrompt, taskGuidancePrompt;
36123
+ var taskItemSchema, taskSchema, taskSystemPrompt;
36124
36124
  var init_taskTool = __esm({
36125
36125
  "src/agent/tasks/taskTool.js"() {
36126
36126
  "use strict";
@@ -36181,11 +36181,6 @@ Tasks = logical units of work, not files or steps.
36181
36181
  - Circular dependencies are rejected
36182
36182
  - attempt_completion is blocked while tasks remain unresolved
36183
36183
  `;
36184
- taskGuidancePrompt = `Does this request have MULTIPLE DISTINCT GOALS?
36185
- - "Do A AND B AND C" (multiple goals) \u2192 Create tasks for each goal
36186
- - "Investigate/explain/find X" (single goal) \u2192 Skip tasks, just answer directly
36187
- Multiple internal steps for ONE goal = NO tasks needed.
36188
- If creating tasks, use the task tool with action="create" first.`;
36189
36184
  }
36190
36185
  });
36191
36186
 
@@ -82671,8 +82666,20 @@ If the solution is clear, you can jump to implementation right away. If not, ask
82671
82666
  - Check imports and existing utilities before creating new helpers \u2014 the project may already have what you need.
82672
82667
 
82673
82668
  # Task Planning
82674
- - If the task tool is available, use it to break complex work into milestones before starting implementation.
82675
- - Stay flexible \u2014 if your understanding changes mid-task, add, remove, or reorganize tasks as needed. The plan should serve you, not constrain you.
82669
+ When the request has **multiple distinct goals** (e.g. "Fix bug A AND add feature B"), use the task tool to track them:
82670
+ - Call the task tool with action="create" and a tasks array. Each task must have an "id" field.
82671
+ - Update task status to "in_progress" when starting and "completed" when done.
82672
+ - All tasks must be completed or cancelled before calling attempt_completion.
82673
+ - Stay flexible \u2014 add, remove, or reorganize tasks as your understanding changes.
82674
+
82675
+ Do NOT create tasks for single-goal requests, even complex ones. Multiple internal steps for one goal (search, read, analyze, implement) do not need tasks.
82676
+
82677
+ # Discovering Project Commands
82678
+ Before building or testing, determine the project's toolchain:
82679
+ - Check for Makefile, package.json (scripts), Cargo.toml, go.mod, pyproject.toml, or similar
82680
+ - Look for CI config (.github/workflows/, .gitlab-ci.yml) to see what commands CI runs
82681
+ - Read README for build/test instructions if the above are unclear
82682
+ - Common patterns: \`make build\`/\`make test\`, \`npm run build\`/\`npm test\`, \`cargo build\`/\`cargo test\`, \`go build ./...\`/\`go test ./...\`, \`python -m pytest\`
82676
82683
 
82677
82684
  # During Implementation
82678
82685
  - Always create a new branch before making changes to the codebase.
@@ -82683,12 +82690,22 @@ If the solution is clear, you can jump to implementation right away. If not, ask
82683
82690
  - When editing files, keep edits focused and minimal. For changes spanning more than a few lines, prefer line-targeted editing (start_line/end_line) over text replacement (old_string) \u2014 it constrains scope and prevents accidental removal of adjacent content. Never include unrelated sections in an edit operation.
82684
82691
  - After every significant change, verify the project still builds and passes linting. Do not wait until the end to discover breakage.
82685
82692
 
82686
- # After Implementation
82687
- - Verify the project builds successfully. If it doesn't, fix the build before moving on.
82688
- - Run lint and typecheck commands if known for the project. Fix any new warnings or errors you introduced.
82689
- - Add tests for any new or changed functionality. Tests must cover the main path and important edge cases.
82690
- - Run the project's full test suite. If any tests fail (including pre-existing ones you may have broken), fix them before finishing.
82691
- - When the task is done, respond to the user with a concise summary of what was implemented, what files were changed, and any relevant details. Include links (e.g. pull request URL) so the user has everything they need.
82693
+ # Writing Tests
82694
+ Every change must include tests. Before writing them:
82695
+ - Find existing test files for the module you changed \u2014 look in \`tests/\`, \`__tests__/\`, \`*_test.go\`, \`*.test.js\`, \`*.spec.ts\`, or co-located test modules (\`#[cfg(test)]\` in Rust).
82696
+ - Read those tests to understand the project's testing patterns: framework, assertion style, mocking approach, file naming, test organization.
82697
+ - Prefer extending an existing test file over creating a new one when your change is in the same module.
82698
+ - Write tests that cover the main path and important edge cases. Include a failing-input test when relevant.
82699
+ - When fixing a bug, write a failing test first that reproduces the bug, then fix the code to make it pass.
82700
+
82701
+ # Verify Changes
82702
+ Before committing or creating a PR, run through this checklist:
82703
+ 1. **Build** \u2014 run the project-appropriate build command (go build, npm run build, cargo build, make, etc.). Fix any compilation errors.
82704
+ 2. **Lint & typecheck** \u2014 run linter/formatter if the project has one (eslint, clippy, golangci-lint, etc.). Fix any new warnings.
82705
+ 3. **Test** \u2014 run the full test suite (go test ./..., npm test, cargo test, make test, pytest, etc.). Fix any failures, including pre-existing tests you may have broken.
82706
+ 4. **Review** \u2014 re-read your diff. Ensure no debug code, no unrelated changes, no secrets, no missing files.
82707
+
82708
+ Do NOT skip verification. Do NOT proceed to PR creation with a broken build or failing tests.
82692
82709
 
82693
82710
  # GitHub Integration
82694
82711
  - Use the \`gh\` CLI for all GitHub operations: issues, pull requests, checks, releases.
@@ -105948,8 +105965,23 @@ __export(ProbeAgent_exports, {
105948
105965
  ENGINE_ACTIVITY_TIMEOUT_DEFAULT: () => ENGINE_ACTIVITY_TIMEOUT_DEFAULT,
105949
105966
  ENGINE_ACTIVITY_TIMEOUT_MAX: () => ENGINE_ACTIVITY_TIMEOUT_MAX,
105950
105967
  ENGINE_ACTIVITY_TIMEOUT_MIN: () => ENGINE_ACTIVITY_TIMEOUT_MIN,
105951
- ProbeAgent: () => ProbeAgent
105968
+ ProbeAgent: () => ProbeAgent,
105969
+ debugLogToolResults: () => debugLogToolResults,
105970
+ debugTruncate: () => debugTruncate
105952
105971
  });
105972
+ function debugTruncate(s5, limit = 200) {
105973
+ if (s5.length <= limit) return s5;
105974
+ const half = Math.floor(limit / 2);
105975
+ return s5.substring(0, half) + ` ... [${s5.length} chars] ... ` + s5.substring(s5.length - half);
105976
+ }
105977
+ function debugLogToolResults(toolResults) {
105978
+ if (!toolResults || toolResults.length === 0) return;
105979
+ for (const tr of toolResults) {
105980
+ const argsStr = JSON.stringify(tr.args || {});
105981
+ const resultStr = typeof tr.result === "string" ? tr.result : JSON.stringify(tr.result || "");
105982
+ console.log(`[DEBUG] tool: ${tr.toolName} | args: ${debugTruncate(argsStr)} | result: ${debugTruncate(resultStr)}`);
105983
+ }
105984
+ }
105953
105985
  var import_dotenv, import_anthropic2, import_openai2, import_google2, import_ai4, import_crypto8, import_events4, import_fs10, import_promises6, import_path15, ENGINE_ACTIVITY_TIMEOUT_DEFAULT, ENGINE_ACTIVITY_TIMEOUT_MIN, ENGINE_ACTIVITY_TIMEOUT_MAX, MAX_TOOL_ITERATIONS, MAX_HISTORY_MESSAGES, MAX_IMAGE_FILE_SIZE, ProbeAgent;
105954
105986
  var init_ProbeAgent = __esm({
105955
105987
  "src/agent/ProbeAgent.js"() {
@@ -106069,6 +106101,7 @@ var init_ProbeAgent = __esm({
106069
106101
  this.enableExecutePlan = !!options.enableExecutePlan;
106070
106102
  this.debug = options.debug || process.env.DEBUG === "1";
106071
106103
  this.cancelled = false;
106104
+ this._abortController = new AbortController();
106072
106105
  this.tracer = options.tracer || null;
106073
106106
  this.outline = !!options.outline;
106074
106107
  this.searchDelegate = options.searchDelegate !== void 0 ? !!options.searchDelegate : true;
@@ -106514,6 +106547,8 @@ var init_ProbeAgent = __esm({
106514
106547
  searchDelegateModel: this.searchDelegateModel,
106515
106548
  delegationManager: this.delegationManager,
106516
106549
  // Per-instance delegation limits
106550
+ parentAbortSignal: this._abortController.signal,
106551
+ // Propagate cancellation to delegations
106517
106552
  outputBuffer: this._outputBuffer,
106518
106553
  concurrencyLimiter: this.concurrencyLimiter,
106519
106554
  // Global AI concurrency limiter
@@ -106961,6 +106996,15 @@ var init_ProbeAgent = __esm({
106961
106996
  }
106962
106997
  const controller = new AbortController();
106963
106998
  const timeoutState = { timeoutId: null };
106999
+ if (this._abortController.signal.aborted) {
107000
+ controller.abort();
107001
+ } else {
107002
+ const onAgentAbort = () => controller.abort();
107003
+ this._abortController.signal.addEventListener("abort", onAgentAbort, { once: true });
107004
+ controller.signal.addEventListener("abort", () => {
107005
+ this._abortController.signal.removeEventListener("abort", onAgentAbort);
107006
+ }, { once: true });
107007
+ }
106964
107008
  if (this.maxOperationTimeout && this.maxOperationTimeout > 0) {
106965
107009
  timeoutState.timeoutId = setTimeout(() => {
106966
107010
  controller.abort();
@@ -107264,7 +107308,8 @@ var init_ProbeAgent = __esm({
107264
107308
  allowEdit: this.allowEdit,
107265
107309
  allowedTools: allowedToolsForDelegate,
107266
107310
  debug: this.debug,
107267
- tracer: this.tracer
107311
+ tracer: this.tracer,
107312
+ parentAbortSignal: this._abortController.signal
107268
107313
  };
107269
107314
  if (this.debug) {
107270
107315
  console.log(`[DEBUG] Executing delegate tool`);
@@ -108426,12 +108471,6 @@ You are working with a workspace. Available paths: ${workspaceDesc}
108426
108471
  });
108427
108472
  const systemMessage = await this.getSystemMessage();
108428
108473
  let userMessage = { role: "user", content: message.trim() };
108429
- if (this.enableTasks) {
108430
- userMessage.content = userMessage.content + "\n\n" + taskGuidancePrompt;
108431
- if (this.debug) {
108432
- console.log("[DEBUG] Task guidance injected into user message");
108433
- }
108434
- }
108435
108474
  if (options.schema && !options._schemaFormatted) {
108436
108475
  const schemaInstructions = generateSchemaInstructions(options.schema, { debug: this.debug });
108437
108476
  userMessage.content = message.trim() + schemaInstructions;
@@ -108587,6 +108626,10 @@ You are working with a workspace. Available paths: ${workspaceDesc}
108587
108626
  completionResult = result;
108588
108627
  completionAttempted = true;
108589
108628
  }, toolContext);
108629
+ if (this.debug) {
108630
+ const toolNames = Object.keys(tools2);
108631
+ console.log(`[DEBUG] Agent tools registered (${toolNames.length}): ${toolNames.join(", ")}`);
108632
+ }
108590
108633
  let maxResponseTokens = this.maxResponseTokens;
108591
108634
  if (!maxResponseTokens) {
108592
108635
  maxResponseTokens = 4e3;
@@ -108626,6 +108669,7 @@ You are working with a workspace. Available paths: ${workspaceDesc}
108626
108669
  }
108627
108670
  if (this.debug) {
108628
108671
  console.log(`[DEBUG] Step ${currentIteration}/${maxIterations} finished (reason: ${finishReason}, tools: ${toolResults?.length || 0})`);
108672
+ debugLogToolResults(toolResults);
108629
108673
  }
108630
108674
  }
108631
108675
  };
@@ -108782,6 +108826,7 @@ Double-check your response based on the criteria above. If everything looks good
108782
108826
  }
108783
108827
  if (this.debug) {
108784
108828
  console.log(`[DEBUG] Completion prompt step finished (reason: ${finishReason}, tools: ${toolResults?.length || 0})`);
108829
+ debugLogToolResults(toolResults);
108785
108830
  }
108786
108831
  }
108787
108832
  };
@@ -109492,6 +109537,9 @@ Convert your previous response content into actual JSON data that follows this s
109492
109537
  * Clean up resources (including MCP connections)
109493
109538
  */
109494
109539
  async cleanup() {
109540
+ if (!this._abortController.signal.aborted) {
109541
+ this._abortController.abort();
109542
+ }
109495
109543
  if (this.mcpBridge) {
109496
109544
  try {
109497
109545
  await this.mcpBridge.cleanup();
@@ -109515,14 +109563,25 @@ Convert your previous response content into actual JSON data that follows this s
109515
109563
  this.clearHistory();
109516
109564
  }
109517
109565
  /**
109518
- * Cancel the current request
109566
+ * Cancel the current request and all in-flight delegations.
109567
+ * Aborts the internal AbortController so streamText, subagents,
109568
+ * and any code checking the signal will stop.
109519
109569
  */
109520
109570
  cancel() {
109521
109571
  this.cancelled = true;
109572
+ this._abortController.abort();
109522
109573
  if (this.debug) {
109523
109574
  console.log(`[DEBUG] Agent cancelled for session ${this.sessionId}`);
109524
109575
  }
109525
109576
  }
109577
+ /**
109578
+ * Get the abort signal for this agent.
109579
+ * Delegations and subagents should check this signal.
109580
+ * @returns {AbortSignal}
109581
+ */
109582
+ get abortSignal() {
109583
+ return this._abortController.signal;
109584
+ }
109526
109585
  };
109527
109586
  }
109528
109587
  });
@@ -109555,12 +109614,17 @@ async function delegate({
109555
109614
  mcpConfigPath = null,
109556
109615
  delegationManager = null,
109557
109616
  // Optional per-instance manager, falls back to default singleton
109558
- concurrencyLimiter = null
109617
+ concurrencyLimiter = null,
109559
109618
  // Optional global AI concurrency limiter
109619
+ parentAbortSignal = null
109620
+ // Optional AbortSignal from parent to cancel this delegation
109560
109621
  }) {
109561
109622
  if (!task || typeof task !== "string") {
109562
109623
  throw new Error("Task parameter is required and must be a string");
109563
109624
  }
109625
+ if (parentAbortSignal?.aborted) {
109626
+ throw new Error("Delegation cancelled: parent operation was aborted");
109627
+ }
109564
109628
  const hasExplicitTimeout = Object.prototype.hasOwnProperty.call(arguments?.[0] ?? {}, "timeout");
109565
109629
  if (!hasExplicitTimeout) {
109566
109630
  const envTimeoutMs = parseInt(process.env.DELEGATION_TIMEOUT_MS || "", 10);
@@ -109645,12 +109709,37 @@ async function delegate({
109645
109709
  }
109646
109710
  const timeoutPromise = new Promise((_, reject2) => {
109647
109711
  timeoutId = setTimeout(() => {
109712
+ subagent.cancel();
109648
109713
  reject2(new Error(`Delegation timed out after ${timeout} seconds`));
109649
109714
  }, timeout * 1e3);
109650
109715
  });
109716
+ let parentAbortHandler;
109717
+ const parentAbortPromise = new Promise((_, reject2) => {
109718
+ if (parentAbortSignal) {
109719
+ if (parentAbortSignal.aborted) {
109720
+ subagent.cancel();
109721
+ reject2(new Error("Delegation cancelled: parent operation was aborted"));
109722
+ return;
109723
+ }
109724
+ parentAbortHandler = () => {
109725
+ subagent.cancel();
109726
+ reject2(new Error("Delegation cancelled: parent operation was aborted"));
109727
+ };
109728
+ parentAbortSignal.addEventListener("abort", parentAbortHandler, { once: true });
109729
+ }
109730
+ });
109651
109731
  const answerOptions = schema ? { schema } : void 0;
109652
109732
  const answerPromise = answerOptions ? subagent.answer(task, [], answerOptions) : subagent.answer(task);
109653
- const response = await Promise.race([answerPromise, timeoutPromise]);
109733
+ const racers = [answerPromise, timeoutPromise];
109734
+ if (parentAbortSignal) racers.push(parentAbortPromise);
109735
+ let response;
109736
+ try {
109737
+ response = await Promise.race(racers);
109738
+ } finally {
109739
+ if (parentAbortHandler && parentAbortSignal) {
109740
+ parentAbortSignal.removeEventListener("abort", parentAbortHandler);
109741
+ }
109742
+ }
109654
109743
  if (timeoutId !== null) {
109655
109744
  clearTimeout(timeoutId);
109656
109745
  timeoutId = null;
@@ -110063,8 +110152,9 @@ Instructions:
110063
110152
  promptType: "code-researcher",
110064
110153
  allowedTools: ["extract"],
110065
110154
  maxIterations: 5,
110066
- delegationManager: options.delegationManager
110155
+ delegationManager: options.delegationManager,
110067
110156
  // Per-instance delegation limits
110157
+ parentAbortSignal: options.parentAbortSignal || null
110068
110158
  // timeout removed - inherit default from delegate (300s)
110069
110159
  });
110070
110160
  return { chunk, result };
@@ -110163,8 +110253,9 @@ Organize all findings into clear categories with items listed under each.${compl
110163
110253
  promptType: "code-researcher",
110164
110254
  allowedTools: [],
110165
110255
  maxIterations: 5,
110166
- delegationManager: options.delegationManager
110256
+ delegationManager: options.delegationManager,
110167
110257
  // Per-instance delegation limits
110258
+ parentAbortSignal: options.parentAbortSignal || null
110168
110259
  // timeout removed - inherit default from delegate (300s)
110169
110260
  });
110170
110261
  return result;
@@ -110228,8 +110319,9 @@ CRITICAL: Do NOT guess keywords. Actually run searches and see what returns resu
110228
110319
  promptType: "code-researcher",
110229
110320
  // Full tool access for exploration and experimentation
110230
110321
  maxIterations: 15,
110231
- delegationManager: options.delegationManager
110322
+ delegationManager: options.delegationManager,
110232
110323
  // Per-instance delegation limits
110324
+ parentAbortSignal: options.parentAbortSignal || null
110233
110325
  // timeout removed - inherit default from delegate (300s)
110234
110326
  });
110235
110327
  const plan = parsePlanningResult(stripResultTags(result));
@@ -110286,8 +110378,9 @@ When done, use the attempt_completion tool with your answer as the result.`;
110286
110378
  promptType: "code-researcher",
110287
110379
  allowedTools: [],
110288
110380
  maxIterations: 5,
110289
- delegationManager: options.delegationManager
110381
+ delegationManager: options.delegationManager,
110290
110382
  // Per-instance delegation limits
110383
+ parentAbortSignal: options.parentAbortSignal || null
110291
110384
  // timeout removed - inherit default from delegate (300s)
110292
110385
  });
110293
110386
  return stripResultTags(result);
@@ -110735,7 +110828,8 @@ var init_vercel = __esm({
110735
110828
  promptType: "code-searcher",
110736
110829
  allowedTools: ["search", "extract", "listFiles", "attempt_completion"],
110737
110830
  searchDelegate: false,
110738
- schema: CODE_SEARCH_SCHEMA
110831
+ schema: CODE_SEARCH_SCHEMA,
110832
+ parentAbortSignal: options.parentAbortSignal || null
110739
110833
  });
110740
110834
  const delegateResult = options.tracer?.withSpan ? await options.tracer.withSpan("search.delegate", runDelegation, {
110741
110835
  "search.query": searchQuery,
@@ -110949,7 +111043,7 @@ var init_vercel = __esm({
110949
111043
  name: "delegate",
110950
111044
  description: delegateDescription,
110951
111045
  inputSchema: delegateSchema,
110952
- execute: async ({ task, currentIteration, maxIterations, parentSessionId, path: path9, provider, model, tracer, searchDelegate }) => {
111046
+ execute: async ({ task, currentIteration, maxIterations, parentSessionId, path: path9, provider, model, tracer, searchDelegate, parentAbortSignal }) => {
110953
111047
  if (!task || typeof task !== "string") {
110954
111048
  throw new Error("Task parameter is required and must be a non-empty string");
110955
111049
  }
@@ -111007,8 +111101,9 @@ var init_vercel = __esm({
111007
111101
  enableMcp,
111008
111102
  mcpConfig,
111009
111103
  mcpConfigPath,
111010
- delegationManager
111104
+ delegationManager,
111011
111105
  // Per-instance delegation limits
111106
+ parentAbortSignal
111012
111107
  });
111013
111108
  return result;
111014
111109
  }
@@ -111046,8 +111141,9 @@ var init_vercel = __esm({
111046
111141
  provider: options.provider,
111047
111142
  model: options.model,
111048
111143
  tracer: options.tracer,
111049
- delegationManager
111144
+ delegationManager,
111050
111145
  // Per-instance delegation limits
111146
+ parentAbortSignal: options.parentAbortSignal || null
111051
111147
  });
111052
111148
  return result;
111053
111149
  } catch (error2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@probelabs/probe",
3
- "version": "0.6.0-rc277",
3
+ "version": "0.6.0-rc279",
4
4
  "description": "Node.js wrapper for the probe code search tool",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -104,7 +104,6 @@ import {
104
104
  TaskManager,
105
105
  createTaskTool,
106
106
  taskSystemPrompt,
107
- taskGuidancePrompt,
108
107
  createTaskCompletionBlockedMessage
109
108
  } from './tasks/index.js';
110
109
  import { z } from 'zod';
@@ -126,6 +125,27 @@ const MAX_HISTORY_MESSAGES = 100;
126
125
  // Maximum image file size (20MB) to prevent OOM attacks
127
126
  const MAX_IMAGE_FILE_SIZE = 20 * 1024 * 1024;
128
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
+
129
149
  /**
130
150
  * ProbeAgent class to handle AI interactions with code search capabilities
131
151
  */
@@ -194,6 +214,7 @@ export class ProbeAgent {
194
214
  this.enableExecutePlan = !!options.enableExecutePlan;
195
215
  this.debug = options.debug || process.env.DEBUG === '1';
196
216
  this.cancelled = false;
217
+ this._abortController = new AbortController();
197
218
  this.tracer = options.tracer || null;
198
219
  this.outline = !!options.outline;
199
220
  this.searchDelegate = options.searchDelegate !== undefined ? !!options.searchDelegate : true;
@@ -793,6 +814,7 @@ export class ProbeAgent {
793
814
  searchDelegateProvider: this.searchDelegateProvider,
794
815
  searchDelegateModel: this.searchDelegateModel,
795
816
  delegationManager: this.delegationManager, // Per-instance delegation limits
817
+ parentAbortSignal: this._abortController.signal, // Propagate cancellation to delegations
796
818
  outputBuffer: this._outputBuffer,
797
819
  concurrencyLimiter: this.concurrencyLimiter, // Global AI concurrency limiter
798
820
  isToolAllowed,
@@ -1363,6 +1385,19 @@ export class ProbeAgent {
1363
1385
  const controller = new AbortController();
1364
1386
  const timeoutState = { timeoutId: null };
1365
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
+
1366
1401
  // Set up overall operation timeout (default 5 minutes)
1367
1402
  if (this.maxOperationTimeout && this.maxOperationTimeout > 0) {
1368
1403
  timeoutState.timeoutId = setTimeout(() => {
@@ -1730,7 +1765,8 @@ export class ProbeAgent {
1730
1765
  allowEdit: this.allowEdit,
1731
1766
  allowedTools: allowedToolsForDelegate,
1732
1767
  debug: this.debug,
1733
- tracer: this.tracer
1768
+ tracer: this.tracer,
1769
+ parentAbortSignal: this._abortController.signal
1734
1770
  };
1735
1771
 
1736
1772
  if (this.debug) {
@@ -3153,14 +3189,6 @@ Follow these instructions carefully:
3153
3189
  // Create user message with optional image support
3154
3190
  let userMessage = { role: 'user', content: message.trim() };
3155
3191
 
3156
- // START CHECKPOINT: Inject task guidance if tasks are enabled
3157
- if (this.enableTasks) {
3158
- userMessage.content = userMessage.content + '\n\n' + taskGuidancePrompt;
3159
- if (this.debug) {
3160
- console.log('[DEBUG] Task guidance injected into user message');
3161
- }
3162
- }
3163
-
3164
3192
  // If schema is provided, prepend JSON format requirement to user message
3165
3193
  if (options.schema && !options._schemaFormatted) {
3166
3194
  const schemaInstructions = generateSchemaInstructions(options.schema, { debug: this.debug });
@@ -3378,6 +3406,11 @@ Follow these instructions carefully:
3378
3406
  completionAttempted = true;
3379
3407
  }, toolContext);
3380
3408
 
3409
+ if (this.debug) {
3410
+ const toolNames = Object.keys(tools);
3411
+ console.log(`[DEBUG] Agent tools registered (${toolNames.length}): ${toolNames.join(', ')}`);
3412
+ }
3413
+
3381
3414
  let maxResponseTokens = this.maxResponseTokens;
3382
3415
  if (!maxResponseTokens) {
3383
3416
  maxResponseTokens = 4000;
@@ -3427,6 +3460,7 @@ Follow these instructions carefully:
3427
3460
 
3428
3461
  if (this.debug) {
3429
3462
  console.log(`[DEBUG] Step ${currentIteration}/${maxIterations} finished (reason: ${finishReason}, tools: ${toolResults?.length || 0})`);
3463
+ debugLogToolResults(toolResults);
3430
3464
  }
3431
3465
  }
3432
3466
  };
@@ -3638,6 +3672,7 @@ Double-check your response based on the criteria above. If everything looks good
3638
3672
  }
3639
3673
  if (this.debug) {
3640
3674
  console.log(`[DEBUG] Completion prompt step finished (reason: ${finishReason}, tools: ${toolResults?.length || 0})`);
3675
+ debugLogToolResults(toolResults);
3641
3676
  }
3642
3677
  }
3643
3678
  };
@@ -4554,6 +4589,11 @@ Convert your previous response content into actual JSON data that follows this s
4554
4589
  * Clean up resources (including MCP connections)
4555
4590
  */
4556
4591
  async cleanup() {
4592
+ // Abort any in-flight operations (delegations, streaming, etc.)
4593
+ if (!this._abortController.signal.aborted) {
4594
+ this._abortController.abort();
4595
+ }
4596
+
4557
4597
  // Clean up MCP bridge
4558
4598
  if (this.mcpBridge) {
4559
4599
  try {
@@ -4583,12 +4623,24 @@ Convert your previous response content into actual JSON data that follows this s
4583
4623
  }
4584
4624
 
4585
4625
  /**
4586
- * 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.
4587
4629
  */
4588
4630
  cancel() {
4589
4631
  this.cancelled = true;
4632
+ this._abortController.abort();
4590
4633
  if (this.debug) {
4591
4634
  console.log(`[DEBUG] Agent cancelled for session ${this.sessionId}`);
4592
4635
  }
4593
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
+ }
4594
4646
  }
@@ -81,8 +81,20 @@ If the solution is clear, you can jump to implementation right away. If not, ask
81
81
  - Check imports and existing utilities before creating new helpers — the project may already have what you need.
82
82
 
83
83
  # Task Planning
84
- - If the task tool is available, use it to break complex work into milestones before starting implementation.
85
- - Stay flexible if your understanding changes mid-task, add, remove, or reorganize tasks as needed. The plan should serve you, not constrain you.
84
+ When the request has **multiple distinct goals** (e.g. "Fix bug A AND add feature B"), use the task tool to track them:
85
+ - Call the task tool with action="create" and a tasks array. Each task must have an "id" field.
86
+ - Update task status to "in_progress" when starting and "completed" when done.
87
+ - All tasks must be completed or cancelled before calling attempt_completion.
88
+ - Stay flexible — add, remove, or reorganize tasks as your understanding changes.
89
+
90
+ Do NOT create tasks for single-goal requests, even complex ones. Multiple internal steps for one goal (search, read, analyze, implement) do not need tasks.
91
+
92
+ # Discovering Project Commands
93
+ Before building or testing, determine the project's toolchain:
94
+ - Check for Makefile, package.json (scripts), Cargo.toml, go.mod, pyproject.toml, or similar
95
+ - Look for CI config (.github/workflows/, .gitlab-ci.yml) to see what commands CI runs
96
+ - Read README for build/test instructions if the above are unclear
97
+ - Common patterns: \`make build\`/\`make test\`, \`npm run build\`/\`npm test\`, \`cargo build\`/\`cargo test\`, \`go build ./...\`/\`go test ./...\`, \`python -m pytest\`
86
98
 
87
99
  # During Implementation
88
100
  - Always create a new branch before making changes to the codebase.
@@ -93,12 +105,22 @@ If the solution is clear, you can jump to implementation right away. If not, ask
93
105
  - When editing files, keep edits focused and minimal. For changes spanning more than a few lines, prefer line-targeted editing (start_line/end_line) over text replacement (old_string) — it constrains scope and prevents accidental removal of adjacent content. Never include unrelated sections in an edit operation.
94
106
  - After every significant change, verify the project still builds and passes linting. Do not wait until the end to discover breakage.
95
107
 
96
- # After Implementation
97
- - Verify the project builds successfully. If it doesn't, fix the build before moving on.
98
- - Run lint and typecheck commands if known for the project. Fix any new warnings or errors you introduced.
99
- - Add tests for any new or changed functionality. Tests must cover the main path and important edge cases.
100
- - Run the project's full test suite. If any tests fail (including pre-existing ones you may have broken), fix them before finishing.
101
- - When the task is done, respond to the user with a concise summary of what was implemented, what files were changed, and any relevant details. Include links (e.g. pull request URL) so the user has everything they need.
108
+ # Writing Tests
109
+ Every change must include tests. Before writing them:
110
+ - Find existing test files for the module you changed look in \`tests/\`, \`__tests__/\`, \`*_test.go\`, \`*.test.js\`, \`*.spec.ts\`, or co-located test modules (\`#[cfg(test)]\` in Rust).
111
+ - Read those tests to understand the project's testing patterns: framework, assertion style, mocking approach, file naming, test organization.
112
+ - Prefer extending an existing test file over creating a new one when your change is in the same module.
113
+ - Write tests that cover the main path and important edge cases. Include a failing-input test when relevant.
114
+ - When fixing a bug, write a failing test first that reproduces the bug, then fix the code to make it pass.
115
+
116
+ # Verify Changes
117
+ Before committing or creating a PR, run through this checklist:
118
+ 1. **Build** — run the project-appropriate build command (go build, npm run build, cargo build, make, etc.). Fix any compilation errors.
119
+ 2. **Lint & typecheck** — run linter/formatter if the project has one (eslint, clippy, golangci-lint, etc.). Fix any new warnings.
120
+ 3. **Test** — run the full test suite (go test ./..., npm test, cargo test, make test, pytest, etc.). Fix any failures, including pre-existing tests you may have broken.
121
+ 4. **Review** — re-read your diff. Ensure no debug code, no unrelated changes, no secrets, no missing files.
122
+
123
+ Do NOT skip verification. Do NOT proceed to PR creation with a broken build or failing tests.
102
124
 
103
125
  # GitHub Integration
104
126
  - Use the \`gh\` CLI for all GitHub operations: issues, pull requests, checks, releases.
package/src/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)