| 1 | # Agent control flow
|
| 2 |
|
| 3 | !!! note "Understanding AI agent basics"
|
| 4 |
|
| 5 | We also recently created a long tutorial on understanding the basics of building an AI agent: [View it here](https://minimal-agent.com).
|
| 6 |
|
| 7 | !!! abstract "Understanding the default agent"
|
| 8 |
|
| 9 | * This guide shows the control flow of the default agent.
|
| 10 | * After this, you're ready to [remix & extend mini](cookbook.md)
|
| 11 |
|
| 12 | The following diagram shows the control flow of the mini agent:
|
| 13 |
|
| 14 | ```mermaid
|
| 15 | flowchart TD
|
| 16 | subgraph run["<b><code>Agent.run(task)</code></b>"]
|
| 17 | direction TB
|
| 18 | A["<b><code>Initialize messages</code></b>"] --> B
|
| 19 | B["<b><code>Agent.step</code></b>"] --> C{"<b><code>Exception?</code></b>"}
|
| 20 | C -->|Yes| D["<b><code>Agent.add_messages</code></b><br/>(also re-raises exceptions that don't inherit from InterruptAgentFlow)"]
|
| 21 | C -->|No| E{"<b><code>messages[-1].role == exit?</code></b>"}
|
| 22 | D --> E
|
| 23 | E -->|No| B
|
| 24 | E -->|Yes| F["<b><code>Return result</code></b>"]
|
| 25 | end
|
| 26 |
|
| 27 | subgraph step["<b><code>Agent.step()</code></b><br>Single iteration</br>"]
|
| 28 | direction TB
|
| 29 | S1["<b><code>Agent.query</code></b>"] --> S2["<b><code>Agent.execute_actions</code></b>"]
|
| 30 | end
|
| 31 |
|
| 32 | subgraph query["<b><code>Agent.query()</code></b><br>Also checks for cost limits</br><br></br>"]
|
| 33 | direction TB
|
| 34 | Q3["<b><code>Model.query</code></b>"] --> Q4["<b><code>Agent.add_messages</code></b>"]
|
| 35 | end
|
| 36 |
|
| 37 | subgraph execute_actions["<b><code>Agent.execute_actions(message)</code></b>"]
|
| 38 | direction TB
|
| 39 | E2["<b><code>Environment.execute</code></b><br/>Also raises the Submitted exception if we're done"] --> E3["<b><code>Model.format_observation_messages</code></b>"]
|
| 40 | E3 --> E4["<b><code>Agent.add_messages</code></b>"]
|
| 41 | end
|
| 42 |
|
| 43 | B -.-> step
|
| 44 | S1 -.-> query
|
| 45 | S2 -.-> execute_actions
|
| 46 | ```
|
| 47 |
|
| 48 | And here is the code that implements it:
|
| 49 |
|
| 50 | ??? note "Default agent class"
|
| 51 |
|
| 52 | - [Read on GitHub](https://github.com/swe-agent/mini-swe-agent/blob/main/src/minisweagent/agents/default.py)
|
| 53 | - [API reference](../reference/agents/default.md)
|
| 54 |
|
| 55 | ```python
|
| 56 | --8<-- "src/minisweagent/agents/default.py"
|
| 57 | ```
|
| 58 |
|
| 59 | Essentially, `DefaultAgent.run` calls `DefaultAgent.step` in a loop until the agent has finished its task.
|
| 60 |
|
| 61 | The `step` method is the core of the agent:
|
| 62 |
|
| 63 | ```python
|
| 64 | def step(self) -> list[dict]:
|
| 65 | return self.execute_actions(self.query())
|
| 66 | ```
|
| 67 |
|
| 68 | It does the following:
|
| 69 |
|
| 70 | 1. Queries the model for a response based on the current messages (`DefaultAgent.query`, calling `Model.query`)
|
| 71 | 2. Executes all actions in the response (`DefaultAgent.execute_actions`, calling `Environment.execute` for each action)
|
| 72 | 3. Formats the observation messages via `Model.format_observation_messages`
|
| 73 | 4. Adds the observations to the messages
|
| 74 |
|
| 75 | Here's `query`:
|
| 76 |
|
| 77 | ```python
|
| 78 | def query(self) -> dict:
|
| 79 | # ... limit checks ...
|
| 80 | message = self.model.query(self.messages)
|
| 81 | self.add_messages(message)
|
| 82 | return message
|
| 83 | ```
|
| 84 |
|
| 85 | And `execute_actions`:
|
| 86 |
|
| 87 | ```python
|
| 88 | def execute_actions(self, message: dict) -> list[dict]:
|
| 89 | outputs = [self.env.execute(action) for action in message.get...
|
| 90 | return self.add_messages(*self.model.format_observation_messages(...))
|
| 91 | ```
|
| 92 |
|
| 93 | The interesting bit is how we handle error conditions and the finish condition:
|
| 94 | This uses exceptions that inherit from `InterruptAgentFlow`. All these exceptions carry messages that get added to the trajectory.
|
| 95 |
|
| 96 | - `Submitted` is raised when the agent has finished its task. For example, the environment checks if the command output starts with a magic string:
|
| 97 |
|
| 98 | ```python
|
| 99 | # In Environment.execute
|
| 100 | def _check_finished(self, output: dict):
|
| 101 | lines = output.get("output", "").lstrip().splitlines(keepends=True)
|
| 102 | if lines and lines[0].strip() == "COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT":
|
| 103 | raise Submitted({"role": "exit", "content": ..., "extra": {...}})
|
| 104 | ```
|
| 105 |
|
| 106 | - `LimitsExceeded` is raised when we hit a cost or step limit
|
| 107 | - `FormatError` is raised when the output from the LM is not in the expected format
|
| 108 | - `TimeoutError` is raised when the action took too long to execute
|
| 109 | - `UserInterruption` is raised when the user interrupts the agent
|
| 110 |
|
| 111 | The `DefaultAgent.run` method catches these exceptions and handles them by adding the corresponding messages to the messages list. The loop continues until a message with `role="exit"` is added.
|
| 112 |
|
| 113 | ```python
|
| 114 | while True:
|
| 115 | try:
|
| 116 | self.step()
|
| 117 | except InterruptAgentFlow as e:
|
| 118 | self.add_messages(*e.messages)
|
| 119 | if self.messages[-1].get("role") == "exit":
|
| 120 | break
|
| 121 | ```
|
| 122 |
|
| 123 | Using exceptions for the control flow is a lot easier than passing around flags and states, especially when extending or subclassing the agent.
|
| 124 |
|
| 125 | {% include-markdown "_footer.md" %}
|
| 126 |
|