mini-swe-agent 1.2.0__py3-none-any.whl → 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mini-swe-agent
3
- Version: 1.2.0
3
+ Version: 1.4.0
4
4
  Summary: Nano SWE Agent - A simple AI software engineering agent
5
5
  Author-email: Kilian Lieret <kilian.lieret@posteo.de>, "Carlos E. Jimenez" <carlosej@princeton.edu>
6
6
  License: MIT License
@@ -1,28 +1,28 @@
1
- mini_swe_agent-1.2.0.dist-info/licenses/LICENSE.md,sha256=D3luWPkdHAe7LBsdD4vzqDAXw6Xewb3G-uczss0uh1s,1094
2
- minisweagent/__init__.py,sha256=O834rP05yuUp1YEVIdVhl0wEGk2r43BSKUTGg7iACJU,1787
1
+ mini_swe_agent-1.4.0.dist-info/licenses/LICENSE.md,sha256=D3luWPkdHAe7LBsdD4vzqDAXw6Xewb3G-uczss0uh1s,1094
2
+ minisweagent/__init__.py,sha256=3w6Aygz0dJetlfY8zRfpQ4rZecN_3XvpkNLJubSeGjA,1787
3
3
  minisweagent/__main__.py,sha256=FIyAOiw--c3FQ2g240FOM1FdL0lk_PxSpixu0pQ7WFo,194
4
4
  minisweagent/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  minisweagent/agents/__init__.py,sha256=cpjJLzg1IGxLM-tZpoMJV9S33ye13XtdBO0x7DU_Lrk,48
6
6
  minisweagent/agents/default.py,sha256=6TZUfKch6e_7m05BXmjqUjV-k068s6ldg7T6zJBbQW8,5438
7
7
  minisweagent/agents/interactive.py,sha256=_DCBabdwdIR4gAojT_TaQW2MSFtmBq997mwmiDGYdRA,7327
8
- minisweagent/agents/interactive_textual.py,sha256=zzNsq1OkEmrBFVK3t1dxnrE7W7xU7Vc-WN47bxZcDIk,12657
8
+ minisweagent/agents/interactive_textual.py,sha256=-GrhQT3nh_bn4qyWFRMf6F7xEqMsRnQ-Ry5n3JJBjsc,16077
9
9
  minisweagent/config/README.md,sha256=tPruhnQDhZ8ugc1FNPKk9tVMRltmmIjdYgvHCmN-3Hs,354
10
10
  minisweagent/config/__init__.py,sha256=UfORdQID1Ek_dduZlybUsIKJjihImkSqNU5tIjpw0hk,694
11
11
  minisweagent/config/default.yaml,sha256=AGhcIq6X6n5Fs71ufO3B6CtZ4PS877tCxkPkrWR5Ylg,4497
12
12
  minisweagent/config/github_issue.yaml,sha256=evvu3AJ52tXYSdami9_B8zfazOAE2r2XXkzVmScBoKc,4539
13
- minisweagent/config/mini.tcss,sha256=ThSOtS6JpXxqEYGX69TLy6gPZzuijngsNLI6SjnEJLY,1821
14
- minisweagent/config/mini.yaml,sha256=U-mTAgnrT2mn9_VxKSnLlq26Einxq0grqiY3esTtmGg,4983
13
+ minisweagent/config/mini.tcss,sha256=VgdZZqWElA5_nn4DJUFMpz8C7Gmi5s5XOtm7pfyM83Q,1122
14
+ minisweagent/config/mini.yaml,sha256=WluQAx4AII9MFk3xDSzsJTosNJfgZti02niCYZWYq_A,5346
15
15
  minisweagent/config/extra/__init__.py,sha256=e1MoAlDn_wc9HnXNoncf1P-B4DQ-iRf6n7Q_txjZGRI,52
16
16
  minisweagent/config/extra/swebench.yaml,sha256=LNpTahpul6HL0HozgAAz-C6kpX3wZA7Tg8uE-ZmgrF4,7577
17
17
  minisweagent/environments/__init__.py,sha256=g5mKac1YgVOZVKvmiAiuyPSevRYpI69V4vYrbCH3gsI,54
18
- minisweagent/environments/docker.py,sha256=woTrx-e8h_AjW60Tc-fC6m_ZQt1KCk91viLdOXm7k6g,3751
18
+ minisweagent/environments/docker.py,sha256=VYk7i0T0IgUF_s-N-DqYkHsBWbfgaIMpJZIIdEtetTw,3871
19
19
  minisweagent/environments/local.py,sha256=-2EV3RqZSB8WEjJE7BHLhRjocPMLpoJ3HbM8QB1WXUU,1060
20
20
  minisweagent/environments/singularity.py,sha256=j7ptRVF8GwDLd-5IjhT5j7fNxEJz9amuLTmVxotaMlI,1796
21
21
  minisweagent/environments/extra/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  minisweagent/environments/extra/swerex_docker.py,sha256=MOhhFdX1sAk_U0g-GOxohfjrldzO4YfrUnHq8qJff7c,1502
23
23
  minisweagent/models/__init__.py,sha256=J4bnvfMByTVG0cL_6p51sm8gdargXhARfbG5c0UZ8Z4,2890
