feat: 增加内置read、write、shell 工具支持

This commit is contained in:
2026-05-28 11:16:41 +08:00
parent f324acb83c
commit 2bc525c16e
4 changed files with 457 additions and 0 deletions

View File

@@ -0,0 +1,184 @@
package com.easyagents.agent.runtime.tool.operate;
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.coding.ShellCommandTool;
import io.agentscope.core.tool.file.ReadFileTool;
import io.agentscope.core.tool.file.WriteFileTool;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.*;
/**
* AgentScope 内置操作工具适配器。
*
* <p>该适配器只负责将 Easy-Agents 的操作工具声明转换为 AgentScope Toolkit 中的原生工具。
* Shell 工具的人工审批不使用 AgentScope {@code ShellCommandTool} 的同步 callback而是通过
* Easy-Agents 现有 {@code ToolHitlInterceptor} 统一处理,以保持 SSE 暂停、恢复和审计语义一致。
*/
public class AgentOperateToolAdapter {
public static final String VIEW_TEXT_FILE_TOOL = "view_text_file";
public static final String LIST_DIRECTORY_TOOL = "list_directory";
public static final String WRITE_TEXT_FILE_TOOL = "write_text_file";
public static final String INSERT_TEXT_FILE_TOOL = "insert_text_file";
public static final String EXECUTE_SHELL_COMMAND_TOOL = "execute_shell_command";
/**
* 将操作工具声明注册到 Toolkit并返回供 HITL 拦截器使用的工具声明。
*
* @param specs 操作工具声明
* @param toolkit AgentScope Toolkit
* @return 已启用操作工具对应的 AgentToolSpec
*/
public List<AgentToolSpec> register(List<AgentOperateToolSpec> specs, Toolkit toolkit) {
List<AgentToolSpec> toolSpecs = new ArrayList<>();
if (specs == null || specs.isEmpty()) {
return toolSpecs;
}
for (AgentOperateToolSpec spec : specs) {
if (spec == null || !spec.isEnabled()) {
continue;
}
registerOne(spec, toolkit, toolSpecs);
}
return toolSpecs;
}
/**
* 解析已启用操作工具会注册到 AgentScope 的工具名称。
*
* @param specs 操作工具声明
* @return 工具名称集合
*/
public Set<String> enabledToolNames(List<AgentOperateToolSpec> specs) {
Set<String> names = new LinkedHashSet<>();
if (specs == null || specs.isEmpty()) {
return names;
}
for (AgentOperateToolSpec spec : specs) {
if (spec == null || !spec.isEnabled() || spec.getType() == null) {
continue;
}
switch (spec.getType()) {
case READ_FILE -> {
names.add(VIEW_TEXT_FILE_TOOL);
names.add(LIST_DIRECTORY_TOOL);
}
case WRITE_FILE -> {
names.add(WRITE_TEXT_FILE_TOOL);
names.add(INSERT_TEXT_FILE_TOOL);
}
case SHELL -> names.add(EXECUTE_SHELL_COMMAND_TOOL);
default -> {
}
}
}
return names;
}
private void registerOne(AgentOperateToolSpec spec, Toolkit toolkit, List<AgentToolSpec> toolSpecs) {
AgentOperateToolType type = spec.getType();
if (type == null) {
throw new AgentRuntimeException("Agent operate tool type is required.");
}
Path baseDir = validateBaseDir(spec);
switch (type) {
case READ_FILE -> {
assertNoToolConflict(toolkit, VIEW_TEXT_FILE_TOOL);
assertNoToolConflict(toolkit, LIST_DIRECTORY_TOOL);
toolkit.registerTool(new ReadFileTool(baseDir.toString()));
toolSpecs.add(toolSpec(spec, VIEW_TEXT_FILE_TOOL, "View text file content.", false));
toolSpecs.add(toolSpec(spec, LIST_DIRECTORY_TOOL, "List files and directories.", false));
}
case WRITE_FILE -> {
assertNoToolConflict(toolkit, WRITE_TEXT_FILE_TOOL);
assertNoToolConflict(toolkit, INSERT_TEXT_FILE_TOOL);
toolkit.registerTool(new WriteFileTool(baseDir.toString()));
toolSpecs.add(toolSpec(spec, WRITE_TEXT_FILE_TOOL, "Write or replace text file content.", true));
toolSpecs.add(toolSpec(spec, INSERT_TEXT_FILE_TOOL, "Insert text into a file.", true));
}
case SHELL -> {
assertNoToolConflict(toolkit, EXECUTE_SHELL_COMMAND_TOOL);
Charset charset = parseCharset(spec);
toolkit.registerAgentTool(new ShellCommandTool(baseDir.toString(), spec.getShellAllowedCommands(), null,
null, charset));
toolSpecs.add(toolSpec(spec, EXECUTE_SHELL_COMMAND_TOOL, "Execute shell command.", true));
}
default -> throw new AgentRuntimeException("Unsupported agent operate tool type: " + type);
}
}
private Path validateBaseDir(AgentOperateToolSpec spec) {
String baseDir = spec.getBaseDir();
if (baseDir == null || baseDir.isBlank()) {
throw new AgentRuntimeException("Agent operate tool baseDir is required.");
}
Path path = Path.of(baseDir).toAbsolutePath().normalize();
if (!Path.of(baseDir).isAbsolute()) {
throw new AgentRuntimeException("Agent operate tool baseDir must be an absolute path: " + baseDir);
}
return path;
}
private Charset parseCharset(AgentOperateToolSpec spec) {
String charsetName = spec.getShellCharset();
if (charsetName == null || charsetName.isBlank()) {
return StandardCharsets.UTF_8;
}
try {
return Charset.forName(charsetName.trim());
} catch (Exception error) {
throw new AgentRuntimeException("Invalid shell charset: " + charsetName, error);
}
}
private AgentToolSpec toolSpec(AgentOperateToolSpec operateSpec,
String toolName,
String description,
boolean defaultApprovalRequired) {
boolean approvalRequired = operateSpec.getApprovalRequired() == null
? defaultApprovalRequired : operateSpec.getApprovalRequired();
AgentToolSpec toolSpec = new AgentToolSpec();
toolSpec.setName(toolName);
toolSpec.setDescription(description);
toolSpec.setCategory(AgentToolCategory.CUSTOM);
toolSpec.setVisibility(AgentToolVisibility.VISIBLE);
toolSpec.setApprovalRequired(approvalRequired);
toolSpec.setApprovalRequest(approvalRequest(operateSpec, approvalRequired));
toolSpec.setMetadata(metadata(operateSpec));
return toolSpec;
}
private AgentToolApprovalRequest approvalRequest(AgentOperateToolSpec operateSpec, boolean approvalRequired) {
AgentToolApprovalRequest request = operateSpec.getApprovalRequest();
if (request != null) {
return request;
}
AgentToolApprovalRequest defaultRequest = new AgentToolApprovalRequest();
if (approvalRequired) {
defaultRequest.setApprovalPrompt("请确认是否允许智能体执行该操作工具。");
}
return defaultRequest;
}
private Map<String, Object> metadata(AgentOperateToolSpec spec) {
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("operateTool", true);
metadata.put("operateToolType", spec.getType().name());
metadata.put("baseDir", spec.getBaseDir());
return metadata;
}
private void assertNoToolConflict(Toolkit toolkit, String toolName) {
if (toolkit.getTool(toolName) != null) {
throw new AgentRuntimeException("Agent operate tool conflicts with existing tool: " + toolName);
}
}
}

