feat(harness): add information gain stop and audit

This commit is contained in:
aruo
2026-07-27 01:03:34 +08:00
parent de5a5b09d9
commit d0452184ee
92 changed files with 5019 additions and 122 deletions
@@ -79,6 +79,40 @@ public final class EvidenceGuard {
: EvidenceGuardResult.invalid(violations);
}
public EvidenceGuardResult validateNoConclusionReferences(
RunContext context, DiagnosisDraft draft) {
Objects.requireNonNull(context, "context must not be null");
if (draft == null) {
return EvidenceGuardResult.invalid(List.of(
violation(EvidenceViolationCode.DRAFT_MISSING, "draft")));
}
if (draft.conclusion() != null) {
throw new IllegalArgumentException("no-conclusion validation requires conclusion=null");
}
List<EvidenceViolation> violations = new ArrayList<>();
for (int index = 0; index < draft.analysis().size(); index++) {
DiagnosisDraft.AnalysisItem analysis = draft.analysis().get(index);
if (analysis == null || analysis.toolCallIds().isEmpty()) {
continue;
}
if (analysis.kind() == null) {
violations.add(violation(
EvidenceViolationCode.ANALYSIS_KIND_MISSING,
"analysis[" + index + "].kind"));
continue;
}
List<VerifiedEvidence> ignoredEvidence = new ArrayList<>();
for (String toolCallId : analysis.toolCallIds()) {
verifyInvocation(
context, analysis, index, toolCallId, ignoredEvidence, violations);
}
}
return violations.isEmpty()
? EvidenceGuardResult.valid(VerifiedEvidenceSnapshot.empty())
: EvidenceGuardResult.invalid(violations);
}
private List<EvidenceViolation> validateDraft(DiagnosisDraft draft) {
List<EvidenceViolation> violations = new ArrayList<>();
if (draft == null) {
@@ -4,6 +4,9 @@ import com.superbiz.agent.harness.core.BudgetExceededException;
import com.superbiz.agent.harness.core.DiagnosisHarnessCore;
import com.superbiz.agent.harness.core.RunAbortedException;
import com.superbiz.agent.harness.core.RunContext;
import com.superbiz.agent.harness.audit.ModelCallAuditor;
import com.superbiz.agent.harness.audit.ModelCallComponent;
import com.superbiz.agent.harness.audit.ModelCallLedger;
import com.superbiz.agent.harness.retry.RetryFailure;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.metadata.Usage;
@@ -26,15 +29,23 @@ public final class GuardModelCall {
private final DiagnosisHarnessCore core;
private final ChatModel chatModel;
private final ExecutorService executor;
private final ModelCallAuditor auditor;
public GuardModelCall(DiagnosisHarnessCore core, ChatModel chatModel,
ExecutorService executor) {
this(core, chatModel, executor, new ModelCallAuditor(core));
}
public GuardModelCall(DiagnosisHarnessCore core, ChatModel chatModel,
ExecutorService executor, ModelCallAuditor auditor) {
this.core = Objects.requireNonNull(core, "core must not be null");
this.chatModel = Objects.requireNonNull(chatModel, "chatModel must not be null");
this.executor = Objects.requireNonNull(executor, "executor must not be null");
this.auditor = Objects.requireNonNull(auditor, "auditor must not be null");
}
public String call(RunContext context, Prompt prompt, Duration timeout, long maxOutputBytes) {
public String call(RunContext context, ModelCallComponent component,
Prompt prompt, Duration timeout, long maxOutputBytes) {
Objects.requireNonNull(context, "context must not be null");
Objects.requireNonNull(prompt, "prompt must not be null");
Objects.requireNonNull(timeout, "timeout must not be null");
@@ -42,20 +53,24 @@ public final class GuardModelCall {
throw new IllegalArgumentException("timeout and output limit must be positive");
}
core.beforeModelCall(context);
Future<String> future = executor.submit(() -> invoke(context, prompt, maxOutputBytes));
ModelCallLedger.Call call = auditor.begin(context, component);
Future<String> future = executor.submit(() -> invoke(context, call, prompt, maxOutputBytes));
context.cancellation().onCancel(ignored -> future.cancel(true));
try {
return future.get(timeout.toNanos(), TimeUnit.NANOSECONDS);
} catch (TimeoutException exception) {
future.cancel(true);
auditor.recordUsage(context, call, 0, 0, false);
throw new GuardModelCallException(
RetryFailure.TIMEOUT, "Guard model attempt timed out", exception);
} catch (CancellationException exception) {
auditor.recordUsage(context, call, 0, 0, false);
core.checkActive(context);
throw new GuardModelCallException(
RetryFailure.TRANSPORT, "Guard model attempt was cancelled", exception);
} catch (InterruptedException exception) {
future.cancel(true);
auditor.recordUsage(context, call, 0, 0, false);
Thread.currentThread().interrupt();
core.checkActive(context);
throw new GuardModelCallException(
@@ -76,15 +91,17 @@ public final class GuardModelCall {
}
}
private String invoke(RunContext context, Prompt prompt, long maxOutputBytes) {
private String invoke(RunContext context, ModelCallLedger.Call call,
Prompt prompt, long maxOutputBytes) {
ChatResponse response;
try {
response = chatModel.call(prompt);
} catch (RuntimeException exception) {
auditor.recordUsage(context, call, 0, 0, false);
throw new GuardModelCallException(
RetryFailure.TRANSPORT, "Guard model transport failed", exception);
}
recordUsage(context, response);
recordUsage(context, call, response);
core.checkActive(context);
AssistantMessage output = response == null || response.getResult() == null
? null : response.getResult().getOutput();
@@ -102,16 +119,18 @@ public final class GuardModelCall {
return output.getText();
}
private void recordUsage(RunContext context, ChatResponse response) {
private void recordUsage(RunContext context, ModelCallLedger.Call call, ChatResponse response) {
if (response == null || response.getMetadata() == null) {
auditor.recordUsage(context, call, 0, 0, false);
return;
}
Usage usage = response.getMetadata().getUsage();
if (usage == null) {
auditor.recordUsage(context, call, 0, 0, false);
return;
}
core.recordTokens(context, nonNegative(usage.getPromptTokens()),
nonNegative(usage.getCompletionTokens()));
auditor.recordUsage(context, call, nonNegative(usage.getPromptTokens()),
nonNegative(usage.getCompletionTokens()), true);
}
private static long nonNegative(Integer value) {
@@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.superbiz.agent.harness.contract.SemanticVerdict;
import com.superbiz.agent.harness.audit.DiagnosisTraceRecorder;
import com.superbiz.agent.harness.audit.TraceAuditEvents;
import com.superbiz.agent.harness.audit.ModelCallComponent;
import com.superbiz.agent.harness.core.DiagnosisHarnessCore;
import com.superbiz.agent.harness.core.RunContext;
import com.superbiz.agent.harness.retry.HarnessRetryExecutor;
@@ -81,7 +82,8 @@ public final class SemanticGuard {
context,
context.retryPolicies().semanticGuard(),
() -> parse(modelCall.call(
context, modelPrompt, remainingTimeout(startedNanos), limits.maxOutputBytes())),
context, ModelCallComponent.SEMANTIC_GUARD,
modelPrompt, remainingTimeout(startedNanos), limits.maxOutputBytes())),
this::classify,
attempt -> {
attemptRecorder.accept(attempt);