Compare commits

...

18 Commits

Author SHA1 Message Date
55434466d4 chore: 发布 easy-agents 1.0.0
- 将 Maven revision 升级为 1.0.0

- 同步示例项目与 easy-agents-bom 依赖版本
2026-06-05 14:05:58 +08:00
7cac558b6c feat: 归档 XL08 异步工具协议
- 新增 AsyncToolSpec 与 AsyncSubTools 五子工具展开能力

- 增加异步工具事件、上下文事件发射和模型可见结果裁剪

- 补充 AgentScope 异步工具协议提示与 runtime 单元测试
2026-06-04 15:23:04 +08:00
43f45956ff feat: 对接 Agent MCP 能力
- 新增 runtime MCP 声明、ClientFactory、Toolkit 适配与工具别名映射

- 增加 MCP 环境检测与 stdio 环境变量透传

- 补齐 MCP 工具事件、审批与生命周期释放测试
2026-05-29 11:08:39 +08:00
2bc525c16e feat: 增加内置read、write、shell 工具支持 2026-05-28 11:16:41 +08:00
f324acb83c perf: 优化向量库连接性能 2026-05-28 11:16:09 +08:00
8b600f9d6c perf: 优化自动压缩记忆的表现 2026-05-28 11:15:52 +08:00
5c7182ac3f perf: 优化中断续聊表现,被中断的回复仍可以进入上下文中,保证记忆连续性
- 续聊逻辑优化
2026-05-25 11:40:10 +08:00
2b5e701ade refactor: 重构升级为有状态 Agent
- 完善 hook 对接机制
- 提供更加明确的调用方式
2026-05-23 21:17:50 +08:00
8356560c26 feat: 先进智能体运行时基座 v1
- 支持高层调用进行可配置智能体业务开发
- 封装基于 agentscope-java
- 支持 tool、skill、知识库等封装
- 支持 tool 审批
- 支持智能体事件回传
- 等等
2026-05-19 17:30:42 +08:00
787ca328f9 chore: 增加 agent-runtime 模块,准备agent 底座开发 2026-05-18 11:29:57 +08:00
e16f1f00dc chore: JDK版本升级至 17 2026-05-17 16:42:18 +08:00
8b34b4ec40 fix: 修复循环节点模板上下文与累计输出
- 统一节点模板渲染上下文,补齐 memory、当前参数与环境变量

- 支持循环体读取父循环上一轮输出,并区分空字符串与缺失值

- 补充模板路径、上下文与循环累计场景回归测试
2026-04-18 21:02:59 +08:00
56ee149e7c refactor: 收敛文档 OCR 配置并补齐 Office 解析兼容
- 统一 MinerU OCR 配置结构并移除分模块冗余属性类

- 补齐 JSON 字符串化结果拆包、XLSX 图片兼容与对应回归测试
2026-04-18 13:01:17 +08:00
b66876d0fd feat: 扩展 Office 文档解析能力
- 重构 document-core 与 MinerU 公共层,补齐 Office 异步任务基础设施

- 新增 PPTX/XLSX 解析模块与 starter 自动装配

- 补充 README 与相关测试覆盖
2026-04-16 21:51:16 +08:00
547d4f6ee0 feat: 完善统一文档解析与分块原文映射
- 兼容 MinerU docx 嵌套压缩工件与数组模型输出

- 补充异步解析日志与 sourceRanges 原文区间映射
2026-04-15 19:27:22 +08:00
0c7b362173 fix: 补齐L09的MinerU自动装配配置类
- 新增 MinerU Spring Boot 配置属性类

- 新增 PDF 文档解析自动装配并暴露统一 DocumentParseService
2026-04-14 19:58:09 +08:00
aa3e90b990 feat: 完成L09统一文档解析模块与MinerU PDF Provider接入
- 新增 easy-agents-document 聚合、document-core 与 document-pdf 模块

- 接入 MinerU PDF provider,支持同步解析、异步任务与 ZIP 结果映射

- 移除 easy-agents-rag-ocr 空壳并补齐 starter 自动装配
2026-04-14 19:57:32 +08:00
090eca5df5 fix: 兼容工作流开始节点字段化参数解析
- 为开始节点输入补齐裸参数名与节点作用域别名映射

- 文本模板优先解析扁平节点路径并补充回归测试
2026-04-12 20:31:02 +08:00
269 changed files with 26992 additions and 265 deletions

View File

@@ -15,6 +15,7 @@ Easy-Agents 是一个轻量、可扩展的 Java AI 应用开发框架,覆盖
- `easy-agents-bom`依赖版本管理BOM - `easy-agents-bom`依赖版本管理BOM
- `easy-agents-core`:核心抽象与基础能力。 - `easy-agents-core`:核心抽象与基础能力。
- `easy-agents-document`:统一文档解析能力域,当前提供 PDF、PPTX、XLSX 解析抽象与 MinerU 复用能力。
- `easy-agents-chat`:对话模型接入实现集合。 - `easy-agents-chat`:对话模型接入实现集合。
- `easy-agents-embedding`:向量化模型实现集合。 - `easy-agents-embedding`:向量化模型实现集合。
- `easy-agents-rerank`:重排模型实现集合。 - `easy-agents-rerank`:重排模型实现集合。
@@ -30,7 +31,7 @@ Easy-Agents 是一个轻量、可扩展的 Java AI 应用开发框架,覆盖
## 环境要求 ## 环境要求
- JDK 8+ - JDK 17+
- Maven 3.8+ - Maven 3.8+
## 构建与安装 ## 构建与安装
@@ -84,4 +85,3 @@ public static void main(String[] args) {
</dependency> </dependency>
</dependencies> </dependencies>
``` ```

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.easyagents</groupId>
<artifactId>easy-agents</artifactId>
<version>${revision}</version>
</parent>
<name>easy-agents-agent-runtime</name>
<artifactId>easy-agents-agent-runtime</artifactId>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<dependency>
<groupId>com.anthropic</groupId>
<artifactId>anthropic-java</artifactId>
<version>2.14.0</version>
</dependency>
<dependency>
<groupId>com.google.genai</groupId>
<artifactId>google-genai</artifactId>
<version>1.38.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,309 @@
package com.easyagents.agent.runtime;
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeSpec;
import com.easyagents.agent.runtime.memory.AgentMemoryPolicy;
import com.easyagents.agent.runtime.mcp.McpSpec;
import com.easyagents.agent.runtime.model.AgentGenerationOptions;
import com.easyagents.agent.runtime.model.AgentModelSpec;
import com.easyagents.agent.runtime.persistence.AgentPersistencePolicy;
import com.easyagents.agent.runtime.skill.AgentSkillBoxSpec;
import com.easyagents.agent.runtime.tool.AgentToolSpec;
import com.easyagents.agent.runtime.tool.operate.AgentOperateToolSpec;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 主入口
* ReAct 智能体的声明式定义
*/
public class AgentDefinition {
private String agentId;
private String agentName;
private String description;
private String systemPrompt;
private AgentModelSpec modelSpec;
private AgentGenerationOptions generationOptions = new AgentGenerationOptions();
private AgentExecutionOptions executionOptions = new AgentExecutionOptions();
private List<AgentToolSpec> toolSpecs = new ArrayList<>();
private List<McpSpec> mcpSpecs = new ArrayList<>();
private List<AgentOperateToolSpec> operateToolSpecs = new ArrayList<>();
private List<AgentKnowledgeSpec> knowledgeSpecs = new ArrayList<>();
private AgentMemoryPolicy memoryPolicy = AgentMemoryPolicy.autoContext();
private AgentPersistencePolicy persistencePolicy = AgentPersistencePolicy.disabled();
private AgentSkillBoxSpec skillBoxSpec;
private Map<String, Object> metadata = new LinkedHashMap<>();
/**
* 获取agent id。
*
* @return 智能体ID
*/
public String getAgentId() {
return agentId;
}
/**
* 设置agent id。
*
* @param agentId 智能体ID
*/
public void setAgentId(String agentId) {
this.agentId = agentId;
}
/**
* 获取agent name。
*
* @return 智能体名称
*/
public String getAgentName() {
return agentName;
}
/**
* 设置agent name。
*
* @param agentName 智能体名称
*/
public void setAgentName(String agentName) {
this.agentName = agentName;
}
/**
* 获取agent description。
*
* @return 智能体描述
*/
public String getDescription() {
return description;
}
/**
* 设置agent description。
*
* @param description 智能体描述
*/
public void setDescription(String description) {
this.description = description;
}
/**
* 获取system prompt。
*
* @return 系统提示词
*/
public String getSystemPrompt() {
return systemPrompt;
}
/**
* 设置system prompt。
*
* @param systemPrompt 系统提示词
*/
public void setSystemPrompt(String systemPrompt) {
this.systemPrompt = systemPrompt;
}
/**
* 获取模型设置。
*
* @return 模型设置
*/
public AgentModelSpec getModelSpec() {
return modelSpec;
}
/**
* 设置模型设置。
*
* @param modelSpec 模型设置
*/
public void setModelSpec(AgentModelSpec modelSpec) {
this.modelSpec = modelSpec;
}
/**
* 获取生成参数。
*
* @return 生成参数
*/
public AgentGenerationOptions getGenerationOptions() {
return generationOptions;
}
/**
* 设置生成参数。
*
* @param generationOptions 生成参数
*/
public void setGenerationOptions(AgentGenerationOptions generationOptions) {
this.generationOptions = generationOptions == null ? new AgentGenerationOptions() : generationOptions;
}
/**
* 获取执行参数。
*
* @return 执行参数
*/
public AgentExecutionOptions getExecutionOptions() {
return executionOptions;
}
/**
* 设置执行参数。
*
* @param executionOptions 执行参数
*/
public void setExecutionOptions(AgentExecutionOptions executionOptions) {
this.executionOptions = executionOptions == null ? new AgentExecutionOptions() : executionOptions;
}
/**
* 获取工具定义。
*
* @return 工具定义
*/
public List<AgentToolSpec> getToolSpecs() {
return toolSpecs;
}
/**
* 设置工具定义。
*
* @param toolSpecs 工具定义
*/
public void setToolSpecs(List<AgentToolSpec> toolSpecs) {
this.toolSpecs = toolSpecs == null ? new ArrayList<>() : new ArrayList<>(toolSpecs);
}
/**
* 获取 MCP 声明。
*
* @return MCP 声明
*/
public List<McpSpec> getMcpSpecs() {
return mcpSpecs;
}
/**
* 设置 MCP 声明。
*
* @param mcpSpecs MCP 声明
*/
public void setMcpSpecs(List<McpSpec> mcpSpecs) {
this.mcpSpecs = mcpSpecs == null ? new ArrayList<>() : new ArrayList<>(mcpSpecs);
}
/**
* 获取操作类工具定义。
*
* @return 操作类工具定义
*/
public List<AgentOperateToolSpec> getOperateToolSpecs() {
return operateToolSpecs;
}
/**
* 设置操作类工具定义。
*
* @param operateToolSpecs 操作类工具定义
*/
public void setOperateToolSpecs(List<AgentOperateToolSpec> operateToolSpecs) {
this.operateToolSpecs = operateToolSpecs == null ? new ArrayList<>() : new ArrayList<>(operateToolSpecs);
}
/**
* 获取知识库定义。
*
* @return 知识库定义
*/
public List<AgentKnowledgeSpec> getKnowledgeSpecs() {
return knowledgeSpecs;
}
/**
* 设置知识库定义。
*
* @param knowledgeSpecs 知识库定义
*/
public void setKnowledgeSpecs(List<AgentKnowledgeSpec> knowledgeSpecs) {
this.knowledgeSpecs = knowledgeSpecs == null ? new ArrayList<>() : new ArrayList<>(knowledgeSpecs);
}
/**
* 获取记忆策略。
*
* @return 记忆策略
*/
public AgentMemoryPolicy getMemoryPolicy() {
return memoryPolicy;
}
/**
* 设置记忆策略。
*
* @param memoryPolicy 记忆策略
*/
public void setMemoryPolicy(AgentMemoryPolicy memoryPolicy) {
this.memoryPolicy = memoryPolicy == null ? AgentMemoryPolicy.autoContext() : memoryPolicy;
}
/**
* 获取持久化策略。
*
* @return 持久化策略
*/
public AgentPersistencePolicy getPersistencePolicy() {
return persistencePolicy;
}
/**
* 设置持久化策略。
*
* @param persistencePolicy 持久化策略
*/
public void setPersistencePolicy(AgentPersistencePolicy persistencePolicy) {
this.persistencePolicy = persistencePolicy == null ? AgentPersistencePolicy.disabled() : persistencePolicy;
}
/**
* 获取SkillBox 设置。
*
* @return SkillBox 设置
*/
public AgentSkillBoxSpec getSkillBoxSpec() {
return skillBoxSpec;
}
/**
* 设置SkillBox 设置。
*
* @param skillBoxSpec SkillBox 设置
*/
public void setSkillBoxSpec(AgentSkillBoxSpec skillBoxSpec) {
this.skillBoxSpec = skillBoxSpec;
}
/**
* 获取元数据。
*
* @return 元数据
*/
public Map<String, Object> getMetadata() {
return metadata;
}
/**
* 设置元数据。
*
* @param metadata 元数据
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
}
}

View File

@@ -0,0 +1,86 @@
package com.easyagents.agent.runtime;
import java.time.Duration;
/**
* 运行时执行参数。
*/
public class AgentExecutionOptions {
private int maxIters = 10;
private Duration timeout = Duration.ofMinutes(5);
private boolean reasoningEnabled = true;
private boolean toolCallingEnabled = true;
/**
* 获取最大 ReAct 迭代次数。
*
* @return 最大迭代次数
*/
public int getMaxIters() {
return maxIters;
}
/**
* 设置最大 ReAct 迭代次数。
*
* @param maxIters 最大迭代次数
*/
public void setMaxIters(int maxIters) {
this.maxIters = maxIters <= 0 ? 10 : maxIters;
}
/**
* 获取超时时间。
*
* @return 超时时间
*/
public Duration getTimeout() {
return timeout;
}
/**
* 设置超时时间。
*
* @param timeout 超时时间
*/
public void setTimeout(Duration timeout) {
this.timeout = timeout == null ? Duration.ofMinutes(5) : timeout;
}
/**
* 返回是否reasoning events are enabled。
*
* @return 启用时为 true
*/
public boolean isReasoningEnabled() {
return reasoningEnabled;
}
/**
* 设置whether reasoning events are enabled。
*
* @param reasoningEnabled 启用时为 true
*/
public void setReasoningEnabled(boolean reasoningEnabled) {
this.reasoningEnabled = reasoningEnabled;
}
/**
* 返回是否tool calls are enabled。
*
* @return 启用时为 true
*/
public boolean isToolCallingEnabled() {
return toolCallingEnabled;
}
/**
* 设置whether tool calls are enabled。
*
* @param toolCallingEnabled 启用时为 true
*/
public void setToolCallingEnabled(boolean toolCallingEnabled) {
this.toolCallingEnabled = toolCallingEnabled;
}
}

View File

@@ -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<String, AgentToolInvoker> toolInvokers = new LinkedHashMap<>();
/**
* 知识库集合实现AgentKnowledgeRetriever接口以进行知识检索动作。
*/
private Map<String, AgentKnowledgeRetriever> knowledgeRetrievers = new LinkedHashMap<>();
/**
* 对话事件记录器,用于记录运行时事件流。
*/
private AgentConversationRecorder conversationRecorder = NoopAgentConversationRecorder.INSTANCE;
/**
* 初始化元数据,用于传递业务侧扩展信息。
*/
private Map<String, Object> 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<String, AgentToolInvoker> getToolInvokers() {
return toolInvokers;
}
/**
* 设置工具调用器。
*
* @param toolInvokers 工具调用器
*/
public void setToolInvokers(Map<String, AgentToolInvoker> toolInvokers) {
this.toolInvokers = toolInvokers == null ? new LinkedHashMap<>() : toolInvokers;
}
/**
* 获取知识库检索器。
*
* @return 知识库检索器
*/
public Map<String, AgentKnowledgeRetriever> getKnowledgeRetrievers() {
return knowledgeRetrievers;
}
/**
* 设置知识库检索器。
*
* @param knowledgeRetrievers 知识库检索器
*/
public void setKnowledgeRetrievers(Map<String, AgentKnowledgeRetriever> 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<String, Object> getMetadata() {
return metadata;
}
/**
* 设置元数据。
*
* @param metadata 元数据
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
}
}

View File

@@ -0,0 +1,131 @@
package com.easyagents.agent.runtime;
import com.easyagents.agent.runtime.hitl.AgentResumeToken;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 智能体挂起运行恢复请求。
*/
public class AgentResumeRequest {
/**
* 挂起运行的恢复令牌。
*/
private AgentResumeToken resumeToken;
/**
* 是否批准继续执行。
*/
private boolean approved;
/**
* 拒绝继续执行时的原因。
*/
private String rejectReason;
/**
* 调用方传入的恢复元数据。
*/
private Map<String, Object> metadata = new LinkedHashMap<>();
/**
* 恢复请求是否已由调用方的持久化 pending store 完成校验和一次性消费。
*
* <p>该字段仅供服务端集成层使用。普通调用方不应设置该标记;设置后 runtime 会跳过
* 当前进程内 {@code AgentToolApprovalCoordinator} 的 token 存在性校验,用于服务重启或跨节点后
* 从 AgentScope session 中继续 pending tool。</p>
*/
private boolean trusted;
/**
* 获取恢复令牌。
*
* @return 恢复令牌
*/
public AgentResumeToken getResumeToken() {
return resumeToken;
}
/**
* 设置恢复令牌。
*
* @param resumeToken 恢复令牌
*/
public void setResumeToken(AgentResumeToken resumeToken) {
this.resumeToken = resumeToken;
}
/**
* 返回是否批准继续执行。
*
* @return 批准时为 true
*/
public boolean isApproved() {
return approved;
}
/**
* 设置是否批准继续执行。
*
* @param approved 批准标记
*/
public void setApproved(boolean approved) {
this.approved = approved;
}
/**
* 获取拒绝原因。
*
* @return 拒绝原因
*/
public String getRejectReason() {
return rejectReason;
}
/**
* 设置拒绝原因。
*
* @param rejectReason 拒绝原因
*/
public void setRejectReason(String rejectReason) {
this.rejectReason = rejectReason;
}
/**
* 获取恢复元数据。
*
* @return 恢复元数据
*/
public Map<String, Object> getMetadata() {
return metadata;
}
/**
* 设置恢复元数据。
*
* @param metadata 恢复元数据
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
}
/**
* 返回恢复请求是否已由调用方持久化层校验。
*
* @return 已校验时为 true
*/
public boolean isTrusted() {
return trusted;
}
/**
* 设置恢复请求是否已由调用方持久化层校验。
*
* @param trusted 已校验标记
*/
public void setTrusted(boolean trusted) {
this.trusted = trusted;
}
}

View File

@@ -0,0 +1,49 @@
package com.easyagents.agent.runtime;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 智能体运行的最终结果。
*/
public class AgentRunResult {
private String content;
private Map<String, Object> metadata = new LinkedHashMap<>();
/**
* 获取最终内容。
*
* @return 最终内容
*/
public String getContent() {
return content;
}
/**
* 设置最终内容。
*
* @param content 最终内容
*/
public void setContent(String content) {
this.content = content;
}
/**
* 获取元数据。
*
* @return 元数据
*/
public Map<String, Object> getMetadata() {
return metadata;
}
/**
* 设置元数据。
*
* @param metadata 元数据
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
}
}

View File

@@ -0,0 +1,40 @@
package com.easyagents.agent.runtime;
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
import com.easyagents.agent.runtime.message.AgentMessage;
import reactor.core.publisher.Flux;
/**
* 有状态智能体运行器。
*/
public interface AgentRuntime {
/**
* 初始化智能体运行器。
*
* @param request 初始化请求
*/
void init(AgentInitRequest request);
/**
* 发送用户消息并流式输出运行事件。
*
* @param userMessage 用户消息
* @return 运行事件流
*/
Flux<AgentRuntimeEvent> stream(AgentMessage userMessage);
/**
* 恢复一次已挂起的运行。
*
* @param request 恢复请求
* @return 运行事件流
*/
Flux<AgentRuntimeEvent> resume(AgentResumeRequest request);
/**
* 关闭运行器并释放底层资源。
*/
default void close() {
}
}

View File

@@ -0,0 +1,125 @@
package com.easyagents.agent.runtime;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 传递给工具和检索器的中立运行时上下文。
*/
public class AgentRuntimeContext {
private String tenantId;
private String userId;
private String userName;
private String sessionId;
private String traceId;
private Map<String, Object> metadata = new LinkedHashMap<>();
/**
* 获取租户ID。
*
* @return 租户ID
*/
public String getTenantId() {
return tenantId;
}
/**
* 设置租户ID。
*
* @param tenantId 租户ID
*/
public void setTenantId(String tenantId) {
this.tenantId = tenantId;
}
/**
* 获取用户ID。
*
* @return 用户ID
*/
public String getUserId() {
return userId;
}
/**
* 设置用户ID。
*
* @param userId 用户ID
*/
public void setUserId(String userId) {
this.userId = userId;
}
/**
* 获取用户名。
*
* @return 用户名
*/
public String getUserName() {
return userName;
}
/**
* 设置用户名。
*
* @param userName 用户名
*/
public void setUserName(String userName) {
this.userName = userName;
}
/**
* 获取会话ID。
*
* @return 会话ID
*/
public String getSessionId() {
return sessionId;
}
/**
* 设置会话ID。
*
* @param sessionId 会话ID
*/
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
/**
* 获取链路ID。
*
* @return 链路ID
*/
public String getTraceId() {
return traceId;
}
/**
* 设置链路ID。
*
* @param traceId 链路ID
*/
public void setTraceId(String traceId) {
this.traceId = traceId;
}
/**
* 获取元数据。
*
* @return 元数据
*/
public Map<String, Object> getMetadata() {
return metadata;
}
/**
* 设置元数据。
*
* @param metadata 元数据
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
}
}

View File

@@ -0,0 +1,26 @@
package com.easyagents.agent.runtime;
/**
* 智能体运行时基础设施抛出的异常。
*/
public class AgentRuntimeException extends RuntimeException {
/**
* 创建包含错误消息的异常。
*
* @param message 错误消息
*/
public AgentRuntimeException(String message) {
super(message);
}
/**
* 创建包含错误消息和根因的异常。
*
* @param message 错误消息
* @param cause 根因
*/
public AgentRuntimeException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,320 @@
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.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 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<String, AgentToolInvoker> toolInvokers = new LinkedHashMap<>();
/**
* 按知识库ID索引的检索器。
*/
private Map<String, AgentKnowledgeRetriever> knowledgeRetrievers = new LinkedHashMap<>();
/**
* 会话状态存储。
*/
private AgentSessionStore sessionStore = NoopAgentSessionStore.INSTANCE;
/**
* 对话事件记录器。
*/
private AgentConversationRecorder conversationRecorder = NoopAgentConversationRecorder.INSTANCE;
/**
* 运行元数据。
*/
private Map<String, Object> metadata = new LinkedHashMap<>();
/**
* 运行取消原因。
*/
private String cancelReason;
/**
* 获取请求ID。
*
* @return 请求ID
*/
public String getRequestId() {
return requestId;
}
/**
* 设置请求ID。
*
* @param requestId 请求ID
*/
public void setRequestId(String requestId) {
this.requestId = requestId;
}
/**
* 获取链路ID。
*
* @return 链路ID
*/
public String getTraceId() {
return traceId;
}
/**
* 设置链路ID。
*
* @param traceId 链路ID
*/
public void setTraceId(String traceId) {
this.traceId = traceId;
}
/**
* 获取会话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 AgentRuntimeContext getRuntimeContext() {
return runtimeContext;
}
/**
* 设置运行时上下文。
*
* @param runtimeContext 运行时上下文
*/
public void setRuntimeContext(AgentRuntimeContext runtimeContext) {
this.runtimeContext = runtimeContext == null ? new AgentRuntimeContext() : runtimeContext;
}
/**
* 获取用户消息。
*
* @return 用户消息
*/
public AgentMessage getUserMessage() {
return userMessage;
}
/**
* 设置用户消息。
*
* @param userMessage 用户消息
*/
public void setUserMessage(AgentMessage userMessage) {
this.userMessage = userMessage;
}
/**
* 获取记忆快照。
*
* @return 记忆快照
*/
public AgentMemorySnapshot getMemorySnapshot() {
return memorySnapshot;
}
/**
* 设置记忆快照。
*
* @param memorySnapshot 记忆快照
*/
public void setMemorySnapshot(AgentMemorySnapshot memorySnapshot) {
this.memorySnapshot = memorySnapshot == null ? new AgentMemorySnapshot() : memorySnapshot;
}
/**
* 获取工具调用器。
*
* @return 工具调用器
*/
public Map<String, AgentToolInvoker> getToolInvokers() {
return toolInvokers;
}
/**
* 设置工具调用器。
*
* @param toolInvokers 工具调用器
*/
public void setToolInvokers(Map<String, AgentToolInvoker> toolInvokers) {
this.toolInvokers = toolInvokers == null ? new LinkedHashMap<>() : toolInvokers;
}
/**
* 获取知识库检索器。
*
* @return 知识库检索器
*/
public Map<String, AgentKnowledgeRetriever> getKnowledgeRetrievers() {
return knowledgeRetrievers;
}
/**
* 设置知识库检索器。
*
* @param knowledgeRetrievers 知识库检索器
*/
public void setKnowledgeRetrievers(Map<String, AgentKnowledgeRetriever> knowledgeRetrievers) {
this.knowledgeRetrievers = knowledgeRetrievers == null ? new LinkedHashMap<>() : knowledgeRetrievers;
}
/**
* 获取会话存储。
*
* @return 会话存储
*/
public AgentSessionStore getSessionStore() {
return sessionStore;
}
/**
* 设置会话存储。
*
* @param sessionStore 会话存储
*/
public void setSessionStore(AgentSessionStore sessionStore) {
this.sessionStore = sessionStore == null ? NoopAgentSessionStore.INSTANCE : sessionStore;
}
/**
* 获取对话记录器。
*
* @return 对话记录器
*/
public AgentConversationRecorder getConversationRecorder() {
return conversationRecorder;
}
/**
* 设置对话记录器。
*
* @param conversationRecorder 对话记录器
*/
public void setConversationRecorder(AgentConversationRecorder conversationRecorder) {
this.conversationRecorder = conversationRecorder == null
? NoopAgentConversationRecorder.INSTANCE
: conversationRecorder;
}
/**
* 获取运行元数据。
*
* @return 运行元数据
*/
public Map<String, Object> getMetadata() {
return metadata;
}
/**
* 设置运行元数据。
*
* @param metadata 运行元数据
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
}
/**
* 获取取消原因。
*
* @return 取消原因
*/
public String getCancelReason() {
return cancelReason;
}
/**
* 设置取消原因。
*
* @param cancelReason 取消原因
*/
public void setCancelReason(String cancelReason) {
this.cancelReason = cancelReason;
}
}

View File

@@ -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<ChatResponse> stream(List<Msg> messages, List<ToolSchema> tools, GenerateOptions options) {
return delegate.stream(sanitizeCompressionMessages(messages), tools, options);
}
/**
* 获取模型名称。
*
* @return 模型名称
*/
@Override
public String getModelName() {
return delegate.getModelName();
}
/**
* 将 AutoContext 摘要材料中的工具协议消息转换为普通文本消息。
*
* @param messages 原始摘要输入
* @return 协议安全的摘要输入
*/
List<Msg> sanitizeCompressionMessages(List<Msg> messages) {
if (messages == null || messages.isEmpty()) {
return messages;
}
List<Msg> 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<ContentBlock> 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<String, Object> 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');
}
}

View File

@@ -0,0 +1,310 @@
package com.easyagents.agent.runtime.agentscope;
import com.easyagents.agent.runtime.AgentRuntimeException;
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;
import io.agentscope.core.rag.model.Document;
import io.agentscope.core.rag.model.DocumentMetadata;
import io.agentscope.core.rag.model.RetrieveConfig;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import java.util.*;
/**
* 将运行时知识库检索器适配为一个聚合 AgentScope Knowledge。
*/
public class AgentScopeKnowledgeAdapter {
/**
* 创建聚合 Knowledge。
*
* @param request 运行请求
* @return 聚合 Knowledge未配置知识库时返回 null
*/
public Knowledge createAggregateKnowledge(AgentRuntimeExecutionContext request) {
return createAggregateKnowledge(request, (Sinks.Many<AgentRuntimeEvent>) null);
}
/**
* 创建带事件 sink 的聚合 Knowledge。
*
* @param request 运行请求
* @param eventSink 事件 sink
* @return 聚合 Knowledge未配置知识库时返回 null
*/
public Knowledge createAggregateKnowledge(AgentRuntimeExecutionContext request, Sinks.Many<AgentRuntimeEvent> 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, turnContextHolder);
}
private AgentRuntimeTurnContextHolder fixedHolder(AgentRuntimeExecutionContext request,
Sinks.Many<AgentRuntimeEvent> eventSink) {
AgentRuntimeTurnContextHolder holder = new AgentRuntimeTurnContextHolder();
AgentRuntimeEventBridge bridge = new AgentRuntimeEventBridge(request, holder);
holder.set(new AgentRuntimeTurnContext(null, eventSink, bridge));
return holder;
}
/**
* 将运行时文档转换为 AgentScope 文档。
*
* @param documents 运行时文档
* @return AgentScope 文档
*/
public List<Document> toDocuments(List<AgentKnowledgeDocument> documents) {
List<Document> converted = new ArrayList<>();
if (documents == null) {
return converted;
}
for (AgentKnowledgeDocument document : documents) {
converted.add(toDocument(document));
}
return converted;
}
private Document toDocument(AgentKnowledgeDocument document) {
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("documentId", document.getDocumentId());
payload.put("documentName", document.getDocumentName());
payload.put("chunkId", document.getChunkId());
payload.put("sourceUri", document.getSourceUri());
payload.put("knowledgeMetadata", document.getKnowledgeMetadata());
payload.put("documentMetadata", document.getMetadata());
payload.putAll(document.getMetadata());
DocumentMetadata metadata = DocumentMetadata.builder()
.content(TextBlock.builder().text(safeContent(document)).build())
.docId(safeDocumentId(document))
.chunkId(safeChunkId(document))
.payload(payload)
.build();
Document converted = new Document(metadata);
converted.setScore(document.getScore());
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 AgentRuntimeExecutionContext request;
private final AgentRuntimeTurnContextHolder turnContextHolder;
private AggregateKnowledge(AgentRuntimeExecutionContext request, AgentRuntimeTurnContextHolder turnContextHolder) {
this.request = request;
this.turnContextHolder = turnContextHolder;
}
/**
* 忽略文档新增,因为知识库索引由 EasyFlow 负责。
*
* @param documents 文档列表
* @return 完成信号
*/
@Override
public Mono<Void> addDocuments(List<Document> documents) {
return Mono.error(new UnsupportedOperationException(
"Easy-Agents agent runtime knowledge does not support addDocuments. Use external knowledge service instead."));
}
/**
* 从已配置的知识源检索文档。
*
* @param query 查询
* @param config 检索配置
* @return 文档列表
*/
@Override
public Mono<List<Document>> retrieve(String query, RetrieveConfig config) {
return Mono.fromCallable(() -> retrieveAll(query, config));
}
/**
* 检索并合并所有已配置的知识源。
*
* @param query 查询
* @param config 检索配置
* @return 合并后的文档
*/
private List<Document> retrieveAll(String query, RetrieveConfig config) {
List<AgentKnowledgeDocument> allDocuments = new ArrayList<>();
int globalLimit = config == null || config.getLimit() <= 0 ? 5 : config.getLimit();
double globalThreshold = config == null ? 0D : config.getScoreThreshold();
for (AgentKnowledgeSpec spec : request.getAgentDefinition().getKnowledgeSpecs()) {
AgentKnowledgeRetriever retriever = request.getKnowledgeRetrievers().get(spec.getKnowledgeId());
if (retriever == null) {
throw new AgentRuntimeException("Knowledge retriever is required: " + spec.getKnowledgeId());
}
AgentKnowledgeRetrievalRequest retrievalRequest = new AgentKnowledgeRetrievalRequest();
retrievalRequest.setQuery(query);
retrievalRequest.setLimit(spec.getLimit());
retrievalRequest.setScoreThreshold(Math.max(spec.getScoreThreshold(), globalThreshold));
retrievalRequest.setKnowledgeSpec(spec);
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<>());
continue;
}
emitKnowledgeRetrievalEvent(query, spec, retrievalRequest, result.getDocuments());
for (AgentKnowledgeDocument document : result.getDocuments()) {
preserveKnowledgeMetadata(spec, document);
allDocuments.add(document);
}
}
allDocuments.sort(Comparator.comparing(
AgentKnowledgeDocument::getScore,
Comparator.nullsLast(Comparator.reverseOrder())
));
if (allDocuments.size() > globalLimit) {
allDocuments = new ArrayList<>(allDocuments.subList(0, globalLimit));
}
return toDocuments(allDocuments);
}
/**
* 在单条文档上保留知识库级元数据。
*
* @param spec 知识库声明
* @param document 文档
*/
private void preserveKnowledgeMetadata(AgentKnowledgeSpec spec, AgentKnowledgeDocument document) {
Map<String, Object> knowledgeMetadata = new LinkedHashMap<>(spec.getMetadata());
knowledgeMetadata.put("knowledgeId", spec.getKnowledgeId());
knowledgeMetadata.put("knowledgeName", spec.getName());
knowledgeMetadata.put("retrievalMode", spec.getRetrievalMode().name());
knowledgeMetadata.putAll(document.getKnowledgeMetadata());
document.setKnowledgeMetadata(knowledgeMetadata);
document.getMetadata().putIfAbsent("knowledgeId", spec.getKnowledgeId());
document.getMetadata().putIfAbsent("knowledgeName", spec.getName());
}
/**
* 发射知识库检索旁路事件,供聊天界面展示检索过程。
*
* <p>知识库检索本身属于 AgentScope RAG 主线路,返回的 Document 会继续进入
* AgentScope 的上下文注入流程;这里发出的 {@code KNOWLEDGE_RETRIEVAL}
* 只是旁路告知调用方,不会回写 memory也不会参与模型消息序列。</p>
*
* @param query 查询
* @param spec 知识库声明
* @param retrievalRequest 检索请求
* @param documents 检索文档
*/
private void emitKnowledgeRetrievalEvent(String query,
AgentKnowledgeSpec spec,
AgentKnowledgeRetrievalRequest retrievalRequest,
List<AgentKnowledgeDocument> documents) {
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));
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 命中片段列表
*/
private List<Map<String, Object>> documentSummaries(List<AgentKnowledgeDocument> documents) {
List<Map<String, Object>> summaries = new ArrayList<>();
if (documents == null) {
return summaries;
}
for (AgentKnowledgeDocument document : documents) {
Map<String, Object> summary = new LinkedHashMap<>();
summary.put("documentId", document.getDocumentId());
summary.put("documentName", document.getDocumentName());
summary.put("chunkId", document.getChunkId());
summary.put("chunkContent", document.getContent());
summary.put("score", document.getScore());
summary.put("sourceUri", document.getSourceUri());
summary.put("metadata", document.getMetadata());
summaries.add(summary);
}
return summaries;
}
}
}

