@probelabs/probe 0.6.0-rc205 → 0.6.0-rc206

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.
@@ -70,6 +70,7 @@ import { RetryManager, createRetryManagerFromEnv } from './RetryManager.js';
70
70
  import { FallbackManager, createFallbackManagerFromEnv, buildFallbackProvidersFromEnv } from './FallbackManager.js';
71
71
  import { handleContextLimitError } from './contextCompactor.js';
72
72
  import { formatErrorForAI, ParameterError } from '../utils/error-types.js';
73
+ import { truncateIfNeeded, getMaxOutputTokens } from './outputTruncator.js';
73
74
  import {
74
75
  TaskManager,
75
76
  createTaskTool,
@@ -145,6 +146,7 @@ export class ProbeAgent {
145
146
  * @param {boolean} [options.fallback.stopOnSuccess=true] - Stop on first success
146
147
  * @param {number} [options.fallback.maxTotalAttempts=10] - Maximum total attempts across all providers
147
148
  * @param {string} [options.completionPrompt] - Custom prompt to run after attempt_completion for validation/review (runs before mermaid/JSON validation)
149
+ * @param {number} [options.maxOutputTokens] - Maximum tokens for tool output before truncation (default: 20000, can also be set via PROBE_MAX_OUTPUT_TOKENS env var)
148
150
  */
149
151
  constructor(options = {}) {
150
152
  // Basic configuration
@@ -237,6 +239,9 @@ export class ProbeAgent {
237
239
  // Initialize token counter
238
240
  this.tokenCounter = new TokenCounter();
239
241
 
242
+ // Maximum output tokens for tool results (truncate if exceeded)
243
+ this.maxOutputTokens = getMaxOutputTokens(options.maxOutputTokens);
244
+
240
245
  if (this.debug) {
241
246
  console.log(`[DEBUG] Generated session ID for agent: ${this.sessionId}`);
242
247
  console.log(`[DEBUG] Maximum tool iterations configured: ${MAX_TOOL_ITERATIONS}`);
@@ -2882,7 +2887,24 @@ Follow these instructions carefully:
2882
2887
  // Execute MCP tool through the bridge
2883
2888
  const executionResult = await this.mcpBridge.mcpTools[toolName].execute(params);
2884
2889
 
2885
- const toolResultContent = typeof executionResult === 'string' ? executionResult : JSON.stringify(executionResult, null, 2);
2890
+ let toolResultContent = typeof executionResult === 'string' ? executionResult : JSON.stringify(executionResult, null, 2);
2891
+
2892
+ // Truncate if output exceeds token limit
2893
+ try {
2894
+ const truncateResult = await truncateIfNeeded(toolResultContent, this.tokenCounter, this.sessionId, this.maxOutputTokens);
2895
+ if (truncateResult.truncated) {
2896
+ toolResultContent = truncateResult.content;
2897
+ if (this.debug) {
2898
+ console.log(`[DEBUG] Tool output truncated: ${truncateResult.originalTokens} tokens -> saved to ${truncateResult.tempFilePath || 'N/A'}`);
2899
+ if (truncateResult.error) {
2900
+ console.log(`[DEBUG] Truncation file error: ${truncateResult.error}`);
2901
+ }
2902
+ }
2903
+ }
2904
+ } catch (truncateError) {
2905
+ // If truncation fails entirely, log and continue with original content
2906
+ console.error(`[WARN] Tool output truncation failed: ${truncateError.message}`);
2907
+ }
2886
2908
 
2887
2909
  // Log MCP tool result in debug mode
2888
2910
  if (this.debug) {
@@ -3059,10 +3081,28 @@ Follow these instructions carefully:
3059
3081
 
3060
3082
  // Add assistant response and tool result to conversation
3061
3083
  currentMessages.push({ role: 'assistant', content: assistantResponseContent });
3062
-
3063
- const toolResultContent = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult, null, 2);
3084
+
3085
+ let toolResultContent = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult, null, 2);
3086
+
3087
+ // Truncate if output exceeds token limit
3088
+ try {
3089
+ const truncateResult = await truncateIfNeeded(toolResultContent, this.tokenCounter, this.sessionId, this.maxOutputTokens);
3090
+ if (truncateResult.truncated) {
3091
+ toolResultContent = truncateResult.content;
3092
+ if (this.debug) {
3093
+ console.log(`[DEBUG] Tool output truncated: ${truncateResult.originalTokens} tokens -> saved to ${truncateResult.tempFilePath || 'N/A'}`);
3094
+ if (truncateResult.error) {
3095
+ console.log(`[DEBUG] Truncation file error: ${truncateResult.error}`);
3096
+ }
3097
+ }
3098
+ }
3099
+ } catch (truncateError) {
3100
+ // If truncation fails entirely, log and continue with original content
3101
+ console.error(`[WARN] Tool output truncation failed: ${truncateError.message}`);
3102
+ }
3103
+
3064
3104
  const toolResultMessage = `<tool_result>\n${toolResultContent}\n</tool_result>`;
3065
-
3105
+
3066
3106
  currentMessages.push({
3067
3107
  role: 'user',
3068
3108
  content: toolResultMessage
@@ -9718,10 +9718,10 @@ var init_vercel = __esm({
9718
9718
  let extractOptions = { cwd: effectiveCwd };
9719
9719
  if (input_content) {
9720
9720
  const { writeFileSync: writeFileSync2, unlinkSync } = await import("fs");
9721
- const { join: join4 } = await import("path");
9722
- const { tmpdir } = await import("os");
9723
- const { randomUUID: randomUUID8 } = await import("crypto");
9724
- tempFilePath = join4(tmpdir(), `probe-extract-${randomUUID8()}.txt`);
9721
+ const { join: join5 } = await import("path");
9722
+ const { tmpdir: tmpdir2 } = await import("os");
9723
+ const { randomUUID: randomUUID9 } = await import("crypto");
9724
+ tempFilePath = join5(tmpdir2(), `probe-extract-${randomUUID9()}.txt`);
9725
9725
  writeFileSync2(tempFilePath, input_content);
9726
9726
  if (debug) {
9727
9727
  console.error(`Created temporary file for input content: ${tempFilePath}`);
@@ -67117,10 +67117,88 @@ var init_contextCompactor = __esm({
67117
67117
  }
67118
67118
  });
67119
67119
 
67120
+ // src/agent/outputTruncator.js
67121
+ import { writeFile, mkdir } from "fs/promises";
67122
+ import { tmpdir } from "os";
67123
+ import { join as join4 } from "path";
67124
+ import { randomUUID as randomUUID4 } from "crypto";
67125
+ function validateTokenLimit(value) {
67126
+ const num = Number(value);
67127
+ if (isNaN(num) || num <= 0) {
67128
+ return DEFAULT_MAX_OUTPUT_TOKENS;
67129
+ }
67130
+ return num;
67131
+ }
67132
+ function getMaxOutputTokens(constructorValue) {
67133
+ if (constructorValue !== void 0 && constructorValue !== null) {
67134
+ const validated = validateTokenLimit(constructorValue);
67135
+ if (validated !== DEFAULT_MAX_OUTPUT_TOKENS || Number(constructorValue) === DEFAULT_MAX_OUTPUT_TOKENS) {
67136
+ return validated;
67137
+ }
67138
+ }
67139
+ if (process.env.PROBE_MAX_OUTPUT_TOKENS) {
67140
+ return validateTokenLimit(process.env.PROBE_MAX_OUTPUT_TOKENS);
67141
+ }
67142
+ return DEFAULT_MAX_OUTPUT_TOKENS;
67143
+ }
67144
+ async function truncateIfNeeded(content, tokenCounter, sessionId, maxTokens) {
67145
+ const limit = validateTokenLimit(maxTokens);
67146
+ const tokenCount = tokenCounter.countTokens(content);
67147
+ if (tokenCount <= limit) {
67148
+ return { truncated: false, content };
67149
+ }
67150
+ const maxChars = limit * CHARS_PER_TOKEN;
67151
+ const truncatedContent = content.substring(0, maxChars);
67152
+ let tempFilePath = null;
67153
+ let fileError = null;
67154
+ try {
67155
+ const tempDir = join4(tmpdir(), "probe-output");
67156
+ await mkdir(tempDir, { recursive: true });
67157
+ tempFilePath = join4(tempDir, `tool-output-${sessionId || "unknown"}-${randomUUID4()}.txt`);
67158
+ await writeFile(tempFilePath, content, "utf8");
67159
+ } catch (err) {
67160
+ fileError = err.message || "Unknown file system error";
67161
+ tempFilePath = null;
67162
+ }
67163
+ let message;
67164
+ if (tempFilePath) {
67165
+ message = `Output exceeded maximum size (${tokenCount} tokens, limit: ${limit}).
67166
+ Full output saved to: ${tempFilePath}
67167
+
67168
+ --- Truncated Output (first ${limit} tokens approx) ---
67169
+ ${truncatedContent}
67170
+ ...
67171
+ --- End of Truncated Output ---`;
67172
+ } else {
67173
+ message = `Output exceeded maximum size (${tokenCount} tokens, limit: ${limit}).
67174
+ Warning: Could not save full output to file (${fileError}).
67175
+
67176
+ --- Truncated Output (first ${limit} tokens approx) ---
67177
+ ${truncatedContent}
67178
+ ...
67179
+ --- End of Truncated Output ---`;
67180
+ }
67181
+ return {
67182
+ truncated: true,
67183
+ content: message,
67184
+ tempFilePath: tempFilePath || void 0,
67185
+ originalTokens: tokenCount,
67186
+ error: fileError || void 0
67187
+ };
67188
+ }
67189
+ var DEFAULT_MAX_OUTPUT_TOKENS, CHARS_PER_TOKEN;
67190
+ var init_outputTruncator = __esm({
67191
+ "src/agent/outputTruncator.js"() {
67192
+ "use strict";
67193
+ DEFAULT_MAX_OUTPUT_TOKENS = 2e4;
67194
+ CHARS_PER_TOKEN = 4;
67195
+ }
67196
+ });
67197
+
67120
67198
  // src/agent/mcp/built-in-server.js
67121
67199
  import { createServer } from "http";
67122
67200
  import { EventEmitter as EventEmitter3 } from "events";
67123
- import { randomUUID as randomUUID4 } from "crypto";
67201
+ import { randomUUID as randomUUID5 } from "crypto";
67124
67202
  import { Server as MCPServer } from "@modelcontextprotocol/sdk/server/index.js";
67125
67203
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
67126
67204
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -67372,7 +67450,7 @@ var init_built_in_server = __esm({
67372
67450
  }
67373
67451
  const eventStore = new InMemoryEventStore();
67374
67452
  transport = new StreamableHTTPServerTransport({
67375
- sessionIdGenerator: () => randomUUID4(),
67453
+ sessionIdGenerator: () => randomUUID5(),
67376
67454
  eventStore,
67377
67455
  // Enable resumability
67378
67456
  onsessioninitialized: (newSessionId) => {
@@ -68593,7 +68671,7 @@ import { createOpenAI as createOpenAI2 } from "@ai-sdk/openai";
68593
68671
  import { createGoogleGenerativeAI as createGoogleGenerativeAI2 } from "@ai-sdk/google";
68594
68672
  import { createAmazonBedrock as createAmazonBedrock2 } from "@ai-sdk/amazon-bedrock";
68595
68673
  import { streamText as streamText2 } from "ai";
68596
- import { randomUUID as randomUUID5 } from "crypto";
68674
+ import { randomUUID as randomUUID6 } from "crypto";
68597
68675
  import { EventEmitter as EventEmitter5 } from "events";
68598
68676
  import { existsSync as existsSync6 } from "fs";
68599
68677
  import { readFile as readFile3, stat, readdir as readdir3 } from "fs/promises";
@@ -68622,6 +68700,7 @@ var init_ProbeAgent = __esm({
68622
68700
  init_FallbackManager();
68623
68701
  init_contextCompactor();
68624
68702
  init_error_types();
68703
+ init_outputTruncator();
68625
68704
  init_tasks();
68626
68705
  dotenv2.config();
68627
68706
  MAX_TOOL_ITERATIONS = (() => {
@@ -68681,9 +68760,10 @@ var init_ProbeAgent = __esm({
68681
68760
  * @param {boolean} [options.fallback.stopOnSuccess=true] - Stop on first success
68682
68761
  * @param {number} [options.fallback.maxTotalAttempts=10] - Maximum total attempts across all providers
68683
68762
  * @param {string} [options.completionPrompt] - Custom prompt to run after attempt_completion for validation/review (runs before mermaid/JSON validation)
68763
+ * @param {number} [options.maxOutputTokens] - Maximum tokens for tool output before truncation (default: 20000, can also be set via PROBE_MAX_OUTPUT_TOKENS env var)
68684
68764
  */
68685
68765
  constructor(options = {}) {
68686
- this.sessionId = options.sessionId || randomUUID5();
68766
+ this.sessionId = options.sessionId || randomUUID6();
68687
68767
  this.customPrompt = options.systemPrompt || options.customPrompt || null;
68688
68768
  this.promptType = options.promptType || "code-explorer";
68689
68769
  this.allowEdit = !!options.allowEdit;
@@ -68742,6 +68822,7 @@ var init_ProbeAgent = __esm({
68742
68822
  this.clientApiKey = null;
68743
68823
  this.clientApiUrl = null;
68744
68824
  this.tokenCounter = new TokenCounter();
68825
+ this.maxOutputTokens = getMaxOutputTokens(options.maxOutputTokens);
68745
68826
  if (this.debug) {
68746
68827
  console.log(`[DEBUG] Generated session ID for agent: ${this.sessionId}`);
68747
68828
  console.log(`[DEBUG] Maximum tool iterations configured: ${MAX_TOOL_ITERATIONS}`);
@@ -70833,7 +70914,21 @@ You are working with a repository located at: ${searchDirectory}
70833
70914
  `);
70834
70915
  }
70835
70916
  const executionResult = await this.mcpBridge.mcpTools[toolName].execute(params);
70836
- const toolResultContent = typeof executionResult === "string" ? executionResult : JSON.stringify(executionResult, null, 2);
70917
+ let toolResultContent = typeof executionResult === "string" ? executionResult : JSON.stringify(executionResult, null, 2);
70918
+ try {
70919
+ const truncateResult = await truncateIfNeeded(toolResultContent, this.tokenCounter, this.sessionId, this.maxOutputTokens);
70920
+ if (truncateResult.truncated) {
70921
+ toolResultContent = truncateResult.content;
70922
+ if (this.debug) {
70923
+ console.log(`[DEBUG] Tool output truncated: ${truncateResult.originalTokens} tokens -> saved to ${truncateResult.tempFilePath || "N/A"}`);
70924
+ if (truncateResult.error) {
70925
+ console.log(`[DEBUG] Truncation file error: ${truncateResult.error}`);
70926
+ }
70927
+ }
70928
+ }
70929
+ } catch (truncateError) {
70930
+ console.error(`[WARN] Tool output truncation failed: ${truncateError.message}`);
70931
+ }
70837
70932
  if (this.debug) {
70838
70933
  const preview = toolResultContent.length > 500 ? toolResultContent.substring(0, 500) + "..." : toolResultContent;
70839
70934
  console.error(`[DEBUG] ========================================`);
@@ -70982,7 +71077,21 @@ ${errorXml}
70982
71077
  throw toolError;
70983
71078
  }
70984
71079
  currentMessages.push({ role: "assistant", content: assistantResponseContent });
70985
- const toolResultContent = typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult, null, 2);
71080
+ let toolResultContent = typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult, null, 2);
71081
+ try {
71082
+ const truncateResult = await truncateIfNeeded(toolResultContent, this.tokenCounter, this.sessionId, this.maxOutputTokens);
71083
+ if (truncateResult.truncated) {
71084
+ toolResultContent = truncateResult.content;
71085
+ if (this.debug) {
71086
+ console.log(`[DEBUG] Tool output truncated: ${truncateResult.originalTokens} tokens -> saved to ${truncateResult.tempFilePath || "N/A"}`);
71087
+ if (truncateResult.error) {
71088
+ console.log(`[DEBUG] Truncation file error: ${truncateResult.error}`);
71089
+ }
71090
+ }
71091
+ }
71092
+ } catch (truncateError) {
71093
+ console.error(`[WARN] Tool output truncation failed: ${truncateError.message}`);
71094
+ }
70986
71095
  const toolResultMessage = `<tool_result>
70987
71096
  ${toolResultContent}
70988
71097
  </tool_result>`;
@@ -71591,7 +71700,7 @@ Convert your previous response content into actual JSON data that follows this s
71591
71700
  */
71592
71701
  clone(options = {}) {
71593
71702
  const {
71594
- sessionId = randomUUID5(),
71703
+ sessionId = randomUUID6(),
71595
71704
  stripInternalMessages = true,
71596
71705
  keepSystemMessage = true,
71597
71706
  deepCopy = true,
@@ -71818,7 +71927,7 @@ import { readFileSync as readFileSync2, existsSync as existsSync7 } from "fs";
71818
71927
  import { resolve as resolve7 } from "path";
71819
71928
 
71820
71929
  // src/agent/acp/server.js
71821
- import { randomUUID as randomUUID6 } from "crypto";
71930
+ import { randomUUID as randomUUID7 } from "crypto";
71822
71931
 
71823
71932
  // src/agent/acp/connection.js
71824
71933
  import { EventEmitter as EventEmitter6 } from "events";
@@ -72310,7 +72419,7 @@ var ACPServer = class {
72310
72419
  * Handle new session request
72311
72420
  */
72312
72421
  async handleNewSession(params) {
72313
- const sessionId = params?.sessionId || randomUUID6();
72422
+ const sessionId = params?.sessionId || randomUUID7();
72314
72423
  const mode = params?.mode || SessionMode.NORMAL;
72315
72424
  const session = new ACPSession(sessionId, mode);
72316
72425
  this.sessions.set(sessionId, session);
@@ -72482,7 +72591,7 @@ var ACPServer = class {
72482
72591
  };
72483
72592
 
72484
72593
  // src/agent/acp/tools.js
72485
- import { randomUUID as randomUUID7 } from "crypto";
72594
+ import { randomUUID as randomUUID8 } from "crypto";
72486
72595
 
72487
72596
  // src/agent/index.js
72488
72597
  dotenv3.config();
@@ -0,0 +1,108 @@
1
+ import { writeFile, mkdir } from 'fs/promises';
2
+ import { tmpdir } from 'os';
3
+ import { join } from 'path';
4
+ import { randomUUID } from 'crypto';
5
+
6
+ const DEFAULT_MAX_OUTPUT_TOKENS = 20000;
7
+ const CHARS_PER_TOKEN = 4; // Conservative approximation
8
+
9
+ /**
10
+ * Validate and normalize a token limit value.
11
+ * Returns the default if the value is invalid (NaN, negative, zero).
12
+ * @param {any} value - The value to validate
13
+ * @returns {number} A valid positive token limit
14
+ */
15
+ function validateTokenLimit(value) {
16
+ const num = Number(value);
17
+ if (isNaN(num) || num <= 0) {
18
+ return DEFAULT_MAX_OUTPUT_TOKENS;
19
+ }
20
+ return num;
21
+ }
22
+
23
+ /**
24
+ * Get the maximum output tokens limit based on priority:
25
+ * 1. Constructor value (if provided and valid)
26
+ * 2. Environment variable PROBE_MAX_OUTPUT_TOKENS (if valid)
27
+ * 3. Default (20000)
28
+ * @param {number|undefined} constructorValue - Value passed to ProbeAgent constructor
29
+ * @returns {number} The maximum output tokens limit (always a valid positive number)
30
+ */
31
+ export function getMaxOutputTokens(constructorValue) {
32
+ if (constructorValue !== undefined && constructorValue !== null) {
33
+ const validated = validateTokenLimit(constructorValue);
34
+ // Only use constructor value if it was valid; otherwise fall through to env/default
35
+ if (validated !== DEFAULT_MAX_OUTPUT_TOKENS || Number(constructorValue) === DEFAULT_MAX_OUTPUT_TOKENS) {
36
+ return validated;
37
+ }
38
+ }
39
+ if (process.env.PROBE_MAX_OUTPUT_TOKENS) {
40
+ return validateTokenLimit(process.env.PROBE_MAX_OUTPUT_TOKENS);
41
+ }
42
+ return DEFAULT_MAX_OUTPUT_TOKENS;
43
+ }
44
+
45
+ /**
46
+ * Truncate tool output if it exceeds the token limit.
47
+ * When truncated, saves full output to a temp file and returns a message with the file path.
48
+ * If file system operations fail, returns truncated content without file reference.
49
+ *
50
+ * @param {string} content - The tool output content to potentially truncate
51
+ * @param {Object} tokenCounter - TokenCounter instance with countTokens method
52
+ * @param {string} sessionId - Session ID for naming temp files
53
+ * @param {number} maxTokens - Maximum tokens allowed (defaults to 20000)
54
+ * @returns {Promise<{truncated: boolean, content: string, tempFilePath?: string, originalTokens?: number, error?: string}>}
55
+ */
56
+ export async function truncateIfNeeded(content, tokenCounter, sessionId, maxTokens) {
57
+ const limit = validateTokenLimit(maxTokens);
58
+ const tokenCount = tokenCounter.countTokens(content);
59
+
60
+ if (tokenCount <= limit) {
61
+ return { truncated: false, content };
62
+ }
63
+
64
+ // Truncate to approximately maxTokens worth of characters
65
+ const maxChars = limit * CHARS_PER_TOKEN;
66
+ const truncatedContent = content.substring(0, maxChars);
67
+
68
+ // Try to write full output to temp file
69
+ let tempFilePath = null;
70
+ let fileError = null;
71
+
72
+ try {
73
+ const tempDir = join(tmpdir(), 'probe-output');
74
+ await mkdir(tempDir, { recursive: true });
75
+ tempFilePath = join(tempDir, `tool-output-${sessionId || 'unknown'}-${randomUUID()}.txt`);
76
+ await writeFile(tempFilePath, content, 'utf8');
77
+ } catch (err) {
78
+ fileError = err.message || 'Unknown file system error';
79
+ tempFilePath = null;
80
+ }
81
+
82
+ let message;
83
+ if (tempFilePath) {
84
+ message = `Output exceeded maximum size (${tokenCount} tokens, limit: ${limit}).
85
+ Full output saved to: ${tempFilePath}
86
+
87
+ --- Truncated Output (first ${limit} tokens approx) ---
88
+ ${truncatedContent}
89
+ ...
90
+ --- End of Truncated Output ---`;
91
+ } else {
92
+ message = `Output exceeded maximum size (${tokenCount} tokens, limit: ${limit}).
93
+ Warning: Could not save full output to file (${fileError}).
94
+
95
+ --- Truncated Output (first ${limit} tokens approx) ---
96
+ ${truncatedContent}
97
+ ...
98
+ --- End of Truncated Output ---`;
99
+ }
100
+
101
+ return {
102
+ truncated: true,
103
+ content: message,
104
+ tempFilePath: tempFilePath || undefined,
105
+ originalTokens: tokenCount,
106
+ error: fileError || undefined
107
+ };
108
+ }