From 4a7f0e211e67e6a6b9ce2fc8fc57cd92ef8d233b Mon Sep 17 00:00:00 2001 From: mateaix <7333791@qq.com> Date: Mon, 14 Sep 2026 21:57:01 +0800 Subject: [PATCH] fix: keep managed JSON goals running until completion is committed --- .../binding/service/AgentBindingService.java | 4 ++ .../agent/graph/StateGraphReActAgent.java | 4 ++ .../agent/graph/node/GoalEvaluationNode.java | 9 ++++- .../plan/StateGraphPlanExecuteAgent.java | 4 ++ .../goal/service/GoalFollowupService.java | 4 +- .../goal/service/GoalJsonProtocolHints.java | 17 ++++++++ .../mate/goal/service/GoalRunCoordinator.java | 8 +++- .../docs/en/managed-json-acceptance.md | 2 + .../docs/zh/managed-json-acceptance.md | 2 + .../binding/AgentBindingServiceTest.java | 2 + .../GoalEvaluationNodeContinuationTest.java | 15 +++++++ .../mate/goal/GoalJsonProtocolPromptTest.java | 40 +++++++++++++++++++ .../goal/service/GoalFollowupServiceTest.java | 15 +++++++ .../goal/service/GoalRunCoordinatorTest.java | 11 +++++ 14 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 mateclaw-server/src/main/java/vip/mate/goal/service/GoalJsonProtocolHints.java create mode 100644 mateclaw-server/src/test/java/vip/mate/goal/GoalJsonProtocolPromptTest.java diff --git a/mateclaw-server/src/main/java/vip/mate/agent/binding/service/AgentBindingService.java b/mateclaw-server/src/main/java/vip/mate/agent/binding/service/AgentBindingService.java index d0e15ef2..bbc28df1 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/binding/service/AgentBindingService.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/binding/service/AgentBindingService.java @@ -730,6 +730,10 @@ public class AgentBindingService implements AgentBindingResolver { "addGoalCriterion", "completeGoal", "getGoalStatus", + // User-selected managed JSON protocol, authorized again inside each tool service. + "getManagedGoalJsonSlots", + "publishManagedGoalJson", + "checkManagedGoalJson", "waitForGoalInput", // Conversation-scoped progress ledger — same rationale as the // goal primitives above. Long multi-step research / drafting diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/StateGraphReActAgent.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/StateGraphReActAgent.java index 8b6a98ef..2d08592e 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/StateGraphReActAgent.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/StateGraphReActAgent.java @@ -700,6 +700,10 @@ public class StateGraphReActAgent extends BaseAgent implements StructuredStreamC goalService.findActiveByConversation(conversationId); if (active != null) { inputs.put(MateClawStateKeys.ACTIVE_GOAL, active); + if (active.isJsonAcceptanceRequired()) { + inputs.put(SYSTEM_PROMPT, inputs.get(SYSTEM_PROMPT) + "\n\n" + + vip.mate.goal.service.GoalJsonProtocolHints.INSTRUCTIONS); + } } } catch (Exception e) { log.warn("[{}] findActiveByConversation failed: {}", agentName, e.getMessage()); diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/node/GoalEvaluationNode.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/node/GoalEvaluationNode.java index 439bd0a2..6a8960d2 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/node/GoalEvaluationNode.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/node/GoalEvaluationNode.java @@ -232,8 +232,15 @@ public class GoalEvaluationNode implements NodeAction { } catch (Throwable t) { log.warn("[GoalEvaluationNode] terminal write failed for goal={} — degrading to evaluated-only: {}", refreshed.getId(), t.toString()); + Map outward = result.toMap(); + if (refreshed.isJsonAcceptanceRequired() && result.completed()) { + outward.put("completed", false); + outward.put("decision", GoalEvaluationResult.DECISION_CONTINUE); + outward.put("gap", "Managed JSON completion was not committed. " + + vip.mate.goal.service.GoalJsonProtocolHints.INSTRUCTIONS); + } return MateClawStateAccessor.output() - .goalEvaluationResult(result.toMap()) + .goalEvaluationResult(outward) .goalEvaluatedThisRun(true) .events(List.of(skippedEvent(refreshed.getId(), "terminal_write_failed"))) .build(); diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/StateGraphPlanExecuteAgent.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/StateGraphPlanExecuteAgent.java index 7c5c6120..e64a6842 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/StateGraphPlanExecuteAgent.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/StateGraphPlanExecuteAgent.java @@ -389,6 +389,10 @@ public class StateGraphPlanExecuteAgent extends BaseAgent implements StructuredS goalService.findActiveByConversation(conversationId); if (active != null) { inputs.put(MateClawStateKeys.ACTIVE_GOAL, active); + if (active.isJsonAcceptanceRequired()) { + inputs.put(MateClawStateKeys.SYSTEM_PROMPT, inputs.get(MateClawStateKeys.SYSTEM_PROMPT) + "\n\n" + + vip.mate.goal.service.GoalJsonProtocolHints.INSTRUCTIONS); + } } } catch (Exception e) { log.warn("[{}] findActiveByConversation failed: {}", agentName, e.getMessage()); diff --git a/mateclaw-server/src/main/java/vip/mate/goal/service/GoalFollowupService.java b/mateclaw-server/src/main/java/vip/mate/goal/service/GoalFollowupService.java index f9573745..e46207b4 100644 --- a/mateclaw-server/src/main/java/vip/mate/goal/service/GoalFollowupService.java +++ b/mateclaw-server/src/main/java/vip/mate/goal/service/GoalFollowupService.java @@ -46,7 +46,8 @@ public class GoalFollowupService { boolean persistent = Boolean.TRUE.equals(goal.getPersistentExecution()); boolean claimedComplete = !fallback && (result.completed() || GoalEvaluationResult.DECISION_COMPLETED.equals(result.decision())); - boolean completionUnverified = claimedComplete && persistent && !hasVerifiedChecklist(goal); + boolean completionUnverified = claimedComplete && (goal.isJsonAcceptanceRequired() + || (persistent && !hasVerifiedChecklist(goal))); if (claimedComplete && !completionUnverified) { return decision(Action.COMPLETE, null, null, "criteria_completed"); } @@ -125,6 +126,7 @@ public class GoalFollowupService { if (gap != null && !gap.isBlank()) { prompt.append("\nLatest evaluation: ").append(bounded(gap, 1000)); } + if (goal.isJsonAcceptanceRequired()) prompt.append("\n").append(GoalJsonProtocolHints.INSTRUCTIONS); if (persistent) { prompt.append("\nIf essential input or permission is still unavailable after checking existing state, ") .append("call waitForGoalInput with the precise missing requirement and ask the user once. ") diff --git a/mateclaw-server/src/main/java/vip/mate/goal/service/GoalJsonProtocolHints.java b/mateclaw-server/src/main/java/vip/mate/goal/service/GoalJsonProtocolHints.java new file mode 100644 index 00000000..4ff6ccf8 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/goal/service/GoalJsonProtocolHints.java @@ -0,0 +1,17 @@ +package vip.mate.goal.service; + +/** Stable runtime guidance; user-controlled requirement text is retrieved through authorized tools. */ +public final class GoalJsonProtocolHints { + private GoalJsonProtocolHints() { } + public static final String INSTRUCTIONS = """ + This goal has user-selected managed JSON acceptance requirements. Before claiming completion, + call getManagedGoalJsonSlots to read current requirements and generations. Produce the requested + JSON using publishManagedGoalJson, then call checkManagedGoalJson for every requirement using + its exact current revision, artifact ID and generation. Publishing a version alone is not a check. + A new version, an edited requirement or goal definition, or expiry invalidates earlier bindings. + Reload after conflicts and check current versions; do not invent PASS results, overwrite blindly, + or substitute ordinary file checks or textual claims. Existing semantic criteria still apply. + Only the platform's committed Goal status establishes completion. If runtime identity or access + is unavailable, report the precise missing access instead of claiming success. + """; +} diff --git a/mateclaw-server/src/main/java/vip/mate/goal/service/GoalRunCoordinator.java b/mateclaw-server/src/main/java/vip/mate/goal/service/GoalRunCoordinator.java index c830d9b4..95984d4a 100644 --- a/mateclaw-server/src/main/java/vip/mate/goal/service/GoalRunCoordinator.java +++ b/mateclaw-server/src/main/java/vip/mate/goal/service/GoalRunCoordinator.java @@ -100,9 +100,15 @@ public class GoalRunCoordinator { private Settlement classify(ClaimedRun run,SegmentOutcome outcome,GoalEntity fresh,LocalDateTime now) { int failures=run.candidate().failures(); - if(fresh!=null && fresh.getStatus()==GoalStatus.COMPLETED || outcome instanceof SegmentOutcome.Complete) { + if(fresh!=null && fresh.getStatus()==GoalStatus.COMPLETED + || outcome instanceof SegmentOutcome.Complete && (fresh==null || !fresh.isJsonAcceptanceRequired())) { return new Settlement("succeeded","completed",now,0,"goal_completed",null); } + if(outcome instanceof SegmentOutcome.Complete && fresh!=null && fresh.isJsonAcceptanceRequired()) { + return eligible(fresh) + ? new Settlement("retryable","retry",now.plusSeconds(5),Math.min(1000,failures+1),"json_completion_not_committed","acceptance") + : new Settlement("cancelled","paused",now,0,"goal_not_runnable",null); + } if(fresh!=null && fresh.getStatus()==GoalStatus.PAUSED && goals.isBudgetExhausted(fresh)) { return new Settlement("succeeded","budget_limited",now,0,goals.exhaustionReason(fresh),null); } diff --git a/mateclaw-server/src/main/resources/docs/en/managed-json-acceptance.md b/mateclaw-server/src/main/resources/docs/en/managed-json-acceptance.md index 82a9acc1..59e00484 100644 --- a/mateclaw-server/src/main/resources/docs/en/managed-json-acceptance.md +++ b/mateclaw-server/src/main/resources/docs/en/managed-json-acceptance.md @@ -32,3 +32,5 @@ After publication, call `POST /checks/{criterionKey}` with `expectedRequirementR `GET /checks` reads each requirement's current eligibility. Requirement edits, goal-definition edits, a new slot version, expiry or failed body integrity checks invalidate previous bindings. Recheck the current inputs. Binding and goal-version updates share a transaction; rollback cannot leave a passing credential. Historical diagnostic APIs retain `acceptanceEligible=false`; only managed checks create bindings. Completion events retain the accepted requirement revisions, artifact IDs and generations. Transaction rollback emits neither a completion event nor completion memory. This is an explicit per-goal managed JSON protocol with a limited scope. The broad execution-evidence ledger retains its existing prerequisites for global ENFORCE. Ordinary tool-success text and diagnostic MATCH results never become bindings automatically. Backend services cover success, invalidation, races and rollback; full browser/service flows, restart and external database validation are still in progress. + +ReAct, Plan and persistent-goal continuations receive managed JSON instructions. Business-skill tool allowlists retain the three goal-level read, publish and check tools, while service identity checks and child-agent restrictions still apply. For selected goals, follow-up and scheduling projections cannot end on a model completion claim or segment Complete alone: the Goal must already have committed completed status. Rejected automatic completion produces a continue result with recheck guidance. diff --git a/mateclaw-server/src/main/resources/docs/zh/managed-json-acceptance.md b/mateclaw-server/src/main/resources/docs/zh/managed-json-acceptance.md index dfabd6b4..310c1ae5 100644 --- a/mateclaw-server/src/main/resources/docs/zh/managed-json-acceptance.md +++ b/mateclaw-server/src/main/resources/docs/zh/managed-json-acceptance.md @@ -32,3 +32,5 @@ `GET /checks` 读取每条要求的当前资格。要求修改、Goal 定义修改、槽出现新版本、版本过期或正文完整性失败都会使旧绑定失效;需要按当前条件重新检查。每次绑定与 Goal version 更新同事务,失败回滚不留下通过凭据。历史诊断接口的 `acceptanceEligible=false` 保持不变,只有此受管版本检查产生绑定。完成事件保留本次受管绑定的条件修订、产物 ID 和 generation 引用;事务回滚不发布完成事件或完成记忆。 这是逐 Goal 显式选择的有限受管 JSON 协议;宽泛执行证据账本的全局 ENFORCE 配置仍遵循原有准入限制。此协议不把任何普通工具成功文本或诊断 MATCH 自动升级为绑定。后端服务已覆盖成功、失效、竞争和回滚;完整浏览器服务闭环、重启和外部数据库验证仍在推进。 + +代理在 ReAct、Plan 和持久 Goal 续跑入口都会收到受管 JSON 操作指引。业务技能的工具白名单保留读取、发布和检查这三个 Goal 通用工具,仍执行服务端身份校验与子代理禁用。选中模式下,follow-up 和调度投影不能凭模型的“已完成”或 segment Complete 声明结束;必须先有已提交的 Goal completed 状态。自动完成被拒绝时,向运行时返回 continue 和重检指引,不暴露已接受完成的信号。 diff --git a/mateclaw-server/src/test/java/vip/mate/agent/binding/AgentBindingServiceTest.java b/mateclaw-server/src/test/java/vip/mate/agent/binding/AgentBindingServiceTest.java index 0f3dc8ff..c833c189 100644 --- a/mateclaw-server/src/test/java/vip/mate/agent/binding/AgentBindingServiceTest.java +++ b/mateclaw-server/src/test/java/vip/mate/agent/binding/AgentBindingServiceTest.java @@ -534,6 +534,8 @@ class AgentBindingServiceTest { Set effective = bindingService.getEffectiveToolNames(agentId); assertNotNull(effective, "toolsDisabled=true 时绝不能返回 null(那会让全局默认工具又流回来)"); assertTrue(effective.contains("record_lesson"), "system-level memory 工具必须保留"); + assertTrue(effective.containsAll(Set.of("getManagedGoalJsonSlots", "publishManagedGoalJson", "checkManagedGoalJson")), + "选中的 JSON 要求不能因业务技能绑定失去受管发布和检查入口"); boolean hasMcp = effective.stream().anyMatch(n -> n != null && n.startsWith("mcp_")); assertFalse(hasMcp, "toolsDisabled=true 时 enabled MCP 工具绝不能自动并入 —— 否则用户的 '禁用所有工具' 意图被违背。" diff --git a/mateclaw-server/src/test/java/vip/mate/agent/graph/node/GoalEvaluationNodeContinuationTest.java b/mateclaw-server/src/test/java/vip/mate/agent/graph/node/GoalEvaluationNodeContinuationTest.java index 3312c8b3..cf75bb43 100644 --- a/mateclaw-server/src/test/java/vip/mate/agent/graph/node/GoalEvaluationNodeContinuationTest.java +++ b/mateclaw-server/src/test/java/vip/mate/agent/graph/node/GoalEvaluationNodeContinuationTest.java @@ -231,6 +231,21 @@ class GoalEvaluationNodeContinuationTest { assertEquals(Boolean.TRUE, out.get(MateClawStateKeys.GOAL_EVALUATED_THIS_RUN)); } + @Test void rejectedManagedCompletionDoesNotExposeACompletedResult() throws Exception { + Fixture f = new Fixture(); + GoalEntity goal = new GoalEntity(); goal.setId(1L); goal.setJsonAcceptanceRequired(true); + when(f.goalService.getById(1L)).thenReturn(goal); + var claim = new GoalEvaluationResult(1, "done", "completed", true, "fixture", 1, 0, List.of(), null); + when(f.evaluationService.evaluate(any(), anyList(), anyString())).thenReturn(claim); + when(f.goalService.markEvaluatedCompleted(1L, claim)).thenThrow(new vip.mate.exception.MateClawException(409, "binding missing")); + var out = f.node().apply(f.state(FinishReason.NORMAL.getValue(), 0, 0)); + var result = (Map) out.get(MateClawStateKeys.GOAL_EVALUATION_RESULT); + assertEquals(false, result.get("completed")); + assertEquals("continue", result.get("decision")); + assertTrue(result.get("gap").toString().contains("checkManagedGoalJson")); + verify(f.goalService, never()).markCompleted(any(), any()); + } + // ===== Test fixture ===== private static final class Fixture { diff --git a/mateclaw-server/src/test/java/vip/mate/goal/GoalJsonProtocolPromptTest.java b/mateclaw-server/src/test/java/vip/mate/goal/GoalJsonProtocolPromptTest.java new file mode 100644 index 00000000..e781bcef --- /dev/null +++ b/mateclaw-server/src/test/java/vip/mate/goal/GoalJsonProtocolPromptTest.java @@ -0,0 +1,40 @@ +package vip.mate.goal; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.test.util.ReflectionTestUtils; +import vip.mate.agent.BaseAgent; +import vip.mate.agent.graph.StateGraphReActAgent; +import vip.mate.agent.graph.plan.StateGraphPlanExecuteAgent; +import vip.mate.agent.graph.state.MateClawStateKeys; +import vip.mate.goal.model.GoalEntity; +import vip.mate.goal.service.GoalService; +import vip.mate.workspace.conversation.ConversationService; + +import java.util.Map; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class GoalJsonProtocolPromptTest { + @ParameterizedTest @ValueSource(booleans = {false, true}) + void bothGraphEntryPointsExplainManagedProtocolOnlyForSelectedGoals(boolean plan) { + var conversations = mock(ConversationService.class); + var client = mock(ChatClient.class); + BaseAgent agent = plan ? new StateGraphPlanExecuteAgent(client, conversations, null, null, null, null) + : new StateGraphReActAgent(client, conversations, null, null, null); + var goals = mock(GoalService.class); + var goal = new GoalEntity(); goal.setId(1L); goal.setJsonAcceptanceRequired(true); + when(goals.findActiveByConversation("conv")).thenReturn(goal); + ReflectionTestUtils.setField(agent, "goalService", goals); + ReflectionTestUtils.setField(agent, "systemPrompt", "base instructions"); + Map selected = ReflectionTestUtils.invokeMethod(agent, "buildInitialState", "write report", "conv"); + assertNotNull(selected); + assertTrue(selected.get(MateClawStateKeys.SYSTEM_PROMPT).toString().contains("checkManagedGoalJson")); + assertTrue(selected.get(MateClawStateKeys.SYSTEM_PROMPT).toString().startsWith("base instructions")); + goal.setJsonAcceptanceRequired(false); + Map legacy = ReflectionTestUtils.invokeMethod(agent, "buildInitialState", "write report", "conv"); + assertNotNull(legacy); + assertEquals("base instructions", legacy.get(MateClawStateKeys.SYSTEM_PROMPT)); + } +} diff --git a/mateclaw-server/src/test/java/vip/mate/goal/service/GoalFollowupServiceTest.java b/mateclaw-server/src/test/java/vip/mate/goal/service/GoalFollowupServiceTest.java index 44a5c396..ac825ede 100644 --- a/mateclaw-server/src/test/java/vip/mate/goal/service/GoalFollowupServiceTest.java +++ b/mateclaw-server/src/test/java/vip/mate/goal/service/GoalFollowupServiceTest.java @@ -280,4 +280,19 @@ class GoalFollowupServiceTest { assertTrue(prompt.contains("difficulty")); assertTrue(prompt.contains("time")); } + @Test void selectedJsonGoalRequiresCommittedCompletionEvenWithPassingChecklist() { + GoalEntity goal = goal(true); + goal.setPersistentExecution(true); + goal.setJsonAcceptanceRequired(true); + goal.setCriteria("[{\"id\":\"C1\",\"text\":\"report\",\"passed\":true,\"evidence\":\"claimed\"}]"); + var claimed = res(1, GoalEvaluationResult.DECISION_COMPLETED); + var decision = svc.decide(goal, claimed, LocalDateTime.now()); + assertEquals(Action.RETRY, decision.action()); + assertTrue(decision.prompt().contains("getManagedGoalJsonSlots")); + assertTrue(decision.prompt().contains("publishManagedGoalJson")); + assertTrue(decision.prompt().contains("checkManagedGoalJson")); + goal.setStatus(GoalStatus.COMPLETED); + assertEquals(Action.COMPLETE, svc.decide(goal, claimed, LocalDateTime.now()).action()); + } + } diff --git a/mateclaw-server/src/test/java/vip/mate/goal/service/GoalRunCoordinatorTest.java b/mateclaw-server/src/test/java/vip/mate/goal/service/GoalRunCoordinatorTest.java index f8d2b212..f5fb5527 100644 --- a/mateclaw-server/src/test/java/vip/mate/goal/service/GoalRunCoordinatorTest.java +++ b/mateclaw-server/src/test/java/vip/mate/goal/service/GoalRunCoordinatorTest.java @@ -87,4 +87,15 @@ class GoalRunCoordinatorTest { assertTrue(coordinator.settle(second,new SegmentOutcome.Continue("unfinished"),secondStart)); assertEquals(secondStart.plusSeconds(600),continuations.get(1L).nextRunAt()); } + @Test void selectedJsonGoalCannotSettleCompletedFromSegmentClaimAlone() { + goal.setJsonAcceptanceRequired(true); + var run=coordinator.claim(continuations.get(1L),goal,now); + assertNotNull(run); + assertTrue(coordinator.markRunning(run,now)); + assertTrue(coordinator.settle(run,new SegmentOutcome.Complete("model claim"),now)); + assertEquals("retry",continuations.get(1L).state()); + assertEquals("retryable",attempts.get(run.attempt().id()).state()); + assertEquals("json_completion_not_committed",continuations.get(1L).reason()); + } + }