mirror of
https://github.com/morluto/rea.git
synced 2026-10-30 11:25:39 +00:00
Since #1277 an error result reaches the client without structuredContent, but toErrorToolResult and the transport recovery still budgeted it as structured JSON plus escaped text. An error whose text fit between about half and all of the response budget was replaced by resource_constraint, and without Evidence retention the original diagnostic was lost. Count only the escaped text for errors. The boundary, process-global and real Ghidra large-result probes now use diagnostics that exceed the text-only budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
67 lines
2.3 KiB
TypeScript
67 lines
2.3 KiB
TypeScript
import { constants as bufferConstants } from "node:buffer";
|
|
|
|
import type { JsonValue } from "../domain/jsonValue.js";
|
|
import { bufferedJsonParts, jsonParts } from "../domain/jsonSerialization.js";
|
|
|
|
export const MCP_RESULT_STRING_LIMIT = bufferConstants.MAX_STRING_LENGTH - 1024;
|
|
|
|
const RESULT_ENVELOPE_BYTES =
|
|
Buffer.byteLength(
|
|
JSON.stringify({
|
|
content: [{ type: "text", text: "" }],
|
|
structuredContent: null,
|
|
}),
|
|
) - 4;
|
|
|
|
const TEXT_RESULT_ENVELOPE_BYTES = Buffer.byteLength(
|
|
JSON.stringify({ content: [{ type: "text", text: "" }] }),
|
|
);
|
|
|
|
/**
|
|
* MCP representations a result carries on the wire. Successful results repeat
|
|
* the JSON as structured content and escaped text; error results carry text only.
|
|
*/
|
|
export type ToolResultRepresentations = "structured-and-text" | "text";
|
|
|
|
/** Result of encoding the complete repeated MCP representations within a wire budget. */
|
|
export type ToolResultEncoding =
|
|
| { readonly ok: true; readonly text: string; readonly bytes: number }
|
|
| {
|
|
readonly ok: false;
|
|
readonly bytesAtLeast: number;
|
|
readonly codeUnitsAtLeast: number;
|
|
readonly constraint: "receive-buffer" | "string-length";
|
|
};
|
|
|
|
/** Account for the delivered JSON representations before allocating the complete text. */
|
|
export const encodeToolResult = (
|
|
value: JsonValue,
|
|
budgetBytes: number,
|
|
representations: ToolResultRepresentations = "structured-and-text",
|
|
): ToolResultEncoding => {
|
|
const structured = representations === "structured-and-text";
|
|
const parts: string[] = [];
|
|
let bytes = structured ? RESULT_ENVELOPE_BYTES : TEXT_RESULT_ENVELOPE_BYTES;
|
|
let codeUnits = bytes;
|
|
for (const part of bufferedJsonParts(jsonParts(value))) {
|
|
const escapedPart = JSON.stringify(part);
|
|
bytes +=
|
|
(structured ? Buffer.byteLength(part) : 0) +
|
|
Buffer.byteLength(escapedPart) -
|
|
2;
|
|
codeUnits += (structured ? part.length : 0) + escapedPart.length - 2;
|
|
if (bytes > budgetBytes || codeUnits > MCP_RESULT_STRING_LIMIT)
|
|
return {
|
|
ok: false,
|
|
bytesAtLeast: bytes,
|
|
codeUnitsAtLeast: codeUnits,
|
|
constraint:
|
|
codeUnits > MCP_RESULT_STRING_LIMIT
|
|
? "string-length"
|
|
: "receive-buffer",
|
|
};
|
|
parts.push(part);
|
|
}
|
|
return { ok: true, text: parts.join(""), bytes };
|
|
};
|