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.
Files changed (99) hide show
  1. strix/__init__.py +0 -0
  2. strix/agents/StrixAgent/__init__.py +4 -0
  3. strix/agents/StrixAgent/strix_agent.py +60 -0
  4. strix/agents/StrixAgent/system_prompt.jinja +504 -0
  5. strix/agents/__init__.py +10 -0
  6. strix/agents/base_agent.py +394 -0
  7. strix/agents/state.py +139 -0
  8. strix/cli/__init__.py +4 -0
  9. strix/cli/app.py +1124 -0
  10. strix/cli/assets/cli.tcss +680 -0
  11. strix/cli/main.py +542 -0
  12. strix/cli/tool_components/__init__.py +39 -0
  13. strix/cli/tool_components/agents_graph_renderer.py +129 -0
  14. strix/cli/tool_components/base_renderer.py +61 -0
  15. strix/cli/tool_components/browser_renderer.py +107 -0
  16. strix/cli/tool_components/file_edit_renderer.py +95 -0
  17. strix/cli/tool_components/finish_renderer.py +32 -0
  18. strix/cli/tool_components/notes_renderer.py +108 -0
  19. strix/cli/tool_components/proxy_renderer.py +255 -0
  20. strix/cli/tool_components/python_renderer.py +34 -0
  21. strix/cli/tool_components/registry.py +72 -0
  22. strix/cli/tool_components/reporting_renderer.py +53 -0
  23. strix/cli/tool_components/scan_info_renderer.py +58 -0
  24. strix/cli/tool_components/terminal_renderer.py +99 -0
  25. strix/cli/tool_components/thinking_renderer.py +29 -0
  26. strix/cli/tool_components/user_message_renderer.py +43 -0
  27. strix/cli/tool_components/web_search_renderer.py +28 -0
  28. strix/cli/tracer.py +308 -0
  29. strix/llm/__init__.py +14 -0
  30. strix/llm/config.py +19 -0
  31. strix/llm/llm.py +310 -0
  32. strix/llm/memory_compressor.py +206 -0
  33. strix/llm/request_queue.py +63 -0
  34. strix/llm/utils.py +84 -0
  35. strix/prompts/__init__.py +113 -0
  36. strix/prompts/coordination/root_agent.jinja +41 -0
  37. strix/prompts/vulnerabilities/authentication_jwt.jinja +129 -0
  38. strix/prompts/vulnerabilities/business_logic.jinja +143 -0
  39. strix/prompts/vulnerabilities/csrf.jinja +168 -0
  40. strix/prompts/vulnerabilities/idor.jinja +164 -0
  41. strix/prompts/vulnerabilities/race_conditions.jinja +194 -0
  42. strix/prompts/vulnerabilities/rce.jinja +222 -0
  43. strix/prompts/vulnerabilities/sql_injection.jinja +216 -0
  44. strix/prompts/vulnerabilities/ssrf.jinja +168 -0
  45. strix/prompts/vulnerabilities/xss.jinja +221 -0
  46. strix/prompts/vulnerabilities/xxe.jinja +276 -0
  47. strix/runtime/__init__.py +19 -0
  48. strix/runtime/docker_runtime.py +298 -0
  49. strix/runtime/runtime.py +25 -0
  50. strix/runtime/tool_server.py +97 -0
  51. strix/tools/__init__.py +64 -0
  52. strix/tools/agents_graph/__init__.py +16 -0
  53. strix/tools/agents_graph/agents_graph_actions.py +610 -0
  54. strix/tools/agents_graph/agents_graph_actions_schema.xml +223 -0
  55. strix/tools/argument_parser.py +120 -0
  56. strix/tools/browser/__init__.py +4 -0
  57. strix/tools/browser/browser_actions.py +236 -0
  58. strix/tools/browser/browser_actions_schema.xml +183 -0
  59. strix/tools/browser/browser_instance.py +533 -0
  60. strix/tools/browser/tab_manager.py +342 -0
  61. strix/tools/executor.py +302 -0
  62. strix/tools/file_edit/__init__.py +4 -0
  63. strix/tools/file_edit/file_edit_actions.py +141 -0
  64. strix/tools/file_edit/file_edit_actions_schema.xml +128 -0
  65. strix/tools/finish/__init__.py +4 -0
  66. strix/tools/finish/finish_actions.py +167 -0
  67. strix/tools/finish/finish_actions_schema.xml +45 -0
  68. strix/tools/notes/__init__.py +14 -0
  69. strix/tools/notes/notes_actions.py +191 -0
  70. strix/tools/notes/notes_actions_schema.xml +150 -0
  71. strix/tools/proxy/__init__.py +20 -0
  72. strix/tools/proxy/proxy_actions.py +101 -0
  73. strix/tools/proxy/proxy_actions_schema.xml +267 -0
  74. strix/tools/proxy/proxy_manager.py +785 -0
  75. strix/tools/python/__init__.py +4 -0
  76. strix/tools/python/python_actions.py +47 -0
  77. strix/tools/python/python_actions_schema.xml +131 -0
  78. strix/tools/python/python_instance.py +172 -0
  79. strix/tools/python/python_manager.py +131 -0
  80. strix/tools/registry.py +196 -0
  81. strix/tools/reporting/__init__.py +6 -0
  82. strix/tools/reporting/reporting_actions.py +63 -0
  83. strix/tools/reporting/reporting_actions_schema.xml +30 -0
  84. strix/tools/terminal/__init__.py +4 -0
  85. strix/tools/terminal/terminal_actions.py +53 -0
  86. strix/tools/terminal/terminal_actions_schema.xml +114 -0
  87. strix/tools/terminal/terminal_instance.py +231 -0
  88. strix/tools/terminal/terminal_manager.py +191 -0
  89. strix/tools/thinking/__init__.py +4 -0
  90. strix/tools/thinking/thinking_actions.py +18 -0
  91. strix/tools/thinking/thinking_actions_schema.xml +52 -0
  92. strix/tools/web_search/__init__.py +4 -0
  93. strix/tools/web_search/web_search_actions.py +80 -0
  94. strix/tools/web_search/web_search_actions_schema.xml +83 -0
  95. strix_agent-0.1.1.dist-info/LICENSE +201 -0
  96. strix_agent-0.1.1.dist-info/METADATA +200 -0
  97. strix_agent-0.1.1.dist-info/RECORD +99 -0
  98. strix_agent-0.1.1.dist-info/WHEEL +4 -0
  99. 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
@@ -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
@@ -0,0 +1,4 @@
1
+ from .file_edit_actions import list_files, search_files, str_replace_editor
2
+
3
+
4
+ __all__ = ["list_files", "search_files", "str_replace_editor"]