@probelabs/probe 0.6.0-rc250 → 0.6.0-rc252

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.
@@ -819,6 +819,11 @@ export class ProbeAgent {
819
819
  // reset at the start of each answer() call
820
820
  this._outputBuffer = { items: [] };
821
821
 
822
+ // Separate accumulator for extracted RAW_OUTPUT blocks from tool results.
823
+ // This is distinct from _outputBuffer to prevent the cycle where:
824
+ // formatSuccess wraps → extract re-adds → next execute_plan re-wraps (issue #438)
825
+ this._extractedRawBlocks = [];
826
+
822
827
  const configOptions = {
823
828
  sessionId: this.sessionId,
824
829
  debug: this.debug,
@@ -2910,6 +2915,8 @@ Follow these instructions carefully:
2910
2915
  // Both must preserve the output buffer so the parent call can append it.
2911
2916
  if (this._outputBuffer && !options?._schemaFormatted && !options?._completionPromptProcessed) {
2912
2917
  this._outputBuffer.items = [];
2918
+ // Also reset the extracted blocks accumulator (issue #438)
2919
+ this._extractedRawBlocks = [];
2913
2920
  }
2914
2921
 
2915
2922
  // START CHECKPOINT: Initialize task management for this request
@@ -3629,15 +3636,17 @@ Follow these instructions carefully:
3629
3636
 
3630
3637
  let toolResultContent = typeof executionResult === 'string' ? executionResult : JSON.stringify(executionResult, null, 2);
3631
3638
 
3632
- // Extract raw output blocks and pass them through to output buffer (before truncation)
3639
+ // Extract raw output blocks from tool result (before truncation)
3633
3640
  // This prevents LLM from processing/hallucinating large structured output from execute_plan
3634
- if (this._outputBuffer) {
3635
- const { cleanedContent, extractedBlocks } = extractRawOutputBlocks(toolResultContent, this._outputBuffer);
3636
- if (extractedBlocks.length > 0) {
3637
- toolResultContent = cleanedContent;
3638
- if (this.debug) {
3639
- console.log(`[DEBUG] Extracted ${extractedBlocks.length} raw output blocks (${extractedBlocks.reduce((sum, b) => sum + b.length, 0)} chars) to output buffer`);
3640
- }
3641
+ // Push to _extractedRawBlocks (NOT _outputBuffer) to prevent the cycle where:
3642
+ // formatSuccess wraps extract re-adds → next execute_plan re-wraps (issue #438)
3643
+ const { cleanedContent, extractedBlocks } = extractRawOutputBlocks(toolResultContent);
3644
+ if (extractedBlocks.length > 0) {
3645
+ toolResultContent = cleanedContent;
3646
+ // Accumulate extracted blocks separately from DSL output() buffer
3647
+ this._extractedRawBlocks.push(...extractedBlocks);
3648
+ if (this.debug) {
3649
+ console.log(`[DEBUG] Extracted ${extractedBlocks.length} raw output blocks (${extractedBlocks.reduce((sum, b) => sum + b.length, 0)} chars) from tool result`);
3641
3650
  }
3642
3651
  }
3643
3652
 
@@ -3887,15 +3896,17 @@ Follow these instructions carefully:
3887
3896
  toolResultContent = toolResultContent.split(wsPrefix).join('');
3888
3897
  }
3889
3898
 
3890
- // Extract raw output blocks and pass them through to output buffer (before truncation)
3899
+ // Extract raw output blocks from tool result (before truncation)
3891
3900
  // This prevents LLM from processing/hallucinating large structured output from execute_plan
3892
- if (this._outputBuffer) {
3893
- const { cleanedContent, extractedBlocks } = extractRawOutputBlocks(toolResultContent, this._outputBuffer);
3894
- if (extractedBlocks.length > 0) {
3895
- toolResultContent = cleanedContent;
3896
- if (this.debug) {
3897
- console.log(`[DEBUG] Extracted ${extractedBlocks.length} raw output blocks (${extractedBlocks.reduce((sum, b) => sum + b.length, 0)} chars) to output buffer`);
3898
- }
3901
+ // Push to _extractedRawBlocks (NOT _outputBuffer) to prevent the cycle where:
3902
+ // formatSuccess wraps extract re-adds → next execute_plan re-wraps (issue #438)
3903
+ const { cleanedContent, extractedBlocks } = extractRawOutputBlocks(toolResultContent);
3904
+ if (extractedBlocks.length > 0) {
3905
+ toolResultContent = cleanedContent;
3906
+ // Accumulate extracted blocks separately from DSL output() buffer
3907
+ this._extractedRawBlocks.push(...extractedBlocks);
3908
+ if (this.debug) {
3909
+ console.log(`[DEBUG] Extracted ${extractedBlocks.length} raw output blocks (${extractedBlocks.reduce((sum, b) => sum + b.length, 0)} chars) from tool result`);
3899
3910
  }
3900
3911
  }
3901
3912
 
@@ -4314,16 +4325,18 @@ After reviewing, provide your final answer using attempt_completion.`;
4314
4325
 
4315
4326
  // Make a follow-up call with the completion prompt
4316
4327
  // Pass _completionPromptProcessed to prevent infinite loops
4317
- // Save output buffer — the recursive answer() must not destroy DSL output() content
4328
+ // Save output buffers — the recursive answer() must not destroy DSL output() content
4318
4329
  const savedOutputItems = this._outputBuffer ? [...this._outputBuffer.items] : [];
4330
+ const savedExtractedBlocks = this._extractedRawBlocks ? [...this._extractedRawBlocks] : [];
4319
4331
  const completionResult = await this.answer(completionPromptMessage, [], {
4320
4332
  ...options,
4321
4333
  _completionPromptProcessed: true
4322
4334
  });
4323
- // Restore output buffer so the parent call can append it to the final result
4335
+ // Restore output buffers so the parent call can append them to the final result
4324
4336
  if (this._outputBuffer) {
4325
4337
  this._outputBuffer.items = savedOutputItems;
4326
4338
  }
4339
+ this._extractedRawBlocks = savedExtractedBlocks;
4327
4340
 
4328
4341
  // Update finalResult with the result from the completion prompt
4329
4342
  finalResult = completionResult;
@@ -4383,7 +4396,8 @@ Convert your previous response content into actual JSON data that follows this s
4383
4396
  // Call answer recursively with _schemaFormatted flag to prevent infinite loop
4384
4397
  finalResult = await this.answer(schemaPrompt, [], {
4385
4398
  ...options,
4386
- _schemaFormatted: true
4399
+ _schemaFormatted: true,
4400
+ _completionPromptProcessed: true // Prevent cascading completion prompts in retry calls
4387
4401
  });
4388
4402
 
4389
4403
  // Step 2: Validate and fix Mermaid diagrams if present (BEFORE cleaning schema)
@@ -4642,7 +4656,8 @@ Convert your previous response content into actual JSON data that follows this s
4642
4656
  finalResult = await this.answer(schemaDefinitionPrompt, [], {
4643
4657
  ...options,
4644
4658
  _schemaFormatted: true,
4645
- _skipValidation: true // Skip validation in recursive correction calls to prevent loops
4659
+ _skipValidation: true, // Skip validation in recursive correction calls to prevent loops
4660
+ _completionPromptProcessed: true // Prevent cascading completion prompts in retry calls
4646
4661
  });
4647
4662
  finalResult = cleanSchemaResponse(finalResult);
4648
4663
  validation = validateJsonResponse(finalResult);
@@ -4702,7 +4717,8 @@ Convert your previous response content into actual JSON data that follows this s
4702
4717
  ...options,
4703
4718
  _schemaFormatted: true,
4704
4719
  _skipValidation: true, // Skip validation in recursive correction calls to prevent loops
4705
- _disableTools: true // Only allow attempt_completion - prevent AI from using search/query tools
4720
+ _disableTools: true, // Only allow attempt_completion - prevent AI from using search/query tools
4721
+ _completionPromptProcessed: true // Prevent cascading completion prompts in retry calls
4706
4722
  });
4707
4723
  finalResult = cleanSchemaResponse(finalResult);
4708
4724
 
@@ -4787,8 +4803,15 @@ Convert your previous response content into actual JSON data that follows this s
4787
4803
  }
4788
4804
 
4789
4805
  // Append DSL output buffer directly to response (bypasses LLM rewriting)
4790
- if (this._outputBuffer && this._outputBuffer.items.length > 0 && !options._schemaFormatted) {
4791
- const outputContent = this._outputBuffer.items.join('\n\n');
4806
+ // Skip during _completionPromptProcessed only the parent answer() should append the buffer.
4807
+ // Combine _outputBuffer (from DSL output() calls) and _extractedRawBlocks (from tool results)
4808
+ // Using separate accumulators prevents the cycle described in issue #438.
4809
+ const allOutputItems = [
4810
+ ...(this._outputBuffer?.items || []),
4811
+ ...(this._extractedRawBlocks || [])
4812
+ ];
4813
+ if (allOutputItems.length > 0 && !options._schemaFormatted && !options._completionPromptProcessed) {
4814
+ const outputContent = allOutputItems.join('\n\n');
4792
4815
  if (options.schema) {
4793
4816
  // Schema response — the finalResult is JSON. Wrap output in RAW_OUTPUT
4794
4817
  // delimiters so clients (visor, etc.) can extract and propagate the
@@ -4801,9 +4824,10 @@ Convert your previous response content into actual JSON data that follows this s
4801
4824
  options.onStream('\n\n' + outputContent);
4802
4825
  }
4803
4826
  if (this.debug) {
4804
- console.log(`[DEBUG] Appended ${this._outputBuffer.items.length} output buffer items (${outputContent.length} chars) to final result${options.schema ? ' (with RAW_OUTPUT delimiters)' : ''}`);
4827
+ console.log(`[DEBUG] Appended ${allOutputItems.length} output items (${outputContent.length} chars) to final result${options.schema ? ' (with RAW_OUTPUT delimiters)' : ''}`);
4805
4828
  }
4806
4829
  this._outputBuffer.items = [];
4830
+ this._extractedRawBlocks = [];
4807
4831
  }
4808
4832
 
4809
4833
  return finalResult;
@@ -28993,8 +28993,13 @@ function stripCodeWrapping(code) {
28993
28993
  s = decodeHtmlEntities(s);
28994
28994
  return s.trim();
28995
28995
  }
28996
- function buildToolImplementations(configOptions) {
28997
- const { sessionId, cwd } = configOptions;
28996
+ function generatePlanSessionId(baseSessionId) {
28997
+ const uniquePart = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID().slice(0, 8) : `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
28998
+ return `${baseSessionId || "plan"}-${uniquePart}`;
28999
+ }
29000
+ function buildToolImplementations(configOptions, planSessionId) {
29001
+ const { cwd } = configOptions;
29002
+ const sessionId = planSessionId || configOptions.sessionId;
28998
29003
  const tools2 = {};
28999
29004
  tools2.search = {
29000
29005
  execute: async (params) => {
@@ -29180,7 +29185,7 @@ function createExecutePlanTool(options) {
29180
29185
  const isMcpToolAllowed = options.isMcpToolAllowed || (() => true);
29181
29186
  let cachedMcpBridge = null;
29182
29187
  let runtime = null;
29183
- function buildRuntime() {
29188
+ function buildRuntime(planSessionId) {
29184
29189
  const currentMcpBridge = getMcpBridge();
29185
29190
  const currentMcpTools = getMcpTools();
29186
29191
  const filteredMcpTools = {};
@@ -29202,7 +29207,7 @@ function createExecutePlanTool(options) {
29202
29207
  } else {
29203
29208
  llmCallFn = llmCallFn || buildLLMCall(options);
29204
29209
  runtimeOptions = {
29205
- toolImplementations: buildToolImplementations(options),
29210
+ toolImplementations: buildToolImplementations(options, planSessionId),
29206
29211
  llmCall: llmCallFn,
29207
29212
  mcpBridge: currentMcpBridge,
29208
29213
  mcpTools: filteredMcpTools,
@@ -29230,12 +29235,15 @@ function createExecutePlanTool(options) {
29230
29235
  description: "Execute a JavaScript DSL program to orchestrate tool calls. Use for batch processing, paginated APIs, multi-step workflows where intermediate data is large. Write simple synchronous-looking code \u2014 do NOT use async/await.",
29231
29236
  parameters: executePlanSchema,
29232
29237
  execute: async ({ code, description }) => {
29238
+ const planSessionId = generatePlanSessionId(options.sessionId);
29233
29239
  const planSpan = tracer?.createToolSpan?.("execute_plan", {
29234
29240
  "dsl.description": description || "",
29235
29241
  "dsl.code_length": code.length,
29236
29242
  "dsl.code": code,
29237
- "dsl.max_retries": maxRetries
29243
+ "dsl.max_retries": maxRetries,
29244
+ "dsl.plan_session_id": planSessionId
29238
29245
  }) || null;
29246
+ const planRuntime = buildRuntime(planSessionId);
29239
29247
  let currentCode = stripCodeWrapping(code);
29240
29248
  let lastError = null;
29241
29249
  let finalOutput;
@@ -29291,7 +29299,7 @@ Original error: ${lastError}`;
29291
29299
  return finalOutput;
29292
29300
  }
29293
29301
  }
29294
- const result = await getRuntime().execute(currentCode, description);
29302
+ const result = await planRuntime.execute(currentCode, description);
29295
29303
  if (result.status === "success") {
29296
29304
  finalOutput = formatSuccess(result, description, attempt, outputBuffer);
29297
29305
  planSpan?.setAttributes?.({
@@ -29389,8 +29397,14 @@ ${userLogs.join("\n")}
29389
29397
  }
29390
29398
  }
29391
29399
  const resultValue = result.result;
29400
+ const hasOutputBufferContent = outputBuffer && outputBuffer.items && outputBuffer.items.length > 0;
29392
29401
  if (resultValue === void 0 || resultValue === null) {
29393
- output += "Plan completed (no return value).";
29402
+ if (hasOutputBufferContent) {
29403
+ const totalChars = outputBuffer.items.reduce((sum, item) => sum + item.length, 0);
29404
+ output += `Plan completed successfully. Output captured (${totalChars} chars) via output() and will be included in the final response.`;
29405
+ } else {
29406
+ output += "Plan completed (no return value).";
29407
+ }
29394
29408
  } else if (typeof resultValue === "string") {
29395
29409
  output += `Result:
29396
29410
  ${resultValue}`;
@@ -81832,6 +81846,7 @@ var init_ProbeAgent = __esm({
81832
81846
  initializeTools() {
81833
81847
  const isToolAllowed = (toolName) => this.allowedTools.isEnabled(toolName);
81834
81848
  this._outputBuffer = { items: [] };
81849
+ this._extractedRawBlocks = [];
81835
81850
  const configOptions = {
81836
81851
  sessionId: this.sessionId,
81837
81852
  debug: this.debug,
@@ -83566,6 +83581,7 @@ You are working with a workspace. Available paths: ${workspaceDesc}
83566
83581
  const oldHistoryLength = this.history.length;
83567
83582
  if (this._outputBuffer && !options?._schemaFormatted && !options?._completionPromptProcessed) {
83568
83583
  this._outputBuffer.items = [];
83584
+ this._extractedRawBlocks = [];
83569
83585
  }
83570
83586
  if (this.enableTasks) {
83571
83587
  try {
@@ -84065,13 +84081,12 @@ You are working with a workspace. Available paths: ${workspaceDesc}
84065
84081
  }
84066
84082
  const executionResult = await this.mcpBridge.mcpTools[toolName].execute(params);
84067
84083
  let toolResultContent = typeof executionResult === "string" ? executionResult : JSON.stringify(executionResult, null, 2);
84068
- if (this._outputBuffer) {
84069
- const { cleanedContent, extractedBlocks } = extractRawOutputBlocks(toolResultContent, this._outputBuffer);
84070
- if (extractedBlocks.length > 0) {
84071
- toolResultContent = cleanedContent;
84072
- if (this.debug) {
84073
- console.log(`[DEBUG] Extracted ${extractedBlocks.length} raw output blocks (${extractedBlocks.reduce((sum, b) => sum + b.length, 0)} chars) to output buffer`);
84074
- }
84084
+ const { cleanedContent, extractedBlocks } = extractRawOutputBlocks(toolResultContent);
84085
+ if (extractedBlocks.length > 0) {
84086
+ toolResultContent = cleanedContent;
84087
+ this._extractedRawBlocks.push(...extractedBlocks);
84088
+ if (this.debug) {
84089
+ console.log(`[DEBUG] Extracted ${extractedBlocks.length} raw output blocks (${extractedBlocks.reduce((sum, b) => sum + b.length, 0)} chars) from tool result`);
84075
84090
  }
84076
84091
  }
84077
84092
  try {
@@ -84280,13 +84295,12 @@ ${errorXml}
84280
84295
  const wsPrefix = this.workspaceRoot.endsWith(sep5) ? this.workspaceRoot : this.workspaceRoot + sep5;
84281
84296
  toolResultContent = toolResultContent.split(wsPrefix).join("");
84282
84297
  }
84283
- if (this._outputBuffer) {
84284
- const { cleanedContent, extractedBlocks } = extractRawOutputBlocks(toolResultContent, this._outputBuffer);
84285
- if (extractedBlocks.length > 0) {
84286
- toolResultContent = cleanedContent;
84287
- if (this.debug) {
84288
- console.log(`[DEBUG] Extracted ${extractedBlocks.length} raw output blocks (${extractedBlocks.reduce((sum, b) => sum + b.length, 0)} chars) to output buffer`);
84289
- }
84298
+ const { cleanedContent, extractedBlocks } = extractRawOutputBlocks(toolResultContent);
84299
+ if (extractedBlocks.length > 0) {
84300
+ toolResultContent = cleanedContent;
84301
+ this._extractedRawBlocks.push(...extractedBlocks);
84302
+ if (this.debug) {
84303
+ console.log(`[DEBUG] Extracted ${extractedBlocks.length} raw output blocks (${extractedBlocks.reduce((sum, b) => sum + b.length, 0)} chars) from tool result`);
84290
84304
  }
84291
84305
  }
84292
84306
  try {
@@ -84595,6 +84609,7 @@ ${finalResult}
84595
84609
 
84596
84610
  After reviewing, provide your final answer using attempt_completion.`;
84597
84611
  const savedOutputItems = this._outputBuffer ? [...this._outputBuffer.items] : [];
84612
+ const savedExtractedBlocks = this._extractedRawBlocks ? [...this._extractedRawBlocks] : [];
84598
84613
  const completionResult = await this.answer(completionPromptMessage, [], {
84599
84614
  ...options,
84600
84615
  _completionPromptProcessed: true
@@ -84602,6 +84617,7 @@ After reviewing, provide your final answer using attempt_completion.`;
84602
84617
  if (this._outputBuffer) {
84603
84618
  this._outputBuffer.items = savedOutputItems;
84604
84619
  }
84620
+ this._extractedRawBlocks = savedExtractedBlocks;
84605
84621
  finalResult = completionResult;
84606
84622
  if (this.debug) {
84607
84623
  console.log(`[DEBUG] Completion prompt finished. New result length: ${finalResult?.length || 0}`);
@@ -84647,7 +84663,9 @@ NOT: {"type": "object", "properties": {"name": {"type": "string"}}}
84647
84663
  Convert your previous response content into actual JSON data that follows this schema structure.`;
84648
84664
  finalResult = await this.answer(schemaPrompt, [], {
84649
84665
  ...options,
84650
- _schemaFormatted: true
84666
+ _schemaFormatted: true,
84667
+ _completionPromptProcessed: true
84668
+ // Prevent cascading completion prompts in retry calls
84651
84669
  });
84652
84670
  if (!this.disableMermaidValidation) {
84653
84671
  try {
@@ -84851,8 +84869,10 @@ Convert your previous response content into actual JSON data that follows this s
84851
84869
  finalResult = await this.answer(schemaDefinitionPrompt, [], {
84852
84870
  ...options,
84853
84871
  _schemaFormatted: true,
84854
- _skipValidation: true
84872
+ _skipValidation: true,
84855
84873
  // Skip validation in recursive correction calls to prevent loops
84874
+ _completionPromptProcessed: true
84875
+ // Prevent cascading completion prompts in retry calls
84856
84876
  });
84857
84877
  finalResult = cleanSchemaResponse(finalResult);
84858
84878
  validation = validateJsonResponse(finalResult);
@@ -84905,8 +84925,10 @@ Convert your previous response content into actual JSON data that follows this s
84905
84925
  _schemaFormatted: true,
84906
84926
  _skipValidation: true,
84907
84927
  // Skip validation in recursive correction calls to prevent loops
84908
- _disableTools: true
84928
+ _disableTools: true,
84909
84929
  // Only allow attempt_completion - prevent AI from using search/query tools
84930
+ _completionPromptProcessed: true
84931
+ // Prevent cascading completion prompts in retry calls
84910
84932
  });
84911
84933
  finalResult = cleanSchemaResponse(finalResult);
84912
84934
  validation = validateJsonResponse(finalResult, { debug: this.debug });
@@ -84976,8 +84998,12 @@ Convert your previous response content into actual JSON data that follows this s
84976
84998
  console.log(`[DEBUG] Removed thinking tags from final result`);
84977
84999
  }
84978
85000
  }
84979
- if (this._outputBuffer && this._outputBuffer.items.length > 0 && !options._schemaFormatted) {
84980
- const outputContent = this._outputBuffer.items.join("\n\n");
85001
+ const allOutputItems = [
85002
+ ...this._outputBuffer?.items || [],
85003
+ ...this._extractedRawBlocks || []
85004
+ ];
85005
+ if (allOutputItems.length > 0 && !options._schemaFormatted && !options._completionPromptProcessed) {
85006
+ const outputContent = allOutputItems.join("\n\n");
84981
85007
  if (options.schema) {
84982
85008
  finalResult = (finalResult || "") + "\n<<<RAW_OUTPUT>>>\n" + outputContent + "\n<<<END_RAW_OUTPUT>>>";
84983
85009
  } else {
@@ -84987,9 +85013,10 @@ Convert your previous response content into actual JSON data that follows this s
84987
85013
  options.onStream("\n\n" + outputContent);
84988
85014
  }
84989
85015
  if (this.debug) {
84990
- console.log(`[DEBUG] Appended ${this._outputBuffer.items.length} output buffer items (${outputContent.length} chars) to final result${options.schema ? " (with RAW_OUTPUT delimiters)" : ""}`);
85016
+ console.log(`[DEBUG] Appended ${allOutputItems.length} output items (${outputContent.length} chars) to final result${options.schema ? " (with RAW_OUTPUT delimiters)" : ""}`);
84991
85017
  }
84992
85018
  this._outputBuffer.items = [];
85019
+ this._extractedRawBlocks = [];
84993
85020
  }
84994
85021
  return finalResult;
84995
85022
  } catch (error) {
@@ -65,14 +65,28 @@ function stripCodeWrapping(code) {
65
65
  return s.trim();
66
66
  }
67
67
 
68
+ /**
69
+ * Generate a unique session ID for this execute_plan invocation.
70
+ * Uses crypto.randomUUID if available, falls back to timestamp + random.
71
+ */
72
+ function generatePlanSessionId(baseSessionId) {
73
+ const uniquePart = typeof crypto !== 'undefined' && crypto.randomUUID
74
+ ? crypto.randomUUID().slice(0, 8)
75
+ : `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
76
+ return `${baseSessionId || 'plan'}-${uniquePart}`;
77
+ }
78
+
68
79
  /**
69
80
  * Build DSL-compatible tool implementations from the agent's configOptions.
70
81
  *
71
82
  * @param {Object} configOptions - Agent config (sessionId, cwd, provider, model, etc.)
83
+ * @param {string} [planSessionId] - Unique session ID for this execute_plan invocation
72
84
  * @returns {Object} toolImplementations for createDSLRuntime
73
85
  */
74
- function buildToolImplementations(configOptions) {
75
- const { sessionId, cwd } = configOptions;
86
+ function buildToolImplementations(configOptions, planSessionId) {
87
+ const { cwd } = configOptions;
88
+ // Use planSessionId for isolated pagination per execute_plan, fall back to global sessionId
89
+ const sessionId = planSessionId || configOptions.sessionId;
76
90
  const tools = {};
77
91
 
78
92
  tools.search = {
@@ -311,9 +325,11 @@ export function createExecutePlanTool(options) {
311
325
 
312
326
  /**
313
327
  * Build or rebuild the DSL runtime.
314
- * Called lazily on first execute() and when MCP bridge changes.
328
+ * Called for each execute() invocation with a unique planSessionId.
329
+ *
330
+ * @param {string} [planSessionId] - Unique session ID for this execute_plan invocation
315
331
  */
316
- function buildRuntime() {
332
+ function buildRuntime(planSessionId) {
317
333
  const currentMcpBridge = getMcpBridge();
318
334
  const currentMcpTools = getMcpTools();
319
335
 
@@ -340,7 +356,7 @@ export function createExecutePlanTool(options) {
340
356
  // Agent configOptions — build everything from the agent's config
341
357
  llmCallFn = llmCallFn || buildLLMCall(options);
342
358
  runtimeOptions = {
343
- toolImplementations: buildToolImplementations(options),
359
+ toolImplementations: buildToolImplementations(options, planSessionId),
344
360
  llmCall: llmCallFn,
345
361
  mcpBridge: currentMcpBridge,
346
362
  mcpTools: filteredMcpTools,
@@ -360,6 +376,7 @@ export function createExecutePlanTool(options) {
360
376
 
361
377
  /**
362
378
  * Get or rebuild the runtime if MCP state has changed.
379
+ * @deprecated Use buildRuntime(planSessionId) directly for unique sessions per execution
363
380
  */
364
381
  function getRuntime() {
365
382
  const currentMcpBridge = getMcpBridge();
@@ -378,14 +395,22 @@ export function createExecutePlanTool(options) {
378
395
  'Write simple synchronous-looking code — do NOT use async/await.',
379
396
  parameters: executePlanSchema,
380
397
  execute: async ({ code, description }) => {
398
+ // Generate a unique session ID for this execute_plan invocation
399
+ // This ensures search pagination is isolated per execute_plan call
400
+ const planSessionId = generatePlanSessionId(options.sessionId);
401
+
381
402
  // Create top-level OTEL span for the entire execute_plan invocation
382
403
  const planSpan = tracer?.createToolSpan?.('execute_plan', {
383
404
  'dsl.description': description || '',
384
405
  'dsl.code_length': code.length,
385
406
  'dsl.code': code,
386
407
  'dsl.max_retries': maxRetries,
408
+ 'dsl.plan_session_id': planSessionId,
387
409
  }) || null;
388
410
 
411
+ // Build runtime with the unique planSessionId for isolated search pagination
412
+ const planRuntime = buildRuntime(planSessionId);
413
+
389
414
  // Strip XML tags and markdown fences LLMs sometimes wrap code in
390
415
  let currentCode = stripCodeWrapping(code);
391
416
  let lastError = null;
@@ -446,7 +471,7 @@ RULES REMINDER:
446
471
  }
447
472
  }
448
473
 
449
- const result = await getRuntime().execute(currentCode, description);
474
+ const result = await planRuntime.execute(currentCode, description);
450
475
 
451
476
  if (result.status === 'success') {
452
477
  finalOutput = formatSuccess(result, description, attempt, outputBuffer);
@@ -574,8 +599,15 @@ function formatSuccess(result, description, attempt, outputBuffer) {
574
599
 
575
600
  // Format the result value
576
601
  const resultValue = result.result;
602
+ const hasOutputBufferContent = outputBuffer && outputBuffer.items && outputBuffer.items.length > 0;
577
603
  if (resultValue === undefined || resultValue === null) {
578
- output += 'Plan completed (no return value).';
604
+ if (hasOutputBufferContent) {
605
+ // output() was used but no return statement — tell LLM the script succeeded
606
+ const totalChars = outputBuffer.items.reduce((sum, item) => sum + item.length, 0);
607
+ output += `Plan completed successfully. Output captured (${totalChars} chars) via output() and will be included in the final response.`;
608
+ } else {
609
+ output += 'Plan completed (no return value).';
610
+ }
579
611
  } else if (typeof resultValue === 'string') {
580
612
  output += `Result:\n${resultValue}`;
581
613
  } else {
@@ -56126,8 +56126,13 @@ function stripCodeWrapping(code) {
56126
56126
  s4 = decodeHtmlEntities(s4);
56127
56127
  return s4.trim();
56128
56128
  }
56129
- function buildToolImplementations(configOptions) {
56130
- const { sessionId, cwd } = configOptions;
56129
+ function generatePlanSessionId(baseSessionId) {
56130
+ const uniquePart = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID().slice(0, 8) : `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
56131
+ return `${baseSessionId || "plan"}-${uniquePart}`;
56132
+ }
56133
+ function buildToolImplementations(configOptions, planSessionId) {
56134
+ const { cwd } = configOptions;
56135
+ const sessionId = planSessionId || configOptions.sessionId;
56131
56136
  const tools2 = {};
56132
56137
  tools2.search = {
56133
56138
  execute: async (params) => {
@@ -56313,7 +56318,7 @@ function createExecutePlanTool(options) {
56313
56318
  const isMcpToolAllowed = options.isMcpToolAllowed || (() => true);
56314
56319
  let cachedMcpBridge = null;
56315
56320
  let runtime = null;
56316
- function buildRuntime() {
56321
+ function buildRuntime(planSessionId) {
56317
56322
  const currentMcpBridge = getMcpBridge();
56318
56323
  const currentMcpTools = getMcpTools();
56319
56324
  const filteredMcpTools = {};
@@ -56335,7 +56340,7 @@ function createExecutePlanTool(options) {
56335
56340
  } else {
56336
56341
  llmCallFn = llmCallFn || buildLLMCall(options);
56337
56342
  runtimeOptions = {
56338
- toolImplementations: buildToolImplementations(options),
56343
+ toolImplementations: buildToolImplementations(options, planSessionId),
56339
56344
  llmCall: llmCallFn,
56340
56345
  mcpBridge: currentMcpBridge,
56341
56346
  mcpTools: filteredMcpTools,
@@ -56363,12 +56368,15 @@ function createExecutePlanTool(options) {
56363
56368
  description: "Execute a JavaScript DSL program to orchestrate tool calls. Use for batch processing, paginated APIs, multi-step workflows where intermediate data is large. Write simple synchronous-looking code \u2014 do NOT use async/await.",
56364
56369
  parameters: executePlanSchema,
56365
56370
  execute: async ({ code, description }) => {
56371
+ const planSessionId = generatePlanSessionId(options.sessionId);
56366
56372
  const planSpan = tracer?.createToolSpan?.("execute_plan", {
56367
56373
  "dsl.description": description || "",
56368
56374
  "dsl.code_length": code.length,
56369
56375
  "dsl.code": code,
56370
- "dsl.max_retries": maxRetries
56376
+ "dsl.max_retries": maxRetries,
56377
+ "dsl.plan_session_id": planSessionId
56371
56378
  }) || null;
56379
+ const planRuntime = buildRuntime(planSessionId);
56372
56380
  let currentCode = stripCodeWrapping(code);
56373
56381
  let lastError = null;
56374
56382
  let finalOutput;
@@ -56424,7 +56432,7 @@ Original error: ${lastError}`;
56424
56432
  return finalOutput;
56425
56433
  }
56426
56434
  }
56427
- const result = await getRuntime().execute(currentCode, description);
56435
+ const result = await planRuntime.execute(currentCode, description);
56428
56436
  if (result.status === "success") {
56429
56437
  finalOutput = formatSuccess(result, description, attempt, outputBuffer);
56430
56438
  planSpan?.setAttributes?.({
@@ -56522,8 +56530,14 @@ ${userLogs.join("\n")}
56522
56530
  }
56523
56531
  }
56524
56532
  const resultValue = result.result;
56533
+ const hasOutputBufferContent = outputBuffer && outputBuffer.items && outputBuffer.items.length > 0;
56525
56534
  if (resultValue === void 0 || resultValue === null) {
56526
- output += "Plan completed (no return value).";
56535
+ if (hasOutputBufferContent) {
56536
+ const totalChars = outputBuffer.items.reduce((sum, item) => sum + item.length, 0);
56537
+ output += `Plan completed successfully. Output captured (${totalChars} chars) via output() and will be included in the final response.`;
56538
+ } else {
56539
+ output += "Plan completed (no return value).";
56540
+ }
56527
56541
  } else if (typeof resultValue === "string") {
56528
56542
  output += `Result:
56529
56543
  ${resultValue}`;
@@ -108531,6 +108545,7 @@ var init_ProbeAgent = __esm({
108531
108545
  initializeTools() {
108532
108546
  const isToolAllowed = (toolName) => this.allowedTools.isEnabled(toolName);
108533
108547
  this._outputBuffer = { items: [] };
108548
+ this._extractedRawBlocks = [];
108534
108549
  const configOptions = {
108535
108550
  sessionId: this.sessionId,
108536
108551
  debug: this.debug,
@@ -110265,6 +110280,7 @@ You are working with a workspace. Available paths: ${workspaceDesc}
110265
110280
  const oldHistoryLength = this.history.length;
110266
110281
  if (this._outputBuffer && !options?._schemaFormatted && !options?._completionPromptProcessed) {
110267
110282
  this._outputBuffer.items = [];
110283
+ this._extractedRawBlocks = [];
110268
110284
  }
110269
110285
  if (this.enableTasks) {
110270
110286
  try {
@@ -110764,13 +110780,12 @@ You are working with a workspace. Available paths: ${workspaceDesc}
110764
110780
  }
110765
110781
  const executionResult = await this.mcpBridge.mcpTools[toolName].execute(params);
110766
110782
  let toolResultContent = typeof executionResult === "string" ? executionResult : JSON.stringify(executionResult, null, 2);
110767
- if (this._outputBuffer) {
110768
- const { cleanedContent, extractedBlocks } = extractRawOutputBlocks(toolResultContent, this._outputBuffer);
110769
- if (extractedBlocks.length > 0) {
110770
- toolResultContent = cleanedContent;
110771
- if (this.debug) {
110772
- console.log(`[DEBUG] Extracted ${extractedBlocks.length} raw output blocks (${extractedBlocks.reduce((sum, b4) => sum + b4.length, 0)} chars) to output buffer`);
110773
- }
110783
+ const { cleanedContent, extractedBlocks } = extractRawOutputBlocks(toolResultContent);
110784
+ if (extractedBlocks.length > 0) {
110785
+ toolResultContent = cleanedContent;
110786
+ this._extractedRawBlocks.push(...extractedBlocks);
110787
+ if (this.debug) {
110788
+ console.log(`[DEBUG] Extracted ${extractedBlocks.length} raw output blocks (${extractedBlocks.reduce((sum, b4) => sum + b4.length, 0)} chars) from tool result`);
110774
110789
  }
110775
110790
  }
110776
110791
  try {
@@ -110979,13 +110994,12 @@ ${errorXml}
110979
110994
  const wsPrefix = this.workspaceRoot.endsWith(import_path17.sep) ? this.workspaceRoot : this.workspaceRoot + import_path17.sep;
110980
110995
  toolResultContent = toolResultContent.split(wsPrefix).join("");
110981
110996
  }
110982
- if (this._outputBuffer) {
110983
- const { cleanedContent, extractedBlocks } = extractRawOutputBlocks(toolResultContent, this._outputBuffer);
110984
- if (extractedBlocks.length > 0) {
110985
- toolResultContent = cleanedContent;
110986
- if (this.debug) {
110987
- console.log(`[DEBUG] Extracted ${extractedBlocks.length} raw output blocks (${extractedBlocks.reduce((sum, b4) => sum + b4.length, 0)} chars) to output buffer`);
110988
- }
110997
+ const { cleanedContent, extractedBlocks } = extractRawOutputBlocks(toolResultContent);
110998
+ if (extractedBlocks.length > 0) {
110999
+ toolResultContent = cleanedContent;
111000
+ this._extractedRawBlocks.push(...extractedBlocks);
111001
+ if (this.debug) {
111002
+ console.log(`[DEBUG] Extracted ${extractedBlocks.length} raw output blocks (${extractedBlocks.reduce((sum, b4) => sum + b4.length, 0)} chars) from tool result`);
110989
111003
  }
110990
111004
  }
110991
111005
  try {
@@ -111294,6 +111308,7 @@ ${finalResult}
111294
111308
 
111295
111309
  After reviewing, provide your final answer using attempt_completion.`;
111296
111310
  const savedOutputItems = this._outputBuffer ? [...this._outputBuffer.items] : [];
111311
+ const savedExtractedBlocks = this._extractedRawBlocks ? [...this._extractedRawBlocks] : [];
111297
111312
  const completionResult = await this.answer(completionPromptMessage, [], {
111298
111313
  ...options,
111299
111314
  _completionPromptProcessed: true
@@ -111301,6 +111316,7 @@ After reviewing, provide your final answer using attempt_completion.`;
111301
111316
  if (this._outputBuffer) {
111302
111317
  this._outputBuffer.items = savedOutputItems;
111303
111318
  }
111319
+ this._extractedRawBlocks = savedExtractedBlocks;
111304
111320
  finalResult = completionResult;
111305
111321
  if (this.debug) {
111306
111322
  console.log(`[DEBUG] Completion prompt finished. New result length: ${finalResult?.length || 0}`);
@@ -111346,7 +111362,9 @@ NOT: {"type": "object", "properties": {"name": {"type": "string"}}}
111346
111362
  Convert your previous response content into actual JSON data that follows this schema structure.`;
111347
111363
  finalResult = await this.answer(schemaPrompt, [], {
111348
111364
  ...options,
111349
- _schemaFormatted: true
111365
+ _schemaFormatted: true,
111366
+ _completionPromptProcessed: true
111367
+ // Prevent cascading completion prompts in retry calls
111350
111368
  });
111351
111369
  if (!this.disableMermaidValidation) {
111352
111370
  try {
@@ -111550,8 +111568,10 @@ Convert your previous response content into actual JSON data that follows this s
111550
111568
  finalResult = await this.answer(schemaDefinitionPrompt, [], {
111551
111569
  ...options,
111552
111570
  _schemaFormatted: true,
111553
- _skipValidation: true
111571
+ _skipValidation: true,
111554
111572
  // Skip validation in recursive correction calls to prevent loops
111573
+ _completionPromptProcessed: true
111574
+ // Prevent cascading completion prompts in retry calls
111555
111575
  });
111556
111576
  finalResult = cleanSchemaResponse(finalResult);
111557
111577
  validation = validateJsonResponse(finalResult);
@@ -111604,8 +111624,10 @@ Convert your previous response content into actual JSON data that follows this s
111604
111624
  _schemaFormatted: true,
111605
111625
  _skipValidation: true,
111606
111626
  // Skip validation in recursive correction calls to prevent loops
111607
- _disableTools: true
111627
+ _disableTools: true,
111608
111628
  // Only allow attempt_completion - prevent AI from using search/query tools
111629
+ _completionPromptProcessed: true
111630
+ // Prevent cascading completion prompts in retry calls
111609
111631
  });
111610
111632
  finalResult = cleanSchemaResponse(finalResult);
111611
111633
  validation = validateJsonResponse(finalResult, { debug: this.debug });
@@ -111675,8 +111697,12 @@ Convert your previous response content into actual JSON data that follows this s
111675
111697
  console.log(`[DEBUG] Removed thinking tags from final result`);
111676
111698
  }
111677
111699
  }
111678
- if (this._outputBuffer && this._outputBuffer.items.length > 0 && !options._schemaFormatted) {
111679
- const outputContent = this._outputBuffer.items.join("\n\n");
111700
+ const allOutputItems = [
111701
+ ...this._outputBuffer?.items || [],
111702
+ ...this._extractedRawBlocks || []
111703
+ ];
111704
+ if (allOutputItems.length > 0 && !options._schemaFormatted && !options._completionPromptProcessed) {
111705
+ const outputContent = allOutputItems.join("\n\n");
111680
111706
  if (options.schema) {
111681
111707
  finalResult = (finalResult || "") + "\n<<<RAW_OUTPUT>>>\n" + outputContent + "\n<<<END_RAW_OUTPUT>>>";
111682
111708
  } else {
@@ -111686,9 +111712,10 @@ Convert your previous response content into actual JSON data that follows this s
111686
111712
  options.onStream("\n\n" + outputContent);
111687
111713
  }
111688
111714
  if (this.debug) {
111689
- console.log(`[DEBUG] Appended ${this._outputBuffer.items.length} output buffer items (${outputContent.length} chars) to final result${options.schema ? " (with RAW_OUTPUT delimiters)" : ""}`);
111715
+ console.log(`[DEBUG] Appended ${allOutputItems.length} output items (${outputContent.length} chars) to final result${options.schema ? " (with RAW_OUTPUT delimiters)" : ""}`);
111690
111716
  }
111691
111717
  this._outputBuffer.items = [];
111718
+ this._extractedRawBlocks = [];
111692
111719
  }
111693
111720
  return finalResult;
111694
111721
  } catch (error2) {
package/cjs/index.cjs CHANGED
@@ -103899,8 +103899,13 @@ function stripCodeWrapping(code) {
103899
103899
  s4 = decodeHtmlEntities2(s4);
103900
103900
  return s4.trim();
103901
103901
  }
103902
- function buildToolImplementations(configOptions) {
103903
- const { sessionId, cwd } = configOptions;
103902
+ function generatePlanSessionId(baseSessionId) {
103903
+ const uniquePart = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID().slice(0, 8) : `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
103904
+ return `${baseSessionId || "plan"}-${uniquePart}`;
103905
+ }
103906
+ function buildToolImplementations(configOptions, planSessionId) {
103907
+ const { cwd } = configOptions;
103908
+ const sessionId = planSessionId || configOptions.sessionId;
103904
103909
  const tools2 = {};
103905
103910
  tools2.search = {
103906
103911
  execute: async (params) => {
@@ -104086,7 +104091,7 @@ function createExecutePlanTool(options) {
104086
104091
  const isMcpToolAllowed = options.isMcpToolAllowed || (() => true);
104087
104092
  let cachedMcpBridge = null;
104088
104093
  let runtime = null;
104089
- function buildRuntime() {
104094
+ function buildRuntime(planSessionId) {
104090
104095
  const currentMcpBridge = getMcpBridge();
104091
104096
  const currentMcpTools = getMcpTools();
104092
104097
  const filteredMcpTools = {};
@@ -104108,7 +104113,7 @@ function createExecutePlanTool(options) {
104108
104113
  } else {
104109
104114
  llmCallFn = llmCallFn || buildLLMCall(options);
104110
104115
  runtimeOptions = {
104111
- toolImplementations: buildToolImplementations(options),
104116
+ toolImplementations: buildToolImplementations(options, planSessionId),
104112
104117
  llmCall: llmCallFn,
104113
104118
  mcpBridge: currentMcpBridge,
104114
104119
  mcpTools: filteredMcpTools,
@@ -104136,12 +104141,15 @@ function createExecutePlanTool(options) {
104136
104141
  description: "Execute a JavaScript DSL program to orchestrate tool calls. Use for batch processing, paginated APIs, multi-step workflows where intermediate data is large. Write simple synchronous-looking code \u2014 do NOT use async/await.",
104137
104142
  parameters: executePlanSchema,
104138
104143
  execute: async ({ code, description }) => {
104144
+ const planSessionId = generatePlanSessionId(options.sessionId);
104139
104145
  const planSpan = tracer?.createToolSpan?.("execute_plan", {
104140
104146
  "dsl.description": description || "",
104141
104147
  "dsl.code_length": code.length,
104142
104148
  "dsl.code": code,
104143
- "dsl.max_retries": maxRetries
104149
+ "dsl.max_retries": maxRetries,
104150
+ "dsl.plan_session_id": planSessionId
104144
104151
  }) || null;
104152
+ const planRuntime = buildRuntime(planSessionId);
104145
104153
  let currentCode = stripCodeWrapping(code);
104146
104154
  let lastError = null;
104147
104155
  let finalOutput;
@@ -104197,7 +104205,7 @@ Original error: ${lastError}`;
104197
104205
  return finalOutput;
104198
104206
  }
104199
104207
  }
104200
- const result = await getRuntime().execute(currentCode, description);
104208
+ const result = await planRuntime.execute(currentCode, description);
104201
104209
  if (result.status === "success") {
104202
104210
  finalOutput = formatSuccess(result, description, attempt, outputBuffer);
104203
104211
  planSpan?.setAttributes?.({
@@ -104295,8 +104303,14 @@ ${userLogs.join("\n")}
104295
104303
  }
104296
104304
  }
104297
104305
  const resultValue = result.result;
104306
+ const hasOutputBufferContent = outputBuffer && outputBuffer.items && outputBuffer.items.length > 0;
104298
104307
  if (resultValue === void 0 || resultValue === null) {
104299
- output += "Plan completed (no return value).";
104308
+ if (hasOutputBufferContent) {
104309
+ const totalChars = outputBuffer.items.reduce((sum, item) => sum + item.length, 0);
104310
+ output += `Plan completed successfully. Output captured (${totalChars} chars) via output() and will be included in the final response.`;
104311
+ } else {
104312
+ output += "Plan completed (no return value).";
104313
+ }
104300
104314
  } else if (typeof resultValue === "string") {
104301
104315
  output += `Result:
104302
104316
  ${resultValue}`;
@@ -106884,6 +106898,7 @@ var init_ProbeAgent = __esm({
106884
106898
  initializeTools() {
106885
106899
  const isToolAllowed = (toolName) => this.allowedTools.isEnabled(toolName);
106886
106900
  this._outputBuffer = { items: [] };
106901
+ this._extractedRawBlocks = [];
106887
106902
  const configOptions = {
106888
106903
  sessionId: this.sessionId,
106889
106904
  debug: this.debug,
@@ -108618,6 +108633,7 @@ You are working with a workspace. Available paths: ${workspaceDesc}
108618
108633
  const oldHistoryLength = this.history.length;
108619
108634
  if (this._outputBuffer && !options?._schemaFormatted && !options?._completionPromptProcessed) {
108620
108635
  this._outputBuffer.items = [];
108636
+ this._extractedRawBlocks = [];
108621
108637
  }
108622
108638
  if (this.enableTasks) {
108623
108639
  try {
@@ -109117,13 +109133,12 @@ You are working with a workspace. Available paths: ${workspaceDesc}
109117
109133
  }
109118
109134
  const executionResult = await this.mcpBridge.mcpTools[toolName].execute(params);
109119
109135
  let toolResultContent = typeof executionResult === "string" ? executionResult : JSON.stringify(executionResult, null, 2);
109120
- if (this._outputBuffer) {
109121
- const { cleanedContent, extractedBlocks } = extractRawOutputBlocks(toolResultContent, this._outputBuffer);
109122
- if (extractedBlocks.length > 0) {
109123
- toolResultContent = cleanedContent;
109124
- if (this.debug) {
109125
- console.log(`[DEBUG] Extracted ${extractedBlocks.length} raw output blocks (${extractedBlocks.reduce((sum, b4) => sum + b4.length, 0)} chars) to output buffer`);
109126
- }
109136
+ const { cleanedContent, extractedBlocks } = extractRawOutputBlocks(toolResultContent);
109137
+ if (extractedBlocks.length > 0) {
109138
+ toolResultContent = cleanedContent;
109139
+ this._extractedRawBlocks.push(...extractedBlocks);
109140
+ if (this.debug) {
109141
+ console.log(`[DEBUG] Extracted ${extractedBlocks.length} raw output blocks (${extractedBlocks.reduce((sum, b4) => sum + b4.length, 0)} chars) from tool result`);
109127
109142
  }
109128
109143
  }
109129
109144
  try {
@@ -109332,13 +109347,12 @@ ${errorXml}
109332
109347
  const wsPrefix = this.workspaceRoot.endsWith(import_path15.sep) ? this.workspaceRoot : this.workspaceRoot + import_path15.sep;
109333
109348
  toolResultContent = toolResultContent.split(wsPrefix).join("");
109334
109349
  }
109335
- if (this._outputBuffer) {
109336
- const { cleanedContent, extractedBlocks } = extractRawOutputBlocks(toolResultContent, this._outputBuffer);
109337
- if (extractedBlocks.length > 0) {
109338
- toolResultContent = cleanedContent;
109339
- if (this.debug) {
109340
- console.log(`[DEBUG] Extracted ${extractedBlocks.length} raw output blocks (${extractedBlocks.reduce((sum, b4) => sum + b4.length, 0)} chars) to output buffer`);
109341
- }
109350
+ const { cleanedContent, extractedBlocks } = extractRawOutputBlocks(toolResultContent);
109351
+ if (extractedBlocks.length > 0) {
109352
+ toolResultContent = cleanedContent;
109353
+ this._extractedRawBlocks.push(...extractedBlocks);
109354
+ if (this.debug) {
109355
+ console.log(`[DEBUG] Extracted ${extractedBlocks.length} raw output blocks (${extractedBlocks.reduce((sum, b4) => sum + b4.length, 0)} chars) from tool result`);
109342
109356
  }
109343
109357
  }
109344
109358
  try {
@@ -109647,6 +109661,7 @@ ${finalResult}
109647
109661
 
109648
109662
  After reviewing, provide your final answer using attempt_completion.`;
109649
109663
  const savedOutputItems = this._outputBuffer ? [...this._outputBuffer.items] : [];
109664
+ const savedExtractedBlocks = this._extractedRawBlocks ? [...this._extractedRawBlocks] : [];
109650
109665
  const completionResult = await this.answer(completionPromptMessage, [], {
109651
109666
  ...options,
109652
109667
  _completionPromptProcessed: true
@@ -109654,6 +109669,7 @@ After reviewing, provide your final answer using attempt_completion.`;
109654
109669
  if (this._outputBuffer) {
109655
109670
  this._outputBuffer.items = savedOutputItems;
109656
109671
  }
109672
+ this._extractedRawBlocks = savedExtractedBlocks;
109657
109673
  finalResult = completionResult;
109658
109674
  if (this.debug) {
109659
109675
  console.log(`[DEBUG] Completion prompt finished. New result length: ${finalResult?.length || 0}`);
@@ -109699,7 +109715,9 @@ NOT: {"type": "object", "properties": {"name": {"type": "string"}}}
109699
109715
  Convert your previous response content into actual JSON data that follows this schema structure.`;
109700
109716
  finalResult = await this.answer(schemaPrompt, [], {
109701
109717
  ...options,
109702
- _schemaFormatted: true
109718
+ _schemaFormatted: true,
109719
+ _completionPromptProcessed: true
109720
+ // Prevent cascading completion prompts in retry calls
109703
109721
  });
109704
109722
  if (!this.disableMermaidValidation) {
109705
109723
  try {
@@ -109903,8 +109921,10 @@ Convert your previous response content into actual JSON data that follows this s
109903
109921
  finalResult = await this.answer(schemaDefinitionPrompt, [], {
109904
109922
  ...options,
109905
109923
  _schemaFormatted: true,
109906
- _skipValidation: true
109924
+ _skipValidation: true,
109907
109925
  // Skip validation in recursive correction calls to prevent loops
109926
+ _completionPromptProcessed: true
109927
+ // Prevent cascading completion prompts in retry calls
109908
109928
  });
109909
109929
  finalResult = cleanSchemaResponse(finalResult);
109910
109930
  validation = validateJsonResponse(finalResult);
@@ -109957,8 +109977,10 @@ Convert your previous response content into actual JSON data that follows this s
109957
109977
  _schemaFormatted: true,
109958
109978
  _skipValidation: true,
109959
109979
  // Skip validation in recursive correction calls to prevent loops
109960
- _disableTools: true
109980
+ _disableTools: true,
109961
109981
  // Only allow attempt_completion - prevent AI from using search/query tools
109982
+ _completionPromptProcessed: true
109983
+ // Prevent cascading completion prompts in retry calls
109962
109984
  });
109963
109985
  finalResult = cleanSchemaResponse(finalResult);
109964
109986
  validation = validateJsonResponse(finalResult, { debug: this.debug });
@@ -110028,8 +110050,12 @@ Convert your previous response content into actual JSON data that follows this s
110028
110050
  console.log(`[DEBUG] Removed thinking tags from final result`);
110029
110051
  }
110030
110052
  }
110031
- if (this._outputBuffer && this._outputBuffer.items.length > 0 && !options._schemaFormatted) {
110032
- const outputContent = this._outputBuffer.items.join("\n\n");
110053
+ const allOutputItems = [
110054
+ ...this._outputBuffer?.items || [],
110055
+ ...this._extractedRawBlocks || []
110056
+ ];
110057
+ if (allOutputItems.length > 0 && !options._schemaFormatted && !options._completionPromptProcessed) {
110058
+ const outputContent = allOutputItems.join("\n\n");
110033
110059
  if (options.schema) {
110034
110060
  finalResult = (finalResult || "") + "\n<<<RAW_OUTPUT>>>\n" + outputContent + "\n<<<END_RAW_OUTPUT>>>";
110035
110061
  } else {
@@ -110039,9 +110065,10 @@ Convert your previous response content into actual JSON data that follows this s
110039
110065
  options.onStream("\n\n" + outputContent);
110040
110066
  }
110041
110067
  if (this.debug) {
110042
- console.log(`[DEBUG] Appended ${this._outputBuffer.items.length} output buffer items (${outputContent.length} chars) to final result${options.schema ? " (with RAW_OUTPUT delimiters)" : ""}`);
110068
+ console.log(`[DEBUG] Appended ${allOutputItems.length} output items (${outputContent.length} chars) to final result${options.schema ? " (with RAW_OUTPUT delimiters)" : ""}`);
110043
110069
  }
110044
110070
  this._outputBuffer.items = [];
110071
+ this._extractedRawBlocks = [];
110045
110072
  }
110046
110073
  return finalResult;
110047
110074
  } catch (error2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@probelabs/probe",
3
- "version": "0.6.0-rc250",
3
+ "version": "0.6.0-rc252",
4
4
  "description": "Node.js wrapper for the probe code search tool",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -819,6 +819,11 @@ export class ProbeAgent {
819
819
  // reset at the start of each answer() call
820
820
  this._outputBuffer = { items: [] };
821
821
 
822
+ // Separate accumulator for extracted RAW_OUTPUT blocks from tool results.
823
+ // This is distinct from _outputBuffer to prevent the cycle where:
824
+ // formatSuccess wraps → extract re-adds → next execute_plan re-wraps (issue #438)
825
+ this._extractedRawBlocks = [];
826
+
822
827
  const configOptions = {
823
828
  sessionId: this.sessionId,
824
829
  debug: this.debug,
@@ -2910,6 +2915,8 @@ Follow these instructions carefully:
2910
2915
  // Both must preserve the output buffer so the parent call can append it.
2911
2916
  if (this._outputBuffer && !options?._schemaFormatted && !options?._completionPromptProcessed) {
2912
2917
  this._outputBuffer.items = [];
2918
+ // Also reset the extracted blocks accumulator (issue #438)
2919
+ this._extractedRawBlocks = [];
2913
2920
  }
2914
2921
 
2915
2922
  // START CHECKPOINT: Initialize task management for this request
@@ -3629,15 +3636,17 @@ Follow these instructions carefully:
3629
3636
 
3630
3637
  let toolResultContent = typeof executionResult === 'string' ? executionResult : JSON.stringify(executionResult, null, 2);
3631
3638
 
3632
- // Extract raw output blocks and pass them through to output buffer (before truncation)
3639
+ // Extract raw output blocks from tool result (before truncation)
3633
3640
  // This prevents LLM from processing/hallucinating large structured output from execute_plan
3634
- if (this._outputBuffer) {
3635
- const { cleanedContent, extractedBlocks } = extractRawOutputBlocks(toolResultContent, this._outputBuffer);
3636
- if (extractedBlocks.length > 0) {
3637
- toolResultContent = cleanedContent;
3638
- if (this.debug) {
3639
- console.log(`[DEBUG] Extracted ${extractedBlocks.length} raw output blocks (${extractedBlocks.reduce((sum, b) => sum + b.length, 0)} chars) to output buffer`);
3640
- }
3641
+ // Push to _extractedRawBlocks (NOT _outputBuffer) to prevent the cycle where:
3642
+ // formatSuccess wraps extract re-adds → next execute_plan re-wraps (issue #438)
3643
+ const { cleanedContent, extractedBlocks } = extractRawOutputBlocks(toolResultContent);
3644
+ if (extractedBlocks.length > 0) {
3645
+ toolResultContent = cleanedContent;
3646
+ // Accumulate extracted blocks separately from DSL output() buffer
3647
+ this._extractedRawBlocks.push(...extractedBlocks);
3648
+ if (this.debug) {
3649
+ console.log(`[DEBUG] Extracted ${extractedBlocks.length} raw output blocks (${extractedBlocks.reduce((sum, b) => sum + b.length, 0)} chars) from tool result`);
3641
3650
  }
3642
3651
  }
3643
3652
 
@@ -3887,15 +3896,17 @@ Follow these instructions carefully:
3887
3896
  toolResultContent = toolResultContent.split(wsPrefix).join('');
3888
3897
  }
3889
3898
 
3890
- // Extract raw output blocks and pass them through to output buffer (before truncation)
3899
+ // Extract raw output blocks from tool result (before truncation)
3891
3900
  // This prevents LLM from processing/hallucinating large structured output from execute_plan
3892
- if (this._outputBuffer) {
3893
- const { cleanedContent, extractedBlocks } = extractRawOutputBlocks(toolResultContent, this._outputBuffer);
3894
- if (extractedBlocks.length > 0) {
3895
- toolResultContent = cleanedContent;
3896
- if (this.debug) {
3897
- console.log(`[DEBUG] Extracted ${extractedBlocks.length} raw output blocks (${extractedBlocks.reduce((sum, b) => sum + b.length, 0)} chars) to output buffer`);
3898
- }
3901
+ // Push to _extractedRawBlocks (NOT _outputBuffer) to prevent the cycle where:
3902
+ // formatSuccess wraps extract re-adds → next execute_plan re-wraps (issue #438)
3903
+ const { cleanedContent, extractedBlocks } = extractRawOutputBlocks(toolResultContent);
3904
+ if (extractedBlocks.length > 0) {
3905
+ toolResultContent = cleanedContent;
3906
+ // Accumulate extracted blocks separately from DSL output() buffer
3907
+ this._extractedRawBlocks.push(...extractedBlocks);
3908
+ if (this.debug) {
3909
+ console.log(`[DEBUG] Extracted ${extractedBlocks.length} raw output blocks (${extractedBlocks.reduce((sum, b) => sum + b.length, 0)} chars) from tool result`);
3899
3910
  }
3900
3911
  }
3901
3912
 
@@ -4314,16 +4325,18 @@ After reviewing, provide your final answer using attempt_completion.`;
4314
4325
 
4315
4326
  // Make a follow-up call with the completion prompt
4316
4327
  // Pass _completionPromptProcessed to prevent infinite loops
4317
- // Save output buffer — the recursive answer() must not destroy DSL output() content
4328
+ // Save output buffers — the recursive answer() must not destroy DSL output() content
4318
4329
  const savedOutputItems = this._outputBuffer ? [...this._outputBuffer.items] : [];
4330
+ const savedExtractedBlocks = this._extractedRawBlocks ? [...this._extractedRawBlocks] : [];
4319
4331
  const completionResult = await this.answer(completionPromptMessage, [], {
4320
4332
  ...options,
4321
4333
  _completionPromptProcessed: true
4322
4334
  });
4323
- // Restore output buffer so the parent call can append it to the final result
4335
+ // Restore output buffers so the parent call can append them to the final result
4324
4336
  if (this._outputBuffer) {
4325
4337
  this._outputBuffer.items = savedOutputItems;
4326
4338
  }
4339
+ this._extractedRawBlocks = savedExtractedBlocks;
4327
4340
 
4328
4341
  // Update finalResult with the result from the completion prompt
4329
4342
  finalResult = completionResult;
@@ -4383,7 +4396,8 @@ Convert your previous response content into actual JSON data that follows this s
4383
4396
  // Call answer recursively with _schemaFormatted flag to prevent infinite loop
4384
4397
  finalResult = await this.answer(schemaPrompt, [], {
4385
4398
  ...options,
4386
- _schemaFormatted: true
4399
+ _schemaFormatted: true,
4400
+ _completionPromptProcessed: true // Prevent cascading completion prompts in retry calls
4387
4401
  });
4388
4402
 
4389
4403
  // Step 2: Validate and fix Mermaid diagrams if present (BEFORE cleaning schema)
@@ -4642,7 +4656,8 @@ Convert your previous response content into actual JSON data that follows this s
4642
4656
  finalResult = await this.answer(schemaDefinitionPrompt, [], {
4643
4657
  ...options,
4644
4658
  _schemaFormatted: true,
4645
- _skipValidation: true // Skip validation in recursive correction calls to prevent loops
4659
+ _skipValidation: true, // Skip validation in recursive correction calls to prevent loops
4660
+ _completionPromptProcessed: true // Prevent cascading completion prompts in retry calls
4646
4661
  });
4647
4662
  finalResult = cleanSchemaResponse(finalResult);
4648
4663
  validation = validateJsonResponse(finalResult);
@@ -4702,7 +4717,8 @@ Convert your previous response content into actual JSON data that follows this s
4702
4717
  ...options,
4703
4718
  _schemaFormatted: true,
4704
4719
  _skipValidation: true, // Skip validation in recursive correction calls to prevent loops
4705
- _disableTools: true // Only allow attempt_completion - prevent AI from using search/query tools
4720
+ _disableTools: true, // Only allow attempt_completion - prevent AI from using search/query tools
4721
+ _completionPromptProcessed: true // Prevent cascading completion prompts in retry calls
4706
4722
  });
4707
4723
  finalResult = cleanSchemaResponse(finalResult);
4708
4724
 
@@ -4787,8 +4803,15 @@ Convert your previous response content into actual JSON data that follows this s
4787
4803
  }
4788
4804
 
4789
4805
  // Append DSL output buffer directly to response (bypasses LLM rewriting)
4790
- if (this._outputBuffer && this._outputBuffer.items.length > 0 && !options._schemaFormatted) {
4791
- const outputContent = this._outputBuffer.items.join('\n\n');
4806
+ // Skip during _completionPromptProcessed only the parent answer() should append the buffer.
4807
+ // Combine _outputBuffer (from DSL output() calls) and _extractedRawBlocks (from tool results)
4808
+ // Using separate accumulators prevents the cycle described in issue #438.
4809
+ const allOutputItems = [
4810
+ ...(this._outputBuffer?.items || []),
4811
+ ...(this._extractedRawBlocks || [])
4812
+ ];
4813
+ if (allOutputItems.length > 0 && !options._schemaFormatted && !options._completionPromptProcessed) {
4814
+ const outputContent = allOutputItems.join('\n\n');
4792
4815
  if (options.schema) {
4793
4816
  // Schema response — the finalResult is JSON. Wrap output in RAW_OUTPUT
4794
4817
  // delimiters so clients (visor, etc.) can extract and propagate the
@@ -4801,9 +4824,10 @@ Convert your previous response content into actual JSON data that follows this s
4801
4824
  options.onStream('\n\n' + outputContent);
4802
4825
  }
4803
4826
  if (this.debug) {
4804
- console.log(`[DEBUG] Appended ${this._outputBuffer.items.length} output buffer items (${outputContent.length} chars) to final result${options.schema ? ' (with RAW_OUTPUT delimiters)' : ''}`);
4827
+ console.log(`[DEBUG] Appended ${allOutputItems.length} output items (${outputContent.length} chars) to final result${options.schema ? ' (with RAW_OUTPUT delimiters)' : ''}`);
4805
4828
  }
4806
4829
  this._outputBuffer.items = [];
4830
+ this._extractedRawBlocks = [];
4807
4831
  }
4808
4832
 
4809
4833
  return finalResult;
@@ -65,14 +65,28 @@ function stripCodeWrapping(code) {
65
65
  return s.trim();
66
66
  }
67
67
 
68
+ /**
69
+ * Generate a unique session ID for this execute_plan invocation.
70
+ * Uses crypto.randomUUID if available, falls back to timestamp + random.
71
+ */
72
+ function generatePlanSessionId(baseSessionId) {
73
+ const uniquePart = typeof crypto !== 'undefined' && crypto.randomUUID
74
+ ? crypto.randomUUID().slice(0, 8)
75
+ : `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
76
+ return `${baseSessionId || 'plan'}-${uniquePart}`;
77
+ }
78
+
68
79
  /**
69
80
  * Build DSL-compatible tool implementations from the agent's configOptions.
70
81
  *
71
82
  * @param {Object} configOptions - Agent config (sessionId, cwd, provider, model, etc.)
83
+ * @param {string} [planSessionId] - Unique session ID for this execute_plan invocation
72
84
  * @returns {Object} toolImplementations for createDSLRuntime
73
85
  */
74
- function buildToolImplementations(configOptions) {
75
- const { sessionId, cwd } = configOptions;
86
+ function buildToolImplementations(configOptions, planSessionId) {
87
+ const { cwd } = configOptions;
88
+ // Use planSessionId for isolated pagination per execute_plan, fall back to global sessionId
89
+ const sessionId = planSessionId || configOptions.sessionId;
76
90
  const tools = {};
77
91
 
78
92
  tools.search = {
@@ -311,9 +325,11 @@ export function createExecutePlanTool(options) {
311
325
 
312
326
  /**
313
327
  * Build or rebuild the DSL runtime.
314
- * Called lazily on first execute() and when MCP bridge changes.
328
+ * Called for each execute() invocation with a unique planSessionId.
329
+ *
330
+ * @param {string} [planSessionId] - Unique session ID for this execute_plan invocation
315
331
  */
316
- function buildRuntime() {
332
+ function buildRuntime(planSessionId) {
317
333
  const currentMcpBridge = getMcpBridge();
318
334
  const currentMcpTools = getMcpTools();
319
335
 
@@ -340,7 +356,7 @@ export function createExecutePlanTool(options) {
340
356
  // Agent configOptions — build everything from the agent's config
341
357
  llmCallFn = llmCallFn || buildLLMCall(options);
342
358
  runtimeOptions = {
343
- toolImplementations: buildToolImplementations(options),
359
+ toolImplementations: buildToolImplementations(options, planSessionId),
344
360
  llmCall: llmCallFn,
345
361
  mcpBridge: currentMcpBridge,
346
362
  mcpTools: filteredMcpTools,
@@ -360,6 +376,7 @@ export function createExecutePlanTool(options) {
360
376
 
361
377
  /**
362
378
  * Get or rebuild the runtime if MCP state has changed.
379
+ * @deprecated Use buildRuntime(planSessionId) directly for unique sessions per execution
363
380
  */
364
381
  function getRuntime() {
365
382
  const currentMcpBridge = getMcpBridge();
@@ -378,14 +395,22 @@ export function createExecutePlanTool(options) {
378
395
  'Write simple synchronous-looking code — do NOT use async/await.',
379
396
  parameters: executePlanSchema,
380
397
  execute: async ({ code, description }) => {
398
+ // Generate a unique session ID for this execute_plan invocation
399
+ // This ensures search pagination is isolated per execute_plan call
400
+ const planSessionId = generatePlanSessionId(options.sessionId);
401
+
381
402
  // Create top-level OTEL span for the entire execute_plan invocation
382
403
  const planSpan = tracer?.createToolSpan?.('execute_plan', {
383
404
  'dsl.description': description || '',
384
405
  'dsl.code_length': code.length,
385
406
  'dsl.code': code,
386
407
  'dsl.max_retries': maxRetries,
408
+ 'dsl.plan_session_id': planSessionId,
387
409
  }) || null;
388
410
 
411
+ // Build runtime with the unique planSessionId for isolated search pagination
412
+ const planRuntime = buildRuntime(planSessionId);
413
+
389
414
  // Strip XML tags and markdown fences LLMs sometimes wrap code in
390
415
  let currentCode = stripCodeWrapping(code);
391
416
  let lastError = null;
@@ -446,7 +471,7 @@ RULES REMINDER:
446
471
  }
447
472
  }
448
473
 
449
- const result = await getRuntime().execute(currentCode, description);
474
+ const result = await planRuntime.execute(currentCode, description);
450
475
 
451
476
  if (result.status === 'success') {
452
477
  finalOutput = formatSuccess(result, description, attempt, outputBuffer);
@@ -574,8 +599,15 @@ function formatSuccess(result, description, attempt, outputBuffer) {
574
599
 
575
600
  // Format the result value
576
601
  const resultValue = result.result;
602
+ const hasOutputBufferContent = outputBuffer && outputBuffer.items && outputBuffer.items.length > 0;
577
603
  if (resultValue === undefined || resultValue === null) {
578
- output += 'Plan completed (no return value).';
604
+ if (hasOutputBufferContent) {
605
+ // output() was used but no return statement — tell LLM the script succeeded
606
+ const totalChars = outputBuffer.items.reduce((sum, item) => sum + item.length, 0);
607
+ output += `Plan completed successfully. Output captured (${totalChars} chars) via output() and will be included in the final response.`;
608
+ } else {
609
+ output += 'Plan completed (no return value).';
610
+ }
579
611
  } else if (typeof resultValue === 'string') {
580
612
  output += `Result:\n${resultValue}`;
581
613
  } else {