View File

@@ -0,0 +1,93 @@
package com.easyagents.agent.runtime.agentscope;
import com.easyagents.agent.runtime.memory.AgentMemoryCompressionParameter;
import com.easyagents.agent.runtime.memory.AgentMemoryPolicy;
import com.easyagents.agent.runtime.memory.AgentMemorySnapshot;
import com.easyagents.agent.runtime.message.AgentMessage;
import io.agentscope.core.memory.InMemoryMemory;
import io.agentscope.core.memory.Memory;
import io.agentscope.core.memory.autocontext.AutoContextConfig;
import io.agentscope.core.memory.autocontext.AutoContextMemory;
import io.agentscope.core.model.Model;
import java.util.List;
/**
* 将运行时记忆设置适配为 AgentScope 记忆。
*/
public class AgentScopeMemoryAdapter {
private final AgentScopeMessageAdapter messageAdapter = new AgentScopeMessageAdapter();
/**
* 创建记忆并附加恢复后的历史消息。
*
* @param snapshot 记忆快照
* @param policy 记忆策略
* @param model 用于自动压缩的模型
* @return AgentScope 记忆
*/
public Memory createMemory(AgentMemorySnapshot snapshot, AgentMemoryPolicy policy, Model model) {
return createMemoryResult(snapshot, policy, model).getMemory();
}
/**
* 创建记忆并返回 AutoContext 配置快照。
*
* <p>有状态 runtime 需要把同一份 {@link AutoContextConfig} 交给 AutoContext干预器
* 用于判断是否进入压缩流程并发出旁路事件。非 AutoContext 记忆不会返回配置。</p>
*
* @param snapshot 记忆快照
* @param policy 记忆策略
* @param model 用于自动压缩的模型
* @return 记忆构建结果
*/
public AgentScopeMemoryBuildResult createMemoryResult(AgentMemorySnapshot snapshot,
AgentMemoryPolicy policy,
Model model) {
AgentMemoryPolicy safePolicy = policy == null ? AgentMemoryPolicy.autoContext() : policy;
Memory memory;
AutoContextConfig autoContextConfig = null;
if (safePolicy.getType() == com.easyagents.agent.runtime.memory.AgentMemoryType.AUTO_CONTEXT
&& safePolicy.getCompressionParameter().isEnabled()) {
autoContextConfig = toAutoContextConfig(safePolicy.getCompressionParameter());
memory = new AutoContextMemory(autoContextConfig, new AgentScopeAutoContextCompressionModel(model));
} else {
memory = new InMemoryMemory();
}
attachMessages(memory, snapshot, safePolicy.getMaxAttachedMessageCount());
return new AgentScopeMemoryBuildResult(memory, autoContextConfig);
}
/**
* 将自动压缩参数转换为 AgentScope 配置。
*
* @param parameter 自动压缩参数
* @return AgentScope 配置
*/
public AutoContextConfig toAutoContextConfig(AgentMemoryCompressionParameter parameter) {
AgentMemoryCompressionParameter safeParameter = parameter == null ? new AgentMemoryCompressionParameter() : parameter;
Integer compressionThreshold = safeParameter.getMinCompressionTokenThreshold();
return AutoContextConfig.builder()
.msgThreshold(safeParameter.getMsgThreshold() == null ? Integer.MAX_VALUE : safeParameter.getMsgThreshold())
.lastKeep(safeParameter.getLastKeep())
.tokenRatio(1.0D)
.maxToken(compressionThreshold == null ? Long.MAX_VALUE : compressionThreshold)
.largePayloadThreshold(safeParameter.getLargePayloadThreshold())
.minCompressionTokenThreshold(compressionThreshold == null ? Integer.MAX_VALUE : compressionThreshold)
.currentRoundCompressionRatio(safeParameter.getCurrentRoundCompressionRatio())
.minConsecutiveToolMessages(safeParameter.getMinConsecutiveToolMessages())
.build();
}
private void attachMessages(Memory memory, AgentMemorySnapshot snapshot, int maxAttachedMessageCount) {
if (snapshot == null || snapshot.getMessages().isEmpty()) {
return;
}
List<AgentMessage> messages = snapshot.getMessages();
int fromIndex = Math.max(0, messages.size() - maxAttachedMessageCount);
for (AgentMessage message : messages.subList(fromIndex, messages.size())) {
memory.addMessage(messageAdapter.toMsg(message));
}
}
}

View File

@@ -0,0 +1,46 @@
package com.easyagents.agent.runtime.agentscope;
import io.agentscope.core.memory.Memory;
import io.agentscope.core.memory.autocontext.AutoContextConfig;
/**
* AgentScope 记忆构建结果。
*
* <p>AutoContext 的压缩入口判断必须使用创建 {@code AutoContextMemory} 时的同一份
* {@link AutoContextConfig}。该结果对象将记忆实例和配置快照一起返回,避免运行时
* 干预器重新推导配置导致事件触发条件与 AgentScope 主线路不一致。</p>
*/
public class AgentScopeMemoryBuildResult {
private final Memory memory;
private final AutoContextConfig autoContextConfig;
/**
* 创建记忆构建结果。
*
* @param memory AgentScope 记忆实例
* @param autoContextConfig AutoContext 配置,非 AutoContext 记忆时为 null
*/
public AgentScopeMemoryBuildResult(Memory memory, AutoContextConfig autoContextConfig) {
this.memory = memory;
this.autoContextConfig = autoContextConfig;
}
/**
* 获取 AgentScope 记忆实例。
*
* @return AgentScope 记忆实例
*/
public Memory getMemory() {
return memory;
}
/**
* 获取 AutoContext 配置。
*
* @return AutoContext 配置,非 AutoContext 记忆时为 null
*/
public AutoContextConfig getAutoContextConfig() {
return autoContextConfig;
}
}

View File

@@ -0,0 +1,345 @@
package com.easyagents.agent.runtime.agentscope;
import com.easyagents.agent.runtime.message.*;
import io.agentscope.core.message.*;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 运行时消息与 AgentScope 消息的双向适配器。
*/
public class AgentScopeMessageAdapter {
/**
* 将运行时消息转换为 AgentScope 消息。
*
* @param message 运行时消息
* @return AgentScope 消息
*/
public Msg toMsg(AgentMessage message) {
if (message == null) {
return null;
}
return Msg.builder()
.id(message.getMessageId())
.name(message.getName())
.role(toRole(message.getRole()))
.content(toContentBlocks(message.getContentBlocks()))
.metadata(copyMap(message.getMetadata()))
.timestamp(message.getCreatedAt() == null ? null : message.getCreatedAt().toString())
.build();
}
/**
* 将 AgentScope 消息转换为运行时消息。
*
* @param msg AgentScope 消息
* @return 运行时消息
*/
public AgentMessage toAgentMessage(Msg msg) {
if (msg == null) {
return null;
}
AgentMessage message = new AgentMessage();
message.setMessageId(msg.getId());
message.setName(msg.getName());
message.setRole(toRole(msg.getRole()));
message.setMetadata(msg.getMetadata());
if (msg.getTimestamp() != null && !msg.getTimestamp().isBlank()) {
Instant parsed = parseTimestamp(msg.getTimestamp());
if (parsed != null) {
message.setCreatedAt(parsed);
} else {
message.getMetadata().put("rawTimestamp", msg.getTimestamp());
}
}
message.setContentBlocks(toBlocks(msg.getContent()));
return message;
}
/**
* 将 AgentScope 块转换为运行时块。
*
* @param block AgentScope 块
* @return 运行时块
*/
public AgentContentBlock toAgentBlock(ContentBlock block) {
if (block == null) {
return null;
}
if (block instanceof TextBlock textBlock) {
return new AgentTextBlock(textBlock.getText());
}
if (block instanceof ThinkingBlock thinkingBlock) {
AgentThinkingBlock converted = new AgentThinkingBlock(thinkingBlock.getThinking());
converted.getMetadata().putAll(copyMap(thinkingBlock.getMetadata()));
return converted;
}
if (block instanceof ToolUseBlock toolUseBlock) {
AgentToolUseBlock converted = new AgentToolUseBlock(toolUseBlock.getId(), toolUseBlock.getName(), copyMap(toolUseBlock.getInput()));
converted.setContent(toolUseBlock.getContent());
converted.getMetadata().putAll(copyMap(toolUseBlock.getMetadata()));
return converted;
}
if (block instanceof ToolResultBlock toolResultBlock) {
AgentToolResultBlock converted = new AgentToolResultBlock(toolResultBlock.getId(), toolResultBlock.getName());
converted.setOutput(toBlocks(toolResultBlock.getOutput()));
converted.setSuspended(toolResultBlock.isSuspended());
converted.getMetadata().putAll(copyMap(toolResultBlock.getMetadata()));
return converted;
}
if (block instanceof ImageBlock imageBlock) {
return toMediaBlock(imageBlock, "image");
}
if (block instanceof AudioBlock audioBlock) {
return toMediaBlock(audioBlock, "audio");
}
if (block instanceof VideoBlock videoBlock) {
return toMediaBlock(videoBlock, "video");
}
AgentUnknownBlock converted = new AgentUnknownBlock();
converted.setSourceClassName(block.getClass().getName());
converted.setSourceTypeName(resolveTypeName(block));
converted.setRawText(block.toString());
converted.getRawMetadata().put("className", block.getClass().getName());
return converted;
}
/**
* 将运行时块转换为 AgentScope 块。
*
* @param block 运行时块
* @return AgentScope 块
*/
public ContentBlock toContentBlock(AgentContentBlock block) {
if (block == null) {
return null;
}
if (block instanceof AgentTextBlock textBlock) {
return TextBlock.builder().text(textBlock.getText()).build();
}
if (block instanceof AgentThinkingBlock thinkingBlock) {
return ThinkingBlock.builder()
.thinking(thinkingBlock.getThinking())
.metadata(copyMap(thinkingBlock.getMetadata()))
.build();
}
if (block instanceof AgentToolUseBlock toolUseBlock) {
return ToolUseBlock.builder()
.id(toolUseBlock.getId())
.name(toolUseBlock.getName())
.input(copyMap(toolUseBlock.getInput()))
.content(toolUseBlock.getContent())
.metadata(copyMap(toolUseBlock.getMetadata()))
.build();
}
if (block instanceof AgentToolResultBlock toolResultBlock) {
Map<String, Object> metadata = copyMap(toolResultBlock.getMetadata());
metadata.put("suspended", toolResultBlock.isSuspended());
return ToolResultBlock.of(
toolResultBlock.getId(),
toolResultBlock.getName(),
toContentBlocks(toolResultBlock.getOutput()),
metadata);
}
if (block instanceof AgentMediaBlock mediaBlock) {
return toMediaBlock(mediaBlock);
}
if (block instanceof AgentUnknownBlock unknownBlock) {
return toUnknownContentBlock(unknownBlock);
}
return null;
}
/**
* 将 AgentScope 块列表转换为运行时块列表。
*
* @param blocks AgentScope 块列表
* @return 运行时块列表
*/
public List<AgentContentBlock> toBlocks(List<ContentBlock> blocks) {
List<AgentContentBlock> converted = new ArrayList<>();
if (blocks == null) {
return converted;
}
for (ContentBlock block : blocks) {
AgentContentBlock agentBlock = toAgentBlock(block);
if (agentBlock != null) {
converted.add(agentBlock);
}
}
return converted;
}
/**
* 将运行时块列表转换为 AgentScope 块列表。
*
* @param blocks 运行时块列表
* @return AgentScope 块列表
*/
public List<ContentBlock> toContentBlocks(List<AgentContentBlock> blocks) {
List<ContentBlock> converted = new ArrayList<>();
if (blocks == null) {
return converted;
}
for (AgentContentBlock block : blocks) {
ContentBlock contentBlock = toContentBlock(block);
if (contentBlock != null) {
converted.add(contentBlock);
}
}
return converted;
}
private MsgRole toRole(AgentMessageRole role) {
if (role == AgentMessageRole.ASSISTANT) {
return MsgRole.ASSISTANT;
}
if (role == AgentMessageRole.SYSTEM) {
return MsgRole.SYSTEM;
}
if (role == AgentMessageRole.TOOL) {
return MsgRole.TOOL;
}
return MsgRole.USER;
}
private AgentMessageRole toRole(MsgRole role) {
if (role == MsgRole.ASSISTANT) {
return AgentMessageRole.ASSISTANT;
}
if (role == MsgRole.SYSTEM) {
return AgentMessageRole.SYSTEM;
}
if (role == MsgRole.TOOL) {
return AgentMessageRole.TOOL;
}
return AgentMessageRole.USER;
}
private Map<String, Object> copyMap(Map<String, Object> metadata) {
return metadata == null ? new LinkedHashMap<>() : new LinkedHashMap<>(metadata);
}
private ContentBlock toMediaBlock(AgentMediaBlock block) {
if (block == null) {
return null;
}
if ("audio".equalsIgnoreCase(block.getMediaKind())) {
return AudioBlock.builder().source(mediaSource(block)).build();
}
if ("video".equalsIgnoreCase(block.getMediaKind())) {
return VideoBlock.builder()
.source(mediaSource(block))
.fps(block.getFps())
.maxFrames(block.getMaxFrames())
.minPixels(block.getMinPixels())
.maxPixels(block.getMaxPixels())
.totalPixels(block.getTotalPixels())
.build();
}
return ImageBlock.builder()
.source(mediaSource(block))
.minPixels(block.getMinPixels())
.maxPixels(block.getMaxPixels())
.build();
}
private AgentContentBlock toMediaBlock(ContentBlock block, String mediaKind) {
AgentMediaBlock converted = new AgentMediaBlock(mediaKind);
if (block instanceof ImageBlock imageBlock) {
readSource(imageBlock.getSource(), converted);
converted.setMinPixels(imageBlock.getMinPixels());
converted.setMaxPixels(imageBlock.getMaxPixels());
} else if (block instanceof AudioBlock audioBlock) {
readSource(audioBlock.getSource(), converted);
} else if (block instanceof VideoBlock videoBlock) {
readSource(videoBlock.getSource(), converted);
converted.setFps(videoBlock.getFps());
converted.setMaxFrames(videoBlock.getMaxFrames());
converted.setMinPixels(videoBlock.getMinPixels());
converted.setMaxPixels(videoBlock.getMaxPixels());
converted.setTotalPixels(videoBlock.getTotalPixels());
}
return converted;
}
private io.agentscope.core.message.Source mediaSource(AgentMediaBlock block) {
if (block.getData() != null && !block.getData().isBlank()) {
return Base64Source.builder()
.mediaType(block.getMimeType())
.data(block.getData())
.build();
}
return URLSource.builder().url(block.getUrl()).build();
}
private void readSource(io.agentscope.core.message.Source source, AgentMediaBlock block) {
if (source instanceof Base64Source base64Source) {
block.setMimeType(base64Source.getMediaType());
block.setData(base64Source.getData());
return;
}
if (source instanceof URLSource urlSource) {
block.setUrl(urlSource.getUrl());
}
}
private ContentBlock toUnknownContentBlock(AgentUnknownBlock block) {
String text = block.getRawText();
if (text == null || text.isBlank()) {
text = "[unsupported content block: " + block.getSourceTypeName() + "]";
}
return TextBlock.builder()
.text(text)
.build();
}
private Instant parseTimestamp(String timestamp) {
try {
return Instant.parse(timestamp);
} catch (Exception ignored) {
// 继续尝试其他格式。
}
try {
return OffsetDateTime.parse(timestamp).toInstant();
} catch (Exception ignored) {
// 继续尝试其他格式。
}
try {
return Instant.ofEpochMilli(Long.parseLong(timestamp));
} catch (Exception ignored) {
return null;
}
}
private String resolveTypeName(ContentBlock block) {
if (block instanceof TextBlock) {
return "text";
}
if (block instanceof ThinkingBlock) {
return "thinking";
}
if (block instanceof ToolUseBlock) {
return "tool_use";
}
if (block instanceof ToolResultBlock) {
return "tool_result";
}
if (block instanceof ImageBlock) {
return "image";
}
if (block instanceof AudioBlock) {
return "audio";
}
if (block instanceof VideoBlock) {
return "video";
}
return block.getClass().getSimpleName();
}
}

View File

@@ -0,0 +1,257 @@
package com.easyagents.agent.runtime.agentscope;
import com.easyagents.agent.runtime.AgentRuntimeException;
import com.easyagents.agent.runtime.model.AgentGenerationOptions;
import com.easyagents.agent.runtime.model.AgentModelFactory;
import com.easyagents.agent.runtime.model.AgentModelProviderType;
import com.easyagents.agent.runtime.model.AgentModelSpec;
import io.agentscope.core.formatter.openai.DeepSeekFormatter;
import io.agentscope.core.formatter.openai.GLMFormatter;
import io.agentscope.core.model.*;
import io.agentscope.core.model.ollama.OllamaOptions;
import java.util.Map;
/**
* 根据运行时模型声明创建 AgentScope 模型实例。
*/
public class AgentScopeModelFactory implements AgentModelFactory<Model> {
private static final String DEEPSEEK_BASE_URL = "https://api.deepseek.com";
private static final String GLM_BASE_URL = "https://open.bigmodel.cn/api/paas/v4";
private static final String MINIMAX_BASE_URL = "https://api.minimax.io/v1";
private static final String MOONSHOT_BASE_URL = "https://api.moonshot.cn/v1";
private static final String ARK_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3";
private static final String SILICONFLOW_BASE_URL = "https://api.siliconflow.cn/v1";
@Override
public Model create(AgentModelSpec modelSpec, AgentGenerationOptions generationOptions) {
if (modelSpec == null) {
throw new AgentRuntimeException("Agent model spec is required.");
}
GenerateOptions options = toGenerateOptions(modelSpec, generationOptions);
AgentModelProviderType providerType = modelSpec.getProviderType();
if (providerType == AgentModelProviderType.OLLAMA) {
return buildOllama(modelSpec, options);
}
if (providerType == AgentModelProviderType.OPENAI) {
return buildOpenAi(modelSpec, options);
}
if (providerType == AgentModelProviderType.ANTHROPIC) {
return buildAnthropic(modelSpec, options);
}
if (providerType == AgentModelProviderType.GEMINI) {
return buildGemini(modelSpec, options);
}
if (providerType == AgentModelProviderType.DEEPSEEK) {
return buildDeepSeek(modelSpec, options);
}
if (providerType == AgentModelProviderType.GLM) {
return buildGlm(modelSpec, options);
}
if (providerType == AgentModelProviderType.MINIMAX) {
return buildOpenAiCompatible(modelSpec, options, MINIMAX_BASE_URL);
}
if (providerType == AgentModelProviderType.MOONSHOT) {
return buildOpenAiCompatible(modelSpec, options, MOONSHOT_BASE_URL);
}
if (providerType == AgentModelProviderType.ARK) {
return buildOpenAiCompatible(modelSpec, options, ARK_BASE_URL);
}
if (providerType == AgentModelProviderType.SILICONFLOW) {
return buildOpenAiCompatible(modelSpec, options, SILICONFLOW_BASE_URL);
}
if (providerType == AgentModelProviderType.DASHSCOPE) {
return buildDashScope(modelSpec, generationOptions, options);
}
if (providerType == AgentModelProviderType.OPENAI_COMPATIBLE || providerType == AgentModelProviderType.CUSTOM) {
return buildOpenAiCompatible(modelSpec, options, null);
}
throw new AgentRuntimeException("Unsupported agent model provider: " + providerType);
}
/**
* 将中立参数转换为 AgentScope 参数。
*
* @param modelSpec 模型声明
* @param options 中立参数
* @return AgentScope 参数
*/
public GenerateOptions toGenerateOptions(AgentModelSpec modelSpec, AgentGenerationOptions options) {
AgentGenerationOptions safeOptions = options == null ? new AgentGenerationOptions() : options;
GenerateOptions.Builder builder = GenerateOptions.builder()
.apiKey(modelSpec.getApiKey())
.baseUrl(modelSpec.getBaseUrl())
.endpointPath(modelSpec.getEndpointPath())
.modelName(modelSpec.getModelName())
.stream(safeOptions.getStream())
.temperature(safeOptions.getTemperature())
.topP(safeOptions.getTopP())
.topK(safeOptions.getTopK())
.maxTokens(safeOptions.getMaxTokens())
.maxCompletionTokens(safeOptions.getMaxCompletionTokens())
.thinkingBudget(safeOptions.getThinkingBudget())
.reasoningEffort(safeOptions.getReasoningEffort());
for (Map.Entry<String, Object> entry : safeOptions.getAdditionalBodyParams().entrySet()) {
builder.additionalBodyParam(entry.getKey(), entry.getValue());
}
for (Map.Entry<String, String> entry : safeOptions.getAdditionalHeaders().entrySet()) {
builder.additionalHeader(entry.getKey(), entry.getValue());
}
for (Map.Entry<String, String> entry : safeOptions.getAdditionalQueryParams().entrySet()) {
builder.additionalQueryParam(entry.getKey(), entry.getValue());
}
return builder.build();
}
/**
* 构建 OpenAI 原生模型。
*
* @param modelSpec 模型声明
* @param options 生成参数
* @return 模型
*/
private Model buildOpenAi(AgentModelSpec modelSpec, GenerateOptions options) {
return buildOpenAiCompatible(modelSpec, options, null);
}
/**
* 通过 OpenAI-compatible 协议构建模型。
*
* @param modelSpec 模型声明
* @param options 生成参数
* @param defaultBaseUrl 默认基础 URL
* @return 模型
*/
private Model buildOpenAiCompatible(AgentModelSpec modelSpec, GenerateOptions options, String defaultBaseUrl) {
OpenAIChatModel.Builder builder = OpenAIChatModel.builder()
.apiKey(modelSpec.getApiKey())
.modelName(modelSpec.getModelName())
.baseUrl(resolveBaseUrl(modelSpec, defaultBaseUrl))
.endpointPath(modelSpec.getEndpointPath())
.stream(Boolean.TRUE.equals(options.getStream()))
.generateOptions(options);
return builder.build();
}
/**
* 构建 Anthropic 模型。
*
* @param modelSpec 模型声明
* @param options 生成参数
* @return 模型
*/
private Model buildAnthropic(AgentModelSpec modelSpec, GenerateOptions options) {
return AnthropicChatModel.builder()
.apiKey(modelSpec.getApiKey())
.modelName(modelSpec.getModelName())
.baseUrl(modelSpec.getBaseUrl())
.stream(Boolean.TRUE.equals(options.getStream()))
.defaultOptions(options)
.build();
}
/**
* 构建 Gemini 模型。
*
* @param modelSpec 模型声明
* @param options 生成参数
* @return 模型
*/
private Model buildGemini(AgentModelSpec modelSpec, GenerateOptions options) {
return GeminiChatModel.builder()
.apiKey(modelSpec.getApiKey())
.modelName(modelSpec.getModelName())
.baseUrl(modelSpec.getBaseUrl())
.streamEnabled(Boolean.TRUE.equals(options.getStream()))
.defaultOptions(options)
.build();
}
/**
* 通过 OpenAI-compatible 协议构建 DeepSeek 模型。
*
* @param modelSpec 模型声明
* @param options 生成参数
* @return 模型
*/
private Model buildDeepSeek(AgentModelSpec modelSpec, GenerateOptions options) {
OpenAIChatModel.Builder builder = OpenAIChatModel.builder()
.apiKey(modelSpec.getApiKey())
.modelName(modelSpec.getModelName())
.baseUrl(resolveBaseUrl(modelSpec, DEEPSEEK_BASE_URL))
.endpointPath(modelSpec.getEndpointPath())
.stream(Boolean.TRUE.equals(options.getStream()))
.formatter(new DeepSeekFormatter())
.generateOptions(options);
return builder.build();
}
/**
* 通过 OpenAI-compatible 协议构建智谱 GLM 模型。
*
* @param modelSpec 模型声明
* @param options 生成参数
* @return 模型
*/
private Model buildGlm(AgentModelSpec modelSpec, GenerateOptions options) {
OpenAIChatModel.Builder builder = OpenAIChatModel.builder()
.apiKey(modelSpec.getApiKey())
.modelName(modelSpec.getModelName())
.baseUrl(resolveBaseUrl(modelSpec, GLM_BASE_URL))
.endpointPath(modelSpec.getEndpointPath())
.stream(Boolean.TRUE.equals(options.getStream()))
.formatter(new GLMFormatter())
.generateOptions(options);
return builder.build();
}
/**
* 构建 Ollama 模型。
*
* @param modelSpec 模型声明
* @param options 生成参数
* @return 模型
*/
private Model buildOllama(AgentModelSpec modelSpec, GenerateOptions options) {
return OllamaChatModel.builder()
.modelName(modelSpec.getModelName())
.baseUrl(modelSpec.getBaseUrl())
.defaultOptions(OllamaOptions.fromGenerateOptions(options))
.build();
}
/**
* 构建 DashScope 模型。
*
* @param modelSpec 模型声明
* @param generationOptions 中立生成参数
* @param options AgentScope 生成参数
* @return 模型
*/
private Model buildDashScope(AgentModelSpec modelSpec, AgentGenerationOptions generationOptions, GenerateOptions options) {
Boolean thinkingEnabled = generationOptions == null ? null : generationOptions.getThinkingEnabled();
return DashScopeChatModel.builder()
.apiKey(modelSpec.getApiKey())
.modelName(modelSpec.getModelName())
.baseUrl(modelSpec.getBaseUrl())
.stream(Boolean.TRUE.equals(options.getStream()))
.enableThinking(thinkingEnabled)
.defaultOptions(options)
.build();
}
/**
* 优先使用调用方传入的基础 URL未传入时使用供应商默认地址。
*
* @param modelSpec 模型声明
* @param defaultBaseUrl 默认基础 URL
* @return 最终基础 URL
*/
private String resolveBaseUrl(AgentModelSpec modelSpec, String defaultBaseUrl) {
if (modelSpec.getBaseUrl() == null || modelSpec.getBaseUrl().isBlank()) {
return defaultBaseUrl;
}
return modelSpec.getBaseUrl();
}
}

