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,518 @@
1
+ import asyncio
2
+ import contextlib
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING, Any, Optional
6
+
7
+
8
+ if TYPE_CHECKING:
9
+ from strix.telemetry.tracer import Tracer
10
+
11
+ from jinja2 import (
12
+ Environment,
13
+ FileSystemLoader,
14
+ select_autoescape,
15
+ )
16
+
17
+ from strix.llm import LLM, LLMConfig, LLMRequestFailedError
18
+ from strix.llm.utils import clean_content
19
+ from strix.tools import process_tool_invocations
20
+
21
+ from .state import AgentState
22
+
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class AgentMeta(type):
28
+ agent_name: str
29
+ jinja_env: Environment
30
+
31
+ def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> type:
32
+ new_cls = super().__new__(cls, name, bases, attrs)
33
+
34
+ if name == "BaseAgent":
35
+ return new_cls
36
+
37
+ agents_dir = Path(__file__).parent
38
+ prompt_dir = agents_dir / name
39
+
40
+ new_cls.agent_name = name
41
+ new_cls.jinja_env = Environment(
42
+ loader=FileSystemLoader(prompt_dir),
43
+ autoescape=select_autoescape(enabled_extensions=(), default_for_string=False),
44
+ )
45
+
46
+ return new_cls
47
+
48
+
49
+ class BaseAgent(metaclass=AgentMeta):
50
+ max_iterations = 300
51
+ agent_name: str = ""
52
+ jinja_env: Environment
53
+ default_llm_config: LLMConfig | None = None
54
+
55
+ def __init__(self, config: dict[str, Any]):
56
+ self.config = config
57
+
58
+ self.local_sources = config.get("local_sources", [])
59
+ self.non_interactive = config.get("non_interactive", False)
60
+
61
+ if "max_iterations" in config:
62
+ self.max_iterations = config["max_iterations"]
63
+
64
+ self.llm_config_name = config.get("llm_config_name", "default")
65
+ self.llm_config = config.get("llm_config", self.default_llm_config)
66
+ if self.llm_config is None:
67
+ raise ValueError("llm_config is required but not provided")
68
+ self.llm = LLM(self.llm_config, agent_name=self.agent_name)
69
+
70
+ state_from_config = config.get("state")
71
+ if state_from_config is not None:
72
+ self.state = state_from_config
73
+ else:
74
+ self.state = AgentState(
75
+ agent_name=self.agent_name,
76
+ max_iterations=self.max_iterations,
77
+ )
78
+
79
+ with contextlib.suppress(Exception):
80
+ self.llm.set_agent_identity(self.agent_name, self.state.agent_id)
81
+ self._current_task: asyncio.Task[Any] | None = None
82
+
83
+ from strix.telemetry.tracer import get_global_tracer
84
+
85
+ tracer = get_global_tracer()
86
+ if tracer:
87
+ tracer.log_agent_creation(
88
+ agent_id=self.state.agent_id,
89
+ name=self.state.agent_name,
90
+ task=self.state.task,
91
+ parent_id=self.state.parent_id,
92
+ )
93
+ if self.state.parent_id is None:
94
+ scan_config = tracer.scan_config or {}
95
+ exec_id = tracer.log_tool_execution_start(
96
+ agent_id=self.state.agent_id,
97
+ tool_name="scan_start_info",
98
+ args=scan_config,
99
+ )
100
+ tracer.update_tool_execution(execution_id=exec_id, status="completed", result={})
101
+
102
+ else:
103
+ exec_id = tracer.log_tool_execution_start(
104
+ agent_id=self.state.agent_id,
105
+ tool_name="subagent_start_info",
106
+ args={
107
+ "name": self.state.agent_name,
108
+ "task": self.state.task,
109
+ "parent_id": self.state.parent_id,
110
+ },
111
+ )
112
+ tracer.update_tool_execution(execution_id=exec_id, status="completed", result={})
113
+
114
+ self._add_to_agents_graph()
115
+
116
+ def _add_to_agents_graph(self) -> None:
117
+ from strix.tools.agents_graph import agents_graph_actions
118
+
119
+ node = {
120
+ "id": self.state.agent_id,
121
+ "name": self.state.agent_name,
122
+ "task": self.state.task,
123
+ "status": "running",
124
+ "parent_id": self.state.parent_id,
125
+ "created_at": self.state.start_time,
126
+ "finished_at": None,
127
+ "result": None,
128
+ "llm_config": self.llm_config_name,
129
+ "agent_type": self.__class__.__name__,
130
+ "state": self.state.model_dump(),
131
+ }
132
+ agents_graph_actions._agent_graph["nodes"][self.state.agent_id] = node
133
+
134
+ agents_graph_actions._agent_instances[self.state.agent_id] = self
135
+ agents_graph_actions._agent_states[self.state.agent_id] = self.state
136
+
137
+ if self.state.parent_id:
138
+ agents_graph_actions._agent_graph["edges"].append(
139
+ {"from": self.state.parent_id, "to": self.state.agent_id, "type": "delegation"}
140
+ )
141
+
142
+ if self.state.agent_id not in agents_graph_actions._agent_messages:
143
+ agents_graph_actions._agent_messages[self.state.agent_id] = []
144
+
145
+ if self.state.parent_id is None and agents_graph_actions._root_agent_id is None:
146
+ agents_graph_actions._root_agent_id = self.state.agent_id
147
+
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
+ async def agent_loop(self, task: str) -> dict[str, Any]: # noqa: PLR0912, PLR0915
154
+ await self._initialize_sandbox_and_state(task)
155
+
156
+ from strix.telemetry.tracer import get_global_tracer
157
+
158
+ tracer = get_global_tracer()
159
+
160
+ while True:
161
+ self._check_agent_messages(self.state)
162
+
163
+ if self.state.is_waiting_for_input():
164
+ await self._wait_for_input()
165
+ continue
166
+
167
+ if self.state.should_stop():
168
+ if self.non_interactive:
169
+ return self.state.final_result or {}
170
+ await self._enter_waiting_state(tracer)
171
+ continue
172
+
173
+ if self.state.llm_failed:
174
+ await self._wait_for_input()
175
+ continue
176
+
177
+ self.state.increment_iteration()
178
+
179
+ if (
180
+ self.state.is_approaching_max_iterations()
181
+ and not self.state.max_iterations_warning_sent
182
+ ):
183
+ self.state.max_iterations_warning_sent = True
184
+ remaining = self.state.max_iterations - self.state.iteration
185
+ warning_msg = (
186
+ f"URGENT: You are approaching the maximum iteration limit. "
187
+ f"Current: {self.state.iteration}/{self.state.max_iterations} "
188
+ f"({remaining} iterations remaining). "
189
+ f"Please prioritize completing your required task(s) and calling "
190
+ f"the appropriate finish tool (finish_scan for root agent, "
191
+ f"agent_finish for sub-agents) as soon as possible."
192
+ )
193
+ self.state.add_message("user", warning_msg)
194
+
195
+ if self.state.iteration == self.state.max_iterations - 3:
196
+ final_warning_msg = (
197
+ "CRITICAL: You have only 3 iterations left! "
198
+ "Your next message MUST be the tool call to the appropriate "
199
+ "finish tool: finish_scan if you are the root agent, or "
200
+ "agent_finish if you are a sub-agent. "
201
+ "No other actions should be taken except finishing your work "
202
+ "immediately."
203
+ )
204
+ self.state.add_message("user", final_warning_msg)
205
+
206
+ try:
207
+ should_finish = await self._process_iteration(tracer)
208
+ if should_finish:
209
+ if self.non_interactive:
210
+ self.state.set_completed({"success": True})
211
+ if tracer:
212
+ tracer.update_agent_status(self.state.agent_id, "completed")
213
+ return self.state.final_result or {}
214
+ await self._enter_waiting_state(tracer, task_completed=True)
215
+ continue
216
+
217
+ except asyncio.CancelledError:
218
+ if self.non_interactive:
219
+ raise
220
+ await self._enter_waiting_state(tracer, error_occurred=False, was_cancelled=True)
221
+ continue
222
+
223
+ 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
+ )
255
+ continue
256
+
257
+ except (RuntimeError, ValueError, TypeError) as e:
258
+ if not await self._handle_iteration_error(e, tracer):
259
+ if self.non_interactive:
260
+ self.state.set_completed({"success": False, "error": str(e)})
261
+ if tracer:
262
+ tracer.update_agent_status(self.state.agent_id, "failed")
263
+ raise
264
+ await self._enter_waiting_state(tracer, error_occurred=True)
265
+ continue
266
+
267
+ async def _wait_for_input(self) -> None:
268
+ import asyncio
269
+
270
+ if self.state.has_waiting_timeout():
271
+ self.state.resume_from_waiting()
272
+ self.state.add_message("assistant", "Waiting timeout reached. Resuming execution.")
273
+
274
+ from strix.telemetry.tracer import get_global_tracer
275
+
276
+ tracer = get_global_tracer()
277
+ if tracer:
278
+ tracer.update_agent_status(self.state.agent_id, "running")
279
+
280
+ try:
281
+ from strix.tools.agents_graph.agents_graph_actions import _agent_graph
282
+
283
+ if self.state.agent_id in _agent_graph["nodes"]:
284
+ _agent_graph["nodes"][self.state.agent_id]["status"] = "running"
285
+ except (ImportError, KeyError):
286
+ pass
287
+
288
+ return
289
+
290
+ await asyncio.sleep(0.5)
291
+
292
+ async def _enter_waiting_state(
293
+ self,
294
+ tracer: Optional["Tracer"],
295
+ task_completed: bool = False,
296
+ error_occurred: bool = False,
297
+ was_cancelled: bool = False,
298
+ ) -> None:
299
+ self.state.enter_waiting_state()
300
+
301
+ if tracer:
302
+ if task_completed:
303
+ tracer.update_agent_status(self.state.agent_id, "completed")
304
+ elif error_occurred:
305
+ tracer.update_agent_status(self.state.agent_id, "error")
306
+ elif was_cancelled:
307
+ tracer.update_agent_status(self.state.agent_id, "stopped")
308
+ else:
309
+ tracer.update_agent_status(self.state.agent_id, "stopped")
310
+
311
+ if task_completed:
312
+ self.state.add_message(
313
+ "assistant",
314
+ "Task completed. I'm now waiting for follow-up instructions or new tasks.",
315
+ )
316
+ elif error_occurred:
317
+ self.state.add_message(
318
+ "assistant", "An error occurred. I'm now waiting for new instructions."
319
+ )
320
+ elif was_cancelled:
321
+ self.state.add_message(
322
+ "assistant", "Execution was cancelled. I'm now waiting for new instructions."
323
+ )
324
+ else:
325
+ self.state.add_message(
326
+ "assistant",
327
+ "Execution paused. I'm now waiting for new instructions or any updates.",
328
+ )
329
+
330
+ async def _initialize_sandbox_and_state(self, task: str) -> None:
331
+ import os
332
+
333
+ sandbox_mode = os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "true"
334
+ if not sandbox_mode and self.state.sandbox_id is None:
335
+ from strix.runtime import get_runtime
336
+
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
344
+
345
+ if "agent_id" in sandbox_info:
346
+ self.state.sandbox_info["agent_id"] = sandbox_info["agent_id"]
347
+
348
+ if not self.state.task:
349
+ self.state.task = task
350
+
351
+ self.state.add_message("user", task)
352
+
353
+ async def _process_iteration(self, tracer: Optional["Tracer"]) -> bool:
354
+ response = await self.llm.generate(self.state.get_conversation_history())
355
+
356
+ content_stripped = (response.content or "").strip()
357
+
358
+ if not content_stripped:
359
+ corrective_message = (
360
+ "You MUST NOT respond with empty messages. "
361
+ "If you currently have nothing to do or say, use an appropriate tool instead:\n"
362
+ "- Use agents_graph_actions.wait_for_message to wait for messages "
363
+ "from user or other agents\n"
364
+ "- Use agents_graph_actions.agent_finish if you are a sub-agent "
365
+ "and your task is complete\n"
366
+ "- Use finish_actions.finish_scan if you are the root/main agent "
367
+ "and the scan is complete"
368
+ )
369
+ self.state.add_message("user", corrective_message)
370
+ return False
371
+
372
+ self.state.add_message("assistant", response.content)
373
+ if tracer:
374
+ tracer.log_chat_message(
375
+ content=clean_content(response.content),
376
+ role="assistant",
377
+ agent_id=self.state.agent_id,
378
+ )
379
+
380
+ actions = (
381
+ response.tool_invocations
382
+ if hasattr(response, "tool_invocations") and response.tool_invocations
383
+ else []
384
+ )
385
+
386
+ if actions:
387
+ return await self._execute_actions(actions, tracer)
388
+
389
+ return False
390
+
391
+ async def _execute_actions(self, actions: list[Any], tracer: Optional["Tracer"]) -> bool:
392
+ """Execute actions and return True if agent should finish."""
393
+ for action in actions:
394
+ self.state.add_action(action)
395
+
396
+ conversation_history = self.state.get_conversation_history()
397
+
398
+ tool_task = asyncio.create_task(
399
+ process_tool_invocations(actions, conversation_history, self.state)
400
+ )
401
+ self._current_task = tool_task
402
+
403
+ try:
404
+ should_agent_finish = await tool_task
405
+ self._current_task = None
406
+ except asyncio.CancelledError:
407
+ self._current_task = None
408
+ self.state.add_error("Tool execution cancelled by user")
409
+ raise
410
+
411
+ self.state.messages = conversation_history
412
+
413
+ if should_agent_finish:
414
+ self.state.set_completed({"success": True})
415
+ if tracer:
416
+ tracer.update_agent_status(self.state.agent_id, "completed")
417
+ if self.non_interactive and self.state.parent_id is None:
418
+ return True
419
+ return True
420
+
421
+ return False
422
+
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
+ def _check_agent_messages(self, state: AgentState) -> None: # noqa: PLR0912
436
+ try:
437
+ from strix.tools.agents_graph.agents_graph_actions import _agent_graph, _agent_messages
438
+
439
+ agent_id = state.agent_id
440
+ if not agent_id or agent_id not in _agent_messages:
441
+ return
442
+
443
+ messages = _agent_messages[agent_id]
444
+ if messages:
445
+ has_new_messages = False
446
+ for message in messages:
447
+ if not message.get("read", False):
448
+ sender_id = message.get("from")
449
+
450
+ if state.is_waiting_for_input():
451
+ if state.llm_failed:
452
+ if sender_id == "user":
453
+ state.resume_from_waiting()
454
+ has_new_messages = True
455
+
456
+ from strix.telemetry.tracer import get_global_tracer
457
+
458
+ tracer = get_global_tracer()
459
+ if tracer:
460
+ tracer.update_agent_status(state.agent_id, "running")
461
+ else:
462
+ state.resume_from_waiting()
463
+ has_new_messages = True
464
+
465
+ from strix.telemetry.tracer import get_global_tracer
466
+
467
+ tracer = get_global_tracer()
468
+ if tracer:
469
+ tracer.update_agent_status(state.agent_id, "running")
470
+
471
+ if sender_id == "user":
472
+ sender_name = "User"
473
+ state.add_message("user", message.get("content", ""))
474
+ else:
475
+ if sender_id and sender_id in _agent_graph.get("nodes", {}):
476
+ sender_name = _agent_graph["nodes"][sender_id]["name"]
477
+
478
+ message_content = f"""<inter_agent_message>
479
+ <delivery_notice>
480
+ <important>You have received a message from another agent. You should acknowledge
481
+ this message and respond appropriately based on its content. However, DO NOT echo
482
+ back or repeat the entire message structure in your response. Simply process the
483
+ content and respond naturally as/if needed.</important>
484
+ </delivery_notice>
485
+ <sender>
486
+ <agent_name>{sender_name}</agent_name>
487
+ <agent_id>{sender_id}</agent_id>
488
+ </sender>
489
+ <message_metadata>
490
+ <type>{message.get("message_type", "information")}</type>
491
+ <priority>{message.get("priority", "normal")}</priority>
492
+ <timestamp>{message.get("timestamp", "")}</timestamp>
493
+ </message_metadata>
494
+ <content>
495
+ {message.get("content", "")}
496
+ </content>
497
+ <delivery_info>
498
+ <note>This message was delivered during your task execution.
499
+ Please acknowledge and respond if needed.</note>
500
+ </delivery_info>
501
+ </inter_agent_message>"""
502
+ state.add_message("user", message_content.strip())
503
+
504
+ message["read"] = True
505
+
506
+ if has_new_messages and not state.is_waiting_for_input():
507
+ from strix.telemetry.tracer import get_global_tracer
508
+
509
+ tracer = get_global_tracer()
510
+ if tracer:
511
+ tracer.update_agent_status(agent_id, "running")
512
+
513
+ except (AttributeError, KeyError, TypeError) as e:
514
+ import logging
515
+
516
+ logger = logging.getLogger(__name__)
517
+ logger.warning(f"Error checking agent messages: {e}")
518
+ return
strix/agents/state.py ADDED
@@ -0,0 +1,163 @@
1
+ import uuid
2
+ from datetime import UTC, datetime
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ def _generate_agent_id() -> str:
9
+ return f"agent_{uuid.uuid4().hex[:8]}"
10
+
11
+
12
+ class AgentState(BaseModel):
13
+ agent_id: str = Field(default_factory=_generate_agent_id)
14
+ agent_name: str = "Strix Agent"
15
+ parent_id: str | None = None
16
+ sandbox_id: str | None = None
17
+ sandbox_token: str | None = None
18
+ sandbox_info: dict[str, Any] | None = None
19
+
20
+ task: str = ""
21
+ iteration: int = 0
22
+ max_iterations: int = 300
23
+ completed: bool = False
24
+ stop_requested: bool = False
25
+ waiting_for_input: bool = False
26
+ llm_failed: bool = False
27
+ waiting_start_time: datetime | None = None
28
+ final_result: dict[str, Any] | None = None
29
+ max_iterations_warning_sent: bool = False
30
+
31
+ messages: list[dict[str, Any]] = Field(default_factory=list)
32
+ context: dict[str, Any] = Field(default_factory=dict)
33
+
34
+ start_time: str = Field(default_factory=lambda: datetime.now(UTC).isoformat())
35
+ last_updated: str = Field(default_factory=lambda: datetime.now(UTC).isoformat())
36
+
37
+ actions_taken: list[dict[str, Any]] = Field(default_factory=list)
38
+ observations: list[dict[str, Any]] = Field(default_factory=list)
39
+
40
+ errors: list[str] = Field(default_factory=list)
41
+
42
+ def increment_iteration(self) -> None:
43
+ self.iteration += 1
44
+ self.last_updated = datetime.now(UTC).isoformat()
45
+
46
+ def add_message(self, role: str, content: Any) -> None:
47
+ self.messages.append({"role": role, "content": content})
48
+ self.last_updated = datetime.now(UTC).isoformat()
49
+
50
+ def add_action(self, action: dict[str, Any]) -> None:
51
+ self.actions_taken.append(
52
+ {
53
+ "iteration": self.iteration,
54
+ "timestamp": datetime.now(UTC).isoformat(),
55
+ "action": action,
56
+ }
57
+ )
58
+
59
+ def add_observation(self, observation: dict[str, Any]) -> None:
60
+ self.observations.append(
61
+ {
62
+ "iteration": self.iteration,
63
+ "timestamp": datetime.now(UTC).isoformat(),
64
+ "observation": observation,
65
+ }
66
+ )
67
+
68
+ def add_error(self, error: str) -> None:
69
+ self.errors.append(f"Iteration {self.iteration}: {error}")
70
+ self.last_updated = datetime.now(UTC).isoformat()
71
+
72
+ def update_context(self, key: str, value: Any) -> None:
73
+ self.context[key] = value
74
+ self.last_updated = datetime.now(UTC).isoformat()
75
+
76
+ def set_completed(self, final_result: dict[str, Any] | None = None) -> None:
77
+ self.completed = True
78
+ self.final_result = final_result
79
+ self.last_updated = datetime.now(UTC).isoformat()
80
+
81
+ def request_stop(self) -> None:
82
+ self.stop_requested = True
83
+ self.last_updated = datetime.now(UTC).isoformat()
84
+
85
+ def should_stop(self) -> bool:
86
+ return self.stop_requested or self.completed or self.has_reached_max_iterations()
87
+
88
+ def is_waiting_for_input(self) -> bool:
89
+ return self.waiting_for_input
90
+
91
+ def enter_waiting_state(self, llm_failed: bool = False) -> None:
92
+ self.waiting_for_input = True
93
+ self.waiting_start_time = datetime.now(UTC)
94
+ self.llm_failed = llm_failed
95
+ self.last_updated = datetime.now(UTC).isoformat()
96
+
97
+ def resume_from_waiting(self, new_task: str | None = None) -> None:
98
+ self.waiting_for_input = False
99
+ self.waiting_start_time = None
100
+ self.stop_requested = False
101
+ self.completed = False
102
+ self.llm_failed = False
103
+ if new_task:
104
+ self.task = new_task
105
+ self.last_updated = datetime.now(UTC).isoformat()
106
+
107
+ def has_reached_max_iterations(self) -> bool:
108
+ return self.iteration >= self.max_iterations
109
+
110
+ def is_approaching_max_iterations(self, threshold: float = 0.85) -> bool:
111
+ return self.iteration >= int(self.max_iterations * threshold)
112
+
113
+ def has_waiting_timeout(self) -> bool:
114
+ if not self.waiting_for_input or not self.waiting_start_time:
115
+ return False
116
+
117
+ if (
118
+ self.stop_requested
119
+ or self.llm_failed
120
+ or self.completed
121
+ or self.has_reached_max_iterations()
122
+ ):
123
+ return False
124
+
125
+ elapsed = (datetime.now(UTC) - self.waiting_start_time).total_seconds()
126
+ return elapsed > 600
127
+
128
+ def has_empty_last_messages(self, count: int = 3) -> bool:
129
+ if len(self.messages) < count:
130
+ return False
131
+
132
+ last_messages = self.messages[-count:]
133
+
134
+ for message in last_messages:
135
+ content = message.get("content", "")
136
+ if isinstance(content, str) and content.strip():
137
+ return False
138
+
139
+ return True
140
+
141
+ def get_conversation_history(self) -> list[dict[str, Any]]:
142
+ return self.messages
143
+
144
+ def get_execution_summary(self) -> dict[str, Any]:
145
+ return {
146
+ "agent_id": self.agent_id,
147
+ "agent_name": self.agent_name,
148
+ "parent_id": self.parent_id,
149
+ "sandbox_id": self.sandbox_id,
150
+ "sandbox_info": self.sandbox_info,
151
+ "task": self.task,
152
+ "iteration": self.iteration,
153
+ "max_iterations": self.max_iterations,
154
+ "completed": self.completed,
155
+ "final_result": self.final_result,
156
+ "start_time": self.start_time,
157
+ "last_updated": self.last_updated,
158
+ "total_actions": len(self.actions_taken),
159
+ "total_observations": len(self.observations),
160
+ "total_errors": len(self.errors),
161
+ "has_errors": len(self.errors) > 0,
162
+ "max_iterations_reached": self.has_reached_max_iterations() and not self.completed,
163
+ }
@@ -0,0 +1,4 @@
1
+ from .main import main
2
+
3
+
4
+ __all__ = ["main"]