Chapter 2
15 min read
Section 10 of 175

The Agent Loop: Perceive-Reason-Act

Agent Architecture Fundamentals

Introduction

Every AI agent, from the simplest automation to the most sophisticated multi-agent system, follows the same fundamental pattern: Perceive-Reason-Act. This loop is the heartbeat of agentic systems, repeating until the goal is achieved or the agent decides to stop.

The Universal Pattern: Perceive your environment, reason about what to do, act on your decision, observe the results, and repeat. Master this loop and you understand the foundation of all agent systems.
🐍agent_loop_overview.py
1def agent_loop(goal: str):
2    """The fundamental agent loop pattern."""
3    state = initialize_state(goal)
4
5    while not should_stop(state):
6        # 1. PERCEIVE: Gather context about the current situation
7        context = perceive(state)
8
9        # 2. REASON: Decide what action to take
10        action = reason(context, goal, available_tools)
11
12        # 3. ACT: Execute the chosen action
13        result = act(action)
14
15        # 4. OBSERVE: Update state based on results
16        state = observe(state, result)
17
18    return state.final_result

Perceive: Gathering Context

The perception phase gathers all relevant information the agent needs to make an informed decision. This includes:

  • Goal state: What are we trying to achieve?
  • Current state: Where are we now?
  • History: What have we done so far?
  • Environment: What's around us? (files, data, etc.)
  • Constraints: What limitations exist?
🐍perceive.py
1class PerceptionSystem:
2    """Gathers context for agent decision-making."""
3
4    def perceive(self, state: AgentState) -> Context:
5        """Compile all relevant context for the current decision."""
6
7        return Context(
8            # The goal we're working toward
9            goal=state.goal,
10
11            # Progress made so far
12            progress=state.progress_summary(),
13
14            # Recent actions and their results
15            recent_history=state.get_recent_actions(n=10),
16
17            # Relevant memories from long-term storage
18            relevant_memories=self.memory.recall(state.goal, k=5),
19
20            # Available tools and their descriptions
21            available_tools=self.get_tool_descriptions(),
22
23            # Current environment state (files, data, etc.)
24            environment=self.scan_environment(state),
25
26            # Any constraints or limitations
27            constraints=self.get_constraints(state),
28        )
29
30    def scan_environment(self, state: AgentState) -> dict:
31        """Scan the current working environment."""
32        return {
33            "current_directory": os.getcwd(),
34            "open_files": state.open_files,
35            "modified_files": state.modified_files,
36            "pending_changes": state.pending_changes,
37        }
38
39    def get_tool_descriptions(self) -> list[dict]:
40        """Get descriptions of all available tools."""
41        return [
42            {
43                "name": tool.name,
44                "description": tool.description,
45                "parameters": tool.parameters,
46            }
47            for tool in self.tools
48        ]

Context Window Management

The perception phase must be efficient. You can't include everything in the LLM's context window. Prioritize recency, relevance, and importance. Use summarization for older history.

Reason: Making Decisions

The reasoning phase is where the LLM shines. Given the context, the agent must decide:

  • What tool to use (or whether to use one at all)
  • What parameters to pass to the chosen tool
  • Whether the goal is achieved (and we should stop)
  • Whether to ask for help (escalate to human)
🐍reason.py
1class ReasoningSystem:
2    """Uses LLM to decide the next action."""
3
4    def reason(self, context: Context) -> Action:
5        """Decide the next action based on context."""
6
7        # Build the decision prompt
8        prompt = self.build_decision_prompt(context)
9
10        # Call the LLM with tool definitions
11        response = self.llm.generate_with_tools(
12            prompt=prompt,
13            tools=context.available_tools,
14            system_prompt=self.system_prompt,
15        )
16
17        # Parse the response into an action
18        return self.parse_response(response)
19
20    def build_decision_prompt(self, context: Context) -> str:
21        """Construct the prompt for decision-making."""
22        return f"""
23You are an AI agent working to accomplish a goal.
24
25## Goal
26{context.goal}
27
28## Progress So Far
29{context.progress}
30
31## Recent Actions
32{self.format_history(context.recent_history)}
33
34## Relevant Context
35{self.format_memories(context.relevant_memories)}
36
37## Current Environment
38{self.format_environment(context.environment)}
39
40## Available Actions
41You can use the following tools:
42{self.format_tools(context.available_tools)}
43
44Or you can:
45- finish: Complete the task with a final result
46- ask_human: Request help or clarification
47
48## Instructions
49Based on the context above, decide your next action.
50Think step by step about what would best advance toward the goal.
51"""
52
53    def parse_response(self, response: dict) -> Action:
54        """Parse LLM response into a structured action."""
55        if response.get("tool_calls"):
56            tool_call = response["tool_calls"][0]
57            return Action(
58                type="tool",
59                name=tool_call["name"],
60                params=tool_call["input"],
61            )
62        elif "finish" in response.get("text", "").lower():
63            return Action(type="finish", result=response["text"])
64        else:
65            return Action(type="think", thought=response["text"])