View File

@@ -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 <T> Hook 事件类型
* @return 处理后的 Hook 事件
*/
@Override
public <T extends HookEvent> Mono<T> onEvent(T event) {
return observationManager.handle(event);
}
}

View File

@@ -0,0 +1,155 @@
package com.easyagents.agent.runtime.agentscope;
import com.easyagents.agent.runtime.persistence.AgentPersistencePolicy;
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.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 将 AgentScope Session 桥接到运行时 AgentSessionStore。
*/
public class AgentScopeSessionAdapter implements Session {
private final AgentSessionStore sessionStore;
/**
* 创建适配器。
*
* @param sessionStore 运行时会话存储
*/
public AgentScopeSessionAdapter(AgentSessionStore sessionStore) {
this.sessionStore = sessionStore;
}
@Override
public void save(SessionKey sessionKey, String name, State state) {
sessionStore.save(toKey(sessionKey), name, state);
}
/**
* 将状态列表保存到运行时存储。
*
* @param sessionKey 会话键
* @param name 状态名称
* @param states 状态列表
*/
@Override
public void save(SessionKey sessionKey, String name, List<? extends State> states) {
sessionStore.saveList(toKey(sessionKey), name, states);
}
/**
* 从运行时存储获取单个指定类型的状态。
*
* @param sessionKey 会话键
* @param name 状态名称
* @param clazz 状态类型
* @param <T> 状态类型
* @return 可选状态
*/
@Override
public <T extends State> Optional<T> get(SessionKey sessionKey, String name, Class<T> clazz) {
return sessionStore.get(toKey(sessionKey), name, clazz);
}
/**
* 从运行时存储获取指定类型的状态列表。
*
* @param sessionKey 会话键
* @param name 状态名称
* @param clazz 状态类型
* @param <T> 状态类型
* @return 状态列表
*/
@Override
public <T extends State> List<T> getList(SessionKey sessionKey, String name, Class<T> clazz) {
return sessionStore.getList(toKey(sessionKey), name, clazz);
}
/**
* 返回会话键是否存在。
*
* @param sessionKey 会话键
* @return 存在时为 true
*/
@Override
public boolean exists(SessionKey sessionKey) {
return sessionStore.exists(toKey(sessionKey));
}
/**
* 删除会话键。
*
* @param sessionKey 会话键
*/
@Override
public void delete(SessionKey sessionKey) {
sessionStore.delete(toKey(sessionKey));
}
/**
* 列出已存储的会话键。
*
* @return 会话键列表
*/
@Override
public Set<SessionKey> listSessionKeys() {
return sessionStore.listSessionKeys().stream()
.map(RuntimeSessionKey::new)
.collect(Collectors.toSet());
}
/**
* 将持久化策略转换为 AgentScope 持久化标记。
*
* @param policy 运行时策略
* @return AgentScope 状态持久化配置
*/
public static StatePersistence toStatePersistence(AgentPersistencePolicy policy) {
if (policy == null || !policy.isEnabled()) {
return StatePersistence.memoryOnly();
}
return StatePersistence.builder()
.memoryManaged(policy.isMemoryManaged())
.toolkitManaged(policy.isToolkitManaged())
.planNotebookManaged(policy.isPlanNotebookManaged())
.statefulToolsManaged(policy.isStatefulToolsManaged())
.build();
}
/**
* 创建会话键。
*
* @param value 键值
* @return 会话键
*/
public static SessionKey sessionKey(String value) {
return new RuntimeSessionKey(value);
}
private String toKey(SessionKey sessionKey) {
return sessionKey == null ? "" : sessionKey.toIdentifier();
}
/**
* 运行时支撑的 AgentScope 会话键。
*/
private record RuntimeSessionKey(String value) implements SessionKey {
/**
* 将键转换为稳定标识符。
*
* @return 标识符
*/
@Override
public String toIdentifier() {
return value;
}
}
}

View File

@@ -0,0 +1,171 @@
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;
import io.agentscope.core.skill.AgentSkill;
import io.agentscope.core.skill.SkillBox;
import io.agentscope.core.tool.AgentTool;
import io.agentscope.core.tool.Toolkit;
import java.util.List;
import java.util.Map;
/**
* 将运行时 Skill 适配为 AgentScope SkillBox。
*/
public class AgentScopeSkillAdapter implements AgentSkillCompiler<AgentSkill> {
@Override
public AgentSkill compile(AgentSkillSpec skillSpec) {
validateSkillSpec(skillSpec);
return new EasyAgentsAgentSkill(
skillSpec.getSkillId(),
skillSpec.getName(),
skillSpec.getDescription(),
skillSpec.getSkillContent(),
skillSpec.getResources(),
skillSpec.getSource());
}
/**
* 创建并绑定 AgentScope SkillBox。
*
* @param spec SkillBox 声明
* @param toolkit Toolkit 实例
* @return SkillBox未配置 Skill 时返回 null
*/
public SkillBox createSkillBox(AgentSkillBoxSpec spec, Toolkit toolkit) {
if (spec == null || spec.getSkills().isEmpty()) {
return null;
}
SkillBox skillBox = spec.getSkillBoxId() == null || spec.getSkillBoxId().isEmpty()
? new SkillBox(toolkit)
: new SkillBox(toolkit, spec.getSkillBoxId());
skillBox.setExposeAllSkillMetadata(spec.isExposeAllSkillMetadata());
for (AgentSkillSpec skillSpec : spec.getSkills()) {
AgentSkill skill = compile(skillSpec);
SkillBox.SkillRegistration registration = skillBox.registration()
.skill(skill)
.toolkit(toolkit)
.enableTools(spec.getEnabledToolNames())
.disableTools(spec.getDisabledToolNames())
.presetParameters(spec.getPresetParameters());
registration.apply();
}
skillBox.syncToolGroupStates();
return skillBox;
}
/**
* 创建并绑定带 Skill 工具的 AgentScope SkillBox。
*
* @param spec SkillBox 声明
* @param toolkit Toolkit 实例
* @param skillTools 按 Skill ID 分组的工具
* @return SkillBox未配置 Skill 时返回 null
*/
public SkillBox createSkillBox(AgentSkillBoxSpec spec, Toolkit toolkit, Map<String, List<AgentTool>> skillTools) {
if (spec == null || spec.getSkills().isEmpty()) {
return null;
}
SkillBox skillBox = spec.getSkillBoxId() == null || spec.getSkillBoxId().isEmpty()
? new SkillBox(toolkit)
: new SkillBox(toolkit, spec.getSkillBoxId());
skillBox.setExposeAllSkillMetadata(spec.isExposeAllSkillMetadata());
for (AgentSkillSpec skillSpec : spec.getSkills()) {
AgentSkill skill = compile(skillSpec);
List<AgentTool> tools = skillTools == null ? List.of() : skillTools.getOrDefault(skillSpec.getSkillId(), List.of());
if (tools.isEmpty()) {
skillBox.registration()
.skill(skill)
.toolkit(toolkit)
.enableTools(spec.getEnabledToolNames())
.disableTools(spec.getDisabledToolNames())
.presetParameters(spec.getPresetParameters())
.apply();
continue;
}
for (AgentTool tool : tools) {
skillBox.registration()
.skill(skill)
.toolkit(toolkit)
.enableTools(spec.getEnabledToolNames())
.disableTools(spec.getDisabledToolNames())
.presetParameters(spec.getPresetParameters())
.agentTool(tool)
.apply();
}
}
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 完全一致。
*
* <p>AgentScope 默认用 {@code name + "_" + source} 生成 Skill ID而 Easy-Agents
* 的工具绑定、旁路事件和调用层配置都以 {@link AgentSkillSpec#getSkillId()} 为准。
* 如果不覆盖这里,模型在 prompt 中看到的 skill-id 会和 Easy 侧绑定 key 不一致,
* 后续 Skill 状态监听也无法做到精准归属。</p>
*/
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<String, String> 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;
}
}
}

View File

@@ -0,0 +1,604 @@
package com.easyagents.agent.runtime.agentscope;
import com.easyagents.agent.runtime.AgentRuntimeException;
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;
import com.easyagents.agent.runtime.skill.AgentSkillBinding;
import com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext;
import com.easyagents.agent.runtime.tool.AgentToolContext;
import com.easyagents.agent.runtime.tool.AgentToolInvoker;
import com.easyagents.agent.runtime.tool.AgentToolResult;
import com.easyagents.agent.runtime.tool.AgentToolSpec;
import io.agentscope.core.message.TextBlock;
import io.agentscope.core.message.ToolResultBlock;
import io.agentscope.core.message.ToolUseBlock;
import io.agentscope.core.tool.AgentTool;
import io.agentscope.core.tool.ToolCallParam;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
/**
* 将运行时工具适配为 AgentScope 工具。
*/
public class AgentScopeToolAdapter {
/**
* 将运行时工具声明和调用器转换为 AgentScope AgentTool。
*
* @param toolSpec 工具声明
* @param invoker 工具调用器
* @param request 运行请求
* @return AgentScope 工具
*/
public AgentTool adapt(AgentToolSpec toolSpec, AgentToolInvoker invoker, AgentRuntimeExecutionContext request) {
return adapt(toolSpec, invoker, request, AgentToolApprovalCoordinator.disabled(),
(Sinks.Many<AgentRuntimeEvent>) null);
}
/**
* 将运行时工具声明和调用器转换为带事件 sink 的 AgentScope AgentTool。
*
* @param toolSpec 工具声明
* @param invoker 工具调用器
* @param request 运行请求
* @param eventSink 事件 sink
* @return AgentScope 工具
*/
public AgentTool adapt(AgentToolSpec toolSpec,
AgentToolInvoker invoker,
AgentRuntimeExecutionContext request,
Sinks.Many<AgentRuntimeEvent> eventSink) {
return adapt(toolSpec, invoker, request, AgentToolApprovalCoordinator.disabled(), fixedHolder(request, eventSink),
null, null);
}
/**
* 将运行时工具声明和调用器转换为带审批协调器和事件 sink 的 AgentScope AgentTool。
*
* @param toolSpec 工具声明
* @param invoker 工具调用器
* @param request 运行请求
* @param approvalCoordinator 审批协调器
* @param eventSink 事件 sink
* @return AgentScope 工具
*/
public AgentTool adapt(AgentToolSpec toolSpec,
AgentToolInvoker invoker,
AgentRuntimeExecutionContext request,
AgentToolApprovalCoordinator approvalCoordinator,
Sinks.Many<AgentRuntimeEvent> eventSink) {
return adapt(toolSpec, invoker, request, approvalCoordinator, fixedHolder(request, eventSink), null, null);
}
/**
* 将运行时工具声明和调用器转换为带 Skill 归属的 AgentScope AgentTool。
*
* @param toolSpec 工具声明
* @param invoker 工具调用器
* @param request 运行请求
* @param approvalCoordinator 审批协调器
* @param eventSink 事件 sink
* @param skillBinding Skill 绑定关系
* @return AgentScope 工具
*/
public AgentTool adapt(AgentToolSpec toolSpec,
AgentToolInvoker invoker,
AgentRuntimeExecutionContext request,
AgentToolApprovalCoordinator approvalCoordinator,
Sinks.Many<AgentRuntimeEvent> eventSink,
AgentSkillBinding skillBinding) {
return adapt(toolSpec, invoker, request, approvalCoordinator, fixedHolder(request, eventSink), null, skillBinding);
}
/**
* 将运行时工具声明和调用器转换为带 Skill 上下文的 AgentScope AgentTool。
*
* @param toolSpec 工具声明
* @param invoker 工具调用器
* @param request 运行请求
* @param approvalCoordinator 审批协调器
* @param eventSink 事件 sink
* @param skillContext Skill 运行时上下文
* @param skillBinding Skill 静态绑定关系
* @return AgentScope 工具
*/
public AgentTool adapt(AgentToolSpec toolSpec,
AgentToolInvoker invoker,
AgentRuntimeExecutionContext request,
AgentToolApprovalCoordinator approvalCoordinator,
Sinks.Many<AgentRuntimeEvent> 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, 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<AgentRuntimeEvent> 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,
AgentRuntimeExecutionContext request,
AgentToolApprovalCoordinator approvalCoordinator,
AgentRuntimeTurnContextHolder turnContextHolder,
AgentSkillRuntimeContext skillContext,
AgentSkillBinding skillBinding,
boolean emitNormalToolResult,
boolean emitSkillStep,
boolean handleApprovalInTool) implements AgentTool {
/**
* 获取工具名称。
*
* @return 工具名称
*/
@Override
public String getName() {
return toolSpec.getName();
}
/**
* 获取工具描述。
*
* @return 工具描述
*/
@Override
public String getDescription() {
return toolSpec.getDescription();
}
/**
* 获取工具参数 Schema。
*
* @return 参数 Schema
*/
@Override
public Map<String, Object> getParameters() {
return toolSpec.getParametersSchema();
}
/**
* 获取输出 Schema。
*
* @return 输出 Schema
*/
@Override
public Map<String, Object> getOutputSchema() {
return toolSpec.getOutputSchema();
}
/**
* 调用运行时工具。
*
* @param param 工具调用参数
* @return 工具结果块
*/
@Override
public Mono<ToolResultBlock> callAsync(ToolCallParam param) {
AgentRuntimeEvent startEvent = toolExecutionStartEvent(param == null ? null : param.getToolUseBlock());
if (startEvent != null) {
emit(startEvent);
}
Map<String, Object> input = param == null || param.getInput() == null
? new LinkedHashMap<>()
: new LinkedHashMap<>(param.getInput());
if (handleApprovalInTool && toolSpec.isApprovalRequired()) {
if (approvalCoordinator == null) {
throw new AgentRuntimeException("Agent tool approval coordinator is required: " + toolSpec.getName());
}
Instant expiresAt = Instant.now().plus(
toolSpec.getApprovalRequest() != null && toolSpec.getApprovalRequest().getTimeout() != null
? toolSpec.getApprovalRequest().getTimeout()
: java.time.Duration.ofMinutes(30));
AgentPendingState pendingState = approvalCoordinator.register(
request.getSessionId(),
request.getAgentDefinition().getAgentId(),
param != null && param.getToolUseBlock() != null ? param.getToolUseBlock().getId() : null,
toolSpec.getName(),
approvalPrompt(toolSpec.getApprovalRequest() == null ? null : toolSpec.getApprovalRequest().getApprovalPrompt()),
input,
toolSpec.getApprovalRequest() == null ? new LinkedHashMap<>() : toolSpec.getApprovalRequest().getMetadata(),
expiresAt);
AgentRuntimeEvent approvalEvent = toolApprovalRequiredEvent(param == null ? null : param.getToolUseBlock(), pendingState);
pendingState.setEventId(approvalEvent.getEventId());
emit(approvalEvent);
return approvalCoordinator.await(pendingState.getResumeToken())
.flatMap(response -> {
if (!response.isApproved()) {
return Mono.error(new AgentToolApprovalRejectedException(
response.getRejectReason()));
}
return Mono.fromCallable(() -> invokeTool(param, input));
});
}
return Mono.fromCallable(() -> invokeTool(param, input));
}
/**
* 构建中立工具上下文。
*
* @param param 工具调用参数
* @return 工具上下文
*/
private AgentToolContext buildContext(ToolCallParam param) {
AgentRuntimeExecutionContext currentRequest = currentRequest();
AgentToolContext context = new AgentToolContext();
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());
}
context.setEventEmitter(this::emit);
context.getMetadata().put("toolName", toolSpec.getName());
context.getMetadata().put("category", toolSpec.getCategory());
appendSkillPayload(context.getMetadata(), activeSkillBinding());
context.getMetadata().putAll(toolSpec.getMetadata());
return context;
}
/**
* 将 AgentToolResult 转换为 AgentScope 结果块。
*
* @param param 工具调用参数
* @param result 运行时结果
* @return AgentScope 结果块
*/
private ToolResultBlock toToolResultBlock(ToolCallParam param, AgentToolResult result) {
AgentToolResult safeResult = result == null ? AgentToolResult.failure("Tool returned null result.") : result;
Map<String, Object> metadata = new LinkedHashMap<>(safeResult.getMetadata());
metadata.put("success", safeResult.isSuccess());
metadata.put("visibility", toolSpec.getVisibility().name());
metadata.put("displayContent", safeResult.getDisplayContent());
appendSkillPayload(metadata, activeSkillBinding());
String output = safeResult.getModelContent();
if (output == null || output.isEmpty()) {
output = safeResult.isSuccess() ? "" : safeResult.getErrorMessage();
}
String id = param != null && param.getToolUseBlock() != null ? param.getToolUseBlock().getId() : null;
return ToolResultBlock.of(id, toolSpec.getName(), TextBlock.builder().text(output).build(), metadata);
}
/**
* 执行真实工具。
*
* @param param 工具调用参数
* @param input 工具输入
* @return 工具结果块
*/
private ToolResultBlock invokeTool(ToolCallParam param, Map<String, Object> input) {
AgentToolContext context = buildContext(param);
AgentToolResult result = invoker.invoke(input, context);
ToolResultBlock block = toToolResultBlock(param, result);
// 有状态 runtime 中,普通工具结果由 AgentScope 原生 PostActingEvent
// 旁路观察器统一发出;旧 sink 辅助路径没有统一 hook因此仍允许 adapter 兼容发射。
if (emitNormalToolResult || (emitSkillStep && activeSkillBinding() != null)) {
emit(toolResultEvent(block));
}
return block;
}
/**
* 通过旁路事件桥发射事件。
*
* <p>这里发射的是 Easy-Agents 对调用方的监察/交互事件,不是 AgentScope
* 主线路消息。主线路的 tool_call/tool_result 顺序仍由 AgentScope 自己维护。</p>
*
* @param event 事件
*/
private void emit(AgentRuntimeEvent event) {
currentEventBridge().emit(event);
}
/**
* 构建工具执行开始事件。
* 普通工具调用由 AgentScope stream 的 ToolUseBlock 表达,这里仅为已激活 Skill 发射步骤事件。
*
* @param block 工具使用块
* @return Skill 步骤事件,普通工具返回 null
*/
private AgentRuntimeEvent toolExecutionStartEvent(ToolUseBlock block) {
AgentSkillBinding activeBinding = activeSkillBinding();
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());
event.getPayload().put("toolName", block.getName());
event.getPayload().put("input", block.getInput());
event.getPayload().put("content", block.getContent());
event.getMetadata().putAll(block.getMetadata() == null ? new LinkedHashMap<>() : block.getMetadata());
} else {
event.getPayload().put("name", toolSpec.getName());
event.getPayload().put("toolName", toolSpec.getName());
}
if (activeBinding != null) {
appendSkillPayload(event.getPayload(), activeBinding);
event.getPayload().put("stepType", "TOOL_CALL");
event.getPayload().put("stepName", toolSpec.getName());
event.getPayload().put("status", "RUNNING");
}
return event;
}
/**
* 构建工具结果事件。
*
* @param block 工具结果块
* @return 运行时事件
*/
private AgentRuntimeEvent toolResultEvent(ToolResultBlock block) {
AgentSkillBinding activeBinding = activeSkillBinding();
AgentRuntimeEvent event = baseEvent(activeBinding == null
? AgentRuntimeEventType.TOOL_RESULT
: AgentRuntimeEventType.SKILL_STEP);
if (block != null) {
event.setToolCallId(block.getId());
event.getPayload().put("name", block.getName());
event.getPayload().put("toolName", block.getName());
event.getPayload().put("text", block.toString());
event.getPayload().put("suspended", block.isSuspended());
event.getMetadata().putAll(block.getMetadata() == null ? new LinkedHashMap<>() : block.getMetadata());
}
if (activeBinding != null) {
appendSkillPayload(event.getPayload(), activeBinding);
event.getPayload().put("stepType", "TOOL_RESULT");
event.getPayload().put("stepName", toolSpec.getName());
event.getPayload().put("status", success(event) ? "SUCCESS" : "FAILED");
event.getPayload().put("success", success(event));
}
return event;
}
/**
* 根据挂起的工具结果构建工具审批事件。
*
* @param toolUseBlock 工具使用块
* @param resultBlock 工具结果块
* @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());
}
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("resumeToken", pendingState == null || pendingState.getResumeToken() == null
? UUID.randomUUID().toString()
: pendingState.getResumeToken().getValue());
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());
payload.put("toolName", pendingState == null ? toolSpec.getName() : pendingState.getToolName());
payload.put("expiresAt", pendingState == null || pendingState.getExpiresAt() == null ? null : pendingState.getExpiresAt().toString());
appendSkillPayload(payload, activeSkillBinding());
if (toolUseBlock != null) {
payload.put("toolCallId", toolUseBlock.getId());
payload.put("toolName", toolUseBlock.getName());
payload.put("input", toolUseBlock.getInput());
}
event.setPayload(payload);
return event;
}
/**
* 解析审批文案。
*
* @param approvalPrompt 审批文案
* @return 审批文案
*/
private String approvalPrompt(String approvalPrompt) {
if (approvalPrompt != null && !approvalPrompt.isBlank()) {
return approvalPrompt;
}
return "是否批准执行该工具?";
}
/**
* 构建带请求身份信息的事件。
*
* @param type 事件类型
* @return 运行时事件
*/
private AgentRuntimeEvent baseEvent(AgentRuntimeEventType type) {
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());
return event;
}
/**
* 追加 Skill 归属字段。
*
* @param target 目标 Map
* @param binding Skill 绑定关系
*/
private void appendSkillPayload(Map<String, Object> target, AgentSkillBinding binding) {
if (binding == null || target == null) {
return;
}
target.put("skillId", binding.getSkillId());
target.put("skillName", binding.getSkillName());
target.put("skillBoxId", binding.getSkillBoxId());
}
/**
* 获取当前已激活的 Skill 绑定关系。
*
* @return 已激活绑定关系
*/
private AgentSkillBinding activeSkillBinding() {
if (skillBinding == null) {
return null;
}
if (skillContext == null) {
return skillBinding;
}
return skillContext.getActiveToolBinding(toolSpec.getName());
}
/**
* 从事件元数据判断工具结果是否成功。
*
* @param event 运行时事件
* @return 成功时为 true
*/
private boolean success(AgentRuntimeEvent event) {
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);
}
}
}

View File

@@ -0,0 +1,236 @@
package com.easyagents.agent.runtime.event;
import com.easyagents.agent.runtime.message.AgentMessage;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
/**
* 智能体执行过程中的中立运行时事件。
*/
public class AgentRuntimeEvent {
private String eventId = UUID.randomUUID().toString();
private AgentRuntimeEventType eventType;
private String traceId;
private String sessionId;
private String agentId;
private String messageId;
private String toolCallId;
private AgentMessage message;
private Map<String, Object> payload = new LinkedHashMap<>();
private Map<String, Object> metadata = new LinkedHashMap<>();
private Instant createdAt = Instant.now();
/**
* 创建指定类型的事件。
*
* @param eventType 事件类型
* @return 新事件
*/
public static AgentRuntimeEvent of(AgentRuntimeEventType eventType) {
AgentRuntimeEvent event = new AgentRuntimeEvent();
event.setEventType(eventType);
return event;
}
/**
* 获取事件ID。
*
* @return 事件ID
*/
public String getEventId() {
return eventId;
}
/**
* 设置事件ID。
*
* @param eventId 事件ID
*/
public void setEventId(String eventId) {
this.eventId = eventId;
}
/**
* 获取事件类型。
*
* @return 事件类型
*/
public AgentRuntimeEventType getEventType() {
return eventType;
}
/**
* 设置事件类型。
*
* @param eventType 事件类型
*/
public void setEventType(AgentRuntimeEventType eventType) {
this.eventType = eventType;
}
/**
* 获取链路ID。
*
* @return 链路ID
*/
public String getTraceId() {
return traceId;
}
/**
* 设置链路ID。
*
* @param traceId 链路ID
*/
public void setTraceId(String traceId) {
this.traceId = traceId;
}
/**
* 获取会话ID。
*
* @return 会话ID
*/
public String getSessionId() {
return sessionId;
}
/**
* 设置会话ID。
*
* @param sessionId 会话ID
*/
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
/**
* 获取智能体ID。
*
* @return 智能体ID
*/
public String getAgentId() {
return agentId;
}
/**
* 设置智能体ID。
*
* @param agentId 智能体ID
*/
public void setAgentId(String agentId) {
this.agentId = agentId;
}
/**
* 获取消息ID。
*
* @return 消息ID
*/
public String getMessageId() {
return messageId;
}
/**
* 设置消息ID。
*
* @param messageId 消息ID
*/
public void setMessageId(String messageId) {
this.messageId = messageId;
}
/**
* 获取工具调用ID。
*
* @return 工具调用ID
*/
public String getToolCallId() {
return toolCallId;
}
/**
* 设置工具调用ID。
*
* @param toolCallId 工具调用ID
*/
public void setToolCallId(String toolCallId) {
this.toolCallId = toolCallId;
}
/**
* 获取结构化消息。
*
* @return 结构化消息
*/
public AgentMessage getMessage() {
return message;
}
/**
* 设置结构化消息。
*
* @param message 结构化消息
*/
public void setMessage(AgentMessage message) {
this.message = message;
}
/**
* 获取载荷。
*
* @return 载荷
*/
public Map<String, Object> getPayload() {
return payload;
}
/**
* 设置载荷。
*
* @param payload 载荷
*/
public void setPayload(Map<String, Object> payload) {
this.payload = payload == null ? new LinkedHashMap<>() : payload;
}
/**
* 获取元数据。
*
* @return 元数据
*/
public Map<String, Object> getMetadata() {
return metadata;
}
/**
* 设置元数据。
*
* @param metadata 元数据
*/
public void setMetadata(Map<String, Object> 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;
}
}

