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
@@ -1,5 +1,6 @@
package com.superbiz.agent.harness.release;
import com.superbiz.agent.harness.agent.DiagnosisAgentExecution;
import com.superbiz.agent.harness.contract.DiagnosisDraft;
import com.superbiz.agent.harness.audit.DiagnosisTraceRecorder;
import com.superbiz.agent.harness.audit.TraceAuditEvents;
@@ -15,10 +16,13 @@ import com.superbiz.agent.harness.guard.evidence.VerifiedEvidenceSnapshot;
import com.superbiz.agent.harness.guard.semantic.SemanticGuard;
import com.superbiz.agent.harness.guard.semantic.SemanticGuardDecision;
import com.superbiz.agent.harness.guard.semantic.SemanticGuardInput;
import com.superbiz.agent.harness.progress.DiagnosisProgressSnapshot;
import com.superbiz.agent.harness.progress.DiagnosisStopReason;
import com.superbiz.agent.harness.retry.RetryExecutionException;
import com.superbiz.agent.harness.retry.RetryFailure;
import java.util.Objects;
import java.util.List;
public final class DiagnosisReleaseUseCase {
@@ -50,12 +54,44 @@ public final class DiagnosisReleaseUseCase {
}
public DiagnosisReleaseResult execute(RunContext context, String query, DiagnosisDraft draft) {
return execute(context, query, DiagnosisAgentExecution.completed(
Objects.requireNonNull(draft, "draft must not be null"),
DiagnosisProgressSnapshot.empty()));
}
public DiagnosisReleaseResult execute(
RunContext context, String query, DiagnosisAgentExecution execution) {
Objects.requireNonNull(context, "context must not be null");
if (query == null || query.isBlank()) {
throw new IllegalArgumentException("query must not be blank");
}
Objects.requireNonNull(draft, "draft must not be null");
Objects.requireNonNull(execution, "execution must not be null");
DiagnosisDraft draft = execution.draft();
if (draft == null) {
return releaseControlledStop(context, execution.progress(), execution.stopReason());
}
if (draft.conclusion() == null) {
return releaseNoConclusion(context, draft, execution.progress());
}
return releaseConclusion(context, query, draft);
}
public DiagnosisReleaseResult releaseInvalidDraft(
RunContext context, DiagnosisProgressSnapshot progress) {
Objects.requireNonNull(context, "context must not be null");
Objects.requireNonNull(progress, "progress must not be null");
if (!progress.hasObservedFacts()) {
throw new IllegalStateException(
"Invalid Diagnosis Draft has no verified publishable progress");
}
return progressFallback(context,
fallbackFactory.insufficientEvidence(progress, List.of()),
FallbackType.INSUFFICIENT_EVIDENCE);
}
private DiagnosisReleaseResult releaseConclusion(
RunContext context, String query, DiagnosisDraft draft) {
DiagnosisDraft candidate = draft;
EvidenceGuardResult evidence = evidenceGuard.validate(context, candidate);
traceRecorder.record(TraceAuditEvents.evidenceValidation(
@@ -101,6 +137,61 @@ public final class DiagnosisReleaseUseCase {
return DiagnosisReleaseResult.fallback(fallbackFactory.semanticUnsupported(snapshot));
}
private DiagnosisReleaseResult releaseNoConclusion(
RunContext context, DiagnosisDraft draft, DiagnosisProgressSnapshot progress) {
EvidenceGuardResult evidence = evidenceGuard.validateNoConclusionReferences(context, draft);
traceRecorder.record(TraceAuditEvents.evidenceValidation(
context, TraceEventType.EVIDENCE_GUARD_INITIAL, evidence, draft));
if (!evidence.valid()) {
return evidenceFailure(context, evidence);
}
List<String> missingInfo = missingInfo(draft);
if (progress.hasObservedFacts()) {
return progressFallback(context,
fallbackFactory.insufficientEvidence(progress, missingInfo),
FallbackType.INSUFFICIENT_EVIDENCE);
}
if (!missingInfo.isEmpty()) {
return progressFallback(context,
fallbackFactory.missingRequiredContext(missingInfo),
FallbackType.MISSING_REQUIRED_CONTEXT);
}
throw new IllegalStateException(
"No-conclusion Diagnosis has neither verified progress nor missing context");
}
private DiagnosisReleaseResult releaseControlledStop(
RunContext context,
DiagnosisProgressSnapshot progress,
DiagnosisStopReason stopReason) {
if (stopReason != DiagnosisStopReason.INFORMATION_SATURATED
&& stopReason != DiagnosisStopReason.BUDGET_LIMIT_REACHED) {
throw new IllegalStateException("Unsupported Diagnosis stop reason");
}
if (!progress.hasObservedFacts()) {
throw new IllegalStateException(
"Controlled Diagnosis stop has no verified publishable progress");
}
return progressFallback(context,
fallbackFactory.insufficientEvidence(progress, List.of()),
FallbackType.INSUFFICIENT_EVIDENCE);
}
private DiagnosisReleaseResult progressFallback(
RunContext context,
com.superbiz.agent.harness.contract.SafeFallback fallback,
FallbackType type) {
traceRecorder.record(TraceAuditEvents.releaseDecision(
context, com.superbiz.agent.harness.contract.ReleaseOutcome.FALLBACK, type));
return DiagnosisReleaseResult.fallback(fallback);
}
private List<String> missingInfo(DiagnosisDraft draft) {
return draft.limitations() == null
? List.of() : draft.limitations().missingInfo();
}
private DiagnosisReleaseResult evidenceFailure(
RunContext context, EvidenceGuardResult evidence) {
traceRecorder.record(TraceAuditEvents.releaseDecision(
@@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.ObjectReader;
import com.superbiz.agent.harness.contract.DiagnosisDraft;
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.guard.evidence.EvidenceViolation;
@@ -92,7 +93,8 @@ public final class EvidenceRepair {
context.retryPolicies().evidenceRepair(),
() -> {
DiagnosisDraft repaired = parse(modelCall.call(
context, modelPrompt, limits.timeout(), limits.maxOutputBytes()));
context, ModelCallComponent.EVIDENCE_REPAIR,
modelPrompt, limits.timeout(), limits.maxOutputBytes()));
if (!originalSemantics.hasSameUserVisibleSemantics(
SemanticDraftView.from(repaired))) {
throw new GuardModelCallException(
@@ -5,6 +5,7 @@ import com.superbiz.agent.harness.contract.SafeFallback;
import com.superbiz.agent.harness.guard.evidence.EvidenceViolation;
import com.superbiz.agent.harness.guard.evidence.VerifiedEvidence;
import com.superbiz.agent.harness.guard.evidence.VerifiedEvidenceSnapshot;
import com.superbiz.agent.harness.progress.DiagnosisProgressSnapshot;
import java.util.ArrayList;
import java.util.LinkedHashMap;
@@ -53,6 +54,47 @@ public final class SafeFallbackFactory {
List.of());
}
public SafeFallback insufficientEvidence(
DiagnosisProgressSnapshot progress, List<String> missingInfo) {
Objects.requireNonNull(progress, "progress must not be null");
if (!progress.hasObservedFacts()) {
throw new IllegalArgumentException(
"insufficient-evidence fallback requires verified progress");
}
List<String> safeMissingInfo = boundedMissingInfo(missingInfo);
List<String> limitations = new ArrayList<>(progress.limitations());
limitations.add("当前已检查范围不足以支持根因结论");
safeMissingInfo.forEach(item -> limitations.add("仍缺少:" + item));
return fallback(
FallbackType.INSUFFICIENT_EVIDENCE,
progress.verifiedSources(),
"已完成有限范围的检查,但现有证据不足以确认根因",
List.copyOf(limitations),
List.of(safeMissingInfo.isEmpty()
? "补充故障对象、发生时间、错误信息或新的可查询范围后重试"
: "补充缺失信息后,在新的明确范围内继续诊断"),
"DIAGNOSIS_COLLECTION",
progress.observedFacts(),
List.of());
}
public SafeFallback missingRequiredContext(List<String> missingInfo) {
List<String> safeMissingInfo = boundedMissingInfo(missingInfo);
if (safeMissingInfo.isEmpty()) {
throw new IllegalArgumentException(
"missing-context fallback requires missing information");
}
return fallback(
FallbackType.MISSING_REQUIRED_CONTEXT,
List.of(),
"当前缺少执行定向诊断所需的信息,暂时无法开始有效查询或确认根因",
safeMissingInfo.stream().map(item -> "缺少:" + item).toList(),
List.of("补充上述故障上下文后重试"),
"DIAGNOSIS_INPUT",
List.of(),
List.of());
}
private SafeFallback fallback(FallbackType type,
List<SafeFallback.VerifiedSource> sources,
String message,
@@ -100,6 +142,20 @@ public final class SafeFallbackFactory {
return List.copyOf(result);
}
private List<String> boundedMissingInfo(List<String> missingInfo) {
List<String> result = new ArrayList<>();
for (String item : missingInfo == null ? List.<String>of() : missingInfo) {
String safe = bounded(item);
if (safe != null && !result.contains(safe)) {
result.add(safe);
}
if (result.size() >= 8) {
break;
}
}
return List.copyOf(result);
}
private String bounded(String value) {
if (value == null || value.isBlank()) {
return null;