strix-agent 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. strix/__init__.py +0 -0
  2. strix/agents/StrixAgent/__init__.py +4 -0
  3. strix/agents/StrixAgent/strix_agent.py +60 -0
  4. strix/agents/StrixAgent/system_prompt.jinja +504 -0
  5. strix/agents/__init__.py +10 -0
  6. strix/agents/base_agent.py +394 -0
  7. strix/agents/state.py +139 -0
  8. strix/cli/__init__.py +4 -0
  9. strix/cli/app.py +1124 -0
  10. strix/cli/assets/cli.tcss +680 -0
  11. strix/cli/main.py +542 -0
  12. strix/cli/tool_components/__init__.py +39 -0
  13. strix/cli/tool_components/agents_graph_renderer.py +129 -0
  14. strix/cli/tool_components/base_renderer.py +61 -0
  15. strix/cli/tool_components/browser_renderer.py +107 -0
  16. strix/cli/tool_components/file_edit_renderer.py +95 -0
  17. strix/cli/tool_components/finish_renderer.py +32 -0
  18. strix/cli/tool_components/notes_renderer.py +108 -0
  19. strix/cli/tool_components/proxy_renderer.py +255 -0
  20. strix/cli/tool_components/python_renderer.py +34 -0
  21. strix/cli/tool_components/registry.py +72 -0
  22. strix/cli/tool_components/reporting_renderer.py +53 -0
  23. strix/cli/tool_components/scan_info_renderer.py +58 -0
  24. strix/cli/tool_components/terminal_renderer.py +99 -0
  25. strix/cli/tool_components/thinking_renderer.py +29 -0
  26. strix/cli/tool_components/user_message_renderer.py +43 -0
  27. strix/cli/tool_components/web_search_renderer.py +28 -0
  28. strix/cli/tracer.py +308 -0
  29. strix/llm/__init__.py +14 -0
  30. strix/llm/config.py +19 -0
  31. strix/llm/llm.py +310 -0
  32. strix/llm/memory_compressor.py +206 -0
  33. strix/llm/request_queue.py +63 -0
  34. strix/llm/utils.py +84 -0
  35. strix/prompts/__init__.py +113 -0
  36. strix/prompts/coordination/root_agent.jinja +41 -0
  37. strix/prompts/vulnerabilities/authentication_jwt.jinja +129 -0
  38. strix/prompts/vulnerabilities/business_logic.jinja +143 -0
  39. strix/prompts/vulnerabilities/csrf.jinja +168 -0
  40. strix/prompts/vulnerabilities/idor.jinja +164 -0
  41. strix/prompts/vulnerabilities/race_conditions.jinja +194 -0
  42. strix/prompts/vulnerabilities/rce.jinja +222 -0
  43. strix/prompts/vulnerabilities/sql_injection.jinja +216 -0
  44. strix/prompts/vulnerabilities/ssrf.jinja +168 -0
  45. strix/prompts/vulnerabilities/xss.jinja +221 -0
  46. strix/prompts/vulnerabilities/xxe.jinja +276 -0
  47. strix/runtime/__init__.py +19 -0
  48. strix/runtime/docker_runtime.py +298 -0
  49. strix/runtime/runtime.py +25 -0
  50. strix/runtime/tool_server.py +97 -0
  51. strix/tools/__init__.py +64 -0
  52. strix/tools/agents_graph/__init__.py +16 -0
  53. strix/tools/agents_graph/agents_graph_actions.py +610 -0
  54. strix/tools/agents_graph/agents_graph_actions_schema.xml +223 -0
  55. strix/tools/argument_parser.py +120 -0
  56. strix/tools/browser/__init__.py +4 -0
  57. strix/tools/browser/browser_actions.py +236 -0
  58. strix/tools/browser/browser_actions_schema.xml +183 -0
  59. strix/tools/browser/browser_instance.py +533 -0
  60. strix/tools/browser/tab_manager.py +342 -0
  61. strix/tools/executor.py +302 -0
  62. strix/tools/file_edit/__init__.py +4 -0
  63. strix/tools/file_edit/file_edit_actions.py +141 -0
  64. strix/tools/file_edit/file_edit_actions_schema.xml +128 -0
  65. strix/tools/finish/__init__.py +4 -0
  66. strix/tools/finish/finish_actions.py +167 -0
  67. strix/tools/finish/finish_actions_schema.xml +45 -0
  68. strix/tools/notes/__init__.py +14 -0
  69. strix/tools/notes/notes_actions.py +191 -0
  70. strix/tools/notes/notes_actions_schema.xml +150 -0
  71. strix/tools/proxy/__init__.py +20 -0
  72. strix/tools/proxy/proxy_actions.py +101 -0
  73. strix/tools/proxy/proxy_actions_schema.xml +267 -0
  74. strix/tools/proxy/proxy_manager.py +785 -0
  75. strix/tools/python/__init__.py +4 -0
  76. strix/tools/python/python_actions.py +47 -0
  77. strix/tools/python/python_actions_schema.xml +131 -0
  78. strix/tools/python/python_instance.py +172 -0
  79. strix/tools/python/python_manager.py +131 -0
  80. strix/tools/registry.py +196 -0
  81. strix/tools/reporting/__init__.py +6 -0
  82. strix/tools/reporting/reporting_actions.py +63 -0
  83. strix/tools/reporting/reporting_actions_schema.xml +30 -0
  84. strix/tools/terminal/__init__.py +4 -0
  85. strix/tools/terminal/terminal_actions.py +53 -0
  86. strix/tools/terminal/terminal_actions_schema.xml +114 -0
  87. strix/tools/terminal/terminal_instance.py +231 -0
  88. strix/tools/terminal/terminal_manager.py +191 -0
  89. strix/tools/thinking/__init__.py +4 -0
  90. strix/tools/thinking/thinking_actions.py +18 -0
  91. strix/tools/thinking/thinking_actions_schema.xml +52 -0
  92. strix/tools/web_search/__init__.py +4 -0
  93. strix/tools/web_search/web_search_actions.py +80 -0
  94. strix/tools/web_search/web_search_actions_schema.xml +83 -0
  95. strix_agent-0.1.1.dist-info/LICENSE +201 -0
  96. strix_agent-0.1.1.dist-info/METADATA +200 -0
  97. strix_agent-0.1.1.dist-info/RECORD +99 -0
  98. strix_agent-0.1.1.dist-info/WHEEL +4 -0
  99. strix_agent-0.1.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,394 @@