View File

@@ -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 运行时的旁路事件桥。
*
* <p>该类只负责 Easy-Agents 自己的旁路监察事件,不负责 AgentScope 主线路事件。
* 主线路事件来自 {@code agent.stream(...)},会进入模型会话、工具结果和最终输出的正常顺序;
* 旁路事件只给调用方观察运行过程,例如知识库检索、自动上下文压缩、工具审批和 Skill 步骤。</p>
*
* <p>旁路事件不会写入 AgentScope memory/session也不会修改 AgentScope {@code Msg}。仅作观察与展示使用</p>
*/
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<AgentRuntimeEvent> 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;
}
/**
* 发射一条旁路事件。
*
* <p>当前没有运行轮次或没有旁路 sink 时直接忽略。这允许 adapter 在非流式测试、
* 初始化阶段或主线路未建立旁路订阅时保持无副作用。</p>
*
* @param event 旁路事件
*/
public void emit(AgentRuntimeEvent event) {
if (event == null) {
return;
}
Optional<Sinks.Many<AgentRuntimeEvent>> 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<String, Object> metadata = context.getMetadata();
if (metadata != null && !metadata.isEmpty()) {
metadata.forEach(event.getMetadata()::putIfAbsent);
}
}
private Optional<Sinks.Many<AgentRuntimeEvent>> eventSink() {
return turnContextHolder == null ? Optional.empty() : turnContextHolder.eventSink();
}
}

View File

@@ -0,0 +1,131 @@
package com.easyagents.agent.runtime.event;
/**
* 智能体运行输出的运行时事件类型。
*/
public enum AgentRuntimeEventType {
/**
* 智能体运行已开始。
*/
STARTED,
/**
* 流式推理内容,用于展示思考过程。
*/
REASONING_DELTA,
/**
* 智能体开始一次模型推理。
*/
REASONING_STARTED,
/**
* 智能体完成一次模型推理。
*/
REASONING_COMPLETED,
/**
* 流式输出内容,用于流式展示聊天内容。
*/
MESSAGE_DELTA,
/**
* 智能体发起工具调用。
*/
TOOL_CALL,
/**
* 工具调用完成并返回结果。
*/
TOOL_RESULT,
/**
* 异步工具已提交任务。
*/
ASYNC_TOOL_SUBMITTED,
/**
* 异步工具已观察任务状态。
*/
ASYNC_TOOL_OBSERVED,
/**
* 异步工具已读取任务结果。
*/
ASYNC_TOOL_RESULT,
/**
* 异步工具已请求取消任务。
*/
ASYNC_TOOL_CANCELLED,
/**
* 异步工具已查询任务列表。
*/
ASYNC_TOOL_LISTED,
/**
* 异步工具执行失败。
*/
ASYNC_TOOL_FAILED,
/**
* 知识库检索完成并返回文档摘要。
*/
KNOWLEDGE_RETRIEVAL,
/**
* 自动上下文压缩已开始。
*/
MEMORY_COMPRESSION_STARTED,
/**
* 自动上下文压缩已完成。
*/
MEMORY_COMPRESSION_COMPLETED,
/**
* 工具执行前需要人工审批。
*/
TOOL_APPROVAL_REQUIRED,
/**
* 智能体开始加载并调用 Skill。
*/
SKILL_CALL,
/**
* Skill 内部执行步骤。
*/
SKILL_STEP,
/**
* Skill 加载或调用完成。
*/
SKILL_RESULT,
/**
* Skill 加载或调用失败。
*/
SKILL_FAILED,
/**
* 智能体处理完成。
*/
COMPLETED,
/**
* 智能体运行已暂停,等待外部输入后继续。
*/
SUSPENDED,
/**
* 智能体运行失败。
*/
FAILED,
/**
* 智能体运行被取消。
*/
CANCELLED
}

View File

@@ -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
*
* <p>干预器允许修改 AgentScope HookEvent因此会影响主线路执行。
* 典型场景包括 AutoContext 在推理前改写输入消息,或后续 HITL 在推理后调用
* {@code stopAgent()} 暂停工具执行。普通运行状态通知应使用 {@link AgentRuntimeObserver}。</p>
* <p>警告本干预器会影响到主线路agent 交互,谨慎使用
* </p>
*/
public interface AgentRuntimeInterceptor {
/**
* 处理并返回可能被修改的 AgentScope Hook 事件。
*
* @param event AgentScope Hook 事件
* @param <T> Hook 事件类型
* @return 处理后的 Hook 事件
*/
<T extends HookEvent> Mono<T> intercept(T event);
/**
* 获取干预器执行优先级。
*
* @return 优先级,数值越小越先执行
*/
default int priority() {
return 100;
}
}

View File

@@ -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 事件的统一观察和干预调度器。
*
* <p>处理顺序固定为:先执行干预器,再执行观察器。干预器属于主线路能力,
* 可以修改 AgentScope HookEvent观察器属于旁线路能力只能查看事件并通过
* {@link AgentRuntimeEventBridge} 发射对外监察事件。</p>
*/
public class AgentRuntimeObservationManager {
private final List<AgentRuntimeInterceptor> interceptors;
private final List<AgentRuntimeObserver> observers;
/**
* 创建观察调度器。
*
* @param interceptors 主线路干预器
* @param observers 旁路观察器
*/
public AgentRuntimeObservationManager(List<AgentRuntimeInterceptor> interceptors,
List<AgentRuntimeObserver> 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 <T> Hook 事件类型
* @return 处理后的 Hook 事件
*/
public <T extends HookEvent> Mono<T> handle(T event) {
Mono<T> 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<AgentRuntimeInterceptor> sortInterceptors(List<AgentRuntimeInterceptor> source) {
List<AgentRuntimeInterceptor> sorted = new ArrayList<>(source == null ? List.of() : source);
sorted.sort(Comparator.comparingInt(AgentRuntimeInterceptor::priority));
return List.copyOf(sorted);
}
private List<AgentRuntimeObserver> sortObservers(List<AgentRuntimeObserver> source) {
List<AgentRuntimeObserver> sorted = new ArrayList<>(source == null ? List.of() : source);
sorted.sort(Comparator.comparingInt(AgentRuntimeObserver::priority));
return List.copyOf(sorted);
}
}

View File

@@ -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
*
* <p>观察器只做监察和旁路事件发射,不允许修改 AgentScope HookEvent。
* 如果能力需要影响主线路,例如修改输入消息、暂停 agent 或替换工具结果,应实现
* {@link AgentRuntimeInterceptor}。</p>
*/
public interface AgentRuntimeObserver {
/**
* 观察 AgentScope Hook 事件。
*
* @param event AgentScope Hook 事件
* @return 完成信号
*/
Mono<Void> observe(HookEvent event);
/**
* 获取观察器执行优先级。
*
* @return 优先级,数值越小越先执行
*/
default int priority() {
return 100;
}
}

View File

@@ -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。
*
* <p>该字段只承载旁线路监察事件,不承载 AgentScope 主线路 stream 事件。</p>
*/
private final Sinks.Many<AgentRuntimeEvent> eventSink;
/**
* 本轮旁路事件桥。
*
* <p>adapter 和 observer 应优先通过 bridge 发射旁路事件,避免各处重复拼接
* trace/session/request 等公共字段。</p>
*/
private final AgentRuntimeEventBridge eventBridge;
/**
* 创建单轮执行上下文。
*
* @param executionContext 本轮运行上下文
* @param eventSink 本轮旁路事件 sink
*/
public AgentRuntimeTurnContext(AgentRuntimeExecutionContext executionContext,
Sinks.Many<AgentRuntimeEvent> eventSink) {
this(executionContext, eventSink, null);
}
/**
* 创建单轮执行上下文。
*
* @param executionContext 本轮运行上下文
* @param eventSink 本轮旁路事件 sink
* @param eventBridge 本轮旁路事件桥
*/
public AgentRuntimeTurnContext(AgentRuntimeExecutionContext executionContext,
Sinks.Many<AgentRuntimeEvent> eventSink,
AgentRuntimeEventBridge eventBridge) {
this.executionContext = executionContext;
this.eventSink = eventSink;
this.eventBridge = eventBridge;
}
/**
* 获取本轮运行上下文。
*
* @return 本轮运行上下文
*/
public AgentRuntimeExecutionContext getExecutionContext() {
return executionContext;
}
/**
* 获取本轮旁路事件 sink。
*
* @return 本轮旁路事件 sink
*/
public Sinks.Many<AgentRuntimeEvent> 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<String, Object> mergedMetadata(AgentRuntimeExecutionContext fallback,
AgentRuntimeExecutionContext current) {
Map<String, Object> 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;
}
}

View File

@@ -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<AgentRuntimeTurnContext> current = new AtomicReference<>();
/**
* 设置当前运行轮次上下文。
*
* @param context 当前轮次上下文
*/
public void set(AgentRuntimeTurnContext context) {
current.set(context);
}
/**
* 清理当前运行轮次上下文。
*/
public void clear() {
current.set(null);
}
/**
* 获取当前运行轮次上下文。
*
* @return 当前轮次上下文
*/
public Optional<AgentRuntimeTurnContext> current() {
return Optional.ofNullable(current.get());
}
/**
* 获取当前轮次事件 sink。
*
* @return 当前轮次旁路事件 sink
*/
public Optional<Sinks.Many<AgentRuntimeEvent>> eventSink() {
return current()
.map(AgentRuntimeTurnContext::getEventSink)
.filter(sink -> sink != null);
}
/**
* 获取当前轮次旁路事件桥。
*
* @return 当前轮次旁路事件桥
*/
public Optional<AgentRuntimeEventBridge> 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);
}
}

View File

