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:
@@ -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
|
||||
}
|
||||
|
||||
+43
@@ -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()) {
|
||||
|
||||
Reference in New Issue
Block a user