strix-agent 0.4.0__py3-none-any.whl → 0.6.2__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 (117) hide show
  1. strix/agents/StrixAgent/strix_agent.py +3 -3
  2. strix/agents/StrixAgent/system_prompt.jinja +30 -26
  3. strix/agents/base_agent.py +159 -75
  4. strix/agents/state.py +5 -2
  5. strix/config/__init__.py +12 -0
  6. strix/config/config.py +172 -0
  7. strix/interface/assets/tui_styles.tcss +195 -230
  8. strix/interface/cli.py +16 -41
  9. strix/interface/main.py +151 -74
  10. strix/interface/streaming_parser.py +119 -0
  11. strix/interface/tool_components/__init__.py +4 -0
  12. strix/interface/tool_components/agent_message_renderer.py +190 -0
  13. strix/interface/tool_components/agents_graph_renderer.py +54 -38
  14. strix/interface/tool_components/base_renderer.py +68 -36
  15. strix/interface/tool_components/browser_renderer.py +106 -91
  16. strix/interface/tool_components/file_edit_renderer.py +117 -36
  17. strix/interface/tool_components/finish_renderer.py +43 -10
  18. strix/interface/tool_components/notes_renderer.py +63 -38
  19. strix/interface/tool_components/proxy_renderer.py +133 -92
  20. strix/interface/tool_components/python_renderer.py +121 -8
  21. strix/interface/tool_components/registry.py +19 -12
  22. strix/interface/tool_components/reporting_renderer.py +196 -28
  23. strix/interface/tool_components/scan_info_renderer.py +22 -19
  24. strix/interface/tool_components/terminal_renderer.py +270 -90
  25. strix/interface/tool_components/thinking_renderer.py +8 -6
  26. strix/interface/tool_components/todo_renderer.py +225 -0
  27. strix/interface/tool_components/user_message_renderer.py +26 -19
  28. strix/interface/tool_components/web_search_renderer.py +7 -6
  29. strix/interface/tui.py +907 -262
  30. strix/interface/utils.py +236 -4
  31. strix/llm/__init__.py +6 -2
  32. strix/llm/config.py +8 -5
  33. strix/llm/dedupe.py +217 -0
  34. strix/llm/llm.py +209 -356
  35. strix/llm/memory_compressor.py +6 -5
  36. strix/llm/utils.py +17 -8
  37. strix/runtime/__init__.py +12 -3
  38. strix/runtime/docker_runtime.py +121 -202
  39. strix/runtime/tool_server.py +55 -95
  40. strix/skills/README.md +64 -0
  41. strix/skills/__init__.py +110 -0
  42. strix/{prompts → skills}/frameworks/nextjs.jinja +26 -0
  43. strix/skills/scan_modes/deep.jinja +145 -0
  44. strix/skills/scan_modes/quick.jinja +63 -0
  45. strix/skills/scan_modes/standard.jinja +91 -0
  46. strix/telemetry/README.md +38 -0
  47. strix/telemetry/__init__.py +7 -1
  48. strix/telemetry/posthog.py +137 -0
  49. strix/telemetry/tracer.py +194 -54
  50. strix/tools/__init__.py +11 -4
  51. strix/tools/agents_graph/agents_graph_actions.py +20 -21
  52. strix/tools/agents_graph/agents_graph_actions_schema.xml +8 -8
  53. strix/tools/browser/browser_actions.py +10 -6
  54. strix/tools/browser/browser_actions_schema.xml +6 -1
  55. strix/tools/browser/browser_instance.py +96 -48
  56. strix/tools/browser/tab_manager.py +121 -102
  57. strix/tools/context.py +12 -0
  58. strix/tools/executor.py +63 -4
  59. strix/tools/file_edit/file_edit_actions.py +6 -3
  60. strix/tools/file_edit/file_edit_actions_schema.xml +45 -3
  61. strix/tools/finish/finish_actions.py +80 -105
  62. strix/tools/finish/finish_actions_schema.xml +121 -14
  63. strix/tools/notes/notes_actions.py +6 -33
  64. strix/tools/notes/notes_actions_schema.xml +50 -46
  65. strix/tools/proxy/proxy_actions.py +14 -2
  66. strix/tools/proxy/proxy_actions_schema.xml +0 -1
  67. strix/tools/proxy/proxy_manager.py +28 -16
  68. strix/tools/python/python_actions.py +2 -2
  69. strix/tools/python/python_actions_schema.xml +9 -1
  70. strix/tools/python/python_instance.py +39 -37
  71. strix/tools/python/python_manager.py +43 -31
  72. strix/tools/registry.py +73 -12
  73. strix/tools/reporting/reporting_actions.py +218 -31
  74. strix/tools/reporting/reporting_actions_schema.xml +256 -8
  75. strix/tools/terminal/terminal_actions.py +2 -2
  76. strix/tools/terminal/terminal_actions_schema.xml +6 -0
  77. strix/tools/terminal/terminal_manager.py +41 -30
  78. strix/tools/thinking/thinking_actions_schema.xml +27 -25
  79. strix/tools/todo/__init__.py +18 -0
  80. strix/tools/todo/todo_actions.py +568 -0
  81. strix/tools/todo/todo_actions_schema.xml +225 -0
  82. strix/utils/__init__.py +0 -0
  83. strix/utils/resource_paths.py +13 -0
  84. {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/METADATA +90 -65
  85. strix_agent-0.6.2.dist-info/RECORD +134 -0
  86. {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/WHEEL +1 -1
  87. strix/llm/request_queue.py +0 -87
  88. strix/prompts/README.md +0 -64
  89. strix/prompts/__init__.py +0 -109
  90. strix_agent-0.4.0.dist-info/RECORD +0 -118
  91. /strix/{prompts → skills}/cloud/.gitkeep +0 -0
  92. /strix/{prompts → skills}/coordination/root_agent.jinja +0 -0
  93. /strix/{prompts → skills}/custom/.gitkeep +0 -0
  94. /strix/{prompts → skills}/frameworks/fastapi.jinja +0 -0
  95. /strix/{prompts → skills}/protocols/graphql.jinja +0 -0
  96. /strix/{prompts → skills}/reconnaissance/.gitkeep +0 -0
  97. /strix/{prompts → skills}/technologies/firebase_firestore.jinja +0 -0
  98. /strix/{prompts → skills}/technologies/supabase.jinja +0 -0
  99. /strix/{prompts → skills}/vulnerabilities/authentication_jwt.jinja +0 -0
  100. /strix/{prompts → skills}/vulnerabilities/broken_function_level_authorization.jinja +0 -0
  101. /strix/{prompts → skills}/vulnerabilities/business_logic.jinja +0 -0
  102. /strix/{prompts → skills}/vulnerabilities/csrf.jinja +0 -0
  103. /strix/{prompts → skills}/vulnerabilities/idor.jinja +0 -0
  104. /strix/{prompts → skills}/vulnerabilities/information_disclosure.jinja +0 -0
  105. /strix/{prompts → skills}/vulnerabilities/insecure_file_uploads.jinja +0 -0
  106. /strix/{prompts → skills}/vulnerabilities/mass_assignment.jinja +0 -0
  107. /strix/{prompts → skills}/vulnerabilities/open_redirect.jinja +0 -0
  108. /strix/{prompts → skills}/vulnerabilities/path_traversal_lfi_rfi.jinja +0 -0
  109. /strix/{prompts → skills}/vulnerabilities/race_conditions.jinja +0 -0
  110. /strix/{prompts → skills}/vulnerabilities/rce.jinja +0 -0
  111. /strix/{prompts → skills}/vulnerabilities/sql_injection.jinja +0 -0
  112. /strix/{prompts → skills}/vulnerabilities/ssrf.jinja +0 -0
  113. /strix/{prompts → skills}/vulnerabilities/subdomain_takeover.jinja +0 -0
  114. /strix/{prompts → skills}/vulnerabilities/xss.jinja +0 -0
  115. /strix/{prompts → skills}/vulnerabilities/xxe.jinja +0 -0
  116. {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/entry_points.txt +0 -0
  117. {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info/licenses}/LICENSE +0 -0
@@ -1,43 +1,56 @@
1
1
  import atexit
2
2
  import contextlib
3
- import signal
4
- import sys
5
3
  import threading
6
4
  from typing import Any
7
5
 
6
+ from strix.tools.context import get_current_agent_id
7
+
8
8
  from .browser_instance import BrowserInstance
9
9
 
10
10
 
11
11
  class BrowserTabManager:
12
12
  def __init__(self) -> None:
13
- self.browser_instance: BrowserInstance | None = None
13
+ self._browsers_by_agent: dict[str, BrowserInstance] = {}
14
14
  self._lock = threading.Lock()
15
15
 
16
16
  self._register_cleanup_handlers()
17
17
 
18
+ def _get_agent_browser(self) -> BrowserInstance | None:
19
+ agent_id = get_current_agent_id()
20
+ with self._lock:
21
+ return self._browsers_by_agent.get(agent_id)
22
+
23
+ def _set_agent_browser(self, browser: BrowserInstance | None) -> None:
24
+ agent_id = get_current_agent_id()
25
+ with self._lock:
26
+ if browser is None:
27
+ self._browsers_by_agent.pop(agent_id, None)
28
+ else:
29
+ self._browsers_by_agent[agent_id] = browser
30
+
18
31
  def launch_browser(self, url: str | None = None) -> dict[str, Any]:
19
32
  with self._lock:
20
- if self.browser_instance is not None:
33
+ agent_id = get_current_agent_id()
34
+ if agent_id in self._browsers_by_agent:
21
35
  raise ValueError("Browser is already launched")
22
36
 
23
37
  try:
24
- self.browser_instance = BrowserInstance()
25
- result = self.browser_instance.launch(url)
38
+ browser = BrowserInstance()
39
+ result = browser.launch(url)
40
+ self._browsers_by_agent[agent_id] = browser
26
41
  result["message"] = "Browser launched successfully"
27
42
  except (OSError, ValueError, RuntimeError) as e:
28
- if self.browser_instance:
29
- self.browser_instance = None
30
43
  raise RuntimeError(f"Failed to launch browser: {e}") from e
31
44
  else:
32
45
  return result
33
46
 
34
47
  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")
48
+ browser = self._get_agent_browser()
49
+ if browser is None:
50
+ raise ValueError("Browser not launched")
38
51
 
39
52
  try:
40
- result = self.browser_instance.goto(url, tab_id)
53
+ result = browser.goto(url, tab_id)
41
54
  result["message"] = f"Navigated to {url}"
42
55
  except (OSError, ValueError, RuntimeError) as e:
43
56
  raise RuntimeError(f"Failed to navigate to URL: {e}") from e
@@ -45,12 +58,12 @@ class BrowserTabManager:
45
58
  return result
46
59
 
47
60
  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")
61
+ browser = self._get_agent_browser()
62
+ if browser is None:
63
+ raise ValueError("Browser not launched")
51
64
 
52
65
  try:
53
- result = self.browser_instance.click(coordinate, tab_id)
66
+ result = browser.click(coordinate, tab_id)
54
67
  result["message"] = f"Clicked at {coordinate}"
55
68
  except (OSError, ValueError, RuntimeError) as e:
56
69
  raise RuntimeError(f"Failed to click: {e}") from e
@@ -58,12 +71,12 @@ class BrowserTabManager:
58
71
  return result
59
72
 
60
73
  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")
74
+ browser = self._get_agent_browser()
75
+ if browser is None:
76
+ raise ValueError("Browser not launched")
64
77
 
65
78
  try:
66
- result = self.browser_instance.type_text(text, tab_id)
79
+ result = browser.type_text(text, tab_id)
67
80
  result["message"] = f"Typed text: {text[:50]}{'...' if len(text) > 50 else ''}"
68
81
  except (OSError, ValueError, RuntimeError) as e:
69
82
  raise RuntimeError(f"Failed to type text: {e}") from e
@@ -71,12 +84,12 @@ class BrowserTabManager:
71
84
  return result
72
85
 
73
86
  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")
87
+ browser = self._get_agent_browser()
88
+ if browser is None:
89
+ raise ValueError("Browser not launched")
77
90
 
78
91
  try:
79
- result = self.browser_instance.scroll(direction, tab_id)
92
+ result = browser.scroll(direction, tab_id)
80
93
  result["message"] = f"Scrolled {direction}"
81
94
  except (OSError, ValueError, RuntimeError) as e:
82
95
  raise RuntimeError(f"Failed to scroll: {e}") from e
@@ -84,12 +97,12 @@ class BrowserTabManager:
84
97
  return result
85
98
 
86
99
  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")
100
+ browser = self._get_agent_browser()
101
+ if browser is None:
102
+ raise ValueError("Browser not launched")
90
103
 
91
104
  try:
92
- result = self.browser_instance.back(tab_id)
105
+ result = browser.back(tab_id)
93
106
  result["message"] = "Navigated back"
94
107
  except (OSError, ValueError, RuntimeError) as e:
95
108
  raise RuntimeError(f"Failed to go back: {e}") from e
@@ -97,12 +110,12 @@ class BrowserTabManager:
97
110
  return result
98
111
 
99
112
  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")
113
+ browser = self._get_agent_browser()
114
+ if browser is None:
115
+ raise ValueError("Browser not launched")
103
116
 
104
117
  try:
105
- result = self.browser_instance.forward(tab_id)
118
+ result = browser.forward(tab_id)
106
119
  result["message"] = "Navigated forward"
107
120
  except (OSError, ValueError, RuntimeError) as e:
108
121
  raise RuntimeError(f"Failed to go forward: {e}") from e
@@ -110,12 +123,12 @@ class BrowserTabManager:
110
123
  return result
111
124
 
112
125
  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")
126
+ browser = self._get_agent_browser()
127
+ if browser is None:
128
+ raise ValueError("Browser not launched")
116
129
 
117
130
  try:
118
- result = self.browser_instance.new_tab(url)
131
+ result = browser.new_tab(url)
119
132
  result["message"] = f"Created new tab {result.get('tab_id', '')}"
120
133
  except (OSError, ValueError, RuntimeError) as e:
121
134
  raise RuntimeError(f"Failed to create new tab: {e}") from e
@@ -123,12 +136,12 @@ class BrowserTabManager:
123
136
  return result
124
137
 
125
138
  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")
139
+ browser = self._get_agent_browser()
140
+ if browser is None:
141
+ raise ValueError("Browser not launched")
129
142
 
130
143
  try:
131
- result = self.browser_instance.switch_tab(tab_id)
144
+ result = browser.switch_tab(tab_id)
132
145
  result["message"] = f"Switched to tab {tab_id}"
133
146
  except (OSError, ValueError, RuntimeError) as e:
134
147
  raise RuntimeError(f"Failed to switch tab: {e}") from e
@@ -136,12 +149,12 @@ class BrowserTabManager:
136
149
  return result
137
150
 
138
151
  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")
152
+ browser = self._get_agent_browser()
153
+ if browser is None:
154
+ raise ValueError("Browser not launched")
142
155
 
143
156
  try:
144
- result = self.browser_instance.close_tab(tab_id)
157
+ result = browser.close_tab(tab_id)
145
158
  result["message"] = f"Closed tab {tab_id}"
146
159
  except (OSError, ValueError, RuntimeError) as e:
147
160
  raise RuntimeError(f"Failed to close tab: {e}") from e
@@ -149,12 +162,12 @@ class BrowserTabManager:
149
162
  return result
150
163
 
151
164
  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")
165
+ browser = self._get_agent_browser()
166
+ if browser is None:
167
+ raise ValueError("Browser not launched")
155
168
 
156
169
  try:
157
- result = self.browser_instance.wait(duration, tab_id)
170
+ result = browser.wait(duration, tab_id)
158
171
  result["message"] = f"Waited {duration}s"
159
172
  except (OSError, ValueError, RuntimeError) as e:
160
173
  raise RuntimeError(f"Failed to wait: {e}") from e
@@ -162,12 +175,12 @@ class BrowserTabManager:
162
175
  return result
163
176
 
164
177
  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")
178
+ browser = self._get_agent_browser()
179
+ if browser is None:
180
+ raise ValueError("Browser not launched")
168
181
 
169
182
  try:
170
- result = self.browser_instance.execute_js(js_code, tab_id)
183
+ result = browser.execute_js(js_code, tab_id)
171
184
  result["message"] = "JavaScript executed successfully"
172
185
  except (OSError, ValueError, RuntimeError) as e:
173
186
  raise RuntimeError(f"Failed to execute JavaScript: {e}") from e
@@ -175,12 +188,12 @@ class BrowserTabManager:
175
188
  return result
176
189
 
177
190
  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")
191
+ browser = self._get_agent_browser()
192
+ if browser is None:
193
+ raise ValueError("Browser not launched")
181
194
 
182
195
  try:
183
- result = self.browser_instance.double_click(coordinate, tab_id)
196
+ result = browser.double_click(coordinate, tab_id)
184
197
  result["message"] = f"Double clicked at {coordinate}"
185
198
  except (OSError, ValueError, RuntimeError) as e:
186
199
  raise RuntimeError(f"Failed to double click: {e}") from e
@@ -188,12 +201,12 @@ class BrowserTabManager:
188
201
  return result
189
202
 
190
203
  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")
204
+ browser = self._get_agent_browser()
205
+ if browser is None:
206
+ raise ValueError("Browser not launched")
194
207
 
195
208
  try:
196
- result = self.browser_instance.hover(coordinate, tab_id)
209
+ result = browser.hover(coordinate, tab_id)
197
210
  result["message"] = f"Hovered at {coordinate}"
198
211
  except (OSError, ValueError, RuntimeError) as e:
199
212
  raise RuntimeError(f"Failed to hover: {e}") from e
@@ -201,12 +214,12 @@ class BrowserTabManager:
201
214
  return result
202
215
 
203
216
  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")
217
+ browser = self._get_agent_browser()
218
+ if browser is None:
219
+ raise ValueError("Browser not launched")
207
220
 
208
221
  try:
209
- result = self.browser_instance.press_key(key, tab_id)
222
+ result = browser.press_key(key, tab_id)
210
223
  result["message"] = f"Pressed key {key}"
211
224
  except (OSError, ValueError, RuntimeError) as e:
212
225
  raise RuntimeError(f"Failed to press key: {e}") from e
@@ -214,12 +227,12 @@ class BrowserTabManager:
214
227
  return result
215
228
 
216
229
  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")
230
+ browser = self._get_agent_browser()
231
+ if browser is None:
232
+ raise ValueError("Browser not launched")
220
233
 
221
234
  try:
222
- result = self.browser_instance.save_pdf(file_path, tab_id)
235
+ result = browser.save_pdf(file_path, tab_id)
223
236
  result["message"] = f"Page saved as PDF: {file_path}"
224
237
  except (OSError, ValueError, RuntimeError) as e:
225
238
  raise RuntimeError(f"Failed to save PDF: {e}") from e
@@ -227,12 +240,12 @@ class BrowserTabManager:
227
240
  return result
228
241
 
229
242
  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")
243
+ browser = self._get_agent_browser()
244
+ if browser is None:
245
+ raise ValueError("Browser not launched")
233
246
 
234
247
  try:
235
- result = self.browser_instance.get_console_logs(tab_id, clear)
248
+ result = browser.get_console_logs(tab_id, clear)
236
249
  action_text = "cleared and retrieved" if clear else "retrieved"
237
250
 
238
251
  logs = result.get("console_logs", [])
@@ -249,12 +262,12 @@ class BrowserTabManager:
249
262
  return result
250
263
 
251
264
  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")
265
+ browser = self._get_agent_browser()
266
+ if browser is None:
267
+ raise ValueError("Browser not launched")
255
268
 
256
269
  try:
257
- result = self.browser_instance.view_source(tab_id)
270
+ result = browser.view_source(tab_id)
258
271
  result["message"] = "Page source retrieved"
259
272
  except (OSError, ValueError, RuntimeError) as e:
260
273
  raise RuntimeError(f"Failed to get page source: {e}") from e
@@ -262,18 +275,18 @@ class BrowserTabManager:
262
275
  return result
263
276
 
264
277
  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}
278
+ browser = self._get_agent_browser()
279
+ if browser is None:
280
+ return {"tabs": {}, "total_count": 0, "current_tab": None}
268
281
 
269
282
  try:
270
283
  tab_info = {}
271
- for tid, tab_page in self.browser_instance.pages.items():
284
+ for tid, tab_page in browser.pages.items():
272
285
  try:
273
286
  tab_info[tid] = {
274
287
  "url": tab_page.url,
275
288
  "title": "Unknown" if tab_page.is_closed() else "Active",
276
- "is_current": tid == self.browser_instance.current_page_id,
289
+ "is_current": tid == browser.current_page_id,
277
290
  }
278
291
  except (AttributeError, RuntimeError):
279
292
  tab_info[tid] = {
@@ -285,19 +298,20 @@ class BrowserTabManager:
285
298
  return {
286
299
  "tabs": tab_info,
287
300
  "total_count": len(tab_info),
288
- "current_tab": self.browser_instance.current_page_id,
301
+ "current_tab": browser.current_page_id,
289
302
  }
290
303
  except (OSError, ValueError, RuntimeError) as e:
291
304
  raise RuntimeError(f"Failed to list tabs: {e}") from e
292
305
 
293
306
  def close_browser(self) -> dict[str, Any]:
307
+ agent_id = get_current_agent_id()
294
308
  with self._lock:
295
- if self.browser_instance is None:
309
+ browser = self._browsers_by_agent.pop(agent_id, None)
310
+ if browser is None:
296
311
  raise ValueError("Browser not launched")
297
312
 
298
313
  try:
299
- self.browser_instance.close()
300
- self.browser_instance = None
314
+ browser.close()
301
315
  except (OSError, ValueError, RuntimeError) as e:
302
316
  raise RuntimeError(f"Failed to close browser: {e}") from e
303
317
  else:
@@ -307,33 +321,38 @@ class BrowserTabManager:
307
321
  "is_running": False,
308
322
  }
309
323
 
324
+ def cleanup_agent(self, agent_id: str) -> None:
325
+ with self._lock:
326
+ browser = self._browsers_by_agent.pop(agent_id, None)
327
+
328
+ if browser:
329
+ with contextlib.suppress(Exception):
330
+ browser.close()
331
+
310
332
  def cleanup_dead_browser(self) -> None:
311
333
  with self._lock:
312
- if self.browser_instance and not self.browser_instance.is_alive():
334
+ dead_agents = []
335
+ for agent_id, browser in self._browsers_by_agent.items():
336
+ if not browser.is_alive():
337
+ dead_agents.append(agent_id)
338
+
339
+ for agent_id in dead_agents:
340
+ browser = self._browsers_by_agent.pop(agent_id)
313
341
  with contextlib.suppress(Exception):
314
- self.browser_instance.close()
315
- self.browser_instance = None
342
+ browser.close()
316
343
 
317
344
  def close_all(self) -> None:
318
345
  with self._lock:
319
- if self.browser_instance:
320
- with contextlib.suppress(Exception):
321
- self.browser_instance.close()
322
- self.browser_instance = None
346
+ browsers = list(self._browsers_by_agent.values())
347
+ self._browsers_by_agent.clear()
348
+
349
+ for browser in browsers:
350
+ with contextlib.suppress(Exception):
351
+ browser.close()
323
352
 
324
353
  def _register_cleanup_handlers(self) -> None:
325
354
  atexit.register(self.close_all)
326
355
 
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
356
 
338
357
  _browser_tab_manager = BrowserTabManager()
339
358
 
strix/tools/context.py ADDED
@@ -0,0 +1,12 @@
1
+ from contextvars import ContextVar
2
+
3
+
4
+ current_agent_id: ContextVar[str] = ContextVar("current_agent_id", default="default")
5
+
6
+
7
+ def get_current_agent_id() -> str:
8
+ return current_agent_id.get()
9
+
10
+
11
+ def set_current_agent_id(agent_id: str) -> None:
12
+ current_agent_id.set(agent_id)
strix/tools/executor.py CHANGED
@@ -4,6 +4,9 @@ from typing import Any
4
4
 
5
5
  import httpx
6
6
 
7
+ from strix.config import Config
8
+ from strix.telemetry import posthog
9
+
7
10
 
8
11
  if os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "false":
9
12
  from strix.runtime import get_runtime
@@ -12,11 +15,17 @@ from .argument_parser import convert_arguments
12
15
  from .registry import (
13
16
  get_tool_by_name,
14
17
  get_tool_names,
18
+ get_tool_param_schema,
15
19
  needs_agent_state,
16
20
  should_execute_in_sandbox,
17
21
  )
18
22
 
19
23
 
24
+ _SERVER_TIMEOUT = float(Config.get("strix_sandbox_execution_timeout") or "120")
25
+ SANDBOX_EXECUTION_TIMEOUT = _SERVER_TIMEOUT + 30
26
+ SANDBOX_CONNECT_TIMEOUT = float(Config.get("strix_sandbox_connect_timeout") or "10")
27
+
28
+
20
29
  async def execute_tool(tool_name: str, agent_state: Any | None = None, **kwargs: Any) -> Any:
21
30
  execute_in_sandbox = should_execute_in_sandbox(tool_name)
22
31
  sandbox_mode = os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "true"
@@ -62,22 +71,31 @@ async def _execute_tool_in_sandbox(tool_name: str, agent_state: Any, **kwargs: A
62
71
  "Content-Type": "application/json",
63
72
  }
64
73
 
74
+ timeout = httpx.Timeout(
75
+ timeout=SANDBOX_EXECUTION_TIMEOUT,
76
+ connect=SANDBOX_CONNECT_TIMEOUT,
77
+ )
78
+
65
79
  async with httpx.AsyncClient(trust_env=False) as client:
66
80
  try:
67
81
  response = await client.post(
68
- request_url, json=request_data, headers=headers, timeout=None
82
+ request_url, json=request_data, headers=headers, timeout=timeout
69
83
  )
70
84
  response.raise_for_status()
71
85
  response_data = response.json()
72
86
  if response_data.get("error"):
87
+ posthog.error("tool_execution_error", f"{tool_name}: {response_data['error']}")
73
88
  raise RuntimeError(f"Sandbox execution error: {response_data['error']}")
74
89
  return response_data.get("result")
75
90
  except httpx.HTTPStatusError as e:
91
+ posthog.error("tool_http_error", f"{tool_name}: HTTP {e.response.status_code}")
76
92
  if e.response.status_code == 401:
77
93
  raise RuntimeError("Authentication failed: Invalid or missing sandbox token") from e
78
94
  raise RuntimeError(f"HTTP error calling tool server: {e.response.status_code}") from e
79
95
  except httpx.RequestError as e:
80
- raise RuntimeError(f"Request error calling tool server: {e}") from e
96
+ error_type = type(e).__name__
97
+ posthog.error("tool_request_error", f"{tool_name}: {error_type}")
98
+ raise RuntimeError(f"Request error calling tool server: {error_type}") from e
81
99
 
82
100
 
83
101
  async def _execute_tool_locally(tool_name: str, agent_state: Any | None, **kwargs: Any) -> Any:
@@ -99,14 +117,51 @@ async def _execute_tool_locally(tool_name: str, agent_state: Any | None, **kwarg
99
117
 
100
118
  def validate_tool_availability(tool_name: str | None) -> tuple[bool, str]:
101
119
  if tool_name is None:
102
- return False, "Tool name is missing"
120
+ available = ", ".join(sorted(get_tool_names()))
121
+ return False, f"Tool name is missing. Available tools: {available}"
103
122
 
104
123
  if tool_name not in get_tool_names():
105
- return False, f"Tool '{tool_name}' is not available"
124
+ available = ", ".join(sorted(get_tool_names()))
125
+ return False, f"Tool '{tool_name}' is not available. Available tools: {available}"
106
126
 
107
127
  return True, ""
108
128
 
109
129
 
130
+ def _validate_tool_arguments(tool_name: str, kwargs: dict[str, Any]) -> str | None:
131
+ param_schema = get_tool_param_schema(tool_name)
132
+ if not param_schema or not param_schema.get("has_params"):
133
+ return None
134
+
135
+ allowed_params: set[str] = param_schema.get("params", set())
136
+ required_params: set[str] = param_schema.get("required", set())
137
+ optional_params = allowed_params - required_params
138
+
139
+ schema_hint = _format_schema_hint(tool_name, required_params, optional_params)
140
+
141
+ unknown_params = set(kwargs.keys()) - allowed_params
142
+ if unknown_params:
143
+ unknown_list = ", ".join(sorted(unknown_params))
144
+ return f"Tool '{tool_name}' received unknown parameter(s): {unknown_list}\n{schema_hint}"
145
+
146
+ missing_required = [
147
+ param for param in required_params if param not in kwargs or kwargs.get(param) in (None, "")
148
+ ]
149
+ if missing_required:
150
+ missing_list = ", ".join(sorted(missing_required))
151
+ return f"Tool '{tool_name}' missing required parameter(s): {missing_list}\n{schema_hint}"
152
+
153
+ return None
154
+
155
+
156
+ def _format_schema_hint(tool_name: str, required: set[str], optional: set[str]) -> str:
157
+ parts = [f"Valid parameters for '{tool_name}':"]
158
+ if required:
159
+ parts.append(f" Required: {', '.join(sorted(required))}")
160
+ if optional:
161
+ parts.append(f" Optional: {', '.join(sorted(optional))}")
162
+ return "\n".join(parts)
163
+
164
+
110
165
  async def execute_tool_with_validation(
111
166
  tool_name: str | None, agent_state: Any | None = None, **kwargs: Any
112
167
  ) -> Any:
@@ -116,6 +171,10 @@ async def execute_tool_with_validation(
116
171
 
117
172
  assert tool_name is not None
118
173
 
174
+ arg_error = _validate_tool_arguments(tool_name, kwargs)
175
+ if arg_error:
176
+ return f"Error: {arg_error}"
177
+
119
178
  try:
120
179
  result = await execute_tool(tool_name, agent_state, **kwargs)
121
180
  except Exception as e: # noqa: BLE001
@@ -3,9 +3,6 @@ import re
3
3
  from pathlib import Path
4
4
  from typing import Any, cast
5
5
 
6
- from openhands_aci import file_editor
7
- from openhands_aci.utils.shell import run_shell_cmd
8
-
9
6
  from strix.tools.registry import register_tool
10
7
 
11
8
 
@@ -33,6 +30,8 @@ def str_replace_editor(
33
30
  new_str: str | None = None,
34
31
  insert_line: int | None = None,
35
32
  ) -> dict[str, Any]:
33
+ from openhands_aci import file_editor
34
+
36
35
  try:
37
36
  path_obj = Path(path)
38
37
  if not path_obj.is_absolute():
@@ -64,6 +63,8 @@ def list_files(
64
63
  path: str,
65
64
  recursive: bool = False,
66
65
  ) -> dict[str, Any]:
66
+ from openhands_aci.utils.shell import run_shell_cmd
67
+
67
68
  try:
68
69
  path_obj = Path(path)
69
70
  if not path_obj.is_absolute():
@@ -116,6 +117,8 @@ def search_files(
116
117
  regex: str,
117
118
  file_pattern: str = "*",
118
119
  ) -> dict[str, Any]:
120
+ from openhands_aci.utils.shell import run_shell_cmd
121
+
119
122
  try:
120
123
  path_obj = Path(path)
121
124
  if not path_obj.is_absolute():