strix-agent 0.4.0__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 (118) hide show
  1. strix/__init__.py +0 -0
  2. strix/agents/StrixAgent/__init__.py +4 -0
  3. strix/agents/StrixAgent/strix_agent.py +89 -0
  4. strix/agents/StrixAgent/system_prompt.jinja +404 -0
  5. strix/agents/__init__.py +10 -0
  6. strix/agents/base_agent.py +518 -0
  7. strix/agents/state.py +163 -0
  8. strix/interface/__init__.py +4 -0
  9. strix/interface/assets/tui_styles.tcss +694 -0
  10. strix/interface/cli.py +230 -0
  11. strix/interface/main.py +500 -0
  12. strix/interface/tool_components/__init__.py +39 -0
  13. strix/interface/tool_components/agents_graph_renderer.py +123 -0
  14. strix/interface/tool_components/base_renderer.py +62 -0
  15. strix/interface/tool_components/browser_renderer.py +120 -0
  16. strix/interface/tool_components/file_edit_renderer.py +99 -0
  17. strix/interface/tool_components/finish_renderer.py +31 -0
  18. strix/interface/tool_components/notes_renderer.py +108 -0
  19. strix/interface/tool_components/proxy_renderer.py +255 -0
  20. strix/interface/tool_components/python_renderer.py +34 -0
  21. strix/interface/tool_components/registry.py +72 -0
  22. strix/interface/tool_components/reporting_renderer.py +53 -0
  23. strix/interface/tool_components/scan_info_renderer.py +64 -0
  24. strix/interface/tool_components/terminal_renderer.py +131 -0
  25. strix/interface/tool_components/thinking_renderer.py +29 -0
  26. strix/interface/tool_components/user_message_renderer.py +43 -0
  27. strix/interface/tool_components/web_search_renderer.py +28 -0
  28. strix/interface/tui.py +1274 -0
  29. strix/interface/utils.py +559 -0
  30. strix/llm/__init__.py +15 -0
  31. strix/llm/config.py +20 -0
  32. strix/llm/llm.py +465 -0
  33. strix/llm/memory_compressor.py +212 -0
  34. strix/llm/request_queue.py +87 -0
  35. strix/llm/utils.py +87 -0
  36. strix/prompts/README.md +64 -0
  37. strix/prompts/__init__.py +109 -0
  38. strix/prompts/cloud/.gitkeep +0 -0
  39. strix/prompts/coordination/root_agent.jinja +41 -0
  40. strix/prompts/custom/.gitkeep +0 -0
  41. strix/prompts/frameworks/fastapi.jinja +142 -0
  42. strix/prompts/frameworks/nextjs.jinja +126 -0
  43. strix/prompts/protocols/graphql.jinja +215 -0
  44. strix/prompts/reconnaissance/.gitkeep +0 -0
  45. strix/prompts/technologies/firebase_firestore.jinja +177 -0
  46. strix/prompts/technologies/supabase.jinja +189 -0
  47. strix/prompts/vulnerabilities/authentication_jwt.jinja +147 -0
  48. strix/prompts/vulnerabilities/broken_function_level_authorization.jinja +146 -0
  49. strix/prompts/vulnerabilities/business_logic.jinja +171 -0
  50. strix/prompts/vulnerabilities/csrf.jinja +174 -0
  51. strix/prompts/vulnerabilities/idor.jinja +195 -0
  52. strix/prompts/vulnerabilities/information_disclosure.jinja +222 -0
  53. strix/prompts/vulnerabilities/insecure_file_uploads.jinja +188 -0
  54. strix/prompts/vulnerabilities/mass_assignment.jinja +141 -0
  55. strix/prompts/vulnerabilities/open_redirect.jinja +177 -0
  56. strix/prompts/vulnerabilities/path_traversal_lfi_rfi.jinja +142 -0
  57. strix/prompts/vulnerabilities/race_conditions.jinja +164 -0
  58. strix/prompts/vulnerabilities/rce.jinja +154 -0
  59. strix/prompts/vulnerabilities/sql_injection.jinja +151 -0
  60. strix/prompts/vulnerabilities/ssrf.jinja +135 -0
  61. strix/prompts/vulnerabilities/subdomain_takeover.jinja +155 -0
  62. strix/prompts/vulnerabilities/xss.jinja +169 -0
  63. strix/prompts/vulnerabilities/xxe.jinja +184 -0
  64. strix/runtime/__init__.py +19 -0
  65. strix/runtime/docker_runtime.py +399 -0
  66. strix/runtime/runtime.py +29 -0
  67. strix/runtime/tool_server.py +205 -0
  68. strix/telemetry/__init__.py +4 -0
  69. strix/telemetry/tracer.py +337 -0
  70. strix/tools/__init__.py +64 -0
  71. strix/tools/agents_graph/__init__.py +16 -0
  72. strix/tools/agents_graph/agents_graph_actions.py +621 -0
  73. strix/tools/agents_graph/agents_graph_actions_schema.xml +226 -0
  74. strix/tools/argument_parser.py +121 -0
  75. strix/tools/browser/__init__.py +4 -0
  76. strix/tools/browser/browser_actions.py +236 -0
  77. strix/tools/browser/browser_actions_schema.xml +183 -0
  78. strix/tools/browser/browser_instance.py +533 -0
  79. strix/tools/browser/tab_manager.py +342 -0
  80. strix/tools/executor.py +305 -0
  81. strix/tools/file_edit/__init__.py +4 -0
  82. strix/tools/file_edit/file_edit_actions.py +141 -0
  83. strix/tools/file_edit/file_edit_actions_schema.xml +128 -0
  84. strix/tools/finish/__init__.py +4 -0
  85. strix/tools/finish/finish_actions.py +174 -0
  86. strix/tools/finish/finish_actions_schema.xml +45 -0
  87. strix/tools/notes/__init__.py +14 -0
  88. strix/tools/notes/notes_actions.py +191 -0
  89. strix/tools/notes/notes_actions_schema.xml +150 -0
  90. strix/tools/proxy/__init__.py +20 -0
  91. strix/tools/proxy/proxy_actions.py +101 -0
  92. strix/tools/proxy/proxy_actions_schema.xml +267 -0
  93. strix/tools/proxy/proxy_manager.py +785 -0
  94. strix/tools/python/__init__.py +4 -0
  95. strix/tools/python/python_actions.py +47 -0
  96. strix/tools/python/python_actions_schema.xml +131 -0
  97. strix/tools/python/python_instance.py +172 -0
  98. strix/tools/python/python_manager.py +131 -0
  99. strix/tools/registry.py +196 -0
  100. strix/tools/reporting/__init__.py +6 -0
  101. strix/tools/reporting/reporting_actions.py +63 -0
  102. strix/tools/reporting/reporting_actions_schema.xml +30 -0
  103. strix/tools/terminal/__init__.py +4 -0
  104. strix/tools/terminal/terminal_actions.py +35 -0
  105. strix/tools/terminal/terminal_actions_schema.xml +146 -0
  106. strix/tools/terminal/terminal_manager.py +151 -0
  107. strix/tools/terminal/terminal_session.py +447 -0
  108. strix/tools/thinking/__init__.py +4 -0
  109. strix/tools/thinking/thinking_actions.py +18 -0
  110. strix/tools/thinking/thinking_actions_schema.xml +52 -0
  111. strix/tools/web_search/__init__.py +4 -0
  112. strix/tools/web_search/web_search_actions.py +80 -0
  113. strix/tools/web_search/web_search_actions_schema.xml +83 -0
  114. strix_agent-0.4.0.dist-info/LICENSE +201 -0
  115. strix_agent-0.4.0.dist-info/METADATA +282 -0
  116. strix_agent-0.4.0.dist-info/RECORD +118 -0
  117. strix_agent-0.4.0.dist-info/WHEEL +4 -0
  118. strix_agent-0.4.0.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,305 @@
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
+ agent_id = getattr(agent_state, "agent_id", "unknown")
53
+
54
+ request_data = {
55
+ "agent_id": agent_id,
56
+ "tool_name": tool_name,
57
+ "kwargs": kwargs,
58
+ }
59
+
60
+ headers = {
61
+ "Authorization": f"Bearer {agent_state.sandbox_token}",
62
+ "Content-Type": "application/json",
63
+ }
64
+
65
+ async with httpx.AsyncClient(trust_env=False) as client:
66
+ try:
67
+ response = await client.post(
68
+ request_url, json=request_data, headers=headers, timeout=None
69
+ )
70
+ response.raise_for_status()
71
+ response_data = response.json()
72
+ if response_data.get("error"):
73
+ raise RuntimeError(f"Sandbox execution error: {response_data['error']}")
74
+ return response_data.get("result")
75
+ except httpx.HTTPStatusError as e:
76
+ if e.response.status_code == 401:
77
+ raise RuntimeError("Authentication failed: Invalid or missing sandbox token") from e
78
+ raise RuntimeError(f"HTTP error calling tool server: {e.response.status_code}") from e
79
+ except httpx.RequestError as e:
80
+ raise RuntimeError(f"Request error calling tool server: {e}") from e
81
+
82
+
83
+ async def _execute_tool_locally(tool_name: str, agent_state: Any | None, **kwargs: Any) -> Any:
84
+ tool_func = get_tool_by_name(tool_name)
85
+ if not tool_func:
86
+ raise ValueError(f"Tool '{tool_name}' not found")
87
+
88
+ converted_kwargs = convert_arguments(tool_func, kwargs)
89
+
90
+ if needs_agent_state(tool_name):
91
+ if agent_state is None:
92
+ raise ValueError(f"Tool '{tool_name}' requires agent_state but none was provided.")
93
+ result = tool_func(agent_state=agent_state, **converted_kwargs)
94
+ else:
95
+ result = tool_func(**converted_kwargs)
96
+
97
+ return await result if inspect.isawaitable(result) else result
98
+
99
+
100
+ def validate_tool_availability(tool_name: str | None) -> tuple[bool, str]:
101
+ if tool_name is None:
102
+ return False, "Tool name is missing"
103
+
104
+ if tool_name not in get_tool_names():
105
+ return False, f"Tool '{tool_name}' is not available"
106
+
107
+ return True, ""
108
+
109
+
110
+ async def execute_tool_with_validation(
111
+ tool_name: str | None, agent_state: Any | None = None, **kwargs: Any
112
+ ) -> Any:
113
+ is_valid, error_msg = validate_tool_availability(tool_name)
114
+ if not is_valid:
115
+ return f"Error: {error_msg}"
116
+
117
+ assert tool_name is not None
118
+
119
+ try:
120
+ result = await execute_tool(tool_name, agent_state, **kwargs)
121
+ except Exception as e: # noqa: BLE001
122
+ error_str = str(e)
123
+ if len(error_str) > 500:
124
+ error_str = error_str[:500] + "... [truncated]"
125
+ return f"Error executing {tool_name}: {error_str}"
126
+ else:
127
+ return result
128
+
129
+
130
+ async def execute_tool_invocation(tool_inv: dict[str, Any], agent_state: Any | None = None) -> Any:
131
+ tool_name = tool_inv.get("toolName")
132
+ tool_args = tool_inv.get("args", {})
133
+
134
+ return await execute_tool_with_validation(tool_name, agent_state, **tool_args)
135
+
136
+
137
+ def _check_error_result(result: Any) -> tuple[bool, Any]:
138
+ is_error = False
139
+ error_payload: Any = None
140
+
141
+ if (isinstance(result, dict) and "error" in result) or (
142
+ isinstance(result, str) and result.strip().lower().startswith("error:")
143
+ ):
144
+ is_error = True
145
+ error_payload = result
146
+
147
+ return is_error, error_payload
148
+
149
+
150
+ def _update_tracer_with_result(
151
+ tracer: Any, execution_id: Any, is_error: bool, result: Any, error_payload: Any
152
+ ) -> None:
153
+ if not tracer or not execution_id:
154
+ return
155
+
156
+ try:
157
+ if is_error:
158
+ tracer.update_tool_execution(execution_id, "error", error_payload)
159
+ else:
160
+ tracer.update_tool_execution(execution_id, "completed", result)
161
+ except (ConnectionError, RuntimeError) as e:
162
+ error_msg = str(e)
163
+ if tracer and execution_id:
164
+ tracer.update_tool_execution(execution_id, "error", error_msg)
165
+ raise
166
+
167
+
168
+ def _format_tool_result(tool_name: str, result: Any) -> tuple[str, list[dict[str, Any]]]:
169
+ images: list[dict[str, Any]] = []
170
+
171
+ screenshot_data = extract_screenshot_from_result(result)
172
+ if screenshot_data:
173
+ images.append(
174
+ {
175
+ "type": "image_url",
176
+ "image_url": {"url": f"data:image/png;base64,{screenshot_data}"},
177
+ }
178
+ )
179
+ result_str = remove_screenshot_from_result(result)
180
+ else:
181
+ result_str = result
182
+
183
+ if result_str is None:
184
+ final_result_str = f"Tool {tool_name} executed successfully"
185
+ else:
186
+ final_result_str = str(result_str)
187
+ if len(final_result_str) > 10000:
188
+ start_part = final_result_str[:4000]
189
+ end_part = final_result_str[-4000:]
190
+ final_result_str = start_part + "\n\n... [middle content truncated] ...\n\n" + end_part
191
+
192
+ observation_xml = (
193
+ f"<tool_result>\n<tool_name>{tool_name}</tool_name>\n"
194
+ f"<result>{final_result_str}</result>\n</tool_result>"
195
+ )
196
+
197
+ return observation_xml, images
198
+
199
+
200
+ async def _execute_single_tool(
201
+ tool_inv: dict[str, Any],
202
+ agent_state: Any | None,
203
+ tracer: Any | None,
204
+ agent_id: str,
205
+ ) -> tuple[str, list[dict[str, Any]], bool]:
206
+ tool_name = tool_inv.get("toolName", "unknown")
207
+ args = tool_inv.get("args", {})
208
+ execution_id = None
209
+ should_agent_finish = False
210
+
211
+ if tracer:
212
+ execution_id = tracer.log_tool_execution_start(agent_id, tool_name, args)
213
+
214
+ try:
215
+ result = await execute_tool_invocation(tool_inv, agent_state)
216
+
217
+ is_error, error_payload = _check_error_result(result)
218
+
219
+ if (
220
+ tool_name in ("finish_scan", "agent_finish")
221
+ and not is_error
222
+ and isinstance(result, dict)
223
+ ):
224
+ if tool_name == "finish_scan":
225
+ should_agent_finish = result.get("scan_completed", False)
226
+ elif tool_name == "agent_finish":
227
+ should_agent_finish = result.get("agent_completed", False)
228
+
229
+ _update_tracer_with_result(tracer, execution_id, is_error, result, error_payload)
230
+
231
+ except (ConnectionError, RuntimeError, ValueError, TypeError, OSError) as e:
232
+ error_msg = str(e)
233
+ if tracer and execution_id:
234
+ tracer.update_tool_execution(execution_id, "error", error_msg)
235
+ raise
236
+
237
+ observation_xml, images = _format_tool_result(tool_name, result)
238
+ return observation_xml, images, should_agent_finish
239
+
240
+
241
+ def _get_tracer_and_agent_id(agent_state: Any | None) -> tuple[Any | None, str]:
242
+ try:
243
+ from strix.telemetry.tracer import get_global_tracer
244
+
245
+ tracer = get_global_tracer()
246
+ agent_id = agent_state.agent_id if agent_state else "unknown_agent"
247
+ except (ImportError, AttributeError):
248
+ tracer = None
249
+ agent_id = "unknown_agent"
250
+
251
+ return tracer, agent_id
252
+
253
+
254
+ async def process_tool_invocations(
255
+ tool_invocations: list[dict[str, Any]],
256
+ conversation_history: list[dict[str, Any]],
257
+ agent_state: Any | None = None,
258
+ ) -> bool:
259
+ observation_parts: list[str] = []
260
+ all_images: list[dict[str, Any]] = []
261
+ should_agent_finish = False
262
+
263
+ tracer, agent_id = _get_tracer_and_agent_id(agent_state)
264
+
265
+ for tool_inv in tool_invocations:
266
+ observation_xml, images, tool_should_finish = await _execute_single_tool(
267
+ tool_inv, agent_state, tracer, agent_id
268
+ )
269
+ observation_parts.append(observation_xml)
270
+ all_images.extend(images)
271
+
272
+ if tool_should_finish:
273
+ should_agent_finish = True
274
+
275
+ if all_images:
276
+ content = [{"type": "text", "text": "Tool Results:\n\n" + "\n\n".join(observation_parts)}]
277
+ content.extend(all_images)
278
+ conversation_history.append({"role": "user", "content": content})
279
+ else:
280
+ observation_content = "Tool Results:\n\n" + "\n\n".join(observation_parts)
281
+ conversation_history.append({"role": "user", "content": observation_content})
282
+
283
+ return should_agent_finish
284
+
285
+
286
+ def extract_screenshot_from_result(result: Any) -> str | None:
287
+ if not isinstance(result, dict):
288
+ return None
289
+
290
+ screenshot = result.get("screenshot")
291
+ if isinstance(screenshot, str) and screenshot:
292
+ return screenshot
293
+
294
+ return None
295
+
296
+
297
+ def remove_screenshot_from_result(result: Any) -> Any:
298
+ if not isinstance(result, dict):
299
+ return result
300
+
301
+ result_copy = result.copy()
302
+ if "screenshot" in result_copy:
303
+ result_copy["screenshot"] = "[Image data extracted - see attached image]"
304
+
305
+ 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"]