docs(harness): annotate entry orchestration, enums, and exception recovery paths
This commit is contained in:
@@ -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 入口的最外层)。
|
||||
*
|
||||
* <p>职责边界:
|
||||
* <ul>
|
||||
* <li>只负责:校验请求、打开 SSE、异步投递、把结果/失败写回客户端</li>
|
||||
* <li>不负责:意图判断、诊断推理、工具调用、门控发布</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>真正的一次请求编排在 {@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 线程执行。
|
||||
*
|
||||
* <p>SSE 事件由 {@link ChatSseSession} 发出(metadata / status / content / done)。
|
||||
* 客户端断开时 session.disconnect,后续可经 RunControl 取消 Run。
|
||||
*/
|
||||
@PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public ResponseEntity<SseEmitter> 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);
|
||||
|
||||
@@ -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}。
|
||||
*
|
||||
* <p>这里是「框架能力」与「Harness 控制面」的粘合点:
|
||||
* <ul>
|
||||
* <li>框架:ChatModel、tools、ReAct 循环、outputSchema</li>
|
||||
* <li>Harness:Model/Tool Interceptor、审计 Hook、禁止并行工具</li>
|
||||
* </ul>
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -4,12 +4,28 @@ import com.superbiz.agent.harness.progress.DiagnosisProgressSnapshot;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Diagnosis Agent 输出/执行失败异常。
|
||||
*
|
||||
* <p>由 {@code DiagnosisAgentUseCase} 抛出;{@code DiagnosisChatExecutor} 仅对
|
||||
* {@link #isDraftContractFailure()} 为 true 的 kind 尝试 {@code recoverInvalidDraft}。
|
||||
*/
|
||||
public final class DiagnosisAgentOutputException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* Agent 失败细分。
|
||||
*
|
||||
* <p>前三种(空/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
|
||||
}
|
||||
|
||||
|
||||
@@ -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}。
|
||||
*
|
||||
* <p>框架负责 ReAct 多轮(模型 ↔ tool_call);Harness 负责:
|
||||
* <ul>
|
||||
* <li>输入/输出字节上限与 Run 预算</li>
|
||||
* <li>通过 Factory 注入 Model/Tool Interceptor 卡住每次消耗</li>
|
||||
* <li>把预算耗尽/信息无增益等可控停止转成 stopped 执行结果</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>由 {@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。
|
||||
*
|
||||
* <p>返回 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 抛出的「受控停止」从异常栈里捞出来,转成正常返回值。
|
||||
*
|
||||
* <p>不是笼统的「业务异常 → 正常」;只识别 Harness 约定的可控信号(沿 cause 链查找,
|
||||
* 因为框架可能再包一层):
|
||||
* <ol>
|
||||
* <li>{@link DiagnosisCollectionStoppedException}:信息饱和等收集该停,
|
||||
* 仍强行 tool → {@code stopped(stopReason)},draft=null</li>
|
||||
* <li>{@link RunAbortedException} 且终态为 {@code BUDGET_EXHAUSTED}:
|
||||
* 标记 progress 预算停 + {@code stopped(BUDGET_LIMIT_REACHED)}</li>
|
||||
* <li>其它 {@link RunAbortedException}(取消/超时/内部失败终态):
|
||||
* <b>原样再抛</b>,不转 stopped——留给 Application 写 CANCELLED/FAILED</li>
|
||||
* <li>{@link BudgetExceededException} 或 lifecycle 已是预算耗尽:同预算 stopped</li>
|
||||
* <li>都不匹配:返回 {@code null},调用方包装为 {@link DiagnosisAgentOutputException}</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>与 {@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 extends Throwable> T findCause(Throwable failure, Class<T> type) {
|
||||
Throwable current = failure;
|
||||
while (current != null) {
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
package com.superbiz.agent.harness.application;
|
||||
|
||||
/**
|
||||
* Chat 应用进度状态(SSE status 事件),不是错误码。
|
||||
*
|
||||
* <p>只表示「当前走到哪一阶段」,成功/失败结局看 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;
|
||||
}
|
||||
|
||||
@@ -23,16 +23,34 @@ import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Chat 应用编排(Harness Application 层):一次请求从创建到公开结果的负责人。
|
||||
*
|
||||
* <p>调用链:
|
||||
* <pre>
|
||||
* Controller → execute()
|
||||
* → core.startRun() // 建 RunContext 边界
|
||||
* → router.route() // Intent Router(Spring AI 单次模型调用)
|
||||
* → executePath(intent) // 按意图分叉
|
||||
* DIAGNOSIS → DiagnosisChatExecutor(Agent → Release)
|
||||
* → completePath / persistFinish
|
||||
* </pre>
|
||||
*
|
||||
* <p>它编排路径,但不做业务根因判断;也不把 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<String> 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 请求的主编排。
|
||||
*
|
||||
* <p>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<RoutingHistory> history;
|
||||
Optional<PreviousTurn> 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 终态处理。
|
||||
*
|
||||
* <p>诊断预算耗尽且已由子路径产出 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 选择执行分支。
|
||||
*
|
||||
* <p>这是 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(),
|
||||
|
||||
@@ -1,11 +1,38 @@
|
||||
package com.superbiz.agent.harness.application;
|
||||
|
||||
/**
|
||||
* Chat 应用层对外失败码(给 SSE failure / 客户端看的粗粒度原因)。
|
||||
*
|
||||
* <p>层级:Application 出口。不要和下列内部码混淆:
|
||||
* <ul>
|
||||
* <li>{@code RunState}:Run 内存生命周期终态</li>
|
||||
* <li>{@code ReleaseOutcome}:诊断发布裁决(SUCCESS/FALLBACK/...)</li>
|
||||
* <li>{@code FallbackType}:FALLBACK 时的细分原因</li>
|
||||
* <li>{@code ToolBoundaryErrorCode}:单次工具边界错误</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>由 {@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
|
||||
}
|
||||
|
||||
+54
@@ -23,9 +23,22 @@ import com.superbiz.agent.harness.release.DiagnosisReleaseUseCase;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 诊断路径子编排(Application 在 intent=DIAGNOSIS 时的执行器)。
|
||||
*
|
||||
* <p>两段式流水线,本身不实现 ReAct 循环:
|
||||
* <ol>
|
||||
* <li>{@link DiagnosisAgentUseCase}:Spring AI Alibaba ReactAgent 收集证据并写 Draft</li>
|
||||
* <li>{@link DiagnosisReleaseUseCase}:Evidence/Semantic 门控 + 发布 SUCCESS 或 FALLBACK</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>由 {@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<ChatApplicationStatus> 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} 时的恢复路径。
|
||||
*
|
||||
* <p>触发条件(见 {@link DiagnosisAgentOutputException#isDraftContractFailure()}):
|
||||
* 空输出 / 非法 JSON / schema 不符。真正的执行崩溃({@code EXECUTION_FAILED})不进恢复,直接再抛。
|
||||
*
|
||||
* <p>除「记审计 + 包装 FALLBACK 返回」外,还承担:
|
||||
* <ol>
|
||||
* <li><b>失败分类闸门</b>:只有 Draft 契约失败可恢复;其它 Agent 异常保持失败语义上抛</li>
|
||||
* <li><b>fail-closed 安全门</b>:必须 {@code progress.hasObservedFacts()},
|
||||
* 否则再抛——禁止在「什么都没查到」时用模板假装一次有依据的降级</li>
|
||||
* <li><b>丢弃非法 draft 正文</b>:不把空串/烂 JSON/错 schema 送进 Evidence/Semantic Guard,
|
||||
* 也不可能走出 SUCCESS;发布只基于已验真 progress(canonical 工具事实)</li>
|
||||
* <li><b>走专用 Release 入口</b>:{@link DiagnosisReleaseUseCase#releaseInvalidDraft},
|
||||
* 而不是 {@code execute(context, query, execution)}——因为没有可校验的 draft</li>
|
||||
* <li><b>SSE 阶段对齐</b>:推 {@code SAFETY_VALIDATING},与正常门控路径对外进度一致</li>
|
||||
* <li><b>固定对外形态</b>:{@code FALLBACK + FallbackContent},{@code publishedResult=null}
|
||||
* (不落可回放的成功发布快照)</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>与 {@code DiagnosisAgentUseCase#controlledExecution} 的分工:
|
||||
* controlledExecution 处理 loop <b>中途</b>被预算/收集收敛打断(常无 draft,转 stopped);
|
||||
* 本方法处理 loop <b>结束后</b>输出契约失败(有/无 progress 决定 FALLBACK 还是失败)。
|
||||
*/
|
||||
private DiagnosisExecutionResult recoverInvalidDraft(
|
||||
RunContext context,
|
||||
DiagnosisAgentOutputException exception,
|
||||
Consumer<ChatApplicationStatus> 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()),
|
||||
|
||||
@@ -29,12 +29,26 @@ import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 意图路由:判断本轮走 SYSTEM_CHAT / KNOWLEDGE_QUERY / DIAGNOSIS。
|
||||
*
|
||||
* <p>在 Harness 中的位置:Application 主编排里的「路由节点」,不是业务执行器。
|
||||
* <ul>
|
||||
* <li>使用 Spring AI 的 {@link Prompt} / Message 构造请求</li>
|
||||
* <li>经 {@link GuardModelCall} 调 ChatModel(单次结构化输出,不是 ReactAgent)</li>
|
||||
* <li>预算、超时、重试、审计由 Harness 包裹</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>只返回 {@link IntentType};由 {@code ChatApplicationUseCase.executePath} 决定后续分支。
|
||||
*/
|
||||
public final class IntentRouter implements IntentRouting {
|
||||
|
||||
/** 输出契约:JSON 只能有 intent 一个字段。 */
|
||||
private static final Set<String> 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 {
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
package com.superbiz.agent.harness.contract;
|
||||
|
||||
/**
|
||||
* 单次工具调用在「证据语义」上的结果状态(工具契约 / 进度)。
|
||||
*
|
||||
* <p>与 {@link InvocationStatus} 分工:
|
||||
* <ul>
|
||||
* <li>InvocationStatus:调用生命周期(投影中/就绪/错误)</li>
|
||||
* <li>EvidenceStatus:这次调用有没有拿到可引用证据</li>
|
||||
* </ul>
|
||||
* NO_EVIDENCE 仍可能是 success 的工具执行(查了但空),常记为信息无增益。
|
||||
*/
|
||||
public enum EvidenceStatus {
|
||||
|
||||
/** 返回了可被引用的证据块。 */
|
||||
EVIDENCE_FOUND,
|
||||
|
||||
/** 执行完成但范围内无证据(空结果,不是必然系统故障)。 */
|
||||
NO_EVIDENCE,
|
||||
|
||||
/** 工具侧错误或无法形成合法证据观察。 */
|
||||
ERROR
|
||||
}
|
||||
|
||||
@@ -1,10 +1,43 @@
|
||||
package com.superbiz.agent.harness.contract;
|
||||
|
||||
/**
|
||||
* FALLBACK 细分类型:在 {@link ReleaseOutcome#FALLBACK} 时说明「为什么降级」。
|
||||
*
|
||||
* <p>层级:Release 内容契约(SafeFallback.type)。
|
||||
* 出现在对外 Fallback 载荷与 Trace 的 RELEASE_DECISION 中。
|
||||
*
|
||||
* <p>注意:
|
||||
* <ul>
|
||||
* <li>{@link #BUDGET_EXHAUSTED} 枚举值仍保留,但当前诊断主路径对预算受控停止
|
||||
* 实际多发布 {@link #INSUFFICIENT_EVIDENCE}(见 harness CONTEXT 命名债务说明)</li>
|
||||
* <li>与 {@code DiagnosisStopReason} 不同:StopReason 是收集阶段为何停;
|
||||
* FallbackType 是发布给用户的降级分类</li>
|
||||
* </ul>
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
package com.superbiz.agent.harness.contract;
|
||||
|
||||
/**
|
||||
* 工具调用在 canonical store 中的生命周期状态。
|
||||
*
|
||||
* <p>READY 的记录才可作为 Evidence Guard 引用目标;
|
||||
* PROJECTING 表示尚未完成投影落库;ERROR 表示本调用失败收尾。
|
||||
*/
|
||||
public enum InvocationStatus {
|
||||
|
||||
/** 已 begin,正在执行/投影,尚不可作为最终引用。 */
|
||||
PROJECTING,
|
||||
|
||||
/** 投影完成且可引用(agentResult + evidenceStatus 已固化)。 */
|
||||
READY,
|
||||
|
||||
/** 本调用以错误结束。 */
|
||||
ERROR
|
||||
}
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
package com.superbiz.agent.harness.contract;
|
||||
|
||||
/**
|
||||
* 诊断发布裁决结果(Release 层):这一次诊断「对外给什么结局」。
|
||||
*
|
||||
* <p>层级:Release / Application 结果。注意与 {@link com.superbiz.agent.harness.core.RunState} 不同:
|
||||
* <ul>
|
||||
* <li>{@code RunState} 回答 Run 是否还在跑、因何技术终态停下(含 TIMED_OUT、BUDGET_EXHAUSTED)</li>
|
||||
* <li>{@code ReleaseOutcome} 回答用户侧内容形态:正常报告 / 安全降级 / 失败 / 取消</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>常见组合:RunState=BUDGET_EXHAUSTED 且有安全进展 → ReleaseOutcome=FALLBACK;
|
||||
* 无进展 → 往往落到 FAILED。
|
||||
*/
|
||||
public enum ReleaseOutcome {
|
||||
|
||||
/** 通过门控,可发布 DIAGNOSIS_REPORT。 */
|
||||
SUCCESS,
|
||||
|
||||
/** 不发布根因报告,发布 SafeFallback(见 {@link FallbackType})。 */
|
||||
FALLBACK,
|
||||
|
||||
/** 无法形成可发布的安全内容(含无 observed facts 的预算停等)。 */
|
||||
FAILED,
|
||||
|
||||
/** 运行被取消,通常不再保证客户端能收到完整 done。 */
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
package com.superbiz.agent.harness.contract;
|
||||
|
||||
/**
|
||||
* Semantic Guard 对「结论是否被证据支撑」的裁决。
|
||||
*
|
||||
* <p>SUPPORTED → 可走向 Release SUCCESS;
|
||||
* UNSUPPORTED → 通常 FallbackType.SEMANTIC_UNSUPPORTED。
|
||||
* (Guard 技术失败走 SEMANTIC_UNAVAILABLE,不一定落本枚举。)
|
||||
*/
|
||||
public enum SemanticVerdict {
|
||||
|
||||
/** 语义上认为草稿结论被已验证证据支持。 */
|
||||
SUPPORTED,
|
||||
|
||||
/** 证据不足以支持当前根因/结论表述。 */
|
||||
UNSUPPORTED
|
||||
}
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
package com.superbiz.agent.harness.contract;
|
||||
|
||||
/**
|
||||
* SSE {@code done} 事件上的粗粒度结局(协议层)。
|
||||
*
|
||||
* <p>与 {@link ReleaseOutcome} 对齐的对外三态;取消场景连接可能已断,
|
||||
* 不一定能发出 done(CANCELLED),故此处通常只有 SUCCESS / FALLBACK / FAILED。
|
||||
*/
|
||||
public enum SseOutcome {
|
||||
|
||||
/** 对应成功报告 content。 */
|
||||
SUCCESS,
|
||||
|
||||
/** 对应安全降级 content。 */
|
||||
FALLBACK,
|
||||
|
||||
/** 对应 failure 事件或失败 done(实现以 ChatSseSession 为准)。 */
|
||||
FAILED
|
||||
}
|
||||
|
||||
@@ -1,11 +1,31 @@
|
||||
package com.superbiz.agent.harness.core;
|
||||
|
||||
/**
|
||||
* 硬预算维度:哪一类资源超限(Core / RunBudget)。
|
||||
*
|
||||
* <p>超限时抛 {@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
|
||||
}
|
||||
|
||||
@@ -10,6 +10,17 @@ import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Harness Core:一次 Run 的执行规则(不是业务编排器)。
|
||||
*
|
||||
* <p>与 Application 的分工:
|
||||
* <ul>
|
||||
* <li>Application:走哪条路径、何时持久化、返回什么内容</li>
|
||||
* <li>Core:是否仍可执行、资源是否允许、哪个终态生效</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>不依赖 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)。
|
||||
*
|
||||
* <p>携带:身份(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));
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
package com.superbiz.agent.harness.core;
|
||||
|
||||
/**
|
||||
* 触发 Run 取消 / 与取消联动写终态的原因(Core)。
|
||||
*
|
||||
* <p>由 {@link RunCancellation#cancel} 携带;Core 会映射到对应 {@link RunState}:
|
||||
* <ul>
|
||||
* <li>CLIENT_DISCONNECTED / USER_REQUESTED → CANCELLED</li>
|
||||
* <li>DEADLINE_EXCEEDED → TIMED_OUT</li>
|
||||
* <li>BUDGET_EXHAUSTED → BUDGET_EXHAUSTED</li>
|
||||
* <li>INTERNAL_FAILURE → FAILED</li>
|
||||
* </ul>
|
||||
*/
|
||||
public enum RunCancellationReason {
|
||||
|
||||
/** SSE/HTTP 客户端断开。 */
|
||||
CLIENT_DISCONNECTED,
|
||||
|
||||
/** 显式用户取消(若产品支持)。 */
|
||||
USER_REQUESTED,
|
||||
|
||||
/** 墙钟超过 Run deadline。 */
|
||||
DEADLINE_EXCEEDED,
|
||||
|
||||
/** 硬预算耗尽(与 BudgetExceededException 联动)。 */
|
||||
BUDGET_EXHAUSTED,
|
||||
|
||||
/** 应用判定内部失败并收尾。 */
|
||||
INTERNAL_FAILURE
|
||||
}
|
||||
|
||||
@@ -1,11 +1,34 @@
|
||||
package com.superbiz.agent.harness.core;
|
||||
|
||||
/**
|
||||
* 一次 Run 的内存生命周期状态(Core / RunLifecycle)。
|
||||
*
|
||||
* <p>层级:执行控制面。first-terminal-wins:第一个写入的终态不可被迟到结果覆盖。
|
||||
*
|
||||
* <p>不要直接当成用户看到的结果:
|
||||
* <ul>
|
||||
* <li>用户内容结局看 {@code ReleaseOutcome} / SSE</li>
|
||||
* <li>本枚举回答「Run 技术上是否还允许继续执行」</li>
|
||||
* </ul>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,25 +1,76 @@
|
||||
package com.superbiz.agent.harness.guard.evidence;
|
||||
|
||||
/**
|
||||
* Evidence Guard 结构/引用违规码(规则门控,通常不调大模型)。
|
||||
*
|
||||
* <p>层级:Guard。校验 DiagnosisDraft 是否引用真实 tool_call、字段是否齐全等。
|
||||
* 失败时可尝试 EvidenceRepair;仍失败则 FallbackType.EVIDENCE_VALIDATION_FAILED。
|
||||
*
|
||||
* <p>与 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
|
||||
}
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
package com.superbiz.agent.harness.progress;
|
||||
|
||||
/**
|
||||
* 证据收集阶段状态机(Progress 层,与 RunState 独立)。
|
||||
*
|
||||
* <p>SATURATED 表示「再查也难有信息增益」,会触发 stop_required;
|
||||
* 不等于 Run 已经 BUDGET_EXHAUSTED 或对外已经 FALLBACK。
|
||||
*/
|
||||
public enum DiagnosisCollectionState {
|
||||
|
||||
/** 仍允许在预算内发起工具调用(需遵守进度协议)。 */
|
||||
COLLECTING,
|
||||
|
||||
/** 已饱和:交付一次 STOP 指示后,再要工具可抛 DiagnosisCollectionStoppedException。 */
|
||||
SATURATED
|
||||
}
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
package com.superbiz.agent.harness.progress;
|
||||
|
||||
/**
|
||||
* 诊断「证据收集」为何受控停止(Progress 层)。
|
||||
*
|
||||
* <p>层级:Agent 收集收敛,不是对外 FallbackType。
|
||||
* 出现在 {@code DiagnosisAgentExecution.stopped(...)} 与 Tool 侧 stop_required 观察里。
|
||||
*
|
||||
* <p>与预算的关系:
|
||||
* <ul>
|
||||
* <li>INFORMATION_SATURATED / PROGRESS_PROTOCOL_VIOLATED:业务上继续查已无价值</li>
|
||||
* <li>BUDGET_LIMIT_REACHED:硬资源没了(可与 RunState.BUDGET_EXHAUSTED 对应)</li>
|
||||
* </ul>
|
||||
* 有安全 observed facts 时,Release 常映射为 FallbackType.INSUFFICIENT_EVIDENCE。
|
||||
*/
|
||||
public enum DiagnosisStopReason {
|
||||
|
||||
/** 连续无信息增益(或等价饱和策略),收集状态进入 SATURATED。 */
|
||||
INFORMATION_SATURATED,
|
||||
|
||||
/** 模型次数/工具次数/Token/bytes 等硬预算触顶。 */
|
||||
BUDGET_LIMIT_REACHED,
|
||||
|
||||
/** 进度协议连续违规达到阈值(envelope / previous_observation 等)。 */
|
||||
PROGRESS_PROTOCOL_VIOLATED
|
||||
}
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
package com.superbiz.agent.harness.progress;
|
||||
|
||||
/**
|
||||
* 单次工具观察相对已有收集是否带来新信息(Progress 协议字段)。
|
||||
*
|
||||
* <p>模型在后续 tool envelope 的 previous_observation 中声明;
|
||||
* 连续 NO_GAIN 可推动 CollectionState → SATURATED。
|
||||
*/
|
||||
public enum InformationGain {
|
||||
|
||||
/** 相对已完成查询有新的可用信息。 */
|
||||
GAINED,
|
||||
|
||||
/** 无新增信息(含重复 scope、空证据等由 Harness 判定的情况)。 */
|
||||
NO_GAIN
|
||||
}
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
package com.superbiz.agent.harness.progress;
|
||||
|
||||
/**
|
||||
* 工具调用「进度协议」违规类型(Progress / ToolInterceptor)。
|
||||
*
|
||||
* <p>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
|
||||
}
|
||||
|
||||
@@ -1,14 +1,43 @@
|
||||
package com.superbiz.agent.harness.retry;
|
||||
|
||||
/**
|
||||
* 重试框架对失败的分类(Retry 层)。
|
||||
*
|
||||
* <p>决定某次 attempt 是否允许再试:仅 TIMEOUT/TRANSPORT 等技术类通常可重试;
|
||||
* CANCELLED / BUDGET_EXHAUSTED / 业务拒绝等必须透出,不能被重试吞掉。
|
||||
*
|
||||
* <p>用于 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
|
||||
}
|
||||
|
||||
@@ -1,17 +1,52 @@
|
||||
package com.superbiz.agent.harness.tool.boundary;
|
||||
|
||||
/**
|
||||
* 工具边界(ToolBoundary)单次调用错误码。
|
||||
*
|
||||
* <p>层级:Tool 执行边界。出现在 ToolBoundaryResult.error / 投影给 Agent 的 error observation。
|
||||
* 多数情况下 <b>不抛异常打断 ReAct</b>,而是把错误变成安全 observation 回注模型;
|
||||
* 预算类还会 markBudgetLimitReached,后续模型轮次再由 ModelInterceptor 闸住。
|
||||
*
|
||||
* <p>与 {@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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user