From 0f0859e34ddabb4f1230d7c7fb174a9aef151169 Mon Sep 17 00:00:00 2001 From: Alexxigang <37231458+Alexxigang@users.noreply.github.com> Date: Thu, 9 Apr 2026 22:26:46 +0800 Subject: [PATCH] fix(core): enable pending tool recovery by default --- .../java/io/agentscope/core/ReActAgent.java | 3485 ++++++++--------- ...ctAgentSessionPendingToolRecoveryTest.java | 191 + 2 files changed, 1933 insertions(+), 1743 deletions(-) create mode 100644 agentscope-core/src/test/java/io/agentscope/core/session/ReActAgentSessionPendingToolRecoveryTest.java diff --git a/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java b/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java index 266c3fd8b..7a4310c6c 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java +++ b/agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java @@ -1,1743 +1,1742 @@ -/* - * Copyright 2024-2026 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.agentscope.core; - -import io.agentscope.core.agent.StructuredOutputCapableAgent; -import io.agentscope.core.agent.accumulator.ReasoningContext; -import io.agentscope.core.hook.ActingChunkEvent; -import io.agentscope.core.hook.Hook; -import io.agentscope.core.hook.HookEvent; -import io.agentscope.core.hook.PendingToolRecoveryHook; -import io.agentscope.core.hook.PostActingEvent; -import io.agentscope.core.hook.PostReasoningEvent; -import io.agentscope.core.hook.PostSummaryEvent; -import io.agentscope.core.hook.PreActingEvent; -import io.agentscope.core.hook.PreReasoningEvent; -import io.agentscope.core.hook.PreSummaryEvent; -import io.agentscope.core.hook.ReasoningChunkEvent; -import io.agentscope.core.hook.SummaryChunkEvent; -import io.agentscope.core.interruption.InterruptContext; -import io.agentscope.core.interruption.InterruptSource; -import io.agentscope.core.memory.InMemoryMemory; -import io.agentscope.core.memory.LongTermMemory; -import io.agentscope.core.memory.LongTermMemoryMode; -import io.agentscope.core.memory.LongTermMemoryTools; -import io.agentscope.core.memory.Memory; -import io.agentscope.core.memory.StaticLongTermMemoryHook; -import io.agentscope.core.message.ContentBlock; -import io.agentscope.core.message.GenerateReason; -import io.agentscope.core.message.MessageMetadataKeys; -import io.agentscope.core.message.Msg; -import io.agentscope.core.message.MsgRole; -import io.agentscope.core.message.TextBlock; -import io.agentscope.core.message.ThinkingBlock; -import io.agentscope.core.message.ToolResultBlock; -import io.agentscope.core.message.ToolUseBlock; -import io.agentscope.core.model.ExecutionConfig; -import io.agentscope.core.model.GenerateOptions; -import io.agentscope.core.model.Model; -import io.agentscope.core.model.StructuredOutputReminder; -import io.agentscope.core.plan.PlanNotebook; -import io.agentscope.core.rag.GenericRAGHook; -import io.agentscope.core.rag.Knowledge; -import io.agentscope.core.rag.KnowledgeRetrievalTools; -import io.agentscope.core.rag.RAGMode; -import io.agentscope.core.rag.model.Document; -import io.agentscope.core.rag.model.RetrieveConfig; -import io.agentscope.core.session.Session; -import io.agentscope.core.shutdown.AgentShuttingDownException; -import io.agentscope.core.shutdown.GracefulShutdownManager; -import io.agentscope.core.shutdown.PartialReasoningPolicy; -import io.agentscope.core.skill.SkillBox; -import io.agentscope.core.skill.SkillHook; -import io.agentscope.core.state.AgentMetaState; -import io.agentscope.core.state.SessionKey; -import io.agentscope.core.state.StatePersistence; -import io.agentscope.core.state.ToolkitState; -import io.agentscope.core.tool.ToolExecutionContext; -import io.agentscope.core.tool.ToolResultMessageBuilder; -import io.agentscope.core.tool.Toolkit; -import io.agentscope.core.util.ExceptionUtils; -import io.agentscope.core.util.MessageUtils; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -/** - * ReAct (Reasoning and Acting) Agent implementation. - * - *
ReAct is an agent design pattern that combines reasoning (thinking and planning) with acting - * (tool execution) in an iterative loop. The agent alternates between these two phases until it - * either completes the task or reaches the maximum iteration limit. - * - *
Key Features: - *
Usage Example: - *
{@code
- * // Create a model
- * DashScopeChatModel model = DashScopeChatModel.builder()
- * .apiKey(System.getenv("DASHSCOPE_API_KEY"))
- * .modelName("qwen-plus")
- * .build();
- *
- * // Create a toolkit with tools
- * Toolkit toolkit = new Toolkit();
- * toolkit.registerObject(new MyToolClass());
- *
- * // Build the agent
- * ReActAgent agent = ReActAgent.builder()
- * .name("Assistant")
- * .sysPrompt("You are a helpful assistant.")
- * .model(model)
- * .toolkit(toolkit)
- * .memory(new InMemoryMemory())
- * .maxIters(10)
- * .build();
- *
- * // Use the agent
- * Msg response = agent.call(Msg.builder()
- * .name("user")
- * .role(MsgRole.USER)
- * .content(TextBlock.builder().text("What's the weather?").build())
- * .build()).block();
- * }
- *
- * @see StructuredOutputCapableAgent
- */
-public class ReActAgent extends StructuredOutputCapableAgent {
-
- private static final Logger log = LoggerFactory.getLogger(ReActAgent.class);
- private static final GracefulShutdownManager shutdownManager =
- GracefulShutdownManager.getInstance();
-
- // ==================== Core Dependencies ====================
-
- private final Memory memory;
- private final String sysPrompt;
- private final Model model;
- private final int maxIters;
- private final ExecutionConfig modelExecutionConfig;
- private final ExecutionConfig toolExecutionConfig;
- private final GenerateOptions generateOptions;
- private final PlanNotebook planNotebook;
- private final ToolExecutionContext toolExecutionContext;
- private final StatePersistence statePersistence;
-
- // ==================== Constructor ====================
-
- private ReActAgent(Builder builder, Toolkit agentToolkit) {
- super(
- builder.name,
- builder.description,
- builder.checkRunning,
- new ArrayList<>(builder.hooks),
- agentToolkit,
- builder.structuredOutputReminder);
-
- this.memory = builder.memory;
- this.sysPrompt = builder.sysPrompt;
- this.model = builder.model;
- this.maxIters = builder.maxIters;
- this.modelExecutionConfig = builder.modelExecutionConfig;
- this.toolExecutionConfig = builder.toolExecutionConfig;
- this.generateOptions = builder.generateOptions;
- this.planNotebook = builder.planNotebook;
- this.toolExecutionContext = builder.toolExecutionContext;
- this.statePersistence =
- builder.statePersistence != null
- ? builder.statePersistence
- : StatePersistence.all();
- }
-
- // ==================== New StateModule API ====================
-
- /**
- * Save agent state to the session using the new API.
- *
- * This method saves the state of all managed components according to the StatePersistence - * configuration: - * - *
This method loads the state of all managed components according to the StatePersistence
- * configuration.
- *
- * @param session the session to load state from
- * @param sessionKey the session identifier
- */
- @Override
- public boolean loadIfExists(Session session, SessionKey sessionKey) {
- shutdownManager.bindSession(this, session, sessionKey);
- return super.loadIfExists(session, sessionKey);
- }
-
- @Override
- public void loadFrom(Session session, SessionKey sessionKey) {
- shutdownManager.bindSession(this, session, sessionKey);
- // Load memory if managed
- if (statePersistence.memoryManaged()) {
- memory.loadFrom(session, sessionKey);
- }
-
- // Load toolkit activeGroups if managed
- if (statePersistence.toolkitManaged() && toolkit != null) {
- session.get(sessionKey, "toolkit_activeGroups", ToolkitState.class)
- .ifPresent(state -> toolkit.setActiveGroups(state.activeGroups()));
- }
-
- // Load PlanNotebook if managed
- if (statePersistence.planNotebookManaged() && planNotebook != null) {
- planNotebook.loadFrom(session, sessionKey);
- }
- }
-
- // ==================== Protected API ====================
-
- @Override
- protected Mono Validation rules:
- * This method streams from the model, accumulates chunks, notifies hooks, and
- * decides whether to continue to acting or return early (HITL stop, gotoReasoning, or finished).
- *
- * @param iter Current iteration number
- * @param ignoreMaxIters If true, skip maxIters check (for gotoReasoning)
- * @return Mono containing the final result message
- */
- private Mono This method executes only pending tools (those without results in memory),
- * notifies hooks for successful tool results, and decides whether to continue iteration
- * or return (HITL stop, suspended tools, or structured output).
- *
- * For tools that throw {@link io.agentscope.core.tool.ToolSuspendException}:
- * The message contains both the ToolUseBlocks and corresponding pending ToolResultBlocks
- * for the suspended tools.
- *
- * @param pendingPairs List of (ToolUseBlock, pending ToolResultBlock) pairs
- * @return Msg with GenerateReason.TOOL_SUSPENDED
- */
- private Msg buildSuspendedMsg(List If tool execution fails (timeout, error, etc.), this method generates error tool results
- * for all pending tool calls instead of propagating the error. This ensures the agent can
- * continue processing and the model receives proper error feedback.
- *
- * @param toolCalls The list of tool calls (potentially modified by PreActingEvent hooks)
- * @return Mono containing list of (ToolUseBlock, ToolResultBlock) pairs
- */
- private Mono Note: Structured output retry is now handled by StructuredOutputHook via gotoReasoning().
- *
- * @param msg The reasoning message
- * @return true if should finish, false if should continue to acting
- */
- private boolean isFinished(Msg msg) {
- if (msg == null) {
- return true;
- }
-
- List This method filters out tool calls that already have corresponding results in memory,
- * preventing duplicate execution when resuming from HITL or partial tool result scenarios.
- *
- * @return List of tool use blocks that don't have results yet, or empty list if all tools
- * have been executed
- */
- private List Hooks can observe or modify events during reasoning, acting, and other phases.
- * Multiple hooks can be added and will be executed in priority order (lower priority
- * values execute first).
- *
- * @param hook The hook to add, must not be null
- * @return This builder instance for method chaining
- * @see Hook
- */
- public Builder hook(Hook hook) {
- this.hooks.add(hook);
- return this;
- }
-
- /**
- * Adds multiple hooks for monitoring and intercepting agent execution events.
- *
- * Hooks can observe or modify events during reasoning, acting, and other phases.
- * All hooks will be executed in priority order (lower priority values execute first).
- *
- * @param hooks The list of hooks to add, must not be null
- * @return This builder instance for method chaining
- * @see Hook
- */
- public Builder hooks(List When enabled, the toolkit will automatically register a meta-tool that provides
- * information about available tools to the agent. This can help the agent understand
- * what tools are available without relying solely on the system prompt.
- *
- * @param enableMetaTool true to enable meta-tool, false to disable
- * @return This builder instance for method chaining
- */
- public Builder enableMetaTool(boolean enableMetaTool) {
- this.enableMetaTool = enableMetaTool;
- return this;
- }
-
- /**
- * Enables or disables automatic recovery from orphaned pending tool calls.
- *
- * When enabled , a {@link PendingToolRecoveryHook} is automatically
- * registered to detect and patch orphaned pending tool calls with synthetic error
- * results before agent processing begins. This prevents {@link IllegalStateException}
- * when tool execution fails, times out, or is interrupted.
- *
- * Disable this if you prefer to handle pending tool calls manually, for example
- * through HITL (Human-in-the-loop) mechanisms or custom error handling strategies.
- *
- * @param enable true to enable auto-recovery, false to disable
- * @return This builder instance for method chaining
- * @see PendingToolRecoveryHook
- */
- public Builder enablePendingToolRecovery(boolean enable) {
- this.enablePendingToolRecovery = enable;
- return this;
- }
-
- /**
- * Sets the execution configuration for model API calls.
- *
- * This configuration controls timeout, retry behavior, and backoff strategy for
- * model requests during the reasoning phase. If not set, the agent will use the
- * model's default execution configuration.
- *
- * @param modelExecutionConfig The execution configuration for model calls, can be null
- * @return This builder instance for method chaining
- * @see ExecutionConfig
- */
- public Builder modelExecutionConfig(ExecutionConfig modelExecutionConfig) {
- this.modelExecutionConfig = modelExecutionConfig;
- return this;
- }
-
- /**
- * Sets the execution configuration for tool executions.
- *
- * This configuration controls timeout, retry behavior, and backoff strategy for
- * tool calls during the acting phase. If not set, the toolkit will use its default
- * execution configuration.
- *
- * @param toolExecutionConfig The execution configuration for tool calls, can be null
- * @return This builder instance for method chaining
- * @see ExecutionConfig
- */
- public Builder toolExecutionConfig(ExecutionConfig toolExecutionConfig) {
- this.toolExecutionConfig = toolExecutionConfig;
- return this;
- }
-
- /**
- * Sets the generation options for model API calls.
- *
- * This configuration controls LLM generation parameters such as temperature, topP,
- * maxTokens, frequencyPenalty, presencePenalty, etc. These options are passed to the
- * model during the reasoning phase.
- *
- * Example usage:
- * Note: If both generateOptions and modelExecutionConfig are set,
- * the modelExecutionConfig's executionConfig will be merged into the generateOptions,
- * with modelExecutionConfig taking precedence for execution settings.
- *
- * @param generateOptions The generation options for model calls, can be null
- * @return This builder instance for method chaining
- * @see GenerateOptions
- */
- public Builder generateOptions(GenerateOptions generateOptions) {
- this.generateOptions = generateOptions;
- return this;
- }
-
- /**
- * Sets the structured output enforcement mode.
- *
- * @param reminder The structured output reminder mode, must not be null
- * @return This builder instance for method chaining
- */
- public Builder structuredOutputReminder(StructuredOutputReminder reminder) {
- this.structuredOutputReminder = reminder;
- return this;
- }
-
- /**
- * Sets the PlanNotebook for plan-based task execution.
- *
- * When provided, the PlanNotebook will be integrated into the agent:
- * The skill box is used to manage the skills for this agent. It will be used to register the skills to the toolkit.
- * Long-term memory enables the agent to remember information across sessions.
- * It can be used in combination with {@link #longTermMemoryMode(LongTermMemoryMode)}
- * to control whether memory management is automatic, agent-controlled, or both.
- *
- * @param longTermMemory The long-term memory implementation
- * @return This builder instance for method chaining
- * @see LongTermMemoryMode
- */
- public Builder longTermMemory(LongTermMemory longTermMemory) {
- this.longTermMemory = longTermMemory;
- return this;
- }
-
- /**
- * Sets the long-term memory mode.
- *
- * This determines how long-term memory is integrated with the agent:
- * Use this to control which components' state is managed by the agent during
- * saveTo/loadFrom operations. By default, all components are managed.
- *
- * Example usage:
- *
- * This is a convenience method equivalent to:
- * This context will be passed to all tools invoked by this agent and can include
- * user identity, session information, permissions, and other metadata. The context
- * from this agent level will override toolkit-level context but can be overridden by
- * call-level context.
- *
- * @param toolExecutionContext The tool execution context
- * @return This builder instance for method chaining
- */
- public Builder toolExecutionContext(ToolExecutionContext toolExecutionContext) {
- this.toolExecutionContext = toolExecutionContext;
- return this;
- }
-
- /**
- * Builds and returns a new ReActAgent instance with the configured settings.
- *
- * @return A new ReActAgent instance
- * @throws IllegalArgumentException if required parameters are missing or invalid
- */
- public ReActAgent build() {
- // Deep copy toolkit to avoid state interference between agents
- Toolkit agentToolkit = this.toolkit.copy();
-
- if (enableMetaTool) {
- agentToolkit.registerMetaTool();
- }
-
- // Register PendingToolRecoveryHook if enabled
- if (enablePendingToolRecovery) {
- hooks.add(new PendingToolRecoveryHook());
- }
-
- // Configure long-term memory if provided
- if (longTermMemory != null) {
- configureLongTermMemory(agentToolkit);
- }
-
- // Configure RAG if knowledge bases are provided
- if (!knowledgeBases.isEmpty()) {
- configureRAG(agentToolkit);
- }
-
- // Configure PlanNotebook if provided
- if (planNotebook != null) {
- configurePlan(agentToolkit);
- }
-
- // Configure SkillBox if provided
- if (skillBox != null) {
- configureSkillBox(agentToolkit);
- }
-
- return new ReActAgent(this, agentToolkit);
- }
-
- /**
- * Configures long-term memory based on the selected mode.
- *
- * This method sets up long-term memory integration:
- * This method automatically sets up the appropriate hooks or tools based on the RAG mode:
- * This method automatically:
- * This method automatically:
- * ReAct is an agent design pattern that combines reasoning (thinking and planning) with acting
+ * (tool execution) in an iterative loop. The agent alternates between these two phases until it
+ * either completes the task or reaches the maximum iteration limit.
+ *
+ * Key Features:
+ * Usage Example:
+ * This method saves the state of all managed components according to the StatePersistence
+ * configuration:
+ *
+ * This method loads the state of all managed components according to the StatePersistence
+ * configuration.
+ *
+ * @param session the session to load state from
+ * @param sessionKey the session identifier
+ */
+ @Override
+ public boolean loadIfExists(Session session, SessionKey sessionKey) {
+ shutdownManager.bindSession(this, session, sessionKey);
+ return super.loadIfExists(session, sessionKey);
+ }
+
+ @Override
+ public void loadFrom(Session session, SessionKey sessionKey) {
+ shutdownManager.bindSession(this, session, sessionKey);
+ // Load memory if managed
+ if (statePersistence.memoryManaged()) {
+ memory.loadFrom(session, sessionKey);
+ }
+
+ // Load toolkit activeGroups if managed
+ if (statePersistence.toolkitManaged() && toolkit != null) {
+ session.get(sessionKey, "toolkit_activeGroups", ToolkitState.class)
+ .ifPresent(state -> toolkit.setActiveGroups(state.activeGroups()));
+ }
+
+ // Load PlanNotebook if managed
+ if (statePersistence.planNotebookManaged() && planNotebook != null) {
+ planNotebook.loadFrom(session, sessionKey);
+ }
+ }
+
+ // ==================== Protected API ====================
+
+ @Override
+ protected Mono Validation rules:
+ * This method streams from the model, accumulates chunks, notifies hooks, and
+ * decides whether to continue to acting or return early (HITL stop, gotoReasoning, or finished).
+ *
+ * @param iter Current iteration number
+ * @param ignoreMaxIters If true, skip maxIters check (for gotoReasoning)
+ * @return Mono containing the final result message
+ */
+ private Mono This method executes only pending tools (those without results in memory),
+ * notifies hooks for successful tool results, and decides whether to continue iteration
+ * or return (HITL stop, suspended tools, or structured output).
+ *
+ * For tools that throw {@link io.agentscope.core.tool.ToolSuspendException}:
+ * The message contains both the ToolUseBlocks and corresponding pending ToolResultBlocks
+ * for the suspended tools.
+ *
+ * @param pendingPairs List of (ToolUseBlock, pending ToolResultBlock) pairs
+ * @return Msg with GenerateReason.TOOL_SUSPENDED
+ */
+ private Msg buildSuspendedMsg(List If tool execution fails (timeout, error, etc.), this method generates error tool results
+ * for all pending tool calls instead of propagating the error. This ensures the agent can
+ * continue processing and the model receives proper error feedback.
+ *
+ * @param toolCalls The list of tool calls (potentially modified by PreActingEvent hooks)
+ * @return Mono containing list of (ToolUseBlock, ToolResultBlock) pairs
+ */
+ private Mono Note: Structured output retry is now handled by StructuredOutputHook via gotoReasoning().
+ *
+ * @param msg The reasoning message
+ * @return true if should finish, false if should continue to acting
+ */
+ private boolean isFinished(Msg msg) {
+ if (msg == null) {
+ return true;
+ }
+
+ List This method filters out tool calls that already have corresponding results in memory,
+ * preventing duplicate execution when resuming from HITL or partial tool result scenarios.
+ *
+ * @return List of tool use blocks that don't have results yet, or empty list if all tools
+ * have been executed
+ */
+ private List Hooks can observe or modify events during reasoning, acting, and other phases.
+ * Multiple hooks can be added and will be executed in priority order (lower priority
+ * values execute first).
+ *
+ * @param hook The hook to add, must not be null
+ * @return This builder instance for method chaining
+ * @see Hook
+ */
+ public Builder hook(Hook hook) {
+ this.hooks.add(hook);
+ return this;
+ }
+
+ /**
+ * Adds multiple hooks for monitoring and intercepting agent execution events.
+ *
+ * Hooks can observe or modify events during reasoning, acting, and other phases.
+ * All hooks will be executed in priority order (lower priority values execute first).
+ *
+ * @param hooks The list of hooks to add, must not be null
+ * @return This builder instance for method chaining
+ * @see Hook
+ */
+ public Builder hooks(List When enabled, the toolkit will automatically register a meta-tool that provides
+ * information about available tools to the agent. This can help the agent understand
+ * what tools are available without relying solely on the system prompt.
+ *
+ * @param enableMetaTool true to enable meta-tool, false to disable
+ * @return This builder instance for method chaining
+ */
+ public Builder enableMetaTool(boolean enableMetaTool) {
+ this.enableMetaTool = enableMetaTool;
+ return this;
+ }
+
+ /**
+ * Enables or disables automatic recovery from orphaned pending tool calls.
+ *
+ * This recovery is enabled by default. When enabled, a {@link PendingToolRecoveryHook}
+ * registered to detect and patch orphaned pending tool calls with synthetic error
+ * results before agent processing begins. This prevents {@link IllegalStateException}
+ * when tool execution fails, times out, or is interrupted.
+ *
+ * Disable this if you prefer to handle pending tool calls manually, for example
+ * through HITL (Human-in-the-loop) mechanisms or custom error handling strategies.
+ *
+ * @param enable true to enable auto-recovery, false to disable
+ * @return This builder instance for method chaining
+ * @see PendingToolRecoveryHook
+ */
+ public Builder enablePendingToolRecovery(boolean enable) {
+ this.enablePendingToolRecovery = enable;
+ return this;
+ }
+
+ /**
+ * Sets the execution configuration for model API calls.
+ *
+ * This configuration controls timeout, retry behavior, and backoff strategy for
+ * model requests during the reasoning phase. If not set, the agent will use the
+ * model's default execution configuration.
+ *
+ * @param modelExecutionConfig The execution configuration for model calls, can be null
+ * @return This builder instance for method chaining
+ * @see ExecutionConfig
+ */
+ public Builder modelExecutionConfig(ExecutionConfig modelExecutionConfig) {
+ this.modelExecutionConfig = modelExecutionConfig;
+ return this;
+ }
+
+ /**
+ * Sets the execution configuration for tool executions.
+ *
+ * This configuration controls timeout, retry behavior, and backoff strategy for
+ * tool calls during the acting phase. If not set, the toolkit will use its default
+ * execution configuration.
+ *
+ * @param toolExecutionConfig The execution configuration for tool calls, can be null
+ * @return This builder instance for method chaining
+ * @see ExecutionConfig
+ */
+ public Builder toolExecutionConfig(ExecutionConfig toolExecutionConfig) {
+ this.toolExecutionConfig = toolExecutionConfig;
+ return this;
+ }
+
+ /**
+ * Sets the generation options for model API calls.
+ *
+ * This configuration controls LLM generation parameters such as temperature, topP,
+ * maxTokens, frequencyPenalty, presencePenalty, etc. These options are passed to the
+ * model during the reasoning phase.
+ *
+ * Example usage:
+ * Note: If both generateOptions and modelExecutionConfig are set,
+ * the modelExecutionConfig's executionConfig will be merged into the generateOptions,
+ * with modelExecutionConfig taking precedence for execution settings.
+ *
+ * @param generateOptions The generation options for model calls, can be null
+ * @return This builder instance for method chaining
+ * @see GenerateOptions
+ */
+ public Builder generateOptions(GenerateOptions generateOptions) {
+ this.generateOptions = generateOptions;
+ return this;
+ }
+
+ /**
+ * Sets the structured output enforcement mode.
+ *
+ * @param reminder The structured output reminder mode, must not be null
+ * @return This builder instance for method chaining
+ */
+ public Builder structuredOutputReminder(StructuredOutputReminder reminder) {
+ this.structuredOutputReminder = reminder;
+ return this;
+ }
+
+ /**
+ * Sets the PlanNotebook for plan-based task execution.
+ *
+ * When provided, the PlanNotebook will be integrated into the agent:
+ * The skill box is used to manage the skills for this agent. It will be used to register the skills to the toolkit.
+ * Long-term memory enables the agent to remember information across sessions.
+ * It can be used in combination with {@link #longTermMemoryMode(LongTermMemoryMode)}
+ * to control whether memory management is automatic, agent-controlled, or both.
+ *
+ * @param longTermMemory The long-term memory implementation
+ * @return This builder instance for method chaining
+ * @see LongTermMemoryMode
+ */
+ public Builder longTermMemory(LongTermMemory longTermMemory) {
+ this.longTermMemory = longTermMemory;
+ return this;
+ }
+
+ /**
+ * Sets the long-term memory mode.
+ *
+ * This determines how long-term memory is integrated with the agent:
+ * Use this to control which components' state is managed by the agent during
+ * saveTo/loadFrom operations. By default, all components are managed.
+ *
+ * Example usage:
+ *
+ * This is a convenience method equivalent to:
+ * This context will be passed to all tools invoked by this agent and can include
+ * user identity, session information, permissions, and other metadata. The context
+ * from this agent level will override toolkit-level context but can be overridden by
+ * call-level context.
+ *
+ * @param toolExecutionContext The tool execution context
+ * @return This builder instance for method chaining
+ */
+ public Builder toolExecutionContext(ToolExecutionContext toolExecutionContext) {
+ this.toolExecutionContext = toolExecutionContext;
+ return this;
+ }
+
+ /**
+ * Builds and returns a new ReActAgent instance with the configured settings.
+ *
+ * @return A new ReActAgent instance
+ * @throws IllegalArgumentException if required parameters are missing or invalid
+ */
+ public ReActAgent build() {
+ // Deep copy toolkit to avoid state interference between agents
+ Toolkit agentToolkit = this.toolkit.copy();
+
+ if (enableMetaTool) {
+ agentToolkit.registerMetaTool();
+ }
+
+ // Register PendingToolRecoveryHook if enabled
+ if (enablePendingToolRecovery) {
+ hooks.add(new PendingToolRecoveryHook());
+ }
+
+ // Configure long-term memory if provided
+ if (longTermMemory != null) {
+ configureLongTermMemory(agentToolkit);
+ }
+
+ // Configure RAG if knowledge bases are provided
+ if (!knowledgeBases.isEmpty()) {
+ configureRAG(agentToolkit);
+ }
+
+ // Configure PlanNotebook if provided
+ if (planNotebook != null) {
+ configurePlan(agentToolkit);
+ }
+
+ // Configure SkillBox if provided
+ if (skillBox != null) {
+ configureSkillBox(agentToolkit);
+ }
+
+ return new ReActAgent(this, agentToolkit);
+ }
+
+ /**
+ * Configures long-term memory based on the selected mode.
+ *
+ * This method sets up long-term memory integration:
+ * This method automatically sets up the appropriate hooks or tools based on the RAG mode:
+ * This method automatically:
+ * This method automatically:
+ *
- *
- *
- * @param msgs The input messages to validate
- * @param pendingIds The set of pending tool use IDs
- * @throws IllegalStateException if validation fails
- */
- private void validateAndAddToolResults(List
- *
- *
- * @param iter Current iteration number
- * @return Mono containing the final result message
- */
- private Mono>> executeToolCalls(
- List
> notifyPreActingHooks(List
{@code
- * ReActAgent agent = ReActAgent.builder()
- * .name("assistant")
- * .model(model)
- * .generateOptions(GenerateOptions.builder()
- * .temperature(0.7)
- * .topP(0.9)
- * .maxTokens(1000)
- * .build())
- * .build();
- * }
- *
- *
- *
- *
- * @param planNotebook The configured PlanNotebook instance, can be null
- * @return This builder instance for method chaining
- */
- public Builder planNotebook(PlanNotebook planNotebook) {
- this.planNotebook = planNotebook;
- return this;
- }
-
- /**
- * Sets the skill box for this agent.
- *
- *
- *
- * @param skillBox The skill box to use for this agent
- * @return This builder instance for method chaining
- */
- public Builder skillBox(SkillBox skillBox) {
- this.skillBox = skillBox;
- return this;
- }
-
- /**
- * Sets the long-term memory for this agent.
- *
- *
- *
- *
- * @param mode The long-term memory mode
- * @return This builder instance for method chaining
- * @see LongTermMemoryMode
- */
- public Builder longTermMemoryMode(LongTermMemoryMode mode) {
- this.longTermMemoryMode = mode;
- return this;
- }
-
- /**
- * Sets the state persistence configuration.
- *
- * {@code
- * ReActAgent agent = ReActAgent.builder()
- * .name("assistant")
- * .model(model)
- * .statePersistence(StatePersistence.builder()
- * .planNotebookManaged(false) // Let user manage PlanNotebook separately
- * .build())
- * .build();
- * }
- *
- * @param statePersistence The state persistence configuration
- * @return This builder instance for method chaining
- * @see StatePersistence
- */
- public Builder statePersistence(StatePersistence statePersistence) {
- this.statePersistence = statePersistence;
- return this;
- }
-
- /**
- * Enables plan functionality with default configuration.
- *
- * {@code
- * planNotebook(PlanNotebook.builder().build())
- * }
- *
- * @return This builder instance for method chaining
- */
- public Builder enablePlan() {
- this.planNotebook = PlanNotebook.builder().build();
- return this;
- }
-
- /**
- * Adds a knowledge base for RAG (Retrieval-Augmented Generation).
- *
- * @param knowledge The knowledge base to add
- * @return This builder instance for method chaining
- */
- public Builder knowledge(Knowledge knowledge) {
- if (knowledge != null) {
- this.knowledgeBases.add(knowledge);
- }
- return this;
- }
-
- /**
- * Adds multiple knowledge bases for RAG.
- *
- * @param knowledges The list of knowledge bases to add
- * @return This builder instance for method chaining
- */
- public Builder knowledges(List
- *
- */
- private void configureLongTermMemory(Toolkit agentToolkit) {
- // If agent control is enabled, register memory tools via adapter
- if (longTermMemoryMode == LongTermMemoryMode.AGENT_CONTROL
- || longTermMemoryMode == LongTermMemoryMode.BOTH) {
- agentToolkit.registerTool(new LongTermMemoryTools(longTermMemory));
- }
-
- // If static control is enabled, register the hook for automatic memory management
- if (longTermMemoryMode == LongTermMemoryMode.STATIC_CONTROL
- || longTermMemoryMode == LongTermMemoryMode.BOTH) {
- StaticLongTermMemoryHook hook =
- new StaticLongTermMemoryHook(longTermMemory, memory);
- hooks.add(hook);
- }
- }
-
- /**
- * Configures RAG (Retrieval-Augmented Generation) based on the selected mode.
- *
- *
- *
- */
- private void configureRAG(Toolkit agentToolkit) {
- // Aggregate knowledge bases if multiple are provided
- Knowledge aggregatedKnowledge;
- if (knowledgeBases.size() == 1) {
- aggregatedKnowledge = knowledgeBases.iterator().next();
- } else {
- aggregatedKnowledge = buildAggregatedKnowledge();
- }
-
- // Configure based on mode
- switch (ragMode) {
- case GENERIC -> {
- // Create and add GenericRAGHook
- GenericRAGHook ragHook =
- new GenericRAGHook(aggregatedKnowledge, retrieveConfig);
- hooks.add(ragHook);
- }
- case AGENTIC -> {
- // Register knowledge retrieval tools
- KnowledgeRetrievalTools tools =
- new KnowledgeRetrievalTools(aggregatedKnowledge, retrieveConfig);
- agentToolkit.registerTool(tools);
- }
- case NONE -> {
- // Do nothing
- }
- }
- }
-
- private Knowledge buildAggregatedKnowledge() {
- return new Knowledge() {
- @Override
- public Mono> retrieve(String query, RetrieveConfig config) {
- return Flux.fromIterable(knowledgeBases)
- .flatMap(kb -> kb.retrieve(query, config))
- .collectList()
- .map(this::mergeAndSortResults);
- }
-
- private List
> allResults) {
- return allResults.stream()
- .flatMap(List::stream)
- .collect(
- Collectors.toMap(
- Document::getId,
- doc -> doc,
- (doc1, doc2) ->
- doc1.getScore() != null
- && doc2.getScore() != null
- && doc1.getScore()
- > doc2.getScore()
- ? doc1
- : doc2))
- .values()
- .stream()
- .sorted(
- Comparator.comparing(
- Document::getScore,
- Comparator.nullsLast(Comparator.reverseOrder())))
- .limit(retrieveConfig.getLimit())
- .toList();
- }
- };
- }
-
- /**
- * Configures PlanNotebook integration.
- *
- *
- *
- */
- private void configurePlan(Toolkit agentToolkit) {
- // Register plan tools to toolkit
- agentToolkit.registerTool(planNotebook);
-
- // Add plan hint hook
- Hook planHintHook =
- new Hook() {
- @Override
- public
- *
- */
- private void configureSkillBox(Toolkit agentToolkit) {
- skillBox.bindToolkit(agentToolkit);
- // Register skill loader tools to toolkit
- skillBox.registerSkillLoadTool();
-
- // If auto upload is enabled, upload skill files
- if (skillBox.isAutoUploadSkill()) {
- skillBox.uploadSkillFiles();
- }
-
- hooks.add(new SkillHook(skillBox));
- }
- }
-}
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.core;
+
+import io.agentscope.core.agent.StructuredOutputCapableAgent;
+import io.agentscope.core.agent.accumulator.ReasoningContext;
+import io.agentscope.core.hook.ActingChunkEvent;
+import io.agentscope.core.hook.Hook;
+import io.agentscope.core.hook.HookEvent;
+import io.agentscope.core.hook.PendingToolRecoveryHook;
+import io.agentscope.core.hook.PostActingEvent;
+import io.agentscope.core.hook.PostReasoningEvent;
+import io.agentscope.core.hook.PostSummaryEvent;
+import io.agentscope.core.hook.PreActingEvent;
+import io.agentscope.core.hook.PreReasoningEvent;
+import io.agentscope.core.hook.PreSummaryEvent;
+import io.agentscope.core.hook.ReasoningChunkEvent;
+import io.agentscope.core.hook.SummaryChunkEvent;
+import io.agentscope.core.interruption.InterruptContext;
+import io.agentscope.core.interruption.InterruptSource;
+import io.agentscope.core.memory.InMemoryMemory;
+import io.agentscope.core.memory.LongTermMemory;
+import io.agentscope.core.memory.LongTermMemoryMode;
+import io.agentscope.core.memory.LongTermMemoryTools;
+import io.agentscope.core.memory.Memory;
+import io.agentscope.core.memory.StaticLongTermMemoryHook;
+import io.agentscope.core.message.ContentBlock;
+import io.agentscope.core.message.GenerateReason;
+import io.agentscope.core.message.MessageMetadataKeys;
+import io.agentscope.core.message.Msg;
+import io.agentscope.core.message.MsgRole;
+import io.agentscope.core.message.TextBlock;
+import io.agentscope.core.message.ThinkingBlock;
+import io.agentscope.core.message.ToolResultBlock;
+import io.agentscope.core.message.ToolUseBlock;
+import io.agentscope.core.model.ExecutionConfig;
+import io.agentscope.core.model.GenerateOptions;
+import io.agentscope.core.model.Model;
+import io.agentscope.core.model.StructuredOutputReminder;
+import io.agentscope.core.plan.PlanNotebook;
+import io.agentscope.core.rag.GenericRAGHook;
+import io.agentscope.core.rag.Knowledge;
+import io.agentscope.core.rag.KnowledgeRetrievalTools;
+import io.agentscope.core.rag.RAGMode;
+import io.agentscope.core.rag.model.Document;
+import io.agentscope.core.rag.model.RetrieveConfig;
+import io.agentscope.core.session.Session;
+import io.agentscope.core.shutdown.AgentShuttingDownException;
+import io.agentscope.core.shutdown.GracefulShutdownManager;
+import io.agentscope.core.shutdown.PartialReasoningPolicy;
+import io.agentscope.core.skill.SkillBox;
+import io.agentscope.core.skill.SkillHook;
+import io.agentscope.core.state.AgentMetaState;
+import io.agentscope.core.state.SessionKey;
+import io.agentscope.core.state.StatePersistence;
+import io.agentscope.core.state.ToolkitState;
+import io.agentscope.core.tool.ToolExecutionContext;
+import io.agentscope.core.tool.ToolResultMessageBuilder;
+import io.agentscope.core.tool.Toolkit;
+import io.agentscope.core.util.ExceptionUtils;
+import io.agentscope.core.util.MessageUtils;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+/**
+ * ReAct (Reasoning and Acting) Agent implementation.
+ *
+ *
+ *
+ *
+ * {@code
+ * // Create a model
+ * DashScopeChatModel model = DashScopeChatModel.builder()
+ * .apiKey(System.getenv("DASHSCOPE_API_KEY"))
+ * .modelName("qwen-plus")
+ * .build();
+ *
+ * // Create a toolkit with tools
+ * Toolkit toolkit = new Toolkit();
+ * toolkit.registerObject(new MyToolClass());
+ *
+ * // Build the agent
+ * ReActAgent agent = ReActAgent.builder()
+ * .name("Assistant")
+ * .sysPrompt("You are a helpful assistant.")
+ * .model(model)
+ * .toolkit(toolkit)
+ * .memory(new InMemoryMemory())
+ * .maxIters(10)
+ * .build();
+ *
+ * // Use the agent
+ * Msg response = agent.call(Msg.builder()
+ * .name("user")
+ * .role(MsgRole.USER)
+ * .content(TextBlock.builder().text("What's the weather?").build())
+ * .build()).block();
+ * }
+ *
+ * @see StructuredOutputCapableAgent
+ */
+public class ReActAgent extends StructuredOutputCapableAgent {
+
+ private static final Logger log = LoggerFactory.getLogger(ReActAgent.class);
+ private static final GracefulShutdownManager shutdownManager =
+ GracefulShutdownManager.getInstance();
+
+ // ==================== Core Dependencies ====================
+
+ private final Memory memory;
+ private final String sysPrompt;
+ private final Model model;
+ private final int maxIters;
+ private final ExecutionConfig modelExecutionConfig;
+ private final ExecutionConfig toolExecutionConfig;
+ private final GenerateOptions generateOptions;
+ private final PlanNotebook planNotebook;
+ private final ToolExecutionContext toolExecutionContext;
+ private final StatePersistence statePersistence;
+
+ // ==================== Constructor ====================
+
+ private ReActAgent(Builder builder, Toolkit agentToolkit) {
+ super(
+ builder.name,
+ builder.description,
+ builder.checkRunning,
+ new ArrayList<>(builder.hooks),
+ agentToolkit,
+ builder.structuredOutputReminder);
+
+ this.memory = builder.memory;
+ this.sysPrompt = builder.sysPrompt;
+ this.model = builder.model;
+ this.maxIters = builder.maxIters;
+ this.modelExecutionConfig = builder.modelExecutionConfig;
+ this.toolExecutionConfig = builder.toolExecutionConfig;
+ this.generateOptions = builder.generateOptions;
+ this.planNotebook = builder.planNotebook;
+ this.toolExecutionContext = builder.toolExecutionContext;
+ this.statePersistence =
+ builder.statePersistence != null
+ ? builder.statePersistence
+ : StatePersistence.all();
+ }
+
+ // ==================== New StateModule API ====================
+
+ /**
+ * Save agent state to the session using the new API.
+ *
+ *
+ *
+ *
+ * @param session the session to save state to
+ * @param sessionKey the session identifier
+ */
+ @Override
+ public void saveTo(Session session, SessionKey sessionKey) {
+ // Save agent metadata
+ session.save(
+ sessionKey,
+ "agent_meta",
+ new AgentMetaState(getAgentId(), getName(), getDescription(), sysPrompt));
+
+ // Save memory if managed
+ if (statePersistence.memoryManaged()) {
+ memory.saveTo(session, sessionKey);
+ }
+
+ // Save toolkit activeGroups if managed
+ if (statePersistence.toolkitManaged() && toolkit != null) {
+ session.save(
+ sessionKey,
+ "toolkit_activeGroups",
+ new ToolkitState(toolkit.getActiveGroups()));
+ }
+
+ // Save PlanNotebook if managed
+ if (statePersistence.planNotebookManaged() && planNotebook != null) {
+ planNotebook.saveTo(session, sessionKey);
+ }
+ }
+
+ /**
+ * Load agent state from the session using the new API.
+ *
+ *
+ *
+ *
+ * @param msgs The input messages to validate
+ * @param pendingIds The set of pending tool use IDs
+ * @throws IllegalStateException if validation fails
+ */
+ private void validateAndAddToolResults(List
+ *
+ *
+ * @param iter Current iteration number
+ * @return Mono containing the final result message
+ */
+ private Mono>> executeToolCalls(
+ List
> notifyPreActingHooks(List
{@code
+ * ReActAgent agent = ReActAgent.builder()
+ * .name("assistant")
+ * .model(model)
+ * .generateOptions(GenerateOptions.builder()
+ * .temperature(0.7)
+ * .topP(0.9)
+ * .maxTokens(1000)
+ * .build())
+ * .build();
+ * }
+ *
+ *
+ *
+ *
+ * @param planNotebook The configured PlanNotebook instance, can be null
+ * @return This builder instance for method chaining
+ */
+ public Builder planNotebook(PlanNotebook planNotebook) {
+ this.planNotebook = planNotebook;
+ return this;
+ }
+
+ /**
+ * Sets the skill box for this agent.
+ *
+ *
+ *
+ * @param skillBox The skill box to use for this agent
+ * @return This builder instance for method chaining
+ */
+ public Builder skillBox(SkillBox skillBox) {
+ this.skillBox = skillBox;
+ return this;
+ }
+
+ /**
+ * Sets the long-term memory for this agent.
+ *
+ *
+ *
+ *
+ * @param mode The long-term memory mode
+ * @return This builder instance for method chaining
+ * @see LongTermMemoryMode
+ */
+ public Builder longTermMemoryMode(LongTermMemoryMode mode) {
+ this.longTermMemoryMode = mode;
+ return this;
+ }
+
+ /**
+ * Sets the state persistence configuration.
+ *
+ * {@code
+ * ReActAgent agent = ReActAgent.builder()
+ * .name("assistant")
+ * .model(model)
+ * .statePersistence(StatePersistence.builder()
+ * .planNotebookManaged(false) // Let user manage PlanNotebook separately
+ * .build())
+ * .build();
+ * }
+ *
+ * @param statePersistence The state persistence configuration
+ * @return This builder instance for method chaining
+ * @see StatePersistence
+ */
+ public Builder statePersistence(StatePersistence statePersistence) {
+ this.statePersistence = statePersistence;
+ return this;
+ }
+
+ /**
+ * Enables plan functionality with default configuration.
+ *
+ * {@code
+ * planNotebook(PlanNotebook.builder().build())
+ * }
+ *
+ * @return This builder instance for method chaining
+ */
+ public Builder enablePlan() {
+ this.planNotebook = PlanNotebook.builder().build();
+ return this;
+ }
+
+ /**
+ * Adds a knowledge base for RAG (Retrieval-Augmented Generation).
+ *
+ * @param knowledge The knowledge base to add
+ * @return This builder instance for method chaining
+ */
+ public Builder knowledge(Knowledge knowledge) {
+ if (knowledge != null) {
+ this.knowledgeBases.add(knowledge);
+ }
+ return this;
+ }
+
+ /**
+ * Adds multiple knowledge bases for RAG.
+ *
+ * @param knowledges The list of knowledge bases to add
+ * @return This builder instance for method chaining
+ */
+ public Builder knowledges(List
+ *
+ */
+ private void configureLongTermMemory(Toolkit agentToolkit) {
+ // If agent control is enabled, register memory tools via adapter
+ if (longTermMemoryMode == LongTermMemoryMode.AGENT_CONTROL
+ || longTermMemoryMode == LongTermMemoryMode.BOTH) {
+ agentToolkit.registerTool(new LongTermMemoryTools(longTermMemory));
+ }
+
+ // If static control is enabled, register the hook for automatic memory management
+ if (longTermMemoryMode == LongTermMemoryMode.STATIC_CONTROL
+ || longTermMemoryMode == LongTermMemoryMode.BOTH) {
+ StaticLongTermMemoryHook hook =
+ new StaticLongTermMemoryHook(longTermMemory, memory);
+ hooks.add(hook);
+ }
+ }
+
+ /**
+ * Configures RAG (Retrieval-Augmented Generation) based on the selected mode.
+ *
+ *
+ *
+ */
+ private void configureRAG(Toolkit agentToolkit) {
+ // Aggregate knowledge bases if multiple are provided
+ Knowledge aggregatedKnowledge;
+ if (knowledgeBases.size() == 1) {
+ aggregatedKnowledge = knowledgeBases.iterator().next();
+ } else {
+ aggregatedKnowledge = buildAggregatedKnowledge();
+ }
+
+ // Configure based on mode
+ switch (ragMode) {
+ case GENERIC -> {
+ // Create and add GenericRAGHook
+ GenericRAGHook ragHook =
+ new GenericRAGHook(aggregatedKnowledge, retrieveConfig);
+ hooks.add(ragHook);
+ }
+ case AGENTIC -> {
+ // Register knowledge retrieval tools
+ KnowledgeRetrievalTools tools =
+ new KnowledgeRetrievalTools(aggregatedKnowledge, retrieveConfig);
+ agentToolkit.registerTool(tools);
+ }
+ case NONE -> {
+ // Do nothing
+ }
+ }
+ }
+
+ private Knowledge buildAggregatedKnowledge() {
+ return new Knowledge() {
+ @Override
+ public Mono> retrieve(String query, RetrieveConfig config) {
+ return Flux.fromIterable(knowledgeBases)
+ .flatMap(kb -> kb.retrieve(query, config))
+ .collectList()
+ .map(this::mergeAndSortResults);
+ }
+
+ private List
> allResults) {
+ return allResults.stream()
+ .flatMap(List::stream)
+ .collect(
+ Collectors.toMap(
+ Document::getId,
+ doc -> doc,
+ (doc1, doc2) ->
+ doc1.getScore() != null
+ && doc2.getScore() != null
+ && doc1.getScore()
+ > doc2.getScore()
+ ? doc1
+ : doc2))
+ .values()
+ .stream()
+ .sorted(
+ Comparator.comparing(
+ Document::getScore,
+ Comparator.nullsLast(Comparator.reverseOrder())))
+ .limit(retrieveConfig.getLimit())
+ .toList();
+ }
+ };
+ }
+
+ /**
+ * Configures PlanNotebook integration.
+ *
+ *
+ *
+ */
+ private void configurePlan(Toolkit agentToolkit) {
+ // Register plan tools to toolkit
+ agentToolkit.registerTool(planNotebook);
+
+ // Add plan hint hook
+ Hook planHintHook =
+ new Hook() {
+ @Override
+ public
+ *
+ */
+ private void configureSkillBox(Toolkit agentToolkit) {
+ skillBox.bindToolkit(agentToolkit);
+ // Register skill loader tools to toolkit
+ skillBox.registerSkillLoadTool();
+
+ // If auto upload is enabled, upload skill files
+ if (skillBox.isAutoUploadSkill()) {
+ skillBox.uploadSkillFiles();
+ }
+
+ hooks.add(new SkillHook(skillBox));
+ }
+ }
+}
diff --git a/agentscope-core/src/test/java/io/agentscope/core/session/ReActAgentSessionPendingToolRecoveryTest.java b/agentscope-core/src/test/java/io/agentscope/core/session/ReActAgentSessionPendingToolRecoveryTest.java
new file mode 100644
index 000000000..a1faea1f3
--- /dev/null
+++ b/agentscope-core/src/test/java/io/agentscope/core/session/ReActAgentSessionPendingToolRecoveryTest.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.core.session;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import io.agentscope.core.ReActAgent;
+import io.agentscope.core.agent.test.MockModel;
+import io.agentscope.core.hook.Hook;
+import io.agentscope.core.hook.PostReasoningEvent;
+import io.agentscope.core.memory.InMemoryMemory;
+import io.agentscope.core.message.Msg;
+import io.agentscope.core.message.MsgRole;
+import io.agentscope.core.message.TextBlock;
+import io.agentscope.core.message.ToolResultBlock;
+import io.agentscope.core.message.ToolUseBlock;
+import io.agentscope.core.tool.Toolkit;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class ReActAgentSessionPendingToolRecoveryTest {
+
+ private static final Duration TEST_TIMEOUT = Duration.ofSeconds(5);
+
+ @Test
+ void shouldRecoverPendingToolCallsAfterSessionRestoreByDefault(@TempDir Path tempDir) {
+ String pendingToolId = "pending-tool-1";
+
+ ReActAgent initialAgent =
+ ReActAgent.builder()
+ .name("session-agent")
+ .model(MockModel.withToolCall("missing_tool", pendingToolId, Map.of()))
+ .toolkit(new Toolkit())
+ .memory(new InMemoryMemory())
+ .checkRunning(false)
+ .hook(createPostReasoningStopHook())
+ .build();
+
+ SessionManager.forSessionId("recover-default")
+ .withSession(new JsonSession(tempDir))
+ .addComponent(initialAgent)
+ .saveIfExists();
+
+ Msg firstResult = initialAgent.call(createUserMsg("first")).block(TEST_TIMEOUT);
+ assertNotNull(firstResult);
+ assertTrue(firstResult.hasContentBlocks(ToolUseBlock.class));
+
+ SessionManager.forSessionId("recover-default")
+ .withSession(new JsonSession(tempDir))
+ .addComponent(initialAgent)
+ .saveSession();
+
+ InMemoryMemory restoredMemory = new InMemoryMemory();
+ MockModel recoveredModel = new MockModel("Recovered after session restore");
+ ReActAgent restoredAgent =
+ ReActAgent.builder()
+ .name("session-agent")
+ .model(recoveredModel)
+ .toolkit(new Toolkit())
+ .memory(restoredMemory)
+ .checkRunning(false)
+ .build();
+
+ SessionManager.forSessionId("recover-default")
+ .withSession(new JsonSession(tempDir))
+ .addComponent(restoredAgent)
+ .loadIfExists();
+
+ Msg result = restoredAgent.call(createUserMsg("resume")).block(TEST_TIMEOUT);
+ assertNotNull(result);
+ assertEquals("Recovered after session restore", extractFirstText(result));
+ assertTrue(
+ containsErrorToolResult(restoredMemory.getMessages(), pendingToolId),
+ "Recovered memory should contain an auto-generated error tool result for the"
+ + " restored pending tool call");
+ assertTrue(
+ modelSawToolResult(recoveredModel.getLastMessages(), pendingToolId),
+ "Recovered model input should include the synthesized tool result before"
+ + " continuing");
+ }
+
+ @Test
+ void shouldStillThrowWhenPendingToolRecoveryIsExplicitlyDisabled(@TempDir Path tempDir) {
+ String pendingToolId = "pending-tool-2";
+
+ ReActAgent initialAgent =
+ ReActAgent.builder()
+ .name("session-agent-disabled")
+ .model(MockModel.withToolCall("missing_tool", pendingToolId, Map.of()))
+ .toolkit(new Toolkit())
+ .memory(new InMemoryMemory())
+ .checkRunning(false)
+ .hook(createPostReasoningStopHook())
+ .build();
+
+ initialAgent.call(createUserMsg("first")).block(TEST_TIMEOUT);
+
+ SessionManager.forSessionId("recover-disabled")
+ .withSession(new JsonSession(tempDir))
+ .addComponent(initialAgent)
+ .saveSession();
+
+ ReActAgent restoredAgent =
+ ReActAgent.builder()
+ .name("session-agent-disabled")
+ .model(new MockModel("Should not reach model"))
+ .toolkit(new Toolkit())
+ .memory(new InMemoryMemory())
+ .checkRunning(false)
+ .enablePendingToolRecovery(false)
+ .build();
+
+ SessionManager.forSessionId("recover-disabled")
+ .withSession(new JsonSession(tempDir))
+ .addComponent(restoredAgent)
+ .loadIfExists();
+
+ IllegalStateException error =
+ assertThrows(
+ IllegalStateException.class,
+ () -> restoredAgent.call(createUserMsg("resume")).block(TEST_TIMEOUT));
+ assertTrue(error.getMessage().contains(pendingToolId));
+ }
+
+ private Hook createPostReasoningStopHook() {
+ return new Hook() {
+ @Override
+ public