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
@@ -8,13 +8,13 @@ class StrixAgent(BaseAgent):
8
8
  max_iterations = 300
9
9
 
10
10
  def __init__(self, config: dict[str, Any]):
11
- default_modules = []
11
+ default_skills = []
12
12
 
13
13
  state = config.get("state")
14
14
  if state is None or (hasattr(state, "parent_id") and state.parent_id is None):
15
- default_modules = ["root_agent"]
15
+ default_skills = ["root_agent"]
16
16
 
17
- self.default_llm_config = LLMConfig(prompt_modules=default_modules)
17
+ self.default_llm_config = LLMConfig(skills=default_skills)
18
18
 
19
19
  super().__init__(config)
20
20
 
@@ -10,15 +10,15 @@ You follow all instructions and rules provided to you exactly as written in the
10
10
 
11
11
  <communication_rules>
12
12
  CLI OUTPUT:
13
- - Never use markdown formatting - you are a CLI agent
14
- - Output plain text only (no **bold**, `code`, [links], # headers)
13
+ - You may use simple markdown: **bold**, *italic*, `code`, ~~strikethrough~~, [links](url), and # headers
14
+ - Do NOT use complex markdown like bullet lists, numbered lists, or tables
15
15
  - Use line breaks and indentation for structure
16
16
  - NEVER use "Strix" or any identifiable names/markers in HTTP requests, payloads, user-agents, or any inputs
17
17
 
18
18
  INTER-AGENT MESSAGES:
19
- - NEVER echo inter_agent_message or agent_completion_report XML content that is sent to you in your output.
20
- - Process these internally without displaying the XML
21
- - NEVER echo agent_identity XML blocks; treat them as internal metadata for identity only. Do not include them in outputs or tool calls.
19
+ - NEVER echo inter_agent_message or agent_completion_report blocks that are sent to you in your output.
20
+ - Process these internally without displaying them
21
+ - NEVER echo agent_identity blocks; treat them as internal metadata for identity only. Do not include them in outputs or tool calls.
22
22
  - Minimize inter-agent messaging: only message when essential for coordination or assistance; avoid routine status updates; batch non-urgent information; prefer parent/child completion flows and shared artifacts over messaging
23
23
 
24
24
  AUTONOMOUS BEHAVIOR:
@@ -134,6 +134,7 @@ VALIDATION REQUIREMENTS:
134
134
  - Keep going until you find something that matters
135
135
  - A vulnerability is ONLY considered reported when a reporting agent uses create_vulnerability_report with full details. Mentions in agent_finish, finish_scan, or generic messages are NOT sufficient
136
136
  - Do NOT patch/fix before reporting: first create the vulnerability report via create_vulnerability_report (by the reporting agent). Only after reporting is completed should fixing/patching proceed
137
+ - DEDUPLICATION: The create_vulnerability_report tool uses LLM-based deduplication. If it rejects your report as a duplicate, DO NOT attempt to re-submit the same vulnerability. Accept the rejection and move on to testing other areas. The vulnerability has already been reported by another agent
137
138
  </execution_guidelines>
138
139
 
139
140
  <vulnerability_focus>
@@ -263,25 +264,25 @@ CRITICAL RULES:
263
264
  - **ONE AGENT = ONE TASK** - Don't let agents do multiple unrelated jobs
264
265
  - **SPAWN REACTIVELY** - Create new agents based on what you discover
265
266
  - **ONLY REPORTING AGENTS** can use create_vulnerability_report tool
266
- - **AGENT SPECIALIZATION MANDATORY** - Each agent must be highly specialized; prefer 1–3 prompt modules, up to 5 for complex contexts
267
+ - **AGENT SPECIALIZATION MANDATORY** - Each agent must be highly specialized; prefer 1–3 skills, up to 5 for complex contexts
267
268
  - **NO GENERIC AGENTS** - Avoid creating broad, multi-purpose agents that dilute focus
268
269
 
269
270
  AGENT SPECIALIZATION EXAMPLES:
270
271
 
271
272
  GOOD SPECIALIZATION:
272
- - "SQLi Validation Agent" with prompt_modules: sql_injection
273
- - "XSS Discovery Agent" with prompt_modules: xss
274
- - "Auth Testing Agent" with prompt_modules: authentication_jwt, business_logic
275
- - "SSRF + XXE Agent" with prompt_modules: ssrf, xxe, rce (related attack vectors)
273
+ - "SQLi Validation Agent" with skills: sql_injection
274
+ - "XSS Discovery Agent" with skills: xss
275
+ - "Auth Testing Agent" with skills: authentication_jwt, business_logic
276
+ - "SSRF + XXE Agent" with skills: ssrf, xxe, rce (related attack vectors)
276
277
 
277
278
  BAD SPECIALIZATION:
278
- - "General Web Testing Agent" with prompt_modules: sql_injection, xss, csrf, ssrf, authentication_jwt (too broad)
279
- - "Everything Agent" with prompt_modules: all available modules (completely unfocused)
280
- - Any agent with more than 5 prompt modules (violates constraints)
279
+ - "General Web Testing Agent" with skills: sql_injection, xss, csrf, ssrf, authentication_jwt (too broad)
280
+ - "Everything Agent" with skills: all available skills (completely unfocused)
281
+ - Any agent with more than 5 skills (violates constraints)
281
282
 
282
283
  FOCUS PRINCIPLES:
283
284
  - Each agent should have deep expertise in 1-3 related vulnerability types
284
- - Agents with single modules have the deepest specialization
285
+ - Agents with single skills have the deepest specialization
285
286
  - Related vulnerabilities (like SSRF+XXE or Auth+Business Logic) can be combined
286
287
  - Never create "kitchen sink" agents that try to do everything
287
288
 
@@ -300,36 +301,39 @@ PERSISTENCE IS MANDATORY:
300
301
  </multi_agent_system>
301
302
 
302
303
  <tool_usage>
303
- Tool calls use XML format:
304
+ Tool call format:
304
305
  <function=tool_name>
305
306
  <parameter=param_name>value</parameter>
306
307
  </function>
307
308
 
308
309
  CRITICAL RULES:
309
310
  0. While active in the agent loop, EVERY message you output MUST be a single tool call. Do not send plain text-only responses.
310
- 1. One tool call per message
311
+ 1. Exactly one tool call per message — never include more than one <function>...</function> block in a single LLM message.
311
312
  2. Tool call must be last in message
312
- 3. End response after </function> tag. It's your stop word. Do not continue after it.
313
- 4. Use ONLY the exact XML format shown above. NEVER use JSON/YAML/INI or any other syntax for tools or parameters.
314
- 5. Tool names must match exactly the tool "name" defined (no module prefixes, dots, or variants).
313
+ 3. EVERY tool call MUST end with </function>. This is MANDATORY. Never omit the closing tag. End your response immediately after </function>.
314
+ 4. Use ONLY the exact format shown above. NEVER use JSON/YAML/INI or any other syntax for tools or parameters.
315
+ 5. When sending ANY multi-line content in tool parameters, use real newlines (actual line breaks). Do NOT emit literal "\n" sequences. Literal "\n" instead of real line breaks will cause tools to fail.
316
+ 6. Tool names must match exactly the tool "name" defined (no module prefixes, dots, or variants).
315
317
  - Correct: <function=think> ... </function>
316
318
  - Incorrect: <thinking_tools.think> ... </function>
317
319
  - Incorrect: <think> ... </think>
318
320
  - Incorrect: {"think": {...}}
319
- 6. Parameters must use <parameter=param_name>value</parameter> exactly. Do NOT pass parameters as JSON or key:value lines. Do NOT add quotes/braces around values.
320
- 7. Do NOT wrap tool calls in markdown/code fences or add any text before or after the tool block.
321
+ 7. Parameters must use <parameter=param_name>value</parameter> exactly. Do NOT pass parameters as JSON or key:value lines. Do NOT add quotes/braces around values.
322
+ 8. Do NOT wrap tool calls in markdown/code fences or add any text before or after the tool block.
321
323
 
322
324
  Example (agent creation tool):
323
325
  <function=create_agent>
324
326
  <parameter=task>Perform targeted XSS testing on the search endpoint</parameter>
325
327
  <parameter=name>XSS Discovery Agent</parameter>
326
- <parameter=prompt_modules>xss</parameter>
328
+ <parameter=skills>xss</parameter>
327
329
  </function>
328
330
 
329
331
  SPRAYING EXECUTION NOTE:
330
332
  - When performing large payload sprays or fuzzing, encapsulate the entire spraying loop inside a single python or terminal tool call (e.g., a Python script using asyncio/aiohttp). Do not issue one tool call per payload.
331
333
  - Favor batch-mode CLI tools (sqlmap, ffuf, nuclei, zaproxy, arjun) where appropriate and check traffic via the proxy when beneficial
332
334
 
335
+ REMINDER: Always close each tool call with </function> before going into the next. Incomplete tool calls will fail.
336
+
333
337
  {{ get_tools_prompt() }}
334
338
  </tool_usage>
335
339
 
@@ -392,12 +396,12 @@ Directories:
392
396
  Default user: pentester (sudo available)
393
397
  </environment>
394
398
 
395
- {% if loaded_module_names %}
399
+ {% if loaded_skill_names %}
396
400
  <specialized_knowledge>
397
- {# Dynamic prompt modules loaded based on agent specialization #}
401
+ {# Dynamic skills loaded based on agent specialization #}
398
402
 
399
- {% for module_name in loaded_module_names %}
400
- {{ get_module(module_name) }}
403
+ {% for skill_name in loaded_skill_names %}
404
+ {{ get_skill(skill_name) }}
401
405
 
402
406
  {% endfor %}
403
407
  </specialized_knowledge>
@@ -1,7 +1,6 @@
1
1
  import asyncio
2
2
  import contextlib
3
3
  import logging
4
- from pathlib import Path
5
4
  from typing import TYPE_CHECKING, Any, Optional
6
5
 
7
6
 
@@ -16,7 +15,9 @@ from jinja2 import (
16
15
 
17
16
  from strix.llm import LLM, LLMConfig, LLMRequestFailedError
18
17
  from strix.llm.utils import clean_content
18
+ from strix.runtime import SandboxInitializationError
19
19
  from strix.tools import process_tool_invocations
20
+ from strix.utils.resource_paths import get_strix_resource_path
20
21
 
21
22
  from .state import AgentState
22
23
 
@@ -34,8 +35,7 @@ class AgentMeta(type):
34
35
  if name == "BaseAgent":
35
36
  return new_cls
36
37
 
37
- agents_dir = Path(__file__).parent
38
- prompt_dir = agents_dir / name
38
+ prompt_dir = get_strix_resource_path("agents", name)
39
39
 
40
40
  new_cls.agent_name = name
41
41
  new_cls.jinja_env = Environment(
@@ -65,20 +65,21 @@ class BaseAgent(metaclass=AgentMeta):
65
65
  self.llm_config = config.get("llm_config", self.default_llm_config)
66
66
  if self.llm_config is None:
67
67
  raise ValueError("llm_config is required but not provided")
68
- self.llm = LLM(self.llm_config, agent_name=self.agent_name)
69
-
70
68
  state_from_config = config.get("state")
71
69
  if state_from_config is not None:
72
70
  self.state = state_from_config
73
71
  else:
74
72
  self.state = AgentState(
75
- agent_name=self.agent_name,
73
+ agent_name="Root Agent",
76
74
  max_iterations=self.max_iterations,
77
75
  )
78
76
 
77
+ self.llm = LLM(self.llm_config, agent_name=self.agent_name)
78
+
79
79
  with contextlib.suppress(Exception):
80
- self.llm.set_agent_identity(self.agent_name, self.state.agent_id)
80
+ self.llm.set_agent_identity(self.state.agent_name, self.state.agent_id)
81
81
  self._current_task: asyncio.Task[Any] | None = None
82
+ self._force_stop = False
82
83
 
83
84
  from strix.telemetry.tracer import get_global_tracer
84
85
 
@@ -145,19 +146,22 @@ class BaseAgent(metaclass=AgentMeta):
145
146
  if self.state.parent_id is None and agents_graph_actions._root_agent_id is None:
146
147
  agents_graph_actions._root_agent_id = self.state.agent_id
147
148
 
148
- def cancel_current_execution(self) -> None:
149
- if self._current_task and not self._current_task.done():
150
- self._current_task.cancel()
151
- self._current_task = None
152
-
153
149
  async def agent_loop(self, task: str) -> dict[str, Any]: # noqa: PLR0912, PLR0915
154
- await self._initialize_sandbox_and_state(task)
155
-
156
150
  from strix.telemetry.tracer import get_global_tracer
157
151
 
158
152
  tracer = get_global_tracer()
159
153
 
154
+ try:
155
+ await self._initialize_sandbox_and_state(task)
156
+ except SandboxInitializationError as e:
157
+ return self._handle_sandbox_error(e, tracer)
158
+
160
159
  while True:
160
+ if self._force_stop:
161
+ self._force_stop = False
162
+ await self._enter_waiting_state(tracer, was_cancelled=True)
163
+ continue
164
+
161
165
  self._check_agent_messages(self.state)
162
166
 
163
167
  if self.state.is_waiting_for_input():
@@ -204,7 +208,11 @@ class BaseAgent(metaclass=AgentMeta):
204
208
  self.state.add_message("user", final_warning_msg)
205
209
 
206
210
  try:
207
- should_finish = await self._process_iteration(tracer)
211
+ iteration_task = asyncio.create_task(self._process_iteration(tracer))
212
+ self._current_task = iteration_task
213
+ should_finish = await iteration_task
214
+ self._current_task = None
215
+
208
216
  if should_finish:
209
217
  if self.non_interactive:
210
218
  self.state.set_completed({"success": True})
@@ -215,43 +223,22 @@ class BaseAgent(metaclass=AgentMeta):
215
223
  continue
216
224
 
217
225
  except asyncio.CancelledError:
226
+ self._current_task = None
227
+ if tracer:
228
+ partial_content = tracer.finalize_streaming_as_interrupted(self.state.agent_id)
229
+ if partial_content and partial_content.strip():
230
+ self.state.add_message(
231
+ "assistant", f"{partial_content}\n\n[ABORTED BY USER]"
232
+ )
218
233
  if self.non_interactive:
219
234
  raise
220
235
  await self._enter_waiting_state(tracer, error_occurred=False, was_cancelled=True)
221
236
  continue
222
237
 
223
238
  except LLMRequestFailedError as e:
224
- error_msg = str(e)
225
- error_details = getattr(e, "details", None)
226
- self.state.add_error(error_msg)
227
-
228
- if self.non_interactive:
229
- self.state.set_completed({"success": False, "error": error_msg})
230
- if tracer:
231
- tracer.update_agent_status(self.state.agent_id, "failed", error_msg)
232
- if error_details:
233
- tracer.log_tool_execution_start(
234
- self.state.agent_id,
235
- "llm_error_details",
236
- {"error": error_msg, "details": error_details},
237
- )
238
- tracer.update_tool_execution(
239
- tracer._next_execution_id - 1, "failed", error_details
240
- )
241
- return {"success": False, "error": error_msg}
242
-
243
- self.state.enter_waiting_state(llm_failed=True)
244
- if tracer:
245
- tracer.update_agent_status(self.state.agent_id, "llm_failed", error_msg)
246
- if error_details:
247
- tracer.log_tool_execution_start(
248
- self.state.agent_id,
249
- "llm_error_details",
250
- {"error": error_msg, "details": error_details},
251
- )
252
- tracer.update_tool_execution(
253
- tracer._next_execution_id - 1, "failed", error_details
254
- )
239
+ result = self._handle_llm_error(e, tracer)
240
+ if result is not None:
241
+ return result
255
242
  continue
256
243
 
257
244
  except (RuntimeError, ValueError, TypeError) as e:
@@ -265,11 +252,12 @@ class BaseAgent(metaclass=AgentMeta):
265
252
  continue
266
253
 
267
254
  async def _wait_for_input(self) -> None:
268
- import asyncio
255
+ if self._force_stop:
256
+ return
269
257
 
270
258
  if self.state.has_waiting_timeout():
271
259
  self.state.resume_from_waiting()
272
- self.state.add_message("assistant", "Waiting timeout reached. Resuming execution.")
260
+ self.state.add_message("user", "Waiting timeout reached. Resuming execution.")
273
261
 
274
262
  from strix.telemetry.tracer import get_global_tracer
275
263
 
@@ -334,16 +322,22 @@ class BaseAgent(metaclass=AgentMeta):
334
322
  if not sandbox_mode and self.state.sandbox_id is None:
335
323
  from strix.runtime import get_runtime
336
324
 
337
- runtime = get_runtime()
338
- sandbox_info = await runtime.create_sandbox(
339
- self.state.agent_id, self.state.sandbox_token, self.local_sources
340
- )
341
- self.state.sandbox_id = sandbox_info["workspace_id"]
342
- self.state.sandbox_token = sandbox_info["auth_token"]
343
- self.state.sandbox_info = sandbox_info
325
+ try:
326
+ runtime = get_runtime()
327
+ sandbox_info = await runtime.create_sandbox(
328
+ self.state.agent_id, self.state.sandbox_token, self.local_sources
329
+ )
330
+ self.state.sandbox_id = sandbox_info["workspace_id"]
331
+ self.state.sandbox_token = sandbox_info["auth_token"]
332
+ self.state.sandbox_info = sandbox_info
333
+
334
+ if "agent_id" in sandbox_info:
335
+ self.state.sandbox_info["agent_id"] = sandbox_info["agent_id"]
336
+ except Exception as e:
337
+ from strix.telemetry import posthog
344
338
 
345
- if "agent_id" in sandbox_info:
346
- self.state.sandbox_info["agent_id"] = sandbox_info["agent_id"]
339
+ posthog.error("sandbox_init_error", str(e))
340
+ raise
347
341
 
348
342
  if not self.state.task:
349
343
  self.state.task = task
@@ -351,9 +345,17 @@ class BaseAgent(metaclass=AgentMeta):
351
345
  self.state.add_message("user", task)
352
346
 
353
347
  async def _process_iteration(self, tracer: Optional["Tracer"]) -> bool:
354
- response = await self.llm.generate(self.state.get_conversation_history())
348
+ final_response = None
349
+
350
+ async for response in self.llm.generate(self.state.get_conversation_history()):
351
+ final_response = response
352
+ if tracer and response.content:
353
+ tracer.update_streaming_content(self.state.agent_id, response.content)
354
+
355
+ if final_response is None:
356
+ return False
355
357
 
356
- content_stripped = (response.content or "").strip()
358
+ content_stripped = (final_response.content or "").strip()
357
359
 
358
360
  if not content_stripped:
359
361
  corrective_message = (
@@ -369,17 +371,19 @@ class BaseAgent(metaclass=AgentMeta):
369
371
  self.state.add_message("user", corrective_message)
370
372
  return False
371
373
 
372
- self.state.add_message("assistant", response.content)
374
+ thinking_blocks = getattr(final_response, "thinking_blocks", None)
375
+ self.state.add_message("assistant", final_response.content, thinking_blocks=thinking_blocks)
373
376
  if tracer:
377
+ tracer.clear_streaming_content(self.state.agent_id)
374
378
  tracer.log_chat_message(
375
- content=clean_content(response.content),
379
+ content=clean_content(final_response.content),
376
380
  role="assistant",
377
381
  agent_id=self.state.agent_id,
378
382
  )
379
383
 
380
384
  actions = (
381
- response.tool_invocations
382
- if hasattr(response, "tool_invocations") and response.tool_invocations
385
+ final_response.tool_invocations
386
+ if hasattr(final_response, "tool_invocations") and final_response.tool_invocations
383
387
  else []
384
388
  )
385
389
 
@@ -420,18 +424,6 @@ class BaseAgent(metaclass=AgentMeta):
420
424
 
421
425
  return False
422
426
 
423
- async def _handle_iteration_error(
424
- self,
425
- error: RuntimeError | ValueError | TypeError | asyncio.CancelledError,
426
- tracer: Optional["Tracer"],
427
- ) -> bool:
428
- error_msg = f"Error in iteration {self.state.iteration}: {error!s}"
429
- logger.exception(error_msg)
430
- self.state.add_error(error_msg)
431
- if tracer:
432
- tracer.update_agent_status(self.state.agent_id, "error")
433
- return True
434
-
435
427
  def _check_agent_messages(self, state: AgentState) -> None: # noqa: PLR0912
436
428
  try:
437
429
  from strix.tools.agents_graph.agents_graph_actions import _agent_graph, _agent_messages
@@ -516,3 +508,95 @@ class BaseAgent(metaclass=AgentMeta):
516
508
  logger = logging.getLogger(__name__)
517
509
  logger.warning(f"Error checking agent messages: {e}")
518
510
  return
511
+
512
+ def _handle_sandbox_error(
513
+ self,
514
+ error: SandboxInitializationError,
515
+ tracer: Optional["Tracer"],
516
+ ) -> dict[str, Any]:
517
+ error_msg = str(error.message)
518
+ error_details = error.details
519
+ self.state.add_error(error_msg)
520
+
521
+ if self.non_interactive:
522
+ self.state.set_completed({"success": False, "error": error_msg})
523
+ if tracer:
524
+ tracer.update_agent_status(self.state.agent_id, "failed", error_msg)
525
+ if error_details:
526
+ exec_id = tracer.log_tool_execution_start(
527
+ self.state.agent_id,
528
+ "sandbox_error_details",
529
+ {"error": error_msg, "details": error_details},
530
+ )
531
+ tracer.update_tool_execution(exec_id, "failed", {"details": error_details})
532
+ return {"success": False, "error": error_msg, "details": error_details}
533
+
534
+ self.state.enter_waiting_state()
535
+ if tracer:
536
+ tracer.update_agent_status(self.state.agent_id, "sandbox_failed", error_msg)
537
+ if error_details:
538
+ exec_id = tracer.log_tool_execution_start(
539
+ self.state.agent_id,
540
+ "sandbox_error_details",
541
+ {"error": error_msg, "details": error_details},
542
+ )
543
+ tracer.update_tool_execution(exec_id, "failed", {"details": error_details})
544
+
545
+ return {"success": False, "error": error_msg, "details": error_details}
546
+
547
+ def _handle_llm_error(
548
+ self,
549
+ error: LLMRequestFailedError,
550
+ tracer: Optional["Tracer"],
551
+ ) -> dict[str, Any] | None:
552
+ error_msg = str(error)
553
+ error_details = getattr(error, "details", None)
554
+ self.state.add_error(error_msg)
555
+
556
+ if self.non_interactive:
557
+ self.state.set_completed({"success": False, "error": error_msg})
558
+ if tracer:
559
+ tracer.update_agent_status(self.state.agent_id, "failed", error_msg)
560
+ if error_details:
561
+ exec_id = tracer.log_tool_execution_start(
562
+ self.state.agent_id,
563
+ "llm_error_details",
564
+ {"error": error_msg, "details": error_details},
565
+ )
566
+ tracer.update_tool_execution(exec_id, "failed", {"details": error_details})
567
+ return {"success": False, "error": error_msg}
568
+
569
+ self.state.enter_waiting_state(llm_failed=True)
570
+ if tracer:
571
+ tracer.update_agent_status(self.state.agent_id, "llm_failed", error_msg)
572
+ if error_details:
573
+ exec_id = tracer.log_tool_execution_start(
574
+ self.state.agent_id,
575
+ "llm_error_details",
576
+ {"error": error_msg, "details": error_details},
577
+ )
578
+ tracer.update_tool_execution(exec_id, "failed", {"details": error_details})
579
+
580
+ return None
581
+
582
+ async def _handle_iteration_error(
583
+ self,
584
+ error: RuntimeError | ValueError | TypeError | asyncio.CancelledError,
585
+ tracer: Optional["Tracer"],
586
+ ) -> bool:
587
+ error_msg = f"Error in iteration {self.state.iteration}: {error!s}"
588
+ logger.exception(error_msg)
589
+ self.state.add_error(error_msg)
590
+ if tracer:
591
+ tracer.update_agent_status(self.state.agent_id, "error")
592
+ return True
593
+
594
+ def cancel_current_execution(self) -> None:
595
+ self._force_stop = True
596
+ if self._current_task and not self._current_task.done():
597
+ try:
598
+ loop = self._current_task.get_loop()
599
+ loop.call_soon_threadsafe(self._current_task.cancel)
600
+ except RuntimeError:
601
+ self._current_task.cancel()
602
+ self._current_task = None
strix/agents/state.py CHANGED
@@ -43,8 +43,11 @@ class AgentState(BaseModel):
43
43
  self.iteration += 1
44
44
  self.last_updated = datetime.now(UTC).isoformat()
45
45
 
46
- def add_message(self, role: str, content: Any) -> None:
47
- self.messages.append({"role": role, "content": content})
46
+ def add_message(self, role: str, content: Any, thinking_blocks: list[dict[str, Any]] | None = None) -> None:
47
+ message = {"role": role, "content": content}
48
+ if thinking_blocks:
49
+ message["thinking_blocks"] = thinking_blocks
50
+ self.messages.append(message)
48
51
  self.last_updated = datetime.now(UTC).isoformat()
49
52
 
50
53
  def add_action(self, action: dict[str, Any]) -> None:
@@ -0,0 +1,12 @@
1
+ from strix.config.config import (
2
+ Config,
3
+ apply_saved_config,
4
+ save_current_config,
5
+ )
6
+
7
+
8
+ __all__ = [
9
+ "Config",
10
+ "apply_saved_config",
11
+ "save_current_config",
12
+ ]