package vip.mate.goal.model; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * Value object carrying one evaluation pass result from * {@code GoalEvaluationService} to {@code GoalEvaluationNode} and on to * {@code GoalService.recordEvaluation}. * *

{@link #completed} means "evaluator judged this turn satisfies every * exit criterion". It does not mean "graph FINISH_REASON should change" — * goal status and graph FinishReason are independent. * *

{@link #llmCallsConsumed} is the evaluator-side delta only; the * agent-side delta is read from graph state by the node itself. * *

{@link #criterionVerdicts} and {@link #bootstrapCriteria} are mutually * exclusive carriers for the checklist: *

* Neither is the outward-facing full list — clients always receive the merged * checklist via {@code GoalResponse.criteria}. */ public record GoalEvaluationResult( double score, String gap, String decision, boolean completed, String evaluatorModel, int llmCallsConsumed, long latencyMs, List criterionVerdicts, List bootstrapCriteria) { public static final String DECISION_COMPLETED = "completed"; public static final String DECISION_CONTINUE = "continue"; public static final String DECISION_FALLBACK = "fallback"; /** Failure fallback for the "no call was made" cases (no goal, empty * answer, no model). Does NOT charge eval_llm_calls_used. */ public static GoalEvaluationResult fallback(String reason) { return new GoalEvaluationResult( 0.0, "evaluator unavailable: " + reason, DECISION_FALLBACK, false, "", 0, 0L, List.of(), null); } /** Failure fallback for cases where the evaluator LLM call already * succeeded but its output was unusable (empty / unparseable). The call * was really spent, so it charges {@code llmCallsConsumed = 1} and * records the model + latency for accurate budget accounting. */ public static GoalEvaluationResult fallbackAfterCall(String reason, String model, long latencyMs) { return new GoalEvaluationResult( 0.0, "evaluator unavailable: " + reason, DECISION_FALLBACK, false, model == null ? "" : model, 1, latencyMs, List.of(), null); } public Map toMap() { Map m = new LinkedHashMap<>(); m.put("completionScore", score); m.put("gap", gap == null ? "" : gap); m.put("decision", decision); m.put("completed", completed); m.put("evaluatorModel", evaluatorModel == null ? "" : evaluatorModel); m.put("llmCallsConsumed", llmCallsConsumed); m.put("latencyMs", latencyMs); // Per-round delta, for debugging/detail only. UI progress is driven by // the full GoalResponse.criteria array, never reconstructed from this. m.put("criterionVerdicts", criterionVerdicts == null ? List.of() : criterionVerdicts); return m; } }