View File

@@ -0,0 +1,150 @@
package com.easyagents.agent.runtime.tool.operate;
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* Agent 操作类工具声明。
*
* <p>操作类工具是 runtime 直接适配的 AgentScope 内置工具,用于读文件、写文件和执行 Shell。
* 这些工具直接作用于后端 JVM 所在宿主环境,调用方必须按 agent、session 或 user 维度传入受控
* 的绝对工作目录。
*/
public class AgentOperateToolSpec {
private AgentOperateToolType type;
private boolean enabled = true;
private String baseDir;
private Boolean approvalRequired;
private AgentToolApprovalRequest approvalRequest;
private Set<String> shellAllowedCommands = new LinkedHashSet<>();
private String shellCharset;
/**
* 获取操作工具类型。
*
* @return 操作工具类型
*/
public AgentOperateToolType getType() {
return type;
}
/**
* 设置操作工具类型。
*
* @param type 操作工具类型
*/
public void setType(AgentOperateToolType type) {
this.type = type;
}
/**
* 返回是否启用该操作工具。
*
* @return 启用时为 true
*/
public boolean isEnabled() {
return enabled;
}
/**
* 设置是否启用该操作工具。
*
* @param enabled 启用标记
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* 获取操作工具工作目录。
*
* @return 绝对工作目录
*/
public String getBaseDir() {
return baseDir;
}
/**
* 设置操作工具工作目录。
*
* @param baseDir 绝对工作目录
*/
public void setBaseDir(String baseDir) {
this.baseDir = baseDir;
}
/**
* 获取审批开关覆盖值。
*
* @return 审批覆盖值null 表示使用工具类型默认值
*/
public Boolean getApprovalRequired() {
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;
}
/**
* 获取 Shell 命令白名单。
*
* @return Shell 命令白名单
*/
public Set<String> getShellAllowedCommands() {
return shellAllowedCommands;
}
/**
* 设置 Shell 命令白名单。
*
* @param shellAllowedCommands Shell 命令白名单
*/
public void setShellAllowedCommands(Set<String> shellAllowedCommands) {
this.shellAllowedCommands = shellAllowedCommands == null ? new LinkedHashSet<>() : new LinkedHashSet<>(shellAllowedCommands);
}
/**
* 获取 Shell 输出字符集名称。
*
* @return 字符集名称
*/
public String getShellCharset() {
return shellCharset;
}
/**
* 设置 Shell 输出字符集名称。
*
* @param shellCharset 字符集名称
*/
public void setShellCharset(String shellCharset) {
this.shellCharset = shellCharset;
}
}

