fix: 完善工具审批调用绑定
- 以 toolCallId、工具名称和入参绑定一次性执行授权 - 支持批次审批、重复调用去重及拒绝过期处理 - 补充多工具审批与授权消费回归测试
This commit is contained in:
@@ -9,7 +9,9 @@ import com.easyagents.agent.runtime.event.observer.AgentRuntimeErrorObserver;
|
||||
import com.easyagents.agent.runtime.event.observer.ReasoningLifecycleObserver;
|
||||
import com.easyagents.agent.runtime.event.observer.SkillExecutionObserver;
|
||||
import com.easyagents.agent.runtime.event.observer.ToolExecutionObserver;
|
||||
import com.easyagents.agent.runtime.hitl.AgentPendingState;
|
||||
import com.easyagents.agent.runtime.hitl.AgentToolApprovalCoordinator;
|
||||
import com.easyagents.agent.runtime.hitl.AgentToolApprovalResolution;
|
||||
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRejectedException;
|
||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeSpec;
|
||||
import com.easyagents.agent.runtime.knowledge.citation.AgentKnowledgeCitationMatcher;
|
||||
@@ -153,6 +155,9 @@ public class AgentScopeReActRuntime implements AgentRuntime {
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
if (approvalCoordinator != null) {
|
||||
approvalCoordinator.cancelAll("Agent runtime has been closed.");
|
||||
}
|
||||
closeMcpClients();
|
||||
initialized.set(false);
|
||||
}
|
||||
@@ -188,17 +193,27 @@ public class AgentScopeReActRuntime implements AgentRuntime {
|
||||
return Flux.error(new AgentRuntimeException("Agent runtime is already streaming."));
|
||||
}
|
||||
AgentRuntimeExecutionContext executionContext = createResumeExecutionContext(request);
|
||||
AgentToolApprovalResolution resolution = null;
|
||||
try {
|
||||
if (!request.isTrusted()) {
|
||||
approvalCoordinator.consume(request);
|
||||
if (request.isTrusted()) {
|
||||
approvalCoordinator.authorizeTrustedExecution(request);
|
||||
} else {
|
||||
resolution = approvalCoordinator.resolve(request);
|
||||
}
|
||||
} catch (RuntimeException error) {
|
||||
running.set(false);
|
||||
throw error;
|
||||
}
|
||||
// 审批拒绝
|
||||
if (!request.isApproved()) {
|
||||
executionContext.setCancelReason(request.getRejectReason());
|
||||
if (resolution != null
|
||||
&& resolution.getStatus() == AgentToolApprovalResolution.Status.WAITING) {
|
||||
return waitingForRemainingApprovals(executionContext, resolution);
|
||||
}
|
||||
if (!request.isApproved()
|
||||
|| resolution != null
|
||||
&& (resolution.getStatus() == AgentToolApprovalResolution.Status.REJECTED
|
||||
|| resolution.getStatus() == AgentToolApprovalResolution.Status.EXPIRED)) {
|
||||
String cancelReason = resolution == null ? request.getRejectReason() : resolution.getReason();
|
||||
executionContext.setCancelReason(cancelReason);
|
||||
return Flux.defer(() -> {
|
||||
saveSession();
|
||||
return Flux.just(started(executionContext), cancelled(executionContext));
|
||||
@@ -240,8 +255,6 @@ public class AgentScopeReActRuntime implements AgentRuntime {
|
||||
AtomicReference<AgentMessage> finalMessage = new AtomicReference<>();
|
||||
// HITL 暂停事件。被设置后,本轮以 SUSPENDED 挂起而不是 COMPLETED 结束。
|
||||
AtomicReference<AgentRuntimeEvent> suspendedEvent = new AtomicReference<>();
|
||||
// 本轮 HITL 待审批项来自旁路交互事件,最终会合并进 SUSPENDED 挂起事件。
|
||||
List<Map<String, Object>> pendingApprovals = new CopyOnWriteArrayList<>();
|
||||
// 知识库引注。
|
||||
Map<String, AgentKnowledgeReference> knowledgeReferences = new LinkedHashMap<>();
|
||||
// 流式输出归一化,防止出现累计快照的重复输出。
|
||||
@@ -250,8 +263,7 @@ public class AgentScopeReActRuntime implements AgentRuntime {
|
||||
AtomicBoolean cancelled = new AtomicBoolean(false);
|
||||
// 旁线路监察事件流式输出。
|
||||
Flux<AgentRuntimeEvent> sideEventFlux = sideEvents.asFlux()
|
||||
.doOnNext(event -> updateKnowledgeReferences(knowledgeReferences, event))
|
||||
.doOnNext(event -> updatePendingApprovals(pendingApprovals, event));
|
||||
.doOnNext(event -> updateKnowledgeReferences(knowledgeReferences, event));
|
||||
// 主线路 agent 交互。resume 场景会传入空列表,让 AgentScope 从 pending tool 继续执行。
|
||||
Flux<AgentRuntimeEvent> mainEventFlux = agent.stream(inputSupplier.get(), streamOptions())
|
||||
.timeout(executionContext.getAgentDefinition().getExecutionOptions().getTimeout())
|
||||
@@ -271,9 +283,8 @@ public class AgentScopeReActRuntime implements AgentRuntime {
|
||||
.concatWith(Flux.defer(() -> {
|
||||
AgentRuntimeEvent suspended = suspendedEvent.get();
|
||||
if (suspended != null) {
|
||||
// 触发 hitl 审批事件,暂时挂起。
|
||||
suspended.getPayload().put("pendingApprovals", pendingApprovals);
|
||||
return Flux.just(suspended);
|
||||
// SUSPENDED 已在主线路中输出,结束阶段不再重复发送。
|
||||
return Flux.empty();
|
||||
}
|
||||
return Flux.just(completed(executionContext, finalText.toString(),
|
||||
finalMessage.get(), knowledgeReferences));
|
||||
@@ -542,9 +553,10 @@ public class AgentScopeReActRuntime implements AgentRuntime {
|
||||
if (sourceEvent.getMessage() != null) {
|
||||
event.setMessage(messageAdapter.toAgentMessage(sourceEvent.getMessage()));
|
||||
}
|
||||
event.getPayload().put("reason", context.getMetadata().getOrDefault("hitlSuspendReason", "TOOL_APPROVAL_REQUIRED"));
|
||||
Object pendingApprovals = context.getMetadata().get("hitlPendingApprovals");
|
||||
event.getPayload().put("pendingApprovals", pendingApprovals instanceof List<?> list ? list : List.of());
|
||||
event.getPayload().put("reason", "TOOL_APPROVAL_REQUIRED");
|
||||
event.getPayload().put("pendingApprovals", approvalCoordinator.pendingStates(context.getSessionId()).stream()
|
||||
.map(this::pendingApprovalPayload)
|
||||
.toList());
|
||||
event.getMetadata().put("source", "AGENTSCOPE_STREAM");
|
||||
event.getMetadata().put("generateReason", sourceEvent.getMessage() == null
|
||||
? GenerateReason.REASONING_STOP_REQUESTED.name()
|
||||
@@ -552,6 +564,46 @@ public class AgentScopeReActRuntime implements AgentRuntime {
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在同一审批批次仍有未决工具时返回挂起事件,并保持 AgentScope pending tools 不执行。
|
||||
*
|
||||
* @param context 本轮恢复上下文
|
||||
* @param resolution 审批批次决议
|
||||
* @return 开始与挂起事件流
|
||||
*/
|
||||
private Flux<AgentRuntimeEvent> waitingForRemainingApprovals(AgentRuntimeExecutionContext context,
|
||||
AgentToolApprovalResolution resolution) {
|
||||
AgentRuntimeEvent suspended = base(context, AgentRuntimeEventType.SUSPENDED);
|
||||
suspended.getPayload().put("reason", "TOOL_APPROVAL_REQUIRED");
|
||||
suspended.getPayload().put("pendingApprovals", resolution.getRemainingStates().stream()
|
||||
.map(this::pendingApprovalPayload)
|
||||
.toList());
|
||||
suspended.getMetadata().put("source", "APPROVAL_COORDINATOR");
|
||||
suspended.getMetadata().put("approvalStatus", AgentToolApprovalResolution.Status.WAITING.name());
|
||||
return Flux.just(started(context), suspended)
|
||||
.doOnNext(event -> context.getConversationRecorder().record(context, event))
|
||||
.doFinally(signalType -> cleanupTurn());
|
||||
}
|
||||
|
||||
/**
|
||||
* 将待审批状态转换为前端可消费的稳定字段。
|
||||
*
|
||||
* @param state 待审批状态
|
||||
* @return 待审批载荷
|
||||
*/
|
||||
private Map<String, Object> pendingApprovalPayload(AgentPendingState state) {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("resumeToken", state.getResumeToken().getValue());
|
||||
payload.put("toolCallId", state.getToolCallId());
|
||||
payload.put("toolName", state.getToolName());
|
||||
payload.put("toolInput", state.getToolInput());
|
||||
payload.put("input", state.getToolInput());
|
||||
payload.put("approvalPrompt", state.getApprovalPrompt());
|
||||
payload.put("approvalMetadata", state.getMetadata());
|
||||
payload.put("expiresAt", state.getExpiresAt() == null ? null : state.getExpiresAt().toString());
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成开始事件。
|
||||
*
|
||||
@@ -742,6 +794,7 @@ public class AgentScopeReActRuntime implements AgentRuntime {
|
||||
* 清理本轮状态。
|
||||
*/
|
||||
private void cleanupTurn() {
|
||||
approvalCoordinator.clearExecutionAuthorizations();
|
||||
turnContextHolder.clear();
|
||||
running.set(false);
|
||||
}
|
||||
@@ -822,26 +875,6 @@ public class AgentScopeReActRuntime implements AgentRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从工具审批旁路事件中收集本轮待审批项。
|
||||
*
|
||||
* @param pendingApprovals 待审批项集合
|
||||
* @param event 运行时事件
|
||||
*/
|
||||
private void updatePendingApprovals(List<Map<String, Object>> pendingApprovals, AgentRuntimeEvent event) {
|
||||
if (event == null || event.getEventType() != AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED) {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> approval = new LinkedHashMap<>();
|
||||
approval.put("resumeToken", event.getPayload().get("resumeToken"));
|
||||
approval.put("toolCallId", event.getPayload().get("toolCallId"));
|
||||
approval.put("toolName", event.getPayload().get("toolName"));
|
||||
approval.put("toolInput", event.getPayload().get("toolInput"));
|
||||
approval.put("expiresAt", event.getPayload().get("expiresAt"));
|
||||
approval.put("approvalPrompt", event.getPayload().get("approvalPrompt"));
|
||||
pendingApprovals.add(approval);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从知识库旁路事件中收集本轮候选引用。
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user