@@ -0,0 +1,356 @@
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 主线路干预器。
*
* <p><strong>特殊约束:</strong>本干预器是对 AgentScope 官方
* {@code AutoContextHook} 的替代实现,不能与官方 {@code AutoContextHook} 同时注册。
* 两者同时存在会导致 {@link AutoContextMemory#compressIfNeeded()}、上下文重写以及
* {@link ContextOffloadTool} 注册被重复执行。</p>
*
* <p>本类承担两类职责。第一类是主线路干预:在 {@link PreCallEvent} 中注册
* AutoContext 工具能力,在 {@link PreReasoningEvent} 中触发记忆压缩并改写
* LLM 输入消息。第二类是旁路通知:通过 {@link AgentRuntimeEventBridge} 发出
* {@link AgentRuntimeEventType#MEMORY_COMPRESSION_STARTED} 和
* {@link AgentRuntimeEventType#MEMORY_COMPRESSION_COMPLETED},这些事件只用于调用方展示,
* 不写入 AgentScope memory/session也不参与 LLM 会话协议。</p>
*/
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 <!-- CONTEXT_OFFLOAD uuid=... -->.\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 主线路干预器。
*
* <p>传入的 {@link AutoContextConfig} 必须是创建目标 {@link AutoContextMemory}
* 时使用的同一份配置。这样压缩开始事件的触发条件才能与 AgentScope
* {@code compressIfNeeded()} 的入口判断保持一致。</p>
*
* @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 <T> Hook 事件类型
* @return 处理后的 Hook 事件
*/
@Override
public <T extends HookEvent> Mono<T> intercept(T event) {
if (event instanceof PreCallEvent preCallEvent) {
@SuppressWarnings("unchecked")
Mono<T> result = (Mono<T>) handlePreCall(preCallEvent);
return result;
}
if (event instanceof PreReasoningEvent preReasoningEvent) {
@SuppressWarnings("unchecked")
Mono<T> result = (Mono<T>) 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<PreCallEvent> 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 输入消息。
*
* <p>这是主线路干预逻辑:{@code compressIfNeeded()} 和 {@code setInputMessages(...)}
* 会影响 AgentScope 本次 reasoning 输入。压缩开始/完成事件则是旁路通知,只发给调用方。</p>
*
* @param event 推理前事件
* @return 改写输入消息后的事件
*/
private Mono<PreReasoningEvent> 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()) {
emitCompressionStarted(compressionCheck);
boolean compressed = autoContextMemory.compressIfNeeded();
List<CompressionEvent> newEvents = newCompressionEvents(autoContextMemory, beforeEventCount);
emitCompressionCompleted(compressionCheck, newEvents, compressed && !newEvents.isEmpty());
}
// 压缩完毕后自动进行当前的会话
event.setInputMessages(buildInputMessages(event, autoContextMemory));
return Mono.just(event);
}
/**
* 计算当前记忆是否达到 AutoContext 官方压缩入口条件。
*
* @param messages 当前工作记忆消息
* @return 压缩入口检查结果
*/
private CompressionCheck compressionCheck(List<Msg> messages) {
List<Msg> 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<CompressionEvent> events = autoContextMemory.getCompressionEvents();
return events == null ? 0 : events.size();
}
/**
* 获取本次压缩新增的 AgentScope 压缩事件。
*
* @param autoContextMemory AutoContext 记忆
* @param beforeEventCount 压缩前事件数量
* @return 新增压缩事件
*/
private List<CompressionEvent> newCompressionEvents(AutoContextMemory autoContextMemory, int beforeEventCount) {
List<CompressionEvent> 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<Msg> buildInputMessages(PreReasoningEvent event, AutoContextMemory autoContextMemory) {
List<Msg> originalInputMessages = event.getInputMessages();
List<Msg> 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<CompressionEvent> events,
boolean compressed) {
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", compressed ? "已整理上下文" : "无需压缩上下文");
event.getPayload().put("compressed", compressed);
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<Map<String, Object>> toPayloadEvents(List<CompressionEvent> events) {
if (events == null || events.isEmpty()) {
return List.of();
}
List<Map<String, Object>> payloadEvents = new ArrayList<>();
for (CompressionEvent event : events) {
Map<String, Object> 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;
}
}
}

View File

@@ -0,0 +1,220 @@
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 主线路干预器。
*
* <p>本 interceptor 专门处理“工具执行前人工审批”。监听 AgentScope 原生
* {@link PostReasoningEvent}</p>
*
* <p>这里包含两类动作:
* <ul>
* <li>主线路干预:发现待审批工具后调用 {@link PostReasoningEvent#stopAgent()}
* 让 AgentScope 返回当前带 ToolUseBlock 的消息并暂停工具执行。</li>
* <li>旁路交互事件:通过 {@link AgentRuntimeEventBridge} 发出
* {@link AgentRuntimeEventType#TOOL_APPROVAL_REQUIRED},通知调用方展示审批交互。</li>
* </ul>
*
* <p>注意:本 interceptor 不执行工具、不写入 AgentScope memory/session也不实现恢复。
* 后续 resume 流程应基于 AgentScope pending tool 状态继续调用 agent stream/call。</p>
*/
public class ToolHitlInterceptor implements AgentRuntimeInterceptor {
private final AgentRuntimeEventBridge eventBridge;
private final AgentToolApprovalCoordinator approvalCoordinator;
private final Map<String, AgentToolSpec> toolSpecs;
/**
* 创建工具 HITL 干预器。
*
* @param eventBridge 旁路事件桥
* @param approvalCoordinator 工具审批协调器
* @param toolSpecs 工具声明列表
*/
public ToolHitlInterceptor(AgentRuntimeEventBridge eventBridge,
AgentToolApprovalCoordinator approvalCoordinator,
List<AgentToolSpec> toolSpecs) {
this.eventBridge = eventBridge;
this.approvalCoordinator = approvalCoordinator;
this.toolSpecs = (toolSpecs == null ? List.<AgentToolSpec>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 <T> Hook 事件类型
* @return 处理后的 Hook 事件
*/
@Override
public <T extends HookEvent> Mono<T> intercept(T event) {
if (event instanceof PostReasoningEvent postReasoningEvent) {
interceptPostReasoning(postReasoningEvent);
}
return Mono.just(event);
}
/**
* 返回执行优先级。
*
* <p>工具审批需要在普通观察器发出 reasoning completed 后保持事件已被标记暂停,
* 但不应早于 AutoContext 的 PreReasoning 干预。当前值用于主线路 reasoning 后检查。</p>
*
* @return 优先级
*/
@Override
public int priority() {
return 50;
}
private void interceptPostReasoning(PostReasoningEvent event) {
Msg reasoningMessage = event.getReasoningMessage();
if (reasoningMessage == null) {
return;
}
List<ToolUseBlock> approvalRequiredTools = approvalRequiredTools(reasoningMessage);
if (approvalRequiredTools.isEmpty()) {
return;
}
List<Map<String, Object>> 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<ToolUseBlock> approvalRequiredTools(Msg reasoningMessage) {
List<ToolUseBlock> 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<String, Object> metadata = approvalRequest == null
? new LinkedHashMap<>()
: new LinkedHashMap<>(approvalRequest.getMetadata());
if (toolSpec.getMetadata() != null && !toolSpec.getMetadata().isEmpty()) {
metadata.putAll(toolSpec.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());
enrichToolPayload(event.getPayload(), toolSpec);
event.getMetadata().put("source", "TOOL_HITL_INTERCEPTOR");
event.getMetadata().put("phase", "POST_REASONING");
return event;
}
private Map<String, Object> pendingApprovalPayload(AgentPendingState pendingState, ToolUseBlock toolUse) {
Map<String, Object> 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 void enrichToolPayload(Map<String, Object> payload, AgentToolSpec toolSpec) {
if (toolSpec == null || toolSpec.getMetadata() == null || toolSpec.getMetadata().isEmpty()) {
return;
}
Map<String, Object> metadata = toolSpec.getMetadata();
putIfPresent(payload, metadata, "toolDisplayName");
putIfPresent(payload, metadata, "rawMcpToolName");
putIfPresent(payload, metadata, "mcpToolName");
putIfPresent(payload, metadata, "mcpName");
putIfPresent(payload, metadata, "mcpTitle");
}
private void putIfPresent(Map<String, Object> payload, Map<String, Object> metadata, String key) {
if (metadata.containsKey(key)) {
payload.put(key, metadata.get(key));
}
}
private String approvalPrompt(AgentToolApprovalRequest approvalRequest) {
if (approvalRequest != null
&& approvalRequest.getApprovalPrompt() != null
&& !approvalRequest.getApprovalPrompt().isBlank()) {
return approvalRequest.getApprovalPrompt();
}
return "是否批准执行该工具?";
}
}

View File

@@ -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 原生错误事件,并发射运行失败旁路事件。
*
* <p>该观察器复用 {@link AgentRuntimeEventType#FAILED},但通过 payload 中的
* {@code source=HOOK} 标识它来自 AgentScope 生命周期观察,不等同于 Easy-Agents
* runtime 外层流已经完成失败收口。</p>
*/
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<Void> 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());
}
}

View File

@@ -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 推理生命周期,并发射思考状态旁路事件。
*
* <p>{@link AgentRuntimeEventType#REASONING_DELTA} 表示主线路中的推理内容片段;
* 本观察器发射的 started/completed 事件只用于前端状态展示,不携带模型上下文,
* 也不会修改 AgentScope 的推理输入或输出。</p>
*/
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<Void> 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();
}
}

View File

@@ -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 旁路事件。
*
* <p>该观察器只做 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}。</p>
*
* <p>Skill 是否激活以 AgentScope {@link SkillBox} 和 {@link Toolkit#getActiveGroups()}
* 为准,本地 {@link AgentSkillRuntimeContext} 只缓存旁路展示所需的归属状态。</p>
*/
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<Void> 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<String, Object> 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<String, Object> nullToEmpty(Map<String, Object> map) {
return map == null ? new LinkedHashMap<>() : map;
}
}

View File

@@ -0,0 +1,199 @@
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 com.easyagents.agent.runtime.tool.AgentToolSpec;
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.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 监听 AgentScope 原生工具执行生命周期,并发射工具状态旁路事件。
*
* <p>该观察器复用 {@link AgentRuntimeEventType#TOOL_CALL} 和
* {@link AgentRuntimeEventType#TOOL_RESULT},用于 EasyFlow 展示工具开始与完成状态。
* 它不修改 AgentScope HookEvent也不写入模型上下文。</p>
*/
public class ToolExecutionObserver implements AgentRuntimeObserver {
private final AgentRuntimeEventBridge eventBridge;
private final AgentSkillRuntimeContext skillContext;
private final Map<String, AgentToolSpec> toolSpecs;
/**
* 创建工具执行观察器。
*
* @param eventBridge 旁路事件桥
*/
public ToolExecutionObserver(AgentRuntimeEventBridge eventBridge) {
this(eventBridge, null, List.of());
}
/**
* 创建工具执行观察器。
*
* @param eventBridge 旁路事件桥
* @param skillContext Skill 上下文,用于跳过由 SkillExecutionObserver 处理的工具
*/
public ToolExecutionObserver(AgentRuntimeEventBridge eventBridge,
AgentSkillRuntimeContext skillContext) {
this(eventBridge, skillContext, List.of());
}
/**
* 创建工具执行观察器。
*
* @param eventBridge 旁路事件桥
* @param skillContext Skill 上下文,用于跳过由 SkillExecutionObserver 处理的工具
* @param toolSpecs 工具声明列表,用于补齐展示名称和治理元数据
*/
public ToolExecutionObserver(AgentRuntimeEventBridge eventBridge,
AgentSkillRuntimeContext skillContext,
List<AgentToolSpec> toolSpecs) {
this.eventBridge = eventBridge;
this.skillContext = skillContext;
this.toolSpecs = (toolSpecs == null ? List.<AgentToolSpec>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));
}
/**
* 观察工具执行前后事件。
*
* @param event AgentScope Hook 事件
* @return 完成信号
*/
@Override
public Mono<Void> 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()));
enrichToolPayload(runtimeEvent, toolUse.getName());
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()));
}
enrichToolPayload(runtimeEvent, toolName);
eventBridge.emit(runtimeEvent);
}
private void enrichToolPayload(AgentRuntimeEvent runtimeEvent, String toolName) {
AgentToolSpec toolSpec = toolSpecs.get(toolName);
if (toolSpec == null || toolSpec.getMetadata() == null || toolSpec.getMetadata().isEmpty()) {
return;
}
Map<String, Object> metadata = toolSpec.getMetadata();
putIfPresent(runtimeEvent.getPayload(), metadata, "toolDisplayName");
putIfPresent(runtimeEvent.getPayload(), metadata, "rawMcpToolName");
putIfPresent(runtimeEvent.getPayload(), metadata, "mcpToolName");
putIfPresent(runtimeEvent.getPayload(), metadata, "mcpName");
putIfPresent(runtimeEvent.getPayload(), metadata, "mcpTitle");
putIfPresent(runtimeEvent.getPayload(), metadata, "source");
runtimeEvent.getMetadata().putAll(metadata);
}
private void putIfPresent(Map<String, Object> payload, Map<String, Object> metadata, String key) {
if (metadata.containsKey(key)) {
payload.put(key, metadata.get(key));
}
}
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<String, Object> nullToEmpty(Map<String, Object> 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;
}
}

View File

@@ -0,0 +1,202 @@
package com.easyagents.agent.runtime.hitl;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 等待工具执行审批的状态。
*/
public class AgentPendingState {
private AgentResumeToken resumeToken = AgentResumeToken.create();
private String sessionId;
private String agentId;
private String eventId;
private String toolCallId;
private String toolName;
private String approvalPrompt;
private Map<String, Object> toolInput = new LinkedHashMap<>();
private Instant expiresAt;
private Map<String, Object> metadata = new LinkedHashMap<>();
/**
* 获取恢复令牌。
*
* @return 恢复令牌
*/
public AgentResumeToken getResumeToken() {
return resumeToken;
}
/**
* 设置恢复令牌。
*
* @param resumeToken 恢复令牌
*/
public void setResumeToken(AgentResumeToken resumeToken) {
this.resumeToken = resumeToken == null ? AgentResumeToken.create() : resumeToken;
}
/**
* 获取会话ID。
*
* @return 会话ID
*/
public String getSessionId() {
return sessionId;
}
/**
* 设置会话ID。
*
* @param sessionId 会话ID
*/
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
/**
* 获取智能体ID。
*
* @return 智能体ID
*/
public String getAgentId() {
return agentId;
}
/**
* 设置智能体ID。
*
* @param agentId 智能体ID
*/
public void setAgentId(String agentId) {
this.agentId = agentId;
}
/**
* 获取事件ID。
*
* @return 事件ID
*/
public String getEventId() {
return eventId;
}
/**
* 设置事件ID。
*
* @param eventId 事件ID
*/
public void setEventId(String eventId) {
this.eventId = eventId;
}
/**
* 获取工具调用ID。
*
* @return 工具调用ID
*/
public String getToolCallId() {
return toolCallId;
}
/**
* 设置工具调用ID。
*
* @param toolCallId 工具调用ID
*/
public void setToolCallId(String toolCallId) {
this.toolCallId = toolCallId;
}
/**
* 获取工具名称。
*
* @return 工具名称
*/
public String getToolName() {
return toolName;
}
/**
* 设置工具名称。
*
* @param toolName 工具名称
*/
public void setToolName(String toolName) {
this.toolName = toolName;
}
/**
* 获取审批提示文案。
*
* @return 审批提示文案
*/
public String getApprovalPrompt() {
return approvalPrompt;
}
/**
* 设置审批提示文案。
*
* @param approvalPrompt 审批提示文案
*/
public void setApprovalPrompt(String approvalPrompt) {
this.approvalPrompt = approvalPrompt;
}
/**
* 获取工具调用参数。
*
* @return 工具调用参数
*/
public Map<String, Object> getToolInput() {
return toolInput;
}
/**
* 设置工具调用参数。
*
* @param toolInput 工具调用参数
*/
public void setToolInput(Map<String, Object> toolInput) {
this.toolInput = toolInput == null ? new LinkedHashMap<>() : toolInput;
}
/**
* 获取过期时间。
*
* @return 过期时间
*/
public Instant getExpiresAt() {
return expiresAt;
}
/**
* 设置过期时间。
*
* @param expiresAt 过期时间
*/
public void setExpiresAt(Instant expiresAt) {
this.expiresAt = expiresAt;
}
/**
* 获取元数据。
*
* @return 元数据
*/
public Map<String, Object> getMetadata() {
return metadata;
}
/**
* 设置元数据。
*
* @param metadata 元数据
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
}
}

View File

@@ -0,0 +1,38 @@
package com.easyagents.agent.runtime.hitl;
import java.util.UUID;
/**
* 用于恢复挂起运行的令牌。
*/
public class AgentResumeToken {
private String value = UUID.randomUUID().toString();
/**
* 创建a new token。
*
* @return 令牌
*/
public static AgentResumeToken create() {
return new AgentResumeToken();
}
/**
* 获取令牌值。
*
* @return 令牌值
*/
public String getValue() {
return value;
}
/**
* 设置令牌值。
*
* @param value 令牌值
*/
public void setValue(String value) {
this.value = value;
}
}

View File

@@ -0,0 +1,27 @@
package com.easyagents.agent.runtime.hitl;
/**
* 已挂起的运行时状态。
*/
public class AgentSuspendState {
private AgentPendingState pendingState;
/**
* 获取挂起状态。
*
* @return 挂起状态
*/
public AgentPendingState getPendingState() {
return pendingState;
}
/**
* 设置挂起状态。
*
* @param pendingState 挂起状态
*/
public void setPendingState(AgentPendingState pendingState) {
this.pendingState = pendingState;
}
}

View File

@@ -0,0 +1,175 @@
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;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
/**
* 工具审批协调器。
*/
public class AgentToolApprovalCoordinator {
private final boolean enabled;
private final Map<String, PendingApproval> approvals = new ConcurrentHashMap<>();
/**
* 创建已启用的协调器。
*
* @return 已启用的协调器
*/
public static AgentToolApprovalCoordinator enabled() {
return new AgentToolApprovalCoordinator(true);
}
/**
* 创建禁用的协调器。
*
* @return 禁用的协调器
*/
public static AgentToolApprovalCoordinator disabled() {
return new AgentToolApprovalCoordinator(false);
}
/**
* 创建协调器。
*
* @param enabled 是否启用
*/
public AgentToolApprovalCoordinator(boolean enabled) {
this.enabled = enabled;
}
/**
* 注册一个待审批请求。
*
* @param sessionId 会话ID
* @param agentId 智能体ID
* @param toolCallId 工具调用ID
* @param toolName 工具名称
* @param approvalPrompt 审批文案
* @param toolInput 工具入参
* @param metadata 元数据
* @param expiresAt 过期时间
* @return 待审批状态
*/
public AgentPendingState register(String sessionId,
String agentId,
String toolCallId,
String toolName,
String approvalPrompt,
Map<String, Object> toolInput,
Map<String, Object> metadata,
Instant expiresAt) {
AgentPendingState state = new AgentPendingState();
state.setSessionId(sessionId);
state.setAgentId(agentId);
state.setToolCallId(toolCallId);
state.setToolName(toolName);
state.setApprovalPrompt(approvalPrompt);
state.setToolInput(toolInput);
state.setMetadata(metadata);
state.setExpiresAt(expiresAt);
if (enabled) {
String token = state.getResumeToken().getValue();
PendingApproval pendingApproval = new PendingApproval(state, new CompletableFuture<>());
approvals.put(token, pendingApproval);
pendingApproval.future.whenComplete((response, error) -> approvals.remove(token));
}
return state;
}
/**
* 等待指定审批令牌的响应。
*
* @param resumeToken 恢复令牌
* @return 恢复请求
*/
public Mono<AgentResumeRequest> await(AgentResumeToken resumeToken) {
if (!enabled) {
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("缺少审批令牌。"));
}
PendingApproval pendingApproval = approvals.get(resumeToken.getValue());
if (pendingApproval == null) {
return Mono.error(new AgentToolApprovalRejectedException("审批请求已失效。"));
}
return Mono.fromFuture(pendingApproval.future);
}
/**
* 消费恢复请求对应的待审批状态。
*
* <p>该方法用于有状态 runtime 的 HITL resume。第一版 pending state 仅保存在
* 当前进程内存中,因此消费成功后会立即移除 token避免重复恢复。</p>
*
* @param request 恢复请求
* @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.");
}
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;
}
/**
* 取消所有挂起审批。
*
* @param reason 取消原因
*/
public void cancelAll(String reason) {
if (!enabled) {
return;
}
for (PendingApproval pendingApproval : approvals.values()) {
AgentResumeRequest request = new AgentResumeRequest();
request.setResumeToken(pendingApproval.state.getResumeToken());
request.setApproved(false);
request.setRejectReason(reason);
pendingApproval.future.complete(request);
}
approvals.clear();
}
/**
* 是否已启用。
*
* @return 启用标记
*/
public boolean isEnabled() {
return enabled;
}
private static class PendingApproval {
private final AgentPendingState state;
private final CompletableFuture<AgentResumeRequest> future;
private PendingApproval(AgentPendingState state, CompletableFuture<AgentResumeRequest> future) {
this.state = Objects.requireNonNull(state, "state");
this.future = Objects.requireNonNull(future, "future");
}
}
}

View File

@@ -0,0 +1,30 @@
package com.easyagents.agent.runtime.hitl;
import com.easyagents.agent.runtime.AgentRuntimeException;
/**
* 工具审批被拒绝时抛出的异常。
*/
public class AgentToolApprovalRejectedException extends AgentRuntimeException {
private final String rejectReason;
/**
* 创建工具审批拒绝异常。
*
* @param rejectReason 拒绝原因
*/
public AgentToolApprovalRejectedException(String rejectReason) {
super(rejectReason == null || rejectReason.isBlank() ? "工具执行已被拒绝。" : rejectReason);
this.rejectReason = rejectReason;
}
/**
* 获取拒绝原因。
*
* @return 拒绝原因
*/
public String getRejectReason() {
return rejectReason;
}
}

View File

@@ -0,0 +1,69 @@
package com.easyagents.agent.runtime.hitl;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 工具执行审批请求。
*/
public class AgentToolApprovalRequest {
private String approvalPrompt;
private Duration timeout = Duration.ofMinutes(30);
private Map<String, Object> metadata = new LinkedHashMap<>();
/**
* 获取审批提示文案。
*
* @return 审批提示文案
*/
public String getApprovalPrompt() {
return approvalPrompt;
}
/**
* 设置审批提示文案。
*
* @param approvalPrompt 审批提示文案
*/
public void setApprovalPrompt(String approvalPrompt) {
this.approvalPrompt = approvalPrompt;
}
/**
* 获取审批等待超时时间。
*
* @return 超时时间
*/
public Duration getTimeout() {
return timeout;
}
/**
* 设置审批等待超时时间。
*
* @param timeout 超时时间
*/
public void setTimeout(Duration timeout) {
this.timeout = timeout == null ? Duration.ofMinutes(30) : timeout;
}
/**
* 获取审批元数据。
*
* @return 审批元数据
*/
public Map<String, Object> getMetadata() {
return metadata;
}
/**
* 设置审批元数据。
*
* @param metadata 审批元数据
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
}
}

View File

@@ -0,0 +1,163 @@
package com.easyagents.agent.runtime.knowledge;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 保留元数据的知识库检索文档。
*/
public class AgentKnowledgeDocument {
private String documentId;
private String documentName;
private String chunkId;
private String content;
private String sourceUri;
private Double score;
private Map<String, Object> knowledgeMetadata = new LinkedHashMap<>();
private Map<String, Object> metadata = new LinkedHashMap<>();
/**
* 获取文档ID。
*
* @return 文档ID
*/
public String getDocumentId() {
return documentId;
}
/**
* 设置文档ID。
*
* @param documentId 文档ID
*/
public void setDocumentId(String documentId) {
this.documentId = documentId;
}
/**
* 获取文档名称。
*
* @return 文档名称
*/
public String getDocumentName() {
return documentName;
}
/**
* 设置文档名称。
*
* @param documentName 文档名称
*/
public void setDocumentName(String documentName) {
this.documentName = documentName;
}
/**
* 获取分片ID。
*
* @return 分片ID
*/
public String getChunkId() {
return chunkId;
}
/**
* 设置分片ID。
*
* @param chunkId 分片ID
*/
public void setChunkId(String chunkId) {
this.chunkId = chunkId;
}
/**
* 获取内容。
*
* @return 内容
*/
public String getContent() {
return content;
}
/**
* 设置内容。
*
* @param content 内容
*/
public void setContent(String content) {
this.content = content;
}
/**
* 获取来源 URI。
*
* @return 来源 URI
*/
public String getSourceUri() {
return sourceUri;
}
/**
* 设置来源 URI。
*
* @param sourceUri 来源 URI
*/
public void setSourceUri(String sourceUri) {
this.sourceUri = sourceUri;
}
/**
* 获取分数。
*
* @return 分数
*/
public Double getScore() {
return score;
}
/**
* 设置分数。
*
* @param score 分数
*/
public void setScore(Double score) {
this.score = score;
}
/**
* 获取知识库级元数据。
*
* @return 知识库级元数据
*/
public Map<String, Object> getKnowledgeMetadata() {
return knowledgeMetadata;
}
/**
* 设置知识库级元数据。
*
* @param knowledgeMetadata 知识库级元数据
*/
public void setKnowledgeMetadata(Map<String, Object> knowledgeMetadata) {
this.knowledgeMetadata = knowledgeMetadata == null ? new LinkedHashMap<>() : knowledgeMetadata;
}
/**
* 获取文档级元数据。
*
* @return 文档级元数据
*/
public Map<String, Object> getMetadata() {
return metadata;
}
/**
* 设置文档级元数据。
*
* @param metadata 文档级元数据
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
}
}

View File

@@ -0,0 +1,10 @@
package com.easyagents.agent.runtime.knowledge;
/**
* 知识库检索策略。
*/
public enum AgentKnowledgePolicy {
AGENTIC,
GENERIC,
DISABLED
}

View File

@@ -0,0 +1,127 @@
package com.easyagents.agent.runtime.knowledge;
import com.easyagents.agent.runtime.AgentRuntimeContext;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 传递给知识库检索器的请求。
*/
public class AgentKnowledgeRetrievalRequest {
private String query;
private int limit;
private double scoreThreshold;
private AgentKnowledgeSpec knowledgeSpec;
private AgentRuntimeContext runtimeContext = new AgentRuntimeContext();
private Map<String, Object> metadata = new LinkedHashMap<>();
/**
* 获取查询。
*
* @return 查询
*/
public String getQuery() {
return query;
}
/**
* 设置查询。
*
* @param query 查询
*/
public void setQuery(String query) {
this.query = query;
}
/**
* 获取检索数量限制。
*
* @return 检索数量限制
*/
public int getLimit() {
return limit;
}
/**
* 设置检索数量限制。
*
* @param limit 检索数量限制
*/
public void setLimit(int limit) {
this.limit = limit;
}
/**
* 获取分数阈值。
*
* @return 分数阈值
*/
public double getScoreThreshold() {
return scoreThreshold;
}
/**
* 设置分数阈值。
*
* @param scoreThreshold 分数阈值
*/
public void setScoreThreshold(double scoreThreshold) {
this.scoreThreshold = scoreThreshold;
}
/**
* 获取知识库声明。
*
* @return 知识库声明
*/
public AgentKnowledgeSpec getKnowledgeSpec() {
return knowledgeSpec;
}
/**
* 设置知识库声明。
*
* @param knowledgeSpec 知识库声明
*/
public void setKnowledgeSpec(AgentKnowledgeSpec knowledgeSpec) {
this.knowledgeSpec = knowledgeSpec;
}
/**
* 获取运行时上下文。
*
* @return 运行时上下文
*/
public AgentRuntimeContext getRuntimeContext() {
return runtimeContext;
}
/**
* 设置运行时上下文。
*
* @param runtimeContext 运行时上下文
*/
public void setRuntimeContext(AgentRuntimeContext runtimeContext) {
this.runtimeContext = runtimeContext == null ? new AgentRuntimeContext() : runtimeContext;
}
/**
* 获取元数据。
*
* @return 元数据
*/
public Map<String, Object> getMetadata() {
return metadata;
}
/**
* 设置元数据。
*
* @param metadata 元数据
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
}
}

View File

@@ -0,0 +1,63 @@
package com.easyagents.agent.runtime.knowledge;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 知识库检索结果。
*/
public class AgentKnowledgeRetrievalResult {
private List<AgentKnowledgeDocument> documents = new ArrayList<>();
private Map<String, Object> metadata = new LinkedHashMap<>();
/**
* 根据检索文档创建结果。
*
* @param documents 检索文档
* @return 检索结果
*/
public static AgentKnowledgeRetrievalResult of(List<AgentKnowledgeDocument> documents) {
AgentKnowledgeRetrievalResult result = new AgentKnowledgeRetrievalResult();
result.setDocuments(documents);
return result;
}
/**
* 获取文档列表。
*
* @return 文档列表
*/
public List<AgentKnowledgeDocument> getDocuments() {
return documents;
}
/**
* 设置文档列表。
*
* @param documents 文档列表
*/
public void setDocuments(List<AgentKnowledgeDocument> documents) {
this.documents = documents == null ? new ArrayList<>() : documents;
}
/**
* 获取元数据。
*
* @return 元数据
*/
public Map<String, Object> getMetadata() {
return metadata;
}
/**
* 设置元数据。
*
* @param metadata 元数据
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
}
}

View File

@@ -0,0 +1,16 @@
package com.easyagents.agent.runtime.knowledge;
/**
* 从单个知识源检索文档。
*/
@FunctionalInterface
public interface AgentKnowledgeRetriever {
/**
* 检索相关文档。
*
* @param request 检索请求
* @return 检索结果
*/
AgentKnowledgeRetrievalResult retrieve(AgentKnowledgeRetrievalRequest request);
}

View File

@@ -0,0 +1,144 @@
package com.easyagents.agent.runtime.knowledge;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 知识库声明。
*/
public class AgentKnowledgeSpec {
private String knowledgeId;
private String name;
private String description;
private AgentKnowledgePolicy retrievalMode = AgentKnowledgePolicy.AGENTIC;
private int limit = 5;
private double scoreThreshold = 0D;
private Map<String, Object> metadata = new LinkedHashMap<>();
/**
* 获取知识库ID。
*
* @return 知识库ID
*/
public String getKnowledgeId() {
return knowledgeId;
}
/**
* 设置知识库ID。
*
* @param knowledgeId 知识库ID
*/
public void setKnowledgeId(String knowledgeId) {
this.knowledgeId = knowledgeId;
}
/**
* 获取知识库名称。
*
* @return 知识库名称
*/
public String getName() {
return name;
}
/**
* 设置知识库名称。
*
* @param name 知识库名称
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取描述。
*
* @return 描述
*/
public String getDescription() {
return description;
}
/**
* 设置描述。
*
* @param description 描述
*/
public void setDescription(String description) {
this.description = description;
}
/**
* 获取检索模式。
*
* @return 检索模式
*/
public AgentKnowledgePolicy getRetrievalMode() {
return retrievalMode;
}
/**
* 设置检索模式。
*
* @param retrievalMode 检索模式
*/
public void setRetrievalMode(AgentKnowledgePolicy retrievalMode) {
this.retrievalMode = retrievalMode == null ? AgentKnowledgePolicy.AGENTIC : retrievalMode;
}
/**
* 获取限制数量。
*
* @return 限制数量
*/
public int getLimit() {
return limit;
}
/**
* 设置限制数量。
*
* @param limit 限制数量
*/
public void setLimit(int limit) {
this.limit = limit <= 0 ? 5 : limit;
}
/**
* 获取分数阈值。
*
* @return 分数阈值
*/
public double getScoreThreshold() {
return scoreThreshold;
}
/**
* 设置分数阈值。
*
* @param scoreThreshold 分数阈值
*/
public void setScoreThreshold(double scoreThreshold) {
this.scoreThreshold = Math.max(0D, scoreThreshold);
}
/**
* 获取元数据。
*
* @return 元数据
*/
public Map<String, Object> getMetadata() {
return metadata;
}
/**
* 设置元数据。
*
* @param metadata 元数据
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
}
}

View File

@@ -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;
/**
* 知识库引用匹配器。
*
* <p>该接口只负责从最终答案和本轮检索候选中选择可展示的引用。实现不应修改答案文本,
* 也不应把引用写入 AgentScope memory/session。</p>
*/
public interface AgentKnowledgeCitationMatcher {
/**
* 匹配最终答案中可由候选知识片段支撑的引用。
*
* @param answerText 最终答案文本
* @param candidates 本轮检索候选引用
* @return 匹配到的知识库引用
*/
List<AgentKnowledgeReference> match(String answerText, Collection<AgentKnowledgeReference> candidates);
}

View File

@@ -0,0 +1,204 @@
package com.easyagents.agent.runtime.knowledge.citation;
import com.easyagents.agent.runtime.message.AgentKnowledgeReference;
import java.util.*;
/**
* 基于文本证据的启发式知识库引用匹配器。
*
* <p>该实现用于模型没有显式输出引用 ID 的场景。它只能说明答案文本与某些检索片段
* 存在较强文本支撑关系,不能证明 LLM 真实使用了该片段。因此匹配策略保持保守:
* 宁可少返回引用,也不为了“看起来有引用”而强行猜测。</p>
*/
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<AgentKnowledgeReference> match(String answerText, Collection<AgentKnowledgeReference> candidates) {
if (candidates == null || candidates.isEmpty()) {
return List.of();
}
String normalizedAnswer = normalize(answerText);
if (normalizedAnswer.length() < MIN_NORMALIZED_ANSWER_LENGTH) {
return List.of();
}
List<ScoredKnowledgeReference> 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<String> charGrams(String text, int size) {
Set<String> 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<String> answerGrams, Set<String> 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();
}
/**
* 计算答案中的数字片段被候选片段覆盖的比例。
*
* <p>数字只能作为弱加权因素,不能单独造成高置信引用,避免日期、章节号等噪声误判。</p>
*
* @param normalizedAnswer 归一化后的答案
* @param normalizedContent 归一化后的候选片段
* @return 数字覆盖比例
*/
private double numericCoverage(String normalizedAnswer, String normalizedContent) {
List<String> 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<String> extractNumericTokens(String text) {
List<String> 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) {
}
}

View File

@@ -0,0 +1,172 @@
package com.easyagents.agent.runtime.mcp;
import io.agentscope.core.tool.mcp.McpClientWrapper;
import io.modelcontextprotocol.spec.McpSchema;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 为 MCP client 增加运行时工具别名。
*/
class AliasedMcpClientWrapper extends McpClientWrapper {
static final String RAW_TOOL_NAME_META_KEY = "easyagentsRawMcpToolName";
private final McpClientWrapper delegate;
private final Map<String, String> rawToAlias;
private final Map<String, String> aliasToRaw;
private final String toolNamePrefix;
/**
* 创建 MCP client 别名包装器。
*
* @param delegate 原始 MCP client
* @param rawToAlias 原始工具名到运行时工具名的映射
*/
AliasedMcpClientWrapper(McpClientWrapper delegate, Map<String, String> rawToAlias) {
this(delegate, rawToAlias, null);
}
/**
* 创建 MCP client 别名包装器。
*
* @param delegate 原始 MCP client
* @param rawToAlias 原始工具名到运行时工具名的映射
* @param toolNamePrefix 动态工具名前缀
*/
AliasedMcpClientWrapper(McpClientWrapper delegate, Map<String, String> rawToAlias, String toolNamePrefix) {
super(delegate == null ? "mcp" : delegate.getName());
this.delegate = delegate;
this.rawToAlias = rawToAlias == null ? Map.of() : new LinkedHashMap<>(rawToAlias);
this.aliasToRaw = new LinkedHashMap<>();
this.toolNamePrefix = toolNamePrefix == null || toolNamePrefix.isBlank() ? null : toolNamePrefix.trim();
this.rawToAlias.forEach((rawName, aliasName) -> {
if (rawName != null && aliasName != null && !aliasName.isBlank()) {
this.aliasToRaw.put(aliasName, rawName);
}
});
}
/**
* 初始化底层 MCP client。
*
* @return 初始化完成信号
*/
@Override
public Mono<Void> initialize() {
return delegate.initialize().doOnSuccess(ignored -> initialized = delegate.isInitialized());
}
/**
* 返回已替换为运行时别名的工具列表。
*
* @return 工具列表
*/
@Override
public Mono<List<McpSchema.Tool>> listTools() {
return delegate.listTools().map(this::aliasTools);
}
/**
* 调用 MCP 工具,运行时别名会映射回原始工具名。
*
* @param toolName 运行时工具名
* @param arguments 工具参数
* @return 工具调用结果
*/
@Override
public Mono<McpSchema.CallToolResult> callTool(String toolName, Map<String, Object> arguments) {
return delegate.callTool(rawToolName(toolName), arguments);
}
/**
* 关闭底层 MCP client。
*/
@Override
public void close() {
delegate.close();
initialized = false;
}
private List<McpSchema.Tool> aliasTools(List<McpSchema.Tool> tools) {
if (tools == null || tools.isEmpty()) {
cachedTools.clear();
return List.of();
}
List<McpSchema.Tool> aliased = new ArrayList<>();
cachedTools.clear();
Map<String, String> usedAliases = new LinkedHashMap<>();
for (McpSchema.Tool tool : tools) {
if (tool == null) {
continue;
}
McpSchema.Tool aliasTool = aliasTool(tool, usedAliases);
cachedTools.put(aliasTool.name(), aliasTool);
aliased.add(aliasTool);
}
return aliased;
}
private McpSchema.Tool aliasTool(McpSchema.Tool tool, Map<String, String> usedAliases) {
String aliasName = uniqueAliasName(aliasName(tool.name()), tool.name(), usedAliases);
if (aliasName == null || aliasName.isBlank() || aliasName.equals(tool.name())) {
return tool;
}
aliasToRaw.put(aliasName, tool.name());
Map<String, Object> meta = new LinkedHashMap<>();
if (tool.meta() != null) {
meta.putAll(tool.meta());
}
meta.put(RAW_TOOL_NAME_META_KEY, tool.name());
return new McpSchema.Tool(aliasName, tool.title(), tool.description(), tool.inputSchema(),
tool.outputSchema(), tool.annotations(), meta);
}
private String uniqueAliasName(String aliasName, String rawName, Map<String, String> usedAliases) {
if (aliasName == null || aliasName.isBlank()) {
return aliasName;
}
String existingRawName = usedAliases.get(aliasName);
if (existingRawName == null || existingRawName.equals(rawName)) {
usedAliases.put(aliasName, rawName);
return aliasName;
}
int suffix = 2;
String candidate = aliasName + "_" + suffix;
while (usedAliases.containsKey(candidate)) {
suffix++;
candidate = aliasName + "_" + suffix;
}
usedAliases.put(candidate, rawName);
return candidate;
}
private String aliasName(String rawName) {
String explicitAlias = rawToAlias.get(rawName);
if (explicitAlias != null && !explicitAlias.isBlank()) {
return explicitAlias;
}
if (toolNamePrefix == null) {
return rawName;
}
return toolNamePrefix + safeToolNameSegment(rawName);
}
private String safeToolNameSegment(String value) {
String normalized = String.valueOf(value == null ? "" : value).trim()
.replaceAll("[^A-Za-z0-9_-]", "_")
.replaceAll("_+", "_");
if (normalized.isBlank()) {
return "tool";
}
return normalized;
}
private String rawToolName(String toolName) {
return aliasToRaw.getOrDefault(toolName, toolName);
}
}

View File

@@ -0,0 +1,41 @@
package com.easyagents.agent.runtime.mcp;
import com.easyagents.agent.runtime.AgentRuntimeException;
import io.agentscope.core.tool.mcp.McpClientBuilder;
import io.agentscope.core.tool.mcp.McpClientWrapper;
import java.time.Duration;
/**
* 创建 AgentScope MCP client。
*/
public class McpClientFactory {
/**
* 根据 MCP 运行时声明创建 AgentScope MCP client。
*
* @param spec MCP 运行时声明
* @return AgentScope MCP client
*/
public McpClientWrapper create(McpSpec spec) {
McpSpecValidator.validateConnection(spec);
McpClientBuilder builder = McpClientBuilder.create(spec.getName())
.timeout(timeout(spec.getTimeout(), Duration.ofSeconds(120)))
.initializationTimeout(timeout(spec.getInitializationTimeout(), Duration.ofSeconds(30)));
switch (spec.getTransportType()) {
case STDIO -> builder.stdioTransport(spec.getCommand(), spec.getArgs(), spec.getEnv());
case SSE -> builder.sseTransport(spec.getUrl())
.headers(spec.getHeaders())
.queryParams(spec.getQueryParams());
case HTTP -> builder.streamableHttpTransport(spec.getUrl())
.headers(spec.getHeaders())
.queryParams(spec.getQueryParams());
default -> throw new AgentRuntimeException("Unsupported MCP transport type: " + spec.getTransportType());
}
return builder.buildAsync().block();
}
private Duration timeout(Duration value, Duration defaultValue) {
return value == null || value.isZero() || value.isNegative() ? defaultValue : value;
}
}

View File

@@ -0,0 +1,54 @@
package com.easyagents.agent.runtime.mcp;
import com.easyagents.agent.runtime.tool.AgentToolSpec;
import io.agentscope.core.tool.mcp.McpClientWrapper;
import java.util.ArrayList;
import java.util.List;
/**
* MCP 注册结果。
*/
public class McpRegistration {
private final List<McpClientWrapper> clients;
private final List<AgentToolSpec> toolSpecs;
/**
* 创建 MCP 注册结果。
*
* @param clients 已创建 MCP client
* @param toolSpecs 已注册工具声明
*/
public McpRegistration(List<McpClientWrapper> clients, List<AgentToolSpec> toolSpecs) {
this.clients = clients == null ? List.of() : new ArrayList<>(clients);
this.toolSpecs = toolSpecs == null ? List.of() : new ArrayList<>(toolSpecs);
}
/**
* 创建空注册结果。
*
* @return 空注册结果
*/
public static McpRegistration empty() {
return new McpRegistration(List.of(), List.of());
}
/**
* 获取已创建 MCP client。
*
* @return 已创建 MCP client
*/
public List<McpClientWrapper> getClients() {
return clients;
}
/**
* 获取已注册工具声明。
*
* @return 已注册工具声明
*/
public List<AgentToolSpec> getToolSpecs() {
return toolSpecs;
}
}

View File

@@ -0,0 +1,426 @@
package com.easyagents.agent.runtime.mcp;
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
import java.time.Duration;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* MCP 运行时声明。
*/
public class McpSpec {
private String name;
private String description;
private McpTransportType transportType = McpTransportType.STDIO;
private String command;
private List<String> args = new ArrayList<>();
private Map<String, String> env = new LinkedHashMap<>();
private String url;
private Map<String, String> headers = new LinkedHashMap<>();
private Map<String, String> queryParams = new LinkedHashMap<>();
private Duration timeout = Duration.ofSeconds(120);
private Duration initializationTimeout = Duration.ofSeconds(30);
private List<String> enableTools = new ArrayList<>();
private List<String> disableTools = new ArrayList<>();
private String groupName;
private Map<String, Map<String, Object>> presetParameters = new LinkedHashMap<>();
private Map<String, String> toolAliases = new LinkedHashMap<>();
private String toolNamePrefix;
private boolean approvalRequired;
private AgentToolApprovalRequest approvalRequest = new AgentToolApprovalRequest();
private Map<String, AgentToolApprovalRequest> toolApprovalRequests = new LinkedHashMap<>();
private Map<String, Object> metadata = new LinkedHashMap<>();
/**
* 获取 MCP client 名称。
*
* @return MCP client 名称
*/
public String getName() {
return name;
}
/**
* 设置 MCP client 名称。
*
* @param name MCP client 名称
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取 MCP 描述。
*
* @return MCP 描述
*/
public String getDescription() {
return description;
}
/**
* 设置 MCP 描述。
*
* @param description MCP 描述
*/
public void setDescription(String description) {
this.description = description;
}
/**
* 获取连接方式。
*
* @return 连接方式
*/
public McpTransportType getTransportType() {
return transportType;
}
/**
* 设置连接方式。
*
* @param transportType 连接方式
*/
public void setTransportType(McpTransportType transportType) {
this.transportType = transportType == null ? McpTransportType.STDIO : transportType;
}
/**
* 通过字符串设置连接方式。
*
* @param transportType 连接方式文本
*/
public void setTransportType(String transportType) {
this.transportType = McpTransportType.from(transportType);
}
/**
* 获取 stdio 命令。
*
* @return stdio 命令
*/
public String getCommand() {
return command;
}
/**
* 设置 stdio 命令。
*
* @param command stdio 命令
*/
public void setCommand(String command) {
this.command = command;
}
/**
* 获取 stdio 参数。
*
* @return stdio 参数
*/
public List<String> getArgs() {
return args;
}
/**
* 设置 stdio 参数。
*
* @param args stdio 参数
*/
public void setArgs(List<String> args) {
this.args = args == null ? new ArrayList<>() : new ArrayList<>(args);
}
/**
* 获取 stdio 环境变量。
*
* @return stdio 环境变量
*/
public Map<String, String> getEnv() {
return env;
}
/**
* 设置 stdio 环境变量。
*
* @param env stdio 环境变量
*/
public void setEnv(Map<String, String> env) {
this.env = env == null ? new LinkedHashMap<>() : new LinkedHashMap<>(env);
}
/**
* 获取 HTTP 地址。
*
* @return HTTP 地址
*/
public String getUrl() {
return url;
}
/**
* 设置 HTTP 地址。
*
* @param url HTTP 地址
*/
public void setUrl(String url) {
this.url = url;
}
/**
* 获取 HTTP 请求头。
*
* @return HTTP 请求头
*/
public Map<String, String> getHeaders() {
return headers;
}
/**
* 设置 HTTP 请求头。
*
* @param headers HTTP 请求头
*/
public void setHeaders(Map<String, String> headers) {
this.headers = headers == null ? new LinkedHashMap<>() : new LinkedHashMap<>(headers);
}
/**
* 获取 HTTP 查询参数。
*
* @return HTTP 查询参数
*/
public Map<String, String> getQueryParams() {
return queryParams;
}
/**
* 设置 HTTP 查询参数。
*
* @param queryParams HTTP 查询参数
*/
public void setQueryParams(Map<String, String> queryParams) {
this.queryParams = queryParams == null ? new LinkedHashMap<>() : new LinkedHashMap<>(queryParams);
}
/**
* 获取请求超时时间。
*
* @return 请求超时时间
*/
public Duration getTimeout() {
return timeout;
}
/**
* 设置请求超时时间。
*
* @param timeout 请求超时时间
*/
public void setTimeout(Duration timeout) {
this.timeout = timeout == null ? Duration.ofSeconds(120) : timeout;
}
/**
* 获取初始化超时时间。
*
* @return 初始化超时时间
*/
public Duration getInitializationTimeout() {
return initializationTimeout;
}
/**
* 设置初始化超时时间。
*
* @param initializationTimeout 初始化超时时间
*/
public void setInitializationTimeout(Duration initializationTimeout) {
this.initializationTimeout = initializationTimeout == null ? Duration.ofSeconds(30) : initializationTimeout;
}
/**
* 获取启用工具白名单。
*
* @return 启用工具白名单
*/
public List<String> getEnableTools() {
return enableTools;
}
/**
* 设置启用工具白名单。
*
* @param enableTools 启用工具白名单
*/
public void setEnableTools(List<String> enableTools) {
this.enableTools = enableTools == null ? new ArrayList<>() : new ArrayList<>(enableTools);
}
/**
* 获取禁用工具黑名单。
*
* @return 禁用工具黑名单
*/
public List<String> getDisableTools() {
return disableTools;
}
/**
* 设置禁用工具黑名单。
*
* @param disableTools 禁用工具黑名单
*/
public void setDisableTools(List<String> disableTools) {
this.disableTools = disableTools == null ? new ArrayList<>() : new ArrayList<>(disableTools);
}
/**
* 获取工具分组名。
*
* @return 工具分组名
*/
public String getGroupName() {
return groupName;
}
/**
* 设置工具分组名。
*
* @param groupName 工具分组名
*/
public void setGroupName(String groupName) {
this.groupName = groupName;
}
/**
* 获取预置参数。
*
* @return 预置参数
*/
public Map<String, Map<String, Object>> getPresetParameters() {
return presetParameters;
}
/**
* 设置预置参数。
*
* @param presetParameters 预置参数
*/
public void setPresetParameters(Map<String, Map<String, Object>> presetParameters) {
this.presetParameters = presetParameters == null ? new LinkedHashMap<>() : new LinkedHashMap<>(presetParameters);
}
/**
* 获取 MCP 原始工具名到运行时工具名的别名映射。
*
* @return 工具别名映射
*/
public Map<String, String> getToolAliases() {
return toolAliases;
}
/**
* 设置 MCP 原始工具名到运行时工具名的别名映射。
*
* @param toolAliases 工具别名映射
*/
public void setToolAliases(Map<String, String> toolAliases) {
this.toolAliases = toolAliases == null ? new LinkedHashMap<>() : new LinkedHashMap<>(toolAliases);
}
/**
* 获取动态工具名前缀。
*
* @return 动态工具名前缀
*/
public String getToolNamePrefix() {
return toolNamePrefix;
}
/**
* 设置动态工具名前缀。
*
* @param toolNamePrefix 动态工具名前缀
*/
public void setToolNamePrefix(String toolNamePrefix) {
this.toolNamePrefix = toolNamePrefix;
}
/**
* 返回 MCP 工具是否默认需要人工审批。
*
* @return 需要审批时为 true
*/
public boolean isApprovalRequired() {
return approvalRequired;
}
/**
* 设置 MCP 工具是否默认需要人工审批。
*
* @param approvalRequired 审批标记
*/
public void setApprovalRequired(boolean approvalRequired) {
this.approvalRequired = approvalRequired;
}
/**
* 获取审批请求。
*
* @return 审批请求
*/
public AgentToolApprovalRequest getApprovalRequest() {
return approvalRequest;
}
/**
* 设置审批请求。
*
* @param approvalRequest 审批请求
*/
public void setApprovalRequest(AgentToolApprovalRequest approvalRequest) {
this.approvalRequest = approvalRequest == null ? new AgentToolApprovalRequest() : approvalRequest;
}
/**
* 获取运行时工具名到审批请求的映射。
*
* @return 工具审批请求映射
*/
public Map<String, AgentToolApprovalRequest> getToolApprovalRequests() {
return toolApprovalRequests;
}
/**
* 设置运行时工具名到审批请求的映射。
*
* @param toolApprovalRequests 工具审批请求映射
*/
public void setToolApprovalRequests(Map<String, AgentToolApprovalRequest> toolApprovalRequests) {
this.toolApprovalRequests = toolApprovalRequests == null
? new LinkedHashMap<>()
: new LinkedHashMap<>(toolApprovalRequests);
}
/**
* 获取元数据。
*
* @return 元数据
*/
public Map<String, Object> getMetadata() {
return metadata;
}
/**
* 设置元数据。
*
* @param metadata 元数据
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata == null ? new LinkedHashMap<>() : new LinkedHashMap<>(metadata);
}
}

View File

@@ -0,0 +1,107 @@
package com.easyagents.agent.runtime.mcp;
import com.easyagents.agent.runtime.AgentRuntimeException;
import com.easyagents.agent.runtime.tool.AgentToolSpec;
import com.easyagents.agent.runtime.tool.operate.AgentOperateToolAdapter;
import com.easyagents.agent.runtime.tool.operate.AgentOperateToolSpec;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* MCP 声明校验器。
*/
public final class McpSpecValidator {
private McpSpecValidator() {
}
/**
* 校验 MCP 连接配置。
*
* @param spec MCP 运行时声明
*/
public static void validateConnection(McpSpec spec) {
if (spec == null) {
throw new AgentRuntimeException("MCP spec is required.");
}
if (spec.getName() == null || spec.getName().isBlank()) {
throw new AgentRuntimeException("MCP name is required.");
}
if (spec.getTransportType() == null) {
throw new AgentRuntimeException("MCP transport type is required: " + spec.getName());
}
validateToolAliases(spec);
switch (spec.getTransportType()) {
case STDIO -> {
if (spec.getCommand() == null || spec.getCommand().isBlank()) {
throw new AgentRuntimeException("MCP stdio command is required: " + spec.getName());
}
}
case SSE, HTTP -> {
if (spec.getUrl() == null || spec.getUrl().isBlank()) {
throw new AgentRuntimeException("MCP url is required: " + spec.getName());
}
}
default -> throw new AgentRuntimeException("Unsupported MCP transport type: " + spec.getTransportType());
}
}
private static void validateToolAliases(McpSpec spec) {
Map<String, String> aliases = spec.getToolAliases();
if (aliases == null || aliases.isEmpty()) {
return;
}
Set<String> runtimeNames = new HashSet<>();
for (Map.Entry<String, String> entry : aliases.entrySet()) {
String rawName = entry.getKey();
String runtimeName = entry.getValue();
if (rawName == null || rawName.isBlank()) {
throw new AgentRuntimeException("MCP raw tool name is required: " + spec.getName());
}
if (runtimeName == null || runtimeName.isBlank()) {
throw new AgentRuntimeException("MCP runtime tool name is required: " + spec.getName());
}
if (!runtimeNames.add(runtimeName)) {
throw new AgentRuntimeException("MCP runtime tool alias conflicts: " + runtimeName);
}
}
}
/**
* 校验 MCP 工具与既有工具名冲突。
*
* @param businessToolSpecs 普通工具声明
* @param mcpToolSpecs MCP 工具声明
* @param operateToolSpecs 操作工具声明
*/
public static void validateToolConflicts(List<AgentToolSpec> businessToolSpecs,
List<AgentToolSpec> mcpToolSpecs,
List<AgentOperateToolSpec> operateToolSpecs) {
Set<String> names = new HashSet<>();
addToolNames(names, businessToolSpecs, "Agent tool conflicts with existing tool: ");
addToolNames(names, mcpToolSpecs, "MCP tool conflicts with existing tool: ");
Set<String> operateToolNames = new AgentOperateToolAdapter().enabledToolNames(operateToolSpecs);
for (String operateToolName : operateToolNames) {
if (!names.add(operateToolName)) {
throw new AgentRuntimeException("Agent operate tool conflicts with existing tool: " + operateToolName);
}
}
}
private static void addToolNames(Set<String> names, List<AgentToolSpec> toolSpecs, String messagePrefix) {
if (toolSpecs == null || toolSpecs.isEmpty()) {
return;
}
for (AgentToolSpec toolSpec : toolSpecs) {
if (toolSpec == null || toolSpec.getName() == null || toolSpec.getName().isBlank()) {
continue;
}
if (!names.add(toolSpec.getName())) {
throw new AgentRuntimeException(messagePrefix + toolSpec.getName());
}
}
}
}

View File

@@ -0,0 +1,241 @@
package com.easyagents.agent.runtime.mcp;
import com.easyagents.agent.runtime.AgentRuntimeException;
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
import com.easyagents.agent.runtime.tool.AgentToolCategory;
import com.easyagents.agent.runtime.tool.AgentToolSpec;
import com.easyagents.agent.runtime.tool.AgentToolVisibility;
import io.agentscope.core.tool.Toolkit;
import io.agentscope.core.tool.mcp.McpClientWrapper;
import io.agentscope.core.tool.mcp.McpTool;
import io.modelcontextprotocol.spec.McpSchema;
import java.util.*;
import java.util.Locale;
/**
* 将 MCP 运行时声明注册到 AgentScope Toolkit。
*/
public class McpToolkitAdapter {
private final McpClientFactory clientFactory;
/**
* 使用默认 MCP client factory 创建适配器。
*/
public McpToolkitAdapter() {
this(new McpClientFactory());
}
/**
* 使用指定 MCP client factory 创建适配器。
*
* @param clientFactory MCP client factory
*/
public McpToolkitAdapter(McpClientFactory clientFactory) {
this.clientFactory = clientFactory == null ? new McpClientFactory() : clientFactory;
}
/**
* 将 MCP 声明注册到 Toolkit。
*
* @param specs MCP 声明
* @param toolkit AgentScope Toolkit
* @return MCP 注册结果
*/
public McpRegistration register(List<McpSpec> specs, Toolkit toolkit) {
if (specs == null || specs.isEmpty()) {
return McpRegistration.empty();
}
if (toolkit == null) {
throw new AgentRuntimeException("AgentScope toolkit is required for MCP registration.");
}
List<McpClientWrapper> clients = new ArrayList<>();
List<AgentToolSpec> toolSpecs = new ArrayList<>();
try {
for (McpSpec spec : specs) {
if (spec == null) {
continue;
}
McpSpecValidator.validateConnection(spec);
McpClientWrapper client = clientFactory.create(spec);
client = applyAliases(spec, client);
clients.add(client);
registerClient(spec, client, toolkit);
toolSpecs.addAll(toToolSpecs(spec, registeredTools(spec, client)));
}
} catch (RuntimeException error) {
closeQuietly(clients);
throw error;
}
return new McpRegistration(clients, toolSpecs);
}
private McpClientWrapper applyAliases(McpSpec spec, McpClientWrapper client) {
boolean hasExplicitAliases = spec.getToolAliases() != null && !spec.getToolAliases().isEmpty();
boolean hasDynamicPrefix = spec.getToolNamePrefix() != null && !spec.getToolNamePrefix().isBlank();
if (!hasExplicitAliases && !hasDynamicPrefix) {
return client;
}
return new AliasedMcpClientWrapper(client, spec.getToolAliases(), spec.getToolNamePrefix());
}
private void registerClient(McpSpec spec, McpClientWrapper client, Toolkit toolkit) {
String groupName = blankToNull(spec.getGroupName());
if (groupName != null && toolkit.getToolGroup(groupName) == null) {
toolkit.createToolGroup(groupName, spec.getDescription(), true);
}
toolkit.registration()
.mcpClient(client)
.enableTools(emptyToNull(spec.getEnableTools()))
.disableTools(emptyToNull(spec.getDisableTools()))
.group(groupName)
.presetParameters(emptyToNull(spec.getPresetParameters()))
.apply();
}
private List<McpSchema.Tool> registeredTools(McpSpec spec, McpClientWrapper client) {
List<McpSchema.Tool> tools = client.listTools().block();
if (tools == null || tools.isEmpty()) {
return List.of();
}
List<McpSchema.Tool> filtered = new ArrayList<>();
for (McpSchema.Tool tool : tools) {
if (tool != null && shouldRegister(tool.name(), spec.getEnableTools(), spec.getDisableTools())) {
filtered.add(tool);
}
}
return filtered;
}
private boolean shouldRegister(String toolName, List<String> enableTools, List<String> disableTools) {
if (enableTools != null && !enableTools.isEmpty()) {
return enableTools.contains(toolName);
}
return disableTools == null || disableTools.isEmpty() || !disableTools.contains(toolName);
}
private List<AgentToolSpec> toToolSpecs(McpSpec spec, List<McpSchema.Tool> tools) {
if (tools == null || tools.isEmpty()) {
return List.of();
}
List<AgentToolSpec> toolSpecs = new ArrayList<>();
for (McpSchema.Tool tool : tools) {
AgentToolSpec toolSpec = new AgentToolSpec();
Set<String> excludedPresetNames = Set.of();
Map<String, Object> toolPresetParameters = spec.getPresetParameters() == null
? null
: spec.getPresetParameters().get(tool.name());
if (toolPresetParameters != null) {
excludedPresetNames = toolPresetParameters.keySet();
}
toolSpec.setName(tool.name());
toolSpec.setDescription(tool.description());
toolSpec.setCategory(AgentToolCategory.MCP);
toolSpec.setVisibility(AgentToolVisibility.VISIBLE);
toolSpec.setParametersSchema(McpTool.convertMcpSchemaToParameters(tool.inputSchema(), excludedPresetNames));
toolSpec.setOutputSchema(tool.outputSchema());
AgentToolApprovalRequest toolApprovalRequest = toolApprovalRequest(spec, tool.name());
toolSpec.setApprovalRequired(spec.isApprovalRequired() || toolApprovalRequest != null);
toolSpec.setApprovalRequest(toolApprovalRequest == null ? spec.getApprovalRequest() : toolApprovalRequest);
toolSpec.setMetadata(metadata(spec, tool));
toolSpecs.add(toolSpec);
}
return toolSpecs;
}
private AgentToolApprovalRequest toolApprovalRequest(McpSpec spec, String toolName) {
if (spec.getToolApprovalRequests() == null || spec.getToolApprovalRequests().isEmpty()) {
return null;
}
return spec.getToolApprovalRequests().get(toolName);
}
private Map<String, Object> metadata(McpSpec spec, McpSchema.Tool tool) {
Map<String, Object> metadata = new LinkedHashMap<>();
if (spec.getMetadata() != null) {
spec.getMetadata().forEach((key, value) -> {
if (!isSensitiveMetadataKey(key)) {
metadata.put(key, value);
}
});
}
metadata.put("source", "MCP");
metadata.put("mcpName", spec.getName());
metadata.put("mcpToolName", tool.name());
metadata.put("rawMcpToolName", rawToolName(spec, tool));
metadata.put("toolDisplayName", toolDisplayName(spec, tool));
metadata.put("transportType", spec.getTransportType().configValue());
return metadata;
}
private String rawToolName(McpSpec spec, McpSchema.Tool tool) {
if (tool != null && tool.meta() != null) {
Object rawName = tool.meta().get(AliasedMcpClientWrapper.RAW_TOOL_NAME_META_KEY);
if (rawName != null && !String.valueOf(rawName).isBlank()) {
return String.valueOf(rawName);
}
}
String toolName = tool == null ? null : tool.name();
if (spec.getToolAliases() != null && !spec.getToolAliases().isEmpty()) {
for (Map.Entry<String, String> entry : spec.getToolAliases().entrySet()) {
if (Objects.equals(entry.getValue(), toolName)) {
return entry.getKey();
}
}
}
return toolName;
}
private String toolDisplayName(McpSpec spec, McpSchema.Tool tool) {
String rawToolName = rawToolName(spec, tool);
String mcpName = spec == null ? null : spec.getDescription();
if (mcpName == null || mcpName.isBlank()) {
mcpName = spec == null ? null : spec.getName();
}
if (mcpName == null || mcpName.isBlank()) {
return rawToolName;
}
if (rawToolName == null || rawToolName.isBlank()) {
return mcpName;
}
return mcpName + " - " + rawToolName;
}
private boolean isSensitiveMetadataKey(String key) {
if (key == null || key.isBlank()) {
return false;
}
String normalized = key.toLowerCase(Locale.ROOT).replace("-", "").replace("_", "");
return normalized.contains("key")
|| normalized.contains("token")
|| normalized.contains("secret")
|| normalized.contains("password")
|| normalized.contains("authorization")
|| normalized.contains("credential");
}
private void closeQuietly(List<McpClientWrapper> clients) {
for (McpClientWrapper client : clients) {
if (client == null) {
continue;
}
try {
client.close();
} catch (Exception ignored) {
}
}
}
private <T> List<T> emptyToNull(List<T> values) {
return values == null || values.isEmpty() ? null : values;
}
private <K, V> Map<K, V> emptyToNull(Map<K, V> values) {
return values == null || values.isEmpty() ? null : values;
}
private String blankToNull(String value) {
return value == null || value.isBlank() ? null : value;
}
}

View File

@@ -0,0 +1,58 @@
package com.easyagents.agent.runtime.mcp;
import com.easyagents.agent.runtime.AgentRuntimeException;
import java.util.Locale;
/**
* MCP 连接方式。
*/
public enum McpTransportType {
/**
* 标准输入输出进程通信。
*/
STDIO,
/**
* HTTP Server-Sent Events 通信。
*/
SSE,
/**
* Streamable HTTP 通信。
*/
HTTP;
/**
* 解析 MCP 连接方式。
*
* @param value 连接方式文本
* @return MCP 连接方式
*/
public static McpTransportType from(String value) {
if (value == null || value.isBlank()) {
return STDIO;
}
String normalized = value.trim().toLowerCase(Locale.ROOT);
return switch (normalized) {
case "stdio" -> STDIO;
case "sse", "http-sse" -> SSE;
case "http", "streamable-http", "http-stream" -> HTTP;
default -> throw new AgentRuntimeException("Unsupported MCP transport type: " + value);
};
}
/**
* 转换为 Easy MCP 配置兼容值。
*
* @return transport 配置值
*/
public String configValue() {
return switch (this) {
case STDIO -> "stdio";
case SSE -> "http-sse";
case HTTP -> "http-stream";
};
}
}

View File

@@ -0,0 +1,179 @@
package com.easyagents.agent.runtime.memory;
/**
* AutoContext 自动压缩参数。
*/
public class AgentMemoryCompressionParameter {
private boolean enabled = true;
private Integer msgThreshold;
private int lastKeep = 8;
private Double tokenRatio;
private Long maxToken;
private long largePayloadThreshold = 2048L;
private Integer minCompressionTokenThreshold;
private double currentRoundCompressionRatio = 0.5D;
private int minConsecutiveToolMessages = 4;
/**
* 判断是否启用自动压缩。
*
* @return 启用时返回 true
*/
public boolean isEnabled() {
return enabled;
}
/**
* 设置压缩开关。
*
* @param enabled 压缩开关
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* 获取消息阈值。
*
* @return 消息阈值
*/
public Integer getMsgThreshold() {
return msgThreshold;
}
/**
* 设置消息阈值。
*
* @param msgThreshold 消息阈值
*/
public void setMsgThreshold(int msgThreshold) {
this.msgThreshold = msgThreshold <= 0 ? null : msgThreshold;
}
/**
* 获取最近保留消息数。
*
* @return 最近保留消息数
*/
public int getLastKeep() {
return lastKeep;
}
/**
* 设置最近保留消息数。
*
* @param lastKeep 最近保留消息数
*/
public void setLastKeep(int lastKeep) {
this.lastKeep = Math.max(0, lastKeep);
}
/**
* 获取Token 比例。
*
* @return Token 比例
*/
public Double getTokenRatio() {
return tokenRatio;
}
/**
* 设置Token 比例。
*
* @param tokenRatio Token 比例
*/
public void setTokenRatio(double tokenRatio) {
this.tokenRatio = tokenRatio <= 0D ? null : tokenRatio;
}
/**
* 获取最大 Token 数。
*
* @return 最大 Token 数
*/
public Long getMaxToken() {
return maxToken;
}
/**
* 设置最大 Token 数。
*
* @param maxToken 最大 Token 数
*/
public void setMaxToken(long maxToken) {
this.maxToken = maxToken <= 0L ? null : maxToken;
}
/**
* 获取大负载阈值。
*
* @return 大负载阈值
*/
public long getLargePayloadThreshold() {
return largePayloadThreshold;
}
/**
* 设置大负载阈值。
*
* @param largePayloadThreshold 大负载阈值
*/
public void setLargePayloadThreshold(long largePayloadThreshold) {
this.largePayloadThreshold = largePayloadThreshold <= 0L ? 2048L : largePayloadThreshold;
}
/**
* 获取最小压缩 Token 阈值。
*
* @return 最小压缩 Token 阈值
*/
public Integer getMinCompressionTokenThreshold() {
return minCompressionTokenThreshold;
}
/**
* 设置最小压缩 Token 阈值。
*
* @param minCompressionTokenThreshold 最小压缩 Token 阈值
*/
public void setMinCompressionTokenThreshold(int minCompressionTokenThreshold) {
this.minCompressionTokenThreshold = minCompressionTokenThreshold <= 0 ? null : minCompressionTokenThreshold;
}
/**
* 获取当前轮压缩比例。
*
* @return 当前轮压缩比例
*/
public double getCurrentRoundCompressionRatio() {
return currentRoundCompressionRatio;
}
/**
* 设置当前轮压缩比例。
*
* @param currentRoundCompressionRatio 当前轮压缩比例
*/
public void setCurrentRoundCompressionRatio(double currentRoundCompressionRatio) {
this.currentRoundCompressionRatio = currentRoundCompressionRatio <= 0D ? 0.5D : currentRoundCompressionRatio;
}
/**
* 获取最小连续工具消息数。
*
* @return 最小连续工具消息数
*/
public int getMinConsecutiveToolMessages() {
return minConsecutiveToolMessages;
}
/**
* 设置最小连续工具消息数。
*
* @param minConsecutiveToolMessages 最小连续工具消息数
*/
public void setMinConsecutiveToolMessages(int minConsecutiveToolMessages) {
this.minConsecutiveToolMessages = Math.max(0, minConsecutiveToolMessages);
}
}

View File

@@ -0,0 +1,89 @@
package com.easyagents.agent.runtime.memory;
/**
* 运行时记忆策略。
*/
public class AgentMemoryPolicy {
private AgentMemoryType type = AgentMemoryType.AUTO_CONTEXT;
private int maxAttachedMessageCount = 50;
private AgentMemoryCompressionParameter compressionParameter = new AgentMemoryCompressionParameter();
/**
* 创建默认 AutoContext 记忆策略。
*
* @return AutoContext 记忆策略
*/
public static AgentMemoryPolicy autoContext() {
AgentMemoryPolicy policy = new AgentMemoryPolicy();
policy.setType(AgentMemoryType.AUTO_CONTEXT);
return policy;
}
/**
* 创建内存策略。
*
* @return 内存策略
*/
public static AgentMemoryPolicy inMemory() {
AgentMemoryPolicy policy = new AgentMemoryPolicy();
policy.setType(AgentMemoryType.IN_MEMORY);
return policy;
}
/**
* 获取记忆类型。
*
* @return 记忆类型
*/
public AgentMemoryType getType() {
return type;
}
/**
* 设置记忆类型。
*
* @param type 记忆类型
*/
public void setType(AgentMemoryType type) {
this.type = type == null ? AgentMemoryType.AUTO_CONTEXT : type;
}
/**
* 获取最大附加消息数。
*
* @return 最大附加消息数
*/
public int getMaxAttachedMessageCount() {
return maxAttachedMessageCount;
}
/**
* 设置最大附加消息数。
*
* @param maxAttachedMessageCount 最大附加消息数
*/
public void setMaxAttachedMessageCount(int maxAttachedMessageCount) {
this.maxAttachedMessageCount = maxAttachedMessageCount <= 0 ? 50 : maxAttachedMessageCount;
}
/**
* 获取自动压缩参数。
*
* @return 自动压缩参数
*/
public AgentMemoryCompressionParameter getCompressionParameter() {
return compressionParameter;
}
/**
* 设置自动压缩参数。
*
* @param compressionParameter 自动压缩参数
*/
public void setCompressionParameter(AgentMemoryCompressionParameter compressionParameter) {
this.compressionParameter = compressionParameter == null
? new AgentMemoryCompressionParameter()
: compressionParameter;
}
}

View File

@@ -0,0 +1,43 @@
package com.easyagents.agent.runtime.memory;
import com.easyagents.agent.runtime.message.AgentMessage;
import java.util.ArrayList;
import java.util.List;
/**
* 由调用方恢复的会话历史快照。
*/
public class AgentMemorySnapshot {
private List<AgentMessage> messages = new ArrayList<>();
/**
* 获取消息列表。
*
* @return 消息列表
*/
public List<AgentMessage> getMessages() {
return messages;
}
/**
* 设置消息列表。
*
* @param messages 消息列表
*/
public void setMessages(List<AgentMessage> messages) {
this.messages = messages == null ? new ArrayList<>() : messages;
}
/**
* 添加消息。
*
* @param message 消息
*/
public void addMessage(AgentMessage message) {
if (message != null) {
messages.add(message);
}
}
}

View File

@@ -0,0 +1,9 @@
package com.easyagents.agent.runtime.memory;
/**
* 记忆实现类型。
*/
public enum AgentMemoryType {
IN_MEMORY,
AUTO_CONTEXT
}

View File

@@ -0,0 +1,11 @@
package com.easyagents.agent.runtime.memory;
/**
* 中立消息角色。
*/
public enum AgentMessageRole {
SYSTEM,
USER,
ASSISTANT,
TOOL
}

View File

@@ -0,0 +1,40 @@
package com.easyagents.agent.runtime.message;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 运行时消息内容块。
*/
public abstract class AgentContentBlock {
private final AgentContentBlockType type;
private final Map<String, Object> metadata = new LinkedHashMap<>();
/**
* 创建内容块。
*
* @param type 块类型
*/
protected AgentContentBlock(AgentContentBlockType type) {
this.type = type == null ? AgentContentBlockType.UNKNOWN : type;
}
/**
* 获取块类型。
*
* @return 块类型
*/
public AgentContentBlockType getType() {
return type;
}
/**
* 获取元数据。
*
* @return 元数据
*/
public Map<String, Object> getMetadata() {
return metadata;
}
}

View File

@@ -0,0 +1,15 @@
package com.easyagents.agent.runtime.message;
/**
* 消息块类型。
*/
public enum AgentContentBlockType {
TEXT,
THINKING,
TOOL_USE,
TOOL_RESULT,
IMAGE,
AUDIO,
VIDEO,
UNKNOWN
}

View File

@@ -0,0 +1,182 @@
package com.easyagents.agent.runtime.message;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 知识库引用信息。
*/
public class AgentKnowledgeReference {
private String knowledgeId;
private String knowledgeName;
private String documentId;
private String documentName;
private String chunkId;
private String chunkContent;
private String sourceUri;
private Double score;
private Map<String, Object> metadata = new LinkedHashMap<>();
/**
* 获取知识库ID。
*
* @return 知识库ID
*/
public String getKnowledgeId() {
return knowledgeId;
}
/**
* 设置知识库ID。
*
* @param knowledgeId 知识库ID
*/
public void setKnowledgeId(String knowledgeId) {
this.knowledgeId = knowledgeId;
}
/**
* 获取知识库名称。
*
* @return 知识库名称
*/
public String getKnowledgeName() {
return knowledgeName;
}
/**
* 设置知识库名称。
*
* @param knowledgeName 知识库名称
*/
public void setKnowledgeName(String knowledgeName) {
this.knowledgeName = knowledgeName;
}
/**
* 获取文档ID。
*
* @return 文档ID
*/
public String getDocumentId() {
return documentId;
}
/**
* 设置文档ID。
*
* @param documentId 文档ID
*/
public void setDocumentId(String documentId) {
this.documentId = documentId;
}
/**
* 获取文档名称。
*
* @return 文档名称
*/
public String getDocumentName() {
return documentName;
}
/**
* 设置文档名称。
*
* @param documentName 文档名称
*/
public void setDocumentName(String documentName) {
this.documentName = documentName;
}
/**
* 获取分片ID。
*
* @return 分片ID
*/
public String getChunkId() {
return chunkId;
}
/**
* 设置分片ID。
*
* @param chunkId 分片ID
*/
public void setChunkId(String chunkId) {
this.chunkId = chunkId;
}
/**
* 获取命中分片原文。
*
* @return 命中分片原文
*/
public String getChunkContent() {
return chunkContent;
}
/**
* 设置命中分片原文。
*
* @param chunkContent 命中分片原文
*/
public void setChunkContent(String chunkContent) {
this.chunkContent = chunkContent;
}
/**
* 获取来源 URI。
*
* @return 来源 URI
*/
public String getSourceUri() {
return sourceUri;
}
/**
* 设置来源 URI。
*
* @param sourceUri 来源 URI
*/
public void setSourceUri(String sourceUri) {
this.sourceUri = sourceUri;
}
/**
* 获取分数。
*
* @return 分数
*/
public Double getScore() {
return score;
}
/**
* 设置分数。
*
* @param score 分数
*/
public void setScore(Double score) {
this.score = score;
}
/**
* 获取元数据。
*
* @return 元数据
*/
public Map<String, Object> getMetadata() {
return metadata;
}
/**
* 设置元数据。
*
* @param metadata 元数据
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
}
}

View File

@@ -0,0 +1,221 @@
package com.easyagents.agent.runtime.message;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 媒体内容块。
*/
public class AgentMediaBlock extends AgentContentBlock {
private String mimeType;
private String url;
private String data;
private Integer minPixels;
private Integer maxPixels;
private Float fps;
private Integer maxFrames;
private Integer totalPixels;
private String mediaKind;
/**
* 创建媒体块。
*
* @param mediaKind 媒体类型
*/
public AgentMediaBlock(String mediaKind) {
super(resolveType(mediaKind));
this.mediaKind = mediaKind;
}
/**
* 获取媒体类型。
*
* @return 媒体类型
*/
public String getMediaKind() {
return mediaKind;
}
/**
* 设置媒体类型。
*
* @param mediaKind 媒体类型
*/
public void setMediaKind(String mediaKind) {
this.mediaKind = mediaKind;
}
/**
* 获取 MIME 类型。
*
* @return MIME 类型
*/
public String getMimeType() {
return mimeType;
}
/**
* 设置 MIME 类型。
*
* @param mimeType MIME 类型
*/
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
/**
* 获取 URL。
*
* @return URL
*/
public String getUrl() {
return url;
}
/**
* 设置 URL。
*
* @param url URL
*/
public void setUrl(String url) {
this.url = url;
}
/**
* 获取 Base64 数据。
*
* @return Base64 数据
*/
public String getData() {
return data;
}
/**
* 设置 Base64 数据。
*
* @param data Base64 数据
*/
public void setData(String data) {
this.data = data;
}
/**
* 获取最小像素。
*
* @return 最小像素
*/
public Integer getMinPixels() {
return minPixels;
}
/**
* 设置最小像素。
*
* @param minPixels 最小像素
*/
public void setMinPixels(Integer minPixels) {
this.minPixels = minPixels;
}
/**
* 获取最大像素。
*
* @return 最大像素
*/
public Integer getMaxPixels() {
return maxPixels;
}
/**
* 设置最大像素。
*
* @param maxPixels 最大像素
*/
public void setMaxPixels(Integer maxPixels) {
this.maxPixels = maxPixels;
}
/**
* 获取帧率。
*
* @return 帧率
*/
public Float getFps() {
return fps;
}
/**
* 设置帧率。
*
* @param fps 帧率
*/
public void setFps(Float fps) {
this.fps = fps;
}
/**
* 获取最大帧数。
*
* @return 最大帧数
*/
public Integer getMaxFrames() {
return maxFrames;
}
/**
* 设置最大帧数。
*
* @param maxFrames 最大帧数
*/
public void setMaxFrames(Integer maxFrames) {
this.maxFrames = maxFrames;
}
/**
* 获取总像素。
*
* @return 总像素
*/
public Integer getTotalPixels() {
return totalPixels;
}
/**
* 设置总像素。
*
* @param totalPixels 总像素
*/
public void setTotalPixels(Integer totalPixels) {
this.totalPixels = totalPixels;
}
private static AgentContentBlockType resolveType(String mediaKind) {
if ("audio".equalsIgnoreCase(mediaKind)) {
return AgentContentBlockType.AUDIO;
}
if ("video".equalsIgnoreCase(mediaKind)) {
return AgentContentBlockType.VIDEO;
}
return AgentContentBlockType.IMAGE;
}
/**
* 额外元数据。
*
* @return 额外元数据
*/
public Map<String, Object> toMetadata() {
Map<String, Object> metadata = new LinkedHashMap<>(getMetadata());
metadata.put("mediaKind", mediaKind);
metadata.put("mimeType", mimeType);
metadata.put("url", url);
metadata.put("data", data);
metadata.put("minPixels", minPixels);
metadata.put("maxPixels", maxPixels);
metadata.put("fps", fps);
metadata.put("maxFrames", maxFrames);
metadata.put("totalPixels", totalPixels);
return metadata;
}
}

View File

@@ -0,0 +1,173 @@
package com.easyagents.agent.runtime.message;
import java.time.Instant;
import java.util.*;
/**
* 智能体聊天消息
*/
public class AgentMessage {
private String messageId = UUID.randomUUID().toString();
private String name;
/**
* 聊天发起者角色
*/
private AgentMessageRole role = AgentMessageRole.USER;
/**
* 消息内容块
*/
private List<AgentContentBlock> contentBlocks = new ArrayList<>();
/**
* 知识库引用
*/
private List<AgentKnowledgeReference> knowledgeReferences = new ArrayList<>();
/**
* 元数据
*/
private Map<String, Object> metadata = new LinkedHashMap<>();
/**
* 创建时间
*/
private Instant createdAt = Instant.now();
/**
* 创建纯文本消息。
*
* @param role 消息角色
* @param content 文本内容
* @return 消息
*/
public static AgentMessage text(AgentMessageRole role, String content) {
AgentMessage message = new AgentMessage();
message.setRole(role);
message.setContentBlocks(List.of(new AgentTextBlock(content)));
return message;
}
/**
* 获取消息ID。
*
* @return 消息ID
*/
public String getMessageId() {
return messageId;
}
/**
* 设置消息ID。
*
* @param messageId 消息ID
*/
public void setMessageId(String messageId) {
this.messageId = messageId;
}
/**
* 获取消息名称。
*
* @return 消息名称
*/
public String getName() {
return name;
}
/**
* 设置消息名称。
*
* @param name 消息名称
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取角色。
*
* @return 角色
*/
public AgentMessageRole getRole() {
return role;
}
/**
* 设置角色。
*
* @param role 角色
*/
public void setRole(AgentMessageRole role) {
this.role = role == null ? AgentMessageRole.USER : role;
}
/**
* 获取内容块列表。
*
* @return 内容块列表
*/
public List<AgentContentBlock> getContentBlocks() {
return contentBlocks;
}
/**
* 设置内容块列表。
*
* @param contentBlocks 内容块列表
*/
public void setContentBlocks(List<AgentContentBlock> contentBlocks) {
this.contentBlocks = contentBlocks == null ? new ArrayList<>() : new ArrayList<>(contentBlocks);
}
/**
* 获取知识库引用。
*
* @return 知识库引用
*/
public List<AgentKnowledgeReference> getKnowledgeReferences() {
return knowledgeReferences;
}
/**
* 设置知识库引用。
*
* @param knowledgeReferences 知识库引用
*/
public void setKnowledgeReferences(List<AgentKnowledgeReference> knowledgeReferences) {
this.knowledgeReferences = knowledgeReferences == null ? new ArrayList<>() : new ArrayList<>(knowledgeReferences);
}
/**
* 获取元数据。
*
* @return 元数据
*/
public Map<String, Object> getMetadata() {
return metadata;
}
/**
* 设置元数据。
*
* @param metadata 元数据
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata == null ? new LinkedHashMap<>() : new LinkedHashMap<>(metadata);
}
/**
* 获取创建时间。
*
* @return 创建时间
*/
public Instant getCreatedAt() {
return createdAt;
}
/**
* 设置创建时间。
*
* @param createdAt 创建时间
*/
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt == null ? Instant.now() : createdAt;
}
}

View File

@@ -0,0 +1,11 @@
package com.easyagents.agent.runtime.message;
/**
* 中立消息角色。
*/
public enum AgentMessageRole {
SYSTEM,
USER,
ASSISTANT,
TOOL
}

View File

@@ -0,0 +1,37 @@
package com.easyagents.agent.runtime.message;
/**
* 文本内容块。
*/
public class AgentTextBlock extends AgentContentBlock {
private String text;
/**
* 创建文本块。
*
* @param text 文本内容
*/
public AgentTextBlock(String text) {
super(AgentContentBlockType.TEXT);
this.text = text;
}
/**
* 获取文本。
*
* @return 文本
*/
public String getText() {
return text;
}
/**
* 设置文本。
*
* @param text 文本
*/
public void setText(String text) {
this.text = text;
}
}

View File

@@ -0,0 +1,37 @@
package com.easyagents.agent.runtime.message;
/**
* 思考内容块。
*/
public class AgentThinkingBlock extends AgentContentBlock {
private String thinking;
/**
* 创建思考块。
*
* @param thinking 思考内容
*/
public AgentThinkingBlock(String thinking) {
super(AgentContentBlockType.THINKING);
this.thinking = thinking;
}
/**
* 获取思考内容。
*
* @return 思考内容
*/
public String getThinking() {
return thinking;
}
/**
* 设置思考内容。
*
* @param thinking 思考内容
*/
public void setThinking(String thinking) {
this.thinking = thinking;
}
}

View File

@@ -0,0 +1,99 @@
package com.easyagents.agent.runtime.message;
import java.util.ArrayList;
import java.util.List;
/**
* 工具结果内容块。
*/
public class AgentToolResultBlock extends AgentContentBlock {
private String id;
private String name;
private List<AgentContentBlock> output = new ArrayList<>();
private boolean suspended;
/**
* 创建工具结果块。
*
* @param id 调用ID
* @param name 工具名
*/
public AgentToolResultBlock(String id, String name) {
super(AgentContentBlockType.TOOL_RESULT);
this.id = id;
this.name = name;
}
/**
* 获取调用ID。
*
* @return 调用ID
*/
public String getId() {
return id;
}
/**
* 设置调用ID。
*
* @param id 调用ID
*/
public void setId(String id) {
this.id = id;
}
/**
* 获取工具名。
*
* @return 工具名
*/
public String getName() {
return name;
}
/**
* 设置工具名。
*
* @param name 工具名
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取输出块列表。
*
* @return 输出块列表
*/
public List<AgentContentBlock> getOutput() {
return output;
}
/**
* 设置输出块列表。
*
* @param output 输出块列表
*/
public void setOutput(List<AgentContentBlock> output) {
this.output = output == null ? new ArrayList<>() : new ArrayList<>(output);
}
/**
* 是否挂起。
*
* @return 是否挂起
*/
public boolean isSuspended() {
return suspended;
}
/**
* 设置挂起状态。
*
* @param suspended 挂起状态
*/
public void setSuspended(boolean suspended) {
this.suspended = suspended;
}
}

View File

@@ -0,0 +1,101 @@
package com.easyagents.agent.runtime.message;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 工具调用内容块。
*/
public class AgentToolUseBlock extends AgentContentBlock {
private String id;
private String name;
private Map<String, Object> input = new LinkedHashMap<>();
private String content;
/**
* 创建工具调用块。
*
* @param id 调用ID
* @param name 工具名
* @param input 输入参数
*/
public AgentToolUseBlock(String id, String name, Map<String, Object> input) {
super(AgentContentBlockType.TOOL_USE);
this.id = id;
this.name = name;
setInput(input);
}
/**
* 获取调用ID。
*
* @return 调用ID
*/
public String getId() {
return id;
}
/**
* 设置调用ID。
*
* @param id 调用ID
*/
public void setId(String id) {
this.id = id;
}
/**
* 获取工具名。
*
* @return 工具名
*/
public String getName() {
return name;
}
/**
* 设置工具名。
*
* @param name 工具名
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取输入参数。
*
* @return 输入参数
*/
public Map<String, Object> getInput() {
return input;
}
/**
* 设置输入参数。
*
* @param input 输入参数
*/
public void setInput(Map<String, Object> input) {
this.input = input == null ? new LinkedHashMap<>() : new LinkedHashMap<>(input);
}
/**
* 获取原始内容。
*
* @return 原始内容
*/
public String getContent() {
return content;
}
/**
* 设置原始内容。
*
* @param content 原始内容
*/
public void setContent(String content) {
this.content = content;
}
}

View File

@@ -0,0 +1,95 @@
package com.easyagents.agent.runtime.message;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 未识别的内容消息块
* 兜底表示。
*/
public class AgentUnknownBlock extends AgentContentBlock {
private String sourceClassName;
private String sourceTypeName;
private String rawText;
private Map<String, Object> rawMetadata = new LinkedHashMap<>();
/**
* 创建未知内容块。
*/
public AgentUnknownBlock() {
super(AgentContentBlockType.UNKNOWN);
}
/**
* 获取来源类名。
*
* @return 来源类名
*/
public String getSourceClassName() {
return sourceClassName;
}
/**
* 设置来源类名。
*
* @param sourceClassName 来源类名
*/
public void setSourceClassName(String sourceClassName) {
this.sourceClassName = sourceClassName;
}
/**
* 获取来源类型名。
*
* @return 来源类型名
*/
public String getSourceTypeName() {
return sourceTypeName;
}
/**
* 设置来源类型名。
*
* @param sourceTypeName 来源类型名
*/
public void setSourceTypeName(String sourceTypeName) {
this.sourceTypeName = sourceTypeName;
}
/**
* 获取原始文本表示。
*
* @return 原始文本表示
*/
public String getRawText() {
return rawText;
}
/**
* 设置原始文本表示。
*
* @param rawText 原始文本表示
*/
public void setRawText(String rawText) {
this.rawText = rawText;
}
/**
* 获取原始元数据。
*
* @return 原始元数据
*/
public Map<String, Object> getRawMetadata() {
return rawMetadata;
}
/**
* 设置原始元数据。
*
* @param rawMetadata 原始元数据
*/
public void setRawMetadata(Map<String, Object> rawMetadata) {
this.rawMetadata = rawMetadata == null ? new LinkedHashMap<>() : new LinkedHashMap<>(rawMetadata);
}
}

View File

@@ -0,0 +1,239 @@
package com.easyagents.agent.runtime.model;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 运行时模型适配器使用的生成参数。
*/
public class AgentGenerationOptions {
private Double temperature;
private Double topP;
private Integer topK;
private Integer maxTokens;
private Integer maxCompletionTokens;
private Integer thinkingBudget;
private String reasoningEffort;
private Boolean thinkingEnabled;
private Boolean stream = true;
private Map<String, Object> additionalBodyParams = new LinkedHashMap<>();
private Map<String, String> additionalHeaders = new LinkedHashMap<>();
private Map<String, String> additionalQueryParams = new LinkedHashMap<>();
/**
* 获取temperature。
*
* @return temperature 参数
*/
public Double getTemperature() {
return temperature;
}
/**
* 设置temperature。
*
* @param temperature temperature 参数
*/
public void setTemperature(Double temperature) {
this.temperature = temperature;
}
/**
* 获取top-p。
*
* @return top-p 参数
*/
public Double getTopP() {
return topP;
}
/**
* 设置top-p。
*
* @param topP top-p 参数
*/
public void setTopP(Double topP) {
this.topP = topP;
}
/**
* 获取top-k。
*
* @return top-k 参数
*/
public Integer getTopK() {
return topK;
}
/**
* 设置top-k。
*
* @param topK top-k 参数
*/
public void setTopK(Integer topK) {
this.topK = topK;
}
/**
* 获取最大 Token 数。
*
* @return 最大 Token 数
*/
public Integer getMaxTokens() {
return maxTokens;
}
/**
* 设置最大 Token 数。
*
* @param maxTokens 最大 Token 数
*/
public void setMaxTokens(Integer maxTokens) {
this.maxTokens = maxTokens;
}
/**
* 获取最大补全 Token 数。
*
* @return 最大补全 Token 数
*/
public Integer getMaxCompletionTokens() {
return maxCompletionTokens;
}
/**
* 设置最大补全 Token 数。
*
* @param maxCompletionTokens 最大补全 Token 数
*/
public void setMaxCompletionTokens(Integer maxCompletionTokens) {
this.maxCompletionTokens = maxCompletionTokens;
}
/**
* 获取思考预算。
*
* @return 思考预算
*/
public Integer getThinkingBudget() {
return thinkingBudget;
}
/**
* 设置思考预算。
*
* @param thinkingBudget 思考预算
*/
public void setThinkingBudget(Integer thinkingBudget) {
this.thinkingBudget = thinkingBudget;
}
/**
* 获取推理强度。
*
* @return 推理强度
*/
public String getReasoningEffort() {
return reasoningEffort;
}
/**
* 设置推理强度。
*
* @param reasoningEffort 推理强度
*/
public void setReasoningEffort(String reasoningEffort) {
this.reasoningEffort = reasoningEffort;
}
/**
* 返回是否provider thinking is enabled。
*
* @return 思考开关
*/
public Boolean getThinkingEnabled() {
return thinkingEnabled;
}
/**
* 设置provider thinking flag。
*
* @param thinkingEnabled 思考开关
*/
public void setThinkingEnabled(Boolean thinkingEnabled) {
this.thinkingEnabled = thinkingEnabled;
}
/**
* 返回是否streaming is requested。
*
* @return 流式开关
*/
public Boolean getStream() {
return stream;
}
/**
* 设置流式开关。
*
* @param stream 流式开关
*/
public void setStream(Boolean stream) {
this.stream = stream;
}
/**
* 获取额外请求体参数。
*
* @return 额外请求体参数
*/
public Map<String, Object> getAdditionalBodyParams() {
return additionalBodyParams;
}
/**
* 设置额外请求体参数。
*
* @param additionalBodyParams 额外请求体参数
*/
public void setAdditionalBodyParams(Map<String, Object> additionalBodyParams) {
this.additionalBodyParams = additionalBodyParams == null ? new LinkedHashMap<>() : additionalBodyParams;
}
/**
* 获取额外请求头。
*
* @return 额外请求头
*/
public Map<String, String> getAdditionalHeaders() {
return additionalHeaders;
}
/**
* 设置额外请求头。
*
* @param additionalHeaders 额外请求头
*/
public void setAdditionalHeaders(Map<String, String> additionalHeaders) {
this.additionalHeaders = additionalHeaders == null ? new LinkedHashMap<>() : additionalHeaders;
}
/**
* 获取额外查询参数。
*
* @return 额外查询参数
*/
public Map<String, String> getAdditionalQueryParams() {
return additionalQueryParams;
}
/**
* 设置额外查询参数。
*
* @param additionalQueryParams 额外查询参数
*/
public void setAdditionalQueryParams(Map<String, String> additionalQueryParams) {
this.additionalQueryParams = additionalQueryParams == null ? new LinkedHashMap<>() : additionalQueryParams;
}
}

View File

@@ -0,0 +1,18 @@
package com.easyagents.agent.runtime.model;
/**
* 供应商无关的模型工厂标记接口。
*
* @param <T> 由适配器持有的具体模型类型
*/
public interface AgentModelFactory<T> {
/**
* 创建a concrete model。
*
* @param modelSpec 模型声明
* @param generationOptions 生成参数
* @return 具体模型
*/
T create(AgentModelSpec modelSpec, AgentGenerationOptions generationOptions);
}

View File

@@ -0,0 +1,20 @@
package com.easyagents.agent.runtime.model;
/**
* 智能体运行时支持的模型供应商类型。
*/
public enum AgentModelProviderType {
OPENAI,
OPENAI_COMPATIBLE,
ANTHROPIC,
GEMINI,
OLLAMA,
DEEPSEEK,
GLM,
MINIMAX,
MOONSHOT,
ARK,
SILICONFLOW,
DASHSCOPE,
CUSTOM
}

View File

@@ -0,0 +1,125 @@
package com.easyagents.agent.runtime.model;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 供应商无关的模型声明。
*/
public class AgentModelSpec {
private AgentModelProviderType providerType = AgentModelProviderType.OPENAI_COMPATIBLE;
private String modelName;
private String baseUrl;
private String endpointPath;
private String apiKey;
private Map<String, Object> metadata = new LinkedHashMap<>();
/**
* 获取供应商类型。
*
* @return 供应商类型
*/
public AgentModelProviderType getProviderType() {
return providerType;
}
/**
* 设置供应商类型。
*
* @param providerType 供应商类型
*/
public void setProviderType(AgentModelProviderType providerType) {
this.providerType = providerType == null ? AgentModelProviderType.OPENAI_COMPATIBLE : providerType;
}
/**
* 获取模型名称。
*
* @return 模型名称
*/
public String getModelName() {
return modelName;
}
/**
* 设置模型名称。
*
* @param modelName 模型名称
*/
public void setModelName(String modelName) {
this.modelName = modelName;
}
/**
* 获取基础 URL。
*
* @return 基础 URL
*/
public String getBaseUrl() {
return baseUrl;
}
/**
* 设置基础 URL。
*
* @param baseUrl 基础 URL
*/
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
/**
* 获取端点路径。
*
* @return 端点路径
*/
public String getEndpointPath() {
return endpointPath;
}
/**
* 设置端点路径。
*
* @param endpointPath 端点路径
*/
public void setEndpointPath(String endpointPath) {
this.endpointPath = endpointPath;
}
/**
* 获取API Key。
*
* @return API Key 值
*/
public String getApiKey() {
return apiKey;
}
/**
* 设置API Key。
*
* @param apiKey API Key 值
*/
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
/**
* 获取元数据。
*
* @return 元数据
*/
public Map<String, Object> getMetadata() {
return metadata;
}
/**
* 设置元数据。
*
* @param metadata 元数据
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
}
}

View File

@@ -0,0 +1,182 @@
package com.easyagents.agent.runtime.persistence;
/**
* 控制运行时状态持久化。
*/
public class AgentPersistencePolicy {
private boolean enabled;
private boolean memoryManaged;
private boolean toolkitManaged;
private boolean planNotebookManaged;
private boolean statefulToolsManaged;
private boolean hitlManaged;
private boolean sessionManaged;
private boolean conversationRecordingEnabled;
/**
* 创建禁用策略。
*
* @return 禁用策略
*/
public static AgentPersistencePolicy disabled() {
return new AgentPersistencePolicy();
}
/**
* 创建仅记忆持久化策略。
*
* @return 仅记忆持久化策略
*/
public static AgentPersistencePolicy memoryOnly() {
AgentPersistencePolicy policy = new AgentPersistencePolicy();
policy.setEnabled(true);
policy.setMemoryManaged(true);
policy.setSessionManaged(true);
return policy;
}
/**
* 返回是否persistence is enabled。
*
* @return 启用标记
*/
public boolean isEnabled() {
return enabled;
}
/**
* 设置启用标记。
*
* @param enabled 启用标记
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* 返回是否memory is managed。
*
* @return 记忆标记
*/
public boolean isMemoryManaged() {
return memoryManaged;
}
/**
* 设置记忆标记。
*
* @param memoryManaged 记忆标记
*/
public void setMemoryManaged(boolean memoryManaged) {
this.memoryManaged = memoryManaged;
}
/**
* 返回是否toolkit is managed。
*
* @return Toolkit 标记
*/
public boolean isToolkitManaged() {
return toolkitManaged;
}
/**
* 设置Toolkit 标记。
*
* @param toolkitManaged Toolkit 标记
*/
public void setToolkitManaged(boolean toolkitManaged) {
this.toolkitManaged = toolkitManaged;
}
/**
* 返回是否plan notebook is managed。
*
* @return 计划笔记标记
*/
public boolean isPlanNotebookManaged() {
return planNotebookManaged;
}
/**
* 设置计划笔记标记。
*
* @param planNotebookManaged 计划笔记标记
*/
public void setPlanNotebookManaged(boolean planNotebookManaged) {
this.planNotebookManaged = planNotebookManaged;
}
/**
* 返回是否stateful tools are managed。
*
* @return 有状态工具标记
*/
public boolean isStatefulToolsManaged() {
return statefulToolsManaged;
}
/**
* 设置有状态工具标记。
*
* @param statefulToolsManaged 有状态工具标记
*/
public void setStatefulToolsManaged(boolean statefulToolsManaged) {
this.statefulToolsManaged = statefulToolsManaged;
}
/**
* 返回是否HITL pending state is managed。
*
* @return HITL 标记
*/
public boolean isHitlManaged() {
return hitlManaged;
}
/**
* 设置HITL 标记。
*
* @param hitlManaged HITL 标记
*/
public void setHitlManaged(boolean hitlManaged) {
this.hitlManaged = hitlManaged;
}
/**
* 返回是否session storage is managed。
*
* @return 会话标记
*/
public boolean isSessionManaged() {
return sessionManaged;
}
/**
* 设置会话标记。
*
* @param sessionManaged 会话标记
*/
public void setSessionManaged(boolean sessionManaged) {
this.sessionManaged = sessionManaged;
}
/**
* 返回是否conversation events should be recorded。
*
* @return 会话记录标记
*/
public boolean isConversationRecordingEnabled() {
return conversationRecordingEnabled;
}
/**
* 设置会话记录标记。
*
* @param conversationRecordingEnabled 会话记录标记
*/
public void setConversationRecordingEnabled(boolean conversationRecordingEnabled) {
this.conversationRecordingEnabled = conversationRecordingEnabled;
}
}

View File

@@ -0,0 +1,107 @@
package com.easyagents.agent.runtime.persistence;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 可恢复状态的运行时检查点。
*/
public class AgentRuntimeCheckpoint {
private String checkpointId;
private String sessionId;
private String agentId;
private Map<String, Object> state = new LinkedHashMap<>();
private Instant createdAt = Instant.now();
/**
* 获取检查点ID。
*
* @return 检查点ID
*/
public String getCheckpointId() {
return checkpointId;
}
/**
* 设置检查点ID。
*
* @param checkpointId 检查点ID
*/
public void setCheckpointId(String checkpointId) {
this.checkpointId = checkpointId;
}
/**
* 获取会话ID。
*
* @return 会话ID
*/
public String getSessionId() {
return sessionId;
}
/**
* 设置会话ID。
*
* @param sessionId 会话ID
*/
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
/**
* 获取智能体ID。
*
* @return 智能体ID
*/
public String getAgentId() {
return agentId;
}
/**
* 设置智能体ID。
*
* @param agentId 智能体ID
*/
public void setAgentId(String agentId) {
this.agentId = agentId;
}
/**
* 获取状态载荷。
*
* @return 状态载荷
*/
public Map<String, Object> getState() {
return state;
}
/**
* 设置状态载荷。
*
* @param state 状态载荷
*/
public void setState(Map<String, Object> state) {
this.state = state == null ? new LinkedHashMap<>() : state;
}
/**
* 获取创建时间。
*
* @return 创建时间
*/
public Instant getCreatedAt() {
return createdAt;
}
/**
* 设置创建时间。
*
* @param createdAt 创建时间
*/
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt == null ? Instant.now() : createdAt;
}
}

View File

@@ -0,0 +1,18 @@
package com.easyagents.agent.runtime.persistence.conversation;
import com.easyagents.agent.runtime.AgentRuntimeExecutionContext;
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
/**
* 记录业务会话事件。
*/
public interface AgentConversationRecorder {
/**
* 记录一条运行时事件。
*
* @param request 运行请求
* @param event 运行时事件
*/
void record(AgentRuntimeExecutionContext request, AgentRuntimeEvent event);
}

View File

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

View File

@@ -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;
/**
* 智能体会话状态存储接口。
* <p>
* 该接口承接 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 <T> 状态类型
* @return 可选状态
*/
<T extends State> Optional<T> get(String sessionKey, String name, Class<T> type);
/**
* 获取状态列表。
*
* @param sessionKey 会话键
* @param name 状态名称
* @param itemType 状态元素类型
* @param <T> 状态元素类型
* @return 状态列表
*/
<T extends State> List<T> getList(String sessionKey, String name, Class<T> itemType);
/**
* 判断会话键是否存在。
*
* @param sessionKey 会话键
* @return 存在时为 true
*/
boolean exists(String sessionKey);
/**
* 删除指定会话键下的全部状态。
*
* @param sessionKey 会话键
*/
void delete(String sessionKey);
/**
* 列出当前存储中的会话键。
*
* @return 会话键列表
*/
Set<String> listSessionKeys();
}

View File

@@ -0,0 +1,28 @@
package com.easyagents.agent.runtime.persistence.session;
import com.easyagents.agent.runtime.AgentRuntimeException;
/**
* 会话持久化异常。
*/
public class AgentSessionStoreException extends AgentRuntimeException {
/**
* 创建会话持久化异常。
*
* @param message 错误消息
*/
public AgentSessionStoreException(String message) {
super(message);
}
/**
* 创建会话持久化异常。
*
* @param message 错误消息
* @param cause 原始异常
*/
public AgentSessionStoreException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -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<String, Map<String, List<State>>> 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 <T extends State> Optional<T> get(String sessionKey, String name, Class<T> type) {
List<T> list = getList(sessionKey, name, type);
return list.isEmpty() ? Optional.empty() : Optional.of(list.get(0));
}
@Override
public <T extends State> List<T> getList(String sessionKey, String name, Class<T> itemType) {
Map<String, List<State>> byName = states.getOrDefault(sessionKey, new LinkedHashMap<>());
List<State> values = byName.getOrDefault(name, new ArrayList<>());
if (itemType == null) {
return new ArrayList<>();
}
List<T> 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<String> listSessionKeys() {
return new LinkedHashSet<>(states.keySet());
}
}

View File

@@ -0,0 +1,48 @@
package com.easyagents.agent.runtime.persistence.session.noop;
import com.easyagents.agent.runtime.persistence.session.AgentSessionStore;
import io.agentscope.core.state.State;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
/**
* 空操作会话存储。
*/
public enum NoopAgentSessionStore implements AgentSessionStore {
INSTANCE;
@Override
public void save(String sessionKey, String name, State state) {
}
@Override
public void saveList(String sessionKey, String name, List<? extends State> states) {
}
@Override
public <T extends State> Optional<T> get(String sessionKey, String name, Class<T> type) {
return Optional.empty();
}
@Override
public <T extends State> List<T> getList(String sessionKey, String name, Class<T> itemType) {
return Collections.emptyList();
}
@Override
public boolean exists(String sessionKey) {
return false;
}
@Override
public void delete(String sessionKey) {
}
@Override
public Set<String> listSessionKeys() {
return Collections.emptySet();
}
}

View File

@@ -0,0 +1,52 @@
package com.easyagents.agent.runtime.skill;
/**
* Skill 与工具的运行时绑定关系。
*/
public class AgentSkillBinding {
private final String skillId;
private final String skillName;
private final String skillBoxId;
/**
* 创建 Skill 绑定关系。
*
* @param skillId Skill ID
* @param skillName Skill 名称
* @param skillBoxId SkillBox ID
*/
public AgentSkillBinding(String skillId, String skillName, String skillBoxId) {
this.skillId = skillId;
this.skillName = skillName;
this.skillBoxId = skillBoxId;
}
/**
* 获取 Skill ID。
*
* @return Skill ID
*/
public String getSkillId() {
return skillId;
}
/**
* 获取 Skill 名称。
*
* @return Skill 名称
*/
public String getSkillName() {
return skillName;
}
/**
* 获取 SkillBox ID。
*
* @return SkillBox ID
*/
public String getSkillBoxId() {
return skillBoxId;
}
}

View File

@@ -0,0 +1,147 @@
package com.easyagents.agent.runtime.skill;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 声明式 SkillBox 配置。
*/
public class AgentSkillBoxSpec {
private String skillBoxId;
private List<AgentSkillSpec> skills = new ArrayList<>();
private Map<String, List<String>> toolBindings = new LinkedHashMap<>();
private List<String> enabledToolNames = new ArrayList<>();
private List<String> disabledToolNames = new ArrayList<>();
private Map<String, Map<String, Object>> presetParameters = new LinkedHashMap<>();
private boolean exposeAllSkillMetadata;
/**
* 获取SkillBox ID。
*
* @return SkillBox ID 标识
*/
public String getSkillBoxId() {
return skillBoxId;
}
/**
* 设置SkillBox ID。
*
* @param skillBoxId SkillBox ID 标识
*/
public void setSkillBoxId(String skillBoxId) {
this.skillBoxId = skillBoxId;
}
/**
* 获取Skill 列表。
*
* @return Skill 列表
*/
public List<AgentSkillSpec> getSkills() {
return skills;
}
/**
* 设置Skill 列表。
*
* @param skills Skill 列表
*/
public void setSkills(List<AgentSkillSpec> skills) {
this.skills = skills == null ? new ArrayList<>() : new ArrayList<>(skills);
}
/**
* 获取工具绑定。
*
* @return 工具绑定
*/
public Map<String, List<String>> getToolBindings() {
return toolBindings;
}
/**
* 设置工具绑定。
*
* @param toolBindings 工具绑定
*/
public void setToolBindings(Map<String, List<String>> toolBindings) {
this.toolBindings = toolBindings == null ? new LinkedHashMap<>() : toolBindings;
}
/**
* 获取启用的工具名称。
*
* @return 启用的工具名称
*/
public List<String> getEnabledToolNames() {
return enabledToolNames;
}
/**
* 设置启用的工具名称。
*
* @param enabledToolNames 启用的工具名称
*/
public void setEnabledToolNames(List<String> enabledToolNames) {
this.enabledToolNames = enabledToolNames == null ? new ArrayList<>() : new ArrayList<>(enabledToolNames);
}
/**
* 获取禁用的工具名称。
*
* @return 禁用的工具名称
*/
public List<String> getDisabledToolNames() {
return disabledToolNames;
}
/**
* 设置禁用的工具名称。
*
* @param disabledToolNames 禁用的工具名称
*/
public void setDisabledToolNames(List<String> disabledToolNames) {
this.disabledToolNames = disabledToolNames == null ? new ArrayList<>() : new ArrayList<>(disabledToolNames);
}
/**
* 获取预设参数。
*
* @return 预设参数
*/
public Map<String, Map<String, Object>> getPresetParameters() {
return presetParameters;
}
/**
* 设置预设参数。
*
* @param presetParameters 预设参数
*/
public void setPresetParameters(Map<String, Map<String, Object>> presetParameters) {
this.presetParameters = presetParameters == null ? new LinkedHashMap<>() : presetParameters;
}
/**
* 返回是否all skill metadata is exposed。
*
* @return 所有元数据暴露时为 true
*/
public boolean isExposeAllSkillMetadata() {
return exposeAllSkillMetadata;
}
/**
* 设置元数据暴露开关。
*
* @param exposeAllSkillMetadata 元数据暴露开关
*/
public void setExposeAllSkillMetadata(boolean exposeAllSkillMetadata) {
this.exposeAllSkillMetadata = exposeAllSkillMetadata;
}
}

View File

@@ -0,0 +1,17 @@
package com.easyagents.agent.runtime.skill;
/**
* 将声明式 Skill 编译为适配器持有的 Skill 类型。
*
* @param <T> 编译后的 Skill 类型
*/
public interface AgentSkillCompiler<T> {
/**
* 编译单个 Skill。
*
* @param skillSpec Skill 声明
* @return 编译后的 Skill
*/
T compile(AgentSkillSpec skillSpec);
}

View File

@@ -0,0 +1,95 @@
package com.easyagents.agent.runtime.skill;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Skill 加载工具调用记录。
*/
public class AgentSkillLoadCall {
private final String toolCallId;
private final String skillId;
private final String skillName;
private final String skillBoxId;
private final String path;
private final Map<String, Object> input;
/**
* 创建 Skill 加载工具调用记录。
*
* @param toolCallId 工具调用 ID
* @param skillId Skill ID
* @param skillName Skill 名称
* @param skillBoxId SkillBox ID
* @param path 资源路径
* @param input 工具输入
*/
public AgentSkillLoadCall(String toolCallId,
String skillId,
String skillName,
String skillBoxId,
String path,
Map<String, Object> input) {
this.toolCallId = toolCallId;
this.skillId = skillId;
this.skillName = skillName;
this.skillBoxId = skillBoxId;
this.path = path;
this.input = input == null ? new LinkedHashMap<>() : new LinkedHashMap<>(input);
}
/**
* 获取工具调用 ID。
*
* @return 工具调用 ID
*/
public String getToolCallId() {
return toolCallId;
}
/**
* 获取 Skill ID。
*
* @return Skill ID
*/
public String getSkillId() {
return skillId;
}
/**
* 获取 Skill 名称。
*
* @return Skill 名称
*/
public String getSkillName() {
return skillName;
}
/**
* 获取 SkillBox ID。
*
* @return SkillBox ID
*/
public String getSkillBoxId() {
return skillBoxId;
}
/**
* 获取资源路径。
*
* @return 资源路径
*/
public String getPath() {
return path;
}
/**
* 获取工具输入。
*
* @return 工具输入
*/
public Map<String, Object> getInput() {
return input;
}
}

View File

@@ -0,0 +1,209 @@
package com.easyagents.agent.runtime.skill;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Skill 运行时上下文。
*/
public class AgentSkillRuntimeContext {
/**
* AgentScope SkillBox 内置的 Skill 加载工具名称。
*/
public static final String LOAD_SKILL_TOOL_NAME = "load_skill_through_path";
private final Map<String, AgentSkillBinding> skillBindings;
private final Map<String, AgentSkillBinding> toolBindings;
private final Map<String, AgentSkillLoadCall> pendingLoadCalls = new ConcurrentHashMap<>();
private final Map<String, Boolean> emittedLoadCalls = new ConcurrentHashMap<>();
private final Map<String, Boolean> activeSkills = new ConcurrentHashMap<>();
private AgentSkillRuntimeContext(Map<String, AgentSkillBinding> skillBindings,
Map<String, AgentSkillBinding> toolBindings) {
this.skillBindings = skillBindings;
this.toolBindings = toolBindings;
}
/**
* 从 SkillBox 配置创建运行时上下文。
*
* @param spec SkillBox 配置
* @return Skill 运行时上下文
*/
public static AgentSkillRuntimeContext from(AgentSkillBoxSpec spec) {
if (spec == null) {
return new AgentSkillRuntimeContext(new LinkedHashMap<>(), new LinkedHashMap<>());
}
Map<String, AgentSkillBinding> skillBindings = new LinkedHashMap<>();
for (AgentSkillSpec skillSpec : spec.getSkills()) {
if (skillSpec.getSkillId() == null || skillSpec.getSkillId().isBlank()) {
continue;
}
skillBindings.put(skillSpec.getSkillId(),
new AgentSkillBinding(skillSpec.getSkillId(), skillSpec.getName(), spec.getSkillBoxId()));
}
Map<String, AgentSkillBinding> toolBindings = new LinkedHashMap<>();
for (Map.Entry<String, List<String>> entry : spec.getToolBindings().entrySet()) {
AgentSkillBinding binding = skillBindings.get(entry.getKey());
if (binding == null || entry.getValue() == null) {
continue;
}
for (String toolName : entry.getValue()) {
if (toolName != null && !toolName.isBlank()) {
toolBindings.put(toolName, binding);
}
}
}
return new AgentSkillRuntimeContext(skillBindings, toolBindings);
}
/**
* 判断是否为 Skill 加载工具。
*
* @param toolName 工具名称
* @return 是 Skill 加载工具时为 true
*/
public boolean isSkillLoadTool(String toolName) {
return LOAD_SKILL_TOOL_NAME.equals(toolName);
}
/**
* 根据 Skill ID 获取绑定关系。
*
* @param skillId Skill ID
* @return 绑定关系
*/
public AgentSkillBinding getSkillBinding(String skillId) {
return skillBindings.get(skillId);
}
/**
* 根据工具名称获取 Skill 绑定关系。
*
* @param toolName 工具名称
* @return 绑定关系
*/
public AgentSkillBinding getToolBinding(String toolName) {
return toolBindings.get(toolName);
}
/**
* 判断工具是否绑定到 Skill。
*
* @param toolName 工具名称
* @return 绑定到 Skill 时为 true
*/
public boolean isSkillBoundTool(String toolName) {
return toolBindings.containsKey(toolName);
}
/**
* 激活指定 Skill。
*
* @param skillId Skill ID
*/
public void activateSkill(String skillId) {
if (skillId != null && !skillId.isBlank()) {
activeSkills.put(skillId, true);
}
}
/**
* 按 AgentScope 的真实 Skill 激活状态同步本地旁路上下文。
*
* <p>该状态只用于 Easy-Agents 判断旁路展示事件归属,不参与 AgentScope
* memory/session也不决定工具是否真的可被模型调用。</p>
*
* @param skillId Skill ID
* @param active 是否激活
*/
public void syncSkillActive(String skillId, boolean active) {
if (skillId == null || skillId.isBlank()) {
return;
}
if (active) {
activeSkills.put(skillId, true);
return;
}
activeSkills.remove(skillId);
}
/**
* 判断 Skill 是否已激活。
*
* @param skillId Skill ID
* @return 已激活时为 true
*/
public boolean isSkillActive(String skillId) {
return Boolean.TRUE.equals(activeSkills.get(skillId));
}
/**
* 获取已激活 Skill 的工具绑定。
*
* @param toolName 工具名称
* @return 已激活 Skill 的绑定关系
*/
public AgentSkillBinding getActiveToolBinding(String toolName) {
AgentSkillBinding binding = getToolBinding(toolName);
if (binding == null || !isSkillActive(binding.getSkillId())) {
return null;
}
return binding;
}
/**
* 记录一次 Skill 加载工具调用。
*
* @param toolCallId 工具调用 ID
* @param input 工具输入
* @return Skill 加载调用
*/
public AgentSkillLoadCall rememberLoadCall(String toolCallId, Map<String, Object> input) {
if (toolCallId == null || input == null) {
return null;
}
String skillId = stringValue(input.get("skillId"));
String path = stringValue(input.get("path"));
AgentSkillBinding binding = getSkillBinding(skillId);
AgentSkillLoadCall call = new AgentSkillLoadCall(toolCallId, skillId,
binding == null ? null : binding.getSkillName(),
binding == null ? null : binding.getSkillBoxId(), path, input);
pendingLoadCalls.put(toolCallId, call);
return call;
}
/**
* 判断 Skill 加载调用事件是否已经发射。
*
* @param toolCallId 工具调用 ID
* @return 首次发射时为 true
*/
public boolean markLoadCallEmitted(String toolCallId) {
if (toolCallId == null) {
return false;
}
return emittedLoadCalls.putIfAbsent(toolCallId, true) == null;
}
/**
* 获取并移除一次 Skill 加载工具调用。
*
* @param toolCallId 工具调用 ID
* @return Skill 加载调用
*/
public AgentSkillLoadCall removeLoadCall(String toolCallId) {
if (toolCallId == null) {
return null;
}
emittedLoadCalls.remove(toolCallId);
return pendingLoadCalls.remove(toolCallId);
}
private static String stringValue(Object value) {
return value == null ? null : String.valueOf(value);
}
}

View File

@@ -0,0 +1,144 @@
package com.easyagents.agent.runtime.skill;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 声明式 Skill 配置。
*/
public class AgentSkillSpec {
private String skillId;
private String name;
private String description;
private String skillContent;
private Map<String, String> resources = new LinkedHashMap<>();
private String source;
private Map<String, Object> metadata = new LinkedHashMap<>();
/**
* 获取Skill ID。
*
* @return Skill ID 标识
*/
public String getSkillId() {
return skillId;
}
/**
* 设置Skill ID。
*
* @param skillId Skill ID 标识
*/
public void setSkillId(String skillId) {
this.skillId = skillId;
}
/**
* 获取Skill 名称。
*
* @return Skill 名称
*/
public String getName() {
return name;
}
/**
* 设置Skill 名称。
*
* @param name Skill 名称
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取描述。
*
* @return 描述
*/
public String getDescription() {
return description;
}
/**
* 设置描述。
*
* @param description 描述
*/
public void setDescription(String description) {
this.description = description;
}
/**
* 获取Skill 内容。
*
* @return Skill 内容
*/
public String getSkillContent() {
return skillContent;
}
/**
* 设置Skill 内容。
*
* @param skillContent Skill 内容
*/
public void setSkillContent(String skillContent) {
this.skillContent = skillContent;
}
/**
* 获取资源映射。
*
* @return 资源映射
*/
public Map<String, String> getResources() {
return resources;
}
/**
* 设置资源映射。
*
* @param resources 资源映射
*/
public void setResources(Map<String, String> resources) {
this.resources = resources == null ? new LinkedHashMap<>() : new LinkedHashMap<>(resources);
}
/**
* 获取来源。
*
* @return 来源
*/
public String getSource() {
return source;
}
/**
* 设置来源。
*
* @param source 来源
*/
public void setSource(String source) {
this.source = source;
}
/**
* 获取元数据。
*
* @return 元数据
*/
public Map<String, Object> getMetadata() {
return metadata;
}
/**
* 设置元数据。
*
* @param metadata 元数据
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata == null ? new LinkedHashMap<>() : new LinkedHashMap<>(metadata);
}
}

View File

@@ -0,0 +1,12 @@
package com.easyagents.agent.runtime.tool;
/**
* 动态工具分类。
*/
public enum AgentToolCategory {
WORKFLOW,
PLUGIN,
MCP,
KNOWLEDGE,
CUSTOM
}

View File

@@ -0,0 +1,181 @@
package com.easyagents.agent.runtime.tool;
import com.easyagents.agent.runtime.AgentRuntimeContext;
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
/**
* 传递给动态工具调用的上下文。
*/
public class AgentToolContext {
private String requestId;
private String traceId;
private String sessionId;
private String agentId;
private String toolCallId;
private AgentRuntimeContext runtimeContext = new AgentRuntimeContext();
private Map<String, Object> metadata = new LinkedHashMap<>();
private Consumer<AgentRuntimeEvent> eventEmitter;
/**
* 获取请求ID。
*
* @return 请求ID
*/
public String getRequestId() {
return requestId;
}
/**
* 设置请求ID。
*
* @param requestId 请求ID
*/
public void setRequestId(String requestId) {
this.requestId = requestId;
}
/**
* 获取链路ID。
*
* @return 链路ID
*/
public String getTraceId() {
return traceId;
}
/**
* 设置链路ID。
*
* @param traceId 链路ID
*/
public void setTraceId(String traceId) {
this.traceId = traceId;
}
/**
* 获取会话ID。
*
* @return 会话ID
*/
public String getSessionId() {
return sessionId;
}
/**
* 设置会话ID。
*
* @param sessionId 会话ID
*/
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
/**
* 获取智能体ID。
*
* @return 智能体ID
*/
public String getAgentId() {
return agentId;
}
/**
* 设置智能体ID。
*
* @param agentId 智能体ID
*/
public void setAgentId(String agentId) {
this.agentId = agentId;
}
/**
* 获取工具调用ID。
*
* @return 工具调用ID
*/
public String getToolCallId() {
return toolCallId;
}
/**
* 设置工具调用ID。
*
* @param toolCallId 工具调用ID
*/
public void setToolCallId(String toolCallId) {
this.toolCallId = toolCallId;
}
/**
* 获取运行时上下文。
*
* @return 运行时上下文
*/
public AgentRuntimeContext getRuntimeContext() {
return runtimeContext;
}
/**
* 设置运行时上下文。
*
* @param runtimeContext 运行时上下文
*/
public void setRuntimeContext(AgentRuntimeContext runtimeContext) {
this.runtimeContext = runtimeContext == null ? new AgentRuntimeContext() : runtimeContext;
}
/**
* 获取元数据。
*
* @return 元数据
*/
public Map<String, Object> getMetadata() {
return metadata;
}
/**
* 设置元数据。
*
* @param metadata 元数据
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
}
/**
* 发射运行时旁路事件。
*
* <p>该方法主要供 runtime 包装层发射业务无关事件,例如异步工具生命周期事件。
* 未配置事件发射器时静默忽略,避免非流式测试路径产生额外副作用。</p>
*
* @param event 运行时事件
*/
public void emitEvent(AgentRuntimeEvent event) {
if (eventEmitter != null && event != null) {
eventEmitter.accept(event);
}
}
/**
* 获取运行时事件发射器。
*
* @return 运行时事件发射器
*/
public Consumer<AgentRuntimeEvent> getEventEmitter() {
return eventEmitter;
}
/**
* 设置运行时事件发射器。
*
* @param eventEmitter 运行时事件发射器
*/
public void setEventEmitter(Consumer<AgentRuntimeEvent> eventEmitter) {
this.eventEmitter = eventEmitter;
}
}

View File

@@ -0,0 +1,19 @@
package com.easyagents.agent.runtime.tool;
import java.util.Map;
/**
* 执行动态工具。
*/
@FunctionalInterface
public interface AgentToolInvoker {
/**
* 使用模型生成的参数调用工具。
*
* @param arguments 工具参数
* @param context 调用上下文
* @return 工具结果
*/
AgentToolResult invoke(Map<String, Object> arguments, AgentToolContext context);
}

View File

@@ -0,0 +1,133 @@
package com.easyagents.agent.runtime.tool;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 动态工具返回的结果。
*/
public class AgentToolResult {
private boolean success = true;
private String modelContent;
private Object displayContent;
private String errorMessage;
private Map<String, Object> metadata = new LinkedHashMap<>();
/**
* 创建成功的文本结果。
*
* @param content 模型可见内容
* @return 结果
*/
public static AgentToolResult success(String content) {
AgentToolResult result = new AgentToolResult();
result.setModelContent(content);
result.setDisplayContent(content);
return result;
}
/**
* 创建失败结果。
*
* @param errorMessage 错误消息
* @return 结果
*/
public static AgentToolResult failure(String errorMessage) {
AgentToolResult result = new AgentToolResult();
result.setSuccess(false);
result.setErrorMessage(errorMessage);
result.setModelContent(errorMessage);
return result;
}
/**
* 返回执行是否成功。
*
* @return 成功时为 true
*/
public boolean isSuccess() {
return success;
}
/**
* 设置成功标记。
*
* @param success 成功标记
*/
public void setSuccess(boolean success) {
this.success = success;
}
/**
* 获取模型可见内容。
*
* @return 模型可见内容
*/
public String getModelContent() {
return modelContent;
}
/**
* 设置模型可见内容。
*
* @param modelContent 模型可见内容
*/
public void setModelContent(String modelContent) {
this.modelContent = modelContent;
}
/**
* 获取展示内容。
*
* @return 展示内容
*/
public Object getDisplayContent() {
return displayContent;
}
/**
* 设置展示内容。
*
* @param displayContent 展示内容
*/
public void setDisplayContent(Object displayContent) {
this.displayContent = displayContent;
}
/**
* 获取错误消息。
*
* @return 错误消息
*/
public String getErrorMessage() {
return errorMessage;
}
/**
* 设置错误消息。
*
* @param errorMessage 错误消息
*/
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
/**
* 获取元数据。
*
* @return 元数据
*/
public Map<String, Object> getMetadata() {
return metadata;
}
/**
* 设置元数据。
*
* @param metadata 元数据
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
}
}

View File

@@ -0,0 +1,184 @@
package com.easyagents.agent.runtime.tool;
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 动态工具声明。
*/
public class AgentToolSpec {
private String name;
private String description;
private AgentToolCategory category = AgentToolCategory.CUSTOM;
private Map<String, Object> parametersSchema = new LinkedHashMap<>();
private Map<String, Object> outputSchema = new LinkedHashMap<>();
private AgentToolVisibility visibility = AgentToolVisibility.VISIBLE;
private boolean approvalRequired;
private AgentToolApprovalRequest approvalRequest = new AgentToolApprovalRequest();
private Map<String, Object> metadata = new LinkedHashMap<>();
/**
* 获取工具名称。
*
* @return 工具名称
*/
public String getName() {
return name;
}
/**
* 设置工具名称。
*
* @param name 工具名称
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取描述。
*
* @return 描述
*/
public String getDescription() {
return description;
}
/**
* 设置描述。
*
* @param description 描述
*/
public void setDescription(String description) {
this.description = description;
}
/**
* 获取分类。
*
* @return 分类
*/
public AgentToolCategory getCategory() {
return category;
}
/**
* 设置分类。
*
* @param category 分类
*/
public void setCategory(AgentToolCategory category) {
this.category = category == null ? AgentToolCategory.CUSTOM : category;
}
/**
* 获取参数 JSON Schema。
*
* @return 参数 Schema
*/
public Map<String, Object> getParametersSchema() {
return parametersSchema;
}
/**
* 设置参数 JSON Schema。
*
* @param parametersSchema 参数 Schema
*/
public void setParametersSchema(Map<String, Object> parametersSchema) {
this.parametersSchema = parametersSchema == null ? new LinkedHashMap<>() : parametersSchema;
}
/**
* 获取输出 Schema。
*
* @return 输出 Schema
*/
public Map<String, Object> getOutputSchema() {
return outputSchema;
}
/**
* 设置输出 Schema。
*
* @param outputSchema 输出 Schema
*/
public void setOutputSchema(Map<String, Object> outputSchema) {
this.outputSchema = outputSchema == null ? new LinkedHashMap<>() : outputSchema;
}
/**
* 获取可见性策略。
*
* @return 可见性策略
*/
public AgentToolVisibility getVisibility() {
return visibility;
}
/**
* 设置可见性策略。
*
* @param visibility 可见性策略
*/
public void setVisibility(AgentToolVisibility visibility) {
this.visibility = visibility == null ? AgentToolVisibility.VISIBLE : visibility;
}
/**
* 返回工具执行前是否需要人工审批。
*
* @return 需要审批时为 true
*/
public boolean isApprovalRequired() {
return approvalRequired;
}
/**
* 设置工具执行前是否需要人工审批。
*
* @param approvalRequired 审批标记
*/
public void setApprovalRequired(boolean approvalRequired) {
this.approvalRequired = approvalRequired;
}
/**
* 获取工具执行审批请求。
*
* @return 工具执行审批请求
*/
public AgentToolApprovalRequest getApprovalRequest() {
return approvalRequest;
}
/**
* 设置工具执行审批请求。
*
* @param approvalRequest 工具执行审批请求
*/
public void setApprovalRequest(AgentToolApprovalRequest approvalRequest) {
this.approvalRequest = approvalRequest == null ? new AgentToolApprovalRequest() : approvalRequest;
}
/**
* 获取元数据。
*
* @return 元数据
*/
public Map<String, Object> getMetadata() {
return metadata;
}
/**
* 设置元数据。
*
* @param metadata 元数据
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
}
}

View File

@@ -0,0 +1,10 @@
package com.easyagents.agent.runtime.tool;
/**
* 工具执行可见性策略。
*/
public enum AgentToolVisibility {
VISIBLE,
HIDDEN,
MODEL_ONLY
}

View File

@@ -0,0 +1,56 @@
package com.easyagents.agent.runtime.tool.asynctool;
import com.easyagents.agent.runtime.tool.AgentToolContext;
import java.util.Map;
/**
* 调用方实现的异步业务子工具集合。
*/
public interface AsyncSubTools {
/**
* 提交异步任务。
*
* @param arguments 模型传入的业务参数
* @param context 工具调用上下文
* @return 提交结果
*/
AsyncToolSubmitResult submit(Map<String, Object> arguments, AgentToolContext context);
/**
* 非阻塞观察任务状态和增量事件。
*
* @param request 观察请求
* @param context 工具调用上下文
* @return 当前任务视图
*/
AsyncToolTaskView observe(AsyncToolObserveRequest request, AgentToolContext context);
/**
* 获取任务结果,未完成时返回当前观察态。
*
* @param request 结果请求
* @param context 工具调用上下文
* @return 当前任务视图
*/
AsyncToolTaskView result(AsyncToolResultRequest request, AgentToolContext context);
/**
* 请求取消任务。
*
* @param request 取消请求
* @param context 工具调用上下文
* @return 取消结果
*/
AsyncToolCancelResult cancel(AsyncToolCancelRequest request, AgentToolContext context);
/**
* 查询当前上下文可见的任务列表。
*
* @param request 列表请求
* @param context 工具调用上下文
* @return 任务列表
*/
AsyncToolTaskListResult list(AsyncToolListRequest request, AgentToolContext context);
}

View File

@@ -0,0 +1,52 @@
package com.easyagents.agent.runtime.tool.asynctool;
/**
* 异步工具取消请求。
*/
public class AsyncToolCancelRequest {
private String taskId;
private String reason;
/**
* 创建空取消请求。
*/
public AsyncToolCancelRequest() {
}
/**
* 获取任务 ID。
*
* @return 任务 ID
*/
public String getTaskId() {
return taskId;
}
/**
* 设置任务 ID。
*
* @param taskId 任务 ID
*/
public void setTaskId(String taskId) {
this.taskId = taskId;
}
/**
* 获取取消原因。
*
* @return 取消原因
*/
public String getReason() {
return reason;
}
/**
* 设置取消原因。
*
* @param reason 取消原因
*/
public void setReason(String reason) {
this.reason = reason;
}
}

Some files were not shown because too many files have changed in this diff Show More