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
@@ -107,8 +107,7 @@ public final class ChatApplicationUseCase {
PathResult path = executePath(
intent, context, request.query(), previousTurn.orElse(null), observer);
core.checkActive(context);
core.completeSuccess(context);
completePath(context, intent, path);
String safeJson = write(path.content());
persistFinish(context, intent, path.outcome(), safeJson,
path.publishedResult(), durationMillis(startedNanos));
@@ -131,6 +130,20 @@ public final class ChatApplicationUseCase {
}
}
private void completePath(RunContext context, IntentType intent, PathResult path) {
if (path.handledBudgetTermination()) {
if (intent != IntentType.DIAGNOSIS
|| path.outcome() != ReleaseOutcome.FALLBACK
|| path.publishedResult() != null
|| context.lifecycle().state() != RunState.BUDGET_EXHAUSTED) {
throw new IllegalStateException("Invalid handled Diagnosis budget fallback");
}
return;
}
core.checkActive(context);
core.completeSuccess(context);
}
private PathResult executePath(IntentType intent,
RunContext context,
String query,
@@ -140,18 +153,19 @@ public final class ChatApplicationUseCase {
case SYSTEM_CHAT -> {
observer.onStatus(ChatApplicationStatus.SYSTEM_RESPONDING);
yield new PathResult(
ReleaseOutcome.SUCCESS, systemChat.execute(context, query), null);
ReleaseOutcome.SUCCESS, systemChat.execute(context, query), null, false);
}
case KNOWLEDGE_QUERY -> {
observer.onStatus(ChatApplicationStatus.KNOWLEDGE_SEARCHING);
observer.onStatus(ChatApplicationStatus.KNOWLEDGE_ANSWERING);
yield new PathResult(
ReleaseOutcome.SUCCESS, knowledgeQuery.execute(context, query), null);
ReleaseOutcome.SUCCESS, knowledgeQuery.execute(context, query), null, false);
}
case DIAGNOSIS -> {
DiagnosisExecutionResult result = diagnosis.execute(
context, query, previousTurn, observer::onStatus);
yield new PathResult(result.outcome(), result.content(), result.publishedResult());
yield new PathResult(result.outcome(), result.content(), result.publishedResult(),
result.handledBudgetTermination());
}
};
}
@@ -251,7 +265,8 @@ public final class ChatApplicationUseCase {
private record PathResult(
ReleaseOutcome outcome,
ChatApplicationContent content,
PublishedResult publishedResult) {
PublishedResult publishedResult,
boolean handledBudgetTermination) {
}
private static final class CoreRunControl implements ChatRunControl {
@@ -8,7 +8,15 @@ import java.util.Objects;
public record DiagnosisExecutionResult(
ReleaseOutcome outcome,
ChatApplicationContent content,
PublishedResult publishedResult) {
PublishedResult publishedResult,
boolean handledBudgetTermination) {
public DiagnosisExecutionResult(
ReleaseOutcome outcome,
ChatApplicationContent content,
PublishedResult publishedResult) {
this(outcome, content, publishedResult, false);
}
public DiagnosisExecutionResult {
Objects.requireNonNull(outcome, "outcome must not be null");
@@ -25,5 +33,9 @@ public record DiagnosisExecutionResult(
if (outcome != ReleaseOutcome.SUCCESS && publishedResult != null) {
throw new IllegalArgumentException("only success can contain PublishedResult");
}
if (handledBudgetTermination && outcome != ReleaseOutcome.FALLBACK) {
throw new IllegalArgumentException(
"handled budget termination requires fallback release");
}
}
}
@@ -1,6 +1,8 @@
package com.superbiz.agent.harness.application.executor;
import com.superbiz.agent.harness.agent.DiagnosisAgentInput;
import com.superbiz.agent.harness.agent.DiagnosisAgentExecution;
import com.superbiz.agent.harness.agent.DiagnosisAgentOutputException;
import com.superbiz.agent.harness.agent.DiagnosisAgentUseCase;
import com.superbiz.agent.harness.application.ChatApplicationStatus;
import com.superbiz.agent.harness.application.DiagnosisContent;
@@ -8,7 +10,8 @@ import com.superbiz.agent.harness.application.DiagnosisExecutionResult;
import com.superbiz.agent.harness.application.DiagnosisOperation;
import com.superbiz.agent.harness.application.FallbackContent;
import com.superbiz.agent.harness.application.persistence.PublishedResultPolicy;
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.contract.PreviousTurn;
import com.superbiz.agent.harness.contract.PublishedResult;
import com.superbiz.agent.harness.contract.ReleaseOutcome;
@@ -25,13 +28,22 @@ public final class DiagnosisChatExecutor implements DiagnosisOperation {
private final DiagnosisAgentUseCase diagnosisAgent;
private final DiagnosisReleaseUseCase releaseUseCase;
private final PublishedResultPolicy publishedPolicy;
private final DiagnosisTraceRecorder traceRecorder;
public DiagnosisChatExecutor(DiagnosisAgentUseCase diagnosisAgent,
DiagnosisReleaseUseCase releaseUseCase,
PublishedResultPolicy publishedPolicy) {
this(diagnosisAgent, releaseUseCase, publishedPolicy, DiagnosisTraceRecorder.noop());
}
public DiagnosisChatExecutor(DiagnosisAgentUseCase diagnosisAgent,
DiagnosisReleaseUseCase releaseUseCase,
PublishedResultPolicy publishedPolicy,
DiagnosisTraceRecorder traceRecorder) {
this.diagnosisAgent = Objects.requireNonNull(diagnosisAgent, "diagnosisAgent must not be null");
this.releaseUseCase = Objects.requireNonNull(releaseUseCase, "releaseUseCase must not be null");
this.publishedPolicy = Objects.requireNonNull(publishedPolicy, "publishedPolicy must not be null");
this.traceRecorder = Objects.requireNonNull(traceRecorder, "traceRecorder must not be null");
}
@Override
@@ -41,15 +53,22 @@ public final class DiagnosisChatExecutor implements DiagnosisOperation {
Consumer<ChatApplicationStatus> statusSink) {
Objects.requireNonNull(statusSink, "statusSink must not be null");
statusSink.accept(ChatApplicationStatus.DIAGNOSIS_RUNNING);
DiagnosisDraft draft = diagnosisAgent.execute(
context, new DiagnosisAgentInput(query, previousTurn));
DiagnosisAgentExecution execution;
try {
execution = diagnosisAgent.execute(
context, new DiagnosisAgentInput(query, previousTurn));
} catch (DiagnosisAgentOutputException exception) {
return recoverInvalidDraft(context, exception, statusSink);
}
statusSink.accept(ChatApplicationStatus.SAFETY_VALIDATING);
DiagnosisReleaseResult released = releaseUseCase.execute(context, query, draft);
DiagnosisReleaseResult released = releaseUseCase.execute(context, query, execution);
if (released.outcome() == ReleaseOutcome.FALLBACK) {
return new DiagnosisExecutionResult(
ReleaseOutcome.FALLBACK,
new FallbackContent(released.fallback()),
null);
null,
execution.stopReason()
== com.superbiz.agent.harness.progress.DiagnosisStopReason.BUDGET_LIMIT_REACHED);
}
PublishedResult published = publishedPolicy.create(
query, released.draft(), released.verifiedEvidence()).orElse(null);
@@ -60,4 +79,26 @@ public final class DiagnosisChatExecutor implements DiagnosisOperation {
released.verifiedEvidence().verifiedSources()),
published);
}
private DiagnosisExecutionResult recoverInvalidDraft(
RunContext context,
DiagnosisAgentOutputException exception,
Consumer<ChatApplicationStatus> statusSink) {
if (!exception.isDraftContractFailure()) {
throw exception;
}
boolean hasProgress = exception.progress().hasObservedFacts();
traceRecorder.record(TraceAuditEvents.agentDraftInvalid(
context, exception.kind(), exception.outputBytes(), hasProgress));
if (!hasProgress) {
throw exception;
}
statusSink.accept(ChatApplicationStatus.SAFETY_VALIDATING);
DiagnosisReleaseResult released = releaseUseCase.releaseInvalidDraft(
context, exception.progress());
return new DiagnosisExecutionResult(
ReleaseOutcome.FALLBACK,
new FallbackContent(released.fallback()),
null);
}
}
@@ -16,6 +16,7 @@ import com.superbiz.agent.harness.contract.SourceDocument;
import com.superbiz.agent.harness.core.DiagnosisHarnessCore;
import com.superbiz.agent.harness.core.RunContext;
import com.superbiz.agent.harness.guard.semantic.GuardModelCall;
import com.superbiz.agent.harness.audit.ModelCallComponent;
import com.superbiz.agent.harness.tool.boundary.ToolBoundaryResult;
import com.superbiz.agent.harness.tool.contract.AgentToolContracts;
import com.superbiz.agent.harness.tool.contract.RagEvidence;
@@ -106,7 +107,7 @@ public final class KnowledgeQueryExecutor implements KnowledgeQueryOperation {
}
core.reserveRunBytes(context, bytes);
String output = modelCall.call(
context,
context, ModelCallComponent.KNOWLEDGE_ANSWER,
new Prompt(List.of(new SystemMessage(prompt), new UserMessage(modelInput))),
limits.modelTimeout(),
limits.maxModelOutputBytes());
@@ -7,6 +7,7 @@ import com.superbiz.agent.harness.application.SystemChatOperation;
import com.superbiz.agent.harness.core.DiagnosisHarnessCore;
import com.superbiz.agent.harness.core.RunContext;
import com.superbiz.agent.harness.guard.semantic.GuardModelCall;
import com.superbiz.agent.harness.audit.ModelCallComponent;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
@@ -47,7 +48,7 @@ public final class SystemChatExecutor implements SystemChatOperation {
}
core.reserveRunBytes(context, bytes);
String answer = modelCall.call(
context,
context, ModelCallComponent.SYSTEM_CHAT,
new Prompt(List.of(new SystemMessage(SYSTEM_PROMPT), new UserMessage(query))),
limits.timeout(),
limits.maxOutputBytes());
@@ -12,6 +12,7 @@ import com.superbiz.agent.harness.contract.PublishedResult;
import com.superbiz.agent.harness.contract.ReleaseOutcome;
import com.superbiz.agent.harness.core.RunBudgetUsage;
import com.superbiz.agent.harness.core.RunContext;
import com.superbiz.agent.harness.audit.ModelCallComponent;
import com.superbiz.agent.repository.ChatSessionRepository;
import com.superbiz.agent.repository.DiagnosisRunRepository;
import org.springframework.stereotype.Component;
@@ -119,6 +120,8 @@ public class JpaChatRunStore implements ChatRunStore {
run.setTotalDurationMs(Math.max(0, durationMs));
RunBudgetUsage usage = context.budget().snapshot();
run.setTotalTokenCount(saturatingInt(usage.totalTokens()));
run.setStepCount(context.modelCalls().componentCallCount(
ModelCallComponent.DIAGNOSIS_AGENT));
run.setToolCallCount(saturatingInt(usage.toolCalls()));
runs.save(run);
if (outcome == ReleaseOutcome.SUCCESS || outcome == ReleaseOutcome.FALLBACK) {
@@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.superbiz.agent.harness.contract.IntentType;
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.application.IntentRouting;
import com.superbiz.agent.harness.core.DiagnosisHarnessCore;
import com.superbiz.agent.harness.core.RunContext;
@@ -86,7 +87,8 @@ public final class IntentRouter implements IntentRouting {
context,
context.retryPolicies().intentRouter(),
() -> parse(modelCall.call(
context, prompt, remaining(started), limits.maxOutputBytes())),
context, ModelCallComponent.INTENT_ROUTER,
prompt, remaining(started), limits.maxOutputBytes())),
this::classify,
attempt -> {
attemptRecorder.accept(attempt);