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