diff --git a/agentscope-core/src/main/java/io/agentscope/core/model/ExecutionConfig.java b/agentscope-core/src/main/java/io/agentscope/core/model/ExecutionConfig.java index 49409ff02e..f7ed9f2bfc 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/model/ExecutionConfig.java +++ b/agentscope-core/src/main/java/io/agentscope/core/model/ExecutionConfig.java @@ -155,6 +155,33 @@ private static boolean isRetryableError(Throwable error) { .retryOn(RETRYABLE_ERRORS) .build(); + /** + * Sentinel value for {@link #timeout} meaning "no timeout". A negative duration is never + * produced by normal usage and is recognised by {@code ToolExecutor.applyTimeout} and + * {@link ModelUtils#applyTimeoutAndRetry ModelUtils.applyTimeoutAndRetry} + * as "skip the timeout operator entirely". + * + *

This is the only way to opt out of the timeout that {@link #TOOL_DEFAULTS} and {@link + * #MODEL_DEFAULTS} always carry, because {@link #mergeConfigs} treats {@code null} as + * "inherit from fallback". + * + *

Supported paths: The sentinel is honoured on the tool path + * ({@code ToolExecutor.applyTimeout}) and the model-flux path + * ({@code ModelUtils.applyTimeoutAndRetry}), where it means genuinely unbounded + * (no timeout operator is applied). Extension consumers that read + * {@link #getTimeout()} directly and pass the value to a framework timeout operator + * must apply the same guard via {@link #isTimeoutDisabled()}: + *

+ */ + public static final Duration NO_TIMEOUT = Duration.ofNanos(-1); + /** * Standard defaults for tool executions. * @@ -184,6 +211,20 @@ public Duration getTimeout() { return timeout; } + /** + * Returns true when the configured timeout is the {@link #NO_TIMEOUT} sentinel, + * meaning consumers should skip applying any timeout operator. + * + *

Only the exact {@link #NO_TIMEOUT} sentinel is recognised; stray negative + * durations (which should be rejected by {@link Builder#timeout(Duration)}) are + * not treated as "no timeout". + * + * @return true if timeout is disabled via {@link #NO_TIMEOUT} + */ + public boolean isTimeoutDisabled() { + return NO_TIMEOUT.equals(timeout); + } + /** * Gets the maximum number of attempts. * @@ -305,14 +346,36 @@ public static class Builder { /** * Sets the timeout duration for a single execution. * - * @param timeout the timeout duration, or null for no timeout + * @param timeout the timeout duration (must be > 0, or {@link #NO_TIMEOUT}), + * or null to inherit from fallback; {@code Duration.ZERO} is not a synonym + * for {@link #noTimeout()} and is therefore rejected * @return this builder instance + * @throws IllegalArgumentException if timeout is a negative duration other than + * {@link #NO_TIMEOUT}, or if timeout is {@code Duration.ZERO} */ public Builder timeout(Duration timeout) { + if (timeout != null + && (timeout.isNegative() || timeout.isZero()) + && !NO_TIMEOUT.equals(timeout)) { + throw new IllegalArgumentException( + "timeout must be positive, got " + + timeout + + "; use NO_TIMEOUT or noTimeout() to disable it"); + } this.timeout = timeout; return this; } + /** + * Opt out of timeout entirely for this call. Equivalent to {@code timeout(NO_TIMEOUT)}. + * This is the only way to prevent the timeout inherited from {@link #TOOL_DEFAULTS} / + * {@link #MODEL_DEFAULTS}, because {@link #mergeConfigs} treats {@code null} as inherit. + */ + public Builder noTimeout() { + this.timeout = NO_TIMEOUT; + return this; + } + /** * Sets the maximum number of attempts (including the initial attempt). * diff --git a/agentscope-core/src/main/java/io/agentscope/core/model/ModelUtils.java b/agentscope-core/src/main/java/io/agentscope/core/model/ModelUtils.java index 17e436a50b..cdae41fc14 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/model/ModelUtils.java +++ b/agentscope-core/src/main/java/io/agentscope/core/model/ModelUtils.java @@ -87,7 +87,7 @@ public static Flux applyTimeoutAndRetry( if (execConfig != null) { // Apply timeout if configured Duration timeout = execConfig.getTimeout(); - if (timeout != null) { + if (timeout != null && !execConfig.isTimeoutDisabled()) { responseFlux = responseFlux.timeout( timeout, diff --git a/agentscope-core/src/main/java/io/agentscope/core/tool/ToolExecutor.java b/agentscope-core/src/main/java/io/agentscope/core/tool/ToolExecutor.java index 4745ad8ef6..c4df694b18 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/tool/ToolExecutor.java +++ b/agentscope-core/src/main/java/io/agentscope/core/tool/ToolExecutor.java @@ -155,7 +155,9 @@ private void invokeChunkCallback( // ==================== Single Tool Execution ==================== /** - * Execute a single tool call with full infrastructure support. + * Execute a single tool call (core execution only: Tracer + {@link #executeCore}; + * no scheduling, timeout, retry, shutdown guard, or id/name stamping). Use + * {@link #executeWithInfrastructure(ToolCallParam, ExecutionConfig)} for the full-infrastructure path. * * @param param Tool call parameters * @return Mono containing execution result @@ -171,8 +173,9 @@ Mono execute(ToolCallParam param) { /** * Execute a single tool call with a per-call tool request config and a per-call internal chunk - * callback. This is the single core entry point; the no-arg {@link #execute(ToolCallParam)} - * resolves the request config from its explicit runtime context and uses no internal callback. + * callback. This is the single core entry point; the 1-param {@link #execute(ToolCallParam)} + * overload resolves the request config from its explicit runtime context and uses no internal + * callback. */ Mono execute( ToolCallParam param, @@ -342,7 +345,10 @@ private Collection resolveActiveGroups(ToolCallParam param) { // ==================== Batch Tool Execution ==================== /** - * Execute multiple tool calls with concurrency control, timeout, and retry. + * Execute multiple tool calls with concurrency control plus full per-call infrastructure + * (scheduling, timeout, retry, shutdown guard, id/name stamping). Each single call is routed + * through {@link #executeWithInfrastructure(ToolUseBlock, ExecutionConfig, Agent, + * RuntimeContext, ToolRequestConfig, BiConsumer)}. * * @param toolCalls List of tool calls to execute * @param parallel Whether to execute in parallel @@ -449,16 +455,20 @@ private boolean isConcurrencySafe(ToolUseBlock toolCall, ToolRequestConfig reque } /** - * Execute a single tool call with infrastructure (scheduling, timeout, retry). + * Execute a single tool call with infrastructure (scheduling, timeout, retry, shutdown + * guard), and stamps the result with the tool call's id/name. + * + *

This overload is used by the batch path ({@link #executeAll(List, boolean, + * ExecutionConfig, Agent, RuntimeContext)}), which routes each {@link ToolUseBlock} with + * the infrastructure config, per-call request config, and chunk callback. */ - private Mono executeWithInfrastructure( + Mono executeWithInfrastructure( ToolUseBlock toolCall, ExecutionConfig executionConfig, Agent agent, RuntimeContext agentRuntimeContext, ToolRequestConfig requestConfig, BiConsumer internalChunkCallback) { - // Build tool call parameter ToolCallParam param = ToolCallParam.builder() .toolUseBlock(toolCall) @@ -466,16 +476,49 @@ private Mono executeWithInfrastructure( .runtimeContext(agentRuntimeContext) .build(); - // Get core execution Mono execution = execute(param, requestConfig, internalChunkCallback); - // Apply infrastructure layers + return applyInfrastructure(execution, executionConfig, toolCall); + } + + /** + * Execute a single tool call with full infrastructure, preserving all fields from the + * original {@link ToolCallParam} (including input). + * + *

This overload is used by {@code Toolkit.callTool} so that user-supplied fields on the + * param object are not silently discarded before reaching {@link #executeCore}. + */ + Mono executeWithInfrastructure( + ToolCallParam param, ExecutionConfig executionConfig) { + ToolUseBlock toolCall = param.getToolUseBlock(); + + Mono execution = execute(param); + + return applyInfrastructure(execution, executionConfig, toolCall); + } + + /** + * Applies the shared infrastructure pipeline (scheduling, timeout, retry, shutdown guard) + * and stamps the result with the tool call's id/name. The four infrastructure layers and + * the error-to-result conversion live here so that both entry points (batch and single) + * stay in sync when a new layer is added. + * + *

Retry semantics: {@link #applyRetry} only fires for the timeout + * {@code RuntimeException} emitted by {@link #applyTimeout}. Tool failures are converted + * to normal {@link ToolResultBlock#error} completions inside {@link #executeCore} before + * this pipeline runs, and {@link #applyShutdownGuard} runs after retry so + * shutdown signals are never seen by {@code retryWhen} either. "Retry" here means + * "retry on timeout", nothing else. + */ + private Mono applyInfrastructure( + Mono execution, + ExecutionConfig executionConfig, + ToolUseBlock toolCall) { execution = applyScheduling(execution); execution = applyTimeout(execution, executionConfig, toolCall); execution = applyRetry(execution, executionConfig, toolCall); execution = applyShutdownGuard(execution); - // Add tool metadata and error handling return execution .map(result -> result.withIdAndName(toolCall.getId(), toolCall.getName())) .onErrorResume( @@ -499,7 +542,8 @@ private Mono applyScheduling(Mono execution) { private Mono applyTimeout( Mono execution, ExecutionConfig config, ToolUseBlock toolCall) { - if (config == null || config.getTimeout() == null) { + // null = inherit from fallback, NO_TIMEOUT sentinel = explicitly disabled + if (config == null || config.getTimeout() == null || config.isTimeoutDisabled()) { return execution; } diff --git a/agentscope-core/src/main/java/io/agentscope/core/tool/ToolRegistry.java b/agentscope-core/src/main/java/io/agentscope/core/tool/ToolRegistry.java index e826e17aac..f382f2e368 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/tool/ToolRegistry.java +++ b/agentscope-core/src/main/java/io/agentscope/core/tool/ToolRegistry.java @@ -28,7 +28,9 @@ * and retrieve tools. * *

Thread Safety: This class is thread-safe, using {@link ConcurrentHashMap} for internal - * storage to support concurrent tool registration and lookup operations. + * storage to support concurrent tool registration and lookup operations. Tool instance and + * registration metadata are stored together in a single compound map entry, so put/remove of the + * two are a single atomic operation. * *

Key Responsibilities: *