只拦截证据类 Tool(RAG / 日志 / MySQL),其余 Tool 原样放行。对证据 Tool 依次执行: + *
模型侧观察(observation)共有三副面孔:正常执行结果、STOP_REQUIRED(含 reason)、 + * 可修复协议错误(repair_required)。 + */ @Override public ToolCallResponse interceptToolCall(ToolCallRequest request, ToolCallHandler handler) { Objects.requireNonNull(request, "request must not be null"); Objects.requireNonNull(handler, "handler must not be null"); + // 只拦截证据类 Tool(RAG/日志/MySQL);其余 Tool 原样放行 if (!evidenceTools.supports(request.getToolName())) { return handler.call(request); } + // ── 第一道门:停止指令已交付后,任何证据 Tool 请求直接受控停止 ── DiagnosisProgressSnapshotState before = context.progress().snapshot(); if (before.collectionState() == DiagnosisCollectionState.SATURATED && before.stopInstructionDelivered()) { @@ -75,27 +95,34 @@ public final class HarnessToolInterceptor extends ToolInterceptor { ParsedAgentToolCall call; String normalizedScope; try { + // ── 第二道门:严格解析 Envelope + 应用模型对上一轮的评价 ── call = evidenceTools.parse(request.getToolName(), request.getArguments(), objectMapper); context.progress().applyPreviousObservation(call.previousObservation()); DiagnosisProgressSnapshotState evaluated = context.progress().snapshot(); + // 审计:模型回传的评价进入 Trace(producer=MODEL) recordModelProgress(before, call, evaluated); + // 评价导致饱和(连续 NO_GAIN 达阈值)→ 本工具不执行,交付停止指令 if (evaluated.collectionState() == DiagnosisCollectionState.SATURATED) { recordRejection(request, "INFORMATION_SATURATED"); return stopRequired(request, evaluated.stopReason()); } + // 规范化当前轮业务输入 → 稳定 scope(判重指纹) normalizedScope = scopeNormalizer.normalize(request.getToolName(), call.businessInput()); } catch (ProgressProtocolViolationException exception) { + // 协议违规:记录违规并返回可修复的 error observation(达阈值则饱和) return handleProgressProtocolViolation(request, exception); } catch (IllegalArgumentException | IllegalStateException exception) { + // 解析/规范化失败(非法 JSON、缺 input 等)统一按 INVALID_ENVELOPE 违规处理 return handleProgressProtocolViolation(request, new ProgressProtocolViolationException( ProgressProtocolViolationType.INVALID_ENVELOPE, "Tool Call Envelope is invalid", null, null, exception)); } + // ── 第三道门:参数级重复检测(backend 执行前拒绝)── if (context.progress().isDuplicate(request.getToolName(), normalizedScope)) { recordRejection(request, "DUPLICATE_SCOPE"); - context.progress().recordDuplicateScope(); + context.progress().recordDuplicateScope(); // 重复直接累计 NO_GAIN DiagnosisProgressSnapshotState duplicate = context.progress().snapshot(); recordProgress(request.getToolCallId(), request.getToolName(), normalizedScope, InformationGain.NO_GAIN, "HARNESS", duplicate); @@ -105,14 +132,17 @@ public final class HarnessToolInterceptor extends ToolInterceptor { return duplicateScope(request); } + // ── 第四道门:真正执行 Tool(ToolBoundary 统一门禁:预算/canonical/审计)── ToolBoundaryResult result = evidenceTools.invoke( context, request.getToolName(), request.getToolCallId(), call.businessArguments()); if (result.status() == InvocationStatus.READY) { + // 双源交叉验证:从 agent_result 重算的 evidence status 必须与声明的值一致 ToolControlView control = viewProjector.controlView(result.agentResult()); if (control.evidenceStatus() != result.evidenceStatus()) { recordRejection(request, "OBSERVATION_CONTRACT_MISMATCH"); return safeError(request, "OBSERVATION_CONTRACT_MISMATCH"); } + // 记录完成:NO_EVIDENCE 立即累计 NO_GAIN;EVIDENCE_FOUND 挂 pending 等模型评价 context.progress().recordCompleted( new CompletedToolCall( request.getToolCallId(), request.getToolName(), normalizedScope), @@ -123,17 +153,20 @@ public final class HarnessToolInterceptor extends ToolInterceptor { recordProgress(request.getToolCallId(), request.getToolName(), normalizedScope, InformationGain.NO_GAIN, "HARNESS", completed); } + // 完成后若饱和:领取一次停止指令(STOP_REQUIRED 只交付一次),观察里带 stop_required boolean stopRequired = completed.collectionState() == DiagnosisCollectionState.SATURATED && context.progress().claimStopInstruction(); if (stopRequired) { traceRecorder.record(TraceAuditEvents.collectionStop( context, request.getToolCallId(), request.getToolName(), completed)); } + // 有界 observation 返回给模型(含停止指令/停止原因) String observation = viewProjector.modelObservation( request.getToolName(), result.agentResult(), normalizedScope, stopRequired, completed.stopReason()); return ToolCallResponse.of(request.getToolCallId(), request.getToolName(), observation); } + // Tool 预算耗尽:标记停止原因(BUDGET_LIMIT_REACHED),其余错误返回稳定 error observation if ("BUDGET_EXHAUSTED".equals(result.errorCode())) { context.progress().markBudgetLimitReached(); traceRecorder.record(TraceAuditEvents.collectionStop( @@ -149,8 +182,17 @@ public final class HarnessToolInterceptor extends ToolInterceptor { .build(); } + /** + * 交付「必须停止」观察(observation 三副面孔之一)。 + * + *
调用前必须已 SATURATED。先领取一次停止指令(STOP_REQUIRED 只交付一次);
+ * 若已被领过(说明上一轮已交付、模型却继续请求 Tool),直接抛
+ * {@link DiagnosisCollectionStoppedException} 把受控停止穿出框架 ReAct loop。
+ * 正常时返回带 stop_required=true + reason 的观察,并记录 collectionStop Trace。
+ */
private ToolCallResponse stopRequired(ToolCallRequest request, DiagnosisStopReason reason) {
if (!context.progress().claimStopInstruction()) {
+ // 停止指令已被交付过 → 模型未听指令,受控停止穿出框架 loop
throw new DiagnosisCollectionStoppedException(reason);
}
traceRecorder.record(TraceAuditEvents.collectionStop(
@@ -164,6 +206,10 @@ public final class HarnessToolInterceptor extends ToolInterceptor {
request.getToolCallId(), request.getToolName(), writeObservation(observation));
}
+ /**
+ * 重复 scope 的观察:告诉模型本次调用被判定为参数级重复、记为 NO_GAIN,
+ * 但 stop_required=false(单次重复不一定饱和,模型可换查询继续)。
+ */
private ToolCallResponse duplicateScope(ToolCallRequest request) {
Map 与 TOOL_INVOCATION 区分:被拒绝的 Tool 从未调用 backend,
+ * 不消耗 Tool 预算、不计入实际执行数。
+ */
private void recordRejection(
ToolCallRequest request,
String errorCode,
@@ -250,6 +318,9 @@ public final class HarnessToolInterceptor extends ToolInterceptor {
violationType, repairPromptDelivered, state));
}
+ /**
+ * 观察序列化:失败时返回稳定的 SERIALIZATION_ERROR 观察(fail closed)。
+ */
private String writeObservation(Map 设计要点:
+ * 产出被 {@code DiagnosisReleaseUseCase} 消费,是发布 INSUFFICIENT_EVIDENCE 类
+ * FALLBACK 的全部原料(progress 与 release 的交汇点)。
+ */
public final class DiagnosisProgressProjector implements DiagnosisProgressProjection {
+ /** 投影事实条数上限:超出记 limitation 并停止投影。 */
private static final int MAX_FACTS = 12;
+ /** 每条事实摘要的最大字符数。 */
private static final int MAX_SUMMARY_CHARS = 320;
+ /** 查询范围(scope)的最大字符数。 */
private static final int MAX_SCOPE_CHARS = 320;
+ /** Canonical 事实存储:按 key 回读 READY 记录(唯一真相源)。 */
private final CanonicalInvocationStore store;
+ /** 生成 canonical key(runId + toolCallId)。 */
private final ToolCallKeyFactory keyFactory;
+ /** JSON 解析:agent_result 反序列化 + normalizedScope 解析。 */
private final ObjectMapper objectMapper;
+ /**
+ * 全参构造:三个依赖全部必填(null 直接 NPE 暴露装配错误)。
+ */
public DiagnosisProgressProjector(CanonicalInvocationStore store,
ToolCallKeyFactory keyFactory,
ObjectMapper objectMapper) {
@@ -34,6 +60,11 @@ public final class DiagnosisProgressProjector implements DiagnosisProgressProjec
this.objectMapper = Objects.requireNonNull(objectMapper, "objectMapper must not be null");
}
+ /**
+ * 主入口:遍历 Tracker 的已完成调用列表,逐个回读 canonical 并投影;
+ * 返回不可变的 ProgressSnapshot(verified sources + observed facts +
+ * limitations + stopReason),供 Release 发布。
+ */
@Override
public DiagnosisProgressSnapshot project(RunContext context) {
Objects.requireNonNull(context, "context must not be null");
@@ -44,11 +75,13 @@ public final class DiagnosisProgressProjector implements DiagnosisProgressProjec
for (CompletedToolCall completed : state.completedToolCalls()) {
CanonicalToolInvocation invocation = resolve(context, completed, limitations);
if (invocation == null) {
+ // 无法验真/不可读:已记 limitation,跳过
continue;
}
try {
projectInvocation(completed, invocation, sources, facts);
} catch (RuntimeException exception) {
+ // 投影异常(agent_result 非合法对象等):排除并记 limitation,不发布 raw
addLimitation(limitations, "部分已完成的工具结果格式无法验证,未纳入已检查事实");
}
if (facts.size() >= MAX_FACTS) {
@@ -63,6 +96,11 @@ public final class DiagnosisProgressProjector implements DiagnosisProgressProjec
state.stopReason());
}
+ /**
+ * 按 identity 回读 canonical 记录,做三重校验:
+ * isReferencableBy(READY + 同 run + agentResult 非空 + evidence 合法)、
+ * toolCallId 一致、toolName 一致——任何一项不过即排除并记 limitation。
+ */
private CanonicalToolInvocation resolve(RunContext context,
CompletedToolCall completed,
List 与 RunBudget 的分工(双停止机制):
+ * 设计要点:
+ * 三类协议违规会被拒绝并抛 {@link ProgressProtocolViolationException}:
+ * 校验通过后才清协议违规计数并应用 GAINED/NO_GAIN。协议错误与无增益是两件事:
+ * 前者 Tool 根本没执行,后者 Tool 执行了但没推进诊断,因此必须分开统计。
+ */
public synchronized void applyPreviousObservation(PreviousObservation observation) {
if (pendingToolCallId == null) {
if (observation != null) {
@@ -48,6 +101,7 @@ public final class DiagnosisProgressTracker {
"previous_observation",
null);
}
+ // 没有 pending 且没带评价:正常(如首次调用),顺带清协议违规计数
clearProtocolViolations();
return;
}
@@ -65,20 +119,40 @@ public final class DiagnosisProgressTracker {
"previous_observation.tool_call_id",
pendingToolCallId);
}
+ // 校验通过:清空 pending,评价生效
pendingToolCallId = null;
clearProtocolViolations();
applyGain(observation.informationGain());
}
+ /**
+ * 重复检测:toolName + normalizedScope 是否已被本 Run 完成过(backend 执行前调用)。
+ */
public synchronized boolean isDuplicate(String toolName, String normalizedScope) {
return completedScopes.contains(new ToolScopeIdentity(toolName, normalizedScope));
}
+ /**
+ * 记录一次被判重的调用:Harness 直接判定为 NO_GAIN(backend 未被调用)。
+ */
public synchronized void recordDuplicateScope() {
clearProtocolViolations();
applyGain(InformationGain.NO_GAIN);
}
+ /**
+ * 记录一次成功的 Tool 完成。
+ *
+ * 协议违规(缺评价/乱序/非法 Envelope)不计入 NO_GAIN——那是 Tool 执行了却没增益,
+ * 而违规时 backend 从未执行。连续违规达到独立阈值后进入 SATURATED。
+ */
public synchronized DiagnosisProgressSnapshotState recordProgressProtocolViolation() {
if (collectionState == DiagnosisCollectionState.SATURATED) {
+ // 已饱和:不再累计,直接返回当前快照
return snapshot();
}
consecutiveProgressProtocolViolations++;
@@ -113,6 +196,13 @@ public final class DiagnosisProgressTracker {
return snapshot();
}
+ /**
+ * 领取一次停止指令(STOP_REQUIRED)。
+ *
+ * 只有 SATURATED 且尚未交付过时返回 true——给模型一次合法完成机会(输出 Draft),
+ * 而不是立即抛错;之后模型仍请求 Tool 时由上层抛
+ * {@code DiagnosisCollectionStoppedException} 穿出框架 ReAct loop。
+ */
public synchronized boolean claimStopInstruction() {
if (collectionState != DiagnosisCollectionState.SATURATED) {
return false;
@@ -124,12 +214,22 @@ public final class DiagnosisProgressTracker {
return true;
}
+ /**
+ * 标记预算触顶(由 RunBudget 侧调用)。
+ *
+ * 只在尚无 stopReason 时设置 BUDGET_LIMIT_REACHED,不覆盖已有的
+ * INFORMATION_SATURATED / PROGRESS_PROTOCOL_VIOLATED——三种停止原因必须分开,
+ * 信息饱和不能伪装成预算耗尽。
+ */
public synchronized void markBudgetLimitReached() {
if (stopReason == null) {
stopReason = DiagnosisStopReason.BUDGET_LIMIT_REACHED;
}
}
+ /**
+ * 返回内部控制快照(计数、pending、停止指令状态与已完成调用列表)。
+ */
public synchronized DiagnosisProgressSnapshotState snapshot() {
return new DiagnosisProgressSnapshotState(
consecutiveNoGain,
@@ -149,6 +249,14 @@ public final class DiagnosisProgressTracker {
return stopAfterConsecutiveProgressProtocolViolations;
}
+ /**
+ * 应用单次增益判定(核心状态迁移):
+ * 决策树(与 progress 的衔接在这里):
+ * 任何 FALLBACK 都通过 SafeFallbackFactory 构造有界安全回退(不泄露 raw/敏感正文),
+ * 终态异常(取消/预算耗尽)向上传播不吞掉。
+ */
public final class DiagnosisReleaseUseCase {
+ /** 验引用真实性:EvidenceGuard 机械校验 evidence_ref 是否真实可引用。 */
private final EvidenceGuard evidenceGuard;
+ /** 引用修复:只修引用不修结论(证据安全链的一环)。 */
private final EvidenceRepair evidenceRepair;
+ /** 结论支持度裁决:隔离判断结论是否被已验证证据支持。 */
private final SemanticGuard semanticGuard;
+ /** 有界安全回退工厂:构造 INSUFFICIENT_EVIDENCE 等 FALLBACK。 */
private final SafeFallbackFactory fallbackFactory;
+ /** Trace 记录器:release 阶段的决策事件(evidence/semantic/release)。 */
private final DiagnosisTraceRecorder traceRecorder;
+ /**
+ * 四参构造:Trace 记录器使用 noop(测试/无审计场景)。
+ */
public DiagnosisReleaseUseCase(EvidenceGuard evidenceGuard,
EvidenceRepair evidenceRepair,
SemanticGuard semanticGuard,
@@ -40,6 +67,9 @@ public final class DiagnosisReleaseUseCase {
DiagnosisTraceRecorder.noop());
}
+ /**
+ * 全参构造:五个依赖全部必填(null 直接 NPE 暴露配置错误)。
+ */
public DiagnosisReleaseUseCase(EvidenceGuard evidenceGuard,
EvidenceRepair evidenceRepair,
SemanticGuard semanticGuard,
@@ -53,12 +83,27 @@ public final class DiagnosisReleaseUseCase {
this.traceRecorder = Objects.requireNonNull(traceRecorder, "traceRecorder must not be null");
}
+ /**
+ * 简化入口:正常收尾(模型输出了合法 Draft)时调用——
+ * 包装成 completed execution(progress 为空),走完整裁决。
+ */
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()));
}
+ /**
+ * 主入口:按执行结果三分支裁决(对外结果的唯一出口)。
+ *
+ * 职责(对一次 Tool 执行):
+ * 边界(明确不做):
+ * 状态-字段约束(构造时强校验):
+ * 两个工厂方法对应两条出口:{@link #ready}(成功投影后)与 {@link #error}(失败)。
+ *
+ * 拦截器拿到它后还会做双源交叉验证(ToolResultViewProjector 重算 evidence status),
+ * 因此 READY 的声明值与内容必须一致。
+ */
public record ToolBoundaryResult(
@JsonProperty("status") InvocationStatus status,
@JsonProperty("evidence_status") EvidenceStatus evidenceStatus,
@@ -12,6 +29,7 @@ public record ToolBoundaryResult(
@JsonProperty("error_code") String errorCode) {
public ToolBoundaryResult {
+ // READY:必须有界证据结果(agent_result + FOUND/NO_EVIDENCE),禁止带错误码
if (status == InvocationStatus.READY) {
if (agentResult == null || evidenceStatus == null
|| (evidenceStatus != EvidenceStatus.EVIDENCE_FOUND
@@ -22,6 +40,7 @@ public record ToolBoundaryResult(
throw new IllegalArgumentException("READY result must not contain errorCode");
}
} else if (status == InvocationStatus.ERROR) {
+ // ERROR:稳定错误码必填 + evidence 必须 ERROR,禁止带投影结果
if (evidenceStatus != EvidenceStatus.ERROR || errorCode == null || errorCode.isBlank()) {
throw new IllegalArgumentException("ERROR result requires errorCode and ERROR evidence status");
}
@@ -29,10 +48,15 @@ public record ToolBoundaryResult(
throw new IllegalArgumentException("ERROR result must not contain agent result");
}
} else {
+ // PROJECTING 等中间状态不允许离开 Boundary
throw new IllegalArgumentException("ToolBoundaryResult must be READY or ERROR");
}
}
+ /**
+ * READY 出口:投影成功后调用,携带有界 agent_result 与客观证据语义
+ * (evidence 由 Projector 按「证据数组空不空」计算)。
+ */
public static ToolBoundaryResult ready(String toolCallId,
String agentResult,
EvidenceStatus evidenceStatus) {
@@ -40,6 +64,9 @@ public record ToolBoundaryResult(
InvocationStatus.READY, evidenceStatus, toolCallId, agentResult, null);
}
+ /**
+ * ERROR 出口:失败时调用,携带稳定错误码(不携带 raw/agent_result)。
+ */
public static ToolBoundaryResult error(String toolCallId, ToolBoundaryErrorCode errorCode) {
return new ToolBoundaryResult(
InvocationStatus.ERROR, EvidenceStatus.ERROR, toolCallId, null, errorCode.name());
diff --git a/src/main/java/com/superbiz/agent/harness/tool/store/CanonicalToolInvocation.java b/src/main/java/com/superbiz/agent/harness/tool/store/CanonicalToolInvocation.java
index 68a2d61..f26c18b 100644
--- a/src/main/java/com/superbiz/agent/harness/tool/store/CanonicalToolInvocation.java
+++ b/src/main/java/com/superbiz/agent/harness/tool/store/CanonicalToolInvocation.java
@@ -7,6 +7,28 @@ import com.superbiz.agent.harness.contract.InvocationStatus;
import java.time.Instant;
import java.util.Objects;
+/**
+ * Canonical Store 中的「唯一真相」Tool 调用记录(不可变 record)。
+ *
+ * 状态机(每次迁移返回新实例,原实例不变):
+ * 字段分工:
+ * 构造时 {@link #validateState} 强制状态-字段一致性:每个状态只允许携带
+ * 该状态合法的字段组合(如 PROJECTING 不得有终态字段、ERROR 不得有 agent_result)。
+ */
public record CanonicalToolInvocation(
@JsonProperty("tool_call_id") String toolCallId,
@JsonProperty("run_id") String runId,
@@ -21,15 +43,21 @@ public record CanonicalToolInvocation(
@JsonProperty("completed_at") Instant completedAt) {
public CanonicalToolInvocation {
+ // 身份/请求字段必填;状态与开始时间必填
requireText(toolCallId, "toolCallId");
requireText(runId, "runId");
requireText(toolName, "toolName");
requireText(request, "request");
Objects.requireNonNull(status, "status must not be null");
Objects.requireNonNull(startedAt, "startedAt must not be null");
+ // 状态-字段一致性:非法组合直接拒绝入库
validateState(status, evidenceStatus, rawResponse, agentResult, errorCode, completedAt);
}
+ /**
+ * 创建 PROJECTING 记录:执行开始时调用,仅携带身份 + request + 开始时间
+ * (终态字段全部为 null)。
+ */
public static CanonicalToolInvocation projecting(String toolCallId,
String runId,
String toolName,
@@ -40,6 +68,10 @@ public record CanonicalToolInvocation(
InvocationStatus.PROJECTING, null, null, startedAt, null);
}
+ /**
+ * PROJECTING → READY 迁移:成功路径(Projector 投影完成后调用)。
+ * 必须携带 raw_response + agent_result + evidence(FOUND/NO_EVIDENCE)+ 完成时间。
+ */
public CanonicalToolInvocation markReady(String rawResponse,
String agentResult,
EvidenceStatus evidenceStatus,
@@ -50,6 +82,10 @@ public record CanonicalToolInvocation(
InvocationStatus.READY, evidenceStatus, null, startedAt, completedAt);
}
+ /**
+ * PROJECTING → ERROR 迁移:失败路径(backend 执行/投影/预算等异常时调用)。
+ * 携带 raw_response(尽力而为)+ 稳定 error_code + 完成时间;agent_result 禁止出现。
+ */
public CanonicalToolInvocation markError(String rawResponse,
String errorCode,
Instant completedAt) {
@@ -59,6 +95,11 @@ public record CanonicalToolInvocation(
InvocationStatus.ERROR, EvidenceStatus.ERROR, errorCode, startedAt, completedAt);
}
+ /**
+ * 验真条件(EvidenceGuard / ProgressProjector 都依赖它):
+ * READY + 属于同一 Run + agent_result 非空 + evidence 是合法证据语义
+ * (FOUND / NO_EVIDENCE)——即该记录可被引用为真实证据。
+ */
public boolean isReferencableBy(String expectedRunId) {
return status == InvocationStatus.READY
&& Objects.equals(runId, expectedRunId)
@@ -67,12 +108,17 @@ public record CanonicalToolInvocation(
|| evidenceStatus == EvidenceStatus.NO_EVIDENCE);
}
+ /** 状态机防御:只有 PROJECTING 允许迁移(READY/ERROR 是终态,不可再变)。 */
private void requireProjecting() {
if (status != InvocationStatus.PROJECTING) {
throw new InvocationStateException("Only PROJECTING invocation can transition");
}
}
+ /**
+ * 状态-字段一致性校验:每种状态只允许合法的字段组合,
+ * 非法组合(如 PROJECTING 带终态字段、ERROR 带 agent_result)直接拒绝构造。
+ */
private static void validateState(InvocationStatus status,
EvidenceStatus evidenceStatus,
String rawResponse,
@@ -81,14 +127,17 @@ public record CanonicalToolInvocation(
Instant completedAt) {
switch (status) {
case PROJECTING -> {
+ // 投影中:不得携带任何终态字段(evidence/agent_result/error/完成时间)
if (evidenceStatus != null || agentResult != null || errorCode != null || completedAt != null) {
throw new InvocationStateException("PROJECTING invocation contains terminal fields");
}
}
case READY -> {
+ // 就绪:必须三者齐全(raw + agent_result + 完成时间)
if (rawResponse == null || agentResult == null || completedAt == null) {
throw new InvocationStateException("READY invocation requires raw, agent result and completion");
}
+ // 就绪:evidence 只能是合法证据语义,且不得带错误码
if (evidenceStatus != EvidenceStatus.EVIDENCE_FOUND
&& evidenceStatus != EvidenceStatus.NO_EVIDENCE) {
throw new InvocationStateException("READY invocation has invalid evidence status");
@@ -98,6 +147,7 @@ public record CanonicalToolInvocation(
}
}
case ERROR -> {
+ // 错误:evidence 必须 ERROR + 完成时间 + 错误码必填,agent_result 禁止
if (evidenceStatus != EvidenceStatus.ERROR || completedAt == null) {
throw new InvocationStateException("ERROR invocation requires ERROR evidence status and completion");
}
@@ -109,6 +159,7 @@ public record CanonicalToolInvocation(
}
}
+ /** 非空校验(用于身份、请求、错误码等必填字段)。 */
private static void requireText(String value, String name) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(name + " must not be blank");
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ */
public final class DiagnosisProgressTracker {
+ /** 连续 NO_GAIN 达到该阈值 → SATURATED + INFORMATION_SATURATED(默认 2)。 */
private final int stopAfterConsecutiveNoGain;
+ /** 连续 progress 协议违规达到该阈值 → SATURATED + PROGRESS_PROTOCOL_VIOLATED(默认 2)。 */
private final int stopAfterConsecutiveProgressProtocolViolations;
+ /** 已完成调用的去重集合:toolName + normalizedScope,backend 执行前判重。 */
private final Set
+ *
+ *
+ *
+ *
+ */
public synchronized void recordCompleted(CompletedToolCall call, EvidenceStatus evidenceStatus) {
if (collectionState == DiagnosisCollectionState.SATURATED) {
throw new IllegalStateException("Cannot record Tool completion after saturation");
@@ -93,15 +167,24 @@ public final class DiagnosisProgressTracker {
}
completedToolCalls.add(call);
if (evidenceStatus == EvidenceStatus.NO_EVIDENCE) {
+ // 空结果无需模型评价:立即累计 NO_GAIN
clearProtocolViolations();
applyGain(InformationGain.NO_GAIN);
} else {
+ // 非空结果:挂起等待模型在下一轮评价语义增益
pendingToolCallId = call.toolCallId();
}
}
+ /**
+ * 记录一次 progress 协议违规,返回最新快照。
+ *
+ *
+ *
+ * 饱和后禁止再次应用(停止权只行使一次)。
+ */
private void applyGain(InformationGain gain) {
if (collectionState == DiagnosisCollectionState.SATURATED) {
throw new IllegalStateException("Collection is already saturated");
@@ -164,6 +272,10 @@ public final class DiagnosisProgressTracker {
}
}
+ /**
+ * 清空协议违规计数:一次合法评价(或没有 pending 的合法调用)都会重置,
+ * 避免历史违规累积导致误饱和(协议违规只按「连续」计数)。
+ */
private void clearProtocolViolations() {
consecutiveProgressProtocolViolations = 0;
}
diff --git a/src/main/java/com/superbiz/agent/harness/release/DiagnosisReleaseUseCase.java b/src/main/java/com/superbiz/agent/harness/release/DiagnosisReleaseUseCase.java
index a4741c8..7776fef 100644
--- a/src/main/java/com/superbiz/agent/harness/release/DiagnosisReleaseUseCase.java
+++ b/src/main/java/com/superbiz/agent/harness/release/DiagnosisReleaseUseCase.java
@@ -24,14 +24,41 @@ import com.superbiz.agent.harness.retry.RetryFailure;
import java.util.Objects;
import java.util.List;
+/**
+ * 对外结果的「唯一发布点」:把 Agent 执行结果(Draft / 受控停止 / 非法 Draft)
+ * 裁决为 {@code SUCCESS / FALLBACK},并保证任何对外发布内容都经过
+ * 证据验证(EvidenceGuard)+ 语义裁决(SemanticGuard)。
+ *
+ *
+ * execute(execution)
+ * ├─ draft == null → 受控停止(progress + stopReason)→ INSUFFICIENT_EVIDENCE
+ * ├─ draft.conclusion == null → 无结论 Draft → 有已验真事实 ? INSUFFICIENT_EVIDENCE
+ * │ : missing_info ? MISSING_REQUIRED_CONTEXT
+ * │ : fail closed(抛异常)
+ * └─ 有结论 Draft → evidenceGuard 验引用 → repair 重试 → semanticGuard 裁决
+ * → SUPPORTED ? SUCCESS : FALLBACK(SEMANTIC_UNSUPPORTED)
+ *
+ *
+ *
+ *
+ */
public DiagnosisReleaseResult execute(
RunContext context, String query, DiagnosisAgentExecution execution) {
Objects.requireNonNull(context, "context must not be null");
@@ -69,19 +114,27 @@ public final class DiagnosisReleaseUseCase {
DiagnosisDraft draft = execution.draft();
if (draft == null) {
+ // 受控停止:没有 Draft,只能靠已完成检查的 progress 快照发布
return releaseControlledStop(context, execution.progress(), execution.stopReason());
}
if (draft.conclusion() == null) {
+ // 无结论 Draft(含 conclusion=null 合法收尾):查引用 + 按进展发布
return releaseNoConclusion(context, draft, execution.progress());
}
+ // 有结论 Draft:证据验证 →(必要时)修复 → 语义裁决
return releaseConclusion(context, query, draft);
}
+ /**
+ * 非法 Draft 专用发布:模型输出不符合 Schema 时,若本 Run 已有可发布事实,
+ * 降级为 INSUFFICIENT_EVIDENCE Fallback(非法 draft 正文永不出现在对外 content)。
+ */
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()) {
+ // 无安全事实 → fail closed:调用方保持原异常(DiagnosisChatExecutor 里再上抛)
throw new IllegalStateException(
"Invalid Diagnosis Draft has no verified publishable progress");
}
@@ -90,6 +143,12 @@ public final class DiagnosisReleaseUseCase {
FallbackType.INSUFFICIENT_EVIDENCE);
}
+ /**
+ * 有结论 Draft 的完整发布链:
+ * ① EvidenceGuard 验引用真实性 → ② 不过则 EvidenceRepair 只修引用再验
+ * → ③ SemanticGuard 裁决结论支持度 → SUPPORTED ? SUCCESS : SEMANTIC_UNSUPPORTED FALLBACK。
+ * 任何环节的终态异常(取消/预算)向上传播,不吞掉。
+ */
private DiagnosisReleaseResult releaseConclusion(
RunContext context, String query, DiagnosisDraft draft) {
DiagnosisDraft candidate = draft;
@@ -98,6 +157,7 @@ public final class DiagnosisReleaseUseCase {
context, TraceEventType.EVIDENCE_GUARD_INITIAL, evidence, candidate));
if (!evidence.valid()) {
try {
+ // 引用不真实:只修引用(EvidenceRepair),不替模型改结论
candidate = evidenceRepair.repair(context, query, draft, evidence.violations());
evidence = evidenceGuard.validate(context, candidate);
traceRecorder.record(TraceAuditEvents.evidenceValidation(
@@ -107,6 +167,7 @@ public final class DiagnosisReleaseUseCase {
return evidenceFailure(context, evidence);
}
if (!evidence.valid()) {
+ // 修复后仍不真实 → 证据验证失败 Fallback(不发布模型原文结论)
return evidenceFailure(context, evidence);
}
}
@@ -117,6 +178,7 @@ public final class DiagnosisReleaseUseCase {
decision = semanticGuard.review(
context, SemanticGuardInput.from(query, candidate, snapshot));
} catch (RuntimeException exception) {
+ // 语义裁决不可用(如模型超时)→ 降级为 SEMANTIC_UNAVAILABLE Fallback
propagateTerminal(exception);
traceRecorder.record(TraceAuditEvents.semanticUnavailable(context));
traceRecorder.record(TraceAuditEvents.releaseDecision(
@@ -127,16 +189,25 @@ public final class DiagnosisReleaseUseCase {
}
traceRecorder.record(TraceAuditEvents.semanticDecision(context, decision.verdict()));
if (decision.verdict() == SemanticVerdict.SUPPORTED) {
+ // 引用真实 + 结论被支持 → 唯一的 SUCCESS 出口
traceRecorder.record(TraceAuditEvents.releaseDecision(
context, com.superbiz.agent.harness.contract.ReleaseOutcome.SUCCESS, null));
return DiagnosisReleaseResult.success(candidate, snapshot);
}
+ // 引用真实但结论不支持 → 语义不支持 Fallback(保留已验证快照)
traceRecorder.record(TraceAuditEvents.releaseDecision(
context, com.superbiz.agent.harness.contract.ReleaseOutcome.FALLBACK,
FallbackType.SEMANTIC_UNSUPPORTED));
return DiagnosisReleaseResult.fallback(fallbackFactory.semanticUnsupported(snapshot));
}
+ /**
+ * 无结论 Draft 路径(conclusion=null 是合法收尾,不是失败):
+ * 先验引用,再按「有无已验真事实 / 是否声明缺失上下文」发布:
+ * 有事实 → INSUFFICIENT_EVIDENCE(展示已查内容 + 缺失项);
+ * 无事实但声明 missing_info → MISSING_REQUIRED_CONTEXT;
+ * 都没有 → 不变量被破坏,fail closed 抛异常。
+ */
private DiagnosisReleaseResult releaseNoConclusion(
RunContext context, DiagnosisDraft draft, DiagnosisProgressSnapshot progress) {
EvidenceGuardResult evidence = evidenceGuard.validateNoConclusionReferences(context, draft);
@@ -148,19 +219,27 @@ public final class DiagnosisReleaseUseCase {
List
+ *
+ *
+ *
+ *
+ */
public final class ToolBoundary {
private static final Logger log = LoggerFactory.getLogger(ToolBoundary.class);
+ /** Harness 核心:Run bytes 预留(reserveRunBytes)、Tool 预算检查(beforeToolCall)。 */
private final DiagnosisHarnessCore core;
+ /** canonical key 生成(runId + toolCallId)。 */
private final ToolCallKeyFactory keyFactory;
+ /** canonical 持久化(唯一真相源)。 */
private final CanonicalInvocationStore store;
+ /** request JSON 合法性校验。 */
private final ObjectMapper objectMapper;
+ /** 时间戳(begin/ready/error/audit 统一时钟)。 */
private final Clock clock;
+ /** 持久化审计 sink(可 noop)。 */
private final ToolInvocationAuditSink auditSink;
+ /** 当前 step id 追踪(可 null:无 step 场景不记录)。 */
private final AgentStepAuditTracker stepTracker;
+ /** 最简构造:审计 noop + stepTracker null(测试/轻量场景)。 */
public ToolBoundary(DiagnosisHarnessCore core,
ToolCallKeyFactory keyFactory,
CanonicalInvocationStore store,
@@ -45,6 +73,7 @@ public final class ToolBoundary {
this(core, keyFactory, store, objectMapper, clock, ToolInvocationAuditSink.noop(), null);
}
+ /** 带审计构造:持久化 Tool 审计,无 stepTracker。 */
public ToolBoundary(DiagnosisHarnessCore core,
ToolCallKeyFactory keyFactory,
CanonicalInvocationStore store,
@@ -54,6 +83,7 @@ public final class ToolBoundary {
this(core, keyFactory, store, objectMapper, clock, auditSink, null);
}
+ /** 全参构造:七个依赖全部必填(null 直接 NPE 暴露装配错误)。 */
public ToolBoundary(DiagnosisHarnessCore core,
ToolCallKeyFactory keyFactory,
CanonicalInvocationStore store,
@@ -70,6 +100,12 @@ public final class ToolBoundary {
this.stepTracker = stepTracker;
}
+ /**
+ * 统一执行入口:计算耗时 → 执行 canonical 状态机 → best-effort 审计 → 返回结果。
+ *
+ * @param executor 具体 backend 的 raw 执行函数(如 MySQL/日志 adapter)
+ * @param projector 该 Tool 的投影器(raw → 有界 agent_result + evidence status)
+ */
public ToolBoundaryResult execute(RunContext context,
ToolCallRequestEnvelope request,
ToolExecutor executor,
@@ -80,6 +116,11 @@ public final class ToolBoundary {
return outcome.result();
}
+ /**
+ * canonical 状态机主流程:所有成功/失败路径都映射为
+ * ToolBoundaryResult.ready / ToolBoundaryResult.error,中间状态不外泄;
+ * 任何异常都先尝试 markError 落库(best-effort),再返回稳定错误码。
+ */
private ExecutionOutcome executeCanonical(RunContext context,
ToolCallRequestEnvelope request,
ToolExecutor executor,
@@ -87,10 +128,12 @@ public final class ToolBoundary {
String toolCallId = request == null ? null : request.toolCallId();
String key;
try {
+ // ── 阶段一:preflight + Tool 预算 + request bytes 预留,通过后写 PROJECTING ──
key = preflight(context, request);
core.beforeToolCall(context, request.toolName());
long requestBytes = store.limits().utf8Bytes(request.requestJson());
if (requestBytes > store.limits().maxRecordBytes()) {
+ // 请求体超限:不落库直接拒绝
return ExecutionOutcome.of(errorAndNoRecord(toolCallId, ToolBoundaryErrorCode.RESULT_TOO_LARGE));
}
core.reserveRunBytes(context, requestBytes);
@@ -98,18 +141,22 @@ public final class ToolBoundary {
request.toolCallId(), request.runId(), request.toolName(),
request.requestJson(), clock.instant()));
} catch (DuplicateInvocationException e) {
+ // 同 key 重复 begin(同一 run+toolCallId 调两次)→ 拒绝
return ExecutionOutcome.of(ToolBoundaryResult.error(toolCallId, ToolBoundaryErrorCode.DUPLICATE_TOOL_CALL));
} catch (RunAbortedException | BudgetExceededException e) {
+ // Run 已取消/预算耗尽:对应终态错误码
return ExecutionOutcome.of(ToolBoundaryResult.error(toolCallId,
e instanceof BudgetExceededException
? ToolBoundaryErrorCode.BUDGET_EXHAUSTED
: ToolBoundaryErrorCode.RUN_INACTIVE));
} catch (IllegalArgumentException e) {
+ // preflight 非法:按异常类型归类错误码
return ExecutionOutcome.of(ToolBoundaryResult.error(toolCallId, classifyPreflightError(e)));
} catch (CanonicalStoreException e) {
return ExecutionOutcome.of(ToolBoundaryResult.error(toolCallId, ToolBoundaryErrorCode.STORE_ERROR));
}
+ // ── 阶段二:真正执行 backend(raw 响应)──
String rawResponse;
try {
rawResponse = Objects.requireNonNull(executor, "executor must not be null")
@@ -118,10 +165,12 @@ public final class ToolBoundary {
throw new IllegalArgumentException("executor returned null");
}
} catch (Exception e) {
+ // backend 执行失败:落 ERROR(无 raw)
markErrorSafely(key, null, ToolBoundaryErrorCode.TOOL_EXECUTION_ERROR);
return ExecutionOutcome.of(ToolBoundaryResult.error(toolCallId, ToolBoundaryErrorCode.TOOL_EXECUTION_ERROR));
}
+ // ── 阶段三:raw 大小校验 + Run bytes 预留 ──
try {
store.limits().validateRawCandidate(request.requestJson(), rawResponse);
core.reserveRunBytes(context, store.limits().utf8Bytes(rawResponse));
@@ -139,6 +188,7 @@ public final class ToolBoundary {
ToolBoundaryResult.error(toolCallId, ToolBoundaryErrorCode.RUN_INACTIVE), rawResponse);
}
+ // ── 阶段四:投影(raw → 有界 agent_result,Projector 计算 evidence status)──
ProjectedToolResult projected;
try {
projected = Objects.requireNonNull(projector, "projector must not be null")
@@ -147,11 +197,13 @@ public final class ToolBoundary {
throw new IllegalArgumentException("projector returned null");
}
} catch (Exception e) {
+ // 投影失败(raw 无法解析等):落 ERROR
markErrorSafely(key, rawResponse, ToolBoundaryErrorCode.PROJECTION_ERROR);
return new ExecutionOutcome(
ToolBoundaryResult.error(toolCallId, ToolBoundaryErrorCode.PROJECTION_ERROR), rawResponse);
}
+ // ── 阶段五:agent_result 校验 + bytes 预留 → 迁移 READY(唯一成功出口)──
try {
store.limits().validateAgentResult(projected.agentResult());
core.reserveRunBytes(context, store.limits().utf8Bytes(projected.agentResult()));
@@ -172,6 +224,7 @@ public final class ToolBoundary {
return new ExecutionOutcome(
ToolBoundaryResult.error(toolCallId, ToolBoundaryErrorCode.RUN_INACTIVE), rawResponse);
} catch (CanonicalStoreException e) {
+ // store 持久化失败:错误码归类(结果过大 vs 投影问题)
ToolBoundaryErrorCode code = e instanceof ResultTooLargeException
? ToolBoundaryErrorCode.RESULT_TOO_LARGE
: ToolBoundaryErrorCode.PROJECTION_ERROR;
@@ -180,17 +233,24 @@ public final class ToolBoundary {
}
}
+ /**
+ * 执行前门禁:run 匹配 / 授权 / 只读意图 / 字段与 JSON 合法性 / key 生成。
+ * 任何一项不过都会抛特定异常,由调用方归类为稳定错误码。
+ */
private String preflight(RunContext context, ToolCallRequestEnvelope request) {
if (context == null || request == null) {
throw new IllegalArgumentException("request/context must not be null");
}
if (!context.runId().equals(request.runId())) {
+ // 信封 run 与当前 context 不一致 → RUN_MISMATCH
throw new RunMismatchException();
}
if (!request.authorized()) {
+ // 未授权调用 → UNAUTHORIZED
throw new UnauthorizedException();
}
if (!request.readOnly()) {
+ // 非只读意图(诊断 Tool 必须是只读)→ NOT_READ_ONLY
throw new NotReadOnlyException();
}
if (request.toolName() == null || request.toolName().isBlank()
@@ -198,6 +258,7 @@ public final class ToolBoundary {
throw new IllegalArgumentException("tool name and request must not be blank");
}
try {
+ // request 必须是合法 JSON 对象
JsonNode root = objectMapper.readTree(request.requestJson());
if (root == null || !root.isObject()) {
throw new IllegalArgumentException("request must be a JSON object");
@@ -208,10 +269,14 @@ public final class ToolBoundary {
try {
return keyFactory.create(request.runId(), request.toolCallId());
} catch (IllegalArgumentException e) {
+ // toolCallId 非法 → INVALID_TOOL_CALL_ID
throw new InvalidToolCallIdException(e);
}
}
+ /**
+ * 安全落 ERROR(best-effort):持久化失败只记 warn 日志,不阻断返回错误结果。
+ */
private void markErrorSafely(String key, String rawResponse, ToolBoundaryErrorCode code) {
try {
store.markError(key, rawResponse, code.name(), clock.instant());
@@ -220,10 +285,12 @@ public final class ToolBoundary {
}
}
+ /** 请求体超限等「无需落库」的错误出口(尚未 begin,无记录可标记)。 */
private ToolBoundaryResult errorAndNoRecord(String toolCallId, ToolBoundaryErrorCode code) {
return ToolBoundaryResult.error(toolCallId, code);
}
+ /** 把 preflight 抛出的 IllegalArgumentException 归类为稳定错误码。 */
private ToolBoundaryErrorCode classifyPreflightError(IllegalArgumentException exception) {
if (exception instanceof InvalidToolCallIdException) {
return ToolBoundaryErrorCode.INVALID_TOOL_CALL_ID;
@@ -240,6 +307,10 @@ public final class ToolBoundary {
return ToolBoundaryErrorCode.INVALID_REQUEST;
}
+ /**
+ * 持久化 Tool 审计事件(best-effort):记录调用元数据(status/耗时/bytes/enrichments),
+ * 不保存完整 request/raw/agent result(敏感正文不进审计);失败只记 warn 不阻断业务。
+ */
private void auditSafely(RunContext context,
ToolCallRequestEnvelope request,
ToolBoundaryResult result,
@@ -268,38 +339,45 @@ public final class ToolBoundary {
}
}
+ /** UTF-8 字节数(null 视为 0),用于 bytes 预算与审计。 */
private static int utf8Bytes(String value) {
return value == null ? 0 : saturatingInt(value.getBytes(StandardCharsets.UTF_8).length);
}
+ /** 防溢出的 int 封顶转换。 */
private static int saturatingInt(long value) {
return value >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) value;
}
+ /** 执行结果 + 原始响应(审计用),rawResponse 仅成功/已产出时携带。 */
private record ExecutionOutcome(ToolBoundaryResult result, String rawResponse) {
static ExecutionOutcome of(ToolBoundaryResult result) {
return new ExecutionOutcome(result, null);
}
}
+ /** preflight 专用异常:toolCallId 非法。 */
private static final class InvalidToolCallIdException extends IllegalArgumentException {
private InvalidToolCallIdException(Throwable cause) {
super("Invalid tool call ID", cause);
}
}
+ /** preflight 专用异常:信封 runId 与 context 不一致。 */
private static final class RunMismatchException extends IllegalArgumentException {
private RunMismatchException() {
super("Run ID does not match context");
}
}
+ /** preflight 专用异常:调用未授权。 */
private static final class UnauthorizedException extends IllegalArgumentException {
private UnauthorizedException() {
super("Tool call is not authorized");
}
}
+ /** preflight 专用异常:意图非只读(诊断 Tool 只读红线)。 */
private static final class NotReadOnlyException extends IllegalArgumentException {
private NotReadOnlyException() {
super("Tool call is not read-only");
diff --git a/src/main/java/com/superbiz/agent/harness/tool/boundary/ToolBoundaryResult.java b/src/main/java/com/superbiz/agent/harness/tool/boundary/ToolBoundaryResult.java
index 8bac1e2..496b7d4 100644
--- a/src/main/java/com/superbiz/agent/harness/tool/boundary/ToolBoundaryResult.java
+++ b/src/main/java/com/superbiz/agent/harness/tool/boundary/ToolBoundaryResult.java
@@ -4,6 +4,23 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.superbiz.agent.harness.contract.EvidenceStatus;
import com.superbiz.agent.harness.contract.InvocationStatus;
+/**
+ * ToolBoundary 向上(Harness)返回的结果:只允许 READY 或 ERROR 两种状态离开 Boundary。
+ *
+ *
+ *
+ *
+ *
+ * PROJECTING ──markReady──▶ READY (成功:raw + agent_result + evidence FOUND/NO_EVIDENCE)
+ * PROJECTING ──markError──▶ ERROR (失败:error_code,无 agent_result)
+ *
+ *
+ *
+ *
+ *
+ *