Reasoning Quality is Everything

The quality of your agent depends primarily on how well the LLM reasons. Invest heavily in system prompts, context organization, and clear tool descriptions.

Act: Taking Actions

The action phase executes the chosen action. This is where the agent interacts with the world:

🐍act.py
1class ActionSystem:
2    """Executes agent actions."""
3
4    def act(self, action: Action) -> ActionResult:
5        """Execute the chosen action."""
6
7        match action.type:
8            case "tool":
9                return self.execute_tool(action.name, action.params)
10            case "finish":
11                return ActionResult(
12                    success=True,
13                    type="finish",
14                    output=action.result,
15                )
16            case "ask_human":
17                return self.request_human_input(action.question)
18            case "think":
19                # Internal reasoning, no external action
20                return ActionResult(
21                    success=True,
22                    type="think",
23                    output=action.thought,
24                )
25            case _:
26                return ActionResult(
27                    success=False,
28                    type="error",
29                    error=f"Unknown action type: {action.type}",
30                )
31
32    def execute_tool(self, tool_name: str, params: dict) -> ActionResult:
33        """Execute a specific tool."""
34        tool = self.tools.get(tool_name)
35        if not tool:
36            return ActionResult(
37                success=False,
38                type="error",
39                error=f"Tool not found: {tool_name}",
40            )
41
42        try:
43            result = tool.execute(**params)
44            return ActionResult(
45                success=True,
46                type="tool",
47                tool_name=tool_name,
48                output=result,
49            )
50        except Exception as e:
51            return ActionResult(
52                success=False,
53                type="error",
54                tool_name=tool_name,
55                error=str(e),
56            )

Error Handling in Actions

Actions can fail. Robust agents handle failures gracefully:

🐍action_error_handling.py
1def execute_with_retry(
2    self,
3    action: Action,
4    max_retries: int = 3,
5) -> ActionResult:
6    """Execute an action with retry logic."""
7
8    for attempt in range(max_retries):
9        result = self.act(action)
10
11        if result.success:
12            return result
13
14        # Log the failure
15        self.logger.warning(
16            f"Action failed (attempt {attempt + 1}/{max_retries}): "
17            f"{result.error}"
18        )
19
20        # Decide whether to retry
21        if not self.is_retryable_error(result.error):
22            break
23
24        # Wait before retrying (exponential backoff)
25        time.sleep(2 ** attempt)
26
27    return result  # Return the last (failed) result

Observe: Processing Results

After acting, the agent must observe and process the results to update its state:

🐍observe.py
1class ObservationSystem:
2    """Processes action results and updates state."""
3
4    def observe(
5        self,
6        state: AgentState,
7        action: Action,
8        result: ActionResult,
9    ) -> AgentState:
10        """Update state based on action results."""
11
12        # Record what happened
13        state.add_action_record(
14            action=action,
15            result=result,
16            timestamp=datetime.now(),
17        )
18
19        # Update progress assessment
20        state.progress = self.assess_progress(state, result)
21
22        # Check for goal completion
23        if result.type == "finish":
24            state.completed = True
25            state.final_result = result.output
26
27        # Check for errors that require replanning
28        if not result.success:
29            state.errors.append(result.error)
30            state.needs_replanning = self.should_replan(state, result)
31
32        # Store in memory if significant
33        if self.is_significant(result):
34            self.memory.add(
35                content=f"Action: {action.name}, Result: {result.output}",
36                metadata={"type": "action_result"},
37            )
38
39        return state
40
41    def assess_progress(
42        self,
43        state: AgentState,
44        result: ActionResult,
45    ) -> float:
46        """Assess how much progress was made (0.0 to 1.0)."""
47        # This can be simple heuristics or LLM-based assessment
48        if result.type == "finish":
49            return 1.0
50        elif result.success:
51            return min(state.progress + 0.1, 0.9)  # Incremental progress
52        else:
53            return state.progress  # No progress on failure
54
55    def should_replan(
56        self,
57        state: AgentState,
58        result: ActionResult,
59    ) -> bool:
60        """Determine if we need to create a new plan."""
61        # Replan if multiple consecutive failures
62        if state.consecutive_failures >= 3:
63            return True
64        # Replan if a critical assumption proved wrong
65        if "not found" in result.error.lower():
66            return True
67        return False

