Autonomous Agents: ReAct, Memory & Tool-Calling Loops
Formulate autonomous LLM decision loops, the ReAct reasoning-action paradigm, episodic and semantic memory architectures, tool schema validation, and self-healing agent execution in Python and Rust.
Standard Large Language Models generate text auto-regressively in a single open-loop pass. While proficient at semantic translation and synthesis, static feedforward generation cannot query live external data, execute deterministic calculations, or dynamically correct mistakes during multi-step problem solving.
Autonomous Agents frame the LLM as a cognitive decision-making core embedded within a closed-loop environment. By interleaving verbal reasoning with external tool execution, agents plan trajectories, observe empirical environmental feedback, and iteratively solve complex multi-step objectives.
Synergistic interleaving of verbal reasoning traces with deterministic tool executions solves hallucinations and ground-truth validation.
1. Cognitive Architecture of an Autonomous Agent#
An autonomous agent system consists of four primary subsystems:
- The Reasoning Core (Brain): The foundation model responsible for semantic parsing, chain-of-thought planning, error reflection, and policy action selection.
- Memory Architectures:
- Short-Term (Working) Memory: The active context window containing recent conversation history, current system instructions, and immediate scratchpad thoughts.
- Long-Term Memory: External retrieval systems (such as vector embeddings or document indices) storing semantic world knowledge and episodic records of past trajectories.
- Procedural Memory: Fixed instructions, behavioral rules, few-shot demonstration exemplars, and strict JSON schemas governing tool capabilities.
- Planning & Decomposition: Breaking down high-level ambiguous goals ("Analyze Q3 churn and summarize anomalies") into discrete, executable atomic sub-tasks.
- Tool Execution Layer: Sandboxed interfaces enabling interaction with external systems, REST APIs, databases, and code interpreters.
2. The ReAct (Reasoning + Acting) Paradigm#
Introduced by Shunyu Yao et al. (2022), ReAct resolves the critical limitations of both pure reasoning (Chain-of-Thought) and pure acting:
- Pure Chain-of-Thought suffers from hallucination because it cannot verify intermediate factual assertions against ground truth.
- Pure Acting lacks strategic foresight, blindly executing commands without synthesizing context or recovering from unexpected failures.
The Formal ReAct Loop#
At discrete time step , the agent receives the complete historical trajectory:
The step executes through three explicit phases:
- Thought Generation (): The agent reasons verbally about its progress, notes missing information, and formulates an intent:
- Action Selection (): The agent chooses an action name and structured arguments:
- Environmental Observation (): The external runtime executes the tool and captures standard output, return data, or exceptions:
The observation is concatenated into . The loop terminates when the agent emits a special terminal action:
3. Reflexion: Verbal Reinforcement Learning#
Traditional reinforcement learning optimizes policy weights via scalar numeric rewards and backpropagation. In contrast, Reflexion (Shinn et al., 2023) implements verbal reinforcement learning:
When an agent fails a task (e.g. unit tests fail or a tool returns an unrecoverable error), an internal Evaluator triggers a self-reflection prompt:
The critique is appended to an episodic memory buffer:
On the subsequent attempt , the agent receives inside its prompt context, allowing it to bypass previous dead ends without altering a single neural network weight.
4. Tool Schemas and Self-Healing Execution Loops#
In production environments, tool calling requires strict interface contracts. Modern LLMs use JSON Schema declarations to define valid function signatures:
1234567891011{ "name": "lookup_customer", "description": "Fetch customer profile by unique ID", "parameters": { "type": "object", "properties": { "customer_id": { "type": "integer", "description": "The customer identifier" } }, "required": ["customer_id"] } }
Self-Healing Execution#
LLMs frequently generate invalid arguments, malformed JSON, or reference non-existent files. Rather than crashing the agent, robust runtimes intercept the error and route it back as an observation:
The LLM inspects the error in and automatically issues a corrected action .
5. From-Scratch Implementation: ReAct Agent Runtime#
Let us construct a complete, zero-dependency ReAct Agent runtime from first principles in both Python and Rust.
The agent features:
- A registry of mock tools:
calculate(expr)andget_weather(city). - A deterministic rule-based mock LLM brain simulating Chain-of-Thought parsing.
- A full Thought -> Action -> Observation loop with step limits to prevent infinite cycles.
Python Implementation (Pure Standard Library)#
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990""" Autonomous ReAct Agent Runtime from scratch in pure Python standard library. Implements Thought-Action-Observation loops, tool dispatch, and step guards. """ import re from typing import Dict, Any, Callable, Tuple # 1. Tool Registry class ToolRegistry: def __init__(self): self.tools: Dict[str, Callable[[str], str]] = {} def register(self, name: str, func: Callable[[str], str]): self.tools[name] = func def execute(self, name: str, arg: str) -> str: if name not in self.tools: return f"Error: Tool '{name}' does not exist. Available tools: {list(self.tools.keys())}" try: return self.tools[name](arg) except Exception as e: return f"ExecutionError: {type(e).__name__}: {str(e)}" # Mock tool functions def tool_calculate(expression: str) -> str: # Safe evaluation of basic arithmetic sanitized = re.sub(r"[^0-9+\-*/(). ]", "", expression) return str(eval(sanitized)) def tool_get_weather(city: str) -> str: db = {"tokyo": "22C, Sunny", "paris": "15C, Rainy", "new york": "18C, Clear"} clean_city = city.strip().lower() return db.get(clean_city, f"City '{city}' not found in weather records.") # 2. Mock Agent Brain simulating ReAct generation class MockReActLLM: """Simulates LLM response based on prompt state to demonstrate mechanics.""" def generate_step(self, trajectory: str) -> str: if "Observation: 22C, Sunny" in trajectory: return "Thought: I have retrieved the weather for Tokyo.\nAction: finish[Tokyo is currently 22C and Sunny.]" elif "Observation:" in trajectory and "Action: weather[" in trajectory: return "Thought: I now have the weather data.\nAction: finish[Weather query complete.]" elif "Action: weather[" not in trajectory: return "Thought: To answer the question about Tokyo's weather, I need to check the weather tool.\nAction: weather[Tokyo]" return "Thought: I cannot proceed further.\nAction: finish[No solution found.]" # 3. Agent Execution Engine class ReActAgent: def __init__(self, registry: ToolRegistry, max_steps: int = 5): self.registry = registry self.max_steps = max_steps self.llm = MockReActLLM() def run(self, query: str) -> str: trajectory = f"Question: {query}\n" print(f"--- Starting ReAct Agent Run ---\nTask: {query}\n") for step in range(1, self.max_steps + 1): response = self.llm.generate_step(trajectory) trajectory += response + "\n" print(f"[Step {step}]\n{response}") # Parse action pattern: Action: tool_name[argument] match = re.search(r"Action:\s*([a-zA-Z0-9_]+)\[(.*?)\]", response) if not match: print("Error: No valid action found. Stopping.") break tool_name, tool_arg = match.group(1), match.group(2) # Check termination if tool_name.lower() == "finish": print(f"\nFinal Answer: {tool_arg}") return tool_arg # Execute tool obs = self.registry.execute(tool_name, tool_arg) obs_str = f"Observation: {obs}" print(f"{obs_str}\n") trajectory += obs_str + "\n" return "Agent stopped: Reached maximum step limit without terminal answer." if __name__ == "__main__": tools = ToolRegistry() tools.register("calculate", tool_calculate) tools.register("weather", tool_get_weather) agent = ReActAgent(tools, max_steps=4) result = agent.run("What is the current weather condition in Tokyo?")
Rust Implementation (Pure Standard Library)#
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111// Autonomous ReAct Agent Loop from scratch in pure Rust standard library. // Demonstrates Thought-Action-Observation orchestration and tool invocation. use std::collections::HashMap; struct ToolRegistry { tools: HashMap<String, Box<dyn Fn(&str) -> String>>, } impl ToolRegistry { fn new() -> Self { Self { tools: HashMap::new(), } } fn register<F>(&mut self, name: &str, func: F) where F: Fn(&str) -> String + 'static, { self.tools.insert(name.to_string(), Box::new(func)); } fn execute(&self, name: &str, arg: &str) -> String { match self.tools.get(name) { Some(func) => func(arg), None => format!("Error: Tool '{}' not registered.", name), } } } // Mock LLM step engine struct MockLLM; impl MockLLM { fn generate_step(&self, history: &str) -> String { if history.contains("Observation: 22C, Sunny") { "Thought: I have the verified weather for Tokyo.\nAction: finish[Tokyo is 22C and Sunny.]".to_string() } else if !history.contains("Action: weather[") { "Thought: I must inspect current weather records for Tokyo.\nAction: weather[Tokyo]".to_string() } else { "Thought: Unrecognized state.\nAction: finish[Unable to resolve query.]".to_string() } } } // Action parser struct ActionCommand { tool: String, argument: String, } fn parse_action(text: &str) -> Option<ActionCommand> { let action_marker = "Action:"; if let Some(pos) = text.find(action_marker) { let remainder = &text[pos + action_marker.len()..].trim(); if let Some(bracket_open) = remainder.find('[') { if let Some(bracket_close) = remainder.find(']') { let tool = remainder[..bracket_open].trim().to_string(); let argument = remainder[bracket_open + 1..bracket_close].trim().to_string(); return Some(ActionCommand { tool, argument }); } } } None } fn main() { let mut registry = ToolRegistry::new(); // Register tools registry.register("weather", |city| { if city.to_lowercase() == "tokyo" { "22C, Sunny".to_string() } else { "City not found".to_string() } }); registry.register("calculate", |expr| { format!("Calculated: {}", expr) }); let llm = MockLLM; let mut history = String::from("Question: What is the weather in Tokyo?\n"); let max_steps = 4; println!("--- ReAct Agent Runtime (Rust) ---"); for step in 1..=max_steps { let step_output = llm.generate_step(&history); history.push_str(&step_output); history.push('\n'); println!("[Step {}]\n{}", step, step_output); if let Some(cmd) = parse_action(&step_output) { if cmd.tool == "finish" { println!("\nTerminated: Final Answer -> {}", cmd.argument); return; } let observation = registry.execute(&cmd.tool, &cmd.argument); let obs_line = format!("Observation: {}\n", observation); println!("{}", obs_line.trim()); history.push_str(&obs_line); } else { println!("No actionable command found. Aborting."); break; } } }
6. Multi-Agent Topologies#
When tasks grow beyond individual context capacities, multi-agent coordination architectures divide cognitive labor:
| Topology | Mechanics | Primary Benefit | Failure Risk |
|---|---|---|---|
| Hierarchical (Manager-Worker) | Manager delegates sub-goals to specialist workers and aggregates results | Strict task decomposition and bounded worker contexts | Single point of failure at manager level |
| Peer Debate (Democratic) | Multiple agents argue differing hypotheses until consensus is reached | Mitigates individual hallucination and bias | High token cost and potential circular deadlocks |
| Swarm / Event-Driven | Autonomous agents publish events to a shared message bus asynchronously | Highly resilient, modular, and parallel | Non-deterministic race conditions and trace opacity |
7. Knowledge Check#
Question
question: "Why is the ReAct loop superior to unconstrained Chain-of-Thought for factual queries?"
options:
- "(Recommended) Because ReAct grounds reasoning traces with empirical observations from external tools, eliminating unverified hallucinations"
- "Because ReAct avoids using an LLM for token generation"
- "Because ReAct guarantees zero latency during inference"
- "Because ReAct permanently updates the foundation model weights after each step"
explanation: "Chain-of-Thought generates internal reasoning traces but cannot verify factual assertions against reality. ReAct executes tools after thoughts, injecting ground-truth environmental observations back into the context."
:::
Question
question: "How does Reflexion differ from traditional Policy Gradient reinforcement learning?"
options:
- "(Recommended) Reflexion optimizes policies through verbal critiques stored in working/episodic memory without computing mathematical gradients or modifying weights"
- "Reflexion requires a quadratic programming solver"
- "Reflexion only works with convolutional neural networks"
- "Reflexion eliminates the need for short-term memory"
explanation: "Reflexion is a form of verbal reinforcement learning. It diagnoses failures through natural language critiques and stores them in episodic memory buffers, improving subsequent iterations purely through in-context learning without gradient descent."
:::