ReAct: Reasoning with Tools
A model can only answer from what it already knows. ReAct (Reason + Act, Yao et al., 2022) lets it ask for more: on each turn the model writes a short thought, then either calls a tool or gives the answer. The agent runs the tool and feeds the result back as an observation on the next turn.
In ASTRA, tools are ordinary plans. The agent decides which tools exist and what they do, so a tool can query beliefs, call a Java module, perform an action in an environment, or ask another agent.
Declaring tools
A ReAct agent declares its tools with beliefs and implements them with plans:
Element |
Purpose |
|---|---|
|
Advertises a tool to the model. The description tells the model what the tool does and what input it expects |
|
Runs a tool. Bind |
|
Limits the number of turns. The default is |
Tool inputs and observations are strings. A tool that needs structured input must parse it, and a tool that returns structured data must describe it in text.
Example: answering from beliefs
This agent from the reasoning-agents project has two tools that look up beliefs. Neither tool can answer the question on its own: the model must find the capital, then look up its population.
import astra.langchain4j.reasoning.*;
agent Tools extends ReAct {
module System S;
types geography {
formula capital(string, string);
formula population(string, string);
}
initial tool("capital", "Returns the capital city of the country given as input."),
tool("population", "Returns the population of the city given as input."),
capital("Ireland", "Dublin"), capital("France", "Paris"),
population("Dublin", "about 1.5 million"), population("Paris", "about 2.1 million");
rule +!main(list args) {
try {
model.loadApiKeyFromEnv("OPENAI_API_KEY");
model.initialize("gpt-4o-mini",
"astra.langchain4j.configuration.OpenAiConfiguration", []);
!solve("What is the population of the capital of Ireland?", string answer);
console.println("Answer: " + answer);
} recover {
console.println("The question could not be answered.");
}
S.exit();
}
rule +!invoke("capital", string country, string observation)
: capital(country, string city) {
observation = city;
}
rule +!invoke("population", string city, string observation)
: population(city, string size) {
observation = size;
}
rule +!invoke(string name, string input, string observation) {
observation = "No information about " + input;
}
}
Run it with mvn -Dastra.main=Tools. The agent prints each turn: the thought, the tool call in the format the model was asked to use, and the observation:
[main] ----- step 1 -----
[main] I need to find the capital of Ireland first.
<action>capital</action><input>Ireland</input>
Observation: Dublin
[main] ----- step 2 -----
[main] Now I need the population of Dublin.
<action>population</action><input>Dublin</input>
Observation: about 1.5 million
[main] ----- step 3 -----
[main] I now know the answer.
[main] Answer: about 1.5 million
The thoughts and the final wording vary between runs.
The two +!invoke plans use context conditions to answer from beliefs. The last plan applies when neither context holds, for example when the model asks for a country the agent knows nothing about. It returns an observation rather than failing, so the model can try something else.
What happens on each turn
!solve(question, answer) builds a list of the tools from the tool beliefs, then runs turns until the model answers or maxSteps is reached:
The prompt contains the question, the tools and the history of earlier turns.
If the reply contains
<answer>...</answer>, that text is the answer and the goal completes.Otherwise the reply is parsed for
<action>TOOL</action><input>INPUT</input>. Anything after the first</input>, such as an observation the model invented, is discarded.If the tool is declared,
!invoke(name, input, observation)is adopted. If it is not declared, the observation is “There is no tool called …”.The turn and its observation are added to the history, and the next turn begins.
The agent keeps the model on track and handles failures without ending the goal:
If an
!invokegoal fails, the observation reports that the tool failed on that input.If the reply follows neither format, the observation repeats the format instructions, and the malformed reply is left out of the history.
If
maxStepsturns pass without an answer, the answer is “No answer found within N steps”. Check for this before using the answer.
To allow more turns, replace the belief before adopting !solve(...):
-maxSteps(8);
+maxSteps(12);
Each turn is a model call whose prompt includes the whole history, so longer runs cost more per turn as well as in total.
Writing good tools
Describe the input. The model sees only the name and description. “Returns the capital city of the country given as input” tells it to send a country name.
Keep observations short and factual. They are copied into every later prompt.
Fail softly. Return an observation such as “No information about Atlantis” rather than failing. The model can recover from an informative observation.
Guard actions. A tool that changes the world, such as one that books, sends or moves something, should check the request against the agent’s beliefs before acting, as any other plan would.
Because a tool is a plan, it can use any ASTRA facility: query beliefs, adopt subgoals, call modules, send messages, or call a remote agent through A2A.
Where next?
Thread and Tree of Thoughts cover long contexts and problems that need search.