Verification
Code: src/IntentSharp/Verification/DeterministicVerifier.cs, ModelJudge.cs,
src/IntentSharp/Results/AIResult.cs.
A result succeeds only when an attempt passes deterministic checks and, if enabled, the judge. Every
failure is a ResultIssue with a message that is fed back to the model on the next attempt.
Deterministic checks
DeterministicVerifier.Verify<T>(AIResponse, PromptPlan, AIRequestOptions?, int attempt) runs, in order:
- Refusal.
AIResponse.IsRefusaladds aProviderissue and stops. - Truncation.
AIResponse.IsTruncatedadds aProviderissue. The attempt always fails. - Empty. After
StripFencesand trimming, empty content is aVerificationissue. - String targets. For
T == stringthe stripped text is the value. No JSON checks. - JSON. Parsed with comments skipped and trailing commas allowed. If parsing fails, the verifier tries
to read a bare primitive (
int,long,double,decimal,bool,Guid,DateTime,DateOnly,TimeSpan, enums). An enum is read by name only: a number, or a combination of names that is no member of an enum without[Flags], is not read. Otherwise aVerificationissue: "The response was not valid JSON". - Envelope. An object with exactly one key
valueis unwrapped. An object that hasvaluetogether with other keys is aVerificationissue that names the unexpected keys ("Reply must be a single envelope object {"value":} and nothing else; unexpected keys: note."), because read as a bare value it would lose the result: every member of a typed target at its default, or the whole object for a runtime shape. The exceptions are targets whose bare value may hold a valuekey itself, a dictionary or a type with its own member serialized asvalue(any case): such an object is read as a bareT. Any other reply is treated as the bare value. - Deserialization to
T. AJsonExceptionorNotSupportedExceptionbecomes aVerificationissue that names the expected type. The deserializer setsRespectNullableAnnotationsandRespectRequiredConstructorParameterson every target framework (net8.0, net9.0 and net10.0), and a presence check reports non-nullable members that are missing from the JSON. Enums are read by name only (JsonStringEnumConverterwithallowIntegerValues: false), so{"value": 7}fails. WhenTis an enum without[Flags]and a reply such as"Neutral, Negative"combines names into a value that is no member, the issue is ""3" is not a member of Sentiment; answer with exactly one of: Positive, Neutral, Negative." - Null. A null payload (
{"value": null}or a barenull) passes only whenTisNullable<>, such asint?. For every otherT, reference types included, it is aVerificationissue: "The value was null but a non-null {T} is required." A successful result therefore never carries a nullValueunlessTisNullable<>, andGetValueOrThrownever returns null for a class target. - Validators. Each function in
AIRequestOptions.Validatorsruns on the parsed value. Every string it returns is aValidatorissue. A validator that throws becomes "Validator threw: ...".
The outcome passes only when there are no issues other than provider issues, and the response was not truncated.
Runtime shapes
A runtime shape is a JsonNode target, usually from TransformAsync(input, intent, JsonNode outputSchema)
(prompting-and-schemas.md). Steps 1 to 5 and 9 are the same as
for any other target. Steps 6 to 8 differ:
- Envelope. As in step 6, but a runtime shape has no members, so the exception never applies: an object
that has
valuetogether with other keys always fails. Any other reply, an object without avaluekey included, is accepted as the bare value. A result whose own top level has avalueproperty therefore has to come back inside the envelope. - No deserialization. The value is returned as a detached copy (
DeepClone) of the parsed JSON, so the result never shares nodes with the parsed reply. - Null.
{"value": null}and a barenullfail with "The value was null but a JSON value is required." - Type. When
Tis a subtype such asJsonObjectand the value is another kind of JSON, the issue names the kind that came back.
The verifier does not check the value against the schema. Validators receive the JsonNode, and the intent
catalog adds one that validates schema shapes against the full schema
(intent-catalog.md).
Validators
A validator is Func<object?, IEnumerable<string>>. An empty sequence means acceptable. Add them with:
var options = new AIRequestOptions()
.Validate<List<int>>(xs => xs.Count == 3 ? Array.Empty<string>() : new[] { "Return exactly 3 numbers." })
.Require<List<int>>(xs => xs.All(x => x > 0), "Every number must be positive.");
Validate<T> reports "Value was not a T." when the value has a different type. The attribute route is
[AIGuardrail(GuardrailType.CustomValidator, typeof(MyValidator))], where MyValidator implements
IGuardrailValidator and has a parameterless constructor. The collection helpers in AIExtensions and the
database runtime add their own validators the same way.
The judge
IResultJudge.JudgeAsync(PromptPlan, string candidateOutput, string? model, CancellationToken) returns a
JudgeVerdict (Passes, Problems) and the raw AIResponse.
ModelJudge sends the intent, the input, the candidate output and any extra output requirements to its
provider, with a strict schema for {passes, problems} and MaxOutputTokens = 1024.
- A judge reply that is not valid JSON counts as a pass, so a broken judge cannot block results. It is
logged at
Warning("The provider judge reply for intent ... was not JSON; the attempt was accepted without a verdict.") through the logger given toModelJudge(provider, logger); the builder's default judge gets one fromWithLoggerFactory. Nothing in theAIResultmarks it. - A reply that deserializes to null fails with "Judge returned no verdict."
- A failing verdict with no problems gets "The judge rejected the output without listing problems."
When it runs:
IntentClientOptions.JudgeMode |
Judge runs |
|---|---|
Never |
never; the builder creates no judge |
OnRequest (default) |
when AIRequestOptions.UseJudge is true |
Always |
on every attempt that passed deterministic checks |
IntentClientBuilder.Build creates new ModelJudge(primary) unless JudgeMode is Never or
WithJudge(...) supplied one. The judge model is IntentClientOptions.JudgeModel, or the provider default.
Each judge call counts toward MaxModelCalls, AIResult.Usage, IntentClient.TotalModelCalls and the client
token totals. When a rate limiter is configured, the judge call first takes a token from the primary
provider's bucket.
Issue sources
IssueSource |
Meaning |
|---|---|
Provider |
The provider refused, truncated, or failed (HTTP error, timeout, transport). |
Verification |
The output was empty, not JSON, an envelope with extra keys, did not deserialize to T (an enum value that names no member included), or, for a runtime shape, was null. |
Validator |
A caller-supplied or built-in validator rejected the parsed value. |
Judge |
The judge listed a problem. |
Guardrail |
The model-call ceiling or the time budget stopped the request. |
ResultIssue.Attempt is the attempt number. Issues raised when the loop ends early use the next attempt
number. AIResultBase.FailureSummary joins the distinct messages; GetValueOrThrow throws an
IntentSharpException with that summary.
Why there is no confidence score
Version 1 reported confidence scores that were constants or heuristics, so they carried no information
(see ../context/history.md). IntentSharp reports only facts that were checked:
success, concrete issues, every attempt's raw output, usage and timing. Code that needs a stronger
guarantee adds a validator or turns on the judge (GuardrailType.RequireJudge on an intent method). The
obsolete GuardrailType.MinConfidenceScore alias, which only switched the judge on, was removed in
1.0.0-rc.1. See
ADR 0004.