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 kind | What the cited reads must establish |
|---|---|
none | A declared absence within the composed search scope. |
not-found | A named miss from absent({ among }), with the inventory's limits. |
complete | Completeness 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:
| Setting | Delivery |
|---|---|
withhold (default) | Withhold the whole answer; the caller receives AnswerValidationError, HTTP hosts return 422 and AG-UI reports RUN_ERROR. |
state-searched | Replace 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.tsNames and numbers from evidence
Every number, identifier and name in the final answer must appear in a tool result the run really read. If one does not, the model typed it rather than read it — a deterministic check, no second model.
Described tool results
describedResult() — a tool returns rows, a series or relationships as typed data, with grain, freshness and coverage traveling with the numbers; a build gate refuses a triage tool that forgets its caveats, by name.
