feat(harness): complete protocol repair stop and archive ISS-016

Add repairable INVALID_PROGRESS_PROTOCOL observations, independent
PROGRESS_PROTOCOL_VIOLATED saturation, and controlled release paths.
Archive the OpenSpec change after syncing main specs and devflow.
This commit is contained in:
zhuyongxin
2026-07-27 19:10:07 +08:00
parent 5c369f3b6c
commit 38f781b157
44 changed files with 977 additions and 80 deletions
@@ -4,6 +4,8 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.superbiz.agent.harness.core.RunContext;
import com.superbiz.agent.harness.progress.ProgressProtocolViolationException;
import com.superbiz.agent.harness.progress.ProgressProtocolViolationType;
import com.superbiz.agent.harness.tool.adapter.MysqlToolAdapter;
import com.superbiz.agent.harness.tool.adapter.QueryLogsToolAdapter;
import com.superbiz.agent.harness.tool.adapter.RagToolAdapter;
@@ -102,10 +104,17 @@ public final class HarnessEvidenceTools {
}
default -> throw new IllegalArgumentException("Unsupported evidence Tool: " + toolName);
};
} catch (ProgressProtocolViolationException exception) {
throw exception;
} catch (IllegalArgumentException exception) {
throw exception;
} catch (Exception exception) {
throw new IllegalArgumentException("Tool Call Envelope is invalid", exception);
throw new ProgressProtocolViolationException(
ProgressProtocolViolationType.INVALID_ENVELOPE,
"Tool Call Envelope is invalid",
null,
null,
exception);
}
}
@@ -119,7 +128,11 @@ public final class HarnessEvidenceTools {
private static void requireInput(Object input) {
if (input == null) {
throw new IllegalArgumentException("Tool Call Envelope input is required");
throw new ProgressProtocolViolationException(
ProgressProtocolViolationType.MISSING_INPUT,
"Tool Call Envelope input is required",
"input",
null);
}
}
@@ -15,10 +15,13 @@ import com.superbiz.agent.harness.progress.DiagnosisCollectionState;
import com.superbiz.agent.harness.progress.DiagnosisProgressSnapshotState;
import com.superbiz.agent.harness.progress.DiagnosisStopReason;
import com.superbiz.agent.harness.progress.InformationGain;
import com.superbiz.agent.harness.progress.ProgressProtocolViolationException;
import com.superbiz.agent.harness.progress.ProgressProtocolViolationType;
import com.superbiz.agent.harness.progress.ToolScopeNormalizer;
import com.superbiz.agent.harness.tool.boundary.ToolBoundaryResult;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -81,9 +84,13 @@ public final class HarnessToolInterceptor extends ToolInterceptor {
return stopRequired(request, evaluated.stopReason());
}
normalizedScope = scopeNormalizer.normalize(request.getToolName(), call.businessInput());
} catch (ProgressProtocolViolationException exception) {
return handleProgressProtocolViolation(request, exception);
} catch (IllegalArgumentException | IllegalStateException exception) {
recordRejection(request, "INVALID_PROGRESS_PROTOCOL");
return safeError(request, "INVALID_PROGRESS_PROTOCOL");
return handleProgressProtocolViolation(request,
new ProgressProtocolViolationException(
ProgressProtocolViolationType.INVALID_ENVELOPE,
"Tool Call Envelope is invalid", null, null, exception));
}
if (context.progress().isDuplicate(request.getToolName(), normalizedScope)) {
@@ -167,6 +174,53 @@ public final class HarnessToolInterceptor extends ToolInterceptor {
request.getToolCallId(), request.getToolName(), writeObservation(observation));
}
private ToolCallResponse handleProgressProtocolViolation(
ToolCallRequest request,
ProgressProtocolViolationException exception) {
DiagnosisProgressSnapshotState state =
context.progress().recordProgressProtocolViolation();
boolean stopRequired = state.collectionState() == DiagnosisCollectionState.SATURATED;
recordRejection(request, "INVALID_PROGRESS_PROTOCOL", exception.violationType(),
!stopRequired, state);
if (stopRequired) {
return stopRequired(request, state.stopReason());
}
return repairableProtocolError(request, exception, state);
}
private ToolCallResponse repairableProtocolError(
ToolCallRequest request,
ProgressProtocolViolationException exception,
DiagnosisProgressSnapshotState state) {
Map<String, Object> observation = new LinkedHashMap<>();
observation.put("evidence_status", "ERROR");
observation.put("tool_call_id", request.getToolCallId());
observation.put("error_code", "INVALID_PROGRESS_PROTOCOL");
observation.put("repair_required", true);
observation.put("violation_type", exception.violationType().name());
if (exception.missingField() != null && !exception.missingField().isBlank()) {
observation.put("missing_field", exception.missingField());
}
if (exception.expectedPreviousToolCallId() != null
&& !exception.expectedPreviousToolCallId().isBlank()) {
observation.put("expected_previous_tool_call_id",
exception.expectedPreviousToolCallId());
}
observation.put("allowed_information_gain", List.of(
InformationGain.GAINED.name(), InformationGain.NO_GAIN.name()));
observation.put("instruction",
"Before calling another tool, evaluate the previous observation.");
observation.put("consecutive_protocol_violations",
state.consecutiveProgressProtocolViolations());
return ToolCallResponse.builder()
.toolCallId(request.getToolCallId())
.toolName(request.getToolName())
.content(writeObservation(observation))
.status("error")
.metadata(Map.of("error", true))
.build();
}
private ToolCallResponse safeError(ToolCallRequest request, String errorCode) {
Map<String, Object> observation = new LinkedHashMap<>();
observation.put("evidence_status", "ERROR");
@@ -182,8 +236,18 @@ public final class HarnessToolInterceptor extends ToolInterceptor {
}
private void recordRejection(ToolCallRequest request, String errorCode) {
recordRejection(request, errorCode, null, false, context.progress().snapshot());
}
private void recordRejection(
ToolCallRequest request,
String errorCode,
ProgressProtocolViolationType violationType,
boolean repairPromptDelivered,
DiagnosisProgressSnapshotState state) {
traceRecorder.record(TraceAuditEvents.toolRequestRejected(
context, request.getToolCallId(), request.getToolName(), errorCode));
context, request.getToolCallId(), request.getToolName(), errorCode,
violationType, repairPromptDelivered, state));
}
private String writeObservation(Map<String, Object> observation) {
@@ -11,6 +11,7 @@ import com.superbiz.agent.harness.guard.evidence.EvidenceGuardResult;
import com.superbiz.agent.harness.guard.evidence.EvidenceViolation;
import com.superbiz.agent.harness.progress.DiagnosisProgressSnapshotState;
import com.superbiz.agent.harness.progress.InformationGain;
import com.superbiz.agent.harness.progress.ProgressProtocolViolationType;
import com.superbiz.agent.harness.retry.RetryAttempt;
import java.util.ArrayList;
@@ -140,10 +141,33 @@ public final class TraceAuditEvents {
public static DiagnosisTraceAuditEvent toolRequestRejected(
RunContext context, String toolCallId, String toolName, String errorCode) {
return toolRequestRejected(context, toolCallId, toolName, errorCode,
null, false, null);
}
public static DiagnosisTraceAuditEvent toolRequestRejected(
RunContext context,
String toolCallId,
String toolName,
String errorCode,
ProgressProtocolViolationType violationType,
boolean repairPromptDelivered,
DiagnosisProgressSnapshotState state) {
Map<String, Object> details = new LinkedHashMap<>();
details.put("tool_call_id", safeIdentifier(toolCallId));
details.put("tool_name", safeIdentifier(toolName));
details.put("error_code", safeIdentifier(errorCode));
if (violationType != null) {
details.put("violation_type", violationType.name());
details.put("repair_prompt_delivered", repairPromptDelivered);
}
if (state != null && state.consecutiveProgressProtocolViolations() > 0) {
details.put("consecutive_protocol_violations",
state.consecutiveProgressProtocolViolations());
}
if (state != null && state.stopReason() != null) {
details.put("stop_reason", state.stopReason().name());
}
return event(context, TracePhase.TOOL, TraceEventType.TOOL_REQUEST_REJECTED,
TraceEventStatus.REJECTED, null, null, details);
}
@@ -18,6 +18,7 @@ public final class DiagnosisHarnessCore {
private final RunBudgetLimits budgetLimits;
private final HarnessRetryPolicies retryPolicies;
private final int stopAfterConsecutiveNoGain;
private final int stopAfterConsecutiveProgressProtocolViolations;
public DiagnosisHarnessCore(Clock clock,
Supplier<String> runIdSupplier,
@@ -33,6 +34,17 @@ public final class DiagnosisHarnessCore {
RunBudgetLimits budgetLimits,
HarnessRetryPolicies retryPolicies,
int stopAfterConsecutiveNoGain) {
this(clock, runIdSupplier, maxRunDuration, budgetLimits, retryPolicies,
stopAfterConsecutiveNoGain, 2);
}
public DiagnosisHarnessCore(Clock clock,
Supplier<String> runIdSupplier,
Duration maxRunDuration,
RunBudgetLimits budgetLimits,
HarnessRetryPolicies retryPolicies,
int stopAfterConsecutiveNoGain,
int stopAfterConsecutiveProgressProtocolViolations) {
this.clock = Objects.requireNonNull(clock, "clock must not be null");
this.runIdSupplier = Objects.requireNonNull(runIdSupplier, "runIdSupplier must not be null");
this.maxRunDuration = requirePositive(maxRunDuration, "maxRunDuration");
@@ -41,7 +53,13 @@ public final class DiagnosisHarnessCore {
if (stopAfterConsecutiveNoGain <= 0) {
throw new IllegalArgumentException("stopAfterConsecutiveNoGain must be positive");
}
if (stopAfterConsecutiveProgressProtocolViolations <= 0) {
throw new IllegalArgumentException(
"stopAfterConsecutiveProgressProtocolViolations must be positive");
}
this.stopAfterConsecutiveNoGain = stopAfterConsecutiveNoGain;
this.stopAfterConsecutiveProgressProtocolViolations =
stopAfterConsecutiveProgressProtocolViolations;
}
public RunContext startRun(String sessionId) {
@@ -61,7 +79,8 @@ public final class DiagnosisHarnessCore {
new ModelCallLedger(),
retryPolicies,
lifecycle,
new DiagnosisProgressTracker(stopAfterConsecutiveNoGain));
new DiagnosisProgressTracker(stopAfterConsecutiveNoGain,
stopAfterConsecutiveProgressProtocolViolations));
cancellation.onCancel(reason -> lifecycle.finish(terminalState(reason), reason.name()));
return context;
}
@@ -4,6 +4,7 @@ import java.util.List;
public record DiagnosisProgressSnapshotState(
int consecutiveNoGain,
int consecutiveProgressProtocolViolations,
DiagnosisCollectionState collectionState,
DiagnosisStopReason stopReason,
String pendingToolCallId,
@@ -10,35 +10,63 @@ import java.util.Set;
public final class DiagnosisProgressTracker {
private final int stopAfterConsecutiveNoGain;
private final int stopAfterConsecutiveProgressProtocolViolations;
private final Set<ToolScopeIdentity> completedScopes = new LinkedHashSet<>();
private final List<CompletedToolCall> completedToolCalls = new ArrayList<>();
private int consecutiveNoGain;
private int consecutiveProgressProtocolViolations;
private DiagnosisCollectionState collectionState = DiagnosisCollectionState.COLLECTING;
private DiagnosisStopReason stopReason;
private String pendingToolCallId;
private boolean stopInstructionDelivered;
public DiagnosisProgressTracker(int stopAfterConsecutiveNoGain) {
this(stopAfterConsecutiveNoGain, 2);
}
public DiagnosisProgressTracker(
int stopAfterConsecutiveNoGain,
int stopAfterConsecutiveProgressProtocolViolations) {
if (stopAfterConsecutiveNoGain <= 0) {
throw new IllegalArgumentException("stopAfterConsecutiveNoGain must be positive");
}
if (stopAfterConsecutiveProgressProtocolViolations <= 0) {
throw new IllegalArgumentException(
"stopAfterConsecutiveProgressProtocolViolations must be positive");
}
this.stopAfterConsecutiveNoGain = stopAfterConsecutiveNoGain;
this.stopAfterConsecutiveProgressProtocolViolations =
stopAfterConsecutiveProgressProtocolViolations;
}
public synchronized void applyPreviousObservation(PreviousObservation observation) {
if (pendingToolCallId == null) {
if (observation != null) {
throw new IllegalArgumentException("No Tool observation is pending evaluation");
throw new ProgressProtocolViolationException(
ProgressProtocolViolationType.UNEXPECTED_PREVIOUS_OBSERVATION,
"No Tool observation is pending evaluation",
"previous_observation",
null);
}
clearProtocolViolations();
return;
}
if (observation == null) {
throw new IllegalArgumentException("Previous Tool observation must be evaluated");
throw new ProgressProtocolViolationException(
ProgressProtocolViolationType.MISSING_PREVIOUS_OBSERVATION,
"Previous Tool observation must be evaluated",
"previous_observation",
pendingToolCallId);
}
if (!pendingToolCallId.equals(observation.toolCallId())) {
throw new IllegalArgumentException("Previous Tool observation ID is out of order");
throw new ProgressProtocolViolationException(
ProgressProtocolViolationType.OUT_OF_ORDER_PREVIOUS_OBSERVATION,
"Previous Tool observation ID is out of order",
"previous_observation.tool_call_id",
pendingToolCallId);
}
pendingToolCallId = null;
clearProtocolViolations();
applyGain(observation.informationGain());
}
@@ -47,6 +75,7 @@ public final class DiagnosisProgressTracker {
}
public synchronized void recordDuplicateScope() {
clearProtocolViolations();
applyGain(InformationGain.NO_GAIN);
}
@@ -64,12 +93,26 @@ public final class DiagnosisProgressTracker {
}
completedToolCalls.add(call);
if (evidenceStatus == EvidenceStatus.NO_EVIDENCE) {
clearProtocolViolations();
applyGain(InformationGain.NO_GAIN);
} else {
pendingToolCallId = call.toolCallId();
}
}
public synchronized DiagnosisProgressSnapshotState recordProgressProtocolViolation() {
if (collectionState == DiagnosisCollectionState.SATURATED) {
return snapshot();
}
consecutiveProgressProtocolViolations++;
if (consecutiveProgressProtocolViolations
>= stopAfterConsecutiveProgressProtocolViolations) {
collectionState = DiagnosisCollectionState.SATURATED;
stopReason = DiagnosisStopReason.PROGRESS_PROTOCOL_VIOLATED;
}
return snapshot();
}
public synchronized boolean claimStopInstruction() {
if (collectionState != DiagnosisCollectionState.SATURATED) {
return false;
@@ -90,6 +133,7 @@ public final class DiagnosisProgressTracker {
public synchronized DiagnosisProgressSnapshotState snapshot() {
return new DiagnosisProgressSnapshotState(
consecutiveNoGain,
consecutiveProgressProtocolViolations,
collectionState,
stopReason,
pendingToolCallId,
@@ -101,6 +145,10 @@ public final class DiagnosisProgressTracker {
return stopAfterConsecutiveNoGain;
}
public int stopAfterConsecutiveProgressProtocolViolations() {
return stopAfterConsecutiveProgressProtocolViolations;
}
private void applyGain(InformationGain gain) {
if (collectionState == DiagnosisCollectionState.SATURATED) {
throw new IllegalStateException("Collection is already saturated");
@@ -115,4 +163,8 @@ public final class DiagnosisProgressTracker {
stopReason = DiagnosisStopReason.INFORMATION_SATURATED;
}
}
private void clearProtocolViolations() {
consecutiveProgressProtocolViolations = 0;
}
}
@@ -2,5 +2,6 @@ package com.superbiz.agent.harness.progress;
public enum DiagnosisStopReason {
INFORMATION_SATURATED,
BUDGET_LIMIT_REACHED
BUDGET_LIMIT_REACHED,
PROGRESS_PROTOCOL_VIOLATED
}
@@ -0,0 +1,43 @@
package com.superbiz.agent.harness.progress;
public final class ProgressProtocolViolationException extends IllegalArgumentException {
private final ProgressProtocolViolationType violationType;
private final String missingField;
private final String expectedPreviousToolCallId;
public ProgressProtocolViolationException(
ProgressProtocolViolationType violationType,
String message,
String missingField,
String expectedPreviousToolCallId) {
super(message);
this.violationType = violationType;
this.missingField = missingField;
this.expectedPreviousToolCallId = expectedPreviousToolCallId;
}
public ProgressProtocolViolationException(
ProgressProtocolViolationType violationType,
String message,
String missingField,
String expectedPreviousToolCallId,
Throwable cause) {
super(message, cause);
this.violationType = violationType;
this.missingField = missingField;
this.expectedPreviousToolCallId = expectedPreviousToolCallId;
}
public ProgressProtocolViolationType violationType() {
return violationType;
}
public String missingField() {
return missingField;
}
public String expectedPreviousToolCallId() {
return expectedPreviousToolCallId;
}
}
@@ -0,0 +1,9 @@
package com.superbiz.agent.harness.progress;
public enum ProgressProtocolViolationType {
MISSING_PREVIOUS_OBSERVATION,
OUT_OF_ORDER_PREVIOUS_OBSERVATION,
UNEXPECTED_PREVIOUS_OBSERVATION,
MISSING_INPUT,
INVALID_ENVELOPE
}
@@ -166,7 +166,8 @@ public final class DiagnosisReleaseUseCase {
DiagnosisProgressSnapshot progress,
DiagnosisStopReason stopReason) {
if (stopReason != DiagnosisStopReason.INFORMATION_SATURATED
&& stopReason != DiagnosisStopReason.BUDGET_LIMIT_REACHED) {
&& stopReason != DiagnosisStopReason.BUDGET_LIMIT_REACHED
&& stopReason != DiagnosisStopReason.PROGRESS_PROTOCOL_VIOLATED) {
throw new IllegalStateException("Unsupported Diagnosis stop reason");
}
if (!progress.hasObservedFacts()) {