From 7cac558b6c89e8cf3136a452a91f267f42e68a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Thu, 4 Jun 2026 15:23:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BD=92=E6=A1=A3=20XL08=20=E5=BC=82?= =?UTF-8?q?=E6=AD=A5=E5=B7=A5=E5=85=B7=E5=8D=8F=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AsyncToolSpec 与 AsyncSubTools 五子工具展开能力 - 增加异步工具事件、上下文事件发射和模型可见结果裁剪 - 补充 AgentScope 异步工具协议提示与 runtime 单元测试 --- easy-agents-agent-runtime/pom.xml | 5 + .../agentscope/AgentScopeReActRuntime.java | 38 +- .../agentscope/AgentScopeToolAdapter.java | 3 +- .../runtime/event/AgentRuntimeEventType.java | 30 + .../agent/runtime/tool/AgentToolContext.java | 35 + .../runtime/tool/asynctool/AsyncSubTools.java | 56 ++ .../asynctool/AsyncToolCancelRequest.java | 52 ++ .../tool/asynctool/AsyncToolCancelResult.java | 131 ++++ .../tool/asynctool/AsyncToolListRequest.java | 33 + .../asynctool/AsyncToolObserveRequest.java | 74 ++ .../tool/asynctool/AsyncToolOptions.java | 196 +++++ .../asynctool/AsyncToolResultRequest.java | 74 ++ .../runtime/tool/asynctool/AsyncToolSpec.java | 173 +++++ .../tool/asynctool/AsyncToolSpecExpander.java | 691 ++++++++++++++++++ .../tool/asynctool/AsyncToolSubmitResult.java | 153 ++++ .../tool/asynctool/AsyncToolTaskEvent.java | 116 +++ .../asynctool/AsyncToolTaskListResult.java | 76 ++ .../tool/asynctool/AsyncToolTaskStatus.java | 78 ++ .../tool/asynctool/AsyncToolTaskSummary.java | 132 ++++ .../tool/asynctool/AsyncToolTaskView.java | 312 ++++++++ .../AgentScopeStatefulRuntimeTest.java | 30 + .../asynctool/AsyncToolSpecExpanderTest.java | 408 +++++++++++ 22 files changed, 2894 insertions(+), 2 deletions(-) create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncSubTools.java create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolCancelRequest.java create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolCancelResult.java create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolListRequest.java create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolObserveRequest.java create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolOptions.java create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolResultRequest.java create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSpec.java create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSpecExpander.java create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSubmitResult.java create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskEvent.java create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskListResult.java create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskStatus.java create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskSummary.java create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskView.java create mode 100644 easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSpecExpanderTest.java diff --git a/easy-agents-agent-runtime/pom.xml b/easy-agents-agent-runtime/pom.xml index 0857fd0..234d859 100644 --- a/easy-agents-agent-runtime/pom.xml +++ b/easy-agents-agent-runtime/pom.xml @@ -23,6 +23,11 @@ agentscope + + com.alibaba.fastjson2 + fastjson2 + + com.anthropic anthropic-java diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeReActRuntime.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeReActRuntime.java index 4d4497c..27e073d 100644 --- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeReActRuntime.java +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeReActRuntime.java @@ -54,6 +54,18 @@ import java.util.function.Supplier; */ public class AgentScopeReActRuntime implements AgentRuntime { + private static final String ASYNC_TOOL_SYSTEM_PROMPT = """ + + Async tool protocol: + - Async tools may expose submit, observe, result, cancel, and list sub-tools. Treat these sub-tools as one user-facing tool. + - Do not ask the user to choose submit, observe, result, cancel, or list. These are internal execution phases. + - For a normal user request to use an async tool, call its submit sub-tool first with the user-provided arguments by default. + - After submit returns task_id, immediately call observe with that task_id to check progress. + - If the task is completed and result is available, use the returned result to answer the user. + - If the task is still running after observation, tell the user that the task is running and keep task_id/next_action for later tool calls. + - Use result, list, or cancel directly only when the user explicitly asks to get a known task result, list tasks, or cancel a task. + """; + private final AgentScopeModelFactory modelFactory; private final AgentScopeToolAdapter toolAdapter; private final AgentScopeKnowledgeAdapter knowledgeAdapter; @@ -1090,7 +1102,7 @@ public class AgentScopeReActRuntime implements AgentRuntime { ReActAgent.Builder builder = ReActAgent.builder() .name(definition.getAgentName()) .description(definition.getDescription()) - .sysPrompt(definition.getSystemPrompt()) + .sysPrompt(systemPrompt(definition)) .model(model) .toolkit(toolkit) .memory(memory) @@ -1110,6 +1122,30 @@ public class AgentScopeReActRuntime implements AgentRuntime { return builder.build(); } + private String systemPrompt(AgentDefinition definition) { + String prompt = definition.getSystemPrompt(); + if (!hasAsyncTool(definition)) { + return prompt; + } + if (prompt == null || prompt.isBlank()) { + return ASYNC_TOOL_SYSTEM_PROMPT.strip(); + } + return prompt.stripTrailing() + ASYNC_TOOL_SYSTEM_PROMPT; + } + + private boolean hasAsyncTool(AgentDefinition definition) { + if (definition == null || definition.getToolSpecs() == null) { + return false; + } + for (AgentToolSpec toolSpec : definition.getToolSpecs()) { + // AsyncToolSpecExpander marks all generated sub-tools with this runtime metadata. + if (toolSpec != null && Boolean.TRUE.equals(toolSpec.getMetadata().get("asyncTool"))) { + return true; + } + } + return false; + } + /** * 构建 AgentScope Toolkit,并返回按 Skill ID 分组的工具。 * diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeToolAdapter.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeToolAdapter.java index c586ea2..ccf460b 100644 --- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeToolAdapter.java +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeToolAdapter.java @@ -364,6 +364,7 @@ public class AgentScopeToolAdapter { 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()); @@ -372,7 +373,7 @@ public class AgentScopeToolAdapter { } /** - * 将运行时结果转换为 AgentScope 结果块。 + * 将 AgentToolResult 转换为 AgentScope 结果块。 * * @param param 工具调用参数 * @param result 运行时结果 diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeEventType.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeEventType.java index 2a5bc1a..545ee30 100644 --- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeEventType.java +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeEventType.java @@ -39,6 +39,36 @@ public enum AgentRuntimeEventType { */ TOOL_RESULT, + /** + * 异步工具已提交任务。 + */ + ASYNC_TOOL_SUBMITTED, + + /** + * 异步工具已观察任务状态。 + */ + ASYNC_TOOL_OBSERVED, + + /** + * 异步工具已读取任务结果。 + */ + ASYNC_TOOL_RESULT, + + /** + * 异步工具已请求取消任务。 + */ + ASYNC_TOOL_CANCELLED, + + /** + * 异步工具已查询任务列表。 + */ + ASYNC_TOOL_LISTED, + + /** + * 异步工具执行失败。 + */ + ASYNC_TOOL_FAILED, + /** * 知识库检索完成并返回文档摘要。 */ diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/AgentToolContext.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/AgentToolContext.java index 357eb18..a0e383c 100644 --- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/AgentToolContext.java +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/AgentToolContext.java @@ -1,9 +1,11 @@ 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; /** * 传递给动态工具调用的上下文。 @@ -17,6 +19,7 @@ public class AgentToolContext { private String toolCallId; private AgentRuntimeContext runtimeContext = new AgentRuntimeContext(); private Map metadata = new LinkedHashMap<>(); + private Consumer eventEmitter; /** * 获取请求ID。 @@ -143,4 +146,36 @@ public class AgentToolContext { public void setMetadata(Map metadata) { this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; } + + /** + * 发射运行时旁路事件。 + * + *

该方法主要供 runtime 包装层发射业务无关事件,例如异步工具生命周期事件。 + * 未配置事件发射器时静默忽略,避免非流式测试路径产生额外副作用。

+ * + * @param event 运行时事件 + */ + public void emitEvent(AgentRuntimeEvent event) { + if (eventEmitter != null && event != null) { + eventEmitter.accept(event); + } + } + + /** + * 获取运行时事件发射器。 + * + * @return 运行时事件发射器 + */ + public Consumer getEventEmitter() { + return eventEmitter; + } + + /** + * 设置运行时事件发射器。 + * + * @param eventEmitter 运行时事件发射器 + */ + public void setEventEmitter(Consumer eventEmitter) { + this.eventEmitter = eventEmitter; + } } diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncSubTools.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncSubTools.java new file mode 100644 index 0000000..eea9d4a --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncSubTools.java @@ -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 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); +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolCancelRequest.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolCancelRequest.java new file mode 100644 index 0000000..181c873 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolCancelRequest.java @@ -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; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolCancelResult.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolCancelResult.java new file mode 100644 index 0000000..043d09c --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolCancelResult.java @@ -0,0 +1,131 @@ +package com.easyagents.agent.runtime.tool.asynctool; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 异步工具取消结果。 + */ +public class AsyncToolCancelResult { + + private String taskId; + private AsyncToolTaskStatus status; + private String message; + private String errorMessage; + private Map payload = new LinkedHashMap<>(); + private Map metadata = new LinkedHashMap<>(); + + /** + * 创建空取消结果。 + */ + public AsyncToolCancelResult() { + } + + /** + * 获取任务 ID。 + * + * @return 任务 ID + */ + public String getTaskId() { + return taskId; + } + + /** + * 设置任务 ID。 + * + * @param taskId 任务 ID + */ + public void setTaskId(String taskId) { + this.taskId = taskId; + } + + /** + * 获取任务状态。 + * + * @return 任务状态 + */ + public AsyncToolTaskStatus getStatus() { + return status; + } + + /** + * 设置任务状态。 + * + * @param status 任务状态 + */ + public void setStatus(AsyncToolTaskStatus status) { + this.status = status; + } + + /** + * 获取取消消息。 + * + * @return 取消消息 + */ + public String getMessage() { + return message; + } + + /** + * 设置取消消息。 + * + * @param message 取消消息 + */ + public void setMessage(String message) { + this.message = message; + } + + /** + * 获取错误消息。 + * + * @return 错误消息 + */ + public String getErrorMessage() { + return errorMessage; + } + + /** + * 设置错误消息。 + * + * @param errorMessage 错误消息 + */ + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + /** + * 获取业务扩展载荷。 + * + * @return 业务扩展载荷 + */ + public Map getPayload() { + return payload; + } + + /** + * 设置业务扩展载荷。 + * + * @param payload 业务扩展载荷 + */ + public void setPayload(Map payload) { + this.payload = payload == null ? new LinkedHashMap<>() : payload; + } + + /** + * 获取元数据。 + * + * @return 元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置元数据。 + * + * @param metadata 元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolListRequest.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolListRequest.java new file mode 100644 index 0000000..f3f55b7 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolListRequest.java @@ -0,0 +1,33 @@ +package com.easyagents.agent.runtime.tool.asynctool; + +/** + * 异步工具列表请求。 + */ +public class AsyncToolListRequest { + + private AsyncToolTaskStatus status; + + /** + * 创建空列表请求。 + */ + public AsyncToolListRequest() { + } + + /** + * 获取状态过滤条件。 + * + * @return 状态过滤条件 + */ + public AsyncToolTaskStatus getStatus() { + return status; + } + + /** + * 设置状态过滤条件。 + * + * @param status 状态过滤条件 + */ + public void setStatus(AsyncToolTaskStatus status) { + this.status = status; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolObserveRequest.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolObserveRequest.java new file mode 100644 index 0000000..2b549bf --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolObserveRequest.java @@ -0,0 +1,74 @@ +package com.easyagents.agent.runtime.tool.asynctool; + +/** + * 异步工具观察请求。 + */ +public class AsyncToolObserveRequest { + + private String taskId; + /** + * 调用方已读取到的事件位置,用于增量读取任务事件,避免重复返回全量日志。 + */ + private Long cursor; + private Integer limit; + + /** + * 创建空观察请求。 + */ + public AsyncToolObserveRequest() { + } + + /** + * 获取任务 ID。 + * + * @return 任务 ID + */ + public String getTaskId() { + return taskId; + } + + /** + * 设置任务 ID。 + * + * @param taskId 任务 ID + */ + public void setTaskId(String taskId) { + this.taskId = taskId; + } + + /** + * 获取已读事件位置。 + * + * @return 已读事件位置 + */ + public Long getCursor() { + return cursor; + } + + /** + * 设置已读事件位置。 + * + * @param cursor 已读事件位置 + */ + public void setCursor(Long cursor) { + this.cursor = cursor; + } + + /** + * 获取事件读取数量。 + * + * @return 事件读取数量 + */ + public Integer getLimit() { + return limit; + } + + /** + * 设置事件读取数量。 + * + * @param limit 事件读取数量 + */ + public void setLimit(Integer limit) { + this.limit = limit; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolOptions.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolOptions.java new file mode 100644 index 0000000..654e1b8 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolOptions.java @@ -0,0 +1,196 @@ +package com.easyagents.agent.runtime.tool.asynctool; + +import java.time.Duration; + +/** + * 异步工具 runtime 包装层的通用选项。 + */ +public class AsyncToolOptions { + + private Duration submitTimeout = Duration.ofSeconds(5); + private Duration observeTimeout = Duration.ofSeconds(3); + private Duration resultTimeout = Duration.ofSeconds(3); + private Duration cancelTimeout = Duration.ofSeconds(3); + private Duration listTimeout = Duration.ofSeconds(3); + private int defaultEventLimit = 20; + private int maxEventLimit = 100; + private int maxModelContentLength = 1200; + private int maxEventTextLength = 800; + + /** + * 创建默认异步工具选项实例。 + */ + public AsyncToolOptions() { + } + + /** + * 创建默认异步工具选项。 + * + * @return 默认选项 + */ + public static AsyncToolOptions defaults() { + return new AsyncToolOptions(); + } + + /** + * 获取提交子工具超时时间。 + * + * @return 超时时间 + */ + public Duration getSubmitTimeout() { + return submitTimeout; + } + + /** + * 设置提交子工具超时时间。 + * + * @param submitTimeout 超时时间 + */ + public void setSubmitTimeout(Duration submitTimeout) { + this.submitTimeout = submitTimeout == null ? Duration.ofSeconds(5) : submitTimeout; + } + + /** + * 获取观察子工具超时时间。 + * + * @return 超时时间 + */ + public Duration getObserveTimeout() { + return observeTimeout; + } + + /** + * 设置观察子工具超时时间。 + * + * @param observeTimeout 超时时间 + */ + public void setObserveTimeout(Duration observeTimeout) { + this.observeTimeout = observeTimeout == null ? Duration.ofSeconds(3) : observeTimeout; + } + + /** + * 获取结果子工具超时时间。 + * + * @return 超时时间 + */ + public Duration getResultTimeout() { + return resultTimeout; + } + + /** + * 设置结果子工具超时时间。 + * + * @param resultTimeout 超时时间 + */ + public void setResultTimeout(Duration resultTimeout) { + this.resultTimeout = resultTimeout == null ? Duration.ofSeconds(3) : resultTimeout; + } + + /** + * 获取取消子工具超时时间。 + * + * @return 超时时间 + */ + public Duration getCancelTimeout() { + return cancelTimeout; + } + + /** + * 设置取消子工具超时时间。 + * + * @param cancelTimeout 超时时间 + */ + public void setCancelTimeout(Duration cancelTimeout) { + this.cancelTimeout = cancelTimeout == null ? Duration.ofSeconds(3) : cancelTimeout; + } + + /** + * 获取列表子工具超时时间。 + * + * @return 超时时间 + */ + public Duration getListTimeout() { + return listTimeout; + } + + /** + * 设置列表子工具超时时间。 + * + * @param listTimeout 超时时间 + */ + public void setListTimeout(Duration listTimeout) { + this.listTimeout = listTimeout == null ? Duration.ofSeconds(3) : listTimeout; + } + + /** + * 获取默认事件读取数量。 + * + * @return 默认事件数量 + */ + public int getDefaultEventLimit() { + return defaultEventLimit; + } + + /** + * 设置默认事件读取数量。 + * + * @param defaultEventLimit 默认事件数量 + */ + public void setDefaultEventLimit(int defaultEventLimit) { + this.defaultEventLimit = defaultEventLimit <= 0 ? 20 : defaultEventLimit; + } + + /** + * 获取最大事件读取数量。 + * + * @return 最大事件数量 + */ + public int getMaxEventLimit() { + return maxEventLimit; + } + + /** + * 设置最大事件读取数量。 + * + * @param maxEventLimit 最大事件数量 + */ + public void setMaxEventLimit(int maxEventLimit) { + this.maxEventLimit = maxEventLimit <= 0 ? 100 : maxEventLimit; + } + + /** + * 获取模型可见内容最大长度。 + * + * @return 最大长度 + */ + public int getMaxModelContentLength() { + return maxModelContentLength; + } + + /** + * 设置模型可见内容最大长度。 + * + * @param maxModelContentLength 最大长度 + */ + public void setMaxModelContentLength(int maxModelContentLength) { + this.maxModelContentLength = maxModelContentLength <= 0 ? 1200 : maxModelContentLength; + } + + /** + * 获取事件文本最大长度。 + * + * @return 最大长度 + */ + public int getMaxEventTextLength() { + return maxEventTextLength; + } + + /** + * 设置事件文本最大长度。 + * + * @param maxEventTextLength 最大长度 + */ + public void setMaxEventTextLength(int maxEventTextLength) { + this.maxEventTextLength = maxEventTextLength <= 0 ? 800 : maxEventTextLength; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolResultRequest.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolResultRequest.java new file mode 100644 index 0000000..77d64a9 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolResultRequest.java @@ -0,0 +1,74 @@ +package com.easyagents.agent.runtime.tool.asynctool; + +/** + * 异步工具结果请求。 + */ +public class AsyncToolResultRequest { + + private String taskId; + /** + * 调用方已读取到的事件位置,用于增量读取任务事件,避免 result 重复返回全量日志。 + */ + private Long cursor; + private Integer limit; + + /** + * 创建空结果请求。 + */ + public AsyncToolResultRequest() { + } + + /** + * 获取任务 ID。 + * + * @return 任务 ID + */ + public String getTaskId() { + return taskId; + } + + /** + * 设置任务 ID。 + * + * @param taskId 任务 ID + */ + public void setTaskId(String taskId) { + this.taskId = taskId; + } + + /** + * 获取已读事件位置。 + * + * @return 已读事件位置 + */ + public Long getCursor() { + return cursor; + } + + /** + * 设置已读事件位置。 + * + * @param cursor 已读事件位置 + */ + public void setCursor(Long cursor) { + this.cursor = cursor; + } + + /** + * 获取事件读取数量。 + * + * @return 事件读取数量 + */ + public Integer getLimit() { + return limit; + } + + /** + * 设置事件读取数量。 + * + * @param limit 事件读取数量 + */ + public void setLimit(Integer limit) { + this.limit = limit; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSpec.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSpec.java new file mode 100644 index 0000000..4fd0fb6 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSpec.java @@ -0,0 +1,173 @@ +package com.easyagents.agent.runtime.tool.asynctool; + +import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 异步工具声明。 + * + *

一个声明会被 runtime 展开为 submit、observe、result、cancel 和 list 五个普通工具。

+ */ +public class AsyncToolSpec { + + private String name; + private String description; + private Map submitParametersSchema = new LinkedHashMap<>(); + private AsyncSubTools subTools; + private AsyncToolOptions options = AsyncToolOptions.defaults(); + private boolean approvalRequired; + private AgentToolApprovalRequest approvalRequest = new AgentToolApprovalRequest(); + private Map metadata = new LinkedHashMap<>(); + + /** + * 创建空异步工具声明。 + */ + public AsyncToolSpec() { + } + + /** + * 获取异步工具基础名称。 + * + * @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; + } + + /** + * 获取提交子工具参数 Schema。 + * + * @return 参数 Schema + */ + public Map getSubmitParametersSchema() { + return submitParametersSchema; + } + + /** + * 设置提交子工具参数 Schema。 + * + * @param submitParametersSchema 参数 Schema + */ + public void setSubmitParametersSchema(Map submitParametersSchema) { + this.submitParametersSchema = submitParametersSchema == null ? new LinkedHashMap<>() : submitParametersSchema; + } + + /** + * 获取业务子工具实现。 + * + * @return 子工具实现 + */ + public AsyncSubTools getSubTools() { + return subTools; + } + + /** + * 设置业务子工具实现。 + * + * @param subTools 子工具实现 + */ + public void setSubTools(AsyncSubTools subTools) { + this.subTools = subTools; + } + + /** + * 获取异步工具选项。 + * + * @return 工具选项 + */ + public AsyncToolOptions getOptions() { + return options; + } + + /** + * 设置异步工具选项。 + * + * @param options 工具选项 + */ + public void setOptions(AsyncToolOptions options) { + this.options = options == null ? AsyncToolOptions.defaults() : options; + } + + /** + * 返回提交子工具是否需要人工审批。 + * + * @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 getMetadata() { + return metadata; + } + + /** + * 设置元数据。 + * + * @param metadata 元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSpecExpander.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSpecExpander.java new file mode 100644 index 0000000..9916d6f --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSpecExpander.java @@ -0,0 +1,691 @@ +package com.easyagents.agent.runtime.tool.asynctool; + +import com.alibaba.fastjson2.JSON; +import com.easyagents.agent.runtime.AgentRuntimeException; +import com.easyagents.agent.runtime.event.AgentRuntimeEvent; +import com.easyagents.agent.runtime.event.AgentRuntimeEventType; +import com.easyagents.agent.runtime.tool.AgentToolCategory; +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 com.easyagents.agent.runtime.tool.AgentToolVisibility; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import java.util.regex.Pattern; + +/** + * 异步工具声明展开器。 + * + *

该类将一个业务无关的 {@link AsyncToolSpec} 展开为五个普通 + * {@link AgentToolSpec} 与 {@link AgentToolInvoker},业务方只需要实现 + * {@link AsyncSubTools}。

+ */ +public class AsyncToolSpecExpander { + + private static final Pattern SAFE_NAME = Pattern.compile("^[a-z][a-z0-9_]*$"); + private static final String PHASE_SUBMIT = "submit"; + private static final String PHASE_OBSERVE = "observe"; + private static final String PHASE_RESULT = "result"; + private static final String PHASE_CANCEL = "cancel"; + private static final String PHASE_LIST = "list"; + private static final String ERROR_TYPE_TIMEOUT = "TIMEOUT"; + private static final String ERROR_TYPE_EXCEPTION = "EXCEPTION"; + + private final ExecutorService executor; + + /** + * 使用公共 ForkJoinPool 创建展开器。 + */ + public AsyncToolSpecExpander() { + this(ForkJoinPool.commonPool()); + } + + /** + * 使用指定执行器创建展开器。 + * + * @param executor 执行器 + */ + public AsyncToolSpecExpander(Executor executor) { + if (executor instanceof ExecutorService executorService) { + this.executor = executorService; + } else { + this.executor = new DelegatingExecutorService(executor); + } + } + + /** + * 展开工具声明。 + * + * @param spec 异步工具声明 + * @return 五个普通工具声明 + */ + public List expandSpecs(AsyncToolSpec spec) { + AsyncToolSpec safeSpec = validate(spec); + List specs = new ArrayList<>(5); + specs.add(toolSpec(safeSpec, PHASE_SUBMIT, safeSpec.getSubmitParametersSchema(), submitOutputSchema())); + specs.add(toolSpec(safeSpec, PHASE_OBSERVE, observeSchema(safeSpec), taskViewOutputSchema())); + specs.add(toolSpec(safeSpec, PHASE_RESULT, observeSchema(safeSpec), taskViewOutputSchema())); + specs.add(toolSpec(safeSpec, PHASE_CANCEL, cancelSchema(), cancelOutputSchema())); + specs.add(toolSpec(safeSpec, PHASE_LIST, listSchema(), listOutputSchema())); + return specs; + } + + /** + * 展开工具调用器。 + * + * @param spec 异步工具声明 + * @return 按工具名索引的调用器 + */ + public Map expandInvokers(AsyncToolSpec spec) { + AsyncToolSpec safeSpec = validate(spec); + Map invokers = new LinkedHashMap<>(); + invokers.put(toolName(safeSpec, PHASE_SUBMIT), (arguments, context) -> submit(safeSpec, arguments, context)); + invokers.put(toolName(safeSpec, PHASE_OBSERVE), (arguments, context) -> observe(safeSpec, arguments, context)); + invokers.put(toolName(safeSpec, PHASE_RESULT), (arguments, context) -> result(safeSpec, arguments, context)); + invokers.put(toolName(safeSpec, PHASE_CANCEL), (arguments, context) -> cancel(safeSpec, arguments, context)); + invokers.put(toolName(safeSpec, PHASE_LIST), (arguments, context) -> list(safeSpec, arguments, context)); + return invokers; + } + + private AsyncToolSpec validate(AsyncToolSpec spec) { + if (spec == null) { + throw new AgentRuntimeException("Async tool spec is required."); + } + if (spec.getName() == null || spec.getName().isBlank()) { + throw new AgentRuntimeException("Async tool name is required."); + } + if (!SAFE_NAME.matcher(spec.getName()).matches()) { + throw new AgentRuntimeException("Async tool name must be safe snake_case: " + spec.getName()); + } + if (spec.getSubTools() == null) { + throw new AgentRuntimeException("Async sub tools are required: " + spec.getName()); + } + if (spec.getSubmitParametersSchema() == null || spec.getSubmitParametersSchema().isEmpty()) { + spec.setSubmitParametersSchema(emptyObjectSchema()); + } + if (spec.getOptions() == null) { + spec.setOptions(AsyncToolOptions.defaults()); + } + return spec; + } + + private AgentToolSpec toolSpec(AsyncToolSpec spec, + String phase, + Map parametersSchema, + Map outputSchema) { + AgentToolSpec toolSpec = new AgentToolSpec(); + toolSpec.setName(toolName(spec, phase)); + toolSpec.setDescription(description(spec, phase)); + toolSpec.setCategory(AgentToolCategory.CUSTOM); + toolSpec.setVisibility(AgentToolVisibility.VISIBLE); + toolSpec.setParametersSchema(parametersSchema); + toolSpec.setOutputSchema(outputSchema); + toolSpec.setApprovalRequired(PHASE_SUBMIT.equals(phase) && spec.isApprovalRequired()); + toolSpec.setApprovalRequest(spec.getApprovalRequest()); + Map metadata = new LinkedHashMap<>(); + metadata.putAll(spec.getMetadata()); + metadata.put("asyncTool", true); + metadata.put("asyncToolName", spec.getName()); + metadata.put("asyncToolPhase", phase); + toolSpec.setMetadata(metadata); + return toolSpec; + } + + private String description(AsyncToolSpec spec, String phase) { + String prefix = spec.getDescription() == null || spec.getDescription().isBlank() + ? "Async tool " + spec.getName() + : spec.getDescription(); + return switch (phase) { + case PHASE_SUBMIT -> prefix + + " This is the default entry point when the user asks to run this tool. Submit an asynchronous task with the normal tool arguments and return task_id."; + case PHASE_OBSERVE -> prefix + + " Use immediately after submit with the returned task_id to check progress and incremental events. Do not ask the user for task_id immediately after submit."; + case PHASE_RESULT -> prefix + + " Use only when a known task_id should return the final result, or the current observation if the task is still running."; + case PHASE_CANCEL -> prefix + + " Use only when the user explicitly asks to cancel a known asynchronous task by task_id."; + case PHASE_LIST -> prefix + + " Use only when the user explicitly asks to list visible asynchronous tasks in the current context."; + default -> prefix; + }; + } + + private AgentToolResult submit(AsyncToolSpec spec, Map arguments, AgentToolContext context) { + return execute(spec, PHASE_SUBMIT, context, spec.getOptions().getSubmitTimeout(), + guardedContext -> { + AsyncToolSubmitResult result = spec.getSubTools().submit(safeMap(arguments), guardedContext); + return wrapSubmit(spec, result, guardedContext); + }); + } + + private AgentToolResult observe(AsyncToolSpec spec, Map arguments, AgentToolContext context) { + return execute(spec, PHASE_OBSERVE, context, spec.getOptions().getObserveTimeout(), + guardedContext -> { + AsyncToolObserveRequest request = observeRequest(arguments, spec.getOptions()); + return wrapTaskView(spec, PHASE_OBSERVE, + spec.getSubTools().observe(request, guardedContext), guardedContext); + }); + } + + private AgentToolResult result(AsyncToolSpec spec, Map arguments, AgentToolContext context) { + return execute(spec, PHASE_RESULT, context, spec.getOptions().getResultTimeout(), + guardedContext -> { + AsyncToolResultRequest request = resultRequest(arguments, spec.getOptions()); + return wrapTaskView(spec, PHASE_RESULT, + spec.getSubTools().result(request, guardedContext), guardedContext); + }); + } + + private AgentToolResult cancel(AsyncToolSpec spec, Map arguments, AgentToolContext context) { + return execute(spec, PHASE_CANCEL, context, spec.getOptions().getCancelTimeout(), + guardedContext -> { + AsyncToolCancelRequest request = cancelRequest(arguments); + return wrapCancel(spec, spec.getSubTools().cancel(request, guardedContext), guardedContext); + }); + } + + private AgentToolResult list(AsyncToolSpec spec, Map arguments, AgentToolContext context) { + return execute(spec, PHASE_LIST, context, spec.getOptions().getListTimeout(), + guardedContext -> { + AsyncToolListRequest request = listRequest(arguments); + return wrapList(spec, spec.getSubTools().list(request, guardedContext), guardedContext); + }); + } + + private AgentToolResult execute(AsyncToolSpec spec, + String phase, + AgentToolContext context, + Duration timeout, + Function supplier) { + AtomicBoolean active = new AtomicBoolean(true); + AgentToolContext guardedContext = guardedContext(context, active); + Future future = executor.submit(() -> supplier.apply(guardedContext)); + try { + return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS); + } catch (TimeoutException error) { + active.set(false); + future.cancel(true); + AgentToolResult result = failureResult(spec, phase, null, AsyncToolTaskStatus.TIMEOUT, + ERROR_TYPE_TIMEOUT, "Async tool " + phase + " timed out."); + emitFailure(spec, phase, context, null, AsyncToolTaskStatus.TIMEOUT, result.getErrorMessage()); + return result; + } catch (InterruptedException error) { + active.set(false); + Thread.currentThread().interrupt(); + AgentToolResult result = failureResult(spec, phase, null, AsyncToolTaskStatus.FAILED, + ERROR_TYPE_EXCEPTION, "Async tool " + phase + " interrupted."); + emitFailure(spec, phase, context, null, AsyncToolTaskStatus.FAILED, result.getErrorMessage()); + return result; + } catch (ExecutionException error) { + active.set(false); + Throwable cause = error.getCause() == null ? error : error.getCause(); + String message = cause.getMessage() == null || cause.getMessage().isBlank() + ? "Async tool " + phase + " failed." + : cause.getMessage(); + AgentToolResult result = failureResult(spec, phase, null, AsyncToolTaskStatus.FAILED, + ERROR_TYPE_EXCEPTION, message); + emitFailure(spec, phase, context, null, AsyncToolTaskStatus.FAILED, result.getErrorMessage()); + return result; + } + } + + private AgentToolContext guardedContext(AgentToolContext source, AtomicBoolean active) { + if (source == null) { + return null; + } + AgentToolContext context = new AgentToolContext(); + context.setRequestId(source.getRequestId()); + context.setTraceId(source.getTraceId()); + context.setSessionId(source.getSessionId()); + context.setAgentId(source.getAgentId()); + context.setToolCallId(source.getToolCallId()); + context.setRuntimeContext(source.getRuntimeContext()); + context.setMetadata(new LinkedHashMap<>(source.getMetadata())); + context.setEventEmitter(event -> { + // 超时后底层业务可能仍在运行,迟到事件不能再覆盖 runtime 已返回的失败语义。 + if (active.get()) { + source.emitEvent(event); + } + }); + return context; + } + + private AgentToolResult wrapSubmit(AsyncToolSpec spec, AsyncToolSubmitResult result, AgentToolContext context) { + AsyncToolSubmitResult safe = result == null ? new AsyncToolSubmitResult() : result; + AsyncToolTaskStatus status = status(safe.getStatus(), AsyncToolTaskStatus.RUNNING); + safe.setStatus(status); + if (safe.getTaskId() == null || safe.getTaskId().isBlank()) { + String message = "Async tool submit must return taskId."; + AgentToolResult toolResult = failureResult(spec, PHASE_SUBMIT, null, AsyncToolTaskStatus.FAILED, + ERROR_TYPE_EXCEPTION, message, safe); + emitFailure(spec, PHASE_SUBMIT, context, null, AsyncToolTaskStatus.FAILED, message); + return toolResult; + } + if (safe.getNextAction() == null || safe.getNextAction().isBlank()) { + safe.setNextAction(toolName(spec, PHASE_OBSERVE) + " 查看任务进度。"); + } + AgentToolResult toolResult = successResult(spec, PHASE_SUBMIT, safe.getTaskId(), status, + modelContent(safe.getTaskId(), status, safe.getNextAction(), safe.getSummary()), safe); + emit(spec, PHASE_SUBMIT, AgentRuntimeEventType.ASYNC_TOOL_SUBMITTED, context, safe.getTaskId(), status, + safe.getCursor(), null, safe.getSummary(), null); + return toolResult; + } + + private AgentToolResult wrapTaskView(AsyncToolSpec spec, + String phase, + AsyncToolTaskView view, + AgentToolContext context) { + AsyncToolTaskView safe = view == null ? new AsyncToolTaskView() : view; + AsyncToolTaskStatus status = status(safe.getStatus(), AsyncToolTaskStatus.RUNNING); + safe.setStatus(status); + if (safe.getTerminal() == null) { + safe.setTerminal(status.isTerminal()); + } + if (safe.getResultAvailable() == null) { + safe.setResultAvailable(status.isSuccess() && safe.getResult() != null); + } + if (safe.getNextAction() == null || safe.getNextAction().isBlank()) { + safe.setNextAction(status.isTerminal() + ? "任务已结束。" + : toolName(spec, PHASE_OBSERVE) + " 继续查看任务进度。"); + } + AgentToolResult toolResult = successResult(spec, phase, safe.getTaskId(), status, + modelContent(safe.getTaskId(), status, safe.getNextAction(), safe.getSummary(), + Boolean.TRUE.equals(safe.getResultAvailable()), safe.getResult()), safe); + emit(spec, phase, PHASE_RESULT.equals(phase) + ? AgentRuntimeEventType.ASYNC_TOOL_RESULT + : AgentRuntimeEventType.ASYNC_TOOL_OBSERVED, + context, safe.getTaskId(), status, safe.getCursor(), safe.getNextCursor(), safe.getSummary(), + safe.getErrorMessage(), safe.getResultAvailable()); + return toolResult; + } + + private AgentToolResult wrapCancel(AsyncToolSpec spec, AsyncToolCancelResult result, AgentToolContext context) { + AsyncToolCancelResult safe = result == null ? new AsyncToolCancelResult() : result; + AsyncToolTaskStatus status = status(safe.getStatus(), AsyncToolTaskStatus.CANCELLING); + safe.setStatus(status); + boolean success = safe.getErrorMessage() == null || safe.getErrorMessage().isBlank(); + AgentToolResult toolResult = success + ? successResult(spec, PHASE_CANCEL, safe.getTaskId(), status, + modelContent(safe.getTaskId(), status, "继续使用 " + toolName(spec, PHASE_OBSERVE) + " 查看取消状态。", + safe.getMessage()), safe) + : failureResult(spec, PHASE_CANCEL, safe.getTaskId(), status, ERROR_TYPE_EXCEPTION, safe.getErrorMessage(), safe); + emit(spec, PHASE_CANCEL, success ? AgentRuntimeEventType.ASYNC_TOOL_CANCELLED : AgentRuntimeEventType.ASYNC_TOOL_FAILED, + context, safe.getTaskId(), status, null, null, safe.getMessage(), safe.getErrorMessage()); + return toolResult; + } + + private AgentToolResult wrapList(AsyncToolSpec spec, AsyncToolTaskListResult result, AgentToolContext context) { + AsyncToolTaskListResult safe = result == null ? new AsyncToolTaskListResult() : result; + String summary = "共 " + safe.getTasks().size() + " 个任务。"; + AgentToolResult toolResult = successResult(spec, PHASE_LIST, null, null, + modelContent(null, null, "按 task_id 使用观察或结果工具查看详情。", summary), safe); + emit(spec, PHASE_LIST, AgentRuntimeEventType.ASYNC_TOOL_LISTED, context, null, null, null, null, summary, null); + return toolResult; + } + + private AgentToolResult successResult(AsyncToolSpec spec, + String phase, + String taskId, + AsyncToolTaskStatus status, + String modelContent, + Object displayContent) { + AgentToolResult result = AgentToolResult.success(truncate(modelContent, spec.getOptions().getMaxModelContentLength())); + result.setDisplayContent(displayContent); + result.setMetadata(metadata(spec, phase, taskId, status)); + return result; + } + + private AgentToolResult failureResult(AsyncToolSpec spec, + String phase, + String taskId, + AsyncToolTaskStatus status, + String errorType, + String errorMessage) { + return failureResult(spec, phase, taskId, status, errorType, errorMessage, null); + } + + private AgentToolResult failureResult(AsyncToolSpec spec, + String phase, + String taskId, + AsyncToolTaskStatus status, + String errorType, + String errorMessage, + Object displayContent) { + String message = errorMessage == null || errorMessage.isBlank() ? "Async tool failed." : errorMessage; + AgentToolResult result = AgentToolResult.failure(message); + result.setDisplayContent(displayContent == null ? Map.of("errorType", errorType, "message", message) : displayContent); + result.setMetadata(metadata(spec, phase, taskId, status)); + result.getMetadata().put("errorType", errorType); + return result; + } + + private void emitFailure(AsyncToolSpec spec, + String phase, + AgentToolContext context, + String taskId, + AsyncToolTaskStatus status, + String errorMessage) { + emit(spec, phase, AgentRuntimeEventType.ASYNC_TOOL_FAILED, context, taskId, status, null, null, null, errorMessage); + } + + private void emit(AsyncToolSpec spec, + String phase, + AgentRuntimeEventType type, + AgentToolContext context, + String taskId, + AsyncToolTaskStatus status, + Long cursor, + Long nextCursor, + String summary, + String errorMessage) { + emit(spec, phase, type, context, taskId, status, cursor, nextCursor, summary, errorMessage, null); + } + + private void emit(AsyncToolSpec spec, + String phase, + AgentRuntimeEventType type, + AgentToolContext context, + String taskId, + AsyncToolTaskStatus status, + Long cursor, + Long nextCursor, + String summary, + String errorMessage, + Boolean resultAvailable) { + if (context == null) { + return; + } + AgentRuntimeEvent event = AgentRuntimeEvent.of(type); + event.setTraceId(context.getTraceId()); + event.setSessionId(context.getSessionId()); + event.setAgentId(context.getAgentId()); + event.setToolCallId(context.getToolCallId()); + event.getMetadata().putAll(spec.getMetadata()); + putIfNotNull(event.getMetadata(), "requestId", context.getRequestId()); + event.getPayload().put("asyncToolName", spec.getName()); + event.getPayload().put("phase", phase); + putIfNotNull(event.getPayload(), "toolDisplayName", spec.getMetadata().get("toolDisplayName")); + putIfNotNull(event.getPayload(), "taskId", taskId); + putIfNotNull(event.getPayload(), "status", status == null ? null : status.name()); + putIfNotNull(event.getPayload(), "cursor", cursor); + putIfNotNull(event.getPayload(), "nextCursor", nextCursor); + putIfNotNull(event.getPayload(), "summary", truncate(summary, spec.getOptions().getMaxEventTextLength())); + putIfNotNull(event.getPayload(), "errorMessage", truncate(errorMessage, spec.getOptions().getMaxEventTextLength())); + putIfNotNull(event.getPayload(), "resultAvailable", resultAvailable); + event.getMetadata().put("asyncTool", true); + event.getMetadata().put("asyncToolName", spec.getName()); + event.getMetadata().put("asyncToolPhase", phase); + context.emitEvent(event); + } + + private Map metadata(AsyncToolSpec spec, String phase, String taskId, AsyncToolTaskStatus status) { + Map metadata = new LinkedHashMap<>(); + metadata.putAll(spec.getMetadata()); + metadata.put("asyncTool", true); + metadata.put("asyncToolName", spec.getName()); + metadata.put("asyncToolPhase", phase); + putIfNotNull(metadata, "taskId", taskId); + putIfNotNull(metadata, "status", status == null ? null : status.name()); + return metadata; + } + + private AsyncToolObserveRequest observeRequest(Map arguments, AsyncToolOptions options) { + AsyncToolObserveRequest request = new AsyncToolObserveRequest(); + request.setTaskId(stringValue(arguments, "taskId")); + request.setCursor(longValue(arguments, "cursor")); + request.setLimit(limit(arguments, options)); + return request; + } + + private AsyncToolResultRequest resultRequest(Map arguments, AsyncToolOptions options) { + AsyncToolResultRequest request = new AsyncToolResultRequest(); + request.setTaskId(stringValue(arguments, "taskId")); + request.setCursor(longValue(arguments, "cursor")); + request.setLimit(limit(arguments, options)); + return request; + } + + private AsyncToolCancelRequest cancelRequest(Map arguments) { + AsyncToolCancelRequest request = new AsyncToolCancelRequest(); + request.setTaskId(stringValue(arguments, "taskId")); + request.setReason(stringValue(arguments, "reason")); + return request; + } + + private AsyncToolListRequest listRequest(Map arguments) { + AsyncToolListRequest request = new AsyncToolListRequest(); + String status = stringValue(arguments, "status"); + if (status != null && !status.isBlank()) { + request.setStatus(AsyncToolTaskStatus.valueOf(status.trim().toUpperCase(Locale.ROOT))); + } + return request; + } + + private Integer limit(Map arguments, AsyncToolOptions options) { + Integer limit = intValue(arguments, "limit"); + if (limit == null || limit <= 0) { + return options.getDefaultEventLimit(); + } + return Math.min(limit, options.getMaxEventLimit()); + } + + private String modelContent(String taskId, AsyncToolTaskStatus status, String nextAction, String summary) { + return modelContent(taskId, status, nextAction, summary, false, null); + } + + private String modelContent(String taskId, + AsyncToolTaskStatus status, + String nextAction, + String summary, + boolean resultAvailable, + Object result) { + StringBuilder builder = new StringBuilder(); + if (taskId != null && !taskId.isBlank()) { + builder.append("task_id: ").append(taskId).append('\n'); + } + if (status != null) { + builder.append("status: ").append(status.name()).append('\n'); + } + if (summary != null && !summary.isBlank()) { + builder.append("summary: ").append(summary).append('\n'); + } + if (resultAvailable) { + builder.append("result_available: true").append('\n'); + builder.append("result: ").append(modelResult(result)).append('\n'); + } + if (nextAction != null && !nextAction.isBlank()) { + builder.append("next_action: ").append(nextAction); + } + return builder.toString(); + } + + private String modelResult(Object result) { + if (result == null) { + return ""; + } + if (result instanceof CharSequence + || result instanceof Number + || result instanceof Boolean + || result instanceof Character + || result instanceof Enum) { + return String.valueOf(result); + } + try { + return JSON.toJSONString(result); + } catch (Exception ignored) { + return String.valueOf(result); + } + } + + private String toolName(AsyncToolSpec spec, String phase) { + return spec.getName() + "_" + phase; + } + + private AsyncToolTaskStatus status(AsyncToolTaskStatus status, AsyncToolTaskStatus defaultStatus) { + return status == null ? defaultStatus : status; + } + + private Map emptyObjectSchema() { + Map schema = new LinkedHashMap<>(); + schema.put("type", "object"); + schema.put("properties", new LinkedHashMap<>()); + return schema; + } + + private Map observeSchema(AsyncToolSpec spec) { + Map schema = new LinkedHashMap<>(); + schema.put("type", "object"); + Map properties = new LinkedHashMap<>(); + properties.put("taskId", property("string", "Task id returned by submit.")); + properties.put("cursor", property("integer", "Event cursor returned by previous observe/result.")); + properties.put("limit", property("integer", "Maximum number of incremental events.")); + schema.put("properties", properties); + schema.put("required", List.of("taskId")); + return schema; + } + + private Map cancelSchema() { + Map schema = new LinkedHashMap<>(); + schema.put("type", "object"); + Map properties = new LinkedHashMap<>(); + properties.put("taskId", property("string", "Task id returned by submit.")); + properties.put("reason", property("string", "Optional cancellation reason.")); + schema.put("properties", properties); + schema.put("required", List.of("taskId")); + return schema; + } + + private Map listSchema() { + Map schema = new LinkedHashMap<>(); + schema.put("type", "object"); + Map properties = new LinkedHashMap<>(); + properties.put("status", property("string", "Optional task status filter.")); + schema.put("properties", properties); + return schema; + } + + private Map submitOutputSchema() { + return outputSchema("AsyncToolSubmitResult"); + } + + private Map taskViewOutputSchema() { + return outputSchema("AsyncToolTaskView"); + } + + private Map cancelOutputSchema() { + return outputSchema("AsyncToolCancelResult"); + } + + private Map listOutputSchema() { + return outputSchema("AsyncToolTaskListResult"); + } + + private Map outputSchema(String title) { + Map schema = new LinkedHashMap<>(); + schema.put("type", "object"); + schema.put("title", title); + return schema; + } + + private Map property(String type, String description) { + Map property = new LinkedHashMap<>(); + property.put("type", type); + property.put("description", description); + return property; + } + + private Map safeMap(Map arguments) { + return arguments == null ? new LinkedHashMap<>() : new LinkedHashMap<>(arguments); + } + + private String stringValue(Map arguments, String key) { + Object value = arguments == null ? null : arguments.get(key); + return value == null ? null : String.valueOf(value); + } + + private Long longValue(Map arguments, String key) { + Object value = arguments == null ? null : arguments.get(key); + if (value instanceof Number number) { + return number.longValue(); + } + if (value == null || String.valueOf(value).isBlank()) { + return null; + } + return Long.parseLong(String.valueOf(value)); + } + + private Integer intValue(Map arguments, String key) { + Object value = arguments == null ? null : arguments.get(key); + if (value instanceof Number number) { + return number.intValue(); + } + if (value == null || String.valueOf(value).isBlank()) { + return null; + } + return Integer.parseInt(String.valueOf(value)); + } + + private void putIfNotNull(Map target, String key, Object value) { + if (value != null) { + target.put(key, value); + } + } + + private String truncate(String value, int maxLength) { + if (value == null || value.length() <= maxLength) { + return value; + } + return value.substring(0, Math.max(0, maxLength)) + "..."; + } + + private static class DelegatingExecutorService extends AbstractExecutorService { + + private final Executor executor; + private volatile boolean shutdown; + + private DelegatingExecutorService(Executor executor) { + this.executor = executor == null ? ForkJoinPool.commonPool() : executor; + } + + @Override + public void shutdown() { + shutdown = true; + } + + @Override + public List shutdownNow() { + shutdown = true; + return List.of(); + } + + @Override + public boolean isShutdown() { + return shutdown; + } + + @Override + public boolean isTerminated() { + return shutdown; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) { + return shutdown; + } + + @Override + public void execute(Runnable command) { + executor.execute(command); + } + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSubmitResult.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSubmitResult.java new file mode 100644 index 0000000..3c6ffe1 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSubmitResult.java @@ -0,0 +1,153 @@ +package com.easyagents.agent.runtime.tool.asynctool; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 异步工具提交结果。 + */ +public class AsyncToolSubmitResult { + + private String taskId; + private AsyncToolTaskStatus status; + /** + * 提交后调用方已读取到的事件位置,后续 observe 可从该位置继续增量读取。 + */ + private Long cursor; + private String summary; + private String nextAction; + private Map payload = new LinkedHashMap<>(); + private Map metadata = new LinkedHashMap<>(); + + /** + * 创建空提交结果。 + */ + public AsyncToolSubmitResult() { + } + + /** + * 获取任务 ID。 + * + * @return 任务 ID + */ + public String getTaskId() { + return taskId; + } + + /** + * 设置任务 ID。 + * + * @param taskId 任务 ID + */ + public void setTaskId(String taskId) { + this.taskId = taskId; + } + + /** + * 获取任务状态。 + * + * @return 任务状态 + */ + public AsyncToolTaskStatus getStatus() { + return status; + } + + /** + * 设置任务状态。 + * + * @param status 任务状态 + */ + public void setStatus(AsyncToolTaskStatus status) { + this.status = status; + } + + /** + * 获取当前事件读取位置。 + * + * @return 当前事件读取位置 + */ + public Long getCursor() { + return cursor; + } + + /** + * 设置当前事件读取位置。 + * + * @param cursor 当前事件读取位置 + */ + public void setCursor(Long cursor) { + this.cursor = cursor; + } + + /** + * 获取摘要。 + * + * @return 摘要 + */ + public String getSummary() { + return summary; + } + + /** + * 设置摘要。 + * + * @param summary 摘要 + */ + public void setSummary(String summary) { + this.summary = summary; + } + + /** + * 获取下一步建议。 + * + * @return 下一步建议 + */ + public String getNextAction() { + return nextAction; + } + + /** + * 设置下一步建议。 + * + * @param nextAction 下一步建议 + */ + public void setNextAction(String nextAction) { + this.nextAction = nextAction; + } + + /** + * 获取业务扩展载荷。 + * + * @return 业务扩展载荷 + */ + public Map getPayload() { + return payload; + } + + /** + * 设置业务扩展载荷。 + * + * @param payload 业务扩展载荷 + */ + public void setPayload(Map payload) { + this.payload = payload == null ? new LinkedHashMap<>() : payload; + } + + /** + * 获取元数据。 + * + * @return 元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置元数据。 + * + * @param metadata 元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskEvent.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskEvent.java new file mode 100644 index 0000000..43fa66b --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskEvent.java @@ -0,0 +1,116 @@ +package com.easyagents.agent.runtime.tool.asynctool; + +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 异步工具任务事件。 + */ +public class AsyncToolTaskEvent { + + /** + * 任务内单调递增事件序号,用于 cursor 增量读取。 + */ + private Long sequence; + private String type; + private String text; + private Instant createdAt = Instant.now(); + private Map payload = new LinkedHashMap<>(); + + /** + * 创建空任务事件。 + */ + public AsyncToolTaskEvent() { + } + + /** + * 获取事件序号。 + * + * @return 事件序号 + */ + public Long getSequence() { + return sequence; + } + + /** + * 设置事件序号。 + * + * @param sequence 事件序号 + */ + public void setSequence(Long sequence) { + this.sequence = sequence; + } + + /** + * 获取事件类型。 + * + * @return 事件类型 + */ + public String getType() { + return type; + } + + /** + * 设置事件类型。 + * + * @param type 事件类型 + */ + public void setType(String type) { + this.type = type; + } + + /** + * 获取事件文本。 + * + * @return 事件文本 + */ + public String getText() { + return text; + } + + /** + * 设置事件文本。 + * + * @param text 事件文本 + */ + public void setText(String text) { + this.text = text; + } + + /** + * 获取事件创建时间。 + * + * @return 事件创建时间 + */ + public Instant getCreatedAt() { + return createdAt; + } + + /** + * 设置事件创建时间。 + * + * @param createdAt 事件创建时间 + */ + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt == null ? Instant.now() : createdAt; + } + + /** + * 获取业务扩展载荷。 + * + * @return 业务扩展载荷 + */ + public Map getPayload() { + return payload; + } + + /** + * 设置业务扩展载荷。 + * + * @param payload 业务扩展载荷 + */ + public void setPayload(Map payload) { + this.payload = payload == null ? new LinkedHashMap<>() : payload; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskListResult.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskListResult.java new file mode 100644 index 0000000..4bc5c67 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskListResult.java @@ -0,0 +1,76 @@ +package com.easyagents.agent.runtime.tool.asynctool; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 异步工具任务列表结果。 + */ +public class AsyncToolTaskListResult { + + private List tasks = new ArrayList<>(); + private Map payload = new LinkedHashMap<>(); + private Map metadata = new LinkedHashMap<>(); + + /** + * 创建空任务列表结果。 + */ + public AsyncToolTaskListResult() { + } + + /** + * 获取任务摘要列表。 + * + * @return 任务摘要列表 + */ + public List getTasks() { + return tasks; + } + + /** + * 设置任务摘要列表。 + * + * @param tasks 任务摘要列表 + */ + public void setTasks(List tasks) { + this.tasks = tasks == null ? new ArrayList<>() : tasks; + } + + /** + * 获取业务扩展载荷。 + * + * @return 业务扩展载荷 + */ + public Map getPayload() { + return payload; + } + + /** + * 设置业务扩展载荷。 + * + * @param payload 业务扩展载荷 + */ + public void setPayload(Map payload) { + this.payload = payload == null ? new LinkedHashMap<>() : payload; + } + + /** + * 获取元数据。 + * + * @return 元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置元数据。 + * + * @param metadata 元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskStatus.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskStatus.java new file mode 100644 index 0000000..b05335a --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskStatus.java @@ -0,0 +1,78 @@ +package com.easyagents.agent.runtime.tool.asynctool; + +/** + * 异步工具任务对 Agent Runtime 暴露的统一状态。 + */ +public enum AsyncToolTaskStatus { + + /** + * 任务已创建,等待业务侧执行。 + */ + PENDING, + + /** + * 任务正在执行。 + */ + RUNNING, + + /** + * 任务执行成功,结果可用。 + */ + SUCCEEDED, + + /** + * 任务执行失败。 + */ + FAILED, + + /** + * 任务正在取消。 + */ + CANCELLING, + + /** + * 任务已取消。 + */ + CANCELLED, + + /** + * 任务执行超时。 + */ + TIMEOUT; + + /** + * 判断状态是否为终态。 + * + * @return 终态返回 true + */ + public boolean isTerminal() { + return this == SUCCEEDED || this == FAILED || this == CANCELLED || this == TIMEOUT; + } + + /** + * 判断状态是否仍在执行或等待执行。 + * + * @return 仍在运行返回 true + */ + public boolean isRunning() { + return this == PENDING || this == RUNNING || this == CANCELLING; + } + + /** + * 判断状态是否表示成功。 + * + * @return 成功返回 true + */ + public boolean isSuccess() { + return this == SUCCEEDED; + } + + /** + * 判断状态是否表示失败类终态。 + * + * @return 失败、取消或超时返回 true + */ + public boolean isFailure() { + return this == FAILED || this == CANCELLED || this == TIMEOUT; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskSummary.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskSummary.java new file mode 100644 index 0000000..6b0de06 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskSummary.java @@ -0,0 +1,132 @@ +package com.easyagents.agent.runtime.tool.asynctool; + +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 异步工具任务摘要。 + */ +public class AsyncToolTaskSummary { + + private String taskId; + private AsyncToolTaskStatus status; + private String summary; + private Instant createdAt; + private Instant updatedAt; + private Map payload = new LinkedHashMap<>(); + + /** + * 创建空任务摘要。 + */ + public AsyncToolTaskSummary() { + } + + /** + * 获取任务 ID。 + * + * @return 任务 ID + */ + public String getTaskId() { + return taskId; + } + + /** + * 设置任务 ID。 + * + * @param taskId 任务 ID + */ + public void setTaskId(String taskId) { + this.taskId = taskId; + } + + /** + * 获取任务状态。 + * + * @return 任务状态 + */ + public AsyncToolTaskStatus getStatus() { + return status; + } + + /** + * 设置任务状态。 + * + * @param status 任务状态 + */ + public void setStatus(AsyncToolTaskStatus status) { + this.status = status; + } + + /** + * 获取任务摘要。 + * + * @return 任务摘要 + */ + public String getSummary() { + return summary; + } + + /** + * 设置任务摘要。 + * + * @param summary 任务摘要 + */ + public void setSummary(String summary) { + this.summary = summary; + } + + /** + * 获取创建时间。 + * + * @return 创建时间 + */ + public Instant getCreatedAt() { + return createdAt; + } + + /** + * 设置创建时间。 + * + * @param createdAt 创建时间 + */ + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + /** + * 获取更新时间。 + * + * @return 更新时间 + */ + public Instant getUpdatedAt() { + return updatedAt; + } + + /** + * 设置更新时间。 + * + * @param updatedAt 更新时间 + */ + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } + + /** + * 获取业务扩展载荷。 + * + * @return 业务扩展载荷 + */ + public Map getPayload() { + return payload; + } + + /** + * 设置业务扩展载荷。 + * + * @param payload 业务扩展载荷 + */ + public void setPayload(Map payload) { + this.payload = payload == null ? new LinkedHashMap<>() : payload; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskView.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskView.java new file mode 100644 index 0000000..90d47c7 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskView.java @@ -0,0 +1,312 @@ +package com.easyagents.agent.runtime.tool.asynctool; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 异步工具任务视图。 + * + *

observe 和 result 子工具共用该实体。未完成时表达观察态,完成时可同时携带最终结果。

+ */ +public class AsyncToolTaskView { + + private String taskId; + private AsyncToolTaskStatus status; + /** + * 调用方本次传入的已读事件位置,用于增量读取任务事件。 + */ + private Long cursor; + /** + * 服务端返回的下一次观察起点,调用方下次应使用该值继续读取增量事件。 + */ + private Long nextCursor; + private Integer progress; + private String summary; + private String nextAction; + private List events = new ArrayList<>(); + private Object result; + private String errorMessage; + private String errorType; + private Boolean terminal; + private Boolean resultAvailable; + private Map payload = new LinkedHashMap<>(); + private Map metadata = new LinkedHashMap<>(); + + /** + * 创建空任务视图。 + */ + public AsyncToolTaskView() { + } + + /** + * 获取任务 ID。 + * + * @return 任务 ID + */ + public String getTaskId() { + return taskId; + } + + /** + * 设置任务 ID。 + * + * @param taskId 任务 ID + */ + public void setTaskId(String taskId) { + this.taskId = taskId; + } + + /** + * 获取任务状态。 + * + * @return 任务状态 + */ + public AsyncToolTaskStatus getStatus() { + return status; + } + + /** + * 设置任务状态。 + * + * @param status 任务状态 + */ + public void setStatus(AsyncToolTaskStatus status) { + this.status = status; + } + + /** + * 获取本次请求的已读事件位置。 + * + * @return 已读事件位置 + */ + public Long getCursor() { + return cursor; + } + + /** + * 设置本次请求的已读事件位置。 + * + * @param cursor 已读事件位置 + */ + public void setCursor(Long cursor) { + this.cursor = cursor; + } + + /** + * 获取下一次观察起点。 + * + * @return 下一次观察起点 + */ + public Long getNextCursor() { + return nextCursor; + } + + /** + * 设置下一次观察起点。 + * + * @param nextCursor 下一次观察起点 + */ + public void setNextCursor(Long nextCursor) { + this.nextCursor = nextCursor; + } + + /** + * 获取进度百分比。 + * + * @return 进度百分比 + */ + public Integer getProgress() { + return progress; + } + + /** + * 设置进度百分比。 + * + * @param progress 进度百分比 + */ + public void setProgress(Integer progress) { + this.progress = progress; + } + + /** + * 获取摘要。 + * + * @return 摘要 + */ + public String getSummary() { + return summary; + } + + /** + * 设置摘要。 + * + * @param summary 摘要 + */ + public void setSummary(String summary) { + this.summary = summary; + } + + /** + * 获取下一步建议。 + * + * @return 下一步建议 + */ + public String getNextAction() { + return nextAction; + } + + /** + * 设置下一步建议。 + * + * @param nextAction 下一步建议 + */ + public void setNextAction(String nextAction) { + this.nextAction = nextAction; + } + + /** + * 获取本次增量事件。 + * + * @return 本次增量事件 + */ + public List getEvents() { + return events; + } + + /** + * 设置本次增量事件。 + * + * @param events 本次增量事件 + */ + public void setEvents(List events) { + this.events = events == null ? new ArrayList<>() : events; + } + + /** + * 获取最终结果。 + * + * @return 最终结果 + */ + public Object getResult() { + return result; + } + + /** + * 设置最终结果。 + * + * @param result 最终结果 + */ + public void setResult(Object result) { + this.result = result; + } + + /** + * 获取错误消息。 + * + * @return 错误消息 + */ + public String getErrorMessage() { + return errorMessage; + } + + /** + * 设置错误消息。 + * + * @param errorMessage 错误消息 + */ + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + /** + * 获取错误类型。 + * + * @return 错误类型 + */ + public String getErrorType() { + return errorType; + } + + /** + * 设置错误类型。 + * + * @param errorType 错误类型 + */ + public void setErrorType(String errorType) { + this.errorType = errorType; + } + + /** + * 获取是否终态。 + * + * @return 是否终态 + */ + public Boolean getTerminal() { + return terminal; + } + + /** + * 设置是否终态。 + * + * @param terminal 是否终态 + */ + public void setTerminal(Boolean terminal) { + this.terminal = terminal; + } + + /** + * 获取最终结果是否可用。 + * + * @return 最终结果是否可用 + */ + public Boolean getResultAvailable() { + return resultAvailable; + } + + /** + * 设置最终结果是否可用。 + * + * @param resultAvailable 最终结果是否可用 + */ + public void setResultAvailable(Boolean resultAvailable) { + this.resultAvailable = resultAvailable; + } + + /** + * 获取业务扩展载荷。 + * + * @return 业务扩展载荷 + */ + public Map getPayload() { + return payload; + } + + /** + * 设置业务扩展载荷。 + * + * @param payload 业务扩展载荷 + */ + public void setPayload(Map payload) { + this.payload = payload == null ? new LinkedHashMap<>() : payload; + } + + /** + * 获取元数据。 + * + * @return 元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置元数据。 + * + * @param metadata 元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } +} diff --git a/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/AgentScopeStatefulRuntimeTest.java b/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/AgentScopeStatefulRuntimeTest.java index 80333b0..ba15b66 100644 --- a/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/AgentScopeStatefulRuntimeTest.java +++ b/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/AgentScopeStatefulRuntimeTest.java @@ -107,6 +107,36 @@ public class AgentScopeStatefulRuntimeTest { Assert.assertFalse(runtime.getAgent().getHooks().stream().anyMatch(AutoContextHook.class::isInstance)); } + @Test + public void shouldKeepSystemPromptUnchangedWithoutAsyncTool() { + AgentScopeReActRuntime runtime = fakeRuntime(); + + runtime.init(initRequest()); + + Assert.assertEquals("system", runtime.getAgent().getSysPrompt()); + } + + @Test + public void shouldAppendAsyncToolProtocolPromptWhenAsyncToolsExist() { + AgentInitRequest request = initRequest(); + AgentToolSpec submit = new AgentToolSpec(); + submit.setName("demo_submit"); + submit.setDescription("submit demo"); + submit.getMetadata().put("asyncTool", true); + submit.getMetadata().put("asyncToolPhase", "submit"); + request.getAgentDefinition().setToolSpecs(List.of(submit)); + request.setToolInvokers(Map.of("demo_submit", (arguments, context) -> AgentToolResult.success("submitted"))); + AgentScopeReActRuntime runtime = fakeRuntime(); + + runtime.init(request); + + Assert.assertTrue(runtime.getAgent().getSysPrompt().startsWith("system")); + Assert.assertTrue(runtime.getAgent().getSysPrompt().contains("Async tool protocol:")); + Assert.assertTrue(runtime.getAgent().getSysPrompt().contains("These are internal execution phases.")); + Assert.assertTrue(runtime.getAgent().getSysPrompt().contains("call its submit sub-tool first with the user-provided arguments by default")); + Assert.assertTrue(runtime.getAgent().getSysPrompt().contains("immediately call observe")); + } + @Test public void shouldEmitSideEventWithRuntimeIdentityFromBridge() throws Exception { AgentRuntimeExecutionContext context = new AgentRuntimeExecutionContext(); diff --git a/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSpecExpanderTest.java b/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSpecExpanderTest.java new file mode 100644 index 0000000..c9431dc --- /dev/null +++ b/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSpecExpanderTest.java @@ -0,0 +1,408 @@ +package com.easyagents.agent.runtime.tool.asynctool; + +import com.easyagents.agent.runtime.event.AgentRuntimeEvent; +import com.easyagents.agent.runtime.event.AgentRuntimeEventType; +import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest; +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 org.junit.Assert; +import org.junit.Test; + +import java.time.Duration; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; + +/** + * 测试异步工具声明展开器。 + */ +public class AsyncToolSpecExpanderTest { + + @Test + public void shouldExpandFiveToolSpecsAndInvokers() { + AsyncToolSpec spec = spec(new StubSubTools()); + AsyncToolSpecExpander expander = new AsyncToolSpecExpander(); + + List specs = expander.expandSpecs(spec); + Map invokers = expander.expandInvokers(spec); + + Assert.assertEquals(List.of("demo_task_submit", "demo_task_observe", "demo_task_result", + "demo_task_cancel", "demo_task_list"), specs.stream().map(AgentToolSpec::getName).toList()); + Assert.assertEquals(5, invokers.size()); + Assert.assertEquals("object", specs.get(1).getParametersSchema().get("type")); + Assert.assertEquals("AsyncToolTaskView", specs.get(1).getOutputSchema().get("title")); + Assert.assertTrue(specs.get(0).getDescription().contains("default entry point when the user asks to run this tool")); + Assert.assertTrue(specs.get(1).getDescription().contains("Use immediately after submit")); + Assert.assertTrue(specs.get(1).getDescription().contains("Do not ask the user for task_id immediately after submit")); + Assert.assertTrue(specs.get(2).getDescription().contains("final result")); + Assert.assertTrue(specs.get(3).getDescription().contains("only when the user explicitly asks to cancel")); + Assert.assertTrue(specs.get(4).getDescription().contains("only when the user explicitly asks to list")); + } + + @Test + public void shouldKeepRuntimeMetadataWhenUserMetadataUsesReservedKeys() { + AsyncToolSpec spec = spec(new StubSubTools()); + spec.setMetadata(Map.of("asyncTool", false, "asyncToolName", "user_name", "custom", "value")); + AsyncToolSpecExpander expander = new AsyncToolSpecExpander(); + + AgentToolSpec toolSpec = expander.expandSpecs(spec).get(0); + AgentToolResult result = expander.expandInvokers(spec).get("demo_task_submit") + .invoke(Map.of(), context(new ArrayList<>())); + + Assert.assertEquals(true, toolSpec.getMetadata().get("asyncTool")); + Assert.assertEquals("demo_task", toolSpec.getMetadata().get("asyncToolName")); + Assert.assertEquals("value", toolSpec.getMetadata().get("custom")); + Assert.assertEquals(true, result.getMetadata().get("asyncTool")); + Assert.assertEquals("demo_task", result.getMetadata().get("asyncToolName")); + } + + @Test + public void shouldApplyApprovalOnlyToSubmitTool() { + AsyncToolSpec spec = spec(new StubSubTools()); + AgentToolApprovalRequest approvalRequest = new AgentToolApprovalRequest(); + approvalRequest.setApprovalPrompt("确认提交任务?"); + spec.setApprovalRequired(true); + spec.setApprovalRequest(approvalRequest); + + List specs = new AsyncToolSpecExpander().expandSpecs(spec); + + Assert.assertTrue(specs.get(0).isApprovalRequired()); + Assert.assertEquals("确认提交任务?", specs.get(0).getApprovalRequest().getApprovalPrompt()); + Assert.assertFalse(specs.get(1).isApprovalRequired()); + Assert.assertFalse(specs.get(2).isApprovalRequired()); + Assert.assertFalse(specs.get(3).isApprovalRequired()); + Assert.assertFalse(specs.get(4).isApprovalRequired()); + } + + @Test + public void shouldSubmitAndEmitSubmittedEvent() { + List events = new ArrayList<>(); + AgentToolResult result = invoke("demo_task_submit", Map.of("input", "hello"), new StubSubTools(), events); + + Assert.assertTrue(result.isSuccess()); + Assert.assertTrue(result.getModelContent().contains("task_id: task-1")); + Assert.assertEquals("task-1", result.getMetadata().get("taskId")); + Assert.assertEquals(AgentRuntimeEventType.ASYNC_TOOL_SUBMITTED, events.get(0).getEventType()); + Assert.assertEquals("task-1", events.get(0).getPayload().get("taskId")); + } + + @Test + public void shouldFailWhenSubmitDoesNotReturnTaskId() { + List events = new ArrayList<>(); + AgentToolResult result = invoke("demo_task_submit", Map.of(), new StubSubTools() { + @Override + public AsyncToolSubmitResult submit(Map arguments, AgentToolContext context) { + AsyncToolSubmitResult submitResult = super.submit(arguments, context); + submitResult.setTaskId(null); + return submitResult; + } + }, events); + + Assert.assertFalse(result.isSuccess()); + Assert.assertTrue(result.getErrorMessage().contains("taskId")); + Assert.assertEquals(AgentRuntimeEventType.ASYNC_TOOL_FAILED, events.get(0).getEventType()); + } + + @Test + public void shouldObserveWithCursorLimitAndContext() { + AtomicReference requestRef = new AtomicReference<>(); + AtomicReference contextRef = new AtomicReference<>(); + StubSubTools subTools = new StubSubTools() { + @Override + public AsyncToolTaskView observe(AsyncToolObserveRequest request, AgentToolContext context) { + requestRef.set(request); + contextRef.set(context); + return super.observe(request, context); + } + }; + List events = new ArrayList<>(); + + AgentToolResult result = invoke("demo_task_observe", Map.of("taskId", "task-1", "cursor", 7, "limit", 999), + subTools, events); + + AsyncToolTaskView view = (AsyncToolTaskView) result.getDisplayContent(); + Assert.assertEquals(Long.valueOf(7), requestRef.get().getCursor()); + Assert.assertEquals(Integer.valueOf(100), requestRef.get().getLimit()); + Assert.assertEquals(Long.valueOf(8), view.getNextCursor()); + Assert.assertEquals("request-1", contextRef.get().getRequestId()); + Assert.assertEquals(AgentRuntimeEventType.ASYNC_TOOL_OBSERVED, events.get(0).getEventType()); + } + + @Test + public void shouldWrapInvalidArgumentsAndEmitFailedEvent() { + List events = new ArrayList<>(); + + AgentToolResult result = invoke("demo_task_observe", Map.of("taskId", "task-1", "cursor", "bad"), + new StubSubTools(), events); + + Assert.assertFalse(result.isSuccess()); + Assert.assertEquals(AgentRuntimeEventType.ASYNC_TOOL_FAILED, events.get(0).getEventType()); + Assert.assertEquals("FAILED", result.getMetadata().get("status")); + } + + @Test + public void shouldReturnObservationFromResultWhenRunningAndFinalResultWhenSucceeded() { + StubSubTools running = new StubSubTools(); + AgentToolResult runningResult = invoke("demo_task_result", Map.of("taskId", "task-1"), running, new ArrayList<>()); + AsyncToolTaskView runningView = (AsyncToolTaskView) runningResult.getDisplayContent(); + + StubSubTools succeeded = new StubSubTools() { + @Override + public AsyncToolTaskView result(AsyncToolResultRequest request, AgentToolContext context) { + AsyncToolTaskView view = super.result(request, context); + view.setStatus(AsyncToolTaskStatus.SUCCEEDED); + view.setResult("done"); + return view; + } + }; + AgentToolResult successResult = invoke("demo_task_result", Map.of("taskId", "task-1"), succeeded, new ArrayList<>()); + AsyncToolTaskView successView = (AsyncToolTaskView) successResult.getDisplayContent(); + + Assert.assertEquals(AsyncToolTaskStatus.RUNNING, runningView.getStatus()); + Assert.assertEquals(AsyncToolTaskStatus.SUCCEEDED, successView.getStatus()); + Assert.assertEquals("done", successView.getResult()); + Assert.assertTrue(successView.getTerminal()); + Assert.assertTrue(successView.getResultAvailable()); + Assert.assertFalse(runningResult.getModelContent().contains("result:")); + Assert.assertTrue(successResult.getModelContent().contains("result_available: true")); + Assert.assertTrue(successResult.getModelContent().contains("result: done")); + } + + @Test + public void shouldExposeCompletedObservationResultToModelContentAsJson() { + List events = new ArrayList<>(); + LinkedHashMap businessResult = new LinkedHashMap<>(); + businessResult.put("answer", "42"); + businessResult.put("items", List.of(1, 2)); + StubSubTools succeeded = new StubSubTools() { + @Override + public AsyncToolTaskView observe(AsyncToolObserveRequest request, AgentToolContext context) { + AsyncToolTaskView view = super.observe(request, context); + view.setStatus(AsyncToolTaskStatus.SUCCEEDED); + view.setResult(businessResult); + return view; + } + }; + + AgentToolResult result = invoke("demo_task_observe", Map.of("taskId", "task-1"), succeeded, events); + AsyncToolTaskView view = (AsyncToolTaskView) result.getDisplayContent(); + + Assert.assertEquals(businessResult, view.getResult()); + Assert.assertTrue(result.getModelContent().contains("result_available: true")); + Assert.assertTrue(result.getModelContent().contains("result: {\"answer\":\"42\",\"items\":[1,2]}")); + Assert.assertEquals(Boolean.TRUE, events.get(0).getPayload().get("resultAvailable")); + Assert.assertFalse(events.get(0).getPayload().containsKey("result")); + } + + @Test + public void shouldCancelAndRepresentUnsupportedCancelAsFailure() { + AgentToolResult success = invoke("demo_task_cancel", Map.of("taskId", "task-1"), new StubSubTools(), new ArrayList<>()); + AgentToolResult failure = invoke("demo_task_cancel", Map.of("taskId", "task-1"), new StubSubTools() { + @Override + public AsyncToolCancelResult cancel(AsyncToolCancelRequest request, AgentToolContext context) { + AsyncToolCancelResult result = super.cancel(request, context); + result.setErrorMessage("不支持取消"); + return result; + } + }, new ArrayList<>()); + + Assert.assertTrue(success.isSuccess()); + Assert.assertFalse(failure.isSuccess()); + Assert.assertEquals("不支持取消", failure.getErrorMessage()); + } + + @Test + public void shouldListTasksWithoutPagination() { + AgentToolResult result = invoke("demo_task_list", Map.of("status", "running"), new StubSubTools(), new ArrayList<>()); + AsyncToolTaskListResult list = (AsyncToolTaskListResult) result.getDisplayContent(); + + Assert.assertTrue(result.isSuccess()); + Assert.assertEquals(1, list.getTasks().size()); + Assert.assertFalse(Arrays.stream(AsyncToolTaskListResult.class.getDeclaredFields()) + .anyMatch(field -> field.getName().contains("PageToken"))); + } + + @Test + public void shouldWrapExceptionAndEmitFailedEvent() { + List events = new ArrayList<>(); + AgentToolResult result = invoke("demo_task_observe", Map.of("taskId", "task-1"), new StubSubTools() { + @Override + public AsyncToolTaskView observe(AsyncToolObserveRequest request, AgentToolContext context) { + throw new IllegalStateException("boom"); + } + }, events); + + Assert.assertFalse(result.isSuccess()); + Assert.assertEquals("boom", result.getErrorMessage()); + Assert.assertEquals(AgentRuntimeEventType.ASYNC_TOOL_FAILED, events.get(0).getEventType()); + } + + @Test + public void shouldTimeoutAndEmitFailedEvent() { + List events = new ArrayList<>(); + AsyncToolOptions options = AsyncToolOptions.defaults(); + options.setObserveTimeout(Duration.ofMillis(20)); + StubSubTools subTools = new StubSubTools() { + @Override + public AsyncToolTaskView observe(AsyncToolObserveRequest request, AgentToolContext context) { + try { + Thread.sleep(200); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + return super.observe(request, context); + } + }; + AgentToolResult result = invoke("demo_task_observe", Map.of("taskId", "task-1"), subTools, events, options); + + Assert.assertFalse(result.isSuccess()); + Assert.assertTrue(result.getErrorMessage().contains("timed out")); + Assert.assertEquals(AgentRuntimeEventType.ASYNC_TOOL_FAILED, events.get(0).getEventType()); + } + + @Test + public void shouldNotPutLargePayloadIntoModelContentOrEventPayload() { + List events = new ArrayList<>(); + AgentToolResult result = invoke("demo_task_submit", Map.of(), new StubSubTools() { + @Override + public AsyncToolSubmitResult submit(Map arguments, AgentToolContext context) { + AsyncToolSubmitResult result = super.submit(arguments, context); + result.getPayload().put("large", "x".repeat(5000)); + return result; + } + }, events); + + Assert.assertFalse(result.getModelContent().contains("xxxxx")); + Assert.assertFalse(events.get(0).getPayload().containsKey("large")); + Assert.assertTrue(((AsyncToolSubmitResult) result.getDisplayContent()).getPayload().containsKey("large")); + } + + @Test + public void shouldTrimLargeResultForModelContentAndKeepFullDisplayContent() { + List events = new ArrayList<>(); + AsyncToolOptions options = AsyncToolOptions.defaults(); + options.setMaxModelContentLength(160); + String largeResult = "x".repeat(5000); + StubSubTools succeeded = new StubSubTools() { + @Override + public AsyncToolTaskView result(AsyncToolResultRequest request, AgentToolContext context) { + AsyncToolTaskView view = super.result(request, context); + view.setStatus(AsyncToolTaskStatus.SUCCEEDED); + view.setResult(Map.of("large", largeResult)); + return view; + } + }; + + AgentToolResult result = invoke("demo_task_result", Map.of("taskId", "task-1"), succeeded, events, options); + AsyncToolTaskView view = (AsyncToolTaskView) result.getDisplayContent(); + + Assert.assertTrue(result.getModelContent().length() <= options.getMaxModelContentLength() + 3); + Assert.assertTrue(result.getModelContent().contains("result_available: true")); + Assert.assertFalse(result.getModelContent().contains(largeResult)); + Assert.assertEquals(Map.of("large", largeResult), view.getResult()); + Assert.assertEquals(Boolean.TRUE, events.get(0).getPayload().get("resultAvailable")); + Assert.assertFalse(events.get(0).getPayload().containsKey("result")); + } + + private AgentToolResult invoke(String toolName, + Map arguments, + AsyncSubTools subTools, + List events) { + return invoke(toolName, arguments, subTools, events, AsyncToolOptions.defaults()); + } + + private AgentToolResult invoke(String toolName, + Map arguments, + AsyncSubTools subTools, + List events, + AsyncToolOptions options) { + AsyncToolSpec spec = spec(subTools); + spec.setOptions(options); + AsyncToolSpecExpander expander = new AsyncToolSpecExpander(Executors.newCachedThreadPool()); + AgentToolContext context = context(events); + return expander.expandInvokers(spec).get(toolName).invoke(arguments, context); + } + + private AsyncToolSpec spec(AsyncSubTools subTools) { + AsyncToolSpec spec = new AsyncToolSpec(); + spec.setName("demo_task"); + spec.setDescription("Demo async task."); + spec.setSubTools(subTools); + spec.setSubmitParametersSchema(Map.of("type", "object", "properties", Map.of("input", Map.of("type", "string")))); + return spec; + } + + private AgentToolContext context(List events) { + AgentToolContext context = new AgentToolContext(); + context.setRequestId("request-1"); + context.setTraceId("trace-1"); + context.setSessionId("session-1"); + context.setAgentId("agent-1"); + context.setToolCallId("tool-call-1"); + context.setEventEmitter(events::add); + return context; + } + + private static class StubSubTools implements AsyncSubTools { + + @Override + public AsyncToolSubmitResult submit(Map arguments, AgentToolContext context) { + AsyncToolSubmitResult result = new AsyncToolSubmitResult(); + result.setTaskId("task-1"); + result.setStatus(AsyncToolTaskStatus.RUNNING); + result.setCursor(0L); + result.setSummary("submitted"); + return result; + } + + @Override + public AsyncToolTaskView observe(AsyncToolObserveRequest request, AgentToolContext context) { + AsyncToolTaskView view = new AsyncToolTaskView(); + view.setTaskId(request.getTaskId()); + view.setStatus(AsyncToolTaskStatus.RUNNING); + view.setCursor(request.getCursor()); + view.setNextCursor(request.getCursor() == null ? 1L : request.getCursor() + 1); + view.setSummary("running"); + AsyncToolTaskEvent event = new AsyncToolTaskEvent(); + event.setSequence(view.getNextCursor()); + event.setType("TASK_LOG"); + event.setText("progress"); + event.setCreatedAt(Instant.now()); + view.setEvents(List.of(event)); + return view; + } + + @Override + public AsyncToolTaskView result(AsyncToolResultRequest request, AgentToolContext context) { + AsyncToolObserveRequest observeRequest = new AsyncToolObserveRequest(); + observeRequest.setTaskId(request.getTaskId()); + observeRequest.setCursor(request.getCursor()); + observeRequest.setLimit(request.getLimit()); + return observe(observeRequest, context); + } + + @Override + public AsyncToolCancelResult cancel(AsyncToolCancelRequest request, AgentToolContext context) { + AsyncToolCancelResult result = new AsyncToolCancelResult(); + result.setTaskId(request.getTaskId()); + result.setStatus(AsyncToolTaskStatus.CANCELLING); + result.setMessage("cancelling"); + return result; + } + + @Override + public AsyncToolTaskListResult list(AsyncToolListRequest request, AgentToolContext context) { + AsyncToolTaskSummary summary = new AsyncToolTaskSummary(); + summary.setTaskId("task-1"); + summary.setStatus(AsyncToolTaskStatus.RUNNING); + summary.setSummary("running"); + AsyncToolTaskListResult result = new AsyncToolTaskListResult(); + result.setTasks(List.of(summary)); + return result; + } + } +}