UNPKG

agentfootprint

Version:

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

192 lines • 11 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.negativeClaimsInstruction = exports.negativeClaimsSchema = exports.assertNegativeClaimsTools = exports.resolveNegativeClaims = exports.NEGATIVE_HANDLE_PATTERN = void 0; /** Negative-claim arms: app configuration and the typed model citation schema. * This module never calls a parser or decides whether a search establishes a claim. * Delivery adds its own statement after parsing; that enriched field is not model input. */ const boundedJson_js_1 = require("../../../lib/boundedJson.js"); const limits_js_1 = require("../../../integrity/negative-claim/limits.js"); const citation_js_1 = require("../../../integrity/negative-claim/citation.js"); const phrases_js_1 = require("../../../integrity/negative-claim/phrases.js"); exports.NEGATIVE_HANDLE_PATTERN = '^r[1-9][0-9]{0,3}$'; const KINDS = ['none', 'not-found', 'complete']; const FIELDS = ['kind', 'about', 'restsOn']; const fail = (message) => { throw new Error(`Agent.negativeClaims: ${message}`); }; const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value); /** Validate once, without keeping any mutable caller-owned arrays. */ function resolveNegativeClaims(options) { let source; try { source = (0, boundedJson_js_1.ownData)(options); } catch { return fail('options must be a plain data object.'); } if (['strictProse', 'enforceProse'].some((key) => Object.hasOwn(source, key))) { return fail('strictProse and enforceProse options are not available; prose is observed, never enforced.'); } if (Object.keys(source).some((key) => !['field', 'tools', 'unestablished', 'prose', 'phrases'].includes(key))) { return fail('options accept only field, tools, unestablished, prose and phrases.'); } const phrases = source.phrases === undefined ? undefined : (0, phrases_js_1.resolveNegativeClaimPhrases)(source.phrases); if (Object.hasOwn(source, 'prose')) { if (source.prose !== true || ['field', 'tools', 'unestablished'].some((key) => Object.hasOwn(source, key))) return fail('prose must be true and cannot be combined with field, tools or unestablished.'); return Object.freeze({ mode: 'prose', ...(phrases !== undefined && { phrases }) }); } const field = source.field; if (typeof field !== 'string' || !/^[A-Za-z_][A-Za-z0-9_-]{0,127}$/.test(field) || field === '_findings') { return fail('field must name one top-level answer property (letters, digits, underscore or hyphen), not _findings.'); } const tools = source.tools; if (!Array.isArray(tools) || tools.length === 0 || tools.length > 256 || tools.some((name) => typeof name !== 'string' || name.trim().length === 0 || name.length > 256)) { return fail('tools must be a non-empty list of at most 256 registered tool names.'); } if (new Set(tools).size !== tools.length) return fail('tools must not repeat a name.'); const unestablished = source.unestablished ?? 'withhold'; if (unestablished !== 'withhold' && unestablished !== 'state-searched') { return fail("unestablished must be 'withhold' or 'state-searched'."); } return Object.freeze({ mode: 'typed', field, tools: Object.freeze([...tools]), unestablished, ...(phrases !== undefined && { phrases }), }); } exports.resolveNegativeClaims = resolveNegativeClaims; /** Provider-only and MCP tools cannot become trusted by being named in this list. */ function assertNegativeClaimsTools(config, tools) { for (const name of config.tools) { const tool = tools.find((candidate) => candidate.schema.name === name); if (tool === undefined) return fail(`tool '${name}' is not registered locally; register it with .tool() or a skill.`); if (tool.source !== undefined) return fail(`tool '${name}' is remote; only local tools may supply typed negative evidence.`); // `among` is returned by execute, not declared on Tool. A local tool with // no search therefore stays eligible, but establishes nothing unless an // admitted among result supplies its coverage at the runtime boundary. } } exports.assertNegativeClaimsTools = assertNegativeClaimsTools; function schemaObject(value, at) { return record(value) ? value : fail(`${at} must be declared directly in the output JSON Schema.`); } /** Build the model-facing copy. A parse function is never executed to infer a schema. * Root/field/item references and unions are deliberately not guessed: expose this * one citation field directly while the rest of the app's schema is preserved. */ function negativeClaimsSchema(config, schema) { if (schema === undefined) return fail('requires outputSchema(..., { jsonSchema }) or a parser with toJsonSchema(); the library cannot inspect a parse function.'); let root; try { root = (0, boundedJson_js_1.copyJson)(schema, 1024 * 1024, false); } catch { return fail('the output JSON Schema must be bounded plain JSON data.'); } if (root.type !== 'object') return fail('the output JSON Schema must declare an object.'); const properties = schemaObject(root.properties, 'output properties'); if (!Object.hasOwn(properties, config.field)) return fail(`field '${config.field}' is not declared by the output JSON Schema.`); if (!Array.isArray(root.required) || !root.required.includes(config.field)) { return fail(`field '${config.field}' must be required; use an empty array when there is no typed claim.`); } const field = schemaObject(properties[config.field], `field '${config.field}'`); if (field.type !== 'array') return fail(`field '${config.field}' must be an array of citations.`); const item = schemaObject(field.items, `items of '${config.field}'`); if (item.type !== 'object') return fail(`items of '${config.field}' must be citation objects.`); const fields = schemaObject(item.properties, `properties of '${config.field}' items`); if (Object.keys(fields).some((key) => !FIELDS.includes(key))) { return fail(`'${config.field}' entries declare only kind, about and restsOn; statement, delivery and version are library-owned output fields.`); } const required = item.required; if (!Array.isArray(required) || !required.includes('kind') || !required.includes('restsOn') || required.some((key) => typeof key !== 'string' || !Object.hasOwn(fields, key))) { return fail(`'${config.field}' entries must require kind and restsOn and declare each required property.`); } const kind = schemaObject(fields.kind, 'kind'); if (kind.type !== 'string' || (kind.enum !== undefined && (!Array.isArray(kind.enum) || kind.enum.length === 0 || kind.enum.some((value) => !KINDS.includes(value))))) { return fail("kind must be a string restricted to 'none', 'not-found' or 'complete'."); } const about = fields.about === undefined ? undefined : schemaObject(fields.about, 'about'); const aboutTypes = about === undefined ? [] : Array.isArray(about.type) ? about.type : [about.type]; if (about !== undefined && (aboutTypes.length === 0 || aboutTypes.some((type) => !['string', 'number', 'integer'].includes(type)))) { return fail('about must declare a string or number, never null.'); } const restsOn = schemaObject(fields.restsOn, 'restsOn'); const handle = schemaObject(restsOn.items, 'restsOn.items'); if (restsOn.type !== 'array' || handle.type !== 'string') return fail('restsOn must be an array of string handles.'); if (handle.pattern !== undefined && handle.pattern !== exports.NEGATIVE_HANDLE_PATTERN) { return fail(`restsOn handles must use pattern '${exports.NEGATIVE_HANDLE_PATTERN}'.`); } const cap = (node, maximum, at) => { for (const key of ['minItems', 'maxItems']) { if (node[key] !== undefined && (typeof node[key] !== 'number' || !Number.isSafeInteger(node[key]) || node[key] < 0)) { return fail(`${at}.${key} must be a non-negative integer.`); } } if (typeof node.minItems === 'number' && node.minItems > maximum) return fail(`${at}.minItems exceeds the checker limit ${maximum}.`); node.maxItems = typeof node.maxItems === 'number' ? Math.min(node.maxItems, maximum) : maximum; if (typeof node.minItems === 'number' && node.minItems > node.maxItems) return fail(`${at} has incompatible item bounds.`); }; cap(field, limits_js_1.MAX_NEGATIVE_CLAIMS, config.field); cap(restsOn, citation_js_1.MAX_CITED_READS, 'restsOn'); restsOn.minItems = Math.max(typeof restsOn.minItems === 'number' ? restsOn.minItems : 0, 1); if (restsOn.maxItems < restsOn.minItems) return fail('restsOn must permit at least one handle.'); handle.pattern = exports.NEGATIVE_HANDLE_PATTERN; kind.enum ??= [...KINDS]; if (about !== undefined && aboutTypes.includes('string')) { about.minLength = Math.max(typeof about.minLength === 'number' ? about.minLength : 0, 1); about.maxLength = Math.min(typeof about.maxLength === 'number' ? about.maxLength : 256, 256); } item.additionalProperties = false; return (0, boundedJson_js_1.copyJson)(root, 1024 * 1024, true); } exports.negativeClaimsSchema = negativeClaimsSchema; /** One always-on instruction; handles themselves are served beside admitted results. */ function negativeClaimsInstruction(config, schema) { const fieldSchema = schema.properties[config.field]; return (`The required ${JSON.stringify(config.field)} field contains typed negative citations. ` + `Use only result handles served by these tools: ${JSON.stringify(config.tools)}. ` + 'Each entry requires kind and restsOn handles. Choose none or not-found only for an absence; ' + 'choose complete only to claim the returned list covers the whole declared search. ' + 'The library verifies that claim and only normalizes none versus not-found over declared absence. ' + 'If about is present, it must include every recorded population value for the cited reads. ' + 'Do not write statement text: the library supplies the actual searched subject and scope. ' + 'State negative claims only through this citation field; do not repeat them in summaries or other prose. ' + 'The library writes the delivered statement. Use [] when making no typed negative claim. ' + `Field JSON Schema: ${JSON.stringify(fieldSchema)}`); } exports.negativeClaimsInstruction = negativeClaimsInstruction; //# sourceMappingURL=config.js.map