@probelabs/probe 0.6.0-rc204 → 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.
- package/bin/binaries/probe-v0.6.0-rc206-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc206-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc206-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc206-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc206-x86_64-unknown-linux-musl.tar.gz +0 -0
- package/build/agent/ProbeAgent.js +53 -10
- package/build/agent/index.js +181 -18
- package/build/agent/outputTruncator.js +108 -0
- package/cjs/agent/ProbeAgent.cjs +6592 -7508
- package/cjs/index.cjs +6617 -7533
- package/package.json +2 -2
- package/src/agent/ProbeAgent.js +53 -10
- package/src/agent/outputTruncator.js +108 -0
- package/bin/binaries/probe-v0.6.0-rc204-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc204-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc204-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc204-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc204-x86_64-unknown-linux-musl.tar.gz +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -3146,10 +3186,13 @@ Remember: Use proper XML format with BOTH opening and closing tags:
|
|
|
3146
3186
|
|
|
3147
3187
|
Available tools: ${validTools.join(', ')}
|
|
3148
3188
|
|
|
3149
|
-
|
|
3150
|
-
<
|
|
3189
|
+
To complete with a direct answer:
|
|
3190
|
+
<attempt_completion>Your final answer here</attempt_completion>
|
|
3191
|
+
|
|
3192
|
+
Or if your previous response already contains a complete, direct answer (not a thinking block or JSON):
|
|
3193
|
+
<attempt_complete></attempt_complete>
|
|
3151
3194
|
|
|
3152
|
-
|
|
3195
|
+
Note: <attempt_complete></attempt_complete> reuses your PREVIOUS assistant message as the final answer. Only use this if that message was already a valid, complete response to the user's question.`;
|
|
3153
3196
|
}
|
|
3154
3197
|
|
|
3155
3198
|
currentMessages.push({
|
|
@@ -4062,9 +4105,9 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
4062
4105
|
return true;
|
|
4063
4106
|
}
|
|
4064
4107
|
|
|
4065
|
-
// Empty attempt_complete reminders
|
|
4066
|
-
if (content.includes('
|
|
4067
|
-
content.includes('
|
|
4108
|
+
// Empty attempt_complete reminders (legacy and new format)
|
|
4109
|
+
if (content.includes('<attempt_complete></attempt_complete>') &&
|
|
4110
|
+
content.includes('reuses your PREVIOUS assistant message')) {
|
|
4068
4111
|
return true;
|
|
4069
4112
|
}
|
|
4070
4113
|
|
package/build/agent/index.js
CHANGED
|
@@ -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:
|
|
9722
|
-
const { tmpdir } = await import("os");
|
|
9723
|
-
const { randomUUID:
|
|
9724
|
-
tempFilePath =
|
|
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}`);
|
|
@@ -31713,6 +31713,8 @@ var init_semantics = __esm({
|
|
|
31713
31713
|
constructor(ctx, knownIds, knownEdgeIds) {
|
|
31714
31714
|
super();
|
|
31715
31715
|
this.edgeCount = 0;
|
|
31716
|
+
this.subgraphStack = [];
|
|
31717
|
+
this.reportedSubgraphIdCollision = /* @__PURE__ */ new Set();
|
|
31716
31718
|
this.validateVisitor();
|
|
31717
31719
|
this.ctx = ctx;
|
|
31718
31720
|
this.knownIds = knownIds;
|
|
@@ -31905,8 +31907,13 @@ var init_semantics = __esm({
|
|
|
31905
31907
|
}
|
|
31906
31908
|
}
|
|
31907
31909
|
subgraph(ctx) {
|
|
31910
|
+
const idTok = ctx.subgraphIdOrFirstWord && ctx.subgraphIdOrFirstWord[0];
|
|
31911
|
+
if (idTok)
|
|
31912
|
+
this.subgraphStack.push({ id: String(idTok.image) });
|
|
31908
31913
|
if (ctx.subgraphStatement)
|
|
31909
31914
|
ctx.subgraphStatement.forEach((s) => this.visit(s));
|
|
31915
|
+
if (idTok)
|
|
31916
|
+
this.subgraphStack.pop();
|
|
31910
31917
|
}
|
|
31911
31918
|
subgraphStatement(ctx) {
|
|
31912
31919
|
for (const k of Object.keys(ctx)) {
|
|
@@ -31969,6 +31976,27 @@ var init_semantics = __esm({
|
|
|
31969
31976
|
}
|
|
31970
31977
|
if (ctx.nodeShape)
|
|
31971
31978
|
ctx.nodeShape.forEach((n) => this.visit(n));
|
|
31979
|
+
const idTok = ctx.nodeId && ctx.nodeId[0];
|
|
31980
|
+
const idNumTok = ctx.nodeIdNum && ctx.nodeIdNum[0];
|
|
31981
|
+
const idToken = idTok || idNumTok;
|
|
31982
|
+
if (idToken && this.subgraphStack.length > 0) {
|
|
31983
|
+
const id = String(idToken.image);
|
|
31984
|
+
const hasCollision = this.subgraphStack.some((sg) => sg.id === id);
|
|
31985
|
+
if (hasCollision) {
|
|
31986
|
+
const key = `${id}:${idToken.startLine ?? 1}:${idToken.startColumn ?? 1}`;
|
|
31987
|
+
if (!this.reportedSubgraphIdCollision.has(key)) {
|
|
31988
|
+
this.reportedSubgraphIdCollision.add(key);
|
|
31989
|
+
this.ctx.errors.push({
|
|
31990
|
+
line: idToken.startLine ?? 1,
|
|
31991
|
+
column: idToken.startColumn ?? 1,
|
|
31992
|
+
severity: "error",
|
|
31993
|
+
code: "FL-SUBGRAPH-ID-COLLISION",
|
|
31994
|
+
message: `Node id '${id}' conflicts with an enclosing subgraph id and creates a cycle in Mermaid.`,
|
|
31995
|
+
hint: "Rename the subgraph id or the node id, or use a quoted subgraph title with no explicit id."
|
|
31996
|
+
});
|
|
31997
|
+
}
|
|
31998
|
+
}
|
|
31999
|
+
}
|
|
31972
32000
|
if (hasAttr) {
|
|
31973
32001
|
const attr = ctx.attrObject?.[0];
|
|
31974
32002
|
const pairs = attr?.children?.attrPair || [];
|
|
@@ -36009,6 +36037,7 @@ function computeFixes(text, errors, level = "safe") {
|
|
|
36009
36037
|
const patchedLines = /* @__PURE__ */ new Set();
|
|
36010
36038
|
const seen = /* @__PURE__ */ new Set();
|
|
36011
36039
|
const piQuoteClosedLines = /* @__PURE__ */ new Set();
|
|
36040
|
+
const subgraphCollisionRename = /* @__PURE__ */ new Map();
|
|
36012
36041
|
function sanitizeAllQuotedSegmentsInShapes(lineText, lineNo) {
|
|
36013
36042
|
const shapes = [
|
|
36014
36043
|
{ open: "[[", close: "]]" },
|
|
@@ -36728,6 +36757,28 @@ function computeFixes(text, errors, level = "safe") {
|
|
|
36728
36757
|
}
|
|
36729
36758
|
continue;
|
|
36730
36759
|
}
|
|
36760
|
+
if (is("FL-SUBGRAPH-ID-COLLISION", e)) {
|
|
36761
|
+
if (level === "all") {
|
|
36762
|
+
const lineText = lineTextAt(text, e.line);
|
|
36763
|
+
if (lineText.trimStart().startsWith("subgraph"))
|
|
36764
|
+
continue;
|
|
36765
|
+
const caret0 = Math.max(0, e.column - 1);
|
|
36766
|
+
const slice = lineText.slice(caret0);
|
|
36767
|
+
const m = slice.match(/^([A-Za-z_][A-Za-z0-9_]*(?:-[A-Za-z0-9_]+)*)/);
|
|
36768
|
+
if (m) {
|
|
36769
|
+
const id = m[1];
|
|
36770
|
+
let newId = subgraphCollisionRename.get(id);
|
|
36771
|
+
if (!newId) {
|
|
36772
|
+
newId = id.endsWith("_node") ? `${id}2` : `${id}_node`;
|
|
36773
|
+
subgraphCollisionRename.set(id, newId);
|
|
36774
|
+
}
|
|
36775
|
+
if (newId !== id) {
|
|
36776
|
+
edits.push(replaceRange(text, { line: e.line, column: caret0 + 1 }, id.length, newId));
|
|
36777
|
+
}
|
|
36778
|
+
}
|
|
36779
|
+
}
|
|
36780
|
+
continue;
|
|
36781
|
+
}
|
|
36731
36782
|
if (is("FL-LABEL-QUOTE-IN-UNQUOTED", e)) {
|
|
36732
36783
|
if (level === "safe" || level === "all") {
|
|
36733
36784
|
const lineText = lineTextAt(text, e.line);
|
|
@@ -67066,10 +67117,88 @@ var init_contextCompactor = __esm({
|
|
|
67066
67117
|
}
|
|
67067
67118
|
});
|
|
67068
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
|
+
|
|
67069
67198
|
// src/agent/mcp/built-in-server.js
|
|
67070
67199
|
import { createServer } from "http";
|
|
67071
67200
|
import { EventEmitter as EventEmitter3 } from "events";
|
|
67072
|
-
import { randomUUID as
|
|
67201
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
67073
67202
|
import { Server as MCPServer } from "@modelcontextprotocol/sdk/server/index.js";
|
|
67074
67203
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
67075
67204
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
@@ -67321,7 +67450,7 @@ var init_built_in_server = __esm({
|
|
|
67321
67450
|
}
|
|
67322
67451
|
const eventStore = new InMemoryEventStore();
|
|
67323
67452
|
transport = new StreamableHTTPServerTransport({
|
|
67324
|
-
sessionIdGenerator: () =>
|
|
67453
|
+
sessionIdGenerator: () => randomUUID5(),
|
|
67325
67454
|
eventStore,
|
|
67326
67455
|
// Enable resumability
|
|
67327
67456
|
onsessioninitialized: (newSessionId) => {
|
|
@@ -68542,7 +68671,7 @@ import { createOpenAI as createOpenAI2 } from "@ai-sdk/openai";
|
|
|
68542
68671
|
import { createGoogleGenerativeAI as createGoogleGenerativeAI2 } from "@ai-sdk/google";
|
|
68543
68672
|
import { createAmazonBedrock as createAmazonBedrock2 } from "@ai-sdk/amazon-bedrock";
|
|
68544
68673
|
import { streamText as streamText2 } from "ai";
|
|
68545
|
-
import { randomUUID as
|
|
68674
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
68546
68675
|
import { EventEmitter as EventEmitter5 } from "events";
|
|
68547
68676
|
import { existsSync as existsSync6 } from "fs";
|
|
68548
68677
|
import { readFile as readFile3, stat, readdir as readdir3 } from "fs/promises";
|
|
@@ -68571,6 +68700,7 @@ var init_ProbeAgent = __esm({
|
|
|
68571
68700
|
init_FallbackManager();
|
|
68572
68701
|
init_contextCompactor();
|
|
68573
68702
|
init_error_types();
|
|
68703
|
+
init_outputTruncator();
|
|
68574
68704
|
init_tasks();
|
|
68575
68705
|
dotenv2.config();
|
|
68576
68706
|
MAX_TOOL_ITERATIONS = (() => {
|
|
@@ -68630,9 +68760,10 @@ var init_ProbeAgent = __esm({
|
|
|
68630
68760
|
* @param {boolean} [options.fallback.stopOnSuccess=true] - Stop on first success
|
|
68631
68761
|
* @param {number} [options.fallback.maxTotalAttempts=10] - Maximum total attempts across all providers
|
|
68632
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)
|
|
68633
68764
|
*/
|
|
68634
68765
|
constructor(options = {}) {
|
|
68635
|
-
this.sessionId = options.sessionId ||
|
|
68766
|
+
this.sessionId = options.sessionId || randomUUID6();
|
|
68636
68767
|
this.customPrompt = options.systemPrompt || options.customPrompt || null;
|
|
68637
68768
|
this.promptType = options.promptType || "code-explorer";
|
|
68638
68769
|
this.allowEdit = !!options.allowEdit;
|
|
@@ -68691,6 +68822,7 @@ var init_ProbeAgent = __esm({
|
|
|
68691
68822
|
this.clientApiKey = null;
|
|
68692
68823
|
this.clientApiUrl = null;
|
|
68693
68824
|
this.tokenCounter = new TokenCounter();
|
|
68825
|
+
this.maxOutputTokens = getMaxOutputTokens(options.maxOutputTokens);
|
|
68694
68826
|
if (this.debug) {
|
|
68695
68827
|
console.log(`[DEBUG] Generated session ID for agent: ${this.sessionId}`);
|
|
68696
68828
|
console.log(`[DEBUG] Maximum tool iterations configured: ${MAX_TOOL_ITERATIONS}`);
|
|
@@ -70782,7 +70914,21 @@ You are working with a repository located at: ${searchDirectory}
|
|
|
70782
70914
|
`);
|
|
70783
70915
|
}
|
|
70784
70916
|
const executionResult = await this.mcpBridge.mcpTools[toolName].execute(params);
|
|
70785
|
-
|
|
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
|
+
}
|
|
70786
70932
|
if (this.debug) {
|
|
70787
70933
|
const preview = toolResultContent.length > 500 ? toolResultContent.substring(0, 500) + "..." : toolResultContent;
|
|
70788
70934
|
console.error(`[DEBUG] ========================================`);
|
|
@@ -70931,7 +71077,21 @@ ${errorXml}
|
|
|
70931
71077
|
throw toolError;
|
|
70932
71078
|
}
|
|
70933
71079
|
currentMessages.push({ role: "assistant", content: assistantResponseContent });
|
|
70934
|
-
|
|
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
|
+
}
|
|
70935
71095
|
const toolResultMessage = `<tool_result>
|
|
70936
71096
|
${toolResultContent}
|
|
70937
71097
|
</tool_result>`;
|
|
@@ -71005,10 +71165,13 @@ Remember: Use proper XML format with BOTH opening and closing tags:
|
|
|
71005
71165
|
|
|
71006
71166
|
Available tools: ${validTools.join(", ")}
|
|
71007
71167
|
|
|
71008
|
-
|
|
71009
|
-
<
|
|
71168
|
+
To complete with a direct answer:
|
|
71169
|
+
<attempt_completion>Your final answer here</attempt_completion>
|
|
71010
71170
|
|
|
71011
|
-
|
|
71171
|
+
Or if your previous response already contains a complete, direct answer (not a thinking block or JSON):
|
|
71172
|
+
<attempt_complete></attempt_complete>
|
|
71173
|
+
|
|
71174
|
+
Note: <attempt_complete></attempt_complete> reuses your PREVIOUS assistant message as the final answer. Only use this if that message was already a valid, complete response to the user's question.`;
|
|
71012
71175
|
}
|
|
71013
71176
|
currentMessages.push({
|
|
71014
71177
|
role: "user",
|
|
@@ -71537,7 +71700,7 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
71537
71700
|
*/
|
|
71538
71701
|
clone(options = {}) {
|
|
71539
71702
|
const {
|
|
71540
|
-
sessionId =
|
|
71703
|
+
sessionId = randomUUID6(),
|
|
71541
71704
|
stripInternalMessages = true,
|
|
71542
71705
|
keepSystemMessage = true,
|
|
71543
71706
|
deepCopy = true,
|
|
@@ -71711,7 +71874,7 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
71711
71874
|
if (content.includes("Your response does not match the expected JSON schema") || content.includes("Please provide a valid JSON response") || content.includes("Schema validation error:")) {
|
|
71712
71875
|
return true;
|
|
71713
71876
|
}
|
|
71714
|
-
if (content.includes("
|
|
71877
|
+
if (content.includes("<attempt_complete></attempt_complete>") && content.includes("reuses your PREVIOUS assistant message")) {
|
|
71715
71878
|
return true;
|
|
71716
71879
|
}
|
|
71717
71880
|
return false;
|
|
@@ -71764,7 +71927,7 @@ import { readFileSync as readFileSync2, existsSync as existsSync7 } from "fs";
|
|
|
71764
71927
|
import { resolve as resolve7 } from "path";
|
|
71765
71928
|
|
|
71766
71929
|
// src/agent/acp/server.js
|
|
71767
|
-
import { randomUUID as
|
|
71930
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
71768
71931
|
|
|
71769
71932
|
// src/agent/acp/connection.js
|
|
71770
71933
|
import { EventEmitter as EventEmitter6 } from "events";
|
|
@@ -72256,7 +72419,7 @@ var ACPServer = class {
|
|
|
72256
72419
|
* Handle new session request
|
|
72257
72420
|
*/
|
|
72258
72421
|
async handleNewSession(params) {
|
|
72259
|
-
const sessionId = params?.sessionId ||
|
|
72422
|
+
const sessionId = params?.sessionId || randomUUID7();
|
|
72260
72423
|
const mode = params?.mode || SessionMode.NORMAL;
|
|
72261
72424
|
const session = new ACPSession(sessionId, mode);
|
|
72262
72425
|
this.sessions.set(sessionId, session);
|
|
@@ -72428,7 +72591,7 @@ var ACPServer = class {
|
|
|
72428
72591
|
};
|
|
72429
72592
|
|
|
72430
72593
|
// src/agent/acp/tools.js
|
|
72431
|
-
import { randomUUID as
|
|
72594
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
72432
72595
|
|
|
72433
72596
|
// src/agent/index.js
|
|
72434
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
|
+
}
|