1
+ import asyncio
2
+ import logging
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING, Any, Optional
5
+
6
+
7
+ if TYPE_CHECKING:
8
+ from strix.cli.tracer import Tracer
9
+
10
+ from jinja2 import (
11
+ Environment,
12
+ FileSystemLoader,
13
+ select_autoescape,
14
+ )
15
+
16
+ from strix.llm import LLM, LLMConfig
17
+ from strix.llm.utils import clean_content
18
+ from strix.tools import process_tool_invocations
19
+
20
+ from .state import AgentState
21
+
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class AgentMeta(type):
27
+ agent_name: str
28
+ jinja_env: Environment
29
+
30
+ def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> type:
31
+ new_cls = super().__new__(cls, name, bases, attrs)
32
+
33
+ if name == "BaseAgent":
34
+ return new_cls
35
+
36
+ agents_dir = Path(__file__).parent
37
+ prompt_dir = agents_dir / name
38
+
39
+ new_cls.agent_name = name
40
+ new_cls.jinja_env = Environment(
41
+ loader=FileSystemLoader(prompt_dir),
42
+ autoescape=select_autoescape(enabled_extensions=(), default_for_string=False),
43
+ )
44
+
45
+ return new_cls
46
+
47
+
48
+ class BaseAgent(metaclass=AgentMeta):
49
+ max_iterations = 200
50
+ agent_name: str = ""
51
+ jinja_env: Environment
52
+ default_llm_config: LLMConfig | None = None
53
+
54
+ def __init__(self, config: dict[str, Any]):
55
+ self.config = config
56
+
57
+ self.local_source_path = config.get("local_source_path")
58
+
59
+ if "max_iterations" in config:
60
+ self.max_iterations = config["max_iterations"]
61
+
62
+ self.llm_config_name = config.get("llm_config_name", "default")
63
+ self.llm_config = config.get("llm_config", self.default_llm_config)
64
+ if self.llm_config is None:
65
+ raise ValueError("llm_config is required but not provided")
66
+ self.llm = LLM(self.llm_config, agent_name=self.agent_name)
67
+
68
+ state_from_config = config.get("state")
69
+ if state_from_config is not None:
70
+ self.state = state_from_config
71
+ else:
72
+ self.state = AgentState(
73
+ agent_name=self.agent_name,
74
+ max_iterations=self.max_iterations,
75
+ )
76
+
77
+ self._current_task: asyncio.Task[Any] | None = None
78
+
79
+ from strix.cli.tracer import get_global_tracer
80
+
81
+ tracer = get_global_tracer()
82
+ if tracer:
83
+ tracer.log_agent_creation(
84
+ agent_id=self.state.agent_id,
85
+ name=self.state.agent_name,
86
+ task=self.state.task,
87
+ parent_id=self.state.parent_id,
88
+ )
89
+ if self.state.parent_id is None:
90
+ scan_config = tracer.scan_config or {}
91
+ exec_id = tracer.log_tool_execution_start(
92
+ agent_id=self.state.agent_id,
93
+ tool_name="scan_start_info",
94
+ args=scan_config,
95
+ )
96
+ tracer.update_tool_execution(execution_id=exec_id, status="completed", result={})
97
+
98
+ else:
99
+ exec_id = tracer.log_tool_execution_start(
100
+ agent_id=self.state.agent_id,
101
+ tool_name="subagent_start_info",
102
+ args={
103
+ "name": self.state.agent_name,
104
+ "task": self.state.task,
105
+ "parent_id": self.state.parent_id,
106
+ },
107
+ )
108
+ tracer.update_tool_execution(execution_id=exec_id, status="completed", result={})
109
+
110
+ self._add_to_agents_graph()
111
+
112
+ def _add_to_agents_graph(self) -> None:
113
+ from strix.tools.agents_graph import agents_graph_actions
114
+
115
+ node = {
116
+ "id": self.state.agent_id,
117
+ "name": self.state.agent_name,
118
+ "task": self.state.task,
119
+ "status": "running",
120
+ "parent_id": self.state.parent_id,
121
+ "created_at": self.state.start_time,
122
+ "finished_at": None,
123
+ "result": None,
124
+ "llm_config": self.llm_config_name,
125
+ "agent_type": self.__class__.__name__,
126
+ "state": self.state.model_dump(),
127
+ }
128
+ agents_graph_actions._agent_graph["nodes"][self.state.agent_id] = node
129
+
130
+ agents_graph_actions._agent_instances[self.state.agent_id] = self
131
+ agents_graph_actions._agent_states[self.state.agent_id] = self.state
132
+
133
+ if self.state.parent_id:
134
+ agents_graph_actions._agent_graph["edges"].append(
135
+ {"from": self.state.parent_id, "to": self.state.agent_id, "type": "delegation"}
136
+ )
137
+
138
+ if self.state.agent_id not in agents_graph_actions._agent_messages:
139
+ agents_graph_actions._agent_messages[self.state.agent_id] = []
140
+
141
+ if self.state.parent_id is None and agents_graph_actions._root_agent_id is None:
142
+ agents_graph_actions._root_agent_id = self.state.agent_id
143
+
144
+ def cancel_current_execution(self) -> None:
145
+ if self._current_task and not self._current_task.done():
146
+ self._current_task.cancel()
147
+ self._current_task = None
148
+
149
+ async def agent_loop(self, task: str) -> dict[str, Any]:
150
+ await self._initialize_sandbox_and_state(task)
151
+
152
+ from strix.cli.tracer import get_global_tracer
153
+
154
+ tracer = get_global_tracer()
155
+
156
+ while True:
157
+ self._check_agent_messages(self.state)
158
+
159
+ if self.state.is_waiting_for_input():
160
+ await self._wait_for_input()
161
+ continue
162
+
163
+ if self.state.should_stop():
164
+ await self._enter_waiting_state(tracer)
165
+ continue
166
+
167
+ self.state.increment_iteration()
168
+
169
+ try:
170
+ should_finish = await self._process_iteration(tracer)
171
+ if should_finish:
172
+ await self._enter_waiting_state(tracer, task_completed=True)
173
+ continue
174
+
175
+ except asyncio.CancelledError:
176
+ await self._enter_waiting_state(tracer, error_occurred=False, was_cancelled=True)
177
+ continue
178
+
179
+ except (RuntimeError, ValueError, TypeError) as e:
180
+ if not await self._handle_iteration_error(e, tracer):
181
+ await self._enter_waiting_state(tracer, error_occurred=True)
182
+ continue
183
+
184
+ async def _wait_for_input(self) -> None:
185
+ import asyncio
186
+
187
+ await asyncio.sleep(0.5)
188
+
189
+ async def _enter_waiting_state(
190
+ self,
191
+ tracer: Optional["Tracer"],
192
+ task_completed: bool = False,
193
+ error_occurred: bool = False,
194
+ was_cancelled: bool = False,
195
+ ) -> None:
196
+ self.state.enter_waiting_state()
197
+
198
+ if tracer:
199
+ if task_completed:
200
+ tracer.update_agent_status(self.state.agent_id, "completed")
201
+ elif error_occurred:
202
+ tracer.update_agent_status(self.state.agent_id, "error")
203
+ elif was_cancelled:
204
+ tracer.update_agent_status(self.state.agent_id, "stopped")
205
+ else:
206
+ tracer.update_agent_status(self.state.agent_id, "stopped")
207
+
208
+ if task_completed:
209
+ self.state.add_message(
210
+ "assistant",
211
+ "Task completed. I'm now waiting for follow-up instructions or new tasks.",
212
+ )
213
+ elif error_occurred:
214
+ self.state.add_message(
215
+ "assistant", "An error occurred. I'm now waiting for new instructions."
216
+ )
217
+ elif was_cancelled:
218
+ self.state.add_message(
219
+ "assistant", "Execution was cancelled. I'm now waiting for new instructions."
220
+ )
221
+ else:
222
+ self.state.add_message(
223
+ "assistant",
224
+ "Execution paused. I'm now waiting for new instructions or any updates.",
225
+ )
226
+
227
+ async def _initialize_sandbox_and_state(self, task: str) -> None:
228
+ import os
229
+
230
+ sandbox_mode = os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "true"
231
+ if not sandbox_mode and self.state.sandbox_id is None:
232
+ from strix.runtime import get_runtime
233
+
234
+ runtime = get_runtime()
235
+ sandbox_info = await runtime.create_sandbox(
236
+ self.state.agent_id, self.state.sandbox_token, self.local_source_path
237
+ )
238
+ self.state.sandbox_id = sandbox_info["workspace_id"]
239
+ self.state.sandbox_token = sandbox_info["auth_token"]
240
+ self.state.sandbox_info = sandbox_info
241
+
242
+ if not self.state.task:
243
+ self.state.task = task
244
+
245
+ self.state.add_message("user", task)
246
+
247
+ async def _process_iteration(self, tracer: Optional["Tracer"]) -> bool:
248
+ response = await self.llm.generate(self.state.get_conversation_history())
249
+
250
+ content_stripped = (response.content or "").strip()
251
+
252
+ if not content_stripped:
253
+ corrective_message = (
254
+ "You MUST NOT respond with empty messages. "
255
+ "If you currently have nothing to do or say, use an appropriate tool instead:\n"
256
+ "- Use agents_graph_actions.wait_for_message to wait for messages "
257
+ "from user or other agents\n"
258
+ "- Use agents_graph_actions.agent_finish if you are a sub-agent "
259
+ "and your task is complete\n"
260
+ "- Use finish_actions.finish_scan if you are the root/main agent "
261
+ "and the scan is complete"
262
+ )
263
+ self.state.add_message("user", corrective_message)
264
+ return False
265
+
266
+ self.state.add_message("assistant", response.content)
267
+ if tracer:
268
+ tracer.log_chat_message(
269
+ content=clean_content(response.content),
270
+ role="assistant",
271
+ agent_id=self.state.agent_id,
272
+ )
273
+
274
+ actions = (
275
+ response.tool_invocations
276
+ if hasattr(response, "tool_invocations") and response.tool_invocations
277
+ else []
278
+ )
279
+
280
+ if actions:
281
+ return await self._execute_actions(actions, tracer)
282
+
283
+ return False
284
+
285
+ async def _execute_actions(self, actions: list[Any], tracer: Optional["Tracer"]) -> bool:
286
+ """Execute actions and return True if agent should finish."""
287
+ for action in actions:
288
+ self.state.add_action(action)
289
+
290
+ conversation_history = self.state.get_conversation_history()
291
+
292
+ tool_task = asyncio.create_task(
293
+ process_tool_invocations(actions, conversation_history, self.state)
294
+ )
295
+ self._current_task = tool_task
296
+
297
+ try:
298
+ should_agent_finish = await tool_task
299
+ self._current_task = None
300
+ except asyncio.CancelledError:
301
+ self._current_task = None
302
+ self.state.add_error("Tool execution cancelled by user")
303
+ raise
304
+
305
+ self.state.messages = conversation_history
306
+
307
+ if should_agent_finish:
308
+ self.state.set_completed({"success": True})
309
+ if tracer:
310
+ tracer.update_agent_status(self.state.agent_id, "completed")
311
+ return True
312
+
313
+ return False
314
+
315
+ async def _handle_iteration_error(
316
+ self,
317
+ error: RuntimeError | ValueError | TypeError | asyncio.CancelledError,
318
+ tracer: Optional["Tracer"],
319
+ ) -> bool:
320
+ error_msg = f"Error in iteration {self.state.iteration}: {error!s}"
321
+ logger.exception(error_msg)
322
+ self.state.add_error(error_msg)
323
+ if tracer:
324
+ tracer.update_agent_status(self.state.agent_id, "error")
325
+ return True
326
+
327
+ def _check_agent_messages(self, state: AgentState) -> None:
328
+ try:
329
+ from strix.tools.agents_graph.agents_graph_actions import _agent_graph, _agent_messages
330
+
331
+ agent_id = state.agent_id
332
+ if not agent_id or agent_id not in _agent_messages:
333
+ return
334
+
335
+ messages = _agent_messages[agent_id]
336
+ if messages:
337
+ has_new_messages = False
338
+ for message in messages:
339
+ if not message.get("read", False):
340
+ if state.is_waiting_for_input():
341
+ state.resume_from_waiting()
342
+ has_new_messages = True
343
+
344
+ sender_name = "Unknown Agent"
345
+ sender_id = message.get("from")
346
+
347
+ if sender_id == "user":
348
+ sender_name = "User"
349
+ state.add_message("user", message.get("content", ""))
350
+ else:
351
+ if sender_id and sender_id in _agent_graph.get("nodes", {}):
352
+ sender_name = _agent_graph["nodes"][sender_id]["name"]
353
+
354
+ message_content = f"""<inter_agent_message>
355
+ <delivery_notice>
356
+ <important>You have received a message from another agent. You should acknowledge
357
+ this message and respond appropriately based on its content. However, DO NOT echo
358
+ back or repeat the entire message structure in your response. Simply process the
359
+ content and respond naturally as/if needed.</important>
360
+ </delivery_notice>
361
+ <sender>
362
+ <agent_name>{sender_name}</agent_name>
363
+ <agent_id>{sender_id}</agent_id>
364
+ </sender>
365
+ <message_metadata>
366
+ <type>{message.get("message_type", "information")}</type>
367
+ <priority>{message.get("priority", "normal")}</priority>
368
+ <timestamp>{message.get("timestamp", "")}</timestamp>
369
+ </message_metadata>
370
+ <content>
371
+ {message.get("content", "")}
372
+ </content>
373
+ <delivery_info>
374
+ <note>This message was delivered during your task execution.
375
+ Please acknowledge and respond if needed.</note>
376
+ </delivery_info>
377
+ </inter_agent_message>"""
378
+ state.add_message("user", message_content.strip())
379
+
380
+ message["read"] = True
381
+
382
+ if has_new_messages and not state.is_waiting_for_input():
383
+ from strix.cli.tracer import get_global_tracer
384
+
385
+ tracer = get_global_tracer()
386
+ if tracer:
387
+ tracer.update_agent_status(agent_id, "running")
388
+
389
+ except (AttributeError, KeyError, TypeError) as e:
390
+ import logging
391
+
392
+ logger = logging.getLogger(__name__)
393
+ logger.warning(f"Error checking agent messages: {e}")
394
+ return
strix/agents/state.py ADDED
@@ -0,0 +1,139 @@
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 = 200
23
+ completed: bool = False
24
+ stop_requested: bool = False
25
+ waiting_for_input: bool = False
26
+ final_result: dict[str, Any] | None = None
27
+
28
+ messages: list[dict[str, Any]] = Field(default_factory=list)
29
+ context: dict[str, Any] = Field(default_factory=dict)
30
+
31
+ start_time: str = Field(default_factory=lambda: datetime.now(UTC).isoformat())
32
+ last_updated: str = Field(default_factory=lambda: datetime.now(UTC).isoformat())
33
+
34
+ actions_taken: list[dict[str, Any]] = Field(default_factory=list)
35
+ observations: list[dict[str, Any]] = Field(default_factory=list)
36
+
37
+ errors: list[str] = Field(default_factory=list)
38
+
39
+ def increment_iteration(self) -> None:
40
+ self.iteration += 1
41
+ self.last_updated = datetime.now(UTC).isoformat()
42
+
43
+ def add_message(self, role: str, content: Any) -> None:
44
+ self.messages.append({"role": role, "content": content})
45
+ self.last_updated = datetime.now(UTC).isoformat()
46
+
47
+ def add_action(self, action: dict[str, Any]) -> None:
48
+ self.actions_taken.append(
49
+ {
50
+ "iteration": self.iteration,
51
+ "timestamp": datetime.now(UTC).isoformat(),
52
+ "action": action,
53
+ }
54
+ )
55
+
56
+ def add_observation(self, observation: dict[str, Any]) -> None:
57
+ self.observations.append(
58
+ {
59
+ "iteration": self.iteration,
60
+ "timestamp": datetime.now(UTC).isoformat(),
61
+ "observation": observation,
62
+ }
63
+ )
64
+
65
+ def add_error(self, error: str) -> None:
66
+ self.errors.append(f"Iteration {self.iteration}: {error}")
67
+ self.last_updated = datetime.now(UTC).isoformat()
68
+
69
+ def update_context(self, key: str, value: Any) -> None:
70
+ self.context[key] = value
71
+ self.last_updated = datetime.now(UTC).isoformat()
72
+
73
+ def set_completed(self, final_result: dict[str, Any] | None = None) -> None:
74
+ self.completed = True
75
+ self.final_result = final_result
76
+ self.last_updated = datetime.now(UTC).isoformat()
77
+
78
+ def request_stop(self) -> None:
79
+ self.stop_requested = True
80
+ self.last_updated = datetime.now(UTC).isoformat()
81
+
82
+ def should_stop(self) -> bool:
83
+ return self.stop_requested or self.completed or self.has_reached_max_iterations()
84
+
85
+ def is_waiting_for_input(self) -> bool:
86
+ return self.waiting_for_input
87
+
88
+ def enter_waiting_state(self) -> None:
89
+ self.waiting_for_input = True
90
+ self.stop_requested = False
91
+ self.last_updated = datetime.now(UTC).isoformat()
92
+
93
+ def resume_from_waiting(self, new_task: str | None = None) -> None:
94
+ self.waiting_for_input = False
95
+ self.stop_requested = False
96
+ self.completed = False
97
+ if new_task:
98
+ self.task = new_task
99
+ self.last_updated = datetime.now(UTC).isoformat()
100
+
101
+ def has_reached_max_iterations(self) -> bool:
102
+ return self.iteration >= self.max_iterations
103
+
104
+ def has_empty_last_messages(self, count: int = 3) -> bool:
105
+ if len(self.messages) < count:
106
+ return False
107
+
108
+ last_messages = self.messages[-count:]
109
+
110
+ for message in last_messages:
111
+ content = message.get("content", "")
112
+ if isinstance(content, str) and content.strip():
113
+ return False
114
+
115
+ return True
116
+
117
+ def get_conversation_history(self) -> list[dict[str, Any]]:
118
+ return self.messages
119
+
120
+ def get_execution_summary(self) -> dict[str, Any]:
121
+ return {
122
+ "agent_id": self.agent_id,
123
+ "agent_name": self.agent_name,
124
+ "parent_id": self.parent_id,
125
+ "sandbox_id": self.sandbox_id,
126
+ "sandbox_info": self.sandbox_info,
127
+ "task": self.task,
128
+ "iteration": self.iteration,
129
+ "max_iterations": self.max_iterations,
130
+ "completed": self.completed,
131
+ "final_result": self.final_result,
132
+ "start_time": self.start_time,
133
+ "last_updated": self.last_updated,
134
+ "total_actions": len(self.actions_taken),
135
+ "total_observations": len(self.observations),
136
+ "total_errors": len(self.errors),
137
+ "has_errors": len(self.errors) > 0,
138
+ "max_iterations_reached": self.has_reached_max_iterations() and not self.completed,
139
+ }
strix/cli/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .main import main
2
+
3
+
4
+ __all__ = ["main"]