feat(graph): add diagnosis routing skeleton

This commit is contained in:
zhuyongxin
2026-07-17 11:17:13 +08:00
parent 581daffdad
commit 42ba204532
26 changed files with 2455 additions and 0 deletions
@@ -0,0 +1,609 @@
package com.superbiz.agent.graph.diagnosis;
import com.alibaba.cloud.ai.graph.CompiledGraph;
import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.superbiz.agent.graph.diagnosis.DiagnosisGraphStatus.ExecutorStatus;
import com.superbiz.agent.graph.diagnosis.DiagnosisGraphStatus.GatekeeperStatus;
import com.superbiz.agent.graph.diagnosis.DiagnosisGraphStatus.PlannerMode;
import com.superbiz.agent.graph.diagnosis.DiagnosisGraphStatus.PlannerStatus;
import com.superbiz.agent.graph.diagnosis.DiagnosisGraphStatus.Verdict;
import com.superbiz.agent.graph.diagnosis.DiagnosisGraphStatus.VerifierStatus;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
class DiagnosisGraphRoutingTest {
private static final String THREAD_ID = "run-routing-test";
@Test
void normalPassPathUsesCompiledGraphAndPreservesEventOrder() throws Exception {
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
planner(script, PlannerStatus.COMPLETED);
executor(script, ExecutorStatus.COMPLETED);
gatekeeper(script, GatekeeperStatus.PASS, 1);
verifiedInput(script);
verifier(script, VerifierStatus.COMPLETED, Verdict.PASS,
Verdict.PASS, Map.of());
composerCompleted(script);
OverAllState state = run(script);
assertEquals(List.of(
DiagnosisGraphTopology.Node.PLANNER,
DiagnosisGraphTopology.Node.EXECUTOR,
DiagnosisGraphTopology.Node.GATEKEEPER,
DiagnosisGraphTopology.Node.VERIFIED_INPUT,
DiagnosisGraphTopology.Node.VERIFIER,
DiagnosisGraphTopology.Node.COMPOSER),
script.sequence());
script.sequence().forEach(node -> assertEquals(
List.of(THREAD_ID),
script.threadIds(node),
"threadId for " + node));
assertEquals(6, DiagnosisGraphState.listValue(
state, DiagnosisGraphState.ORCHESTRATION_EVENTS).size());
DiagnosisOrchestrationTrace trace =
new DiagnosisOrchestrationTraceBuilder().build(state);
assertEquals(DiagnosisGraphTopology.Node.COMPOSER, trace.finalNode());
assertFalse(trace.degraded());
assertEquals(5, trace.transitions().size());
}
@ParameterizedTest
@ValueSource(strings = {"INVALID_OUTPUT", "RETRYABLE_FAILED"})
void plannerTechnicalFailureRetriesOnlyPlannerOnce(String firstStatus)
throws Exception {
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
planner(script, firstStatus);
planner(script, PlannerStatus.COMPLETED);
appendNormalAfterPlanner(script);
OverAllState state = run(script);
assertEquals(List.of(
DiagnosisGraphTopology.Node.PLANNER,
DiagnosisGraphTopology.Node.PLANNER,
DiagnosisGraphTopology.Node.EXECUTOR,
DiagnosisGraphTopology.Node.GATEKEEPER,
DiagnosisGraphTopology.Node.VERIFIED_INPUT,
DiagnosisGraphTopology.Node.VERIFIER,
DiagnosisGraphTopology.Node.COMPOSER),
script.sequence());
assertEquals(1, DiagnosisGraphState.intValue(
state, DiagnosisGraphState.PLANNER_RETRY_COUNT));
assertEquals(2, script.calls(DiagnosisGraphTopology.Node.PLANNER));
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.EXECUTOR));
}
@Test
void plannerSecondTechnicalFailureFallsBackWithoutExecutor()
throws Exception {
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
planner(script, PlannerStatus.INVALID_OUTPUT);
planner(script, PlannerStatus.RETRYABLE_FAILED);
fallback(script);
OverAllState state = run(script);
assertEquals(List.of(
DiagnosisGraphTopology.Node.PLANNER,
DiagnosisGraphTopology.Node.PLANNER,
DiagnosisGraphTopology.Node.FALLBACK),
script.sequence());
assertEquals(1, DiagnosisGraphState.intValue(
state, DiagnosisGraphState.PLANNER_RETRY_COUNT));
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.EXECUTOR));
assertFallbackTrace(state);
}
@ParameterizedTest
@ValueSource(strings = {"NON_RETRYABLE_FAILED", "UNKNOWN"})
void plannerNonRetryableOrUnknownStatusFailsClosed(String status)
throws Exception {
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
planner(script, status);
fallback(script);
OverAllState state = run(script);
assertEquals(List.of(
DiagnosisGraphTopology.Node.PLANNER,
DiagnosisGraphTopology.Node.FALLBACK),
script.sequence());
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.EXECUTOR));
assertFallbackTrace(state);
}
@ParameterizedTest
@ValueSource(strings = {
"INVALID_OUTPUT", "TOOL_BLOCKED", "FAILED", "UNKNOWN"
})
void executorFailureNeverRetriesOrInvokesGatekeeper(String status)
throws Exception {
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
planner(script, PlannerStatus.COMPLETED);
executor(script, status);
fallback(script);
OverAllState state = run(script);
assertEquals(List.of(
DiagnosisGraphTopology.Node.PLANNER,
DiagnosisGraphTopology.Node.EXECUTOR,
DiagnosisGraphTopology.Node.FALLBACK),
script.sequence());
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.EXECUTOR));
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.GATEKEEPER));
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.VERIFIER));
assertFallbackTrace(state);
}
@Test
void executorCompletedNoEvidenceStillInvokesGatekeeper() throws Exception {
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
planner(script, PlannerStatus.COMPLETED);
script.step(
DiagnosisGraphTopology.Node.EXECUTOR,
ExecutorStatus.COMPLETED.name(),
"legal_no_evidence",
Map.of(
DiagnosisGraphState.EXECUTOR_STATUS,
ExecutorStatus.COMPLETED.name(),
DiagnosisGraphState.EXECUTOR_OUTPUT,
Map.of("claims", List.of())));
gatekeeper(script, GatekeeperStatus.PASS, 0);
verifiedInput(script);
verifier(script, VerifierStatus.COMPLETED, Verdict.PASS,
Verdict.PASS, Map.of());
composerCompleted(script);
run(script);
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.GATEKEEPER));
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.VERIFIER));
}
@ParameterizedTest
@MethodSource("gatekeeperFallbackCases")
void gatekeeperUnsafeOutcomesSkipVerifier(
String status,
int verifiedBindingCount) throws Exception {
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
planner(script, PlannerStatus.COMPLETED);
executor(script, ExecutorStatus.COMPLETED);
gatekeeper(script, status, verifiedBindingCount);
fallback(script);
OverAllState state = run(script);
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.VERIFIED_INPUT));
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.VERIFIER));
assertFallbackTrace(state);
}
@Test
void gatekeeperLowConfidenceWithVerifiedBindingRunsVerifiedInput()
throws Exception {
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
planner(script, PlannerStatus.COMPLETED);
executor(script, ExecutorStatus.COMPLETED);
gatekeeper(script, GatekeeperStatus.LOW_CONFID, 1);
verifiedInput(script);
verifier(script, VerifierStatus.COMPLETED, Verdict.LOW_CONFID,
Verdict.LOW_CONFID, Map.of());
composerCompleted(script);
OverAllState state = run(script);
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.VERIFIED_INPUT));
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.VERIFIER));
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.EVIDENCE_RETRY));
assertEquals(Verdict.LOW_CONFID,
DiagnosisGraphState.enumValue(
state,
DiagnosisGraphState.EFFECTIVE_VERDICT,
Verdict.class)
.orElseThrow());
}
@ParameterizedTest
@ValueSource(strings = {"INVALID_OUTPUT", "RETRYABLE_FAILED"})
void verifierTechnicalFailureRetriesOnlyVerifier(String firstStatus)
throws Exception {
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
appendThroughVerifiedInput(script);
verifierFailure(script, firstStatus);
verifier(script, VerifierStatus.COMPLETED, Verdict.PASS,
Verdict.PASS, Map.of());
composerCompleted(script);
OverAllState state = run(script);
assertEquals(2, script.calls(DiagnosisGraphTopology.Node.VERIFIER));
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.GATEKEEPER));
assertEquals(1, DiagnosisGraphState.intValue(
state, DiagnosisGraphState.VERIFIER_RETRY_COUNT));
assertEquals(0, DiagnosisGraphState.intValue(
state, DiagnosisGraphState.EVIDENCE_RETRY_COUNT));
}
@Test
void verifierSecondTechnicalFailureFallsBack() throws Exception {
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
appendThroughVerifiedInput(script);
verifierFailure(script, VerifierStatus.INVALID_OUTPUT.name());
verifierFailure(script, VerifierStatus.RETRYABLE_FAILED.name());
fallback(script);
OverAllState state = run(script);
assertEquals(2, script.calls(DiagnosisGraphTopology.Node.VERIFIER));
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.COMPOSER));
assertEquals(1, DiagnosisGraphState.intValue(
state, DiagnosisGraphState.VERIFIER_RETRY_COUNT));
assertFallbackTrace(state);
}
@ParameterizedTest
@ValueSource(strings = {"NON_RETRYABLE_FAILED", "UNKNOWN"})
void verifierNonRetryableOrUnknownStatusFailsClosed(String status)
throws Exception {
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
appendThroughVerifiedInput(script);
verifierFailure(script, status);
fallback(script);
OverAllState state = run(script);
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.VERIFIER));
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.COMPOSER));
assertFallbackTrace(state);
}
@Test
void evidenceRetryRunsOnceAndResetsPlannerStageCounter()
throws Exception {
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
planner(script, PlannerStatus.INVALID_OUTPUT);
planner(script, PlannerStatus.COMPLETED);
appendExecutorThroughVerifiedInput(script);
verifier(script, VerifierStatus.COMPLETED, Verdict.LOW_CONFID,
Verdict.PASS, verifierOutputWithGap());
evidenceRetry(script);
planner(script, PlannerStatus.INVALID_OUTPUT);
planner(script, PlannerStatus.COMPLETED);
appendExecutorThroughVerifiedInput(script);
verifier(script, VerifierStatus.COMPLETED, Verdict.LOW_CONFID,
Verdict.PASS, verifierOutputWithGap());
composerCompleted(script);
OverAllState state = run(script);
assertEquals(4, script.calls(DiagnosisGraphTopology.Node.PLANNER));
assertEquals(2, script.calls(DiagnosisGraphTopology.Node.EXECUTOR));
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.EVIDENCE_RETRY));
assertEquals(1, DiagnosisGraphState.intValue(
state, DiagnosisGraphState.EVIDENCE_RETRY_COUNT));
assertEquals(1, DiagnosisGraphState.intValue(
state, DiagnosisGraphState.PLANNER_RETRY_COUNT));
assertEquals(0, DiagnosisGraphState.intValue(
state, DiagnosisGraphState.VERIFIER_RETRY_COUNT));
assertEquals(PlannerMode.EVIDENCE_GAP_ONLY,
DiagnosisGraphState.enumValue(
state,
DiagnosisGraphState.PLANNER_MODE,
PlannerMode.class)
.orElseThrow());
}
@Test
void verifierLowConfidenceWithoutGapDoesNotRetryEvidence()
throws Exception {
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
appendThroughVerifiedInput(script);
verifier(script, VerifierStatus.COMPLETED, Verdict.LOW_CONFID,
Verdict.PASS, Map.of("facts_checked", List.of(
Map.of("fact", "cpu is high",
"verification", "direct_support"))));
composerCompleted(script);
run(script);
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.EVIDENCE_RETRY));
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.COMPOSER));
}
@Test
void verifierRejectStillRunsComposerWithSafeMaterial() throws Exception {
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
appendThroughVerifiedInput(script);
verifier(script, VerifierStatus.COMPLETED, Verdict.REJECT,
Verdict.PASS, Map.of());
composerCompleted(script);
run(script);
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.COMPOSER));
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.FALLBACK));
}
@Test
void verifierCompletedWithoutEffectiveVerdictFailsClosed()
throws Exception {
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
appendThroughVerifiedInput(script);
verifier(script, VerifierStatus.COMPLETED, null,
Verdict.PASS, Map.of());
fallback(script);
OverAllState state = run(script);
assertEquals(0, script.calls(DiagnosisGraphTopology.Node.COMPOSER));
assertFallbackTrace(state);
}
@ParameterizedTest
@ValueSource(strings = {"INVALID_OUTPUT", "RETRYABLE_FAILED"})
void composerTechnicalFailureRetriesOnlyComposer(String firstStatus)
throws Exception {
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
appendThroughCompletedVerifier(script);
composer(script, firstStatus);
composerCompleted(script);
OverAllState state = run(script);
assertEquals(2, script.calls(DiagnosisGraphTopology.Node.COMPOSER));
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.VERIFIER));
assertEquals(1, DiagnosisGraphState.intValue(
state, DiagnosisGraphState.COMPOSER_RETRY_COUNT));
}
@Test
void composerSecondTechnicalFailureFallsBackAndTerminates()
throws Exception {
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
appendThroughCompletedVerifier(script);
composer(script, "INVALID_OUTPUT");
composer(script, "RETRYABLE_FAILED");
fallback(script);
OverAllState state = run(script);
assertEquals(2, script.calls(DiagnosisGraphTopology.Node.COMPOSER));
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.FALLBACK));
assertEquals(1, DiagnosisGraphState.intValue(
state, DiagnosisGraphState.COMPOSER_RETRY_COUNT));
assertFallbackTrace(state);
}
@ParameterizedTest
@ValueSource(strings = {"NON_RETRYABLE_FAILED", "UNKNOWN"})
void composerNonRetryableOrUnknownStatusFailsClosed(String status)
throws Exception {
ScriptedDiagnosisGraphActions script = new ScriptedDiagnosisGraphActions();
appendThroughCompletedVerifier(script);
composer(script, status);
fallback(script);
OverAllState state = run(script);
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.COMPOSER));
assertEquals(1, script.calls(DiagnosisGraphTopology.Node.FALLBACK));
assertFallbackTrace(state);
}
private static Stream<Arguments> gatekeeperFallbackCases() {
return Stream.of(
Arguments.of(GatekeeperStatus.REJECT.name(), 1),
Arguments.of(GatekeeperStatus.LOW_CONFID.name(), 0),
Arguments.of("UNKNOWN", 1));
}
private OverAllState run(ScriptedDiagnosisGraphActions script)
throws Exception {
CompiledGraph graph = new DiagnosisGraphFactory().compile(script.actions());
assertEquals(DiagnosisGraphTopology.RECURSION_LIMIT,
graph.getMaxIterations());
return graph.invoke(
initialState(),
RunnableConfig.builder().threadId(THREAD_ID).build())
.orElseThrow();
}
private Map<String, Object> initialState() {
return Map.of(
DiagnosisGraphState.PLANNER_RETRY_COUNT, 0,
DiagnosisGraphState.VERIFIER_RETRY_COUNT, 0,
DiagnosisGraphState.COMPOSER_RETRY_COUNT, 0,
DiagnosisGraphState.EVIDENCE_RETRY_COUNT, 0,
DiagnosisGraphState.PLANNER_MODE, PlannerMode.NORMAL.name());
}
private void appendNormalAfterPlanner(
ScriptedDiagnosisGraphActions script) {
executor(script, ExecutorStatus.COMPLETED);
gatekeeper(script, GatekeeperStatus.PASS, 1);
verifiedInput(script);
verifier(script, VerifierStatus.COMPLETED, Verdict.PASS,
Verdict.PASS, Map.of());
composerCompleted(script);
}
private void appendThroughVerifiedInput(
ScriptedDiagnosisGraphActions script) {
planner(script, PlannerStatus.COMPLETED);
appendExecutorThroughVerifiedInput(script);
}
private void appendExecutorThroughVerifiedInput(
ScriptedDiagnosisGraphActions script) {
executor(script, ExecutorStatus.COMPLETED);
gatekeeper(script, GatekeeperStatus.PASS, 1);
verifiedInput(script);
}
private void appendThroughCompletedVerifier(
ScriptedDiagnosisGraphActions script) {
appendThroughVerifiedInput(script);
verifier(script, VerifierStatus.COMPLETED, Verdict.PASS,
Verdict.PASS, Map.of());
}
private void planner(
ScriptedDiagnosisGraphActions script,
PlannerStatus status) {
planner(script, status.name());
}
private void planner(
ScriptedDiagnosisGraphActions script,
String status) {
script.step(
DiagnosisGraphTopology.Node.PLANNER,
status,
reason(status),
Map.of(DiagnosisGraphState.PLANNER_STATUS, status));
}
private void executor(
ScriptedDiagnosisGraphActions script,
ExecutorStatus status) {
executor(script, status.name());
}
private void executor(
ScriptedDiagnosisGraphActions script,
String status) {
script.step(
DiagnosisGraphTopology.Node.EXECUTOR,
status,
reason(status),
Map.of(DiagnosisGraphState.EXECUTOR_STATUS, status));
}
private void gatekeeper(
ScriptedDiagnosisGraphActions script,
GatekeeperStatus status,
int verifiedBindingCount) {
gatekeeper(script, status.name(), verifiedBindingCount);
}
private void gatekeeper(
ScriptedDiagnosisGraphActions script,
String status,
int verifiedBindingCount) {
script.step(
DiagnosisGraphTopology.Node.GATEKEEPER,
status,
reason(status),
Map.of(
DiagnosisGraphState.GATEKEEPER_STATUS, status,
DiagnosisGraphState.VERIFIED_BINDING_COUNT,
verifiedBindingCount));
}
private void verifiedInput(ScriptedDiagnosisGraphActions script) {
script.step(
DiagnosisGraphTopology.Node.VERIFIED_INPUT,
"COMPLETED",
"verified_input_completed",
Map.of());
}
private void verifier(
ScriptedDiagnosisGraphActions script,
VerifierStatus status,
Verdict verdict,
Verdict ceiling,
Map<String, Object> verifierOutput) {
Map<String, Object> update = new java.util.LinkedHashMap<>();
update.put(DiagnosisGraphState.VERIFIER_STATUS, status.name());
if (verdict != null) {
update.put(DiagnosisGraphState.EFFECTIVE_VERDICT, verdict.name());
}
if (ceiling != null) {
update.put(DiagnosisGraphState.VERIFIER_VERDICT_CEILING,
ceiling.name());
}
update.put(DiagnosisGraphState.VERIFIER_OUTPUT, verifierOutput);
script.step(
DiagnosisGraphTopology.Node.VERIFIER,
status.name(),
reason(status.name()),
update);
}
private void verifierFailure(
ScriptedDiagnosisGraphActions script,
String status) {
script.step(
DiagnosisGraphTopology.Node.VERIFIER,
status,
reason(status),
Map.of(DiagnosisGraphState.VERIFIER_STATUS, status));
}
private void composerCompleted(ScriptedDiagnosisGraphActions script) {
composer(script, "COMPLETED");
}
private void composer(
ScriptedDiagnosisGraphActions script,
String status) {
script.step(
DiagnosisGraphTopology.Node.COMPOSER,
status,
"composer_" + reason(status),
Map.of(DiagnosisGraphState.COMPOSER_STATUS, status));
}
private void evidenceRetry(ScriptedDiagnosisGraphActions script) {
script.step(
DiagnosisGraphTopology.Node.EVIDENCE_RETRY,
"COMPLETED",
DiagnosisGraphTopology.Reason.EVIDENCE_RETRY,
Map.of(DiagnosisGraphState.RETRY_CONTEXT,
Map.of("mode", PlannerMode.EVIDENCE_GAP_ONLY.name())));
}
private Map<String, Object> verifierOutputWithGap() {
return Map.of("facts_checked", List.of(
Map.of(
"fact", "database saturation remains unverified",
"verification", "no_evidence")));
}
private void fallback(ScriptedDiagnosisGraphActions script) {
script.step(
DiagnosisGraphTopology.Node.FALLBACK,
"COMPLETED",
DiagnosisGraphTopology.Reason.FALLBACK_COMPLETED,
Map.of(DiagnosisGraphState.FINAL_ANSWER, "safe fallback"));
}
private void assertFallbackTrace(OverAllState state) {
DiagnosisOrchestrationTrace trace =
new DiagnosisOrchestrationTraceBuilder().build(state);
assertEquals(DiagnosisGraphTopology.Node.FALLBACK, trace.finalNode());
assertEquals(DiagnosisGraphTopology.Reason.FALLBACK_COMPLETED,
trace.terminationReason());
assertEquals(true, trace.degraded());
}
private String reason(String status) {
return status.toLowerCase(java.util.Locale.ROOT);
}
}
@@ -0,0 +1,155 @@
package com.superbiz.agent.graph.diagnosis;
import com.alibaba.cloud.ai.graph.OverAllState;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class DiagnosisOrchestrationTraceBuilderTest {
private final DiagnosisOrchestrationTraceBuilder builder =
new DiagnosisOrchestrationTraceBuilder();
@Test
void buildsExactTransitionsAndRoutingOnlyMap() {
OverAllState state = stateWithEvents(List.of(
event("planner", "COMPLETED", "planner_completed", 1),
event("executor", "COMPLETED", "executor_completed", 1),
event("gatekeeper", "PASS", "gatekeeper_pass", 1),
event("verifier", "LOW_CONFID", "verifier_low_confid", 1),
event("evidence_retry", "COMPLETED", "evidence_retry", 1),
event("planner", "COMPLETED", "planner_completed", 2),
event("composer", "COMPLETED", "composer_completed", 1)
), 1);
DiagnosisOrchestrationTrace trace = builder.build(state);
assertEquals("stategraph-v1", trace.version());
assertEquals("composer", trace.finalNode());
assertEquals("composer_completed", trace.terminationReason());
assertEquals(1, trace.evidenceRetryCount());
assertFalse(trace.degraded());
assertEquals(List.of(
"planner->executor",
"executor->gatekeeper",
"gatekeeper->verifier",
"verifier->evidence_retry",
"evidence_retry->planner",
"planner->composer"),
trace.transitions().stream()
.map(value -> value.from() + "->" + value.to())
.toList());
assertEquals("verifier_low_confid",
trace.transitions().get(3).reasonCode());
Map<String, Object> traceMap = trace.toMap();
assertEquals(Set.of(
"version",
"transitions",
"final_node",
"termination_reason",
"degraded",
"evidence_retry_count"),
traceMap.keySet());
@SuppressWarnings("unchecked")
Map<String, Object> transitionMap =
((List<Map<String, Object>>) traceMap.get("transitions")).get(0);
assertEquals(Set.of("from", "to", "reason_code", "attempt"),
transitionMap.keySet());
assertFalse(traceMap.toString().contains("prompt"));
assertFalse(traceMap.toString().contains("tool_output"));
}
@Test
void fallbackFinalEventMarksTraceDegraded() {
OverAllState state = stateWithEvents(List.of(
event("planner", "NON_RETRYABLE_FAILED",
"planner_non_retryable_failed", 1),
event("fallback", "COMPLETED", "fallback_completed", 1)
), 0);
DiagnosisOrchestrationTrace trace = builder.build(state);
assertEquals("fallback", trace.finalNode());
assertEquals("fallback_completed", trace.terminationReason());
assertTrue(trace.degraded());
}
@Test
void emptyEventsFailExplicitly() {
OverAllState state = new OverAllState(Map.of());
IllegalStateException error = assertThrows(
IllegalStateException.class,
() -> builder.build(state));
assertEquals("orchestration events must not be empty",
error.getMessage());
}
@Test
void unsupportedEventValueFailsExplicitly() {
OverAllState state = new OverAllState(Map.of(
DiagnosisGraphState.ORCHESTRATION_EVENTS,
List.of(Map.of("node", "planner"))));
IllegalArgumentException error = assertThrows(
IllegalArgumentException.class,
() -> builder.build(state));
assertTrue(error.getMessage().contains("unsupported value"));
}
@Test
void eventRejectsBlankRoutingMetadataAndInvalidAttempt() {
assertThrows(IllegalArgumentException.class,
() -> event("", "COMPLETED", "completed", 1));
assertThrows(IllegalArgumentException.class,
() -> event("planner", " ", "completed", 1));
assertThrows(IllegalArgumentException.class,
() -> event("planner", "COMPLETED", null, 1));
assertThrows(IllegalArgumentException.class,
() -> event("planner", "COMPLETED", "completed", 0));
}
@Test
void traceAndTransitionCopiesAreImmutable() {
DiagnosisOrchestrationTrace trace = builder.build(stateWithEvents(
List.of(
event("planner", "COMPLETED", "completed", 1),
event("composer", "COMPLETED", "completed", 1)),
0));
assertThrows(UnsupportedOperationException.class,
() -> trace.transitions().add(
new OrchestrationTransition("x", "y", "z", 1)));
assertThrows(UnsupportedOperationException.class,
() -> trace.toMap().put("prompt", "forbidden"));
assertThrows(UnsupportedOperationException.class,
() -> trace.transitions().get(0).toMap()
.put("tool_output", "forbidden"));
}
private OverAllState stateWithEvents(
List<OrchestrationEvent> events,
int evidenceRetryCount) {
return new OverAllState(Map.of(
DiagnosisGraphState.ORCHESTRATION_EVENTS, events,
DiagnosisGraphState.EVIDENCE_RETRY_COUNT, evidenceRetryCount));
}
private OrchestrationEvent event(
String node,
String outcome,
String reasonCode,
int attempt) {
return new OrchestrationEvent(node, outcome, reasonCode, attempt);
}
}
@@ -0,0 +1,130 @@
package com.superbiz.agent.graph.diagnosis;
import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.action.AsyncNodeActionWithConfig;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
final class ScriptedDiagnosisGraphActions {
private final Map<String, ScriptedNode> nodes = new LinkedHashMap<>();
private final List<String> sequence = new ArrayList<>();
ScriptedDiagnosisGraphActions() {
nodes.put(DiagnosisGraphTopology.Node.PLANNER,
new ScriptedNode(DiagnosisGraphTopology.Node.PLANNER));
nodes.put(DiagnosisGraphTopology.Node.EXECUTOR,
new ScriptedNode(DiagnosisGraphTopology.Node.EXECUTOR));
nodes.put(DiagnosisGraphTopology.Node.GATEKEEPER,
new ScriptedNode(DiagnosisGraphTopology.Node.GATEKEEPER));
nodes.put(DiagnosisGraphTopology.Node.VERIFIED_INPUT,
new ScriptedNode(DiagnosisGraphTopology.Node.VERIFIED_INPUT));
nodes.put(DiagnosisGraphTopology.Node.VERIFIER,
new ScriptedNode(DiagnosisGraphTopology.Node.VERIFIER));
nodes.put(DiagnosisGraphTopology.Node.EVIDENCE_RETRY,
new ScriptedNode(DiagnosisGraphTopology.Node.EVIDENCE_RETRY));
nodes.put(DiagnosisGraphTopology.Node.COMPOSER,
new ScriptedNode(DiagnosisGraphTopology.Node.COMPOSER));
nodes.put(DiagnosisGraphTopology.Node.FALLBACK,
new ScriptedNode(DiagnosisGraphTopology.Node.FALLBACK));
}
ScriptedDiagnosisGraphActions step(
String node,
String outcome,
String reasonCode,
Map<String, Object> update) {
node(node).steps.addLast(new Step(outcome, reasonCode, Map.copyOf(update)));
return this;
}
DiagnosisGraphActions actions() {
return new DiagnosisGraphActions(
node(DiagnosisGraphTopology.Node.PLANNER).action(),
node(DiagnosisGraphTopology.Node.EXECUTOR).action(),
node(DiagnosisGraphTopology.Node.GATEKEEPER).action(),
node(DiagnosisGraphTopology.Node.VERIFIED_INPUT).action(),
node(DiagnosisGraphTopology.Node.VERIFIER).action(),
node(DiagnosisGraphTopology.Node.EVIDENCE_RETRY).action(),
node(DiagnosisGraphTopology.Node.COMPOSER).action(),
node(DiagnosisGraphTopology.Node.FALLBACK).action());
}
List<String> sequence() {
return List.copyOf(sequence);
}
int calls(String node) {
return node(node).calls;
}
List<String> threadIds(String node) {
return List.copyOf(node(node).threadIds);
}
private ScriptedNode node(String name) {
ScriptedNode node = nodes.get(name);
if (node == null) {
throw new IllegalArgumentException("unknown node: " + name);
}
return node;
}
private final class ScriptedNode {
private final String name;
private final Deque<Step> steps = new ArrayDeque<>();
private final List<String> threadIds = new ArrayList<>();
private int calls;
private ScriptedNode(String name) {
this.name = name;
}
private AsyncNodeActionWithConfig action() {
return this::apply;
}
private CompletableFuture<Map<String, Object>> apply(
OverAllState state,
RunnableConfig config) {
if (steps.isEmpty()) {
throw new IllegalStateException(
"unexpected invocation of node " + name);
}
Step step = steps.removeFirst();
calls++;
sequence.add(name);
threadIds.add(config.threadId().orElse(null));
Map<String, Object> update = new LinkedHashMap<>(step.update());
update.put(DiagnosisGraphState.ORCHESTRATION_EVENTS,
List.of(new OrchestrationEvent(
name,
step.outcome(),
step.reasonCode(),
calls)));
return CompletableFuture.completedFuture(update);
}
}
private record Step(
String outcome,
String reasonCode,
Map<String, Object> update) {
private Step {
Objects.requireNonNull(outcome, "outcome");
Objects.requireNonNull(reasonCode, "reasonCode");
Objects.requireNonNull(update, "update");
}
}
}