24
24
  minisweagent/models/anthropic.py,sha256=D8nHvvbgzPjla0He8p0O9kaXASPWg1Sai0pHsAj_Yn8,855
25
- minisweagent/models/litellm_model.py,sha256=5Nzln0Iq85Syluz-wGRbv5J_E6T7Mym_0M28tS8lteU,2039
25
+ minisweagent/models/litellm_model.py,sha256=jMWsFYPPoXgBaN6Ypa5MR6A9hOpvacnYEXZkzuAkgNc,2283
26
26
  minisweagent/models/test_models.py,sha256=oB3jmZUire5TkVT8ebUCD3jLuLhPIbcTiTqdIix85Yw,1174
27
27
  minisweagent/models/utils/cache_control.py,sha256=mG9cE56HQaUwXfoqvXoH6LcbMV_G1vlEE1aBBpikXYg,1608
28
28
  minisweagent/models/utils/key_per_thread.py,sha256=Vlxt--rapNNYCgIHrMCu1WVAkuiVIhC_awbarkbnkZQ,644
@@ -30,17 +30,17 @@ minisweagent/run/__init__.py,sha256=WIoYgHVl7iZF2YncrfV3IttupG6P5KogroKHKECka3A,
30
30
  minisweagent/run/github_issue.py,sha256=GWOkGM09jOYV93p6xIM_kKWmC1yP_d5lprafWlqoBN0,2748
31
31
  minisweagent/run/hello_world.py,sha256=erLnEwNmPFLxq3-8zyv66Vy1kIqMqQf97vISX7LrQXg,959
32
32
  minisweagent/run/inspector.py,sha256=QnY3oYzm-yq3w9Jzs112Lco2Rg84vSocAWrQRVz_1lc,7127
33
- minisweagent/run/mini.py,sha256=9EzTUT1cra6sHkqUTGu5oqx9Esgt_XOSGW9cxFyXLd8,4339
33
+ minisweagent/run/mini.py,sha256=Q-B5LDFQtEoqxMC3cHpbQr8IW1qNpiTh_eaQpYHz954,4589
34
34
  minisweagent/run/mini_extra.py,sha256=ecA1PnTWElpO60G9RktvVLtUOf3bZ_ESmnSttS6izhQ,1465
35
35
  minisweagent/run/extra/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
36
  minisweagent/run/extra/config.py,sha256=paMHfplhKsqNmzhCmozxhXWHvBzBCUlwUWD8N7ytCPc,3277
37
- minisweagent/run/extra/swebench.py,sha256=x50nFj4tdXkdoad6TkK7tP8CSgf-WkrY0IdSMe8_oX0,9564
37
+ minisweagent/run/extra/swebench.py,sha256=m5_PZI4ojkUyCxzkkMtel_vlnYmjziWrXu73yHoZGFs,9688
38
38
  minisweagent/run/extra/swebench_single.py,sha256=L3Kk4G65o3MCPLMEwGNIs77-AFf6Lfc8o1oxrbN-ZWM,1991
39
39
  minisweagent/run/extra/utils/batch_progress.py,sha256=u__khJ-fipZLxTJu43LamGAtPUCqEZYEi8J7SfH7X6A,6211
40
40
  minisweagent/run/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
- minisweagent/run/utils/save.py,sha256=3_kuutw-uAGIhEoDawA3_FPeSz1vWuCWpJl80j5u7_s,893
42
- mini_swe_agent-1.2.0.dist-info/METADATA,sha256=4JI2AGHnBGJhAtkSFhmTBwH7-2QO0ymepR2g8TQGaAA,13459
43
- mini_swe_agent-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
44
- mini_swe_agent-1.2.0.dist-info/entry_points.txt,sha256=d1_yRbTaGjs1UXHa6JQK0sKDGBIVGm8oeW0k2kfbJgQ,182
45
- mini_swe_agent-1.2.0.dist-info/top_level.txt,sha256=zKF4t8bFpV87fdVABZt2Da-vnb4Vkh_CxkwQx5YT4Ew,13
46
- mini_swe_agent-1.2.0.dist-info/RECORD,,
41
+ minisweagent/run/utils/save.py,sha256=q7omf7zYHg73k8-Iyp9w5YVSYvDAacRrh4X9L_4VhNM,942
42
+ mini_swe_agent-1.4.0.dist-info/METADATA,sha256=tM-cskbK7S2xIRUegGm39qXgPT3tOfpfZOlOZfk-lcg,13459
43
+ mini_swe_agent-1.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
44
+ mini_swe_agent-1.4.0.dist-info/entry_points.txt,sha256=d1_yRbTaGjs1UXHa6JQK0sKDGBIVGm8oeW0k2kfbJgQ,182
45
+ mini_swe_agent-1.4.0.dist-info/top_level.txt,sha256=zKF4t8bFpV87fdVABZt2Da-vnb4Vkh_CxkwQx5YT4Ew,13
46
+ mini_swe_agent-1.4.0.dist-info/RECORD,,
minisweagent/__init__.py CHANGED
@@ -8,7 +8,7 @@ This file provides:
8
8
  unless you want the static type checking.
