发布 v1.1.0 #2

Merged
czm merged 34 commits from develop into main 2026-08-20 11:35:40 +08:00
14 changed files with 1043 additions and 13 deletions
Showing only changes of commit 49a7de34bb - Show all commits

View File

@@ -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");
} }

View File

@@ -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

View File

@@ -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));
}
}

View File

@@ -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);
}

View File

@@ -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;
}
/** /**
* 获取元数据。 * 获取元数据。
* *

View File

@@ -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
View 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>

View File

@@ -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);
}
}
}

View File

@@ -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";
}
}

View File

@@ -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;
}
}

View File

@@ -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\""));
}
}

View File

@@ -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;
}
}

View File

@@ -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
View File

@@ -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>