Build

Negative claims

Observe prose claims or deliver library-written typed statements, with the search scope and gaps preserved.

.negativeClaims() has two forms. { prose: true } observes ordinary answers and files warnings. { field, tools, unestablished } makes the library write typed negative statements from cited searches. Prose observation never verifies or withholds an answer. Typed enforcement is available, not recommended until its benchmark gates pass. Offline replays are diagnostic; structured-path accuracy and usefulness remain unmeasured.

Observe an ordinary answer

No output schema, citation field, tool-name list or answer validator is required. Native tool dispatch records supply the observations; assistant messages and summaries never become evidence. The example's local search declares a source gap, then the mock model makes a broader claim.

const agent = Agent.create({ provider, model: 'mock' })  .tool(listIncidents)  .negativeClaims({ prose: true })  .build();agent.on('agentfootprint.findings.negative_claim', ({ payload }) => {  warnings.push(payload);  const codes = payload.kind === 'negative-claim' ? payload.notes : [payload.code];  console.log(`[prose warning] ${codes.join(', ')}`);});const answer = await agent.run({  message: input,  identity: {    tenant: 'example-tenant',    principal: 'example-user',    conversationId: 'prose-example',  },});// The warning observes the source gap; it cannot rewrite, block or verify prose.console.log(answer);

The prose-only arm adds no correction request or delivery buffer. It preserves normal streaming, and the returned answer, final event, history and memory use the same delivered text. It observes the terminal answer after output middleware; it does not rewrite the answer, call a tool again or turn a warning into a veto. An independent .outputSchema() or .answerValidation() retains its own behavior, including retries, buffering and refusal. .outputFallback() is refused with the prose-only arm because a fallback can replace an answer after observation.

The English grammar is a heuristic. The prior detector failed every free-replay benchmark gate; the revised, data-anchored path remains unmeasured. It can miss negatives and flag correct statements. Comparing a claim with read evidence requires an anchor in the same clause: a dispatched read's declared said words, actual sent value or resolved name or alias. Host-only and kept-out values cannot supply that anchor; another clause or a user message cannot supply evidence. Anchoring permits an observation, not a binding or proof. Entity resolution and coverage checks remain separate, so a sent name can still produce entity-unresolved.

If the grammar detects an eligible negative and no dispatched native read is recorded for the current turn, the observer files one negative-claim-note with code no-read per answer, within the observation budgets below. For example, answering "Payments has no incidents. There are no incidents anywhere." without a recorded read produces one warning. A read from an earlier turn does not suppress it. A recorded dispatched read does suppress this note even if it failed or its result was kept out; that presence check does not establish usable evidence. Because the ledger can be incomplete, no-read does not prove that no read occurred.

The observer filters the exact normalized ordinary clauses "None.", "No action is needed." and "Tests: 0 failed."; they produce no no-read warning. Added words or a separate negative clause remain eligible. The note records missing read evidence; it never verifies, blocks or rewrites the answer.

agent.findings() retains negative-claim and negative-claim-note rows. A current-turn row makes the assessment not-sure with reason negative-prose-unverified, including a detected claim with no coverage note. No finding does not mean verified. The observer is not an exhaustive proof that the answer is true.

Subscribe to agentfootprint.findings.negative_claim before running the agent. It emits once per claim or unread-unit row, with kind, field and span, phrase-pack version, iteration and turn, and either claim/note codes with native result references or an unread-unit code. Answer text, candidate labels and tool values are excluded. Metadata is bounded; truncated: true means that metadata was shortened or references omitted. The typed arm emits the same warnings.

Observation examines at most 65,536 UTF-16 code units across 256 string values, with at most 32 detected claims and unjudged units combined. One shared work budget also bounds pattern preparation and evidence/name matching across all fields; exhausting it produces one input-limit note and discards partial findings. An oversized or unreadable structured input, or a field path over 4,096 code units, produces an input-limit note; too many units produce too-many-claims. Each replaces partial findings with one whole-answer note. Recognized code and unsupported-language units produce in-code-block or language-unsupported notes. Language detection uses a closed heuristic and can miss non-English text; it is not multilingual verification. strictProse and enforceProse remain unavailable.

Add literal phrases

// Optional configuration: bounded literal words, never regexes or callbacks.export const customObservation = {  prose: true,  phrases: { none: ['came up empty'], complete: ['the entire inventory is here'] },} satisfies NegativeClaimsOptions;

NegativeClaimPhraseExtensions describes the optional phrases object. Pass this configuration to .negativeClaims(customObservation). At most 32 phrases total may extend none and complete, with at most 128 UTF-16 code units each. They are literal words, not regexes, slot patterns or executable predicates. Extensions use the same language, code, anchoring and budget rules. Their frozen effective pack has its own hash in the reported version; adding phrases never enables enforcement.

