diff --git a/src/main/java/com/superbiz/agent/controller/ChatController.java b/src/main/java/com/superbiz/agent/controller/ChatController.java index 07b6fef..e990c50 100644 --- a/src/main/java/com/superbiz/agent/controller/ChatController.java +++ b/src/main/java/com/superbiz/agent/controller/ChatController.java @@ -24,12 +24,24 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadPoolExecutor; -/** HTTP and SSE protocol adapter for Chat. */ +/** + * HTTP / SSE 协议适配层(Harness 入口的最外层)。 + * + *

职责边界: + *

+ * + *

真正的一次请求编排在 {@link ChatApplicationUseCase}。 + */ @RestController @RequestMapping("/api") public class ChatController { + /** 应用编排入口:建 Run → 路由 → 分叉执行 → 收尾。 */ private final ChatApplicationUseCase chatApplication; + /** Chat 专用工作线程池;避免在 HTTP 线程上阻塞跑 LLM。 */ private final ThreadPoolExecutor chatWorkerExecutor; private final ChatHarnessProperties harnessProperties; @@ -41,6 +53,12 @@ public class ChatController { this.harnessProperties = harnessProperties; } + /** + * POST /api/chat:立即返回 SSE 流;业务在 worker 线程执行。 + * + *

SSE 事件由 {@link ChatSseSession} 发出(metadata / status / content / done)。 + * 客户端断开时 session.disconnect,后续可经 RunControl 取消 Run。 + */ @PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public ResponseEntity chat(@RequestBody ChatRequest request) { if (request == null || request.getQuestion() == null || request.getQuestion().isBlank()) { @@ -48,12 +66,14 @@ public class ChatController { } SseEmitter emitter = new SseEmitter(harnessProperties.getSseTimeout().toMillis()); + // session 同时是 ChatApplicationObserver:编排过程中的 status/取消都经它推 SSE ChatSseSession session = new ChatSseSession(emitter); emitter.onTimeout(session::disconnect); emitter.onError(ignored -> session.disconnect()); emitter.onCompletion(session::disconnect); try { + // 异步执行:HTTP 线程只持有 SSE 连接,不跑模型 chatWorkerExecutor.execute(() -> executeChat(request, session)); } catch (RejectedExecutionException rejected) { return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build(); @@ -63,8 +83,10 @@ public class ChatController { .body(emitter); } + /** 工作线程:把协议请求转成 Application 请求,统一成功/失败出口。 */ private void executeChat(ChatRequest request, ChatSseSession session) { try { + // Id=sessionId(可多轮复用),Question=本轮 query → 一问一 run ChatApplicationResult result = chatApplication.execute( new ChatApplicationRequest(request.getQuestion(), request.getId()), session); session.complete(result); diff --git a/src/main/java/com/superbiz/agent/harness/agent/DiagnosisAgentFactory.java b/src/main/java/com/superbiz/agent/harness/agent/DiagnosisAgentFactory.java index 5633a4d..2c39708 100644 --- a/src/main/java/com/superbiz/agent/harness/agent/DiagnosisAgentFactory.java +++ b/src/main/java/com/superbiz/agent/harness/agent/DiagnosisAgentFactory.java @@ -12,6 +12,15 @@ import org.springframework.ai.chat.model.ChatModel; import java.util.List; import java.util.Objects; +/** + * 组装 Diagnosis 用的 Spring AI Alibaba {@link ReactAgent}。 + * + *

这里是「框架能力」与「Harness 控制面」的粘合点: + *

+ */ public final class DiagnosisAgentFactory { public static final String AGENT_NAME = "diagnosis_agent"; @@ -62,22 +71,27 @@ public final class DiagnosisAgentFactory { this.prompt = DiagnosisAgentPrompt.load(); } + /** + * 为当前 Run 构建 ReactAgent 实例(与 RunContext 绑定,不可跨 run 复用)。 + */ public ReactAgent create(RunContext context) { Objects.requireNonNull(context, "context must not be null"); return ReactAgent.builder() .name(AGENT_NAME) .description("Collects bounded evidence and authors one diagnosis draft") - .model(chatModel) + .model(chatModel) // Spring AI ChatModel .systemPrompt(prompt) - .tools(evidenceTools.callbacks()) + .tools(evidenceTools.callbacks()) // 证据工具(如 lookup_knowledge) .interceptors( + // 每次模型调用前:checkActive + 预算 + token 审计 new HarnessModelInterceptor(core, context, modelCallAuditor), + // 每次工具调用:边界投影给 Agent + 完整轨迹进审计 new HarnessToolInterceptor( context, evidenceTools, objectMapper, traceRecorder)) - .hooks(auditHooks) + .hooks(auditHooks) // 如 agent_step 落库 .outputSchema(new DiagnosisDraftOutputSchema(objectMapper).getFormat()) .returnReasoningContents(true) - .parallelToolExecution(false) + .parallelToolExecution(false) // 串行工具:预算与 step 绑定可解释 .releaseThread(true) .build(); } diff --git a/src/main/java/com/superbiz/agent/harness/agent/DiagnosisAgentOutputException.java b/src/main/java/com/superbiz/agent/harness/agent/DiagnosisAgentOutputException.java index 9d01e60..1f21f71 100644 --- a/src/main/java/com/superbiz/agent/harness/agent/DiagnosisAgentOutputException.java +++ b/src/main/java/com/superbiz/agent/harness/agent/DiagnosisAgentOutputException.java @@ -4,12 +4,28 @@ import com.superbiz.agent.harness.progress.DiagnosisProgressSnapshot; import java.util.Objects; +/** + * Diagnosis Agent 输出/执行失败异常。 + * + *

由 {@code DiagnosisAgentUseCase} 抛出;{@code DiagnosisChatExecutor} 仅对 + * {@link #isDraftContractFailure()} 为 true 的 kind 尝试 {@code recoverInvalidDraft}。 + */ public final class DiagnosisAgentOutputException extends RuntimeException { + /** + * Agent 失败细分。 + * + *

前三种(空/JSON/schema)算 Draft 契约失败,有 observed facts 时可 FALLBACK; + * {@link #EXECUTION_FAILED} 是 loop/框架执行失败,不走非法 draft 恢复。 + */ public enum Kind { + /** agent.call 抛错且非受控停止,或未归类执行失败。 */ EXECUTION_FAILED, + /** 模型返回空文本,没有 draft。 */ EMPTY_DRAFT, + /** 输出不是合法 JSON。 */ INVALID_JSON, + /** JSON 可解析但不符合 DiagnosisDraft schema。 */ SCHEMA_INVALID } diff --git a/src/main/java/com/superbiz/agent/harness/agent/DiagnosisAgentUseCase.java b/src/main/java/com/superbiz/agent/harness/agent/DiagnosisAgentUseCase.java index b41eecb..c63be39 100644 --- a/src/main/java/com/superbiz/agent/harness/agent/DiagnosisAgentUseCase.java +++ b/src/main/java/com/superbiz/agent/harness/agent/DiagnosisAgentUseCase.java @@ -21,8 +21,21 @@ import org.springframework.ai.chat.messages.AssistantMessage; import java.nio.charset.StandardCharsets; import java.util.Objects; +/** + * Diagnosis Agent 用例:在 Harness 边界内跑一次 Spring AI Alibaba {@link ReactAgent}。 + * + *

框架负责 ReAct 多轮(模型 ↔ tool_call);Harness 负责: + *

+ * + *

由 {@code DiagnosisChatExecutor} 调用;成功产出 {@link DiagnosisDraft} 草稿(尚未对外发布)。 + */ public final class DiagnosisAgentUseCase { + /** 写入 RunnableConfig metadata,供 hook/interceptor 取回同一 RunContext。 */ public static final String RUN_CONTEXT_METADATA = "runContext"; private final DiagnosisHarnessCore core; @@ -55,6 +68,11 @@ public final class DiagnosisAgentUseCase { progressProjection, "progressProjection must not be null"); } + /** + * 在当前 Run 内执行 Diagnosis Agent。 + * + *

返回 completed(draft) 或 stopped(stopReason);草稿仍需经 Release 才能对外 SUCCESS。 + */ public DiagnosisAgentExecution execute(RunContext context, DiagnosisAgentInput input) { Objects.requireNonNull(context, "context must not be null"); Objects.requireNonNull(input, "input must not be null"); @@ -71,6 +89,7 @@ public final class DiagnosisAgentUseCase { checkLimit("input", inputBytes, limits.maxInputBytes()); core.reserveRunBytes(context, inputBytes); + // 框架线程/配置:把 RunContext 显式挂进 metadata,避免隐式 ThreadLocal RunnableConfig config = RunnableConfig.builder() .threadId(context.runId()) .addMetadata("sessionId", context.sessionId()) @@ -78,12 +97,15 @@ public final class DiagnosisAgentUseCase { .addMetadata(RUN_CONTEXT_METADATA, context) .addMetadata("_stream_", false) .build(); + // 每次 run 新建 Agent,绑定本 run 的 interceptor(预算/投影/审计) ReactAgent agent = agentFactory.create(context); AssistantMessage response; try { + // ★ Spring AI Alibaba:内部多轮 model + tool,直到产出最终文本或被 interceptor 打断 response = agent.call(inputJson, config); core.checkActive(context); } catch (Exception e) { + // 预算/收敛等可控停止 → stopped;其它异常包装为 Agent 输出失败 DiagnosisAgentExecution controlled = controlledExecution(context, e); if (controlled != null) { return controlled; @@ -127,13 +149,37 @@ public final class DiagnosisAgentUseCase { } } + /** + * 把 ReactAgent / interceptor 抛出的「受控停止」从异常栈里捞出来,转成正常返回值。 + * + *

不是笼统的「业务异常 → 正常」;只识别 Harness 约定的可控信号(沿 cause 链查找, + * 因为框架可能再包一层): + *

    + *
  1. {@link DiagnosisCollectionStoppedException}:信息饱和等收集该停, + * 仍强行 tool → {@code stopped(stopReason)},draft=null
  2. + *
  3. {@link RunAbortedException} 且终态为 {@code BUDGET_EXHAUSTED}: + * 标记 progress 预算停 + {@code stopped(BUDGET_LIMIT_REACHED)}
  4. + *
  5. 其它 {@link RunAbortedException}(取消/超时/内部失败终态): + * 原样再抛,不转 stopped——留给 Application 写 CANCELLED/FAILED
  6. + *
  7. {@link BudgetExceededException} 或 lifecycle 已是预算耗尽:同预算 stopped
  8. + *
  9. 都不匹配:返回 {@code null},调用方包装为 {@link DiagnosisAgentOutputException}
  10. + *
+ * + *

与 {@code DiagnosisChatExecutor#recoverInvalidDraft} 的分工: + * 本方法处理「loop 被预算/收敛打断、往往还没有合法 draft」; + * recoverInvalidDraft 处理「loop 跑完了,但输出不是合法 DiagnosisDraft」。 + * + * @return 可交给 Release 的 stopped 执行结果;无法识别时 null + */ private DiagnosisAgentExecution controlledExecution(RunContext context, Throwable failure) { + // 1) 收集侧受控停止(ToolInterceptor 在 SATURATED 后仍收到 tool 请求) DiagnosisCollectionStoppedException stopped = findCause( failure, DiagnosisCollectionStoppedException.class); if (stopped != null) { return DiagnosisAgentExecution.stopped( progressProjection.project(context), stopped.stopReason()); } + // 2) Run 已终态:仅预算耗尽可降为 stopped;取消/超时必须继续上抛 RunAbortedException aborted = findCause(failure, RunAbortedException.class); if (aborted != null) { if (aborted.termination().state() == RunState.BUDGET_EXHAUSTED) { @@ -143,15 +189,18 @@ public final class DiagnosisAgentUseCase { } throw aborted; } + // 3) 预算异常(可能尚未被包成 RunAborted,或 lifecycle 已先置 BUDGET_EXHAUSTED) BudgetExceededException budget = findCause(failure, BudgetExceededException.class); if (budget != null || context.lifecycle().state() == RunState.BUDGET_EXHAUSTED) { context.progress().markBudgetLimitReached(); return DiagnosisAgentExecution.stopped( progressProjection.project(context), DiagnosisStopReason.BUDGET_LIMIT_REACHED); } + // 4) 未知失败:交给外层当 Agent 执行失败 return null; } + /** 沿 cause 链查找目标异常类型(框架包装后根因仍在链上)。 */ private static T findCause(Throwable failure, Class type) { Throwable current = failure; while (current != null) { diff --git a/src/main/java/com/superbiz/agent/harness/application/ChatApplicationStatus.java b/src/main/java/com/superbiz/agent/harness/application/ChatApplicationStatus.java index ca00414..8be48dd 100644 --- a/src/main/java/com/superbiz/agent/harness/application/ChatApplicationStatus.java +++ b/src/main/java/com/superbiz/agent/harness/application/ChatApplicationStatus.java @@ -1,11 +1,28 @@ package com.superbiz.agent.harness.application; +/** + * Chat 应用进度状态(SSE status 事件),不是错误码。 + * + *

只表示「当前走到哪一阶段」,成功/失败结局看 ReleaseOutcome / ChatFailureCode。 + */ public enum ChatApplicationStatus { + + /** Intent Router 识别请求类型。 */ ROUTING("正在识别请求类型"), + + /** 系统闲聊路径生成回答。 */ SYSTEM_RESPONDING("正在生成回答"), + + /** 知识库检索中。 */ KNOWLEDGE_SEARCHING("正在查询知识库"), + + /** 知识答案整理中。 */ KNOWLEDGE_ANSWERING("正在整理知识答案"), + + /** 诊断 Agent 收集证据 / 写草稿(ReAct 循环中)。 */ DIAGNOSIS_RUNNING("正在收集诊断证据"), + + /** Evidence / Semantic 门控或无效 draft 的安全发布阶段。 */ SAFETY_VALIDATING("正在进行安全校验"); private final String message; @@ -14,6 +31,7 @@ public enum ChatApplicationStatus { this.message = message; } + /** 面向用户的简短进度文案。 */ public String message() { return message; } diff --git a/src/main/java/com/superbiz/agent/harness/application/ChatApplicationUseCase.java b/src/main/java/com/superbiz/agent/harness/application/ChatApplicationUseCase.java index 59e6224..178b850 100644 --- a/src/main/java/com/superbiz/agent/harness/application/ChatApplicationUseCase.java +++ b/src/main/java/com/superbiz/agent/harness/application/ChatApplicationUseCase.java @@ -23,16 +23,34 @@ import java.util.Optional; import java.util.function.Supplier; import java.util.regex.Pattern; +/** + * Chat 应用编排(Harness Application 层):一次请求从创建到公开结果的负责人。 + * + *

调用链: + *

+ *   Controller → execute()
+ *     → core.startRun()           // 建 RunContext 边界
+ *     → router.route()            // Intent Router(Spring AI 单次模型调用)
+ *     → executePath(intent)       // 按意图分叉
+ *         DIAGNOSIS → DiagnosisChatExecutor(Agent → Release)
+ *     → completePath / persistFinish
+ * 
+ * + *

它编排路径,但不做业务根因判断;也不把 HTTP/SSE 细节塞进 Core。 + */ public final class ChatApplicationUseCase { private static final Pattern SAFE_ID = Pattern.compile("[A-Za-z0-9][A-Za-z0-9._-]{0,63}"); + /** 执行规则:预算、deadline、取消、终态(first-terminal-wins)。 */ private final DiagnosisHarnessCore core; private final Supplier sessionIdSupplier; private final ChatRunStore runStore; + /** 意图路由:只产出 IntentType,不执行诊断。 */ private final IntentRouting router; private final SystemChatOperation systemChat; private final KnowledgeQueryOperation knowledgeQuery; + /** 诊断子路径:Agent 收集证据写草稿 + Release 门控发布。 */ private final DiagnosisOperation diagnosis; private final ObjectMapper objectMapper; private final DiagnosisTraceRecorder traceRecorder; @@ -74,12 +92,18 @@ public final class ChatApplicationUseCase { return execute(request, ChatApplicationObserver.noop()); } + /** + * 一次 Chat 请求的主编排。 + * + *

observer 通常是 SSE session:onStarted 推 metadata,onStatus 推进度。 + */ public ChatApplicationResult execute(ChatApplicationRequest request, ChatApplicationObserver observer) { Objects.requireNonNull(request, "request must not be null"); Objects.requireNonNull(observer, "observer must not be null"); String sessionId = resolveSessionId(request.sessionId()); + // 会话级上下文:上一路由结果 + 上一轮诊断摘要(给 Router / Diagnosis 用) Optional history; Optional previousTurn; try { @@ -90,14 +114,19 @@ public final class ChatApplicationUseCase { ChatFailureCode.RUN_PERSISTENCE_FAILED, "无法读取会话上下文,请稍后重试", exception); } + + // ★ 步骤1:创建 Run 边界(runId/deadline/budget/cancel/lifecycle),显式向下传递 RunContext context = core.startRun(sessionId); long startedNanos = System.nanoTime(); IntentType intent = null; try { + // ★ 步骤2:落库 RUN 开始 + 对外/对内可观测 persistStart(context, request.query()); traceRecorder.record(TraceAuditEvents.runStarted(context)); - observer.onStarted(new CoreRunControl(core, context)); + observer.onStarted(new CoreRunControl(core, context)); // SSE metadata + 取消句柄 observer.onStatus(ChatApplicationStatus.ROUTING); + + // ★ 步骤3:意图路由——只回答「走哪条应用分支」,不调业务工具 intent = router.route(context, new IntentRouterInput( request.query(), history.map(RoutingHistory::intent).orElse(null), @@ -105,8 +134,11 @@ public final class ChatApplicationUseCase { traceRecorder.record(TraceAuditEvents.routingDecision(context, intent)); persistIntent(context.runId(), intent); + // ★ 步骤4:按 intent 分叉执行(编排决策点) PathResult path = executePath( intent, context, request.query(), previousTurn.orElse(null), observer); + + // ★ 步骤5:写入 Run 终态(成功)并持久化公开结果 completePath(context, intent, path); String safeJson = write(path.content()); persistFinish(context, intent, path.outcome(), safeJson, @@ -117,6 +149,7 @@ public final class ChatApplicationUseCase { context.sessionId(), context.runId(), intent, path.outcome(), path.content().contentType(), path.content()); } catch (RuntimeException exception) { + // 统一失败出口:尽量落终态,再映射成安全的对外失败码 ReleaseOutcome terminal = terminalOutcome(context); try { runStore.finish(context, intent, terminal, null, null, @@ -130,6 +163,12 @@ public final class ChatApplicationUseCase { } } + /** + * 路径执行完成后的 Run 终态处理。 + * + *

诊断预算耗尽且已由子路径产出 FALLBACK 时,不二次 completeSuccess + *(lifecycle 已是 BUDGET_EXHAUSTED)。 + */ private void completePath(RunContext context, IntentType intent, PathResult path) { if (path.handledBudgetTermination()) { if (intent != IntentType.DIAGNOSIS @@ -144,6 +183,12 @@ public final class ChatApplicationUseCase { core.completeSuccess(context); } + /** + * 根据 Router 产出的 intent 选择执行分支。 + * + *

这是 Application 的核心编排决策:Router 只给枚举,分支执行权在这里。 + * DIAGNOSIS 继续进入 {@link com.superbiz.agent.harness.application.executor.DiagnosisChatExecutor}。 + */ private PathResult executePath(IntentType intent, RunContext context, String query, @@ -162,6 +207,7 @@ public final class ChatApplicationUseCase { ReleaseOutcome.SUCCESS, knowledgeQuery.execute(context, query), null, false); } case DIAGNOSIS -> { + // 诊断子编排:Agent(ReAct) → Guard/Release,不在本类展开 DiagnosisExecutionResult result = diagnosis.execute( context, query, previousTurn, observer::onStatus); yield new PathResult(result.outcome(), result.content(), result.publishedResult(), diff --git a/src/main/java/com/superbiz/agent/harness/application/ChatFailureCode.java b/src/main/java/com/superbiz/agent/harness/application/ChatFailureCode.java index 0162aaa..441c26b 100644 --- a/src/main/java/com/superbiz/agent/harness/application/ChatFailureCode.java +++ b/src/main/java/com/superbiz/agent/harness/application/ChatFailureCode.java @@ -1,11 +1,38 @@ package com.superbiz.agent.harness.application; +/** + * Chat 应用层对外失败码(给 SSE failure / 客户端看的粗粒度原因)。 + * + *

层级:Application 出口。不要和下列内部码混淆: + *

+ * + *

由 {@link ChatApplicationUseCase} 在 catch 中映射,文案对用户安全,不暴露内部堆栈。 + */ public enum ChatFailureCode { + + /** Intent Router 不可用或输出无法解析,无法决定走哪条路径。 */ ROUTING_UNAVAILABLE, + + /** 系统闲聊分支暂时无法回答。 */ SYSTEM_CHAT_UNAVAILABLE, + + /** 知识问答分支暂时无法完成检索/作答。 */ KNOWLEDGE_UNAVAILABLE, + + /** 诊断分支整体不可用(非具体 FALLBACK 细分)。 */ DIAGNOSIS_UNAVAILABLE, + + /** session/run 读写落库失败(start/intent/finish 等)。 */ RUN_PERSISTENCE_FAILED, + + /** Run 被取消(客户端断开、用户取消等),对应 RunState.CANCELLED。 */ RUN_CANCELLED, + + /** 未归类的内部失败;兜底码,应尽量少用、并靠 Trace 排查。 */ INTERNAL_FAILURE } diff --git a/src/main/java/com/superbiz/agent/harness/application/executor/DiagnosisChatExecutor.java b/src/main/java/com/superbiz/agent/harness/application/executor/DiagnosisChatExecutor.java index 0ce8e2b..0dae689 100644 --- a/src/main/java/com/superbiz/agent/harness/application/executor/DiagnosisChatExecutor.java +++ b/src/main/java/com/superbiz/agent/harness/application/executor/DiagnosisChatExecutor.java @@ -23,9 +23,22 @@ import com.superbiz.agent.harness.release.DiagnosisReleaseUseCase; import java.util.Objects; import java.util.function.Consumer; +/** + * 诊断路径子编排(Application 在 intent=DIAGNOSIS 时的执行器)。 + * + *

两段式流水线,本身不实现 ReAct 循环: + *

    + *
  1. {@link DiagnosisAgentUseCase}:Spring AI Alibaba ReactAgent 收集证据并写 Draft
  2. + *
  3. {@link DiagnosisReleaseUseCase}:Evidence/Semantic 门控 + 发布 SUCCESS 或 FALLBACK
  4. + *
+ * + *

由 {@code ChatApplicationUseCase.executePath} 在路由完成后调用。 + */ public final class DiagnosisChatExecutor implements DiagnosisOperation { + /** Agent 接入:内部创建 ReactAgent 并 agent.call。 */ private final DiagnosisAgentUseCase diagnosisAgent; + /** 验证与发布:未证明的结论不能 SUCCESS 出口。 */ private final DiagnosisReleaseUseCase releaseUseCase; private final PublishedResultPolicy publishedPolicy; private final DiagnosisTraceRecorder traceRecorder; @@ -46,27 +59,38 @@ public final class DiagnosisChatExecutor implements DiagnosisOperation { this.traceRecorder = Objects.requireNonNull(traceRecorder, "traceRecorder must not be null"); } + /** + * 诊断子路径:Agent 出草稿 → Release 裁决对外形态。 + * + * @param statusSink 回写 SSE 进度(DIAGNOSIS_RUNNING / SAFETY_VALIDATING) + */ @Override public DiagnosisExecutionResult execute(RunContext context, String query, PreviousTurn previousTurn, Consumer statusSink) { Objects.requireNonNull(statusSink, "statusSink must not be null"); + // SSE:诊断 Agent 运行中(内部可能多轮模型 + 工具) statusSink.accept(ChatApplicationStatus.DIAGNOSIS_RUNNING); DiagnosisAgentExecution execution; try { + // ★ 段1:ReAct Agent——规划 tool_call、收证据、产出 DiagnosisDraft execution = diagnosisAgent.execute( context, new DiagnosisAgentInput(query, previousTurn)); } catch (DiagnosisAgentOutputException exception) { + // Draft 契约失败:有观察事实可降级 FALLBACK,否则上抛 return recoverInvalidDraft(context, exception, statusSink); } + // SSE:进入门控(Evidence 结构 + Semantic 语义) statusSink.accept(ChatApplicationStatus.SAFETY_VALIDATING); + // ★ 段2:验证与发布——决定 SUCCESS 报告还是 SAFE_FALLBACK DiagnosisReleaseResult released = releaseUseCase.execute(context, query, execution); if (released.outcome() == ReleaseOutcome.FALLBACK) { return new DiagnosisExecutionResult( ReleaseOutcome.FALLBACK, new FallbackContent(released.fallback()), null, + // 预算耗尽导致的 FALLBACK:上层 completePath 不再 completeSuccess execution.stopReason() == com.superbiz.agent.harness.progress.DiagnosisStopReason.BUDGET_LIMIT_REACHED); } @@ -80,22 +104,52 @@ public final class DiagnosisChatExecutor implements DiagnosisOperation { published); } + /** + * Agent「已经跑完」但最终文本不是合法 {@code DiagnosisDraft} 时的恢复路径。 + * + *

触发条件(见 {@link DiagnosisAgentOutputException#isDraftContractFailure()}): + * 空输出 / 非法 JSON / schema 不符。真正的执行崩溃({@code EXECUTION_FAILED})不进恢复,直接再抛。 + * + *

除「记审计 + 包装 FALLBACK 返回」外,还承担: + *

    + *
  1. 失败分类闸门:只有 Draft 契约失败可恢复;其它 Agent 异常保持失败语义上抛
  2. + *
  3. fail-closed 安全门:必须 {@code progress.hasObservedFacts()}, + * 否则再抛——禁止在「什么都没查到」时用模板假装一次有依据的降级
  4. + *
  5. 丢弃非法 draft 正文:不把空串/烂 JSON/错 schema 送进 Evidence/Semantic Guard, + * 也不可能走出 SUCCESS;发布只基于已验真 progress(canonical 工具事实)
  6. + *
  7. 走专用 Release 入口:{@link DiagnosisReleaseUseCase#releaseInvalidDraft}, + * 而不是 {@code execute(context, query, execution)}——因为没有可校验的 draft
  8. + *
  9. SSE 阶段对齐:推 {@code SAFETY_VALIDATING},与正常门控路径对外进度一致
  10. + *
  11. 固定对外形态:{@code FALLBACK + FallbackContent},{@code publishedResult=null} + * (不落可回放的成功发布快照)
  12. + *
+ * + *

与 {@code DiagnosisAgentUseCase#controlledExecution} 的分工: + * controlledExecution 处理 loop 中途被预算/收集收敛打断(常无 draft,转 stopped); + * 本方法处理 loop 结束后输出契约失败(有/无 progress 决定 FALLBACK 还是失败)。 + */ private DiagnosisExecutionResult recoverInvalidDraft( RunContext context, DiagnosisAgentOutputException exception, Consumer statusSink) { + // 1) 仅 Draft 契约失败可恢复;EXECUTION_FAILED 等保持原异常 if (!exception.isDraftContractFailure()) { throw exception; } + // 2) 是否已有可发布的观察事实(来自工具 canonical,不是模型胡写的 draft) boolean hasProgress = exception.progress().hasObservedFacts(); + // 3) 审计:记录 kind / 输出字节 / 有无 progress,便于区分「模型格式烂」vs「彻底空跑」 traceRecorder.record(TraceAuditEvents.agentDraftInvalid( context, exception.kind(), exception.outputBytes(), hasProgress)); + // 4) 无安全事实 → fail closed,交给 Application 写 FAILED if (!hasProgress) { throw exception; } + // 5) 有事实:对齐 SSE 阶段,走「无合法 draft」专用发布(INSUFFICIENT_EVIDENCE 类 FALLBACK) statusSink.accept(ChatApplicationStatus.SAFETY_VALIDATING); DiagnosisReleaseResult released = releaseUseCase.releaseInvalidDraft( context, exception.progress()); + // 6) 对外只给安全 Fallback;非法 draft 正文永不出现在 content 里 return new DiagnosisExecutionResult( ReleaseOutcome.FALLBACK, new FallbackContent(released.fallback()), diff --git a/src/main/java/com/superbiz/agent/harness/application/routing/IntentRouter.java b/src/main/java/com/superbiz/agent/harness/application/routing/IntentRouter.java index da8663f..5995cf4 100644 --- a/src/main/java/com/superbiz/agent/harness/application/routing/IntentRouter.java +++ b/src/main/java/com/superbiz/agent/harness/application/routing/IntentRouter.java @@ -29,12 +29,26 @@ import java.util.Objects; import java.util.Set; import java.util.function.Consumer; +/** + * 意图路由:判断本轮走 SYSTEM_CHAT / KNOWLEDGE_QUERY / DIAGNOSIS。 + * + *

在 Harness 中的位置:Application 主编排里的「路由节点」,不是业务执行器。 + *

+ * + *

只返回 {@link IntentType};由 {@code ChatApplicationUseCase.executePath} 决定后续分支。 + */ public final class IntentRouter implements IntentRouting { + /** 输出契约:JSON 只能有 intent 一个字段。 */ private static final Set OUTPUT_FIELDS = Set.of("intent"); private final DiagnosisHarnessCore core; private final HarnessRetryExecutor retryExecutor; + /** 统一模型调用壳:入账 token、受 RunContext 约束。 */ private final GuardModelCall modelCall; private final ObjectMapper objectMapper; private final IntentRouterLimits limits; @@ -69,6 +83,11 @@ public final class IntentRouter implements IntentRouting { this.systemPrompt = IntentRouterPrompt.load(); } + /** + * 对当前 query(+ 可选历史 intent/query)做一次意图分类。 + * + * @return 仅 IntentType;Application 据此 switch 到对应 Executor + */ @Override public IntentType route(RunContext context, IntentRouterInput input) { Objects.requireNonNull(context, "context must not be null"); @@ -78,11 +97,14 @@ public final class IntentRouter implements IntentRouting { if (bytes > limits.maxInputBytes()) { throw new IntentRoutingException(new IllegalArgumentException("Router input exceeded limit")); } + // 先占 Run 字节预算,再调模型 core.reserveRunBytes(context, bytes); + // Spring AI Prompt:system 规则 + user 侧结构化输入 JSON Prompt prompt = new Prompt(List.of( new SystemMessage(systemPrompt), new UserMessage(json))); long started = System.nanoTime(); try { + // 技术失败可按 policy 重试;取消/预算耗尽不能被重试吞掉 return retryExecutor.execute( context, context.retryPolicies().intentRouter(), @@ -103,6 +125,7 @@ public final class IntentRouter implements IntentRouting { } } + /** 严格解析:字段集合必须恰好为 {intent},值必须是 IntentType 枚举名。 */ private IntentType parse(String output) { JsonNode root; try { diff --git a/src/main/java/com/superbiz/agent/harness/contract/EvidenceStatus.java b/src/main/java/com/superbiz/agent/harness/contract/EvidenceStatus.java index d279d5e..8747f27 100644 --- a/src/main/java/com/superbiz/agent/harness/contract/EvidenceStatus.java +++ b/src/main/java/com/superbiz/agent/harness/contract/EvidenceStatus.java @@ -1,7 +1,23 @@ package com.superbiz.agent.harness.contract; +/** + * 单次工具调用在「证据语义」上的结果状态(工具契约 / 进度)。 + * + *

与 {@link InvocationStatus} 分工: + *

+ * NO_EVIDENCE 仍可能是 success 的工具执行(查了但空),常记为信息无增益。 + */ public enum EvidenceStatus { + + /** 返回了可被引用的证据块。 */ EVIDENCE_FOUND, + + /** 执行完成但范围内无证据(空结果,不是必然系统故障)。 */ NO_EVIDENCE, + + /** 工具侧错误或无法形成合法证据观察。 */ ERROR } diff --git a/src/main/java/com/superbiz/agent/harness/contract/FallbackType.java b/src/main/java/com/superbiz/agent/harness/contract/FallbackType.java index 79d29d8..f0b5df1 100644 --- a/src/main/java/com/superbiz/agent/harness/contract/FallbackType.java +++ b/src/main/java/com/superbiz/agent/harness/contract/FallbackType.java @@ -1,10 +1,43 @@ package com.superbiz.agent.harness.contract; +/** + * FALLBACK 细分类型:在 {@link ReleaseOutcome#FALLBACK} 时说明「为什么降级」。 + * + *

层级:Release 内容契约(SafeFallback.type)。 + * 出现在对外 Fallback 载荷与 Trace 的 RELEASE_DECISION 中。 + * + *

注意: + *

+ */ public enum FallbackType { + + /** Evidence Guard:引用/结构校验未通过(含 repair 后仍失败)。 */ EVIDENCE_VALIDATION_FAILED, + + /** Semantic Guard:结论不被证据支撑(verdict=UNSUPPORTED)。 */ SEMANTIC_UNSUPPORTED, + + /** Semantic Guard:技术上无法完成语义评审(超时/不可用等),不发布根因。 */ SEMANTIC_UNAVAILABLE, + + /** + * 历史/枚举保留:预算耗尽类降级。 + * 当前主路径预算受控停止有安全进展时,通常映射为 {@link #INSUFFICIENT_EVIDENCE}。 + */ BUDGET_EXHAUSTED, + + /** + * 已做有限检查,但证据不足以确认根因。 + * 典型来源:信息饱和 stopped、预算 stopped 有 facts、非法 draft 有 progress 的 + * {@code releaseInvalidDraft} / {@code releaseControlledStop}。 + */ INSUFFICIENT_EVIDENCE, + + /** 缺少定向诊断所需上下文(对象/时间窗等),尚未形成有效查询进展。 */ MISSING_REQUIRED_CONTEXT } diff --git a/src/main/java/com/superbiz/agent/harness/contract/InvocationStatus.java b/src/main/java/com/superbiz/agent/harness/contract/InvocationStatus.java index 43cff22..446cf54 100644 --- a/src/main/java/com/superbiz/agent/harness/contract/InvocationStatus.java +++ b/src/main/java/com/superbiz/agent/harness/contract/InvocationStatus.java @@ -1,7 +1,19 @@ package com.superbiz.agent.harness.contract; +/** + * 工具调用在 canonical store 中的生命周期状态。 + * + *

READY 的记录才可作为 Evidence Guard 引用目标; + * PROJECTING 表示尚未完成投影落库;ERROR 表示本调用失败收尾。 + */ public enum InvocationStatus { + + /** 已 begin,正在执行/投影,尚不可作为最终引用。 */ PROJECTING, + + /** 投影完成且可引用(agentResult + evidenceStatus 已固化)。 */ READY, + + /** 本调用以错误结束。 */ ERROR } diff --git a/src/main/java/com/superbiz/agent/harness/contract/ReleaseOutcome.java b/src/main/java/com/superbiz/agent/harness/contract/ReleaseOutcome.java index 73ec17a..ba788cc 100644 --- a/src/main/java/com/superbiz/agent/harness/contract/ReleaseOutcome.java +++ b/src/main/java/com/superbiz/agent/harness/contract/ReleaseOutcome.java @@ -1,8 +1,28 @@ package com.superbiz.agent.harness.contract; +/** + * 诊断发布裁决结果(Release 层):这一次诊断「对外给什么结局」。 + * + *

层级:Release / Application 结果。注意与 {@link com.superbiz.agent.harness.core.RunState} 不同: + *

+ * + *

常见组合:RunState=BUDGET_EXHAUSTED 且有安全进展 → ReleaseOutcome=FALLBACK; + * 无进展 → 往往落到 FAILED。 + */ public enum ReleaseOutcome { + + /** 通过门控,可发布 DIAGNOSIS_REPORT。 */ SUCCESS, + + /** 不发布根因报告,发布 SafeFallback(见 {@link FallbackType})。 */ FALLBACK, + + /** 无法形成可发布的安全内容(含无 observed facts 的预算停等)。 */ FAILED, + + /** 运行被取消,通常不再保证客户端能收到完整 done。 */ CANCELLED } diff --git a/src/main/java/com/superbiz/agent/harness/contract/SemanticVerdict.java b/src/main/java/com/superbiz/agent/harness/contract/SemanticVerdict.java index da8f0d0..5f67f0f 100644 --- a/src/main/java/com/superbiz/agent/harness/contract/SemanticVerdict.java +++ b/src/main/java/com/superbiz/agent/harness/contract/SemanticVerdict.java @@ -1,6 +1,17 @@ package com.superbiz.agent.harness.contract; +/** + * Semantic Guard 对「结论是否被证据支撑」的裁决。 + * + *

SUPPORTED → 可走向 Release SUCCESS; + * UNSUPPORTED → 通常 FallbackType.SEMANTIC_UNSUPPORTED。 + * (Guard 技术失败走 SEMANTIC_UNAVAILABLE,不一定落本枚举。) + */ public enum SemanticVerdict { + + /** 语义上认为草稿结论被已验证证据支持。 */ SUPPORTED, + + /** 证据不足以支持当前根因/结论表述。 */ UNSUPPORTED } diff --git a/src/main/java/com/superbiz/agent/harness/contract/SseOutcome.java b/src/main/java/com/superbiz/agent/harness/contract/SseOutcome.java index e61f888..5b99879 100644 --- a/src/main/java/com/superbiz/agent/harness/contract/SseOutcome.java +++ b/src/main/java/com/superbiz/agent/harness/contract/SseOutcome.java @@ -1,7 +1,19 @@ package com.superbiz.agent.harness.contract; +/** + * SSE {@code done} 事件上的粗粒度结局(协议层)。 + * + *

与 {@link ReleaseOutcome} 对齐的对外三态;取消场景连接可能已断, + * 不一定能发出 done(CANCELLED),故此处通常只有 SUCCESS / FALLBACK / FAILED。 + */ public enum SseOutcome { + + /** 对应成功报告 content。 */ SUCCESS, + + /** 对应安全降级 content。 */ FALLBACK, + + /** 对应 failure 事件或失败 done(实现以 ChatSseSession 为准)。 */ FAILED } diff --git a/src/main/java/com/superbiz/agent/harness/core/BudgetKind.java b/src/main/java/com/superbiz/agent/harness/core/BudgetKind.java index f5edb56..c6f96c7 100644 --- a/src/main/java/com/superbiz/agent/harness/core/BudgetKind.java +++ b/src/main/java/com/superbiz/agent/harness/core/BudgetKind.java @@ -1,11 +1,31 @@ package com.superbiz.agent.harness.core; +/** + * 硬预算维度:哪一类资源超限(Core / RunBudget)。 + * + *

超限时抛 {@link BudgetExceededException},并常将 RunState 置为 {@code BUDGET_EXHAUSTED}。 + * 这是资源账,不是「信息是否还有增益」(后者看 progress / DiagnosisStopReason)。 + */ public enum BudgetKind { + + /** 整次 Run 允许的模型调用次数。 */ MODEL_CALLS, + + /** 整次 Run 允许的工具调用总次数。 */ TOOL_CALLS, + + /** 单个工具名的调用次数上限。 */ TOOL_CALLS_PER_TOOL, + + /** 累计 input tokens。 */ INPUT_TOKENS, + + /** 累计 output tokens。 */ OUTPUT_TOKENS, + + /** 累计 total tokens。 */ TOTAL_TOKENS, + + /** Run 级 UTF-8 字节预算(输入、工具结果、draft 等占用)。 */ RUN_BYTES } diff --git a/src/main/java/com/superbiz/agent/harness/core/DiagnosisHarnessCore.java b/src/main/java/com/superbiz/agent/harness/core/DiagnosisHarnessCore.java index 9db5c86..a58d63f 100644 --- a/src/main/java/com/superbiz/agent/harness/core/DiagnosisHarnessCore.java +++ b/src/main/java/com/superbiz/agent/harness/core/DiagnosisHarnessCore.java @@ -10,6 +10,17 @@ import java.time.Instant; import java.util.Objects; import java.util.function.Supplier; +/** + * Harness Core:一次 Run 的执行规则(不是业务编排器)。 + * + *

与 Application 的分工: + *

+ * + *

不依赖 Spring AI;Router/Agent/Tool 在每次消耗前调用本类 API。 + */ public final class DiagnosisHarnessCore { private final Clock clock; @@ -66,6 +77,12 @@ public final class DiagnosisHarnessCore { return startRun(sessionId, runIdSupplier.get()); } + /** + * 创建本次请求的 RunContext(显式上下文,不用 ThreadLocal)。 + * + *

携带:身份(session/run)、deadline、预算、取消、生命周期、模型账本、进度收敛器。 + * 后续所有模型与 Tool 调用必须传入同一个 context。 + */ public RunContext startRun(String sessionId, String runId) { Instant deadline = clock.instant().plus(maxRunDuration); RunCancellation cancellation = new RunCancellation(); @@ -81,10 +98,15 @@ public final class DiagnosisHarnessCore { lifecycle, new DiagnosisProgressTracker(stopAfterConsecutiveNoGain, stopAfterConsecutiveProgressProtocolViolations)); + // 取消信号与终态联动:第一个终态获胜,迟到结果不能覆盖 cancellation.onCancel(reason -> lifecycle.finish(terminalState(reason), reason.name())); return context; } + /** + * 执行前闸门:已终态 / 超时 / 已取消 → 抛 RunAbortedException。 + * Router、Agent、Tool、Guard 在关键步骤前都会走到这里。 + */ public void checkActive(RunContext context) { Objects.requireNonNull(context, "context must not be null"); context.lifecycle().termination().ifPresent(termination -> { @@ -102,11 +124,13 @@ public final class DiagnosisHarnessCore { } } + /** 模型调用前:检查 active + 预留模型调用预算。 */ public void beforeModelCall(RunContext context) { checkActive(context); applyBudget(context, context.budget()::reserveModelCall); } + /** 工具调用前:检查 active + 按工具名预留工具预算。 */ public void beforeToolCall(RunContext context, String toolName) { checkActive(context); applyBudget(context, () -> context.budget().reserveToolCall(toolName)); diff --git a/src/main/java/com/superbiz/agent/harness/core/RunCancellationReason.java b/src/main/java/com/superbiz/agent/harness/core/RunCancellationReason.java index 1f5cb31..91a233b 100644 --- a/src/main/java/com/superbiz/agent/harness/core/RunCancellationReason.java +++ b/src/main/java/com/superbiz/agent/harness/core/RunCancellationReason.java @@ -1,9 +1,30 @@ package com.superbiz.agent.harness.core; +/** + * 触发 Run 取消 / 与取消联动写终态的原因(Core)。 + * + *

由 {@link RunCancellation#cancel} 携带;Core 会映射到对应 {@link RunState}: + *

+ */ public enum RunCancellationReason { + + /** SSE/HTTP 客户端断开。 */ CLIENT_DISCONNECTED, + + /** 显式用户取消(若产品支持)。 */ USER_REQUESTED, + + /** 墙钟超过 Run deadline。 */ DEADLINE_EXCEEDED, + + /** 硬预算耗尽(与 BudgetExceededException 联动)。 */ BUDGET_EXHAUSTED, + + /** 应用判定内部失败并收尾。 */ INTERNAL_FAILURE } diff --git a/src/main/java/com/superbiz/agent/harness/core/RunState.java b/src/main/java/com/superbiz/agent/harness/core/RunState.java index 5f0f515..da20fa6 100644 --- a/src/main/java/com/superbiz/agent/harness/core/RunState.java +++ b/src/main/java/com/superbiz/agent/harness/core/RunState.java @@ -1,11 +1,34 @@ package com.superbiz.agent.harness.core; +/** + * 一次 Run 的内存生命周期状态(Core / RunLifecycle)。 + * + *

层级:执行控制面。first-terminal-wins:第一个写入的终态不可被迟到结果覆盖。 + * + *

不要直接当成用户看到的结果: + *

+ */ public enum RunState { + + /** 仍可执行(未终态)。 */ RUNNING(false), + + /** 正常完成(Application completeSuccess)。 */ SUCCESS(true), + + /** 内部失败终态(completeFailure 等)。 */ FAILED(true), + + /** 取消终态(客户端断开、用户请求等)。 */ CANCELLED(true), + + /** 超过 Run deadline。 */ TIMED_OUT(true), + + /** 模型/工具/Token/字节等硬预算耗尽。可与 ReleaseOutcome.FALLBACK 并存。 */ BUDGET_EXHAUSTED(true); private final boolean terminal; @@ -14,6 +37,7 @@ public enum RunState { this.terminal = terminal; } + /** 是否已是终态(终态后 checkActive 会 abort)。 */ public boolean isTerminal() { return terminal; } diff --git a/src/main/java/com/superbiz/agent/harness/guard/evidence/EvidenceViolationCode.java b/src/main/java/com/superbiz/agent/harness/guard/evidence/EvidenceViolationCode.java index 4894627..6e73e38 100644 --- a/src/main/java/com/superbiz/agent/harness/guard/evidence/EvidenceViolationCode.java +++ b/src/main/java/com/superbiz/agent/harness/guard/evidence/EvidenceViolationCode.java @@ -1,25 +1,76 @@ package com.superbiz.agent.harness.guard.evidence; +/** + * Evidence Guard 结构/引用违规码(规则门控,通常不调大模型)。 + * + *

层级:Guard。校验 DiagnosisDraft 是否引用真实 tool_call、字段是否齐全等。 + * 失败时可尝试 EvidenceRepair;仍失败则 FallbackType.EVIDENCE_VALIDATION_FAILED。 + * + *

与 SemanticVerdict 不同:这里只问「引用是否真实、结构是否合法」, + * 不问「结论语义是否夸大」。 + */ public enum EvidenceViolationCode { + + /** 缺少整份 draft。 */ DRAFT_MISSING, + + /** 缺少 analysis 列表或为空。 */ ANALYSIS_MISSING, + + /** analysis 条目缺少 id。 */ ANALYSIS_ID_MISSING, + + /** analysis id 重复。 */ ANALYSIS_ID_DUPLICATE, + + /** analysis 缺少 kind。 */ ANALYSIS_KIND_MISSING, + + /** analysis 缺少正文。 */ ANALYSIS_TEXT_MISSING, + + /** 需要工具引用但未提供。 */ TOOL_REFERENCE_MISSING, + + /** 报告级必填文本缺失。 */ REPORT_TEXT_MISSING, + + /** 结论等引用了 analysis,但引用列表缺失。 */ ANALYSIS_REFERENCE_MISSING, + + /** 引用了不存在的 analysis id。 */ ANALYSIS_REFERENCE_UNKNOWN, + + /** 缺少 limitations(诊断契约要求声明范围/缺口)。 */ LIMITATIONS_MISSING, + + /** tool 引用字段非法。 */ TOOL_REFERENCE_INVALID, + + /** 引用的 tool_call 在本 Run 账本中不存在。 */ INVOCATION_MISSING, + + /** tool_call_id 与账本记录不匹配。 */ INVOCATION_ID_MISMATCH, + + /** 该 invocation 状态不可被引用(非 READY 等)。 */ INVOCATION_NOT_REFERENCABLE, + + /** 证据 kind 与工具/投影约定不符。 */ EVIDENCE_KIND_MISMATCH, + + /** 引用了不支持的工具名。 */ TOOL_UNSUPPORTED, + + /** 投影结果不可用于校验。 */ PROJECTION_INVALID, + + /** 投影 id 与引用不一致。 */ PROJECTION_ID_MISMATCH, + + /** 投影状态与引用期望不一致。 */ PROJECTION_STATUS_MISMATCH, + + /** 从 canonical store 查找调用记录失败。 */ CANONICAL_LOOKUP_FAILED } diff --git a/src/main/java/com/superbiz/agent/harness/progress/DiagnosisCollectionState.java b/src/main/java/com/superbiz/agent/harness/progress/DiagnosisCollectionState.java index 9a903b9..9977062 100644 --- a/src/main/java/com/superbiz/agent/harness/progress/DiagnosisCollectionState.java +++ b/src/main/java/com/superbiz/agent/harness/progress/DiagnosisCollectionState.java @@ -1,6 +1,16 @@ package com.superbiz.agent.harness.progress; +/** + * 证据收集阶段状态机(Progress 层,与 RunState 独立)。 + * + *

SATURATED 表示「再查也难有信息增益」,会触发 stop_required; + * 不等于 Run 已经 BUDGET_EXHAUSTED 或对外已经 FALLBACK。 + */ public enum DiagnosisCollectionState { + + /** 仍允许在预算内发起工具调用(需遵守进度协议)。 */ COLLECTING, + + /** 已饱和:交付一次 STOP 指示后,再要工具可抛 DiagnosisCollectionStoppedException。 */ SATURATED } diff --git a/src/main/java/com/superbiz/agent/harness/progress/DiagnosisStopReason.java b/src/main/java/com/superbiz/agent/harness/progress/DiagnosisStopReason.java index c9e3d34..aee0537 100644 --- a/src/main/java/com/superbiz/agent/harness/progress/DiagnosisStopReason.java +++ b/src/main/java/com/superbiz/agent/harness/progress/DiagnosisStopReason.java @@ -1,7 +1,26 @@ package com.superbiz.agent.harness.progress; +/** + * 诊断「证据收集」为何受控停止(Progress 层)。 + * + *

层级:Agent 收集收敛,不是对外 FallbackType。 + * 出现在 {@code DiagnosisAgentExecution.stopped(...)} 与 Tool 侧 stop_required 观察里。 + * + *

与预算的关系: + *

+ * 有安全 observed facts 时,Release 常映射为 FallbackType.INSUFFICIENT_EVIDENCE。 + */ public enum DiagnosisStopReason { + + /** 连续无信息增益(或等价饱和策略),收集状态进入 SATURATED。 */ INFORMATION_SATURATED, + + /** 模型次数/工具次数/Token/bytes 等硬预算触顶。 */ BUDGET_LIMIT_REACHED, + + /** 进度协议连续违规达到阈值(envelope / previous_observation 等)。 */ PROGRESS_PROTOCOL_VIOLATED } diff --git a/src/main/java/com/superbiz/agent/harness/progress/InformationGain.java b/src/main/java/com/superbiz/agent/harness/progress/InformationGain.java index 6a8a934..9eb7a1d 100644 --- a/src/main/java/com/superbiz/agent/harness/progress/InformationGain.java +++ b/src/main/java/com/superbiz/agent/harness/progress/InformationGain.java @@ -1,6 +1,16 @@ package com.superbiz.agent.harness.progress; +/** + * 单次工具观察相对已有收集是否带来新信息(Progress 协议字段)。 + * + *

模型在后续 tool envelope 的 previous_observation 中声明; + * 连续 NO_GAIN 可推动 CollectionState → SATURATED。 + */ public enum InformationGain { + + /** 相对已完成查询有新的可用信息。 */ GAINED, + + /** 无新增信息(含重复 scope、空证据等由 Harness 判定的情况)。 */ NO_GAIN } diff --git a/src/main/java/com/superbiz/agent/harness/progress/ProgressProtocolViolationType.java b/src/main/java/com/superbiz/agent/harness/progress/ProgressProtocolViolationType.java index 71cd0e0..e23a7b5 100644 --- a/src/main/java/com/superbiz/agent/harness/progress/ProgressProtocolViolationType.java +++ b/src/main/java/com/superbiz/agent/harness/progress/ProgressProtocolViolationType.java @@ -1,9 +1,26 @@ package com.superbiz.agent.harness.progress; +/** + * 工具调用「进度协议」违规类型(Progress / ToolInterceptor)。 + * + *

Agent 每次调证据工具应带合法 Envelope(business input + 可选 previous_observation)。 + * 违规通常先返回可修复的 error observation;连续违规可导致 + * {@link DiagnosisStopReason#PROGRESS_PROTOCOL_VIOLATED}。 + */ public enum ProgressProtocolViolationType { + + /** 非首次工具调用缺少对上一观察的 previous_observation。 */ MISSING_PREVIOUS_OBSERVATION, + + /** previous_observation 指向的 tool_call_id 与账本顺序不符。 */ OUT_OF_ORDER_PREVIOUS_OBSERVATION, + + /** 出现了协议不允许的 previous_observation(例如首次就带、或指向未知 id)。 */ UNEXPECTED_PREVIOUS_OBSERVATION, + + /** 缺少必填 business input。 */ MISSING_INPUT, + + /** 整体 Envelope 结构非法(JSON/字段形态不对)。 */ INVALID_ENVELOPE } diff --git a/src/main/java/com/superbiz/agent/harness/retry/RetryFailure.java b/src/main/java/com/superbiz/agent/harness/retry/RetryFailure.java index 62ca7a2..1cd7128 100644 --- a/src/main/java/com/superbiz/agent/harness/retry/RetryFailure.java +++ b/src/main/java/com/superbiz/agent/harness/retry/RetryFailure.java @@ -1,14 +1,43 @@ package com.superbiz.agent.harness.retry; +/** + * 重试框架对失败的分类(Retry 层)。 + * + *

决定某次 attempt 是否允许再试:仅 TIMEOUT/TRANSPORT 等技术类通常可重试; + * CANCELLED / BUDGET_EXHAUSTED / 业务拒绝等必须透出,不能被重试吞掉。 + * + *

用于 Intent Router、Semantic Guard 等包在 {@code HarnessRetryExecutor} 里的模型调用, + * 不是 Diagnosis Agent 主 ReAct 的自动重试分类(Agent 默认不做隐藏重试)。 + */ public enum RetryFailure { + + /** 单次 attempt 或总超时。 */ TIMEOUT, + + /** 网络/传输层失败。 */ TRANSPORT, + + /** 模型输出内容不符合约定(字段/枚举等)。 */ INVALID_OUTPUT, + + /** JSON 等解析失败。 */ PARSE_ERROR, + + /** 结构符合 JSON 但 schema/字段约束失败。 */ SCHEMA_INVALID, + + /** 业务上判定无可用证据(若某组件使用该分类)。 */ NO_EVIDENCE, + + /** 业务策略拒绝,不可靠重试改变结果。 */ BUSINESS_REJECTION, + + /** Run 已取消,禁止重试。 */ CANCELLED, + + /** 预算耗尽,禁止重试。 */ BUDGET_EXHAUSTED, + + /** 未归类。 */ UNKNOWN } diff --git a/src/main/java/com/superbiz/agent/harness/tool/boundary/ToolBoundaryErrorCode.java b/src/main/java/com/superbiz/agent/harness/tool/boundary/ToolBoundaryErrorCode.java index 96fcde0..f2d935d 100644 --- a/src/main/java/com/superbiz/agent/harness/tool/boundary/ToolBoundaryErrorCode.java +++ b/src/main/java/com/superbiz/agent/harness/tool/boundary/ToolBoundaryErrorCode.java @@ -1,17 +1,52 @@ package com.superbiz.agent.harness.tool.boundary; +/** + * 工具边界(ToolBoundary)单次调用错误码。 + * + *

层级:Tool 执行边界。出现在 ToolBoundaryResult.error / 投影给 Agent 的 error observation。 + * 多数情况下 不抛异常打断 ReAct,而是把错误变成安全 observation 回注模型; + * 预算类还会 markBudgetLimitReached,后续模型轮次再由 ModelInterceptor 闸住。 + * + *

与 {@code ChatFailureCode}、{@code FallbackType} 不同:这是单次 tool 级错误,不是整次 Chat 结局。 + */ public enum ToolBoundaryErrorCode { + + /** 请求 JSON / 参数不合法。 */ INVALID_REQUEST, + + /** tool_call_id 缺失或格式非法。 */ INVALID_TOOL_CALL_ID, + + /** 请求中的 runId 与当前 RunContext 不一致。 */ RUN_MISMATCH, + + /** 未授权调用该工具(策略拒绝)。 */ UNAUTHORIZED, + + /** 工具被要求只读,但请求带有写副作用语义。 */ NOT_READ_ONLY, + + /** Run 已终态/取消/超时,不再执行工具(对应 RunAborted)。 */ RUN_INACTIVE, + + /** 本工具调用触达硬预算(次数/bytes 等)。 */ BUDGET_EXHAUSTED, + + /** 相同 tool_call_id 重复执行(幂等/防重)。 */ DUPLICATE_TOOL_CALL, + + /** 原始结果或投影结果超过大小上限。 */ RESULT_TOO_LARGE, + + /** 底层 executor 执行失败或返回 null。 */ TOOL_EXECUTION_ERROR, + + /** raw → agent 投影失败。 */ PROJECTION_ERROR, + + /** 投影后的 evidence_status 与契约不符。 */ INVALID_EVIDENCE_STATUS, + + /** canonical store 读写失败。 */ STORE_ERROR }