发布 v1.1.0 #2
@@ -7,6 +7,7 @@ import com.easyagents.agent.runtime.event.AgentRuntimeEventBridge;
|
|||||||
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
||||||
import com.easyagents.agent.runtime.event.AgentRuntimeInterceptor;
|
import com.easyagents.agent.runtime.event.AgentRuntimeInterceptor;
|
||||||
import com.easyagents.agent.runtime.hitl.AgentPendingState;
|
import com.easyagents.agent.runtime.hitl.AgentPendingState;
|
||||||
|
import com.easyagents.agent.runtime.hitl.AgentToolApprovalEvaluation;
|
||||||
import com.easyagents.agent.runtime.hitl.AgentToolApprovalCoordinator;
|
import com.easyagents.agent.runtime.hitl.AgentToolApprovalCoordinator;
|
||||||
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
|
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
|
||||||
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
||||||
@@ -22,9 +23,11 @@ import java.time.Duration;
|
|||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -140,7 +143,12 @@ public class ToolHitlInterceptor implements AgentRuntimeInterceptor {
|
|||||||
private void interceptPreActing(PreActingEvent event) {
|
private void interceptPreActing(PreActingEvent event) {
|
||||||
ToolUseBlock toolUse = event.getToolUse();
|
ToolUseBlock toolUse = event.getToolUse();
|
||||||
AgentToolSpec toolSpec = toolUse == null ? null : toolSpecs.get(toolUse.getName());
|
AgentToolSpec toolSpec = toolUse == null ? null : toolSpecs.get(toolUse.getName());
|
||||||
if (toolSpec == null || !toolSpec.isApprovalRequired()) {
|
if (!requiresApproval(toolSpec, toolUse)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, Object> approvalMetadata = approvalMetadata(toolSpec, toolUse);
|
||||||
|
if (!requiresForcedApproval(toolSpec, toolUse)
|
||||||
|
&& approvalCoordinator.isReusableApprovalGranted(approvalMetadata)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 执行授权与 toolCallId、工具名称及入参同时绑定,并且只能消费一次。
|
// 执行授权与 toolCallId、工具名称及入参同时绑定,并且只能消费一次。
|
||||||
@@ -239,7 +247,121 @@ public class ToolHitlInterceptor implements AgentRuntimeInterceptor {
|
|||||||
*/
|
*/
|
||||||
private boolean isApprovalRequired(ToolUseBlock toolUse) {
|
private boolean isApprovalRequired(ToolUseBlock toolUse) {
|
||||||
AgentToolSpec toolSpec = toolUse == null ? null : toolSpecs.get(toolUse.getName());
|
AgentToolSpec toolSpec = toolUse == null ? null : toolSpecs.get(toolUse.getName());
|
||||||
return toolSpec != null && toolSpec.isApprovalRequired();
|
return requiresApproval(toolSpec, toolUse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断工具声明或当前调用是否要求审批。
|
||||||
|
*
|
||||||
|
* @param toolSpec 工具声明
|
||||||
|
* @param toolUse 当前工具调用
|
||||||
|
* @return 需要审批时为 true
|
||||||
|
*/
|
||||||
|
private boolean requiresApproval(AgentToolSpec toolSpec, ToolUseBlock toolUse) {
|
||||||
|
if (toolSpec == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
AgentToolApprovalEvaluation evaluation = approvalEvaluation(toolSpec, toolUse);
|
||||||
|
if (evaluation != null && !evaluation.valid()) {
|
||||||
|
// 无效命令直接进入工具并返回结构化拒绝,避免产生必然失败的审批请求。
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return toolSpec.isApprovalRequired()
|
||||||
|
|| (evaluation != null && evaluation.approvalRequired())
|
||||||
|
|| requiresLegacyForcedApproval(toolSpec, toolUse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据工具声明中的强制审批命令规则检查当前调用。
|
||||||
|
*
|
||||||
|
* <p>命令首词解析与受控 Shell 的引号、反斜杠规则保持一致,避免通过
|
||||||
|
* {@code 'rm'} 或 {@code r\m} 绕过动态审批。畸形命令仍由 Shell 工具拒绝。</p>
|
||||||
|
*
|
||||||
|
* @param toolSpec 工具声明
|
||||||
|
* @param toolUse 当前工具调用
|
||||||
|
* @return 命中强制审批命令时为 true
|
||||||
|
*/
|
||||||
|
private boolean requiresForcedApproval(AgentToolSpec toolSpec, ToolUseBlock toolUse) {
|
||||||
|
AgentToolApprovalEvaluation evaluation = approvalEvaluation(toolSpec, toolUse);
|
||||||
|
if (evaluation != null) {
|
||||||
|
return evaluation.valid() && evaluation.forced();
|
||||||
|
}
|
||||||
|
return requiresLegacyForcedApproval(toolSpec, toolUse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用旧版元数据规则判断当前调用是否命中强制审批命令。
|
||||||
|
*
|
||||||
|
* @param toolSpec 工具声明
|
||||||
|
* @param toolUse 当前工具调用
|
||||||
|
* @return 命中旧版强制审批规则时为 true
|
||||||
|
*/
|
||||||
|
private boolean requiresLegacyForcedApproval(AgentToolSpec toolSpec, ToolUseBlock toolUse) {
|
||||||
|
if (toolSpec == null || toolUse == null || toolSpec.getMetadata() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Object commandsValue = toolSpec.getMetadata().get("forceApprovalCommands");
|
||||||
|
Object argumentValue = toolSpec.getMetadata().get("forceApprovalCommandArgument");
|
||||||
|
if (!(commandsValue instanceof Iterable<?> commands) || !(argumentValue instanceof String argumentName)
|
||||||
|
|| argumentName.isBlank() || toolUse.getInput() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Object commandValue = toolUse.getInput().get(argumentName);
|
||||||
|
if (!(commandValue instanceof String command)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String executable = firstCommandToken(command);
|
||||||
|
if (executable == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (Object forcedCommand : commands) {
|
||||||
|
if (forcedCommand instanceof String value && executable.equals(value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析受限命令行的首个参数。
|
||||||
|
*
|
||||||
|
* @param command 命令行
|
||||||
|
* @return 首个参数;无有效参数时返回 null
|
||||||
|
*/
|
||||||
|
private String firstCommandToken(String command) {
|
||||||
|
if (command == null || command.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
StringBuilder token = new StringBuilder();
|
||||||
|
char quote = 0;
|
||||||
|
boolean escaping = false;
|
||||||
|
boolean started = false;
|
||||||
|
for (int index = 0; index < command.length(); index++) {
|
||||||
|
char character = command.charAt(index);
|
||||||
|
if (!started && Character.isWhitespace(character)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
started = true;
|
||||||
|
if (escaping) {
|
||||||
|
token.append(character);
|
||||||
|
escaping = false;
|
||||||
|
} else if (character == '\\' && quote != '\'') {
|
||||||
|
escaping = true;
|
||||||
|
} else if (character == '\'' || character == '"') {
|
||||||
|
if (quote == 0) {
|
||||||
|
quote = character;
|
||||||
|
} else if (quote == character) {
|
||||||
|
quote = 0;
|
||||||
|
} else {
|
||||||
|
token.append(character);
|
||||||
|
}
|
||||||
|
} else if (Character.isWhitespace(character) && quote == 0) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
token.append(character);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return token.isEmpty() ? null : token.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -270,9 +392,26 @@ public class ToolHitlInterceptor implements AgentRuntimeInterceptor {
|
|||||||
if (toolUses == null || toolUses.isEmpty()) {
|
if (toolUses == null || toolUses.isEmpty()) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
return toolUses.stream()
|
List<ToolUseBlock> approvalTools = new ArrayList<>();
|
||||||
.filter(this::isApprovalRequired)
|
Set<String> pendingReusableScopes = new LinkedHashSet<>();
|
||||||
.toList();
|
for (ToolUseBlock toolUse : toolUses) {
|
||||||
|
AgentToolSpec toolSpec = toolUse == null ? null : toolSpecs.get(toolUse.getName());
|
||||||
|
if (!requiresApproval(toolSpec, toolUse)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Map<String, Object> approvalMetadata = approvalMetadata(toolSpec, toolUse);
|
||||||
|
if (!requiresForcedApproval(toolSpec, toolUse)
|
||||||
|
&& approvalCoordinator.isReusableApprovalGranted(approvalMetadata)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String reusableScope = approvalCoordinator.reusableApprovalScope(approvalMetadata);
|
||||||
|
if (reusableScope != null && !pendingReusableScopes.add(reusableScope)) {
|
||||||
|
// 同一推理消息中同一 MCP 的多个工具共享一个审批请求。
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
approvalTools.add(toolUse);
|
||||||
|
}
|
||||||
|
return approvalTools;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -294,12 +433,18 @@ public class ToolHitlInterceptor implements AgentRuntimeInterceptor {
|
|||||||
Map<String, Object> metadata = approvalRequest == null
|
Map<String, Object> metadata = approvalRequest == null
|
||||||
? new LinkedHashMap<>()
|
? new LinkedHashMap<>()
|
||||||
: new LinkedHashMap<>(approvalRequest.getMetadata());
|
: new LinkedHashMap<>(approvalRequest.getMetadata());
|
||||||
|
metadata.putAll(toolUse.getMetadata() == null ? Map.of() : toolUse.getMetadata());
|
||||||
if (toolSpec.getMetadata() != null && !toolSpec.getMetadata().isEmpty()) {
|
if (toolSpec.getMetadata() != null && !toolSpec.getMetadata().isEmpty()) {
|
||||||
|
// ToolSpec 由工具编译阶段生成,必须覆盖模型返回的同名治理字段(例如 toolType、mcpId)。
|
||||||
metadata.putAll(toolSpec.getMetadata());
|
metadata.putAll(toolSpec.getMetadata());
|
||||||
}
|
}
|
||||||
|
AgentToolApprovalEvaluation evaluation = approvalEvaluation(toolSpec, toolUse);
|
||||||
|
if (evaluation != null && evaluation.metadata() != null) {
|
||||||
|
// 动态策略由受信任工具实例计算,必须覆盖模型与静态声明中的同名字段。
|
||||||
|
metadata.putAll(evaluation.metadata());
|
||||||
|
}
|
||||||
metadata.put("phase", "POST_REASONING");
|
metadata.put("phase", "POST_REASONING");
|
||||||
metadata.put("source", "TOOL_HITL_INTERCEPTOR");
|
metadata.put("source", "TOOL_HITL_INTERCEPTOR");
|
||||||
metadata.putAll(toolUse.getMetadata() == null ? Map.of() : toolUse.getMetadata());
|
|
||||||
return approvalCoordinator.register(
|
return approvalCoordinator.register(
|
||||||
context == null ? null : context.getSessionId(),
|
context == null ? null : context.getSessionId(),
|
||||||
context == null || context.getAgentDefinition() == null ? null : context.getAgentDefinition().getAgentId(),
|
context == null || context.getAgentDefinition() == null ? null : context.getAgentDefinition().getAgentId(),
|
||||||
@@ -312,6 +457,40 @@ public class ToolHitlInterceptor implements AgentRuntimeInterceptor {
|
|||||||
approvalBatchId);
|
approvalBatchId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用工具声明中的受信任动态审批策略。
|
||||||
|
*
|
||||||
|
* @param toolSpec 工具声明
|
||||||
|
* @param toolUse 当前工具调用
|
||||||
|
* @return 动态审批判定;未配置策略时返回 null
|
||||||
|
*/
|
||||||
|
private AgentToolApprovalEvaluation approvalEvaluation(AgentToolSpec toolSpec, ToolUseBlock toolUse) {
|
||||||
|
if (toolSpec == null || toolSpec.getApprovalPolicy() == null || toolUse == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return toolSpec.getApprovalPolicy().evaluate(
|
||||||
|
toolUse.getInput() == null ? Map.of() : toolUse.getInput());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并静态工具元数据与动态审批元数据。
|
||||||
|
*
|
||||||
|
* @param toolSpec 工具声明
|
||||||
|
* @param toolUse 当前工具调用
|
||||||
|
* @return 用于审批作用域判断的受信任元数据
|
||||||
|
*/
|
||||||
|
private Map<String, Object> approvalMetadata(AgentToolSpec toolSpec, ToolUseBlock toolUse) {
|
||||||
|
Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
if (toolSpec != null && toolSpec.getMetadata() != null) {
|
||||||
|
metadata.putAll(toolSpec.getMetadata());
|
||||||
|
}
|
||||||
|
AgentToolApprovalEvaluation evaluation = approvalEvaluation(toolSpec, toolUse);
|
||||||
|
if (evaluation != null && evaluation.metadata() != null) {
|
||||||
|
metadata.putAll(evaluation.metadata());
|
||||||
|
}
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建工具审批请求事件。
|
* 构建工具审批请求事件。
|
||||||
*
|
*
|
||||||
@@ -373,6 +552,7 @@ public class ToolHitlInterceptor implements AgentRuntimeInterceptor {
|
|||||||
putIfPresent(payload, metadata, "toolDisplayName");
|
putIfPresent(payload, metadata, "toolDisplayName");
|
||||||
putIfPresent(payload, metadata, "rawMcpToolName");
|
putIfPresent(payload, metadata, "rawMcpToolName");
|
||||||
putIfPresent(payload, metadata, "mcpToolName");
|
putIfPresent(payload, metadata, "mcpToolName");
|
||||||
|
putIfPresent(payload, metadata, "mcpId");
|
||||||
putIfPresent(payload, metadata, "mcpName");
|
putIfPresent(payload, metadata, "mcpName");
|
||||||
putIfPresent(payload, metadata, "mcpTitle");
|
putIfPresent(payload, metadata, "mcpTitle");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ import java.util.concurrent.CompletableFuture;
|
|||||||
*/
|
*/
|
||||||
public class AgentToolApprovalCoordinator {
|
public class AgentToolApprovalCoordinator {
|
||||||
|
|
||||||
|
/** MCP 工具类型。 */
|
||||||
|
private static final String MCP_TOOL_TYPE = "MCP";
|
||||||
|
|
||||||
/** 是否启用内存审批协调。 */
|
/** 是否启用内存审批协调。 */
|
||||||
private final boolean enabled;
|
private final boolean enabled;
|
||||||
/** 恢复令牌到待审批项的索引。 */
|
/** 恢复令牌到待审批项的索引。 */
|
||||||
@@ -29,6 +32,8 @@ public class AgentToolApprovalCoordinator {
|
|||||||
private final Map<String, String> tokensByToolCallId = new LinkedHashMap<>();
|
private final Map<String, String> tokensByToolCallId = new LinkedHashMap<>();
|
||||||
/** 工具调用ID到一次性执行授权的索引。 */
|
/** 工具调用ID到一次性执行授权的索引。 */
|
||||||
private final Map<String, ExecutionAuthorization> executionAuthorizations = new LinkedHashMap<>();
|
private final Map<String, ExecutionAuthorization> executionAuthorizations = new LinkedHashMap<>();
|
||||||
|
/** 当前 Turn 已批准的可复用工具作用域。 */
|
||||||
|
private final Set<String> reusableApprovalScopes = new LinkedHashSet<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建已启用的协调器。
|
* 创建已启用的协调器。
|
||||||
@@ -300,11 +305,12 @@ public class AgentToolApprovalCoordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据服务端持久化审批结果签发受信任的一次性执行授权。
|
* 根据服务端持久化审批结果签发受信任的执行授权。
|
||||||
*
|
*
|
||||||
* <p>恢复元数据应提供单个 {@code toolCallId/toolName/toolInput},或通过
|
* <p>恢复元数据应提供单个 {@code toolCallId/toolName/toolInput},或通过
|
||||||
* {@code approvedToolCalls} 提供上述字段组成的列表。该入口仅供已经完成持久化令牌
|
* {@code approvedToolCalls} 提供上述字段组成的列表。MCP 调用可额外携带受信任的
|
||||||
* 校验和一次性消费的服务端集成层使用。</p>
|
* {@code toolType/mcpId},用于签发当前 Turn 的复用作用域。该入口仅供已经完成
|
||||||
|
* 持久化令牌校验和一次性消费的服务端集成层使用。</p>
|
||||||
*
|
*
|
||||||
* @param request 受信任恢复请求
|
* @param request 受信任恢复请求
|
||||||
*/
|
*/
|
||||||
@@ -318,16 +324,17 @@ public class AgentToolApprovalCoordinator {
|
|||||||
: request.getMetadata();
|
: request.getMetadata();
|
||||||
Object approvedToolCalls = metadata.get("approvedToolCalls");
|
Object approvedToolCalls = metadata.get("approvedToolCalls");
|
||||||
Map<String, ExecutionAuthorization> trustedAuthorizations = new LinkedHashMap<>();
|
Map<String, ExecutionAuthorization> trustedAuthorizations = new LinkedHashMap<>();
|
||||||
|
Set<String> trustedReusableScopes = new LinkedHashSet<>();
|
||||||
int authorizationCount = 0;
|
int authorizationCount = 0;
|
||||||
if (approvedToolCalls instanceof List<?> calls) {
|
if (approvedToolCalls instanceof List<?> calls) {
|
||||||
for (Object call : calls) {
|
for (Object call : calls) {
|
||||||
if (call instanceof Map<?, ?> callMap) {
|
if (call instanceof Map<?, ?> callMap) {
|
||||||
authorizeTrustedCall(callMap, trustedAuthorizations);
|
authorizeTrustedCall(callMap, trustedAuthorizations, trustedReusableScopes);
|
||||||
authorizationCount++;
|
authorizationCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (metadata.containsKey("toolCallId")) {
|
} else if (metadata.containsKey("toolCallId")) {
|
||||||
authorizeTrustedCall(metadata, trustedAuthorizations);
|
authorizeTrustedCall(metadata, trustedAuthorizations, trustedReusableScopes);
|
||||||
authorizationCount++;
|
authorizationCount++;
|
||||||
}
|
}
|
||||||
if (authorizationCount == 0) {
|
if (authorizationCount == 0) {
|
||||||
@@ -335,6 +342,7 @@ public class AgentToolApprovalCoordinator {
|
|||||||
"Trusted resume metadata must include approved toolCallId, toolName, and toolInput.");
|
"Trusted resume metadata must include approved toolCallId, toolName, and toolInput.");
|
||||||
}
|
}
|
||||||
executionAuthorizations.putAll(trustedAuthorizations);
|
executionAuthorizations.putAll(trustedAuthorizations);
|
||||||
|
reusableApprovalScopes.addAll(trustedReusableScopes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -363,6 +371,45 @@ public class AgentToolApprovalCoordinator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断工具元数据对应的复用作用域是否已在当前 Turn 获得批准。
|
||||||
|
*
|
||||||
|
* @param metadata 服务端工具元数据
|
||||||
|
* @return 当前 Turn 已批准时为 true
|
||||||
|
*/
|
||||||
|
public synchronized boolean isReusableApprovalGranted(Map<String, Object> metadata) {
|
||||||
|
String approvalScope = reusableApprovalScope(metadata);
|
||||||
|
return approvalScope != null && reusableApprovalScopes.contains(approvalScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析可在当前 Turn 复用的审批作用域。
|
||||||
|
*
|
||||||
|
* <p>MCP 使用稳定 {@code mcpId} 生成作用域;受控 Shell 脚本仅接受动态审批策略写入的
|
||||||
|
* 内容摘要作用域。缺少受信任标识时返回 null,使调用方继续执行逐调用审批。</p>
|
||||||
|
*
|
||||||
|
* @param metadata 服务端工具元数据
|
||||||
|
* @return 可复用审批作用域;不可复用时返回 null
|
||||||
|
*/
|
||||||
|
public String reusableApprovalScope(Map<String, Object> metadata) {
|
||||||
|
if (metadata == null || metadata.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String explicitScope = stringValue(metadata.get("approvalScope"));
|
||||||
|
if (Boolean.TRUE.equals(metadata.get("operateTool"))
|
||||||
|
&& "SHELL".equalsIgnoreCase(stringValue(metadata.get("operateToolType")))
|
||||||
|
&& explicitScope != null
|
||||||
|
&& explicitScope.startsWith("SHELL_SCRIPT:")) {
|
||||||
|
return explicitScope;
|
||||||
|
}
|
||||||
|
String toolType = stringValue(metadata.get("toolType"));
|
||||||
|
String mcpId = stringValue(metadata.get("mcpId"));
|
||||||
|
if (!MCP_TOOL_TYPE.equalsIgnoreCase(toolType) || mcpId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return MCP_TOOL_TYPE + ":" + mcpId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清理尚未消费的一次性执行授权。
|
* 清理尚未消费的一次性执行授权。
|
||||||
*/
|
*/
|
||||||
@@ -370,6 +417,13 @@ public class AgentToolApprovalCoordinator {
|
|||||||
executionAuthorizations.clear();
|
executionAuthorizations.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理当前 Turn 的可复用工具审批作用域。
|
||||||
|
*/
|
||||||
|
public synchronized void clearReusableApprovalScopes() {
|
||||||
|
reusableApprovalScopes.clear();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取指定会话当前仍待处理的审批状态。
|
* 获取指定会话当前仍待处理的审批状态。
|
||||||
*
|
*
|
||||||
@@ -399,6 +453,7 @@ public class AgentToolApprovalCoordinator {
|
|||||||
approvals.clear();
|
approvals.clear();
|
||||||
tokensByToolCallId.clear();
|
tokensByToolCallId.clear();
|
||||||
executionAuthorizations.clear();
|
executionAuthorizations.clear();
|
||||||
|
reusableApprovalScopes.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -459,6 +514,11 @@ public class AgentToolApprovalCoordinator {
|
|||||||
*/
|
*/
|
||||||
private void authorize(PendingApproval pendingApproval) {
|
private void authorize(PendingApproval pendingApproval) {
|
||||||
AgentPendingState state = pendingApproval.state;
|
AgentPendingState state = pendingApproval.state;
|
||||||
|
String approvalScope = reusableApprovalScope(state.getMetadata());
|
||||||
|
if (approvalScope != null) {
|
||||||
|
reusableApprovalScopes.add(approvalScope);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (state.getToolCallId() == null || state.getToolCallId().isBlank()) {
|
if (state.getToolCallId() == null || state.getToolCallId().isBlank()) {
|
||||||
throw new AgentRuntimeException("Approved tool call is missing toolCallId.");
|
throw new AgentRuntimeException("Approved tool call is missing toolCallId.");
|
||||||
}
|
}
|
||||||
@@ -471,10 +531,12 @@ public class AgentToolApprovalCoordinator {
|
|||||||
* 为服务端持久化审批结果签发一次性执行授权。
|
* 为服务端持久化审批结果签发一次性执行授权。
|
||||||
*
|
*
|
||||||
* @param callMap 已批准调用元数据
|
* @param callMap 已批准调用元数据
|
||||||
* @param trustedAuthorizations 本次恢复待签发的临时授权集合
|
* @param trustedAuthorizations 本次恢复待签发的一次性授权集合
|
||||||
|
* @param trustedReusableScopes 本次恢复待签发的可复用作用域集合
|
||||||
*/
|
*/
|
||||||
private void authorizeTrustedCall(Map<?, ?> callMap,
|
private void authorizeTrustedCall(Map<?, ?> callMap,
|
||||||
Map<String, ExecutionAuthorization> trustedAuthorizations) {
|
Map<String, ExecutionAuthorization> trustedAuthorizations,
|
||||||
|
Set<String> trustedReusableScopes) {
|
||||||
String toolCallId = stringValue(callMap.get("toolCallId"));
|
String toolCallId = stringValue(callMap.get("toolCallId"));
|
||||||
String toolName = stringValue(callMap.get("toolName"));
|
String toolName = stringValue(callMap.get("toolName"));
|
||||||
if (toolCallId == null || toolName == null) {
|
if (toolCallId == null || toolName == null) {
|
||||||
@@ -482,6 +544,17 @@ public class AgentToolApprovalCoordinator {
|
|||||||
"Trusted resume metadata must include non-empty toolCallId and toolName.");
|
"Trusted resume metadata must include non-empty toolCallId and toolName.");
|
||||||
}
|
}
|
||||||
Map<String, Object> toolInput = stringKeyMap(callMap.get("toolInput"));
|
Map<String, Object> toolInput = stringKeyMap(callMap.get("toolInput"));
|
||||||
|
Map<String, Object> scopeMetadata = new LinkedHashMap<>();
|
||||||
|
scopeMetadata.put("toolType", callMap.get("toolType"));
|
||||||
|
scopeMetadata.put("mcpId", callMap.get("mcpId"));
|
||||||
|
scopeMetadata.put("operateTool", callMap.get("operateTool"));
|
||||||
|
scopeMetadata.put("operateToolType", callMap.get("operateToolType"));
|
||||||
|
scopeMetadata.put("approvalScope", callMap.get("approvalScope"));
|
||||||
|
String reusableScope = reusableApprovalScope(scopeMetadata);
|
||||||
|
if (reusableScope != null) {
|
||||||
|
trustedReusableScopes.add(reusableScope);
|
||||||
|
return;
|
||||||
|
}
|
||||||
ExecutionAuthorization authorization = new ExecutionAuthorization(toolName, toolInput);
|
ExecutionAuthorization authorization = new ExecutionAuthorization(toolName, toolInput);
|
||||||
ExecutionAuthorization previous = trustedAuthorizations.put(toolCallId, authorization);
|
ExecutionAuthorization previous = trustedAuthorizations.put(toolCallId, authorization);
|
||||||
if (previous != null
|
if (previous != null
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.easyagents.agent.runtime.hitl;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单次工具调用的动态审批判定。
|
||||||
|
*
|
||||||
|
* @param valid 调用是否通过审批前静态校验
|
||||||
|
* @param approvalRequired 是否需要人工审批
|
||||||
|
* @param forced 是否禁止复用既有审批
|
||||||
|
* @param reusableScope 可复用审批作用域;为空表示逐调用审批
|
||||||
|
* @param metadata 写入审批事件的受信任元数据
|
||||||
|
*/
|
||||||
|
public record AgentToolApprovalEvaluation(
|
||||||
|
boolean valid,
|
||||||
|
boolean approvalRequired,
|
||||||
|
boolean forced,
|
||||||
|
String reusableScope,
|
||||||
|
Map<String, Object> metadata) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建审批前静态校验失败的判定。
|
||||||
|
*
|
||||||
|
* @return 无需弹出审批的无效判定
|
||||||
|
*/
|
||||||
|
public static AgentToolApprovalEvaluation invalid() {
|
||||||
|
return new AgentToolApprovalEvaluation(false, false, false, null, Map.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建通过静态校验的判定。
|
||||||
|
*
|
||||||
|
* @param approvalRequired 是否需要审批
|
||||||
|
* @param forced 是否强制逐调用审批
|
||||||
|
* @param reusableScope 可复用作用域
|
||||||
|
* @return 动态审批判定
|
||||||
|
*/
|
||||||
|
public static AgentToolApprovalEvaluation valid(boolean approvalRequired,
|
||||||
|
boolean forced,
|
||||||
|
String reusableScope) {
|
||||||
|
Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
if (reusableScope != null && !reusableScope.isBlank()) {
|
||||||
|
metadata.put("approvalScope", reusableScope);
|
||||||
|
}
|
||||||
|
return new AgentToolApprovalEvaluation(
|
||||||
|
true, approvalRequired, forced, reusableScope, Map.copyOf(metadata));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.easyagents.agent.runtime.hitl;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据单次工具入参执行审批前校验并计算动态审批策略。
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface AgentToolApprovalPolicy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评估一次工具调用。
|
||||||
|
*
|
||||||
|
* @param toolInput 工具调用入参
|
||||||
|
* @return 动态审批判定
|
||||||
|
*/
|
||||||
|
AgentToolApprovalEvaluation evaluate(Map<String, Object> toolInput);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.easyagents.agent.runtime.tool;
|
package com.easyagents.agent.runtime.tool;
|
||||||
|
|
||||||
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
|
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
|
||||||
|
import com.easyagents.agent.runtime.hitl.AgentToolApprovalPolicy;
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -18,6 +19,7 @@ public class AgentToolSpec {
|
|||||||
private AgentToolVisibility visibility = AgentToolVisibility.VISIBLE;
|
private AgentToolVisibility visibility = AgentToolVisibility.VISIBLE;
|
||||||
private boolean approvalRequired;
|
private boolean approvalRequired;
|
||||||
private AgentToolApprovalRequest approvalRequest = new AgentToolApprovalRequest();
|
private AgentToolApprovalRequest approvalRequest = new AgentToolApprovalRequest();
|
||||||
|
private AgentToolApprovalPolicy approvalPolicy;
|
||||||
private Map<String, Object> metadata = new LinkedHashMap<>();
|
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -164,6 +166,24 @@ public class AgentToolSpec {
|
|||||||
this.approvalRequest = approvalRequest == null ? new AgentToolApprovalRequest() : approvalRequest;
|
this.approvalRequest = approvalRequest == null ? new AgentToolApprovalRequest() : approvalRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单次调用动态审批策略。
|
||||||
|
*
|
||||||
|
* @return 动态审批策略;未配置时返回 null
|
||||||
|
*/
|
||||||
|
public AgentToolApprovalPolicy getApprovalPolicy() {
|
||||||
|
return approvalPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置单次调用动态审批策略。
|
||||||
|
*
|
||||||
|
* @param approvalPolicy 动态审批策略
|
||||||
|
*/
|
||||||
|
public void setApprovalPolicy(AgentToolApprovalPolicy approvalPolicy) {
|
||||||
|
this.approvalPolicy = approvalPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取元数据。
|
* 获取元数据。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -158,6 +158,138 @@ public class AgentToolApprovalCoordinatorTest {
|
|||||||
coordinator, "call-1", "search", Map.of("q", "easyflow"));
|
coordinator, "call-1", "search", Map.of("q", "easyflow"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证受信任恢复可以签发当前 Turn 内可复用的 MCP 审批作用域。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldAuthorizeTrustedMcpScope() {
|
||||||
|
AgentToolApprovalCoordinator coordinator = AgentToolApprovalCoordinator.enabled();
|
||||||
|
AgentResumeRequest request = new AgentResumeRequest();
|
||||||
|
AgentResumeToken token = new AgentResumeToken();
|
||||||
|
token.setValue("persisted-token");
|
||||||
|
request.setResumeToken(token);
|
||||||
|
request.setApproved(true);
|
||||||
|
request.setTrusted(true);
|
||||||
|
request.setMetadata(Map.of(
|
||||||
|
"toolCallId", "call-1",
|
||||||
|
"toolName", "mcp_101_search",
|
||||||
|
"toolInput", Map.of("q", "easyflow"),
|
||||||
|
"toolType", "MCP",
|
||||||
|
"mcpId", "101"));
|
||||||
|
|
||||||
|
coordinator.authorizeTrustedExecution(request);
|
||||||
|
|
||||||
|
Assert.assertTrue(coordinator.isReusableApprovalGranted(
|
||||||
|
Map.of("toolType", "MCP", "mcpId", "101")));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证 MCP 批准可在当前 Turn 按稳定 mcpId 复用。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldReuseApprovedMcpScopeWithinCurrentTurn() {
|
||||||
|
AgentToolApprovalCoordinator coordinator = AgentToolApprovalCoordinator.enabled();
|
||||||
|
Map<String, Object> mcpMetadata = Map.of("toolType", "MCP", "mcpId", "101");
|
||||||
|
AgentPendingState pending = coordinator.register(
|
||||||
|
"session-1",
|
||||||
|
"agent-1",
|
||||||
|
"call-resolve",
|
||||||
|
"mcp_101_resolve_library_id",
|
||||||
|
"approve",
|
||||||
|
Map.of("libraryName", "AG-UI"),
|
||||||
|
mcpMetadata,
|
||||||
|
Instant.now().plusSeconds(60),
|
||||||
|
"batch-mcp");
|
||||||
|
|
||||||
|
coordinator.resolve(resume(pending, true));
|
||||||
|
|
||||||
|
Assert.assertTrue(coordinator.isReusableApprovalGranted(mcpMetadata));
|
||||||
|
Assert.assertFalse(coordinator.isReusableApprovalGranted(
|
||||||
|
Map.of("toolType", "MCP", "mcpId", "102")));
|
||||||
|
Assert.assertFalse(coordinator.isReusableApprovalGranted(
|
||||||
|
Map.of("toolType", "MCP", "mcpName", "context7")));
|
||||||
|
|
||||||
|
coordinator.clearReusableApprovalScopes();
|
||||||
|
|
||||||
|
Assert.assertFalse(coordinator.isReusableApprovalGranted(mcpMetadata));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证受控 Shell 脚本只能按受信任内容摘要在当前 Turn 复用审批。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldReuseApprovedShellScriptScopeWithinCurrentTurn() {
|
||||||
|
AgentToolApprovalCoordinator coordinator = AgentToolApprovalCoordinator.enabled();
|
||||||
|
Map<String, Object> scriptMetadata = Map.of(
|
||||||
|
"operateTool", true,
|
||||||
|
"operateToolType", "SHELL",
|
||||||
|
"approvalScope", "SHELL_SCRIPT:abc123");
|
||||||
|
AgentPendingState pending = coordinator.register(
|
||||||
|
"session-1", "agent-1", "call-script", "execute_shell_command", "approve",
|
||||||
|
Map.of("command", "python3 report.py"), scriptMetadata,
|
||||||
|
Instant.now().plusSeconds(60), "batch-script");
|
||||||
|
|
||||||
|
coordinator.resolve(resume(pending, true));
|
||||||
|
|
||||||
|
Assert.assertTrue(coordinator.isReusableApprovalGranted(scriptMetadata));
|
||||||
|
Assert.assertFalse(coordinator.isReusableApprovalGranted(Map.of(
|
||||||
|
"operateTool", true,
|
||||||
|
"operateToolType", "SHELL",
|
||||||
|
"approvalScope", "SHELL_SCRIPT:changed")));
|
||||||
|
Assert.assertNull(coordinator.reusableApprovalScope(Map.of(
|
||||||
|
"approvalScope", "SHELL_SCRIPT:abc123")));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证跨节点受信任恢复可恢复 Shell 脚本内容摘要作用域。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldAuthorizeTrustedShellScriptScope() {
|
||||||
|
AgentToolApprovalCoordinator coordinator = AgentToolApprovalCoordinator.enabled();
|
||||||
|
AgentResumeRequest request = new AgentResumeRequest();
|
||||||
|
AgentResumeToken token = new AgentResumeToken();
|
||||||
|
token.setValue("persisted-token");
|
||||||
|
request.setResumeToken(token);
|
||||||
|
request.setApproved(true);
|
||||||
|
request.setTrusted(true);
|
||||||
|
request.setMetadata(Map.of(
|
||||||
|
"toolCallId", "call-script",
|
||||||
|
"toolName", "execute_shell_command",
|
||||||
|
"toolInput", Map.of("command", "node report.mjs"),
|
||||||
|
"operateTool", true,
|
||||||
|
"operateToolType", "SHELL",
|
||||||
|
"approvalScope", "SHELL_SCRIPT:def456"));
|
||||||
|
|
||||||
|
coordinator.authorizeTrustedExecution(request);
|
||||||
|
|
||||||
|
Assert.assertTrue(coordinator.isReusableApprovalGranted(Map.of(
|
||||||
|
"operateTool", true,
|
||||||
|
"operateToolType", "SHELL",
|
||||||
|
"approvalScope", "SHELL_SCRIPT:def456")));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证拒绝和过期不会产生可复用 MCP 批准。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldNotReuseRejectedOrExpiredMcpApproval() {
|
||||||
|
AgentToolApprovalCoordinator coordinator = AgentToolApprovalCoordinator.enabled();
|
||||||
|
Map<String, Object> rejectedMetadata = Map.of("toolType", "MCP", "mcpId", "201");
|
||||||
|
AgentPendingState rejected = coordinator.register(
|
||||||
|
"session-1", "agent-1", "call-rejected", "mcp_rejected", "approve",
|
||||||
|
Map.of(), rejectedMetadata, Instant.now().plusSeconds(60), "batch-rejected");
|
||||||
|
coordinator.resolve(resume(rejected, false));
|
||||||
|
|
||||||
|
Map<String, Object> expiredMetadata = Map.of("toolType", "MCP", "mcpId", "202");
|
||||||
|
AgentPendingState expired = coordinator.register(
|
||||||
|
"session-1", "agent-1", "call-expired-mcp", "mcp_expired", "approve",
|
||||||
|
Map.of(), expiredMetadata, Instant.now().minusSeconds(1), "batch-expired-mcp");
|
||||||
|
coordinator.resolve(resume(expired, true));
|
||||||
|
|
||||||
|
Assert.assertFalse(coordinator.isReusableApprovalGranted(rejectedMetadata));
|
||||||
|
Assert.assertFalse(coordinator.isReusableApprovalGranted(expiredMetadata));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证同一 toolCallId 不能被重新绑定到不同工具内容。
|
* 验证同一 toolCallId 不能被重新绑定到不同工具内容。
|
||||||
*/
|
*/
|
||||||
|
|||||||
36
easy-agents-agui/pom.xml
Normal file
36
easy-agents-agui/pom.xml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>com.easyagents</groupId>
|
||||||
|
<artifactId>easy-agents</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<name>easy-agents-agui</name>
|
||||||
|
<artifactId>easy-agents-agui</artifactId>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.release>17</maven.compiler.release>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easyagents</groupId>
|
||||||
|
<artifactId>easy-agents-agent-runtime</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.agentscope</groupId>
|
||||||
|
<artifactId>agentscope-extensions-agui</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package com.easyagents.agui;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||||
|
import io.agentscope.core.agui.model.AguiMessage;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AgentScope 1.0.12 尚未提供的 AG-UI 标准线级事件。
|
||||||
|
*
|
||||||
|
* <p>该补充层只覆盖当前官方扩展缺失的标准事件,不复制 AG-UI 事件枚举。</p>
|
||||||
|
*/
|
||||||
|
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
|
||||||
|
@JsonSubTypes({
|
||||||
|
@JsonSubTypes.Type(value = AguiExtendedEvent.RunError.class, name = "RUN_ERROR"),
|
||||||
|
@JsonSubTypes.Type(value = AguiExtendedEvent.MessagesSnapshot.class, name = "MESSAGES_SNAPSHOT")
|
||||||
|
})
|
||||||
|
public sealed interface AguiExtendedEvent
|
||||||
|
permits AguiExtendedEvent.RunError, AguiExtendedEvent.MessagesSnapshot {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行失败事件。
|
||||||
|
*
|
||||||
|
* @param threadId AG-UI 线程 ID
|
||||||
|
* @param runId AG-UI 运行 ID
|
||||||
|
* @param message 可安全展示的错误消息
|
||||||
|
* @param code 稳定错误码
|
||||||
|
*/
|
||||||
|
record RunError(String threadId, String runId, String message, String code)
|
||||||
|
implements AguiExtendedEvent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建运行失败事件。
|
||||||
|
*
|
||||||
|
* @param threadId AG-UI 线程 ID
|
||||||
|
* @param runId AG-UI 运行 ID
|
||||||
|
* @param message 可安全展示的错误消息
|
||||||
|
* @param code 稳定错误码
|
||||||
|
*/
|
||||||
|
@JsonCreator
|
||||||
|
public RunError(
|
||||||
|
@JsonProperty("threadId") String threadId,
|
||||||
|
@JsonProperty("runId") String runId,
|
||||||
|
@JsonProperty("message") String message,
|
||||||
|
@JsonProperty("code") String code) {
|
||||||
|
this.threadId = Objects.requireNonNull(threadId, "threadId cannot be null");
|
||||||
|
this.runId = Objects.requireNonNull(runId, "runId cannot be null");
|
||||||
|
this.message = Objects.requireNonNull(message, "message cannot be null");
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息全量快照事件。
|
||||||
|
*
|
||||||
|
* @param threadId AG-UI 线程 ID
|
||||||
|
* @param runId AG-UI 运行 ID
|
||||||
|
* @param messages 消息快照
|
||||||
|
*/
|
||||||
|
record MessagesSnapshot(String threadId, String runId, List<AguiMessage> messages)
|
||||||
|
implements AguiExtendedEvent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建消息全量快照事件。
|
||||||
|
*
|
||||||
|
* @param threadId AG-UI 线程 ID
|
||||||
|
* @param runId AG-UI 运行 ID
|
||||||
|
* @param messages 消息快照
|
||||||
|
*/
|
||||||
|
@JsonCreator
|
||||||
|
public MessagesSnapshot(
|
||||||
|
@JsonProperty("threadId") String threadId,
|
||||||
|
@JsonProperty("runId") String runId,
|
||||||
|
@JsonProperty("messages") List<AguiMessage> messages) {
|
||||||
|
this.threadId = Objects.requireNonNull(threadId, "threadId cannot be null");
|
||||||
|
this.runId = Objects.requireNonNull(runId, "runId cannot be null");
|
||||||
|
this.messages = messages == null ? List.of() : List.copyOf(messages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.easyagents.agui;
|
||||||
|
|
||||||
|
import io.agentscope.core.agui.AguiException;
|
||||||
|
import io.agentscope.core.util.JsonException;
|
||||||
|
import io.agentscope.core.util.JsonUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 AG-UI 事件编码为 JSON 或 SSE 数据帧。
|
||||||
|
*
|
||||||
|
* <p>编码器无可变状态,可安全跨请求复用。</p>
|
||||||
|
*/
|
||||||
|
public final class AguiProtocolEventEncoder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编码为 JSON。
|
||||||
|
*
|
||||||
|
* @param event AG-UI 官方事件或补充标准事件
|
||||||
|
* @return JSON 文本
|
||||||
|
* @throws AguiException.EncodingException 序列化失败时抛出
|
||||||
|
*/
|
||||||
|
public String encodeToJson(Object event) {
|
||||||
|
try {
|
||||||
|
return JsonUtils.getJsonCodec().toJson(event);
|
||||||
|
} catch (JsonException exception) {
|
||||||
|
throw new AguiException.EncodingException("Failed to encode AG-UI event", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编码为 SSE data 帧。
|
||||||
|
*
|
||||||
|
* @param event AG-UI 官方事件或补充标准事件
|
||||||
|
* @return 完整 SSE data 帧
|
||||||
|
* @throws AguiException.EncodingException 序列化失败时抛出
|
||||||
|
*/
|
||||||
|
public String encode(Object event) {
|
||||||
|
return "data: " + encodeToJson(event) + "\n\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
package com.easyagents.agui;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||||
|
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
||||||
|
import io.agentscope.core.agui.event.AguiEvent;
|
||||||
|
import io.agentscope.core.util.JsonException;
|
||||||
|
import io.agentscope.core.util.JsonUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Easy-Agents 中立运行时事件有序投影为 AG-UI 事件。
|
||||||
|
*
|
||||||
|
* <p>实例绑定单个 run 且非线程安全。调用方应按运行顺序串行调用 {@link #project(AgentRuntimeEvent)}。</p>
|
||||||
|
*/
|
||||||
|
public final class AguiRuntimeEventProjector {
|
||||||
|
|
||||||
|
private static final String DEFAULT_ERROR_MESSAGE = "Agent runtime failed.";
|
||||||
|
|
||||||
|
/** AG-UI thread ID。 */
|
||||||
|
private final String threadId;
|
||||||
|
/** AG-UI run ID。 */
|
||||||
|
private final String runId;
|
||||||
|
/** 已开始但尚未收到结果的工具调用。 */
|
||||||
|
private final Set<String> knownToolCallIds = new LinkedHashSet<>();
|
||||||
|
|
||||||
|
private boolean runStarted;
|
||||||
|
private boolean terminated;
|
||||||
|
private String openMessageId;
|
||||||
|
private String openReasoningMessageId;
|
||||||
|
private long generatedMessageSequence;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建不含业务 Custom Event 的投影器。
|
||||||
|
*
|
||||||
|
* @param threadId AG-UI thread ID
|
||||||
|
* @param runId AG-UI run ID
|
||||||
|
*/
|
||||||
|
public AguiRuntimeEventProjector(String threadId, String runId) {
|
||||||
|
this.threadId = requireText(threadId, "threadId");
|
||||||
|
this.runId = requireText(runId, "runId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按输入顺序投影一条运行时事件。
|
||||||
|
*
|
||||||
|
* @param event Easy-Agents 运行时事件
|
||||||
|
* @return 零到多条 AG-UI 官方或补充标准事件
|
||||||
|
*/
|
||||||
|
public List<Object> project(AgentRuntimeEvent event) {
|
||||||
|
if (event == null || event.getEventType() == null || terminated) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<Object> output = new ArrayList<>();
|
||||||
|
switch (event.getEventType()) {
|
||||||
|
case STARTED -> startRun(output);
|
||||||
|
case MESSAGE_DELTA -> projectMessageDelta(event, output);
|
||||||
|
case REASONING_STARTED -> startReasoning(event, output);
|
||||||
|
case REASONING_DELTA -> projectReasoningDelta(event, output);
|
||||||
|
case REASONING_COMPLETED -> closeReasoning(output);
|
||||||
|
case TOOL_CALL -> projectToolCall(event, output);
|
||||||
|
case TOOL_RESULT -> projectToolResult(event, output);
|
||||||
|
case COMPLETED -> finishSuccessfully(output);
|
||||||
|
case FAILED -> finishWithError(event, "AGENT_RUNTIME_FAILED", output);
|
||||||
|
case CANCELLED -> finishWithError(event, "RUN_CANCELLED", output);
|
||||||
|
default -> {
|
||||||
|
// EasyFlow 等上层业务扩展由各自协议边界映射为 CUSTOM,通用模块保持业务无关。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前投影是否已经产生协议终态。
|
||||||
|
*
|
||||||
|
* @return 已产生 RUN_FINISHED 或 RUN_ERROR 时为 true
|
||||||
|
*/
|
||||||
|
public boolean isTerminated() {
|
||||||
|
return terminated;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startRun(List<Object> output) {
|
||||||
|
if (!runStarted) {
|
||||||
|
output.add(new AguiEvent.RunStarted(threadId, runId));
|
||||||
|
runStarted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void projectMessageDelta(AgentRuntimeEvent event, List<Object> output) {
|
||||||
|
startRun(output);
|
||||||
|
String messageId = eventMessageId(event, "assistant");
|
||||||
|
if (!Objects.equals(openMessageId, messageId)) {
|
||||||
|
closeMessage(output);
|
||||||
|
output.add(new AguiEvent.TextMessageStart(threadId, runId, messageId, "assistant"));
|
||||||
|
openMessageId = messageId;
|
||||||
|
}
|
||||||
|
String delta = stringValue(event.getPayload(), "text");
|
||||||
|
if (!delta.isEmpty()) {
|
||||||
|
output.add(new AguiEvent.TextMessageContent(threadId, runId, messageId, delta));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startReasoning(AgentRuntimeEvent event, List<Object> output) {
|
||||||
|
startRun(output);
|
||||||
|
if (openReasoningMessageId != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openReasoningMessageId = eventMessageId(event, "reasoning");
|
||||||
|
output.add(new AguiEvent.ReasoningMessageStart(
|
||||||
|
threadId, runId, openReasoningMessageId, "reasoning"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void projectReasoningDelta(AgentRuntimeEvent event, List<Object> output) {
|
||||||
|
startReasoning(event, output);
|
||||||
|
String delta = stringValue(event.getPayload(), "reasoning");
|
||||||
|
if (!delta.isEmpty()) {
|
||||||
|
output.add(new AguiEvent.ReasoningMessageContent(
|
||||||
|
threadId, runId, openReasoningMessageId, delta));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void projectToolCall(AgentRuntimeEvent event, List<Object> output) {
|
||||||
|
startRun(output);
|
||||||
|
String toolCallId = firstText(event.getToolCallId(), stringValue(event.getPayload(), "toolCallId"));
|
||||||
|
if (toolCallId == null || !knownToolCallIds.add(toolCallId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String toolName = firstText(
|
||||||
|
stringValue(event.getPayload(), "toolName"),
|
||||||
|
stringValue(event.getPayload(), "name"),
|
||||||
|
"tool");
|
||||||
|
output.add(new AguiEvent.ToolCallStart(threadId, runId, toolCallId, toolName));
|
||||||
|
output.add(new AguiEvent.ToolCallArgs(
|
||||||
|
threadId, runId, toolCallId, jsonValue(event.getPayload().get("input"))));
|
||||||
|
output.add(new AguiEvent.ToolCallEnd(threadId, runId, toolCallId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void projectToolResult(AgentRuntimeEvent event, List<Object> output) {
|
||||||
|
startRun(output);
|
||||||
|
String toolCallId = firstText(event.getToolCallId(), stringValue(event.getPayload(), "toolCallId"));
|
||||||
|
if (toolCallId == null || !knownToolCallIds.contains(toolCallId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String messageId = eventMessageId(event, "tool-" + toolCallId);
|
||||||
|
String content = nullToEmpty(firstText(
|
||||||
|
stringValue(event.getPayload(), "text"),
|
||||||
|
event.getPayload().containsKey("result")
|
||||||
|
? jsonValue(event.getPayload().get("result"))
|
||||||
|
: null));
|
||||||
|
output.add(new AguiEvent.ToolCallResult(
|
||||||
|
threadId, runId, toolCallId, content, "tool", messageId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishSuccessfully(List<Object> output) {
|
||||||
|
startRun(output);
|
||||||
|
closeOpenFragments(output);
|
||||||
|
output.add(new AguiEvent.RunFinished(threadId, runId));
|
||||||
|
terminated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishWithError(AgentRuntimeEvent event, String code, List<Object> output) {
|
||||||
|
startRun(output);
|
||||||
|
closeOpenFragments(output);
|
||||||
|
String message = firstText(
|
||||||
|
stringValue(event.getPayload(), "message"),
|
||||||
|
stringValue(event.getPayload(), "reason"),
|
||||||
|
DEFAULT_ERROR_MESSAGE);
|
||||||
|
output.add(new AguiExtendedEvent.RunError(threadId, runId, message, code));
|
||||||
|
terminated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeOpenFragments(List<Object> output) {
|
||||||
|
closeReasoning(output);
|
||||||
|
closeMessage(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeMessage(List<Object> output) {
|
||||||
|
if (openMessageId != null) {
|
||||||
|
output.add(new AguiEvent.TextMessageEnd(threadId, runId, openMessageId));
|
||||||
|
openMessageId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeReasoning(List<Object> output) {
|
||||||
|
if (openReasoningMessageId != null) {
|
||||||
|
output.add(new AguiEvent.ReasoningMessageEnd(
|
||||||
|
threadId, runId, openReasoningMessageId));
|
||||||
|
openReasoningMessageId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String eventMessageId(AgentRuntimeEvent event, String suffix) {
|
||||||
|
String messageId = firstText(
|
||||||
|
event.getMessageId(),
|
||||||
|
event.getMessage() == null ? null : event.getMessage().getMessageId());
|
||||||
|
if (messageId != null) {
|
||||||
|
return messageId;
|
||||||
|
}
|
||||||
|
generatedMessageSequence++;
|
||||||
|
return runId + "-" + suffix + "-" + generatedMessageSequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String stringValue(Map<String, Object> payload, String key) {
|
||||||
|
if (payload == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
Object value = payload.get(key);
|
||||||
|
return value instanceof String text ? text : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String jsonValue(Object value) {
|
||||||
|
if (value == null) {
|
||||||
|
return "{}";
|
||||||
|
}
|
||||||
|
if (value instanceof String text) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JsonUtils.getJsonCodec().toJson(value);
|
||||||
|
} catch (JsonException exception) {
|
||||||
|
throw new IllegalArgumentException("Failed to encode AG-UI payload", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String firstText(String... values) {
|
||||||
|
for (String value : values) {
|
||||||
|
if (value != null && !value.isBlank()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String requireText(String value, String name) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
throw new IllegalArgumentException(name + " cannot be blank");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String nullToEmpty(String value) {
|
||||||
|
return value == null ? "" : value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.easyagents.agui;
|
||||||
|
|
||||||
|
import io.agentscope.core.agui.event.AguiEvent;
|
||||||
|
import io.agentscope.core.agui.model.AguiMessage;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link AguiProtocolEventEncoder} 的线级协议测试。
|
||||||
|
*/
|
||||||
|
public class AguiProtocolEventEncoderTest {
|
||||||
|
|
||||||
|
private final AguiProtocolEventEncoder encoder = new AguiProtocolEventEncoder();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证 AgentScope 官方事件保留 AG-UI type 字段。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldEncodeOfficialEvent() {
|
||||||
|
String json = encoder.encodeToJson(new AguiEvent.RunStarted("thread-1", "run-1"));
|
||||||
|
|
||||||
|
Assert.assertTrue(json.contains("\"type\":\"RUN_STARTED\""));
|
||||||
|
Assert.assertTrue(json.contains("\"threadId\":\"thread-1\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证补充的失败和消息快照事件使用现代 AG-UI 标准事件名。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldEncodeExtendedStandardEvents() {
|
||||||
|
String error = encoder.encodeToJson(
|
||||||
|
new AguiExtendedEvent.RunError("thread-1", "run-1", "boom", "FAILED"));
|
||||||
|
String snapshot = encoder.encodeToJson(new AguiExtendedEvent.MessagesSnapshot(
|
||||||
|
"thread-1", "run-1", List.of(AguiMessage.userMessage("message-1", "hello"))));
|
||||||
|
|
||||||
|
Assert.assertTrue(error.contains("\"type\":\"RUN_ERROR\""));
|
||||||
|
Assert.assertTrue(error.contains("\"code\":\"FAILED\""));
|
||||||
|
Assert.assertTrue(snapshot.contains("\"type\":\"MESSAGES_SNAPSHOT\""));
|
||||||
|
Assert.assertTrue(snapshot.contains("\"role\":\"user\""));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package com.easyagents.agui;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||||
|
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
||||||
|
import io.agentscope.core.agui.event.AguiEvent;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link AguiRuntimeEventProjector} 的协议顺序与终态测试。
|
||||||
|
*/
|
||||||
|
public class AguiRuntimeEventProjectorTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证文本、推理、工具和成功终态按 AG-UI 顺序投影。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldProjectSuccessfulRunInProtocolOrder() {
|
||||||
|
AguiRuntimeEventProjector projector = new AguiRuntimeEventProjector("thread-1", "run-1");
|
||||||
|
List<Object> events = new ArrayList<>();
|
||||||
|
|
||||||
|
events.addAll(projector.project(event(AgentRuntimeEventType.STARTED, null, null, Map.of())));
|
||||||
|
events.addAll(projector.project(event(
|
||||||
|
AgentRuntimeEventType.REASONING_STARTED, "reasoning-1", null, Map.of())));
|
||||||
|
events.addAll(projector.project(event(
|
||||||
|
AgentRuntimeEventType.REASONING_DELTA, "reasoning-1", null, Map.of("reasoning", "分析"))));
|
||||||
|
events.addAll(projector.project(event(
|
||||||
|
AgentRuntimeEventType.REASONING_COMPLETED, "reasoning-1", null, Map.of())));
|
||||||
|
events.addAll(projector.project(event(
|
||||||
|
AgentRuntimeEventType.MESSAGE_DELTA, "message-1", null, Map.of("text", "你好"))));
|
||||||
|
events.addAll(projector.project(event(
|
||||||
|
AgentRuntimeEventType.TOOL_CALL, null, "tool-1",
|
||||||
|
Map.of("toolName", "search", "input", Map.of("q", "AG-UI")))));
|
||||||
|
events.addAll(projector.project(event(
|
||||||
|
AgentRuntimeEventType.TOOL_RESULT, null, "tool-1", Map.of("text", "done"))));
|
||||||
|
events.addAll(projector.project(event(AgentRuntimeEventType.COMPLETED, null, null, Map.of())));
|
||||||
|
|
||||||
|
Assert.assertEquals(List.of(
|
||||||
|
"RunStarted",
|
||||||
|
"ReasoningMessageStart",
|
||||||
|
"ReasoningMessageContent",
|
||||||
|
"ReasoningMessageEnd",
|
||||||
|
"TextMessageStart",
|
||||||
|
"TextMessageContent",
|
||||||
|
"ToolCallStart",
|
||||||
|
"ToolCallArgs",
|
||||||
|
"ToolCallEnd",
|
||||||
|
"ToolCallResult",
|
||||||
|
"TextMessageEnd",
|
||||||
|
"RunFinished"), events.stream().map(value -> value.getClass().getSimpleName()).toList());
|
||||||
|
Assert.assertEquals(
|
||||||
|
"reasoning", ((AguiEvent.ReasoningMessageStart) events.get(1)).role());
|
||||||
|
Assert.assertTrue(projector.isTerminated());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证失败终态不会追加成功事件,终态后的迟到事件会被丢弃。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldEmitSingleErrorTerminalAndIgnoreLateEvents() {
|
||||||
|
AguiRuntimeEventProjector projector = new AguiRuntimeEventProjector("thread-1", "run-1");
|
||||||
|
|
||||||
|
List<Object> failed = projector.project(event(
|
||||||
|
AgentRuntimeEventType.FAILED, null, null, Map.of("message", "boom")));
|
||||||
|
List<Object> late = projector.project(event(AgentRuntimeEventType.COMPLETED, null, null, Map.of()));
|
||||||
|
|
||||||
|
Assert.assertEquals(2, failed.size());
|
||||||
|
Assert.assertTrue(failed.get(0) instanceof AguiEvent.RunStarted);
|
||||||
|
Assert.assertEquals(
|
||||||
|
new AguiExtendedEvent.RunError("thread-1", "run-1", "boom", "AGENT_RUNTIME_FAILED"),
|
||||||
|
failed.get(1));
|
||||||
|
Assert.assertTrue(late.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AgentRuntimeEvent event(
|
||||||
|
AgentRuntimeEventType type,
|
||||||
|
String messageId,
|
||||||
|
String toolCallId,
|
||||||
|
Map<String, Object> payload) {
|
||||||
|
AgentRuntimeEvent event = AgentRuntimeEvent.of(type);
|
||||||
|
event.setMessageId(messageId);
|
||||||
|
event.setToolCallId(toolCallId);
|
||||||
|
event.setPayload(payload);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -264,6 +264,10 @@
|
|||||||
<groupId>com.easyagents</groupId>
|
<groupId>com.easyagents</groupId>
|
||||||
<artifactId>easy-agents-agent-runtime</artifactId>
|
<artifactId>easy-agents-agent-runtime</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easyagents</groupId>
|
||||||
|
<artifactId>easy-agents-agui</artifactId>
|
||||||
|
</dependency>
|
||||||
<!--agent runtime end-->
|
<!--agent runtime end-->
|
||||||
|
|
||||||
<!--search engines start-->
|
<!--search engines start-->
|
||||||
|
|||||||
13
pom.xml
13
pom.xml
@@ -29,6 +29,7 @@
|
|||||||
<module>easy-agents-mcp</module>
|
<module>easy-agents-mcp</module>
|
||||||
<module>easy-agents-skill</module>
|
<module>easy-agents-skill</module>
|
||||||
<module>easy-agents-agent-runtime</module>
|
<module>easy-agents-agent-runtime</module>
|
||||||
|
<module>easy-agents-agui</module>
|
||||||
<module>easy-agents-flow</module>
|
<module>easy-agents-flow</module>
|
||||||
<module>easy-agents-support</module>
|
<module>easy-agents-support</module>
|
||||||
</modules>
|
</modules>
|
||||||
@@ -456,12 +457,24 @@
|
|||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easyagents</groupId>
|
||||||
|
<artifactId>easy-agents-agui</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.agentscope</groupId>
|
<groupId>io.agentscope</groupId>
|
||||||
<artifactId>agentscope</artifactId>
|
<artifactId>agentscope</artifactId>
|
||||||
<version>${agentscope.version}</version>
|
<version>${agentscope.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.agentscope</groupId>
|
||||||
|
<artifactId>agentscope-extensions-agui</artifactId>
|
||||||
|
<version>${agentscope.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user