Deliver typed negative statements

The model supplies restsOn result handles. The library derives the claim type and searched subject from the admitted result records, then writes the scope, gaps and delivered statement. Model wording never selects the subject or supplies evidence.

NegativeClaimsOptions configures the field, eligible tools and failure behavior. NegativeClaimCitation is the model-input shape; DeliveredNegative adds the library's statement and version. agent.answerNegatives() returns the AnswerNegatives metadata with per-source checked counts, diagnostics and dispatched action outcomes; an action outcome does not prove an external effect.

Other fields remain unchecked model text. Prose observation is automatic beside the typed arm; do not combine prose: true with field, tools or unestablished. Either form accepts the optional literal phrases extension.

Declare the search and bind the caller

Use a local tool with a search declaration. The declaration must describe the query the tool actually runs. Fill caller scope through hostFilled from the host's authenticated identity, rather than asking the model to supply it. A local absent({ among }) result can also support a named miss. MCP and ToolProvider declarations cannot establish a typed negative.

const listIncidents = hostFilled(  defineTool({    name: 'list_incidents',    description: 'Read open incidents for one service in the caller tenant.',    inputSchema: {      type: 'object',      properties: { service: { type: 'string' }, tenantId: { type: 'string' } },      required: ['service', 'tenantId'],    },    effects: 'read',    search: {      source: { what: 'the incident store', said: ['the incident store'] },      subject: { what: 'incidents', said: ['incidents'] },      fixed: [{ name: 'status', value: 'open', said: ['open'] }],      arguments: [        { name: 'service', role: 'population' },        { name: 'tenantId', role: 'filter' },      ],      complete: 'within-arguments',      gaps: [        {          what: 'incidents filed by email',          why: 'email reports never enter this store',          bears: 'source',        },      ],    },    execute: ({ service, tenantId }: { service: string; tenantId: string }) => {      // A real implementation queries its store with BOTH of these filters.      // This synthetic store has no rows for the demonstration tenant.      if (tenantId !== 'example-tenant') throw new Error('Unknown demonstration tenant');      return absent({ what: `open incidents for ${service}`, checked: 'declared' });    },  }),  { tenantId: fromIdentity('tenant') },);

An empty result is different from an unread search: a completed search can find zero rows. Missing observations, skipped checks and missing caller scope remain unverified. A declaration that misdescribes the backend is outside this check's guarantee; declarations and the host's identity are the trusted inputs.

Give the model a citation field

The field must be a required top-level array. Each entry requires kind and restsOn. The kind selects the claim to verify; optional about names its subject. Supply jsonSchema explicitly or use a parser that provides toJsonSchema(); the library does not infer a shape by executing a parser.

const jsonSchema = {  type: 'object',  properties: {    summary: { type: 'string' },    negatives: {      type: 'array',      items: {        type: 'object',        properties: {          kind: { type: 'string', enum: ['none', 'not-found', 'complete'] },          about: { type: 'string' },          restsOn: { type: 'array', items: { type: 'string' } },        },        required: ['kind', 'restsOn'],        additionalProperties: false,      },    },  },  required: ['summary', 'negatives'],  additionalProperties: false,};

The library serves the short-handle pattern ^r[1-9][0-9]{0,3}$, limits the field to 32 claims and each citation to 16 reads, and closes each entry to extra fields. The model receives handles such as r1 beside admitted results. Provider call IDs never select evidence. Each read must be within the admitted caller scope. The checker compares other reads using the cited searches' recorded subjects and populations, so an unrelated model label cannot hide contradicting results.

Requested kindWhat the cited reads must establish
noneA declared absence within the composed search scope.
not-foundA named miss from absent({ among }), with the inventory's limits.
completeCompleteness for the executed arguments, without an open page, reached cap or partial result. The statement describes rows actually read.

Evidence interpretation and existing parsers

The model's kind selects the proposition. none and not-found can be normalized to the appropriate absence spelling only when every cited read explicitly declares absence. A negative against returned rows is blocked, even if that read is complete. The library never turns a negative into a complete-list claim. A changed absence spelling gets an audit-only absence-kind-normalized note.

complete must be requested explicitly and satisfy the recorded completeness declaration and every completeness guard. Proving an absence alone does not establish completeness. Explicit completeness evidence can cover a zero-row search; a bare empty list cannot supply it. For a failed complete-list claim whose narrower absence independently passes all checks, the fallback says: "The requested complete-list claim is not established; only this absence is." It retains every gap and the failed complete-list verdict. Default withhold still withholds the answer; state-searched may deliver this explicit fallback. It never gets as-checked.

A raw empty list, failed read or missing coverage never becomes declared absence. A known native empty read without an absence declaration is reported as undeclared-empty, without minting evidence or treating it as a citation typo. Multiple reads retain their individual scopes; the library does not invent a union.

When about is supplied, it must contain every cited read's public population value as a complete name or identifier after the library's normalization. "Open incidents for payments" matches a payments search; "payments" cannot label a billing search, and pay does not match payments. Unknown or all-population values cannot be validated against a supplied phrase. This checks name inclusion, not the meaning of surrounding prose. The original phrase is never delivered as the subject.

Keep kind and restsOn required in both your parser and JSON Schema. about may remain required by your app or be omitted. The library does not rewrite an opaque parser or invent values to satisfy it. Surrounding answer prose remains unchecked.

Deliver the statement and its marker together

const agent = Agent.create({ provider, model: 'mock' })  .tool(listIncidents)  .outputSchema(parser, { jsonSchema })  .negativeClaims({ field: 'negatives', tools: ['list_incidents'] })  .build();// The host supplies identity after authenticating the caller; the model// cannot set the tenantId tool argument that hostFilled owns.const answer = await agent.runTyped<{ summary: string; negatives: DeliveredNegative[] }>({  message: input,  identity: {    tenant: 'example-tenant',    principal: 'example-user',    conversationId: 'negative-example',  },});for (const claim of answer.negatives) {  console.log(`[${claim.delivery}] ${claim.statement}`);}

An established entry gains its checked kind, statement, delivery: 'record-composed' and version. Display that marker beside the sentence. Delivered about is the single public population value from the admitted search, never the model's phrase. It is omitted for multiple or unavailable subjects; the statement keeps each read's scope. Host-only identity values never become subject labels. The answer-level delivery is as-checked only when its sole field consists of established library statements. Other model fields make delivery recorded; this never verifies surrounding prose. The model-input parser describes only citations; runTyped() returns the enriched delivered object without running that parser over it again.

Every execution mode delivers the library statement, including when an app's .answerValidation() uses observe mode. The final answer is buffered so a draft cannot escape through streaming. The person, turn_end.answerNegatives, conversation history and memory receive the same statements and gaps.

unestablished chooses the failure behavior:

SettingDelivery
withhold (default)Withhold the whole answer; the caller receives AnswerValidationError, HTTP hosts return 422 and AG-UI reports RUN_ERROR.
state-searchedReplace the entry with what the admitted record says was searched and any admitted contrary result. The failed or unverified standing stays failed or unverified.

A contrary read is named by its result handle and shows bounded, quoted native row values when they survived middleware, placement and truncation unchanged. Otherwise its count is reported without re-exposing hidden values. A positive result without a coverage declaration is explicitly labelled that way.

Complete statements count every admitted fact, series point and edge. They show row identities only when all native rows survived the final tool-result boundary unchanged; removing or replacing a collection suppresses those identities. These statements use template version 7.

A bad citation gets one correction request naming valid handles, with no tools available. If .outputSchema() enables retries, schema correction shares that same one-request budget with citation and evidence correction, even when retries is greater than one. Rejected drafts and correction instructions stay out of conversation history and memory. A correction cannot repeat a write to satisfy the checker. A later dispatched call that may write invalidates an earlier read even if the call fails; a local effects: 'read' declaration can identify a read-only tool. Remote read-only hints cannot. Reads retained through a pause remain eligible within the same turn; earlier-turn reads are stale and their refusal identifies the earlier turn and recorded date.

This does not verify the rest of an answer. An empty citation array makes no typed claim. Every original free-form model string, including nested fields and about, is observed before the library writes its statements. Detected negatives file negative-claim findings; unread units file negative-claim-note findings. Both make the answer fold not-sure, without changing delivery. The grammar has known false positives and misses; prose remains unchecked even when it produces no finding.

The same whole-answer observation budgets described above apply. The model is instructed to state negatives only through cited searches. These instructions do not guarantee compliance.

Typed delivery introduced the global answer-account templateVersion 12, including for agents without .negativeClaims(). The standalone prose arm retains that version and the negative-prose-unverified assessment reason.

The checker compares other observations only within its declared subject and scope rules. Independent host-authored answer checks continue to apply, and a library statement never turns an unverified answer into a verified one.

Both mock examples run without credentials or network access:

npm run example -- examples/features/110-negative-claims.ts
npm run example -- examples/features/111-negative-prose-observe.ts

On this page