mini-swe-agent 1.3.0__py3-none-any.whl → 1.4.1__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.
- {mini_swe_agent-1.3.0.dist-info → mini_swe_agent-1.4.1.dist-info}/METADATA +1 -1
- {mini_swe_agent-1.3.0.dist-info → mini_swe_agent-1.4.1.dist-info}/RECORD +15 -15
- minisweagent/__init__.py +1 -1
- minisweagent/agents/default.py +1 -1
- minisweagent/agents/interactive.py +5 -3
- minisweagent/agents/interactive_textual.py +168 -82
- minisweagent/config/mini.tcss +32 -75
- minisweagent/environments/docker.py +3 -1
- minisweagent/models/litellm_model.py +1 -1
- minisweagent/run/mini.py +3 -2
- minisweagent/run/utils/save.py +1 -0
- {mini_swe_agent-1.3.0.dist-info → mini_swe_agent-1.4.1.dist-info}/WHEEL +0 -0
- {mini_swe_agent-1.3.0.dist-info → mini_swe_agent-1.4.1.dist-info}/entry_points.txt +0 -0
- {mini_swe_agent-1.3.0.dist-info → mini_swe_agent-1.4.1.dist-info}/licenses/LICENSE.md +0 -0
- {mini_swe_agent-1.3.0.dist-info → mini_swe_agent-1.4.1.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mini-swe-agent
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.1
|
|
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
|
-
minisweagent/__init__.py,sha256=
|
|
1
|
+
mini_swe_agent-1.4.1.dist-info/licenses/LICENSE.md,sha256=D3luWPkdHAe7LBsdD4vzqDAXw6Xewb3G-uczss0uh1s,1094
|
|
2
|
+
minisweagent/__init__.py,sha256=Fao4tu0VXB-yMYWz02SQVn7_hu-zj9am7sgP6swBWzs,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
|
-
minisweagent/agents/default.py,sha256=
|
|
7
|
-
minisweagent/agents/interactive.py,sha256=
|
|
8
|
-
minisweagent/agents/interactive_textual.py,sha256
|
|
6
|
+
minisweagent/agents/default.py,sha256=9LxgX3KWHFGkGE4FjGOZwK5oA8ykzg9KgmpgyqlEuoo,5460
|
|
7
|
+
minisweagent/agents/interactive.py,sha256=7HW2cffaV5f66DIjxvtIbL8mo_S5aZSwgNLSmHp6VC0,7450
|
|
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=
|
|
13
|
+
minisweagent/config/mini.tcss,sha256=VgdZZqWElA5_nn4DJUFMpz8C7Gmi5s5XOtm7pfyM83Q,1122
|
|
14
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=
|
|
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=
|
|
25
|
+
minisweagent/models/litellm_model.py,sha256=2-Q7gEoKK-2kbUsfDxrTBxIQasEqw1raE_1Xk28XXUc,2289
|
|
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,7 +30,7 @@ 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=
|
|
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
|
|
@@ -38,9 +38,9 @@ minisweagent/run/extra/swebench.py,sha256=m5_PZI4ojkUyCxzkkMtel_vlnYmjziWrXu73yH
|
|
|
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=
|
|
42
|
-
mini_swe_agent-1.
|
|
43
|
-
mini_swe_agent-1.
|
|
44
|
-
mini_swe_agent-1.
|
|
45
|
-
mini_swe_agent-1.
|
|
46
|
-
mini_swe_agent-1.
|
|
41
|
+
minisweagent/run/utils/save.py,sha256=q7omf7zYHg73k8-Iyp9w5YVSYvDAacRrh4X9L_4VhNM,942
|
|
42
|
+
mini_swe_agent-1.4.1.dist-info/METADATA,sha256=wbuaTIHIypAE-u6bBYi2uXnmtXRv96vVmognFS6WG2U,13459
|
|
43
|
+
mini_swe_agent-1.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
44
|
+
mini_swe_agent-1.4.1.dist-info/entry_points.txt,sha256=d1_yRbTaGjs1UXHa6JQK0sKDGBIVGm8oeW0k2kfbJgQ,182
|
|
45
|
+
mini_swe_agent-1.4.1.dist-info/top_level.txt,sha256=zKF4t8bFpV87fdVABZt2Da-vnb4Vkh_CxkwQx5YT4Ew,13
|
|
46
|
+
mini_swe_agent-1.4.1.dist-info/RECORD,,
|
minisweagent/__init__.py
CHANGED
minisweagent/agents/default.py
CHANGED
|
@@ -72,7 +72,7 @@ class DefaultAgent:
|
|
|
72
72
|
def run(self, task: str) -> tuple[str, str]:
|
|
73
73
|
"""Run step() until agent is finished. Return exit status & message"""
|
|
74
74
|
self.messages = []
|
|
75
|
-
self.add_message("system", self.config.system_template)
|
|
75
|
+
self.add_message("system", self.render_template(self.config.system_template))
|
|
76
76
|
self.add_message("user", self.render_template(self.config.instance_template, task=task))
|
|
77
77
|
while True:
|
|
78
78
|
try:
|
|
@@ -35,8 +35,8 @@ class InteractiveAgentConfig(AgentConfig):
|
|
|
35
35
|
class InteractiveAgent(DefaultAgent):
|
|
36
36
|
_MODE_COMMANDS_MAPPING = {"/u": "human", "/c": "confirm", "/y": "yolo"}
|
|
37
37
|
|
|
38
|
-
def __init__(self, *args, **kwargs):
|
|
39
|
-
super().__init__(*args, config_class=
|
|
38
|
+
def __init__(self, *args, config_class=InteractiveAgentConfig, **kwargs):
|
|
39
|
+
super().__init__(*args, config_class=config_class, **kwargs)
|
|
40
40
|
self.cost_last_confirmed = 0.0
|
|
41
41
|
|
|
42
42
|
def add_message(self, role: str, content: str):
|
|
@@ -59,7 +59,9 @@ class InteractiveAgent(DefaultAgent):
|
|
|
59
59
|
case "/y" | "/c": # Just go to the super query, which queries the LM for the next action
|
|
60
60
|
pass
|
|
61
61
|
case _:
|
|
62
|
-
|
|
62
|
+
msg = {"content": f"\n```bash\n{command}\n```"}
|
|
63
|
+
self.add_message("assistant", msg["content"])
|
|
64
|
+
return msg
|
|
63
65
|
try:
|
|
64
66
|
with console.status("Waiting for the LM to respond..."):
|
|
65
67
|
return super().query()
|
|
@@ -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 == "
|
|
59
|
-
|
|
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
|
-
|
|
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
|
|
122
|
+
class SmartInputContainer(Container):
|
|
91
123
|
def __init__(self, app: "AgentApp"):
|
|
92
|
-
"""
|
|
93
|
-
super().__init__(
|
|
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.
|
|
127
|
+
self._multiline_mode = False
|
|
96
128
|
self.can_focus = True
|
|
97
129
|
self.display = False
|
|
98
130
|
|
|
99
|
-
self.
|
|
100
|
-
self.
|
|
101
|
-
self.
|
|
131
|
+
self.pending_prompt: str | None = None
|
|
132
|
+
self._input_event = threading.Event()
|
|
133
|
+
self._input_result: str | None = None
|
|
102
134
|
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
"
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
223
|
+
"""Handle key events."""
|
|
224
|
+
if event.key == "ctrl+t" and not self._multiline_mode:
|
|
145
225
|
event.prevent_default()
|
|
146
|
-
|
|
147
|
-
|
|
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.
|
|
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.
|
|
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
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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.
|
|
271
|
-
self.agent_state = "
|
|
272
|
-
self.
|
|
273
|
-
|
|
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.
|
|
298
|
-
|
|
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 -
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
vs.scroll_to(y=vs.scroll_target_y - 15)
|
|
408
|
+
self._vscroll.scroll_to(y=self._vscroll.scroll_target_y - 15)
|
minisweagent/config/mini.tcss
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Screen {
|
|
2
2
|
layout: grid;
|
|
3
3
|
grid-size: 1;
|
|
4
|
-
grid-rows: 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
|
}
|
|
@@ -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
|
-
|
|
54
|
+
self.config.container_timeout,
|
|
53
55
|
]
|
|
54
56
|
print(f"Starting container with command: {shlex.join(cmd)}")
|
|
55
57
|
result = subprocess.run(
|
|
@@ -31,7 +31,7 @@ class LitellmModel:
|
|
|
31
31
|
self.cost = 0.0
|
|
32
32
|
self.n_calls = 0
|
|
33
33
|
if self.config.litellm_model_registry is not None:
|
|
34
|
-
litellm.utils.register_model(json.loads(self.config.litellm_model_registry.read_text()))
|
|
34
|
+
litellm.utils.register_model(json.loads(Path(self.config.litellm_model_registry).read_text()))
|
|
35
35
|
|
|
36
36
|
@retry(
|
|
37
37
|
stop=stop_after_attempt(10),
|
minisweagent/run/mini.py
CHANGED
|
@@ -24,6 +24,7 @@ from minisweagent.run.extra.config import configure_if_first_time
|
|
|
24
24
|
from minisweagent.run.utils.save import save_traj
|
|
25
25
|
|
|
26
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"
|
|
27
28
|
console = Console(highlight=False)
|
|
28
29
|
app = typer.Typer(rich_markup_mode="rich")
|
|
29
30
|
prompt_session = PromptSession(history=FileHistory(global_config_dir / "mini_task_history.txt"))
|
|
@@ -83,7 +84,7 @@ def main(
|
|
|
83
84
|
yolo: bool = typer.Option(False, "-y", "--yolo", help="Run without confirmation"),
|
|
84
85
|
cost_limit: float | None = typer.Option(None, "-l", "--cost-limit", help="Cost limit. Set to 0 to disable."),
|
|
85
86
|
config_spec: Path = typer.Option(DEFAULT_CONFIG, "-c", "--config", help="Path to config file"),
|
|
86
|
-
output: Path | None = typer.Option(
|
|
87
|
+
output: Path | None = typer.Option(DEFAULT_OUTPUT, "-o", "--output", help="Output trajectory file"),
|
|
87
88
|
exit_immediately: bool = typer.Option(
|
|
88
89
|
False, "--exit-immediately", help="Exit immediately when the agent wants to finish instead of prompting."
|
|
89
90
|
),
|
|
@@ -107,7 +108,7 @@ def main(
|
|
|
107
108
|
config["agent"]["mode"] = "confirm" if not yolo else "yolo"
|
|
108
109
|
if cost_limit:
|
|
109
110
|
config["agent"]["cost_limit"] = cost_limit
|
|
110
|
-
if
|
|
111
|
+
if exit_immediately:
|
|
111
112
|
config["agent"]["confirm_exit"] = False
|
|
112
113
|
model = get_model(model_name, config.get("model", {}))
|
|
113
114
|
env = LocalEnvironment(**config.get("env", {}))
|
minisweagent/run/utils/save.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|