How ReActAgent Handles Tool Selection and Execution in AgentScope
The ReActAgent orchestrates tool selection and execution through a reasoning-acting loop that uses the tool_choice parameter to control LLM behavior, extracts tool_use blocks from model responses, and delegates execution to the Toolkit class for dynamic function resolution and streaming result processing.
The ReActAgent in the agentscope-ai/agentscope repository implements the classic ReAct (Reasoning and Acting) pattern, enabling large language models to interleave analytical steps with external tool calls. Understanding how this agent handles tool selection and execution in AgentScope requires examining the interaction between the agent's reasoning loop, the LLM's tool-choice configuration, and the Toolkit's function registry.
ReActAgent Architecture and Core Components
The implementation spans several key modules within the codebase. The primary logic resides in src/agentscope/agent/_react_agent.py, which extends ReActAgentBase from src/agentscope/agent/_react_agent_base.py. Tool management is centralized in src/agentscope/tool/_toolkit.py, while message structures including ToolUseBlock and ToolResultBlock are defined in src/agentscope/message.py.
At the heart of the agent lie the _reasoning and _acting methods. The _reasoning method interfaces with the LLM to determine whether tool usage is necessary, while _acting handles the actual invocation and result processing. This separation allows the agent to maintain coherent memory while executing arbitrary tool functions.
Controlling Tool Selection with tool_choice
Tool selection begins before the LLM generates a response through the tool_choice parameter. This local variable dictates whether the model must, may, or must not emit tool calls.
Structured Output Enforcement
When the agent is initialized with a structured-output model, the code forces tool_choice to "required" to ensure the model calls a specific finish function that returns structured data. This mechanism appears in src/agentscope/agent/_react_agent.py between lines 405 and 423:
tool_choice: Literal["auto", "none", "required"] | None = None
...
if structured_model:
self.toolkit.register_tool_function(getattr(self, self.finish_function_name))
self.toolkit.set_extended_model(self.finish_function_name, structured_model)
tool_choice = "required"
else:
self.toolkit.remove_tool_function(self.finish_function_name)
Default Auto Mode
Without structured output requirements, tool_choice defaults to None (equivalent to "auto"), allowing the LLM to decide whether to emit tool_use blocks based on the conversation context. The value is passed directly to the model API via self.model(..., tool_choice=tool_choice) within the _reasoning method.
The Reasoning-Acting Loop Execution
Each iteration of the agent's main loop follows a strict sequence to process tool calls efficiently.
Parallel and Sequential Tool Execution
After obtaining a response from _reasoning, the agent extracts all tool_use blocks and dispatches them to _acting. The execution mode depends on self.parallel_tool_calls, which determines whether tools run concurrently or sequentially. This orchestration occurs in src/agentscope/agent/_react_agent.py around lines 428-445:
msg_reasoning = await self._reasoning(tool_choice)
futures = [
self._acting(tool_call)
for tool_call in msg_reasoning.get_content_blocks("tool_use")
]
The _acting Method Implementation
The _acting method constructs a ToolResultBlock to track the execution state, then delegates the actual function call to the Toolkit. It also handles special logic for the finish function, extracting structured metadata when the task completes. The implementation in src/agentscope/agent/_react_agent.py (lines 555-573) appears as follows:
async def _acting(self, tool_call: ToolUseBlock) -> dict | None:
tool_res_msg = Msg(
"system",
[ToolResultBlock(type="tool_result", id=tool_call["id"],
name=tool_call["name"], output=[])],
"system",
)
tool_res = await self.toolkit.call_tool_function(tool_call)
async for chunk in tool_res:
tool_res_msg.content[0]["output"] = chunk.content
await self.print(tool_res_msg, chunk.is_last)
if (tool_call["name"] == self.finish_function_name
and chunk.metadata and chunk.metadata.get("success", False)):
return chunk.metadata.get("structured_output")
Toolkit Responsibilities for Tool Execution
The Toolkit class in src/agentscope/tool/_toolkit.py serves as the execution engine, managing registration, validation, and invocation of tool functions.
Function Registration and Schema Generation
When tools are added via register_tool_function, the Toolkit parses the function's docstring to generate a JSON schema compatible with OpenAI-style function calling. These definitions are stored in self.tools for runtime lookup.
Runtime Execution and Validation
The call_tool_function method (lines 93-111) handles the complete execution pipeline:
- Validation: Verifies the requested tool exists in the registry
- Group Checking: Ensures the tool's assigned group is currently active (non-basic groups require explicit activation)
- Parameter Merging: Combines preset kwargs defined at registration with runtime arguments from the LLM
- Execution: Handles synchronous, asynchronous, or generator-based functions uniformly
- Streaming: Returns an async generator of
ToolResponseobjects to support real-time output
async def call_tool_function(self, tool_call: ToolUseBlock) -> AsyncGenerator[ToolResponse, None]:
if tool_call["name"] not in self.tools:
return _object_wrapper(ToolResponse(...), None)
tool_func = self.tools[tool_call["name"]]
if tool_func.group != "basic" and not self.groups[tool_func.group].active:
return _object_wrapper(ToolResponse(...), None)
kwargs = {**tool_func.preset_kwargs, **(tool_call.get("input", {}) or {})}
...
Dynamic Tool Group Management
AgentScope supports runtime modification of available tools through the reset_equipped_tools meta-tool. This function, defined in src/agentscope/tool/_toolkit.py (lines 27-63), enables agents to activate or deactivate tool groups based on context:
def reset_equipped_tools(self, **kwargs) -> ToolResponse:
self.update_tool_groups(list(self.groups.keys()), active=False)
to_activate = [k for k, v in kwargs.items() if v]
self.update_tool_groups(to_activate, active=True)
When groups are toggled, the system regenerates the tool descriptions injected into the LLM's system prompt, ensuring the model only attempts to call currently available functions.
Complete Implementation Example
The following example demonstrates registering a custom tool, configuring the ReActAgent with structured output support, and executing a query:
from agentscope.agent import ReActAgent
from agentscope.tool import Toolkit
from agentscope.model import OpenAIChatModel # any ChatModelBase implementation
from agentscope.formatter import OpenAIFormatter
# 1️⃣ Create a toolkit with a simple tool
toolkit = Toolkit()
def get_current_time():
"""Return the current UTC time as a string."""
import datetime
return datetime.datetime.utcnow().isoformat()
toolkit.register_tool_function(get_current_time)
# 2️⃣ Build the ReAct agent
agent = ReActAgent(
name="assistant",
sys_prompt="You are a helpful assistant.",
model=OpenAIChatModel(model="gpt-4o-mini"),
formatter=OpenAIFormatter(),
toolkit=toolkit,
max_iters=5,
)
# 3️⃣ Ask a question that requires the tool
reply = await agent.reply(
Msg("user", "What time is it now?"),
)
print(reply.get_text_content()) # → the LLM will call `get_current_time` and embed the result
This workflow illustrates how the agent automatically selects the appropriate tool through the reasoning loop and streams the result back to the conversation.
Summary
- The ReActAgent in AgentScope implements the reasoning-acting pattern through coordinated
_reasoningand_actingmethods insrc/agentscope/agent/_react_agent.py. - Tool selection is controlled via the
tool_choiceparameter, which defaults to "auto" but becomes "required" when structured output models are configured. - Parallel execution is supported through
parallel_tool_calls, allowing multiple tools to run simultaneously when the LLM requests them. - The Toolkit in
src/agentscope/tool/_toolkit.pymanages function registration, group-based activation, and unified execution across sync, async, and generator-based tools. - Dynamic tool management allows runtime modification of available tool groups via
reset_equipped_tools, with automatic updates to the system prompt context.
Frequently Asked Questions
How does ReActAgent decide when to use a tool?
The agent relies on the LLM's judgment when tool_choice is set to None (auto mode). The model analyzes the conversation context and emits tool_use blocks when it determines external data or computation is necessary. For structured output scenarios, the code forces tool_choice to "required", mandating a tool call to ensure valid response formatting.
What happens if a tool belongs to an inactive group?
The Toolkit.call_tool_function method checks the activation status of non-basic tool groups before execution. If a tool's group is inactive, the method returns an error response wrapped in a ToolResponse object without invoking the function, effectively preventing unauthorized tool usage during specific agent states.
Can ReActAgent execute multiple tools simultaneously?
Yes. When self.parallel_tool_calls is enabled, the agent collects all tool_use blocks from a reasoning step and dispatches them concurrently. The results stream back individually through async generators, allowing the agent to process outputs as they complete rather than sequentially.
How are tool results formatted for the LLM?
The _acting method constructs ToolResultBlock objects that encapsulate the tool's output, execution ID, and name. These blocks are wrapped in Msg objects with role "system" and streamed back to the model conversation history, enabling subsequent reasoning steps to reference the tool's output naturally.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →