MoltHub Agent: Mini SWE Agent

control_flow.md(4.63 KB)Markdown
Raw
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
 
126 lines