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,610 @@
|
|
1
|
+
import threading
|
2
|
+
from datetime import UTC, datetime
|
3
|
+
from typing import Any, Literal
|
4
|
+
|
5
|
+
from strix.tools.registry import register_tool
|
6
|
+
|
7
|
+
|
8
|
+
_agent_graph: dict[str, Any] = {
|
9
|
+
"nodes": {},
|
10
|
+
"edges": [],
|
11
|
+
}
|
12
|
+
|
13
|
+
_root_agent_id: str | None = None
|
14
|
+
|
15
|
+
_agent_messages: dict[str, list[dict[str, Any]]] = {}
|
16
|
+
|
17
|
+
_running_agents: dict[str, threading.Thread] = {}
|
18
|
+
|
19
|
+
_agent_instances: dict[str, Any] = {}
|
20
|
+
|
21
|
+
_agent_states: dict[str, Any] = {}
|
22
|
+
|
23
|
+
|
24
|
+
def _run_agent_in_thread(
|
25
|
+
agent: Any, state: Any, inherited_messages: list[dict[str, Any]]
|
26
|
+
) -> dict[str, Any]:
|
27
|
+
try:
|
28
|
+
if inherited_messages:
|
29
|
+
state.add_message("user", "<inherited_context_from_parent>")
|
30
|
+
for msg in inherited_messages:
|
31
|
+
state.add_message(msg["role"], msg["content"])
|
32
|
+
state.add_message("user", "</inherited_context_from_parent>")
|
33
|
+
|
34
|
+
parent_info = _agent_graph["nodes"].get(state.parent_id, {})
|
35
|
+
parent_name = parent_info.get("name", "Unknown Parent")
|
36
|
+
|
37
|
+
context_status = (
|
38
|
+
"inherited conversation context from your parent for background understanding"
|
39
|
+
if inherited_messages
|
40
|
+
else "started with a fresh context"
|
41
|
+
)
|
42
|
+
|
43
|
+
task_xml = f"""<agent_delegation>
|
44
|
+
<identity>
|
45
|
+
⚠️ You are NOT your parent agent. You are a NEW, SEPARATE sub-agent (not root).
|
46
|
+
|
47
|
+
Your Info: {state.agent_name} ({state.agent_id})
|
48
|
+
Parent Info: {parent_name} ({state.parent_id})
|
49
|
+
</identity>
|
50
|
+
|
51
|
+
<your_task>{state.task}</your_task>
|
52
|
+
|
53
|
+
<instructions>
|
54
|
+
- You have {context_status}
|
55
|
+
- Inherited context is for BACKGROUND ONLY - don't continue parent's work
|
56
|
+
- Focus EXCLUSIVELY on your delegated task above
|
57
|
+
- Work independently with your own approach
|
58
|
+
- Use agent_finish when complete to report back to parent
|
59
|
+
- You are a SPECIALIST for this specific task
|
60
|
+
</instructions>
|
61
|
+
</agent_delegation>"""
|
62
|
+
|
63
|
+
state.add_message("user", task_xml)
|
64
|
+
|
65
|
+
_agent_states[state.agent_id] = state
|
66
|
+
|
67
|
+
_agent_graph["nodes"][state.agent_id]["state"] = state.model_dump()
|
68
|
+
|
69
|
+
import asyncio
|
70
|
+
|
71
|
+
loop = asyncio.new_event_loop()
|
72
|
+
asyncio.set_event_loop(loop)
|
73
|
+
try:
|
74
|
+
result = loop.run_until_complete(agent.agent_loop(state.task))
|
75
|
+
finally:
|
76
|
+
loop.close()
|
77
|
+
|
78
|
+
except Exception as e:
|
79
|
+
_agent_graph["nodes"][state.agent_id]["status"] = "error"
|
80
|
+
_agent_graph["nodes"][state.agent_id]["finished_at"] = datetime.now(UTC).isoformat()
|
81
|
+
_agent_graph["nodes"][state.agent_id]["result"] = {"error": str(e)}
|
82
|
+
_running_agents.pop(state.agent_id, None)
|
83
|
+
_agent_instances.pop(state.agent_id, None)
|
84
|
+
raise
|
85
|
+
else:
|
86
|
+
if state.stop_requested:
|
87
|
+
_agent_graph["nodes"][state.agent_id]["status"] = "stopped"
|
88
|
+
else:
|
89
|
+
_agent_graph["nodes"][state.agent_id]["status"] = "completed"
|
90
|
+
_agent_graph["nodes"][state.agent_id]["finished_at"] = datetime.now(UTC).isoformat()
|
91
|
+
_agent_graph["nodes"][state.agent_id]["result"] = result
|
92
|
+
_running_agents.pop(state.agent_id, None)
|
93
|
+
_agent_instances.pop(state.agent_id, None)
|
94
|
+
|
95
|
+
return {"result": result}
|
96
|
+
|
97
|
+
|
98
|
+
@register_tool(sandbox_execution=False)
|
99
|
+
def view_agent_graph(agent_state: Any) -> dict[str, Any]:
|
100
|
+
try:
|
101
|
+
structure_lines = ["=== AGENT GRAPH STRUCTURE ==="]
|
102
|
+
|
103
|
+
def _build_tree(agent_id: str, depth: int = 0) -> None:
|
104
|
+
node = _agent_graph["nodes"][agent_id]
|
105
|
+
indent = " " * depth
|
106
|
+
|
107
|
+
you_indicator = " ← This is you" if agent_id == agent_state.agent_id else ""
|
108
|
+
|
109
|
+
structure_lines.append(f"{indent}* {node['name']} ({agent_id}){you_indicator}")
|
110
|
+
structure_lines.append(f"{indent} Task: {node['task']}")
|
111
|
+
structure_lines.append(f"{indent} Status: {node['status']}")
|
112
|
+
|
113
|
+
children = [
|
114
|
+
edge["to"]
|
115
|
+
for edge in _agent_graph["edges"]
|
116
|
+
if edge["from"] == agent_id and edge["type"] == "delegation"
|
117
|
+
]
|
118
|
+
|
119
|
+
if children:
|
120
|
+
structure_lines.append(f"{indent} Children:")
|
121
|
+
for child_id in children:
|
122
|
+
_build_tree(child_id, depth + 2)
|
123
|
+
|
124
|
+
root_agent_id = _root_agent_id
|
125
|
+
if not root_agent_id and _agent_graph["nodes"]:
|
126
|
+
for agent_id, node in _agent_graph["nodes"].items():
|
127
|
+
if node.get("parent_id") is None:
|
128
|
+
root_agent_id = agent_id
|
129
|
+
break
|
130
|
+
if not root_agent_id:
|
131
|
+
root_agent_id = next(iter(_agent_graph["nodes"].keys()))
|
132
|
+
|
133
|
+
if root_agent_id and root_agent_id in _agent_graph["nodes"]:
|
134
|
+
_build_tree(root_agent_id)
|
135
|
+
else:
|
136
|
+
structure_lines.append("No agents in the graph yet")
|
137
|
+
|
138
|
+
graph_structure = "\n".join(structure_lines)
|
139
|
+
|
140
|
+
total_nodes = len(_agent_graph["nodes"])
|
141
|
+
running_count = sum(
|
142
|
+
1 for node in _agent_graph["nodes"].values() if node["status"] == "running"
|
143
|
+
)
|
144
|
+
waiting_count = sum(
|
145
|
+
1 for node in _agent_graph["nodes"].values() if node["status"] == "waiting"
|
146
|
+
)
|
147
|
+
stopping_count = sum(
|
148
|
+
1 for node in _agent_graph["nodes"].values() if node["status"] == "stopping"
|
149
|
+
)
|
150
|
+
completed_count = sum(
|
151
|
+
1 for node in _agent_graph["nodes"].values() if node["status"] == "completed"
|
152
|
+
)
|
153
|
+
stopped_count = sum(
|
154
|
+
1 for node in _agent_graph["nodes"].values() if node["status"] == "stopped"
|
155
|
+
)
|
156
|
+
failed_count = sum(
|
157
|
+
1 for node in _agent_graph["nodes"].values() if node["status"] in ["failed", "error"]
|
158
|
+
)
|
159
|
+
|
160
|
+
except Exception as e: # noqa: BLE001
|
161
|
+
return {
|
162
|
+
"error": f"Failed to view agent graph: {e}",
|
163
|
+
"graph_structure": "Error retrieving graph structure",
|
164
|
+
}
|
165
|
+
else:
|
166
|
+
return {
|
167
|
+
"graph_structure": graph_structure,
|
168
|
+
"summary": {
|
169
|
+
"total_agents": total_nodes,
|
170
|
+
"running": running_count,
|
171
|
+
"waiting": waiting_count,
|
172
|
+
"stopping": stopping_count,
|
173
|
+
"completed": completed_count,
|
174
|
+
"stopped": stopped_count,
|
175
|
+
"failed": failed_count,
|
176
|
+
},
|
177
|
+
}
|
178
|
+
|
179
|
+
|
180
|
+
@register_tool(sandbox_execution=False)
|
181
|
+
def create_agent(
|
182
|
+
agent_state: Any,
|
183
|
+
task: str,
|
184
|
+
name: str,
|
185
|
+
inherit_context: bool = True,
|
186
|
+
prompt_modules: str | None = None,
|
187
|
+
) -> dict[str, Any]:
|
188
|
+
try:
|
189
|
+
parent_id = agent_state.agent_id
|
190
|
+
|
191
|
+
module_list = []
|
192
|
+
if prompt_modules:
|
193
|
+
module_list = [m.strip() for m in prompt_modules.split(",") if m.strip()]
|
194
|
+
|
195
|
+
if "root_agent" in module_list:
|
196
|
+
return {
|
197
|
+
"success": False,
|
198
|
+
"error": (
|
199
|
+
"The 'root_agent' module is reserved for the main agent "
|
200
|
+
"and cannot be used by sub-agents"
|
201
|
+
),
|
202
|
+
"agent_id": None,
|
203
|
+
}
|
204
|
+
|
205
|
+
if len(module_list) > 3:
|
206
|
+
return {
|
207
|
+
"success": False,
|
208
|
+
"error": (
|
209
|
+
"Cannot specify more than 3 prompt modules for an agent "
|
210
|
+
"(use comma-separated format)"
|
211
|
+
),
|
212
|
+
"agent_id": None,
|
213
|
+
}
|
214
|
+
|
215
|
+
if module_list:
|
216
|
+
from strix.prompts import get_all_module_names, validate_module_names
|
217
|
+
|
218
|
+
validation = validate_module_names(module_list)
|
219
|
+
if validation["invalid"]:
|
220
|
+
available_modules = list(get_all_module_names())
|
221
|
+
return {
|
222
|
+
"success": False,
|
223
|
+
"error": (
|
224
|
+
f"Invalid prompt modules: {validation['invalid']}. "
|
225
|
+
f"Available modules: {', '.join(available_modules)}"
|
226
|
+
),
|
227
|
+
"agent_id": None,
|
228
|
+
}
|
229
|
+
|
230
|
+
from strix.agents import StrixAgent
|
231
|
+
from strix.agents.state import AgentState
|
232
|
+
from strix.llm.config import LLMConfig
|
233
|
+
|
234
|
+
state = AgentState(task=task, agent_name=name, parent_id=parent_id, max_iterations=200)
|
235
|
+
|
236
|
+
llm_config = LLMConfig(prompt_modules=module_list)
|
237
|
+
agent = StrixAgent(
|
238
|
+
{
|
239
|
+
"llm_config": llm_config,
|
240
|
+
"state": state,
|
241
|
+
}
|
242
|
+
)
|
243
|
+
|
244
|
+
inherited_messages = []
|
245
|
+
if inherit_context:
|
246
|
+
inherited_messages = agent_state.get_conversation_history()
|
247
|
+
|
248
|
+
_agent_instances[state.agent_id] = agent
|
249
|
+
|
250
|
+
thread = threading.Thread(
|
251
|
+
target=_run_agent_in_thread,
|
252
|
+
args=(agent, state, inherited_messages),
|
253
|
+
daemon=True,
|
254
|
+
name=f"Agent-{name}-{state.agent_id}",
|
255
|
+
)
|
256
|
+
thread.start()
|
257
|
+
_running_agents[state.agent_id] = thread
|
258
|
+
|
259
|
+
except Exception as e: # noqa: BLE001
|
260
|
+
return {"success": False, "error": f"Failed to create agent: {e}", "agent_id": None}
|
261
|
+
else:
|
262
|
+
return {
|
263
|
+
"success": True,
|
264
|
+
"agent_id": state.agent_id,
|
265
|
+
"message": f"Agent '{name}' created and started asynchronously",
|
266
|
+
"agent_info": {
|
267
|
+
"id": state.agent_id,
|
268
|
+
"name": name,
|
269
|
+
"status": "running",
|
270
|
+
"parent_id": parent_id,
|
271
|
+
},
|
272
|
+
}
|
273
|
+
|
274
|
+
|
275
|
+
@register_tool(sandbox_execution=False)
|
276
|
+
def send_message_to_agent(
|
277
|
+
agent_state: Any,
|
278
|
+
target_agent_id: str,
|
279
|
+
message: str,
|
280
|
+
message_type: Literal["query", "instruction", "information"] = "information",
|
281
|
+
priority: Literal["low", "normal", "high", "urgent"] = "normal",
|
282
|
+
) -> dict[str, Any]:
|
283
|
+
try:
|
284
|
+
if target_agent_id not in _agent_graph["nodes"]:
|
285
|
+
return {
|
286
|
+
"success": False,
|
287
|
+
"error": f"Target agent '{target_agent_id}' not found in graph",
|
288
|
+
"message_id": None,
|
289
|
+
}
|
290
|
+
|
291
|
+
sender_id = agent_state.agent_id
|
292
|
+
|
293
|
+
from uuid import uuid4
|
294
|
+
|
295
|
+
message_id = f"msg_{uuid4().hex[:8]}"
|
296
|
+
message_data = {
|
297
|
+
"id": message_id,
|
298
|
+
"from": sender_id,
|
299
|
+
"to": target_agent_id,
|
300
|
+
"content": message,
|
301
|
+
"message_type": message_type,
|
302
|
+
"priority": priority,
|
303
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
304
|
+
"delivered": False,
|
305
|
+
"read": False,
|
306
|
+
}
|
307
|
+
|
308
|
+
if target_agent_id not in _agent_messages:
|
309
|
+
_agent_messages[target_agent_id] = []
|
310
|
+
|
311
|
+
_agent_messages[target_agent_id].append(message_data)
|
312
|
+
|
313
|
+
_agent_graph["edges"].append(
|
314
|
+
{
|
315
|
+
"from": sender_id,
|
316
|
+
"to": target_agent_id,
|
317
|
+
"type": "message",
|
318
|
+
"message_id": message_id,
|
319
|
+
"message_type": message_type,
|
320
|
+
"priority": priority,
|
321
|
+
"created_at": datetime.now(UTC).isoformat(),
|
322
|
+
}
|
323
|
+
)
|
324
|
+
|
325
|
+
message_data["delivered"] = True
|
326
|
+
|
327
|
+
target_name = _agent_graph["nodes"][target_agent_id]["name"]
|
328
|
+
sender_name = _agent_graph["nodes"][sender_id]["name"]
|
329
|
+
|
330
|
+
return {
|
331
|
+
"success": True,
|
332
|
+
"message_id": message_id,
|
333
|
+
"message": f"Message sent from '{sender_name}' to '{target_name}'",
|
334
|
+
"delivery_status": "delivered",
|
335
|
+
"target_agent": {
|
336
|
+
"id": target_agent_id,
|
337
|
+
"name": target_name,
|
338
|
+
"status": _agent_graph["nodes"][target_agent_id]["status"],
|
339
|
+
},
|
340
|
+
}
|
341
|
+
|
342
|
+
except Exception as e: # noqa: BLE001
|
343
|
+
return {"success": False, "error": f"Failed to send message: {e}", "message_id": None}
|
344
|
+
|
345
|
+
|
346
|
+
@register_tool(sandbox_execution=False)
|
347
|
+
def agent_finish(
|
348
|
+
agent_state: Any,
|
349
|
+
result_summary: str,
|
350
|
+
findings: list[str] | None = None,
|
351
|
+
success: bool = True,
|
352
|
+
report_to_parent: bool = True,
|
353
|
+
final_recommendations: list[str] | None = None,
|
354
|
+
) -> dict[str, Any]:
|
355
|
+
try:
|
356
|
+
if not hasattr(agent_state, "parent_id") or agent_state.parent_id is None:
|
357
|
+
return {
|
358
|
+
"agent_completed": False,
|
359
|
+
"error": (
|
360
|
+
"This tool can only be used by subagents. "
|
361
|
+
"Root/main agents must use finish_scan instead."
|
362
|
+
),
|
363
|
+
"parent_notified": False,
|
364
|
+
}
|
365
|
+
|
366
|
+
agent_id = agent_state.agent_id
|
367
|
+
|
368
|
+
if agent_id not in _agent_graph["nodes"]:
|
369
|
+
return {"agent_completed": False, "error": "Current agent not found in graph"}
|
370
|
+
|
371
|
+
agent_node = _agent_graph["nodes"][agent_id]
|
372
|
+
|
373
|
+
agent_node["status"] = "finished" if success else "failed"
|
374
|
+
agent_node["finished_at"] = datetime.now(UTC).isoformat()
|
375
|
+
agent_node["result"] = {
|
376
|
+
"summary": result_summary,
|
377
|
+
"findings": findings or [],
|
378
|
+
"success": success,
|
379
|
+
"recommendations": final_recommendations or [],
|
380
|
+
}
|
381
|
+
|
382
|
+
parent_notified = False
|
383
|
+
|
384
|
+
if report_to_parent and agent_node["parent_id"]:
|
385
|
+
parent_id = agent_node["parent_id"]
|
386
|
+
|
387
|
+
if parent_id in _agent_graph["nodes"]:
|
388
|
+
findings_xml = "\n".join(
|
389
|
+
f" <finding>{finding}</finding>" for finding in (findings or [])
|
390
|
+
)
|
391
|
+
recommendations_xml = "\n".join(
|
392
|
+
f" <recommendation>{rec}</recommendation>"
|
393
|
+
for rec in (final_recommendations or [])
|
394
|
+
)
|
395
|
+
|
396
|
+
report_message = f"""<agent_completion_report>
|
397
|
+
<agent_info>
|
398
|
+
<agent_name>{agent_node["name"]}</agent_name>
|
399
|
+
<agent_id>{agent_id}</agent_id>
|
400
|
+
<task>{agent_node["task"]}</task>
|
401
|
+
<status>{"SUCCESS" if success else "FAILED"}</status>
|
402
|
+
<completion_time>{agent_node["finished_at"]}</completion_time>
|
403
|
+
</agent_info>
|
404
|
+
<results>
|
405
|
+
<summary>{result_summary}</summary>
|
406
|
+
<findings>
|
407
|
+
{findings_xml}
|
408
|
+
</findings>
|
409
|
+
<recommendations>
|
410
|
+
{recommendations_xml}
|
411
|
+
</recommendations>
|
412
|
+
</results>
|
413
|
+
</agent_completion_report>"""
|
414
|
+
|
415
|
+
if parent_id not in _agent_messages:
|
416
|
+
_agent_messages[parent_id] = []
|
417
|
+
|
418
|
+
from uuid import uuid4
|
419
|
+
|
420
|
+
_agent_messages[parent_id].append(
|
421
|
+
{
|
422
|
+
"id": f"report_{uuid4().hex[:8]}",
|
423
|
+
"from": agent_id,
|
424
|
+
"to": parent_id,
|
425
|
+
"content": report_message,
|
426
|
+
"message_type": "information",
|
427
|
+
"priority": "high",
|
428
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
429
|
+
"delivered": True,
|
430
|
+
"read": False,
|
431
|
+
}
|
432
|
+
)
|
433
|
+
|
434
|
+
parent_notified = True
|
435
|
+
|
436
|
+
_running_agents.pop(agent_id, None)
|
437
|
+
|
438
|
+
return {
|
439
|
+
"agent_completed": True,
|
440
|
+
"parent_notified": parent_notified,
|
441
|
+
"completion_summary": {
|
442
|
+
"agent_id": agent_id,
|
443
|
+
"agent_name": agent_node["name"],
|
444
|
+
"task": agent_node["task"],
|
445
|
+
"success": success,
|
446
|
+
"findings_count": len(findings or []),
|
447
|
+
"has_recommendations": bool(final_recommendations),
|
448
|
+
"finished_at": agent_node["finished_at"],
|
449
|
+
},
|
450
|
+
}
|
451
|
+
|
452
|
+
except Exception as e: # noqa: BLE001
|
453
|
+
return {
|
454
|
+
"agent_completed": False,
|
455
|
+
"error": f"Failed to complete agent: {e}",
|
456
|
+
"parent_notified": False,
|
457
|
+
}
|
458
|
+
|
459
|
+
|
460
|
+
def stop_agent(agent_id: str) -> dict[str, Any]:
|
461
|
+
try:
|
462
|
+
if agent_id not in _agent_graph["nodes"]:
|
463
|
+
return {
|
464
|
+
"success": False,
|
465
|
+
"error": f"Agent '{agent_id}' not found in graph",
|
466
|
+
"agent_id": agent_id,
|
467
|
+
}
|
468
|
+
|
469
|
+
agent_node = _agent_graph["nodes"][agent_id]
|
470
|
+
|
471
|
+
if agent_node["status"] in ["completed", "error", "failed", "stopped"]:
|
472
|
+
return {
|
473
|
+
"success": True,
|
474
|
+
"message": f"Agent '{agent_node['name']}' was already stopped",
|
475
|
+
"agent_id": agent_id,
|
476
|
+
"previous_status": agent_node["status"],
|
477
|
+
}
|
478
|
+
|
479
|
+
if agent_id in _agent_states:
|
480
|
+
agent_state = _agent_states[agent_id]
|
481
|
+
agent_state.request_stop()
|
482
|
+
|
483
|
+
if agent_id in _agent_instances:
|
484
|
+
agent_instance = _agent_instances[agent_id]
|
485
|
+
if hasattr(agent_instance, "state"):
|
486
|
+
agent_instance.state.request_stop()
|
487
|
+
if hasattr(agent_instance, "cancel_current_execution"):
|
488
|
+
agent_instance.cancel_current_execution()
|
489
|
+
|
490
|
+
agent_node["status"] = "stopping"
|
491
|
+
|
492
|
+
try:
|
493
|
+
from strix.cli.tracer import get_global_tracer
|
494
|
+
|
495
|
+
tracer = get_global_tracer()
|
496
|
+
if tracer:
|
497
|
+
tracer.update_agent_status(agent_id, "stopping")
|
498
|
+
except (ImportError, AttributeError):
|
499
|
+
pass
|
500
|
+
|
501
|
+
agent_node["result"] = {
|
502
|
+
"summary": "Agent stop requested by user",
|
503
|
+
"success": False,
|
504
|
+
"stopped_by_user": True,
|
505
|
+
}
|
506
|
+
|
507
|
+
return {
|
508
|
+
"success": True,
|
509
|
+
"message": f"Stop request sent to agent '{agent_node['name']}'",
|
510
|
+
"agent_id": agent_id,
|
511
|
+
"agent_name": agent_node["name"],
|
512
|
+
"note": "Agent will stop gracefully after current iteration",
|
513
|
+
}
|
514
|
+
|
515
|
+
except Exception as e: # noqa: BLE001
|
516
|
+
return {
|
517
|
+
"success": False,
|
518
|
+
"error": f"Failed to stop agent: {e}",
|
519
|
+
"agent_id": agent_id,
|
520
|
+
}
|
521
|
+
|
522
|
+
|
523
|
+
def send_user_message_to_agent(agent_id: str, message: str) -> dict[str, Any]:
|
524
|
+
try:
|
525
|
+
if agent_id not in _agent_graph["nodes"]:
|
526
|
+
return {
|
527
|
+
"success": False,
|
528
|
+
"error": f"Agent '{agent_id}' not found in graph",
|
529
|
+
"agent_id": agent_id,
|
530
|
+
}
|
531
|
+
|
532
|
+
agent_node = _agent_graph["nodes"][agent_id]
|
533
|
+
|
534
|
+
if agent_id not in _agent_messages:
|
535
|
+
_agent_messages[agent_id] = []
|
536
|
+
|
537
|
+
from uuid import uuid4
|
538
|
+
|
539
|
+
message_data = {
|
540
|
+
"id": f"user_msg_{uuid4().hex[:8]}",
|
541
|
+
"from": "user",
|
542
|
+
"to": agent_id,
|
543
|
+
"content": message,
|
544
|
+
"message_type": "instruction",
|
545
|
+
"priority": "high",
|
546
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
547
|
+
"delivered": True,
|
548
|
+
"read": False,
|
549
|
+
}
|
550
|
+
|
551
|
+
_agent_messages[agent_id].append(message_data)
|
552
|
+
|
553
|
+
return {
|
554
|
+
"success": True,
|
555
|
+
"message": f"Message sent to agent '{agent_node['name']}'",
|
556
|
+
"agent_id": agent_id,
|
557
|
+
"agent_name": agent_node["name"],
|
558
|
+
}
|
559
|
+
|
560
|
+
except Exception as e: # noqa: BLE001
|
561
|
+
return {
|
562
|
+
"success": False,
|
563
|
+
"error": f"Failed to send message to agent: {e}",
|
564
|
+
"agent_id": agent_id,
|
565
|
+
}
|
566
|
+
|
567
|
+
|
568
|
+
@register_tool(sandbox_execution=False)
|
569
|
+
def wait_for_message(
|
570
|
+
agent_state: Any,
|
571
|
+
reason: str = "Waiting for messages from other agents or user input",
|
572
|
+
) -> dict[str, Any]:
|
573
|
+
try:
|
574
|
+
agent_id = agent_state.agent_id
|
575
|
+
agent_name = agent_state.agent_name
|
576
|
+
|
577
|
+
agent_state.enter_waiting_state()
|
578
|
+
|
579
|
+
if agent_id in _agent_graph["nodes"]:
|
580
|
+
_agent_graph["nodes"][agent_id]["status"] = "waiting"
|
581
|
+
_agent_graph["nodes"][agent_id]["waiting_reason"] = reason
|
582
|
+
|
583
|
+
try:
|
584
|
+
from strix.cli.tracer import get_global_tracer
|
585
|
+
|
586
|
+
tracer = get_global_tracer()
|
587
|
+
if tracer:
|
588
|
+
tracer.update_agent_status(agent_id, "waiting")
|
589
|
+
except (ImportError, AttributeError):
|
590
|
+
pass
|
591
|
+
|
592
|
+
except Exception as e: # noqa: BLE001
|
593
|
+
return {"success": False, "error": f"Failed to enter waiting state: {e}", "status": "error"}
|
594
|
+
else:
|
595
|
+
return {
|
596
|
+
"success": True,
|
597
|
+
"status": "waiting",
|
598
|
+
"message": f"Agent '{agent_name}' is now waiting for messages",
|
599
|
+
"reason": reason,
|
600
|
+
"agent_info": {
|
601
|
+
"id": agent_id,
|
602
|
+
"name": agent_name,
|
603
|
+
"status": "waiting",
|
604
|
+
},
|
605
|
+
"resume_conditions": [
|
606
|
+
"Message from another agent",
|
607
|
+
"Message from user",
|
608
|
+
"Direct communication",
|
609
|
+
],
|
610
|
+
}
|