View File

@@ -0,0 +1,22 @@
package com.easyagents.agent.runtime.tool.operate;
/**
* Agent 操作类工具类型。
*/
public enum AgentOperateToolType {
/**
* 读取文本文件与列出目录。
*/
READ_FILE,
/**
* 写入、覆盖或插入文本文件。
*/
WRITE_FILE,
/**
* 在服务进程所在宿主环境执行 Shell 命令。
*/
SHELL
}

View File

@@ -0,0 +1,101 @@
package com.easyagents.agent.runtime.tool.operate;
import com.easyagents.agent.runtime.AgentRuntimeException;
import com.easyagents.agent.runtime.tool.AgentToolSpec;
import io.agentscope.core.tool.Toolkit;
import org.junit.Assert;
import org.junit.Test;
import java.util.List;
import java.util.Set;
/**
* 测试 Agent 操作类工具适配器。
*/
public class AgentOperateToolAdapterTest {
private final AgentOperateToolAdapter adapter = new AgentOperateToolAdapter();
@Test
public void shouldRegisterReadFileToolsWithDefaultHitlDisabled() {
Toolkit toolkit = new Toolkit();
AgentOperateToolSpec spec = spec(AgentOperateToolType.READ_FILE);
List<AgentToolSpec> toolSpecs = adapter.register(List.of(spec), toolkit);
Assert.assertNotNull(toolkit.getTool(AgentOperateToolAdapter.VIEW_TEXT_FILE_TOOL));
Assert.assertNotNull(toolkit.getTool(AgentOperateToolAdapter.LIST_DIRECTORY_TOOL));
Assert.assertEquals(2, toolSpecs.size());
Assert.assertTrue(toolSpecs.stream().noneMatch(AgentToolSpec::isApprovalRequired));
}
@Test
public void shouldRegisterWriteFileToolsWithDefaultHitlEnabled() {
Toolkit toolkit = new Toolkit();
AgentOperateToolSpec spec = spec(AgentOperateToolType.WRITE_FILE);
List<AgentToolSpec> toolSpecs = adapter.register(List.of(spec), toolkit);
Assert.assertNotNull(toolkit.getTool(AgentOperateToolAdapter.WRITE_TEXT_FILE_TOOL));
Assert.assertNotNull(toolkit.getTool(AgentOperateToolAdapter.INSERT_TEXT_FILE_TOOL));
Assert.assertEquals(2, toolSpecs.size());
Assert.assertTrue(toolSpecs.stream().allMatch(AgentToolSpec::isApprovalRequired));
}
@Test
public void shouldRegisterShellToolWithEmptyWhitelist() {
Toolkit toolkit = new Toolkit();
AgentOperateToolSpec spec = spec(AgentOperateToolType.SHELL);
spec.setShellAllowedCommands(Set.of());
List<AgentToolSpec> toolSpecs = adapter.register(List.of(spec), toolkit);
Assert.assertNotNull(toolkit.getTool(AgentOperateToolAdapter.EXECUTE_SHELL_COMMAND_TOOL));
Assert.assertEquals(1, toolSpecs.size());
Assert.assertTrue(toolSpecs.get(0).isApprovalRequired());
}
@Test
public void shouldSkipDisabledOperateTool() {
Toolkit toolkit = new Toolkit();
AgentOperateToolSpec spec = spec(AgentOperateToolType.SHELL);
spec.setEnabled(false);
List<AgentToolSpec> toolSpecs = adapter.register(List.of(spec), toolkit);
Assert.assertNull(toolkit.getTool(AgentOperateToolAdapter.EXECUTE_SHELL_COMMAND_TOOL));
Assert.assertTrue(toolSpecs.isEmpty());
}
@Test(expected = AgentRuntimeException.class)
public void shouldRejectMissingBaseDir() {
AgentOperateToolSpec spec = spec(AgentOperateToolType.READ_FILE);
spec.setBaseDir(null);
adapter.register(List.of(spec), new Toolkit());
}
@Test(expected = AgentRuntimeException.class)
public void shouldRejectRelativeBaseDir() {
AgentOperateToolSpec spec = spec(AgentOperateToolType.READ_FILE);
spec.setBaseDir("relative/workspace");
adapter.register(List.of(spec), new Toolkit());
}
@Test(expected = AgentRuntimeException.class)
public void shouldRejectToolNameConflict() {
Toolkit toolkit = new Toolkit();
AgentOperateToolSpec first = spec(AgentOperateToolType.READ_FILE);
AgentOperateToolSpec second = spec(AgentOperateToolType.READ_FILE);
adapter.register(List.of(first, second), toolkit);
}
private AgentOperateToolSpec spec(AgentOperateToolType type) {
AgentOperateToolSpec spec = new AgentOperateToolSpec();
spec.setType(type);
spec.setBaseDir(System.getProperty("java.io.tmpdir"));
return spec;
}
}