TnsAI
Quality

Validation and Invariants

The Quality module provides multi-agent parallel validation (`ParallelValidationExecutor`), runtime invariant checking (`InvariantChecker`), conditional rule evaluation (`RuleEngine`), and input validation (`ValidationService`).

ParallelValidationExecutor

The ParallelValidationExecutor sends the same prompt to multiple agents (or LLM providers) simultaneously and compares their responses. This is useful for high-stakes tasks where you want cross-validation: if multiple independent models agree on an answer, you can be more confident it is correct.

ParallelValidationExecutor executor = ParallelValidationExecutor.builder()
    .addAgent(claudeAgent)
    .addAgent(geminiAgent)
    .addAgent(ollamaAgent)
    .aggregationStrategy(AggregationStrategy.CONSENSUS)
    .timeout(Duration.ofSeconds(60))
    .synthesisLLM(claudeClient)       // Required for CONSENSUS and ENSEMBLE
    .similarityThreshold(0.5)         // Jaccard similarity for response comparison
    .build();

ValidationResult result = executor.execute("Review this code for security issues");

result.getBestSolution();       // Best response based on strategy
result.getConsensusPoints();    // Points all agents agree on
result.getDivergences();        // Points where agents disagree
result.getMetrics();            // Execution metrics (timing, agreement ratio)
result.getResponses();          // Individual agent responses with confidence scores

// Async execution
CompletableFuture<ValidationResult> future = executor.executeAsync(prompt);

You can also add raw LLMClient instances instead of full agents:

ParallelValidationExecutor executor = ParallelValidationExecutor.builder()
    .addLLM(openaiClient)
    .addLLM(anthropicClient)
    .aggregationStrategy(AggregationStrategy.BEST_OF)
    .build();

AggregationStrategy Enum

After collecting responses from all agents, the executor combines them using one of four strategies. Choose based on your use case -- voting for simple decisions, consensus for nuanced analysis.

StrategyDescriptionRequires Synthesis LLMBest For
VOTINGSelects the most common response (Jaccard similarity)NoClassification, yes/no decisions
BEST_OFSelects the response with highest confidence scoreNoQuality-critical tasks
CONSENSUSExtracts and combines common points from all responsesYesCode review, fact extraction
ENSEMBLECombines all unique insights from every responseYesBrainstorming, comprehensive analysis

Auto-select a strategy based on task type:

AggregationStrategy strategy = AggregationStrategy.forTaskType("code review");
// Returns CONSENSUS

AggregationStrategy strategy = AggregationStrategy.forTaskType("critical security audit");
// Returns BEST_OF

Confidence Estimation

Each agent response receives an automatically estimated confidence score (0.0-1.0). The score is based on heuristics about the response content, not a self-reported confidence from the LLM.

  • Base: 0.5
  • Length bonus: +0.1 for \>500 chars, +0.1 for \>1000 chars
  • Uncertainty penalty: -0.1 for phrases like "I think", "possibly"
  • Certainty bonus: +0.1 for "definitely", "certainly"
  • Structure bonus: +0.1 for bulleted/numbered lists

InvariantChecker

The InvariantChecker enforces correctness rules on your agents at runtime. You define preconditions, postconditions, and invariants using annotations on your role classes, and the checker verifies them before and after each action. This catches bugs early by ensuring your agent's state remains valid throughout execution.

Supported Annotations

Invariants are defined through these annotations on your action methods and state fields.

  • @ActionSpec(precondition = "health > 0") -- checked before action execution
  • @ActionSpec(postcondition = "state == COMPLETED") -- checked after execution
  • @ActionSpec(invariants = {"energy >= 0", "DEAD if health <= 0"}) -- checked before and after
  • @State(invariants = {"value >= 0"}) -- checked after any state change

Usage

You can check invariants manually around action execution, or use the batch convenience methods that handle all checks in a single call.

InvariantChecker checker = new InvariantChecker(roleObject);

// Before action
Method method = role.getClass().getMethod("attack");
checker.checkPrecondition(method);
checker.checkActionInvariants(method);

// Execute the action...

// After action
checker.checkPostcondition(method);
checker.checkActionInvariants(method);
checker.checkStateInvariants();

// Or use batch methods
checker.checkBeforeAction(method);  // precondition + invariants
checker.checkAfterAction(method);   // postcondition + invariants + state invariants

// Non-strict mode (logs warnings instead of throwing)
InvariantChecker lenient = new InvariantChecker(roleObject, false);

// Collect all violations without throwing
List<String> violations = checker.collectViolations(method);

// Static convenience methods
InvariantChecker.check(roleObject, "attack");       // check before
InvariantChecker.checkAfter(roleObject, "attack");   // check after

Expression Support

The invariant parser understands comparison operators, boolean logic, and conditional rules. These are written as plain strings in annotation values.

  • Comparisons: health > 0, state == ALIVE, energy != 0
  • Boolean operators: health > 0 && energy > 0, state == DEAD || health <= 0
  • Conditional rules: DEAD if neighbors < 2 (underpopulation) -- see RuleEngine below

Natural language invariants (more than 3 words with no operators) are silently skipped.

RuleEngine

The RuleEngine evaluates conditional state transition rules at runtime. It is useful for game-like scenarios or state machines where you want to verify that an object's state matches what the rules predict. For example, in Conway's Game of Life, you can define rules like "a cell should be DEAD if it has fewer than 2 neighbors" and check whether the actual state matches.

RuleEngine engine = new RuleEngine(cellRole);
engine.setStateField("state");  // Which field to check effects against

engine.addRule("DEAD if neighbors < 2 (underpopulation)");
engine.addRule("ALIVE if neighbors == 3 (reproduction)");
engine.addRule("unchanged if neighbors == 2 (survival)");

// Evaluate all rules
List<RuleEngine.RuleViolation> violations = engine.evaluateAllRules();

for (RuleEngine.RuleViolation v : violations) {
    System.out.printf("Expected %s=%s when %s, but actual=%s%n",
        "state", v.getExpectedValue(), v.getRule().getCondition(), v.getActualValue());
}

Rule Evaluation Semantics

Each rule follows the format "EXPECTED_STATE if CONDITION". Here is how the engine evaluates a rule.

  1. Evaluate the condition: neighbors < 2
  2. If condition is false, the rule does not apply -- skip
  3. If condition is true, the expected effect is DEAD
  4. Read the actual value of the state field via reflection
  5. If actual value does not match DEAD, report a violation

Rules with unchanged as the effect are documentation-only and never produce violations.

Static Factory Methods

For quick setup, you can create a rule engine and populate it from an array of invariant strings in one call. The enforce method additionally throws if any violations are found, making it convenient for test assertions.

// Create and populate from an array of invariant strings
RuleEngine engine = RuleEngine.from(targetObject, new String[]{
    "DEAD if health <= 0",
    "ALIVE if health > 0"
});

// Evaluate and throw if any violations
RuleEngine.enforce(targetObject, invariants, "state");
// Throws RuleViolationException with all violations

ValidationService

See the Security documentation for the full ValidationService reference, including:

  • maxStringLength / maxNestingDepth / maxParameterCount configuration
  • SQL injection, command injection, path traversal, and XSS pattern detection
  • blockDangerousPatterns toggle
  • sanitize() for HTML entity escaping and null byte removal
  • validateOrThrow() for fail-fast validation

On this page