strands-swarms 0.1.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.
@@ -0,0 +1,88 @@
1
+ """Dynamic multi-agent orchestration for Strands Agents.
2
+
3
+ This package provides DynamicSwarm - an orchestrator-driven approach to multi-agent
4
+ workflows where an LLM automatically designs and executes agent pipelines
5
+ based on user queries.
6
+
7
+ The orchestrator agent handles three responsibilities in a single conversation:
8
+ 1. Planning and creating subagents - Analyze the task and spawn specialized agents
9
+ 2. Assigning tasks - Create and assign tasks to the spawned agents
10
+ 3. Generating final response - Synthesize results into a cohesive response
11
+
12
+ Using a single orchestrator agent for all three phases (rather than separate agents)
13
+ provides better context - the orchestrator knows exactly what it planned and why,
14
+ leading to more coherent final responses.
15
+
16
+ For static multi-agent workflows, use the Strands SDK directly:
17
+ - strands.multiagent.swarm.Swarm - dynamic handoffs between agents
18
+ - strands.multiagent.graph.Graph - dependency-based execution
19
+
20
+ Example:
21
+ from strands import tool
22
+ from strands_swarms import DynamicSwarm
23
+
24
+ @tool
25
+ def search_web(query: str) -> str:
26
+ '''Search the web.'''
27
+ return f"Results for: {query}"
28
+
29
+ swarm = DynamicSwarm(
30
+ available_tools={"search_web": search_web},
31
+ verbose=True,
32
+ )
33
+ result = swarm.execute("Research AI trends and summarize")
34
+ """
35
+
36
+ from .swarm import DynamicSwarm, DynamicSwarmResult
37
+ from .orchestrator import create_orchestrator_agent
38
+ from .events import (
39
+ # Planning/Orchestration events
40
+ SwarmStartedEvent,
41
+ PlanningStartedEvent,
42
+ AgentSpawnedEvent,
43
+ TaskCreatedEvent,
44
+ PlanningCompletedEvent,
45
+ # Execution events
46
+ ExecutionStartedEvent,
47
+ TaskStartedEvent,
48
+ TaskCompletedEvent,
49
+ TaskFailedEvent,
50
+ ExecutionCompletedEvent,
51
+ SwarmCompletedEvent,
52
+ SwarmFailedEvent,
53
+ # Hook provider
54
+ PrintingHookProvider,
55
+ )
56
+
57
+ # Re-export strands types for convenience
58
+ from strands.hooks import HookProvider, HookRegistry
59
+ from strands.multiagent.base import Status
60
+
61
+ __version__ = "0.1.0"
62
+
63
+ __all__ = [
64
+ # Main API
65
+ "DynamicSwarm",
66
+ "DynamicSwarmResult",
67
+ # Orchestrator (handles both planning AND completion in same conversation)
68
+ "create_orchestrator_agent",
69
+ # Status enum
70
+ "Status",
71
+ # Events (for custom hooks)
72
+ "SwarmStartedEvent",
73
+ "PlanningStartedEvent",
74
+ "AgentSpawnedEvent",
75
+ "TaskCreatedEvent",
76
+ "PlanningCompletedEvent",
77
+ "ExecutionStartedEvent",
78
+ "TaskStartedEvent",
79
+ "TaskCompletedEvent",
80
+ "TaskFailedEvent",
81
+ "ExecutionCompletedEvent",
82
+ "SwarmCompletedEvent",
83
+ "SwarmFailedEvent",
84
+ # Hook system
85
+ "PrintingHookProvider",
86
+ "HookProvider",
87
+ "HookRegistry",
88
+ ]
@@ -0,0 +1,564 @@
1
+ """Event types for DynamicSwarm execution.
2
+
3
+ This module extends strands' hook system with events specific to dynamic
4
+ swarm planning and execution. Events follow the same patterns as
5
+ strands.hooks.events for consistency.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from dataclasses import dataclass, field
12
+ from typing import Any
13
+
14
+ from strands.hooks.registry import BaseHookEvent, HookProvider, HookRegistry
15
+
16
+
17
+ # =============================================================================
18
+ # Planning Events
19
+ # =============================================================================
20
+
21
+
22
+ @dataclass
23
+ class SwarmStartedEvent(BaseHookEvent):
24
+ """Event triggered when a dynamic swarm begins execution.
25
+
26
+ Attributes:
27
+ query: The user's input query.
28
+ available_tools: List of tool names available to spawned agents.
29
+ available_models: List of model names available to spawned agents.
30
+ """
31
+
32
+ query: str
33
+ available_tools: list[str] = field(default_factory=list)
34
+ available_models: list[str] = field(default_factory=list)
35
+
36
+
37
+ @dataclass
38
+ class PlanningStartedEvent(BaseHookEvent):
39
+ """Event triggered when the planning phase begins."""
40
+
41
+ pass
42
+
43
+
44
+ @dataclass
45
+ class AgentSpawnedEvent(BaseHookEvent):
46
+ """Event triggered when a new agent is dynamically spawned.
47
+
48
+ Attributes:
49
+ name: The agent's unique identifier.
50
+ role: The agent's role description.
51
+ instructions: Additional instructions for the agent.
52
+ tools: List of tool names assigned to this agent.
53
+ model: Model name assigned to this agent.
54
+ color: ANSI color code assigned to this agent.
55
+ """
56
+
57
+ name: str
58
+ role: str
59
+ instructions: str | None = None
60
+ tools: list[str] = field(default_factory=list)
61
+ model: str | None = None
62
+ color: str | None = None # ANSI color code for consistent display
63
+
64
+
65
+ @dataclass
66
+ class TaskCreatedEvent(BaseHookEvent):
67
+ """Event triggered when a new task is created.
68
+
69
+ Attributes:
70
+ name: The task's unique identifier.
71
+ agent: Name of the agent assigned to this task.
72
+ description: Description of what the task does.
73
+ depends_on: List of task names this task depends on.
74
+ """
75
+
76
+ name: str
77
+ agent: str
78
+ description: str | None = None
79
+ depends_on: list[str] = field(default_factory=list)
80
+
81
+
82
+ @dataclass
83
+ class AgentInfo:
84
+ """Info about a spawned agent for summary display."""
85
+ name: str
86
+ role: str
87
+ tools: list[str]
88
+ model: str | None
89
+ color: str | None = None
90
+
91
+
92
+ @dataclass
93
+ class TaskInfo:
94
+ """Info about a created task for summary display."""
95
+ name: str
96
+ agent: str
97
+ description: str | None
98
+ depends_on: list[str]
99
+
100
+
101
+ @dataclass
102
+ class PlanningCompletedEvent(BaseHookEvent):
103
+ """Event triggered when planning phase completes.
104
+
105
+ Attributes:
106
+ entry_task: The entry point task, if specified.
107
+ agents: List of agent info for summary display.
108
+ tasks: List of task info for summary display.
109
+ """
110
+
111
+ entry_task: str | None = None
112
+ agents: list[AgentInfo] = field(default_factory=list)
113
+ tasks: list[TaskInfo] = field(default_factory=list)
114
+
115
+
116
+ # =============================================================================
117
+ # Execution Events
118
+ # =============================================================================
119
+
120
+
121
+ @dataclass
122
+ class ExecutionStartedEvent(BaseHookEvent):
123
+ """Event triggered when the execution phase begins.
124
+
125
+ Attributes:
126
+ tasks: List of task names to execute.
127
+ """
128
+
129
+ tasks: list[str] = field(default_factory=list)
130
+
131
+
132
+ @dataclass
133
+ class TaskStartedEvent(BaseHookEvent):
134
+ """Event triggered when a task begins executing.
135
+
136
+ Attributes:
137
+ name: The task name.
138
+ agent_role: Role of the agent executing this task.
139
+ """
140
+
141
+ name: str
142
+ agent_role: str | None = None
143
+
144
+
145
+ @dataclass
146
+ class TaskCompletedEvent(BaseHookEvent):
147
+ """Event triggered when a task completes successfully.
148
+
149
+ Attributes:
150
+ name: The task name.
151
+ """
152
+
153
+ name: str
154
+
155
+ @property
156
+ def should_reverse_callbacks(self) -> bool:
157
+ """Cleanup events should reverse callback order."""
158
+ return True
159
+
160
+
161
+ @dataclass
162
+ class TaskFailedEvent(BaseHookEvent):
163
+ """Event triggered when a task fails.
164
+
165
+ Attributes:
166
+ name: The task name.
167
+ error: Error message or description.
168
+ """
169
+
170
+ name: str
171
+ error: str | None = None
172
+
173
+ @property
174
+ def should_reverse_callbacks(self) -> bool:
175
+ """Cleanup events should reverse callback order."""
176
+ return True
177
+
178
+
179
+ @dataclass
180
+ class ExecutionCompletedEvent(BaseHookEvent):
181
+ """Event triggered when execution phase completes.
182
+
183
+ Attributes:
184
+ status: Final execution status.
185
+ agent_count: Number of agents used.
186
+ task_count: Number of tasks completed.
187
+ """
188
+
189
+ status: str
190
+ agent_count: int = 0
191
+ task_count: int = 0
192
+
193
+ @property
194
+ def should_reverse_callbacks(self) -> bool:
195
+ """Cleanup events should reverse callback order."""
196
+ return True
197
+
198
+
199
+ @dataclass
200
+ class SwarmCompletedEvent(BaseHookEvent):
201
+ """Event triggered when swarm completes successfully."""
202
+
203
+ pass
204
+
205
+ @property
206
+ def should_reverse_callbacks(self) -> bool:
207
+ """Cleanup events should reverse callback order."""
208
+ return True
209
+
210
+
211
+ @dataclass
212
+ class SwarmFailedEvent(BaseHookEvent):
213
+ """Event triggered when swarm fails.
214
+
215
+ Attributes:
216
+ error: Error message or description.
217
+ """
218
+
219
+ error: str
220
+
221
+ @property
222
+ def should_reverse_callbacks(self) -> bool:
223
+ """Cleanup events should reverse callback order."""
224
+ return True
225
+
226
+
227
+ # =============================================================================
228
+ # Default Hook Provider
229
+ # =============================================================================
230
+
231
+ # ANSI Color Constants (for consistent agent output coloring)
232
+ AGENT_COLORS = [
233
+ "\033[94m", # Blue
234
+ "\033[92m", # Green
235
+ "\033[93m", # Yellow
236
+ "\033[95m", # Magenta
237
+ "\033[96m", # Cyan
238
+ "\033[91m", # Red
239
+ ]
240
+ RESET = "\033[0m"
241
+ BOLD = "\033[1m"
242
+ DIM = "\033[2m"
243
+
244
+
245
+ # =============================================================================
246
+ # Colored Callback Handler
247
+ # =============================================================================
248
+
249
+
250
+ # Patterns for internal LLM reasoning tags that should be filtered from output
251
+ _INTERNAL_TAG_PATTERN = re.compile(
252
+ r'<(/?)(?:thinking|result|task_quality_reflection|task_quality_score|'
253
+ r'search_quality_reflection|search_quality_score|reflection|score)>',
254
+ re.IGNORECASE
255
+ )
256
+
257
+
258
+ def _filter_internal_tags(text: str) -> str:
259
+ """Filter out internal LLM reasoning tags from streamed text.
260
+
261
+ These tags are generated by the LLM for internal reasoning but shouldn't
262
+ be displayed to users in verbose output.
263
+ """
264
+ return _INTERNAL_TAG_PATTERN.sub('', text)
265
+
266
+
267
+ def create_colored_callback_handler(color: str, agent_name: str):
268
+ """Create a callback handler that prints agent output with a specific color.
269
+
270
+ This ensures all output from a specific agent (text, reasoning, tool calls)
271
+ is displayed with the same color for easy visual tracking.
272
+
273
+ Internal reasoning tags (like <thinking>, <result>, etc.) are automatically
274
+ filtered out from the streamed output.
275
+
276
+ Args:
277
+ color: ANSI color code to use for this agent's output.
278
+ agent_name: Name of the agent (for tool call headers).
279
+
280
+ Returns:
281
+ A callback handler function for the strands Agent.
282
+ """
283
+ # Track which tool calls we've already printed to avoid duplicates
284
+ # (strands fires current_tool_use multiple times as input streams in)
285
+ printed_tool_ids: set[str] = set()
286
+
287
+ def handler(**kwargs: Any) -> None:
288
+ nonlocal printed_tool_ids
289
+
290
+ # Text generation events
291
+ if "data" in kwargs:
292
+ # Filter internal reasoning tags and stream text chunk with color
293
+ filtered_text = _filter_internal_tags(kwargs['data'])
294
+ if filtered_text:
295
+ print(f"{color}{filtered_text}{RESET}", end="", flush=True)
296
+
297
+ # Tool usage events - only print once per tool call
298
+ elif "current_tool_use" in kwargs:
299
+ tool = kwargs["current_tool_use"]
300
+ tool_id = tool.get("toolUseId", "")
301
+
302
+ # Skip if we've already printed this tool call
303
+ if tool_id in printed_tool_ids:
304
+ return
305
+ printed_tool_ids.add(tool_id)
306
+
307
+ tool_name = tool.get("name", "unknown")
308
+ # Print tool header with color
309
+ print(f"\n{color}{BOLD}Tool: {tool_name}{RESET}")
310
+
311
+ # Tool result events - print the input when tool completes
312
+ elif "tool_result" in kwargs:
313
+ result = kwargs["tool_result"]
314
+ tool_id = result.get("toolUseId", "")
315
+ tool_input = result.get("input")
316
+
317
+ if tool_input:
318
+ if isinstance(tool_input, dict):
319
+ for key, value in tool_input.items():
320
+ # Truncate long values for readability
321
+ str_value = str(value)
322
+ if len(str_value) > 100:
323
+ str_value = str_value[:100] + "..."
324
+ print(f"{color} {key}: {str_value}{RESET}")
325
+ else:
326
+ str_input = str(tool_input)
327
+ if len(str_input) > 200:
328
+ str_input = str_input[:200] + "..."
329
+ print(f"{color} Input: {str_input}{RESET}")
330
+
331
+ # Reasoning/thinking events - also filter internal tags
332
+ elif "reasoningText" in kwargs:
333
+ filtered_text = _filter_internal_tags(kwargs['reasoningText'])
334
+ if filtered_text:
335
+ print(f"{color}{filtered_text}{RESET}", end="", flush=True)
336
+ elif "reasoning" in kwargs:
337
+ # Start of reasoning block
338
+ pass # The reasoningText will contain the actual content
339
+
340
+ return handler
341
+
342
+
343
+ class PrintingHookProvider(HookProvider):
344
+ """Hook provider that prints formatted output to console.
345
+
346
+ This provides a nice CLI experience with emoji indicators and colors.
347
+ Use with DynamicSwarm for live status updates.
348
+
349
+ Example:
350
+ swarm = DynamicSwarm(
351
+ ...,
352
+ hooks=[PrintingHookProvider()],
353
+ )
354
+ """
355
+
356
+ # Use same colors as agent output for consistency
357
+ COLORS = AGENT_COLORS
358
+
359
+ def __init__(self, use_colors: bool = True) -> None:
360
+ """Initialize the hook provider with state tracking.
361
+
362
+ Args:
363
+ use_colors: Whether to use ANSI colors for agent differentiation.
364
+ """
365
+ self._agents_header_printed = False
366
+ self._tasks_header_printed = False
367
+ self._use_colors = use_colors
368
+ self._agent_colors: dict[str, str] = {} # Populated from AgentSpawnedEvent
369
+ self._task_agents: dict[str, str] = {} # task_name -> agent_name
370
+
371
+ def _get_agent_color(self, agent_name: str | None) -> str:
372
+ """Get a consistent color for an agent."""
373
+ if not self._use_colors or not agent_name:
374
+ return ""
375
+ return self._agent_colors.get(agent_name, "")
376
+
377
+ def _colored(self, text: str, agent_name: str | None = None, bold: bool = False) -> str:
378
+ """Apply color to text."""
379
+ if not self._use_colors:
380
+ return text
381
+ color = self._get_agent_color(agent_name) if agent_name else ""
382
+ bold_code = BOLD if bold else ""
383
+ return f"{bold_code}{color}{text}{RESET}"
384
+
385
+ def _get_task_agent(self, task_name: str) -> str | None:
386
+ """Get the agent assigned to a task."""
387
+ return self._task_agents.get(task_name)
388
+
389
+ def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None:
390
+ """Register all printing callbacks."""
391
+ # Planning events
392
+ registry.add_callback(SwarmStartedEvent, self._on_swarm_started)
393
+ registry.add_callback(PlanningStartedEvent, self._on_planning_started)
394
+ registry.add_callback(AgentSpawnedEvent, self._on_agent_spawned)
395
+ registry.add_callback(TaskCreatedEvent, self._on_task_created)
396
+ registry.add_callback(PlanningCompletedEvent, self._on_planning_completed)
397
+
398
+ # Execution events
399
+ registry.add_callback(ExecutionStartedEvent, self._on_execution_started)
400
+ registry.add_callback(TaskStartedEvent, self._on_task_started)
401
+ registry.add_callback(TaskCompletedEvent, self._on_task_completed)
402
+ registry.add_callback(TaskFailedEvent, self._on_task_failed)
403
+ registry.add_callback(ExecutionCompletedEvent, self._on_execution_completed)
404
+
405
+ # Completion events
406
+ registry.add_callback(SwarmCompletedEvent, self._on_swarm_completed)
407
+ registry.add_callback(SwarmFailedEvent, self._on_swarm_failed)
408
+
409
+ def _on_swarm_started(self, event: SwarmStartedEvent) -> None:
410
+ # Reset state for new swarm execution
411
+ self._agents_header_printed = False
412
+ self._tasks_header_printed = False
413
+ self._agent_colors.clear()
414
+ self._task_agents.clear()
415
+
416
+ print("\n" + "=" * 60)
417
+ print("๐Ÿš€ DYNAMIC SWARM STARTING")
418
+ print("=" * 60)
419
+ query = event.query
420
+ print(f"\n๐Ÿ“ Query: {query[:200]}{'...' if len(query) > 200 else ''}")
421
+ print(f"๐Ÿ“ฆ Available tools: {event.available_tools or ['none']}")
422
+ print(f"๐Ÿง  Available models: {event.available_models or ['default']}")
423
+
424
+ def _on_planning_started(self, event: PlanningStartedEvent) -> None:
425
+ print("\n" + "-" * 40)
426
+ print("๐Ÿ“ PHASE 1: PLANNING")
427
+ print("-" * 40)
428
+
429
+ def _on_agent_spawned(self, event: AgentSpawnedEvent) -> None:
430
+ # Store the color from the event (assigned by registry) for later use
431
+ # Don't print here - we'll print a clean summary after planning completes
432
+ if event.color:
433
+ self._agent_colors[event.name] = event.color
434
+
435
+ def _on_task_created(self, event: TaskCreatedEvent) -> None:
436
+ # Track task-agent mapping for coloring during execution
437
+ # Don't print here - we'll print a clean summary after planning completes
438
+ self._task_agents[event.name] = event.agent
439
+
440
+ def _on_planning_completed(self, event: PlanningCompletedEvent) -> None:
441
+ # Print agents summary
442
+ if event.agents:
443
+ print("\n" + "ยท" * 40)
444
+ print("๐Ÿค– AGENTS")
445
+ print("ยท" * 40)
446
+ for agent in event.agents:
447
+ # Store color for later use
448
+ if agent.color:
449
+ self._agent_colors[agent.name] = agent.color
450
+
451
+ # Color the entire agent block
452
+ c = self._colored
453
+ print(f"\n {c(f'[{agent.name}]', agent.name, bold=True)}")
454
+ print(f" {c('Role:', agent.name, bold=True)} {c(agent.role, agent.name)}")
455
+ print(f" {c('Tools:', agent.name, bold=True)} {c(str(agent.tools or ['none']), agent.name)}")
456
+ print(f" {c('Model:', agent.name, bold=True)} {c(agent.model or 'default', agent.name)}")
457
+
458
+ # Print tasks summary with dependency visualization
459
+ if event.tasks:
460
+ print("\n" + "ยท" * 40)
461
+ print("๐Ÿ“‹ TASKS & DEPENDENCIES")
462
+ print("ยท" * 40)
463
+ for task in event.tasks:
464
+ # Track task-agent mapping
465
+ self._task_agents[task.name] = task.agent
466
+
467
+ # Color the entire task block with the agent's color
468
+ c = self._colored
469
+ agent_name = task.agent
470
+
471
+ print(f"\n {c(f'[{task.name}]', agent_name, bold=True)}")
472
+ print(f" {c('Agent:', agent_name, bold=True)} {c(agent_name, agent_name)}")
473
+ if task.description:
474
+ print(f" {c('Description:', agent_name, bold=True)} {c(task.description, agent_name)}")
475
+ if task.depends_on:
476
+ deps_colored = [self._colored(d, self._task_agents.get(d), bold=True) for d in task.depends_on]
477
+ print(f" {c('โณ Waits for:', agent_name, bold=True)} {', '.join(deps_colored)}")
478
+ else:
479
+ print(f" {c('โšก Can start immediately', agent_name)}")
480
+
481
+ # Print execution summary
482
+ print("\n" + "ยท" * 40)
483
+ print("โœ… PLAN READY")
484
+ print("ยท" * 40)
485
+ print(f" Entry: {event.entry_task or 'auto'}")
486
+ print(f" Total: {len(event.agents)} agents, {len(event.tasks)} tasks")
487
+
488
+ def _on_execution_started(self, event: ExecutionStartedEvent) -> None:
489
+ print("\n" + "-" * 40)
490
+ print("โšก PHASE 2: EXECUTION")
491
+ print("-" * 40)
492
+ # Color each task name by its agent
493
+ tasks_colored = [self._colored(t, self._get_task_agent(t), bold=True) for t in event.tasks]
494
+ print(f"๐Ÿ“‹ Tasks to execute: [{', '.join(tasks_colored)}]")
495
+
496
+ def _on_task_started(self, event: TaskStartedEvent) -> None:
497
+ agent = self._get_task_agent(event.name)
498
+ task_str = self._colored(event.name, agent, bold=True)
499
+ print(f"\nโ–ถ๏ธ Executing task: {task_str}")
500
+ if event.agent_role:
501
+ role_str = self._colored(event.agent_role, agent)
502
+ print(f" Agent role: {role_str}")
503
+
504
+ def _on_task_completed(self, event: TaskCompletedEvent) -> None:
505
+ agent = self._get_task_agent(event.name)
506
+ task_str = self._colored(event.name, agent)
507
+ print(f" โœ“ Completed: {task_str}")
508
+
509
+ def _on_task_failed(self, event: TaskFailedEvent) -> None:
510
+ task_str = self._colored(event.name, self._get_task_agent(event.name))
511
+ print(f" โŒ Failed: {task_str}")
512
+ if event.error:
513
+ print(f" Error: {event.error}")
514
+
515
+ def _on_execution_completed(self, event: ExecutionCompletedEvent) -> None:
516
+ print("\n" + "-" * 40)
517
+ print("๐Ÿ EXECUTION COMPLETE")
518
+ print("-" * 40)
519
+ print(f" Status: {event.status}")
520
+ print(f" Agents used: {event.agent_count}")
521
+ print(f" Tasks completed: {event.task_count}")
522
+
523
+ def _on_swarm_completed(self, event: SwarmCompletedEvent) -> None:
524
+ print("\n" + "=" * 60)
525
+ print("โœ… SWARM COMPLETED SUCCESSFULLY")
526
+ print("=" * 60)
527
+
528
+ def _on_swarm_failed(self, event: SwarmFailedEvent) -> None:
529
+ print("\n" + "=" * 60)
530
+ print(f"โŒ SWARM FAILED: {event.error}")
531
+ print("=" * 60)
532
+
533
+
534
+ # Re-export strands types for convenience
535
+ __all__ = [
536
+ # Events
537
+ "SwarmStartedEvent",
538
+ "PlanningStartedEvent",
539
+ "AgentSpawnedEvent",
540
+ "TaskCreatedEvent",
541
+ "PlanningCompletedEvent",
542
+ "ExecutionStartedEvent",
543
+ "TaskStartedEvent",
544
+ "TaskCompletedEvent",
545
+ "TaskFailedEvent",
546
+ "ExecutionCompletedEvent",
547
+ "SwarmCompletedEvent",
548
+ "SwarmFailedEvent",
549
+ # Data types
550
+ "AgentInfo",
551
+ "TaskInfo",
552
+ # Hook provider
553
+ "PrintingHookProvider",
554
+ # Callback handler factory
555
+ "create_colored_callback_handler",
556
+ # Color constants
557
+ "AGENT_COLORS",
558
+ "RESET",
559
+ "BOLD",
560
+ "DIM",
561
+ # Re-exports from strands
562
+ "HookProvider",
563
+ "HookRegistry",
564
+ ]