9
9
  """
10
10
 
11
- __version__ = "1.2.0"
11
+ __version__ = "1.4.0"
12
12
 
13
13
  import os
14
14
  from pathlib import Path
@@ -8,6 +8,7 @@ import os
8
8
  import re
9
9
  import threading
10
10
  import time
11
+ import traceback
11
12
  from dataclasses import dataclass, field
12
13
  from pathlib import Path
13
14
  from typing import Literal
@@ -19,9 +20,9 @@ from textual.binding import Binding
19
20
  from textual.containers import Container, Vertical, VerticalScroll
20
21
  from textual.css.query import NoMatches
21
22
  from textual.events import Key
22
- from textual.widgets import Footer, Header, Static, TextArea
23
+ from textual.widgets import Footer, Header, Input, Static, TextArea
23
24
 
24
- from minisweagent.agents.default import AgentConfig, DefaultAgent, NonTerminatingException
25
+ from minisweagent.agents.default import AgentConfig, DefaultAgent, NonTerminatingException, Submitted
25
26
 
26
27
 
27
28
  @dataclass
@@ -30,6 +31,8 @@ class TextualAgentConfig(AgentConfig):
30
31
  """Mode for action execution: 'confirm' requires user confirmation, 'yolo' executes immediately."""
31
32
  whitelist_actions: list[str] = field(default_factory=list)
32
33
  """Never confirm actions that match these regular expressions."""
34
+ confirm_exit: bool = True
35
+ """If the agent wants to finish, do we ask for confirmation from user?"""
33
36
 
34
37
 
35
38
  class TextualAgent(DefaultAgent):
@@ -37,17 +40,29 @@ class TextualAgent(DefaultAgent):
37
40
  """Connects the DefaultAgent to the TextualApp."""
38
41
  self.app = app
39
42
  super().__init__(*args, config_class=TextualAgentConfig, **kwargs)
43
+ self._current_action_from_human = False
40
44
 
41
45
  def add_message(self, role: str, content: str):
42
46
  super().add_message(role, content)
43
47
  if self.app.agent_state != "UNINITIALIZED":
44
48
  self.app.call_from_thread(self.app.on_message_added)
45
49
 
50
+ def query(self) -> dict:
51
+ if self.config.mode == "human":
52
+ human_input = self.app.input_container.request_input("Enter your command:")
53
+ self._current_action_from_human = True
54
+ msg = {"content": f"\n```bash\n{human_input}\n```"}
55
+ self.add_message("assistant", msg["content"])
56
+ return msg
57
+ self._current_action_from_human = False
58
+ return super().query()
59
+
46
60
  def run(self, task: str) -> tuple[str, str]:
47
61
  try:
48
62
  exit_status, result = super().run(task)
49
63
  except Exception as e:
50
64
  result = str(e)
65
+ print(traceback.format_exc())
51
66
  self.app.call_from_thread(self.app.on_agent_finished, "ERROR", result)
52
67
  return "ERROR", result
53
68
  else:
@@ -55,13 +70,30 @@ class TextualAgent(DefaultAgent):
55
70
  return exit_status, result
56
71
 
57
72
  def execute_action(self, action: dict) -> dict:
58
- if self.config.mode == "confirm" and not any(
59
- re.match(r, action["action"]) for r in self.config.whitelist_actions
73
+ if self.config.mode == "human" and not self._current_action_from_human: # threading, grrrrr
74
+ raise NonTerminatingException("Command not executed because user switched to manual mode.")
75
+ if (
76
+ self.config.mode == "confirm"
77
+ and action["action"].strip()
78
+ and not any(re.match(r, action["action"]) for r in self.config.whitelist_actions)
60
79
  ):
61
- if result := self.app.confirmation_container.request_confirmation(action["action"]):
80
+ result = self.app.input_container.request_input("Press ENTER to confirm or provide rejection reason")
81
+ if result: # Non-empty string means rejection
62
82
  raise NonTerminatingException(f"Command not executed: {result}")
63
83
  return super().execute_action(action)
64
84
 
85
+ def has_finished(self, output: dict[str, str]):
86
+ try:
87
+ return super().has_finished(output)
88
+ except Submitted as e:
89
+ if self.config.confirm_exit:
90
+ if new_task := self.app.input_container.request_input(
91
+ "[bold green]Agent wants to finish.[/bold green] "
92
+ "[green]Type a comment to give it a new task or press enter to quit.\n"
93
+ ).strip():
94
+ raise NonTerminatingException(f"The user added a new task: {new_task}")
95
+ raise e
96
+
65
97
 
66
98
  class AddLogEmitCallback(logging.Handler):
67
99
  def __init__(self, callback):
@@ -87,77 +119,123 @@ def _messages_to_steps(messages: list[dict]) -> list[list[dict]]:
87
119
  return steps
88
120
 
89
121
 
90
- class ConfirmationPromptContainer(Container):
122
+ class SmartInputContainer(Container):
91
123
  def __init__(self, app: "AgentApp"):
92
- """This class is responsible for handling the action execution confirmation."""
93
- super().__init__(id="confirmation-container")
124
+ """Smart input container supporting single-line and multi-line input modes."""
125
+ super().__init__(classes="smart-input-container")
94
126
  self._app = app
95
- self.rejecting = False
127
+ self._multiline_mode = False
96
128
  self.can_focus = True
97
129
  self.display = False
98
130
 
99
- self._pending_action: str | None = None
100
- self._confirmation_event = threading.Event()
101
- self._confirmation_result: str | None = None
131
+ self.pending_prompt: str | None = None
132
+ self._input_event = threading.Event()
133
+ self._input_result: str | None = None
102
134
 
103
- def compose(self) -> ComposeResult:
104
- yield Static(
105
- "Press [bold]ENTER[/bold] to confirm action or [bold]BACKSPACE[/bold] to reject (or [bold]y[/bold] to toggle YOLO mode)",
106
- classes="confirmation-prompt",
135
+ self._header_display = Static(
136
+ "USER INPUT REQUESTED", id="input-header-display", classes="message-header input-request-header"
107
137
  )
108
- yield TextArea(id="rejection-input")
109
- rejection_help = Static(
110
- "Press [bold]Ctrl+D[/bold] to submit rejection message",
111
- id="rejection-help",
112
- classes="rejection-help",
138
+ self._hint_text = Static(
139
+ "[bold]Enter[/bold] to submit, [bold]Ctrl+T[/bold] to switch to multi-line input, [bold]Tab[/bold] to switch focus with other controls",
140
+ classes="hint-text",
141
+ )
142
+ self._single_input = Input(placeholder="Type your input...")
143
+ self._multi_input = TextArea("", show_line_numbers=False, classes="multi-input")
144
+
145
+ self._input_elements_container = Vertical(
146
+ self._header_display,
147
+ self._hint_text,
148
+ self._single_input,
149
+ self._multi_input,
150
+ classes="message-container",
113
151
  )
114
- rejection_help.display = False
115
- yield rejection_help
116
-
117
- def request_confirmation(self, action: str) -> str | None:
118
- """Request confirmation for an action. Returns rejection message or None."""
119
- self._confirmation_event.clear()
120
- self._confirmation_result = None
121
- self._pending_action = action
122
- self._app.call_from_thread(self._app.update_content)
123
- self._confirmation_event.wait()
124
- return self._confirmation_result
125
152
 
126
- def _complete_confirmation(self, rejection_message: str | None):
127
- """Internal method to complete the confirmation process."""
128
- self._confirmation_result = rejection_message
129
- self._pending_action = None
153
+ def compose(self) -> ComposeResult:
154
+ yield self._input_elements_container
155
+
156
+ def on_mount(self) -> None:
157
+ """Initialize the widget state."""
158
+ self._multi_input.display = False
159
+ self._update_mode_display()
160
+
161
+ def on_focus(self) -> None:
162
+ """Called when the container gains focus."""
163
+ if self._multiline_mode:
164
+ self._multi_input.focus()
165
+ else:
166
+ self._single_input.focus()
167
+
168
+ def request_input(self, prompt: str) -> str:
169
+ """Request input from user. Returns input text (empty string if confirmed without reason)."""
170
+ self._input_event.clear()
171
+ self._input_result = None
172
+ self.pending_prompt = prompt
173
+ self._header_display.update(prompt)
174
+ self._update_mode_display()
175
+ self._app.call_from_thread(self._app.update_content)
176
+ self._input_event.wait()
177
+ return self._input_result or ""
178
+
179
+ def _complete_input(self, input_text: str):
180
+ """Internal method to complete the input process."""
181
+ self._input_result = input_text
182
+ self.pending_prompt = None
183
+ self._header_display.update("USER INPUT REQUESTED")
130
184
  self.display = False
131
- self.rejecting = False
132
- rejection_input = self.query_one("#rejection-input", TextArea)
133
- rejection_input.display = False
134
- rejection_input.text = ""
135
- rejection_help = self.query_one("#rejection-help", Static)
136
- rejection_help.display = False
137
- # Reset agent state to RUNNING after confirmation is completed
138
- if rejection_message is None:
139
- self._app.agent_state = "RUNNING"
140
- self._confirmation_event.set()
185
+ self._single_input.value = ""
186
+ self._multi_input.text = ""
187
+ self._multiline_mode = False
188
+ self._update_mode_display()
189
+ self._app.agent_state = "RUNNING"
141
190
  self._app.update_content()
191
+ # Reset scroll position to bottom since input container disappearing changes layout
192
+ # somehow scroll_to doesn't work.
193
+ self._app._vscroll.scroll_y = 0
194
+ self._input_event.set()
195
+
196
+ def action_toggle_mode(self) -> None:
197
+ """Switch from single-line to multi-line mode (one-way only)."""
198
+ if self.pending_prompt is None or self._multiline_mode:
199
+ return
200
+
201
+ self._multiline_mode = True
202
+ self._update_mode_display()
203
+ self.on_focus()
204
+
205
+ def _update_mode_display(self) -> None:
206
+ """Update the display based on current mode."""
207
+ if self._multiline_mode:
208
+ self._multi_input.text = self._single_input.value
209
+ self._single_input.display = False
210
+ self._multi_input.display = True
211
+
212
+ else:
213
+ self._multi_input.display = False
214
+ self._single_input.display = True
215
+
216
+ def on_input_submitted(self, event: Input.Submitted) -> None:
217
+ """Handle single-line input submission."""
218
+ if not self._multiline_mode:
219
+ text = event.input.value.strip()
220
+ self._complete_input(text)
142
221
 
143
222
  def on_key(self, event: Key) -> None:
144
- if self.rejecting and event.key == "ctrl+d":
223
+ """Handle key events."""
224
+ if event.key == "ctrl+t" and not self._multiline_mode:
145
225
  event.prevent_default()
146
- rejection_input = self.query_one("#rejection-input", TextArea)
147
- self._complete_confirmation(rejection_input.text)
226
+ self.action_toggle_mode()
227
+ return
228
+
229
+ if self._multiline_mode and event.key == "ctrl+d":
230
+ event.prevent_default()
231
+ self._complete_input(self._multi_input.text.strip())
232
+ return
233
+
234
+ if event.key == "escape":
235
+ event.prevent_default()
236
+ self.can_focus = False
237
+ self._app.set_focus(None)
148
238
  return
149
- if not self.rejecting:
150
- if event.key == "enter":
151
- event.prevent_default()
152
- self._complete_confirmation(None)
153
- elif event.key == "backspace":
154
- event.prevent_default()
155
- self.rejecting = True
156
- rejection_input = self.query_one("#rejection-input", TextArea)
157
- rejection_input.display = True
158
- rejection_input.focus()
159
- rejection_help = self.query_one("#rejection-help", Static)
160
- rejection_help.display = True
161
239
 
162
240
 
163
241
  class AgentApp(App):
@@ -171,6 +249,7 @@ class AgentApp(App):
171
249
  Binding("q", "quit", "Quit"),
172
250
  Binding("y", "yolo", "Switch to YOLO Mode"),
173
251
  Binding("c", "confirm", "Switch to Confirm Mode"),
252
+ Binding("u", "human", "Switch to Human Mode"),
174
253
  ]
175
254
 
176
255
  def __init__(self, model, env, task: str, **kwargs):
@@ -182,13 +261,15 @@ class AgentApp(App):
182
261
  self.agent = TextualAgent(self, model=model, env=env, **kwargs)
183
262
  self._i_step = 0
184
263
  self.n_steps = 1
185
- self.confirmation_container = ConfirmationPromptContainer(self)
264
+ self.input_container = SmartInputContainer(self)
186
265
  self.log_handler = AddLogEmitCallback(lambda record: self.call_from_thread(self.on_log_message_emitted, record))
187
266
  logging.getLogger().addHandler(self.log_handler)
188
267
  self._spinner = Spinner("dots")
189
268
  self.exit_status: str | None = None
190
269
  self.result: str | None = None
191
270
 
271
+ self._vscroll = VerticalScroll()
272
+
192
273
  # --- Basics ---
193
274
 
194
275
  @property
@@ -201,15 +282,16 @@ class AgentApp(App):
201
282
  """Set current step index, automatically clamping to valid bounds."""
202
283
  if value != self._i_step:
203
284
  self._i_step = max(0, min(value, self.n_steps - 1))
204
- self.query_one(VerticalScroll).scroll_to(y=0, animate=False)
285
+ self._vscroll.scroll_to(y=0, animate=False)
205
286
  self.update_content()
206
287
 
207
288
  def compose(self) -> ComposeResult:
208
289
  yield Header()
209
290
  with Container(id="main"):
210
- with VerticalScroll():
211
- yield Vertical(id="content")
212
- yield self.confirmation_container
291
+ with self._vscroll:
292
+ with Vertical(id="content"):
293
+ pass
294
+ yield self.input_container
213
295
  yield Footer()
214
296
 
215
297
  def on_mount(self) -> None:
@@ -221,8 +303,7 @@ class AgentApp(App):
221
303
  # --- Reacting to events ---
222
304
 
223
305
  def on_message_added(self) -> None:
224
- vs = self.query_one(VerticalScroll)
225
- auto_follow = self.i_step == self.n_steps - 1 and vs.scroll_target_y <= 1
306
+ auto_follow = self.i_step == self.n_steps - 1 and self._vscroll.scroll_y <= 1
226
307
  self.n_steps = len(_messages_to_steps(self.agent.messages))
227
308
  self.update_content()
228
309
  if auto_follow:
@@ -267,13 +348,11 @@ class AgentApp(App):
267
348
  message_container.mount(Static(role.upper(), classes="message-header"))
268
349
  message_container.mount(Static(Text(content_str, no_wrap=False), classes="message-content"))
269
350
 
270
- if self.confirmation_container._pending_action is not None:
271
- self.agent_state = "AWAITING_CONFIRMATION"
272
- self.confirmation_container.display = (
273
- self.confirmation_container._pending_action is not None and self.i_step == len(items) - 1
274
- )
275
- if self.confirmation_container.display:
276
- self.confirmation_container.focus()
351
+ if self.input_container.pending_prompt is not None:
352
+ self.agent_state = "AWAITING_INPUT"
353
+ self.input_container.display = self.input_container.pending_prompt is not None and self.i_step == len(items) - 1
354
+ if self.input_container.display:
355
+ self.input_container.on_focus()
277
356
 
278
357
  self._update_headers()
279
358
  self.refresh()
@@ -294,12 +373,21 @@ class AgentApp(App):
294
373
 
295
374
  def action_yolo(self):
296
375
  self.agent.config.mode = "yolo"
297
- self.confirmation_container._complete_confirmation(None)
298
- self.notify("YOLO mode enabled - actions will execute immediately")
376
+ if self.input_container.pending_prompt is not None:
377
+ self.input_container._complete_input("") # accept
378
+ self.notify("YOLO mode enabled - LM actions will execute immediately")
379
+
380
+ def action_human(self):
381
+ if self.agent.config.mode == "confirm" and self.input_container.pending_prompt is not None:
382
+ self.input_container._complete_input("User switched to manual mode, this command will be ignored")
383
+ self.agent.config.mode = "human"
384
+ self.notify("Human mode enabled - you can now type commands directly")
299
385
 
300
386
  def action_confirm(self):
387
+ if self.agent.config.mode == "human" and self.input_container.pending_prompt is not None:
388
+ self.input_container._complete_input("") # just submit blank action
301
389
  self.agent.config.mode = "confirm"
302
- self.notify("Confirm mode enabled - actions will require confirmation")
390
+ self.notify("Confirm mode enabled - LM proposes commands and you confirm/reject them")
303
391
 
304
392
  def action_next_step(self) -> None:
305
393
  self.i_step += 1
@@ -314,9 +402,7 @@ class AgentApp(App):
314
402
  self.i_step = self.n_steps - 1
315
403
 
316
404
  def action_scroll_down(self) -> None:
317
- vs = self.query_one(VerticalScroll)
318
- vs.scroll_to(y=vs.scroll_target_y + 15)
405
+ self._vscroll.scroll_to(y=self._vscroll.scroll_target_y + 15)
319
406
 
320
407
  def action_scroll_up(self) -> None:
321
- vs = self.query_one(VerticalScroll)
322
- vs.scroll_to(y=vs.scroll_target_y - 15)
408
+ self._vscroll.scroll_to(y=self._vscroll.scroll_target_y - 15)
@@ -1,12 +1,13 @@
1
1
  Screen {
2
2
  layout: grid;
3
3
  grid-size: 1;
4
- grid-rows: 1fr 8 1fr;
4
+ grid-rows: auto 1fr auto;
5
5
  }
6
6
 
7
7
  #main {
8
8
  height: 100%;
9
9
  padding: 1;
10
+ layout: vertical;
10
11
  }
11
12
 
12
13
  Footer {
@@ -16,6 +17,32 @@ Footer {
16
17
 
17
18
  #content {
18
19
  height: auto;
20
+ min-height: 0;
21
+ }
22
+
23
+ .smart-input-container {
24
+ height: auto;
25
+ margin-top: 0;
26
+ padding: 0;
27
+ min-height: 0;
28
+ }
29
+
30
+ .multi-input {
31
+ height: auto;
32
+ max-height: 20;
33
+ min-height: 3;
34
+ }
35
+
36
+ .prompt-display {
37
+ margin-bottom: 1;
38
+ padding: 0 1;
39
+ text-style: bold;
40
+ }
41
+
42
+ .hint-text{
43
+ margin-bottom: 1;
44
+ padding: 0 1;
45
+ color: white;
19
46
  }
20
47
 
21
48
  .message-container {
@@ -33,6 +60,10 @@ Footer {
33
60
  text-style: bold;
34
61
  }
35
62
 
63
+ .input-request-header {
64
+ color: $warning;
65
+ }
66
+
36
67
  .message-content {
37
68
  margin-top: 1;
38
69
  padding: 0 1;
@@ -42,29 +73,6 @@ Header.running {
42
73
  background: $error;
43
74
  }
44
75
 
45
- .confirmation-modal {
46
- layout: vertical;
47
- background: $surface;
48
- margin: 1 4;
49
- min-width: 40;
50
- padding: 1;
51
- border: tall $primary;
52
- height: auto;
53
- }
54
-
55
- .modal-title {
56
- text-align: center;
57
- text-style: bold;
58
- margin-bottom: 1;
59
- }
60
-
61
- .modal-content {
62
- margin: 1 2;
63
- min-height: 1;
64
- max-height: 10;
65
- overflow-y: auto;
66
- }
67
-
68
76
  .button-container {
69
77
  layout: horizontal;
70
78
  align-horizontal: center;
@@ -74,55 +82,4 @@ Header.running {
74
82
  .button-container Button {
75
83
  margin: 0 1;
76
84
  min-width: 10;
77
- }
78
-
79
- .confirmation-container {
80
- background: $boost;
81
- border: heavy $primary;
82
- padding: 1;
83
- margin: 1;
84
- }
85
-
86
- .confirmation-header {
87
- color: $warning;
88
- text-style: bold;
89
- }
90
-
91
- .command-to-confirm {
92
- background: $surface;
93
- margin: 1 0;
94
- padding: 1;
95
- color: $text;
96
- }
97
-
98
- #confirmation-input {
99
- margin-top: 1;
100
- }
101
-
102
- .confirmation-prompt {
103
- background: $boost;
104
- border: heavy $warning;
105
- padding: 1;
106
- margin: 1;
107
- text-align: center;
108
- color: $warning;
109
- }
110
-
111
- .confirmation-prompt:focus {
112
- border: heavy $accent;
113
- background: $panel;
114
- }
115
-
116
- #rejection-input {
117
- display: none;
118
- margin: 1;
119
- }
120
-
121
- .rejection-help {
122
- background: $boost;
123
- border: heavy $warning;
124
- padding: 1;
125
- margin: 1;
126
- text-align: center;
127
- color: $warning;
128
85
  }
@@ -23,13 +23,15 @@ agent:
23
23
  You can execute bash commands and edit files to implement the necessary changes.
24
24
 
25
25
  ## Recommended Workflow
26
- 1. Analyze the codebase by finding and reading relevant files.
27
- If present, you might want to take a look at the following files that set additional guidelines
28
- for your work: CLAUDE.md, .cursor/rules/<relevant rules>
29
- 2. Create a script to reproduce the issue
30
- 3. Edit the source code to resolve the issue
31
- 4. Verify your fix works by running your script again
32
- 5. Test edge cases to ensure your fix is robust
26
+
27
+ 1. If present, you might want to take a look at the following files that set additional guidelines
28
+ for your work: CLAUDE.md, .github/copilot-instructions.md. Use a find command to locate all of them at once.
29
+ If files corresponding to multiple AI systems are present, it's enough to read the ones for one of them.
30
+ 2. Analyze the codebase by finding and reading relevant files.
31
+ 3. Create a script to reproduce the issue
32
+ 4. Edit the source code to resolve the issue
33
+ 5. Verify your fix works by running your script again
34
+ 6. Test edge cases to ensure your fix is robust
33
35
 
34
36
  ## Important Rules
35
37
 
@@ -140,6 +142,9 @@ agent:
140
142
  <action>
141
143
  ```
142
144
  </response_example>
145
+
146
+ Note: In rare cases, if you need to reference a similar format in your command, you might have
147
+ to proceed in two steps, first writing TRIPLEBACKTICKSBASH, then replacing them with ```bash.
143
148
  step_limit: 0.
144
149
  cost_limit: 3.
145
150
  mode: confirm
@@ -24,6 +24,8 @@ class DockerEnvironmentConfig:
24
24
  """Path to the docker/container executable."""
25
25
  run_args: list[str] = field(default_factory=list)
26
26
  """Additional arguments to pass to the docker/container executable."""
27
+ container_timeout: str = "2h"
28
+ """Max duration to keep container running. Uses the same format as the sleep command."""
27
29
 
28
30
 
29
31
  class DockerEnvironment:
@@ -49,7 +51,7 @@ class DockerEnvironment:
49
51
  *self.config.run_args,
50
52
  self.config.image,
51
53
  "sleep",
52
- "infinity", # Keep container running
54
+ self.config.container_timeout,
53
55
  ]
54
56
  print(f"Starting container with command: {shlex.join(cmd)}")
55
57
  result = subprocess.run(
@@ -1,5 +1,7 @@
1
+ import json
1
2
  import logging
2
3
  from dataclasses import dataclass, field
4
+ from pathlib import Path
3
5
  from typing import Any
4
6
 
5
7
  import litellm
@@ -20,6 +22,7 @@ logger = logging.getLogger("litellm_model")
20
22
  class LitellmModelConfig:
21
23
  model_name: str
22
24
  model_kwargs: dict[str, Any] = field(default_factory=dict)
25
+ litellm_model_registry: Path | None = None
23
26
 
24
27
 
25
28
  class LitellmModel:
@@ -27,6 +30,8 @@ class LitellmModel:
27
30
  self.config = LitellmModelConfig(**kwargs)
28
31
  self.cost = 0.0
29
32
  self.n_calls = 0
33
+ if self.config.litellm_model_registry is not None:
34
+ litellm.utils.register_model(json.loads(self.config.litellm_model_registry.read_text()))
30
35
 
31
36
  @retry(
32
37
  stop=stop_after_attempt(10),
@@ -1,11 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- """Run mini-SWE-agent on SWEBench instances.
4
-
5
- [not dim]
6
- More information about the usage: [bold green]https://mini-swe-agent.com/latest/usage/swebench/[/bold green]
7
- [/not dim]
8
- """
3
+ """Run mini-SWE-agent on SWE-bench instances in batch mode."""
4
+ # Read this first: https://mini-swe-agent.com/latest/usage/swebench/ (usage docs)
9
5
 
10
6
  import concurrent.futures
11
7
  import json
@@ -28,6 +24,13 @@ from minisweagent.models import get_model
28
24
  from minisweagent.run.extra.utils.batch_progress import RunBatchProgressManager
29
25
  from minisweagent.run.utils.save import save_traj
30
26
 
27
+ _HELP_TEXT = """Run mini-SWE-agent on SWEBench instances.
28
+
29
+ [not dim]
30
+ More information about the usage: [bold green]https://mini-swe-agent.com/latest/usage/swebench/[/bold green]
31
+ [/not dim]
32
+ """
33
+
31
34
  app = typer.Typer(rich_markup_mode="rich", add_completion=False)
32
35
 
33
36
  DATASET_MAPPING = {
@@ -168,7 +171,7 @@ def filter_instances(
168
171
  return instances
169
172
 
170
173
 
171
- @app.command()
174
+ @app.command(help=_HELP_TEXT)
172
175
  def main(
173
176
  subset: str = typer.Option("lite", "--subset", help="SWEBench subset to use or path to a dataset"),
174
177
  split: str = typer.Option("dev", "--split", help="Dataset split"),
@@ -183,7 +186,6 @@ def main(
183
186
  builtin_config_dir / "extra" / "swebench.yaml", "-c", "--config", help="Path to a config file"
184
187
  ),
185
188
  ) -> None:
186
- """Run mini-SWE-agent on SWEBench instances"""
187
189
  dataset_path = DATASET_MAPPING.get(subset, subset)
188
190
  print(f"Loading dataset {dataset_path}, split {split}...")
189
191
  instances = list(load_dataset(dataset_path, split=split))
minisweagent/run/mini.py CHANGED
@@ -1,16 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- """Run mini-SWE-agent in your local environment.
4
-
5
- [not dim]
6
- There are two different user interfaces:
7
-
8
- [bold green]mini[/bold green] Simple REPL-style interface
9
- [bold green]mini -v[/bold green] Pager-style interface (Textual)
10
-
11
- More information about the usage: [bold green]https://mini-swe-agent.com/latest/usage/mini/[/bold green]
12
- [/not dim]
13
- """
3
+ """Run mini-SWE-agent in your local environment. This is the default executable `mini`."""
4
+ # Read this first: https://mini-swe-agent.com/latest/usage/mini/ (usage)
14
5
 
15
6
  import os
16
7
  from pathlib import Path
@@ -33,9 +24,21 @@ from minisweagent.run.extra.config import configure_if_first_time
33
24
  from minisweagent.run.utils.save import save_traj
34
25
 
35
26
  DEFAULT_CONFIG = Path(os.getenv("MSWEA_MINI_CONFIG_PATH", builtin_config_dir / "mini.yaml"))
27
+ DEFAULT_OUTPUT = global_config_dir / "last_mini_run.traj.json"
36
28
  console = Console(highlight=False)
37
29
  app = typer.Typer(rich_markup_mode="rich")
38
30
  prompt_session = PromptSession(history=FileHistory(global_config_dir / "mini_task_history.txt"))
31
+ _HELP_TEXT = """Run mini-SWE-agent in your local environment.
32
+
33
+ [not dim]
34
+ There are two different user interfaces:
35
+
36
+ [bold green]mini[/bold green] Simple REPL-style interface
37
+ [bold green]mini -v[/bold green] Pager-style interface (Textual)
38
+
39
+ More information about the usage: [bold green]https://mini-swe-agent.com/latest/usage/mini/[/bold green]
40
+ [/not dim]
41
+ """
39
42
 
40
43
 
41
44
  def run_interactive(model: Model, env: Environment, agent_config: dict, task: str, output: Path | None = None) -> Any:
@@ -68,7 +71,7 @@ def run_textual(model: Model, env: Environment, agent_config: dict, task: str, o
68
71
  save_traj(agent_app.agent, output, exit_status=agent_app.exit_status, result=agent_app.result)
69
72
 
70
73
 
71
- @app.command(help=__doc__)
74
+ @app.command(help=_HELP_TEXT)
72
75
  def main(
73
76
  visual: bool = typer.Option(False, "-v", "--visual", help="Use visual (pager-style) UI (Textual)"),
74
77
  model_name: str | None = typer.Option(
@@ -81,7 +84,7 @@ def main(
81
84
  yolo: bool = typer.Option(False, "-y", "--yolo", help="Run without confirmation"),
82
85
  cost_limit: float | None = typer.Option(None, "-l", "--cost-limit", help="Cost limit. Set to 0 to disable."),
83
86
  config_spec: Path = typer.Option(DEFAULT_CONFIG, "-c", "--config", help="Path to config file"),
84
- output: Path | None = typer.Option(None, "-o", "--output", help="Output file"),
87
+ output: Path | None = typer.Option(DEFAULT_OUTPUT, "-o", "--output", help="Output trajectory file"),
85
88
  exit_immediately: bool = typer.Option(
86
89
  False, "--exit-immediately", help="Exit immediately when the agent wants to finish instead of prompting."
87
90
  ),
@@ -105,7 +108,7 @@ def main(
105
108
  config["agent"]["mode"] = "confirm" if not yolo else "yolo"
106
109
  if cost_limit:
107
110
  config["agent"]["cost_limit"] = cost_limit
108
- if not visual and exit_immediately:
111
+ if exit_immediately:
109
112
  config["agent"]["confirm_exit"] = False
110
113
  model = get_model(model_name, config.get("model", {}))
111
114
  env = LocalEnvironment(**config.get("env", {}))
@@ -23,6 +23,7 @@ def save_traj(
23
23
  },
24
24
  },
25
25
  "messages": [],
26
+ "trajectory_format": "mini-swe-agent-1",
26
27
  } | kwargs
27
28
  if agent is not None:
28
29
  data["info"]["model_stats"]["instance_cost"] = agent.model.cost