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,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
+ }