feat: 归档 XL08 异步工具协议
- 新增 AsyncToolSpec 与 AsyncSubTools 五子工具展开能力 - 增加异步工具事件、上下文事件发射和模型可见结果裁剪 - 补充 AgentScope 异步工具协议提示与 runtime 单元测试
This commit is contained in:
@@ -23,6 +23,11 @@
|
||||
<artifactId>agentscope</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.anthropic</groupId>
|
||||
<artifactId>anthropic-java</artifactId>
|
||||
|
||||
@@ -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 分组的工具。
|
||||
*
|
||||
|
||||
@@ -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 运行时结果
|
||||
|
||||
@@ -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,
|
||||
|
||||
/**
|
||||
* 知识库检索完成并返回文档摘要。
|
||||
*/
|
||||
|
||||
@@ -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<String, Object> metadata = new LinkedHashMap<>();
|
||||
private Consumer<AgentRuntimeEvent> eventEmitter;
|
||||
|
||||
/**
|
||||
* 获取请求ID。
|
||||
@@ -143,4 +146,36 @@ public class AgentToolContext {
|
||||
public void setMetadata(Map<String, Object> metadata) {
|
||||
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发射运行时旁路事件。
|
||||
*
|
||||
* <p>该方法主要供 runtime 包装层发射业务无关事件,例如异步工具生命周期事件。
|
||||
* 未配置事件发射器时静默忽略,避免非流式测试路径产生额外副作用。</p>
|
||||
*
|
||||
* @param event 运行时事件
|
||||
*/
|
||||
public void emitEvent(AgentRuntimeEvent event) {
|
||||
if (eventEmitter != null && event != null) {
|
||||
eventEmitter.accept(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运行时事件发射器。
|
||||
*
|
||||
* @return 运行时事件发射器
|
||||
*/
|
||||
public Consumer<AgentRuntimeEvent> getEventEmitter() {
|
||||
return eventEmitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置运行时事件发射器。
|
||||
*
|
||||
* @param eventEmitter 运行时事件发射器
|
||||
*/
|
||||
public void setEventEmitter(Consumer<AgentRuntimeEvent> eventEmitter) {
|
||||
this.eventEmitter = eventEmitter;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.easyagents.agent.runtime.tool.asynctool;
|
||||
|
||||
import com.easyagents.agent.runtime.tool.AgentToolContext;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 调用方实现的异步业务子工具集合。
|
||||
*/
|
||||
public interface AsyncSubTools {
|
||||
|
||||
/**
|
||||
* 提交异步任务。
|
||||
*
|
||||
* @param arguments 模型传入的业务参数
|
||||
* @param context 工具调用上下文
|
||||
* @return 提交结果
|
||||
*/
|
||||
AsyncToolSubmitResult submit(Map<String, Object> arguments, AgentToolContext context);
|
||||
|
||||
/**
|
||||
* 非阻塞观察任务状态和增量事件。
|
||||
*
|
||||
* @param request 观察请求
|
||||
* @param context 工具调用上下文
|
||||
* @return 当前任务视图
|
||||
*/
|
||||
AsyncToolTaskView observe(AsyncToolObserveRequest request, AgentToolContext context);
|
||||
|
||||
/**
|
||||
* 获取任务结果,未完成时返回当前观察态。
|
||||
*
|
||||
* @param request 结果请求
|
||||
* @param context 工具调用上下文
|
||||
* @return 当前任务视图
|
||||
*/
|
||||
AsyncToolTaskView result(AsyncToolResultRequest request, AgentToolContext context);
|
||||
|
||||
/**
|
||||
* 请求取消任务。
|
||||
*
|
||||
* @param request 取消请求
|
||||
* @param context 工具调用上下文
|
||||
* @return 取消结果
|
||||
*/
|
||||
AsyncToolCancelResult cancel(AsyncToolCancelRequest request, AgentToolContext context);
|
||||
|
||||
/**
|
||||
* 查询当前上下文可见的任务列表。
|
||||
*
|
||||
* @param request 列表请求
|
||||
* @param context 工具调用上下文
|
||||
* @return 任务列表
|
||||
*/
|
||||
AsyncToolTaskListResult list(AsyncToolListRequest request, AgentToolContext context);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> payload = new LinkedHashMap<>();
|
||||
private Map<String, Object> 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<String, Object> getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置业务扩展载荷。
|
||||
*
|
||||
* @param payload 业务扩展载荷
|
||||
*/
|
||||
public void setPayload(Map<String, Object> payload) {
|
||||
this.payload = payload == null ? new LinkedHashMap<>() : payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元数据。
|
||||
*
|
||||
* @return 元数据
|
||||
*/
|
||||
public Map<String, Object> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置元数据。
|
||||
*
|
||||
* @param metadata 元数据
|
||||
*/
|
||||
public void setMetadata(Map<String, Object> metadata) {
|
||||
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 异步工具声明。
|
||||
*
|
||||
* <p>一个声明会被 runtime 展开为 submit、observe、result、cancel 和 list 五个普通工具。</p>
|
||||
*/
|
||||
public class AsyncToolSpec {
|
||||
|
||||
private String name;
|
||||
private String description;
|
||||
private Map<String, Object> submitParametersSchema = new LinkedHashMap<>();
|
||||
private AsyncSubTools subTools;
|
||||
private AsyncToolOptions options = AsyncToolOptions.defaults();
|
||||
private boolean approvalRequired;
|
||||
private AgentToolApprovalRequest approvalRequest = new AgentToolApprovalRequest();
|
||||
private Map<String, Object> 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<String, Object> getSubmitParametersSchema() {
|
||||
return submitParametersSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置提交子工具参数 Schema。
|
||||
*
|
||||
* @param submitParametersSchema 参数 Schema
|
||||
*/
|
||||
public void setSubmitParametersSchema(Map<String, Object> 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<String, Object> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置元数据。
|
||||
*
|
||||
* @param metadata 元数据
|
||||
*/
|
||||
public void setMetadata(Map<String, Object> metadata) {
|
||||
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 异步工具声明展开器。
|
||||
*
|
||||
* <p>该类将一个业务无关的 {@link AsyncToolSpec} 展开为五个普通
|
||||
* {@link AgentToolSpec} 与 {@link AgentToolInvoker},业务方只需要实现
|
||||
* {@link AsyncSubTools}。</p>
|
||||
*/
|
||||
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<AgentToolSpec> expandSpecs(AsyncToolSpec spec) {
|
||||
AsyncToolSpec safeSpec = validate(spec);
|
||||
List<AgentToolSpec> 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<String, AgentToolInvoker> expandInvokers(AsyncToolSpec spec) {
|
||||
AsyncToolSpec safeSpec = validate(spec);
|
||||
Map<String, AgentToolInvoker> 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<String, Object> parametersSchema,
|
||||
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<AgentToolContext, AgentToolResult> supplier) {
|
||||
AtomicBoolean active = new AtomicBoolean(true);
|
||||
AgentToolContext guardedContext = guardedContext(context, active);
|
||||
Future<AgentToolResult> 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<String, Object> metadata(AsyncToolSpec spec, String phase, String taskId, AsyncToolTaskStatus status) {
|
||||
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> arguments) {
|
||||
AsyncToolCancelRequest request = new AsyncToolCancelRequest();
|
||||
request.setTaskId(stringValue(arguments, "taskId"));
|
||||
request.setReason(stringValue(arguments, "reason"));
|
||||
return request;
|
||||
}
|
||||
|
||||
private AsyncToolListRequest listRequest(Map<String, Object> 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<String, Object> 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<String, Object> emptyObjectSchema() {
|
||||
Map<String, Object> schema = new LinkedHashMap<>();
|
||||
schema.put("type", "object");
|
||||
schema.put("properties", new LinkedHashMap<>());
|
||||
return schema;
|
||||
}
|
||||
|
||||
private Map<String, Object> observeSchema(AsyncToolSpec spec) {
|
||||
Map<String, Object> schema = new LinkedHashMap<>();
|
||||
schema.put("type", "object");
|
||||
Map<String, Object> 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<String, Object> cancelSchema() {
|
||||
Map<String, Object> schema = new LinkedHashMap<>();
|
||||
schema.put("type", "object");
|
||||
Map<String, Object> 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<String, Object> listSchema() {
|
||||
Map<String, Object> schema = new LinkedHashMap<>();
|
||||
schema.put("type", "object");
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
properties.put("status", property("string", "Optional task status filter."));
|
||||
schema.put("properties", properties);
|
||||
return schema;
|
||||
}
|
||||
|
||||
private Map<String, Object> submitOutputSchema() {
|
||||
return outputSchema("AsyncToolSubmitResult");
|
||||
}
|
||||
|
||||
private Map<String, Object> taskViewOutputSchema() {
|
||||
return outputSchema("AsyncToolTaskView");
|
||||
}
|
||||
|
||||
private Map<String, Object> cancelOutputSchema() {
|
||||
return outputSchema("AsyncToolCancelResult");
|
||||
}
|
||||
|
||||
private Map<String, Object> listOutputSchema() {
|
||||
return outputSchema("AsyncToolTaskListResult");
|
||||
}
|
||||
|
||||
private Map<String, Object> outputSchema(String title) {
|
||||
Map<String, Object> schema = new LinkedHashMap<>();
|
||||
schema.put("type", "object");
|
||||
schema.put("title", title);
|
||||
return schema;
|
||||
}
|
||||
|
||||
private Map<String, Object> property(String type, String description) {
|
||||
Map<String, Object> property = new LinkedHashMap<>();
|
||||
property.put("type", type);
|
||||
property.put("description", description);
|
||||
return property;
|
||||
}
|
||||
|
||||
private Map<String, Object> safeMap(Map<String, Object> arguments) {
|
||||
return arguments == null ? new LinkedHashMap<>() : new LinkedHashMap<>(arguments);
|
||||
}
|
||||
|
||||
private String stringValue(Map<String, Object> arguments, String key) {
|
||||
Object value = arguments == null ? null : arguments.get(key);
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
|
||||
private Long longValue(Map<String, Object> 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<String, Object> 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<String, Object> 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<Runnable> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> payload = new LinkedHashMap<>();
|
||||
private Map<String, Object> 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<String, Object> getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置业务扩展载荷。
|
||||
*
|
||||
* @param payload 业务扩展载荷
|
||||
*/
|
||||
public void setPayload(Map<String, Object> payload) {
|
||||
this.payload = payload == null ? new LinkedHashMap<>() : payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元数据。
|
||||
*
|
||||
* @return 元数据
|
||||
*/
|
||||
public Map<String, Object> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置元数据。
|
||||
*
|
||||
* @param metadata 元数据
|
||||
*/
|
||||
public void setMetadata(Map<String, Object> metadata) {
|
||||
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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<String, Object> getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置业务扩展载荷。
|
||||
*
|
||||
* @param payload 业务扩展载荷
|
||||
*/
|
||||
public void setPayload(Map<String, Object> payload) {
|
||||
this.payload = payload == null ? new LinkedHashMap<>() : payload;
|
||||
}
|
||||
}
|
||||
@@ -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<AsyncToolTaskSummary> tasks = new ArrayList<>();
|
||||
private Map<String, Object> payload = new LinkedHashMap<>();
|
||||
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* 创建空任务列表结果。
|
||||
*/
|
||||
public AsyncToolTaskListResult() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务摘要列表。
|
||||
*
|
||||
* @return 任务摘要列表
|
||||
*/
|
||||
public List<AsyncToolTaskSummary> getTasks() {
|
||||
return tasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置任务摘要列表。
|
||||
*
|
||||
* @param tasks 任务摘要列表
|
||||
*/
|
||||
public void setTasks(List<AsyncToolTaskSummary> tasks) {
|
||||
this.tasks = tasks == null ? new ArrayList<>() : tasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取业务扩展载荷。
|
||||
*
|
||||
* @return 业务扩展载荷
|
||||
*/
|
||||
public Map<String, Object> getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置业务扩展载荷。
|
||||
*
|
||||
* @param payload 业务扩展载荷
|
||||
*/
|
||||
public void setPayload(Map<String, Object> payload) {
|
||||
this.payload = payload == null ? new LinkedHashMap<>() : payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元数据。
|
||||
*
|
||||
* @return 元数据
|
||||
*/
|
||||
public Map<String, Object> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置元数据。
|
||||
*
|
||||
* @param metadata 元数据
|
||||
*/
|
||||
public void setMetadata(Map<String, Object> metadata) {
|
||||
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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<String, Object> getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置业务扩展载荷。
|
||||
*
|
||||
* @param payload 业务扩展载荷
|
||||
*/
|
||||
public void setPayload(Map<String, Object> payload) {
|
||||
this.payload = payload == null ? new LinkedHashMap<>() : payload;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 异步工具任务视图。
|
||||
*
|
||||
* <p>observe 和 result 子工具共用该实体。未完成时表达观察态,完成时可同时携带最终结果。</p>
|
||||
*/
|
||||
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<AsyncToolTaskEvent> events = new ArrayList<>();
|
||||
private Object result;
|
||||
private String errorMessage;
|
||||
private String errorType;
|
||||
private Boolean terminal;
|
||||
private Boolean resultAvailable;
|
||||
private Map<String, Object> payload = new LinkedHashMap<>();
|
||||
private Map<String, Object> 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<AsyncToolTaskEvent> getEvents() {
|
||||
return events;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置本次增量事件。
|
||||
*
|
||||
* @param events 本次增量事件
|
||||
*/
|
||||
public void setEvents(List<AsyncToolTaskEvent> 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<String, Object> getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置业务扩展载荷。
|
||||
*
|
||||
* @param payload 业务扩展载荷
|
||||
*/
|
||||
public void setPayload(Map<String, Object> payload) {
|
||||
this.payload = payload == null ? new LinkedHashMap<>() : payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元数据。
|
||||
*
|
||||
* @return 元数据
|
||||
*/
|
||||
public Map<String, Object> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置元数据。
|
||||
*
|
||||
* @param metadata 元数据
|
||||
*/
|
||||
public void setMetadata(Map<String, Object> metadata) {
|
||||
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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<AgentToolSpec> specs = expander.expandSpecs(spec);
|
||||
Map<String, AgentToolInvoker> 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<AgentToolSpec> 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<AgentRuntimeEvent> 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<AgentRuntimeEvent> events = new ArrayList<>();
|
||||
AgentToolResult result = invoke("demo_task_submit", Map.of(), new StubSubTools() {
|
||||
@Override
|
||||
public AsyncToolSubmitResult submit(Map<String, Object> 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<AsyncToolObserveRequest> requestRef = new AtomicReference<>();
|
||||
AtomicReference<AgentToolContext> 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<AgentRuntimeEvent> 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<AgentRuntimeEvent> 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<AgentRuntimeEvent> events = new ArrayList<>();
|
||||
LinkedHashMap<String, Object> 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<AgentRuntimeEvent> 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<AgentRuntimeEvent> 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<AgentRuntimeEvent> events = new ArrayList<>();
|
||||
AgentToolResult result = invoke("demo_task_submit", Map.of(), new StubSubTools() {
|
||||
@Override
|
||||
public AsyncToolSubmitResult submit(Map<String, Object> 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<AgentRuntimeEvent> 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<String, Object> arguments,
|
||||
AsyncSubTools subTools,
|
||||
List<AgentRuntimeEvent> events) {
|
||||
return invoke(toolName, arguments, subTools, events, AsyncToolOptions.defaults());
|
||||
}
|
||||
|
||||
private AgentToolResult invoke(String toolName,
|
||||
Map<String, Object> arguments,
|
||||
AsyncSubTools subTools,
|
||||
List<AgentRuntimeEvent> 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<AgentRuntimeEvent> 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<String, Object> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user