strix-agent 0.1.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.
- strix/__init__.py +0 -0
- strix/agents/StrixAgent/__init__.py +4 -0
- strix/agents/StrixAgent/strix_agent.py +60 -0
- strix/agents/StrixAgent/system_prompt.jinja +504 -0
- strix/agents/__init__.py +10 -0
- strix/agents/base_agent.py +394 -0
- strix/agents/state.py +139 -0
- strix/cli/__init__.py +4 -0
- strix/cli/app.py +1124 -0
- strix/cli/assets/cli.tcss +680 -0
- strix/cli/main.py +542 -0
- strix/cli/tool_components/__init__.py +39 -0
- strix/cli/tool_components/agents_graph_renderer.py +129 -0
- strix/cli/tool_components/base_renderer.py +61 -0
- strix/cli/tool_components/browser_renderer.py +107 -0
- strix/cli/tool_components/file_edit_renderer.py +95 -0
- strix/cli/tool_components/finish_renderer.py +32 -0
- strix/cli/tool_components/notes_renderer.py +108 -0
- strix/cli/tool_components/proxy_renderer.py +255 -0
- strix/cli/tool_components/python_renderer.py +34 -0
- strix/cli/tool_components/registry.py +72 -0
- strix/cli/tool_components/reporting_renderer.py +53 -0
- strix/cli/tool_components/scan_info_renderer.py +58 -0
- strix/cli/tool_components/terminal_renderer.py +99 -0
- strix/cli/tool_components/thinking_renderer.py +29 -0
- strix/cli/tool_components/user_message_renderer.py +43 -0
- strix/cli/tool_components/web_search_renderer.py +28 -0
- strix/cli/tracer.py +308 -0
- strix/llm/__init__.py +14 -0
- strix/llm/config.py +19 -0
- strix/llm/llm.py +310 -0
- strix/llm/memory_compressor.py +206 -0
- strix/llm/request_queue.py +63 -0
- strix/llm/utils.py +84 -0
- strix/prompts/__init__.py +113 -0
- strix/prompts/coordination/root_agent.jinja +41 -0
- strix/prompts/vulnerabilities/authentication_jwt.jinja +129 -0
- strix/prompts/vulnerabilities/business_logic.jinja +143 -0
- strix/prompts/vulnerabilities/csrf.jinja +168 -0
- strix/prompts/vulnerabilities/idor.jinja +164 -0
- strix/prompts/vulnerabilities/race_conditions.jinja +194 -0
- strix/prompts/vulnerabilities/rce.jinja +222 -0
- strix/prompts/vulnerabilities/sql_injection.jinja +216 -0
- strix/prompts/vulnerabilities/ssrf.jinja +168 -0
- strix/prompts/vulnerabilities/xss.jinja +221 -0
- strix/prompts/vulnerabilities/xxe.jinja +276 -0
- strix/runtime/__init__.py +19 -0
- strix/runtime/docker_runtime.py +298 -0
- strix/runtime/runtime.py +25 -0
- strix/runtime/tool_server.py +97 -0
- strix/tools/__init__.py +64 -0
- strix/tools/agents_graph/__init__.py +16 -0
- strix/tools/agents_graph/agents_graph_actions.py +610 -0
- strix/tools/agents_graph/agents_graph_actions_schema.xml +223 -0
- strix/tools/argument_parser.py +120 -0
- strix/tools/browser/__init__.py +4 -0
- strix/tools/browser/browser_actions.py +236 -0
- strix/tools/browser/browser_actions_schema.xml +183 -0
- strix/tools/browser/browser_instance.py +533 -0
- strix/tools/browser/tab_manager.py +342 -0
- strix/tools/executor.py +302 -0
- strix/tools/file_edit/__init__.py +4 -0
- strix/tools/file_edit/file_edit_actions.py +141 -0
- strix/tools/file_edit/file_edit_actions_schema.xml +128 -0
- strix/tools/finish/__init__.py +4 -0
- strix/tools/finish/finish_actions.py +167 -0
- strix/tools/finish/finish_actions_schema.xml +45 -0
- strix/tools/notes/__init__.py +14 -0
- strix/tools/notes/notes_actions.py +191 -0
- strix/tools/notes/notes_actions_schema.xml +150 -0
- strix/tools/proxy/__init__.py +20 -0
- strix/tools/proxy/proxy_actions.py +101 -0
- strix/tools/proxy/proxy_actions_schema.xml +267 -0
- strix/tools/proxy/proxy_manager.py +785 -0
- strix/tools/python/__init__.py +4 -0
- strix/tools/python/python_actions.py +47 -0
- strix/tools/python/python_actions_schema.xml +131 -0
- strix/tools/python/python_instance.py +172 -0
- strix/tools/python/python_manager.py +131 -0
- strix/tools/registry.py +196 -0
- strix/tools/reporting/__init__.py +6 -0
- strix/tools/reporting/reporting_actions.py +63 -0
- strix/tools/reporting/reporting_actions_schema.xml +30 -0
- strix/tools/terminal/__init__.py +4 -0
- strix/tools/terminal/terminal_actions.py +53 -0
- strix/tools/terminal/terminal_actions_schema.xml +114 -0
- strix/tools/terminal/terminal_instance.py +231 -0
- strix/tools/terminal/terminal_manager.py +191 -0
- strix/tools/thinking/__init__.py +4 -0
- strix/tools/thinking/thinking_actions.py +18 -0
- strix/tools/thinking/thinking_actions_schema.xml +52 -0
- strix/tools/web_search/__init__.py +4 -0
- strix/tools/web_search/web_search_actions.py +80 -0
- strix/tools/web_search/web_search_actions_schema.xml +83 -0
- strix_agent-0.1.1.dist-info/LICENSE +201 -0
- strix_agent-0.1.1.dist-info/METADATA +200 -0
- strix_agent-0.1.1.dist-info/RECORD +99 -0
- strix_agent-0.1.1.dist-info/WHEEL +4 -0
- strix_agent-0.1.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,342 @@
|
|
1
|
+
import atexit
|
2
|
+
import contextlib
|
3
|
+
import signal
|
4
|
+
import sys
|
5
|
+
import threading
|
6
|
+
from typing import Any
|
7
|
+
|
8
|
+
from .browser_instance import BrowserInstance
|
9
|
+
|
10
|
+
|
11
|
+
class BrowserTabManager:
|
12
|
+
def __init__(self) -> None:
|
13
|
+
self.browser_instance: BrowserInstance | None = None
|
14
|
+
self._lock = threading.Lock()
|
15
|
+
|
16
|
+
self._register_cleanup_handlers()
|
17
|
+
|
18
|
+
def launch_browser(self, url: str | None = None) -> dict[str, Any]:
|
19
|
+
with self._lock:
|
20
|
+
if self.browser_instance is not None:
|
21
|
+
raise ValueError("Browser is already launched")
|
22
|
+
|
23
|
+
try:
|
24
|
+
self.browser_instance = BrowserInstance()
|
25
|
+
result = self.browser_instance.launch(url)
|
26
|
+
result["message"] = "Browser launched successfully"
|
27
|
+
except (OSError, ValueError, RuntimeError) as e:
|
28
|
+
if self.browser_instance:
|
29
|
+
self.browser_instance = None
|
30
|
+
raise RuntimeError(f"Failed to launch browser: {e}") from e
|
31
|
+
else:
|
32
|
+
return result
|
33
|
+
|
34
|
+
def goto_url(self, url: str, tab_id: str | None = None) -> dict[str, Any]:
|
35
|
+
with self._lock:
|
36
|
+
if self.browser_instance is None:
|
37
|
+
raise ValueError("Browser not launched")
|
38
|
+
|
39
|
+
try:
|
40
|
+
result = self.browser_instance.goto(url, tab_id)
|
41
|
+
result["message"] = f"Navigated to {url}"
|
42
|
+
except (OSError, ValueError, RuntimeError) as e:
|
43
|
+
raise RuntimeError(f"Failed to navigate to URL: {e}") from e
|
44
|
+
else:
|
45
|
+
return result
|
46
|
+
|
47
|
+
def click(self, coordinate: str, tab_id: str | None = None) -> dict[str, Any]:
|
48
|
+
with self._lock:
|
49
|
+
if self.browser_instance is None:
|
50
|
+
raise ValueError("Browser not launched")
|
51
|
+
|
52
|
+
try:
|
53
|
+
result = self.browser_instance.click(coordinate, tab_id)
|
54
|
+
result["message"] = f"Clicked at {coordinate}"
|
55
|
+
except (OSError, ValueError, RuntimeError) as e:
|
56
|
+
raise RuntimeError(f"Failed to click: {e}") from e
|
57
|
+
else:
|
58
|
+
return result
|
59
|
+
|
60
|
+
def type_text(self, text: str, tab_id: str | None = None) -> dict[str, Any]:
|
61
|
+
with self._lock:
|
62
|
+
if self.browser_instance is None:
|
63
|
+
raise ValueError("Browser not launched")
|
64
|
+
|
65
|
+
try:
|
66
|
+
result = self.browser_instance.type_text(text, tab_id)
|
67
|
+
result["message"] = f"Typed text: {text[:50]}{'...' if len(text) > 50 else ''}"
|
68
|
+
except (OSError, ValueError, RuntimeError) as e:
|
69
|
+
raise RuntimeError(f"Failed to type text: {e}") from e
|
70
|
+
else:
|
71
|
+
return result
|
72
|
+
|
73
|
+
def scroll(self, direction: str, tab_id: str | None = None) -> dict[str, Any]:
|
74
|
+
with self._lock:
|
75
|
+
if self.browser_instance is None:
|
76
|
+
raise ValueError("Browser not launched")
|
77
|
+
|
78
|
+
try:
|
79
|
+
result = self.browser_instance.scroll(direction, tab_id)
|
80
|
+
result["message"] = f"Scrolled {direction}"
|
81
|
+
except (OSError, ValueError, RuntimeError) as e:
|
82
|
+
raise RuntimeError(f"Failed to scroll: {e}") from e
|
83
|
+
else:
|
84
|
+
return result
|
85
|
+
|
86
|
+
def back(self, tab_id: str | None = None) -> dict[str, Any]:
|
87
|
+
with self._lock:
|
88
|
+
if self.browser_instance is None:
|
89
|
+
raise ValueError("Browser not launched")
|
90
|
+
|
91
|
+
try:
|
92
|
+
result = self.browser_instance.back(tab_id)
|
93
|
+
result["message"] = "Navigated back"
|
94
|
+
except (OSError, ValueError, RuntimeError) as e:
|
95
|
+
raise RuntimeError(f"Failed to go back: {e}") from e
|
96
|
+
else:
|
97
|
+
return result
|
98
|
+
|
99
|
+
def forward(self, tab_id: str | None = None) -> dict[str, Any]:
|
100
|
+
with self._lock:
|
101
|
+
if self.browser_instance is None:
|
102
|
+
raise ValueError("Browser not launched")
|
103
|
+
|
104
|
+
try:
|
105
|
+
result = self.browser_instance.forward(tab_id)
|
106
|
+
result["message"] = "Navigated forward"
|
107
|
+
except (OSError, ValueError, RuntimeError) as e:
|
108
|
+
raise RuntimeError(f"Failed to go forward: {e}") from e
|
109
|
+
else:
|
110
|
+
return result
|
111
|
+
|
112
|
+
def new_tab(self, url: str | None = None) -> dict[str, Any]:
|
113
|
+
with self._lock:
|
114
|
+
if self.browser_instance is None:
|
115
|
+
raise ValueError("Browser not launched")
|
116
|
+
|
117
|
+
try:
|
118
|
+
result = self.browser_instance.new_tab(url)
|
119
|
+
result["message"] = f"Created new tab {result.get('tab_id', '')}"
|
120
|
+
except (OSError, ValueError, RuntimeError) as e:
|
121
|
+
raise RuntimeError(f"Failed to create new tab: {e}") from e
|
122
|
+
else:
|
123
|
+
return result
|
124
|
+
|
125
|
+
def switch_tab(self, tab_id: str) -> dict[str, Any]:
|
126
|
+
with self._lock:
|
127
|
+
if self.browser_instance is None:
|
128
|
+
raise ValueError("Browser not launched")
|
129
|
+
|
130
|
+
try:
|
131
|
+
result = self.browser_instance.switch_tab(tab_id)
|
132
|
+
result["message"] = f"Switched to tab {tab_id}"
|
133
|
+
except (OSError, ValueError, RuntimeError) as e:
|
134
|
+
raise RuntimeError(f"Failed to switch tab: {e}") from e
|
135
|
+
else:
|
136
|
+
return result
|
137
|
+
|
138
|
+
def close_tab(self, tab_id: str) -> dict[str, Any]:
|
139
|
+
with self._lock:
|
140
|
+
if self.browser_instance is None:
|
141
|
+
raise ValueError("Browser not launched")
|
142
|
+
|
143
|
+
try:
|
144
|
+
result = self.browser_instance.close_tab(tab_id)
|
145
|
+
result["message"] = f"Closed tab {tab_id}"
|
146
|
+
except (OSError, ValueError, RuntimeError) as e:
|
147
|
+
raise RuntimeError(f"Failed to close tab: {e}") from e
|
148
|
+
else:
|
149
|
+
return result
|
150
|
+
|
151
|
+
def wait_browser(self, duration: float, tab_id: str | None = None) -> dict[str, Any]:
|
152
|
+
with self._lock:
|
153
|
+
if self.browser_instance is None:
|
154
|
+
raise ValueError("Browser not launched")
|
155
|
+
|
156
|
+
try:
|
157
|
+
result = self.browser_instance.wait(duration, tab_id)
|
158
|
+
result["message"] = f"Waited {duration}s"
|
159
|
+
except (OSError, ValueError, RuntimeError) as e:
|
160
|
+
raise RuntimeError(f"Failed to wait: {e}") from e
|
161
|
+
else:
|
162
|
+
return result
|
163
|
+
|
164
|
+
def execute_js(self, js_code: str, tab_id: str | None = None) -> dict[str, Any]:
|
165
|
+
with self._lock:
|
166
|
+
if self.browser_instance is None:
|
167
|
+
raise ValueError("Browser not launched")
|
168
|
+
|
169
|
+
try:
|
170
|
+
result = self.browser_instance.execute_js(js_code, tab_id)
|
171
|
+
result["message"] = "JavaScript executed successfully"
|
172
|
+
except (OSError, ValueError, RuntimeError) as e:
|
173
|
+
raise RuntimeError(f"Failed to execute JavaScript: {e}") from e
|
174
|
+
else:
|
175
|
+
return result
|
176
|
+
|
177
|
+
def double_click(self, coordinate: str, tab_id: str | None = None) -> dict[str, Any]:
|
178
|
+
with self._lock:
|
179
|
+
if self.browser_instance is None:
|
180
|
+
raise ValueError("Browser not launched")
|
181
|
+
|
182
|
+
try:
|
183
|
+
result = self.browser_instance.double_click(coordinate, tab_id)
|
184
|
+
result["message"] = f"Double clicked at {coordinate}"
|
185
|
+
except (OSError, ValueError, RuntimeError) as e:
|
186
|
+
raise RuntimeError(f"Failed to double click: {e}") from e
|
187
|
+
else:
|
188
|
+
return result
|
189
|
+
|
190
|
+
def hover(self, coordinate: str, tab_id: str | None = None) -> dict[str, Any]:
|
191
|
+
with self._lock:
|
192
|
+
if self.browser_instance is None:
|
193
|
+
raise ValueError("Browser not launched")
|
194
|
+
|
195
|
+
try:
|
196
|
+
result = self.browser_instance.hover(coordinate, tab_id)
|
197
|
+
result["message"] = f"Hovered at {coordinate}"
|
198
|
+
except (OSError, ValueError, RuntimeError) as e:
|
199
|
+
raise RuntimeError(f"Failed to hover: {e}") from e
|
200
|
+
else:
|
201
|
+
return result
|
202
|
+
|
203
|
+
def press_key(self, key: str, tab_id: str | None = None) -> dict[str, Any]:
|
204
|
+
with self._lock:
|
205
|
+
if self.browser_instance is None:
|
206
|
+
raise ValueError("Browser not launched")
|
207
|
+
|
208
|
+
try:
|
209
|
+
result = self.browser_instance.press_key(key, tab_id)
|
210
|
+
result["message"] = f"Pressed key {key}"
|
211
|
+
except (OSError, ValueError, RuntimeError) as e:
|
212
|
+
raise RuntimeError(f"Failed to press key: {e}") from e
|
213
|
+
else:
|
214
|
+
return result
|
215
|
+
|
216
|
+
def save_pdf(self, file_path: str, tab_id: str | None = None) -> dict[str, Any]:
|
217
|
+
with self._lock:
|
218
|
+
if self.browser_instance is None:
|
219
|
+
raise ValueError("Browser not launched")
|
220
|
+
|
221
|
+
try:
|
222
|
+
result = self.browser_instance.save_pdf(file_path, tab_id)
|
223
|
+
result["message"] = f"Page saved as PDF: {file_path}"
|
224
|
+
except (OSError, ValueError, RuntimeError) as e:
|
225
|
+
raise RuntimeError(f"Failed to save PDF: {e}") from e
|
226
|
+
else:
|
227
|
+
return result
|
228
|
+
|
229
|
+
def get_console_logs(self, tab_id: str | None = None, clear: bool = False) -> dict[str, Any]:
|
230
|
+
with self._lock:
|
231
|
+
if self.browser_instance is None:
|
232
|
+
raise ValueError("Browser not launched")
|
233
|
+
|
234
|
+
try:
|
235
|
+
result = self.browser_instance.get_console_logs(tab_id, clear)
|
236
|
+
action_text = "cleared and retrieved" if clear else "retrieved"
|
237
|
+
|
238
|
+
logs = result.get("console_logs", [])
|
239
|
+
truncated = any(log.get("text", "").startswith("[TRUNCATED:") for log in logs)
|
240
|
+
truncated_text = " (truncated)" if truncated else ""
|
241
|
+
|
242
|
+
result["message"] = (
|
243
|
+
f"Console logs {action_text} for tab "
|
244
|
+
f"{result.get('tab_id', 'current')}{truncated_text}"
|
245
|
+
)
|
246
|
+
except (OSError, ValueError, RuntimeError) as e:
|
247
|
+
raise RuntimeError(f"Failed to get console logs: {e}") from e
|
248
|
+
else:
|
249
|
+
return result
|
250
|
+
|
251
|
+
def view_source(self, tab_id: str | None = None) -> dict[str, Any]:
|
252
|
+
with self._lock:
|
253
|
+
if self.browser_instance is None:
|
254
|
+
raise ValueError("Browser not launched")
|
255
|
+
|
256
|
+
try:
|
257
|
+
result = self.browser_instance.view_source(tab_id)
|
258
|
+
result["message"] = "Page source retrieved"
|
259
|
+
except (OSError, ValueError, RuntimeError) as e:
|
260
|
+
raise RuntimeError(f"Failed to get page source: {e}") from e
|
261
|
+
else:
|
262
|
+
return result
|
263
|
+
|
264
|
+
def list_tabs(self) -> dict[str, Any]:
|
265
|
+
with self._lock:
|
266
|
+
if self.browser_instance is None:
|
267
|
+
return {"tabs": {}, "total_count": 0, "current_tab": None}
|
268
|
+
|
269
|
+
try:
|
270
|
+
tab_info = {}
|
271
|
+
for tid, tab_page in self.browser_instance.pages.items():
|
272
|
+
try:
|
273
|
+
tab_info[tid] = {
|
274
|
+
"url": tab_page.url,
|
275
|
+
"title": "Unknown" if tab_page.is_closed() else "Active",
|
276
|
+
"is_current": tid == self.browser_instance.current_page_id,
|
277
|
+
}
|
278
|
+
except (AttributeError, RuntimeError):
|
279
|
+
tab_info[tid] = {
|
280
|
+
"url": "Unknown",
|
281
|
+
"title": "Closed",
|
282
|
+
"is_current": False,
|
283
|
+
}
|
284
|
+
|
285
|
+
return {
|
286
|
+
"tabs": tab_info,
|
287
|
+
"total_count": len(tab_info),
|
288
|
+
"current_tab": self.browser_instance.current_page_id,
|
289
|
+
}
|
290
|
+
except (OSError, ValueError, RuntimeError) as e:
|
291
|
+
raise RuntimeError(f"Failed to list tabs: {e}") from e
|
292
|
+
|
293
|
+
def close_browser(self) -> dict[str, Any]:
|
294
|
+
with self._lock:
|
295
|
+
if self.browser_instance is None:
|
296
|
+
raise ValueError("Browser not launched")
|
297
|
+
|
298
|
+
try:
|
299
|
+
self.browser_instance.close()
|
300
|
+
self.browser_instance = None
|
301
|
+
except (OSError, ValueError, RuntimeError) as e:
|
302
|
+
raise RuntimeError(f"Failed to close browser: {e}") from e
|
303
|
+
else:
|
304
|
+
return {
|
305
|
+
"message": "Browser closed successfully",
|
306
|
+
"screenshot": "",
|
307
|
+
"is_running": False,
|
308
|
+
}
|
309
|
+
|
310
|
+
def cleanup_dead_browser(self) -> None:
|
311
|
+
with self._lock:
|
312
|
+
if self.browser_instance and not self.browser_instance.is_alive():
|
313
|
+
with contextlib.suppress(Exception):
|
314
|
+
self.browser_instance.close()
|
315
|
+
self.browser_instance = None
|
316
|
+
|
317
|
+
def close_all(self) -> None:
|
318
|
+
with self._lock:
|
319
|
+
if self.browser_instance:
|
320
|
+
with contextlib.suppress(Exception):
|
321
|
+
self.browser_instance.close()
|
322
|
+
self.browser_instance = None
|
323
|
+
|
324
|
+
def _register_cleanup_handlers(self) -> None:
|
325
|
+
atexit.register(self.close_all)
|
326
|
+
|
327
|
+
signal.signal(signal.SIGTERM, self._signal_handler)
|
328
|
+
signal.signal(signal.SIGINT, self._signal_handler)
|
329
|
+
|
330
|
+
if hasattr(signal, "SIGHUP"):
|
331
|
+
signal.signal(signal.SIGHUP, self._signal_handler)
|
332
|
+
|
333
|
+
def _signal_handler(self, _signum: int, _frame: Any) -> None:
|
334
|
+
self.close_all()
|
335
|
+
sys.exit(0)
|
336
|
+
|
337
|
+
|
338
|
+
_browser_tab_manager = BrowserTabManager()
|
339
|
+
|
340
|
+
|
341
|
+
def get_browser_tab_manager() -> BrowserTabManager:
|
342
|
+
return _browser_tab_manager
|
strix/tools/executor.py
ADDED
@@ -0,0 +1,302 @@
|
|
1
|
+
import inspect
|
2
|
+
import os
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
import httpx
|
6
|
+
|
7
|
+
|
8
|
+
if os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "false":
|
9
|
+
from strix.runtime import get_runtime
|
10
|
+
|
11
|
+
from .argument_parser import convert_arguments
|
12
|
+
from .registry import (
|
13
|
+
get_tool_by_name,
|
14
|
+
get_tool_names,
|
15
|
+
needs_agent_state,
|
16
|
+
should_execute_in_sandbox,
|
17
|
+
)
|
18
|
+
|
19
|
+
|
20
|
+
async def execute_tool(tool_name: str, agent_state: Any | None = None, **kwargs: Any) -> Any:
|
21
|
+
execute_in_sandbox = should_execute_in_sandbox(tool_name)
|
22
|
+
sandbox_mode = os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "true"
|
23
|
+
|
24
|
+
if execute_in_sandbox and not sandbox_mode:
|
25
|
+
return await _execute_tool_in_sandbox(tool_name, agent_state, **kwargs)
|
26
|
+
|
27
|
+
return await _execute_tool_locally(tool_name, agent_state, **kwargs)
|
28
|
+
|
29
|
+
|
30
|
+
async def _execute_tool_in_sandbox(tool_name: str, agent_state: Any, **kwargs: Any) -> Any:
|
31
|
+
if not hasattr(agent_state, "sandbox_id") or not agent_state.sandbox_id:
|
32
|
+
raise ValueError("Agent state with a valid sandbox_id is required for sandbox execution.")
|
33
|
+
|
34
|
+
if not hasattr(agent_state, "sandbox_token") or not agent_state.sandbox_token:
|
35
|
+
raise ValueError(
|
36
|
+
"Agent state with a valid sandbox_token is required for sandbox execution."
|
37
|
+
)
|
38
|
+
|
39
|
+
if (
|
40
|
+
not hasattr(agent_state, "sandbox_info")
|
41
|
+
or "tool_server_port" not in agent_state.sandbox_info
|
42
|
+
):
|
43
|
+
raise ValueError(
|
44
|
+
"Agent state with a valid sandbox_info containing tool_server_port is required."
|
45
|
+
)
|
46
|
+
|
47
|
+
runtime = get_runtime()
|
48
|
+
tool_server_port = agent_state.sandbox_info["tool_server_port"]
|
49
|
+
server_url = await runtime.get_sandbox_url(agent_state.sandbox_id, tool_server_port)
|
50
|
+
request_url = f"{server_url}/execute"
|
51
|
+
|
52
|
+
request_data = {
|
53
|
+
"tool_name": tool_name,
|
54
|
+
"kwargs": kwargs,
|
55
|
+
}
|
56
|
+
|
57
|
+
headers = {
|
58
|
+
"Authorization": f"Bearer {agent_state.sandbox_token}",
|
59
|
+
"Content-Type": "application/json",
|
60
|
+
}
|
61
|
+
|
62
|
+
async with httpx.AsyncClient() as client:
|
63
|
+
try:
|
64
|
+
response = await client.post(
|
65
|
+
request_url, json=request_data, headers=headers, timeout=None
|
66
|
+
)
|
67
|
+
response.raise_for_status()
|
68
|
+
response_data = response.json()
|
69
|
+
if response_data.get("error"):
|
70
|
+
raise RuntimeError(f"Sandbox execution error: {response_data['error']}")
|
71
|
+
return response_data.get("result")
|
72
|
+
except httpx.HTTPStatusError as e:
|
73
|
+
if e.response.status_code == 401:
|
74
|
+
raise RuntimeError("Authentication failed: Invalid or missing sandbox token") from e
|
75
|
+
raise RuntimeError(f"HTTP error calling tool server: {e.response.status_code}") from e
|
76
|
+
except httpx.RequestError as e:
|
77
|
+
raise RuntimeError(f"Request error calling tool server: {e}") from e
|
78
|
+
|
79
|
+
|
80
|
+
async def _execute_tool_locally(tool_name: str, agent_state: Any | None, **kwargs: Any) -> Any:
|
81
|
+
tool_func = get_tool_by_name(tool_name)
|
82
|
+
if not tool_func:
|
83
|
+
raise ValueError(f"Tool '{tool_name}' not found")
|
84
|
+
|
85
|
+
converted_kwargs = convert_arguments(tool_func, kwargs)
|
86
|
+
|
87
|
+
if needs_agent_state(tool_name):
|
88
|
+
if agent_state is None:
|
89
|
+
raise ValueError(f"Tool '{tool_name}' requires agent_state but none was provided.")
|
90
|
+
result = tool_func(agent_state=agent_state, **converted_kwargs)
|
91
|
+
else:
|
92
|
+
result = tool_func(**converted_kwargs)
|
93
|
+
|
94
|
+
return await result if inspect.isawaitable(result) else result
|
95
|
+
|
96
|
+
|
97
|
+
def validate_tool_availability(tool_name: str | None) -> tuple[bool, str]:
|
98
|
+
if tool_name is None:
|
99
|
+
return False, "Tool name is missing"
|
100
|
+
|
101
|
+
if tool_name not in get_tool_names():
|
102
|
+
return False, f"Tool '{tool_name}' is not available"
|
103
|
+
|
104
|
+
return True, ""
|
105
|
+
|
106
|
+
|
107
|
+
async def execute_tool_with_validation(
|
108
|
+
tool_name: str | None, agent_state: Any | None = None, **kwargs: Any
|
109
|
+
) -> Any:
|
110
|
+
is_valid, error_msg = validate_tool_availability(tool_name)
|
111
|
+
if not is_valid:
|
112
|
+
return f"Error: {error_msg}"
|
113
|
+
|
114
|
+
assert tool_name is not None
|
115
|
+
|
116
|
+
try:
|
117
|
+
result = await execute_tool(tool_name, agent_state, **kwargs)
|
118
|
+
except Exception as e: # noqa: BLE001
|
119
|
+
error_str = str(e)
|
120
|
+
if len(error_str) > 500:
|
121
|
+
error_str = error_str[:500] + "... [truncated]"
|
122
|
+
return f"Error executing {tool_name}: {error_str}"
|
123
|
+
else:
|
124
|
+
return result
|
125
|
+
|
126
|
+
|
127
|
+
async def execute_tool_invocation(tool_inv: dict[str, Any], agent_state: Any | None = None) -> Any:
|
128
|
+
tool_name = tool_inv.get("toolName")
|
129
|
+
tool_args = tool_inv.get("args", {})
|
130
|
+
|
131
|
+
return await execute_tool_with_validation(tool_name, agent_state, **tool_args)
|
132
|
+
|
133
|
+
|
134
|
+
def _check_error_result(result: Any) -> tuple[bool, Any]:
|
135
|
+
is_error = False
|
136
|
+
error_payload: Any = None
|
137
|
+
|
138
|
+
if (isinstance(result, dict) and "error" in result) or (
|
139
|
+
isinstance(result, str) and result.strip().lower().startswith("error:")
|
140
|
+
):
|
141
|
+
is_error = True
|
142
|
+
error_payload = result
|
143
|
+
|
144
|
+
return is_error, error_payload
|
145
|
+
|
146
|
+
|
147
|
+
def _update_tracer_with_result(
|
148
|
+
tracer: Any, execution_id: Any, is_error: bool, result: Any, error_payload: Any
|
149
|
+
) -> None:
|
150
|
+
if not tracer or not execution_id:
|
151
|
+
return
|
152
|
+
|
153
|
+
try:
|
154
|
+
if is_error:
|
155
|
+
tracer.update_tool_execution(execution_id, "error", error_payload)
|
156
|
+
else:
|
157
|
+
tracer.update_tool_execution(execution_id, "completed", result)
|
158
|
+
except (ConnectionError, RuntimeError) as e:
|
159
|
+
error_msg = str(e)
|
160
|
+
if tracer and execution_id:
|
161
|
+
tracer.update_tool_execution(execution_id, "error", error_msg)
|
162
|
+
raise
|
163
|
+
|
164
|
+
|
165
|
+
def _format_tool_result(tool_name: str, result: Any) -> tuple[str, list[dict[str, Any]]]:
|
166
|
+
images: list[dict[str, Any]] = []
|
167
|
+
|
168
|
+
screenshot_data = extract_screenshot_from_result(result)
|
169
|
+
if screenshot_data:
|
170
|
+
images.append(
|
171
|
+
{
|
172
|
+
"type": "image_url",
|
173
|
+
"image_url": {"url": f"data:image/png;base64,{screenshot_data}"},
|
174
|
+
}
|
175
|
+
)
|
176
|
+
result_str = remove_screenshot_from_result(result)
|
177
|
+
else:
|
178
|
+
result_str = result
|
179
|
+
|
180
|
+
if result_str is None:
|
181
|
+
final_result_str = f"Tool {tool_name} executed successfully"
|
182
|
+
else:
|
183
|
+
final_result_str = str(result_str)
|
184
|
+
if len(final_result_str) > 10000:
|
185
|
+
start_part = final_result_str[:4000]
|
186
|
+
end_part = final_result_str[-4000:]
|
187
|
+
final_result_str = start_part + "\n\n... [middle content truncated] ...\n\n" + end_part
|
188
|
+
|
189
|
+
observation_xml = (
|
190
|
+
f"<tool_result>\n<tool_name>{tool_name}</tool_name>\n"
|
191
|
+
f"<result>{final_result_str}</result>\n</tool_result>"
|
192
|
+
)
|
193
|
+
|
194
|
+
return observation_xml, images
|
195
|
+
|
196
|
+
|
197
|
+
async def _execute_single_tool(
|
198
|
+
tool_inv: dict[str, Any],
|
199
|
+
agent_state: Any | None,
|
200
|
+
tracer: Any | None,
|
201
|
+
agent_id: str,
|
202
|
+
) -> tuple[str, list[dict[str, Any]], bool]:
|
203
|
+
tool_name = tool_inv.get("toolName", "unknown")
|
204
|
+
args = tool_inv.get("args", {})
|
205
|
+
execution_id = None
|
206
|
+
should_agent_finish = False
|
207
|
+
|
208
|
+
if tracer:
|
209
|
+
execution_id = tracer.log_tool_execution_start(agent_id, tool_name, args)
|
210
|
+
|
211
|
+
try:
|
212
|
+
result = await execute_tool_invocation(tool_inv, agent_state)
|
213
|
+
|
214
|
+
is_error, error_payload = _check_error_result(result)
|
215
|
+
|
216
|
+
if (
|
217
|
+
tool_name in ("finish_scan", "agent_finish")
|
218
|
+
and not is_error
|
219
|
+
and isinstance(result, dict)
|
220
|
+
):
|
221
|
+
if tool_name == "finish_scan":
|
222
|
+
should_agent_finish = result.get("scan_completed", False)
|
223
|
+
elif tool_name == "agent_finish":
|
224
|
+
should_agent_finish = result.get("agent_completed", False)
|
225
|
+
|
226
|
+
_update_tracer_with_result(tracer, execution_id, is_error, result, error_payload)
|
227
|
+
|
228
|
+
except (ConnectionError, RuntimeError, ValueError, TypeError, OSError) as e:
|
229
|
+
error_msg = str(e)
|
230
|
+
if tracer and execution_id:
|
231
|
+
tracer.update_tool_execution(execution_id, "error", error_msg)
|
232
|
+
raise
|
233
|
+
|
234
|
+
observation_xml, images = _format_tool_result(tool_name, result)
|
235
|
+
return observation_xml, images, should_agent_finish
|
236
|
+
|
237
|
+
|
238
|
+
def _get_tracer_and_agent_id(agent_state: Any | None) -> tuple[Any | None, str]:
|
239
|
+
try:
|
240
|
+
from strix.cli.tracer import get_global_tracer
|
241
|
+
|
242
|
+
tracer = get_global_tracer()
|
243
|
+
agent_id = agent_state.agent_id if agent_state else "unknown_agent"
|
244
|
+
except (ImportError, AttributeError):
|
245
|
+
tracer = None
|
246
|
+
agent_id = "unknown_agent"
|
247
|
+
|
248
|
+
return tracer, agent_id
|
249
|
+
|
250
|
+
|
251
|
+
async def process_tool_invocations(
|
252
|
+
tool_invocations: list[dict[str, Any]],
|
253
|
+
conversation_history: list[dict[str, Any]],
|
254
|
+
agent_state: Any | None = None,
|
255
|
+
) -> bool:
|
256
|
+
observation_parts: list[str] = []
|
257
|
+
all_images: list[dict[str, Any]] = []
|
258
|
+
should_agent_finish = False
|
259
|
+
|
260
|
+
tracer, agent_id = _get_tracer_and_agent_id(agent_state)
|
261
|
+
|
262
|
+
for tool_inv in tool_invocations:
|
263
|
+
observation_xml, images, tool_should_finish = await _execute_single_tool(
|
264
|
+
tool_inv, agent_state, tracer, agent_id
|
265
|
+
)
|
266
|
+
observation_parts.append(observation_xml)
|
267
|
+
all_images.extend(images)
|
268
|
+
|
269
|
+
if tool_should_finish:
|
270
|
+
should_agent_finish = True
|
271
|
+
|
272
|
+
if all_images:
|
273
|
+
content = [{"type": "text", "text": "Tool Results:\n\n" + "\n\n".join(observation_parts)}]
|
274
|
+
content.extend(all_images)
|
275
|
+
conversation_history.append({"role": "user", "content": content})
|
276
|
+
else:
|
277
|
+
observation_content = "Tool Results:\n\n" + "\n\n".join(observation_parts)
|
278
|
+
conversation_history.append({"role": "user", "content": observation_content})
|
279
|
+
|
280
|
+
return should_agent_finish
|
281
|
+
|
282
|
+
|
283
|
+
def extract_screenshot_from_result(result: Any) -> str | None:
|
284
|
+
if not isinstance(result, dict):
|
285
|
+
return None
|
286
|
+
|
287
|
+
screenshot = result.get("screenshot")
|
288
|
+
if isinstance(screenshot, str) and screenshot:
|
289
|
+
return screenshot
|
290
|
+
|
291
|
+
return None
|
292
|
+
|
293
|
+
|
294
|
+
def remove_screenshot_from_result(result: Any) -> Any:
|
295
|
+
if not isinstance(result, dict):
|
296
|
+
return result
|
297
|
+
|
298
|
+
result_copy = result.copy()
|
299
|
+
if "screenshot" in result_copy:
|
300
|
+
result_copy["screenshot"] = "[Image data extracted - see attached image]"
|
301
|
+
|
302
|
+
return result_copy
|