From 2b5e701ade62357324ef77ffc5167008f35ba18d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com>
Date: Sat, 23 May 2026 21:17:50 +0800
Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E5=8D=87?=
=?UTF-8?q?=E7=BA=A7=E4=B8=BA=E6=9C=89=E7=8A=B6=E6=80=81=20Agent=20-=20?=
=?UTF-8?q?=E5=AE=8C=E5=96=84=20hook=20=E5=AF=B9=E6=8E=A5=E6=9C=BA?=
=?UTF-8?q?=E5=88=B6=20-=20=E6=8F=90=E4=BE=9B=E6=9B=B4=E5=8A=A0=E6=98=8E?=
=?UTF-8?q?=E7=A1=AE=E7=9A=84=E8=B0=83=E7=94=A8=E6=96=B9=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../agent/runtime/AgentInitRequest.java | 203 +++
...lResponse.java => AgentResumeRequest.java} | 35 +-
.../agent/runtime/AgentRunHandle.java | 30 -
.../agent/runtime/AgentRuntime.java | 26 +-
...java => AgentRuntimeExecutionContext.java} | 87 +-
...AgentScopeAutoContextCompressionModel.java | 157 ++
.../agentscope/AgentScopeEventHook.java | 28 -
.../AgentScopeKnowledgeAdapter.java | 132 +-
.../agentscope/AgentScopeMemoryAdapter.java | 32 +-
.../AgentScopeMemoryBuildResult.java | 46 +
.../agentscope/AgentScopeReActRuntime.java | 1444 ++++++++++-------
.../agentscope/AgentScopeRuntimeHook.java | 37 +
.../agentscope/AgentScopeSessionAdapter.java | 46 +-
.../agentscope/AgentScopeSkillAdapter.java | 89 +-
.../agentscope/AgentScopeToolAdapter.java | 232 ++-
.../event/AgentRuntimeEventBridge.java | 130 ++
.../runtime/event/AgentRuntimeEventType.java | 25 +
.../event/AgentRuntimeInterceptor.java | 34 +
.../event/AgentRuntimeObservationManager.java | 72 +
.../runtime/event/AgentRuntimeObserver.java | 31 +
.../event/AgentRuntimeTurnContext.java | 137 ++
.../event/AgentRuntimeTurnContextHolder.java | 74 +
.../interceptor/AutoContextInterceptor.java | 357 ++++
.../interceptor/ToolHitlInterceptor.java | 198 +++
.../observer/AgentRuntimeErrorObserver.java | 62 +
.../observer/ReasoningLifecycleObserver.java | 77 +
.../observer/SkillExecutionObserver.java | 300 ++++
.../event/observer/ToolExecutionObserver.java | 154 ++
.../hitl/AgentToolApprovalCoordinator.java | 58 +-
.../AgentKnowledgeCitationMatcher.java | 24 +
.../HeuristicKnowledgeCitationMatcher.java | 204 +++
.../AgentMemoryCompressionParameter.java | 24 +-
.../runtime/memory/AgentMemorySnapshot.java | 2 +-
.../message/AgentKnowledgeReference.java | 19 +
.../persistence/AgentRuntimeState.java | 86 -
.../persistence/AgentSessionStore.java | 70 -
.../AgentConversationRecorder.java | 6 +-
.../noop/NoopAgentConversationRecorder.java | 16 +
.../json/AgentSessionStateCodec.java | 62 -
.../json/AgentSessionStoreBackend.java | 69 -
.../json/FileAgentSessionStoreBackend.java | 172 --
.../json/JsonAgentSessionStore.java | 106 --
.../json/SerializedAgentRuntimeState.java | 107 --
.../memory/InMemoryAgentSessionStore.java | 59 -
.../noop/NoopAgentConversationRecorder.java | 16 -
.../session/AgentSessionStore.java | 79 +
.../AgentSessionStoreException.java | 2 +-
.../memory/InMemoryAgentSessionStore.java | 69 +
.../noop/NoopAgentSessionStore.java | 14 +-
.../skill/AgentSkillRuntimeContext.java | 20 +
.../agentscope/AgentScopeAdapterTest.java | 951 -----------
.../AgentScopeStatefulRuntimeTest.java | 1025 ++++++++++++
...HeuristicKnowledgeCitationMatcherTest.java | 96 ++
53 files changed, 5108 insertions(+), 2523 deletions(-)
create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentInitRequest.java
rename easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/{hitl/AgentToolApprovalResponse.java => AgentResumeRequest.java} (70%)
delete mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRunHandle.java
rename easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/{AgentRunRequest.java => AgentRuntimeExecutionContext.java} (79%)
create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeAutoContextCompressionModel.java
delete mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeEventHook.java
create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeMemoryBuildResult.java
create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeRuntimeHook.java
create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeEventBridge.java
create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeInterceptor.java
create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeObservationManager.java
create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeObserver.java
create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeTurnContext.java
create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeTurnContextHolder.java
create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/interceptor/AutoContextInterceptor.java
create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/interceptor/ToolHitlInterceptor.java
create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/observer/AgentRuntimeErrorObserver.java
create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/observer/ReasoningLifecycleObserver.java
create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/observer/SkillExecutionObserver.java
create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/observer/ToolExecutionObserver.java
create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/citation/AgentKnowledgeCitationMatcher.java
create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/citation/HeuristicKnowledgeCitationMatcher.java
delete mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentRuntimeState.java
delete mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentSessionStore.java
rename easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/{ => conversation}/AgentConversationRecorder.java (57%)
create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/conversation/noop/NoopAgentConversationRecorder.java
delete mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/AgentSessionStateCodec.java
delete mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/AgentSessionStoreBackend.java
delete mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/FileAgentSessionStoreBackend.java
delete mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/JsonAgentSessionStore.java
delete mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/SerializedAgentRuntimeState.java
delete mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/memory/InMemoryAgentSessionStore.java
delete mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/noop/NoopAgentConversationRecorder.java
create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/session/AgentSessionStore.java
rename easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/{ => session}/AgentSessionStoreException.java (91%)
create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/session/memory/InMemoryAgentSessionStore.java
rename easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/{ => session}/noop/NoopAgentSessionStore.java (54%)
delete mode 100644 easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/AgentScopeAdapterTest.java
create mode 100644 easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/AgentScopeStatefulRuntimeTest.java
create mode 100644 easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/knowledge/citation/HeuristicKnowledgeCitationMatcherTest.java
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentInitRequest.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentInitRequest.java
new file mode 100644
index 0000000..ac2c7aa
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentInitRequest.java
@@ -0,0 +1,203 @@
+package com.easyagents.agent.runtime;
+
+import com.easyagents.agent.runtime.knowledge.AgentKnowledgeRetriever;
+import com.easyagents.agent.runtime.persistence.conversation.AgentConversationRecorder;
+import com.easyagents.agent.runtime.persistence.conversation.noop.NoopAgentConversationRecorder;
+import com.easyagents.agent.runtime.persistence.session.AgentSessionStore;
+import com.easyagents.agent.runtime.persistence.session.noop.NoopAgentSessionStore;
+import com.easyagents.agent.runtime.tool.AgentToolInvoker;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 智能体初始化创建请求参数
+ */
+public class AgentInitRequest {
+
+ /**
+ * 会话ID,用于从会话存储中加载和保存当前智能体的上下文状态。
+ */
+ private String sessionId;
+
+ /**
+ * 智能体定义,包含模型、系统提示词、工具、知识库、记忆策略等静态配置。
+ */
+ private AgentDefinition agentDefinition;
+
+ /**
+ * 会话状态存储实现,可以实现此接口以管理 session
+ */
+ private AgentSessionStore sessionStore = NoopAgentSessionStore.INSTANCE;
+
+ /**
+ * 运行时上下文,传递租户、用户、链路等调用环境信息。
+ */
+ private AgentRuntimeContext runtimeContext = new AgentRuntimeContext();
+
+ /**
+ * 工具集合。
+ */
+ private Map toolInvokers = new LinkedHashMap<>();
+
+ /**
+ * 知识库集合,实现AgentKnowledgeRetriever接口以进行知识检索动作。
+ */
+ private Map knowledgeRetrievers = new LinkedHashMap<>();
+
+ /**
+ * 对话事件记录器,用于记录运行时事件流。
+ */
+ private AgentConversationRecorder conversationRecorder = NoopAgentConversationRecorder.INSTANCE;
+
+ /**
+ * 初始化元数据,用于传递业务侧扩展信息。
+ */
+ private Map metadata = new LinkedHashMap<>();
+
+ /**
+ * 获取会话ID。
+ *
+ * @return 会话ID
+ */
+ public String getSessionId() {
+ return sessionId;
+ }
+
+ /**
+ * 设置会话ID。
+ *
+ * @param sessionId 会话ID
+ */
+ public void setSessionId(String sessionId) {
+ this.sessionId = sessionId;
+ }
+
+ /**
+ * 获取智能体定义。
+ *
+ * @return 智能体定义
+ */
+ public AgentDefinition getAgentDefinition() {
+ return agentDefinition;
+ }
+
+ /**
+ * 设置智能体定义。
+ *
+ * @param agentDefinition 智能体定义
+ */
+ public void setAgentDefinition(AgentDefinition agentDefinition) {
+ this.agentDefinition = agentDefinition;
+ }
+
+ /**
+ * 获取会话存储。
+ *
+ * @return 会话存储
+ */
+ public AgentSessionStore getSessionStore() {
+ return sessionStore;
+ }
+
+ /**
+ * 设置会话存储。
+ *
+ * @param sessionStore 会话存储
+ */
+ public void setSessionStore(AgentSessionStore sessionStore) {
+ this.sessionStore = sessionStore == null ? NoopAgentSessionStore.INSTANCE : sessionStore;
+ }
+
+ /**
+ * 获取运行时上下文。
+ *
+ * @return 运行时上下文
+ */
+ public AgentRuntimeContext getRuntimeContext() {
+ return runtimeContext;
+ }
+
+ /**
+ * 设置运行时上下文。
+ *
+ * @param runtimeContext 运行时上下文
+ */
+ public void setRuntimeContext(AgentRuntimeContext runtimeContext) {
+ this.runtimeContext = runtimeContext == null ? new AgentRuntimeContext() : runtimeContext;
+ }
+
+ /**
+ * 获取工具调用器。
+ *
+ * @return 工具调用器
+ */
+ public Map getToolInvokers() {
+ return toolInvokers;
+ }
+
+ /**
+ * 设置工具调用器。
+ *
+ * @param toolInvokers 工具调用器
+ */
+ public void setToolInvokers(Map toolInvokers) {
+ this.toolInvokers = toolInvokers == null ? new LinkedHashMap<>() : toolInvokers;
+ }
+
+ /**
+ * 获取知识库检索器。
+ *
+ * @return 知识库检索器
+ */
+ public Map getKnowledgeRetrievers() {
+ return knowledgeRetrievers;
+ }
+
+ /**
+ * 设置知识库检索器。
+ *
+ * @param knowledgeRetrievers 知识库检索器
+ */
+ public void setKnowledgeRetrievers(Map knowledgeRetrievers) {
+ this.knowledgeRetrievers = knowledgeRetrievers == null ? new LinkedHashMap<>() : knowledgeRetrievers;
+ }
+
+ /**
+ * 获取会话记录器。
+ *
+ * @return 会话记录器
+ */
+ public AgentConversationRecorder getConversationRecorder() {
+ return conversationRecorder;
+ }
+
+ /**
+ * 设置会话记录器。
+ *
+ * @param conversationRecorder 会话记录器
+ */
+ public void setConversationRecorder(AgentConversationRecorder conversationRecorder) {
+ this.conversationRecorder = conversationRecorder == null
+ ? NoopAgentConversationRecorder.INSTANCE
+ : conversationRecorder;
+ }
+
+ /**
+ * 获取元数据。
+ *
+ * @return 元数据
+ */
+ public Map getMetadata() {
+ return metadata;
+ }
+
+ /**
+ * 设置元数据。
+ *
+ * @param metadata 元数据
+ */
+ public void setMetadata(Map metadata) {
+ this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentToolApprovalResponse.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentResumeRequest.java
similarity index 70%
rename from easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentToolApprovalResponse.java
rename to easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentResumeRequest.java
index fadf97d..0bd311f 100644
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentToolApprovalResponse.java
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentResumeRequest.java
@@ -1,16 +1,33 @@
-package com.easyagents.agent.runtime.hitl;
+package com.easyagents.agent.runtime;
+
+import com.easyagents.agent.runtime.hitl.AgentResumeToken;
import java.util.LinkedHashMap;
import java.util.Map;
/**
- * 工具执行审批响应。
+ * 智能体挂起运行恢复请求。
*/
-public class AgentToolApprovalResponse {
+public class AgentResumeRequest {
+ /**
+ * 挂起运行的恢复令牌。
+ */
private AgentResumeToken resumeToken;
+
+ /**
+ * 是否批准继续执行。
+ */
private boolean approved;
+
+ /**
+ * 拒绝继续执行时的原因。
+ */
private String rejectReason;
+
+ /**
+ * 调用方传入的恢复元数据。
+ */
private Map metadata = new LinkedHashMap<>();
/**
@@ -32,7 +49,7 @@ public class AgentToolApprovalResponse {
}
/**
- * 返回是否批准执行工具。
+ * 返回是否批准继续执行。
*
* @return 批准时为 true
*/
@@ -41,7 +58,7 @@ public class AgentToolApprovalResponse {
}
/**
- * 设置是否批准执行工具。
+ * 设置是否批准继续执行。
*
* @param approved 批准标记
*/
@@ -68,18 +85,18 @@ public class AgentToolApprovalResponse {
}
/**
- * 获取审批元数据。
+ * 获取恢复元数据。
*
- * @return 审批元数据
+ * @return 恢复元数据
*/
public Map getMetadata() {
return metadata;
}
/**
- * 设置审批元数据。
+ * 设置恢复元数据。
*
- * @param metadata 审批元数据
+ * @param metadata 恢复元数据
*/
public void setMetadata(Map metadata) {
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRunHandle.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRunHandle.java
deleted file mode 100644
index b10903e..0000000
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRunHandle.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.easyagents.agent.runtime;
-
-import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
-import com.easyagents.agent.runtime.hitl.AgentToolApprovalResponse;
-import reactor.core.publisher.Flux;
-
-/**
- * 可取消的单次智能体运行句柄。
- */
-public interface AgentRunHandle {
-
- /**
- * 获取本次运行的事件流。
- *
- * @return 事件流
- */
- Flux stream();
-
- /**
- * 取消本次运行。
- */
- void cancel();
-
- /**
- * 提交工具审批结果。
- *
- * @param response 审批响应
- */
- void submitToolApproval(AgentToolApprovalResponse response);
-}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRuntime.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRuntime.java
index d0497ab..22c4d7b 100644
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRuntime.java
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRuntime.java
@@ -1,26 +1,34 @@
package com.easyagents.agent.runtime;
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
+import com.easyagents.agent.runtime.message.AgentMessage;
import reactor.core.publisher.Flux;
/**
- * 执行声明式 ReAct 智能体并流式输出运行事件。
+ * 有状态智能体运行器。
*/
public interface AgentRuntime {
/**
- * 启动单次智能体运行并返回可取消句柄。
+ * 初始化智能体运行器。
*
- * @param request 包含定义、输入、记忆和适配器的运行请求
- * @return 本次运行句柄
+ * @param request 初始化请求
*/
- AgentRunHandle start(AgentRunRequest request);
+ void init(AgentInitRequest request);
/**
- * 启动单次智能体运行。
+ * 发送用户消息并流式输出运行事件。
*
- * @param request 包含定义、输入、记忆和适配器的运行请求
- * @return 本次运行的事件流
+ * @param userMessage 用户消息
+ * @return 运行事件流
*/
- Flux stream(AgentRunRequest request);
+ Flux stream(AgentMessage userMessage);
+
+ /**
+ * 恢复一次已挂起的运行。
+ *
+ * @param request 恢复请求
+ * @return 运行事件流
+ */
+ Flux resume(AgentResumeRequest request);
}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRunRequest.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRuntimeExecutionContext.java
similarity index 79%
rename from easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRunRequest.java
rename to easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRuntimeExecutionContext.java
index f0ed6cb..eeaead7 100644
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRunRequest.java
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRuntimeExecutionContext.java
@@ -3,32 +3,83 @@ package com.easyagents.agent.runtime;
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeRetriever;
import com.easyagents.agent.runtime.memory.AgentMemorySnapshot;
import com.easyagents.agent.runtime.message.AgentMessage;
-import com.easyagents.agent.runtime.persistence.AgentConversationRecorder;
-import com.easyagents.agent.runtime.persistence.AgentSessionStore;
-import com.easyagents.agent.runtime.persistence.noop.NoopAgentConversationRecorder;
-import com.easyagents.agent.runtime.persistence.noop.NoopAgentSessionStore;
+import com.easyagents.agent.runtime.persistence.conversation.AgentConversationRecorder;
+import com.easyagents.agent.runtime.persistence.conversation.noop.NoopAgentConversationRecorder;
+import com.easyagents.agent.runtime.persistence.session.AgentSessionStore;
+import com.easyagents.agent.runtime.persistence.session.noop.NoopAgentSessionStore;
import com.easyagents.agent.runtime.tool.AgentToolInvoker;
import java.util.LinkedHashMap;
import java.util.Map;
/**
- * 一次智能体运行请求。
+ * 单轮智能体运行的内部执行上下文。
*/
-public class AgentRunRequest {
+public class AgentRuntimeExecutionContext {
+ /**
+ * 本轮运行请求ID。
+ */
private String requestId;
+
+ /**
+ * 本轮运行链路ID。
+ */
private String traceId;
+
+ /**
+ * 当前会话ID。
+ */
private String sessionId;
+
+ /**
+ * 智能体定义。
+ */
private AgentDefinition agentDefinition;
+
+ /**
+ * 业务运行时上下文。
+ */
private AgentRuntimeContext runtimeContext = new AgentRuntimeContext();
+
+ /**
+ * 本轮用户消息。
+ */
private AgentMessage userMessage;
+
+ /**
+ * 本轮初始记忆快照。
+ */
private AgentMemorySnapshot memorySnapshot = new AgentMemorySnapshot();
+
+ /**
+ * 按工具名称索引的工具调用器。
+ */
private Map toolInvokers = new LinkedHashMap<>();
+
+ /**
+ * 按知识库ID索引的检索器。
+ */
private Map knowledgeRetrievers = new LinkedHashMap<>();
+
+ /**
+ * 会话状态存储。
+ */
private AgentSessionStore sessionStore = NoopAgentSessionStore.INSTANCE;
+
+ /**
+ * 对话事件记录器。
+ */
private AgentConversationRecorder conversationRecorder = NoopAgentConversationRecorder.INSTANCE;
+
+ /**
+ * 运行元数据。
+ */
private Map metadata = new LinkedHashMap<>();
+
+ /**
+ * 运行取消原因。
+ */
private String cancelReason;
/**
@@ -158,7 +209,7 @@ public class AgentRunRequest {
}
/**
- * 获取tool invokers by tool name。
+ * 获取工具调用器。
*
* @return 工具调用器
*/
@@ -176,7 +227,7 @@ public class AgentRunRequest {
}
/**
- * 获取knowledge retrievers by knowledge id。
+ * 获取知识库检索器。
*
* @return 知识库检索器
*/
@@ -212,36 +263,38 @@ public class AgentRunRequest {
}
/**
- * 获取会话记录器。
+ * 获取对话记录器。
*
- * @return 会话记录器
+ * @return 对话记录器
*/
public AgentConversationRecorder getConversationRecorder() {
return conversationRecorder;
}
/**
- * 设置会话记录器。
+ * 设置对话记录器。
*
- * @param conversationRecorder 会话记录器
+ * @param conversationRecorder 对话记录器
*/
public void setConversationRecorder(AgentConversationRecorder conversationRecorder) {
- this.conversationRecorder = conversationRecorder == null ? NoopAgentConversationRecorder.INSTANCE : conversationRecorder;
+ this.conversationRecorder = conversationRecorder == null
+ ? NoopAgentConversationRecorder.INSTANCE
+ : conversationRecorder;
}
/**
- * 获取元数据。
+ * 获取运行元数据。
*
- * @return 元数据
+ * @return 运行元数据
*/
public Map getMetadata() {
return metadata;
}
/**
- * 设置元数据。
+ * 设置运行元数据。
*
- * @param metadata 元数据
+ * @param metadata 运行元数据
*/
public void setMetadata(Map metadata) {
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeAutoContextCompressionModel.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeAutoContextCompressionModel.java
new file mode 100644
index 0000000..1f1fd2d
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeAutoContextCompressionModel.java
@@ -0,0 +1,157 @@
+package com.easyagents.agent.runtime.agentscope;
+
+import io.agentscope.core.message.*;
+import io.agentscope.core.model.ChatResponse;
+import io.agentscope.core.model.GenerateOptions;
+import io.agentscope.core.model.Model;
+import io.agentscope.core.model.ToolSchema;
+import reactor.core.publisher.Flux;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 面向 AutoContext 摘要调用的模型包装器。
+ */
+class AgentScopeAutoContextCompressionModel implements Model {
+
+ private final Model delegate;
+
+ /**
+ * 创建 AutoContext 摘要模型包装器。
+ *
+ * @param delegate 实际调用的模型
+ */
+ AgentScopeAutoContextCompressionModel(Model delegate) {
+ this.delegate = delegate;
+ }
+
+ /**
+ * 对 AutoContext 压缩摘要输入做协议安全化后转发给实际模型。
+ *
+ * @param messages AgentScope 消息
+ * @param tools 工具声明
+ * @param options 生成参数
+ * @return 模型响应流
+ */
+ @Override
+ public Flux stream(List messages, List tools, GenerateOptions options) {
+ return delegate.stream(sanitizeCompressionMessages(messages), tools, options);
+ }
+
+ /**
+ * 获取模型名称。
+ *
+ * @return 模型名称
+ */
+ @Override
+ public String getModelName() {
+ return delegate.getModelName();
+ }
+
+ /**
+ * 将 AutoContext 摘要材料中的工具协议消息转换为普通文本消息。
+ *
+ * @param messages 原始摘要输入
+ * @return 协议安全的摘要输入
+ */
+ List sanitizeCompressionMessages(List messages) {
+ if (messages == null || messages.isEmpty()) {
+ return messages;
+ }
+ List sanitized = new ArrayList<>(messages.size());
+ for (Msg message : messages) {
+ if (message == null) {
+ continue;
+ }
+ if (requiresPlainText(message)) {
+ sanitized.add(toPlainTextUserMessage(message));
+ } else {
+ sanitized.add(message);
+ }
+ }
+ return sanitized;
+ }
+
+ private boolean requiresPlainText(Msg message) {
+ return message.getRole() == MsgRole.TOOL
+ || message.hasContentBlocks(ToolUseBlock.class)
+ || message.hasContentBlocks(ToolResultBlock.class);
+ }
+
+ private Msg toPlainTextUserMessage(Msg message) {
+ return Msg.builder()
+ .id(message.getId())
+ .role(MsgRole.USER)
+ .name("context")
+ .content(TextBlock.builder().text(renderMessage(message)).build())
+ .metadata(message.getMetadata())
+ .timestamp(message.getTimestamp())
+ .build();
+ }
+
+ private String renderMessage(Msg message) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("Context message role=").append(message.getRole());
+ if (message.getName() != null && !message.getName().isBlank()) {
+ builder.append(", name=").append(message.getName());
+ }
+ builder.append('\n');
+ for (ContentBlock block : message.getContent()) {
+ appendBlock(builder, block);
+ }
+ return builder.toString().trim();
+ }
+
+ private void appendBlock(StringBuilder builder, ContentBlock block) {
+ if (block instanceof TextBlock textBlock) {
+ appendSection(builder, "text", textBlock.getText());
+ return;
+ }
+ if (block instanceof ThinkingBlock thinkingBlock) {
+ appendSection(builder, "thinking", thinkingBlock.getThinking());
+ return;
+ }
+ if (block instanceof ToolUseBlock toolUseBlock) {
+ appendSection(builder, "tool_use.id", toolUseBlock.getId());
+ appendSection(builder, "tool_use.name", toolUseBlock.getName());
+ appendSection(builder, "tool_use.input", renderMap(toolUseBlock.getInput()));
+ return;
+ }
+ if (block instanceof ToolResultBlock toolResultBlock) {
+ appendSection(builder, "tool_result.id", toolResultBlock.getId());
+ appendSection(builder, "tool_result.name", toolResultBlock.getName());
+ appendSection(builder, "tool_result.output", renderBlocks(toolResultBlock.getOutput()));
+ return;
+ }
+ appendSection(builder, "content", String.valueOf(block));
+ }
+
+ private String renderBlocks(List blocks) {
+ if (blocks == null || blocks.isEmpty()) {
+ return "";
+ }
+ StringBuilder builder = new StringBuilder();
+ for (ContentBlock block : blocks) {
+ if (block instanceof TextBlock textBlock) {
+ builder.append(textBlock.getText());
+ } else {
+ builder.append(String.valueOf(block));
+ }
+ builder.append('\n');
+ }
+ return builder.toString().trim();
+ }
+
+ private String renderMap(Map map) {
+ return map == null ? "{}" : map.toString();
+ }
+
+ private void appendSection(StringBuilder builder, String label, String value) {
+ if (value == null || value.isBlank()) {
+ return;
+ }
+ builder.append(label).append(": ").append(value).append('\n');
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeEventHook.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeEventHook.java
deleted file mode 100644
index 4b77656..0000000
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeEventHook.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.easyagents.agent.runtime.agentscope;
-
-import com.easyagents.agent.runtime.AgentRunRequest;
-import io.agentscope.core.hook.Hook;
-import io.agentscope.core.hook.HookEvent;
-import reactor.core.publisher.Mono;
-
-/**
- * 从 AgentScope Hook 回调发射不在主事件流中的运行时事件。
- */
-public class AgentScopeEventHook implements Hook {
-
- private final AgentRunRequest request;
-
- /**
- * 创建 Hook。
- *
- * @param request 运行请求
- */
- public AgentScopeEventHook(AgentRunRequest request) {
- this.request = request;
- }
-
- @Override
- public Mono onEvent(T event) {
- return Mono.just(event);
- }
-}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeKnowledgeAdapter.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeKnowledgeAdapter.java
index 97b2720..59e5c99 100644
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeKnowledgeAdapter.java
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeKnowledgeAdapter.java
@@ -1,9 +1,8 @@
package com.easyagents.agent.runtime.agentscope;
-import com.easyagents.agent.runtime.AgentRunRequest;
import com.easyagents.agent.runtime.AgentRuntimeException;
-import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
-import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
+import com.easyagents.agent.runtime.AgentRuntimeExecutionContext;
+import com.easyagents.agent.runtime.event.*;
import com.easyagents.agent.runtime.knowledge.*;
import io.agentscope.core.message.TextBlock;
import io.agentscope.core.rag.Knowledge;
@@ -26,8 +25,8 @@ public class AgentScopeKnowledgeAdapter {
* @param request 运行请求
* @return 聚合 Knowledge;未配置知识库时返回 null
*/
- public Knowledge createAggregateKnowledge(AgentRunRequest request) {
- return createAggregateKnowledge(request, null);
+ public Knowledge createAggregateKnowledge(AgentRuntimeExecutionContext request) {
+ return createAggregateKnowledge(request, (Sinks.Many) null);
}
/**
@@ -37,11 +36,31 @@ public class AgentScopeKnowledgeAdapter {
* @param eventSink 事件 sink
* @return 聚合 Knowledge;未配置知识库时返回 null
*/
- public Knowledge createAggregateKnowledge(AgentRunRequest request, Sinks.Many eventSink) {
+ public Knowledge createAggregateKnowledge(AgentRuntimeExecutionContext request, Sinks.Many eventSink) {
+ return createAggregateKnowledge(request, fixedHolder(request, eventSink));
+ }
+
+ /**
+ * 创建可读取当前运行轮次事件出口的聚合 Knowledge。
+ *
+ * @param request 运行时级上下文
+ * @param turnContextHolder 当前运行轮次上下文持有器
+ * @return 聚合 Knowledge;未配置知识库时返回 null
+ */
+ public Knowledge createAggregateKnowledge(AgentRuntimeExecutionContext request,
+ AgentRuntimeTurnContextHolder turnContextHolder) {
if (request.getAgentDefinition().getKnowledgeSpecs().isEmpty()) {
return null;
}
- return new AggregateKnowledge(request, eventSink);
+ return new AggregateKnowledge(request, turnContextHolder);
+ }
+
+ private AgentRuntimeTurnContextHolder fixedHolder(AgentRuntimeExecutionContext request,
+ Sinks.Many eventSink) {
+ AgentRuntimeTurnContextHolder holder = new AgentRuntimeTurnContextHolder();
+ AgentRuntimeEventBridge bridge = new AgentRuntimeEventBridge(request, holder);
+ holder.set(new AgentRuntimeTurnContext(null, eventSink, bridge));
+ return holder;
}
/**
@@ -71,9 +90,9 @@ public class AgentScopeKnowledgeAdapter {
payload.put("documentMetadata", document.getMetadata());
payload.putAll(document.getMetadata());
DocumentMetadata metadata = DocumentMetadata.builder()
- .content(TextBlock.builder().text(document.getContent()).build())
- .docId(document.getDocumentId())
- .chunkId(document.getChunkId())
+ .content(TextBlock.builder().text(safeContent(document)).build())
+ .docId(safeDocumentId(document))
+ .chunkId(safeChunkId(document))
.payload(payload)
.build();
Document converted = new Document(metadata);
@@ -81,17 +100,59 @@ public class AgentScopeKnowledgeAdapter {
return converted;
}
+ /**
+ * 获取 AgentScope 要求的非空文档 ID。
+ *
+ * @param document 知识文档
+ * @return 非空文档 ID
+ */
+ private String safeDocumentId(AgentKnowledgeDocument document) {
+ if (document.getDocumentId() != null && !document.getDocumentId().isBlank()) {
+ return document.getDocumentId();
+ }
+ if (document.getChunkId() != null && !document.getChunkId().isBlank()) {
+ return document.getChunkId();
+ }
+ return "knowledge-document";
+ }
+
+ /**
+ * 获取 AgentScope 要求的非空分片 ID。
+ *
+ * @param document 知识文档
+ * @return 非空分片 ID
+ */
+ private String safeChunkId(AgentKnowledgeDocument document) {
+ if (document.getChunkId() != null && !document.getChunkId().isBlank()) {
+ return document.getChunkId();
+ }
+ if (document.getDocumentId() != null && !document.getDocumentId().isBlank()) {
+ return document.getDocumentId();
+ }
+ return "0";
+ }
+
+ /**
+ * 获取 AgentScope 要求的非空文档内容。
+ *
+ * @param document 知识文档
+ * @return 文档内容
+ */
+ private String safeContent(AgentKnowledgeDocument document) {
+ return document.getContent() == null ? "" : document.getContent();
+ }
+
/**
* 将检索调用分发到多个知识源的聚合 Knowledge 实现。
*/
private class AggregateKnowledge implements Knowledge {
- private final AgentRunRequest request;
- private final Sinks.Many eventSink;
+ private final AgentRuntimeExecutionContext request;
+ private final AgentRuntimeTurnContextHolder turnContextHolder;
- private AggregateKnowledge(AgentRunRequest request, Sinks.Many eventSink) {
+ private AggregateKnowledge(AgentRuntimeExecutionContext request, AgentRuntimeTurnContextHolder turnContextHolder) {
this.request = request;
- this.eventSink = eventSink;
+ this.turnContextHolder = turnContextHolder;
}
/**
@@ -139,9 +200,10 @@ public class AgentScopeKnowledgeAdapter {
retrievalRequest.setLimit(spec.getLimit());
retrievalRequest.setScoreThreshold(Math.max(spec.getScoreThreshold(), globalThreshold));
retrievalRequest.setKnowledgeSpec(spec);
- retrievalRequest.setRuntimeContext(request.getRuntimeContext());
- retrievalRequest.getMetadata().put("traceId", request.getTraceId());
- retrievalRequest.getMetadata().put("sessionId", request.getSessionId());
+ AgentRuntimeExecutionContext currentRequest = currentRequest();
+ retrievalRequest.setRuntimeContext(currentRequest.getRuntimeContext());
+ retrievalRequest.getMetadata().put("traceId", currentRequest.getTraceId());
+ retrievalRequest.getMetadata().put("sessionId", currentRequest.getSessionId());
AgentKnowledgeRetrievalResult result = retriever.retrieve(retrievalRequest);
if (result == null || result.getDocuments() == null) {
emitKnowledgeRetrievalEvent(query, spec, retrievalRequest, new ArrayList<>());
@@ -181,7 +243,11 @@ public class AgentScopeKnowledgeAdapter {
}
/**
- * 发射知识库检索事件,供聊天界面展示检索过程。
+ * 发射知识库检索旁路事件,供聊天界面展示检索过程。
+ *
+ * 知识库检索本身属于 AgentScope RAG 主线路,返回的 Document 会继续进入
+ * AgentScope 的上下文注入流程;这里发出的 {@code KNOWLEDGE_RETRIEVAL}
+ * 只是旁路告知调用方,不会回写 memory,也不会参与模型消息序列。
*
* @param query 查询
* @param spec 知识库声明
@@ -192,32 +258,35 @@ public class AgentScopeKnowledgeAdapter {
AgentKnowledgeSpec spec,
AgentKnowledgeRetrievalRequest retrievalRequest,
List documents) {
- if (eventSink == null) {
- return;
- }
- AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.KNOWLEDGE_RETRIEVAL);
- event.setTraceId(request.getTraceId());
- event.setSessionId(request.getSessionId());
- event.setAgentId(request.getAgentDefinition().getAgentId());
- event.getMetadata().put("requestId", request.getRequestId());
+ AgentRuntimeEvent event = currentEventBridge().event(AgentRuntimeEventType.KNOWLEDGE_RETRIEVAL);
event.getPayload().put("query", query);
event.getPayload().put("knowledgeId", spec.getKnowledgeId());
event.getPayload().put("knowledgeName", spec.getName());
+ event.getPayload().put("knowledgeType", spec.getMetadata().get("knowledgeType"));
+ event.getPayload().put("faqCollection", spec.getMetadata().get("faqCollection"));
event.getPayload().put("limit", retrievalRequest.getLimit());
event.getPayload().put("scoreThreshold", retrievalRequest.getScoreThreshold());
event.getPayload().put("documentCount", documents == null ? 0 : documents.size());
event.getPayload().put("documents", documentSummaries(documents));
- Sinks.EmitResult result = eventSink.tryEmitNext(event);
- if (result.isFailure()) {
- throw new AgentRuntimeException("Failed to emit knowledge retrieval event: " + result);
+ currentEventBridge().emit(event);
+ }
+
+ private AgentRuntimeExecutionContext currentRequest() {
+ return turnContextHolder == null ? request : turnContextHolder.executionContext(request);
+ }
+
+ private AgentRuntimeEventBridge currentEventBridge() {
+ if (turnContextHolder != null && turnContextHolder.eventBridge().isPresent()) {
+ return turnContextHolder.eventBridge().get();
}
+ return new AgentRuntimeEventBridge(request, turnContextHolder);
}
/**
- * 构建用于事件展示的文档摘要,避免把全文内容放入事件。
+ * 构建用于事件展示的命中片段,保留前端引注需要的原始 chunk 内容。
*
* @param documents 检索文档
- * @return 文档摘要列表
+ * @return 命中片段列表
*/
private List
+ * @param userMessage 用户消息
+ * @return 运行事件流
+ */
+ @Override
+ public Flux stream(AgentMessage userMessage) {
+ validateStreamRequest(userMessage);
+ return Flux.defer(() -> runAgentStream(createTurnExecutionContext(userMessage), () -> buildInput(userMessage)));
+ }
+
+ /**
+ * 恢复一次已挂起的运行。
+ *
+ * @param request 恢复请求
+ * @return 运行事件流
+ */
+ @Override
+ public Flux resume(AgentResumeRequest request) {
+ validateResumeRequest(request);
return Flux.defer(() -> {
- try {
- return start(request).stream();
- } catch (Throwable error) {
- return Flux.just(failed(request, error));
+ if (!running.compareAndSet(false, true)) {
+ return Flux.error(new AgentRuntimeException("Agent runtime is already streaming."));
}
+ AgentRuntimeExecutionContext executionContext = createResumeExecutionContext(request);
+ try {
+ approvalCoordinator.consume(request);
+ } catch (RuntimeException error) {
+ running.set(false);
+ throw error;
+ }
+ // 审批拒绝
+ if (!request.isApproved()) {
+ executionContext.setCancelReason(request.getRejectReason());
+ return Flux.defer(() -> {
+ saveSession();
+ return Flux.just(started(executionContext), cancelled(executionContext));
+ }).doOnNext(event -> executionContext.getConversationRecorder().record(executionContext, event))
+ .doFinally(signalType -> cleanupTurn());
+ }
+ return runAgentStreamAfterLock(executionContext, List::of);
});
}
- @Override
- public AgentRunHandle start(AgentRunRequest request) {
- validate(request);
- AgentToolApprovalCoordinator approvalCoordinator = AgentToolApprovalCoordinator.enabled();
+ /**
+ * 执行一轮 AgentScope 主线路并合并旁路事件。
+ *
+ * @param executionContext 本轮执行上下文
+ * @param inputSupplier AgentScope 主线路输入供应器
+ * @return 运行事件流
+ */
+ private Flux runAgentStream(AgentRuntimeExecutionContext executionContext,
+ Supplier> inputSupplier) {
+ return Flux.defer(() -> {
+ // 单个实例禁止并发 stream/resume,避免两轮对话同时读写 memory、session 和 turn context。
+ if (!running.compareAndSet(false, true)) {
+ return Flux.error(new AgentRuntimeException("Agent runtime is already streaming."));
+ }
+ return runAgentStreamAfterLock(executionContext, inputSupplier);
+ });
+ }
+
+ private Flux runAgentStreamAfterLock(AgentRuntimeExecutionContext executionContext,
+ Supplier> inputSupplier) {
+ // 旁线路事件监听。Tool、Knowledge、AutoContext、Skill observer 都通过 bridge 写入这里。
Sinks.Many sideEvents = Sinks.many().unicast().onBackpressureBuffer();
- AgentSkillRuntimeContext skillContext = AgentSkillRuntimeContext.from(request.getAgentDefinition().getSkillBoxSpec());
+ AgentRuntimeEventBridge eventBridge = new AgentRuntimeEventBridge(runtimeContext, turnContextHolder);
+ // 将本轮上下文挂到 holder 后,adapter / observer 在 AgentScope 回调线程里也能拿到当前 requestId。
+ turnContextHolder.set(new AgentRuntimeTurnContext(executionContext, sideEvents, eventBridge));
+ // agent 输出的完整纯文本消息,用于知识库引注等。
StringBuilder finalText = new StringBuilder();
+ // agent 输出的完整结构化消息。
AtomicReference finalMessage = new AtomicReference<>();
+ // HITL 暂停事件。被设置后,本轮以 SUSPENDED 挂起而不是 COMPLETED 结束。
+ AtomicReference suspendedEvent = new AtomicReference<>();
+ // 本轮 HITL 待审批项来自旁路交互事件,最终会合并进 SUSPENDED 挂起事件。
+ List> pendingApprovals = new CopyOnWriteArrayList<>();
+ // 知识库引注。
Map knowledgeReferences = new LinkedHashMap<>();
+ // 流式输出归一化,防止出现累计快照的重复输出。
+ StreamDeltaNormalizer deltaNormalizer = new StreamDeltaNormalizer();
+ // 取消输出标记。
AtomicBoolean cancelled = new AtomicBoolean(false);
- ReActAgent agent = buildAgent(request, sideEvents, approvalCoordinator, skillContext);
- loadSessionIfNeeded(request, agent);
- List input = buildInput(request);
- StreamOptions streamOptions = StreamOptions.builder()
+ // 旁线路监察事件流式输出。
+ Flux sideEventFlux = sideEvents.asFlux()
+ .doOnNext(event -> updateKnowledgeReferences(knowledgeReferences, event))
+ .doOnNext(event -> updatePendingApprovals(pendingApprovals, event));
+ // 主线路 agent 交互。resume 场景会传入空列表,让 AgentScope 从 pending tool 继续执行。
+ Flux mainEventFlux = agent.stream(inputSupplier.get(), streamOptions())
+ .timeout(executionContext.getAgentDefinition().getExecutionOptions().getTimeout())
+ // 保存输出完毕的完整消息
+ .doOnNext(event -> captureFinalAgentResult(finalText, finalMessage, event))
+ // 映射主线路事件:推理增量、文本增量和 HITL 暂停信号,并执行消息归一化。
+ .flatMapIterable(event -> mapStreamEvent(executionContext, event))
+ .map(deltaNormalizer::normalize)
+ .doOnNext(event -> updateSuspendedEvent(suspendedEvent, event))
+ .doOnNext(event -> updateFinalText(finalText, event))
+ .doOnNext(event -> updateFinalMessage(finalMessage, event))
+ // 输出完毕后持久化 memory/session/toolkit 等状态。
+ .doOnComplete(this::saveSession)
+ // 主线路结束后关闭旁路监听事件,让 merge 流可以自然完成。
+ .doFinally(signalType -> sideEvents.tryEmitComplete())
+ // 本次输出完毕。
+ .concatWith(Flux.defer(() -> {
+ AgentRuntimeEvent suspended = suspendedEvent.get();
+ if (suspended != null) {
+ // 触发 hitl 审批事件,暂时挂起。
+ suspended.getPayload().put("pendingApprovals", pendingApprovals);
+ return Flux.just(suspended);
+ }
+ return Flux.just(completed(executionContext, finalText.toString(),
+ finalMessage.get(), knowledgeReferences));
+ }));
+ // STARTED 先发出;随后主线路和旁线路并行输出。
+ return Flux.concat(Flux.just(started(executionContext)), Flux.merge(sideEventFlux, mainEventFlux))
+ .onErrorResume(error -> errorEventFlux(executionContext, error))
+ // 输出所有事件统一交给调用方存储事件记录,默认是空实现即不记录。
+ .doOnNext(event -> executionContext.getConversationRecorder().record(executionContext, event))
+ // 处理中断请求
+ .doOnCancel(() -> cancelInternal(executionContext, sideEvents, cancelled))
+ // 释放运行锁并清掉 turn context。
+ .doFinally(signalType -> cleanupTurn());
+ }
+
+ /**
+ * 校验流式运行的必要条件。
+ *
+ * @param userMessage 用户消息
+ */
+ private void validateStreamRequest(AgentMessage userMessage) {
+ if (!initialized.get() || agent == null) {
+ throw new AgentRuntimeException("Agent runtime has not been initialized.");
+ }
+ if (userMessage == null) {
+ throw new AgentRuntimeException("Agent user message is required.");
+ }
+ if (userMessage.getContentBlocks() == null || userMessage.getContentBlocks().isEmpty()) {
+ throw new AgentRuntimeException("Agent user message content is required.");
+ }
+ }
+
+ /**
+ * 校验恢复请求的必要条件。
+ *
+ * @param request 恢复请求
+ */
+ private void validateResumeRequest(AgentResumeRequest request) {
+ if (!initialized.get() || agent == null) {
+ throw new AgentRuntimeException("Agent runtime has not been initialized.");
+ }
+ if (request == null) {
+ throw new AgentRuntimeException("Agent resume request is required.");
+ }
+ if (request.getResumeToken() == null
+ || request.getResumeToken().getValue() == null
+ || request.getResumeToken().getValue().isBlank()) {
+ throw new AgentRuntimeException("Agent resume token is required.");
+ }
+ }
+
+ /**
+ * 创建单轮运行上下文。
+ *
+ * @param userMessage 用户消息
+ * @return 单轮运行上下文
+ */
+ private AgentRuntimeExecutionContext createTurnExecutionContext(AgentMessage userMessage) {
+ AgentRuntimeExecutionContext context = new AgentRuntimeExecutionContext();
+ context.setRequestId(UUID.randomUUID().toString());
+ context.setTraceId(runtimeContext.getTraceId());
+ context.setSessionId(runtimeContext.getSessionId());
+ context.setAgentDefinition(runtimeContext.getAgentDefinition());
+ context.setRuntimeContext(runtimeContext.getRuntimeContext());
+ context.setUserMessage(userMessage);
+ context.setToolInvokers(runtimeContext.getToolInvokers());
+ context.setKnowledgeRetrievers(runtimeContext.getKnowledgeRetrievers());
+ context.setSessionStore(runtimeContext.getSessionStore());
+ context.setConversationRecorder(runtimeContext.getConversationRecorder());
+ context.setMetadata(runtimeContext.getMetadata());
+ return context;
+ }
+
+ /**
+ * 创建恢复运行上下文。
+ *
+ * @param request 恢复请求
+ * @return 恢复运行上下文
+ */
+ private AgentRuntimeExecutionContext createResumeExecutionContext(AgentResumeRequest request) {
+ AgentRuntimeExecutionContext context = new AgentRuntimeExecutionContext();
+ context.setRequestId(UUID.randomUUID().toString());
+ context.setTraceId(runtimeContext.getTraceId());
+ context.setSessionId(runtimeContext.getSessionId());
+ context.setAgentDefinition(runtimeContext.getAgentDefinition());
+ context.setRuntimeContext(runtimeContext.getRuntimeContext());
+ context.setToolInvokers(runtimeContext.getToolInvokers());
+ context.setKnowledgeRetrievers(runtimeContext.getKnowledgeRetrievers());
+ context.setSessionStore(runtimeContext.getSessionStore());
+ context.setConversationRecorder(runtimeContext.getConversationRecorder());
+ Map metadata = new LinkedHashMap<>(runtimeContext.getMetadata());
+ metadata.putAll(request.getMetadata());
+ metadata.put("resume", true);
+ metadata.put("resumeToken", request.getResumeToken().getValue());
+ context.setMetadata(metadata);
+ return context;
+ }
+
+ /**
+ * 构建传入 AgentScope 主线路的用户输入。
+ *
+ * 这里只传当前用户消息。历史上下文、AutoContext 压缩和 session 状态均由
+ * AgentScope memory/session 主线路负责管理。
+ *
+ * @param userMessage 用户消息
+ * @return AgentScope 输入消息
+ */
+ private List buildInput(AgentMessage userMessage) {
+ Msg message = messageAdapter.toMsg(userMessage);
+ return message == null ? List.of() : List.of(message);
+ }
+
+ /**
+ * 构建 AgentScope 流式选项。
+ *
+ * @return 流式选项
+ */
+ private StreamOptions streamOptions() {
+ return StreamOptions.builder()
.eventTypes(EventType.ALL)
.incremental(true)
- .includeReasoningChunk(request.getAgentDefinition().getExecutionOptions().isReasoningEnabled())
- .includeReasoningResult(true)
+ .includeReasoningChunk(runtimeContext.getAgentDefinition().getExecutionOptions().isReasoningEnabled())
+ .includeReasoningResult(false)
.includeActingChunk(true)
.includeSummaryChunk(true)
.includeSummaryResult(true)
.build();
- Flux mappedAgentEvents = agent.stream(input, streamOptions)
- .timeout(request.getAgentDefinition().getExecutionOptions().getTimeout())
- .flatMapIterable(event -> mapEvent(request, event, skillContext))
- .doOnNext(event -> updateFinalText(finalText, event))
- .doOnNext(event -> updateFinalMessage(finalMessage, event))
- .doOnNext(event -> updateKnowledgeReferences(knowledgeReferences, event))
- .doOnComplete(() -> saveSessionIfNeeded(request, agent))
- .doFinally(signalType -> sideEvents.tryEmitComplete())
- .concatWith(Flux.defer(() -> Flux.just(completed(request, finalText.toString(), finalMessage.get(), knowledgeReferences))));
- Flux stream = Flux.concat(Flux.just(started(request)), Flux.merge(sideEvents.asFlux(), mappedAgentEvents))
- .doOnNext(event -> request.getConversationRecorder().record(request, event))
- .onErrorResume(error -> {
- if (error instanceof AgentToolApprovalRejectedException approvalRejectedException) {
- // 工具执行被拒绝
- request.setCancelReason(approvalRejectedException.getRejectReason());
- saveSessionIfNeeded(request, agent);
- if (cancelled.compareAndSet(false, true)) {
- return Flux.just(cancelled(request))
- .doOnNext(event -> request.getConversationRecorder().record(request, event));
- }
- return Flux.empty();
- }
- return Flux.just(failed(request, error))
- .doOnNext(event -> request.getConversationRecorder().record(request, event));
- });
- return new AgentRunHandle() {
- private final AtomicBoolean cancelled = new AtomicBoolean(false);
-
- @Override
- public Flux stream() {
- return stream.doOnCancel(() -> cancelInternal(agent, approvalCoordinator, sideEvents, request, cancelled));
- }
-
- @Override
- public void cancel() {
- cancelInternal(agent, approvalCoordinator, sideEvents, request, cancelled);
- }
-
- @Override
- public void submitToolApproval(AgentToolApprovalResponse response) {
- approvalCoordinator.submit(response);
- }
- };
}
/**
- * 为单次请求构建 ReActAgent。
+ * 将 AgentScope 主线路事件映射为 Easy-Agents 运行时事件。
*
- * @param request 运行请求
- * @param sideEvents 旁路事件 sink
- * @return ReActAgent 实例
+ * 这里仅处理 AgentScope {@code agent.stream(...)} 主线路中需要展示给调用方的
+ * 模型内容。工具、知识库、AutoContext 和 Skill 状态都由旁路 bridge 通过
+ * observer/interceptor 发出,不参与这里的 AgentScope 主输出映射。
+ *
+ * @param context 本轮运行上下文
+ * @param event AgentScope 主线路事件
+ * @return 运行时事件列表
*/
- public ReActAgent buildAgent(AgentRunRequest request, Sinks.Many sideEvents) {
- return buildAgent(request, sideEvents, AgentToolApprovalCoordinator.disabled());
+ private List mapStreamEvent(AgentRuntimeExecutionContext context, Event event) {
+ if (event == null) {
+ return List.of();
+ }
+ if (event.getType() == EventType.REASONING) {
+ return mapReasoningEvent(context, event);
+ }
+ if (event.getType() == EventType.SUMMARY) {
+ return mapSummaryEvent(context, event);
+ }
+ if (event.getType() == EventType.AGENT_RESULT) {
+ return mapAgentResultEvent(context, event);
+ }
+ // HINT 属于 AgentScope 注入给模型的上下文提示,不作为聊天主输出展示。
+ return List.of();
}
/**
- * 为单次请求构建 ReActAgent。
+ * 映射 AgentScope reasoning chunk。
*
- * @param request 运行请求
- * @param sideEvents 旁路事件 sink
- * @param approvalCoordinator 审批协调器
- * @return ReActAgent 实例
+ * @param context 本轮运行上下文
+ * @param event AgentScope reasoning 事件
+ * @return 运行时事件列表
*/
- public ReActAgent buildAgent(AgentRunRequest request,
- Sinks.Many sideEvents,
- AgentToolApprovalCoordinator approvalCoordinator) {
- return buildAgent(request, sideEvents, approvalCoordinator,
- AgentSkillRuntimeContext.from(request.getAgentDefinition().getSkillBoxSpec()));
+ private List mapReasoningEvent(AgentRuntimeExecutionContext context, Event event) {
+ if (event.isLast() || event.getMessage() == null || event.getMessage().getContent() == null) {
+ return List.of();
+ }
+ List events = new ArrayList<>();
+ List blocks = event.getMessage().getContent();
+ for (int blockIndex = 0; blockIndex < blocks.size(); blockIndex++) {
+ ContentBlock block = blocks.get(blockIndex);
+ if (block instanceof ThinkingBlock thinkingBlock) {
+ AgentRuntimeEvent runtimeEvent = base(context, AgentRuntimeEventType.REASONING_DELTA);
+ runtimeEvent.setMessageId(event.getMessageId());
+ runtimeEvent.getPayload().put("reasoning", thinkingBlock.getThinking());
+ runtimeEvent.getPayload().put("last", event.isLast());
+ runtimeEvent.getMetadata().put("source", "AGENTSCOPE_STREAM");
+ runtimeEvent.getMetadata().put("blockIndex", blockIndex);
+ runtimeEvent.getMetadata().putAll(nullToEmpty(thinkingBlock.getMetadata()));
+ events.add(runtimeEvent);
+ continue;
+ }
+ if (block instanceof TextBlock textBlock) {
+ AgentRuntimeEvent runtimeEvent = base(context, AgentRuntimeEventType.MESSAGE_DELTA);
+ runtimeEvent.setMessageId(event.getMessageId());
+ runtimeEvent.getPayload().put("text", textBlock.getText());
+ runtimeEvent.getPayload().put("last", event.isLast());
+ runtimeEvent.getMetadata().put("source", "AGENTSCOPE_STREAM");
+ runtimeEvent.getMetadata().put("blockIndex", blockIndex);
+ events.add(runtimeEvent);
+ continue;
+ }
+ }
+ return events;
}
/**
- * 为单次请求构建 ReActAgent。
+ * 映射 AgentScope summary chunk。
*
- * @param request 运行请求
- * @param sideEvents 旁路事件 sink
- * @param approvalCoordinator 审批协调器
- * @param skillContext Skill 运行时上下文
- * @return ReActAgent 实例
+ * @param context 本轮运行上下文
+ * @param event AgentScope summary 事件
+ * @return 运行时事件列表
*/
- public ReActAgent buildAgent(AgentRunRequest request,
- Sinks.Many sideEvents,
- AgentToolApprovalCoordinator approvalCoordinator,
- AgentSkillRuntimeContext skillContext) {
+ private List mapSummaryEvent(AgentRuntimeExecutionContext context, Event event) {
+ if (event.getMessage() == null || event.getMessage().getContent() == null) {
+ return List.of();
+ }
+ List events = new ArrayList<>();
+ List blocks = event.getMessage().getContent();
+ for (int blockIndex = 0; blockIndex < blocks.size(); blockIndex++) {
+ ContentBlock block = blocks.get(blockIndex);
+ if (block instanceof TextBlock textBlock) {
+ AgentRuntimeEvent runtimeEvent = base(context, AgentRuntimeEventType.MESSAGE_DELTA);
+ runtimeEvent.setMessageId(event.getMessageId());
+ runtimeEvent.getPayload().put("text", textBlock.getText());
+ runtimeEvent.getPayload().put("last", event.isLast());
+ runtimeEvent.getPayload().put("summary", true);
+ runtimeEvent.getMetadata().put("source", "AGENTSCOPE_STREAM");
+ runtimeEvent.getMetadata().put("blockIndex", blockIndex);
+ runtimeEvent.getMetadata().put("streamType", "SUMMARY");
+ events.add(runtimeEvent);
+ }
+ }
+ return events;
+ }
+
+ /**
+ * 映射 AgentScope 最终结果事件。
+ *
+ * 普通最终结果只用于后端收口,不作为 {@link AgentRuntimeEventType#MESSAGE_DELTA}
+ * 下发给前端。HITL 暂停是例外:AgentScope 用最终结果事件携带
+ * {@link GenerateReason#REASONING_STOP_REQUESTED},这里需要转换为 Easy-Agents
+ * 的 {@link AgentRuntimeEventType#SUSPENDED}。
+ *
+ * @param context 本轮运行上下文
+ * @param event AgentScope 最终结果事件
+ * @return 运行时事件列表
+ */
+ private List mapAgentResultEvent(AgentRuntimeExecutionContext context, Event event) {
+ if (event.getMessage() != null
+ && event.getMessage().getGenerateReason() == GenerateReason.REASONING_STOP_REQUESTED) {
+ return List.of(suspended(context, event));
+ }
+ return List.of();
+ }
+
+ /**
+ * 生成暂停事件。
+ *
+ * 该事件表示 AgentScope 主线路被 HITL interceptor 主动暂停并挂起,等待外部审批后恢复。
+ *
+ * @param context 本轮运行上下文
+ * @param sourceEvent AgentScope 结果事件
+ * @return 暂停事件
+ */
+ private AgentRuntimeEvent suspended(AgentRuntimeExecutionContext context, Event sourceEvent) {
+ AgentRuntimeEvent event = base(context, AgentRuntimeEventType.SUSPENDED);
+ event.setMessageId(sourceEvent.getMessageId());
+ 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.getMetadata().put("source", "AGENTSCOPE_STREAM");
+ event.getMetadata().put("generateReason", sourceEvent.getMessage() == null
+ ? GenerateReason.REASONING_STOP_REQUESTED.name()
+ : sourceEvent.getMessage().getGenerateReason().name());
+ return event;
+ }
+
+ /**
+ * 生成开始事件。
+ *
+ * @param context 本轮运行上下文
+ * @return 开始事件
+ */
+ private AgentRuntimeEvent started(AgentRuntimeExecutionContext context) {
+ AgentRuntimeEvent event = base(context, AgentRuntimeEventType.STARTED);
+ event.getPayload().put("requestId", context.getRequestId());
+ if (Boolean.TRUE.equals(context.getMetadata().get("resume"))) {
+ event.getPayload().put("resume", true);
+ event.getPayload().put("resumeToken", context.getMetadata().get("resumeToken"));
+ }
+ return event;
+ }
+
+ /**
+ * 生成完成事件。
+ *
+ * @param context 本轮运行上下文
+ * @param text 最终文本
+ * @param message 最终结构化消息
+ * @param knowledgeReferences 本轮知识候选
+ * @return 完成事件
+ */
+ private AgentRuntimeEvent completed(AgentRuntimeExecutionContext context,
+ String text,
+ AgentMessage message,
+ Map knowledgeReferences) {
+ String finalText = text == null ? "" : text;
+ AgentMessage finalMessage = message == null ? AgentMessage.text(AgentMessageRole.ASSISTANT, finalText) : message;
+ List matchedReferences = citationMatcher.match(finalText, knowledgeReferences.values());
+ finalMessage.setKnowledgeReferences(matchedReferences);
+ AgentRuntimeEvent event = base(context, AgentRuntimeEventType.COMPLETED);
+ event.getPayload().put("text", finalText);
+ event.setMessage(finalMessage);
+ return event;
+ }
+
+ /**
+ * 生成失败事件。
+ *
+ * @param context 本轮运行上下文
+ * @param error 运行异常
+ * @return 失败事件
+ */
+ private AgentRuntimeEvent failed(AgentRuntimeExecutionContext context, Throwable error) {
+ AgentRuntimeEvent event = base(context, AgentRuntimeEventType.FAILED);
+ event.getPayload().put("message", error == null ? "Agent runtime failed." : error.getMessage());
+ event.getPayload().put("errorType", error == null ? null : error.getClass().getName());
+ return event;
+ }
+
+ /**
+ * 生成取消事件。
+ *
+ * @param context 本轮运行上下文
+ * @return 取消事件
+ */
+ private AgentRuntimeEvent cancelled(AgentRuntimeExecutionContext context) {
+ AgentRuntimeEvent event = base(context, AgentRuntimeEventType.CANCELLED);
+ event.getPayload().put("reason", context.getCancelReason());
+ if (Boolean.TRUE.equals(context.getMetadata().get("resume"))) {
+ event.getPayload().put("resume", true);
+ event.getPayload().put("resumeToken", context.getMetadata().get("resumeToken"));
+ }
+ return event;
+ }
+
+ /**
+ * 将异常转换为运行时事件流。
+ *
+ * @param context 本轮运行上下文
+ * @param error 异常
+ * @return 错误收口事件流
+ */
+ private Flux errorEventFlux(AgentRuntimeExecutionContext context, Throwable error) {
+ if (error instanceof AgentToolApprovalRejectedException approvalRejectedException) {
+ context.setCancelReason(approvalRejectedException.getRejectReason());
+ saveSession();
+ return Flux.just(cancelled(context));
+ }
+ return Flux.just(failed(context, error));
+ }
+
+ /**
+ * 保存 AgentScope 会话状态。
+ */
+ private void saveSession() {
+ agent.saveTo(session, sessionKey);
+ }
+
+ /**
+ * 响应 Reactor 取消信号。
+ *
+ * 取消本身是调用方终止订阅行为,因此当前订阅者通常无法再收到 {@code CANCELLED}
+ * 事件。这里的职责是中断 AgentScope 主线路、释放等待中的审批请求,并完成旁路 sink。
+ *
+ * @param context 本轮运行上下文
+ * @param sideEvents 旁路事件 sink
+ * @param cancelled 取消去重标记
+ */
+ private void cancelInternal(AgentRuntimeExecutionContext context,
+ Sinks.Many sideEvents,
+ AtomicBoolean cancelled) {
+ if (!cancelled.compareAndSet(false, true)) {
+ return;
+ }
+ context.setCancelReason("cancelled");
+ approvalCoordinator.cancelAll(context.getCancelReason());
+ agent.interrupt();
+ sideEvents.tryEmitComplete();
+ }
+
+ /**
+ * 清理本轮状态。
+ */
+ private void cleanupTurn() {
+ turnContextHolder.clear();
+ running.set(false);
+ }
+
+ /**
+ * 从 AgentScope 最终结果中捕获后端收口需要的完整消息。
+ *
+ * 这是后端内部状态收集
+ *
+ * @param finalText 最终文本
+ * @param finalMessage 最终结构化消息引用
+ * @param event AgentScope 主线路事件
+ */
+ private void captureFinalAgentResult(StringBuilder finalText,
+ AtomicReference finalMessage,
+ Event event) {
+ if (event == null
+ || event.getType() != EventType.AGENT_RESULT
+ || event.getMessage() == null
+ || event.getMessage().getGenerateReason() == GenerateReason.REASONING_STOP_REQUESTED) {
+ return;
+ }
+ AgentMessage message = messageAdapter.toAgentMessage(event.getMessage());
+ finalMessage.set(message);
+ String messageText = textContent(message);
+ if (!messageText.isBlank()) {
+ finalText.setLength(0);
+ finalText.append(messageText);
+ }
+ }
+
+ /**
+ * 从消息增量事件中收集最终助手文本。
+ *
+ * @param finalText 最终文本
+ * @param event 运行时事件
+ */
+ private void updateFinalText(StringBuilder finalText, AgentRuntimeEvent event) {
+ if (event.getEventType() != AgentRuntimeEventType.MESSAGE_DELTA) {
+ return;
+ }
+ Object text = event.getPayload().get("text");
+ if (text instanceof String textValue && !textValue.isEmpty()) {
+ finalText.append(textValue);
+ }
+ if (event.getMessage() != null && Boolean.TRUE.equals(event.getPayload().get("last"))) {
+ String messageText = textContent(event.getMessage());
+ if (!messageText.isBlank()) {
+ finalText.setLength(0);
+ finalText.append(messageText);
+ }
+ }
+ }
+
+ /**
+ * 从最终增量中收集结构化助手消息。
+ *
+ * @param finalMessage 最终消息引用
+ * @param event 运行时事件
+ */
+ private void updateFinalMessage(AtomicReference finalMessage, AgentRuntimeEvent event) {
+ if (event.getEventType() == AgentRuntimeEventType.MESSAGE_DELTA
+ && Boolean.TRUE.equals(event.getPayload().get("last"))
+ && event.getMessage() != null) {
+ finalMessage.set(event.getMessage());
+ }
+ }
+
+ /**
+ * 记录本轮是否已被 HITL 暂停。
+ *
+ * @param suspendedEvent 暂停事件引用
+ * @param event 运行时事件
+ */
+ private void updateSuspendedEvent(AtomicReference suspendedEvent, AgentRuntimeEvent event) {
+ if (event.getEventType() == AgentRuntimeEventType.SUSPENDED) {
+ suspendedEvent.set(event);
+ }
+ }
+
+ /**
+ * 从工具审批旁路事件中收集本轮待审批项。
+ *
+ * @param pendingApprovals 待审批项集合
+ * @param event 运行时事件
+ */
+ private void updatePendingApprovals(List> pendingApprovals, AgentRuntimeEvent event) {
+ if (event == null || event.getEventType() != AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED) {
+ return;
+ }
+ Map 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);
+ }
+
+ /**
+ * 从知识库旁路事件中收集本轮候选引用。
+ *
+ * 知识库检索仍属于 AgentScope RAG 主线路;这里仅读取旁路事件 payload,
+ * 为最终消息展示引用做保守匹配,不影响 AgentScope memory/session。
+ *
+ * @param knowledgeReferences 候选引用集合
+ * @param event 运行时事件
+ */
+ @SuppressWarnings("unchecked")
+ private void updateKnowledgeReferences(Map knowledgeReferences,
+ AgentRuntimeEvent event) {
+ if (event == null || event.getEventType() != AgentRuntimeEventType.KNOWLEDGE_RETRIEVAL) {
+ return;
+ }
+ Object documentsObject = event.getPayload().get("documents");
+ if (!(documentsObject instanceof List> documents) || documents.isEmpty()) {
+ return;
+ }
+ Object knowledgeId = event.getPayload().get("knowledgeId");
+ Object knowledgeName = event.getPayload().get("knowledgeName");
+ Object knowledgeType = event.getPayload().get("knowledgeType");
+ Object faqCollection = event.getPayload().get("faqCollection");
+ for (Object documentObject : documents) {
+ if (!(documentObject instanceof Map, ?> documentMap)) {
+ continue;
+ }
+ AgentKnowledgeReference reference = new AgentKnowledgeReference();
+ reference.setKnowledgeId(stringValue(knowledgeId));
+ reference.setKnowledgeName(stringValue(knowledgeName));
+ reference.setDocumentId(stringValue(documentMap.get("documentId")));
+ reference.setDocumentName(stringValue(documentMap.get("documentName")));
+ reference.setChunkId(stringValue(documentMap.get("chunkId")));
+ reference.setChunkContent(stringValue(documentMap.get("chunkContent")));
+ reference.setSourceUri(stringValue(documentMap.get("sourceUri")));
+ reference.setScore(documentMap.get("score") instanceof Number score ? score.doubleValue() : null);
+ Object metadata = documentMap.get("metadata");
+ Map referenceMetadata = metadata instanceof Map, ?> map
+ ? new LinkedHashMap<>((Map) map)
+ : new LinkedHashMap<>();
+ if (knowledgeType != null) {
+ referenceMetadata.putIfAbsent("knowledgeType", knowledgeType);
+ }
+ if (faqCollection != null) {
+ referenceMetadata.putIfAbsent("faqCollection", faqCollection);
+ }
+ reference.setMetadata(referenceMetadata);
+ knowledgeReferences.putIfAbsent(knowledgeReferenceKey(reference), reference);
+ }
+ }
+
+ /**
+ * 构建候选知识引用去重键。
+ *
+ * @param reference 知识引用
+ * @return 去重键
+ */
+ private String knowledgeReferenceKey(AgentKnowledgeReference reference) {
+ return String.join(":",
+ stringValue(reference.getKnowledgeId()),
+ stringValue(reference.getDocumentId()),
+ stringValue(reference.getChunkId()));
+ }
+
+ /**
+ * 提取结构化消息中的文本正文。
+ *
+ * @param message 结构化消息
+ * @return 文本正文
+ */
+ private String textContent(AgentMessage message) {
+ if (message == null || message.getContentBlocks() == null) {
+ return "";
+ }
+ StringBuilder builder = new StringBuilder();
+ for (AgentContentBlock block : message.getContentBlocks()) {
+ if (block instanceof AgentTextBlock textBlock && textBlock.getText() != null) {
+ builder.append(textBlock.getText());
+ }
+ }
+ return builder.toString();
+ }
+
+ /**
+ * 构建携带公共身份信息的运行时事件。
+ *
+ * @param context 本轮运行上下文
+ * @param type 事件类型
+ * @return 运行时事件
+ */
+ private AgentRuntimeEvent base(AgentRuntimeExecutionContext context, AgentRuntimeEventType type) {
+ AgentRuntimeEvent event = AgentRuntimeEvent.of(type);
+ event.setTraceId(context.getTraceId());
+ event.setSessionId(context.getSessionId());
+ event.setAgentId(context.getAgentDefinition().getAgentId());
+ event.getMetadata().put("requestId", context.getRequestId());
+ event.getMetadata().putAll(nullToEmpty(context.getMetadata()));
+ return event;
+ }
+
+ /**
+ * 空安全 Map。
+ *
+ * @param map 原 Map
+ * @return 非空 Map
+ */
+ private Map nullToEmpty(Map map) {
+ return map == null ? new LinkedHashMap<>() : map;
+ }
+
+ /**
+ * 将对象转换为字符串。
+ *
+ * @param value 值
+ * @return 字符串
+ */
+ private String stringValue(Object value) {
+ return value == null ? null : String.valueOf(value);
+ }
+
+ /**
+ * 将 AgentScope 可能输出的累计快照归一化为增量。
+ *
+ * 主线路 mapper 要尽量保持 AgentScope 原始顺序,但不同模型或底层适配器可能
+ * 输出累计文本。该归一化器只修正同一 message/block 的文本增量,不触碰旁路事件。
+ */
+ private static final class StreamDeltaNormalizer {
+
+ private final Map previousValues = new LinkedHashMap<>();
+
+ /**
+ * 归一化流式事件。
+ *
+ * @param event 原事件
+ * @return 归一化后的事件
+ */
+ private AgentRuntimeEvent normalize(AgentRuntimeEvent event) {
+ if (event == null || event.getEventType() == null) {
+ return event;
+ }
+ if (event.getEventType() == AgentRuntimeEventType.REASONING_DELTA) {
+ normalizePayloadText(event, "reasoning");
+ }
+ if (event.getEventType() == AgentRuntimeEventType.MESSAGE_DELTA) {
+ normalizePayloadText(event, "text");
+ }
+ return event;
+ }
+
+ private void normalizePayloadText(AgentRuntimeEvent event, String payloadKey) {
+ Object currentValue = event.getPayload().get(payloadKey);
+ if (!(currentValue instanceof String currentText) || currentText.isEmpty()) {
+ return;
+ }
+ String key = streamKey(event, payloadKey);
+ String previousText = previousValues.get(key);
+ previousValues.put(key, currentText);
+ if (previousText != null && !previousText.isEmpty() && currentText.startsWith(previousText)) {
+ event.getPayload().put(payloadKey, currentText.substring(previousText.length()));
+ }
+ }
+
+ private String streamKey(AgentRuntimeEvent event, String payloadKey) {
+ Object blockIndex = event.getMetadata().get("blockIndex");
+ return String.join(":",
+ event.getMessageId() == null || event.getMessageId().isBlank() ? "default-message" : event.getMessageId(),
+ event.getEventType().name(),
+ payloadKey,
+ blockIndex == null ? "0" : String.valueOf(blockIndex));
+ }
+ }
+
+ /**
+ * 校验初始化请求的必要字段。
+ *
+ * @param request 初始化请求
+ */
+ private void validateInitRequest(AgentInitRequest request) {
+ if (request == null) {
+ throw new AgentRuntimeException("Agent runtime init request is required.");
+ }
+ if (request.getSessionId() == null || request.getSessionId().isBlank()) {
+ throw new AgentRuntimeException("Agent runtime session id is required.");
+ }
+ if (request.getSessionStore() == null || request.getSessionStore() == NoopAgentSessionStore.INSTANCE) {
+ throw new AgentRuntimeException("Agent runtime session store is required.");
+ }
AgentDefinition definition = request.getAgentDefinition();
+ if (definition == null) {
+ throw new AgentRuntimeException("Agent definition is required.");
+ }
+ if (definition.getModelSpec() == null) {
+ throw new AgentRuntimeException("Agent model spec is required.");
+ }
+ }
+
+ /**
+ * 从初始化请求创建运行时级上下文。
+ *
+ * @param request 初始化请求
+ * @return 运行时级上下文
+ */
+ private AgentRuntimeExecutionContext createRuntimeContext(AgentInitRequest request) {
+ AgentRuntimeExecutionContext context = new AgentRuntimeExecutionContext();
+ context.setSessionId(request.getSessionId());
+ context.setTraceId(request.getRuntimeContext().getTraceId());
+ context.setAgentDefinition(request.getAgentDefinition());
+ context.setRuntimeContext(request.getRuntimeContext());
+ context.setToolInvokers(request.getToolInvokers());
+ context.setKnowledgeRetrievers(request.getKnowledgeRetrievers());
+ context.setSessionStore(request.getSessionStore());
+ context.setConversationRecorder(request.getConversationRecorder());
+ context.setMetadata(request.getMetadata());
+ return context;
+ }
+
+ /**
+ * 构建并配置 AgentScope ReActAgent。
+ *
+ * @param context 运行时级上下文
+ * @return AgentScope ReActAgent
+ */
+ private ReActAgent buildAgent(AgentRuntimeExecutionContext context) {
+ AgentDefinition definition = context.getAgentDefinition();
Model model = modelFactory.create(definition.getModelSpec(), definition.getGenerationOptions());
Toolkit toolkit = new Toolkit();
- Map> skillTools = buildToolkit(request, toolkit, sideEvents, approvalCoordinator, skillContext);
- Memory memory = memoryAdapter.createMemory(request.getMemorySnapshot(), definition.getMemoryPolicy(), model);
- Knowledge knowledge = knowledgeAdapter.createAggregateKnowledge(request, sideEvents);
+ Map> skillTools = buildToolkit(context, toolkit);
+ AgentScopeMemoryBuildResult memoryResult = memoryAdapter.createMemoryResult(null, definition.getMemoryPolicy(), model);
+ Memory memory = memoryResult.getMemory();
+ Knowledge knowledge = knowledgeAdapter.createAggregateKnowledge(context, turnContextHolder);
SkillBox skillBox = skillAdapter.createSkillBox(definition.getSkillBoxSpec(), toolkit, skillTools);
- // 构建ReActAgent
+ // AutoContextInterceptor 是官方 AutoContextHook 的替代实现。这里仍只注册统一 runtime hook,
+ // 避免官方 hook 与 Easy-Agents interceptor 同时触发压缩和 inputMessages 改写。
+ AgentRuntimeEventBridge eventBridge = new AgentRuntimeEventBridge(context, turnContextHolder);
+ List interceptors = new ArrayList<>();
+ if (memory instanceof AutoContextMemory) {
+ interceptors.add(new AutoContextInterceptor(eventBridge, memoryResult.getAutoContextConfig()));
+ }
+ interceptors.add(new ToolHitlInterceptor(eventBridge, approvalCoordinator, definition.getToolSpecs()));
+ // 注册旁路事件监听器与主线路干预器。观察器只发旁路事件,不修改 AgentScope HookEvent。
+ List observers = new ArrayList<>();
+ observers.add(new SkillExecutionObserver(eventBridge, skillContext, skillBox));
+ observers.add(new ToolExecutionObserver(eventBridge, skillContext));
+ observers.add(new ReasoningLifecycleObserver(eventBridge));
+ observers.add(new AgentRuntimeErrorObserver(eventBridge));
+ AgentRuntimeObservationManager observationManager =
+ new AgentRuntimeObservationManager(interceptors, observers);
ReActAgent.Builder builder = ReActAgent.builder()
.name(definition.getAgentName())
.description(definition.getDescription())
@@ -217,7 +982,8 @@ public class AgentScopeReActRuntime implements AgentRuntime {
.memory(memory)
.maxIters(definition.getExecutionOptions().getMaxIters())
.generateOptions(modelFactory.toGenerateOptions(definition.getModelSpec(), definition.getGenerationOptions()))
- .hook(new AgentScopeEventHook(request))
+ .hook(new AgentScopeRuntimeHook(observationManager))
+ .enablePendingToolRecovery(true)
.statePersistence(AgentScopeSessionAdapter.toStatePersistence(definition.getPersistencePolicy()));
if (knowledge != null) {
builder.knowledge(knowledge)
@@ -230,51 +996,24 @@ public class AgentScopeReActRuntime implements AgentRuntime {
return builder.build();
}
- private Toolkit buildToolkit(AgentRunRequest request, Sinks.Many sideEvents) {
- return buildToolkit(request, sideEvents, AgentToolApprovalCoordinator.disabled());
- }
-
/**
- * 构建工具箱。
+ * 构建 AgentScope Toolkit,并返回按 Skill ID 分组的工具。
*
- * @param request 运行请求
- * @param sideEvents 旁路事件 sink
- * @param approvalCoordinator 审批协调器
- * @return 工具箱
- */
- private Toolkit buildToolkit(AgentRunRequest request,
- Sinks.Many sideEvents,
- AgentToolApprovalCoordinator approvalCoordinator) {
- Toolkit toolkit = new Toolkit();
- buildToolkit(request, toolkit, sideEvents, approvalCoordinator,
- AgentSkillRuntimeContext.from(request.getAgentDefinition().getSkillBoxSpec()));
- return toolkit;
- }
-
- /**
- * 构建工具箱并返回 Skill 绑定工具。
- *
- * @param request 运行请求
- * @param toolkit 工具箱
- * @param sideEvents 旁路事件 sink
- * @param approvalCoordinator 审批协调器
- * @param skillContext Skill 运行时上下文
+ * @param context 运行时级上下文
+ * @param toolkit AgentScope Toolkit
* @return 按 Skill ID 分组的工具
*/
- private Map> buildToolkit(AgentRunRequest request,
- Toolkit toolkit,
- Sinks.Many sideEvents,
- AgentToolApprovalCoordinator approvalCoordinator,
- AgentSkillRuntimeContext skillContext) {
+ private Map> buildToolkit(AgentRuntimeExecutionContext context,
+ Toolkit toolkit) {
Map> skillTools = new LinkedHashMap<>();
- if (!request.getAgentDefinition().getExecutionOptions().isToolCallingEnabled()) {
+ if (!context.getAgentDefinition().getExecutionOptions().isToolCallingEnabled()) {
return skillTools;
}
- for (AgentToolSpec toolSpec : request.getAgentDefinition().getToolSpecs()) {
- AgentToolInvoker invoker = request.getToolInvokers().get(toolSpec.getName());
+ for (AgentToolSpec toolSpec : context.getAgentDefinition().getToolSpecs()) {
+ AgentToolInvoker invoker = context.getToolInvokers().get(toolSpec.getName());
AgentSkillBinding skillBinding = skillContext.getToolBinding(toolSpec.getName());
- AgentTool agentTool = toolAdapter.adapt(toolSpec, invoker, request, approvalCoordinator, sideEvents,
- skillContext, skillBinding);
+ AgentTool agentTool = toolAdapter.adapt(toolSpec, invoker, context, approvalCoordinator, turnContextHolder,
+ skillContext, skillBinding, false, false, false);
if (skillBinding == null) {
toolkit.registerAgentTool(agentTool);
} else {
@@ -284,182 +1023,6 @@ public class AgentScopeReActRuntime implements AgentRuntime {
return skillTools;
}
- /**
- * 构建当前轮的 AgentScope 输入消息。
- *
- * @param request 运行请求
- * @return 输入消息
- */
- private List buildInput(AgentRunRequest request) {
- List input = new ArrayList<>();
- AgentMessage userMessage = request.getUserMessage();
- if (userMessage != null) {
- input.add(messageAdapter.toMsg(userMessage));
- }
- return input;
- }
-
- /**
- * 将单个 AgentScope 事件映射为运行时事件。
- *
- * @param request 运行请求
- * @param event AgentScope 事件
- * @return 运行时事件
- */
- private List mapEvent(AgentRunRequest request, Event event) {
- return mapEvent(request, event, AgentSkillRuntimeContext.from(request.getAgentDefinition().getSkillBoxSpec()));
- }
-
- /**
- * 将单个 AgentScope 事件映射为运行时事件。
- *
- * @param request 运行请求
- * @param event AgentScope 事件
- * @param skillContext Skill 运行时上下文
- * @return 运行时事件
- */
- private List mapEvent(AgentRunRequest request, Event event, AgentSkillRuntimeContext skillContext) {
- List events = new ArrayList<>();
- if (event == null) {
- return events;
- }
- if (event.getType() == EventType.REASONING) {
- events.addAll(mapSkillCalls(request, event, skillContext));
- AgentRuntimeEvent runtimeEvent = base(request, AgentRuntimeEventType.REASONING_DELTA);
- runtimeEvent.setMessageId(event.getMessageId());
- runtimeEvent.getPayload().put("text", event.getMessage() == null ? null : event.getMessage().getTextContent());
- runtimeEvent.getPayload().put("last", event.isLast());
- events.add(runtimeEvent);
- return events;
- }
- if (event.getType() == EventType.TOOL_RESULT) {
- events.addAll(mapSkillResults(request, event, skillContext));
- return events;
- }
- if (event.getType() == EventType.AGENT_RESULT) {
- AgentRuntimeEvent runtimeEvent = base(request, AgentRuntimeEventType.MESSAGE_DELTA);
- runtimeEvent.setMessageId(event.getMessageId());
- runtimeEvent.getPayload().put("text", event.getMessage() == null ? null : event.getMessage().getTextContent());
- runtimeEvent.getPayload().put("last", event.isLast());
- if (event.isLast() && event.getMessage() != null) {
- runtimeEvent.setMessage(messageAdapter.toAgentMessage(event.getMessage()));
- }
- events.add(runtimeEvent);
- return events;
- }
- return events;
- }
-
- /**
- * 从推理消息中识别 Skill 加载调用。
- *
- * @param request 运行请求
- * @param event AgentScope 事件
- * @param skillContext Skill 运行时上下文
- * @return Skill 调用事件
- */
- private List mapSkillCalls(AgentRunRequest request,
- Event event,
- AgentSkillRuntimeContext skillContext) {
- List events = new ArrayList<>();
- if (event.getMessage() == null) {
- return events;
- }
- for (ToolUseBlock block : event.getMessage().getContentBlocks(ToolUseBlock.class)) {
- if (!skillContext.isSkillLoadTool(block.getName())) {
- continue;
- }
- AgentSkillLoadCall call = skillContext.rememberLoadCall(block.getId(), block.getInput());
- if (!skillContext.markLoadCallEmitted(block.getId())) {
- continue;
- }
- AgentRuntimeEvent runtimeEvent = base(request, AgentRuntimeEventType.SKILL_CALL);
- runtimeEvent.setMessageId(event.getMessageId());
- runtimeEvent.setToolCallId(block.getId());
- appendSkillLoadPayload(runtimeEvent, call);
- runtimeEvent.getPayload().put("toolName", block.getName());
- runtimeEvent.getPayload().put("input", block.getInput());
- runtimeEvent.getPayload().put("status", "RUNNING");
- events.add(runtimeEvent);
- }
- return events;
- }
-
- /**
- * 从工具结果中识别 Skill 加载结果。
- *
- * @param request 运行请求
- * @param event AgentScope 事件
- * @param skillContext Skill 运行时上下文
- * @return Skill 结果事件
- */
- private List mapSkillResults(AgentRunRequest request,
- Event event,
- AgentSkillRuntimeContext skillContext) {
- List events = new ArrayList<>();
- if (event.getMessage() == null) {
- return events;
- }
- for (ToolResultBlock block : event.getMessage().getContentBlocks(ToolResultBlock.class)) {
- if (!skillContext.isSkillLoadTool(block.getName())) {
- continue;
- }
- AgentSkillLoadCall call = skillContext.removeLoadCall(block.getId());
- AgentRuntimeEvent runtimeEvent = base(request, skillResultType(block));
- if (runtimeEvent.getEventType() == AgentRuntimeEventType.SKILL_RESULT && call != null) {
- skillContext.activateSkill(call.getSkillId());
- }
- runtimeEvent.setMessageId(event.getMessageId());
- runtimeEvent.setToolCallId(block.getId());
- appendSkillLoadPayload(runtimeEvent, call);
- runtimeEvent.getPayload().put("toolName", block.getName());
- runtimeEvent.getPayload().put("text", block.toString());
- runtimeEvent.getPayload().put("status", runtimeEvent.getEventType() == AgentRuntimeEventType.SKILL_RESULT ? "SUCCESS" : "FAILED");
- runtimeEvent.getPayload().put("success", runtimeEvent.getEventType() == AgentRuntimeEventType.SKILL_RESULT);
- runtimeEvent.getPayload().put("suspended", block.isSuspended());
- runtimeEvent.getMetadata().putAll(block.getMetadata() == null ? new LinkedHashMap<>() : block.getMetadata());
- events.add(runtimeEvent);
- }
- return events;
- }
-
- /**
- * 判断 Skill 结果事件类型。
- *
- * @param block 工具结果块
- * @return Skill 事件类型
- */
- private AgentRuntimeEventType skillResultType(ToolResultBlock block) {
- Object success = block.getMetadata() == null ? null : block.getMetadata().get("success");
- if (Boolean.FALSE.equals(success)) {
- return AgentRuntimeEventType.SKILL_FAILED;
- }
- String text = block.toString();
- if (text != null && text.toLowerCase().contains("error")) {
- return AgentRuntimeEventType.SKILL_FAILED;
- }
- return AgentRuntimeEventType.SKILL_RESULT;
- }
-
- /**
- * 追加 Skill 加载事件载荷。
- *
- * @param event 运行时事件
- * @param call Skill 加载调用
- */
- private void appendSkillLoadPayload(AgentRuntimeEvent event, AgentSkillLoadCall call) {
- if (call == null) {
- return;
- }
- event.getPayload().put("skillId", call.getSkillId());
- event.getPayload().put("skillName", call.getSkillName());
- event.getPayload().put("skillBoxId", call.getSkillBoxId());
- event.getPayload().put("path", call.getPath());
- event.getMetadata().put("skillId", call.getSkillId());
- event.getMetadata().put("skillName", call.getSkillName());
- event.getMetadata().put("skillBoxId", call.getSkillBoxId());
- }
-
/**
* 构建聚合知识库的默认检索配置。
*
@@ -482,251 +1045,16 @@ public class AgentScopeReActRuntime implements AgentRuntime {
.build();
}
- /**
- * 从消息增量事件中收集最终助手文本。
- *
- * @param finalText 最终文本引用
- * @param event 运行时事件
- */
- private void updateFinalText(StringBuilder finalText, AgentRuntimeEvent event) {
- if (event.getEventType() == AgentRuntimeEventType.MESSAGE_DELTA) {
- Object text = event.getPayload().get("text");
- if (text != null) {
- String chunk = String.valueOf(text);
- if (Boolean.TRUE.equals(event.getPayload().get("last")) && finalText.length() > chunk.length()) {
- finalText.setLength(0);
- }
- finalText.append(chunk);
- }
- }
+ public AgentInitRequest getInitRequest() {
+ return initRequest;
}
/**
- * 从最终消息事件中收集结构化助手消息。
+ * 获取当前已构建的 AgentScope ReActAgent。
*
- * @param finalMessage 最终消息引用
- * @param event 运行时事件
+ * @return AgentScope ReActAgent
*/
- private void updateFinalMessage(AtomicReference finalMessage, AgentRuntimeEvent event) {
- if (event.getEventType() == AgentRuntimeEventType.MESSAGE_DELTA
- && Boolean.TRUE.equals(event.getPayload().get("last"))
- && event.getMessage() != null) {
- finalMessage.set(event.getMessage());
- }
- }
-
- /**
- * 校验请求的最小必要字段。
- *
- * @param request 运行请求
- */
- private void validate(AgentRunRequest request) {
- if (request == null) {
- throw new AgentRuntimeException("Agent run request is required.");
- }
- if (request.getAgentDefinition() == null) {
- throw new AgentRuntimeException("Agent definition is required.");
- }
- if (request.getAgentDefinition().getModelSpec() == null) {
- throw new AgentRuntimeException("Agent model spec is required.");
- }
- }
-
- /**
- * 启用时加载 AgentScope 会话状态。
- *
- * @param request 运行请求
- * @param agent AgentScope 智能体
- */
- private void loadSessionIfNeeded(AgentRunRequest request, ReActAgent agent) {
- if (request.getAgentDefinition().getPersistencePolicy().isEnabled()
- && request.getAgentDefinition().getPersistencePolicy().isSessionManaged()
- && request.getSessionId() != null) {
- Session session = new AgentScopeSessionAdapter(request.getSessionStore());
- agent.loadIfExists(session, AgentScopeSessionAdapter.sessionKey(request.getSessionId()));
- }
- }
-
- /**
- * 启用时保存 AgentScope 会话状态。
- *
- * @param request 运行请求
- * @param agent AgentScope 智能体
- */
- private void saveSessionIfNeeded(AgentRunRequest request, ReActAgent agent) {
- if (request.getAgentDefinition().getPersistencePolicy().isEnabled()
- && request.getAgentDefinition().getPersistencePolicy().isSessionManaged()
- && request.getSessionId() != null) {
- Session session = new AgentScopeSessionAdapter(request.getSessionStore());
- agent.saveTo(session, AgentScopeSessionAdapter.sessionKey(request.getSessionId()));
- }
- }
-
- /**
- * 构建开始事件。
- *
- * @param request 运行请求
- * @return 开始事件
- */
- private AgentRuntimeEvent started(AgentRunRequest request) {
- AgentRuntimeEvent event = base(request, AgentRuntimeEventType.STARTED);
- event.getPayload().put("requestId", request.getRequestId());
- return event;
- }
-
- /**
- * 构建完成事件。
- *
- * @param request 运行请求
- * @param text 最终文本
- * @return 完成事件
- */
- private AgentRuntimeEvent completed(AgentRunRequest request, String text, AgentMessage message) {
- AgentRuntimeEvent event = base(request, AgentRuntimeEventType.COMPLETED);
- event.getPayload().put("text", text);
- event.setMessage(message);
- return event;
- }
-
- /**
- * 构建完成事件,并附带知识库引用。
- *
- * @param request 运行请求
- * @param text 最终文本
- * @param message 最终消息
- * @param knowledgeReferences 知识库引用
- * @return 完成事件
- */
- private AgentRuntimeEvent completed(AgentRunRequest request,
- String text,
- AgentMessage message,
- Map knowledgeReferences) {
- if (message != null && knowledgeReferences != null && !knowledgeReferences.isEmpty()) {
- message.setKnowledgeReferences(new ArrayList<>(knowledgeReferences.values()));
- }
- return completed(request, text, message);
- }
-
- /**
- * 构建失败事件。
- *
- * @param request 运行请求
- * @param error 错误
- * @return 失败事件
- */
- private AgentRuntimeEvent failed(AgentRunRequest request, Throwable error) {
- AgentRuntimeEvent event = request == null || request.getAgentDefinition() == null
- ? AgentRuntimeEvent.of(AgentRuntimeEventType.FAILED)
- : base(request, AgentRuntimeEventType.FAILED);
- event.getPayload().put("message", error.getMessage());
- event.getPayload().put("errorType", error.getClass().getName());
- return event;
- }
-
- /**
- * 构建取消事件。
- *
- * @param request 运行请求
- * @return 取消事件
- */
- private AgentRuntimeEvent cancelled(AgentRunRequest request) {
- AgentRuntimeEvent event = base(request, AgentRuntimeEventType.CANCELLED);
- event.getPayload().put("reason", request.getCancelReason());
- return event;
- }
-
- /**
- * 取消本次运行并避免重复发出取消事件。
- *
- * @param agent AgentScope 智能体
- * @param sideEvents 旁路事件 sink
- * @param request 运行请求
- * @param cancelled 取消标记
- */
- private void cancelInternal(ReActAgent agent,
- AgentToolApprovalCoordinator approvalCoordinator,
- Sinks.Many sideEvents,
- AgentRunRequest request,
- java.util.concurrent.atomic.AtomicBoolean cancelled) {
- if (!cancelled.compareAndSet(false, true)) {
- return;
- }
- approvalCoordinator.cancelAll(request.getCancelReason() == null ? "运行已取消" : request.getCancelReason());
- saveSessionIfNeeded(request, agent);
- agent.interrupt();
- sideEvents.tryEmitNext(cancelled(request));
- }
-
- /**
- * 收集知识库引用,供最终消息 metadata 使用。
- *
- * @param knowledgeReferences 知识库引用
- * @param event 运行时事件
- */
- @SuppressWarnings("unchecked")
- private void updateKnowledgeReferences(Map knowledgeReferences, AgentRuntimeEvent event) {
- if (event == null || event.getEventType() != AgentRuntimeEventType.KNOWLEDGE_RETRIEVAL) {
- return;
- }
- Object documentsObject = event.getPayload().get("documents");
- if (!(documentsObject instanceof List> documents) || documents.isEmpty()) {
- return;
- }
- Object knowledgeId = event.getPayload().get("knowledgeId");
- Object knowledgeName = event.getPayload().get("knowledgeName");
- for (Object documentObject : documents) {
- if (!(documentObject instanceof Map, ?> documentMap)) {
- continue;
- }
- AgentKnowledgeReference reference = new AgentKnowledgeReference();
- reference.setKnowledgeId(stringValue(knowledgeId));
- reference.setKnowledgeName(stringValue(knowledgeName));
- reference.setDocumentId(stringValue(documentMap.get("documentId")));
- reference.setDocumentName(stringValue(documentMap.get("documentName")));
- reference.setChunkId(stringValue(documentMap.get("chunkId")));
- reference.setSourceUri(stringValue(documentMap.get("sourceUri")));
- reference.setScore(documentMap.get("score") instanceof Number score ? score.doubleValue() : null);
- Object metadata = documentMap.get("metadata");
- reference.setMetadata(metadata instanceof Map, ?> map ? new LinkedHashMap<>((Map) map) : new LinkedHashMap<>());
- String key = String.valueOf(knowledgeId) + "|" + String.valueOf(documentMap.get("documentId")) + "|" + String.valueOf(documentMap.get("chunkId"));
- knowledgeReferences.putIfAbsent(key, reference);
- }
- }
-
- /**
- * 将对象转换为字符串。
- *
- * @param value 值
- * @return 字符串值
- */
- private String stringValue(Object value) {
- return value == null ? null : String.valueOf(value);
- }
-
- /**
- * 构建携带通用标识的运行时事件。
- *
- * @param request 运行请求
- * @param type 事件类型
- * @return 事件
- */
- private AgentRuntimeEvent base(AgentRunRequest request, AgentRuntimeEventType type) {
- AgentRuntimeEvent event = AgentRuntimeEvent.of(type);
- event.setTraceId(request.getTraceId());
- event.setSessionId(request.getSessionId());
- event.setAgentId(request.getAgentDefinition().getAgentId());
- event.getMetadata().put("requestId", request.getRequestId());
- event.getMetadata().putAll(nullToEmpty(request.getMetadata()));
- return event;
- }
-
- /**
- * 当来源元数据为空时返回空 Map。
- *
- * @param map 来源 Map
- * @return 非空 Map
- */
- private Map nullToEmpty(Map map) {
- return map == null ? new LinkedHashMap<>() : map;
+ ReActAgent getAgent() {
+ return agent;
}
}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeRuntimeHook.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeRuntimeHook.java
new file mode 100644
index 0000000..2abf641
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeRuntimeHook.java
@@ -0,0 +1,37 @@
+package com.easyagents.agent.runtime.agentscope;
+
+import com.easyagents.agent.runtime.event.AgentRuntimeObservationManager;
+import io.agentscope.core.hook.Hook;
+import io.agentscope.core.hook.HookEvent;
+import reactor.core.publisher.Mono;
+
+/**
+ * AgentScope Hook 的统一入口。
+ */
+public class AgentScopeRuntimeHook implements Hook {
+
+ private final AgentRuntimeObservationManager observationManager;
+
+ /**
+ * 创建统一 Hook 入口。
+ *
+ * @param observationManager 观察和干预调度器
+ */
+ public AgentScopeRuntimeHook(AgentRuntimeObservationManager observationManager) {
+ this.observationManager = observationManager == null
+ ? AgentRuntimeObservationManager.empty()
+ : observationManager;
+ }
+
+ /**
+ * 处理 AgentScope Hook 事件。
+ *
+ * @param event Hook 事件
+ * @param Hook 事件类型
+ * @return 处理后的 Hook 事件
+ */
+ @Override
+ public Mono onEvent(T event) {
+ return observationManager.handle(event);
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeSessionAdapter.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeSessionAdapter.java
index 5d56385..e1b3302 100644
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeSessionAdapter.java
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeSessionAdapter.java
@@ -1,14 +1,12 @@
package com.easyagents.agent.runtime.agentscope;
import com.easyagents.agent.runtime.persistence.AgentPersistencePolicy;
-import com.easyagents.agent.runtime.persistence.AgentRuntimeState;
-import com.easyagents.agent.runtime.persistence.AgentSessionStore;
+import com.easyagents.agent.runtime.persistence.session.AgentSessionStore;
import io.agentscope.core.session.Session;
import io.agentscope.core.state.SessionKey;
import io.agentscope.core.state.State;
import io.agentscope.core.state.StatePersistence;
-import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -32,7 +30,7 @@ public class AgentScopeSessionAdapter implements Session {
@Override
public void save(SessionKey sessionKey, String name, State state) {
- sessionStore.save(toKey(sessionKey), name, AgentRuntimeState.of(name, state));
+ sessionStore.save(toKey(sessionKey), name, state);
}
/**
@@ -44,13 +42,7 @@ public class AgentScopeSessionAdapter implements Session {
*/
@Override
public void save(SessionKey sessionKey, String name, List extends State> states) {
- List converted = new ArrayList<>();
- if (states != null) {
- for (State state : states) {
- converted.add(AgentRuntimeState.of(name, state));
- }
- }
- sessionStore.saveList(toKey(sessionKey), name, converted);
+ sessionStore.saveList(toKey(sessionKey), name, states);
}
/**
@@ -64,10 +56,7 @@ public class AgentScopeSessionAdapter implements Session {
*/
@Override
public Optional get(SessionKey sessionKey, String name, Class clazz) {
- return sessionStore.get(toKey(sessionKey), name)
- .map(AgentRuntimeState::getValue)
- .filter(clazz::isInstance)
- .map(clazz::cast);
+ return sessionStore.get(toKey(sessionKey), name, clazz);
}
/**
@@ -81,11 +70,7 @@ public class AgentScopeSessionAdapter implements Session {
*/
@Override
public List getList(SessionKey sessionKey, String name, Class clazz) {
- return sessionStore.getList(toKey(sessionKey), name).stream()
- .map(AgentRuntimeState::getValue)
- .filter(clazz::isInstance)
- .map(clazz::cast)
- .collect(Collectors.toList());
+ return sessionStore.getList(toKey(sessionKey), name, clazz);
}
/**
@@ -129,7 +114,7 @@ public class AgentScopeSessionAdapter implements Session {
*/
public static StatePersistence toStatePersistence(AgentPersistencePolicy policy) {
if (policy == null || !policy.isEnabled()) {
- return StatePersistence.none();
+ return StatePersistence.memoryOnly();
}
return StatePersistence.builder()
.memoryManaged(policy.isMemoryManaged())
@@ -154,24 +139,17 @@ public class AgentScopeSessionAdapter implements Session {
}
/**
- * 运行时支撑的 AgentScope 会话键。
- */
- private static class RuntimeSessionKey implements SessionKey {
-
- private final String value;
-
- private RuntimeSessionKey(String value) {
- this.value = value;
- }
-
+ * 运行时支撑的 AgentScope 会话键。
+ */
+ private record RuntimeSessionKey(String value) implements SessionKey {
/**
- * 将键转换为稳定标识符。
+ * 将键转换为稳定标识符。
*
* @return 标识符
*/
@Override
public String toIdentifier() {
- return value;
- }
+ return value;
+ }
}
}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeSkillAdapter.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeSkillAdapter.java
index 80ba50d..0a2e9bb 100644
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeSkillAdapter.java
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeSkillAdapter.java
@@ -1,5 +1,6 @@
package com.easyagents.agent.runtime.agentscope;
+import com.easyagents.agent.runtime.AgentRuntimeException;
import com.easyagents.agent.runtime.skill.AgentSkillBoxSpec;
import com.easyagents.agent.runtime.skill.AgentSkillCompiler;
import com.easyagents.agent.runtime.skill.AgentSkillSpec;
@@ -8,7 +9,6 @@ import io.agentscope.core.skill.SkillBox;
import io.agentscope.core.tool.AgentTool;
import io.agentscope.core.tool.Toolkit;
-import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -19,18 +19,14 @@ public class AgentScopeSkillAdapter implements AgentSkillCompiler {
@Override
public AgentSkill compile(AgentSkillSpec skillSpec) {
- Map metadata = new LinkedHashMap<>(skillSpec.getMetadata());
- metadata.put("skillId", skillSpec.getSkillId());
- metadata.put("name", skillSpec.getName());
- metadata.put("description", skillSpec.getDescription());
- return AgentSkill.builder()
- .name(skillSpec.getName())
- .description(skillSpec.getDescription())
- .skillContent(skillSpec.getSkillContent())
- .metadata(metadata)
- .resources(skillSpec.getResources())
- .source(skillSpec.getSource())
- .build();
+ validateSkillSpec(skillSpec);
+ return new EasyAgentsAgentSkill(
+ skillSpec.getSkillId(),
+ skillSpec.getName(),
+ skillSpec.getDescription(),
+ skillSpec.getSkillContent(),
+ skillSpec.getResources(),
+ skillSpec.getSource());
}
/**
@@ -105,4 +101,71 @@ public class AgentScopeSkillAdapter implements AgentSkillCompiler {
skillBox.syncToolGroupStates();
return skillBox;
}
+
+ /**
+ * 校验 Skill 声明是否具备 AgentScope 注册和模型提示所需的必要信息。
+ *
+ * @param skillSpec Skill 声明
+ * @throws AgentRuntimeException Skill 声明为空或核心字段缺失时抛出
+ */
+ private void validateSkillSpec(AgentSkillSpec skillSpec) {
+ if (skillSpec == null) {
+ throw new AgentRuntimeException("Agent skill spec is required.");
+ }
+ if (skillSpec.getSkillId() == null || skillSpec.getSkillId().isBlank()) {
+ throw new AgentRuntimeException("Agent skill id is required.");
+ }
+ if (skillSpec.getName() == null || skillSpec.getName().isBlank()) {
+ throw new AgentRuntimeException("Agent skill name is required: " + skillSpec.getSkillId());
+ }
+ if (skillSpec.getDescription() == null || skillSpec.getDescription().isBlank()) {
+ throw new AgentRuntimeException("Agent skill description is required: " + skillSpec.getSkillId());
+ }
+ if (skillSpec.getSkillContent() == null || skillSpec.getSkillContent().isBlank()) {
+ throw new AgentRuntimeException("Agent skill content is required: " + skillSpec.getSkillId());
+ }
+ }
+
+ /**
+ * 保持 Easy-Agents Skill ID 与 AgentScope Skill ID 完全一致。
+ *
+ * AgentScope 默认用 {@code name + "_" + source} 生成 Skill ID,而 Easy-Agents
+ * 的工具绑定、旁路事件和调用层配置都以 {@link AgentSkillSpec#getSkillId()} 为准。
+ * 如果不覆盖这里,模型在 prompt 中看到的 skill-id 会和 Easy 侧绑定 key 不一致,
+ * 后续 Skill 状态监听也无法做到精准归属。
+ */
+ private static class EasyAgentsAgentSkill extends AgentSkill {
+
+ private final String skillId;
+
+ /**
+ * 创建 ID 对齐的 AgentScope Skill。
+ *
+ * @param skillId Easy-Agents Skill ID
+ * @param name Skill 展示名称
+ * @param description Skill 描述
+ * @param skillContent Skill 内容
+ * @param resources Skill 资源
+ * @param source Skill 来源
+ */
+ EasyAgentsAgentSkill(String skillId,
+ String name,
+ String description,
+ String skillContent,
+ Map resources,
+ String source) {
+ super(name, description, skillContent, resources, source);
+ this.skillId = skillId;
+ }
+
+ /**
+ * 获取 Easy-Agents 声明的 Skill ID。
+ *
+ * @return Easy-Agents Skill ID
+ */
+ @Override
+ public String getSkillId() {
+ return skillId;
+ }
+ }
}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeToolAdapter.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeToolAdapter.java
index 475969a..c586ea2 100644
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeToolAdapter.java
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeToolAdapter.java
@@ -1,9 +1,8 @@
package com.easyagents.agent.runtime.agentscope;
-import com.easyagents.agent.runtime.AgentRunRequest;
import com.easyagents.agent.runtime.AgentRuntimeException;
-import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
-import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
+import com.easyagents.agent.runtime.AgentRuntimeExecutionContext;
+import com.easyagents.agent.runtime.event.*;
import com.easyagents.agent.runtime.hitl.AgentPendingState;
import com.easyagents.agent.runtime.hitl.AgentToolApprovalCoordinator;
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRejectedException;
@@ -39,8 +38,9 @@ public class AgentScopeToolAdapter {
* @param request 运行请求
* @return AgentScope 工具
*/
- public AgentTool adapt(AgentToolSpec toolSpec, AgentToolInvoker invoker, AgentRunRequest request) {
- return adapt(toolSpec, invoker, request, AgentToolApprovalCoordinator.disabled(), null);
+ public AgentTool adapt(AgentToolSpec toolSpec, AgentToolInvoker invoker, AgentRuntimeExecutionContext request) {
+ return adapt(toolSpec, invoker, request, AgentToolApprovalCoordinator.disabled(),
+ (Sinks.Many) null);
}
/**
@@ -54,9 +54,10 @@ public class AgentScopeToolAdapter {
*/
public AgentTool adapt(AgentToolSpec toolSpec,
AgentToolInvoker invoker,
- AgentRunRequest request,
+ AgentRuntimeExecutionContext request,
Sinks.Many eventSink) {
- return adapt(toolSpec, invoker, request, AgentToolApprovalCoordinator.disabled(), eventSink);
+ return adapt(toolSpec, invoker, request, AgentToolApprovalCoordinator.disabled(), fixedHolder(request, eventSink),
+ null, null);
}
/**
@@ -71,10 +72,10 @@ public class AgentScopeToolAdapter {
*/
public AgentTool adapt(AgentToolSpec toolSpec,
AgentToolInvoker invoker,
- AgentRunRequest request,
+ AgentRuntimeExecutionContext request,
AgentToolApprovalCoordinator approvalCoordinator,
Sinks.Many eventSink) {
- return adapt(toolSpec, invoker, request, approvalCoordinator, eventSink, null);
+ return adapt(toolSpec, invoker, request, approvalCoordinator, fixedHolder(request, eventSink), null, null);
}
/**
@@ -90,11 +91,11 @@ public class AgentScopeToolAdapter {
*/
public AgentTool adapt(AgentToolSpec toolSpec,
AgentToolInvoker invoker,
- AgentRunRequest request,
+ AgentRuntimeExecutionContext request,
AgentToolApprovalCoordinator approvalCoordinator,
Sinks.Many eventSink,
AgentSkillBinding skillBinding) {
- return adapt(toolSpec, invoker, request, approvalCoordinator, eventSink, null, skillBinding);
+ return adapt(toolSpec, invoker, request, approvalCoordinator, fixedHolder(request, eventSink), null, skillBinding);
}
/**
@@ -111,27 +112,153 @@ public class AgentScopeToolAdapter {
*/
public AgentTool adapt(AgentToolSpec toolSpec,
AgentToolInvoker invoker,
- AgentRunRequest request,
+ AgentRuntimeExecutionContext request,
AgentToolApprovalCoordinator approvalCoordinator,
Sinks.Many eventSink,
AgentSkillRuntimeContext skillContext,
AgentSkillBinding skillBinding) {
+ return adapt(toolSpec, invoker, request, approvalCoordinator, fixedHolder(request, eventSink), skillContext, skillBinding);
+ }
+
+ /**
+ * 将运行时工具声明和调用器转换为可读取当前运行轮次的 AgentScope AgentTool。
+ *
+ * @param toolSpec 工具声明
+ * @param invoker 工具调用器
+ * @param request 运行时级上下文
+ * @param approvalCoordinator 审批协调器
+ * @param turnContextHolder 当前运行轮次上下文持有器
+ * @param skillContext Skill 运行时上下文
+ * @param skillBinding Skill 静态绑定关系
+ * @return AgentScope 工具
+ */
+ public AgentTool adapt(AgentToolSpec toolSpec,
+ AgentToolInvoker invoker,
+ AgentRuntimeExecutionContext request,
+ AgentToolApprovalCoordinator approvalCoordinator,
+ AgentRuntimeTurnContextHolder turnContextHolder,
+ AgentSkillRuntimeContext skillContext,
+ AgentSkillBinding skillBinding) {
+ return adapt(toolSpec, invoker, request, approvalCoordinator, turnContextHolder, skillContext, skillBinding, true);
+ }
+
+ /**
+ * 将运行时工具声明和调用器转换为可读取当前运行轮次的 AgentScope AgentTool。
+ *
+ * @param toolSpec 工具声明
+ * @param invoker 工具调用器
+ * @param request 运行时级上下文
+ * @param approvalCoordinator 审批协调器
+ * @param turnContextHolder 当前运行轮次上下文持有器
+ * @param skillContext Skill 运行时上下文
+ * @param skillBinding Skill 静态绑定关系
+ * @param emitNormalToolResult 是否由 adapter 发出普通工具结果旁路事件
+ * @return AgentScope 工具
+ */
+ public AgentTool adapt(AgentToolSpec toolSpec,
+ AgentToolInvoker invoker,
+ AgentRuntimeExecutionContext request,
+ AgentToolApprovalCoordinator approvalCoordinator,
+ AgentRuntimeTurnContextHolder turnContextHolder,
+ AgentSkillRuntimeContext skillContext,
+ AgentSkillBinding skillBinding,
+ boolean emitNormalToolResult) {
if (toolSpec == null || toolSpec.getName() == null) {
throw new AgentRuntimeException("Agent tool spec and name are required.");
}
if (invoker == null) {
throw new AgentRuntimeException("Agent tool invoker is required: " + toolSpec.getName());
}
- return new RuntimeAgentTool(toolSpec, invoker, request, approvalCoordinator, eventSink, skillContext, skillBinding);
+ return new RuntimeAgentTool(toolSpec, invoker, request, approvalCoordinator, turnContextHolder,
+ skillContext, skillBinding, emitNormalToolResult, true, true);
+ }
+
+ /**
+ * 将运行时工具声明和调用器转换为可读取当前运行轮次的 AgentScope AgentTool。
+ *
+ * @param toolSpec 工具声明
+ * @param invoker 工具调用器
+ * @param request 运行时级上下文
+ * @param approvalCoordinator 审批协调器
+ * @param turnContextHolder 当前运行轮次上下文持有器
+ * @param skillContext Skill 运行时上下文
+ * @param skillBinding Skill 静态绑定关系
+ * @param emitNormalToolResult 是否由 adapter 发出普通工具结果旁路事件
+ * @param emitSkillStep 是否由 adapter 发出 Skill 步骤旁路事件
+ * @return AgentScope 工具
+ */
+ public AgentTool adapt(AgentToolSpec toolSpec,
+ AgentToolInvoker invoker,
+ AgentRuntimeExecutionContext request,
+ AgentToolApprovalCoordinator approvalCoordinator,
+ AgentRuntimeTurnContextHolder turnContextHolder,
+ AgentSkillRuntimeContext skillContext,
+ AgentSkillBinding skillBinding,
+ boolean emitNormalToolResult,
+ boolean emitSkillStep) {
+ if (toolSpec == null || toolSpec.getName() == null) {
+ throw new AgentRuntimeException("Agent tool spec and name are required.");
+ }
+ if (invoker == null) {
+ throw new AgentRuntimeException("Agent tool invoker is required: " + toolSpec.getName());
+ }
+ return new RuntimeAgentTool(toolSpec, invoker, request, approvalCoordinator, turnContextHolder,
+ skillContext, skillBinding, emitNormalToolResult, emitSkillStep, true);
+ }
+
+ /**
+ * 将运行时工具声明和调用器转换为可读取当前运行轮次的 AgentScope AgentTool。
+ *
+ * @param toolSpec 工具声明
+ * @param invoker 工具调用器
+ * @param request 运行时级上下文
+ * @param approvalCoordinator 审批协调器
+ * @param turnContextHolder 当前运行轮次上下文持有器
+ * @param skillContext Skill 运行时上下文
+ * @param skillBinding Skill 静态绑定关系
+ * @param emitNormalToolResult 是否由 adapter 发出普通工具结果旁路事件
+ * @param emitSkillStep 是否由 adapter 发出 Skill 步骤旁路事件
+ * @param handleApprovalInTool 是否在工具执行阶段处理审批
+ * @return AgentScope 工具
+ */
+ public AgentTool adapt(AgentToolSpec toolSpec,
+ AgentToolInvoker invoker,
+ AgentRuntimeExecutionContext request,
+ AgentToolApprovalCoordinator approvalCoordinator,
+ AgentRuntimeTurnContextHolder turnContextHolder,
+ AgentSkillRuntimeContext skillContext,
+ AgentSkillBinding skillBinding,
+ boolean emitNormalToolResult,
+ boolean emitSkillStep,
+ boolean handleApprovalInTool) {
+ if (toolSpec == null || toolSpec.getName() == null) {
+ throw new AgentRuntimeException("Agent tool spec and name are required.");
+ }
+ if (invoker == null) {
+ throw new AgentRuntimeException("Agent tool invoker is required: " + toolSpec.getName());
+ }
+ return new RuntimeAgentTool(toolSpec, invoker, request, approvalCoordinator, turnContextHolder,
+ skillContext, skillBinding, emitNormalToolResult, emitSkillStep, handleApprovalInTool);
+ }
+
+ private AgentRuntimeTurnContextHolder fixedHolder(AgentRuntimeExecutionContext request,
+ Sinks.Many eventSink) {
+ AgentRuntimeTurnContextHolder holder = new AgentRuntimeTurnContextHolder();
+ AgentRuntimeEventBridge bridge = new AgentRuntimeEventBridge(request, holder);
+ holder.set(new AgentRuntimeTurnContext(null, eventSink, bridge));
+ return holder;
}
private record RuntimeAgentTool(AgentToolSpec toolSpec,
AgentToolInvoker invoker,
- AgentRunRequest request,
+ AgentRuntimeExecutionContext request,
AgentToolApprovalCoordinator approvalCoordinator,
- Sinks.Many eventSink,
+ AgentRuntimeTurnContextHolder turnContextHolder,
AgentSkillRuntimeContext skillContext,
- AgentSkillBinding skillBinding) implements AgentTool {
+ AgentSkillBinding skillBinding,
+ boolean emitNormalToolResult,
+ boolean emitSkillStep,
+ boolean handleApprovalInTool) implements AgentTool {
/**
* 获取工具名称。
@@ -181,11 +308,14 @@ public class AgentScopeToolAdapter {
*/
@Override
public Mono callAsync(ToolCallParam param) {
- emit(toolCallEvent(param == null ? null : param.getToolUseBlock()));
+ AgentRuntimeEvent startEvent = toolExecutionStartEvent(param == null ? null : param.getToolUseBlock());
+ if (startEvent != null) {
+ emit(startEvent);
+ }
Map input = param == null || param.getInput() == null
? new LinkedHashMap<>()
: new LinkedHashMap<>(param.getInput());
- if (toolSpec.isApprovalRequired()) {
+ if (handleApprovalInTool && toolSpec.isApprovalRequired()) {
if (approvalCoordinator == null) {
throw new AgentRuntimeException("Agent tool approval coordinator is required: " + toolSpec.getName());
}
@@ -224,12 +354,13 @@ public class AgentScopeToolAdapter {
* @return 工具上下文
*/
private AgentToolContext buildContext(ToolCallParam param) {
+ AgentRuntimeExecutionContext currentRequest = currentRequest();
AgentToolContext context = new AgentToolContext();
- context.setRequestId(request.getRequestId());
- context.setTraceId(request.getTraceId());
- context.setSessionId(request.getSessionId());
- context.setAgentId(request.getAgentDefinition().getAgentId());
- context.setRuntimeContext(request.getRuntimeContext());
+ context.setRequestId(currentRequest.getRequestId());
+ context.setTraceId(currentRequest.getTraceId());
+ context.setSessionId(currentRequest.getSessionId());
+ context.setAgentId(currentRequest.getAgentDefinition().getAgentId());
+ context.setRuntimeContext(currentRequest.getRuntimeContext());
if (param != null && param.getToolUseBlock() != null) {
context.setToolCallId(param.getToolUseBlock().getId());
}
@@ -273,35 +404,39 @@ public class AgentScopeToolAdapter {
AgentToolContext context = buildContext(param);
AgentToolResult result = invoker.invoke(input, context);
ToolResultBlock block = toToolResultBlock(param, result);
- emit(toolResultEvent(block));
+ // 有状态 runtime 中,普通工具结果由 AgentScope 原生 PostActingEvent
+ // 旁路观察器统一发出;旧 sink 辅助路径没有统一 hook,因此仍允许 adapter 兼容发射。
+ if (emitNormalToolResult || (emitSkillStep && activeSkillBinding() != null)) {
+ emit(toolResultEvent(block));
+ }
return block;
}
/**
- * 存在 sink 时发射一条事件。
+ * 通过旁路事件桥发射事件。
+ *
+ * 这里发射的是 Easy-Agents 对调用方的监察/交互事件,不是 AgentScope
+ * 主线路消息。主线路的 tool_call/tool_result 顺序仍由 AgentScope 自己维护。
*
* @param event 事件
*/
private void emit(AgentRuntimeEvent event) {
- if (eventSink != null && event != null) {
- Sinks.EmitResult result = eventSink.tryEmitNext(event);
- if (result.isFailure()) {
- throw new AgentRuntimeException("Failed to emit agent runtime event: " + result);
- }
- }
+ currentEventBridge().emit(event);
}
/**
- * 构建工具调用事件。
+ * 构建工具执行开始事件。
+ * 普通工具调用由 AgentScope stream 的 ToolUseBlock 表达,这里仅为已激活 Skill 发射步骤事件。
*
* @param block 工具使用块
- * @return 运行时事件
+ * @return Skill 步骤事件,普通工具返回 null
*/
- private AgentRuntimeEvent toolCallEvent(ToolUseBlock block) {
+ private AgentRuntimeEvent toolExecutionStartEvent(ToolUseBlock block) {
AgentSkillBinding activeBinding = activeSkillBinding();
- AgentRuntimeEvent event = baseEvent(activeBinding == null
- ? AgentRuntimeEventType.TOOL_CALL
- : AgentRuntimeEventType.SKILL_STEP);
+ if (!emitSkillStep || activeBinding == null) {
+ return null;
+ }
+ AgentRuntimeEvent event = baseEvent(AgentRuntimeEventType.SKILL_STEP);
if (block != null) {
event.setToolCallId(block.getId());
event.getPayload().put("name", block.getName());
@@ -359,6 +494,7 @@ public class AgentScopeToolAdapter {
* @return 工具审批事件
*/
private AgentRuntimeEvent toolApprovalRequiredEvent(ToolUseBlock toolUseBlock, AgentPendingState pendingState) {
+ AgentRuntimeExecutionContext currentRequest = currentRequest();
AgentRuntimeEvent event = baseEvent(AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED);
if (pendingState != null && pendingState.getToolCallId() != null) {
event.setToolCallId(pendingState.getToolCallId());
@@ -367,8 +503,8 @@ public class AgentScopeToolAdapter {
payload.put("resumeToken", pendingState == null || pendingState.getResumeToken() == null
? UUID.randomUUID().toString()
: pendingState.getResumeToken().getValue());
- payload.put("sessionId", request.getSessionId());
- payload.put("agentId", request.getAgentDefinition().getAgentId());
+ payload.put("sessionId", currentRequest.getSessionId());
+ payload.put("agentId", currentRequest.getAgentDefinition().getAgentId());
payload.put("approvalPrompt", approvalPrompt(pendingState == null ? null : pendingState.getApprovalPrompt()));
payload.put("approvalMetadata", pendingState == null ? new LinkedHashMap<>() : pendingState.getMetadata());
payload.put("toolInput", pendingState == null ? new LinkedHashMap<>() : pendingState.getToolInput());
@@ -404,11 +540,8 @@ public class AgentScopeToolAdapter {
* @return 运行时事件
*/
private AgentRuntimeEvent baseEvent(AgentRuntimeEventType type) {
- AgentRuntimeEvent event = AgentRuntimeEvent.of(type);
- event.setTraceId(request.getTraceId());
- event.setSessionId(request.getSessionId());
- event.setAgentId(request.getAgentDefinition().getAgentId());
- event.getMetadata().put("requestId", request.getRequestId());
+ AgentRuntimeExecutionContext currentRequest = currentRequest();
+ AgentRuntimeEvent event = currentEventBridge().event(type);
event.getMetadata().put("toolCategory", toolSpec.getCategory().name());
event.getMetadata().put("visibility", toolSpec.getVisibility().name());
appendSkillPayload(event.getMetadata(), activeSkillBinding());
@@ -455,5 +588,16 @@ public class AgentScopeToolAdapter {
Object success = event.getMetadata().get("success");
return !(success instanceof Boolean) || Boolean.TRUE.equals(success);
}
+
+ private AgentRuntimeExecutionContext currentRequest() {
+ return turnContextHolder == null ? request : turnContextHolder.executionContext(request);
+ }
+
+ private AgentRuntimeEventBridge currentEventBridge() {
+ if (turnContextHolder != null && turnContextHolder.eventBridge().isPresent()) {
+ return turnContextHolder.eventBridge().get();
+ }
+ return new AgentRuntimeEventBridge(request, turnContextHolder);
+ }
}
}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeEventBridge.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeEventBridge.java
new file mode 100644
index 0000000..d377a65
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeEventBridge.java
@@ -0,0 +1,130 @@
+package com.easyagents.agent.runtime.event;
+
+import com.easyagents.agent.runtime.AgentRuntimeException;
+import com.easyagents.agent.runtime.AgentRuntimeExecutionContext;
+import reactor.core.publisher.Sinks;
+
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * AgentScope 运行时的旁路事件桥。
+ *
+ * 该类只负责 Easy-Agents 自己的旁路监察事件,不负责 AgentScope 主线路事件。
+ * 主线路事件来自 {@code agent.stream(...)},会进入模型会话、工具结果和最终输出的正常顺序;
+ * 旁路事件只给调用方观察运行过程,例如知识库检索、自动上下文压缩、工具审批和 Skill 步骤。
+ *
+ * 旁路事件不会写入 AgentScope memory/session,也不会修改 AgentScope {@code Msg}。仅作观察与展示使用
+ */
+public class AgentRuntimeEventBridge {
+
+ private final AgentRuntimeExecutionContext fallbackContext;
+ private final AgentRuntimeTurnContextHolder turnContextHolder;
+
+ /**
+ * 创建旁路事件桥。
+ *
+ * @param fallbackContext 运行时级上下文,当前轮次未设置时作为身份信息来源
+ * @param turnContextHolder 当前运行轮次上下文持有器
+ */
+ public AgentRuntimeEventBridge(AgentRuntimeExecutionContext fallbackContext,
+ AgentRuntimeTurnContextHolder turnContextHolder) {
+ this.fallbackContext = fallbackContext;
+ this.turnContextHolder = turnContextHolder;
+ }
+
+ /**
+ * 创建固定 sink 的旁路事件桥,主要用于旧测试和旧辅助构造路径。
+ *
+ * @param fallbackContext 运行时级上下文
+ * @param eventSink 旁路事件 sink
+ * @return 旁路事件桥
+ */
+ public static AgentRuntimeEventBridge fixed(AgentRuntimeExecutionContext fallbackContext,
+ Sinks.Many eventSink) {
+ AgentRuntimeTurnContextHolder holder = new AgentRuntimeTurnContextHolder();
+ AgentRuntimeEventBridge bridge = new AgentRuntimeEventBridge(fallbackContext, holder);
+ holder.set(new AgentRuntimeTurnContext(null, eventSink, bridge));
+ return bridge;
+ }
+
+ /**
+ * 创建指定类型的旁路事件,并自动补齐公共运行身份信息。
+ *
+ * @param type 旁路事件类型
+ * @return 已补齐公共字段的运行时事件
+ */
+ public AgentRuntimeEvent event(AgentRuntimeEventType type) {
+ AgentRuntimeEvent event = AgentRuntimeEvent.of(type);
+ enrich(event);
+ return event;
+ }
+
+ /**
+ * 发射一条旁路事件。
+ *
+ * 当前没有运行轮次或没有旁路 sink 时直接忽略。这允许 adapter 在非流式测试、
+ * 初始化阶段或主线路未建立旁路订阅时保持无副作用。
+ *
+ * @param event 旁路事件
+ */
+ public void emit(AgentRuntimeEvent event) {
+ if (event == null) {
+ return;
+ }
+ Optional> sink = eventSink();
+ if (sink.isEmpty()) {
+ return;
+ }
+ enrich(event);
+ Sinks.EmitResult result = sink.get().tryEmitNext(event);
+ if (result.isFailure()) {
+ throw new AgentRuntimeException("Failed to emit agent runtime side event: " + result);
+ }
+ }
+
+ /**
+ * 获取当前轮次合并后的执行上下文。
+ *
+ * @return 当前执行上下文
+ */
+ public AgentRuntimeExecutionContext executionContext() {
+ return turnContextHolder == null ? fallbackContext : turnContextHolder.executionContext(fallbackContext);
+ }
+
+ /**
+ * 补齐旁路事件的公共身份信息。
+ *
+ * @param event 旁路事件
+ */
+ public void enrich(AgentRuntimeEvent event) {
+ if (event == null) {
+ return;
+ }
+ AgentRuntimeExecutionContext context = executionContext();
+ if (context == null) {
+ return;
+ }
+ if (event.getTraceId() == null || event.getTraceId().isBlank()) {
+ event.setTraceId(context.getTraceId());
+ }
+ if (event.getSessionId() == null || event.getSessionId().isBlank()) {
+ event.setSessionId(context.getSessionId());
+ }
+ if ((event.getAgentId() == null || event.getAgentId().isBlank())
+ && context.getAgentDefinition() != null) {
+ event.setAgentId(context.getAgentDefinition().getAgentId());
+ }
+ if (context.getRequestId() != null && !context.getRequestId().isBlank()) {
+ event.getMetadata().putIfAbsent("requestId", context.getRequestId());
+ }
+ Map metadata = context.getMetadata();
+ if (metadata != null && !metadata.isEmpty()) {
+ metadata.forEach(event.getMetadata()::putIfAbsent);
+ }
+ }
+
+ private Optional> eventSink() {
+ return turnContextHolder == null ? Optional.empty() : turnContextHolder.eventSink();
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeEventType.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeEventType.java
index 0b6e20e..2a5bc1a 100644
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeEventType.java
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeEventType.java
@@ -14,6 +14,16 @@ public enum AgentRuntimeEventType {
*/
REASONING_DELTA,
+ /**
+ * 智能体开始一次模型推理。
+ */
+ REASONING_STARTED,
+
+ /**
+ * 智能体完成一次模型推理。
+ */
+ REASONING_COMPLETED,
+
/**
* 流式输出内容,用于流式展示聊天内容。
*/
@@ -34,6 +44,16 @@ public enum AgentRuntimeEventType {
*/
KNOWLEDGE_RETRIEVAL,
+ /**
+ * 自动上下文压缩已开始。
+ */
+ MEMORY_COMPRESSION_STARTED,
+
+ /**
+ * 自动上下文压缩已完成。
+ */
+ MEMORY_COMPRESSION_COMPLETED,
+
/**
* 工具执行前需要人工审批。
*/
@@ -64,6 +84,11 @@ public enum AgentRuntimeEventType {
*/
COMPLETED,
+ /**
+ * 智能体运行已暂停,等待外部输入后继续。
+ */
+ SUSPENDED,
+
/**
* 智能体运行失败。
*/
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeInterceptor.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeInterceptor.java
new file mode 100644
index 0000000..65234f2
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeInterceptor.java
@@ -0,0 +1,34 @@
+package com.easyagents.agent.runtime.event;
+
+import io.agentscope.core.hook.HookEvent;
+import reactor.core.publisher.Mono;
+
+/**
+ * AgentScope Hook 事件的主线路干预器。对接 AgentScope 的原生生命周期hook
+ *
+ * 干预器允许修改 AgentScope HookEvent,因此会影响主线路执行。
+ * 典型场景包括 AutoContext 在推理前改写输入消息,或后续 HITL 在推理后调用
+ * {@code stopAgent()} 暂停工具执行。普通运行状态通知应使用 {@link AgentRuntimeObserver}。
+ * 警告:本干预器会影响到主线路agent 交互,谨慎使用
+ *
+ */
+public interface AgentRuntimeInterceptor {
+
+ /**
+ * 处理并返回可能被修改的 AgentScope Hook 事件。
+ *
+ * @param event AgentScope Hook 事件
+ * @param Hook 事件类型
+ * @return 处理后的 Hook 事件
+ */
+ Mono intercept(T event);
+
+ /**
+ * 获取干预器执行优先级。
+ *
+ * @return 优先级,数值越小越先执行
+ */
+ default int priority() {
+ return 100;
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeObservationManager.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeObservationManager.java
new file mode 100644
index 0000000..77d76a8
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeObservationManager.java
@@ -0,0 +1,72 @@
+package com.easyagents.agent.runtime.event;
+
+import io.agentscope.core.hook.HookEvent;
+import reactor.core.publisher.Mono;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * AgentScope Hook 事件的统一观察和干预调度器。
+ *
+ * 处理顺序固定为:先执行干预器,再执行观察器。干预器属于主线路能力,
+ * 可以修改 AgentScope HookEvent;观察器属于旁线路能力,只能查看事件并通过
+ * {@link AgentRuntimeEventBridge} 发射对外监察事件。
+ */
+public class AgentRuntimeObservationManager {
+
+ private final List interceptors;
+ private final List observers;
+
+ /**
+ * 创建观察调度器。
+ *
+ * @param interceptors 主线路干预器
+ * @param observers 旁路观察器
+ */
+ public AgentRuntimeObservationManager(List interceptors,
+ List observers) {
+ this.interceptors = sortInterceptors(interceptors);
+ this.observers = sortObservers(observers);
+ }
+
+ /**
+ * 创建空观察调度器。
+ *
+ * @return 空调度器
+ */
+ public static AgentRuntimeObservationManager empty() {
+ return new AgentRuntimeObservationManager(List.of(), List.of());
+ }
+
+ /**
+ * 处理 AgentScope Hook 事件。
+ *
+ * @param event Hook 事件
+ * @param Hook 事件类型
+ * @return 处理后的 Hook 事件
+ */
+ public Mono handle(T event) {
+ Mono chain = Mono.just(event);
+ for (AgentRuntimeInterceptor interceptor : interceptors) {
+ chain = chain.flatMap(interceptor::intercept);
+ }
+ for (AgentRuntimeObserver observer : observers) {
+ chain = chain.flatMap(current -> observer.observe(current).thenReturn(current));
+ }
+ return chain;
+ }
+
+ private List sortInterceptors(List source) {
+ List sorted = new ArrayList<>(source == null ? List.of() : source);
+ sorted.sort(Comparator.comparingInt(AgentRuntimeInterceptor::priority));
+ return List.copyOf(sorted);
+ }
+
+ private List sortObservers(List source) {
+ List sorted = new ArrayList<>(source == null ? List.of() : source);
+ sorted.sort(Comparator.comparingInt(AgentRuntimeObserver::priority));
+ return List.copyOf(sorted);
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeObserver.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeObserver.java
new file mode 100644
index 0000000..4158583
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeObserver.java
@@ -0,0 +1,31 @@
+package com.easyagents.agent.runtime.event;
+
+import io.agentscope.core.hook.HookEvent;
+import reactor.core.publisher.Mono;
+
+/**
+ * AgentScope Hook 事件的旁路观察器。对接 AgentScope 的原生生命周期hook
+ *
+ * 观察器只做监察和旁路事件发射,不允许修改 AgentScope HookEvent。
+ * 如果能力需要影响主线路,例如修改输入消息、暂停 agent 或替换工具结果,应实现
+ * {@link AgentRuntimeInterceptor}。
+ */
+public interface AgentRuntimeObserver {
+
+ /**
+ * 观察 AgentScope Hook 事件。
+ *
+ * @param event AgentScope Hook 事件
+ * @return 完成信号
+ */
+ Mono observe(HookEvent event);
+
+ /**
+ * 获取观察器执行优先级。
+ *
+ * @return 优先级,数值越小越先执行
+ */
+ default int priority() {
+ return 100;
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeTurnContext.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeTurnContext.java
new file mode 100644
index 0000000..de79974
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeTurnContext.java
@@ -0,0 +1,137 @@
+package com.easyagents.agent.runtime.event;
+
+import com.easyagents.agent.runtime.AgentRuntimeExecutionContext;
+import reactor.core.publisher.Sinks;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * AgentScope 运行时的单轮执行上下文。
+ */
+public class AgentRuntimeTurnContext {
+
+ /**
+ * 本轮运行上下文。
+ */
+ private final AgentRuntimeExecutionContext executionContext;
+
+ /**
+ * 本轮旁路事件 sink。
+ *
+ * 该字段只承载旁线路监察事件,不承载 AgentScope 主线路 stream 事件。
+ */
+ private final Sinks.Many eventSink;
+
+ /**
+ * 本轮旁路事件桥。
+ *
+ * adapter 和 observer 应优先通过 bridge 发射旁路事件,避免各处重复拼接
+ * trace/session/request 等公共字段。
+ */
+ private final AgentRuntimeEventBridge eventBridge;
+
+ /**
+ * 创建单轮执行上下文。
+ *
+ * @param executionContext 本轮运行上下文
+ * @param eventSink 本轮旁路事件 sink
+ */
+ public AgentRuntimeTurnContext(AgentRuntimeExecutionContext executionContext,
+ Sinks.Many eventSink) {
+ this(executionContext, eventSink, null);
+ }
+
+ /**
+ * 创建单轮执行上下文。
+ *
+ * @param executionContext 本轮运行上下文
+ * @param eventSink 本轮旁路事件 sink
+ * @param eventBridge 本轮旁路事件桥
+ */
+ public AgentRuntimeTurnContext(AgentRuntimeExecutionContext executionContext,
+ Sinks.Many eventSink,
+ AgentRuntimeEventBridge eventBridge) {
+ this.executionContext = executionContext;
+ this.eventSink = eventSink;
+ this.eventBridge = eventBridge;
+ }
+
+ /**
+ * 获取本轮运行上下文。
+ *
+ * @return 本轮运行上下文
+ */
+ public AgentRuntimeExecutionContext getExecutionContext() {
+ return executionContext;
+ }
+
+ /**
+ * 获取本轮旁路事件 sink。
+ *
+ * @return 本轮旁路事件 sink
+ */
+ public Sinks.Many getEventSink() {
+ return eventSink;
+ }
+
+ /**
+ * 获取本轮旁路事件桥。
+ *
+ * @return 本轮旁路事件桥
+ */
+ public AgentRuntimeEventBridge getEventBridge() {
+ return eventBridge;
+ }
+
+ /**
+ * 构建以当前轮信息覆盖运行时信息后的上下文。
+ *
+ * @param fallback 运行时级上下文
+ * @return 当前轮上下文,未设置时返回运行时级上下文
+ */
+ public AgentRuntimeExecutionContext mergeWith(AgentRuntimeExecutionContext fallback) {
+ if (executionContext == null) {
+ return fallback;
+ }
+ AgentRuntimeExecutionContext merged = new AgentRuntimeExecutionContext();
+ merged.setRequestId(firstNonBlank(executionContext.getRequestId(), fallback == null ? null : fallback.getRequestId()));
+ merged.setTraceId(firstNonBlank(executionContext.getTraceId(), fallback == null ? null : fallback.getTraceId()));
+ merged.setSessionId(firstNonBlank(executionContext.getSessionId(), fallback == null ? null : fallback.getSessionId()));
+ merged.setAgentDefinition(executionContext.getAgentDefinition() == null && fallback != null
+ ? fallback.getAgentDefinition()
+ : executionContext.getAgentDefinition());
+ merged.setRuntimeContext(executionContext.getRuntimeContext() == null && fallback != null
+ ? fallback.getRuntimeContext()
+ : executionContext.getRuntimeContext());
+ merged.setUserMessage(executionContext.getUserMessage());
+ merged.setMemorySnapshot(executionContext.getMemorySnapshot());
+ merged.setToolInvokers(fallback == null ? executionContext.getToolInvokers() : fallback.getToolInvokers());
+ merged.setKnowledgeRetrievers(fallback == null
+ ? executionContext.getKnowledgeRetrievers()
+ : fallback.getKnowledgeRetrievers());
+ merged.setSessionStore(fallback == null ? executionContext.getSessionStore() : fallback.getSessionStore());
+ merged.setConversationRecorder(fallback == null
+ ? executionContext.getConversationRecorder()
+ : fallback.getConversationRecorder());
+ merged.setMetadata(mergedMetadata(fallback, executionContext));
+ merged.setCancelReason(executionContext.getCancelReason());
+ return merged;
+ }
+
+ private Map mergedMetadata(AgentRuntimeExecutionContext fallback,
+ AgentRuntimeExecutionContext current) {
+ Map metadata = new LinkedHashMap<>();
+ if (fallback != null && fallback.getMetadata() != null) {
+ metadata.putAll(fallback.getMetadata());
+ }
+ if (current != null && current.getMetadata() != null) {
+ metadata.putAll(current.getMetadata());
+ }
+ return metadata;
+ }
+
+ private String firstNonBlank(String first, String second) {
+ return first == null || first.isBlank() ? second : first;
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeTurnContextHolder.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeTurnContextHolder.java
new file mode 100644
index 0000000..5ede50b
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeTurnContextHolder.java
@@ -0,0 +1,74 @@
+package com.easyagents.agent.runtime.event;
+
+import com.easyagents.agent.runtime.AgentRuntimeExecutionContext;
+import reactor.core.publisher.Sinks;
+
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * 保存当前运行轮次上下文。
+ */
+public class AgentRuntimeTurnContextHolder {
+
+ private final AtomicReference current = new AtomicReference<>();
+
+ /**
+ * 设置当前运行轮次上下文。
+ *
+ * @param context 当前轮次上下文
+ */
+ public void set(AgentRuntimeTurnContext context) {
+ current.set(context);
+ }
+
+ /**
+ * 清理当前运行轮次上下文。
+ */
+ public void clear() {
+ current.set(null);
+ }
+
+ /**
+ * 获取当前运行轮次上下文。
+ *
+ * @return 当前轮次上下文
+ */
+ public Optional current() {
+ return Optional.ofNullable(current.get());
+ }
+
+ /**
+ * 获取当前轮次事件 sink。
+ *
+ * @return 当前轮次旁路事件 sink
+ */
+ public Optional> eventSink() {
+ return current()
+ .map(AgentRuntimeTurnContext::getEventSink)
+ .filter(sink -> sink != null);
+ }
+
+ /**
+ * 获取当前轮次旁路事件桥。
+ *
+ * @return 当前轮次旁路事件桥
+ */
+ public Optional eventBridge() {
+ return current()
+ .map(AgentRuntimeTurnContext::getEventBridge)
+ .filter(bridge -> bridge != null);
+ }
+
+ /**
+ * 合并当前轮次上下文和运行时级上下文。
+ *
+ * @param fallback 运行时级上下文
+ * @return 合并后的上下文
+ */
+ public AgentRuntimeExecutionContext executionContext(AgentRuntimeExecutionContext fallback) {
+ return current()
+ .map(context -> context.mergeWith(fallback))
+ .orElse(fallback);
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/interceptor/AutoContextInterceptor.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/interceptor/AutoContextInterceptor.java
new file mode 100644
index 0000000..5261fe3
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/interceptor/AutoContextInterceptor.java
@@ -0,0 +1,357 @@
+package com.easyagents.agent.runtime.event.interceptor;
+
+import com.easyagents.agent.runtime.AgentRuntimeException;
+import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
+import com.easyagents.agent.runtime.event.AgentRuntimeEventBridge;
+import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
+import com.easyagents.agent.runtime.event.AgentRuntimeInterceptor;
+import io.agentscope.core.ReActAgent;
+import io.agentscope.core.agent.Agent;
+import io.agentscope.core.hook.HookEvent;
+import io.agentscope.core.hook.PreCallEvent;
+import io.agentscope.core.hook.PreReasoningEvent;
+import io.agentscope.core.memory.Memory;
+import io.agentscope.core.memory.autocontext.*;
+import io.agentscope.core.message.Msg;
+import io.agentscope.core.message.MsgRole;
+import io.agentscope.core.message.TextBlock;
+import io.agentscope.core.plan.PlanNotebook;
+import io.agentscope.core.tool.Toolkit;
+import reactor.core.publisher.Mono;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * AutoContext 主线路干预器。
+ *
+ * 特殊约束:本干预器是对 AgentScope 官方
+ * {@code AutoContextHook} 的替代实现,不能与官方 {@code AutoContextHook} 同时注册。
+ * 两者同时存在会导致 {@link AutoContextMemory#compressIfNeeded()}、上下文重写以及
+ * {@link ContextOffloadTool} 注册被重复执行。
+ *
+ * 本类承担两类职责。第一类是主线路干预:在 {@link PreCallEvent} 中注册
+ * AutoContext 工具能力,在 {@link PreReasoningEvent} 中触发记忆压缩并改写
+ * LLM 输入消息。第二类是旁路通知:通过 {@link AgentRuntimeEventBridge} 发出
+ * {@link AgentRuntimeEventType#MEMORY_COMPRESSION_STARTED} 和
+ * {@link AgentRuntimeEventType#MEMORY_COMPRESSION_COMPLETED},这些事件只用于调用方展示,
+ * 不写入 AgentScope memory/session,也不参与 LLM 会话协议。
+ */
+public class AutoContextInterceptor implements AgentRuntimeInterceptor {
+
+ private static final String STATUS_KEY = "memory-compression";
+ private static final String AUTO_CONTEXT_SYSTEM_INSTRUCTION =
+ "You may see compressed messages containing .\n"
+ + "- Use the UUID to call context_reload if you need full details.\n"
+ + "- NEVER mention, quote, or refer to UUIDs, offload tags, or internal metadata in your response.";
+
+ private final AgentRuntimeEventBridge eventBridge;
+ private final AutoContextConfig autoContextConfig;
+ private final AtomicBoolean registered = new AtomicBoolean(false);
+
+ /**
+ * 创建 AutoContext 主线路干预器。
+ *
+ * 传入的 {@link AutoContextConfig} 必须是创建目标 {@link AutoContextMemory}
+ * 时使用的同一份配置。这样压缩开始事件的触发条件才能与 AgentScope
+ * {@code compressIfNeeded()} 的入口判断保持一致。
+ *
+ * @param eventBridge 旁路事件桥
+ * @param autoContextConfig AutoContext 配置
+ */
+ public AutoContextInterceptor(AgentRuntimeEventBridge eventBridge,
+ AutoContextConfig autoContextConfig) {
+ this.eventBridge = eventBridge;
+ this.autoContextConfig = autoContextConfig;
+ }
+
+ /**
+ * 处理 AutoContext 相关 AgentScope Hook 事件。
+ *
+ * @param event AgentScope Hook 事件
+ * @param Hook 事件类型
+ * @return 处理后的 Hook 事件
+ */
+ @Override
+ public Mono intercept(T event) {
+ if (event instanceof PreCallEvent preCallEvent) {
+ @SuppressWarnings("unchecked")
+ Mono result = (Mono) handlePreCall(preCallEvent);
+ return result;
+ }
+ if (event instanceof PreReasoningEvent preReasoningEvent) {
+ @SuppressWarnings("unchecked")
+ Mono result = (Mono) handlePreReasoning(preReasoningEvent);
+ return result;
+ }
+ return Mono.just(event);
+ }
+
+ /**
+ * 获取干预器优先级。
+ *
+ * @return 优先级,保持与官方 AutoContextHook 一致
+ */
+ @Override
+ public int priority() {
+ return 0;
+ }
+
+ /**
+ * 判断 AutoContext 工具集成是否已经注册。
+ *
+ * @return 已注册时返回 true
+ */
+ public boolean isRegistered() {
+ return registered.get();
+ }
+
+ /**
+ * 处理调用前事件,注册 AutoContext 的上下文重载工具和计划本。
+ *
+ * @param event 调用前事件
+ * @return 原事件
+ */
+ private Mono handlePreCall(PreCallEvent event) {
+ if (registered.get()) {
+ return Mono.just(event);
+ }
+ Agent agent = event.getAgent();
+ if (!(agent instanceof ReActAgent reActAgent)) {
+ return Mono.just(event);
+ }
+ Memory memory = reActAgent.getMemory();
+ if (!(memory instanceof AutoContextMemory autoContextMemory)) {
+ return Mono.just(event);
+ }
+ if (!registered.compareAndSet(false, true)) {
+ return Mono.just(event);
+ }
+ try {
+ Toolkit toolkit = reActAgent.getToolkit();
+ if (toolkit != null) {
+ toolkit.registerTool(new ContextOffloadTool(autoContextMemory));
+ }
+ PlanNotebook planNotebook = reActAgent.getPlanNotebook();
+ if (planNotebook != null) {
+ autoContextMemory.attachPlanNote(planNotebook);
+ }
+ } catch (Exception e) {
+ registered.set(false);
+ throw new AgentRuntimeException("Failed to register AutoContext integration.", e);
+ }
+ return Mono.just(event);
+ }
+
+ /**
+ * 处理推理前事件,触发 AutoContext 压缩并改写 LLM 输入消息。
+ *
+ * 这是主线路干预逻辑:{@code compressIfNeeded()} 和 {@code setInputMessages(...)}
+ * 会影响 AgentScope 本次 reasoning 输入。压缩开始/完成事件则是旁路通知,只发给调用方。
+ *
+ * @param event 推理前事件
+ * @return 改写输入消息后的事件
+ */
+ private Mono handlePreReasoning(PreReasoningEvent event) {
+ Agent agent = event.getAgent();
+ if (!(agent instanceof ReActAgent reActAgent)) {
+ return Mono.just(event);
+ }
+ Memory memory = reActAgent.getMemory();
+ if (!(memory instanceof AutoContextMemory autoContextMemory)) {
+ return Mono.just(event);
+ }
+ // 判断是否达到压缩条件
+ CompressionCheck compressionCheck = compressionCheck(autoContextMemory.getMessages());
+ int beforeEventCount = compressionEventCount(autoContextMemory);
+ if (compressionCheck.thresholdReached()) {
+ boolean compressed = autoContextMemory.compressIfNeeded();
+ List newEvents = newCompressionEvents(autoContextMemory, beforeEventCount);
+ if (compressed && !newEvents.isEmpty()) {
+ emitCompressionStarted(compressionCheck);
+ emitCompressionCompleted(compressionCheck, newEvents);
+ }
+ }
+ // 压缩完毕后自动进行当前的会话
+ event.setInputMessages(buildInputMessages(event, autoContextMemory));
+ return Mono.just(event);
+ }
+
+ /**
+ * 计算当前记忆是否达到 AutoContext 官方压缩入口条件。
+ *
+ * @param messages 当前工作记忆消息
+ * @return 压缩入口检查结果
+ */
+ private CompressionCheck compressionCheck(List messages) {
+ List safeMessages = messages == null ? List.of() : messages;
+ int messageCount = safeMessages.size();
+ int tokenCount = TokenCounterUtil.calculateToken(safeMessages);
+ int thresholdMessageCount = autoContextConfig == null ? Integer.MAX_VALUE : autoContextConfig.getMsgThreshold();
+ long thresholdTokenCount = autoContextConfig == null
+ ? Long.MAX_VALUE
+ : (long) (autoContextConfig.getMaxToken() * autoContextConfig.getTokenRatio());
+ boolean messageThresholdReached = messageCount >= thresholdMessageCount;
+ boolean tokenThresholdReached = tokenCount >= thresholdTokenCount;
+ return new CompressionCheck(messageCount, tokenCount, thresholdMessageCount, thresholdTokenCount,
+ messageThresholdReached, tokenThresholdReached);
+ }
+
+ /**
+ * 获取当前压缩事件数量。
+ *
+ * @param autoContextMemory AutoContext 记忆
+ * @return 压缩事件数量
+ */
+ private int compressionEventCount(AutoContextMemory autoContextMemory) {
+ List events = autoContextMemory.getCompressionEvents();
+ return events == null ? 0 : events.size();
+ }
+
+ /**
+ * 获取本次压缩新增的 AgentScope 压缩事件。
+ *
+ * @param autoContextMemory AutoContext 记忆
+ * @param beforeEventCount 压缩前事件数量
+ * @return 新增压缩事件
+ */
+ private List newCompressionEvents(AutoContextMemory autoContextMemory, int beforeEventCount) {
+ List events = autoContextMemory.getCompressionEvents();
+ if (events == null || events.size() <= beforeEventCount) {
+ return List.of();
+ }
+ return new ArrayList<>(events.subList(beforeEventCount, events.size()));
+ }
+
+ /**
+ * 构建 AutoContext 压缩后传给 LLM 的输入消息。
+ *
+ * @param event 推理前事件
+ * @param autoContextMemory AutoContext 记忆
+ * @return 更新后的输入消息
+ */
+ private List buildInputMessages(PreReasoningEvent event, AutoContextMemory autoContextMemory) {
+ List originalInputMessages = event.getInputMessages();
+ List newInputMessages = new ArrayList<>();
+ if (!originalInputMessages.isEmpty() && originalInputMessages.get(0).getRole() == MsgRole.SYSTEM) {
+ Msg originalSystemMsg = originalInputMessages.get(0);
+ String originalSystemText = originalSystemMsg.getTextContent();
+ String newSystemText = originalSystemText != null
+ ? originalSystemText + "\n\n" + AUTO_CONTEXT_SYSTEM_INSTRUCTION
+ : AUTO_CONTEXT_SYSTEM_INSTRUCTION;
+ newInputMessages.add(Msg.builder()
+ .role(MsgRole.SYSTEM)
+ .name(originalSystemMsg.getName())
+ .content(TextBlock.builder().text(newSystemText).build())
+ .metadata(originalSystemMsg.getMetadata())
+ .build());
+ } else {
+ newInputMessages.add(Msg.builder()
+ .role(MsgRole.SYSTEM)
+ .name("system")
+ .content(TextBlock.builder().text(AUTO_CONTEXT_SYSTEM_INSTRUCTION).build())
+ .build());
+ }
+ newInputMessages.addAll(autoContextMemory.getMessages());
+ return newInputMessages;
+ }
+
+ /**
+ * 发射上下文压缩开始旁路事件。
+ *
+ * @param compressionCheck 压缩入口检查结果
+ */
+ private void emitCompressionStarted(CompressionCheck compressionCheck) {
+ AgentRuntimeEvent event = eventBridge.event(AgentRuntimeEventType.MEMORY_COMPRESSION_STARTED);
+ event.getPayload().put("statusKey", STATUS_KEY);
+ event.getPayload().put("phase", "started");
+ event.getPayload().put("status", "running");
+ event.getPayload().put("label", "正在整理上下文");
+ putCompressionCheckPayload(event, compressionCheck);
+ eventBridge.emit(event);
+ }
+
+ /**
+ * 发射上下文压缩完成旁路事件。
+ *
+ * @param compressionCheck 压缩入口检查结果
+ * @param events 新增压缩事件
+ */
+ private void emitCompressionCompleted(CompressionCheck compressionCheck,
+ List events) {
+ AgentRuntimeEvent event = eventBridge.event(AgentRuntimeEventType.MEMORY_COMPRESSION_COMPLETED);
+ event.getPayload().put("statusKey", STATUS_KEY);
+ event.getPayload().put("phase", "completed");
+ event.getPayload().put("status", "done");
+ event.getPayload().put("label", "已整理上下文");
+ event.getPayload().put("compressed", true);
+ event.getPayload().put("eventCount", events == null ? 0 : events.size());
+ event.getPayload().put("events", toPayloadEvents(events));
+ putCompressionCheckPayload(event, compressionCheck);
+ eventBridge.emit(event);
+ }
+
+ /**
+ * 填充压缩入口判断相关载荷。
+ *
+ * @param event 运行时事件
+ * @param compressionCheck 压缩入口检查结果
+ */
+ private void putCompressionCheckPayload(AgentRuntimeEvent event, CompressionCheck compressionCheck) {
+ event.getPayload().put("messageCount", compressionCheck.messageCount());
+ event.getPayload().put("tokenCount", compressionCheck.tokenCount());
+ event.getPayload().put("thresholdMessageCount", compressionCheck.thresholdMessageCount());
+ event.getPayload().put("thresholdTokenCount", compressionCheck.thresholdTokenCount());
+ event.getPayload().put("messageThresholdReached", compressionCheck.messageThresholdReached());
+ event.getPayload().put("tokenThresholdReached", compressionCheck.tokenThresholdReached());
+ }
+
+ /**
+ * 将 AgentScope 压缩事件转换为旁路事件载荷。
+ *
+ * @param events AgentScope 压缩事件
+ * @return 压缩事件载荷
+ */
+ private List> toPayloadEvents(List events) {
+ if (events == null || events.isEmpty()) {
+ return List.of();
+ }
+ List> payloadEvents = new ArrayList<>();
+ for (CompressionEvent event : events) {
+ Map payload = new LinkedHashMap<>();
+ payload.put("eventType", event.getEventType());
+ payload.put("timestamp", event.getTimestamp());
+ payload.put("compressedMessageCount", event.getCompressedMessageCount());
+ payload.put("previousMessageId", event.getPreviousMessageId());
+ payload.put("nextMessageId", event.getNextMessageId());
+ payload.put("compressedMessageId", event.getCompressedMessageId());
+ payload.put("tokenBefore", event.getTokenBefore());
+ payload.put("tokenAfter", event.getTokenAfter());
+ payload.put("tokenReduction", event.getTokenReduction());
+ payload.put("inputToken", event.getCompressInputToken());
+ payload.put("outputToken", event.getCompressOutputToken());
+ payloadEvents.add(payload);
+ }
+ return payloadEvents;
+ }
+
+ private record CompressionCheck(int messageCount,
+ int tokenCount,
+ int thresholdMessageCount,
+ long thresholdTokenCount,
+ boolean messageThresholdReached,
+ boolean tokenThresholdReached) {
+
+ /**
+ * 判断是否达到 AutoContext 官方压缩入口条件。
+ *
+ * @return 达到任一阈值时返回 true
+ */
+ private boolean thresholdReached() {
+ return messageThresholdReached || tokenThresholdReached;
+ }
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/interceptor/ToolHitlInterceptor.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/interceptor/ToolHitlInterceptor.java
new file mode 100644
index 0000000..f13c747
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/interceptor/ToolHitlInterceptor.java
@@ -0,0 +1,198 @@
+package com.easyagents.agent.runtime.event.interceptor;
+
+import com.easyagents.agent.runtime.AgentRuntimeExecutionContext;
+import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
+import com.easyagents.agent.runtime.event.AgentRuntimeEventBridge;
+import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
+import com.easyagents.agent.runtime.event.AgentRuntimeInterceptor;
+import com.easyagents.agent.runtime.hitl.AgentPendingState;
+import com.easyagents.agent.runtime.hitl.AgentToolApprovalCoordinator;
+import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
+import com.easyagents.agent.runtime.tool.AgentToolSpec;
+import io.agentscope.core.hook.HookEvent;
+import io.agentscope.core.hook.PostReasoningEvent;
+import io.agentscope.core.message.Msg;
+import io.agentscope.core.message.ToolUseBlock;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 工具 HITL 主线路干预器。
+ *
+ * 本 interceptor 专门处理“工具执行前人工审批”。监听 AgentScope 原生
+ * {@link PostReasoningEvent}
+ *
+ * 这里包含两类动作:
+ *
+ * - 主线路干预:发现待审批工具后调用 {@link PostReasoningEvent#stopAgent()},
+ * 让 AgentScope 返回当前带 ToolUseBlock 的消息并暂停工具执行。
+ * - 旁路交互事件:通过 {@link AgentRuntimeEventBridge} 发出
+ * {@link AgentRuntimeEventType#TOOL_APPROVAL_REQUIRED},通知调用方展示审批交互。
+ *
+ *
+ * 注意:本 interceptor 不执行工具、不写入 AgentScope memory/session,也不实现恢复。
+ * 后续 resume 流程应基于 AgentScope pending tool 状态继续调用 agent stream/call。
+ */
+public class ToolHitlInterceptor implements AgentRuntimeInterceptor {
+
+ private final AgentRuntimeEventBridge eventBridge;
+ private final AgentToolApprovalCoordinator approvalCoordinator;
+ private final Map toolSpecs;
+
+ /**
+ * 创建工具 HITL 干预器。
+ *
+ * @param eventBridge 旁路事件桥
+ * @param approvalCoordinator 工具审批协调器
+ * @param toolSpecs 工具声明列表
+ */
+ public ToolHitlInterceptor(AgentRuntimeEventBridge eventBridge,
+ AgentToolApprovalCoordinator approvalCoordinator,
+ List toolSpecs) {
+ this.eventBridge = eventBridge;
+ this.approvalCoordinator = approvalCoordinator;
+ this.toolSpecs = (toolSpecs == null ? List.of() : toolSpecs).stream()
+ .filter(spec -> spec != null && spec.getName() != null && !spec.getName().isBlank())
+ .collect(Collectors.toMap(AgentToolSpec::getName, Function.identity(), (left, right) -> left,
+ LinkedHashMap::new));
+ }
+
+ /**
+ * 处理 AgentScope Hook 事件。
+ *
+ * @param event AgentScope Hook 事件
+ * @param Hook 事件类型
+ * @return 处理后的 Hook 事件
+ */
+ @Override
+ public Mono intercept(T event) {
+ if (event instanceof PostReasoningEvent postReasoningEvent) {
+ interceptPostReasoning(postReasoningEvent);
+ }
+ return Mono.just(event);
+ }
+
+ /**
+ * 返回执行优先级。
+ *
+ * 工具审批需要在普通观察器发出 reasoning completed 后保持事件已被标记暂停,
+ * 但不应早于 AutoContext 的 PreReasoning 干预。当前值用于主线路 reasoning 后检查。
+ *
+ * @return 优先级
+ */
+ @Override
+ public int priority() {
+ return 50;
+ }
+
+ private void interceptPostReasoning(PostReasoningEvent event) {
+ Msg reasoningMessage = event.getReasoningMessage();
+ if (reasoningMessage == null) {
+ return;
+ }
+ List approvalRequiredTools = approvalRequiredTools(reasoningMessage);
+ if (approvalRequiredTools.isEmpty()) {
+ return;
+ }
+ List> pendingApprovals = new ArrayList<>();
+ for (ToolUseBlock toolUse : approvalRequiredTools) {
+ AgentToolSpec toolSpec = toolSpecs.get(toolUse.getName());
+ AgentPendingState pendingState = registerPendingState(toolSpec, toolUse);
+ AgentRuntimeEvent approvalEvent = toolApprovalRequiredEvent(toolSpec, toolUse, pendingState);
+ pendingState.setEventId(approvalEvent.getEventId());
+ pendingApprovals.add(pendingApprovalPayload(pendingState, toolUse));
+ eventBridge.emit(approvalEvent);
+ }
+ AgentRuntimeExecutionContext context = eventBridge.executionContext();
+ if (context != null) {
+ context.getMetadata().put("hitlSuspended", true);
+ context.getMetadata().put("hitlSuspendReason", "TOOL_APPROVAL_REQUIRED");
+ context.getMetadata().put("hitlPendingApprovals", pendingApprovals);
+ }
+ event.stopAgent();
+ }
+
+ private List approvalRequiredTools(Msg reasoningMessage) {
+ List toolUses = reasoningMessage.getContentBlocks(ToolUseBlock.class);
+ if (toolUses == null || toolUses.isEmpty()) {
+ return List.of();
+ }
+ return toolUses.stream()
+ .filter(toolUse -> {
+ AgentToolSpec toolSpec = toolUse == null ? null : toolSpecs.get(toolUse.getName());
+ return toolSpec != null && toolSpec.isApprovalRequired();
+ })
+ .toList();
+ }
+
+ private AgentPendingState registerPendingState(AgentToolSpec toolSpec, ToolUseBlock toolUse) {
+ AgentRuntimeExecutionContext context = eventBridge.executionContext();
+ AgentToolApprovalRequest approvalRequest = toolSpec.getApprovalRequest();
+ Duration timeout = approvalRequest == null || approvalRequest.getTimeout() == null
+ ? Duration.ofMinutes(30)
+ : approvalRequest.getTimeout();
+ Map metadata = approvalRequest == null
+ ? new LinkedHashMap<>()
+ : new LinkedHashMap<>(approvalRequest.getMetadata());
+ metadata.put("phase", "POST_REASONING");
+ metadata.put("source", "TOOL_HITL_INTERCEPTOR");
+ metadata.putAll(toolUse.getMetadata() == null ? Map.of() : toolUse.getMetadata());
+ return approvalCoordinator.register(
+ context == null ? null : context.getSessionId(),
+ context == null || context.getAgentDefinition() == null ? null : context.getAgentDefinition().getAgentId(),
+ toolUse.getId(),
+ toolUse.getName(),
+ approvalPrompt(approvalRequest),
+ toolUse.getInput(),
+ metadata,
+ Instant.now().plus(timeout));
+ }
+
+ private AgentRuntimeEvent toolApprovalRequiredEvent(AgentToolSpec toolSpec,
+ ToolUseBlock toolUse,
+ AgentPendingState pendingState) {
+ AgentRuntimeExecutionContext context = eventBridge.executionContext();
+ AgentRuntimeEvent event = eventBridge.event(AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED);
+ event.setToolCallId(toolUse.getId());
+ event.getPayload().putAll(pendingApprovalPayload(pendingState, toolUse));
+ event.getPayload().put("sessionId", context == null ? null : context.getSessionId());
+ event.getPayload().put("agentId", context == null || context.getAgentDefinition() == null
+ ? null
+ : context.getAgentDefinition().getAgentId());
+ event.getPayload().put("approvalPrompt", approvalPrompt(toolSpec.getApprovalRequest()));
+ event.getPayload().put("approvalMetadata", pendingState.getMetadata());
+ event.getPayload().put("toolDescription", toolSpec.getDescription());
+ event.getMetadata().put("source", "TOOL_HITL_INTERCEPTOR");
+ event.getMetadata().put("phase", "POST_REASONING");
+ return event;
+ }
+
+ private Map pendingApprovalPayload(AgentPendingState pendingState, ToolUseBlock toolUse) {
+ Map payload = new LinkedHashMap<>();
+ payload.put("resumeToken", pendingState.getResumeToken().getValue());
+ payload.put("toolCallId", pendingState.getToolCallId());
+ payload.put("toolName", pendingState.getToolName());
+ payload.put("toolInput", pendingState.getToolInput());
+ payload.put("input", toolUse.getInput());
+ payload.put("content", toolUse.getContent());
+ payload.put("expiresAt", pendingState.getExpiresAt() == null ? null : pendingState.getExpiresAt().toString());
+ return payload;
+ }
+
+ private String approvalPrompt(AgentToolApprovalRequest approvalRequest) {
+ if (approvalRequest != null
+ && approvalRequest.getApprovalPrompt() != null
+ && !approvalRequest.getApprovalPrompt().isBlank()) {
+ return approvalRequest.getApprovalPrompt();
+ }
+ return "是否批准执行该工具?";
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/observer/AgentRuntimeErrorObserver.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/observer/AgentRuntimeErrorObserver.java
new file mode 100644
index 0000000..dd4917d
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/observer/AgentRuntimeErrorObserver.java
@@ -0,0 +1,62 @@
+package com.easyagents.agent.runtime.event.observer;
+
+import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
+import com.easyagents.agent.runtime.event.AgentRuntimeEventBridge;
+import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
+import com.easyagents.agent.runtime.event.AgentRuntimeObserver;
+import io.agentscope.core.agent.Agent;
+import io.agentscope.core.hook.ErrorEvent;
+import io.agentscope.core.hook.HookEvent;
+import reactor.core.publisher.Mono;
+
+/**
+ * 监听 AgentScope 原生错误事件,并发射运行失败旁路事件。
+ *
+ * 该观察器复用 {@link AgentRuntimeEventType#FAILED},但通过 payload 中的
+ * {@code source=HOOK} 标识它来自 AgentScope 生命周期观察,不等同于 Easy-Agents
+ * runtime 外层流已经完成失败收口。
+ */
+public class AgentRuntimeErrorObserver implements AgentRuntimeObserver {
+
+ private final AgentRuntimeEventBridge eventBridge;
+
+ /**
+ * 创建运行错误观察器。
+ *
+ * @param eventBridge 旁路事件桥
+ */
+ public AgentRuntimeErrorObserver(AgentRuntimeEventBridge eventBridge) {
+ this.eventBridge = eventBridge;
+ }
+
+ /**
+ * 观察 AgentScope 错误事件。
+ *
+ * @param event AgentScope Hook 事件
+ * @return 完成信号
+ */
+ @Override
+ public Mono observe(HookEvent event) {
+ if (!(event instanceof ErrorEvent errorEvent)) {
+ return Mono.empty();
+ }
+ Throwable error = errorEvent.getError();
+ AgentRuntimeEvent runtimeEvent = eventBridge.event(AgentRuntimeEventType.FAILED);
+ runtimeEvent.getPayload().put("source", "HOOK");
+ runtimeEvent.getPayload().put("phase", "ERROR");
+ runtimeEvent.getPayload().put("stage", "AGENTSCOPE_HOOK");
+ runtimeEvent.getPayload().put("errorType", error == null ? null : error.getClass().getName());
+ runtimeEvent.getPayload().put("message", error == null ? "AgentScope hook error." : error.getMessage());
+ appendAgent(runtimeEvent, errorEvent.getAgent());
+ eventBridge.emit(runtimeEvent);
+ return Mono.empty();
+ }
+
+ private void appendAgent(AgentRuntimeEvent event, Agent agent) {
+ if (agent == null) {
+ return;
+ }
+ event.getPayload().put("agentName", agent.getName());
+ event.getPayload().put("agentScopeAgentId", agent.getAgentId());
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/observer/ReasoningLifecycleObserver.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/observer/ReasoningLifecycleObserver.java
new file mode 100644
index 0000000..727340f
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/observer/ReasoningLifecycleObserver.java
@@ -0,0 +1,77 @@
+package com.easyagents.agent.runtime.event.observer;
+
+import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
+import com.easyagents.agent.runtime.event.AgentRuntimeEventBridge;
+import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
+import com.easyagents.agent.runtime.event.AgentRuntimeObserver;
+import io.agentscope.core.hook.HookEvent;
+import io.agentscope.core.hook.PostReasoningEvent;
+import io.agentscope.core.hook.PreReasoningEvent;
+import io.agentscope.core.message.Msg;
+import reactor.core.publisher.Mono;
+
+/**
+ * 监听 AgentScope 推理生命周期,并发射思考状态旁路事件。
+ *
+ * {@link AgentRuntimeEventType#REASONING_DELTA} 表示主线路中的推理内容片段;
+ * 本观察器发射的 started/completed 事件只用于前端状态展示,不携带模型上下文,
+ * 也不会修改 AgentScope 的推理输入或输出。
+ */
+public class ReasoningLifecycleObserver implements AgentRuntimeObserver {
+
+ private final AgentRuntimeEventBridge eventBridge;
+
+ /**
+ * 创建推理生命周期观察器。
+ *
+ * @param eventBridge 旁路事件桥
+ */
+ public ReasoningLifecycleObserver(AgentRuntimeEventBridge eventBridge) {
+ this.eventBridge = eventBridge;
+ }
+
+ /**
+ * 观察推理开始和完成事件。
+ *
+ * @param event AgentScope Hook 事件
+ * @return 完成信号
+ */
+ @Override
+ public Mono observe(HookEvent event) {
+ if (event instanceof PreReasoningEvent preReasoningEvent) {
+ emitStarted(preReasoningEvent);
+ return Mono.empty();
+ }
+ if (event instanceof PostReasoningEvent postReasoningEvent) {
+ emitCompleted(postReasoningEvent);
+ }
+ return Mono.empty();
+ }
+
+ private void emitStarted(PreReasoningEvent event) {
+ AgentRuntimeEvent runtimeEvent = eventBridge.event(AgentRuntimeEventType.REASONING_STARTED);
+ runtimeEvent.getPayload().put("modelName", event.getModelName());
+ runtimeEvent.getPayload().put("messageCount", event.getInputMessages() == null ? 0 : event.getInputMessages().size());
+ runtimeEvent.getPayload().put("source", "HOOK");
+ runtimeEvent.getPayload().put("phase", "PRE_REASONING");
+ eventBridge.emit(runtimeEvent);
+ }
+
+ private void emitCompleted(PostReasoningEvent event) {
+ AgentRuntimeEvent runtimeEvent = eventBridge.event(AgentRuntimeEventType.REASONING_COMPLETED);
+ runtimeEvent.getPayload().put("modelName", event.getModelName());
+ runtimeEvent.getPayload().put("stopRequested", event.isStopRequested());
+ runtimeEvent.getPayload().put("gotoReasoningRequested", event.isGotoReasoningRequested());
+ runtimeEvent.getPayload().put("text", reasoningText(event.getReasoningMessage()));
+ runtimeEvent.getPayload().put("source", "HOOK");
+ runtimeEvent.getPayload().put("phase", "POST_REASONING");
+ eventBridge.emit(runtimeEvent);
+ }
+
+ private String reasoningText(Msg message) {
+ if (message == null || message.getTextContent() == null) {
+ return "";
+ }
+ return message.getTextContent();
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/observer/SkillExecutionObserver.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/observer/SkillExecutionObserver.java
new file mode 100644
index 0000000..a2e9740
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/observer/SkillExecutionObserver.java
@@ -0,0 +1,300 @@
+package com.easyagents.agent.runtime.event.observer;
+
+import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
+import com.easyagents.agent.runtime.event.AgentRuntimeEventBridge;
+import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
+import com.easyagents.agent.runtime.event.AgentRuntimeObserver;
+import com.easyagents.agent.runtime.skill.AgentSkillBinding;
+import com.easyagents.agent.runtime.skill.AgentSkillLoadCall;
+import com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext;
+import io.agentscope.core.hook.HookEvent;
+import io.agentscope.core.hook.PostActingEvent;
+import io.agentscope.core.hook.PreActingEvent;
+import io.agentscope.core.message.ContentBlock;
+import io.agentscope.core.message.TextBlock;
+import io.agentscope.core.message.ToolResultBlock;
+import io.agentscope.core.message.ToolUseBlock;
+import io.agentscope.core.skill.SkillBox;
+import io.agentscope.core.tool.Toolkit;
+import reactor.core.publisher.Mono;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.StringJoiner;
+
+/**
+ * 监听 AgentScope 工具执行生命周期,并发射 Skill 旁路事件。
+ *
+ * 该观察器只做 Easy-Agents 的旁路监察,不修改 AgentScope {@link HookEvent}。
+ * Skill 加载工具 {@link AgentSkillRuntimeContext#LOAD_SKILL_TOOL_NAME} 本身仍是
+ * AgentScope 主线路中的工具调用;本观察器只把它翻译成调用方可展示的
+ * {@link AgentRuntimeEventType#SKILL_CALL}、{@link AgentRuntimeEventType#SKILL_RESULT}
+ * 和 {@link AgentRuntimeEventType#SKILL_FAILED}。已激活 Skill 内部的普通工具调用会
+ * 被翻译成 {@link AgentRuntimeEventType#SKILL_STEP}。
+ *
+ * Skill 是否激活以 AgentScope {@link SkillBox} 和 {@link Toolkit#getActiveGroups()}
+ * 为准,本地 {@link AgentSkillRuntimeContext} 只缓存旁路展示所需的归属状态。
+ */
+public class SkillExecutionObserver implements AgentRuntimeObserver {
+
+ private final AgentRuntimeEventBridge eventBridge;
+ private final AgentSkillRuntimeContext skillContext;
+ private final SkillBox skillBox;
+
+ /**
+ * 创建 Skill 执行观察器。
+ *
+ * @param eventBridge 旁路事件桥
+ * @param skillContext Skill 运行时上下文
+ */
+ public SkillExecutionObserver(AgentRuntimeEventBridge eventBridge,
+ AgentSkillRuntimeContext skillContext) {
+ this(eventBridge, skillContext, null);
+ }
+
+ /**
+ * 创建 Skill 执行观察器。
+ *
+ * @param eventBridge 旁路事件桥
+ * @param skillContext Skill 运行时上下文
+ * @param skillBox AgentScope SkillBox,作为 Skill 激活状态的权威来源
+ */
+ public SkillExecutionObserver(AgentRuntimeEventBridge eventBridge,
+ AgentSkillRuntimeContext skillContext,
+ SkillBox skillBox) {
+ this.eventBridge = eventBridge;
+ this.skillContext = skillContext;
+ this.skillBox = skillBox;
+ }
+
+ /**
+ * 观察 Skill 加载和已激活 Skill 内部工具执行。
+ *
+ * @param event AgentScope Hook 事件
+ * @return 完成信号
+ */
+ @Override
+ public Mono observe(HookEvent event) {
+ if (skillContext == null) {
+ return Mono.empty();
+ }
+ if (event instanceof PreActingEvent preActingEvent) {
+ observePreActing(preActingEvent);
+ return Mono.empty();
+ }
+ if (event instanceof PostActingEvent postActingEvent) {
+ observePostActing(postActingEvent);
+ }
+ return Mono.empty();
+ }
+
+ private void observePreActing(PreActingEvent event) {
+ ToolUseBlock toolUse = event.getToolUse();
+ if (toolUse == null) {
+ return;
+ }
+ syncSkillState(toolUse.getName(), event.getToolkit());
+ if (skillContext.isSkillLoadTool(toolUse.getName())) {
+ emitSkillCall(event, toolUse);
+ return;
+ }
+ AgentSkillBinding activeBinding = skillContext.getActiveToolBinding(toolUse.getName());
+ if (activeBinding != null) {
+ emitSkillStepCall(toolUse, activeBinding);
+ }
+ }
+
+ private void observePostActing(PostActingEvent event) {
+ ToolResultBlock result = event.getToolResult();
+ ToolUseBlock toolUse = event.getToolUse();
+ String toolName = result == null ? toolName(toolUse) : result.getName();
+ if (skillContext.isSkillLoadTool(toolName)) {
+ emitSkillResult(result, toolUse, event.getToolkit());
+ return;
+ }
+ syncSkillState(toolName, event.getToolkit());
+ AgentSkillBinding activeBinding = skillContext.getActiveToolBinding(toolName);
+ if (activeBinding != null) {
+ emitSkillStepResult(result, toolUse, activeBinding);
+ }
+ }
+
+ private void emitSkillCall(PreActingEvent event, ToolUseBlock toolUse) {
+ AgentSkillLoadCall call = skillContext.rememberLoadCall(toolUse.getId(), toolUse.getInput());
+ if (!skillContext.markLoadCallEmitted(toolUse.getId())) {
+ return;
+ }
+ AgentRuntimeEvent runtimeEvent = eventBridge.event(AgentRuntimeEventType.SKILL_CALL);
+ runtimeEvent.setToolCallId(toolUse.getId());
+ runtimeEvent.getPayload().put("toolCallId", toolUse.getId());
+ runtimeEvent.getPayload().put("toolName", toolUse.getName());
+ runtimeEvent.getPayload().put("input", toolUse.getInput());
+ runtimeEvent.getPayload().put("status", "RUNNING");
+ runtimeEvent.getPayload().put("source", "HOOK");
+ runtimeEvent.getPayload().put("phase", "PRE_ACTING");
+ appendSkillLoadPayload(runtimeEvent, call);
+ runtimeEvent.getMetadata().putAll(nullToEmpty(toolUse.getMetadata()));
+ eventBridge.emit(runtimeEvent);
+ }
+
+ private void emitSkillResult(ToolResultBlock result, ToolUseBlock toolUse, Toolkit toolkit) {
+ String toolCallId = result == null ? toolCallId(toolUse) : result.getId();
+ AgentSkillLoadCall call = skillContext.removeLoadCall(toolCallId);
+ boolean active = syncSkillState(call, toolkit);
+ AgentRuntimeEvent runtimeEvent = eventBridge.event(skillResultType(result, active));
+ runtimeEvent.setToolCallId(toolCallId);
+ runtimeEvent.getPayload().put("toolCallId", toolCallId);
+ runtimeEvent.getPayload().put("toolName", result == null ? toolName(toolUse) : result.getName());
+ runtimeEvent.getPayload().put("text", resultText(result));
+ runtimeEvent.getPayload().put("status", runtimeEvent.getEventType() == AgentRuntimeEventType.SKILL_RESULT
+ ? "SUCCESS"
+ : "FAILED");
+ runtimeEvent.getPayload().put("success", runtimeEvent.getEventType() == AgentRuntimeEventType.SKILL_RESULT);
+ runtimeEvent.getPayload().put("suspended", result != null && result.isSuspended());
+ runtimeEvent.getPayload().put("active", active);
+ runtimeEvent.getPayload().put("source", "HOOK");
+ runtimeEvent.getPayload().put("phase", "POST_ACTING");
+ if (result != null) {
+ runtimeEvent.getMetadata().putAll(nullToEmpty(result.getMetadata()));
+ }
+ appendSkillLoadPayload(runtimeEvent, call);
+ eventBridge.emit(runtimeEvent);
+ }
+
+ private void emitSkillStepCall(ToolUseBlock toolUse, AgentSkillBinding binding) {
+ AgentRuntimeEvent runtimeEvent = eventBridge.event(AgentRuntimeEventType.SKILL_STEP);
+ runtimeEvent.setToolCallId(toolUse.getId());
+ runtimeEvent.getPayload().put("toolCallId", toolUse.getId());
+ runtimeEvent.getPayload().put("name", toolUse.getName());
+ runtimeEvent.getPayload().put("toolName", toolUse.getName());
+ runtimeEvent.getPayload().put("input", toolUse.getInput());
+ runtimeEvent.getPayload().put("content", toolUse.getContent());
+ runtimeEvent.getPayload().put("stepType", "TOOL_CALL");
+ runtimeEvent.getPayload().put("stepName", toolUse.getName());
+ runtimeEvent.getPayload().put("status", "RUNNING");
+ runtimeEvent.getPayload().put("source", "HOOK");
+ runtimeEvent.getPayload().put("phase", "PRE_ACTING");
+ appendSkillPayload(runtimeEvent.getPayload(), binding);
+ appendSkillPayload(runtimeEvent.getMetadata(), binding);
+ runtimeEvent.getMetadata().putAll(nullToEmpty(toolUse.getMetadata()));
+ eventBridge.emit(runtimeEvent);
+ }
+
+ private void emitSkillStepResult(ToolResultBlock result,
+ ToolUseBlock toolUse,
+ AgentSkillBinding binding) {
+ AgentRuntimeEvent runtimeEvent = eventBridge.event(AgentRuntimeEventType.SKILL_STEP);
+ String toolCallId = result == null ? toolCallId(toolUse) : result.getId();
+ String toolName = result == null ? toolName(toolUse) : result.getName();
+ runtimeEvent.setToolCallId(toolCallId);
+ runtimeEvent.getPayload().put("toolCallId", toolCallId);
+ runtimeEvent.getPayload().put("name", toolName);
+ runtimeEvent.getPayload().put("toolName", toolName);
+ runtimeEvent.getPayload().put("text", resultText(result));
+ runtimeEvent.getPayload().put("suspended", result != null && result.isSuspended());
+ runtimeEvent.getPayload().put("stepType", "TOOL_RESULT");
+ runtimeEvent.getPayload().put("stepName", toolName);
+ runtimeEvent.getPayload().put("status", success(result) ? "SUCCESS" : "FAILED");
+ runtimeEvent.getPayload().put("success", success(result));
+ runtimeEvent.getPayload().put("source", "HOOK");
+ runtimeEvent.getPayload().put("phase", "POST_ACTING");
+ appendSkillPayload(runtimeEvent.getPayload(), binding);
+ appendSkillPayload(runtimeEvent.getMetadata(), binding);
+ if (result != null) {
+ runtimeEvent.getMetadata().putAll(nullToEmpty(result.getMetadata()));
+ }
+ eventBridge.emit(runtimeEvent);
+ }
+
+ private AgentRuntimeEventType skillResultType(ToolResultBlock result, boolean active) {
+ return success(result) && active ? AgentRuntimeEventType.SKILL_RESULT : AgentRuntimeEventType.SKILL_FAILED;
+ }
+
+ private boolean success(ToolResultBlock result) {
+ if (result == null) {
+ return false;
+ }
+ Object success = result.getMetadata() == null ? null : result.getMetadata().get("success");
+ return !(success instanceof Boolean) || Boolean.TRUE.equals(success);
+ }
+
+ private String resultText(ToolResultBlock result) {
+ if (result == null || result.getOutput() == null || result.getOutput().isEmpty()) {
+ return "";
+ }
+ StringJoiner joiner = new StringJoiner("\n");
+ for (ContentBlock output : result.getOutput()) {
+ if (output instanceof TextBlock textBlock && textBlock.getText() != null) {
+ joiner.add(textBlock.getText());
+ } else if (output != null) {
+ joiner.add(output.toString());
+ }
+ }
+ return joiner.toString();
+ }
+
+ private boolean syncSkillState(AgentSkillLoadCall call, Toolkit toolkit) {
+ if (call == null) {
+ return false;
+ }
+ boolean active = isAgentScopeSkillActive(call.getSkillId(), toolkit);
+ skillContext.syncSkillActive(call.getSkillId(), active);
+ return active;
+ }
+
+ private void syncSkillState(String toolName, Toolkit toolkit) {
+ AgentSkillBinding binding = skillContext.getToolBinding(toolName);
+ if (binding == null) {
+ return;
+ }
+ skillContext.syncSkillActive(binding.getSkillId(), isAgentScopeSkillActive(binding.getSkillId(), toolkit));
+ }
+
+ private boolean isAgentScopeSkillActive(String skillId, Toolkit toolkit) {
+ if (skillId == null || skillId.isBlank()) {
+ return false;
+ }
+ if (skillBox != null && skillBox.isSkillActive(skillId)) {
+ return true;
+ }
+ return toolkit != null && toolkit.getActiveGroups().contains(skillToolGroupName(skillId));
+ }
+
+ private String skillToolGroupName(String skillId) {
+ return skillId + "_skill_tools";
+ }
+
+ private void appendSkillLoadPayload(AgentRuntimeEvent event, AgentSkillLoadCall call) {
+ if (call == null) {
+ return;
+ }
+ event.getPayload().put("skillId", call.getSkillId());
+ event.getPayload().put("skillName", call.getSkillName());
+ event.getPayload().put("skillBoxId", call.getSkillBoxId());
+ event.getPayload().put("path", call.getPath());
+ event.getMetadata().put("skillId", call.getSkillId());
+ event.getMetadata().put("skillName", call.getSkillName());
+ event.getMetadata().put("skillBoxId", call.getSkillBoxId());
+ }
+
+ private void appendSkillPayload(Map target, AgentSkillBinding binding) {
+ if (binding == null || target == null) {
+ return;
+ }
+ target.put("skillId", binding.getSkillId());
+ target.put("skillName", binding.getSkillName());
+ target.put("skillBoxId", binding.getSkillBoxId());
+ }
+
+ private String toolCallId(ToolUseBlock toolUse) {
+ return toolUse == null ? null : toolUse.getId();
+ }
+
+ private String toolName(ToolUseBlock toolUse) {
+ return toolUse == null ? null : toolUse.getName();
+ }
+
+ private Map nullToEmpty(Map map) {
+ return map == null ? new LinkedHashMap<>() : map;
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/observer/ToolExecutionObserver.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/observer/ToolExecutionObserver.java
new file mode 100644
index 0000000..1d6b466
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/observer/ToolExecutionObserver.java
@@ -0,0 +1,154 @@
+package com.easyagents.agent.runtime.event.observer;
+
+import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
+import com.easyagents.agent.runtime.event.AgentRuntimeEventBridge;
+import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
+import com.easyagents.agent.runtime.event.AgentRuntimeObserver;
+import com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext;
+import io.agentscope.core.hook.HookEvent;
+import io.agentscope.core.hook.PostActingEvent;
+import io.agentscope.core.hook.PreActingEvent;
+import io.agentscope.core.message.ContentBlock;
+import io.agentscope.core.message.TextBlock;
+import io.agentscope.core.message.ToolResultBlock;
+import io.agentscope.core.message.ToolUseBlock;
+import reactor.core.publisher.Mono;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 监听 AgentScope 原生工具执行生命周期,并发射工具状态旁路事件。
+ *
+ * 该观察器复用 {@link AgentRuntimeEventType#TOOL_CALL} 和
+ * {@link AgentRuntimeEventType#TOOL_RESULT},用于 EasyFlow 展示工具开始与完成状态。
+ * 它不修改 AgentScope HookEvent,也不写入模型上下文。
+ */
+public class ToolExecutionObserver implements AgentRuntimeObserver {
+
+ private final AgentRuntimeEventBridge eventBridge;
+ private final AgentSkillRuntimeContext skillContext;
+
+ /**
+ * 创建工具执行观察器。
+ *
+ * @param eventBridge 旁路事件桥
+ */
+ public ToolExecutionObserver(AgentRuntimeEventBridge eventBridge) {
+ this(eventBridge, null);
+ }
+
+ /**
+ * 创建工具执行观察器。
+ *
+ * @param eventBridge 旁路事件桥
+ * @param skillContext Skill 上下文,用于跳过由 SkillExecutionObserver 处理的工具
+ */
+ public ToolExecutionObserver(AgentRuntimeEventBridge eventBridge,
+ AgentSkillRuntimeContext skillContext) {
+ this.eventBridge = eventBridge;
+ this.skillContext = skillContext;
+ }
+
+ /**
+ * 观察工具执行前后事件。
+ *
+ * @param event AgentScope Hook 事件
+ * @return 完成信号
+ */
+ @Override
+ public Mono observe(HookEvent event) {
+ if (event instanceof PreActingEvent preActingEvent) {
+ emitToolCall(preActingEvent);
+ return Mono.empty();
+ }
+ if (event instanceof PostActingEvent postActingEvent) {
+ emitToolResult(postActingEvent);
+ }
+ return Mono.empty();
+ }
+
+ private void emitToolCall(PreActingEvent event) {
+ ToolUseBlock toolUse = event.getToolUse();
+ if (toolUse == null) {
+ return;
+ }
+ if (isSkillTool(toolUse.getName())) {
+ return;
+ }
+ AgentRuntimeEvent runtimeEvent = eventBridge.event(AgentRuntimeEventType.TOOL_CALL);
+ runtimeEvent.setToolCallId(toolUse.getId());
+ runtimeEvent.getPayload().put("toolCallId", toolUse.getId());
+ runtimeEvent.getPayload().put("name", toolUse.getName());
+ runtimeEvent.getPayload().put("toolName", toolUse.getName());
+ runtimeEvent.getPayload().put("input", toolUse.getInput());
+ runtimeEvent.getPayload().put("content", toolUse.getContent());
+ runtimeEvent.getPayload().put("status", "RUNNING");
+ runtimeEvent.getPayload().put("source", "HOOK");
+ runtimeEvent.getPayload().put("phase", "PRE_ACTING");
+ runtimeEvent.getMetadata().putAll(nullToEmpty(toolUse.getMetadata()));
+ eventBridge.emit(runtimeEvent);
+ }
+
+ private void emitToolResult(PostActingEvent event) {
+ ToolResultBlock result = event.getToolResult();
+ ToolUseBlock toolUse = event.getToolUse();
+ if (result == null && toolUse == null) {
+ return;
+ }
+ String toolCallId = result == null ? toolUse.getId() : result.getId();
+ String toolName = result == null ? toolUse.getName() : result.getName();
+ if (isSkillTool(toolName)) {
+ return;
+ }
+ AgentRuntimeEvent runtimeEvent = eventBridge.event(AgentRuntimeEventType.TOOL_RESULT);
+ runtimeEvent.setToolCallId(toolCallId);
+ runtimeEvent.getPayload().put("toolCallId", toolCallId);
+ runtimeEvent.getPayload().put("name", toolName);
+ runtimeEvent.getPayload().put("toolName", toolName);
+ runtimeEvent.getPayload().put("text", resultText(result));
+ runtimeEvent.getPayload().put("suspended", result != null && result.isSuspended());
+ runtimeEvent.getPayload().put("status", success(result) ? "SUCCESS" : "FAILED");
+ runtimeEvent.getPayload().put("success", success(result));
+ runtimeEvent.getPayload().put("source", "HOOK");
+ runtimeEvent.getPayload().put("phase", "POST_ACTING");
+ if (result != null) {
+ runtimeEvent.getMetadata().putAll(nullToEmpty(result.getMetadata()));
+ }
+ eventBridge.emit(runtimeEvent);
+ }
+
+ private boolean success(ToolResultBlock result) {
+ if (result == null) {
+ return false;
+ }
+ Object success = result.getMetadata() == null ? null : result.getMetadata().get("success");
+ return !(success instanceof Boolean) || Boolean.TRUE.equals(success);
+ }
+
+ private String resultText(ToolResultBlock result) {
+ if (result == null || result.getOutput() == null || result.getOutput().isEmpty()) {
+ return "";
+ }
+ StringBuilder builder = new StringBuilder();
+ for (ContentBlock block : result.getOutput()) {
+ if (block instanceof TextBlock textBlock) {
+ builder.append(textBlock.getText());
+ } else {
+ builder.append(block);
+ }
+ }
+ return builder.toString();
+ }
+
+ private Map nullToEmpty(Map map) {
+ return map == null ? new LinkedHashMap<>() : map;
+ }
+
+ private boolean isSkillTool(String toolName) {
+ if (skillContext == null) {
+ return false;
+ }
+ return skillContext.isSkillLoadTool(toolName) || skillContext.getActiveToolBinding(toolName) != null;
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentToolApprovalCoordinator.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentToolApprovalCoordinator.java
index 7b1313b..340b527 100644
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentToolApprovalCoordinator.java
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentToolApprovalCoordinator.java
@@ -1,5 +1,7 @@
package com.easyagents.agent.runtime.hitl;
+import com.easyagents.agent.runtime.AgentResumeRequest;
+import com.easyagents.agent.runtime.AgentRuntimeException;
import reactor.core.publisher.Mono;
import java.time.Instant;
@@ -86,14 +88,14 @@ public class AgentToolApprovalCoordinator {
* 等待指定审批令牌的响应。
*
* @param resumeToken 恢复令牌
- * @return 审批响应
+ * @return 恢复请求
*/
- public Mono await(AgentResumeToken resumeToken) {
+ public Mono await(AgentResumeToken resumeToken) {
if (!enabled) {
- AgentToolApprovalResponse response = new AgentToolApprovalResponse();
- response.setResumeToken(resumeToken);
- response.setApproved(true);
- return Mono.just(response);
+ AgentResumeRequest request = new AgentResumeRequest();
+ request.setResumeToken(resumeToken);
+ request.setApproved(true);
+ return Mono.just(request);
}
if (resumeToken == null || resumeToken.getValue() == null || resumeToken.getValue().isBlank()) {
return Mono.error(new AgentToolApprovalRejectedException("缺少审批令牌。"));
@@ -106,19 +108,31 @@ public class AgentToolApprovalCoordinator {
}
/**
- * 提交审批响应。
+ * 消费恢复请求对应的待审批状态。
*
- * @param response 审批响应
+ * 该方法用于有状态 runtime 的 HITL resume。第一版 pending state 仅保存在
+ * 当前进程内存中,因此消费成功后会立即移除 token,避免重复恢复。
+ *
+ * @param request 恢复请求
+ * @return 待审批状态
*/
- public void submit(AgentToolApprovalResponse response) {
- if (!enabled || response == null || response.getResumeToken() == null
- || response.getResumeToken().getValue() == null) {
- return;
+ public AgentPendingState consume(AgentResumeRequest request) {
+ if (request == null || request.getResumeToken() == null
+ || request.getResumeToken().getValue() == null
+ || request.getResumeToken().getValue().isBlank()) {
+ throw new AgentRuntimeException("Agent resume token is required.");
}
- PendingApproval pendingApproval = approvals.get(response.getResumeToken().getValue());
- if (pendingApproval != null) {
- pendingApproval.future.complete(response);
+ if (!enabled) {
+ AgentPendingState state = new AgentPendingState();
+ state.setResumeToken(request.getResumeToken());
+ return state;
}
+ PendingApproval pendingApproval = approvals.remove(request.getResumeToken().getValue());
+ if (pendingApproval == null) {
+ throw new AgentRuntimeException("Agent resume token is invalid or expired.");
+ }
+ pendingApproval.future.complete(request);
+ return pendingApproval.state;
}
/**
@@ -131,11 +145,11 @@ public class AgentToolApprovalCoordinator {
return;
}
for (PendingApproval pendingApproval : approvals.values()) {
- AgentToolApprovalResponse response = new AgentToolApprovalResponse();
- response.setResumeToken(pendingApproval.state.getResumeToken());
- response.setApproved(false);
- response.setRejectReason(reason);
- pendingApproval.future.complete(response);
+ AgentResumeRequest request = new AgentResumeRequest();
+ request.setResumeToken(pendingApproval.state.getResumeToken());
+ request.setApproved(false);
+ request.setRejectReason(reason);
+ pendingApproval.future.complete(request);
}
approvals.clear();
}
@@ -151,9 +165,9 @@ public class AgentToolApprovalCoordinator {
private static class PendingApproval {
private final AgentPendingState state;
- private final CompletableFuture future;
+ private final CompletableFuture future;
- private PendingApproval(AgentPendingState state, CompletableFuture future) {
+ private PendingApproval(AgentPendingState state, CompletableFuture future) {
this.state = Objects.requireNonNull(state, "state");
this.future = Objects.requireNonNull(future, "future");
}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/citation/AgentKnowledgeCitationMatcher.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/citation/AgentKnowledgeCitationMatcher.java
new file mode 100644
index 0000000..9e331b9
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/citation/AgentKnowledgeCitationMatcher.java
@@ -0,0 +1,24 @@
+package com.easyagents.agent.runtime.knowledge.citation;
+
+import com.easyagents.agent.runtime.message.AgentKnowledgeReference;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 知识库引用匹配器。
+ *
+ * 该接口只负责从最终答案和本轮检索候选中选择可展示的引用。实现不应修改答案文本,
+ * 也不应把引用写入 AgentScope memory/session。
+ */
+public interface AgentKnowledgeCitationMatcher {
+
+ /**
+ * 匹配最终答案中可由候选知识片段支撑的引用。
+ *
+ * @param answerText 最终答案文本
+ * @param candidates 本轮检索候选引用
+ * @return 匹配到的知识库引用
+ */
+ List match(String answerText, Collection candidates);
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/citation/HeuristicKnowledgeCitationMatcher.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/citation/HeuristicKnowledgeCitationMatcher.java
new file mode 100644
index 0000000..e8df3a0
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/citation/HeuristicKnowledgeCitationMatcher.java
@@ -0,0 +1,204 @@
+package com.easyagents.agent.runtime.knowledge.citation;
+
+import com.easyagents.agent.runtime.message.AgentKnowledgeReference;
+
+import java.util.*;
+
+/**
+ * 基于文本证据的启发式知识库引用匹配器。
+ *
+ * 该实现用于模型没有显式输出引用 ID 的场景。它只能说明答案文本与某些检索片段
+ * 存在较强文本支撑关系,不能证明 LLM 真实使用了该片段。因此匹配策略保持保守:
+ * 宁可少返回引用,也不为了“看起来有引用”而强行猜测。
+ */
+public class HeuristicKnowledgeCitationMatcher implements AgentKnowledgeCitationMatcher {
+
+ private static final int MIN_NORMALIZED_ANSWER_LENGTH = 4;
+ private static final int MIN_CONTENT_LENGTH = 6;
+ private static final int MAX_REFERENCES = 5;
+ private static final double MIN_SUPPORT_SCORE = 0.42D;
+
+ /**
+ * 根据最终答案和候选知识片段匹配引用。
+ *
+ * @param answerText 最终答案文本
+ * @param candidates 本轮检索候选引用
+ * @return 高置信引用列表
+ */
+ @Override
+ public List match(String answerText, Collection candidates) {
+ if (candidates == null || candidates.isEmpty()) {
+ return List.of();
+ }
+ String normalizedAnswer = normalize(answerText);
+ if (normalizedAnswer.length() < MIN_NORMALIZED_ANSWER_LENGTH) {
+ return List.of();
+ }
+ List scoredReferences = new ArrayList<>();
+ for (AgentKnowledgeReference candidate : candidates) {
+ double supportScore = supportScore(normalizedAnswer, normalize(candidate == null ? null : candidate.getChunkContent()));
+ if (supportScore >= MIN_SUPPORT_SCORE) {
+ scoredReferences.add(new ScoredKnowledgeReference(candidate, supportScore));
+ }
+ }
+ scoredReferences.sort(Comparator.comparingDouble(ScoredKnowledgeReference::score).reversed());
+ return scoredReferences.stream()
+ .limit(MAX_REFERENCES)
+ .map(ScoredKnowledgeReference::reference)
+ .toList();
+ }
+
+ /**
+ * 计算答案与候选片段之间的文本支撑分。
+ *
+ * @param normalizedAnswer 归一化后的答案
+ * @param normalizedContent 归一化后的候选片段
+ * @return 支撑分
+ */
+ private double supportScore(String normalizedAnswer, String normalizedContent) {
+ if (normalizedAnswer.isBlank()
+ || normalizedContent.isBlank()
+ || normalizedContent.length() < MIN_CONTENT_LENGTH) {
+ return 0D;
+ }
+ if (normalizedContent.contains(normalizedAnswer)) {
+ return 1D;
+ }
+ int longestCommon = longestCommonSubstringLength(normalizedAnswer, normalizedContent);
+ double gramOverlap = gramOverlapRatio(charGrams(normalizedAnswer, 2), charGrams(normalizedContent, 2));
+ double numericCoverage = numericCoverage(normalizedAnswer, normalizedContent);
+
+ // 长连续文本片段比零散关键词更能说明答案来自该 chunk。
+ if (longestCommon >= 8 && gramOverlap >= 0.12D) {
+ double numericBoost = numericCoverage >= 0.8D ? 0.12D : numericCoverage * 0.08D;
+ return Math.max(0.55D, gramOverlap + Math.min(0.35D, longestCommon / 60D) + numericBoost);
+ }
+ if (gramOverlap >= 0.28D && longestCommon >= 5) {
+ return gramOverlap + Math.min(0.25D, longestCommon / 80D);
+ }
+ return gramOverlap * 0.7D + Math.min(0.2D, longestCommon / 80D) + numericCoverage * 0.08D;
+ }
+
+ /**
+ * 归一化用于引用匹配的文本。
+ *
+ * @param text 原始文本
+ * @return 低噪声文本
+ */
+ private String normalize(String text) {
+ if (text == null) {
+ return "";
+ }
+ return text.toLowerCase()
+ .replace('至', '到')
+ .replaceAll("[^\\p{IsHan}a-z0-9]", "");
+ }
+
+ /**
+ * 生成字符 ngram。
+ *
+ * @param text 文本
+ * @param size ngram 长度
+ * @return ngram 集合
+ */
+ private Set charGrams(String text, int size) {
+ Set grams = new HashSet<>();
+ if (text == null || text.length() < size) {
+ return grams;
+ }
+ for (int i = 0; i <= text.length() - size; i++) {
+ grams.add(text.substring(i, i + size));
+ }
+ return grams;
+ }
+
+ /**
+ * 计算答案 ngram 被候选片段覆盖的比例。
+ *
+ * @param answerGrams 答案 ngram
+ * @param contentGrams 候选片段 ngram
+ * @return 覆盖比例
+ */
+ private double gramOverlapRatio(Set answerGrams, Set contentGrams) {
+ if (answerGrams == null || answerGrams.isEmpty() || contentGrams == null || contentGrams.isEmpty()) {
+ return 0D;
+ }
+ int matched = 0;
+ for (String gram : answerGrams) {
+ if (contentGrams.contains(gram)) {
+ matched++;
+ }
+ }
+ return (double) matched / (double) answerGrams.size();
+ }
+
+ /**
+ * 计算答案中的数字片段被候选片段覆盖的比例。
+ *
+ * 数字只能作为弱加权因素,不能单独造成高置信引用,避免日期、章节号等噪声误判。
+ *
+ * @param normalizedAnswer 归一化后的答案
+ * @param normalizedContent 归一化后的候选片段
+ * @return 数字覆盖比例
+ */
+ private double numericCoverage(String normalizedAnswer, String normalizedContent) {
+ List numbers = extractNumericTokens(normalizedAnswer);
+ if (numbers.isEmpty()) {
+ return 0D;
+ }
+ int matched = 0;
+ for (String number : numbers) {
+ if (normalizedContent.contains(number)) {
+ matched++;
+ }
+ }
+ return (double) matched / (double) numbers.size();
+ }
+
+ /**
+ * 提取数字片段。
+ *
+ * @param text 文本
+ * @return 数字片段
+ */
+ private List extractNumericTokens(String text) {
+ List numbers = new ArrayList<>();
+ if (text == null || text.isBlank()) {
+ return numbers;
+ }
+ java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("\\d+").matcher(text);
+ while (matcher.find()) {
+ numbers.add(matcher.group());
+ }
+ return numbers;
+ }
+
+ /**
+ * 计算最长公共子串长度。
+ *
+ * @param first 第一段文本
+ * @param second 第二段文本
+ * @return 最长公共子串长度
+ */
+ private int longestCommonSubstringLength(String first, String second) {
+ if (first == null || second == null || first.isEmpty() || second.isEmpty()) {
+ return 0;
+ }
+ int[] previous = new int[second.length() + 1];
+ int max = 0;
+ for (int i = 1; i <= first.length(); i++) {
+ int[] current = new int[second.length() + 1];
+ for (int j = 1; j <= second.length(); j++) {
+ if (first.charAt(i - 1) == second.charAt(j - 1)) {
+ current[j] = previous[j - 1] + 1;
+ max = Math.max(max, current[j]);
+ }
+ }
+ previous = current;
+ }
+ return max;
+ }
+
+ private record ScoredKnowledgeReference(AgentKnowledgeReference reference, double score) {
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/memory/AgentMemoryCompressionParameter.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/memory/AgentMemoryCompressionParameter.java
index d7cc12d..0a65aa4 100644
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/memory/AgentMemoryCompressionParameter.java
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/memory/AgentMemoryCompressionParameter.java
@@ -6,12 +6,12 @@ package com.easyagents.agent.runtime.memory;
public class AgentMemoryCompressionParameter {
private boolean enabled = true;
- private int msgThreshold = 20;
+ private Integer msgThreshold;
private int lastKeep = 8;
- private double tokenRatio = 0.7D;
- private long maxToken = 12000L;
+ private Double tokenRatio;
+ private Long maxToken;
private long largePayloadThreshold = 2048L;
- private int minCompressionTokenThreshold = 1000;
+ private Integer minCompressionTokenThreshold;
private double currentRoundCompressionRatio = 0.5D;
private int minConsecutiveToolMessages = 4;
@@ -38,7 +38,7 @@ public class AgentMemoryCompressionParameter {
*
* @return 消息阈值
*/
- public int getMsgThreshold() {
+ public Integer getMsgThreshold() {
return msgThreshold;
}
@@ -48,7 +48,7 @@ public class AgentMemoryCompressionParameter {
* @param msgThreshold 消息阈值
*/
public void setMsgThreshold(int msgThreshold) {
- this.msgThreshold = msgThreshold <= 0 ? 20 : msgThreshold;
+ this.msgThreshold = msgThreshold <= 0 ? null : msgThreshold;
}
/**
@@ -74,7 +74,7 @@ public class AgentMemoryCompressionParameter {
*
* @return Token 比例
*/
- public double getTokenRatio() {
+ public Double getTokenRatio() {
return tokenRatio;
}
@@ -84,7 +84,7 @@ public class AgentMemoryCompressionParameter {
* @param tokenRatio Token 比例
*/
public void setTokenRatio(double tokenRatio) {
- this.tokenRatio = tokenRatio <= 0D ? 0.7D : tokenRatio;
+ this.tokenRatio = tokenRatio <= 0D ? null : tokenRatio;
}
/**
@@ -92,7 +92,7 @@ public class AgentMemoryCompressionParameter {
*
* @return 最大 Token 数
*/
- public long getMaxToken() {
+ public Long getMaxToken() {
return maxToken;
}
@@ -102,7 +102,7 @@ public class AgentMemoryCompressionParameter {
* @param maxToken 最大 Token 数
*/
public void setMaxToken(long maxToken) {
- this.maxToken = maxToken <= 0L ? 12000L : maxToken;
+ this.maxToken = maxToken <= 0L ? null : maxToken;
}
/**
@@ -128,7 +128,7 @@ public class AgentMemoryCompressionParameter {
*
* @return 最小压缩 Token 阈值
*/
- public int getMinCompressionTokenThreshold() {
+ public Integer getMinCompressionTokenThreshold() {
return minCompressionTokenThreshold;
}
@@ -138,7 +138,7 @@ public class AgentMemoryCompressionParameter {
* @param minCompressionTokenThreshold 最小压缩 Token 阈值
*/
public void setMinCompressionTokenThreshold(int minCompressionTokenThreshold) {
- this.minCompressionTokenThreshold = Math.max(0, minCompressionTokenThreshold);
+ this.minCompressionTokenThreshold = minCompressionTokenThreshold <= 0 ? null : minCompressionTokenThreshold;
}
/**
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/memory/AgentMemorySnapshot.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/memory/AgentMemorySnapshot.java
index 6c03ade..ac3917a 100644
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/memory/AgentMemorySnapshot.java
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/memory/AgentMemorySnapshot.java
@@ -31,7 +31,7 @@ public class AgentMemorySnapshot {
}
/**
- * 添加one message。
+ * 添加消息。
*
* @param message 消息
*/
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentKnowledgeReference.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentKnowledgeReference.java
index 081cc3a..ba26ed4 100644
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentKnowledgeReference.java
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentKnowledgeReference.java
@@ -13,6 +13,7 @@ public class AgentKnowledgeReference {
private String documentId;
private String documentName;
private String chunkId;
+ private String chunkContent;
private String sourceUri;
private Double score;
private Map metadata = new LinkedHashMap<>();
@@ -107,6 +108,24 @@ public class AgentKnowledgeReference {
this.chunkId = chunkId;
}
+ /**
+ * 获取命中分片原文。
+ *
+ * @return 命中分片原文
+ */
+ public String getChunkContent() {
+ return chunkContent;
+ }
+
+ /**
+ * 设置命中分片原文。
+ *
+ * @param chunkContent 命中分片原文
+ */
+ public void setChunkContent(String chunkContent) {
+ this.chunkContent = chunkContent;
+ }
+
/**
* 获取来源 URI。
*
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentRuntimeState.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentRuntimeState.java
deleted file mode 100644
index bea03c0..0000000
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentRuntimeState.java
+++ /dev/null
@@ -1,86 +0,0 @@
-package com.easyagents.agent.runtime.persistence;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-/**
- * 运行时状态项。
- *
- * MVP 中 value 是 JVM 对象态,不是稳定的传输格式,
- * 非内存 {@link AgentSessionStore} 实现必须显式完成序列化,
- * 再写入外部存储。
- */
-public class AgentRuntimeState {
-
- private String name;
- private Object value;
- private Map metadata = new LinkedHashMap<>();
-
- /**
- * 创建a 状态项。
- *
- * @param name 状态名称
- * @param value 状态值
- * @return 状态项
- */
- public static AgentRuntimeState of(String name, Object value) {
- AgentRuntimeState state = new AgentRuntimeState();
- state.setName(name);
- state.setValue(value);
- return state;
- }
-
- /**
- * 获取状态名称。
- *
- * @return 状态名称
- */
- public String getName() {
- return name;
- }
-
- /**
- * 设置状态名称。
- *
- * @param name 状态名称
- */
- public void setName(String name) {
- this.name = name;
- }
-
- /**
- * 获取状态值。
- *
- * @return 状态值
- */
- public Object getValue() {
- return value;
- }
-
- /**
- * 设置状态值。
- *
- * @param value 状态值
- */
- public void setValue(Object value) {
- this.value = value;
- }
-
- /**
- * 获取元数据。
- *
- * @return 元数据
- */
- public Map getMetadata() {
- return metadata;
- }
-
- /**
- * 设置元数据。
- *
- * @param metadata 元数据
- */
- public void setMetadata(Map metadata) {
- this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
- }
-}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentSessionStore.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentSessionStore.java
deleted file mode 100644
index f2a8d2a..0000000
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentSessionStore.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package com.easyagents.agent.runtime.persistence;
-
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-
-/**
- * 用于 AgentScope 类运行时会话状态的存储 SPI。
- *
- */
-public interface AgentSessionStore {
-
- /**
- * 保存one state value。
- *
- * @param sessionKey 会话键
- * @param name 状态名称
- * @param state 状态值
- */
- void save(String sessionKey, String name, AgentRuntimeState state);
-
- /**
- * 保存a state list。
- *
- * @param sessionKey 会话键
- * @param name 状态名称
- * @param states 状态列表
- */
- void saveList(String sessionKey, String name, List states);
-
- /**
- * 获取one state value。
- *
- * @param sessionKey 会话键
- * @param name 状态名称
- * @return 可选状态
- */
- Optional get(String sessionKey, String name);
-
- /**
- * 获取a state list。
- *
- * @param sessionKey 会话键
- * @param name 状态名称
- * @return 状态列表
- */
- List getList(String sessionKey, String name);
-
- /**
- * 返回是否session key exists。
- *
- * @param sessionKey 会话键
- * @return 存在时为 true
- */
- boolean exists(String sessionKey);
-
- /**
- * 删除a session key。
- *
- * @param sessionKey 会话键
- */
- void delete(String sessionKey);
-
- /**
- * 列出会话键列表。
- *
- * @return 会话键列表
- */
- Set listSessionKeys();
-}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentConversationRecorder.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/conversation/AgentConversationRecorder.java
similarity index 57%
rename from easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentConversationRecorder.java
rename to easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/conversation/AgentConversationRecorder.java
index 2c93307..3cfae04 100644
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentConversationRecorder.java
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/conversation/AgentConversationRecorder.java
@@ -1,6 +1,6 @@
-package com.easyagents.agent.runtime.persistence;
+package com.easyagents.agent.runtime.persistence.conversation;
-import com.easyagents.agent.runtime.AgentRunRequest;
+import com.easyagents.agent.runtime.AgentRuntimeExecutionContext;
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
/**
@@ -14,5 +14,5 @@ public interface AgentConversationRecorder {
* @param request 运行请求
* @param event 运行时事件
*/
- void record(AgentRunRequest request, AgentRuntimeEvent event);
+ void record(AgentRuntimeExecutionContext request, AgentRuntimeEvent event);
}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/conversation/noop/NoopAgentConversationRecorder.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/conversation/noop/NoopAgentConversationRecorder.java
new file mode 100644
index 0000000..36c9dcb
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/conversation/noop/NoopAgentConversationRecorder.java
@@ -0,0 +1,16 @@
+package com.easyagents.agent.runtime.persistence.conversation.noop;
+
+import com.easyagents.agent.runtime.AgentRuntimeExecutionContext;
+import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
+import com.easyagents.agent.runtime.persistence.conversation.AgentConversationRecorder;
+
+/**
+ * 空操作会话记录器。
+ */
+public enum NoopAgentConversationRecorder implements AgentConversationRecorder {
+ INSTANCE;
+
+ @Override
+ public void record(AgentRuntimeExecutionContext request, AgentRuntimeEvent event) {
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/AgentSessionStateCodec.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/AgentSessionStateCodec.java
deleted file mode 100644
index c9ff425..0000000
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/AgentSessionStateCodec.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package com.easyagents.agent.runtime.persistence.json;
-
-import com.easyagents.agent.runtime.persistence.AgentRuntimeState;
-import com.easyagents.agent.runtime.persistence.AgentSessionStoreException;
-import io.agentscope.core.state.State;
-import io.agentscope.core.util.JsonUtils;
-
-/**
- * AgentScope 状态的 JSON 编解码器。
- */
-public class AgentSessionStateCodec {
-
- /**
- * 将状态对象编码为可持久化记录。
- *
- * @param state 状态对象
- * @return 可持久化记录
- */
- public SerializedAgentRuntimeState encode(AgentRuntimeState state) {
- if (state == null || state.getValue() == null) {
- return null;
- }
- Object value = state.getValue();
- if (!(value instanceof State)) {
- throw new AgentSessionStoreException("Only AgentScope State values can be serialized: "
- + value.getClass().getName());
- }
- SerializedAgentRuntimeState serialized = new SerializedAgentRuntimeState();
- serialized.setName(state.getName());
- serialized.setStateClassName(value.getClass().getName());
- serialized.setStateJson(JsonUtils.getJsonCodec().toJson(value));
- serialized.setMetadata(state.getMetadata());
- return serialized;
- }
-
- /**
- * 将持久化记录解码为运行时状态。
- *
- * @param serialized 可持久化记录
- * @return 运行时状态
- */
- @SuppressWarnings("unchecked")
- public AgentRuntimeState decode(SerializedAgentRuntimeState serialized) {
- if (serialized == null) {
- return null;
- }
- try {
- Class> clazz = Class.forName(serialized.getStateClassName());
- if (!State.class.isAssignableFrom(clazz)) {
- throw new AgentSessionStoreException("Serialized state class is not AgentScope State: "
- + serialized.getStateClassName());
- }
- State value = JsonUtils.getJsonCodec().fromJson(serialized.getStateJson(), (Class extends State>) clazz);
- AgentRuntimeState state = AgentRuntimeState.of(serialized.getName(), value);
- state.setMetadata(serialized.getMetadata());
- return state;
- } catch (ClassNotFoundException e) {
- throw new AgentSessionStoreException("Serialized state class not found: "
- + serialized.getStateClassName(), e);
- }
- }
-}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/AgentSessionStoreBackend.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/AgentSessionStoreBackend.java
deleted file mode 100644
index 0cb27dd..0000000
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/AgentSessionStoreBackend.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package com.easyagents.agent.runtime.persistence.json;
-
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-
-/**
- * JSON 会话状态的底层键值存储后端。
- */
-public interface AgentSessionStoreBackend {
-
- /**
- * 保存单个状态记录。
- *
- * @param sessionKey 会话键
- * @param name 状态名称
- * @param state 状态记录
- */
- void save(String sessionKey, String name, SerializedAgentRuntimeState state);
-
- /**
- * 保存状态记录列表。
- *
- * @param sessionKey 会话键
- * @param name 状态名称
- * @param states 状态记录列表
- */
- void saveList(String sessionKey, String name, List states);
-
- /**
- * 获取单个状态记录。
- *
- * @param sessionKey 会话键
- * @param name 状态名称
- * @return 状态记录
- */
- Optional get(String sessionKey, String name);
-
- /**
- * 获取状态记录列表。
- *
- * @param sessionKey 会话键
- * @param name 状态名称
- * @return 状态记录列表
- */
- List getList(String sessionKey, String name);
-
- /**
- * 判断会话是否存在。
- *
- * @param sessionKey 会话键
- * @return 存在时为 true
- */
- boolean exists(String sessionKey);
-
- /**
- * 删除会话。
- *
- * @param sessionKey 会话键
- */
- void delete(String sessionKey);
-
- /**
- * 列出全部会话键。
- *
- * @return 会话键集合
- */
- Set listSessionKeys();
-}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/FileAgentSessionStoreBackend.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/FileAgentSessionStoreBackend.java
deleted file mode 100644
index 938c6b0..0000000
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/FileAgentSessionStoreBackend.java
+++ /dev/null
@@ -1,172 +0,0 @@
-package com.easyagents.agent.runtime.persistence.json;
-
-import com.easyagents.agent.runtime.persistence.AgentSessionStoreException;
-import io.agentscope.core.util.JsonUtils;
-
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
-import java.util.*;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-/**
- * 基于本地文件的 JSON 会话状态后端。
- */
-public class FileAgentSessionStoreBackend implements AgentSessionStoreBackend {
-
- private static final Pattern SAFE_NAME = Pattern.compile("[A-Za-z0-9._-]+");
- private final Path rootDirectory;
-
- /**
- * 创建文件后端。
- *
- * @param rootDirectory 根目录
- */
- public FileAgentSessionStoreBackend(Path rootDirectory) {
- if (rootDirectory == null) {
- throw new AgentSessionStoreException("Agent session root directory is required.");
- }
- this.rootDirectory = rootDirectory;
- try {
- Files.createDirectories(rootDirectory);
- } catch (IOException e) {
- throw new AgentSessionStoreException("Failed to create agent session root directory: " + rootDirectory, e);
- }
- }
-
- @Override
- public void save(String sessionKey, String name, SerializedAgentRuntimeState state) {
- if (state == null) {
- return;
- }
- Path path = statePath(sessionKey, name);
- createParent(path);
- try {
- Files.writeString(path, JsonUtils.getJsonCodec().toPrettyJson(state), StandardCharsets.UTF_8,
- StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
- } catch (IOException e) {
- throw new AgentSessionStoreException("Failed to save agent session state: " + sessionKey + "/" + name, e);
- }
- }
-
- @Override
- public void saveList(String sessionKey, String name, List states) {
- Path path = listPath(sessionKey, name);
- createParent(path);
- try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8,
- StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
- if (states == null) {
- return;
- }
- for (SerializedAgentRuntimeState state : states) {
- writer.write(JsonUtils.getJsonCodec().toJson(state));
- writer.newLine();
- }
- } catch (IOException e) {
- throw new AgentSessionStoreException("Failed to save agent session state list: " + sessionKey + "/" + name, e);
- }
- }
-
- @Override
- public Optional get(String sessionKey, String name) {
- Path path = statePath(sessionKey, name);
- if (!Files.exists(path)) {
- return Optional.empty();
- }
- try {
- String json = Files.readString(path, StandardCharsets.UTF_8);
- return Optional.of(JsonUtils.getJsonCodec().fromJson(json, SerializedAgentRuntimeState.class));
- } catch (IOException e) {
- throw new AgentSessionStoreException("Failed to load agent session state: " + sessionKey + "/" + name, e);
- }
- }
-
- @Override
- public List getList(String sessionKey, String name) {
- Path path = listPath(sessionKey, name);
- if (!Files.exists(path)) {
- return List.of();
- }
- List states = new ArrayList<>();
- try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
- String line;
- while ((line = reader.readLine()) != null) {
- if (!line.isBlank()) {
- states.add(JsonUtils.getJsonCodec().fromJson(line, SerializedAgentRuntimeState.class));
- }
- }
- return states;
- } catch (IOException e) {
- throw new AgentSessionStoreException("Failed to load agent session state list: " + sessionKey + "/" + name, e);
- }
- }
-
- @Override
- public boolean exists(String sessionKey) {
- return Files.isDirectory(sessionDirectory(sessionKey));
- }
-
- @Override
- public void delete(String sessionKey) {
- Path directory = sessionDirectory(sessionKey);
- if (!Files.exists(directory)) {
- return;
- }
- try (Stream paths = Files.walk(directory)) {
- List ordered = paths.sorted(Comparator.reverseOrder()).collect(Collectors.toList());
- for (Path path : ordered) {
- Files.deleteIfExists(path);
- }
- } catch (IOException e) {
- throw new AgentSessionStoreException("Failed to delete agent session: " + sessionKey, e);
- }
- }
-
- @Override
- public Set listSessionKeys() {
- if (!Files.exists(rootDirectory)) {
- return Set.of();
- }
- try (Stream paths = Files.list(rootDirectory)) {
- return paths.filter(Files::isDirectory)
- .map(path -> path.getFileName().toString())
- .collect(Collectors.toCollection(LinkedHashSet::new));
- } catch (IOException e) {
- throw new AgentSessionStoreException("Failed to list agent sessions.", e);
- }
- }
-
- private Path statePath(String sessionKey, String name) {
- return sessionDirectory(sessionKey).resolve(safeName(name) + ".json");
- }
-
- private Path listPath(String sessionKey, String name) {
- return sessionDirectory(sessionKey).resolve(safeName(name) + ".jsonl");
- }
-
- private Path sessionDirectory(String sessionKey) {
- return rootDirectory.resolve(safeName(sessionKey));
- }
-
- private String safeName(String value) {
- if (value == null || value.isBlank() || !SAFE_NAME.matcher(value).matches()
- || value.contains("..")) {
- throw new AgentSessionStoreException("Unsafe agent session storage key: " + value);
- }
- return value;
- }
-
- private void createParent(Path path) {
- try {
- Files.createDirectories(path.getParent());
- } catch (IOException e) {
- throw new AgentSessionStoreException("Failed to create agent session directory: " + path.getParent(), e);
- }
- }
-}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/JsonAgentSessionStore.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/JsonAgentSessionStore.java
deleted file mode 100644
index fc13d20..0000000
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/JsonAgentSessionStore.java
+++ /dev/null
@@ -1,106 +0,0 @@
-package com.easyagents.agent.runtime.persistence.json;
-
-import com.easyagents.agent.runtime.persistence.AgentRuntimeState;
-import com.easyagents.agent.runtime.persistence.AgentSessionStore;
-import com.easyagents.agent.runtime.persistence.AgentSessionStoreException;
-
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-
-/**
- * 基于 JSON 记录的默认非内存会话存储。
- */
-public class JsonAgentSessionStore implements AgentSessionStore {
-
- private final AgentSessionStoreBackend backend;
- private final AgentSessionStateCodec codec;
-
- /**
- * 创建文件型 JSON 会话存储。
- *
- * @param rootDirectory 根目录
- */
- public JsonAgentSessionStore(Path rootDirectory) {
- this(new FileAgentSessionStoreBackend(rootDirectory), new AgentSessionStateCodec());
- }
-
- /**
- * 创建可替换后端的 JSON 会话存储。
- *
- * @param backend 底层存储后端
- */
- public JsonAgentSessionStore(AgentSessionStoreBackend backend) {
- this(backend, new AgentSessionStateCodec());
- }
-
- /**
- * 创建可替换后端和编解码器的 JSON 会话存储。
- *
- * @param backend 底层存储后端
- * @param codec 状态编解码器
- */
- public JsonAgentSessionStore(AgentSessionStoreBackend backend, AgentSessionStateCodec codec) {
- if (backend == null) {
- throw new AgentSessionStoreException("Agent session store backend is required.");
- }
- this.backend = backend;
- this.codec = codec == null ? new AgentSessionStateCodec() : codec;
- }
-
- @Override
- public void save(String sessionKey, String name, AgentRuntimeState state) {
- SerializedAgentRuntimeState serialized = codec.encode(state);
- if (serialized != null) {
- backend.save(sessionKey, name, serialized);
- }
- }
-
- @Override
- public void saveList(String sessionKey, String name, List states) {
- List serialized = new ArrayList<>();
- if (states != null) {
- for (AgentRuntimeState state : states) {
- SerializedAgentRuntimeState item = codec.encode(state);
- if (item != null) {
- serialized.add(item);
- }
- }
- }
- backend.saveList(sessionKey, name, serialized);
- }
-
- @Override
- public Optional get(String sessionKey, String name) {
- return backend.get(sessionKey, name).map(codec::decode);
- }
-
- @Override
- public List getList(String sessionKey, String name) {
- List states = new ArrayList<>();
- for (SerializedAgentRuntimeState item : backend.getList(sessionKey, name)) {
- AgentRuntimeState state = codec.decode(item);
- if (state != null) {
- states.add(state);
- }
- }
- return states;
- }
-
- @Override
- public boolean exists(String sessionKey) {
- return backend.exists(sessionKey);
- }
-
- @Override
- public void delete(String sessionKey) {
- backend.delete(sessionKey);
- }
-
- @Override
- public Set listSessionKeys() {
- return backend.listSessionKeys();
- }
-}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/SerializedAgentRuntimeState.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/SerializedAgentRuntimeState.java
deleted file mode 100644
index 2cb01fc..0000000
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/SerializedAgentRuntimeState.java
+++ /dev/null
@@ -1,107 +0,0 @@
-package com.easyagents.agent.runtime.persistence.json;
-
-import java.time.Instant;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-/**
- * 可跨进程持久化的 AgentScope 状态记录。
- */
-public class SerializedAgentRuntimeState {
-
- private String name;
- private String stateClassName;
- private String stateJson;
- private Map metadata = new LinkedHashMap<>();
- private Instant createdAt = Instant.now();
-
- /**
- * 获取状态名称。
- *
- * @return 状态名称
- */
- public String getName() {
- return name;
- }
-
- /**
- * 设置状态名称。
- *
- * @param name 状态名称
- */
- public void setName(String name) {
- this.name = name;
- }
-
- /**
- * 获取状态类名。
- *
- * @return 状态类名
- */
- public String getStateClassName() {
- return stateClassName;
- }
-
- /**
- * 设置状态类名。
- *
- * @param stateClassName 状态类名
- */
- public void setStateClassName(String stateClassName) {
- this.stateClassName = stateClassName;
- }
-
- /**
- * 获取状态 JSON。
- *
- * @return 状态 JSON
- */
- public String getStateJson() {
- return stateJson;
- }
-
- /**
- * 设置状态 JSON。
- *
- * @param stateJson 状态 JSON
- */
- public void setStateJson(String stateJson) {
- this.stateJson = stateJson;
- }
-
- /**
- * 获取元数据。
- *
- * @return 元数据
- */
- public Map getMetadata() {
- return metadata;
- }
-
- /**
- * 设置元数据。
- *
- * @param metadata 元数据
- */
- public void setMetadata(Map metadata) {
- this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
- }
-
- /**
- * 获取创建时间。
- *
- * @return 创建时间
- */
- public Instant getCreatedAt() {
- return createdAt;
- }
-
- /**
- * 设置创建时间。
- *
- * @param createdAt 创建时间
- */
- public void setCreatedAt(Instant createdAt) {
- this.createdAt = createdAt == null ? Instant.now() : createdAt;
- }
-}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/memory/InMemoryAgentSessionStore.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/memory/InMemoryAgentSessionStore.java
deleted file mode 100644
index 9470f89..0000000
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/memory/InMemoryAgentSessionStore.java
+++ /dev/null
@@ -1,59 +0,0 @@
-package com.easyagents.agent.runtime.persistence.memory;
-
-import com.easyagents.agent.runtime.persistence.AgentRuntimeState;
-import com.easyagents.agent.runtime.persistence.AgentSessionStore;
-
-import java.util.*;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * 用于测试和单节点 MVP 的内存会话存储。
- */
-public class InMemoryAgentSessionStore implements AgentSessionStore {
-
- private final Map>> states = new ConcurrentHashMap<>();
-
- @Override
- public void save(String sessionKey, String name, AgentRuntimeState state) {
- if (sessionKey == null || name == null || state == null) {
- return;
- }
- saveList(sessionKey, name, List.of(state));
- }
-
- @Override
- public void saveList(String sessionKey, String name, List states) {
- if (sessionKey == null || name == null) {
- return;
- }
- this.states.computeIfAbsent(sessionKey, key -> new ConcurrentHashMap<>())
- .put(name, states == null ? new ArrayList<>() : new ArrayList<>(states));
- }
-
- @Override
- public Optional get(String sessionKey, String name) {
- List list = getList(sessionKey, name);
- return list.isEmpty() ? Optional.empty() : Optional.ofNullable(list.get(0));
- }
-
- @Override
- public List getList(String sessionKey, String name) {
- Map> byName = states.getOrDefault(sessionKey, new LinkedHashMap<>());
- return new ArrayList<>(byName.getOrDefault(name, new ArrayList<>()));
- }
-
- @Override
- public boolean exists(String sessionKey) {
- return states.containsKey(sessionKey);
- }
-
- @Override
- public void delete(String sessionKey) {
- states.remove(sessionKey);
- }
-
- @Override
- public Set listSessionKeys() {
- return new LinkedHashSet<>(states.keySet());
- }
-}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/noop/NoopAgentConversationRecorder.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/noop/NoopAgentConversationRecorder.java
deleted file mode 100644
index 433aba0..0000000
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/noop/NoopAgentConversationRecorder.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.easyagents.agent.runtime.persistence.noop;
-
-import com.easyagents.agent.runtime.AgentRunRequest;
-import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
-import com.easyagents.agent.runtime.persistence.AgentConversationRecorder;
-
-/**
- * 空操作会话记录器。
- */
-public enum NoopAgentConversationRecorder implements AgentConversationRecorder {
- INSTANCE;
-
- @Override
- public void record(AgentRunRequest request, AgentRuntimeEvent event) {
- }
-}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/session/AgentSessionStore.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/session/AgentSessionStore.java
new file mode 100644
index 0000000..a992eaa
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/session/AgentSessionStore.java
@@ -0,0 +1,79 @@
+package com.easyagents.agent.runtime.persistence.session;
+
+import io.agentscope.core.state.State;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * 智能体会话状态存储接口。
+ *
+ * 该接口承接 AgentScope {@code Session} 的持久化读写能力,用于按会话键保存和恢复
+ * Agent 运行过程中产生的状态,例如 memory、toolkit、plan notebook 或有状态工具数据。
+ * 具体实现可以基于内存、Redis、MySQL 等存储介质。
+ */
+public interface AgentSessionStore {
+
+ /**
+ * 保存单个状态项。
+ *
+ * @param sessionKey 会话键
+ * @param name 状态名称
+ * @param state 状态值
+ */
+ void save(String sessionKey, String name, State state);
+
+ /**
+ * 保存状态列表。
+ *
+ * @param sessionKey 会话键
+ * @param name 状态名称
+ * @param states 状态列表
+ */
+ void saveList(String sessionKey, String name, List extends State> states);
+
+ /**
+ * 获取单个状态项。
+ *
+ * @param sessionKey 会话键
+ * @param name 状态名称
+ * @param type 状态类型
+ * @param 状态类型
+ * @return 可选状态
+ */
+ Optional get(String sessionKey, String name, Class type);
+
+ /**
+ * 获取状态列表。
+ *
+ * @param sessionKey 会话键
+ * @param name 状态名称
+ * @param itemType 状态元素类型
+ * @param 状态元素类型
+ * @return 状态列表
+ */
+ List getList(String sessionKey, String name, Class itemType);
+
+ /**
+ * 判断会话键是否存在。
+ *
+ * @param sessionKey 会话键
+ * @return 存在时为 true
+ */
+ boolean exists(String sessionKey);
+
+ /**
+ * 删除指定会话键下的全部状态。
+ *
+ * @param sessionKey 会话键
+ */
+ void delete(String sessionKey);
+
+ /**
+ * 列出当前存储中的会话键。
+ *
+ * @return 会话键列表
+ */
+ Set listSessionKeys();
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentSessionStoreException.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/session/AgentSessionStoreException.java
similarity index 91%
rename from easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentSessionStoreException.java
rename to easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/session/AgentSessionStoreException.java
index c743c8f..dd8dfb5 100644
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentSessionStoreException.java
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/session/AgentSessionStoreException.java
@@ -1,4 +1,4 @@
-package com.easyagents.agent.runtime.persistence;
+package com.easyagents.agent.runtime.persistence.session;
import com.easyagents.agent.runtime.AgentRuntimeException;
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/session/memory/InMemoryAgentSessionStore.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/session/memory/InMemoryAgentSessionStore.java
new file mode 100644
index 0000000..87a0569
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/session/memory/InMemoryAgentSessionStore.java
@@ -0,0 +1,69 @@
+package com.easyagents.agent.runtime.persistence.session.memory;
+
+import com.easyagents.agent.runtime.persistence.session.AgentSessionStore;
+import io.agentscope.core.state.State;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 用于测试和单节点 MVP 的内存会话存储。
+ */
+public class InMemoryAgentSessionStore implements AgentSessionStore {
+
+ private final Map>> states = new ConcurrentHashMap<>();
+
+ @Override
+ public void save(String sessionKey, String name, State state) {
+ if (sessionKey == null || name == null || state == null) {
+ return;
+ }
+ saveList(sessionKey, name, List.of(state));
+ }
+
+ @Override
+ public void saveList(String sessionKey, String name, List extends State> states) {
+ if (sessionKey == null || name == null) {
+ return;
+ }
+ this.states.computeIfAbsent(sessionKey, key -> new ConcurrentHashMap<>())
+ .put(name, states == null ? new ArrayList<>() : new ArrayList<>(states));
+ }
+
+ @Override
+ public Optional get(String sessionKey, String name, Class type) {
+ List list = getList(sessionKey, name, type);
+ return list.isEmpty() ? Optional.empty() : Optional.of(list.get(0));
+ }
+
+ @Override
+ public List getList(String sessionKey, String name, Class itemType) {
+ Map> byName = states.getOrDefault(sessionKey, new LinkedHashMap<>());
+ List values = byName.getOrDefault(name, new ArrayList<>());
+ if (itemType == null) {
+ return new ArrayList<>();
+ }
+ List typedValues = new ArrayList<>();
+ for (State value : values) {
+ if (itemType.isInstance(value)) {
+ typedValues.add(itemType.cast(value));
+ }
+ }
+ return typedValues;
+ }
+
+ @Override
+ public boolean exists(String sessionKey) {
+ return states.containsKey(sessionKey);
+ }
+
+ @Override
+ public void delete(String sessionKey) {
+ states.remove(sessionKey);
+ }
+
+ @Override
+ public Set listSessionKeys() {
+ return new LinkedHashSet<>(states.keySet());
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/noop/NoopAgentSessionStore.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/session/noop/NoopAgentSessionStore.java
similarity index 54%
rename from easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/noop/NoopAgentSessionStore.java
rename to easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/session/noop/NoopAgentSessionStore.java
index 36240e8..1205da8 100644
--- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/noop/NoopAgentSessionStore.java
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/session/noop/NoopAgentSessionStore.java
@@ -1,7 +1,7 @@
-package com.easyagents.agent.runtime.persistence.noop;
+package com.easyagents.agent.runtime.persistence.session.noop;
-import com.easyagents.agent.runtime.persistence.AgentRuntimeState;
-import com.easyagents.agent.runtime.persistence.AgentSessionStore;
+import com.easyagents.agent.runtime.persistence.session.AgentSessionStore;
+import io.agentscope.core.state.State;
import java.util.Collections;
import java.util.List;
@@ -15,20 +15,20 @@ public enum NoopAgentSessionStore implements AgentSessionStore {
INSTANCE;
@Override
- public void save(String sessionKey, String name, AgentRuntimeState state) {
+ public void save(String sessionKey, String name, State state) {
}
@Override
- public void saveList(String sessionKey, String name, List states) {
+ public void saveList(String sessionKey, String name, List extends State> states) {
}
@Override
- public Optional get(String sessionKey, String name) {
+ public Optional get(String sessionKey, String name, Class type) {
return Optional.empty();
}
@Override
- public List getList(String sessionKey, String name) {
+ public