refactor: Extract code execution tool to plugin

linuztx committed Mar 8, 2026 at 09:52 UTC 63651deb9ec921c8b9327fe4ea6394c0764bcb91
27 files changed +416 -181
agent.py
-5
@@ -319,11 +319,6 @@ class AgentConfig:
319 browser_http_headers: dict[str, str] = field(
320 default_factory=dict
321 ) # Custom HTTP headers for browser requests
322 - code_exec_ssh_enabled: bool = True
323 - code_exec_ssh_addr: str = "localhost"
324 - code_exec_ssh_port: int = 55022
325 - code_exec_ssh_user: str = "root"
326 - code_exec_ssh_pass: str = ""
322 additional: Dict[str, Any] = field(default_factory=dict)
323
324
docker/run/fs/exe/run_A0.sh
-5
@@ -11,8 +11,3 @@ exec python /a0/run_ui.py \
11 --dockerized=true \
12 --port=80 \
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" \
18 - # --code_exec_ssh_pass="toor"
docker/run/fs/exe/run_tunnel_api.sh
+1 -7
@@ -15,10 +15,4 @@ exec python /a0/run_tunnel.py \
15 --dockerized=true \
16 --port=80 \
17 --tunnel_api_port=55520 \
18 - --host="0.0.0.0" \
19 - --code_exec_docker_enabled=false \
20 - --code_exec_ssh_enabled=true \
21 - # --code_exec_ssh_addr="localhost" \
22 - # --code_exec_ssh_port=22 \
23 - # --code_exec_ssh_user="root" \
24 - # --code_exec_ssh_pass="toor"
18 + --host="0.0.0.0"
helpers/settings.py
+3 -28
@@ -112,9 +112,7 @@ class Settings(TypedDict):
112 rfc_url: str
113 rfc_password: str
114 rfc_port_http: int
115 - rfc_port_ssh: int
115
117 - shell_interface: Literal['local','ssh']
116 websocket_server_restart_enabled: bool
117 uvicorn_access_logs_enabled: bool
118
@@ -189,7 +187,6 @@ class ModelProvider(ProvidersFO):
187 class SettingsOutputAdditional(TypedDict):
188 chat_providers: list[ModelProvider]
189 embedding_providers: list[ModelProvider]
192 - shell_interfaces: list[FieldOption]
190 agent_subdirs: list[FieldOption]
191 knowledge_subdirs: list[FieldOption]
192 stt_models: list[FieldOption]
@@ -231,7 +228,6 @@ def convert_out(settings: Settings) -> SettingsOutput:
228 additional = SettingsOutputAdditional(
229 chat_providers=get_providers("chat"),
230 embedding_providers=get_providers("embedding"),
234 - shell_interfaces=[{"value": "local", "label": "Local Python TTY"}, {"value": "ssh", "label": "SSH"}],
231 is_dockerized=runtime.is_dockerized(),
232 agent_subdirs=[{"value": item["key"], "label": item["label"]}
233 for item in subagents.get_all_agents_list()
@@ -269,7 +265,6 @@ def convert_out(settings: Settings) -> SettingsOutput:
265 additional["chat_providers"] = _ensure_option_present(additional.get("chat_providers"), current.get("util_model_provider"))
266 additional["chat_providers"] = _ensure_option_present(additional.get("chat_providers"), current.get("browser_model_provider"))
267 additional["embedding_providers"] = _ensure_option_present(additional.get("embedding_providers"), current.get("embed_model_provider"))
272 - additional["shell_interfaces"] = _ensure_option_present(additional.get("shell_interfaces"), current.get("shell_interface"))
268 additional["agent_subdirs"] = _ensure_option_present(additional.get("agent_subdirs"), current.get("agent_profile"))
269 additional["knowledge_subdirs"] = _ensure_option_present(additional.get("knowledge_subdirs"), current.get("agent_knowledge_subdir"))
270 additional["stt_models"] = _ensure_option_present(additional.get("stt_models"), current.get("stt_model_size"))
@@ -549,8 +544,6 @@ def get_default_settings() -> Settings:
544 rfc_url=get_default_value("rfc_url", "localhost"),
545 rfc_password="",
546 rfc_port_http=get_default_value("rfc_port_http", 55080),
552 - rfc_port_ssh=get_default_value("rfc_port_ssh", 55022),
553 - shell_interface=get_default_value("shell_interface", "local" if runtime.is_dockerized() else "ssh"),
547 websocket_server_restart_enabled=get_default_value("websocket_server_restart_enabled", True),
548 uvicorn_access_logs_enabled=get_default_value("uvicorn_access_logs_enabled", False),
549 stt_model_size=get_default_value("stt_model_size", "base"),
@@ -750,27 +743,9 @@ def set_root_password(password: str):
743
744
745 def get_runtime_config(set: Settings):
753 - if runtime.is_dockerized():
754 - return {
755 - "code_exec_ssh_enabled": set["shell_interface"] == "ssh",
756 - "code_exec_ssh_addr": "localhost",
757 - "code_exec_ssh_port": 22,
758 - "code_exec_ssh_user": "root",
759 - }
760 - else:
761 - host = set["rfc_url"]
762 - if "//" in host:
763 - host = host.split("//")[1]
764 - if ":" in host:
765 - host, port = host.split(":")
766 - if host.endswith("/"):
767 - host = host[:-1]
768 - return {
769 - "code_exec_ssh_enabled": set["shell_interface"] == "ssh",
770 - "code_exec_ssh_addr": host,
771 - "code_exec_ssh_port": set["rfc_port_ssh"],
772 - "code_exec_ssh_user": "root",
773 - }
746 + # SSH config is now managed by the code_execution plugin.
747 + # This function is kept for backward compatibility but returns an empty dict.
748 + return {}
749
750
751 def create_auth_token() -> str:
initialize.py
+1 -10
@@ -82,13 +82,8 @@ def initialize_agent(override_settings: dict | None = None):
82 knowledge_subdirs=[current_settings["agent_knowledge_subdir"], "default"],
83 mcp_servers=current_settings["mcp_servers"],
84 browser_http_headers=current_settings["browser_http_headers"],
85 - # code_exec params get initialized in _set_runtime_config
86 - # additional = {},
85 )
86
89 - # update SSH and docker settings
90 - _set_runtime_config(config, current_settings)
91 -
87 # update config with runtime args
88 _args_override(config)
89
@@ -175,8 +170,4 @@ def _args_override(config):
170 setattr(config, key, value)
171
172
178 -def _set_runtime_config(config: AgentConfig, set: settings.Settings):
179 - ssh_conf = settings.get_runtime_config(set)
180 - for key, value in ssh_conf.items():
181 - if hasattr(config, key):
182 - setattr(config, key, value)
173 +
plugins/code_execution/default_config.yaml new
+36
@@ -0,0 +1,36 @@
1 +# Shell interface: false = local TTY, true = SSH
2 +ssh_enabled: false
3 +
4 +# SSH credentials (used when ssh_enabled is true)
5 +ssh_addr: localhost
6 +ssh_port: 22
7 +ssh_user: root
8 +ssh_pass: ""
9 +
10 +# Timeouts for python/nodejs/terminal runtimes (seconds)
11 +code_exec_first_output_timeout: 30
12 +code_exec_between_output_timeout: 15
13 +code_exec_max_exec_timeout: 180
14 +code_exec_dialog_timeout: 5
15 +
16 +# Timeouts for "output" runtime (seconds)
17 +output_first_output_timeout: 90
18 +output_between_output_timeout: 45
19 +output_max_exec_timeout: 300
20 +output_dialog_timeout: 5
21 +
22 +# Shell prompt detection patterns (one regex per line)
23 +# When matched, output is returned immediately without waiting for timeout
24 +prompt_patterns: |
25 + (\(venv\)).+[$#] ?$
26 + root@[^:]+:[^#]+# ?$
27 + [a-zA-Z0-9_.-]+@[^:]+:[^$#]+[$#] ?$
28 + \(?.*\)?\s*PS\s+[^>]+> ?$
29 +
30 +# Dialog detection patterns (one regex per line, case-insensitive)
31 +# When matched after dialog_timeout, control is returned to the agent
32 +dialog_patterns: |
33 + Y/N
34 + yes/no
35 + :\s*$
36 + \?\s*$
plugins/code_execution/extensions/python/system_prompt/_20_code_execution_prompt.py new
+14
@@ -0,0 +1,14 @@
1 +from helpers.extension import Extension
2 +from agent import LoopData
3 +
4 +
5 +class CodeExecutionPrompt(Extension):
6 +
7 + async def execute(
8 + self,
9 + system_prompt: list[str] = [],
10 + loop_data: LoopData = LoopData(),
11 + **kwargs,
12 + ):
13 + prompt = self.agent.read_prompt("agent.system.tool.code_exe.md")
14 + system_prompt.append(prompt)
plugins/code_execution/extensions/webui/get_message_handler/code-exe-handler.js new
+7
@@ -0,0 +1,7 @@
1 +import { drawMessageCodeExe } from "/js/messages.js";
2 +
3 +export default async function registerCodeExeHandler(extData) {
4 + if (extData?.type === "code_exe") {
5 + extData.handler = drawMessageCodeExe;
6 + }
7 +}
plugins/code_execution/helpers/__init__.py
plugins/code_execution/helpers/shell_local.py renamed
+3 -2
@@ -4,8 +4,9 @@ import subprocess
4 import time
5 import sys
6 from typing import Optional, Tuple
7 -from helpers import tty_session, runtime
8 -from helpers.shell_ssh import clean_string
7 +from helpers import runtime
8 +from plugins.code_execution.helpers import tty_session
9 +from plugins.code_execution.helpers.shell_ssh import clean_string
10
11 class LocalInteractiveSession:
12 def __init__(self, cwd: str|None = None):
plugins/code_execution/helpers/shell_ssh.py renamed
plugins/code_execution/helpers/tty_session.py renamed
plugins/code_execution/plugin.yaml new
+8
@@ -0,0 +1,8 @@
1 +name: Code Execution
2 +description: Provides terminal, Python, and Node.js code execution via local TTY or SSH.
3 +version: 1.0.0
4 +always_enabled: true
5 +settings_sections:
6 + - agent
7 +per_project_config: true
8 +per_agent_config: true
plugins/code_execution/prompts/agent.system.tool.code_exe.md renamed
plugins/code_execution/prompts/fw.code.info.md renamed
plugins/code_execution/prompts/fw.code.max_time.md renamed
plugins/code_execution/prompts/fw.code.no_out_time.md renamed
plugins/code_execution/prompts/fw.code.no_output.md renamed
plugins/code_execution/prompts/fw.code.pause_dialog.md renamed
plugins/code_execution/prompts/fw.code.pause_time.md renamed
plugins/code_execution/prompts/fw.code.reset.md renamed
plugins/code_execution/prompts/fw.code.running.md renamed
plugins/code_execution/prompts/fw.code.runtime_wrong.md renamed
plugins/code_execution/tools/code_execution_tool.py renamed
+140 -98
@@ -1,31 +1,93 @@
1 import asyncio
2 from dataclasses import dataclass
3 +import re
4 import shlex
5 import time
6 +
7 from helpers.tool import Tool, Response
6 -from helpers import files, rfc_exchange, projects, runtime, settings
8 +from helpers import files, projects, runtime, settings
9 from helpers.print_style import PrintStyle
8 -from helpers.shell_local import LocalInteractiveSession
9 -from helpers.shell_ssh import SSHInteractiveSession
10 from helpers.strings import truncate_text as truncate_text_string
11 from helpers.messages import truncate_text as truncate_text_agent
12 -import re
12 +from helpers import plugins
13 +
14 +from plugins.code_execution.helpers.shell_local import LocalInteractiveSession
15 +from plugins.code_execution.helpers.shell_ssh import SSHInteractiveSession
16 +
17 +
18 +def _get_config(agent) -> dict:
19 + cfg = plugins.get_plugin_config("code_execution", agent=agent) or {}
20 +
21 + # SSH / TTY switch
22 + ssh_enabled = bool(cfg.get("ssh_enabled", False))
23 +
24 + # SSH credentials
25 + ssh_addr = str(cfg.get("ssh_addr", "localhost"))
26 + ssh_port = int(cfg.get("ssh_port", 22))
27 + ssh_user = str(cfg.get("ssh_user", "root"))
28 + ssh_pass = str(cfg.get("ssh_pass", ""))
29 +
30 + # Timeouts for python/nodejs/terminal runtimes
31 + code_exec_timeouts = {
32 + "first_output_timeout": int(cfg.get("code_exec_first_output_timeout", 30)),
33 + "between_output_timeout": int(cfg.get("code_exec_between_output_timeout", 15)),
34 + "max_exec_timeout": int(cfg.get("code_exec_max_exec_timeout", 180)),
35 + "dialog_timeout": int(cfg.get("code_exec_dialog_timeout", 5)),
36 + }
37 +
38 + # Timeouts for "output" runtime
39 + output_timeouts = {
40 + "first_output_timeout": int(cfg.get("output_first_output_timeout", 90)),
41 + "between_output_timeout": int(cfg.get("output_between_output_timeout", 45)),
42 + "max_exec_timeout": int(cfg.get("output_max_exec_timeout", 300)),
43 + "dialog_timeout": int(cfg.get("output_dialog_timeout", 5)),
44 + }
45 +
46 + # Prompt patterns (one regex per line, or a list)
47 + prompt_patterns_raw = cfg.get(
48 + "prompt_patterns",
49 + r"(\(venv\)).+[$#] ?$" + "\n"
50 + + r"root@[^:]+:[^#]+# ?$" + "\n"
51 + + r"[a-zA-Z0-9_.-]+@[^:]+:[^$#]+[$#] ?$" + "\n"
52 + + r"\(?.*\)?\s*PS\s+[^>]+> ?$",
53 + )
54 + if isinstance(prompt_patterns_raw, list):
55 + prompt_lines = [str(p) for p in prompt_patterns_raw]
56 + else:
57 + prompt_lines = str(prompt_patterns_raw).splitlines()
58 + prompt_patterns = [
59 + re.compile(p.strip())
60 + for p in prompt_lines
61 + if p.strip()
62 + ]
63 +
64 + # Dialog patterns (one regex per line, or a list)
65 + dialog_patterns_raw = cfg.get(
66 + "dialog_patterns",
67 + "Y/N\nyes/no\n:\\s*$\n\\?\\s*$",
68 + )
69 + if isinstance(dialog_patterns_raw, list):
70 + dialog_lines = [str(p) for p in dialog_patterns_raw]
71 + else:
72 + dialog_lines = str(dialog_patterns_raw).splitlines()
73 + dialog_patterns = [
74 + re.compile(p.strip(), re.IGNORECASE)
75 + for p in dialog_lines
76 + if p.strip()
77 + ]
78 +
79 + return {
80 + "ssh_enabled": ssh_enabled,
81 + "ssh_addr": ssh_addr,
82 + "ssh_port": ssh_port,
83 + "ssh_user": ssh_user,
84 + "ssh_pass": ssh_pass,
85 + "code_exec_timeouts": code_exec_timeouts,
86 + "output_timeouts": output_timeouts,
87 + "prompt_patterns": prompt_patterns,
88 + "dialog_patterns": dialog_patterns,
89 + }
90
14 -# Timeouts for python, nodejs, and terminal runtimes.
15 -CODE_EXEC_TIMEOUTS: dict[str, int] = {
16 - "first_output_timeout": 30,
17 - "between_output_timeout": 15,
18 - "max_exec_timeout": 180,
19 - "dialog_timeout": 5,
20 -}
21 -
22 -# Timeouts for output runtime.
23 -OUTPUT_TIMEOUTS: dict[str, int] = {
24 - "first_output_timeout": 90,
25 - "between_output_timeout": 45,
26 - "max_exec_timeout": 300,
27 - "dialog_timeout": 5,
28 -}
91
92 @dataclass
93 class ShellWrap:
@@ -33,6 +95,7 @@ class ShellWrap:
95 session: LocalInteractiveSession | SSHInteractiveSession
96 running: bool
97
98 +
99 @dataclass
100 class State:
101 ssh_enabled: bool
@@ -41,51 +104,37 @@ class State:
104
105 class CodeExecution(Tool):
106
44 - # Common shell prompt regex patterns (add more as needed)
45 - prompt_patterns = [
46 - re.compile(r"\\(venv\\).+[$#] ?$"), # (venv) ...$ or (venv) ...#
47 - re.compile(r"root@[^:]+:[^#]+# ?$"), # root@container:~#
48 - re.compile(r"[a-zA-Z0-9_.-]+@[^:]+:[^$#]+[$#] ?$"), # user@host:~$
49 - re.compile(r"\(?.*\)?\s*PS\s+[^>]+> ?$"), # PowerShell prompt like (base) PS C:\...>
50 - ]
51 - # potential dialog detection
52 - dialog_patterns = [
53 - re.compile(r"Y/N", re.IGNORECASE), # Y/N anywhere in line
54 - re.compile(r"yes/no", re.IGNORECASE), # yes/no anywhere in line
55 - re.compile(r":\s*$"), # line ending with colon
56 - re.compile(r"\?\s*$"), # line ending with question mark
57 - ]
58 -
107 async def execute(self, **kwargs) -> Response:
108
109 await self.agent.handle_intervention() # wait for intervention and handle it, if paused
110
63 - runtime = self.args.get("runtime", "").lower().strip()
111 + runtime_arg = self.args.get("runtime", "").lower().strip()
112 session = int(self.args.get("session", 0))
113 self.allow_running = bool(self.args.get("allow_running", False))
66 - reset = bool(self.args.get("reset", False) or runtime == "reset")
114 + reset = bool(self.args.get("reset", False) or runtime_arg == "reset")
115
68 - if runtime == "python":
116 + if runtime_arg == "python":
117 response = await self.execute_python_code(
118 code=self.args["code"], session=session, reset=reset
119 )
72 - elif runtime == "nodejs":
120 + elif runtime_arg == "nodejs":
121 response = await self.execute_nodejs_code(
122 code=self.args["code"], session=session, reset=reset
123 )
76 - elif runtime == "terminal":
124 + elif runtime_arg == "terminal":
125 response = await self.execute_terminal_command(
126 command=self.args["code"], session=session, reset=reset
127 )
80 - elif runtime == "output":
128 + elif runtime_arg == "output":
129 + cfg = _get_config(self.agent)
130 response = await self.get_terminal_output(
82 - session=session, timeouts=OUTPUT_TIMEOUTS
131 + session=session, timeouts=cfg["output_timeouts"]
132 )
84 - elif runtime == "reset":
133 + elif runtime_arg == "reset":
134 response = await self.reset_terminal(session=session)
135 else:
136 response = self.agent.read_prompt(
88 - "fw.code.runtime_wrong.md", runtime=runtime
137 + "fw.code.runtime_wrong.md", runtime=runtime_arg
138 )
139
140 if not response:
@@ -105,7 +154,6 @@ class CodeExecution(Tool):
154 def get_heading(self, text: str = ""):
155 if not text:
156 text = f"{self.name} - {self.args['runtime'] if 'runtime' in self.args else 'unknown'}"
108 - # text = truncate_text_string(text, 60) # don't truncate here, log.py takes care of it
157 session = self.args.get("session", None)
158 session_text = f"[{session}] " if session or session == 0 else ""
159 return f"icon://terminal {session_text}{text}"
@@ -115,9 +163,11 @@ class CodeExecution(Tool):
163
164 async def prepare_state(self, reset=False, session: int | None = None):
165 self.state: State | None = self.agent.get_data("_cet_state")
166 + cfg = _get_config(self.agent)
167 + ssh_enabled = cfg["ssh_enabled"]
168 +
169 # always reset state when ssh_enabled changes
119 - if not self.state or self.state.ssh_enabled != self.agent.config.code_exec_ssh_enabled:
120 - # initialize shells dictionary if not exists
170 + if not self.state or self.state.ssh_enabled != ssh_enabled:
171 shells: dict[int, ShellWrap] = {}
172 else:
173 shells = self.state.shells.copy()
@@ -132,21 +182,16 @@ class CodeExecution(Tool):
182 await shells[s].session.close()
183 shells = {}
184
135 - # initialize local or remote interactive shell interface for session 0 if needed
185 + # initialize local or remote interactive shell interface for session if needed
186 if session is not None and session not in shells:
187 cwd = await self.ensure_cwd()
138 - if self.agent.config.code_exec_ssh_enabled:
139 - pswd = (
140 - self.agent.config.code_exec_ssh_pass
141 - if self.agent.config.code_exec_ssh_pass
142 - else await rfc_exchange.get_root_password()
143 - )
188 + if ssh_enabled:
189 shell = SSHInteractiveSession(
190 self.agent.context.log,
146 - self.agent.config.code_exec_ssh_addr,
147 - self.agent.config.code_exec_ssh_port,
148 - self.agent.config.code_exec_ssh_user,
149 - pswd,
191 + cfg["ssh_addr"],
192 + cfg["ssh_port"],
193 + cfg["ssh_user"],
194 + cfg["ssh_pass"],
195 cwd=cwd,
196 )
197 else:
@@ -155,7 +200,7 @@ class CodeExecution(Tool):
200 shells[session] = ShellWrap(id=session, session=shell, running=False)
201 await shell.connect()
202
158 - self.state = State(shells=shells, ssh_enabled=self.agent.config.code_exec_ssh_enabled)
203 + self.state = State(shells=shells, ssh_enabled=ssh_enabled)
204 self.agent.set_data("_cet_state", self.state)
205 return self.state
206
@@ -174,13 +219,17 @@ class CodeExecution(Tool):
219 async def execute_terminal_command(
220 self, session: int, command: str, reset: bool = False
221 ):
177 - prefix = ("bash>" if not runtime.is_windows() or self.agent.config.code_exec_ssh_enabled else "PS>") + self.format_command_for_output(command) + "\n\n"
222 + cfg = _get_config(self.agent)
223 + prefix = (
224 + ("bash>" if not runtime.is_windows() or cfg["ssh_enabled"] else "PS>")
225 + + self.format_command_for_output(command)
226 + + "\n\n"
227 + )
228 return await self.terminal_session(session, command, reset, prefix)
229
230 async def terminal_session(
231 self, session: int, command: str, reset: bool = False, prefix: str = "", timeouts: dict | None = None
232 ):
183 -
233 self.state = await self.prepare_state(reset=reset, session=session)
234
235 await self.agent.handle_intervention() # wait for intervention and handle it, if paused
@@ -189,11 +238,12 @@ class CodeExecution(Tool):
238 if not self.allow_running:
239 if response := await self.handle_running_session(session):
240 return response
192 -
241 +
242 + cfg = _get_config(self.agent)
243 +
244 # try again on lost connection
245 for i in range(2):
246 try:
196 -
247 self.state.shells[session].running = True
248 await self.state.shells[session].session.send_command(command)
249
@@ -210,11 +260,14 @@ class CodeExecution(Tool):
260 PrintStyle(
261 background_color="white", font_color="#1B4F72", bold=True
262 ).print(f"{self.agent.agent_name} code execution output{locl}")
213 - return await self.get_terminal_output(session=session, prefix=prefix, timeouts=(timeouts or CODE_EXEC_TIMEOUTS))
263 + return await self.get_terminal_output(
264 + session=session,
265 + prefix=prefix,
266 + timeouts=(timeouts or cfg["code_exec_timeouts"]),
267 + )
268
269 except Exception as e:
270 if i == 1:
217 - # try again on lost connection
271 PrintStyle.error(str(e))
272 await self.prepare_state(reset=True, session=session)
273 continue
@@ -222,13 +275,8 @@ class CodeExecution(Tool):
275 raise e
276
277 def format_command_for_output(self, command: str):
225 - # truncate long commands
278 short_cmd = command[:200]
227 - # normalize whitespace for cleaner output
279 short_cmd = " ".join(short_cmd.split())
229 - # replace any sequence of ', ", or ` with a single '
230 - # short_cmd = re.sub(r"['\"`]+", "'", short_cmd) # no need anymore
231 - # final length
280 short_cmd = truncate_text_string(short_cmd, 100)
281 return f"{short_cmd}"
282
@@ -236,16 +284,14 @@ class CodeExecution(Tool):
284 self,
285 session=0,
286 reset_full_output=True,
239 - first_output_timeout=30, # Wait up to x seconds for first output
240 - between_output_timeout=15, # Wait up to x seconds between outputs
241 - dialog_timeout=5, # potential dialog detection timeout
242 - max_exec_timeout=180, # hard cap on total runtime
287 + first_output_timeout=30,
288 + between_output_timeout=15,
289 + dialog_timeout=5,
290 + max_exec_timeout=180,
291 sleep_time=0.5,
292 prefix="",
293 timeouts: dict | None = None,
294 ):
247 -
248 - # if not self.state:
295 self.state = await self.prepare_state(session=session)
296
297 # Override timeouts if a dict is provided
@@ -255,6 +301,10 @@ class CodeExecution(Tool):
301 dialog_timeout = timeouts.get("dialog_timeout", dialog_timeout)
302 max_exec_timeout = timeouts.get("max_exec_timeout", max_exec_timeout)
303
304 + cfg = _get_config(self.agent)
305 + prompt_patterns = cfg["prompt_patterns"]
306 + dialog_patterns = cfg["dialog_patterns"]
307 +
308 start_time = time.time()
309 last_output_time = start_time
310 full_output = ""
@@ -277,7 +327,6 @@ class CodeExecution(Tool):
327 now = time.time()
328 if partial_output:
329 PrintStyle(font_color="#85C1E9").stream(partial_output)
280 - # full_output += partial_output # Append new output
330 truncated_output = self.fix_full_output(full_output)
331 self.set_progress(truncated_output)
332 heading = self.get_heading_from_output(truncated_output, 0)
@@ -291,7 +340,7 @@ class CodeExecution(Tool):
340 )
341 last_lines.reverse()
342 for idx, line in enumerate(last_lines):
294 - for pat in self.prompt_patterns:
343 + for pat in prompt_patterns:
344 if pat.search(line.strip()):
345 PrintStyle.info(
346 "Detected shell prompt, returning output early."
@@ -343,12 +392,11 @@ class CodeExecution(Tool):
392
393 # potential dialog detection
394 if now - last_output_time > dialog_timeout:
346 - # Check for dialog prompt at the end of output
395 last_lines = (
396 truncated_output.splitlines()[-2:] if truncated_output else []
397 )
398 for line in last_lines:
351 - for pat in self.dialog_patterns:
399 + for pat in dialog_patterns:
400 if pat.search(line.strip()):
401 PrintStyle.info(
402 "Detected dialog prompt, returning output early."
@@ -374,14 +422,18 @@ class CodeExecution(Tool):
422 async def handle_running_session(
423 self,
424 session=0,
377 - reset_full_output=True,
425 + reset_full_output=True,
426 prefix=""
427 ):
428 if not self.state or session not in self.state.shells:
429 return None
430 if not self.state.shells[session].running:
431 return None
384 -
432 +
433 + cfg = _get_config(self.agent)
434 + prompt_patterns = cfg["prompt_patterns"]
435 + dialog_patterns = cfg["dialog_patterns"]
436 +
437 full_output, _ = await self.state.shells[session].session.read_output(
438 timeout=1, reset_full_output=reset_full_output
439 )
@@ -394,7 +446,7 @@ class CodeExecution(Tool):
446 )
447 last_lines.reverse()
448 for idx, line in enumerate(last_lines):
397 - for pat in self.prompt_patterns:
449 + for pat in prompt_patterns:
450 if pat.search(line.strip()):
451 PrintStyle.info(
452 "Detected shell prompt, returning output early."
@@ -402,9 +454,9 @@ class CodeExecution(Tool):
454 self.mark_session_idle(session)
455 return None
456
405 - has_dialog = False
457 + has_dialog = False
458 for line in last_lines:
407 - for pat in self.dialog_patterns:
459 + for pat in dialog_patterns:
460 if pat.search(line.strip()):
461 has_dialog = True
462 break
@@ -412,7 +464,7 @@ class CodeExecution(Tool):
464 break
465
466 if has_dialog:
415 - sys_info = self.agent.read_prompt("fw.code.pause_dialog.md", timeout=1)
467 + sys_info = self.agent.read_prompt("fw.code.pause_dialog.md", timeout=1)
468 else:
469 sys_info = self.agent.read_prompt("fw.code.running.md", session=session)
470
@@ -422,14 +474,12 @@ class CodeExecution(Tool):
474 PrintStyle(font_color="#FFA500", bold=True).print(response)
475 self.log.update(content=prefix + response, heading=heading)
476 return response
425 -
477 +
478 def mark_session_idle(self, session: int = 0):
427 - # Mark session as idle - command finished
479 if self.state and session in self.state.shells:
480 self.state.shells[session].running = False
481
482 async def reset_terminal(self, session=0, reason: str | None = None):
432 - # Print the reason for the reset to the console if provided
483 if reason:
484 PrintStyle(font_color="#FFA500", bold=True).print(
485 f"Resetting terminal session {session}... Reason: {reason}"
@@ -439,7 +489,6 @@ class CodeExecution(Tool):
489 f"Resetting terminal session {session}..."
490 )
491
442 - # Only reset the specified session while preserving others
492 await self.prepare_state(reset=True, session=session)
493 response = self.agent.read_prompt(
494 "fw.code.info.md", info=self.agent.read_prompt("fw.code.reset.md")
@@ -453,9 +502,7 @@ class CodeExecution(Tool):
502 if not output:
503 return self.get_heading() + done_icon
504
456 - # find last non-empty line with skip
505 lines = output.splitlines()
458 - # Start from len(lines) - skip_lines - 1 down to 0
506 for i in range(len(lines) - skip_lines - 1, -1, -1):
507 line = lines[i].strip()
508 if not line:
@@ -465,11 +512,8 @@ class CodeExecution(Tool):
512 return self.get_heading() + done_icon
513
514 def fix_full_output(self, output: str):
468 - # remove any single byte \xXX escapes
515 output = re.sub(r"(?<!\\)\\x[0-9A-Fa-f]{2}", "", output)
470 - # Strip every line of output before truncation
471 - # output = "\n".join(line.strip() for line in output.splitlines())
472 - output = truncate_text_agent(agent=self.agent, output=output, threshold=1000000) # ~1MB, larger outputs should be dumped to file, not read from terminal
516 + output = truncate_text_agent(agent=self.agent, output=output, threshold=1000000)
517 return output
518
519 async def ensure_cwd(self) -> str | None:
@@ -487,9 +531,7 @@ class CodeExecution(Tool):
531 await runtime.call_development_function(make_dir, normalized)
532 return normalized
533
534 +
535 def make_dir(path: str):
536 import os
537 os.makedirs(path, exist_ok=True)
493 -
494 -
495 -
\ No newline at end of file
plugins/code_execution/webui/config.html new
+202
@@ -0,0 +1,202 @@
1 +<html>
2 +<head>
3 + <title>Code Execution</title>
4 +</head>
5 +
6 +<body>
7 + <div x-data>
8 + <template x-if="$store.pluginSettings.settings">
9 + <div>
10 + <div class="section-title">Code Execution</div>
11 + <div class="section-description">
12 + Configuration for the code execution tool. Controls the shell interface, SSH credentials, execution timeouts, and prompt/dialog detection patterns.
13 + </div>
14 +
15 + <!-- Shell Interface -->
16 + <div class="field">
17 + <div class="field-label">
18 + <div class="field-title">Use SSH</div>
19 + <div class="field-description">
20 + When enabled, code execution connects via SSH using the credentials below. When disabled, a local Python TTY is used.
21 + </div>
22 + </div>
23 + <div class="field-control">
24 + <label class="toggle">
25 + <input type="checkbox" x-model="$store.pluginSettings.settings.ssh_enabled" />
26 + <span class="toggler"></span>
27 + </label>
28 + </div>
29 + </div>
30 +
31 + <!-- SSH credentials (shown only when SSH is enabled) -->
32 + <template x-if="$store.pluginSettings.settings.ssh_enabled">
33 + <div>
34 + <div class="field">
35 + <div class="field-label">
36 + <div class="field-title">SSH Address</div>
37 + <div class="field-description">Hostname or IP of the SSH target.</div>
38 + </div>
39 + <div class="field-control">
40 + <input type="text" x-model="$store.pluginSettings.settings.ssh_addr" placeholder="localhost" />
41 + </div>
42 + </div>
43 +
44 + <div class="field">
45 + <div class="field-label">
46 + <div class="field-title">SSH Port</div>
47 + <div class="field-description">SSH port on the target host.</div>
48 + </div>
49 + <div class="field-control">
50 + <input type="number" min="1" max="65535" x-model.number="$store.pluginSettings.settings.ssh_port" />
51 + </div>
52 + </div>
53 +
54 + <div class="field">
55 + <div class="field-label">
56 + <div class="field-title">SSH User</div>
57 + <div class="field-description">Username for SSH login.</div>
58 + </div>
59 + <div class="field-control">
60 + <input type="text" x-model="$store.pluginSettings.settings.ssh_user" placeholder="root" />
61 + </div>
62 + </div>
63 +
64 + <div class="field">
65 + <div class="field-label">
66 + <div class="field-title">SSH Password</div>
67 + <div class="field-description">Password for SSH login.</div>
68 + </div>
69 + <div class="field-control">
70 + <input type="password" autocomplete="off" x-model="$store.pluginSettings.settings.ssh_pass" />
71 + </div>
72 + </div>
73 + </div>
74 + </template>
75 +
76 + <!-- Execution timeouts -->
77 + <div class="section-title">Execution Timeouts</div>
78 + <div class="section-description">
79 + Timeouts (in seconds) for python, nodejs, and terminal runtimes.
80 + </div>
81 +
82 + <div class="field">
83 + <div class="field-label">
84 + <div class="field-title">First output timeout</div>
85 + <div class="field-description">Seconds to wait for the first output before returning control to the agent.</div>
86 + </div>
87 + <div class="field-control">
88 + <input type="number" min="1" x-model.number="$store.pluginSettings.settings.code_exec_first_output_timeout" />
89 + </div>
90 + </div>
91 +
92 + <div class="field">
93 + <div class="field-label">
94 + <div class="field-title">Between output timeout</div>
95 + <div class="field-description">Seconds to wait between output chunks before returning control.</div>
96 + </div>
97 + <div class="field-control">
98 + <input type="number" min="1" x-model.number="$store.pluginSettings.settings.code_exec_between_output_timeout" />
99 + </div>
100 + </div>
101 +
102 + <div class="field">
103 + <div class="field-label">
104 + <div class="field-title">Max execution timeout</div>
105 + <div class="field-description">Hard cap on total execution time in seconds.</div>
106 + </div>
107 + <div class="field-control">
108 + <input type="number" min="1" x-model.number="$store.pluginSettings.settings.code_exec_max_exec_timeout" />
109 + </div>
110 + </div>
111 +
112 + <div class="field">
113 + <div class="field-label">
114 + <div class="field-title">Dialog detection timeout</div>
115 + <div class="field-description">Seconds of idle output before checking for dialog prompts.</div>
116 + </div>
117 + <div class="field-control">
118 + <input type="number" min="1" x-model.number="$store.pluginSettings.settings.code_exec_dialog_timeout" />
119 + </div>
120 + </div>
121 +
122 + <!-- Output runtime timeouts -->
123 + <div class="section-title">Output Runtime Timeouts</div>
124 + <div class="section-description">
125 + Timeouts (in seconds) for the "output" runtime (waiting for long-running processes).
126 + </div>
127 +
128 + <div class="field">
129 + <div class="field-label">
130 + <div class="field-title">First output timeout</div>
131 + <div class="field-description">Seconds to wait for the first output.</div>
132 + </div>
133 + <div class="field-control">
134 + <input type="number" min="1" x-model.number="$store.pluginSettings.settings.output_first_output_timeout" />
135 + </div>
136 + </div>
137 +
138 + <div class="field">
139 + <div class="field-label">
140 + <div class="field-title">Between output timeout</div>
141 + <div class="field-description">Seconds to wait between output chunks.</div>
142 + </div>
143 + <div class="field-control">
144 + <input type="number" min="1" x-model.number="$store.pluginSettings.settings.output_between_output_timeout" />
145 + </div>
146 + </div>
147 +
148 + <div class="field">
149 + <div class="field-label">
150 + <div class="field-title">Max execution timeout</div>
151 + <div class="field-description">Hard cap on total wait time in seconds.</div>
152 + </div>
153 + <div class="field-control">
154 + <input type="number" min="1" x-model.number="$store.pluginSettings.settings.output_max_exec_timeout" />
155 + </div>
156 + </div>
157 +
158 + <div class="field">
159 + <div class="field-label">
160 + <div class="field-title">Dialog detection timeout</div>
161 + <div class="field-description">Seconds of idle output before checking for dialog prompts.</div>
162 + </div>
163 + <div class="field-control">
164 + <input type="number" min="1" x-model.number="$store.pluginSettings.settings.output_dialog_timeout" />
165 + </div>
166 + </div>
167 +
168 + <!-- Patterns -->
169 + <div class="section-title">Detection Patterns</div>
170 + <div class="section-description">
171 + Regular expressions used to detect shell prompts and interactive dialogs. One pattern per line.
172 + </div>
173 +
174 + <div class="field">
175 + <div class="field-label">
176 + <div class="field-title">Prompt patterns</div>
177 + <div class="field-description">
178 + When any pattern matches the last output lines, execution is considered complete and output is returned immediately.
179 + </div>
180 + </div>
181 + <div class="field-control">
182 + <textarea rows="5" x-model="$store.pluginSettings.settings.prompt_patterns"></textarea>
183 + </div>
184 + </div>
185 +
186 + <div class="field">
187 + <div class="field-label">
188 + <div class="field-title">Dialog patterns</div>
189 + <div class="field-description">
190 + When any pattern matches after the dialog timeout, control is returned to the agent to handle the interactive prompt.
191 + </div>
192 + </div>
193 + <div class="field-control">
194 + <textarea rows="4" x-model="$store.pluginSettings.settings.dialog_patterns"></textarea>
195 + </div>
196 + </div>
197 + </div>
198 + </template>
199 + </div>
200 +</body>
201 +
202 +</html>
tools/input.py
+1 -1
@@ -1,6 +1,6 @@
1 from agent import Agent, UserMessage
2 from helpers.tool import Tool, Response
3 -from tools.code_execution_tool import CodeExecution
3 +from plugins.code_execution.tools.code_execution_tool import CodeExecution
4
5
6 class Input(Tool):
webui/components/settings/developer/dev.html
-25
@@ -12,22 +12,6 @@
12 Parameters for A0 framework development. RFCs (remote function calls) are used to call functions on another A0 instance. You can develop and debug A0 natively on your local system while redirecting some functions to A0 instance in docker. This is crucial for development as A0 needs to run in standardized environment to support all features.
13 </div>
14
15 - <div class="field">
16 - <div class="field-label">
17 - <div class="field-title">Shell Interface</div>
18 - <div class="field-description">
19 - Terminal interface used for Code Execution Tool. Local Python TTY works locally in both dockerized and development environments. SSH always connects to dockerized environment (automatically at localhost or RFC host address).
20 - </div>
21 - </div>
22 - <div class="field-control">
23 - <select x-model="$store.settings.settings.shell_interface">
24 - <template x-for="option in $store.settings.additional?.shell_interfaces" :key="option.value">
25 - <option :value="option.value" :selected="option.value === $store.settings.settings.shell_interface" x-text="option.label"></option>
26 - </template>
27 - </select>
28 - </div>
29 - </div>
30 -
15 <template x-if="!$store.settings.additional?.is_dockerized">
16 <div>
17 <div class="field">
@@ -68,15 +52,6 @@
52 </div>
53 </div>
54
71 - <div class="field">
72 - <div class="field-label">
73 - <div class="field-title">RFC SSH port</div>
74 - <div class="field-description">SSH port for dockerized instance of A0.</div>
75 - </div>
76 - <div class="field-control">
77 - <input type="number" x-model.number="$store.settings.settings.rfc_port_ssh" />
78 - </div>
79 - </div>
55 </div>
56 </template>
57