local TTY implementation
frdel committed
Aug 7, 2025 at 13:32 UTC
97575a2d8b1b318df759639f885e3079a8983fca
11 files changed
+234
-219
agent.py
-12
@@ -221,18 +221,6 @@ class AgentConfig:
221
profile: str = ""
222
memory_subdir: str = ""
223
knowledge_subdirs: list[str] = field(default_factory=lambda: ["default", "custom"])
224
- code_exec_docker_enabled: bool = False
225
- code_exec_docker_name: str = "A0-dev"
226
- code_exec_docker_image: str = "agent0ai/agent-zero-run:development"
227
- code_exec_docker_ports: dict[str, int] = field(
228
- default_factory=lambda: {"22/tcp": 55022, "80/tcp": 55080}
229
- )
230
- code_exec_docker_volumes: dict[str, dict[str, str]] = field(
231
- default_factory=lambda: {
232
- files.get_base_dir(): {"bind": "/a0", "mode": "rw"},
233
- files.get_abs_path("work_dir"): {"bind": "/root", "mode": "rw"},
234
- }
235
- )
224
code_exec_ssh_enabled: bool = True
225
code_exec_ssh_addr: str = "localhost"
226
code_exec_ssh_port: int = 55022
docker/run/fs/exe/run_A0.sh
+2
-3
@@ -10,9 +10,8 @@ echo "Starting A0..."
10
exec python /a0/run_ui.py \
11
--dockerized=true \
12
--port=80 \
13
- --host="0.0.0.0" \
14
- --code_exec_docker_enabled=false \
15
- --code_exec_ssh_enabled=true \
13
+ --host="0.0.0.0"
14
+ # --code_exec_ssh_enabled=true \
15
# --code_exec_ssh_addr="localhost" \
16
# --code_exec_ssh_port=22 \
17
# --code_exec_ssh_user="root" \
initialize.py
+1
-29
@@ -79,19 +79,7 @@ def initialize_agent():
79
memory_subdir=current_settings["agent_memory_subdir"],
80
knowledge_subdirs=[current_settings["agent_knowledge_subdir"], "default"],
81
mcp_servers=current_settings["mcp_servers"],
82
- code_exec_docker_enabled=False,
83
- # code_exec_docker_name = "A0-dev",
84
- # code_exec_docker_image = "agent0ai/agent-zero:development",
85
- # code_exec_docker_ports = { "22/tcp": 55022, "80/tcp": 55080 }
86
- # code_exec_docker_volumes = {
87
- # files.get_base_dir(): {"bind": "/a0", "mode": "rw"},
88
- # files.get_abs_path("work_dir"): {"bind": "/root", "mode": "rw"},
89
- # },
90
- # code_exec_ssh_enabled = True,
91
- # code_exec_ssh_addr = "localhost",
92
- # code_exec_ssh_port = 55022,
93
- # code_exec_ssh_user = "root",
94
- # code_exec_ssh_pass = "",
82
+ # code_exec params get initialized in _set_runtime_config
83
# additional = {},
84
)
85
@@ -176,19 +164,3 @@ def _set_runtime_config(config: AgentConfig, set: settings.Settings):
164
for key, value in ssh_conf.items():
165
if hasattr(config, key):
166
setattr(config, key, value)
179
-
180
- # if config.code_exec_docker_enabled:
181
- # config.code_exec_docker_ports["22/tcp"] = ssh_conf["code_exec_ssh_port"]
182
- # config.code_exec_docker_ports["80/tcp"] = ssh_conf["code_exec_http_port"]
183
- # config.code_exec_docker_name = f"{config.code_exec_docker_name}-{ssh_conf['code_exec_ssh_port']}-{ssh_conf['code_exec_http_port']}"
184
-
185
- # dman = docker.DockerContainerManager(
186
- # logger=log.Log(),
187
- # name=config.code_exec_docker_name,
188
- # image=config.code_exec_docker_image,
189
- # ports=config.code_exec_docker_ports,
190
- # volumes=config.code_exec_docker_volumes,
191
- # )
192
- # dman.start_container()
193
-
194
- # config.code_exec_ssh_pass = asyncio.run(rfc_exchange.get_root_password())
python/api/rfc.py
+4
@@ -8,6 +8,10 @@ class RFC(ApiHandler):
8
def requires_csrf(cls) -> bool:
9
return False
10
11
+ @classmethod
12
+ def requires_auth(cls) -> bool:
13
+ return False
14
+
15
async def process(self, input: dict, request: Request) -> dict | Response:
16
result = await runtime.handle_rfc(input) # type: ignore
17
return result
python/helpers/settings.py
+58
-11
@@ -71,7 +71,6 @@ class Settings(TypedDict):
71
memory_memorize_consolidation: bool
72
memory_memorize_replace_threshold: float
73
74
-
74
api_keys: dict[str, str]
75
76
auth_login: str
@@ -113,7 +112,15 @@ class SettingsField(TypedDict, total=False):
112
title: str
113
description: str
114
type: Literal[
116
- "text", "number", "select", "range", "textarea", "password", "switch", "button", "html"
115
+ "text",
116
+ "number",
117
+ "select",
118
+ "range",
119
+ "textarea",
120
+ "password",
121
+ "switch",
122
+ "button",
123
+ "html",
124
]
125
value: Any
126
min: float
@@ -141,7 +148,6 @@ SETTINGS_FILE = files.get_abs_path("tmp/settings.json")
148
_settings: Settings | None = None
149
150
144
-
151
def convert_out(settings: Settings) -> SettingsOutput:
152
default_settings = get_default_settings()
153
@@ -478,7 +484,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
484
"type": "number",
485
"value": settings["browser_model_rl_output"],
486
}
481
- )
487
+ )
488
489
browser_model_fields.append(
490
{
@@ -498,7 +504,6 @@ def convert_out(settings: Settings) -> SettingsOutput:
504
"tab": "agent",
505
}
506
501
-
507
# basic auth section
508
auth_fields: list[SettingsField] = []
509
@@ -599,7 +604,8 @@ def convert_out(settings: Settings) -> SettingsOutput:
604
"value": settings["agent_profile"],
605
"options": [
606
{"value": subdir, "label": subdir}
602
- for subdir in files.get_subdirectories("agents") if subdir != "_example"
607
+ for subdir in files.get_subdirectories("agents")
608
+ if subdir != "_example"
609
],
610
}
611
)
@@ -626,7 +632,6 @@ def convert_out(settings: Settings) -> SettingsOutput:
632
"tab": "agent",
633
}
634
629
-
635
memory_fields: list[SettingsField] = []
636
637
memory_fields.append(
@@ -856,6 +861,46 @@ def convert_out(settings: Settings) -> SettingsOutput:
861
"tab": "developer",
862
}
863
864
+ # code_exec_fields: list[SettingsField] = []
865
+
866
+ # code_exec_fields.append(
867
+ # {
868
+ # "id": "code_exec_ssh_enabled",
869
+ # "title": "Use SSH for code execution",
870
+ # "description": "Code execution will use SSH to connect to the terminal. When disabled, a local python terminal interface is used instead. SSH should only be used in development environment or when encountering issues with the local python terminal interface.",
871
+ # "type": "switch",
872
+ # "value": settings["code_exec_ssh_enabled"],
873
+ # }
874
+ # )
875
+
876
+ # code_exec_fields.append(
877
+ # {
878
+ # "id": "code_exec_ssh_addr",
879
+ # "title": "Code execution SSH address",
880
+ # "description": "Address of the SSH server for code execution. Only applies when SSH is enabled.",
881
+ # "type": "text",
882
+ # "value": settings["code_exec_ssh_addr"],
883
+ # }
884
+ # )
885
+
886
+ # code_exec_fields.append(
887
+ # {
888
+ # "id": "code_exec_ssh_port",
889
+ # "title": "Code execution SSH port",
890
+ # "description": "Port of the SSH server for code execution. Only applies when SSH is enabled.",
891
+ # "type": "text",
892
+ # "value": settings["code_exec_ssh_port"],
893
+ # }
894
+ # )
895
+
896
+ # code_exec_section: SettingsSection = {
897
+ # "id": "code_exec",
898
+ # "title": "Code execution",
899
+ # "description": "Configuration of code execution by the agent.",
900
+ # "fields": code_exec_fields,
901
+ # "tab": "developer",
902
+ # }
903
+
904
# Speech to text section
905
stt_fields: list[SettingsField] = []
906
@@ -1085,6 +1130,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
1130
mcp_server_section,
1131
backup_section,
1132
dev_section,
1133
+ # code_exec_section,
1134
]
1135
}
1136
return result
@@ -1147,7 +1193,7 @@ def normalize_settings(settings: Settings) -> Settings:
1193
# adjust settings values to match current version if needed
1194
if "version" not in copy or copy["version"] != default["version"]:
1195
_adjust_to_version(copy, default)
1150
- copy["version"] = default["version"] # sync version
1196
+ copy["version"] = default["version"] # sync version
1197
1198
# remove keys that are not in default
1199
keys_to_remove = [key for key in copy if key not in default]
@@ -1162,7 +1208,7 @@ def normalize_settings(settings: Settings) -> Settings:
1208
try:
1209
copy[key] = type(value)(copy[key]) # type: ignore
1210
if isinstance(copy[key], str):
1165
- copy[key] = copy[key].strip() # strip strings
1211
+ copy[key] = copy[key].strip() # strip strings
1212
except (ValueError, TypeError):
1213
copy[key] = value # make default instead
1214
@@ -1179,6 +1225,7 @@ def _adjust_to_version(settings: Settings, default: Settings):
1225
if "agent_profile" not in settings or settings["agent_profile"] == "default":
1226
settings["agent_profile"] = "agent0"
1227
1228
+
1229
def _read_settings_file() -> Settings | None:
1230
if os.path.exists(SETTINGS_FILE):
1231
content = files.read_file(SETTINGS_FILE)
@@ -1441,9 +1488,9 @@ def set_root_password(password: str):
1488
def get_runtime_config(set: Settings):
1489
if runtime.is_dockerized():
1490
return {
1491
+ "code_exec_ssh_enabled": False,
1492
"code_exec_ssh_addr": "localhost",
1493
"code_exec_ssh_port": 22,
1446
- "code_exec_http_port": 80,
1494
"code_exec_ssh_user": "root",
1495
}
1496
else:
@@ -1455,9 +1502,9 @@ def get_runtime_config(set: Settings):
1502
if host.endswith("/"):
1503
host = host[:-1]
1504
return {
1505
+ "code_exec_ssh_enabled": True,
1506
"code_exec_ssh_addr": host,
1507
"code_exec_ssh_port": set["rfc_port_ssh"],
1460
- "code_exec_http_port": set["rfc_port_http"],
1508
"code_exec_ssh_user": "root",
1509
}
1510
python/helpers/shell_local.py
+27
-49
@@ -3,70 +3,48 @@ import subprocess
3
import time
4
import sys
5
from typing import Optional, Tuple
6
+from python.helpers import tty_session
7
+from python.helpers.shell_ssh import clean_string
8
9
class LocalInteractiveSession:
10
def __init__(self):
9
- self.process = None
11
+ self.session: tty_session.TTYSession|None = None
12
self.full_output = ''
13
14
async def connect(self):
13
- # Start a new subprocess with the appropriate shell for the OS
14
- if sys.platform.startswith('win'):
15
- # Windows
16
- self.process = subprocess.Popen(
17
- ['cmd.exe'],
18
- stdin=subprocess.PIPE,
19
- stdout=subprocess.PIPE,
20
- stderr=subprocess.PIPE,
21
- text=True,
22
- bufsize=1
23
- )
24
- else:
25
- # macOS and Linux
26
- self.process = subprocess.Popen(
27
- ['/bin/bash'],
28
- stdin=subprocess.PIPE,
29
- stdout=subprocess.PIPE,
30
- stderr=subprocess.PIPE,
31
- text=True,
32
- bufsize=1
33
- )
15
+ self.session = tty_session.TTYSession("/bin/bash")
16
+ await self.session.start()
17
+ await self.session.read_full_until_idle(idle_timeout=1, total_timeout=1)
18
35
- def close(self):
36
- if self.process:
37
- self.process.terminate()
38
- self.process.wait()
19
+ async def close(self):
20
+ if self.session:
21
+ self.session.kill()
22
+ # self.session.wait()
23
40
- def send_command(self, command: str):
41
- if not self.process:
24
+ async def send_command(self, command: str):
25
+ if not self.session:
26
raise Exception("Shell not connected")
27
self.full_output = ""
44
- self.process.stdin.write(command + '\n') # type: ignore
45
- self.process.stdin.flush() # type: ignore
28
+ await self.session.sendline(command)
29
30
async def read_output(self, timeout: float = 0, reset_full_output: bool = False) -> Tuple[str, Optional[str]]:
48
- if not self.process:
31
+ if not self.session:
32
raise Exception("Shell not connected")
33
34
if reset_full_output:
35
self.full_output = ""
53
- partial_output = ''
54
- start_time = time.time()
55
-
56
- while (timeout <= 0 or time.time() - start_time < timeout):
57
- rlist, _, _ = select.select([self.process.stdout], [], [], 0.1)
58
- if rlist:
59
- line = self.process.stdout.readline() # type: ignore
60
- if line:
61
- partial_output += line
62
- self.full_output += line
63
- time.sleep(0.1)
64
- else:
65
- break # No more output
66
- else:
67
- break # No data available
36
+
37
+ # get output from terminal
38
+ partial_output = await self.session.read_full_until_idle(idle_timeout=0.01, total_timeout=timeout)
39
+ self.full_output += partial_output
40
+
41
+ # clean output
42
+ partial_output = clean_string(partial_output)
43
+ clean_full_output = clean_string(self.full_output)
44
+
45
+ print("\n\n"+self.full_output.encode("unicode_escape").decode("ascii")+"\n\n")
46
+
47
48
if not partial_output:
70
- return self.full_output, None
71
-
72
- return self.full_output, partial_output
\ No newline at end of file
49
+ return clean_full_output, None
50
+ return clean_full_output, partial_output
\ No newline at end of file
python/helpers/shell_ssh.py
+52
-33
@@ -28,11 +28,20 @@ class SSHInteractiveSession:
28
self.last_command = b""
29
self.trimmed_command_length = 0 # Initialize trimmed_command_length
30
31
- async def connect(self):
32
- # try 3 times with wait and then except
31
+ async def connect(self, keepalive_interval: int = 5):
32
+ """
33
+ Establish the SSH connection and start an interactive shell.
34
+
35
+ Parameters
36
+ ----------
37
+ keepalive_interval : int
38
+ Interval in **seconds** between keep-alive packets sent by Paramiko.
39
+ A value ≤ 0 disables Paramiko’s keep-alive feature.
40
+ """
41
errors = 0
42
while True:
43
try:
44
+ # --- establish TCP/SSH session ---------------------------------
45
self.client.connect(
46
self.hostname,
47
self.port,
@@ -41,16 +50,25 @@ class SSHInteractiveSession:
50
allow_agent=False,
51
look_for_keys=False,
52
)
53
+
54
+ # --------- NEW: enable transport-level keep-alives -------------
55
+ transport = self.client.get_transport()
56
+ if transport and keepalive_interval > 0:
57
+ # sends an SSH_MSG_IGNORE every <keepalive_interval> seconds
58
+ transport.set_keepalive(keepalive_interval)
59
+ # ----------------------------------------------------------------
60
+
61
+ # invoke interactive shell
62
self.shell = self.client.invoke_shell(width=100, height=50)
45
- # self.shell.send(f'PS1="{SSHInteractiveSession.ps1_label}"'.encode())
46
- # return
47
- self.shell.send("stty -echo\n".encode()) # disable shell echo
63
+ self.shell.send("stty -echo\n".encode()) # disable local echo
64
49
- while True: # wait for end of initial output
65
+ # wait for initial prompt/output to settle
66
+ while True:
67
full, part = await self.read_output()
68
if full and not part:
69
return
70
time.sleep(0.1)
71
+
72
except Exception as e:
73
errors += 1
74
if errors < 3:
@@ -60,18 +78,17 @@ class SSHInteractiveSession:
78
content=f"SSH Connection attempt {errors}...",
79
temp=True,
80
)
63
-
81
time.sleep(5)
82
else:
83
raise e
84
68
- def close(self):
85
+ async def close(self):
86
if self.shell:
87
self.shell.close()
88
if self.client:
89
self.client.close()
90
74
- def send_command(self, command: str):
91
+ async def send_command(self, command: str):
92
if not self.shell:
93
raise Exception("Shell not connected")
94
self.full_output = b""
@@ -137,8 +154,8 @@ class SSHInteractiveSession:
154
decoded_partial_output = partial_output.decode("utf-8", errors="replace")
155
decoded_full_output = self.full_output.decode("utf-8", errors="replace")
156
140
- decoded_partial_output = self.clean_string(decoded_partial_output)
141
- decoded_full_output = self.clean_string(decoded_full_output)
157
+ decoded_partial_output = clean_string(decoded_partial_output)
158
+ decoded_full_output = clean_string(decoded_full_output)
159
160
return decoded_full_output, decoded_partial_output
161
@@ -190,32 +207,34 @@ class SSHInteractiveSession:
207
208
return data
209
193
- def clean_string(self, input_string):
194
- # Remove ANSI escape codes
195
- ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
196
- cleaned = ansi_escape.sub("", input_string)
210
+def clean_string(input_string):
211
+ # Remove ANSI escape codes
212
+ ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
213
+ cleaned = ansi_escape.sub("", input_string)
214
198
- # remove null bytes
199
- cleaned = cleaned.replace("\x00", "")
215
+ # remove null bytes
216
+ cleaned = cleaned.replace("\x00", "")
217
201
- # remove ipython \r\r\n> sequences from the start
202
- cleaned = re.sub(r'^[ \r]*(?:\r*\n>[ \r]*)*', '', cleaned)
218
+ # remove ipython \r\r\n> sequences from the start
219
+ cleaned = re.sub(r'^[ \r]*(?:\r*\n>[ \r]*)*', '', cleaned)
220
+ # also remove any amount of '> ' sequences from the start
221
+ cleaned = re.sub(r'^(>\s*)+', '', cleaned)
222
204
- # Replace '\r\n' with '\n'
205
- cleaned = cleaned.replace("\r\n", "\n")
223
+ # Replace '\r\n' with '\n'
224
+ cleaned = cleaned.replace("\r\n", "\n")
225
207
- # remove leading \r and spaces
208
- cleaned = cleaned.lstrip("\r ")
226
+ # remove leading \r and spaces
227
+ cleaned = cleaned.lstrip("\r ")
228
210
- # Split the string by newline characters to process each segment separately
211
- lines = cleaned.split("\n")
229
+ # Split the string by newline characters to process each segment separately
230
+ lines = cleaned.split("\n")
231
213
- for i in range(len(lines)):
214
- # Handle carriage returns '\r' by splitting and taking the last part
215
- parts = [part for part in lines[i].split("\r") if part.strip()]
216
- if parts:
217
- lines[i] = parts[
218
- -1
219
- ].rstrip() # Overwrite with the last part after the last '\r'
232
+ for i in range(len(lines)):
233
+ # Handle carriage returns '\r' by splitting and taking the last part
234
+ parts = [part for part in lines[i].split("\r") if part.strip()]
235
+ if parts:
236
+ lines[i] = parts[
237
+ -1
238
+ ].rstrip() # Overwrite with the last part after the last '\r'
239
221
- return "\n".join(lines)
240
+ return "\n".join(lines)
python/helpers/tty_session.py
+28
-4
@@ -15,9 +15,7 @@ sys.stdout.reconfigure(errors="replace") # type: ignore
15
16
17
class TTYSession:
18
- def __init__(
19
- self, cmd, *, cwd=None, env=None, encoding="utf-8", echo=False
20
- ): # ← NEW kw-arg `echo`
18
+ def __init__(self, cmd, *, cwd=None, env=None, encoding="utf-8", echo=False):
19
self.cmd = cmd if isinstance(cmd, str) else " ".join(cmd)
20
self.cwd = cwd
21
self.env = env or os.environ.copy()
@@ -26,6 +24,17 @@ class TTYSession:
24
self._proc = None
25
self._buf = asyncio.Queue()
26
27
+ def __del__(self):
28
+ # Simple cleanup on object destruction
29
+ import nest_asyncio
30
+
31
+ nest_asyncio.apply()
32
+ if hasattr(self, "close"):
33
+ try:
34
+ asyncio.run(self.close())
35
+ except Exception:
36
+ pass
37
+
38
# ── user-facing coroutines ────────────────────────────────────────
39
async def start(self):
40
if _IS_WIN:
@@ -36,7 +45,22 @@ class TTYSession:
45
self._proc = await _spawn_posix_pty(
46
self.cmd, self.cwd, self.env, self.echo
47
) # ← pass echo
39
- asyncio.create_task(self._pump_stdout())
48
+ self._pump_task = asyncio.create_task(self._pump_stdout())
49
+
50
+ async def close(self):
51
+ # Cancel the pump task if it exists
52
+ if hasattr(self, "_pump_task") and self._pump_task:
53
+ self._pump_task.cancel()
54
+ try:
55
+ await self._pump_task
56
+ except asyncio.CancelledError:
57
+ pass
58
+ # Terminate the process if it exists
59
+ if self._proc:
60
+ self._proc.terminate()
61
+ await self._proc.wait()
62
+ self._proc = None
63
+ self._pump_task = None
64
65
async def send(self, data: str | bytes):
66
if self._proc is None:
python/tools/code_execution_tool.py
+56
-78
@@ -16,7 +16,6 @@ import re
16
@dataclass
17
class State:
18
shells: dict[int, LocalInteractiveSession | SSHInteractiveSession]
19
- docker: DockerContainerManager | None
19
20
21
class CodeExecution(Tool):
@@ -25,10 +24,6 @@ class CodeExecution(Tool):
24
25
await self.agent.handle_intervention() # wait for intervention and handle it, if paused
26
28
- await self.prepare_state()
29
-
30
- # os.chdir(files.get_abs_path("./work_dir")) #change CWD to work_dir
31
-
27
runtime = self.args.get("runtime", "").lower().strip()
28
session = int(self.args.get("session", 0))
29
@@ -80,59 +75,48 @@ class CodeExecution(Tool):
75
async def after_execution(self, response, **kwargs):
76
self.agent.hist_add_tool_result(self.name, response.message)
77
83
- async def prepare_state(self, reset=False, session=None):
84
- self.state = self.agent.get_data("_cet_state")
85
- if not self.state or reset:
86
-
87
- # initialize docker container if execution in docker is configured
88
- if not self.state and self.agent.config.code_exec_docker_enabled:
89
- docker = DockerContainerManager(
90
- logger=self.agent.context.log,
91
- name=self.agent.config.code_exec_docker_name,
92
- image=self.agent.config.code_exec_docker_image,
93
- ports=self.agent.config.code_exec_docker_ports,
94
- volumes=self.agent.config.code_exec_docker_volumes,
78
+ async def prepare_state(self, reset=False, session: int | None = None):
79
+ self.state: State | None = self.agent.get_data("_cet_state")
80
+ if not self.state:
81
+ # initialize shells dictionary if not exists
82
+ shells: dict[int, LocalInteractiveSession | SSHInteractiveSession] = {}
83
+ else:
84
+ shells = self.state.shells.copy()
85
+
86
+ # Only reset the specified session if provided
87
+ if reset and session is not None and session in shells:
88
+ await shells[session].close()
89
+ del shells[session]
90
+ elif reset and not session:
91
+ # Close all sessions if full reset requested
92
+ for s in list(shells.keys()):
93
+ await shells[s].close()
94
+ shells = {}
95
+
96
+ # initialize local or remote interactive shell interface for session 0 if needed
97
+ if session is not None and session not in shells:
98
+ if self.agent.config.code_exec_ssh_enabled:
99
+ pswd = (
100
+ self.agent.config.code_exec_ssh_pass
101
+ if self.agent.config.code_exec_ssh_pass
102
+ else await rfc_exchange.get_root_password()
103
+ )
104
+ shell = SSHInteractiveSession(
105
+ self.agent.context.log,
106
+ self.agent.config.code_exec_ssh_addr,
107
+ self.agent.config.code_exec_ssh_port,
108
+ self.agent.config.code_exec_ssh_user,
109
+ pswd,
110
)
96
- docker.start_container()
111
else:
98
- docker = self.state.docker if self.state else None
99
-
100
- # initialize shells dictionary if not exists
101
- shells = {} if not self.state else self.state.shells.copy()
102
-
103
- # Only reset the specified session if provided
104
- if session is not None and session in shells:
105
- shells[session].close()
106
- del shells[session]
107
- elif reset and not session:
108
- # Close all sessions if full reset requested
109
- for s in list(shells.keys()):
110
- shells[s].close()
111
- shells = {}
112
-
113
- # initialize local or remote interactive shell interface for session 0 if needed
114
- if 0 not in shells:
115
- if self.agent.config.code_exec_ssh_enabled:
116
- pswd = (
117
- self.agent.config.code_exec_ssh_pass
118
- if self.agent.config.code_exec_ssh_pass
119
- else await rfc_exchange.get_root_password()
120
- )
121
- shell = SSHInteractiveSession(
122
- self.agent.context.log,
123
- self.agent.config.code_exec_ssh_addr,
124
- self.agent.config.code_exec_ssh_port,
125
- self.agent.config.code_exec_ssh_user,
126
- pswd,
127
- )
128
- else:
129
- shell = LocalInteractiveSession()
112
+ shell = LocalInteractiveSession()
113
131
- shells[0] = shell
132
- await shell.connect()
114
+ shells[session] = shell
115
+ await shell.connect()
116
134
- self.state = State(shells=shells, docker=docker)
117
+ self.state = State(shells=shells)
118
self.agent.set_data("_cet_state", self.state)
119
+ return self.state
120
121
async def execute_python_code(self, session: int, code: str, reset: bool = False):
122
escaped_code = shlex.quote(code)
@@ -156,45 +140,35 @@ class CodeExecution(Tool):
140
self, session: int, command: str, reset: bool = False, prefix: str = ""
141
):
142
143
+ self.state = await self.prepare_state(reset=reset, session=session)
144
+
145
await self.agent.handle_intervention() # wait for intervention and handle it, if paused
146
# try again on lost connection
147
for i in range(2):
148
try:
149
164
- if reset:
165
- await self.reset_terminal()
166
-
167
- if session not in self.state.shells:
168
- if self.agent.config.code_exec_ssh_enabled:
169
- pswd = (
170
- self.agent.config.code_exec_ssh_pass
171
- if self.agent.config.code_exec_ssh_pass
172
- else await rfc_exchange.get_root_password()
173
- )
174
- shell = SSHInteractiveSession(
175
- self.agent.context.log,
176
- self.agent.config.code_exec_ssh_addr,
177
- self.agent.config.code_exec_ssh_port,
178
- self.agent.config.code_exec_ssh_user,
179
- pswd,
180
- )
181
- else:
182
- shell = LocalInteractiveSession()
183
- self.state.shells[session] = shell
184
- await shell.connect()
185
-
186
- self.state.shells[session].send_command(command)
150
+ await self.state.shells[session].send_command(command)
151
+
152
+ locl = (
153
+ " (local)"
154
+ if isinstance(self.state.shells[session], LocalInteractiveSession)
155
+ else (
156
+ " (remote)"
157
+ if isinstance(self.state.shells[session], SSHInteractiveSession)
158
+ else " (unknown)"
159
+ )
160
+ )
161
162
PrintStyle(
163
background_color="white", font_color="#1B4F72", bold=True
190
- ).print(f"{self.agent.agent_name} code execution output")
164
+ ).print(f"{self.agent.agent_name} code execution output{locl}")
165
return await self.get_terminal_output(session=session, prefix=prefix)
166
167
except Exception as e:
168
if i == 1:
169
# try again on lost connection
170
PrintStyle.error(str(e))
197
- await self.prepare_state(reset=True)
171
+ await self.prepare_state(reset=True, session=session)
172
continue
173
else:
174
raise e
@@ -221,6 +195,10 @@ class CodeExecution(Tool):
195
sleep_time=0.1,
196
prefix="",
197
):
198
+
199
+ # if not self.state:
200
+ self.state = await self.prepare_state(session=session)
201
+
202
# Common shell prompt regex patterns (add more as needed)
203
prompt_patterns = [
204
re.compile(r"\\(venv\\).+[$#] ?$"), # (venv) ...$ or (venv) ...#
webui/components/messages/action-buttons/simple-action-buttons.js
+5
@@ -9,6 +9,11 @@ function getTextContent(element) {
9
for (const child of element.children) {
10
// Skip action buttons
11
if (child.classList.contains("action-buttons")) continue;
12
+ // If the child is an image, copy its src URL
13
+ if (child.tagName && child.tagName.toLowerCase() === "img") {
14
+ if (child.src) textParts.push(child.src);
15
+ continue;
16
+ }
17
// Get text content from the child
18
const text = child.innerText || "";
19
if (text.trim()) {
webui/public/code_exec.svg
new
+1
@@ -0,0 +1 @@
1
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M192-212q-26 0-43-17t-17-43v-416q0-26 17-43t43-17h576q26 0 43 17t17 43v416q0 26-17 43t-43 17H192Zm0-28h576q12 0 22-10t10-22v-368H160v368q0 12 10 22t22 10Zm108-77-19-19 103-104-104-104 20-19 123 123-123 123Zm206 11v-28h188v28H506Z"/></svg>
\ No newline at end of file