The Complete Loop

Here's how all the pieces fit together:

🐍complete_agent_loop.py
1class Agent:
2    """Complete agent implementation with Perceive-Reason-Act loop."""
3
4    def __init__(
5        self,
6        llm: LLM,
7        tools: list[Tool],
8        memory: Memory,
9        max_iterations: int = 50,
10    ):
11        self.perception = PerceptionSystem(memory, tools)
12        self.reasoning = ReasoningSystem(llm)
13        self.action = ActionSystem(tools)
14        self.observation = ObservationSystem(memory)
15        self.max_iterations = max_iterations
16
17    def run(self, goal: str) -> AgentResult:
18        """Execute the agent loop to accomplish a goal."""
19
20        # Initialize state
21        state = AgentState(goal=goal)
22
23        for iteration in range(self.max_iterations):
24            # 1. PERCEIVE: Gather context
25            context = self.perception.perceive(state)
26
27            # 2. REASON: Decide action
28            action = self.reasoning.reason(context)
29
30            # Log for observability
31            self.log_decision(iteration, action)
32
33            # 3. ACT: Execute action
34            result = self.action.act(action)
35
36            # 4. OBSERVE: Update state
37            state = self.observation.observe(state, action, result)
38
39            # Check termination conditions
40            if state.completed:
41                return AgentResult(
42                    success=True,
43                    output=state.final_result,
44                    iterations=iteration + 1,
45                )
46
47            if state.needs_replanning:
48                self.replan(state)
49                state.needs_replanning = False
50
51        # Max iterations reached
52        return AgentResult(
53            success=False,
54            output="Max iterations reached",
55            iterations=self.max_iterations,
56        )

Visualizing the Loop

📝loop_diagram.txt
1┌─────────────────────────────────────────────────────┐
2│                    AGENT LOOP                        │
3├─────────────────────────────────────────────────────┤
4│                                                      │
5│    ┌──────────┐                                      │
6│    │   GOAL   │                                      │
7│    └────┬─────┘                                      │
8│         ▼                                            │
9│    ┌──────────┐     ┌──────────┐                     │
10│    │ PERCEIVE │ ──▶ │  REASON  │                     │
11│    └──────────┘     └────┬─────┘                     │
12│         ▲                │                           │
13│         │                ▼                           │
14│    ┌──────────┐     ┌──────────┐                     │
15│    │ OBSERVE  │ ◀── │   ACT    │                     │
16│    └──────────┘     └──────────┘                     │
17│         │                                            │
18│         ▼                                            │
19│    ┌──────────────┐                                  │
20│    │ Goal Complete?│                                 │
21│    └──────┬───────┘                                  │
22│      No   │   Yes                                    │
23│      ▲    │    ▼                                     │
24│      │    │  ┌──────────┐                            │
25│      └────┘  │  FINISH  │                            │
26│              └──────────┘                            │
27└─────────────────────────────────────────────────────┘

Summary

The Perceive-Reason-Act loop is the foundation of all agent systems:

  1. Perceive: Gather context (goal, state, history, environment)
  2. Reason: Use LLM to decide the next action
  3. Act: Execute the chosen action (tool call, finish, etc.)
  4. Observe: Process results and update state
  5. Loop: Repeat until goal is achieved or max iterations
The Power of the Loop: This simple pattern enables remarkably complex behavior. In the next section, we'll explore each core component in more detail.