UNPKG

agentfootprint

Version:

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

522 lines • 23 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.negativeRecordOf = exports.serveNegativeHandle = exports.keepNegativeResultOut = exports.captureNegativeRefusal = exports.settleNegativeCall = exports.pausedNegativeResultId = exports.pauseNegativeCall = exports.captureNegativeCoverage = exports.captureNegativeResult = exports.startNegativeCall = exports.createNegativeRuntimeState = void 0; /** * Runtime evidence for typed negatives. Only dispatch and native tool returns write here; * assistant text, summaries and after-tool rewrites never become observations. * Role: core/agent, opt-in. The checker and coverage rules remain their single owners. */ const footprintjs_1 = require("footprintjs"); const capture_js_1 = require("../../../integrity/claim-binding/capture.js"); const normalize_js_1 = require("../../../lib/normalize.js"); const jsonFaithful_js_1 = require("../../../lib/jsonFaithful.js"); const stableJson_js_1 = require("../../../lib/stableJson.js"); const runRedaction_js_1 = require("../../../redaction/runRedaction.js"); const envelope_js_1 = require("../../../semantics/envelope.js"); const rows_js_1 = require("../../../time/rows.js"); const overclaim_js_1 = require("../../../semantics/overclaim.js"); const toolEffects_js_1 = require("../../../semantics/toolEffects.js"); const among_js_1 = require("../../../semantics/coverage/among.js"); const recognize_js_1 = require("../../../semantics/coverage/recognize.js"); const facets_js_1 = require("../../../semantics/coverage/search/facets.js"); const inEffect_js_1 = require("../../../semantics/coverage/search/inEffect.js"); const hostFilled_js_1 = require("../../../tools/hostFilled.js"); const searchOf_js_1 = require("../../../tools/searchOf.js"); const tools_js_1 = require("../../../tools/tools.js"); const emptiness_js_1 = require("../emptiness.js"); const verificationState_js_1 = require("../findings/verificationState.js"); const MAX_CALLS = 1_000; const MAX_BYTES = 2_000_000; const record = (v) => v !== null && typeof v === 'object' && !Array.isArray(v); const stateOf = (scope) => scope.$getValue('negativeClaimState'); const sameIdentity = (a, b) => { try { return (0, stableJson_js_1.stableJson)(a ?? null) === (0, stableJson_js_1.stableJson)(b ?? null); } catch { return false; } }; function createNegativeRuntimeState(turn, identity, prior, anonymousOwner = false) { // A scope-backed object can be a Proxy. Copy this closed tuple through its // fields instead of passing a live scope proxy into structuredClone. This // is ownership bookkeeping; only actual host fills can prove trusted context. const ownerIdentity = identity === undefined ? undefined : { conversationId: identity.conversationId, ...(identity.tenant !== undefined && { tenant: identity.tenant }), ...(identity.principal !== undefined && { principal: identity.principal }), }; const inherited = prior !== undefined && sameIdentity(prior.ownerIdentity, ownerIdentity); // Only an explicitly continued anonymous conversation may carry these stale // addresses between its per-run namespaces. No earlier payload or scope is // transferred, and no named owner can enter this path. const anonymousPast = !inherited && anonymousOwner && prior?.anonymousOwner === true && ownerIdentity?.tenant === undefined && ownerIdentity?.principal === undefined && prior.ownerIdentity?.tenant === undefined && prior.ownerIdentity?.principal === undefined && turn > prior.turn && prior.calls.every((call) => !call.trustedContext); const kept = inherited || anonymousPast; return { version: 1, turn, complete: kept ? prior.complete : true, calls: inherited ? prior.calls : anonymousPast ? prior.calls.map(staleMetadataOf) : [], tools: inherited ? prior.tools : {}, pending: kept ? prior.pending : [], overclaims: inherited ? prior.overclaims ?? [] : [], ...(inherited && prior.pausedResultId !== undefined && { pausedResultId: prior.pausedResultId }), ...(ownerIdentity !== undefined && { ownerIdentity }), ...(anonymousOwner && { anonymousOwner: true }), }; } exports.createNegativeRuntimeState = createNegativeRuntimeState; /** A citation address and date, never an observation that can establish a claim. */ function staleMetadataOf(call) { return { resultId: call.resultId, toolName: call.toolName, turn: call.turn, batch: call.batch, dispatched: call.dispatched, outcome: call.outcome, effect: call.effect, arguments: {}, trustedContext: false, keptOut: true, ...(call.readAt !== undefined && { readAt: call.readAt }), ...(call.row !== undefined && { row: { kind: call.row.kind, toolName: call.toolName, iteration: call.batch, composed: call.row.composed, resultId: call.resultId, handle: call.row.handle, checked: [], notChecked: [], cannotCover: [], complete: false, gaps: [], }, }), }; } function publish(scope, next) { try { if (JSON.stringify(next).length > MAX_BYTES) { const prior = stateOf(scope); if (prior !== undefined) scope.negativeClaimState = { ...prior, complete: false }; return; } scope.negativeClaimState = next; } catch { const prior = stateOf(scope); if (prior !== undefined) scope.negativeClaimState = { ...prior, complete: false }; } } function replace(scope, id, update) { const state = stateOf(scope); if (state === undefined) return; publish(scope, { ...state, calls: state.calls.map((call) => (call.resultId === id ? update(call) : call)), }); } /** Publish the attempt before the tool's code can await, throw or pause. */ function startNegativeCall(scope, input) { const { tool, context } = input; const runId = input.runId || 'unidentified-run'; let dispatch = input.dispatch; if (dispatch === undefined) { const reserved = (0, verificationState_js_1.reserveResultIdentity)(scope.resultIdentities ?? (0, verificationState_js_1.createVerificationState)(runId), runId); scope.resultIdentities = reserved.state; dispatch = { resultId: reserved.id, handle: `r${reserved.identity.ordinal}`, dispatchedAt: context.time?.dispatchedAt ?? new Date().toISOString(), }; } const identity = scope.$getValue('runIdentity'); const state = stateOf(scope) ?? createNegativeRuntimeState(scope.turnNumber, identity); let args = {}; let keptOut = false; try { if (!(0, jsonFaithful_js_1.isJsonFaithful)(input.args)) keptOut = true; args = structuredClone(input.args); } catch { keptOut = true; } const remote = tool.source !== undefined || input.party?.channel === 'provider'; const search = (0, searchOf_js_1.searchOf)(tool).search; const fills = (0, hostFilled_js_1.hostFillsOf)(tool); const bindings = Object.entries(fills?.identity ?? {}); const actualIdentity = context.identity; const expectedIdentity = identity; const trustedContext = !remote && bindings.length > 0 && bindings.every(([argument, key]) => !Object.hasOwn(args, argument) && typeof actualIdentity?.[key] === 'string' && actualIdentity[key] !== '' && actualIdentity[key] === expectedIdentity?.[key]); // A privacy rule must never be bypassed by moving evidence into a new carrier. keptOut ||= [ 'negativeClaimState', 'coverageDeclared', 'toolResults', 'history', 'args', 'result', ...Object.keys(args), ].some((key) => (0, runRedaction_js_1.runKeepsOut)(scope, key)); let composedScope; if (search !== undefined) { composedScope = (0, facets_js_1.composeChecked)({ search, readings: search.arguments.map((arg) => [arg, (0, inEffect_js_1.readArgument)(arg, args, fills)]), period: tool.period, dispatchedAt: dispatch.dispatchedAt, }); } const call = { resultId: dispatch.resultId, toolCallId: input.toolCallId, toolName: tool.schema.name, turn: scope.turnNumber, batch: input.iteration, dispatched: true, outcome: 'error', effect: !remote && (search !== undefined || (0, tools_js_1.readOnlyByDeclaration)(tool)) ? 'read' : 'not-read', arguments: keptOut ? {} : args, trustedContext, ...(composedScope !== undefined && !keptOut && { scope: composedScope }), readAt: dispatch.dispatchedAt, ...(keptOut && { keptOut: true }), }; if (state.calls.length >= MAX_CALLS) publish(scope, { ...state, complete: false }); else publish(scope, { ...state, turn: scope.turnNumber, calls: [...state.calls, structuredClone(call)], pending: [...state.pending, dispatch.resultId], tools: { ...state.tools, [tool.schema.name]: { name: tool.schema.name, ...(search !== undefined && !keptOut && { search: structuredClone(search) }), }, }, }); return { ...dispatch, args, local: !remote }; } exports.startNegativeCall = startNegativeCall; /** Bind a standalone among miss to the actual argument, never its looked-for prose. */ function amongRow(call, value, handle, tool) { const absence = (0, recognize_js_1.readAbsence)(value); if (absence === undefined) return undefined; const names = (0, among_js_1.namesFacetOf)(absence); if (names === undefined) return undefined; const sent = call.arguments[names.argument]; if ((typeof sent !== 'string' && typeof sent !== 'number') || !(0, normalize_js_1.sameName)(String(sent), absence.looked_for)) return undefined; const fills = (0, hostFilled_js_1.hostFillsOf)(tool); const host = Object.entries(fills?.identity ?? {}).map(([argument, identity]) => ({ what: argument, facet: 'filter', argument, value: { identity }, from: 'host', })); const gaps = [...(absence.not_checked ?? []), ...(absence.cannot_cover ?? [])].map((gap) => ({ kind: gap.kind === 'scope' ? 'scope' : 'prose', what: gap.what, ...(gap.why !== undefined && { why: gap.why }), ...(gap.bears !== undefined && { bears: gap.bears }), })); return { kind: 'absence', toolName: call.toolName, toolCallId: call.toolCallId, iteration: call.batch, composed: tool.source === undefined ? 'local' : 'remote', resultId: call.resultId, handle, lookedFor: absence.looked_for, checked: [ { what: names.source, facet: 'source', from: 'result' }, { what: names.argument, facet: 'population', argument: names.argument, value: sent, from: 'call', quote: true, }, { ...names, measuredAt: absence.provenance?.measured_at }, ...host, ], notChecked: absence.not_checked ?? [], cannotCover: absence.cannot_cover ?? [], complete: false, gaps, }; } /** All described data slots, in a stable order shared by capture and display admission. */ function describedRowsOf(value) { if (!record(value)) return undefined; const collections = ['facts', 'series', 'edges'].flatMap((key) => value[key] === undefined ? [] : [value[key]]); if (collections.length === 0 || !collections.every((rows) => Array.isArray(rows) && rows.every(record))) return undefined; return collections.flat(); } /** The same recognized row shapes at native capture and final display admission. * Final model text is used only to check that native rows survived; never as evidence. */ function rowsOf(value) { const envelope = (0, toolEffects_js_1.readToolResultEnvelope)(value); const raw = envelope === undefined ? value : envelope.content; const sem = (0, envelope_js_1.readSemantics)(raw); const ledger = (0, recognize_js_1.readCoverageLedger)(raw) ?? (0, recognize_js_1.readDeclaredLedger)(raw); if (sem !== undefined) return describedRowsOf(sem); let rows = ledger?.result ?? raw; if (!Array.isArray(rows) && record(rows) && Array.isArray(rows.rows)) rows = rows.rows; return Array.isArray(rows) && rows.every(record) ? rows : undefined; } /** Snapshot immediately on native return, before teardown, observers and middleware. */ function captureNegativeResult(scope, dispatch, value, tool) { try { if (!(0, jsonFaithful_js_1.isJsonFaithful)(value)) { keepNegativeResultOut(scope, dispatch.resultId); return value; } const copy = structuredClone(value); const keys = []; const visit = (part) => { if (part === null || typeof part !== 'object') return; for (const [key, item] of Object.entries(part)) { keys.push(key); visit(item); } }; visit(copy); if (keys.some((key) => (0, runRedaction_js_1.runKeepsOut)(scope, key))) { keepNegativeResultOut(scope, dispatch.resultId); return copy; } const envelope = (0, toolEffects_js_1.readToolResultEnvelope)(copy); const raw = envelope === undefined ? copy : envelope.content; const sem = (0, envelope_js_1.readSemantics)(raw); const absent = (0, recognize_js_1.readAbsence)(raw) ?? (0, recognize_js_1.readDeclaredAbsence)(raw); const ledger = (0, recognize_js_1.readCoverageLedger)(raw) ?? (0, recognize_js_1.readDeclaredLedger)(raw); const read = (0, emptiness_js_1.readEmptiness)(raw, { door: { absent: absent !== undefined, bounded: ledger !== undefined, ...(sem !== undefined && { described: sem }), }, }); const rowsRead = rowsOf(raw); const countedRows = read.rows ?? (sem !== undefined ? rowsRead?.length : undefined); const notRead = envelope?.status !== undefined && ['invalid', 'denied', 'failure', 'pending'].includes(envelope.status); if (notRead) captureNegativeRefusal(scope, tool.schema.name, raw, dispatch.args); replace(scope, dispatch.resultId, (call) => { if (call.keptOut) return call; const among = amongRow(call, raw, dispatch.handle, tool); return { ...call, outcome: notRead ? 'error' : 'ok', emptiness: notRead ? 'unknown' : read.emptiness, ...(countedRows !== undefined && { rows: countedRows }), ...(rowsRead !== undefined && { rowsRead }), ...(among !== undefined && { row: { ...among, composed: dispatch.local ? 'local' : 'remote' }, // A returned absence says what was looked up, not whether this call // also wrote. Only the app's dispatch-time declaration exempts it // from write invalidation. effect: call.effect, }), }; }); return copy; } catch { replace(scope, dispatch.resultId, (call) => ({ ...call, keptOut: true })); return value; } } exports.captureNegativeResult = captureNegativeResult; function captureNegativeCoverage(scope, row) { replace(scope, row.resultId, (call) => call.keptOut ? call : { ...call, row: structuredClone(row) }); } exports.captureNegativeCoverage = captureNegativeCoverage; /** Park the native occurrence itself; provider call ids are never a selector. */ function pauseNegativeCall(scope, resultId) { const state = stateOf(scope); if (state !== undefined) publish(scope, { ...state, pausedResultId: resultId }); } exports.pauseNegativeCall = pauseNegativeCall; function pausedNegativeResultId(scope) { return stateOf(scope)?.pausedResultId; } exports.pausedNegativeResultId = pausedNegativeResultId; /** The terminal bracket settles an attempt; a human answer is not a native observation. */ function settleNegativeCall(scope, resultId, outcome) { const state = stateOf(scope); if (state === undefined || resultId === undefined) return; const { pausedResultId, ...rest } = state; publish(scope, { ...rest, ...(pausedResultId !== undefined && pausedResultId !== resultId && { pausedResultId }), pending: state.pending.filter((id) => id !== resultId), calls: state.calls.map((c) => c.resultId === resultId ? { ...c, outcome: outcome === 'ok' && c.emptiness !== undefined ? c.outcome : outcome === 'ok' ? 'error' : outcome, } : c), }); } exports.settleNegativeCall = settleNegativeCall; /** Existing semantic recognition, filed as codes only; a refusal never manufactures a read. */ function captureNegativeRefusal(scope, tool, value, args) { const state = stateOf(scope); if (state === undefined) return; try { const direct = (0, overclaim_js_1.refusalIssues)(value, 'result'); const text = typeof value === 'string' ? value : JSON.stringify(value); const contextual = Object.entries(args ?? {}).some(([argument, sent]) => (0, overclaim_js_1.refusalIssues)({ text, argument, ...(typeof sent === 'string' && { value: sent }) }, 'result').length > 0); if (direct.length === 0 && !contextual) return; if (state.overclaims.some((row) => row.turn === scope.turnNumber && row.tool === tool)) return; if (state.overclaims.length >= MAX_CALLS) { publish(scope, { ...state, complete: false }); return; } if ((0, footprintjs_1.isDevMode)() && !state.overclaims.some((row) => row.tool === tool)) { // eslint-disable-next-line no-console console.warn(`agentfootprint negative-claims: tool '${tool}' — refusal-asserts-absence; a refusal cannot establish absence.`); } publish(scope, { ...state, overclaims: [ ...state.overclaims, { tool, code: 'refusal-asserts-absence', turn: scope.turnNumber }, ], }); } catch { publish(scope, { ...state, complete: false }); } } exports.captureNegativeRefusal = captureNegativeRefusal; /** The transform cannot supply evidence; unreadable delivered output also withholds its handle. */ function keepNegativeResultOut(scope, resultId) { if (resultId !== undefined) replace(scope, resultId, (call) => ({ ...call, keptOut: true })); } exports.keepNegativeResultOut = keepNegativeResultOut; /** A framework suffix, after transforms/caps. Gap notes and periods survive; inventory names never do. */ function serveNegativeHandle(scope, resultId, text) { const call = stateOf(scope)?.calls.find((c) => c.resultId === resultId); // This boundary receives the serialized result AFTER middleware, artifact // placement and truncation. A projection may hide native row values; it cannot // introduce display evidence. Older checkpoints carry no display permission. if (call?.rowsRead !== undefined && !call.keptOut && call.outcome === 'ok') { let rowsMayDisplay = false; try { if (text.length <= MAX_BYTES) { const delivered = JSON.parse(text); // semanticsForModel removes the private marker but retains these data // slots. A slot can only permit display by matching the native rowset. const candidates = [rowsOf(delivered), describedRowsOf(delivered)]; const native = (0, stableJson_js_1.stableJson)(call.rowsRead); rowsMayDisplay = native !== undefined && candidates.some((candidate) => Array.isArray(candidate) && candidate.every(record) && (0, stableJson_js_1.stableJson)(candidate) === native); } } catch { /* A summary or truncation marker is not an unchanged rowset. */ } replace(scope, call.resultId, (current) => { const next = { ...current }; delete next.rowsMayDisplay; return { ...next, ...(rowsMayDisplay && { rowsMayDisplay: true }) }; }); } if (call?.row === undefined || call.keptOut || call.outcome !== 'ok') return text; const row = call.row; return `${text}\n${JSON.stringify({ af_search: { handle: row.handle, gaps: row.gaps, notChecked: row.notChecked, cannotCover: row.cannotCover, ...(row.period !== undefined && { period: row.period }), ...(row.inProgress !== undefined && { inProgress: row.inProgress }), }, })}`; } exports.serveNegativeHandle = serveNegativeHandle; function negativeRecordOf(scope, runId) { const state = stateOf(scope); const identity = scope.$getValue('runIdentity'); const ids = scope.resultIdentities; let namespace = ids?.evidence.namespace ?? ''; if (namespace === '' && state?.calls[0] !== undefined) { try { namespace = (0, capture_js_1.readResultIdentity)(state.calls[0].resultId).namespace; } catch { /* unknown */ } } const clock = (0, rows_js_1.clockOf)(scope.findingsLedger); const owned = state !== undefined && sameIdentity(state.ownerIdentity, identity); return { leg: { namespace, runId: runId ?? ids?.evidence.runIds.at(-1) ?? '' }, turn: scope.turnNumber, clock: (clock?.turn === scope.turnNumber ? clock.now : undefined) ?? (Number.isFinite(scope.turnStartMs) ? new Date(scope.turnStartMs).toISOString() : ''), calls: owned ? state.calls : [], tools: owned ? state.tools : {}, ledgerComplete: owned && state.complete && state.pending.length === 0, }; } exports.negativeRecordOf = negativeRecordOf; //# sourceMappingURL=capture.js.map