UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

223 lines • 9.88 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.readToolResult = exports.isToolError = exports.resultModeOf = void 0; const plainData_js_1 = require("./plainData.js"); /** Validate before opening a transport; a misspelled opt-in must not disappear. */ function resultModeOf(value, name) { if (value === undefined) return 'text'; if (value === 'text' || value === 'structured' || value === 'structured-or-json') return value; throw new Error(`mcpClient[${name}]: resultMode must be 'text', 'structured' or 'structured-or-json'.`); } exports.resultModeOf = resultModeOf; /** * Whether a `tools/call` answer is the tool's own error (`isError: true`, an * own data property). Read once, never throwing: anything it cannot read is * not a tool error — the decoder refuses it on its own. */ function isToolError(result) { if (result === null || typeof result !== 'object') return false; try { return ownValue(result, 'isError') === true; } catch { return false; } } exports.isToolError = isToolError; /** Internal shared decoder for the real connection and curated mock. */ function readToolResult(result, toolName, serverName, mode = 'text') { if (mode === 'text') return readTextResult(result, toolName, serverName); const prefix = `MCP tool '${toolName}' (server '${serverName}')`; const invalid = () => new Error(`${prefix} did not provide valid structuredContent. ` + (mode === 'structured-or-json' ? 'Expected a JSON object in structuredContent or, when absent, exactly one JSON-object text block. ' : 'Expected a JSON object in structuredContent. ') + 'Malformed, cyclic or over-limit data is refused; result data is not included in this error.'); if (typeof result !== 'object' || result === null) throw invalid(); // Error status belongs to the tool, even if it also returned a usable object // or a legacy arm. Never launder a failed call into successful data. let isError; try { isError = ownValue(result, 'isError'); } catch { throw invalid(); } if (isError === true) { const diagnostic = errorDiagnostic(result); throw new Error(diagnostic === undefined ? `${prefix} returned an error; its structured result was not read.` : `${prefix} returned an error: ${diagnostic}`); } try { const structured = Object.getOwnPropertyDescriptor(result, 'structuredContent'); if (structured !== undefined) { // A copy of this realm's own objects, never the transport's value: see plainData.ts. const copied = 'value' in structured ? (0, plainData_js_1.copyJsonObject)(structured.value) : undefined; if (copied === undefined) throw invalid(); return copied; } if (mode !== 'structured-or-json') throw invalid(); const content = ownValue(result, 'content'); if (!Array.isArray(content) || content.length !== 1) throw invalid(); const block = ownValue(content, '0'); if (typeof block !== 'object' || block === null || ownValue(block, 'type') !== 'text') throw invalid(); const text = ownValue(block, 'text'); if (typeof text !== 'string' || text.length > 16_000_000) throw invalid(); const parsed = JSON.parse(text); const copied = (0, plainData_js_1.copyJsonObject)(parsed); if (copied === undefined) throw invalid(); return copied; } catch { // Parser and getter exceptions may echo source bytes. Keep the refusal // authored here rather than forwarding arbitrary transport payloads. throw invalid(); } } exports.readToolResult = readToolResult; /** Tool-authored error text is corrective evidence, not a successful result. * Read only own data properties; never inspect structuredContent, invoke a * getter/toJSON or stringify a non-string diagnostic. Over-limit/malformed * diagnostics use the generic refusal rather than partial advice. */ function errorDiagnostic(result) { try { const content = ownValue(result, 'content'); if (!Array.isArray(content)) return undefined; const length = ownValue(content, 'length'); if (typeof length !== 'number' || length < 1 || length > 32) return undefined; const texts = []; let chars = 0; for (let index = 0; index < length; index++) { const block = ownValue(content, String(index)); if (typeof block !== 'object' || block === null || ownValue(block, 'type') !== 'text') return undefined; const text = ownValue(block, 'text'); if (typeof text !== 'string') return undefined; chars += text.length + (index === 0 ? 0 : 1); if (chars > 8_192) return undefined; texts.push(text); } const diagnostic = texts.join('\n'); return diagnostic.trim() ? diagnostic : undefined; } catch { return undefined; } } function ownValue(value, key) { const descriptor = Object.getOwnPropertyDescriptor(value, key); if (descriptor !== undefined && !('value' in descriptor)) throw new Error('Accessor is not JSON data'); return descriptor?.value; } // ─── Reading a tools/call answer (both arms of the protocol) ─────── /** * Turn a `tools/call` answer into the text the agent hands the model. * * Three cases, because the protocol has three and pretending otherwise is what * broke on a server that answered the old way: * * 1. **content blocks** — today's shape. Text blocks are concatenated; * non-text blocks (images, resources) are summarized with their type. * `isError: true` becomes a thrown tool error, exactly as before. * 2. **`toolResult`** — the 2024-10-07 shape, which carries no `content` and * no `isError`. The value BECOMES the tool text: a string verbatim, * anything else JSON-stringified. That conversion is stated here and in * the docs rather than left to be inferred from a mangled answer. * 3. **neither** — a corrective error naming the SHAPE that arrived (its * type, or its keys) and never the payload: an unrecognised answer is * still somebody's data, and a tool error is read by the model. * * **Order matters, and here is why the legacy check runs first.** The SDK's * default result schema declares `content` with a default of `[]`, so a legacy * `{ toolResult }` payload arrives here wearing an empty `content` it never * sent. Reading that array first would answer a real result with an empty * string — the same silence in a friendlier coat. So a `toolResult` beside an * EMPTY `content` is read as the legacy answer it is. A NON-empty `content` * always wins: a server that sent blocks meant the blocks. */ function readTextResult(result, toolName, serverName) { if (isLegacyResult(result) && !hasNonEmptyContent(result)) { return stringifyLegacyResult(result.toolResult); } if (hasContentBlocks(result)) { const text = result.content .map((c) => (c.type === 'text' && c.text ? c.text : `[${c.type}]`)) .join('\n'); if (result.isError) { throw new Error(`MCP tool '${toolName}' (server '${serverName}') returned an error: ${text}`); } return text; } throw new Error(`MCP tool '${toolName}' (server '${serverName}') answered with a shape this client does ` + `not understand: ${describeShape(result)}. A tools/call result carries either 'content' ` + `blocks or a legacy 'toolResult'; this had neither.`); } /** * The current arm. Checked on the wire value, not on the declared type — and * defensively, because a `null` or a scalar is exactly the kind of answer that * has to reach the corrective error rather than crash on the way to it. */ function hasContentBlocks(result) { return Array.isArray(contentOf(result)); } /** Blocks the server actually sent, as opposed to the schema's default `[]`. */ function hasNonEmptyContent(result) { const content = contentOf(result); return Array.isArray(content) && content.length > 0; } function contentOf(result) { if (typeof result !== 'object' || result === null) return undefined; return result.content; } /** The 2024-10-07 arm — the key's PRESENCE is the signal; its value may be anything. */ function isLegacyResult(result) { return typeof result === 'object' && result !== null && 'toolResult' in result; } /** A legacy `toolResult` as tool text: a string verbatim, anything else as JSON. */ function stringifyLegacyResult(value) { if (typeof value === 'string') return value; if (value === undefined) return ''; try { return JSON.stringify(value) ?? String(value); } catch { // Circular or otherwise unserializable — say what we can rather than // throwing from a path whose whole job is to report an answer. return String(value); } } /** Name a value's SHAPE for an error message. Never its contents. */ function describeShape(value) { if (value === null) return 'null'; if (Array.isArray(value)) return `an array of ${value.length} item(s)`; if (typeof value !== 'object') return `a ${typeof value}`; const keys = Object.keys(value); if (keys.length === 0) return 'an object with no keys'; const shown = keys.slice(0, 8); return `an object with keys [${shown.join(', ')}${keys.length > shown.length ? ', …' : ''}]`; } //# sourceMappingURL=toolResult.js.map