metadata) {
+ this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSpecExpander.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSpecExpander.java
new file mode 100644
index 0000000..9916d6f
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSpecExpander.java
@@ -0,0 +1,691 @@
+package com.easyagents.agent.runtime.tool.asynctool;
+
+import com.alibaba.fastjson2.JSON;
+import com.easyagents.agent.runtime.AgentRuntimeException;
+import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
+import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
+import com.easyagents.agent.runtime.tool.AgentToolCategory;
+import com.easyagents.agent.runtime.tool.AgentToolContext;
+import com.easyagents.agent.runtime.tool.AgentToolInvoker;
+import com.easyagents.agent.runtime.tool.AgentToolResult;
+import com.easyagents.agent.runtime.tool.AgentToolSpec;
+import com.easyagents.agent.runtime.tool.AgentToolVisibility;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Function;
+import java.util.regex.Pattern;
+
+/**
+ * 异步工具声明展开器。
+ *
+ * 该类将一个业务无关的 {@link AsyncToolSpec} 展开为五个普通
+ * {@link AgentToolSpec} 与 {@link AgentToolInvoker},业务方只需要实现
+ * {@link AsyncSubTools}。
+ */
+public class AsyncToolSpecExpander {
+
+ private static final Pattern SAFE_NAME = Pattern.compile("^[a-z][a-z0-9_]*$");
+ private static final String PHASE_SUBMIT = "submit";
+ private static final String PHASE_OBSERVE = "observe";
+ private static final String PHASE_RESULT = "result";
+ private static final String PHASE_CANCEL = "cancel";
+ private static final String PHASE_LIST = "list";
+ private static final String ERROR_TYPE_TIMEOUT = "TIMEOUT";
+ private static final String ERROR_TYPE_EXCEPTION = "EXCEPTION";
+
+ private final ExecutorService executor;
+
+ /**
+ * 使用公共 ForkJoinPool 创建展开器。
+ */
+ public AsyncToolSpecExpander() {
+ this(ForkJoinPool.commonPool());
+ }
+
+ /**
+ * 使用指定执行器创建展开器。
+ *
+ * @param executor 执行器
+ */
+ public AsyncToolSpecExpander(Executor executor) {
+ if (executor instanceof ExecutorService executorService) {
+ this.executor = executorService;
+ } else {
+ this.executor = new DelegatingExecutorService(executor);
+ }
+ }
+
+ /**
+ * 展开工具声明。
+ *
+ * @param spec 异步工具声明
+ * @return 五个普通工具声明
+ */
+ public List expandSpecs(AsyncToolSpec spec) {
+ AsyncToolSpec safeSpec = validate(spec);
+ List specs = new ArrayList<>(5);
+ specs.add(toolSpec(safeSpec, PHASE_SUBMIT, safeSpec.getSubmitParametersSchema(), submitOutputSchema()));
+ specs.add(toolSpec(safeSpec, PHASE_OBSERVE, observeSchema(safeSpec), taskViewOutputSchema()));
+ specs.add(toolSpec(safeSpec, PHASE_RESULT, observeSchema(safeSpec), taskViewOutputSchema()));
+ specs.add(toolSpec(safeSpec, PHASE_CANCEL, cancelSchema(), cancelOutputSchema()));
+ specs.add(toolSpec(safeSpec, PHASE_LIST, listSchema(), listOutputSchema()));
+ return specs;
+ }
+
+ /**
+ * 展开工具调用器。
+ *
+ * @param spec 异步工具声明
+ * @return 按工具名索引的调用器
+ */
+ public Map expandInvokers(AsyncToolSpec spec) {
+ AsyncToolSpec safeSpec = validate(spec);
+ Map invokers = new LinkedHashMap<>();
+ invokers.put(toolName(safeSpec, PHASE_SUBMIT), (arguments, context) -> submit(safeSpec, arguments, context));
+ invokers.put(toolName(safeSpec, PHASE_OBSERVE), (arguments, context) -> observe(safeSpec, arguments, context));
+ invokers.put(toolName(safeSpec, PHASE_RESULT), (arguments, context) -> result(safeSpec, arguments, context));
+ invokers.put(toolName(safeSpec, PHASE_CANCEL), (arguments, context) -> cancel(safeSpec, arguments, context));
+ invokers.put(toolName(safeSpec, PHASE_LIST), (arguments, context) -> list(safeSpec, arguments, context));
+ return invokers;
+ }
+
+ private AsyncToolSpec validate(AsyncToolSpec spec) {
+ if (spec == null) {
+ throw new AgentRuntimeException("Async tool spec is required.");
+ }
+ if (spec.getName() == null || spec.getName().isBlank()) {
+ throw new AgentRuntimeException("Async tool name is required.");
+ }
+ if (!SAFE_NAME.matcher(spec.getName()).matches()) {
+ throw new AgentRuntimeException("Async tool name must be safe snake_case: " + spec.getName());
+ }
+ if (spec.getSubTools() == null) {
+ throw new AgentRuntimeException("Async sub tools are required: " + spec.getName());
+ }
+ if (spec.getSubmitParametersSchema() == null || spec.getSubmitParametersSchema().isEmpty()) {
+ spec.setSubmitParametersSchema(emptyObjectSchema());
+ }
+ if (spec.getOptions() == null) {
+ spec.setOptions(AsyncToolOptions.defaults());
+ }
+ return spec;
+ }
+
+ private AgentToolSpec toolSpec(AsyncToolSpec spec,
+ String phase,
+ Map parametersSchema,
+ Map outputSchema) {
+ AgentToolSpec toolSpec = new AgentToolSpec();
+ toolSpec.setName(toolName(spec, phase));
+ toolSpec.setDescription(description(spec, phase));
+ toolSpec.setCategory(AgentToolCategory.CUSTOM);
+ toolSpec.setVisibility(AgentToolVisibility.VISIBLE);
+ toolSpec.setParametersSchema(parametersSchema);
+ toolSpec.setOutputSchema(outputSchema);
+ toolSpec.setApprovalRequired(PHASE_SUBMIT.equals(phase) && spec.isApprovalRequired());
+ toolSpec.setApprovalRequest(spec.getApprovalRequest());
+ Map metadata = new LinkedHashMap<>();
+ metadata.putAll(spec.getMetadata());
+ metadata.put("asyncTool", true);
+ metadata.put("asyncToolName", spec.getName());
+ metadata.put("asyncToolPhase", phase);
+ toolSpec.setMetadata(metadata);
+ return toolSpec;
+ }
+
+ private String description(AsyncToolSpec spec, String phase) {
+ String prefix = spec.getDescription() == null || spec.getDescription().isBlank()
+ ? "Async tool " + spec.getName()
+ : spec.getDescription();
+ return switch (phase) {
+ case PHASE_SUBMIT -> prefix
+ + " This is the default entry point when the user asks to run this tool. Submit an asynchronous task with the normal tool arguments and return task_id.";
+ case PHASE_OBSERVE -> prefix
+ + " Use immediately after submit with the returned task_id to check progress and incremental events. Do not ask the user for task_id immediately after submit.";
+ case PHASE_RESULT -> prefix
+ + " Use only when a known task_id should return the final result, or the current observation if the task is still running.";
+ case PHASE_CANCEL -> prefix
+ + " Use only when the user explicitly asks to cancel a known asynchronous task by task_id.";
+ case PHASE_LIST -> prefix
+ + " Use only when the user explicitly asks to list visible asynchronous tasks in the current context.";
+ default -> prefix;
+ };
+ }
+
+ private AgentToolResult submit(AsyncToolSpec spec, Map arguments, AgentToolContext context) {
+ return execute(spec, PHASE_SUBMIT, context, spec.getOptions().getSubmitTimeout(),
+ guardedContext -> {
+ AsyncToolSubmitResult result = spec.getSubTools().submit(safeMap(arguments), guardedContext);
+ return wrapSubmit(spec, result, guardedContext);
+ });
+ }
+
+ private AgentToolResult observe(AsyncToolSpec spec, Map arguments, AgentToolContext context) {
+ return execute(spec, PHASE_OBSERVE, context, spec.getOptions().getObserveTimeout(),
+ guardedContext -> {
+ AsyncToolObserveRequest request = observeRequest(arguments, spec.getOptions());
+ return wrapTaskView(spec, PHASE_OBSERVE,
+ spec.getSubTools().observe(request, guardedContext), guardedContext);
+ });
+ }
+
+ private AgentToolResult result(AsyncToolSpec spec, Map arguments, AgentToolContext context) {
+ return execute(spec, PHASE_RESULT, context, spec.getOptions().getResultTimeout(),
+ guardedContext -> {
+ AsyncToolResultRequest request = resultRequest(arguments, spec.getOptions());
+ return wrapTaskView(spec, PHASE_RESULT,
+ spec.getSubTools().result(request, guardedContext), guardedContext);
+ });
+ }
+
+ private AgentToolResult cancel(AsyncToolSpec spec, Map arguments, AgentToolContext context) {
+ return execute(spec, PHASE_CANCEL, context, spec.getOptions().getCancelTimeout(),
+ guardedContext -> {
+ AsyncToolCancelRequest request = cancelRequest(arguments);
+ return wrapCancel(spec, spec.getSubTools().cancel(request, guardedContext), guardedContext);
+ });
+ }
+
+ private AgentToolResult list(AsyncToolSpec spec, Map arguments, AgentToolContext context) {
+ return execute(spec, PHASE_LIST, context, spec.getOptions().getListTimeout(),
+ guardedContext -> {
+ AsyncToolListRequest request = listRequest(arguments);
+ return wrapList(spec, spec.getSubTools().list(request, guardedContext), guardedContext);
+ });
+ }
+
+ private AgentToolResult execute(AsyncToolSpec spec,
+ String phase,
+ AgentToolContext context,
+ Duration timeout,
+ Function supplier) {
+ AtomicBoolean active = new AtomicBoolean(true);
+ AgentToolContext guardedContext = guardedContext(context, active);
+ Future future = executor.submit(() -> supplier.apply(guardedContext));
+ try {
+ return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
+ } catch (TimeoutException error) {
+ active.set(false);
+ future.cancel(true);
+ AgentToolResult result = failureResult(spec, phase, null, AsyncToolTaskStatus.TIMEOUT,
+ ERROR_TYPE_TIMEOUT, "Async tool " + phase + " timed out.");
+ emitFailure(spec, phase, context, null, AsyncToolTaskStatus.TIMEOUT, result.getErrorMessage());
+ return result;
+ } catch (InterruptedException error) {
+ active.set(false);
+ Thread.currentThread().interrupt();
+ AgentToolResult result = failureResult(spec, phase, null, AsyncToolTaskStatus.FAILED,
+ ERROR_TYPE_EXCEPTION, "Async tool " + phase + " interrupted.");
+ emitFailure(spec, phase, context, null, AsyncToolTaskStatus.FAILED, result.getErrorMessage());
+ return result;
+ } catch (ExecutionException error) {
+ active.set(false);
+ Throwable cause = error.getCause() == null ? error : error.getCause();
+ String message = cause.getMessage() == null || cause.getMessage().isBlank()
+ ? "Async tool " + phase + " failed."
+ : cause.getMessage();
+ AgentToolResult result = failureResult(spec, phase, null, AsyncToolTaskStatus.FAILED,
+ ERROR_TYPE_EXCEPTION, message);
+ emitFailure(spec, phase, context, null, AsyncToolTaskStatus.FAILED, result.getErrorMessage());
+ return result;
+ }
+ }
+
+ private AgentToolContext guardedContext(AgentToolContext source, AtomicBoolean active) {
+ if (source == null) {
+ return null;
+ }
+ AgentToolContext context = new AgentToolContext();
+ context.setRequestId(source.getRequestId());
+ context.setTraceId(source.getTraceId());
+ context.setSessionId(source.getSessionId());
+ context.setAgentId(source.getAgentId());
+ context.setToolCallId(source.getToolCallId());
+ context.setRuntimeContext(source.getRuntimeContext());
+ context.setMetadata(new LinkedHashMap<>(source.getMetadata()));
+ context.setEventEmitter(event -> {
+ // 超时后底层业务可能仍在运行,迟到事件不能再覆盖 runtime 已返回的失败语义。
+ if (active.get()) {
+ source.emitEvent(event);
+ }
+ });
+ return context;
+ }
+
+ private AgentToolResult wrapSubmit(AsyncToolSpec spec, AsyncToolSubmitResult result, AgentToolContext context) {
+ AsyncToolSubmitResult safe = result == null ? new AsyncToolSubmitResult() : result;
+ AsyncToolTaskStatus status = status(safe.getStatus(), AsyncToolTaskStatus.RUNNING);
+ safe.setStatus(status);
+ if (safe.getTaskId() == null || safe.getTaskId().isBlank()) {
+ String message = "Async tool submit must return taskId.";
+ AgentToolResult toolResult = failureResult(spec, PHASE_SUBMIT, null, AsyncToolTaskStatus.FAILED,
+ ERROR_TYPE_EXCEPTION, message, safe);
+ emitFailure(spec, PHASE_SUBMIT, context, null, AsyncToolTaskStatus.FAILED, message);
+ return toolResult;
+ }
+ if (safe.getNextAction() == null || safe.getNextAction().isBlank()) {
+ safe.setNextAction(toolName(spec, PHASE_OBSERVE) + " 查看任务进度。");
+ }
+ AgentToolResult toolResult = successResult(spec, PHASE_SUBMIT, safe.getTaskId(), status,
+ modelContent(safe.getTaskId(), status, safe.getNextAction(), safe.getSummary()), safe);
+ emit(spec, PHASE_SUBMIT, AgentRuntimeEventType.ASYNC_TOOL_SUBMITTED, context, safe.getTaskId(), status,
+ safe.getCursor(), null, safe.getSummary(), null);
+ return toolResult;
+ }
+
+ private AgentToolResult wrapTaskView(AsyncToolSpec spec,
+ String phase,
+ AsyncToolTaskView view,
+ AgentToolContext context) {
+ AsyncToolTaskView safe = view == null ? new AsyncToolTaskView() : view;
+ AsyncToolTaskStatus status = status(safe.getStatus(), AsyncToolTaskStatus.RUNNING);
+ safe.setStatus(status);
+ if (safe.getTerminal() == null) {
+ safe.setTerminal(status.isTerminal());
+ }
+ if (safe.getResultAvailable() == null) {
+ safe.setResultAvailable(status.isSuccess() && safe.getResult() != null);
+ }
+ if (safe.getNextAction() == null || safe.getNextAction().isBlank()) {
+ safe.setNextAction(status.isTerminal()
+ ? "任务已结束。"
+ : toolName(spec, PHASE_OBSERVE) + " 继续查看任务进度。");
+ }
+ AgentToolResult toolResult = successResult(spec, phase, safe.getTaskId(), status,
+ modelContent(safe.getTaskId(), status, safe.getNextAction(), safe.getSummary(),
+ Boolean.TRUE.equals(safe.getResultAvailable()), safe.getResult()), safe);
+ emit(spec, phase, PHASE_RESULT.equals(phase)
+ ? AgentRuntimeEventType.ASYNC_TOOL_RESULT
+ : AgentRuntimeEventType.ASYNC_TOOL_OBSERVED,
+ context, safe.getTaskId(), status, safe.getCursor(), safe.getNextCursor(), safe.getSummary(),
+ safe.getErrorMessage(), safe.getResultAvailable());
+ return toolResult;
+ }
+
+ private AgentToolResult wrapCancel(AsyncToolSpec spec, AsyncToolCancelResult result, AgentToolContext context) {
+ AsyncToolCancelResult safe = result == null ? new AsyncToolCancelResult() : result;
+ AsyncToolTaskStatus status = status(safe.getStatus(), AsyncToolTaskStatus.CANCELLING);
+ safe.setStatus(status);
+ boolean success = safe.getErrorMessage() == null || safe.getErrorMessage().isBlank();
+ AgentToolResult toolResult = success
+ ? successResult(spec, PHASE_CANCEL, safe.getTaskId(), status,
+ modelContent(safe.getTaskId(), status, "继续使用 " + toolName(spec, PHASE_OBSERVE) + " 查看取消状态。",
+ safe.getMessage()), safe)
+ : failureResult(spec, PHASE_CANCEL, safe.getTaskId(), status, ERROR_TYPE_EXCEPTION, safe.getErrorMessage(), safe);
+ emit(spec, PHASE_CANCEL, success ? AgentRuntimeEventType.ASYNC_TOOL_CANCELLED : AgentRuntimeEventType.ASYNC_TOOL_FAILED,
+ context, safe.getTaskId(), status, null, null, safe.getMessage(), safe.getErrorMessage());
+ return toolResult;
+ }
+
+ private AgentToolResult wrapList(AsyncToolSpec spec, AsyncToolTaskListResult result, AgentToolContext context) {
+ AsyncToolTaskListResult safe = result == null ? new AsyncToolTaskListResult() : result;
+ String summary = "共 " + safe.getTasks().size() + " 个任务。";
+ AgentToolResult toolResult = successResult(spec, PHASE_LIST, null, null,
+ modelContent(null, null, "按 task_id 使用观察或结果工具查看详情。", summary), safe);
+ emit(spec, PHASE_LIST, AgentRuntimeEventType.ASYNC_TOOL_LISTED, context, null, null, null, null, summary, null);
+ return toolResult;
+ }
+
+ private AgentToolResult successResult(AsyncToolSpec spec,
+ String phase,
+ String taskId,
+ AsyncToolTaskStatus status,
+ String modelContent,
+ Object displayContent) {
+ AgentToolResult result = AgentToolResult.success(truncate(modelContent, spec.getOptions().getMaxModelContentLength()));
+ result.setDisplayContent(displayContent);
+ result.setMetadata(metadata(spec, phase, taskId, status));
+ return result;
+ }
+
+ private AgentToolResult failureResult(AsyncToolSpec spec,
+ String phase,
+ String taskId,
+ AsyncToolTaskStatus status,
+ String errorType,
+ String errorMessage) {
+ return failureResult(spec, phase, taskId, status, errorType, errorMessage, null);
+ }
+
+ private AgentToolResult failureResult(AsyncToolSpec spec,
+ String phase,
+ String taskId,
+ AsyncToolTaskStatus status,
+ String errorType,
+ String errorMessage,
+ Object displayContent) {
+ String message = errorMessage == null || errorMessage.isBlank() ? "Async tool failed." : errorMessage;
+ AgentToolResult result = AgentToolResult.failure(message);
+ result.setDisplayContent(displayContent == null ? Map.of("errorType", errorType, "message", message) : displayContent);
+ result.setMetadata(metadata(spec, phase, taskId, status));
+ result.getMetadata().put("errorType", errorType);
+ return result;
+ }
+
+ private void emitFailure(AsyncToolSpec spec,
+ String phase,
+ AgentToolContext context,
+ String taskId,
+ AsyncToolTaskStatus status,
+ String errorMessage) {
+ emit(spec, phase, AgentRuntimeEventType.ASYNC_TOOL_FAILED, context, taskId, status, null, null, null, errorMessage);
+ }
+
+ private void emit(AsyncToolSpec spec,
+ String phase,
+ AgentRuntimeEventType type,
+ AgentToolContext context,
+ String taskId,
+ AsyncToolTaskStatus status,
+ Long cursor,
+ Long nextCursor,
+ String summary,
+ String errorMessage) {
+ emit(spec, phase, type, context, taskId, status, cursor, nextCursor, summary, errorMessage, null);
+ }
+
+ private void emit(AsyncToolSpec spec,
+ String phase,
+ AgentRuntimeEventType type,
+ AgentToolContext context,
+ String taskId,
+ AsyncToolTaskStatus status,
+ Long cursor,
+ Long nextCursor,
+ String summary,
+ String errorMessage,
+ Boolean resultAvailable) {
+ if (context == null) {
+ return;
+ }
+ AgentRuntimeEvent event = AgentRuntimeEvent.of(type);
+ event.setTraceId(context.getTraceId());
+ event.setSessionId(context.getSessionId());
+ event.setAgentId(context.getAgentId());
+ event.setToolCallId(context.getToolCallId());
+ event.getMetadata().putAll(spec.getMetadata());
+ putIfNotNull(event.getMetadata(), "requestId", context.getRequestId());
+ event.getPayload().put("asyncToolName", spec.getName());
+ event.getPayload().put("phase", phase);
+ putIfNotNull(event.getPayload(), "toolDisplayName", spec.getMetadata().get("toolDisplayName"));
+ putIfNotNull(event.getPayload(), "taskId", taskId);
+ putIfNotNull(event.getPayload(), "status", status == null ? null : status.name());
+ putIfNotNull(event.getPayload(), "cursor", cursor);
+ putIfNotNull(event.getPayload(), "nextCursor", nextCursor);
+ putIfNotNull(event.getPayload(), "summary", truncate(summary, spec.getOptions().getMaxEventTextLength()));
+ putIfNotNull(event.getPayload(), "errorMessage", truncate(errorMessage, spec.getOptions().getMaxEventTextLength()));
+ putIfNotNull(event.getPayload(), "resultAvailable", resultAvailable);
+ event.getMetadata().put("asyncTool", true);
+ event.getMetadata().put("asyncToolName", spec.getName());
+ event.getMetadata().put("asyncToolPhase", phase);
+ context.emitEvent(event);
+ }
+
+ private Map metadata(AsyncToolSpec spec, String phase, String taskId, AsyncToolTaskStatus status) {
+ Map metadata = new LinkedHashMap<>();
+ metadata.putAll(spec.getMetadata());
+ metadata.put("asyncTool", true);
+ metadata.put("asyncToolName", spec.getName());
+ metadata.put("asyncToolPhase", phase);
+ putIfNotNull(metadata, "taskId", taskId);
+ putIfNotNull(metadata, "status", status == null ? null : status.name());
+ return metadata;
+ }
+
+ private AsyncToolObserveRequest observeRequest(Map arguments, AsyncToolOptions options) {
+ AsyncToolObserveRequest request = new AsyncToolObserveRequest();
+ request.setTaskId(stringValue(arguments, "taskId"));
+ request.setCursor(longValue(arguments, "cursor"));
+ request.setLimit(limit(arguments, options));
+ return request;
+ }
+
+ private AsyncToolResultRequest resultRequest(Map arguments, AsyncToolOptions options) {
+ AsyncToolResultRequest request = new AsyncToolResultRequest();
+ request.setTaskId(stringValue(arguments, "taskId"));
+ request.setCursor(longValue(arguments, "cursor"));
+ request.setLimit(limit(arguments, options));
+ return request;
+ }
+
+ private AsyncToolCancelRequest cancelRequest(Map arguments) {
+ AsyncToolCancelRequest request = new AsyncToolCancelRequest();
+ request.setTaskId(stringValue(arguments, "taskId"));
+ request.setReason(stringValue(arguments, "reason"));
+ return request;
+ }
+
+ private AsyncToolListRequest listRequest(Map arguments) {
+ AsyncToolListRequest request = new AsyncToolListRequest();
+ String status = stringValue(arguments, "status");
+ if (status != null && !status.isBlank()) {
+ request.setStatus(AsyncToolTaskStatus.valueOf(status.trim().toUpperCase(Locale.ROOT)));
+ }
+ return request;
+ }
+
+ private Integer limit(Map arguments, AsyncToolOptions options) {
+ Integer limit = intValue(arguments, "limit");
+ if (limit == null || limit <= 0) {
+ return options.getDefaultEventLimit();
+ }
+ return Math.min(limit, options.getMaxEventLimit());
+ }
+
+ private String modelContent(String taskId, AsyncToolTaskStatus status, String nextAction, String summary) {
+ return modelContent(taskId, status, nextAction, summary, false, null);
+ }
+
+ private String modelContent(String taskId,
+ AsyncToolTaskStatus status,
+ String nextAction,
+ String summary,
+ boolean resultAvailable,
+ Object result) {
+ StringBuilder builder = new StringBuilder();
+ if (taskId != null && !taskId.isBlank()) {
+ builder.append("task_id: ").append(taskId).append('\n');
+ }
+ if (status != null) {
+ builder.append("status: ").append(status.name()).append('\n');
+ }
+ if (summary != null && !summary.isBlank()) {
+ builder.append("summary: ").append(summary).append('\n');
+ }
+ if (resultAvailable) {
+ builder.append("result_available: true").append('\n');
+ builder.append("result: ").append(modelResult(result)).append('\n');
+ }
+ if (nextAction != null && !nextAction.isBlank()) {
+ builder.append("next_action: ").append(nextAction);
+ }
+ return builder.toString();
+ }
+
+ private String modelResult(Object result) {
+ if (result == null) {
+ return "";
+ }
+ if (result instanceof CharSequence
+ || result instanceof Number
+ || result instanceof Boolean
+ || result instanceof Character
+ || result instanceof Enum>) {
+ return String.valueOf(result);
+ }
+ try {
+ return JSON.toJSONString(result);
+ } catch (Exception ignored) {
+ return String.valueOf(result);
+ }
+ }
+
+ private String toolName(AsyncToolSpec spec, String phase) {
+ return spec.getName() + "_" + phase;
+ }
+
+ private AsyncToolTaskStatus status(AsyncToolTaskStatus status, AsyncToolTaskStatus defaultStatus) {
+ return status == null ? defaultStatus : status;
+ }
+
+ private Map emptyObjectSchema() {
+ Map schema = new LinkedHashMap<>();
+ schema.put("type", "object");
+ schema.put("properties", new LinkedHashMap<>());
+ return schema;
+ }
+
+ private Map observeSchema(AsyncToolSpec spec) {
+ Map schema = new LinkedHashMap<>();
+ schema.put("type", "object");
+ Map properties = new LinkedHashMap<>();
+ properties.put("taskId", property("string", "Task id returned by submit."));
+ properties.put("cursor", property("integer", "Event cursor returned by previous observe/result."));
+ properties.put("limit", property("integer", "Maximum number of incremental events."));
+ schema.put("properties", properties);
+ schema.put("required", List.of("taskId"));
+ return schema;
+ }
+
+ private Map cancelSchema() {
+ Map schema = new LinkedHashMap<>();
+ schema.put("type", "object");
+ Map properties = new LinkedHashMap<>();
+ properties.put("taskId", property("string", "Task id returned by submit."));
+ properties.put("reason", property("string", "Optional cancellation reason."));
+ schema.put("properties", properties);
+ schema.put("required", List.of("taskId"));
+ return schema;
+ }
+
+ private Map listSchema() {
+ Map schema = new LinkedHashMap<>();
+ schema.put("type", "object");
+ Map properties = new LinkedHashMap<>();
+ properties.put("status", property("string", "Optional task status filter."));
+ schema.put("properties", properties);
+ return schema;
+ }
+
+ private Map submitOutputSchema() {
+ return outputSchema("AsyncToolSubmitResult");
+ }
+
+ private Map taskViewOutputSchema() {
+ return outputSchema("AsyncToolTaskView");
+ }
+
+ private Map cancelOutputSchema() {
+ return outputSchema("AsyncToolCancelResult");
+ }
+
+ private Map listOutputSchema() {
+ return outputSchema("AsyncToolTaskListResult");
+ }
+
+ private Map outputSchema(String title) {
+ Map schema = new LinkedHashMap<>();
+ schema.put("type", "object");
+ schema.put("title", title);
+ return schema;
+ }
+
+ private Map property(String type, String description) {
+ Map property = new LinkedHashMap<>();
+ property.put("type", type);
+ property.put("description", description);
+ return property;
+ }
+
+ private Map safeMap(Map arguments) {
+ return arguments == null ? new LinkedHashMap<>() : new LinkedHashMap<>(arguments);
+ }
+
+ private String stringValue(Map arguments, String key) {
+ Object value = arguments == null ? null : arguments.get(key);
+ return value == null ? null : String.valueOf(value);
+ }
+
+ private Long longValue(Map arguments, String key) {
+ Object value = arguments == null ? null : arguments.get(key);
+ if (value instanceof Number number) {
+ return number.longValue();
+ }
+ if (value == null || String.valueOf(value).isBlank()) {
+ return null;
+ }
+ return Long.parseLong(String.valueOf(value));
+ }
+
+ private Integer intValue(Map arguments, String key) {
+ Object value = arguments == null ? null : arguments.get(key);
+ if (value instanceof Number number) {
+ return number.intValue();
+ }
+ if (value == null || String.valueOf(value).isBlank()) {
+ return null;
+ }
+ return Integer.parseInt(String.valueOf(value));
+ }
+
+ private void putIfNotNull(Map target, String key, Object value) {
+ if (value != null) {
+ target.put(key, value);
+ }
+ }
+
+ private String truncate(String value, int maxLength) {
+ if (value == null || value.length() <= maxLength) {
+ return value;
+ }
+ return value.substring(0, Math.max(0, maxLength)) + "...";
+ }
+
+ private static class DelegatingExecutorService extends AbstractExecutorService {
+
+ private final Executor executor;
+ private volatile boolean shutdown;
+
+ private DelegatingExecutorService(Executor executor) {
+ this.executor = executor == null ? ForkJoinPool.commonPool() : executor;
+ }
+
+ @Override
+ public void shutdown() {
+ shutdown = true;
+ }
+
+ @Override
+ public List shutdownNow() {
+ shutdown = true;
+ return List.of();
+ }
+
+ @Override
+ public boolean isShutdown() {
+ return shutdown;
+ }
+
+ @Override
+ public boolean isTerminated() {
+ return shutdown;
+ }
+
+ @Override
+ public boolean awaitTermination(long timeout, TimeUnit unit) {
+ return shutdown;
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ executor.execute(command);
+ }
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSubmitResult.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSubmitResult.java
new file mode 100644
index 0000000..3c6ffe1
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSubmitResult.java
@@ -0,0 +1,153 @@
+package com.easyagents.agent.runtime.tool.asynctool;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 异步工具提交结果。
+ */
+public class AsyncToolSubmitResult {
+
+ private String taskId;
+ private AsyncToolTaskStatus status;
+ /**
+ * 提交后调用方已读取到的事件位置,后续 observe 可从该位置继续增量读取。
+ */
+ private Long cursor;
+ private String summary;
+ private String nextAction;
+ private Map payload = new LinkedHashMap<>();
+ private Map metadata = new LinkedHashMap<>();
+
+ /**
+ * 创建空提交结果。
+ */
+ public AsyncToolSubmitResult() {
+ }
+
+ /**
+ * 获取任务 ID。
+ *
+ * @return 任务 ID
+ */
+ public String getTaskId() {
+ return taskId;
+ }
+
+ /**
+ * 设置任务 ID。
+ *
+ * @param taskId 任务 ID
+ */
+ public void setTaskId(String taskId) {
+ this.taskId = taskId;
+ }
+
+ /**
+ * 获取任务状态。
+ *
+ * @return 任务状态
+ */
+ public AsyncToolTaskStatus getStatus() {
+ return status;
+ }
+
+ /**
+ * 设置任务状态。
+ *
+ * @param status 任务状态
+ */
+ public void setStatus(AsyncToolTaskStatus status) {
+ this.status = status;
+ }
+
+ /**
+ * 获取当前事件读取位置。
+ *
+ * @return 当前事件读取位置
+ */
+ public Long getCursor() {
+ return cursor;
+ }
+
+ /**
+ * 设置当前事件读取位置。
+ *
+ * @param cursor 当前事件读取位置
+ */
+ public void setCursor(Long cursor) {
+ this.cursor = cursor;
+ }
+
+ /**
+ * 获取摘要。
+ *
+ * @return 摘要
+ */
+ public String getSummary() {
+ return summary;
+ }
+
+ /**
+ * 设置摘要。
+ *
+ * @param summary 摘要
+ */
+ public void setSummary(String summary) {
+ this.summary = summary;
+ }
+
+ /**
+ * 获取下一步建议。
+ *
+ * @return 下一步建议
+ */
+ public String getNextAction() {
+ return nextAction;
+ }
+
+ /**
+ * 设置下一步建议。
+ *
+ * @param nextAction 下一步建议
+ */
+ public void setNextAction(String nextAction) {
+ this.nextAction = nextAction;
+ }
+
+ /**
+ * 获取业务扩展载荷。
+ *
+ * @return 业务扩展载荷
+ */
+ public Map getPayload() {
+ return payload;
+ }
+
+ /**
+ * 设置业务扩展载荷。
+ *
+ * @param payload 业务扩展载荷
+ */
+ public void setPayload(Map payload) {
+ this.payload = payload == null ? new LinkedHashMap<>() : payload;
+ }
+
+ /**
+ * 获取元数据。
+ *
+ * @return 元数据
+ */
+ public Map getMetadata() {
+ return metadata;
+ }
+
+ /**
+ * 设置元数据。
+ *
+ * @param metadata 元数据
+ */
+ public void setMetadata(Map metadata) {
+ this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskEvent.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskEvent.java
new file mode 100644
index 0000000..43fa66b
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskEvent.java
@@ -0,0 +1,116 @@
+package com.easyagents.agent.runtime.tool.asynctool;
+
+import java.time.Instant;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 异步工具任务事件。
+ */
+public class AsyncToolTaskEvent {
+
+ /**
+ * 任务内单调递增事件序号,用于 cursor 增量读取。
+ */
+ private Long sequence;
+ private String type;
+ private String text;
+ private Instant createdAt = Instant.now();
+ private Map payload = new LinkedHashMap<>();
+
+ /**
+ * 创建空任务事件。
+ */
+ public AsyncToolTaskEvent() {
+ }
+
+ /**
+ * 获取事件序号。
+ *
+ * @return 事件序号
+ */
+ public Long getSequence() {
+ return sequence;
+ }
+
+ /**
+ * 设置事件序号。
+ *
+ * @param sequence 事件序号
+ */
+ public void setSequence(Long sequence) {
+ this.sequence = sequence;
+ }
+
+ /**
+ * 获取事件类型。
+ *
+ * @return 事件类型
+ */
+ public String getType() {
+ return type;
+ }
+
+ /**
+ * 设置事件类型。
+ *
+ * @param type 事件类型
+ */
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ /**
+ * 获取事件文本。
+ *
+ * @return 事件文本
+ */
+ public String getText() {
+ return text;
+ }
+
+ /**
+ * 设置事件文本。
+ *
+ * @param text 事件文本
+ */
+ public void setText(String text) {
+ this.text = text;
+ }
+
+ /**
+ * 获取事件创建时间。
+ *
+ * @return 事件创建时间
+ */
+ public Instant getCreatedAt() {
+ return createdAt;
+ }
+
+ /**
+ * 设置事件创建时间。
+ *
+ * @param createdAt 事件创建时间
+ */
+ public void setCreatedAt(Instant createdAt) {
+ this.createdAt = createdAt == null ? Instant.now() : createdAt;
+ }
+
+ /**
+ * 获取业务扩展载荷。
+ *
+ * @return 业务扩展载荷
+ */
+ public Map getPayload() {
+ return payload;
+ }
+
+ /**
+ * 设置业务扩展载荷。
+ *
+ * @param payload 业务扩展载荷
+ */
+ public void setPayload(Map payload) {
+ this.payload = payload == null ? new LinkedHashMap<>() : payload;
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskListResult.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskListResult.java
new file mode 100644
index 0000000..4bc5c67
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskListResult.java
@@ -0,0 +1,76 @@
+package com.easyagents.agent.runtime.tool.asynctool;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 异步工具任务列表结果。
+ */
+public class AsyncToolTaskListResult {
+
+ private List tasks = new ArrayList<>();
+ private Map payload = new LinkedHashMap<>();
+ private Map metadata = new LinkedHashMap<>();
+
+ /**
+ * 创建空任务列表结果。
+ */
+ public AsyncToolTaskListResult() {
+ }
+
+ /**
+ * 获取任务摘要列表。
+ *
+ * @return 任务摘要列表
+ */
+ public List getTasks() {
+ return tasks;
+ }
+
+ /**
+ * 设置任务摘要列表。
+ *
+ * @param tasks 任务摘要列表
+ */
+ public void setTasks(List tasks) {
+ this.tasks = tasks == null ? new ArrayList<>() : tasks;
+ }
+
+ /**
+ * 获取业务扩展载荷。
+ *
+ * @return 业务扩展载荷
+ */
+ public Map getPayload() {
+ return payload;
+ }
+
+ /**
+ * 设置业务扩展载荷。
+ *
+ * @param payload 业务扩展载荷
+ */
+ public void setPayload(Map payload) {
+ this.payload = payload == null ? new LinkedHashMap<>() : payload;
+ }
+
+ /**
+ * 获取元数据。
+ *
+ * @return 元数据
+ */
+ public Map getMetadata() {
+ return metadata;
+ }
+
+ /**
+ * 设置元数据。
+ *
+ * @param metadata 元数据
+ */
+ public void setMetadata(Map metadata) {
+ this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskStatus.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskStatus.java
new file mode 100644
index 0000000..b05335a
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskStatus.java
@@ -0,0 +1,78 @@
+package com.easyagents.agent.runtime.tool.asynctool;
+
+/**
+ * 异步工具任务对 Agent Runtime 暴露的统一状态。
+ */
+public enum AsyncToolTaskStatus {
+
+ /**
+ * 任务已创建,等待业务侧执行。
+ */
+ PENDING,
+
+ /**
+ * 任务正在执行。
+ */
+ RUNNING,
+
+ /**
+ * 任务执行成功,结果可用。
+ */
+ SUCCEEDED,
+
+ /**
+ * 任务执行失败。
+ */
+ FAILED,
+
+ /**
+ * 任务正在取消。
+ */
+ CANCELLING,
+
+ /**
+ * 任务已取消。
+ */
+ CANCELLED,
+
+ /**
+ * 任务执行超时。
+ */
+ TIMEOUT;
+
+ /**
+ * 判断状态是否为终态。
+ *
+ * @return 终态返回 true
+ */
+ public boolean isTerminal() {
+ return this == SUCCEEDED || this == FAILED || this == CANCELLED || this == TIMEOUT;
+ }
+
+ /**
+ * 判断状态是否仍在执行或等待执行。
+ *
+ * @return 仍在运行返回 true
+ */
+ public boolean isRunning() {
+ return this == PENDING || this == RUNNING || this == CANCELLING;
+ }
+
+ /**
+ * 判断状态是否表示成功。
+ *
+ * @return 成功返回 true
+ */
+ public boolean isSuccess() {
+ return this == SUCCEEDED;
+ }
+
+ /**
+ * 判断状态是否表示失败类终态。
+ *
+ * @return 失败、取消或超时返回 true
+ */
+ public boolean isFailure() {
+ return this == FAILED || this == CANCELLED || this == TIMEOUT;
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskSummary.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskSummary.java
new file mode 100644
index 0000000..6b0de06
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskSummary.java
@@ -0,0 +1,132 @@
+package com.easyagents.agent.runtime.tool.asynctool;
+
+import java.time.Instant;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 异步工具任务摘要。
+ */
+public class AsyncToolTaskSummary {
+
+ private String taskId;
+ private AsyncToolTaskStatus status;
+ private String summary;
+ private Instant createdAt;
+ private Instant updatedAt;
+ private Map payload = new LinkedHashMap<>();
+
+ /**
+ * 创建空任务摘要。
+ */
+ public AsyncToolTaskSummary() {
+ }
+
+ /**
+ * 获取任务 ID。
+ *
+ * @return 任务 ID
+ */
+ public String getTaskId() {
+ return taskId;
+ }
+
+ /**
+ * 设置任务 ID。
+ *
+ * @param taskId 任务 ID
+ */
+ public void setTaskId(String taskId) {
+ this.taskId = taskId;
+ }
+
+ /**
+ * 获取任务状态。
+ *
+ * @return 任务状态
+ */
+ public AsyncToolTaskStatus getStatus() {
+ return status;
+ }
+
+ /**
+ * 设置任务状态。
+ *
+ * @param status 任务状态
+ */
+ public void setStatus(AsyncToolTaskStatus status) {
+ this.status = status;
+ }
+
+ /**
+ * 获取任务摘要。
+ *
+ * @return 任务摘要
+ */
+ public String getSummary() {
+ return summary;
+ }
+
+ /**
+ * 设置任务摘要。
+ *
+ * @param summary 任务摘要
+ */
+ public void setSummary(String summary) {
+ this.summary = summary;
+ }
+
+ /**
+ * 获取创建时间。
+ *
+ * @return 创建时间
+ */
+ public Instant getCreatedAt() {
+ return createdAt;
+ }
+
+ /**
+ * 设置创建时间。
+ *
+ * @param createdAt 创建时间
+ */
+ public void setCreatedAt(Instant createdAt) {
+ this.createdAt = createdAt;
+ }
+
+ /**
+ * 获取更新时间。
+ *
+ * @return 更新时间
+ */
+ public Instant getUpdatedAt() {
+ return updatedAt;
+ }
+
+ /**
+ * 设置更新时间。
+ *
+ * @param updatedAt 更新时间
+ */
+ public void setUpdatedAt(Instant updatedAt) {
+ this.updatedAt = updatedAt;
+ }
+
+ /**
+ * 获取业务扩展载荷。
+ *
+ * @return 业务扩展载荷
+ */
+ public Map getPayload() {
+ return payload;
+ }
+
+ /**
+ * 设置业务扩展载荷。
+ *
+ * @param payload 业务扩展载荷
+ */
+ public void setPayload(Map payload) {
+ this.payload = payload == null ? new LinkedHashMap<>() : payload;
+ }
+}
diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskView.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskView.java
new file mode 100644
index 0000000..90d47c7
--- /dev/null
+++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolTaskView.java
@@ -0,0 +1,312 @@
+package com.easyagents.agent.runtime.tool.asynctool;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 异步工具任务视图。
+ *
+ * observe 和 result 子工具共用该实体。未完成时表达观察态,完成时可同时携带最终结果。
+ */
+public class AsyncToolTaskView {
+
+ private String taskId;
+ private AsyncToolTaskStatus status;
+ /**
+ * 调用方本次传入的已读事件位置,用于增量读取任务事件。
+ */
+ private Long cursor;
+ /**
+ * 服务端返回的下一次观察起点,调用方下次应使用该值继续读取增量事件。
+ */
+ private Long nextCursor;
+ private Integer progress;
+ private String summary;
+ private String nextAction;
+ private List events = new ArrayList<>();
+ private Object result;
+ private String errorMessage;
+ private String errorType;
+ private Boolean terminal;
+ private Boolean resultAvailable;
+ private Map payload = new LinkedHashMap<>();
+ private Map metadata = new LinkedHashMap<>();
+
+ /**
+ * 创建空任务视图。
+ */
+ public AsyncToolTaskView() {
+ }
+
+ /**
+ * 获取任务 ID。
+ *
+ * @return 任务 ID
+ */
+ public String getTaskId() {
+ return taskId;
+ }
+
+ /**
+ * 设置任务 ID。
+ *
+ * @param taskId 任务 ID
+ */
+ public void setTaskId(String taskId) {
+ this.taskId = taskId;
+ }
+
+ /**
+ * 获取任务状态。
+ *
+ * @return 任务状态
+ */
+ public AsyncToolTaskStatus getStatus() {
+ return status;
+ }
+
+ /**
+ * 设置任务状态。
+ *
+ * @param status 任务状态
+ */
+ public void setStatus(AsyncToolTaskStatus status) {
+ this.status = status;
+ }
+
+ /**
+ * 获取本次请求的已读事件位置。
+ *
+ * @return 已读事件位置
+ */
+ public Long getCursor() {
+ return cursor;
+ }
+
+ /**
+ * 设置本次请求的已读事件位置。
+ *
+ * @param cursor 已读事件位置
+ */
+ public void setCursor(Long cursor) {
+ this.cursor = cursor;
+ }
+
+ /**
+ * 获取下一次观察起点。
+ *
+ * @return 下一次观察起点
+ */
+ public Long getNextCursor() {
+ return nextCursor;
+ }
+
+ /**
+ * 设置下一次观察起点。
+ *
+ * @param nextCursor 下一次观察起点
+ */
+ public void setNextCursor(Long nextCursor) {
+ this.nextCursor = nextCursor;
+ }
+
+ /**
+ * 获取进度百分比。
+ *
+ * @return 进度百分比
+ */
+ public Integer getProgress() {
+ return progress;
+ }
+
+ /**
+ * 设置进度百分比。
+ *
+ * @param progress 进度百分比
+ */
+ public void setProgress(Integer progress) {
+ this.progress = progress;
+ }
+
+ /**
+ * 获取摘要。
+ *
+ * @return 摘要
+ */
+ public String getSummary() {
+ return summary;
+ }
+
+ /**
+ * 设置摘要。
+ *
+ * @param summary 摘要
+ */
+ public void setSummary(String summary) {
+ this.summary = summary;
+ }
+
+ /**
+ * 获取下一步建议。
+ *
+ * @return 下一步建议
+ */
+ public String getNextAction() {
+ return nextAction;
+ }
+
+ /**
+ * 设置下一步建议。
+ *
+ * @param nextAction 下一步建议
+ */
+ public void setNextAction(String nextAction) {
+ this.nextAction = nextAction;
+ }
+
+ /**
+ * 获取本次增量事件。
+ *
+ * @return 本次增量事件
+ */
+ public List getEvents() {
+ return events;
+ }
+
+ /**
+ * 设置本次增量事件。
+ *
+ * @param events 本次增量事件
+ */
+ public void setEvents(List events) {
+ this.events = events == null ? new ArrayList<>() : events;
+ }
+
+ /**
+ * 获取最终结果。
+ *
+ * @return 最终结果
+ */
+ public Object getResult() {
+ return result;
+ }
+
+ /**
+ * 设置最终结果。
+ *
+ * @param result 最终结果
+ */
+ public void setResult(Object result) {
+ this.result = result;
+ }
+
+ /**
+ * 获取错误消息。
+ *
+ * @return 错误消息
+ */
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ /**
+ * 设置错误消息。
+ *
+ * @param errorMessage 错误消息
+ */
+ public void setErrorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+
+ /**
+ * 获取错误类型。
+ *
+ * @return 错误类型
+ */
+ public String getErrorType() {
+ return errorType;
+ }
+
+ /**
+ * 设置错误类型。
+ *
+ * @param errorType 错误类型
+ */
+ public void setErrorType(String errorType) {
+ this.errorType = errorType;
+ }
+
+ /**
+ * 获取是否终态。
+ *
+ * @return 是否终态
+ */
+ public Boolean getTerminal() {
+ return terminal;
+ }
+
+ /**
+ * 设置是否终态。
+ *
+ * @param terminal 是否终态
+ */
+ public void setTerminal(Boolean terminal) {
+ this.terminal = terminal;
+ }
+
+ /**
+ * 获取最终结果是否可用。
+ *
+ * @return 最终结果是否可用
+ */
+ public Boolean getResultAvailable() {
+ return resultAvailable;
+ }
+
+ /**
+ * 设置最终结果是否可用。
+ *
+ * @param resultAvailable 最终结果是否可用
+ */
+ public void setResultAvailable(Boolean resultAvailable) {
+ this.resultAvailable = resultAvailable;
+ }
+
+ /**
+ * 获取业务扩展载荷。
+ *
+ * @return 业务扩展载荷
+ */
+ public Map getPayload() {
+ return payload;
+ }
+
+ /**
+ * 设置业务扩展载荷。
+ *
+ * @param payload 业务扩展载荷
+ */
+ public void setPayload(Map payload) {
+ this.payload = payload == null ? new LinkedHashMap<>() : payload;
+ }
+
+ /**
+ * 获取元数据。
+ *
+ * @return 元数据
+ */
+ public Map getMetadata() {
+ return metadata;
+ }
+
+ /**
+ * 设置元数据。
+ *
+ * @param metadata 元数据
+ */
+ public void setMetadata(Map metadata) {
+ this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
+ }
+}
diff --git a/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/AgentScopeStatefulRuntimeTest.java b/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/AgentScopeStatefulRuntimeTest.java
index 80333b0..ba15b66 100644
--- a/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/AgentScopeStatefulRuntimeTest.java
+++ b/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/AgentScopeStatefulRuntimeTest.java
@@ -107,6 +107,36 @@ public class AgentScopeStatefulRuntimeTest {
Assert.assertFalse(runtime.getAgent().getHooks().stream().anyMatch(AutoContextHook.class::isInstance));
}
+ @Test
+ public void shouldKeepSystemPromptUnchangedWithoutAsyncTool() {
+ AgentScopeReActRuntime runtime = fakeRuntime();
+
+ runtime.init(initRequest());
+
+ Assert.assertEquals("system", runtime.getAgent().getSysPrompt());
+ }
+
+ @Test
+ public void shouldAppendAsyncToolProtocolPromptWhenAsyncToolsExist() {
+ AgentInitRequest request = initRequest();
+ AgentToolSpec submit = new AgentToolSpec();
+ submit.setName("demo_submit");
+ submit.setDescription("submit demo");
+ submit.getMetadata().put("asyncTool", true);
+ submit.getMetadata().put("asyncToolPhase", "submit");
+ request.getAgentDefinition().setToolSpecs(List.of(submit));
+ request.setToolInvokers(Map.of("demo_submit", (arguments, context) -> AgentToolResult.success("submitted")));
+ AgentScopeReActRuntime runtime = fakeRuntime();
+
+ runtime.init(request);
+
+ Assert.assertTrue(runtime.getAgent().getSysPrompt().startsWith("system"));
+ Assert.assertTrue(runtime.getAgent().getSysPrompt().contains("Async tool protocol:"));
+ Assert.assertTrue(runtime.getAgent().getSysPrompt().contains("These are internal execution phases."));
+ Assert.assertTrue(runtime.getAgent().getSysPrompt().contains("call its submit sub-tool first with the user-provided arguments by default"));
+ Assert.assertTrue(runtime.getAgent().getSysPrompt().contains("immediately call observe"));
+ }
+
@Test
public void shouldEmitSideEventWithRuntimeIdentityFromBridge() throws Exception {
AgentRuntimeExecutionContext context = new AgentRuntimeExecutionContext();
diff --git a/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSpecExpanderTest.java b/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSpecExpanderTest.java
new file mode 100644
index 0000000..c9431dc
--- /dev/null
+++ b/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/tool/asynctool/AsyncToolSpecExpanderTest.java
@@ -0,0 +1,408 @@
+package com.easyagents.agent.runtime.tool.asynctool;
+
+import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
+import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
+import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
+import com.easyagents.agent.runtime.tool.AgentToolContext;
+import com.easyagents.agent.runtime.tool.AgentToolInvoker;
+import com.easyagents.agent.runtime.tool.AgentToolResult;
+import com.easyagents.agent.runtime.tool.AgentToolSpec;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.*;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * 测试异步工具声明展开器。
+ */
+public class AsyncToolSpecExpanderTest {
+
+ @Test
+ public void shouldExpandFiveToolSpecsAndInvokers() {
+ AsyncToolSpec spec = spec(new StubSubTools());
+ AsyncToolSpecExpander expander = new AsyncToolSpecExpander();
+
+ List specs = expander.expandSpecs(spec);
+ Map invokers = expander.expandInvokers(spec);
+
+ Assert.assertEquals(List.of("demo_task_submit", "demo_task_observe", "demo_task_result",
+ "demo_task_cancel", "demo_task_list"), specs.stream().map(AgentToolSpec::getName).toList());
+ Assert.assertEquals(5, invokers.size());
+ Assert.assertEquals("object", specs.get(1).getParametersSchema().get("type"));
+ Assert.assertEquals("AsyncToolTaskView", specs.get(1).getOutputSchema().get("title"));
+ Assert.assertTrue(specs.get(0).getDescription().contains("default entry point when the user asks to run this tool"));
+ Assert.assertTrue(specs.get(1).getDescription().contains("Use immediately after submit"));
+ Assert.assertTrue(specs.get(1).getDescription().contains("Do not ask the user for task_id immediately after submit"));
+ Assert.assertTrue(specs.get(2).getDescription().contains("final result"));
+ Assert.assertTrue(specs.get(3).getDescription().contains("only when the user explicitly asks to cancel"));
+ Assert.assertTrue(specs.get(4).getDescription().contains("only when the user explicitly asks to list"));
+ }
+
+ @Test
+ public void shouldKeepRuntimeMetadataWhenUserMetadataUsesReservedKeys() {
+ AsyncToolSpec spec = spec(new StubSubTools());
+ spec.setMetadata(Map.of("asyncTool", false, "asyncToolName", "user_name", "custom", "value"));
+ AsyncToolSpecExpander expander = new AsyncToolSpecExpander();
+
+ AgentToolSpec toolSpec = expander.expandSpecs(spec).get(0);
+ AgentToolResult result = expander.expandInvokers(spec).get("demo_task_submit")
+ .invoke(Map.of(), context(new ArrayList<>()));
+
+ Assert.assertEquals(true, toolSpec.getMetadata().get("asyncTool"));
+ Assert.assertEquals("demo_task", toolSpec.getMetadata().get("asyncToolName"));
+ Assert.assertEquals("value", toolSpec.getMetadata().get("custom"));
+ Assert.assertEquals(true, result.getMetadata().get("asyncTool"));
+ Assert.assertEquals("demo_task", result.getMetadata().get("asyncToolName"));
+ }
+
+ @Test
+ public void shouldApplyApprovalOnlyToSubmitTool() {
+ AsyncToolSpec spec = spec(new StubSubTools());
+ AgentToolApprovalRequest approvalRequest = new AgentToolApprovalRequest();
+ approvalRequest.setApprovalPrompt("确认提交任务?");
+ spec.setApprovalRequired(true);
+ spec.setApprovalRequest(approvalRequest);
+
+ List specs = new AsyncToolSpecExpander().expandSpecs(spec);
+
+ Assert.assertTrue(specs.get(0).isApprovalRequired());
+ Assert.assertEquals("确认提交任务?", specs.get(0).getApprovalRequest().getApprovalPrompt());
+ Assert.assertFalse(specs.get(1).isApprovalRequired());
+ Assert.assertFalse(specs.get(2).isApprovalRequired());
+ Assert.assertFalse(specs.get(3).isApprovalRequired());
+ Assert.assertFalse(specs.get(4).isApprovalRequired());
+ }
+
+ @Test
+ public void shouldSubmitAndEmitSubmittedEvent() {
+ List events = new ArrayList<>();
+ AgentToolResult result = invoke("demo_task_submit", Map.of("input", "hello"), new StubSubTools(), events);
+
+ Assert.assertTrue(result.isSuccess());
+ Assert.assertTrue(result.getModelContent().contains("task_id: task-1"));
+ Assert.assertEquals("task-1", result.getMetadata().get("taskId"));
+ Assert.assertEquals(AgentRuntimeEventType.ASYNC_TOOL_SUBMITTED, events.get(0).getEventType());
+ Assert.assertEquals("task-1", events.get(0).getPayload().get("taskId"));
+ }
+
+ @Test
+ public void shouldFailWhenSubmitDoesNotReturnTaskId() {
+ List events = new ArrayList<>();
+ AgentToolResult result = invoke("demo_task_submit", Map.of(), new StubSubTools() {
+ @Override
+ public AsyncToolSubmitResult submit(Map arguments, AgentToolContext context) {
+ AsyncToolSubmitResult submitResult = super.submit(arguments, context);
+ submitResult.setTaskId(null);
+ return submitResult;
+ }
+ }, events);
+
+ Assert.assertFalse(result.isSuccess());
+ Assert.assertTrue(result.getErrorMessage().contains("taskId"));
+ Assert.assertEquals(AgentRuntimeEventType.ASYNC_TOOL_FAILED, events.get(0).getEventType());
+ }
+
+ @Test
+ public void shouldObserveWithCursorLimitAndContext() {
+ AtomicReference requestRef = new AtomicReference<>();
+ AtomicReference contextRef = new AtomicReference<>();
+ StubSubTools subTools = new StubSubTools() {
+ @Override
+ public AsyncToolTaskView observe(AsyncToolObserveRequest request, AgentToolContext context) {
+ requestRef.set(request);
+ contextRef.set(context);
+ return super.observe(request, context);
+ }
+ };
+ List events = new ArrayList<>();
+
+ AgentToolResult result = invoke("demo_task_observe", Map.of("taskId", "task-1", "cursor", 7, "limit", 999),
+ subTools, events);
+
+ AsyncToolTaskView view = (AsyncToolTaskView) result.getDisplayContent();
+ Assert.assertEquals(Long.valueOf(7), requestRef.get().getCursor());
+ Assert.assertEquals(Integer.valueOf(100), requestRef.get().getLimit());
+ Assert.assertEquals(Long.valueOf(8), view.getNextCursor());
+ Assert.assertEquals("request-1", contextRef.get().getRequestId());
+ Assert.assertEquals(AgentRuntimeEventType.ASYNC_TOOL_OBSERVED, events.get(0).getEventType());
+ }
+
+ @Test
+ public void shouldWrapInvalidArgumentsAndEmitFailedEvent() {
+ List events = new ArrayList<>();
+
+ AgentToolResult result = invoke("demo_task_observe", Map.of("taskId", "task-1", "cursor", "bad"),
+ new StubSubTools(), events);
+
+ Assert.assertFalse(result.isSuccess());
+ Assert.assertEquals(AgentRuntimeEventType.ASYNC_TOOL_FAILED, events.get(0).getEventType());
+ Assert.assertEquals("FAILED", result.getMetadata().get("status"));
+ }
+
+ @Test
+ public void shouldReturnObservationFromResultWhenRunningAndFinalResultWhenSucceeded() {
+ StubSubTools running = new StubSubTools();
+ AgentToolResult runningResult = invoke("demo_task_result", Map.of("taskId", "task-1"), running, new ArrayList<>());
+ AsyncToolTaskView runningView = (AsyncToolTaskView) runningResult.getDisplayContent();
+
+ StubSubTools succeeded = new StubSubTools() {
+ @Override
+ public AsyncToolTaskView result(AsyncToolResultRequest request, AgentToolContext context) {
+ AsyncToolTaskView view = super.result(request, context);
+ view.setStatus(AsyncToolTaskStatus.SUCCEEDED);
+ view.setResult("done");
+ return view;
+ }
+ };
+ AgentToolResult successResult = invoke("demo_task_result", Map.of("taskId", "task-1"), succeeded, new ArrayList<>());
+ AsyncToolTaskView successView = (AsyncToolTaskView) successResult.getDisplayContent();
+
+ Assert.assertEquals(AsyncToolTaskStatus.RUNNING, runningView.getStatus());
+ Assert.assertEquals(AsyncToolTaskStatus.SUCCEEDED, successView.getStatus());
+ Assert.assertEquals("done", successView.getResult());
+ Assert.assertTrue(successView.getTerminal());
+ Assert.assertTrue(successView.getResultAvailable());
+ Assert.assertFalse(runningResult.getModelContent().contains("result:"));
+ Assert.assertTrue(successResult.getModelContent().contains("result_available: true"));
+ Assert.assertTrue(successResult.getModelContent().contains("result: done"));
+ }
+
+ @Test
+ public void shouldExposeCompletedObservationResultToModelContentAsJson() {
+ List events = new ArrayList<>();
+ LinkedHashMap businessResult = new LinkedHashMap<>();
+ businessResult.put("answer", "42");
+ businessResult.put("items", List.of(1, 2));
+ StubSubTools succeeded = new StubSubTools() {
+ @Override
+ public AsyncToolTaskView observe(AsyncToolObserveRequest request, AgentToolContext context) {
+ AsyncToolTaskView view = super.observe(request, context);
+ view.setStatus(AsyncToolTaskStatus.SUCCEEDED);
+ view.setResult(businessResult);
+ return view;
+ }
+ };
+
+ AgentToolResult result = invoke("demo_task_observe", Map.of("taskId", "task-1"), succeeded, events);
+ AsyncToolTaskView view = (AsyncToolTaskView) result.getDisplayContent();
+
+ Assert.assertEquals(businessResult, view.getResult());
+ Assert.assertTrue(result.getModelContent().contains("result_available: true"));
+ Assert.assertTrue(result.getModelContent().contains("result: {\"answer\":\"42\",\"items\":[1,2]}"));
+ Assert.assertEquals(Boolean.TRUE, events.get(0).getPayload().get("resultAvailable"));
+ Assert.assertFalse(events.get(0).getPayload().containsKey("result"));
+ }
+
+ @Test
+ public void shouldCancelAndRepresentUnsupportedCancelAsFailure() {
+ AgentToolResult success = invoke("demo_task_cancel", Map.of("taskId", "task-1"), new StubSubTools(), new ArrayList<>());
+ AgentToolResult failure = invoke("demo_task_cancel", Map.of("taskId", "task-1"), new StubSubTools() {
+ @Override
+ public AsyncToolCancelResult cancel(AsyncToolCancelRequest request, AgentToolContext context) {
+ AsyncToolCancelResult result = super.cancel(request, context);
+ result.setErrorMessage("不支持取消");
+ return result;
+ }
+ }, new ArrayList<>());
+
+ Assert.assertTrue(success.isSuccess());
+ Assert.assertFalse(failure.isSuccess());
+ Assert.assertEquals("不支持取消", failure.getErrorMessage());
+ }
+
+ @Test
+ public void shouldListTasksWithoutPagination() {
+ AgentToolResult result = invoke("demo_task_list", Map.of("status", "running"), new StubSubTools(), new ArrayList<>());
+ AsyncToolTaskListResult list = (AsyncToolTaskListResult) result.getDisplayContent();
+
+ Assert.assertTrue(result.isSuccess());
+ Assert.assertEquals(1, list.getTasks().size());
+ Assert.assertFalse(Arrays.stream(AsyncToolTaskListResult.class.getDeclaredFields())
+ .anyMatch(field -> field.getName().contains("PageToken")));
+ }
+
+ @Test
+ public void shouldWrapExceptionAndEmitFailedEvent() {
+ List events = new ArrayList<>();
+ AgentToolResult result = invoke("demo_task_observe", Map.of("taskId", "task-1"), new StubSubTools() {
+ @Override
+ public AsyncToolTaskView observe(AsyncToolObserveRequest request, AgentToolContext context) {
+ throw new IllegalStateException("boom");
+ }
+ }, events);
+
+ Assert.assertFalse(result.isSuccess());
+ Assert.assertEquals("boom", result.getErrorMessage());
+ Assert.assertEquals(AgentRuntimeEventType.ASYNC_TOOL_FAILED, events.get(0).getEventType());
+ }
+
+ @Test
+ public void shouldTimeoutAndEmitFailedEvent() {
+ List events = new ArrayList<>();
+ AsyncToolOptions options = AsyncToolOptions.defaults();
+ options.setObserveTimeout(Duration.ofMillis(20));
+ StubSubTools subTools = new StubSubTools() {
+ @Override
+ public AsyncToolTaskView observe(AsyncToolObserveRequest request, AgentToolContext context) {
+ try {
+ Thread.sleep(200);
+ } catch (InterruptedException ignored) {
+ Thread.currentThread().interrupt();
+ }
+ return super.observe(request, context);
+ }
+ };
+ AgentToolResult result = invoke("demo_task_observe", Map.of("taskId", "task-1"), subTools, events, options);
+
+ Assert.assertFalse(result.isSuccess());
+ Assert.assertTrue(result.getErrorMessage().contains("timed out"));
+ Assert.assertEquals(AgentRuntimeEventType.ASYNC_TOOL_FAILED, events.get(0).getEventType());
+ }
+
+ @Test
+ public void shouldNotPutLargePayloadIntoModelContentOrEventPayload() {
+ List events = new ArrayList<>();
+ AgentToolResult result = invoke("demo_task_submit", Map.of(), new StubSubTools() {
+ @Override
+ public AsyncToolSubmitResult submit(Map arguments, AgentToolContext context) {
+ AsyncToolSubmitResult result = super.submit(arguments, context);
+ result.getPayload().put("large", "x".repeat(5000));
+ return result;
+ }
+ }, events);
+
+ Assert.assertFalse(result.getModelContent().contains("xxxxx"));
+ Assert.assertFalse(events.get(0).getPayload().containsKey("large"));
+ Assert.assertTrue(((AsyncToolSubmitResult) result.getDisplayContent()).getPayload().containsKey("large"));
+ }
+
+ @Test
+ public void shouldTrimLargeResultForModelContentAndKeepFullDisplayContent() {
+ List events = new ArrayList<>();
+ AsyncToolOptions options = AsyncToolOptions.defaults();
+ options.setMaxModelContentLength(160);
+ String largeResult = "x".repeat(5000);
+ StubSubTools succeeded = new StubSubTools() {
+ @Override
+ public AsyncToolTaskView result(AsyncToolResultRequest request, AgentToolContext context) {
+ AsyncToolTaskView view = super.result(request, context);
+ view.setStatus(AsyncToolTaskStatus.SUCCEEDED);
+ view.setResult(Map.of("large", largeResult));
+ return view;
+ }
+ };
+
+ AgentToolResult result = invoke("demo_task_result", Map.of("taskId", "task-1"), succeeded, events, options);
+ AsyncToolTaskView view = (AsyncToolTaskView) result.getDisplayContent();
+
+ Assert.assertTrue(result.getModelContent().length() <= options.getMaxModelContentLength() + 3);
+ Assert.assertTrue(result.getModelContent().contains("result_available: true"));
+ Assert.assertFalse(result.getModelContent().contains(largeResult));
+ Assert.assertEquals(Map.of("large", largeResult), view.getResult());
+ Assert.assertEquals(Boolean.TRUE, events.get(0).getPayload().get("resultAvailable"));
+ Assert.assertFalse(events.get(0).getPayload().containsKey("result"));
+ }
+
+ private AgentToolResult invoke(String toolName,
+ Map arguments,
+ AsyncSubTools subTools,
+ List events) {
+ return invoke(toolName, arguments, subTools, events, AsyncToolOptions.defaults());
+ }
+
+ private AgentToolResult invoke(String toolName,
+ Map arguments,
+ AsyncSubTools subTools,
+ List events,
+ AsyncToolOptions options) {
+ AsyncToolSpec spec = spec(subTools);
+ spec.setOptions(options);
+ AsyncToolSpecExpander expander = new AsyncToolSpecExpander(Executors.newCachedThreadPool());
+ AgentToolContext context = context(events);
+ return expander.expandInvokers(spec).get(toolName).invoke(arguments, context);
+ }
+
+ private AsyncToolSpec spec(AsyncSubTools subTools) {
+ AsyncToolSpec spec = new AsyncToolSpec();
+ spec.setName("demo_task");
+ spec.setDescription("Demo async task.");
+ spec.setSubTools(subTools);
+ spec.setSubmitParametersSchema(Map.of("type", "object", "properties", Map.of("input", Map.of("type", "string"))));
+ return spec;
+ }
+
+ private AgentToolContext context(List events) {
+ AgentToolContext context = new AgentToolContext();
+ context.setRequestId("request-1");
+ context.setTraceId("trace-1");
+ context.setSessionId("session-1");
+ context.setAgentId("agent-1");
+ context.setToolCallId("tool-call-1");
+ context.setEventEmitter(events::add);
+ return context;
+ }
+
+ private static class StubSubTools implements AsyncSubTools {
+
+ @Override
+ public AsyncToolSubmitResult submit(Map arguments, AgentToolContext context) {
+ AsyncToolSubmitResult result = new AsyncToolSubmitResult();
+ result.setTaskId("task-1");
+ result.setStatus(AsyncToolTaskStatus.RUNNING);
+ result.setCursor(0L);
+ result.setSummary("submitted");
+ return result;
+ }
+
+ @Override
+ public AsyncToolTaskView observe(AsyncToolObserveRequest request, AgentToolContext context) {
+ AsyncToolTaskView view = new AsyncToolTaskView();
+ view.setTaskId(request.getTaskId());
+ view.setStatus(AsyncToolTaskStatus.RUNNING);
+ view.setCursor(request.getCursor());
+ view.setNextCursor(request.getCursor() == null ? 1L : request.getCursor() + 1);
+ view.setSummary("running");
+ AsyncToolTaskEvent event = new AsyncToolTaskEvent();
+ event.setSequence(view.getNextCursor());
+ event.setType("TASK_LOG");
+ event.setText("progress");
+ event.setCreatedAt(Instant.now());
+ view.setEvents(List.of(event));
+ return view;
+ }
+
+ @Override
+ public AsyncToolTaskView result(AsyncToolResultRequest request, AgentToolContext context) {
+ AsyncToolObserveRequest observeRequest = new AsyncToolObserveRequest();
+ observeRequest.setTaskId(request.getTaskId());
+ observeRequest.setCursor(request.getCursor());
+ observeRequest.setLimit(request.getLimit());
+ return observe(observeRequest, context);
+ }
+
+ @Override
+ public AsyncToolCancelResult cancel(AsyncToolCancelRequest request, AgentToolContext context) {
+ AsyncToolCancelResult result = new AsyncToolCancelResult();
+ result.setTaskId(request.getTaskId());
+ result.setStatus(AsyncToolTaskStatus.CANCELLING);
+ result.setMessage("cancelling");
+ return result;
+ }
+
+ @Override
+ public AsyncToolTaskListResult list(AsyncToolListRequest request, AgentToolContext context) {
+ AsyncToolTaskSummary summary = new AsyncToolTaskSummary();
+ summary.setTaskId("task-1");
+ summary.setStatus(AsyncToolTaskStatus.RUNNING);
+ summary.setSummary("running");
+ AsyncToolTaskListResult result = new AsyncToolTaskListResult();
+ result.setTasks(List.of(summary));
+ return result;
+ }
+ }
+}