From eca4229751ca54ee3e3c46ca18df7936cdbfe1bd Mon Sep 17 00:00:00 2001 From: matevip Date: Sun, 21 Jun 2026 21:20:58 +0800 Subject: [PATCH] feat(plans): per-step agent delegation + fix kanban pending column (issue #385) --- .../vip/mate/agent/AgentGraphBuilder.java | 29 ++++- .../graph/plan/node/PlanGenerationNode.java | 109 +++++++++++++++++- .../graph/plan/node/StepExecutionNode.java | 102 ++++++++++++++++ .../mate/planning/model/SubPlanEntity.java | 8 ++ .../planning/service/PlanningService.java | 41 ++++++- .../mate/tool/builtin/DelegateAgentTool.java | 20 ++++ .../h2/V156__sub_plan_assigned_agent.sql | 10 ++ .../V156__sub_plan_assigned_agent.sql | 3 + .../mysql/V156__sub_plan_assigned_agent.sql | 15 +++ .../graph/state/SourceEvidenceLedgerTest.java | 4 +- .../src/components/agents/PlanBoard.vue | 63 ++++++++++ .../src/components/agents/PlanDetailPanel.vue | 26 +++++ mateclaw-ui/src/i18n/locales/en-US.ts | 1 + mateclaw-ui/src/i18n/locales/zh-CN.ts | 1 + mateclaw-ui/src/types/index.ts | 3 + 15 files changed, 427 insertions(+), 8 deletions(-) create mode 100644 mateclaw-server/src/main/resources/db/migration/h2/V156__sub_plan_assigned_agent.sql create mode 100644 mateclaw-server/src/main/resources/db/migration/kingbase/V156__sub_plan_assigned_agent.sql create mode 100644 mateclaw-server/src/main/resources/db/migration/mysql/V156__sub_plan_assigned_agent.sql diff --git a/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java b/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java index 635dac64..d42f2cad 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java @@ -157,6 +157,30 @@ public class AgentGraphBuilder { this.auditEventService = s; } + /** + * Optional per-step delegation dependencies for the Plan-Execute graph. + * Setter injection (like {@link #auditEventService}) breaks the + * {@code AgentService ⇆ AgentGraphBuilder} construction cycle. Null when not + * wired (legacy / test) — per-step delegation is then simply disabled. + */ + private AgentService agentService; + + // @Lazy on the injection point: inject a lazy-resolution proxy so the + // AgentService ⇆ AgentGraphBuilder cycle is broken at bean-creation time + // (the real bean is resolved on first use, when the graph is built). + @org.springframework.beans.factory.annotation.Autowired(required = false) + public void setAgentService(@org.springframework.context.annotation.Lazy AgentService agentService) { + this.agentService = agentService; + } + + private vip.mate.tool.builtin.DelegateAgentTool delegateAgentTool; + + @org.springframework.beans.factory.annotation.Autowired(required = false) + public void setDelegateAgentTool( + @org.springframework.context.annotation.Lazy vip.mate.tool.builtin.DelegateAgentTool delegateAgentTool) { + this.delegateAgentTool = delegateAgentTool; + } + /** * 根据 AgentEntity 构建完整的 Agent 实例(沿用 Agent / 全局默认模型)。 */ @@ -554,8 +578,11 @@ public class AgentGraphBuilder { if (auditEventService != null) { executor.setAuditEventService(auditEventService); } - PlanGenerationNode planGenerationNode = new PlanGenerationNode(chatModel, planningService, streamingHelper, conversationWindowManager, toolSet, goalService, goalProperties); + PlanGenerationNode planGenerationNode = new PlanGenerationNode(chatModel, planningService, streamingHelper, conversationWindowManager, toolSet, goalService, goalProperties, agentService); StepExecutionNode stepExecutionNode = new StepExecutionNode(chatModel, toolSet, executor, planningService, streamTracker, reasoningEffort, streamingHelper, conversationWindowManager, skillCatalogRenderer); + // Per-step delegation: route a step assigned to a specialist agent + // through DelegateAgentTool (null when delegation deps aren't wired). + stepExecutionNode.setDelegateAgentTool(delegateAgentTool); PlanSummaryNode planSummaryNode = new PlanSummaryNode(chatModel, planningService, streamingHelper); DirectAnswerNode directAnswerNode = new DirectAnswerNode(); diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/PlanGenerationNode.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/PlanGenerationNode.java index 1b7ed0e4..33280fc7 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/PlanGenerationNode.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/PlanGenerationNode.java @@ -10,8 +10,11 @@ import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.converter.BeanOutputConverter; +import org.springframework.util.StringUtils; +import vip.mate.agent.AgentService; import vip.mate.agent.AgentToolSet; import vip.mate.agent.GraphEventPublisher; +import vip.mate.agent.model.AgentEntity; import vip.mate.agent.graph.NodeStreamingChatHelper; import vip.mate.agent.graph.plan.state.PlanStateAccessor; import vip.mate.agent.graph.plan.state.PlanStateKeys; @@ -27,6 +30,7 @@ import vip.mate.goal.service.GoalService; import vip.mate.planning.service.PlanningService; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -61,6 +65,9 @@ public class PlanGenerationNode implements NodeAction { /** Optional — auto-derive a goal from the plan. Null disables the feature (legacy/test). */ private final GoalService goalService; private final GoalProperties goalProperties; + /** Optional — advertise delegatable specialist agents to the planner and + * resolve per-step assignments. Null disables per-step delegation (legacy/test). */ + private final AgentService agentService; /** Plan steps below this size are trivial tool tasks, not goal-worthy. */ private static final int MIN_STEPS_FOR_AUTO_GOAL = 2; @@ -75,7 +82,12 @@ public class PlanGenerationNode implements NodeAction { @JsonProperty("needs_planning") boolean needsPlanning, @JsonProperty("direct_answer") String directAnswer, @JsonProperty("plan_type") String planType, - @JsonProperty("steps") List steps + @JsonProperty("steps") List steps, + // Optional per-step delegation: agent names parallel to steps (same + // order). An empty string / missing entry means "run with the parent + // agent". Only populated when delegatable specialist agents are + // advertised to the planner; absent for backward compatibility. + @JsonProperty("step_agents") List stepAgents ) {} private static final String PLANNING_PROMPT = """ @@ -190,6 +202,16 @@ public class PlanGenerationNode implements NodeAction { ConversationWindowManager conversationWindowManager, AgentToolSet toolSet, GoalService goalService, GoalProperties goalProperties) { + this(chatModel, planningService, streamingHelper, conversationWindowManager, toolSet, + goalService, goalProperties, null); + } + + public PlanGenerationNode(ChatModel chatModel, PlanningService planningService, + NodeStreamingChatHelper streamingHelper, + ConversationWindowManager conversationWindowManager, + AgentToolSet toolSet, + GoalService goalService, GoalProperties goalProperties, + AgentService agentService) { this.chatModel = chatModel; this.planningService = planningService; this.streamingHelper = streamingHelper; @@ -197,6 +219,7 @@ public class PlanGenerationNode implements NodeAction { this.toolSet = toolSet; this.goalService = goalService; this.goalProperties = goalProperties; + this.agentService = agentService; } /** @@ -265,6 +288,60 @@ public class PlanGenerationNode implements NodeAction { } } + /** + * Enabled agents in the given workspace, excluding the parent (plan) agent + * itself — these are the agents a step can be delegated to. Empty when + * delegation is unavailable (no {@link AgentService}) or no peers exist. + */ + private List listDelegatableAgents(Long workspaceId, String parentAgentId) { + if (agentService == null || workspaceId == null) { + return List.of(); + } + try { + return agentService.listAgentsByWorkspace(workspaceId, true).stream() + .filter(a -> a.getId() != null && !String.valueOf(a.getId()).equals(parentAgentId)) + .collect(Collectors.toList()); + } catch (Exception e) { + log.warn("[PlanGeneration] Failed to list delegatable agents (non-fatal): {}", e.toString()); + return List.of(); + } + } + + /** + * Map the planner's {@code step_agents} (agent names, parallel to steps) to + * agent ids. Returns {@code null} when nothing is delegated so {@code createPlan} + * stays on the legacy path. Names are matched case-insensitively against the + * delegatable agents; blank / unknown / parent-agent names resolve to {@code null} + * (that step runs with the parent agent). + */ + private List resolveStepAgents(List steps, List stepAgents, + Long workspaceId, String parentAgentId) { + if (stepAgents == null || stepAgents.isEmpty() || steps == null || steps.isEmpty()) { + return null; + } + List delegatable = listDelegatableAgents(workspaceId, parentAgentId); + if (delegatable.isEmpty()) { + return null; + } + Map byName = new HashMap<>(); + for (AgentEntity a : delegatable) { + if (a.getName() != null) { + byName.put(a.getName().trim().toLowerCase(), a.getId()); + } + } + List ids = new ArrayList<>(); + boolean any = false; + for (int i = 0; i < steps.size(); i++) { + String name = i < stepAgents.size() ? stepAgents.get(i) : null; + Long id = (name == null || name.isBlank()) ? null : byName.get(name.trim().toLowerCase()); + if (id != null) { + any = true; + } + ids.add(id); + } + return any ? ids : null; + } + @Override public Map apply(OverAllState state) throws Exception { PlanStateAccessor accessor = new PlanStateAccessor(state); @@ -341,6 +418,24 @@ public class PlanGenerationNode implements NodeAction { + "\n单次工具调用应归为单步(B),不要拆成多步。")); } + // Advertise delegatable specialist agents so the planner can assign a + // multi-step plan's step to a dedicated agent (e.g. a test step to a + // QA agent, a UI step to a frontend agent). Only fills the step's + // step_agents slot; unassigned steps stay with the parent agent. + // Skipped entirely when no peer agents exist in the workspace. + List delegatable = listDelegatableAgents(chatOrigin.workspaceId(), agentId); + if (!delegatable.isEmpty()) { + String agentLines = delegatable.stream() + .map(a -> "- " + a.getName() + + (StringUtils.hasText(a.getDescription()) ? ":" + a.getDescription() : "")) + .collect(Collectors.joining("\n")); + promptMessages.add(new UserMessage( + "可委派的专职 Agent(仅当某步骤明显属于其专长时才指派,否则该步骤留空、由你自己执行):\n" + + agentLines + + "\n若要委派,在 step_agents 数组对应位置填写 Agent 名称(与 steps 同序、等长);" + + "不委派的步骤填空字符串。多数步骤通常不需要委派。")); + } + // Inject working context (rolling conversation summary) so triage respects // prior constraints without re-reading full history. String workingContext = accessor.workingContext(); @@ -455,9 +550,15 @@ public class PlanGenerationNode implements NodeAction { steps = List.of(goal); } - var plan = planningService.createPlan(agentId, conversationId, goal, steps); - log.info("[PlanGeneration] Plan created: id={}, steps={} ({})", - plan.getId(), steps.size(), steps.size() == 1 ? "single-step" : "multi-step"); + // Resolve any per-step agent delegation the planner asked for. Null + // when nothing is delegated, keeping createPlan on the legacy path. + List stepAgentIds = resolveStepAgents(steps, + triage != null ? triage.stepAgents() : null, + chatOrigin.workspaceId(), agentId); + var plan = planningService.createPlan(agentId, conversationId, goal, steps, stepAgentIds); + log.info("[PlanGeneration] Plan created: id={}, steps={} ({}){}", + plan.getId(), steps.size(), steps.size() == 1 ? "single-step" : "multi-step", + stepAgentIds != null ? ", per-step delegation=" + stepAgentIds : ""); events.add(GraphEventPublisher.planCreated(plan.getId(), steps)); diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/StepExecutionNode.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/StepExecutionNode.java index 3f306efb..d19066e3 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/StepExecutionNode.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/plan/node/StepExecutionNode.java @@ -27,7 +27,9 @@ import vip.mate.agent.context.RuntimeContextInjector; import vip.mate.agent.graph.executor.ToolExecutionExecutor; import vip.mate.channel.web.ChatStreamTracker; import vip.mate.planning.service.PlanningService; +import vip.mate.agent.context.ChatOrigin; import vip.mate.skill.runtime.SkillCatalogRenderer; +import vip.mate.tool.builtin.DelegateAgentTool; import java.util.ArrayList; import java.util.List; @@ -68,6 +70,17 @@ public class StepExecutionNode implements NodeAction { */ private final SkillCatalogRenderer skillCatalogRenderer; + /** + * Optional per-step delegation executor. Set after construction (this node is + * built by AgentGraphBuilder, not Spring) so a plan step assigned to a + * specialist agent runs on that agent. Null disables per-step delegation. + */ + private DelegateAgentTool delegateAgentTool; + + public void setDelegateAgentTool(DelegateAgentTool delegateAgentTool) { + this.delegateAgentTool = delegateAgentTool; + } + /** * Per-step tool-call ceiling, aligned with {@code BaseAgent.MAX_ITERATIONS_HARD_CEILING}. * Matching the agent-level cap means this constant is never the bottleneck — @@ -198,6 +211,19 @@ public class StepExecutionNode implements NodeAction { // "react_step" / "first_turn" markers when both stream into the // same SSE feed. boolean iterationEventsOn = streamTracker == null || streamTracker.isIterationEventsEnabled(); + + // Per-step delegation: when this step is assigned to a different + // specialist agent, run it on that agent (as an isolated child) and use + // its reply as the step result, instead of executing locally with the + // parent agent's tools. assignedAgentId comes from the DB so it survives + // replay / approval-resume. + Long assignedAgentId = planningService.getStepAssignedAgent(planId, stepIndex); + if (delegateAgentTool != null && assignedAgentId != null + && !assignedAgentId.equals(parseLongOrNull(agentId))) { + return executeDelegatedStep(accessor, stepIndex, step, planId, assignedAgentId, + chatOrigin, events, iterationEventsOn); + } + if (iterationEventsOn) { events.add(GraphEventPublisher.iterationStart(stepIndex, "plan_step", "parent", null)); } @@ -622,6 +648,82 @@ public class StepExecutionNode implements NodeAction { .build(); } + /** + * Execute a step by delegating it to its assigned specialist agent. The + * delegated agent runs the step description as a self-contained goal and its + * reply becomes the step result. Mirrors the success/failure bookkeeping of + * the local execution path (sub-plan status, completed-results accumulation, + * incremental working-context update) so the rest of the plan graph is + * unaffected by where the step ran. + */ + private Map executeDelegatedStep( + PlanStateAccessor accessor, int stepIndex, String step, Long planId, + Long assignedAgentId, ChatOrigin chatOrigin, + List events, boolean iterationEventsOn) { + + if (iterationEventsOn) { + events.add(GraphEventPublisher.iterationStart(stepIndex, "plan_step", "parent", null)); + } + events.add(GraphEventPublisher.stepStarted(stepIndex, step)); + events.add(GraphEventPublisher.phase("executing", Map.of( + "stepIndex", stepIndex, "stepTitle", step, + "delegatedAgentId", String.valueOf(assignedAgentId)))); + planningService.updateSubPlanStatus(planId, stepIndex, "running"); + + log.info("[StepExecution] Delegating step {} to agent {}", stepIndex + 1, assignedAgentId); + + String result; + try { + result = delegateAgentTool.delegateByAgentId(assignedAgentId, step, chatOrigin); + } catch (Exception e) { + log.error("[StepExecution] Delegated step {} threw: {}", stepIndex, e.getMessage(), e); + result = "[错误] 委派执行异常:" + e.getMessage(); + } + + String finalResult = result != null ? result : ""; + boolean failed = finalResult.isEmpty() || finalResult.startsWith("[错误]"); + if (failed) { + planningService.updateSubPlanFailure(planId, stepIndex, finalResult); + } else { + planningService.updateSubPlanResult(planId, stepIndex, finalResult); + } + + events.add(GraphEventPublisher.stepCompleted(stepIndex, finalResult)); + if (iterationEventsOn) { + events.add(GraphEventPublisher.iterationEnd(stepIndex, "parent", null, finalResult.length(), 0)); + } + + // Keep the rolling working-context in sync exactly like the local path + // so later steps see this delegated step's result. + String prevWorkingContext = accessor.workingContext(); + String formattedNewStep = formatStepResult(stepIndex, finalResult); + String updatedWorkingContext = prevWorkingContext.isEmpty() + ? rebuildWorkingContext(accessor, appendOne(accessor.completedResults(), formattedNewStep)) + : appendStepIncremental(prevWorkingContext, formattedNewStep); + + return PlanStateAccessor.output() + .currentStepResult(finalResult) + .completedResults(formattedNewStep) + .currentStepIndex(stepIndex + 1) + .workingContext(updatedWorkingContext) + .currentPhase("step_completed") + .contentStreamed(true) + .events(events) + .build(); + } + + /** Parse a string id to Long, or null when blank / non-numeric. */ + private static Long parseLongOrNull(String s) { + if (s == null || s.isBlank()) { + return null; + } + try { + return Long.parseLong(s.trim()); + } catch (NumberFormatException e) { + return null; + } + } + /** * RFC-052: assemble the final answer text from direct tool outputs in this * step. Mirrors {@code FinalAnswerNode#assembleDirectAnswer} so the user diff --git a/mateclaw-server/src/main/java/vip/mate/planning/model/SubPlanEntity.java b/mateclaw-server/src/main/java/vip/mate/planning/model/SubPlanEntity.java index 3b0ab79c..1775637e 100644 --- a/mateclaw-server/src/main/java/vip/mate/planning/model/SubPlanEntity.java +++ b/mateclaw-server/src/main/java/vip/mate/planning/model/SubPlanEntity.java @@ -29,6 +29,14 @@ public class SubPlanEntity { /** 步骤状态:pending / running / completed / failed */ private String status; + /** + * Delegated agent id for this step. When non-null, the executor routes this + * step to that specialist agent instead of the parent (plan) agent. Null = + * run with the parent agent (original behavior). The frontend resolves the + * display name from this id via the agent store. + */ + private Long assignedAgentId; + /** 步骤执行结果 */ @TableField(value = "result", updateStrategy = FieldStrategy.ALWAYS) private String result; diff --git a/mateclaw-server/src/main/java/vip/mate/planning/service/PlanningService.java b/mateclaw-server/src/main/java/vip/mate/planning/service/PlanningService.java index 689f8b0a..9ef9ed6f 100644 --- a/mateclaw-server/src/main/java/vip/mate/planning/service/PlanningService.java +++ b/mateclaw-server/src/main/java/vip/mate/planning/service/PlanningService.java @@ -43,11 +43,27 @@ public class PlanningService { */ @Transactional public PlanEntity createPlan(String agentId, String conversationId, String goal, List steps) { + return createPlan(agentId, conversationId, goal, steps, null); + } + + /** + * 创建执行计划,并为每个步骤可选地指派专职子 agent。 + * stepAgentIds 与 steps 等长同序(按位置对应);某位为 null 表示该步骤由父 agent 执行。 + * stepAgentIds 整体可空(无委派的历史调用方)。 + */ + @Transactional + public PlanEntity createPlan(String agentId, String conversationId, String goal, + List steps, List stepAgentIds) { PlanEntity plan = new PlanEntity(); plan.setAgentId(agentId); plan.setConversationId(conversationId); plan.setGoal(goal); - plan.setStatus("running"); + // A freshly generated plan is queued, not yet running: it sits in the + // board's "pending" column until its first step actually starts + // (updateSubPlanStatus promotes the plan to "running" then). This makes + // the board's pending column meaningful for queued / approval-gated + // plans instead of being perpetually empty. + plan.setStatus("pending"); plan.setTotalSteps(steps.size()); plan.setCompletedSteps(0); planMapper.insert(plan); @@ -58,6 +74,11 @@ public class PlanningService { sub.setStepIndex(i); sub.setDescription(steps.get(i)); sub.setStatus("pending"); + // Per-step delegation: only set when an assignment exists for this + // index; otherwise the step runs with the parent agent. + if (stepAgentIds != null && i < stepAgentIds.size()) { + sub.setAssignedAgentId(stepAgentIds.get(i)); + } subPlanMapper.insert(sub); }); @@ -74,6 +95,13 @@ public class PlanningService { sub.setStatus(status); if ("running".equals(status)) { sub.setStartTime(LocalDateTime.now()); + // Promote the parent plan out of the "pending" (queued) column + // the moment its first step actually starts executing. + PlanEntity plan = planMapper.selectById(planId); + if (plan != null && "pending".equals(plan.getStatus())) { + plan.setStatus("running"); + planMapper.updateById(plan); + } } subPlanMapper.updateById(sub); } @@ -156,6 +184,17 @@ public class PlanningService { .orderByAsc(SubPlanEntity::getStepIndex)); } + /** + * The agent delegated to run a given step, or {@code null} when the step + * runs with the parent (plan) agent. Read by the executor to route a step to + * its specialist agent. Sourced from the DB (not graph state) so it survives + * replay / approval-resume. + */ + public Long getStepAssignedAgent(Long planId, int stepIndex) { + SubPlanEntity sub = getSubPlan(planId, stepIndex); + return sub != null ? sub.getAssignedAgentId() : null; + } + /** * 标记计划失败 */ diff --git a/mateclaw-server/src/main/java/vip/mate/tool/builtin/DelegateAgentTool.java b/mateclaw-server/src/main/java/vip/mate/tool/builtin/DelegateAgentTool.java index 06921bf1..4fd4de33 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/builtin/DelegateAgentTool.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/builtin/DelegateAgentTool.java @@ -324,6 +324,26 @@ public class DelegateAgentTool { return result.toToolResponse(target.getName()); } + /** + * Delegate a task to an agent by id — used by per-step plan delegation so a + * plan step can run on a dedicated specialist agent. Resolves the target by + * id, then reuses {@link #delegateToAgent}'s isolated-child execution + * (sub-agent registry, event relay, depth guard). The parent {@link ChatOrigin} + * is forwarded so the child inherits channel / workspace binding. Returns the + * child's reply text, or an error string when the agent is missing/disabled. + */ + public String delegateByAgentId(Long agentId, String task, ChatOrigin parentOrigin) { + if (agentId == null) { + return "[错误] 未指定委派 Agent。"; + } + AgentEntity target = agentMapper.selectById(agentId); + if (target == null || !Boolean.TRUE.equals(target.getEnabled())) { + return "[错误] 未找到 id=" + agentId + " 的已启用 Agent。"; + } + ToolContext ctx = (parentOrigin != null ? parentOrigin : ChatOrigin.EMPTY).toToolContext(); + return delegateToAgent(target.getName(), task, false, ctx); + } + // ==================== Parallel delegation ==================== @vip.mate.tool.ConcurrencyUnsafe("internally fans out to its own thread pool; outer executor must not double-parallelize") diff --git a/mateclaw-server/src/main/resources/db/migration/h2/V156__sub_plan_assigned_agent.sql b/mateclaw-server/src/main/resources/db/migration/h2/V156__sub_plan_assigned_agent.sql new file mode 100644 index 00000000..9e4e31de --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/h2/V156__sub_plan_assigned_agent.sql @@ -0,0 +1,10 @@ +-- V156: Per-step agent delegation for plan-execute. +-- +-- A plan step can now be delegated to a dedicated specialist agent (e.g. a test +-- step handed to a "QA agent", a UI step to a "frontend agent"). assigned_agent_id +-- records which agent should run the step; the executor routes that step to the +-- delegated agent instead of the parent agent. +-- +-- Nullable: a NULL assigned_agent_id means "run with the parent (plan) agent", +-- which is the original behavior — legacy rows and unassigned steps are unaffected. +ALTER TABLE mate_sub_plan ADD COLUMN IF NOT EXISTS assigned_agent_id BIGINT; diff --git a/mateclaw-server/src/main/resources/db/migration/kingbase/V156__sub_plan_assigned_agent.sql b/mateclaw-server/src/main/resources/db/migration/kingbase/V156__sub_plan_assigned_agent.sql new file mode 100644 index 00000000..e9bdb410 --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/kingbase/V156__sub_plan_assigned_agent.sql @@ -0,0 +1,3 @@ +-- V156: Per-step agent delegation for plan-execute (see H2 file for context). +-- KingbaseES (PostgreSQL-compatible) supports ADD COLUMN IF NOT EXISTS. +ALTER TABLE mate_sub_plan ADD COLUMN IF NOT EXISTS assigned_agent_id BIGINT; diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V156__sub_plan_assigned_agent.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V156__sub_plan_assigned_agent.sql new file mode 100644 index 00000000..598f3c51 --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V156__sub_plan_assigned_agent.sql @@ -0,0 +1,15 @@ +-- V156: Per-step agent delegation for plan-execute (see H2 file for context). +-- MySQL 8.0 doesn't support `ADD COLUMN IF NOT EXISTS`, so the existence check +-- goes through INFORMATION_SCHEMA + a prepared statement. +SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'mate_sub_plan' + AND COLUMN_NAME = 'assigned_agent_id' +); +SET @ddl := IF(@col_exists = 0, + 'ALTER TABLE mate_sub_plan ADD COLUMN assigned_agent_id BIGINT NULL', + 'SELECT 1'); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/mateclaw-server/src/test/java/vip/mate/agent/graph/state/SourceEvidenceLedgerTest.java b/mateclaw-server/src/test/java/vip/mate/agent/graph/state/SourceEvidenceLedgerTest.java index 603fcde3..91b5212d 100644 --- a/mateclaw-server/src/test/java/vip/mate/agent/graph/state/SourceEvidenceLedgerTest.java +++ b/mateclaw-server/src/test/java/vip/mate/agent/graph/state/SourceEvidenceLedgerTest.java @@ -290,8 +290,8 @@ class SourceEvidenceLedgerTest { [1] MAST-Data数据集 """); - assertTrue("来源: header must be present: " + rendered, - rendered.contains("来源:")); + assertTrue(rendered.contains("来源:"), + "来源: header must be present: " + rendered); assertTrue(rendered.contains("[1] MAST-Data数据集"), "source line content must be preserved: " + rendered); assertTrue(ledger.validateAnswer(rendered).valid()); diff --git a/mateclaw-ui/src/components/agents/PlanBoard.vue b/mateclaw-ui/src/components/agents/PlanBoard.vue index 280ab3c1..515883dd 100644 --- a/mateclaw-ui/src/components/agents/PlanBoard.vue +++ b/mateclaw-ui/src/components/agents/PlanBoard.vue @@ -83,6 +83,19 @@ {{ group.latest.completedSteps }}/{{ group.latest.totalSteps }} ×{{ group.plans.length }} + +
+ + {{ stepDist(group.latest).running }} {{ t('plans.col.running') }} + + + {{ stepDist(group.latest).pending }} {{ t('plans.col.pending') }} + + + {{ stepDist(group.latest).completed }} {{ t('plans.col.completed') }} + +