mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 11:13:43 +08:00
feat(plans): per-step agent delegation + fix kanban pending column (issue #385)
This commit is contained in:
parent
1373b78b0a
commit
eca4229751
@ -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();
|
||||
|
||||
|
||||
@ -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<String> steps
|
||||
@JsonProperty("steps") List<String> 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<String> 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<AgentEntity> 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<Long> resolveStepAgents(List<String> steps, List<String> stepAgents,
|
||||
Long workspaceId, String parentAgentId) {
|
||||
if (stepAgents == null || stepAgents.isEmpty() || steps == null || steps.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
List<AgentEntity> delegatable = listDelegatableAgents(workspaceId, parentAgentId);
|
||||
if (delegatable.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Map<String, Long> byName = new HashMap<>();
|
||||
for (AgentEntity a : delegatable) {
|
||||
if (a.getName() != null) {
|
||||
byName.put(a.getName().trim().toLowerCase(), a.getId());
|
||||
}
|
||||
}
|
||||
List<Long> 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<String, Object> 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<AgentEntity> 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<Long> 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));
|
||||
|
||||
|
||||
@ -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<String, Object> executeDelegatedStep(
|
||||
PlanStateAccessor accessor, int stepIndex, String step, Long planId,
|
||||
Long assignedAgentId, ChatOrigin chatOrigin,
|
||||
List<GraphEventPublisher.GraphEvent> 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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -43,11 +43,27 @@ public class PlanningService {
|
||||
*/
|
||||
@Transactional
|
||||
public PlanEntity createPlan(String agentId, String conversationId, String goal, List<String> 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<String> steps, List<Long> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记计划失败
|
||||
*/
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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());
|
||||
|
||||
@ -83,6 +83,19 @@
|
||||
<span class="pb-card__steps">{{ group.latest.completedSteps }}/{{ group.latest.totalSteps }}</span>
|
||||
<span v-if="group.plans.length > 1" class="pb-card__runs" :title="t('plans.runs', { n: group.plans.length })">×{{ group.plans.length }}</span>
|
||||
</div>
|
||||
<!-- Sub-task breakdown: surfaces how many steps are still pending /
|
||||
running, so an in-progress plan no longer hides its queued work. -->
|
||||
<div v-if="showDist(group.latest)" class="pb-card__dist">
|
||||
<span v-if="stepDist(group.latest).running" class="pb-distchip is-running">
|
||||
{{ stepDist(group.latest).running }} {{ t('plans.col.running') }}
|
||||
</span>
|
||||
<span v-if="stepDist(group.latest).pending" class="pb-distchip is-pending">
|
||||
{{ stepDist(group.latest).pending }} {{ t('plans.col.pending') }}
|
||||
</span>
|
||||
<span v-if="stepDist(group.latest).completed" class="pb-distchip is-completed">
|
||||
{{ stepDist(group.latest).completed }} {{ t('plans.col.completed') }}
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<button
|
||||
@ -258,6 +271,24 @@ function progressPct(plan: Plan): number {
|
||||
return Math.round((plan.completedSteps / plan.totalSteps) * 100)
|
||||
}
|
||||
|
||||
// Sub-task status breakdown for a plan card. Plan-execute runs steps
|
||||
// sequentially, so at most one step is "running" at any time; the rest of the
|
||||
// not-yet-done steps are pending. Derived from the plan summary fields so the
|
||||
// list endpoint stays cheap (no per-plan step fetch).
|
||||
function stepDist(plan: Plan): { pending: number; running: number; completed: number } {
|
||||
const total = plan.totalSteps ?? 0
|
||||
const completed = Math.min(plan.completedSteps ?? 0, total)
|
||||
const running = plan.status === 'running' && completed < total ? 1 : 0
|
||||
const pending = Math.max(0, total - completed - running)
|
||||
return { pending, running, completed }
|
||||
}
|
||||
|
||||
// Only worth showing for multi-step plans that are still active; a finished or
|
||||
// single-step plan is already fully described by the progress bar.
|
||||
function showDist(plan: Plan): boolean {
|
||||
return (plan.totalSteps ?? 0) > 1 && (plan.status === 'running' || plan.status === 'pending')
|
||||
}
|
||||
|
||||
function laneLetter(name: string): string {
|
||||
return (name || '?').trim().charAt(0).toUpperCase()
|
||||
}
|
||||
@ -556,6 +587,38 @@ onMounted(reload)
|
||||
gap: 8px;
|
||||
margin-top: 9px;
|
||||
}
|
||||
|
||||
/* Sub-task breakdown chips */
|
||||
.pb-card__dist {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.pb-distchip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 1px 7px;
|
||||
border-radius: var(--mc-radius-full);
|
||||
font-size: 10.5px;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 16px;
|
||||
background: var(--mc-bg-muted);
|
||||
color: var(--mc-text-tertiary);
|
||||
}
|
||||
.pb-distchip.is-running {
|
||||
background: var(--mc-primary-bg);
|
||||
color: var(--mc-primary);
|
||||
}
|
||||
.pb-distchip.is-pending {
|
||||
background: var(--mc-bg-muted);
|
||||
color: var(--mc-text-secondary);
|
||||
}
|
||||
.pb-distchip.is-completed {
|
||||
background: var(--mc-success-bg, var(--mc-bg-muted));
|
||||
color: var(--mc-success);
|
||||
}
|
||||
.pb-card__steps {
|
||||
font-size: 11px;
|
||||
color: var(--mc-text-tertiary);
|
||||
|
||||
@ -68,6 +68,10 @@
|
||||
<span class="pd-step__dot" :class="`is-${step.status}`"></span>
|
||||
<div class="pd-step__body">
|
||||
<div class="pd-step__title"><b>{{ step.stepIndex + 1 }}.</b> {{ step.description }}</div>
|
||||
<div v-if="delegatedAgentName(step)" class="pd-step__agent">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
<span>{{ t('plans.delegatedTo') }} {{ delegatedAgentName(step) }}</span>
|
||||
</div>
|
||||
<div v-if="step.result && expanded === String(step.id)" class="pd-step__result markdown-body" v-html="resultHtml"></div>
|
||||
<div v-else-if="step.result" class="pd-step__hint">{{ t('plans.viewResult') }}</div>
|
||||
</div>
|
||||
@ -85,6 +89,7 @@ import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import SkillIcon from '@/components/common/SkillIcon.vue'
|
||||
import { useStreamingMarkdown } from '@/composables/useStreamingMarkdown'
|
||||
import { useAgentStore } from '@/stores/useAgentStore'
|
||||
import type { Plan, SubPlan } from '@/types'
|
||||
|
||||
const props = defineProps<{
|
||||
@ -98,8 +103,17 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{ close: [] }>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const agentStore = useAgentStore()
|
||||
const expanded = ref<string>('')
|
||||
|
||||
// Resolve the delegated agent's display name from its id via the agent store.
|
||||
// Empty string when the step isn't delegated (runs with the parent agent).
|
||||
function delegatedAgentName(step: SubPlan): string {
|
||||
if (step.assignedAgentId == null || step.assignedAgentId === '') return ''
|
||||
const a = agentStore.agents.find((x) => String(x.id) === String(step.assignedAgentId))
|
||||
return a?.name || ''
|
||||
}
|
||||
|
||||
// Render the plan output and the expanded step result as markdown, reusing the
|
||||
// same renderer the chat uses. `streaming = false` → full-fidelity one-shot
|
||||
// render (code highlight + cache), since this content is already complete.
|
||||
@ -433,6 +447,18 @@ html.dark .pd-tile {
|
||||
font-size: 11.5px;
|
||||
color: var(--mc-primary);
|
||||
}
|
||||
.pd-step__agent {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-top: 5px;
|
||||
padding: 1px 8px;
|
||||
border-radius: var(--mc-radius-full);
|
||||
background: var(--mc-primary-bg);
|
||||
color: var(--mc-primary);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.pd-panel {
|
||||
|
||||
@ -441,6 +441,7 @@ export default {
|
||||
untitled: 'Untitled plan',
|
||||
steps: 'steps',
|
||||
viewResult: 'Click to view result',
|
||||
delegatedTo: 'Delegated to',
|
||||
col: {
|
||||
pending: 'To Do',
|
||||
running: 'In Progress',
|
||||
|
||||
@ -441,6 +441,7 @@ export default {
|
||||
untitled: '未命名计划',
|
||||
steps: '步骤',
|
||||
viewResult: '点击查看结果',
|
||||
delegatedTo: '委派给',
|
||||
col: {
|
||||
pending: '待执行',
|
||||
running: '执行中',
|
||||
|
||||
@ -718,6 +718,9 @@ export interface SubPlan {
|
||||
result?: string
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
/** Delegated specialist agent for this step (snowflake id — keep as string).
|
||||
* Absent/null means the step runs with the parent (plan) agent. */
|
||||
assignedAgentId?: string | number
|
||||
}
|
||||
|
||||
export interface Plan {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user