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
@@ -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;
}
}