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.
- strix/__init__.py +0 -0
- strix/agents/StrixAgent/__init__.py +4 -0
- strix/agents/StrixAgent/strix_agent.py +60 -0
- strix/agents/StrixAgent/system_prompt.jinja +504 -0
- strix/agents/__init__.py +10 -0
- strix/agents/base_agent.py +394 -0
- strix/agents/state.py +139 -0
- strix/cli/__init__.py +4 -0
- strix/cli/app.py +1124 -0
- strix/cli/assets/cli.tcss +680 -0
- strix/cli/main.py +542 -0
- strix/cli/tool_components/__init__.py +39 -0
- strix/cli/tool_components/agents_graph_renderer.py +129 -0
- strix/cli/tool_components/base_renderer.py +61 -0
- strix/cli/tool_components/browser_renderer.py +107 -0
- strix/cli/tool_components/file_edit_renderer.py +95 -0
- strix/cli/tool_components/finish_renderer.py +32 -0
- strix/cli/tool_components/notes_renderer.py +108 -0
- strix/cli/tool_components/proxy_renderer.py +255 -0
- strix/cli/tool_components/python_renderer.py +34 -0
- strix/cli/tool_components/registry.py +72 -0
- strix/cli/tool_components/reporting_renderer.py +53 -0
- strix/cli/tool_components/scan_info_renderer.py +58 -0
- strix/cli/tool_components/terminal_renderer.py +99 -0
- strix/cli/tool_components/thinking_renderer.py +29 -0
- strix/cli/tool_components/user_message_renderer.py +43 -0
- strix/cli/tool_components/web_search_renderer.py +28 -0
- strix/cli/tracer.py +308 -0
- strix/llm/__init__.py +14 -0
- strix/llm/config.py +19 -0
- strix/llm/llm.py +310 -0
- strix/llm/memory_compressor.py +206 -0
- strix/llm/request_queue.py +63 -0
- strix/llm/utils.py +84 -0
- strix/prompts/__init__.py +113 -0
- strix/prompts/coordination/root_agent.jinja +41 -0
- strix/prompts/vulnerabilities/authentication_jwt.jinja +129 -0
- strix/prompts/vulnerabilities/business_logic.jinja +143 -0
- strix/prompts/vulnerabilities/csrf.jinja +168 -0
- strix/prompts/vulnerabilities/idor.jinja +164 -0
- strix/prompts/vulnerabilities/race_conditions.jinja +194 -0
- strix/prompts/vulnerabilities/rce.jinja +222 -0
- strix/prompts/vulnerabilities/sql_injection.jinja +216 -0
- strix/prompts/vulnerabilities/ssrf.jinja +168 -0
- strix/prompts/vulnerabilities/xss.jinja +221 -0
- strix/prompts/vulnerabilities/xxe.jinja +276 -0
- strix/runtime/__init__.py +19 -0
- strix/runtime/docker_runtime.py +298 -0
- strix/runtime/runtime.py +25 -0
- strix/runtime/tool_server.py +97 -0
- strix/tools/__init__.py +64 -0
- strix/tools/agents_graph/__init__.py +16 -0
- strix/tools/agents_graph/agents_graph_actions.py +610 -0
- strix/tools/agents_graph/agents_graph_actions_schema.xml +223 -0
- strix/tools/argument_parser.py +120 -0
- strix/tools/browser/__init__.py +4 -0
- strix/tools/browser/browser_actions.py +236 -0
- strix/tools/browser/browser_actions_schema.xml +183 -0
- strix/tools/browser/browser_instance.py +533 -0
- strix/tools/browser/tab_manager.py +342 -0
- strix/tools/executor.py +302 -0
- strix/tools/file_edit/__init__.py +4 -0
- strix/tools/file_edit/file_edit_actions.py +141 -0
- strix/tools/file_edit/file_edit_actions_schema.xml +128 -0
- strix/tools/finish/__init__.py +4 -0
- strix/tools/finish/finish_actions.py +167 -0
- strix/tools/finish/finish_actions_schema.xml +45 -0
- strix/tools/notes/__init__.py +14 -0
- strix/tools/notes/notes_actions.py +191 -0
- strix/tools/notes/notes_actions_schema.xml +150 -0
- strix/tools/proxy/__init__.py +20 -0
- strix/tools/proxy/proxy_actions.py +101 -0
- strix/tools/proxy/proxy_actions_schema.xml +267 -0
- strix/tools/proxy/proxy_manager.py +785 -0
- strix/tools/python/__init__.py +4 -0
- strix/tools/python/python_actions.py +47 -0
- strix/tools/python/python_actions_schema.xml +131 -0
- strix/tools/python/python_instance.py +172 -0
- strix/tools/python/python_manager.py +131 -0
- strix/tools/registry.py +196 -0
- strix/tools/reporting/__init__.py +6 -0
- strix/tools/reporting/reporting_actions.py +63 -0
- strix/tools/reporting/reporting_actions_schema.xml +30 -0
- strix/tools/terminal/__init__.py +4 -0
- strix/tools/terminal/terminal_actions.py +53 -0
- strix/tools/terminal/terminal_actions_schema.xml +114 -0
- strix/tools/terminal/terminal_instance.py +231 -0
- strix/tools/terminal/terminal_manager.py +191 -0
- strix/tools/thinking/__init__.py +4 -0
- strix/tools/thinking/thinking_actions.py +18 -0
- strix/tools/thinking/thinking_actions_schema.xml +52 -0
- strix/tools/web_search/__init__.py +4 -0
- strix/tools/web_search/web_search_actions.py +80 -0
- strix/tools/web_search/web_search_actions_schema.xml +83 -0
- strix_agent-0.1.1.dist-info/LICENSE +201 -0
- strix_agent-0.1.1.dist-info/METADATA +200 -0
- strix_agent-0.1.1.dist-info/RECORD +99 -0
- strix_agent-0.1.1.dist-info/WHEEL +4 -0
- 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