mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 11:58:34 +08:00
feat(agent): multi-agent delegation with parallel execution (RFC-004)
This commit is contained in:
parent
46f77c281b
commit
80b7b8e126
@ -177,6 +177,19 @@ public class ToolExecutionExecutor {
|
|||||||
|
|
||||||
events.add(GraphEventPublisher.toolStart(toolName, arguments));
|
events.add(GraphEventPublisher.toolStart(toolName, arguments));
|
||||||
|
|
||||||
|
// 0. 子会话工具拦截:委派上下文中的子 Agent 禁止调用特定工具
|
||||||
|
if (vip.mate.tool.builtin.DelegationContext.currentDepth() > 0) {
|
||||||
|
java.util.Set<String> denied = vip.mate.tool.builtin.DelegationContext.childDeniedTools();
|
||||||
|
if (denied.contains(toolName)) {
|
||||||
|
String msg = "[安全限制] 子 Agent 不允许使用工具: " + toolName;
|
||||||
|
log.info("[ToolExecutor] Child agent blocked from using tool: {}", toolName);
|
||||||
|
events.add(GraphEventPublisher.toolComplete(toolName, msg, false));
|
||||||
|
allResponses.add(new org.springframework.ai.chat.messages.ToolResponseMessage.ToolResponse(
|
||||||
|
toolCall.id(), toolName, msg));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 1. JSON 校验
|
// 1. JSON 校验
|
||||||
if (arguments != null && !arguments.isBlank()) {
|
if (arguments != null && !arguments.isBlank()) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -101,6 +101,30 @@ public class ChatStreamTracker {
|
|||||||
|
|
||||||
private final ConcurrentHashMap<String, RunState> runs = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, RunState> runs = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/** 事件 relay:子会话事件转发到父会话(用于 Agent 委派进度可见性) */
|
||||||
|
private final ConcurrentHashMap<String, List<java.util.function.BiConsumer<String, String>>> eventRelays = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册事件 relay:将 sourceConversationId 的广播事件同时转发给 listener。
|
||||||
|
* 返回一个 Runnable,调用后取消注册。
|
||||||
|
*/
|
||||||
|
public Runnable addEventRelay(String sourceConversationId,
|
||||||
|
java.util.function.BiConsumer<String, String> listener) {
|
||||||
|
eventRelays.computeIfAbsent(sourceConversationId, k -> new java.util.concurrent.CopyOnWriteArrayList<>())
|
||||||
|
.add(listener);
|
||||||
|
log.debug("Event relay registered for conversation {}", sourceConversationId);
|
||||||
|
return () -> {
|
||||||
|
List<java.util.function.BiConsumer<String, String>> listeners = eventRelays.get(sourceConversationId);
|
||||||
|
if (listeners != null) {
|
||||||
|
listeners.remove(listener);
|
||||||
|
if (listeners.isEmpty()) {
|
||||||
|
eventRelays.remove(sourceConversationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.debug("Event relay removed for conversation {}", sourceConversationId);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** 心跳调度线程池(守护线程) */
|
/** 心跳调度线程池(守护线程) */
|
||||||
private final ScheduledExecutorService heartbeatScheduler =
|
private final ScheduledExecutorService heartbeatScheduler =
|
||||||
Executors.newSingleThreadScheduledExecutor(r -> {
|
Executors.newSingleThreadScheduledExecutor(r -> {
|
||||||
@ -214,6 +238,18 @@ public class ChatStreamTracker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 事件 relay:转发给注册的监听器(用于子会话→父会话进度传递)
|
||||||
|
List<java.util.function.BiConsumer<String, String>> relays = eventRelays.get(conversationId);
|
||||||
|
if (relays != null) {
|
||||||
|
for (var relay : relays) {
|
||||||
|
try {
|
||||||
|
relay.accept(eventName, jsonData);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Event relay error for {}: {}", conversationId, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package vip.mate.tool.builtin;
|
package vip.mate.tool.builtin;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.ai.tool.annotation.Tool;
|
import org.springframework.ai.tool.annotation.Tool;
|
||||||
@ -9,16 +11,24 @@ import org.springframework.stereotype.Component;
|
|||||||
import vip.mate.agent.AgentService;
|
import vip.mate.agent.AgentService;
|
||||||
import vip.mate.agent.model.AgentEntity;
|
import vip.mate.agent.model.AgentEntity;
|
||||||
import vip.mate.agent.repository.AgentMapper;
|
import vip.mate.agent.repository.AgentMapper;
|
||||||
|
import vip.mate.channel.web.ChatStreamTracker;
|
||||||
|
import vip.mate.workspace.conversation.ConversationService;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.*;
|
||||||
import java.util.UUID;
|
import java.util.concurrent.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 内置工具:Agent 委派
|
* 内置工具:Agent 委派(多 Agent 协作)
|
||||||
* <p>
|
* <p>
|
||||||
* 允许当前 Agent 将子任务委派给另一个 Agent 执行,实现多 Agent 协作。
|
* 支持两种模式:
|
||||||
* 被委派的 Agent 在独立会话中运行,结果作为工具观察返回给调用方。
|
* <ul>
|
||||||
|
* <li>{@link #delegateToAgent} — 单任务委派(串行)</li>
|
||||||
|
* <li>{@link #delegateParallel} — 多任务并行委派(最多 3 个子 Agent 同时执行)</li>
|
||||||
|
* </ul>
|
||||||
|
* 被委派的 Agent 在独立子会话中运行(记录父子关系),
|
||||||
|
* 执行期间通过 SSE 事件 relay 向父会话实时推送进度。
|
||||||
|
* 子 Agent 的工具集自动收窄——禁止递归委派和 Agent 发现工具。
|
||||||
*
|
*
|
||||||
* @author MateClaw Team
|
* @author MateClaw Team
|
||||||
*/
|
*/
|
||||||
@ -29,19 +39,37 @@ public class DelegateAgentTool {
|
|||||||
|
|
||||||
private static final int MAX_DELEGATION_DEPTH = 3;
|
private static final int MAX_DELEGATION_DEPTH = 3;
|
||||||
private static final int MAX_RESULT_LENGTH = 4000;
|
private static final int MAX_RESULT_LENGTH = 4000;
|
||||||
|
private static final int MAX_PARALLEL_CHILDREN = 3;
|
||||||
|
private static final int PARALLEL_TIMEOUT_SECONDS = 300; // 5 分钟
|
||||||
|
|
||||||
|
/** 子 Agent 禁用的工具:防递归 + 防副作用 */
|
||||||
|
private static final Set<String> CHILD_DENIED_TOOLS = Set.of(
|
||||||
|
"delegateToAgent", // 禁止递归委派
|
||||||
|
"delegateParallel", // 禁止并行递归
|
||||||
|
"listAvailableAgents" // 子 Agent 不需要发现其他 Agent
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 并行委派执行器:JDK 21 虚拟线程,每个子 Agent 一个轻量级虚拟线程 */
|
||||||
|
private static final ExecutorService DELEGATION_EXECUTOR =
|
||||||
|
Executors.newVirtualThreadPerTaskExecutor();
|
||||||
|
|
||||||
private final AgentService agentService;
|
private final AgentService agentService;
|
||||||
private final AgentMapper agentMapper;
|
private final AgentMapper agentMapper;
|
||||||
|
private final ChatStreamTracker streamTracker;
|
||||||
|
private final ConversationService conversationService;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
// ==================== 单任务委派 ====================
|
||||||
|
|
||||||
@Tool(description = """
|
@Tool(description = """
|
||||||
Delegate a task to another Agent for multi-agent collaboration. \
|
Delegate a task to another Agent for multi-agent collaboration. \
|
||||||
Target Agent executes in an independent session and returns its final reply. \
|
Target Agent executes in an independent session and returns its final reply. \
|
||||||
Provide complete task context.""")
|
Parent receives real-time progress updates during execution. \
|
||||||
|
For multiple parallel tasks, use delegateParallel instead.""")
|
||||||
public String delegateToAgent(
|
public String delegateToAgent(
|
||||||
@ToolParam(description = "Target Agent name (exact match)") String agentName,
|
@ToolParam(description = "Target Agent name (exact match)") String agentName,
|
||||||
@ToolParam(description = "Task description with complete context information") String task) {
|
@ToolParam(description = "Task description with complete context information") String task) {
|
||||||
|
|
||||||
// 1. 参数校验
|
|
||||||
if (agentName == null || agentName.isBlank()) {
|
if (agentName == null || agentName.isBlank()) {
|
||||||
return "[错误] 请指定目标 Agent 名称。" + availableAgentsHint();
|
return "[错误] 请指定目标 Agent 名称。" + availableAgentsHint();
|
||||||
}
|
}
|
||||||
@ -49,52 +77,244 @@ public class DelegateAgentTool {
|
|||||||
return "[错误] 请提供任务描述。";
|
return "[错误] 请提供任务描述。";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 递归深度检查
|
|
||||||
int depth = DelegationContext.currentDepth();
|
int depth = DelegationContext.currentDepth();
|
||||||
if (depth >= MAX_DELEGATION_DEPTH) {
|
if (depth >= MAX_DELEGATION_DEPTH) {
|
||||||
return "[错误] 委派层级已达上限(" + MAX_DELEGATION_DEPTH + " 层),无法继续委派,请直接处理任务。";
|
return "[错误] 委派层级已达上限(" + MAX_DELEGATION_DEPTH + " 层),请直接处理任务。";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 按名称查找目标 Agent
|
AgentEntity target = findAgent(agentName);
|
||||||
AgentEntity target = agentMapper.selectOne(
|
|
||||||
new LambdaQueryWrapper<AgentEntity>()
|
|
||||||
.eq(AgentEntity::getName, agentName.trim())
|
|
||||||
.eq(AgentEntity::getEnabled, true));
|
|
||||||
|
|
||||||
if (target == null) {
|
if (target == null) {
|
||||||
return "[错误] 未找到名为「" + agentName + "」的已启用 Agent。" + availableAgentsHint();
|
return "[错误] 未找到名为「" + agentName + "」的已启用 Agent。" + availableAgentsHint();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 在独立会话中执行
|
String parentConversationId = resolveParentConversationId();
|
||||||
String tempConversationId = "delegate-" + UUID.randomUUID();
|
String childConversationId = createChildConv(target, parentConversationId);
|
||||||
log.info("Agent 委派: depth={}, target={}({}), task={}",
|
|
||||||
depth + 1, target.getName(), target.getId(),
|
|
||||||
task.length() > 100 ? task.substring(0, 100) + "..." : task);
|
|
||||||
|
|
||||||
DelegationContext.enter();
|
log.info("Agent 委派: depth={}, target={}({}), childConv={}, parentConv={}",
|
||||||
|
depth + 1, target.getName(), target.getId(), childConversationId, parentConversationId);
|
||||||
|
|
||||||
|
// SSE 广播 + relay
|
||||||
|
boolean hasParent = parentConversationId != null && streamTracker.isRunning(parentConversationId);
|
||||||
|
if (hasParent) {
|
||||||
|
streamTracker.broadcastObject(parentConversationId, "delegation_start", Map.of(
|
||||||
|
"childConversationId", childConversationId,
|
||||||
|
"childAgentName", target.getName(),
|
||||||
|
"task", truncate(task, 200)));
|
||||||
|
}
|
||||||
|
Runnable stopRelay = hasParent ? registerRelay(childConversationId, parentConversationId, target.getName()) : null;
|
||||||
|
|
||||||
|
// 执行
|
||||||
|
ChildResult result = runSingleChild(0, target, task, parentConversationId, childConversationId);
|
||||||
|
|
||||||
|
// 清理 + 广播结果
|
||||||
|
if (stopRelay != null) stopRelay.run();
|
||||||
|
if (hasParent) {
|
||||||
|
broadcastEnd(parentConversationId, childConversationId, target.getName(), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toToolResponse(target.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 并行委派 ====================
|
||||||
|
|
||||||
|
@Tool(description = """
|
||||||
|
Delegate multiple tasks to different Agents in parallel (max 3). \
|
||||||
|
Each task runs concurrently in an independent child session. \
|
||||||
|
Use this when you have multiple independent sub-tasks that can run simultaneously. \
|
||||||
|
Input is a JSON array: [{"agentName":"Agent名称","task":"任务描述"}, ...]""")
|
||||||
|
public String delegateParallel(
|
||||||
|
@ToolParam(description = "JSON array of tasks: [{\"agentName\":\"X\",\"task\":\"Y\"}, ...]")
|
||||||
|
String tasksJson) {
|
||||||
|
|
||||||
|
// 1. 解析任务列表
|
||||||
|
List<Map<String, String>> tasks;
|
||||||
try {
|
try {
|
||||||
String result = agentService.chat(target.getId(), task, tempConversationId);
|
tasks = objectMapper.readValue(tasksJson, new TypeReference<>() {});
|
||||||
String truncated = truncate(result, MAX_RESULT_LENGTH);
|
|
||||||
return "[Agent「" + target.getName() + "」的回复]\n\n" + truncated;
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Agent 委派执行失败: target={}, error={}", target.getName(), e.getMessage(), e);
|
return "[错误] 无法解析任务 JSON:" + e.getMessage() + "\n格式: [{\"agentName\":\"X\",\"task\":\"Y\"}]";
|
||||||
return "[错误] Agent「" + target.getName() + "」执行失败: " + e.getMessage();
|
}
|
||||||
|
|
||||||
|
if (tasks == null || tasks.isEmpty()) {
|
||||||
|
return "[错误] 任务列表为空。";
|
||||||
|
}
|
||||||
|
if (tasks.size() > MAX_PARALLEL_CHILDREN) {
|
||||||
|
return "[错误] 最多支持 " + MAX_PARALLEL_CHILDREN + " 个并行任务,当前 " + tasks.size() + " 个。";
|
||||||
|
}
|
||||||
|
|
||||||
|
int depth = DelegationContext.currentDepth();
|
||||||
|
if (depth >= MAX_DELEGATION_DEPTH) {
|
||||||
|
return "[错误] 委派层级已达上限(" + MAX_DELEGATION_DEPTH + " 层),请直接处理任务。";
|
||||||
|
}
|
||||||
|
|
||||||
|
String parentConversationId = resolveParentConversationId();
|
||||||
|
boolean hasParent = parentConversationId != null && streamTracker.isRunning(parentConversationId);
|
||||||
|
|
||||||
|
// 2. 主线程:校验所有 Agent + 创建子会话 + 注册 relay
|
||||||
|
record PreparedChild(int index, AgentEntity agent, String task, String childConvId, Runnable stopRelay) {}
|
||||||
|
List<PreparedChild> prepared = new ArrayList<>();
|
||||||
|
List<String> errors = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < tasks.size(); i++) {
|
||||||
|
Map<String, String> t = tasks.get(i);
|
||||||
|
String agentName = t.get("agentName");
|
||||||
|
String task = t.get("task");
|
||||||
|
|
||||||
|
if (agentName == null || agentName.isBlank() || task == null || task.isBlank()) {
|
||||||
|
errors.add("[任务 " + (i + 1) + "] agentName 或 task 为空");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentEntity agent = findAgent(agentName);
|
||||||
|
if (agent == null) {
|
||||||
|
errors.add("[任务 " + (i + 1) + "] 未找到 Agent「" + agentName + "」");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String childConvId = createChildConv(agent, parentConversationId);
|
||||||
|
Runnable stopRelay = hasParent ? registerRelay(childConvId, parentConversationId, agent.getName()) : null;
|
||||||
|
prepared.add(new PreparedChild(i, agent, task, childConvId, stopRelay));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prepared.isEmpty()) {
|
||||||
|
return "[错误] 所有任务校验失败:\n" + String.join("\n", errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("并行委派: {} 个任务, parentConv={}", prepared.size(), parentConversationId);
|
||||||
|
|
||||||
|
// 3. 广播 delegation_start(并行模式)
|
||||||
|
if (hasParent) {
|
||||||
|
List<Map<String, String>> childrenInfo = prepared.stream().map(p -> Map.of(
|
||||||
|
"childConversationId", p.childConvId,
|
||||||
|
"childAgentName", p.agent.getName(),
|
||||||
|
"task", truncate(p.task, 100)
|
||||||
|
)).toList();
|
||||||
|
streamTracker.broadcastObject(parentConversationId, "delegation_start", Map.of(
|
||||||
|
"parallel", true,
|
||||||
|
"children", childrenInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 并行执行
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
Map<Integer, CompletableFuture<ChildResult>> futures = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
for (PreparedChild p : prepared) {
|
||||||
|
CompletableFuture<ChildResult> future = CompletableFuture.supplyAsync(
|
||||||
|
() -> runSingleChild(p.index, p.agent, p.task, parentConversationId, p.childConvId),
|
||||||
|
DELEGATION_EXECUTOR);
|
||||||
|
futures.put(p.index, future);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 等待全部完成(带超时)
|
||||||
|
List<ChildResult> results = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
CompletableFuture.allOf(futures.values().toArray(new CompletableFuture[0]))
|
||||||
|
.get(PARALLEL_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
} catch (TimeoutException e) {
|
||||||
|
log.warn("并行委派超时 ({}s),收集已完成的结果", PARALLEL_TIMEOUT_SECONDS);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("并行委派异常: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集结果(已完成的 + 超时的)
|
||||||
|
for (var entry : futures.entrySet()) {
|
||||||
|
int idx = entry.getKey();
|
||||||
|
CompletableFuture<ChildResult> f = entry.getValue();
|
||||||
|
PreparedChild p = prepared.stream().filter(pp -> pp.index == idx).findFirst().orElse(null);
|
||||||
|
String agentName = p != null ? p.agent.getName() : "Unknown";
|
||||||
|
|
||||||
|
if (f.isDone() && !f.isCompletedExceptionally()) {
|
||||||
|
try {
|
||||||
|
results.add(f.get());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
results.add(ChildResult.error(idx, agentName, ex.getMessage()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
f.cancel(true);
|
||||||
|
results.add(ChildResult.error(idx, agentName, "超时 (" + PARALLEL_TIMEOUT_SECONDS + "s)"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
long totalDurationMs = System.currentTimeMillis() - startTime;
|
||||||
|
|
||||||
|
// 6. 清理 relay
|
||||||
|
for (PreparedChild p : prepared) {
|
||||||
|
if (p.stopRelay != null) p.stopRelay.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 广播 delegation_end
|
||||||
|
if (hasParent) {
|
||||||
|
streamTracker.broadcastObject(parentConversationId, "delegation_end", Map.of(
|
||||||
|
"parallel", true,
|
||||||
|
"totalDurationMs", totalDurationMs,
|
||||||
|
"success", results.stream().allMatch(r -> r.success),
|
||||||
|
"completedCount", results.stream().filter(r -> r.success).count(),
|
||||||
|
"totalCount", results.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 构建返回结果
|
||||||
|
results.sort(Comparator.comparingInt(r -> r.taskIndex));
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
sb.append("⚠️ 部分任务未执行:\n");
|
||||||
|
errors.forEach(e -> sb.append(" ").append(e).append("\n"));
|
||||||
|
sb.append("\n");
|
||||||
|
}
|
||||||
|
sb.append("并行执行 ").append(results.size()).append(" 个任务(总耗时 ")
|
||||||
|
.append(totalDurationMs / 1000).append("s):\n\n");
|
||||||
|
for (ChildResult r : results) {
|
||||||
|
sb.append("---\n### [任务 ").append(r.taskIndex + 1).append("] Agent「").append(r.agentName).append("」");
|
||||||
|
sb.append(r.success ? " ✓" : " ✗").append(" (").append(r.durationMs / 1000).append("s)\n\n");
|
||||||
|
sb.append(r.success ? r.result : "[错误] " + r.error).append("\n\n");
|
||||||
|
}
|
||||||
|
return truncate(sb.toString(), MAX_RESULT_LENGTH * 2); // 并行结果允许更长
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 子 Agent 执行(单/并行共用) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行单个子 Agent。在子线程内独立设置 DelegationContext,解决 ThreadLocal 并行问题。
|
||||||
|
*/
|
||||||
|
private ChildResult runSingleChild(int taskIndex, AgentEntity target, String task,
|
||||||
|
String parentConversationId, String childConversationId) {
|
||||||
|
DelegationContext.enter(parentConversationId, CHILD_DENIED_TOOLS);
|
||||||
|
try {
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
String result = agentService.chat(target.getId(), task, childConversationId);
|
||||||
|
long durationMs = System.currentTimeMillis() - startTime;
|
||||||
|
return ChildResult.success(taskIndex, target.getName(), truncate(result, MAX_RESULT_LENGTH), durationMs);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("子 Agent 执行失败: taskIndex={}, agent={}, error={}",
|
||||||
|
taskIndex, target.getName(), e.getMessage());
|
||||||
|
return ChildResult.error(taskIndex, target.getName(), e.getMessage());
|
||||||
} finally {
|
} finally {
|
||||||
DelegationContext.exit();
|
DelegationContext.exit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 子 Agent 执行结果 */
|
||||||
|
private record ChildResult(int taskIndex, String agentName, boolean success,
|
||||||
|
String result, String error, long durationMs) {
|
||||||
|
static ChildResult success(int idx, String name, String result, long ms) {
|
||||||
|
return new ChildResult(idx, name, true, result, null, ms);
|
||||||
|
}
|
||||||
|
static ChildResult error(int idx, String name, String err) {
|
||||||
|
return new ChildResult(idx, name, false, null, err != null ? err : "Unknown error", 0);
|
||||||
|
}
|
||||||
|
String toToolResponse(String agentName) {
|
||||||
|
if (success) return "[Agent「" + agentName + "」的回复]\n\n" + result;
|
||||||
|
return "[错误] Agent「" + agentName + "」执行失败: " + error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 辅助方法 ====================
|
||||||
|
|
||||||
@Tool(description = "List all available Agents (enabled), including name, type, and description.")
|
@Tool(description = "List all available Agents (enabled), including name, type, and description.")
|
||||||
public String listAvailableAgents() {
|
public String listAvailableAgents() {
|
||||||
List<AgentEntity> agents = agentMapper.selectList(
|
List<AgentEntity> agents = agentMapper.selectList(
|
||||||
new LambdaQueryWrapper<AgentEntity>()
|
new LambdaQueryWrapper<AgentEntity>()
|
||||||
.eq(AgentEntity::getEnabled, true)
|
.eq(AgentEntity::getEnabled, true)
|
||||||
.orderByAsc(AgentEntity::getName));
|
.orderByAsc(AgentEntity::getName));
|
||||||
|
if (agents.isEmpty()) return "当前没有可用的 Agent。";
|
||||||
if (agents.isEmpty()) {
|
|
||||||
return "当前没有可用的 Agent。";
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder("可用 Agent 列表:\n\n");
|
StringBuilder sb = new StringBuilder("可用 Agent 列表:\n\n");
|
||||||
for (AgentEntity agent : agents) {
|
for (AgentEntity agent : agents) {
|
||||||
sb.append("- **").append(agent.getName()).append("**");
|
sb.append("- **").append(agent.getName()).append("**");
|
||||||
@ -107,23 +327,67 @@ public class DelegateAgentTool {
|
|||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String availableAgentsHint() {
|
private AgentEntity findAgent(String name) {
|
||||||
List<AgentEntity> agents = agentMapper.selectList(
|
return agentMapper.selectOne(new LambdaQueryWrapper<AgentEntity>()
|
||||||
new LambdaQueryWrapper<AgentEntity>()
|
.eq(AgentEntity::getName, name.trim())
|
||||||
.eq(AgentEntity::getEnabled, true)
|
.eq(AgentEntity::getEnabled, true));
|
||||||
.select(AgentEntity::getName));
|
}
|
||||||
if (agents.isEmpty()) {
|
|
||||||
return "";
|
private String createChildConv(AgentEntity target, String parentConversationId) {
|
||||||
|
String childConvId = "child-" + UUID.randomUUID().toString().substring(0, 12);
|
||||||
|
try {
|
||||||
|
conversationService.createChildConversation(childConvId, target.getId(), "system",
|
||||||
|
target.getWorkspaceId() != null ? target.getWorkspaceId() : 1L, parentConversationId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to create child conversation: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
String names = agents.stream()
|
return childConvId;
|
||||||
.map(AgentEntity::getName)
|
}
|
||||||
.collect(Collectors.joining("、"));
|
|
||||||
return "\n可用 Agent: " + names;
|
private Runnable registerRelay(String childConvId, String parentConvId, String childAgentName) {
|
||||||
|
return streamTracker.addEventRelay(childConvId, (eventName, jsonData) -> {
|
||||||
|
if ("tool_call_started".equals(eventName) || "tool_call_completed".equals(eventName) || "phase".equals(eventName)) {
|
||||||
|
try {
|
||||||
|
streamTracker.broadcastObject(parentConvId, "delegation_progress", Map.of(
|
||||||
|
"childConversationId", childConvId,
|
||||||
|
"childAgentName", childAgentName,
|
||||||
|
"originalEvent", eventName,
|
||||||
|
"data", jsonData));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Relay error: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void broadcastEnd(String parentConvId, String childConvId, String agentName, ChildResult result) {
|
||||||
|
streamTracker.broadcastObject(parentConvId, "delegation_end", Map.of(
|
||||||
|
"childConversationId", childConvId,
|
||||||
|
"childAgentName", agentName,
|
||||||
|
"success", result.success,
|
||||||
|
"durationMs", result.durationMs,
|
||||||
|
"resultPreview", result.success ? truncate(result.result, 200) : (result.error != null ? result.error : "")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveParentConversationId() {
|
||||||
|
try {
|
||||||
|
String ctxConvId = ToolExecutionContext.conversationId();
|
||||||
|
if (ctxConvId != null && !ctxConvId.isBlank()) return ctxConvId;
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
return DelegationContext.parentConversationId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String availableAgentsHint() {
|
||||||
|
List<AgentEntity> agents = agentMapper.selectList(new LambdaQueryWrapper<AgentEntity>()
|
||||||
|
.eq(AgentEntity::getEnabled, true).select(AgentEntity::getName));
|
||||||
|
if (agents.isEmpty()) return "";
|
||||||
|
return "\n可用 Agent: " + agents.stream().map(AgentEntity::getName).collect(Collectors.joining("、"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String truncate(String text, int maxLength) {
|
private static String truncate(String text, int maxLength) {
|
||||||
if (text == null) return "";
|
if (text == null) return "";
|
||||||
if (text.length() <= maxLength) return text;
|
if (text.length() <= maxLength) return text;
|
||||||
return text.substring(0, maxLength) + "\n\n... [结果已截断,原文共 " + text.length() + " 字符]";
|
return text.substring(0, maxLength) + "\n... [截断,原文 " + text.length() + " 字符]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,20 @@
|
|||||||
package vip.mate.tool.builtin;
|
package vip.mate.tool.builtin;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 跟踪 Agent 委派调用深度,防止无限递归。
|
* 跟踪 Agent 委派调用的上下文信息,防止无限递归并传递父会话信息。
|
||||||
* <p>
|
* <p>
|
||||||
* 使用 ThreadLocal 存储当前线程的委派层级。
|
* 使用 ThreadLocal 存储当前线程的委派层级、父会话 ID 和子 Agent 禁用工具集。
|
||||||
* 每次 {@link DelegateAgentTool} 发起委派时 +1,返回后 -1。
|
* 每次 {@link DelegateAgentTool} 发起委派时调用 enter(),返回后调用 exit()。
|
||||||
*
|
*
|
||||||
* @author MateClaw Team
|
* @author MateClaw Team
|
||||||
*/
|
*/
|
||||||
public final class DelegationContext {
|
public final class DelegationContext {
|
||||||
|
|
||||||
private static final ThreadLocal<Integer> DEPTH = ThreadLocal.withInitial(() -> 0);
|
private static final ThreadLocal<Integer> DEPTH = ThreadLocal.withInitial(() -> 0);
|
||||||
|
private static final ThreadLocal<String> PARENT_CONVERSATION_ID = new ThreadLocal<>();
|
||||||
|
private static final ThreadLocal<Set<String>> CHILD_DENIED_TOOLS = new ThreadLocal<>();
|
||||||
|
|
||||||
private DelegationContext() {}
|
private DelegationContext() {}
|
||||||
|
|
||||||
@ -19,9 +23,29 @@ public final class DelegationContext {
|
|||||||
return DEPTH.get();
|
return DEPTH.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 进入下一层委派 */
|
/** 获取父会话 ID(用于事件 relay) */
|
||||||
public static void enter() {
|
public static String parentConversationId() {
|
||||||
|
return PARENT_CONVERSATION_ID.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取子 Agent 禁用的工具集 */
|
||||||
|
public static Set<String> childDeniedTools() {
|
||||||
|
Set<String> denied = CHILD_DENIED_TOOLS.get();
|
||||||
|
return denied != null ? denied : Set.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 进入下一层委派(带父会话 ID 和子 Agent 工具限制) */
|
||||||
|
public static void enter(String parentConversationId, Set<String> deniedTools) {
|
||||||
DEPTH.set(DEPTH.get() + 1);
|
DEPTH.set(DEPTH.get() + 1);
|
||||||
|
PARENT_CONVERSATION_ID.set(parentConversationId);
|
||||||
|
if (deniedTools != null) {
|
||||||
|
CHILD_DENIED_TOOLS.set(deniedTools);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 进入下一层委派(兼容旧调用) */
|
||||||
|
public static void enter() {
|
||||||
|
enter(null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 退出当前委派层 */
|
/** 退出当前委派层 */
|
||||||
@ -29,6 +53,8 @@ public final class DelegationContext {
|
|||||||
int current = DEPTH.get();
|
int current = DEPTH.get();
|
||||||
if (current <= 1) {
|
if (current <= 1) {
|
||||||
DEPTH.remove();
|
DEPTH.remove();
|
||||||
|
PARENT_CONVERSATION_ID.remove();
|
||||||
|
CHILD_DENIED_TOOLS.remove();
|
||||||
} else {
|
} else {
|
||||||
DEPTH.set(current - 1);
|
DEPTH.set(current - 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,8 +60,10 @@ public class ConversationService {
|
|||||||
*/
|
*/
|
||||||
public List<ConversationVO> listConversations(String username, Long workspaceId) {
|
public List<ConversationVO> listConversations(String username, Long workspaceId) {
|
||||||
// 同时返回当前用户的会话 和 定时任务(system)产生的会话
|
// 同时返回当前用户的会话 和 定时任务(system)产生的会话
|
||||||
|
// 排除子会话(委派产生的子会话不在侧边栏显示)
|
||||||
LambdaQueryWrapper<ConversationEntity> wrapper = new LambdaQueryWrapper<ConversationEntity>()
|
LambdaQueryWrapper<ConversationEntity> wrapper = new LambdaQueryWrapper<ConversationEntity>()
|
||||||
.in(ConversationEntity::getUsername, username, SYSTEM_USER)
|
.in(ConversationEntity::getUsername, username, SYSTEM_USER)
|
||||||
|
.isNull(ConversationEntity::getParentConversationId)
|
||||||
.orderByDesc(ConversationEntity::getLastActiveTime);
|
.orderByDesc(ConversationEntity::getLastActiveTime);
|
||||||
if (workspaceId != null) {
|
if (workspaceId != null) {
|
||||||
wrapper.eq(ConversationEntity::getWorkspaceId, workspaceId);
|
wrapper.eq(ConversationEntity::getWorkspaceId, workspaceId);
|
||||||
@ -129,6 +131,20 @@ public class ConversationService {
|
|||||||
return conv;
|
return conv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建子会话(委派场景),关联父会话 ID。
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public ConversationEntity createChildConversation(String childConversationId, Long agentId,
|
||||||
|
String username, Long workspaceId,
|
||||||
|
String parentConversationId) {
|
||||||
|
ConversationEntity conv = getOrCreateConversation(childConversationId, agentId, username, workspaceId);
|
||||||
|
conv.setParentConversationId(parentConversationId);
|
||||||
|
conv.setTitle("子任务");
|
||||||
|
conversationMapper.updateById(conv);
|
||||||
|
return conv;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取或创建共享渠道会话。
|
* 获取或创建共享渠道会话。
|
||||||
* <p>
|
* <p>
|
||||||
|
|||||||
@ -45,6 +45,9 @@ public class ConversationEntity {
|
|||||||
/** 所属工作区 ID(默认 1 = default) */
|
/** 所属工作区 ID(默认 1 = default) */
|
||||||
private Long workspaceId;
|
private Long workspaceId;
|
||||||
|
|
||||||
|
/** 父会话 ID(委派场景下,子会话记录其父会话的 conversationId) */
|
||||||
|
private String parentConversationId;
|
||||||
|
|
||||||
@TableField(fill = FieldFill.INSERT)
|
@TableField(fill = FieldFill.INSERT)
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
-- V5: Add parent_conversation_id to mate_conversation for multi-agent delegation tracking
|
||||||
|
ALTER TABLE mate_conversation ADD COLUMN IF NOT EXISTS parent_conversation_id VARCHAR(64) DEFAULT NULL;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_conversation_parent ON mate_conversation(parent_conversation_id);
|
||||||
@ -158,6 +158,7 @@ CREATE TABLE IF NOT EXISTS mate_conversation (
|
|||||||
last_active_time DATETIME,
|
last_active_time DATETIME,
|
||||||
stream_status VARCHAR(16) NOT NULL DEFAULT 'idle',
|
stream_status VARCHAR(16) NOT NULL DEFAULT 'idle',
|
||||||
workspace_id BIGINT NOT NULL DEFAULT 1,
|
workspace_id BIGINT NOT NULL DEFAULT 1,
|
||||||
|
parent_conversation_id VARCHAR(64) DEFAULT NULL,
|
||||||
create_time DATETIME NOT NULL,
|
create_time DATETIME NOT NULL,
|
||||||
update_time DATETIME NOT NULL,
|
update_time DATETIME NOT NULL,
|
||||||
deleted INT NOT NULL DEFAULT 0
|
deleted INT NOT NULL DEFAULT 0
|
||||||
|
|||||||
@ -597,6 +597,86 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ===== Agent 委派事件 =====
|
||||||
|
stream.on('delegation_start', (data) => {
|
||||||
|
if (isStaleEvent(data)) return
|
||||||
|
streamPhase.value = 'executing_tool'
|
||||||
|
if (currentAssistantId.value) {
|
||||||
|
const segs = currentSegments.value
|
||||||
|
// 关闭之前的 thinking/content segment
|
||||||
|
const runningSeg = segs.findLast((s: MessageSegment) => s.status === 'running')
|
||||||
|
if (runningSeg) runningSeg.status = 'completed'
|
||||||
|
|
||||||
|
if (data.parallel && Array.isArray(data.children)) {
|
||||||
|
// 并行模式:为每个子任务创建一个 delegation segment
|
||||||
|
for (const child of data.children) {
|
||||||
|
segs.push({
|
||||||
|
id: genSegId(),
|
||||||
|
type: 'tool_call',
|
||||||
|
status: 'running',
|
||||||
|
toolName: `→ ${child.childAgentName || 'Agent'}`,
|
||||||
|
toolArgs: child.task || '',
|
||||||
|
timestamp: Date.now()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 单任务模式
|
||||||
|
segs.push({
|
||||||
|
id: genSegId(),
|
||||||
|
type: 'tool_call',
|
||||||
|
status: 'running',
|
||||||
|
toolName: `→ ${data.childAgentName || 'Agent'}`,
|
||||||
|
toolArgs: data.task || '',
|
||||||
|
timestamp: Date.now()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
flushSegmentsToMessage()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
stream.on('delegation_progress', (data) => {
|
||||||
|
if (isStaleEvent(data)) return
|
||||||
|
if (currentAssistantId.value && data.originalEvent === 'tool_call_started') {
|
||||||
|
const segs = currentSegments.value
|
||||||
|
// 按 childAgentName 匹配对应的 delegation segment(并行时多个)
|
||||||
|
const childName = data.childAgentName || ''
|
||||||
|
const delegSeg = segs.findLast((s: MessageSegment) =>
|
||||||
|
s.type === 'tool_call' && s.status === 'running' && s.toolName === `→ ${childName}`)
|
||||||
|
|| segs.findLast((s: MessageSegment) => s.type === 'tool_call' && s.status === 'running' && s.toolName?.startsWith('→'))
|
||||||
|
if (delegSeg) {
|
||||||
|
const childData = typeof data.data === 'string' ? data.data : JSON.stringify(data.data)
|
||||||
|
delegSeg.toolArgs = (delegSeg.toolArgs || '') + '\n [子任务] ' + childData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
stream.on('delegation_end', (data) => {
|
||||||
|
if (isStaleEvent(data)) return
|
||||||
|
if (currentAssistantId.value) {
|
||||||
|
const segs = currentSegments.value
|
||||||
|
if (data.parallel) {
|
||||||
|
// 并行模式:关闭所有 running 的 delegation segments
|
||||||
|
const totalMs = data.totalDurationMs ? Math.round(data.totalDurationMs / 1000) : 0
|
||||||
|
segs.filter((s: MessageSegment) => s.type === 'tool_call' && s.status === 'running' && s.toolName?.startsWith('→'))
|
||||||
|
.forEach((s: MessageSegment) => {
|
||||||
|
s.status = 'completed'
|
||||||
|
s.toolName = (s.toolName || '') + (data.success ? ' ✓' : ' ✗')
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 单任务模式
|
||||||
|
const delegSeg = segs.findLast((s: MessageSegment) => s.type === 'tool_call' && s.status === 'running' && s.toolName?.startsWith('→'))
|
||||||
|
if (delegSeg) {
|
||||||
|
delegSeg.status = 'completed'
|
||||||
|
delegSeg.toolName = (delegSeg.toolName || '') + (data.success ? ' ✓' : ' ✗')
|
||||||
|
if (data.durationMs) {
|
||||||
|
delegSeg.toolArgs = (delegSeg.toolArgs || '') + `\n 耗时: ${Math.round(data.durationMs / 1000)}s`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flushSegmentsToMessage()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
stream.on('plan_created', (data) => {
|
stream.on('plan_created', (data) => {
|
||||||
if (isStaleEvent(data)) return
|
if (isStaleEvent(data)) return
|
||||||
if (currentAssistantId.value) {
|
if (currentAssistantId.value) {
|
||||||
|
|||||||
@ -39,6 +39,10 @@ export type SSEEventType =
|
|||||||
| 'tts_ready'
|
| 'tts_ready'
|
||||||
// 浏览器执行事件
|
// 浏览器执行事件
|
||||||
| 'browser_action'
|
| 'browser_action'
|
||||||
|
// Agent 委派事件
|
||||||
|
| 'delegation_start'
|
||||||
|
| 'delegation_progress'
|
||||||
|
| 'delegation_end'
|
||||||
|
|
||||||
export interface SSEEvent {
|
export interface SSEEvent {
|
||||||
type: SSEEventType
|
type: SSEEventType
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user