Judge Agent Pattern
JudgeCoordinator wraps a coordination cycle with a JudgePolicy. After each attempt the policy returns a JudgeVerdict of APPROVE, RETRY, or REJECT. This is cycle-end quality control for one agent's output, not a tournament that ranks several candidates. Package: com.tnsai.coordination.judge.
Quick Start
JudgePolicy policy = LLMJudgePolicy.builder()
.llmClient(llmClient)
.criteria("Evaluate for correctness, completeness, and clarity.")
.build();
JudgeCoordinator<String> coordinator = JudgeCoordinator.<String>builder()
.policy(policy)
.maxRetries(3)
.timeout(Duration.ofSeconds(30))
.executor((agentId, feedback) -> produceOutput(agentId, feedback))
.build();
JudgeCoordinator.Result<String> result = coordinator.execute("agent-1");
if (result.isApproved()) {
System.out.println(result.output());
}LLMJudgePolicy has no public constructor. The builder accepts only llmClient and criteria. Scores are always in [0.0, 1.0].
JudgePolicy
JudgePolicy is a functional interface. JudgeCoordinator calls it once per iteration:
| Parameter | Type | Role |
|---|---|---|
agentId | String | Agent whose output is being judged |
output | Object | Cycle output (type depends on the executor) |
context | JudgeContext | Iteration number, history, and time budget |
The return type is JudgeVerdict, not a ranked list of candidates. There is no CandidateOutput, JudgeResult, or ScoredCandidate type. Built-in policies are constructed in code; they are not loaded from META-INF/services.
ThresholdJudgePolicy
Numeric gate. The caller supplies the output class and a scoring function. The policy approves when the score is at least the threshold; otherwise it retries, or rejects on the final iteration.
JudgePolicy policy = new ThresholdJudgePolicy<>(
0.8,
String.class,
output -> score(output)
);
JudgeVerdict verdict = policy.evaluate(
"agent-1",
"draft answer",
JudgeContext.first(3)
);
if (verdict.isApproved()) {
System.out.println(verdict.score());
}Best for: deterministic CI gates and tests that must not call an LLM.
LLMJudgePolicy
Sends one output to an LLM with your natural-language criteria. The model must answer in a fixed DECISION / SCORE / REASON / SUGGESTIONS shape. The only builder methods are llmClient(LLMClient) and criteria(String).
| Parameter | Default | Description |
|---|---|---|
llmClient | required | LLMClient used for evaluation |
criteria | required | Natural-language quality criteria |
There is no llm(), scoringScale(), requireReasoning(), or model() setter.
JudgeCoordinator
JudgeCoordinator<T> runs the executor, judges the output, and retries with the previous verdict's suggestions until the policy approves, rejects, or maxRetries is exhausted.
| Parameter | Default | Description |
|---|---|---|
policy | required | JudgePolicy used after each attempt |
executor | required | (agentId, feedback) -> T. Feedback is empty on the first call |
maxRetries | 3 | Total attempts, including the first |
timeout | unset | Optional bound for the whole cycle |
execute(String agentId) is the only run method. There is no evaluate(task, candidates) and no runAndJudge(...).
Result
JudgeCoordinator.Result<T> is the value returned by execute:
| Accessor | Meaning |
|---|---|
output() | Best output produced, or null if every attempt failed |
finalVerdict() | Last JudgeVerdict |
verdictHistory() | Verdict from every iteration |
iterations() | Attempt count |
duration() | Wall time for the cycle |
isApproved() | Whether finalVerdict() is APPROVE |
score() | finalVerdict().score() |
JudgeVerdict
| Component | Meaning |
|---|---|
decision() | APPROVE, REJECT, or RETRY |
reason() | Human-readable explanation |
score() | Quality in [0.0, 1.0] |
suggestions() | Feedback for the next retry (empty on approve) |
isApproved() | decision() == APPROVE |
Factories: JudgeVerdict.approve(reason, score), retry(...), reject(...).
JudgeContext
| Component | Meaning |
|---|---|
iteration() | 1-based attempt number |
maxIterations() | Same bound as maxRetries |
previousVerdicts() | Earlier verdicts (empty on the first attempt) |
timeBudget() | Optional remaining budget |
isFinalIteration() | iteration >= maxIterations |
JudgeContext.first(maxIterations) builds the first attempt.
Integration with Team
Use JudgeCoordinator around Team.execute. Team.execute returns TeamResult, not String. Team.builder() requires a mission and adds members with member(...), not addMember(...).
Team team = Team.builder()
.name("Draft team")
.mission(task)
.formation(TeamFormation.PARALLEL)
.member(agent1, TeamRole.MEMBER)
.member(agent2, TeamRole.MEMBER)
.build();
team.start();
JudgeCoordinator<TeamResult> coordinator = JudgeCoordinator.<TeamResult>builder()
.policy(LLMJudgePolicy.builder()
.llmClient(llmClient)
.criteria("Approve only if the aggregated team output is complete and accurate.")
.build())
.maxRetries(3)
.executor((agentId, feedback) -> team.execute(task))
.build();
JudgeCoordinator.Result<TeamResult> judged = coordinator.execute(team.getId());
TeamResult teamResult = judged.output();For group shapes themselves, see Topologies.
Negotiation
TnsAI.Coordination provides a pluggable negotiation framework with 4 built-in protocols, configurable concession strategies, and a unified NegotiationExecutor that resolves the correct protocol from configuration.
AutoTeamBuilder
TnsAI.Intelligence generates a configured multi-agent team from a natural-language task. AutoTeamBuilder decomposes the task, writes per-agent prompts, picks a formation, and instantiates the agents. Package: com.tnsai.autoteam.