fix(goal): preserve managed JSON selection in streamed snapshots

This commit is contained in:
mateaix 2026-09-14 23:40:52 +08:00
parent 66d8cc6c3b
commit 978ec99a93
2 changed files with 33 additions and 5 deletions

View File

@ -401,6 +401,7 @@ public class GoalEvaluationNode implements NodeAction {
snapshot.put("successCheckPrompt", goal.getSuccessCheckPrompt()); snapshot.put("successCheckPrompt", goal.getSuccessCheckPrompt());
snapshot.put("status", goal.getStatus() == null ? null : goal.getStatus().getValue()); snapshot.put("status", goal.getStatus() == null ? null : goal.getStatus().getValue());
snapshot.put("persistentExecution", goal.getPersistentExecution()); snapshot.put("persistentExecution", goal.getPersistentExecution());
snapshot.put("jsonAcceptanceRequired", goal.isJsonAcceptanceRequired());
snapshot.put("turnBudget", goal.getTurnBudget()); snapshot.put("turnBudget", goal.getTurnBudget());
snapshot.put("turnsUsed", goal.getTurnsUsed()); snapshot.put("turnsUsed", goal.getTurnsUsed());
snapshot.put("llmCallBudget", goal.getLlmCallBudget()); snapshot.put("llmCallBudget", goal.getLlmCallBudget());

View File

@ -131,7 +131,15 @@ class GoalJsonGraphIntegrationTest {
when(model.stream(any(Prompt.class))).thenAnswer(invocation -> Flux.just(script.answer(invocation))); when(model.stream(any(Prompt.class))).thenAnswer(invocation -> Flux.just(script.answer(invocation)));
var agent = graphAgent(plan, toolSet, model); var agent = graphAgent(plan, toolSet, model);
ChatOriginHolder.set(origin); ChatOriginHolder.set(origin);
try { assertNotNull(agent.chat("Produce and check the managed JSON report.", conversation)); } try {
if (automatic) {
var deltas = structured(agent, "Produce and check the managed JSON report.", conversation);
var completed = deltas.stream().filter(d -> "goal_completed".equals(d.eventType())).toList();
assertEquals(1, completed.size(), "One committed completion event");
var snapshot = (Map<?, ?>) completed.getFirst().eventData().get("goal");
assertEquals(Boolean.TRUE, snapshot.get("jsonAcceptanceRequired"), "SSE must preserve the selected acceptance protocol");
} else assertNotNull(agent.chat("Produce and check the managed JSON report.", conversation));
}
finally { ChatOriginHolder.clear(); } finally { ChatOriginHolder.clear(); }
assertEquals(GoalStatus.COMPLETED, goals.getById(goal.getId()).getStatus()); assertEquals(GoalStatus.COMPLETED, goals.getById(goal.getId()).getStatus());
assertTrue(bindings.state(goal.getId(), username).getFirst().acceptanceEligible()); assertTrue(bindings.state(goal.getId(), username).getFirst().acceptanceEligible());
@ -144,9 +152,9 @@ class GoalJsonGraphIntegrationTest {
assertTrue(calls.get() >= (automatic ? 5 : 6) && calls.get() <= 10, "Bounded scripted model calls: " + calls.get()); assertTrue(calls.get() >= (automatic ? 5 : 6) && calls.get() <= 10, "Bounded scripted model calls: " + calls.get());
} }
@org.junit.jupiter.params.ParameterizedTest @org.junit.jupiter.params.ParameterizedTest
@org.junit.jupiter.params.provider.ValueSource(booleans = {false, true}) @org.junit.jupiter.params.provider.CsvSource({"false,false", "true,false", "false,true", "true,true"})
void automaticGraphCannotPromoteAPassingSemanticVerdictWithoutManagedBytes(boolean plan) throws Exception { void automaticGraphCannotPromoteAPassingSemanticVerdictWithoutManagedBytes(boolean plan, boolean scheduled) throws Exception {
Fixture fixture = configuredGoal(false); Fixture fixture = configuredGoal(scheduled);
when(evaluator.evaluate(any(), anyList(), anyString())).thenReturn(new GoalEvaluationResult( when(evaluator.evaluate(any(), anyList(), anyString())).thenReturn(new GoalEvaluationResult(
1, "PASS from offline semantic fixture", "completed", true, "fixture", 1, 0, 1, "PASS from offline semantic fixture", "completed", true, "fixture", 1, 0,
List.of(new GoalChecklistVerdict.CriterionVerdict("C1", true, "fixture only")), null)); List.of(new GoalChecklistVerdict.CriterionVerdict("C1", true, "fixture only")), null));
@ -166,16 +174,35 @@ class GoalJsonGraphIntegrationTest {
when(model.stream(any(Prompt.class))).thenAnswer(invocation -> Flux.just(script.answer(invocation))); when(model.stream(any(Prompt.class))).thenAnswer(invocation -> Flux.just(script.answer(invocation)));
var agent = graphAgent(plan, toolSet, model); var agent = graphAgent(plan, toolSet, model);
ChatOriginHolder.set(fixture.origin()); ChatOriginHolder.set(fixture.origin());
try { assertNotNull(agent.chat("Read the current managed JSON requirements and report status.", fixture.conversation())); } try {
var deltas = structured(agent, "Read the current managed JSON requirements and report status.", fixture.conversation());
assertTrue(deltas.stream().noneMatch(d -> "goal_completed".equals(d.eventType())));
assertTrue(deltas.stream().anyMatch(d -> "goal_evaluated".equals(d.eventType())
&& Boolean.TRUE.equals(d.eventData().get("skipped"))
&& "terminal_write_failed".equals(d.eventData().get("reason"))), "The retry consumer receives a failed completion event");
}
finally { ChatOriginHolder.clear(); } finally { ChatOriginHolder.clear(); }
verify(evaluator, atLeastOnce()).evaluate(any(), anyList(), anyString()); verify(evaluator, atLeastOnce()).evaluate(any(), anyList(), anyString());
assertEquals(GoalStatus.ACTIVE, goals.getById(fixture.goal().getId()).getStatus()); assertEquals(GoalStatus.ACTIVE, goals.getById(fixture.goal().getId()).getStatus());
assertTrue(goals.getById(fixture.goal().getId()).isJsonAcceptanceRequired()); assertTrue(goals.getById(fixture.goal().getId()).isJsonAcceptanceRequired());
assertEquals("NO_ARTIFACT", bindings.state(fixture.goal().getId(), fixture.username()).getFirst().status()); assertEquals("NO_ARTIFACT", bindings.state(fixture.goal().getId(), fixture.username()).getFirst().status());
assertTrue(goals.listEvents(fixture.goal().getId(), 30).stream().noneMatch(e -> "completed".equals(e.getEventType()))); assertTrue(goals.listEvents(fixture.goal().getId(), 30).stream().noneMatch(e -> "completed".equals(e.getEventType())));
if (scheduled) {
assertTrue(coordinator.settle(fixture.run(), new SegmentOutcome.Retry("evaluation", "evaluation_unavailable"), java.time.LocalDateTime.now()));
assertEquals("retry", continuations.get(fixture.goal().getId()).state());
}
assertTrue(calls.get() < 10, "Bounded offline rejection flow: " + calls.get()); assertTrue(calls.get() < 10, "Bounded offline rejection flow: " + calls.get());
} }
private List<vip.mate.agent.AgentService.StreamDelta> structured(vip.mate.agent.BaseAgent agent, String prompt, String conversation) {
var stream = agent instanceof StateGraphReActAgent react
? react.chatStructuredStream(prompt, conversation)
: ((vip.mate.agent.graph.plan.StateGraphPlanExecuteAgent) agent).chatStructuredStream(prompt, conversation);
var deltas = stream.collectList().block(java.time.Duration.ofSeconds(30));
assertNotNull(deltas);
return deltas;
}
private record Fixture(String username, String conversation, GoalEntity goal, private record Fixture(String username, String conversation, GoalEntity goal,
GoalRunCoordinator.ClaimedRun run, ChatOrigin origin) { } GoalRunCoordinator.ClaimedRun run, ChatOrigin origin) { }